gloomberb 0.18.1 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +106 -5
- package/src/api-client/commodity-balances.ts +6 -0
- package/src/api-client/company-kpis.ts +9 -0
- package/src/api-client/crypto-markets.ts +9 -0
- package/src/api-client/data.ts +21 -5
- package/src/api-client/equity-screener.ts +2 -0
- package/src/api-client/index.ts +2 -0
- package/src/api-client/request.ts +5 -3
- package/src/api-client/telemetry.ts +17 -0
- package/src/api-client/types.ts +5 -0
- package/src/app/global-shortcuts.ts +1 -16
- package/src/app/pane-runtime/plugin-bindings.ts +3 -1
- package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
- package/src/app/runtime/plugin-bindings.ts +6 -2
- package/src/app/runtime/update.ts +28 -8
- package/src/architecture/pane-tabs-allowlist.json +65 -0
- package/src/brokers/logos.ts +66 -0
- package/src/brokers/profile-form.ts +3 -0
- package/src/brokers/remote-broker-adapter.ts +13 -3
- package/src/capabilities/index.ts +1 -1
- package/src/capabilities/service-capabilities.ts +7 -1
- package/src/cli/base-converter.ts +60 -18
- package/src/cli/commands/market.ts +354 -187
- package/src/cli/commands/news-search.ts +35 -8
- package/src/cli/commands/system.ts +35 -1
- package/src/cli/commands/ticker.ts +167 -60
- package/src/cli/currency-hints.ts +27 -0
- package/src/cli/fx-pair.ts +41 -4
- package/src/cli/glossary.ts +57 -4
- package/src/cli/help.ts +1 -1
- package/src/cli/helpers.ts +36 -3
- package/src/cli/index.ts +12 -1
- package/src/cli/issuer-membership.ts +103 -0
- package/src/cli/known-gaps.ts +37 -1
- package/src/cli/listing-arg.ts +51 -3
- package/src/cli/name-entries.ts +51 -12
- package/src/cli/pane-functions/capabilities.ts +2 -0
- package/src/cli/pane-functions/catalog.ts +19 -1
- package/src/cli/pane-functions/discovery.ts +15 -1
- package/src/cli/pane-functions/dom.ts +11 -5
- package/src/cli/pane-functions/freshness.ts +4 -4
- package/src/cli/pane-functions/function-help.ts +27 -3
- package/src/cli/pane-functions/headless.ts +31 -11
- package/src/cli/pane-functions/index.ts +67 -7
- package/src/cli/pane-functions/lock-note.ts +7 -4
- package/src/cli/pane-functions/resolver.ts +21 -6
- package/src/cli/quote-answer.ts +179 -0
- package/src/cli/report-tables.ts +136 -18
- package/src/cli/result.ts +12 -4
- package/src/cli/series-search.ts +2 -1
- package/src/cli/symbol-list.ts +47 -11
- package/src/cli/types.ts +4 -0
- package/src/components/chart/composite/composite-chart.tsx +25 -7
- package/src/components/chart/composite/drawing-store.ts +26 -6
- package/src/components/chart/composite/format.ts +13 -1
- package/src/components/chart/composite/interactions.ts +2 -1
- package/src/components/chart/composite/level-labels.ts +30 -0
- package/src/components/chart/composite/levels.ts +17 -6
- package/src/components/chart/composite/panel-surface.tsx +69 -8
- package/src/components/chart/composite/types.ts +6 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
- package/src/components/command-bar/routes/root/command-items.ts +32 -6
- package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
- package/src/components/data-table/view.tsx +23 -3
- package/src/components/index.ts +3 -0
- package/src/components/layout/detached-pane-shell.tsx +1 -0
- package/src/components/layout/floating-pane.tsx +1 -0
- package/src/components/layout/header.tsx +228 -55
- package/src/components/layout/pane/content.tsx +16 -14
- package/src/components/layout/pane/footer/index.tsx +22 -3
- package/src/components/layout/pane/footer/model.ts +99 -4
- package/src/components/layout/pane/header-tabs.tsx +15 -1
- package/src/components/layout/pane/header.tsx +10 -3
- package/src/components/layout/pane/index.tsx +1 -0
- package/src/components/layout/pane/pane-tabs.tsx +9 -1
- package/src/components/onboarding/onboarding-frame.tsx +9 -11
- package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
- package/src/components/onboarding/wizard-model.ts +10 -3
- package/src/components/ui/button.tsx +11 -4
- package/src/components/ui/choice-dialog.tsx +8 -5
- package/src/components/ui/display.tsx +11 -6
- package/src/components/ui/fields.tsx +100 -20
- package/src/components/ui/icon.tsx +6 -1
- package/src/components/ui/list-row-lines.tsx +87 -0
- package/src/components/ui/list-view.tsx +14 -0
- package/src/components/ui/logo-mark.tsx +64 -0
- package/src/components/ui/number-step.ts +54 -0
- package/src/components/ui/select-button.tsx +5 -4
- package/src/components/ui/select-field.tsx +3 -3
- package/src/components/ui/stat-grid.tsx +28 -3
- package/src/components/ui/status.tsx +20 -4
- package/src/components/ui/tabs.tsx +9 -1
- package/src/components/ui/toggle.tsx +17 -3
- package/src/core/app-services.ts +4 -0
- package/src/core/state/app/instrument.ts +8 -3
- package/src/data/config/store/normalize.ts +4 -1
- package/src/data/plugin-cache.ts +32 -6
- package/src/docs/snapshot.json +5 -5
- package/src/i18n/es.ts +31 -3
- package/src/i18n/ja.ts +31 -3
- package/src/i18n/ko.ts +31 -3
- package/src/i18n/zh-cn.ts +32 -4
- package/src/i18n/zh-tw.ts +32 -4
- package/src/market-data/commodity-names.ts +80 -0
- package/src/market-data/coordinator/auxiliary.ts +3 -1
- package/src/market-data/coordinator/chart.ts +3 -1
- package/src/market-data/futures-board.ts +38 -1
- package/src/market-data/futures-contract.ts +91 -0
- package/src/market-data/fx-closes.ts +78 -1
- package/src/market-data/market/change-basis.ts +219 -0
- package/src/market-data/market/format.ts +28 -12
- package/src/market-data/market/freshness.ts +38 -0
- package/src/market-data/market/status.ts +55 -9
- package/src/market-data/market/year-to-date.ts +46 -0
- package/src/market-data/quotes/contributions.ts +12 -0
- package/src/market-data/quotes/delisting.ts +34 -0
- package/src/market-data/quotes/live-valuation.ts +23 -0
- package/src/market-data/quotes/resolution.ts +8 -2
- package/src/market-data/tax-equivalent.ts +10 -2
- package/src/notifications/app-notifier.ts +2 -2
- package/src/notifications/notification-log.ts +8 -6
- package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
- package/src/plugins/builtin/alerts/cli.ts +26 -18
- package/src/plugins/builtin/alerts/events.ts +1 -0
- package/src/plugins/builtin/alerts/headless.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +13 -4
- package/src/plugins/builtin/alerts/research-rules.ts +1 -0
- package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
- package/src/plugins/builtin/attention/pane.tsx +16 -14
- package/src/plugins/builtin/attention/trending.tsx +8 -6
- package/src/plugins/builtin/awards/pane.tsx +6 -6
- package/src/plugins/builtin/backtest/client.ts +5 -2
- package/src/plugins/builtin/backtest/export.ts +1 -1
- package/src/plugins/builtin/backtest/window.ts +16 -3
- package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
- package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
- package/src/plugins/builtin/broker-manager/index.tsx +20 -9
- package/src/plugins/builtin/broker-manager/model.ts +5 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
- package/src/plugins/builtin/broker-manager/table.ts +5 -1
- package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
- package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
- package/src/plugins/builtin/broker-trading/command.ts +103 -0
- package/src/plugins/builtin/broker-trading/controller.ts +688 -0
- package/src/plugins/builtin/broker-trading/dock.tsx +184 -0
- package/src/plugins/builtin/broker-trading/headless.ts +76 -0
- package/src/plugins/builtin/broker-trading/index.tsx +77 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
- package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
- package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
- package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
- package/src/plugins/builtin/broker-trading/pane.tsx +507 -0
- package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
- package/src/plugins/builtin/broker-trading/ticket.tsx +578 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
- package/src/plugins/builtin/buildout/format.ts +1 -0
- package/src/plugins/builtin/buildout/model/index.ts +16 -7
- package/src/plugins/builtin/buildout/pane/index.tsx +11 -7
- package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
- package/src/plugins/builtin/catalysts/client.ts +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +32 -11
- package/src/plugins/builtin/chart-composer/presets.ts +2 -2
- package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
- package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
- package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
- package/src/plugins/builtin/commodity-balances/client.ts +6 -4
- package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
- package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
- package/src/plugins/builtin/commodity-balances/model.ts +22 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +39 -9
- package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
- package/src/plugins/builtin/company-attention/pane.tsx +13 -11
- package/src/plugins/builtin/company-kpis/client.ts +3 -2
- package/src/plugins/builtin/company-kpis/headless.ts +5 -7
- package/src/plugins/builtin/company-kpis/model.ts +85 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +30 -15
- package/src/plugins/builtin/composite-plugins.ts +3 -2
- package/src/plugins/builtin/correlation/headless.ts +2 -1
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
- package/src/plugins/builtin/correlation/settings.ts +1 -0
- package/src/plugins/builtin/cot/headless.ts +1 -1
- package/src/plugins/builtin/cot/index.tsx +1 -1
- package/src/plugins/builtin/cot/model.ts +35 -12
- package/src/plugins/builtin/cot/pane.tsx +4 -3
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
- package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
- package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
- package/src/plugins/builtin/crypto-board/client.ts +33 -2
- package/src/plugins/builtin/crypto-board/headless.ts +5 -2
- package/src/plugins/builtin/crypto-board/model.ts +28 -8
- package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
- package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/doe/pane.tsx +1 -1
- package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
- package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
- package/src/plugins/builtin/econ/calendar-model.ts +35 -0
- package/src/plugins/builtin/econ/index.tsx +31 -4
- package/src/plugins/builtin/econ/print-watch.ts +279 -0
- package/src/plugins/builtin/equity-screener/headless.ts +30 -5
- package/src/plugins/builtin/equity-screener/model.ts +68 -2
- package/src/plugins/builtin/equity-screener/pane.tsx +129 -119
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
- package/src/plugins/builtin/exposure/client.ts +4 -1
- package/src/plugins/builtin/exposure/editor.tsx +3 -3
- package/src/plugins/builtin/exposure/evidence.ts +4 -1
- package/src/plugins/builtin/exposure/holdings.ts +3 -1
- package/src/plugins/builtin/exposure/pane.tsx +43 -21
- package/src/plugins/builtin/filing-events/index.tsx +9 -3
- package/src/plugins/builtin/futures/table.tsx +6 -3
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/gpu/board.tsx +4 -1
- package/src/plugins/builtin/gpu/changes.tsx +10 -10
- package/src/plugins/builtin/gpu/equities.tsx +4 -1
- package/src/plugins/builtin/gpu/history.tsx +4 -2
- package/src/plugins/builtin/gpu/pane.tsx +5 -3
- package/src/plugins/builtin/holders/client.ts +2 -1
- package/src/plugins/builtin/holders/headless.ts +2 -1
- package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
- package/src/plugins/builtin/insider/base-values.ts +76 -0
- package/src/plugins/builtin/insider/client.ts +3 -0
- package/src/plugins/builtin/insider/headless.ts +48 -10
- package/src/plugins/builtin/insider/index.tsx +17 -8
- package/src/plugins/builtin/insider/issuer.ts +34 -4
- package/src/plugins/builtin/insider/signals-client.ts +2 -2
- package/src/plugins/builtin/insider/table-model.ts +35 -13
- package/src/plugins/builtin/insider/table.tsx +5 -2
- package/src/plugins/builtin/iv-history/events.ts +145 -0
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +91 -7
- package/src/plugins/builtin/jobs/headless.ts +2 -2
- package/src/plugins/builtin/jobs/lock-note.ts +14 -3
- package/src/plugins/builtin/jodi/pane.tsx +4 -4
- package/src/plugins/builtin/market-halts/model.ts +31 -6
- package/src/plugins/builtin/market-halts/pane.tsx +3 -0
- package/src/plugins/builtin/market-movers/client.ts +4 -3
- package/src/plugins/builtin/market-movers/screener.ts +4 -3
- package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
- package/src/plugins/builtin/members/pane.tsx +8 -3
- package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
- package/src/plugins/builtin/news/wire/index.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
- package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
- package/src/plugins/builtin/notes/cli.ts +1 -1
- package/src/plugins/builtin/notes/export.ts +17 -6
- package/src/plugins/builtin/notes/files.ts +16 -5
- package/src/plugins/builtin/options/enrichment-model.ts +10 -9
- package/src/plugins/builtin/options/footer.ts +25 -20
- package/src/plugins/builtin/options/live-quotes.ts +33 -18
- package/src/plugins/builtin/options/model-spot.ts +70 -4
- package/src/plugins/builtin/options/view.tsx +118 -35
- package/src/plugins/builtin/options-calculator/pane.tsx +7 -4
- package/src/plugins/builtin/options-positioning/headless.ts +11 -3
- package/src/plugins/builtin/options-positioning/model.ts +24 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
- package/src/plugins/builtin/options-scenario/client.ts +19 -1
- package/src/plugins/builtin/options-scenario/headless.ts +13 -6
- package/src/plugins/builtin/options-scenario/pane.tsx +54 -11
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
- package/src/plugins/builtin/power/client.ts +2 -2
- package/src/plugins/builtin/power/pane.tsx +36 -31
- package/src/plugins/builtin/primary-dealers/client.ts +137 -62
- package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
- package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
- package/src/plugins/builtin/primary-dealers/model.ts +179 -129
- package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
- package/src/plugins/builtin/research/analyst-model.ts +5 -1
- package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
- package/src/plugins/builtin/risk-factors/index.tsx +12 -3
- package/src/plugins/builtin/scanner/feed.ts +6 -1
- package/src/plugins/builtin/scanner/headless.ts +1 -1
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/seasonality/client.ts +2 -1
- package/src/plugins/builtin/sectors/client.ts +4 -3
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
- package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
- package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
- package/src/plugins/builtin/shared/free-routes.ts +24 -0
- package/src/plugins/builtin/shared/headless-options.ts +11 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
- package/src/plugins/builtin/shared/research-trading.tsx +64 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
- package/src/plugins/builtin/shared/volatility/options.ts +2 -1
- package/src/plugins/builtin/social-mentions/client.ts +2 -1
- package/src/plugins/builtin/social-mentions/headless.ts +1 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
- package/src/plugins/builtin/themes/headless.ts +8 -2
- package/src/plugins/builtin/themes/model.ts +43 -10
- package/src/plugins/builtin/themes/pane.tsx +8 -6
- package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
- package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
- package/src/plugins/builtin/time-sales/pane.tsx +2 -2
- package/src/plugins/builtin/trace-bonds/client.ts +14 -3
- package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
- package/src/plugins/builtin/vol-surface/client.ts +23 -3
- package/src/plugins/builtin/vol-surface/headless.ts +33 -14
- package/src/plugins/builtin/vol-surface/model.ts +30 -10
- package/src/plugins/builtin/vol-surface/pane.tsx +35 -15
- package/src/plugins/builtin/vol-surface/stored.ts +34 -3
- package/src/plugins/builtin/vol-surface/tables.ts +14 -0
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/compat.ts +3 -0
- package/src/plugins/external-runtime.ts +5 -0
- package/src/plugins/loader.ts +6 -0
- package/src/plugins/plugin-icon.ts +61 -0
- package/src/plugins/registry/host-actions.ts +2 -1
- package/src/plugins/registry/index.ts +9 -1
- package/src/plugins/runtime/context.tsx +5 -0
- package/src/plugins/runtime/index.tsx +3 -0
- package/src/portfolio/column-values.ts +13 -4
- package/src/portfolio/cost-fx.ts +5 -28
- package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
- package/src/public/broker.ts +8 -0
- package/src/public/market-data.ts +5 -0
- package/src/renderers/dom/desktop/control-styles.ts +7 -1
- package/src/renderers/dom/desktop/controls.tsx +40 -8
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/list-view.tsx +51 -19
- package/src/renderers/dom/desktop/menu.tsx +10 -1
- package/src/renderers/dom/desktop/query-bar.tsx +2 -0
- package/src/renderers/dom/desktop/select-field.tsx +8 -6
- package/src/renderers/dom/host/style.ts +6 -1
- package/src/renderers/dom/host/tabs.tsx +22 -5
- package/src/renderers/dom/host/top-surface.ts +127 -0
- package/src/renderers/dom/styles.css +51 -5
- package/src/renderers/electrobun/bun/core-capabilities.ts +21 -1
- package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
- package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
- package/src/renderers/electrobun/view/external-plugins.ts +1 -0
- package/src/renderers/electrobun/view/news-source.ts +4 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
- package/src/renderers/opentui/start.tsx +7 -0
- package/src/series/validate.ts +2 -2
- package/src/sources/gloomberb-cloud/index.ts +57 -17
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
- package/src/sources/history-result.ts +3 -0
- package/src/sources/provider-errors.ts +3 -1
- package/src/sources/provider-router/history.ts +49 -6
- package/src/sources/provider-router/index.ts +18 -1
- package/src/state/app/activity.ts +13 -0
- package/src/sync/controller.ts +4 -0
- package/src/sync/core-contributors.ts +62 -11
- package/src/sync/local-only.ts +121 -0
- package/src/sync/react.ts +6 -0
- package/src/sync/types.ts +3 -0
- package/src/telemetry/crash-reports.ts +4 -0
- package/src/telemetry/update-failures-node.ts +59 -0
- package/src/telemetry/update-failures.ts +307 -0
- package/src/tickers/company-name-listing.ts +9 -7
- package/src/tickers/company-name.ts +43 -0
- package/src/tickers/listing-fate.ts +57 -8
- package/src/tickers/search/index.ts +24 -3
- package/src/time-series/price-comparison.ts +35 -0
- package/src/time-series/resolve.ts +52 -11
- package/src/time-series/spec.ts +1 -1
- package/src/time-series/types.ts +5 -0
- package/src/types/broker.ts +56 -1
- package/src/types/config.ts +6 -0
- package/src/types/data-provider.ts +11 -1
- package/src/types/financials.ts +23 -0
- package/src/types/headless.ts +19 -2
- package/src/types/instrument.ts +4 -0
- package/src/types/issuer-listings.ts +28 -0
- package/src/types/plugin.ts +24 -6
- package/src/types/price-history.ts +6 -1
- package/src/types/trading.ts +67 -0
- package/src/ui/host.tsx +4 -0
- package/src/updater/failure.ts +163 -0
- package/src/updater/index.ts +87 -23
- package/src/utils/base-currency.ts +108 -0
- package/src/utils/currency-names.ts +128 -0
- package/src/utils/currency-pair.ts +28 -1
- package/src/utils/currency-units.ts +32 -1
- package/src/utils/dom-screenshot.ts +22 -9
- package/src/utils/exchange-notation.generated.ts +360 -0
- package/src/utils/exchanges.ts +15 -322
- package/src/utils/format.ts +31 -1
- package/src/utils/instrument-identity.ts +38 -1
- package/src/utils/options.ts +46 -1
- package/src/utils/price-history.ts +4 -0
- package/src/utils/reported-money.ts +8 -4
- package/src/utils/sec.ts +68 -0
|
@@ -2,10 +2,19 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
|
2
2
|
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
3
3
|
import { createRealizedVolatilityDependencies, loadRealizedVolatilityHistory } from "../realized-vol/client";
|
|
4
4
|
import { createHvDependencies, loadIvHistory, loadIvScreen, loadScreenVolatilities } from "./client";
|
|
5
|
-
import { formatPoints, formatRank, formatSpot, formatStat, formatVol, readingLabel, sharedDates, skewHeader, VCA_DEFINITIONS, verdictLabel } from "./format";
|
|
5
|
+
import { formatPoints, formatRank, formatSpot, formatStat, formatVol, IMPLIED_VOL_WORDS, readingLabel, sharedDates, skewHeader, VCA_DEFINITIONS, verdictLabel } from "./format";
|
|
6
6
|
import { HV_WINDOWS, type HvWindow, type IvLookback, type IvStatRow, projectIvHistory, projectRichCheap, type RichCheapRow, richCheapDates, screenCoverage, VCA_LIMIT, VCA_PRESETS } from "./model";
|
|
7
7
|
import { vcaUniverse } from "./universe";
|
|
8
|
-
import {
|
|
8
|
+
import { offsetText, releaseOffsetRows } from "./events";
|
|
9
|
+
import { loadMacroReleases } from "../macro-day/client";
|
|
10
|
+
import { MACRO_EVENT_KINDS, MACRO_EVENT_LABELS, MACRO_RELEASE_SOURCES, staleMacroReleasesNotice, type MacroEventKind, type MacroRelease,
|
|
11
|
+
type MacroReleaseList } from "../macro-day/releases";
|
|
12
|
+
import { lookupScheduledDates } from "../alerts/scheduled";
|
|
13
|
+
import { fetchCpiBoard } from "../cpi/client";
|
|
14
|
+
import { fetchRatePath } from "../rate-path/client";
|
|
15
|
+
import type { HeadlessPaneContext } from "../../../types/headless";
|
|
16
|
+
import { zonedDateKey } from "../../../utils/zoned-date-time";
|
|
17
|
+
import { listingFateNotes, listingFateReport, loadListingFate, MovedSymbolError } from "../../../tickers/listing-fate";
|
|
9
18
|
import { startTickerChangeLookup } from "../../../tickers/ticker-changes";
|
|
10
19
|
|
|
11
20
|
const METHODOLOGY = "docs/research-data.md#implied-volatility-history";
|
|
@@ -17,23 +26,33 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
17
26
|
usListingsOnly: "Implied volatility history",
|
|
18
27
|
freshness: { status: "not-a-feed", basis: "daily implied volatility", cadence: "daily", observedKey: "date", oldest: null },
|
|
19
28
|
describe: (args) => `HIVG ${args.symbols[0] ?? ""}`,
|
|
20
|
-
discovery: { aliases: ["HIVG"], screenshotReadiness: "live-dom", dataRequirements: ["Cloud stored implied volatility", "Daily price history"],
|
|
29
|
+
discovery: { aliases: ["HIVG"], suggestFor: IMPLIED_VOL_WORDS, screenshotReadiness: "live-dom", dataRequirements: ["Cloud stored implied volatility", "Daily price history"],
|
|
21
30
|
limitations: ["History starts February 2024 (daily option trade closes).", "Rank and percentile use trade-close readings only.",
|
|
22
31
|
"IV1Y needs long-dated trades: it can start later, has gaps, and ranks from 60 prior sessions.",
|
|
23
32
|
"skew25d (put-call) and rr25d (call-put) come from the daily quote captures and are blank before the first one."] },
|
|
24
33
|
options: [
|
|
25
34
|
{ key: "lookback", type: "enum", values: [{ value: "1Y" }, { value: "2Y" }, { value: "ALL" }], defaultValue: "1Y", description: "Visible history" },
|
|
26
35
|
{ key: "hvWindow", type: "enum", values: HV_WINDOWS.map((window) => ({ value: String(window) })), defaultValue: "20", description: "Realized volatility window in sessions" },
|
|
36
|
+
{ key: "event", type: "enum", aliases: ["release"], values: MACRO_EVENT_KINDS.map((value) => ({ value, ...value === "jobs" ? { aliases: ["nfp", "payrolls"] } : {} })),
|
|
37
|
+
description: "One row per release of this kind whose session at --offset has closed (the next scheduled FOMC or CPI included), in place of the daily history",
|
|
38
|
+
example: "--event fomc --offset -6", shot: { reportOnly: "lists the readings around past release days" } },
|
|
39
|
+
{ key: "offset", type: "integer", minimum: -60, maximum: 60, defaultValue: 0,
|
|
40
|
+
description: "Sessions from each release with --event: 0 is the first session on or after the release day, -6 the sixth session before that",
|
|
41
|
+
example: "--event cpi --offset -1", shot: { reportOnly: "picks the session read around each release" } },
|
|
27
42
|
],
|
|
28
43
|
async load(args, ctx) {
|
|
44
|
+
const event = typeof args.options.event === "string" && args.options.event ? args.options.event as MacroEventKind : null;
|
|
45
|
+
const offset = Number(args.options.offset) || 0;
|
|
46
|
+
if (!event && offset !== 0) throw new Error("--offset counts sessions from a release day: add --event fomc, cpi or jobs.");
|
|
29
47
|
const instrument = await resolveHeadlessInstrument(ctx, args.symbols[0]!);
|
|
30
48
|
// What became of the symbol, as `quote` reads it, looked up beside the figures: FB's are the fund listed
|
|
31
49
|
// there now, TWTR stopped trading, and HFC, which became DINO, has none of its own.
|
|
32
50
|
const changes = startTickerChangeLookup(ctx.marketData, instrument.symbol, instrument.exchange);
|
|
33
51
|
const quote = ctx.marketData.getQuote(instrument.symbol, instrument.exchange ?? "").catch(() => null);
|
|
34
|
-
const [payload, prices] = await Promise.all([
|
|
52
|
+
const [payload, prices, releases] = await Promise.all([
|
|
35
53
|
loadIvHistory(instrument.symbol, { signal: ctx.signal }, ctx.apiClient),
|
|
36
54
|
loadRealizedVolatilityHistory({ instrument, signal: ctx.signal }, createRealizedVolatilityDependencies(ctx.marketData)),
|
|
55
|
+
event ? eventReleases(event, ctx) : null,
|
|
37
56
|
]);
|
|
38
57
|
const fate = await loadListingFate(instrument, ctx.marketData, { quote, changes });
|
|
39
58
|
if (fate.moved) throw new MovedSymbolError(fate.moved);
|
|
@@ -57,11 +76,47 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
57
76
|
const skewColumns = [{ key: "skew25d", header: skewHeader("skew25d", "put-call"), format: points },
|
|
58
77
|
{ key: "rr25d", header: skewHeader("rr25d", "call-put"), format: points }];
|
|
59
78
|
const served = model.iv30.length > 0 || prices.history.length > 0;
|
|
60
|
-
const fateNotes = listingFateNotes(fate, { served });
|
|
61
79
|
// With nothing served, the fate is why: no "not a ticker" or missing-history errors beside it.
|
|
62
|
-
const
|
|
63
|
-
|
|
80
|
+
const { notes: fateNotes, errors } = listingFateReport(fate, { served },
|
|
81
|
+
[prices.error, ...model.warnings].filter((value): value is string => !!value));
|
|
64
82
|
const notes = [...fateNotes, ...[model.oneYearNote, model.skewNote].filter((note): note is string => !!note)];
|
|
83
|
+
if (event && releases) {
|
|
84
|
+
const lookback = String(args.options.lookback) as IvLookback;
|
|
85
|
+
const label = MACRO_EVENT_LABELS[event];
|
|
86
|
+
const offsets = releaseOffsetRows(model, releases.list.releases, event, offset, {
|
|
87
|
+
lookback, readThrough: payload.coverage?.backfilledThrough ?? null });
|
|
88
|
+
const plural = (count: number, one: string, many: string) => count === 1 ? one : many;
|
|
89
|
+
const eventNotes = [
|
|
90
|
+
offsets.beforeLookback > 0
|
|
91
|
+
? `${offsets.beforeLookback} earlier ${label} ${plural(offsets.beforeLookback, "release is", "releases are")} outside the ${lookback} lookback; --lookback ALL adds ${plural(offsets.beforeLookback, "it", "them")}.` : null,
|
|
92
|
+
// Said once the lookback says nothing: the history's own start is then where the rows stop.
|
|
93
|
+
offsets.beforeHistory > 0 && offsets.beforeLookback === 0
|
|
94
|
+
? `${offsets.beforeHistory} earlier ${label} ${plural(offsets.beforeHistory, "release predates", "releases predate")} the IV history, which starts ${offsets.historyStart}.` : null,
|
|
95
|
+
offsets.pending.length > 0
|
|
96
|
+
? `Left out ${label} ${offsets.pending.join(", ")}: no closing reading yet for ${plural(offsets.pending.length, "its session", "their sessions")} at --offset ${offset}.` : null,
|
|
97
|
+
releases.upcomingNote,
|
|
98
|
+
staleMacroReleasesNotice(releases.list),
|
|
99
|
+
].filter((note): note is string => !!note);
|
|
100
|
+
const eventColumns = [{ key: "releaseDate", header: "Release date" }, { key: "session", header: "Session" },
|
|
101
|
+
...historyColumns.slice(1), ...skewColumns];
|
|
102
|
+
return {
|
|
103
|
+
sections: [{
|
|
104
|
+
title: `${label} releases, ${offsetText(offset)}`,
|
|
105
|
+
// The export names the release and the offset on every row, so files of several offsets or releases join.
|
|
106
|
+
columns: [{ key: "release", header: "Release" }, eventColumns[0]!, { key: "offset", header: "Offset (sessions)" }, ...eventColumns.slice(1)],
|
|
107
|
+
rows: offsets.rows.map(({ date, ...row }) => ({ ...row, session: date, release: label })),
|
|
108
|
+
textColumns: offsets.rows.some((row) => row.skew25d != null) ? eventColumns : eventColumns.slice(0, -skewColumns.length),
|
|
109
|
+
}],
|
|
110
|
+
complete: payload.status === "ready" && errors.length === 0,
|
|
111
|
+
unavailableSymbols: offsets.rows.some((row) => row.iv30 != null) ? [] : [instrument.symbol], errors,
|
|
112
|
+
...(notes.length + eventNotes.length > 0 ? { notes: [...notes, ...eventNotes] } : {}),
|
|
113
|
+
// Dated by the history read, not by the newest release's session: an old release is no stale reading.
|
|
114
|
+
...(model.asOf ? { freshness: { asOf: model.asOf } } : {}),
|
|
115
|
+
metadata: { status: payload.status, coverage: payload.coverage, event, offset, releasesThrough: releases.list.coveredThrough,
|
|
116
|
+
nextRelease: releases.upcoming,
|
|
117
|
+
releaseSource: MACRO_RELEASE_SOURCES[event], unit: "decimal annualized volatility", methodology: METHODOLOGY },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
65
120
|
return {
|
|
66
121
|
sections: [
|
|
67
122
|
{ title: "Statistics", columns: [
|
|
@@ -153,3 +208,32 @@ export const ivScreenHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
153
208
|
};
|
|
154
209
|
},
|
|
155
210
|
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The releases of `event` HIVG reads around: the days MDAY studies (the list
|
|
214
|
+
* the app ships with, extended by Gloom Cloud's), and the next one scheduled,
|
|
215
|
+
* from the CPI board's release calendar or the FOMC meetings WIRP prices, as
|
|
216
|
+
* scheduled alerts read them. Payrolls have no schedule to read.
|
|
217
|
+
*/
|
|
218
|
+
async function eventReleases(event: MacroEventKind, ctx: HeadlessPaneContext): Promise<{
|
|
219
|
+
list: MacroReleaseList; upcoming: string | null; upcomingNote: string | null;
|
|
220
|
+
}> {
|
|
221
|
+
const label = MACRO_EVENT_LABELS[event];
|
|
222
|
+
const [published, next] = await Promise.all([
|
|
223
|
+
loadMacroReleases({}, () => ctx.apiClient.getCloudMacroReleaseDays()),
|
|
224
|
+
event === "jobs" ? null : lookupScheduledDates([{ kind: "macro", event }], {
|
|
225
|
+
earnings: async () => [],
|
|
226
|
+
cpi: () => fetchCpiBoard(ctx.apiClient),
|
|
227
|
+
fomc: async () => (await fetchRatePath(ctx.apiClient)).meetings,
|
|
228
|
+
}, zonedDateKey(Date.now(), "America/New_York")).then((dates) => dates.get(`macro:${event}`) ?? { date: null }),
|
|
229
|
+
]);
|
|
230
|
+
const upcoming = next && "date" in next ? next.date : null;
|
|
231
|
+
const listed = upcoming != null && published.releases.some((release) => release.kind === event && release.date === upcoming);
|
|
232
|
+
const releases: MacroRelease[] = upcoming && !listed ? [...published.releases, { kind: event, date: upcoming }] : published.releases;
|
|
233
|
+
return {
|
|
234
|
+
list: { ...published, releases },
|
|
235
|
+
upcoming,
|
|
236
|
+
upcomingNote: next == null ? `The next ${label} release is not scheduled here, so it has no row yet.`
|
|
237
|
+
: "error" in next ? `The next ${label} date could not be loaded (${next.error}), so it has no row.` : null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -183,11 +183,11 @@ export const jobsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
183
183
|
const symbol = args.symbols[0];
|
|
184
184
|
if (symbol) {
|
|
185
185
|
const state = await fetchJobs(symbol, { force: true, client: ctx.apiClient as JobsCloudClient });
|
|
186
|
-
if (state.kind === "denied") throw explainRefusal(new ApiRequestError(state.message, state.status), jobsLockNote, { symbol });
|
|
186
|
+
if (state.kind === "denied") throw await explainRefusal(new ApiRequestError(state.message, state.status), jobsLockNote, { symbol, context: ctx });
|
|
187
187
|
return companyBundle(symbol, state, limit);
|
|
188
188
|
}
|
|
189
189
|
const payload = await fetchJobsMovers(ctx.apiClient as JobsCloudClient)
|
|
190
|
-
.catch((error: unknown) => { throw explainRefusal(error, jobsLockNote, { symbol: null }); });
|
|
190
|
+
.catch(async (error: unknown) => { throw await explainRefusal(error, jobsLockNote, { symbol: null }); });
|
|
191
191
|
const rows = buildMoverRows(payload.movers).slice(0, Math.max(limit, 15)).map((row) => ({
|
|
192
192
|
ticker: row.ticker,
|
|
193
193
|
company: row.company,
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import type { PaneLock } from "../../../types/plugin";
|
|
2
|
-
import {
|
|
2
|
+
import type { HiringPayload } from "../../../api-client/hiring";
|
|
3
|
+
import { fetchAttention } from "../company-attention/client";
|
|
4
|
+
import { proLockNote, type FreeRoute } from "../shared/pro-lock-note";
|
|
5
|
+
|
|
6
|
+
/** HIRE's preview, for a company whose careers system it reads: Coffee Holding (JVA) is mapped but uncovered. */
|
|
7
|
+
const HIRE_ROUTE: FreeRoute = {
|
|
8
|
+
text: (ticker, company) => `gloomberb fn HIRE ${ticker} previews ${company}'s hiring with its latest roles, without the full history.`,
|
|
9
|
+
answers: async (symbol, context) => {
|
|
10
|
+
const data = await fetchAttention("hiring", symbol, { limit: 1 }, context.apiClient, context.signal) as HiringPayload;
|
|
11
|
+
return data.evidence.length + data.series.length + data.functions.length > 0;
|
|
12
|
+
},
|
|
13
|
+
};
|
|
3
14
|
|
|
4
15
|
/**
|
|
5
16
|
* JOBS is Pro whole. HIRE reads the same careers systems and needs no
|
|
6
17
|
* account: a company's latest roles, without the full history.
|
|
7
18
|
*/
|
|
8
|
-
export function jobsLockNote(lock: PaneLock): string {
|
|
19
|
+
export function jobsLockNote(lock: PaneLock): Promise<string> {
|
|
9
20
|
return proLockNote(lock, {
|
|
10
21
|
token: "JOBS",
|
|
11
22
|
argument: "optional",
|
|
12
23
|
does: "it reads each company's careers system into open roles, hiring pace and the mix by function and country",
|
|
13
24
|
shows: "it",
|
|
14
|
-
free:
|
|
25
|
+
free: [HIRE_ROUTE],
|
|
15
26
|
});
|
|
16
27
|
}
|
|
@@ -39,7 +39,7 @@ export function JodiPane({ focused, width, height }: PaneProps) {
|
|
|
39
39
|
const balances = resource.data;
|
|
40
40
|
const gas = balances?.gas ?? null;
|
|
41
41
|
const [storedTab, setTab] = usePluginPaneState<string>("commodity", "oil");
|
|
42
|
-
const tab: BalanceCommodity =
|
|
42
|
+
const tab: BalanceCommodity = storedTab === "gas" ? "gas" : "oil";
|
|
43
43
|
const board = tab === "gas" ? gas : balances?.oil ?? null;
|
|
44
44
|
const rows = board?.rows ?? EMPTY_ROWS;
|
|
45
45
|
const [selectedId, setSelectedId] = usePluginPaneState<string | null>("selected", null);
|
|
@@ -54,7 +54,7 @@ export function JodiPane({ focused, width, height }: PaneProps) {
|
|
|
54
54
|
const selectTab = useCallback((value: string) => {
|
|
55
55
|
setTab(value === "gas" ? "gas" : "oil");
|
|
56
56
|
}, [setTab]);
|
|
57
|
-
const { strip: tabs, rows: tabRows } = usePaneTabs(
|
|
57
|
+
const { strip: tabs, rows: tabRows } = usePaneTabs({
|
|
58
58
|
tabs: TABS,
|
|
59
59
|
activeValue: tab,
|
|
60
60
|
onSelect: selectTab,
|
|
@@ -62,7 +62,7 @@ export function JodiPane({ focused, width, height }: PaneProps) {
|
|
|
62
62
|
compact: true,
|
|
63
63
|
variant: "underline",
|
|
64
64
|
queryBarWidth: width,
|
|
65
|
-
}
|
|
65
|
+
});
|
|
66
66
|
const info = useMemo(
|
|
67
67
|
() => (resource.error || !board ? [] : [{ id: "month", parts: [{ text: formatBalanceMonth(board.month) }] }]),
|
|
68
68
|
[board, resource.error],
|
|
@@ -139,7 +139,7 @@ export function JodiPane({ focused, width, height }: PaneProps) {
|
|
|
139
139
|
onRootKeyDown={onRootKeyDown}
|
|
140
140
|
renderCell={renderCell}
|
|
141
141
|
selectedTextOverridesCellColor
|
|
142
|
-
emptyStateTitle="No balances for this month."
|
|
142
|
+
emptyStateTitle={board ? "No balances for this month." : tab === "gas" ? "Gas balances unavailable." : "Oil balances unavailable."}
|
|
143
143
|
/>
|
|
144
144
|
</Box>
|
|
145
145
|
);
|
|
@@ -180,14 +180,33 @@ const DAY_MS = 86_400_000;
|
|
|
180
180
|
/** Unresolved halts move out of the recent views at 30 elapsed days. */
|
|
181
181
|
const LONG_TERM_MS = 30 * DAY_MS;
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
/** How long a halt has kept trading stopped: to the trade resumption, or to now while it lasts. Quotes resuming do not end it. */
|
|
184
|
+
function haltDurationMs(record: HaltRecord, now: number): number {
|
|
184
185
|
const end = record.tradeResumeAt == null ? now : Math.min(now, record.tradeResumeAt);
|
|
185
|
-
|
|
186
|
+
return Math.max(0, end - record.haltedAt);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** How long since the halt began, whether or not trading has resumed. */
|
|
190
|
+
function haltAgeMs(record: HaltRecord, now: number): number {
|
|
191
|
+
return Math.max(0, now - record.haltedAt);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatElapsed(elapsed: number): string {
|
|
186
195
|
if (elapsed >= DAY_MS) return `${Math.floor(elapsed / DAY_MS)}d`;
|
|
187
196
|
if (elapsed >= 3_600_000) return `${Math.floor(elapsed / 3_600_000)}h`;
|
|
188
197
|
return `${Math.floor(elapsed / 60_000)}m`;
|
|
189
198
|
}
|
|
190
199
|
|
|
200
|
+
/** HALTED FOR: RAIL halted 16:40 and resumed 17:15 was halted for 35m, read at any later time. */
|
|
201
|
+
export function formatHaltDuration(record: HaltRecord, now: number): string {
|
|
202
|
+
return formatElapsed(haltDurationMs(record, now));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** AGE: RAIL's 16:40 halt is 1h old at 17:45, though it lasted 35m. */
|
|
206
|
+
export function formatHaltAge(record: HaltRecord, now: number): string {
|
|
207
|
+
return formatElapsed(haltAgeMs(record, now));
|
|
208
|
+
}
|
|
209
|
+
|
|
191
210
|
export const HALT_FILTERS: ReadonlyArray<{ label: string; value: HaltFilter }> = [
|
|
192
211
|
{ label: "All", value: "all" },
|
|
193
212
|
{ label: "Active", value: "active" },
|
|
@@ -221,6 +240,7 @@ export type HaltColumnId =
|
|
|
221
240
|
| "halted"
|
|
222
241
|
| "quote"
|
|
223
242
|
| "trade"
|
|
243
|
+
| "duration"
|
|
224
244
|
| "status";
|
|
225
245
|
|
|
226
246
|
export type HaltColumn = DataTableColumn & { id: HaltColumnId };
|
|
@@ -235,6 +255,7 @@ export const HALT_SORT_COLUMN_IDS: readonly HaltColumnId[] = [
|
|
|
235
255
|
"halted",
|
|
236
256
|
"quote",
|
|
237
257
|
"trade",
|
|
258
|
+
"duration",
|
|
238
259
|
"status",
|
|
239
260
|
];
|
|
240
261
|
|
|
@@ -262,16 +283,18 @@ const HALT_COLUMNS: readonly HaltColumn[] = [
|
|
|
262
283
|
{ id: "halted", label: "HALT ET", width: 9, align: "left" },
|
|
263
284
|
{ id: "quote", label: "QUOTE ET", width: 11, align: "left" },
|
|
264
285
|
{ id: "trade", label: "TRADE ET", width: 11, align: "left" },
|
|
286
|
+
{ id: "duration", label: "HALTED FOR", width: 12, align: "right" },
|
|
265
287
|
{ id: "status", label: "STATUS", width: 8, align: "left" },
|
|
266
288
|
];
|
|
267
289
|
|
|
268
290
|
/**
|
|
269
291
|
* What a narrow pane gives up first, so STATUS never scrolls out of view. MKT
|
|
270
292
|
* is only the venue, STATUS already reads QUOTE once quoting resumes, and
|
|
271
|
-
* REASON spells out CODE. Trade resumption
|
|
272
|
-
* the
|
|
293
|
+
* REASON spells out CODE. Trade resumption, halt time and then the age, which
|
|
294
|
+
* the date and halt time also give, go only in the narrowest panes; symbol,
|
|
295
|
+
* reason, halt date, how long it was halted for and status always stay.
|
|
273
296
|
*/
|
|
274
|
-
const NARROW_DROP_ORDER: readonly HaltColumnId[] = ["market", "quote", "code", "company", "trade", "halted"];
|
|
297
|
+
const NARROW_DROP_ORDER: readonly HaltColumnId[] = ["market", "quote", "code", "company", "trade", "halted", "age"];
|
|
275
298
|
|
|
276
299
|
export function buildHaltColumns(width: number): HaltColumn[] {
|
|
277
300
|
const columns = fitChartTableColumns(HALT_COLUMNS, width, NARROW_DROP_ORDER);
|
|
@@ -304,7 +327,9 @@ function haltSortValue(columnId: HaltColumnId, record: HaltRecord, now: number):
|
|
|
304
327
|
case "halted":
|
|
305
328
|
return record.haltedAt;
|
|
306
329
|
case "age":
|
|
307
|
-
return
|
|
330
|
+
return haltAgeMs(record, now);
|
|
331
|
+
case "duration":
|
|
332
|
+
return haltDurationMs(record, now);
|
|
308
333
|
case "quote":
|
|
309
334
|
return record.quoteResumeAt;
|
|
310
335
|
case "trade":
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
formatEtResumption,
|
|
30
30
|
formatEtTime,
|
|
31
31
|
formatHaltAge,
|
|
32
|
+
formatHaltDuration,
|
|
32
33
|
haltStatusColor,
|
|
33
34
|
haltStatusLabel,
|
|
34
35
|
nextHaltFilter,
|
|
@@ -179,6 +180,8 @@ export function MarketHaltsPane({ focused, width, height }: PaneProps) {
|
|
|
179
180
|
text: formatEtResumption(row.tradeResumeAt, row.haltedAt),
|
|
180
181
|
color: selectedColor ?? colors.textDim,
|
|
181
182
|
};
|
|
183
|
+
case "duration":
|
|
184
|
+
return { text: formatHaltDuration(row, now), color: selectedColor ?? colors.textMuted };
|
|
182
185
|
case "status": {
|
|
183
186
|
const rowStatus = resolveHaltStatus(row, now);
|
|
184
187
|
return {
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
CloudSessionMoversSide,
|
|
6
6
|
} from "../../../api-client/market-movers";
|
|
7
7
|
import type { DataProvider } from "../../../types/data-provider";
|
|
8
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
8
9
|
import {
|
|
9
10
|
CATEGORY_MAP,
|
|
10
11
|
moverRegularSession,
|
|
@@ -63,7 +64,7 @@ async function hydrateTrending(
|
|
|
63
64
|
if (provider.getQuotesBatch) {
|
|
64
65
|
const results = await provider.getQuotesBatch(
|
|
65
66
|
symbols.map((symbol) => ({ symbol, exchange: "" })),
|
|
66
|
-
).catch((
|
|
67
|
+
).catch(logFailure("market-movers", "Trending quote batch failed", []));
|
|
67
68
|
for (const result of results) {
|
|
68
69
|
if (result.quote) bySymbol.set(result.target.symbol, screenerQuoteFromQuote(result.target.symbol, result.quote));
|
|
69
70
|
}
|
|
@@ -72,8 +73,8 @@ async function hydrateTrending(
|
|
|
72
73
|
try {
|
|
73
74
|
const quote = await provider.getQuote(symbol, "");
|
|
74
75
|
bySymbol.set(symbol, screenerQuoteFromQuote(symbol, quote));
|
|
75
|
-
} catch {
|
|
76
|
-
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logFailure("market-movers", "Trending quote failed")(error);
|
|
77
78
|
}
|
|
78
79
|
}));
|
|
79
80
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ScreenerQuote } from "../../../api-client/market-discovery";
|
|
2
2
|
export type { ScreenerQuote } from "../../../api-client/market-discovery";
|
|
3
3
|
import { resolveCurrencyUnit } from "../../../utils/currency-units";
|
|
4
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
4
5
|
import type { PluginPersistence } from "../../../types/plugin";
|
|
5
6
|
import { apiClient } from "../../../api-client";
|
|
6
7
|
import type {
|
|
@@ -324,7 +325,7 @@ async function bestEffortMarketMetadata(
|
|
|
324
325
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
325
326
|
try {
|
|
326
327
|
return await Promise.race([
|
|
327
|
-
request.then((result) => result.data).catch((
|
|
328
|
+
request.then((result) => result.data).catch(logFailure("market-movers", "Market metadata failed", [])),
|
|
328
329
|
new Promise<ScreenerQuote[]>((resolve) => {
|
|
329
330
|
timeout = setTimeout(() => resolve([]), waitMs);
|
|
330
331
|
}),
|
|
@@ -392,8 +393,8 @@ export async function fetchPreferredMarketMovers(
|
|
|
392
393
|
stale: response.stale === true || response.data.stale === true,
|
|
393
394
|
};
|
|
394
395
|
}
|
|
395
|
-
} catch {
|
|
396
|
-
|
|
396
|
+
} catch (error) {
|
|
397
|
+
logFailure("market-movers", "Live rankings failed")(error);
|
|
397
398
|
}
|
|
398
399
|
|
|
399
400
|
const fallback = await marketMetadata;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import {
|
|
3
|
+
DELAYED_SEGMENT,
|
|
3
4
|
PaneStatusBody, type DataTableCell,
|
|
4
5
|
type DataTableColumn,
|
|
5
6
|
type PaneFooterSegment
|
|
@@ -140,7 +141,7 @@ export function MarketValuationPane({ focused, width, height }: PaneProps) {
|
|
|
140
141
|
if (!selectedView) return [];
|
|
141
142
|
const info: PaneFooterSegment[] = [
|
|
142
143
|
{ id: "as-of", parts: [{ text: `as of ${selectedView.asOf}`, tone: "muted" }] },
|
|
143
|
-
...(width >= 80 ? [
|
|
144
|
+
...(width >= 80 ? [DELAYED_SEGMENT] : []),
|
|
144
145
|
];
|
|
145
146
|
if (selectedView.observationStale) {
|
|
146
147
|
info.push({ id: "stale", parts: [{ text: "STALE", tone: "warning", bold: true }] });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { FundChange, FundMember } from "../../../api-client/members";
|
|
3
|
-
import { DataTableStackView, DataTableView, DetailScrollBody, EmptyState, KeyValueRow, PaneFooterScope, PaneStatusBody, Prose, QueryBar, StatGrid,
|
|
3
|
+
import { delayedFooterSegment, DataTableStackView, DataTableView, DetailScrollBody, EmptyState, KeyValueRow, PaneFooterScope, PaneStatusBody, Prose, QueryBar, StatGrid,
|
|
4
4
|
isSectionedItemRow, renderSectionedRowHeader, useExternalLinkFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, useQueryBarSearch,
|
|
5
5
|
type DataTableCell, type DataTableColumn, type PaneFooterSegment, type SectionedRow } from "../../../components";
|
|
6
6
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
@@ -49,8 +49,13 @@ function Holdings({ fund, tab, width, height, focused, active }: ViewProps & { t
|
|
|
49
49
|
return [
|
|
50
50
|
{ id: "coverage", parts: [{ text: coverageText(data.aggregate, width < 60), tone: partial ? "warning" : "muted" }] },
|
|
51
51
|
// A capture's status line says the quotes' date and delay too; it does not say returns are unavailable.
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
...(data.quotesUnavailable
|
|
53
|
+
? [{ id: "quote-date", title: `${date ?? "Quote date unavailable"} · 15m delayed · holdings ${data.asOf}`, parts: [{ text: "returns unavailable", tone: "muted" as const }] }]
|
|
54
|
+
: [
|
|
55
|
+
delayedFooterSegment({ id: "quote-delay", detail: "15m delayed", delay: "15m", quoteStatus: true }),
|
|
56
|
+
...(date ? [{ id: "quote-date", title: `${date} · 15m delayed · holdings ${data.asOf}`, quoteStatus: true,
|
|
57
|
+
parts: [{ text: width < 60 && dates.length === 1 ? date.slice(5) : date, tone: "muted" as const }] }] : []),
|
|
58
|
+
]),
|
|
54
59
|
...(width >= 60 ? [{ id: "holdings-date", asOf: true, parts: [{ text: `holdings ${data.asOf}`, tone: "muted" as const }] }] : []),
|
|
55
60
|
];
|
|
56
61
|
}, [data, width]);
|
|
@@ -9,11 +9,13 @@ const USER_FEEDS_KEY = "feeds";
|
|
|
9
9
|
const DISABLED_DEFAULT_FEED_IDS_KEY = "disabledDefaultFeedIds";
|
|
10
10
|
|
|
11
11
|
const DEFAULT_FEED_IDS = new Set(DEFAULT_FEEDS.map((feed) => feed.id));
|
|
12
|
+
const UNREADABLE_SETTINGS = "News feed settings could not be read. Saved settings were kept.";
|
|
12
13
|
|
|
13
14
|
export interface NewsFeedSettings {
|
|
14
15
|
userFeeds: RssFeedConfig[];
|
|
15
16
|
disabledDefaultFeedIds: string[];
|
|
16
17
|
needsMigration: boolean;
|
|
18
|
+
error?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface UserNewsFeedInput {
|
|
@@ -25,15 +27,15 @@ export interface UserNewsFeedInput {
|
|
|
25
27
|
enabled?: boolean;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
function parseJsonArray(value: unknown): unknown[] {
|
|
30
|
+
function parseJsonArray(value: unknown): unknown[] | null {
|
|
29
31
|
if (Array.isArray(value)) return value;
|
|
30
|
-
if (typeof value !== "string"
|
|
32
|
+
if (typeof value !== "string") return [];
|
|
31
33
|
try {
|
|
32
34
|
const parsed = JSON.parse(value);
|
|
33
35
|
return Array.isArray(parsed) ? parsed : [];
|
|
34
36
|
} catch (error) {
|
|
35
37
|
logFailure("news.feed-config", "Read stored news feed settings failed")(error);
|
|
36
|
-
return
|
|
38
|
+
return null;
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -134,14 +136,20 @@ function sameIdList(raw: unknown, normalized: string[]): boolean {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
export function loadNewsFeedSettings(configState: PluginConfigState): NewsFeedSettings {
|
|
137
|
-
|
|
139
|
+
return parseNewsFeedSettings(configState.get(USER_FEEDS_KEY), configState.get(DISABLED_DEFAULT_FEED_IDS_KEY));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function parseNewsFeedSettings(rawUserFeeds: unknown, rawDisabledIds: unknown): NewsFeedSettings {
|
|
138
143
|
const userFeedValues = parseJsonArray(rawUserFeeds);
|
|
139
|
-
const userFeeds = userFeedValues
|
|
144
|
+
const userFeeds = (userFeedValues ?? [])
|
|
140
145
|
.map(normalizeUserFeed)
|
|
141
146
|
.filter((feed): feed is RssFeedConfig => !!feed);
|
|
142
147
|
|
|
143
|
-
const
|
|
144
|
-
const disabledDefaultFeedIds = normalizeDisabledDefaultFeedIds(
|
|
148
|
+
const disabledIdValues = parseJsonArray(rawDisabledIds);
|
|
149
|
+
const disabledDefaultFeedIds = normalizeDisabledDefaultFeedIds(disabledIdValues ?? []);
|
|
150
|
+
if (userFeedValues === null || disabledIdValues === null) {
|
|
151
|
+
return { userFeeds, disabledDefaultFeedIds, needsMigration: false, error: UNREADABLE_SETTINGS };
|
|
152
|
+
}
|
|
145
153
|
|
|
146
154
|
const needsMigration = (rawUserFeeds !== null && !sameUserFeeds(rawUserFeeds, userFeeds))
|
|
147
155
|
|| (rawDisabledIds !== null && !sameIdList(rawDisabledIds, disabledDefaultFeedIds));
|
|
@@ -149,15 +157,24 @@ export function loadNewsFeedSettings(configState: PluginConfigState): NewsFeedSe
|
|
|
149
157
|
return { userFeeds, disabledDefaultFeedIds, needsMigration };
|
|
150
158
|
}
|
|
151
159
|
|
|
160
|
+
function requireReadableSettings(configState: PluginConfigState): NewsFeedSettings {
|
|
161
|
+
const settings = loadNewsFeedSettings(configState);
|
|
162
|
+
if (settings.error) throw new Error(settings.error);
|
|
163
|
+
return settings;
|
|
164
|
+
}
|
|
165
|
+
|
|
152
166
|
export async function saveNewsFeedSettings(
|
|
153
167
|
configState: PluginConfigState,
|
|
154
168
|
settings: Pick<NewsFeedSettings, "userFeeds" | "disabledDefaultFeedIds">,
|
|
155
169
|
): Promise<void> {
|
|
170
|
+
requireReadableSettings(configState);
|
|
156
171
|
await configState.set(USER_FEEDS_KEY, settings.userFeeds);
|
|
172
|
+
requireReadableSettings(configState);
|
|
157
173
|
await configState.set(DISABLED_DEFAULT_FEED_IDS_KEY, settings.disabledDefaultFeedIds);
|
|
158
174
|
}
|
|
159
175
|
|
|
160
|
-
export function getEnabledNewsFeeds(settings: Pick<NewsFeedSettings, "userFeeds" | "disabledDefaultFeedIds">): RssFeedConfig[] {
|
|
176
|
+
export function getEnabledNewsFeeds(settings: Pick<NewsFeedSettings, "userFeeds" | "disabledDefaultFeedIds" | "error">): RssFeedConfig[] {
|
|
177
|
+
if (settings.error) return [];
|
|
161
178
|
const disabled = new Set(settings.disabledDefaultFeedIds);
|
|
162
179
|
return [
|
|
163
180
|
...DEFAULT_FEEDS.filter((feed) => feed.enabled && !disabled.has(feed.id)),
|
|
@@ -169,7 +186,7 @@ export async function addUserNewsFeed(
|
|
|
169
186
|
configState: PluginConfigState,
|
|
170
187
|
input: UserNewsFeedInput,
|
|
171
188
|
): Promise<RssFeedConfig> {
|
|
172
|
-
const settings =
|
|
189
|
+
const settings = requireReadableSettings(configState);
|
|
173
190
|
const feed = createUserFeed(input);
|
|
174
191
|
const nextUserFeeds = [
|
|
175
192
|
...settings.userFeeds.filter((entry) => entry.id !== feed.id),
|
|
@@ -187,7 +204,7 @@ export async function updateUserNewsFeed(
|
|
|
187
204
|
feedId: string,
|
|
188
205
|
patch: Partial<UserNewsFeedInput>,
|
|
189
206
|
): Promise<RssFeedConfig | null> {
|
|
190
|
-
const settings =
|
|
207
|
+
const settings = requireReadableSettings(configState);
|
|
191
208
|
const existing = settings.userFeeds.find((feed) => feed.id === feedId);
|
|
192
209
|
if (!existing) return null;
|
|
193
210
|
const nextFeed = createUserFeed({
|
|
@@ -206,7 +223,7 @@ export async function removeUserNewsFeed(
|
|
|
206
223
|
configState: PluginConfigState,
|
|
207
224
|
feedId: string,
|
|
208
225
|
): Promise<boolean> {
|
|
209
|
-
const settings =
|
|
226
|
+
const settings = requireReadableSettings(configState);
|
|
210
227
|
const nextUserFeeds = settings.userFeeds.filter((feed) => feed.id !== feedId);
|
|
211
228
|
if (nextUserFeeds.length === settings.userFeeds.length) return false;
|
|
212
229
|
await saveNewsFeedSettings(configState, {
|
|
@@ -222,7 +239,7 @@ export async function setDefaultNewsFeedEnabled(
|
|
|
222
239
|
enabled: boolean,
|
|
223
240
|
): Promise<boolean> {
|
|
224
241
|
if (!DEFAULT_FEED_IDS.has(feedId)) return false;
|
|
225
|
-
const settings =
|
|
242
|
+
const settings = requireReadableSettings(configState);
|
|
226
243
|
const disabled = new Set(settings.disabledDefaultFeedIds);
|
|
227
244
|
if (enabled) disabled.delete(feedId);
|
|
228
245
|
else disabled.add(feedId);
|
|
@@ -26,6 +26,7 @@ import { createRssNewsCapability } from "./rss/source";
|
|
|
26
26
|
import { rssCliCommand } from "./rss/cli";
|
|
27
27
|
import { newsFeedHeadless } from "../headless";
|
|
28
28
|
import type { PaneDef } from "../../../../types/plugin";
|
|
29
|
+
import { logFailure } from "../../../../utils/log-failure";
|
|
29
30
|
|
|
30
31
|
const TopPane = createNewsPresetPane({
|
|
31
32
|
paneKey: "top:curated",
|
|
@@ -195,11 +196,15 @@ export const newsWireModule: PluginModule = {
|
|
|
195
196
|
setup(ctx) {
|
|
196
197
|
const initialSettings = loadNewsFeedSettings(ctx.configState);
|
|
197
198
|
if (initialSettings.needsMigration) {
|
|
198
|
-
void saveNewsFeedSettings(ctx.configState, initialSettings)
|
|
199
|
+
void saveNewsFeedSettings(ctx.configState, initialSettings)
|
|
200
|
+
.catch(logFailure("news.feed-config", "Migrate news feed settings failed"));
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
const source = createRssNewsCapability(
|
|
202
|
-
() =>
|
|
204
|
+
() => {
|
|
205
|
+
const settings = loadNewsFeedSettings(ctx.configState);
|
|
206
|
+
return { feeds: getEnabledNewsFeeds(settings), error: settings.error };
|
|
207
|
+
},
|
|
203
208
|
{ persistence: ctx.persistence },
|
|
204
209
|
);
|
|
205
210
|
ctx.registerCapability(source);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
2
|
import { Box } from "../../../../../ui";
|
|
3
3
|
import type { NewsArticle, NewsQuery } from "../../../../../news/types";
|
|
4
4
|
import { useLoadNewsStory, useNewsArticles, useNewsTableLoadMore } from "../../../../../news/hooks";
|
|
5
5
|
import type { PaneProps } from "../../../../../types/plugin";
|
|
6
|
-
import { useDebouncedPluginPaneState, usePluginPaneState } from "../../../../runtime";
|
|
6
|
+
import { useDebouncedPluginPaneState, usePluginConfigState, usePluginPaneState } from "../../../../runtime";
|
|
7
7
|
import { PaneStatusBody, QueryBar, type QueryBarSearch } from "../../../../../components";
|
|
8
8
|
import { NewsDetailView, useNewsArticleDetail } from "./detail-view";
|
|
9
9
|
import {
|
|
@@ -18,6 +18,7 @@ import { newsListEmptyCopy } from "../filter-articles";
|
|
|
18
18
|
import { NEWS_LIST_SEARCH_PLACEHOLDER, useNewsListSearch, useNewsListSearchHint } from "./list-search";
|
|
19
19
|
import { newsMutesApplyToFeed, useNewsMuteFilter } from "../mutes";
|
|
20
20
|
import { usePopOutNewsArticle } from "./pop-out";
|
|
21
|
+
import { parseNewsFeedSettings } from "../feed-config";
|
|
21
22
|
|
|
22
23
|
interface NewsArticleStackOptions {
|
|
23
24
|
/** Prefix of every key the pane persists, and of its footer registration. */
|
|
@@ -146,6 +147,13 @@ export function NewsPresetPane({
|
|
|
146
147
|
emptyStateTitle,
|
|
147
148
|
emptyStateHint,
|
|
148
149
|
}: PaneProps & NewsPresetPaneConfig) {
|
|
150
|
+
const [storedFeeds] = usePluginConfigState<unknown>("feeds", null);
|
|
151
|
+
const [storedDisabledIds] = usePluginConfigState<unknown>("disabledDefaultFeedIds", null);
|
|
152
|
+
// The first query can finish before RSS registers, and desktop queries may
|
|
153
|
+
// use Cloud alone. A settings failure must remain visible in either case.
|
|
154
|
+
const settingsError = useMemo(() => query.feed === "latest"
|
|
155
|
+
? parseNewsFeedSettings(storedFeeds, storedDisabledIds).error
|
|
156
|
+
: undefined, [query.feed, storedFeeds, storedDisabledIds]);
|
|
149
157
|
const newsState = useNewsArticles(query);
|
|
150
158
|
const persisted = usePersistedNewsArticles(`${paneKey}:articles`, newsState.articles);
|
|
151
159
|
const articles = useNewsMuteFilter(persisted, newsMutesApplyToFeed(query.feed));
|
|
@@ -154,7 +162,7 @@ export function NewsPresetPane({
|
|
|
154
162
|
// body rather than a definitive empty wire.
|
|
155
163
|
const loading = newsState.phase === "loading"
|
|
156
164
|
|| (newsState.phase === "refreshing" && persisted.length === 0);
|
|
157
|
-
const error = newsState.error;
|
|
165
|
+
const error = settingsError ?? newsState.error;
|
|
158
166
|
const { search, searchQuery, ...stack } = useNewsArticleStack({
|
|
159
167
|
paneKey,
|
|
160
168
|
articles,
|
|
@@ -26,8 +26,9 @@ export const rssCliCommand: CliCommandDef = {
|
|
|
26
26
|
authority: 50,
|
|
27
27
|
enabled: true,
|
|
28
28
|
}]);
|
|
29
|
-
const
|
|
30
|
-
ctx.
|
|
29
|
+
const page = await capability.provider.fetchNewsPage!({ feed: "latest", limit: ctx.cliOptions.limit ?? 20 });
|
|
30
|
+
if (page.error) ctx.fail(page.error);
|
|
31
|
+
ctx.printResult({ data: page.articles.map((article) => ({
|
|
31
32
|
title: article.title,
|
|
32
33
|
source: article.source,
|
|
33
34
|
publishedAt: article.publishedAt.toISOString(),
|