gloomberb 0.18.2 → 0.18.4
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/README.md +8 -8
- package/README.zh-CN.md +11 -11
- package/bin/{gloomberb → gloom} +3 -1
- package/package.json +6 -2
- package/patches/electrobun@1.18.1.patch +106 -5
- package/src/account/checkout-handoff.tsx +225 -0
- package/src/account/device-signin-dialog.tsx +2 -2
- package/src/account/device-signin.ts +2 -2
- package/src/account/upgrade-dialog.tsx +1 -1
- package/src/api-client/askg.ts +2 -2
- package/src/api-client/auth.ts +2 -2
- package/src/api-client/central-bank-rates.ts +6 -0
- package/src/api-client/collections.ts +5 -1
- package/src/api-client/cot.ts +2 -0
- package/src/api-client/crypto-etf-flows.ts +37 -0
- package/src/api-client/crypto-markets.ts +60 -0
- package/src/api-client/data.ts +99 -5
- package/src/api-client/doe.ts +15 -0
- package/src/api-client/equity-screener.ts +28 -0
- package/src/api-client/exchange-announcements.ts +38 -0
- package/src/api-client/fund-facts.ts +39 -0
- package/src/api-client/futures-curve.ts +9 -1
- package/src/api-client/hiring.ts +301 -0
- package/src/api-client/index.ts +24 -5
- package/src/api-client/market-discovery.ts +14 -0
- package/src/api-client/members.ts +6 -0
- package/src/api-client/mna.ts +6 -0
- package/src/api-client/paths.ts +18 -1
- package/src/api-client/request.ts +26 -7
- package/src/api-client/short-side-publications.ts +37 -0
- package/src/api-client/socket.ts +64 -4
- package/src/api-client/swap-curve.ts +35 -0
- package/src/api-client/telemetry.ts +17 -0
- package/src/api-client/themes.ts +7 -1
- package/src/api-client/types.ts +90 -2
- package/src/app/global-shortcuts.ts +1 -16
- package/src/app/pane-runtime/plugin-bindings.ts +9 -2
- package/src/app/runtime/desktop-deeplink.ts +5 -5
- package/src/app/runtime/plugin-bindings.ts +2 -2
- package/src/app/runtime/startup.ts +13 -0
- package/src/app/runtime/update.ts +28 -8
- package/src/app/star-prompt/status-line.tsx +2 -2
- package/src/architecture/core-builtin-allowlist.json +5 -2
- package/src/architecture/pane-tabs-allowlist.json +65 -0
- package/src/architecture/ui-literals-allowlist.json +3 -3
- package/src/architecture/ui-tokens-allowlist.json +1 -1
- package/src/brokers/broker-portfolio-sync.ts +37 -0
- package/src/brokers/directory.ts +11 -0
- package/src/brokers/logos.ts +3 -1
- package/src/brokers/signed-in/adapter.ts +304 -26
- package/src/brokers/signed-in/client.ts +14 -2
- package/src/brokers/signed-in/connect.ts +10 -2
- package/src/brokers/sync-broker-instance.ts +9 -1
- package/src/capabilities/chart-series.ts +22 -6
- package/src/cli/bank-section.ts +1 -1
- package/src/cli/base-converter.ts +23 -14
- package/src/cli/central-bank-events.ts +1 -1
- package/src/cli/command-options.ts +59 -4
- package/src/cli/commands/api.ts +9 -6
- package/src/cli/commands/broker.ts +2 -2
- package/src/cli/commands/command-utils.ts +2 -2
- package/src/cli/commands/crypto-hints.ts +7 -20
- package/src/cli/commands/fred-series.ts +4 -2
- package/src/cli/commands/keybindings.ts +2 -2
- package/src/cli/commands/market.ts +1189 -296
- package/src/cli/commands/news-hints.ts +2 -2
- package/src/cli/commands/news-search.ts +59 -6
- package/src/cli/commands/options-leaps.ts +41 -26
- package/src/cli/commands/overview.ts +121 -48
- package/src/cli/commands/plugins.ts +9 -9
- package/src/cli/commands/remote.ts +13 -10
- package/src/cli/commands/search.ts +64 -44
- package/src/cli/commands/system.ts +27 -19
- package/src/cli/commands/themes.ts +1 -1
- package/src/cli/commands/ticker.ts +272 -70
- package/src/cli/context.ts +3 -2
- package/src/cli/desktop-pane-shot-view.ts +2 -2
- package/src/cli/desktop-pane-shot.ts +1 -1
- package/src/cli/earnings-calendar.ts +4 -4
- package/src/cli/entry.ts +18 -1
- package/src/cli/errors.ts +20 -2
- package/src/cli/exit-codes.ts +1 -1
- package/src/cli/export-source.ts +10 -4
- package/src/cli/freshness.ts +2 -8
- package/src/cli/fx-forward.ts +130 -0
- package/src/cli/fx-pair.ts +10 -9
- package/src/cli/glossary.ts +64 -20
- package/src/cli/help.ts +20 -14
- package/src/cli/history-export.ts +126 -0
- package/src/cli/history-rows.ts +196 -43
- package/src/cli/index-roots.ts +5 -45
- package/src/cli/index.ts +32 -18
- package/src/cli/issuer-membership.ts +103 -0
- package/src/cli/known-gaps.ts +118 -25
- package/src/cli/listing-arg.ts +315 -43
- package/src/cli/name-entries.ts +73 -22
- package/src/cli/native-smoke.ts +2 -0
- package/src/cli/not-a-ticker.ts +19 -9
- package/src/cli/opentui-libc.ts +5 -0
- package/src/cli/options.ts +12 -0
- package/src/cli/pane-functions/capabilities.ts +77 -20
- package/src/cli/pane-functions/catalog.ts +40 -23
- package/src/cli/pane-functions/discovery.ts +1 -1
- package/src/cli/pane-functions/dom.ts +12 -8
- package/src/cli/pane-functions/financial-ratio-shot.ts +2 -2
- package/src/cli/pane-functions/freshness.ts +174 -23
- package/src/cli/pane-functions/function-help.ts +66 -27
- package/src/cli/pane-functions/headless.ts +48 -16
- package/src/cli/pane-functions/index.ts +82 -21
- package/src/cli/pane-functions/options.ts +22 -2
- package/src/cli/pane-functions/report-notices.ts +12 -4
- package/src/cli/pane-functions/report.ts +5 -5
- package/src/cli/pane-functions/resolver.ts +65 -14
- package/src/cli/pane-functions/screenshot-assessment.ts +322 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +152 -0
- package/src/cli/pane-functions/screenshot-chart-evidence.ts +396 -0
- package/src/cli/pane-functions/screenshot-data-evidence.ts +383 -0
- package/src/cli/pane-functions/screenshot-evidence.ts +1 -1
- package/src/cli/pane-functions/screenshot-payload.ts +453 -0
- package/src/cli/pane-functions/screenshot-render.ts +316 -0
- package/src/cli/pane-functions/screenshot-types.ts +162 -0
- package/src/cli/pane-functions/shot-options.ts +8 -3
- package/src/cli/peso-dollar-rates.ts +79 -0
- package/src/cli/quote-answer.ts +38 -3
- package/src/cli/report-tables.ts +190 -27
- package/src/cli/restore-plugins.ts +29 -14
- package/src/cli/result.ts +122 -25
- package/src/cli/series-search.ts +5 -5
- package/src/cli/statement-filings.ts +19 -0
- package/src/cli/suffixed-listing-rows.ts +49 -0
- package/src/cli/symbol-args.ts +50 -15
- package/src/cli/symbol-cells.ts +120 -0
- package/src/cli/symbol-list.ts +226 -60
- package/src/cli/symbol-rows.ts +131 -25
- package/src/cli/ticker-resolution.ts +108 -11
- package/src/cli/treasury-yields.ts +5 -5
- package/src/cli/types.ts +2 -0
- package/src/cli/uncovered-instrument.ts +3 -3
- package/src/cli/unknown-command.ts +6 -6
- package/src/components/chart/composite/chart-layout.ts +131 -0
- package/src/components/chart/composite/composite-chart.tsx +387 -21
- package/src/components/chart/composite/drawing-store.ts +26 -6
- package/src/components/chart/composite/interactions.ts +9 -3
- package/src/components/chart/composite/legend.tsx +75 -3
- package/src/components/chart/composite/levels.ts +5 -0
- package/src/components/chart/composite/panel-gestures.ts +41 -0
- package/src/components/chart/composite/panel-labels.ts +2 -1
- package/src/components/chart/composite/panel-surface.tsx +316 -11
- package/src/components/chart/composite/price-axis-labels.tsx +27 -3
- package/src/components/chart/composite/rasterizer.ts +44 -19
- package/src/components/chart/composite/scene.ts +16 -4
- package/src/components/chart/composite/text-renderer.ts +48 -21
- package/src/components/chart/composite/toolbar.tsx +52 -5
- package/src/components/chart/composite/types.ts +19 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +120 -13
- package/src/components/chart/composite/use-composite-value-scales.ts +44 -0
- package/src/components/chart/composite/use-composite-viewport.ts +12 -3
- package/src/components/chart/composite/value-scale.ts +149 -0
- package/src/components/chart/composite/volume-profile-paint.ts +6 -3
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart-table/index.ts +2 -0
- package/src/components/chart-table/read.tsx +74 -0
- package/src/components/command-bar/commands/collection-list.ts +41 -19
- package/src/components/command-bar/commands/collection.ts +9 -3
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/commands/direct/runtime.ts +3 -2
- package/src/components/command-bar/pane-templates/items.ts +59 -6
- package/src/components/command-bar/pane-templates/workflow.ts +10 -1
- package/src/components/command-bar/routes/root/command-items.ts +32 -6
- package/src/components/command-bar/routes/root/plugin-install.ts +1 -1
- package/src/components/command-bar/routes/root/provider-search.ts +42 -11
- package/src/components/command-bar/routes/root/results.ts +18 -4
- package/src/components/command-bar/routes/root/runtime.ts +15 -3
- package/src/components/command-bar/routes/root/search-providers.ts +25 -2
- package/src/components/command-bar/routes/root/selection.ts +6 -1
- package/src/components/command-bar/routes/root/shortcut-feedback.ts +4 -1
- package/src/components/command-bar/routes/root/shortcuts.ts +30 -3
- package/src/components/command-bar/routes/ticker-search/results.ts +73 -4
- package/src/components/command-bar/selection-runtime.ts +50 -10
- package/src/components/command-bar/surface/index.tsx +24 -2
- package/src/components/command-bar/view-model.ts +7 -0
- package/src/components/command-bar/workflow/broker.ts +2 -2
- package/src/components/command-bar/workflow/collection-actions.ts +46 -7
- package/src/components/command-bar/workflow/collection-ticker.ts +32 -4
- package/src/components/command-bar/workflow/fields.ts +8 -2
- package/src/components/command-bar/workflow/ops.ts +18 -4
- package/src/components/command-bar/workflow/portfolio-position.ts +53 -4
- package/src/components/command-bar/workflow/submit.ts +3 -2
- package/src/components/command-bar/workflow/tickers.ts +5 -1
- package/src/components/company-logo.tsx +39 -0
- package/src/components/data-table/sections.ts +4 -2
- package/src/components/data-table/view.tsx +23 -3
- package/src/components/form-modal/confirm.tsx +2 -1
- package/src/components/form-modal/request.ts +5 -0
- package/src/components/form-modal/routes.ts +4 -1
- package/src/components/index.ts +13 -6
- 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 +234 -60
- package/src/components/layout/market-summary.ts +9 -6
- package/src/components/layout/pane/capture-status.ts +1 -1
- package/src/components/layout/pane/footer/model.ts +11 -5
- 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/layout/shell/chrome.ts +19 -6
- package/src/components/layout/shell/index.tsx +2 -1
- package/src/components/layout/shell/pane/layers.tsx +19 -1
- package/src/components/layout/status-bar.tsx +2 -2
- package/src/components/market-board/index.tsx +11 -3
- package/src/components/onboarding/onboarding-wizard.tsx +1 -0
- package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +2 -2
- package/src/components/onboarding/wizard-broker-sync.ts +4 -1
- package/src/components/pane-settings-dialog/desktop.tsx +20 -7
- package/src/components/pane-settings-dialog/tui.tsx +17 -9
- package/src/components/pane-settings-dialog/value.ts +8 -1
- package/src/components/pane-settings-dialog.tsx +44 -13
- package/src/components/sign-in-code-panel.tsx +27 -19
- package/src/components/ui/column-grid.tsx +48 -4
- package/src/components/ui/country-flag-art.tsx +180 -0
- package/src/components/ui/country-flag.tsx +118 -12
- package/src/components/ui/data-table/opentui/index.tsx +61 -2
- package/src/components/ui/data-table/opentui/model.ts +109 -2
- package/src/components/ui/data-table/types.ts +1 -1
- package/src/components/ui/display.tsx +5 -1
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/logo-mark.tsx +11 -2
- package/src/components/ui/query-bar.tsx +1 -1
- package/src/components/ui/row-notes.ts +79 -0
- package/src/components/ui/stat-grid.tsx +28 -3
- package/src/components/ui/status.tsx +3 -1
- package/src/core/app-services.ts +4 -0
- package/src/core/state/app/layout.ts +4 -4
- package/src/data/cached-query.ts +8 -1
- package/src/data/config/chart-settings.ts +3 -3
- package/src/data/config/home.ts +10 -2
- package/src/data/config/layout.ts +4 -1
- package/src/data/config/store/node.ts +4 -0
- package/src/data/config/store/normalize.ts +13 -3
- package/src/data/config/store/portfolio-as-of-column.ts +2 -2
- package/src/data/json-storage.ts +2 -0
- package/src/data/saved-copies.ts +84 -0
- package/src/docs/snapshot.json +65 -32
- package/src/i18n/es.ts +51 -12
- package/src/i18n/ja.ts +51 -12
- package/src/i18n/ko.ts +51 -12
- package/src/i18n/zh-cn.ts +51 -12
- package/src/i18n/zh-tw.ts +51 -12
- package/src/layout/desks.ts +1 -1
- package/src/layout/launch-layout.ts +1 -1
- package/src/layout/pane-manager/floating-actions.ts +5 -3
- package/src/layout/pane-manager/floating.ts +43 -1
- package/src/layout/pane-manager/index.ts +2 -0
- package/src/layout/selected-ticker.ts +74 -0
- package/src/market-data/depositary-pair.ts +251 -0
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +124 -40
- package/src/market-data/fx-closes.ts +17 -2
- package/src/market-data/hooks.tsx +19 -11
- package/src/market-data/market/change-basis.ts +49 -7
- package/src/market-data/market/freshness.ts +34 -5
- package/src/market-data/market/period-return.ts +163 -0
- package/src/market-data/market/trading-sessions.ts +13 -0
- package/src/market-data/market/year-to-date.ts +8 -13
- package/src/market-data/performance.ts +20 -14
- package/src/market-data/perpetual-premium.ts +32 -0
- package/src/market-data/published-apac-sessions.ts +2 -1
- package/src/market-data/published-futures-sessions.ts +69 -0
- package/src/market-data/quotes/contributions.ts +5 -1
- package/src/market-data/quotes/freshness.ts +42 -0
- package/src/market-data/quotes/intraday-bar.ts +37 -0
- package/src/market-data/quotes/regular-session-valuation.ts +13 -8
- package/src/market-data/quotes/resolution.ts +4 -2
- package/src/market-data/tax-equivalent.ts +1 -1
- package/src/market-data/ticker-price-returns.ts +3 -1
- package/src/news/aggregator.ts +5 -1
- package/src/news/news-model.ts +4 -0
- package/src/news/types.ts +11 -0
- package/src/notifications/app-notifier.ts +1 -1
- package/src/plugins/auto-update.ts +2 -2
- package/src/plugins/builtin/account-management/index.tsx +1 -0
- package/src/plugins/builtin/account-management/pane.tsx +9 -8
- package/src/plugins/builtin/alerts/alert-engine.ts +30 -18
- package/src/plugins/builtin/alerts/calendar-events.ts +378 -0
- package/src/plugins/builtin/alerts/cli.ts +485 -76
- package/src/plugins/builtin/alerts/command.ts +5 -10
- package/src/plugins/builtin/alerts/event-rows.ts +9 -5
- package/src/plugins/builtin/alerts/events-pane.tsx +71 -56
- package/src/plugins/builtin/alerts/events.ts +108 -5
- package/src/plugins/builtin/alerts/fired-log.ts +78 -0
- package/src/plugins/builtin/alerts/format.ts +2 -1
- package/src/plugins/builtin/alerts/headless.ts +100 -39
- package/src/plugins/builtin/alerts/history-pane.tsx +105 -62
- package/src/plugins/builtin/alerts/history-rows.ts +89 -0
- package/src/plugins/builtin/alerts/index.tsx +45 -18
- package/src/plugins/builtin/alerts/iv-level.ts +365 -0
- package/src/plugins/builtin/alerts/levels.ts +3 -3
- package/src/plugins/builtin/alerts/pane.tsx +97 -29
- package/src/plugins/builtin/alerts/price.ts +35 -0
- package/src/plugins/builtin/alerts/quotes.ts +24 -0
- package/src/plugins/builtin/alerts/research-builder.ts +84 -11
- package/src/plugins/builtin/alerts/research-rules.ts +77 -3
- package/src/plugins/builtin/alerts/scheduled-check.ts +206 -32
- package/src/plugins/builtin/alerts/scheduled-sources.ts +26 -3
- package/src/plugins/builtin/alerts/scheduled.ts +188 -29
- package/src/plugins/builtin/alerts/storage.ts +13 -10
- package/src/plugins/builtin/alerts/targets.ts +12 -3
- package/src/plugins/builtin/analytics/index.tsx +8 -2
- package/src/plugins/builtin/analytics/metrics.ts +1 -1
- package/src/plugins/builtin/analytics/risk-book.ts +215 -0
- package/src/plugins/builtin/analytics/risk-client.ts +13 -6
- package/src/plugins/builtin/analytics/risk-headless.ts +55 -21
- package/src/plugins/builtin/analytics/risk-math.ts +6 -3
- package/src/plugins/builtin/analytics/risk-model.ts +297 -104
- package/src/plugins/builtin/analytics/risk-options.ts +17 -2
- package/src/plugins/builtin/analytics/risk-pane.tsx +107 -43
- package/src/plugins/builtin/attention/pane.tsx +22 -16
- package/src/plugins/builtin/awards/headless.ts +7 -3
- package/src/plugins/builtin/awards/model.ts +18 -0
- package/src/plugins/builtin/awards/pane.tsx +12 -9
- package/src/plugins/builtin/backtest/export.ts +1 -1
- package/src/plugins/builtin/backtest/headless.ts +3 -3
- package/src/plugins/builtin/backtest/rules.ts +41 -5
- package/src/plugins/builtin/broker-manager/add-flow.tsx +12 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +47 -19
- package/src/plugins/builtin/broker-manager/footer.ts +27 -11
- package/src/plugins/builtin/broker-manager/index.tsx +21 -12
- package/src/plugins/builtin/broker-manager/keyboard.ts +1 -1
- package/src/plugins/builtin/broker-manager/model.ts +12 -2
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +183 -57
- package/src/plugins/builtin/broker-manager/table.ts +28 -12
- package/src/plugins/builtin/broker-trading/account-picker.tsx +2 -1
- package/src/plugins/builtin/broker-trading/chart-levels.ts +82 -13
- package/src/plugins/builtin/broker-trading/controller.ts +45 -13
- package/src/plugins/builtin/broker-trading/dock.tsx +24 -4
- package/src/plugins/builtin/broker-trading/headless.ts +2 -1
- package/src/plugins/builtin/broker-trading/index.tsx +7 -1
- package/src/plugins/builtin/broker-trading/linked-orders.ts +148 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +75 -7
- package/src/plugins/builtin/broker-trading/orders.tsx +109 -31
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +9 -3
- package/src/plugins/builtin/broker-trading/pane-model.ts +30 -2
- package/src/plugins/builtin/broker-trading/pane.tsx +115 -26
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +36 -7
- package/src/plugins/builtin/broker-trading/ticket.tsx +122 -36
- package/src/plugins/builtin/buildout/cells.tsx +4 -0
- package/src/plugins/builtin/buildout/detail/index.tsx +5 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +7 -4
- package/src/plugins/builtin/buildout/pane/index.tsx +34 -18
- package/src/plugins/builtin/buildout/pane/status.tsx +0 -4
- package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
- package/src/plugins/builtin/catalysts/client.ts +2 -2
- package/src/plugins/builtin/catalysts/detail.tsx +3 -2
- package/src/plugins/builtin/catalysts/index.tsx +1 -1
- package/src/plugins/builtin/catalysts/model.ts +49 -4
- package/src/plugins/builtin/catalysts/pane.tsx +9 -11
- package/src/plugins/builtin/central-bank-rates/client.ts +5 -0
- package/src/plugins/builtin/central-bank-rates/headless.ts +36 -10
- package/src/plugins/builtin/central-bank-rates/model.ts +33 -8
- package/src/plugins/builtin/changelog/index.tsx +1 -0
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +1 -0
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +2 -1
- package/src/plugins/builtin/chart-composer/chart-spec.ts +7 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +2 -5
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +4 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +28 -26
- package/src/plugins/builtin/chart-composer/headless.ts +253 -35
- package/src/plugins/builtin/chart-composer/index.tsx +12 -2
- package/src/plugins/builtin/chart-composer/leverage-overlay.ts +142 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +128 -28
- package/src/plugins/builtin/chart-composer/series-catalog.ts +1 -0
- package/src/plugins/builtin/chart-composer/series-expression.ts +65 -6
- package/src/plugins/builtin/chart-composer/session-history.ts +15 -21
- package/src/plugins/builtin/chart-composer/session-range.ts +4 -2
- package/src/plugins/builtin/chart-composer/settings.ts +31 -19
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +5 -6
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +18 -8
- package/src/plugins/builtin/chat/attachments/model.ts +3 -1
- package/src/plugins/builtin/chat/content/index.tsx +1 -1
- package/src/plugins/builtin/chat/pane-links/open.ts +1 -1
- package/src/plugins/builtin/chat/people.ts +1 -1
- package/src/plugins/builtin/clinical-trials/client.ts +5 -2
- package/src/plugins/builtin/clinical-trials/model.ts +13 -4
- package/src/plugins/builtin/clinical-trials/pane.tsx +32 -12
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +17 -0
- package/src/plugins/builtin/cloud/askg/controller.ts +1 -1
- package/src/plugins/builtin/cloud/askg/lock-note.ts +1 -1
- package/src/plugins/builtin/cloud/askg/tool-display.ts +1 -1
- package/src/plugins/builtin/cloud/auth-commands.ts +3 -3
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +105 -47
- package/src/plugins/builtin/cloud/team/pane-ui.tsx +42 -4
- package/src/plugins/builtin/cloud/team/pane.tsx +204 -64
- package/src/plugins/builtin/cloud/thesis/store.ts +45 -22
- package/src/plugins/builtin/cloud-tweets/registration.ts +1 -0
- package/src/plugins/builtin/commodities-energy/index.tsx +29 -2
- package/src/plugins/builtin/commodities-energy/view-state.ts +2 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +14 -8
- package/src/plugins/builtin/company-attention/app-rank.tsx +11 -10
- package/src/plugins/builtin/company-attention/building-model.ts +222 -0
- package/src/plugins/builtin/company-attention/building.tsx +172 -0
- package/src/plugins/builtin/company-attention/client.ts +117 -1
- package/src/plugins/builtin/company-attention/evidence.ts +16 -3
- package/src/plugins/builtin/company-attention/headless.ts +101 -5
- package/src/plugins/builtin/company-attention/index.tsx +6 -3
- package/src/plugins/builtin/company-attention/model.ts +33 -7
- package/src/plugins/builtin/company-attention/moves-model.ts +253 -0
- package/src/plugins/builtin/company-attention/moves.tsx +159 -0
- package/src/plugins/builtin/company-attention/pane.tsx +89 -31
- package/src/plugins/builtin/company-attention/signals-model.ts +229 -0
- package/src/plugins/builtin/company-attention/signals.tsx +160 -0
- package/src/plugins/builtin/company-kpis/headless.ts +3 -2
- package/src/plugins/builtin/company-kpis/model.ts +2 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +5 -4
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/congress-trades/headless.ts +1 -1
- package/src/plugins/builtin/connections/index.ts +1 -0
- package/src/plugins/builtin/correlation/headless-schema.ts +3 -2
- package/src/plugins/builtin/correlation/headless.ts +7 -4
- 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 +28 -6
- package/src/plugins/builtin/cot/model.ts +12 -0
- package/src/plugins/builtin/cot/pane.tsx +7 -5
- package/src/plugins/builtin/cpi/headless.ts +5 -1
- package/src/plugins/builtin/cpi/pane.tsx +11 -9
- package/src/plugins/builtin/credit-boards/model.ts +2 -0
- package/src/plugins/builtin/credit-documents/headless.ts +1 -1
- package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
- package/src/plugins/builtin/crypto-board/client.ts +86 -0
- package/src/plugins/builtin/crypto-board/defi-view.tsx +218 -0
- package/src/plugins/builtin/crypto-board/defi.ts +239 -0
- package/src/plugins/builtin/crypto-board/headless.ts +117 -6
- package/src/plugins/builtin/crypto-board/index.tsx +8 -3
- package/src/plugins/builtin/crypto-board/model.ts +40 -8
- package/src/plugins/builtin/crypto-board/pane.tsx +55 -21
- package/src/plugins/builtin/crypto-board/series.ts +19 -1
- package/src/plugins/builtin/crypto-etf-flows/client.ts +60 -0
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +132 -0
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +44 -0
- package/src/plugins/builtin/crypto-etf-flows/model.ts +121 -0
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +193 -0
- package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
- package/src/plugins/builtin/debug/index.tsx +1 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
- package/src/plugins/builtin/docs/index.tsx +1 -1
- package/src/plugins/builtin/docs/pane.tsx +2 -2
- package/src/plugins/builtin/docs/targets.ts +1 -1
- package/src/plugins/builtin/doe/client.ts +5 -1
- package/src/plugins/builtin/doe/headless.ts +12 -6
- package/src/plugins/builtin/doe/model.ts +43 -6
- package/src/plugins/builtin/doe/pane.tsx +19 -12
- package/src/plugins/builtin/earnings/board-model.ts +13 -6
- package/src/plugins/builtin/earnings/board-table.ts +4 -1
- package/src/plugins/builtin/earnings/board.tsx +253 -50
- package/src/plugins/builtin/earnings/calendar-model.ts +188 -0
- package/src/plugins/builtin/earnings/calendar.tsx +338 -0
- package/src/plugins/builtin/earnings/client.ts +4 -3
- package/src/plugins/builtin/earnings/headless.ts +53 -15
- package/src/plugins/builtin/earnings/history-model.ts +53 -36
- package/src/plugins/builtin/earnings/index.tsx +15 -22
- package/src/plugins/builtin/earnings/model.ts +17 -12
- package/src/plugins/builtin/earnings/scope.ts +89 -0
- package/src/plugins/builtin/earnings-calls/calendar.tsx +310 -0
- package/src/plugins/builtin/earnings-calls/data.ts +146 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +151 -9
- package/src/plugins/builtin/earnings-calls/index.tsx +3 -3
- package/src/plugins/builtin/earnings-calls/lock-note.ts +1 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +149 -31
- package/src/plugins/builtin/earnings-calls/upcoming.ts +195 -0
- package/src/plugins/builtin/earnings-ripple/pane.tsx +10 -5
- package/src/plugins/builtin/econ/calendar-model.ts +132 -20
- package/src/plugins/builtin/econ/detail-view.tsx +48 -10
- package/src/plugins/builtin/econ/fred-catalog.ts +23 -6
- package/src/plugins/builtin/econ/fred-series-map.ts +4 -0
- package/src/plugins/builtin/econ/index.tsx +112 -25
- package/src/plugins/builtin/econ/print-watch.ts +2 -1
- package/src/plugins/builtin/econ/types.ts +32 -1
- package/src/plugins/builtin/econ-statistics/client.ts +16 -3
- package/src/plugins/builtin/econ-statistics/defs.ts +4 -0
- package/src/plugins/builtin/econ-statistics/detail.tsx +2 -2
- package/src/plugins/builtin/econ-statistics/headless.ts +13 -2
- package/src/plugins/builtin/econ-statistics/index.tsx +3 -3
- package/src/plugins/builtin/econ-statistics/pane.tsx +2 -1
- package/src/plugins/builtin/econ-statistics/stats.ts +103 -0
- package/src/plugins/builtin/econ-statistics/view.ts +16 -1
- package/src/plugins/builtin/equity-screener/client.ts +21 -3
- package/src/plugins/builtin/equity-screener/headless.ts +77 -5
- package/src/plugins/builtin/equity-screener/live.ts +1 -1
- package/src/plugins/builtin/equity-screener/model.ts +34 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +148 -106
- package/src/plugins/builtin/equity-screener/screen-check.ts +90 -0
- package/src/plugins/builtin/equity-screener/ticker-check.tsx +148 -0
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -1
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +3 -3
- package/src/plugins/builtin/estimate-revisions/model.ts +8 -10
- package/src/plugins/builtin/exposure/headless.ts +51 -6
- package/src/plugins/builtin/exposure/index.tsx +28 -2
- package/src/plugins/builtin/exposure/model.ts +7 -0
- package/src/plugins/builtin/exposure/pane.tsx +16 -10
- package/src/plugins/builtin/fear-greed/index.tsx +1 -1
- package/src/plugins/builtin/filing-events/index.tsx +1 -1
- package/src/plugins/builtin/futures/model.ts +10 -9
- package/src/plugins/builtin/futures/returns.ts +8 -36
- package/src/plugins/builtin/futures/table.tsx +1 -1
- package/src/plugins/builtin/futures-curve/client.ts +40 -9
- package/src/plugins/builtin/futures-curve/headless.ts +39 -17
- package/src/plugins/builtin/futures-curve/model.ts +258 -69
- package/src/plugins/builtin/futures-curve/pane.tsx +46 -17
- package/src/plugins/builtin/fx-matrix/client.ts +15 -8
- package/src/plugins/builtin/fx-matrix/headless.ts +32 -14
- package/src/plugins/builtin/fx-matrix/index.tsx +13 -11
- package/src/plugins/builtin/fx-matrix/live-legs.ts +22 -0
- package/src/plugins/builtin/fx-matrix/settings.ts +2 -1
- 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/headless.ts +9 -4
- package/src/plugins/builtin/gpu/history.tsx +4 -2
- package/src/plugins/builtin/gpu/pane.tsx +5 -3
- package/src/plugins/builtin/help/index.tsx +13 -3
- package/src/plugins/builtin/holders/beneficial-model.ts +8 -11
- package/src/plugins/builtin/holders/beneficial-report.ts +6 -5
- package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
- package/src/plugins/builtin/holders/client.ts +15 -2
- package/src/plugins/builtin/holders/format.ts +28 -17
- package/src/plugins/builtin/holders/headless.ts +48 -8
- package/src/plugins/builtin/holders/index-holders.ts +264 -0
- package/src/plugins/builtin/holders/pane.tsx +79 -15
- package/src/plugins/builtin/holders/report-rows.ts +35 -7
- package/src/plugins/builtin/holders/table-model.ts +2 -1
- package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
- package/src/plugins/builtin/holders/treemap.tsx +2 -2
- package/src/plugins/builtin/insider/base-values.ts +11 -7
- package/src/plugins/builtin/insider/client.ts +3 -0
- package/src/plugins/builtin/insider/headless.ts +20 -7
- 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 +3 -3
- package/src/plugins/builtin/insider/signals.tsx +1 -1
- package/src/plugins/builtin/insider/table-model.ts +35 -13
- package/src/plugins/builtin/insider/table.tsx +5 -2
- package/src/plugins/builtin/ipo-calendar/pane.tsx +7 -4
- package/src/plugins/builtin/iv-history/client.ts +20 -0
- 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/iv-history/index.tsx +1 -1
- package/src/plugins/builtin/iv-history/model.ts +13 -4
- package/src/plugins/builtin/iv-history/series.ts +3 -1
- package/src/plugins/builtin/jobs/lock-note.ts +1 -1
- package/src/plugins/builtin/jobs/model.ts +4 -15
- package/src/plugins/builtin/jobs/pane.tsx +7 -5
- package/src/plugins/builtin/jobs/share-bars.tsx +11 -5
- package/src/plugins/builtin/jodi/pane.tsx +14 -8
- package/src/plugins/builtin/layout-manager/index.tsx +1 -0
- package/src/plugins/builtin/macro-day/headless.ts +20 -7
- package/src/plugins/builtin/macro-day/index.tsx +3 -2
- package/src/plugins/builtin/macro-day/model.ts +29 -8
- package/src/plugins/builtin/macro-day/pane.tsx +46 -10
- package/src/plugins/builtin/macro-day/releases.ts +55 -13
- package/src/plugins/builtin/market-heatmap/data.ts +9 -2
- package/src/plugins/builtin/market-heatmap/evidence.ts +1 -1
- package/src/plugins/builtin/market-heatmap/headless.ts +9 -6
- package/src/plugins/builtin/market-heatmap/index.tsx +3 -3
- package/src/plugins/builtin/market-heatmap/model.ts +10 -7
- package/src/plugins/builtin/market-movers/client.ts +36 -21
- package/src/plugins/builtin/market-movers/headless.ts +8 -2
- package/src/plugins/builtin/market-movers/index.tsx +16 -10
- package/src/plugins/builtin/market-movers/screener.ts +33 -11
- package/src/plugins/builtin/market-movers/session-body.tsx +4 -2
- package/src/plugins/builtin/market-movers/session-table.ts +4 -2
- package/src/plugins/builtin/market-movers/table.ts +4 -1
- package/src/plugins/builtin/market-valuation/headless.ts +2 -0
- package/src/plugins/builtin/market-valuation/view.ts +5 -0
- package/src/plugins/builtin/members/headless.ts +6 -5
- package/src/plugins/builtin/members/pane.tsx +11 -4
- package/src/plugins/builtin/mna/client.ts +1 -0
- package/src/plugins/builtin/mna/detail.tsx +20 -32
- package/src/plugins/builtin/mna/distress/detail.tsx +16 -4
- package/src/plugins/builtin/mna/distress/model.ts +3 -2
- package/src/plugins/builtin/mna/distress/pane.tsx +7 -4
- package/src/plugins/builtin/mna/headless.ts +6 -13
- package/src/plugins/builtin/mna/model.ts +93 -15
- package/src/plugins/builtin/mna/pane.tsx +5 -4
- package/src/plugins/builtin/news/headless.ts +12 -6
- package/src/plugins/builtin/news/index.tsx +10 -1
- package/src/plugins/builtin/news/wire/index.tsx +3 -2
- package/src/plugins/builtin/news/wire/news/command-bar-search.ts +26 -10
- package/src/plugins/builtin/news/wire/news/query-presets.ts +6 -2
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -3
- package/src/plugins/builtin/notes/cli.ts +50 -15
- package/src/plugins/builtin/notes/export.ts +17 -6
- package/src/plugins/builtin/notes/files.ts +16 -5
- package/src/plugins/builtin/openfda/pane.tsx +1 -1
- package/src/plugins/builtin/options/carry.ts +2 -2
- package/src/plugins/builtin/options/enrichment-model.ts +10 -9
- package/src/plugins/builtin/options/index.tsx +1 -1
- package/src/plugins/builtin/options/model-spot.ts +70 -4
- package/src/plugins/builtin/options/view.tsx +51 -23
- package/src/plugins/builtin/options-calculator/headless.ts +46 -7
- package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
- 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 +142 -29
- package/src/plugins/builtin/options-scenario/collar.ts +8 -6
- package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +127 -0
- package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +165 -0
- package/src/plugins/builtin/options-scenario/csp-screen.ts +258 -0
- package/src/plugins/builtin/options-scenario/csp.ts +101 -21
- package/src/plugins/builtin/options-scenario/headless.ts +49 -21
- package/src/plugins/builtin/options-scenario/hedge.ts +20 -14
- package/src/plugins/builtin/options-scenario/index.tsx +28 -3
- package/src/plugins/builtin/options-scenario/model.ts +13 -0
- package/src/plugins/builtin/options-scenario/pane.tsx +22 -10
- package/src/plugins/builtin/perps/equity-row.tsx +11 -3
- package/src/plugins/builtin/perps/freshness.ts +8 -5
- package/src/plugins/builtin/perps/headless.ts +3 -1
- package/src/plugins/builtin/perps/list-perpetuals.ts +25 -4
- package/src/plugins/builtin/perps/live-rows.ts +161 -0
- package/src/plugins/builtin/perps/markets-section.tsx +154 -0
- package/src/plugins/builtin/perps/pane.tsx +36 -17
- package/src/plugins/builtin/perps/venue-markets.ts +96 -0
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -1
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +145 -60
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +78 -27
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +158 -141
- package/src/plugins/builtin/portfolio-list/cli/render.ts +49 -20
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +3 -3
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +29 -24
- package/src/plugins/builtin/portfolio-list/headless.ts +6 -3
- package/src/plugins/builtin/portfolio-list/income.ts +13 -2
- package/src/plugins/builtin/portfolio-list/index.tsx +9 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +21 -8
- package/src/plugins/builtin/portfolio-list/pane/purchase-date-fx.ts +46 -0
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +16 -9
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +2 -15
- package/src/plugins/builtin/power/client.ts +6 -3
- package/src/plugins/builtin/power/evidence.ts +22 -7
- package/src/plugins/builtin/power/gas.tsx +5 -1
- package/src/plugins/builtin/power/pane.tsx +44 -32
- package/src/plugins/builtin/power/prices.tsx +10 -3
- package/src/plugins/builtin/research/analyst-headless.ts +25 -8
- package/src/plugins/builtin/research/analyst-model.ts +68 -3
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +63 -14
- package/src/plugins/builtin/research/event-model.ts +3 -1
- package/src/plugins/builtin/research/event-sources.ts +12 -4
- package/src/plugins/builtin/research/index.tsx +2 -1
- package/src/plugins/builtin/research/relative-valuation-basis.ts +58 -0
- package/src/plugins/builtin/research/relative-valuation-columns.ts +31 -2
- package/src/plugins/builtin/research/relative-valuation-currency.ts +12 -5
- package/src/plugins/builtin/research/relative-valuation-headless.ts +84 -26
- package/src/plugins/builtin/research/relative-valuation-model.ts +88 -19
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +76 -9
- package/src/plugins/builtin/research/relative-valuation-peers.ts +1 -1
- package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +10 -5
- package/src/plugins/builtin/risk-factors/index.tsx +2 -2
- package/src/plugins/builtin/scanner/lock-note.ts +1 -1
- package/src/plugins/builtin/seasonality/client.ts +2 -1
- package/src/plugins/builtin/sec/forms.ts +13 -0
- package/src/plugins/builtin/sec/fund-abroad.ts +32 -0
- package/src/plugins/builtin/sec/fund-facts.ts +68 -8
- package/src/plugins/builtin/sec/headless.ts +85 -8
- package/src/plugins/builtin/sec/index.tsx +58 -12
- package/src/plugins/builtin/sectors/client.ts +6 -4
- package/src/plugins/builtin/sectors/sector-model.ts +11 -15
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/broker-reconnect.ts +107 -22
- package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
- package/src/plugins/builtin/shared/cloud-upgrade.ts +34 -6
- package/src/plugins/builtin/shared/earnings-calendar.ts +375 -0
- package/src/plugins/builtin/{earnings/estimate-basis.ts → shared/earnings-estimate-basis.ts} +1 -1
- package/src/plugins/builtin/shared/earnings-report.ts +3 -3
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +23 -1
- package/src/plugins/builtin/shared/job-posting.ts +129 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +3 -3
- package/src/plugins/builtin/shared/report-freshness.ts +3 -0
- package/src/plugins/builtin/shared/research-trading.tsx +23 -0
- package/src/plugins/builtin/shared/series-cache.ts +2 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
- package/src/plugins/builtin/shared/ticker-surface.ts +2 -1
- package/src/plugins/builtin/shared/volatility/options.ts +2 -1
- package/src/plugins/builtin/short-interest/client.ts +2 -0
- package/src/plugins/builtin/short-interest/headless.ts +41 -12
- package/src/plugins/builtin/short-interest/index.tsx +23 -1
- package/src/plugins/builtin/short-interest/model.ts +70 -19
- package/src/plugins/builtin/short-interest/pane.tsx +19 -2
- package/src/plugins/builtin/short-interest/publications-client.ts +82 -0
- package/src/plugins/builtin/short-interest/publications-headless.ts +29 -0
- package/src/plugins/builtin/short-interest/publications-model.ts +50 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +66 -0
- package/src/plugins/builtin/short-interest/schedule.ts +35 -2
- package/src/plugins/builtin/short-interest/surface.tsx +11 -4
- package/src/plugins/builtin/short-interest/types.ts +9 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +14 -8
- package/src/plugins/builtin/short-interest/watch-model.ts +12 -25
- package/src/plugins/builtin/short-interest/watch-pane.tsx +5 -1
- package/src/plugins/builtin/social-mentions/client.ts +2 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +33 -9
- package/src/plugins/builtin/supply-chain/model.ts +51 -3
- package/src/plugins/builtin/supply-chain/pane.tsx +78 -39
- package/src/plugins/builtin/supply-chain/row-evidence.tsx +1 -1
- package/src/plugins/builtin/themes/headless.ts +8 -5
- package/src/plugins/builtin/themes/model.ts +14 -4
- package/src/plugins/builtin/themes/pane.tsx +24 -5
- package/src/plugins/builtin/thirteenf/headless.ts +5 -5
- package/src/plugins/builtin/thirteenf/signals.ts +3 -1
- package/src/plugins/builtin/ticker-detail/adr-pair/headless.ts +123 -0
- package/src/plugins/builtin/ticker-detail/adr-pair/model.ts +132 -0
- package/src/plugins/builtin/ticker-detail/adr-pair/tab.tsx +87 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +40 -26
- package/src/plugins/builtin/ticker-detail/data-panes/history-bars.ts +8 -4
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +67 -3
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +112 -5
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +17 -2
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +21 -7
- package/src/plugins/builtin/ticker-detail/financials/model.ts +36 -1
- package/src/plugins/builtin/ticker-detail/financials/offerings.ts +56 -0
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +62 -13
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +30 -4
- package/src/plugins/builtin/ticker-detail/headless-schema.ts +14 -2
- package/src/plugins/builtin/ticker-detail/headless.ts +148 -35
- package/src/plugins/builtin/ticker-detail/index.tsx +37 -5
- package/src/plugins/builtin/ticker-detail/overview/model.ts +65 -10
- package/src/plugins/builtin/ticker-detail/overview/pane.tsx +9 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +68 -11
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -0
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +8 -3
- package/src/plugins/builtin/ticker-detail/research-tabs.ts +11 -0
- package/src/plugins/builtin/vol-surface/client.ts +28 -7
- package/src/plugins/builtin/vol-surface/headless.ts +43 -15
- package/src/plugins/builtin/vol-surface/model.ts +34 -12
- package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
- package/src/plugins/builtin/vol-surface/stored.ts +34 -3
- package/src/plugins/builtin/vol-surface/tables.ts +14 -0
- package/src/plugins/builtin/volatility/model.ts +5 -10
- package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +4 -3
- package/src/plugins/builtin/world-venue-map/basemap.ts +3 -1
- package/src/plugins/builtin/world-venue-map/chokepoint-chart.ts +40 -0
- package/src/plugins/builtin/world-venue-map/evidence.ts +1 -1
- package/src/plugins/builtin/world-venue-map/geo-series.ts +90 -11
- package/src/plugins/builtin/world-venue-map/headless.ts +1 -1
- package/src/plugins/builtin/world-venue-map/index.ts +3 -26
- package/src/plugins/builtin/world-venue-map/layers.ts +1 -1
- package/src/plugins/builtin/world-venue-map/map.tsx +2 -1
- package/src/plugins/builtin/world-venue-map/pane.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/venue-header.tsx +15 -4
- package/src/plugins/builtin/yield-curve/client.ts +25 -0
- package/src/plugins/builtin/yield-curve/curves.ts +7 -4
- package/src/plugins/builtin/yield-curve/headless.ts +38 -4
- package/src/plugins/builtin/yield-curve/history.ts +4 -3
- package/src/plugins/builtin/yield-curve/index.tsx +12 -6
- package/src/plugins/builtin/yield-curve/swaps-model.ts +93 -0
- package/src/plugins/builtin/yield-curve/swaps.tsx +99 -0
- package/src/plugins/bundle.ts +1 -1
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/compat.ts +3 -3
- package/src/plugins/dependencies.ts +75 -14
- package/src/plugins/extracted-plugins.ts +132 -34
- package/src/plugins/host-link.ts +149 -34
- package/src/plugins/installer.ts +35 -10
- package/src/plugins/loader.ts +206 -38
- package/src/plugins/manager-node.ts +2 -2
- package/src/plugins/marketplace/deactivation.ts +22 -0
- package/src/plugins/marketplace/model.ts +2 -2
- package/src/plugins/marketplace/packs.ts +1 -1
- package/src/plugins/ownership.ts +1 -1
- package/src/plugins/peer-retry.ts +155 -0
- package/src/plugins/registry/contributions.ts +3 -1
- package/src/plugins/registry/index.ts +4 -3
- package/src/plugins/registry/plugin-state.ts +3 -0
- package/src/plugins/seed-notice.ts +20 -0
- package/src/plugins/shortcut-argument.ts +39 -0
- package/src/portfolio/allocation.ts +1 -1
- package/src/portfolio/collection-name.ts +31 -0
- package/src/portfolio/column-values.ts +10 -8
- package/src/portfolio/cost-fx.ts +296 -19
- package/src/portfolio/manual-lots.ts +75 -0
- package/src/portfolio/mutations.ts +60 -22
- package/src/portfolio/suffixed-listings.ts +209 -0
- package/src/portfolio/totals-currency.ts +24 -0
- package/src/public/__snapshots__/public-api.test.ts.snap +14 -0
- package/src/public/time-series.ts +11 -8
- package/src/remote/schema.ts +1 -1
- package/src/renderers/browser/config-host.ts +3 -0
- package/src/renderers/browser/main.tsx +23 -3
- package/src/renderers/dom/boot-frame-page.ts +187 -0
- package/src/renderers/dom/boot-frame.ts +237 -0
- package/src/renderers/dom/build-assets.ts +46 -8
- package/src/renderers/dom/desktop/controls.tsx +1 -1
- package/src/renderers/dom/desktop/query-bar.tsx +2 -0
- package/src/renderers/dom/dom-ui-host.tsx +4 -29
- package/src/renderers/dom/host/ascii-text.ts +16 -2
- package/src/renderers/dom/host/box.tsx +43 -2
- package/src/renderers/dom/host/chart-surface.tsx +2 -0
- package/src/renderers/dom/host/hls.chunk.ts +3 -0
- package/src/renderers/dom/host/image-surface.tsx +49 -0
- package/src/renderers/dom/host/media-surface.tsx +24 -18
- package/src/renderers/dom/host/mouse.ts +30 -0
- package/src/renderers/dom/host/size-observer.ts +1 -1
- package/src/renderers/dom/host/style.ts +1 -1
- package/src/renderers/dom/host/tabs.tsx +16 -5
- package/src/renderers/dom/host/top-surface.ts +127 -0
- package/src/renderers/dom/styles.css +115 -0
- package/src/renderers/dom/test-utils.tsx +2 -1
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -3
- package/src/renderers/electrobun/bun/core-capabilities.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +56 -3
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +1 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -3
- package/src/renderers/electrobun/bun/desktop/initialization.ts +81 -18
- package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
- package/src/renderers/electrobun/bun/external-plugins.ts +29 -7
- package/src/renderers/electrobun/bun/index.ts +11 -1
- package/src/renderers/electrobun/bun/window/boot-frame.ts +67 -0
- package/src/renderers/electrobun/bun/window/create.ts +4 -1
- package/src/renderers/electrobun/shared/boot-frame.ts +7 -0
- package/src/renderers/electrobun/shared/protocol.ts +6 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
- package/src/renderers/electrobun/view/config-host.ts +3 -0
- package/src/renderers/electrobun/view/fatal-screen.tsx +1 -1
- package/src/renderers/electrobun/view/main.tsx +30 -4
- package/src/renderers/electrobun/view/news-source.ts +9 -7
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -2
- package/src/renderers/electrobun/view/ui-host.tsx +1 -1
- package/src/renderers/opentui/browser-open.ts +88 -0
- package/src/renderers/opentui/host.tsx +5 -14
- package/src/renderers/opentui/image/loader.ts +3 -2
- package/src/renderers/opentui/native-smoke.ts +10 -1
- package/src/renderers/opentui/pane-image/index.ts +2 -1
- package/src/renderers/opentui/render-lib.ts +253 -0
- package/src/renderers/opentui/render-timing-smoke.ts +1 -1
- package/src/renderers/opentui/start.tsx +8 -1
- package/src/renderers/share/layout-view.tsx +2 -2
- package/src/renderers/share/main.tsx +4 -4
- package/src/renderers/share/view.tsx +1 -1
- package/src/series/resolve.ts +22 -6
- package/src/series/standalone.ts +30 -2
- package/src/series/types.ts +8 -1
- package/src/shares/api.ts +4 -1
- package/src/shares/pane.ts +1 -1
- package/src/sources/gloomberb-cloud/fred-series.ts +46 -1
- package/src/sources/gloomberb-cloud/index.ts +39 -16
- package/src/sources/gloomberb-cloud/news.ts +39 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +14 -1
- package/src/sources/gloomberb-cloud/perpetuals.ts +49 -1
- package/src/sources/listing-symbols.ts +37 -0
- package/src/sources/provider-errors.ts +16 -2
- package/src/sources/provider-router/financial-routes.ts +21 -3
- package/src/sources/provider-router/financials.ts +15 -2
- package/src/sources/provider-router/history.ts +30 -8
- package/src/sources/provider-router/index.ts +16 -0
- package/src/sources/provider-router/primary.ts +1 -1
- package/src/state/app/bootstrap.ts +23 -0
- package/src/state/hooks/inline-tickers.ts +30 -5
- package/src/sync/controller.ts +1 -1
- package/src/sync/core-contributors.ts +46 -14
- package/src/team/collections-sync.ts +2 -1
- package/src/team/collections.ts +178 -17
- package/src/telemetry/crash-reports.ts +21 -4
- package/src/telemetry/update-failures-node.ts +59 -0
- package/src/telemetry/update-failures.ts +307 -0
- package/src/test-support/builtin-panes.ts +1 -1
- package/src/theme/colors.ts +3 -30
- package/src/theme/ui-tokens.ts +37 -1
- package/src/tickers/company-name-listing.ts +215 -36
- package/src/tickers/company-name.ts +43 -0
- package/src/tickers/index-roots.ts +47 -0
- package/src/tickers/instrument-label.ts +5 -1
- package/src/tickers/listing-fate.ts +30 -0
- package/src/tickers/metadata.ts +10 -0
- package/src/tickers/perpetual-listing.ts +5 -2
- package/src/tickers/search/home-listing.ts +75 -0
- package/src/tickers/search/index.ts +76 -27
- package/src/tickers/search/ranking.ts +54 -4
- package/src/tickers/search/upsert.ts +29 -0
- package/src/tickers/terminal-codes.ts +416 -40
- package/src/time-series/bar-aggregation.ts +243 -0
- package/src/time-series/base-series.ts +307 -0
- package/src/time-series/chart-bounds.ts +349 -0
- package/src/time-series/chart-seed.ts +142 -0
- package/src/time-series/economic.ts +5 -0
- package/src/time-series/fundamentals.ts +18 -3
- package/src/time-series/history-acquisition.ts +499 -0
- package/src/time-series/leverage.ts +100 -0
- package/src/time-series/market-resolution.ts +4 -2
- package/src/time-series/market.ts +2 -1
- package/src/time-series/price-comparison.ts +175 -30
- package/src/time-series/range.ts +9 -1
- package/src/time-series/resolution.ts +174 -16
- package/src/time-series/resolve-context.ts +157 -0
- package/src/time-series/resolve.ts +126 -1577
- package/src/time-series/series-name.ts +38 -0
- package/src/time-series/series-presentation.ts +208 -0
- package/src/time-series/spec.ts +11 -6
- package/src/time-series/studies.ts +109 -12
- package/src/time-series/tail-reconcile.ts +121 -0
- package/src/time-series/types.ts +29 -2
- package/src/time-series/use-chart-resolution.ts +4 -5
- package/src/types/broker.ts +10 -0
- package/src/types/config.ts +14 -0
- package/src/types/data-provider.ts +20 -0
- package/src/types/exchange-rate.ts +6 -0
- package/src/types/financials.ts +104 -7
- package/src/types/headless.ts +83 -6
- package/src/types/instrument.ts +3 -0
- package/src/types/issuer-listings.ts +28 -0
- package/src/types/plugin.ts +65 -8
- package/src/types/price-history.ts +2 -0
- package/src/types/ticker.ts +13 -0
- package/src/types/trading.ts +59 -1
- package/src/ui/ascii-font.ts +13 -1
- package/src/ui/host.tsx +6 -1
- package/src/updater/command-names.ts +175 -0
- package/src/updater/failure.ts +163 -0
- package/src/updater/index.ts +97 -28
- package/src/utils/calendar-date.ts +32 -15
- package/src/utils/cash-runway.ts +241 -0
- package/src/utils/central-banks.ts +42 -0
- package/src/utils/cli-output.ts +259 -80
- package/src/utils/contract-month.ts +16 -0
- package/src/utils/crypto-pair.ts +61 -3
- package/src/utils/currency-pair.ts +26 -0
- package/src/utils/currency-units.ts +29 -0
- package/src/utils/datetime-format.ts +10 -0
- package/src/utils/debug-log.ts +1 -1
- package/src/utils/decimal-comma.ts +39 -6
- package/src/utils/exchange-announcements.ts +71 -0
- package/src/utils/exchange-rate-snapshot.ts +10 -0
- package/src/utils/financial-statements.ts +83 -4
- package/src/utils/format.ts +34 -16
- package/src/utils/fundamentals.ts +150 -34
- package/src/utils/fx-market-hours.ts +10 -1
- package/src/utils/latest-financial-period.ts +8 -0
- package/src/utils/linux-libc.ts +54 -0
- package/src/utils/market-capitalization.ts +111 -15
- package/src/utils/max-drawdown.ts +64 -0
- package/src/utils/network-failure.ts +83 -0
- package/src/utils/option-expiry.ts +139 -7
- package/src/utils/price-earnings.ts +11 -5
- package/src/utils/price-history.ts +18 -8
- package/src/utils/price-window.ts +131 -0
- package/src/utils/release-date.ts +47 -0
- package/src/utils/sec.ts +71 -0
- package/src/utils/throttled-fetch.ts +11 -4
- package/src/utils/trailing-eps.ts +67 -0
- package/src/utils/typed-text.ts +21 -0
- package/src/utils/utc-time.ts +24 -1
- package/src/version.ts +6 -0
- package/src/cli/pane-functions/screenshot.ts +0 -2122
|
@@ -1,45 +1,32 @@
|
|
|
1
1
|
import { logFailure } from "../utils/log-failure";
|
|
2
|
-
import { resolveAssetDisplayKind } from "../market-data/market/format";
|
|
3
2
|
import { hasValidQuoteObservationTime } from "../market-data/quotes/freshness";
|
|
4
3
|
import { delistingLabel } from "../market-data/quotes/delisting";
|
|
5
|
-
import { SnapshotHistoryUnavailableError } from "../market-data/snapshot-provider";
|
|
6
4
|
import { financialPeriodCoverage, financialPeriodCoverageWarnings, limitSeriesObservations } from "./financial-period-coverage";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { HISTORY_RETENTION_MAX_AGE_MS, canonicalHistoryInterval, historyRetentionNotice, historyStartNotice, isHistoryRetentionError, parseHistoryRecoveryCandidate, type HistoryRecoveryCandidate, type HistoryRetention, type HistoryRetentionError } from "../sources/history-retention";
|
|
10
|
-
import { getRouterEntityKey } from "../sources/provider-router/cache";
|
|
11
|
-
import { publicListingTarget } from "../sources/listing-target";
|
|
12
|
-
import { fetchHistoryResult } from "../sources/history-result";
|
|
13
|
-
import type { HistorySession, PriceHistoryResult } from "../types/price-history";
|
|
5
|
+
import { historyCoverageNotice } from "../sources/history-coverage";
|
|
6
|
+
import { historyRetentionNotice, historyStartNotice } from "../sources/history-retention";
|
|
14
7
|
import type { BrokerContractRef } from "../types/instrument";
|
|
15
8
|
import { FINANCIAL_VINTAGE_NOTICE, SEC_EPS_BASIS_NOTICE } from "../utils/financial-statements";
|
|
16
|
-
import {
|
|
17
|
-
import { LiveBarAccumulator } from "./live-bars";
|
|
18
|
-
import {
|
|
19
|
-
getTimeRangeForDateWindow,
|
|
20
|
-
isDateWindowWithinTimeRange,
|
|
21
|
-
subtractTimeRange,
|
|
22
|
-
} from "./date-window";
|
|
9
|
+
import { subtractTimeRange } from "./date-window";
|
|
23
10
|
import {
|
|
24
11
|
CHART_RESOLUTION_STEP_MS,
|
|
12
|
+
chartBarSizeSource,
|
|
25
13
|
clampTimeRangeToMaxRange,
|
|
26
14
|
DEFAULT_CHART_RESOLUTION_SUPPORT,
|
|
27
|
-
getBestSupportedResolutionForVisibleWindow,
|
|
28
|
-
getNextBufferRange,
|
|
29
|
-
getSupportedPresetResolution,
|
|
30
15
|
getSupportMaxRange,
|
|
31
16
|
intersectChartResolutionSupport,
|
|
17
|
+
isCustomChartResolution,
|
|
32
18
|
isIntradayResolution,
|
|
33
19
|
normalizeChartResolutionSupport,
|
|
34
20
|
type ChartResolutionSupport,
|
|
35
21
|
type ManualChartResolution,
|
|
36
22
|
} from "./resolution";
|
|
23
|
+
import { aggregatePriceBars, futuresOpenMinuteOf, withBuiltBarSize } from "./bar-aggregation";
|
|
37
24
|
import { TIME_RANGES, type TimeRange } from "./range";
|
|
38
|
-
import type { DataProvider
|
|
39
|
-
import type { Quote,
|
|
25
|
+
import type { DataProvider } from "../types/data-provider";
|
|
26
|
+
import type { Quote, TickerFinancials } from "../types/financials";
|
|
40
27
|
import { mergeQuoteMetadata, quoteMetadataFromQuote, quoteMetadataMatchesTarget } from "../market-data/quotes/metadata";
|
|
41
|
-
import type {
|
|
42
|
-
import {
|
|
28
|
+
import type { FredSeriesRequest } from "../sources/gloomberb-cloud/fred-series";
|
|
29
|
+
import { fredCreditCoverageNotice } from "./economic";
|
|
43
30
|
import {
|
|
44
31
|
getTimeSeriesField,
|
|
45
32
|
isFundamentalFieldId,
|
|
@@ -48,52 +35,36 @@ import {
|
|
|
48
35
|
} from "./field-catalog";
|
|
49
36
|
import {
|
|
50
37
|
fundamentalSeriesUsesAvailabilityFallback,
|
|
51
|
-
unreportedStatementPeriod,
|
|
52
|
-
valuationCurrencyWarning,
|
|
53
|
-
valuationPriceIssues,
|
|
54
38
|
valuationSeriesUsesLiveQuote,
|
|
55
39
|
valuationSeriesUsesPriceHistory,
|
|
56
40
|
} from "./fundamentals";
|
|
57
41
|
import { FORWARD_PE_BASIS_NOTICE, REALIZED_NTM_PE_BASIS_NOTICE } from "./forward-valuation";
|
|
58
42
|
import { valuationPriceWarning } from "./valuation-price";
|
|
59
43
|
import { VALUATION_CURRENCY_WARNING_PREFIX } from "./valuation-currency";
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
62
|
-
activeStudyInputSeriesIds,
|
|
63
|
-
maxStudyWarmupPoints,
|
|
64
|
-
resolveStudies,
|
|
65
|
-
} from "./studies";
|
|
66
|
-
import { applyResolvedSeriesTransform } from "./transforms";
|
|
44
|
+
import { chartPriceHistoryIntegrityNotices } from "./market";
|
|
45
|
+
import { maxStudyWarmupPoints, resolveStudies } from "./studies";
|
|
67
46
|
import { clipSeriesToWindow } from "./alignment";
|
|
68
|
-
import { bufferPriceComparison, clipPriceComparison, comparedBufferPoints, priceComparisonBoundsForSeries, priceComparisonSeriesIds, priceObservationWindowFilter, resolvePriceComparison, type PriceComparison } from "./price-comparison";
|
|
69
|
-
import { reportingCurrencySeries } from "./reporting-currency";
|
|
70
|
-
import { chartQuoteOverrideKeyForSource, compareChartQuoteRecency } from "./live-quotes";
|
|
71
|
-
import { chartSeriesSourceKey } from "../capabilities/chart-series";
|
|
72
|
-
import { isResolutionFineEnoughForMarketPeriod, resolutionForExplicitMarketPeriods } from "./market-resolution";
|
|
73
|
-
import { marketSeriesFrequency } from "./market";
|
|
74
47
|
import {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
48
|
+
bufferPriceComparison,
|
|
49
|
+
clipPriceComparison,
|
|
50
|
+
priceComparisonBoundsForSeries,
|
|
51
|
+
priceComparisonSeriesIds,
|
|
52
|
+
resolvePriceComparison,
|
|
53
|
+
} from "./price-comparison";
|
|
54
|
+
import { chartQuoteOverrideKeyForSource } from "./live-quotes";
|
|
55
|
+
import { chartSeriesSourceKey } from "../capabilities/chart-series";
|
|
56
|
+
import { rememberSeedListingFacts } from "./parsed-history-cache";
|
|
80
57
|
import {
|
|
81
58
|
canonicalExchange,
|
|
82
59
|
isUsListingExchange,
|
|
83
60
|
parsePublicTickerKey,
|
|
84
|
-
publicTickerKey,
|
|
85
|
-
resolveExchangeTimeZone,
|
|
86
61
|
} from "../utils/exchanges";
|
|
87
|
-
import {
|
|
88
|
-
import {
|
|
89
|
-
import {
|
|
90
|
-
import { latestSessionStart, latestTradingSessionOpen, listingTimeZone, priorSessionClose } from "../market-data/market/trading-sessions";
|
|
91
|
-
import { futuresGenericCaption, futuresGenericListing, futuresListingPriceBasis } from "../utils/futures-generic";
|
|
62
|
+
import { isPriceHistoryStaleForCurrentWindow } from "../utils/price-history";
|
|
63
|
+
import { listingTimeZone } from "../market-data/market/trading-sessions";
|
|
64
|
+
import { futuresGenericListing, futuresListingPriceBasis } from "../utils/futures-generic";
|
|
92
65
|
import { classifyInstrumentType, kindFromListingSyntax } from "../tickers/instrument-kind";
|
|
93
|
-
import {
|
|
94
|
-
import {
|
|
95
|
-
import { isDatasetChart, isOhlcSeriesStyle } from "./spec";
|
|
96
|
-
import { SERIES_COLORS } from "../theme/series-colors";
|
|
66
|
+
import { isDatasetChart } from "./spec";
|
|
67
|
+
import { seriesMessageName } from "./series-name";
|
|
97
68
|
import type {
|
|
98
69
|
ChartResolutionResult,
|
|
99
70
|
ChartSeriesSpec,
|
|
@@ -101,242 +72,67 @@ import type {
|
|
|
101
72
|
ResolvedSeries,
|
|
102
73
|
TimeSeriesPoint,
|
|
103
74
|
} from "./types";
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
/** Raw source data retained while live quotes recompute the chart tail. */
|
|
167
|
-
export class ChartResolveCache {
|
|
168
|
-
readonly financialsByInstrument = new Map<string, Promise<TickerFinancials | null>>();
|
|
169
|
-
readonly quoteMetadataByInstrument = new Map<string, Promise<QuoteMetadata | null>>();
|
|
170
|
-
/** Each futures symbol's contract price unit, asked once per chart once known. */
|
|
171
|
-
readonly futuresPriceUnits = new Map<string, Promise<string | null>>();
|
|
172
|
-
readonly priceHistoryByRequest = new Map<string, Promise<LoadedPriceHistory>>();
|
|
173
|
-
readonly priceHistoryExpiryByRequest = new Map<string, number>();
|
|
174
|
-
readonly priceHistoryRefreshAfter = new Map<string, number>();
|
|
175
|
-
readonly accumulatedPriceHistory = new Map<string, LoadedPriceHistory>();
|
|
176
|
-
readonly resolutionSupportByInstrument = new Map<string, Promise<ChartResolutionSupport[]>>();
|
|
177
|
-
readonly settledResolutionSupport = new Map<string, ChartResolutionSupport[]>();
|
|
178
|
-
readonly rejectedResolutionSupport = new Set<string>();
|
|
179
|
-
readonly fredSeriesByRequest = new Map<string, Promise<FredSeriesLoadResult>>();
|
|
180
|
-
readonly capabilitySeriesByRequest = new Map<string, Promise<ResolvedSeries>>();
|
|
181
|
-
/** Forming bars per accumulated history, kept across live re-resolves. */
|
|
182
|
-
readonly liveBars = new Map<string, LiveBarAccumulator>();
|
|
183
|
-
/** The loaded history each accumulated history last merged, so an unchanged load is not merged again. */
|
|
184
|
-
readonly accumulatedFrom = new Map<string, LoadedPriceHistory>();
|
|
185
|
-
/** Histories plotted by the latest resolve that follow the present. */
|
|
186
|
-
liveTails: ReadonlyMap<string, LiveTailRequest> = new Map();
|
|
187
|
-
tailReconcile: Promise<boolean> | null = null;
|
|
188
|
-
/**
|
|
189
|
-
* Bumped whenever source data enters the cache. A resolve that ran without a
|
|
190
|
-
* bump changed at most what the quotes change: the newest points.
|
|
191
|
-
*/
|
|
192
|
-
revision = 0;
|
|
193
|
-
|
|
194
|
-
/** Broker history endpoints are paced per account, so their recent windows are requested less often. */
|
|
195
|
-
get liveTailsUseBroker(): boolean {
|
|
196
|
-
for (const tail of this.liveTails.values()) if (requestContext(tail.source).brokerId) return true;
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
liveBarsFor(accumulationKey: string): LiveBarAccumulator {
|
|
201
|
-
let bars = this.liveBars.get(accumulationKey);
|
|
202
|
-
if (!bars) {
|
|
203
|
-
bars = new LiveBarAccumulator();
|
|
204
|
-
this.liveBars.set(accumulationKey, bars);
|
|
205
|
-
}
|
|
206
|
-
return bars;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** True once after a streamed quote met a gap between the loaded history and now. */
|
|
210
|
-
takeTailReconcileRequest(): boolean {
|
|
211
|
-
let requested = false;
|
|
212
|
-
for (const bars of this.liveBars.values()) requested = bars.takeReconcileRequest() || requested;
|
|
213
|
-
return requested;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
interface DateBounds {
|
|
218
|
-
start: number | null;
|
|
219
|
-
/** Inclusive upper bound. */
|
|
220
|
-
end: number | null;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
interface PriceHistoryRequest {
|
|
224
|
-
bounds: DateBounds;
|
|
225
|
-
visibleBounds: DateBounds;
|
|
226
|
-
explicitWindow: boolean;
|
|
227
|
-
/** Dates the chart was given, not a view the user panned or zoomed to. */
|
|
228
|
-
authoredWindow: boolean;
|
|
229
|
-
fallbackRange: TimeRange;
|
|
230
|
-
resolution: ManualChartResolution;
|
|
231
|
-
allowProviderDefaultFallback: boolean;
|
|
232
|
-
support: readonly ChartResolutionSupport[];
|
|
233
|
-
requiredWarmupPoints: number;
|
|
234
|
-
historyRequestKey?: string;
|
|
235
|
-
/** Ask for pre-market and after-hours bars. */
|
|
236
|
-
historySession?: "extended";
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
interface LoadedPriceHistory extends PriceHistoryResult {
|
|
240
|
-
expiresAt?: number;
|
|
241
|
-
requestKey?: string;
|
|
242
|
-
accumulationKey?: string | null;
|
|
243
|
-
/** The first bar the source has at this cadence, once it has said there is nothing older. */
|
|
244
|
-
historyStart?: number;
|
|
245
|
-
recovery?: {
|
|
246
|
-
sourceKey: string;
|
|
247
|
-
/** First bar requested from the retained window. */
|
|
248
|
-
start: number;
|
|
249
|
-
end: number;
|
|
250
|
-
requiredWarmupPoints: number;
|
|
251
|
-
usableWarmupPoints: number;
|
|
252
|
-
/** The validated source evidence the recovery was requested with. */
|
|
253
|
-
retention: HistoryRetention;
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
class PriceHistoryAcquisitionError extends Error {
|
|
258
|
-
constructor(message: string, readonly retryAt: number) { super(message); }
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** Compatible intervals from different source/session contracts cannot share a capture. */
|
|
262
|
-
export function priceHistoryAcquisitionIdentity(result: Pick<PriceHistoryResult, "session" | "sourceKey">): string {
|
|
263
|
-
const { observedAt: _observedAt, ...session } = result.session ?? {};
|
|
264
|
-
return JSON.stringify([result.sourceKey ?? null, result.session ? session : null]);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** Merged history keeps the actual acquisition clock belonging to its finite tail. */
|
|
268
|
-
export function priceHistoryTailAcquisition<T extends { points: TickerFinancials["priceHistory"] }>(previous: T | undefined, incoming: T): T {
|
|
269
|
-
const latest = (value: T) => value.points.reduce((last, point) => {
|
|
270
|
-
const time = getPricePointTimestamp(point);
|
|
271
|
-
return Number.isFinite(point.close) && Number.isFinite(time) ? Math.max(last, time) : last;
|
|
272
|
-
}, Number.NEGATIVE_INFINITY);
|
|
273
|
-
return previous && latest(previous) > latest(incoming) ? previous : incoming;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
277
|
-
const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
278
|
-
|
|
279
|
-
function requestContext(spec: Extract<ChartSeriesSpec["source"], { kind: "security" }>): MarketDataRequestContext {
|
|
280
|
-
return {
|
|
281
|
-
brokerId: spec.instrument.instrument?.brokerId ?? spec.instrument.brokerId,
|
|
282
|
-
brokerInstanceId: spec.instrument.instrument?.brokerInstanceId ?? spec.instrument.brokerInstanceId,
|
|
283
|
-
instrument: spec.instrument.instrument ?? null,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function instrumentKey(spec: Extract<ChartSeriesSpec["source"], { kind: "security" }>): string {
|
|
288
|
-
return chartQuoteOverrideKeyForSource(spec);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function instrumentLabel(spec: Extract<ChartSeriesSpec["source"], { kind: "security" }>): string {
|
|
292
|
-
return publicTickerKey(spec.instrument.symbol, spec.instrument.exchange);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Shown while a current chart draws a saved copy that no refetch brought up
|
|
297
|
-
* to date: the last saved bar on the venue's clock, as the axis reads it.
|
|
298
|
-
*/
|
|
299
|
-
function behindHistoryNotice(
|
|
300
|
-
source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
|
|
301
|
-
points: TickerFinancials["priceHistory"],
|
|
302
|
-
now: number,
|
|
303
|
-
): string | null {
|
|
304
|
-
const latest = points.findLast((point) => Number.isFinite(point.close) && Number.isFinite(getPricePointTimestamp(point)));
|
|
305
|
-
if (!latest) return null;
|
|
306
|
-
const zone = listingTimeZone(source.instrument.symbol, source.instrument.exchange) ?? "UTC";
|
|
307
|
-
const wall = new Date(zonedWallClockMs(getPricePointTimestamp(latest), zone)).toISOString();
|
|
308
|
-
const sameYear = wall.slice(0, 4) === zonedDateKey(now, zone).slice(0, 4);
|
|
309
|
-
const day = formatShortDate(wall.slice(0, 10), { utc: true, year: sameYear ? false : "numeric" });
|
|
310
|
-
return `${instrumentLabel(source)}: newer bars are unavailable; showing saved bars as of ${day} ${wall.slice(11, 16)} ${timeZoneLabel(zone)}.`;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// A series that fails to load keeps its place with no observations. Dropping it
|
|
314
|
-
// made a series the user had added disappear from the legend while it still sat
|
|
315
|
-
// in the series editor, with nothing on screen to explain the difference.
|
|
316
|
-
function unloadableSeries(spec: ChartSeriesSpec, index: number, warning: string): ResolvedSeries {
|
|
317
|
-
const field = spec.source.kind === "security" ? getTimeSeriesField(spec.source.fieldId) : undefined;
|
|
318
|
-
const label = spec.source.kind === "security"
|
|
319
|
-
? `${instrumentLabel(spec.source)} ${field?.seriesLabel ?? field?.shortLabel ?? spec.source.fieldId.split(".").at(-1) ?? "Series"}`
|
|
320
|
-
: spec.source.kind === "economic"
|
|
321
|
-
? `FRED ${spec.source.seriesId}`
|
|
322
|
-
: spec.source.seriesId;
|
|
323
|
-
return {
|
|
324
|
-
id: spec.id,
|
|
325
|
-
label: spec.label?.trim() || label,
|
|
326
|
-
color: spec.color ?? SERIES_COLORS[index % SERIES_COLORS.length]!,
|
|
327
|
-
unit: field?.unit ?? "",
|
|
328
|
-
unitGroup: field?.unitGroup ?? "unknown",
|
|
329
|
-
nativeFrequency: field?.nativeFrequency ?? "daily",
|
|
330
|
-
dataShape: field?.dataShape ?? "scalar",
|
|
331
|
-
style: spec.style,
|
|
332
|
-
transform: spec.transform,
|
|
333
|
-
axis: spec.axis === "right" ? "right" : "left",
|
|
334
|
-
panelId: spec.panelId,
|
|
335
|
-
interpolation: spec.interpolation,
|
|
336
|
-
warning,
|
|
337
|
-
points: [],
|
|
338
|
-
};
|
|
339
|
-
}
|
|
75
|
+
import {
|
|
76
|
+
type ChartResolveSources,
|
|
77
|
+
type ChartResolveOptions,
|
|
78
|
+
type LiveTailRequest,
|
|
79
|
+
ChartResolveCache,
|
|
80
|
+
type LoadedPriceHistory,
|
|
81
|
+
requestContext,
|
|
82
|
+
instrumentKey,
|
|
83
|
+
instrumentLabel,
|
|
84
|
+
} from "./resolve-context";
|
|
85
|
+
import {
|
|
86
|
+
dateOnly,
|
|
87
|
+
explicitBounds,
|
|
88
|
+
rangeDurationBounds,
|
|
89
|
+
requestedBounds,
|
|
90
|
+
runtimeAutoBounds,
|
|
91
|
+
runtimeRequestBounds,
|
|
92
|
+
sameBounds,
|
|
93
|
+
requestResolution,
|
|
94
|
+
calculationBounds,
|
|
95
|
+
barsAhead,
|
|
96
|
+
trailingRangeForStart,
|
|
97
|
+
filterPoints,
|
|
98
|
+
comparisonDisplayBounds,
|
|
99
|
+
followLatestMarketObservation,
|
|
100
|
+
capabilityRequestBounds,
|
|
101
|
+
endAtNewestObservation,
|
|
102
|
+
latestSessionView,
|
|
103
|
+
} from "./chart-bounds";
|
|
104
|
+
import {
|
|
105
|
+
type PriceHistoryRequest,
|
|
106
|
+
PriceHistoryAcquisitionError,
|
|
107
|
+
priceHistoryAcquisitionIdentity,
|
|
108
|
+
priceHistoryTailAcquisition,
|
|
109
|
+
behindHistoryNotice,
|
|
110
|
+
mergePriceHistoryWindows,
|
|
111
|
+
historyIntersectsBounds,
|
|
112
|
+
clampHistoryBoundsToSupport,
|
|
113
|
+
retainedWarmup,
|
|
114
|
+
loadPriceHistory,
|
|
115
|
+
} from "./history-acquisition";
|
|
116
|
+
import {
|
|
117
|
+
unloadableSeries,
|
|
118
|
+
emptyFinancials,
|
|
119
|
+
chartCalculationSeriesIds,
|
|
120
|
+
latestQuote,
|
|
121
|
+
mergeHistory,
|
|
122
|
+
baseSecuritySeries,
|
|
123
|
+
baseEconomicSeries,
|
|
124
|
+
baseCapabilitySeries,
|
|
125
|
+
staleFredWarning,
|
|
126
|
+
} from "./base-series";
|
|
127
|
+
import {
|
|
128
|
+
resolvePriceHistoryIntegrity,
|
|
129
|
+
assignAxes,
|
|
130
|
+
adoptPriceUnits,
|
|
131
|
+
prepareBaseSeriesForStudies,
|
|
132
|
+
presentationBounds,
|
|
133
|
+
applyStudyPresentationTransforms,
|
|
134
|
+
studyAnchors,
|
|
135
|
+
} from "./series-presentation";
|
|
340
136
|
|
|
341
137
|
function withQuoteExchange(
|
|
342
138
|
source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
|
|
@@ -356,433 +152,6 @@ function withQuoteExchange(
|
|
|
356
152
|
};
|
|
357
153
|
}
|
|
358
154
|
|
|
359
|
-
function finiteDate(value: string | Date | undefined): Date | null {
|
|
360
|
-
if (!value) return null;
|
|
361
|
-
const date = value instanceof Date ? new Date(value) : new Date(value);
|
|
362
|
-
return Number.isFinite(date.getTime()) ? date : null;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function inclusiveEndDate(value: string | Date | undefined): Date | null {
|
|
366
|
-
const parsed = finiteDate(value);
|
|
367
|
-
if (!parsed) return null;
|
|
368
|
-
return typeof value === "string" && DATE_ONLY_PATTERN.test(value.trim())
|
|
369
|
-
? new Date(parsed.getTime() + DAY_MS - 1)
|
|
370
|
-
: parsed;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function dateOnly(date: Date): string {
|
|
374
|
-
return date.toISOString().slice(0, 10);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function explicitBounds(spec: ChartSpec): DateBounds | null {
|
|
378
|
-
const explicitStart = finiteDate(spec.viewport.dateWindow?.start);
|
|
379
|
-
const explicitEnd = inclusiveEndDate(spec.viewport.dateWindow?.end);
|
|
380
|
-
if (!explicitStart || !explicitEnd || explicitStart.getTime() > explicitEnd.getTime()) return null;
|
|
381
|
-
return { start: explicitStart.getTime(), end: explicitEnd.getTime() };
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/** Calendar range limits use the authored dates; observation filters include the final day. */
|
|
385
|
-
function rangeDurationBounds(spec: ChartSpec, bounds: DateBounds): DateBounds {
|
|
386
|
-
const window = spec.viewport.dateWindow;
|
|
387
|
-
if (!window || !DATE_ONLY_PATTERN.test(window.start) || !DATE_ONLY_PATTERN.test(window.end)
|
|
388
|
-
|| bounds.end !== inclusiveEndDate(window.end)?.getTime()) return bounds;
|
|
389
|
-
return { ...bounds, end: finiteDate(window.end)!.getTime() };
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function requestedBounds(spec: ChartSpec, latestObservation: Date): DateBounds {
|
|
393
|
-
const explicit = explicitBounds(spec);
|
|
394
|
-
if (explicit) return explicit;
|
|
395
|
-
return {
|
|
396
|
-
start: spec.viewport.range === "ALL"
|
|
397
|
-
? null
|
|
398
|
-
: subtractTimeRange(latestObservation, spec.viewport.range).getTime(),
|
|
399
|
-
end: latestObservation.getTime(),
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function runtimeAutoBounds(options: ChartResolveOptions): DateBounds | null {
|
|
404
|
-
const start = options.autoViewport?.start.getTime();
|
|
405
|
-
const end = options.autoViewport?.end.getTime();
|
|
406
|
-
return typeof start === "number"
|
|
407
|
-
&& Number.isFinite(start)
|
|
408
|
-
&& typeof end === "number"
|
|
409
|
-
&& Number.isFinite(end)
|
|
410
|
-
&& start <= end
|
|
411
|
-
? { start, end }
|
|
412
|
-
: null;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function runtimeRequestBounds(options: ChartResolveOptions): DateBounds | null {
|
|
416
|
-
const start = options.requestViewport?.start.getTime();
|
|
417
|
-
const end = options.requestViewport?.end.getTime();
|
|
418
|
-
return typeof start === "number"
|
|
419
|
-
&& Number.isFinite(start)
|
|
420
|
-
&& typeof end === "number"
|
|
421
|
-
&& Number.isFinite(end)
|
|
422
|
-
&& start <= end
|
|
423
|
-
? { start, end }
|
|
424
|
-
: null;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function sameBounds(left: DateBounds | null, right: DateBounds | null): boolean {
|
|
428
|
-
return left?.start === right?.start && left?.end === right?.end;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function boundsRange(bounds: DateBounds): TimeRange {
|
|
432
|
-
if (bounds.start === null || bounds.end === null) return "ALL";
|
|
433
|
-
return getTimeRangeForDateWindow({
|
|
434
|
-
start: new Date(bounds.start),
|
|
435
|
-
end: new Date(bounds.end),
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function requestResolution(
|
|
440
|
-
spec: ChartSpec,
|
|
441
|
-
bounds: DateBounds,
|
|
442
|
-
calculationSeriesIds: ReadonlySet<string>,
|
|
443
|
-
options: ChartResolveOptions,
|
|
444
|
-
sharedSupport: readonly ChartResolutionSupport[],
|
|
445
|
-
provisionalSupport = false,
|
|
446
|
-
): ManualChartResolution {
|
|
447
|
-
const duration = rangeDurationBounds(spec, bounds);
|
|
448
|
-
if (spec.viewport.resolution === "auto" && spec.studies.some((study) => study.kind === "realized-vol" && study.visible !== false)) {
|
|
449
|
-
// A 252-session estimator must see daily observations even on a long chart.
|
|
450
|
-
const maxRange = getSupportMaxRange(sharedSupport, "1d");
|
|
451
|
-
if (sharedSupport.length === 0 || provisionalSupport || maxRange === "ALL"
|
|
452
|
-
|| (maxRange !== null && duration.start !== null && duration.end !== null
|
|
453
|
-
&& isDateWindowWithinTimeRange(new Date(duration.start), new Date(duration.end), maxRange))) return "1d";
|
|
454
|
-
}
|
|
455
|
-
if (spec.viewport.resolution !== "auto") {
|
|
456
|
-
const maxRange = getSupportMaxRange(sharedSupport, spec.viewport.resolution);
|
|
457
|
-
// A placeholder list cannot rule out a manual pick; the real list decides
|
|
458
|
-
// on the next pass.
|
|
459
|
-
const supported = sharedSupport.length === 0
|
|
460
|
-
|| provisionalSupport
|
|
461
|
-
|| maxRange === "ALL"
|
|
462
|
-
|| (
|
|
463
|
-
maxRange !== null
|
|
464
|
-
&& duration.start !== null
|
|
465
|
-
&& duration.end !== null
|
|
466
|
-
&& isDateWindowWithinTimeRange(
|
|
467
|
-
new Date(duration.start),
|
|
468
|
-
new Date(duration.end),
|
|
469
|
-
maxRange,
|
|
470
|
-
)
|
|
471
|
-
);
|
|
472
|
-
if (supported) return spec.viewport.resolution;
|
|
473
|
-
}
|
|
474
|
-
const runtimeBounds = runtimeAutoBounds(options);
|
|
475
|
-
const adaptive = runtimeBounds && runtimeBounds.start !== null && runtimeBounds.end !== null
|
|
476
|
-
? getBestSupportedResolutionForVisibleWindow(
|
|
477
|
-
{ start: new Date(runtimeBounds.start), end: new Date(runtimeBounds.end) },
|
|
478
|
-
sharedSupport,
|
|
479
|
-
options.targetPointCount ?? 120,
|
|
480
|
-
options.currentResolution,
|
|
481
|
-
)
|
|
482
|
-
: null;
|
|
483
|
-
const preferred = adaptive
|
|
484
|
-
?? getSupportedPresetResolution(
|
|
485
|
-
explicitBounds(spec) ? boundsRange(duration) : spec.viewport.range,
|
|
486
|
-
provisionalSupport ? [] : sharedSupport,
|
|
487
|
-
duration.start !== null && duration.end !== null
|
|
488
|
-
? { start: new Date(duration.start), end: new Date(duration.end) }
|
|
489
|
-
: null,
|
|
490
|
-
);
|
|
491
|
-
const activeSeries = spec.series.filter((entry) => calculationSeriesIds.has(entry.id));
|
|
492
|
-
return resolutionForExplicitMarketPeriods(preferred, activeSeries);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function calculationBounds(
|
|
496
|
-
spec: ChartSpec,
|
|
497
|
-
visibleBounds: DateBounds,
|
|
498
|
-
resolution: ManualChartResolution,
|
|
499
|
-
): DateBounds {
|
|
500
|
-
if (visibleBounds.start === null) return visibleBounds;
|
|
501
|
-
const warmupPoints = maxStudyWarmupPoints(spec.studies);
|
|
502
|
-
|
|
503
|
-
const duration = rangeDurationBounds(spec, visibleBounds);
|
|
504
|
-
const visibleEnd = duration.end ?? visibleBounds.start;
|
|
505
|
-
const bufferedRange = getNextBufferRange(boundsRange(duration));
|
|
506
|
-
let start = bufferedRange === "ALL"
|
|
507
|
-
? subtractTimeRange(new Date(visibleEnd), "ALL").getTime()
|
|
508
|
-
: subtractTimeRange(new Date(visibleEnd), bufferedRange).getTime();
|
|
509
|
-
|
|
510
|
-
if (warmupPoints > 0) {
|
|
511
|
-
// Calendar gaps and closed sessions mean N observations often span more
|
|
512
|
-
// than N nominal bars. The doubled point window is a bounded safety margin.
|
|
513
|
-
const pointWarmup = warmupPoints * CHART_RESOLUTION_STEP_MS[resolution] * 2;
|
|
514
|
-
start = Math.min(start, visibleBounds.start - pointWarmup);
|
|
515
|
-
}
|
|
516
|
-
return { start: Math.min(start, visibleBounds.start), end: visibleBounds.end };
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* How far past a view moved into the past its history reaches: half a view,
|
|
521
|
-
* as far as the interval's longest window allows with the view still in it.
|
|
522
|
-
* Those are the bars a pan forward meets first. Without them, a view whose
|
|
523
|
-
* bars were loaded for it alone (a zoom to finer bars, then a pan) ends where
|
|
524
|
-
* they do, and a pan forward stops there with nothing to ask for.
|
|
525
|
-
*/
|
|
526
|
-
function barsAhead(view: DateBounds, maxRange: TimeRange | null, now: number): number {
|
|
527
|
-
if (view.start === null || view.end === null || view.end >= now) return 0;
|
|
528
|
-
const room = maxRange && maxRange !== "ALL"
|
|
529
|
-
? view.start - subtractTimeRange(new Date(view.end), maxRange).getTime()
|
|
530
|
-
: Number.POSITIVE_INFINITY;
|
|
531
|
-
return Math.max(0, Math.min((view.end - view.start) / 2, room, now - view.end));
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function trailingRangeForStart(start: number | null, referenceDate: Date): TimeRange {
|
|
535
|
-
if (start === null) return "ALL";
|
|
536
|
-
for (const range of TIME_RANGES) {
|
|
537
|
-
if (range === "ALL" || start >= subtractTimeRange(referenceDate, range).getTime()) return range;
|
|
538
|
-
}
|
|
539
|
-
return "ALL";
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function exclusiveEnd(bounds: DateBounds): Date | null {
|
|
543
|
-
return bounds.end === null ? null : new Date(bounds.end + 1);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function filterPoints(points: readonly TimeSeriesPoint[], bounds: DateBounds): TimeSeriesPoint[] {
|
|
547
|
-
return points.filter((point) => {
|
|
548
|
-
const time = point.date.getTime();
|
|
549
|
-
return Number.isFinite(time)
|
|
550
|
-
&& (bounds.start === null || time >= bounds.start)
|
|
551
|
-
&& (bounds.end === null || time <= bounds.end);
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function resolvePriceHistoryIntegrity(
|
|
556
|
-
spec: ChartSpec,
|
|
557
|
-
requestedSeries: readonly ResolvedSeries[],
|
|
558
|
-
visibleSeries: readonly ResolvedSeries[],
|
|
559
|
-
bounds: DateBounds,
|
|
560
|
-
resolution: ManualChartResolution | "auto",
|
|
561
|
-
dateWindow: ChartSpec["viewport"]["dateWindow"] | null,
|
|
562
|
-
) {
|
|
563
|
-
const filters = new Map(requestedSeries.map((entry) => [entry.id, priceObservationWindowFilter(entry, bounds, resolution, dateWindow)]));
|
|
564
|
-
const requestedIds = activeStudyInputSeriesIds(spec.studies);
|
|
565
|
-
spec.series.filter((entry) => entry.visible !== false).forEach((entry) => requestedIds.add(entry.id));
|
|
566
|
-
const selected = requestedSeries.filter((entry) => requestedIds.has(entry.id)).map((entry) => limitSeriesObservations(spec, {
|
|
567
|
-
...entry, points: entry.points.filter((point) => filters.get(entry.id)!(point.date.getTime())),
|
|
568
|
-
}));
|
|
569
|
-
return [
|
|
570
|
-
...collectPriceHistoryIntegrity(selected, "requested-observation", (id, time) => filters.get(id)!(time)),
|
|
571
|
-
...collectPriceHistoryIntegrity(visibleSeries, "visible-calculation"),
|
|
572
|
-
];
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function comparisonDisplayBounds(bounds: DateBounds, comparison: PriceComparison | null): DateBounds {
|
|
576
|
-
if (comparison?.alignment !== "session-date" || comparison.start === null || comparison.end === null) return bounds;
|
|
577
|
-
// A date-only selection can include a local session that began on the prior
|
|
578
|
-
// UTC day. Keep its original timestamp visible in the rendering viewport.
|
|
579
|
-
return {
|
|
580
|
-
start: bounds.start === null ? null : Math.min(bounds.start, comparison.start),
|
|
581
|
-
end: bounds.end === null ? null : Math.max(bounds.end, comparison.end),
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/**
|
|
586
|
-
* An untouched market chart ends at its newest bar. A quote that arrives just
|
|
587
|
-
* after the reference time moves it on; a closed market (a night, a weekend)
|
|
588
|
-
* moves it back, so the plot does not end in an empty stretch that the first
|
|
589
|
-
* pan would then collapse.
|
|
590
|
-
*/
|
|
591
|
-
function followLatestMarketObservation(
|
|
592
|
-
bounds: DateBounds,
|
|
593
|
-
series: readonly ResolvedSeries[],
|
|
594
|
-
): DateBounds {
|
|
595
|
-
if (bounds.end === null) return bounds;
|
|
596
|
-
let latest = Number.NEGATIVE_INFINITY;
|
|
597
|
-
for (const entry of series) {
|
|
598
|
-
if (entry.timeBasis?.kind !== "market") continue;
|
|
599
|
-
for (const point of entry.points) {
|
|
600
|
-
const timestamp = point.date.getTime();
|
|
601
|
-
if (Number.isFinite(timestamp) && timestamp > latest) latest = timestamp;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
if (!Number.isFinite(latest) || latest === bounds.end) return bounds;
|
|
605
|
-
const shift = latest - bounds.end;
|
|
606
|
-
return {
|
|
607
|
-
start: bounds.start === null ? null : bounds.start + shift,
|
|
608
|
-
end: latest,
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* The dates a capability series (CHOKE's transits) is asked for. A view the
|
|
614
|
-
* user panned or zoomed keeps the range's dates loaded too: the chart's
|
|
615
|
-
* navigation stops at the newest observation it has, so a series asked for
|
|
616
|
-
* the moved view alone ended where the view did, and the view could never go
|
|
617
|
-
* forward to the newest data again, not even by zooming out.
|
|
618
|
-
*/
|
|
619
|
-
function capabilityRequestBounds(view: DateBounds, range: DateBounds, moved: boolean): DateBounds {
|
|
620
|
-
if (!moved) return view;
|
|
621
|
-
return {
|
|
622
|
-
start: view.start === null || range.start === null ? null : Math.min(view.start, range.start),
|
|
623
|
-
end: view.end === null || range.end === null ? null : Math.max(view.end, range.end),
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* A chart of capability series alone (CHOKE's vessel transits) ends at its
|
|
629
|
-
* newest observation, not today: such data is published days late, and an
|
|
630
|
-
* axis running on to today would read as days with nothing in them. The
|
|
631
|
-
* start stays where the range put it, so no empty span opens on the left.
|
|
632
|
-
*/
|
|
633
|
-
function endAtNewestObservation(bounds: DateBounds, series: readonly ResolvedSeries[]): DateBounds {
|
|
634
|
-
if (bounds.end === null) return bounds;
|
|
635
|
-
let newest = Number.NEGATIVE_INFINITY;
|
|
636
|
-
for (const entry of series) {
|
|
637
|
-
for (const point of entry.points) {
|
|
638
|
-
const timestamp = point.date.getTime();
|
|
639
|
-
if (point.value !== null && Number.isFinite(timestamp) && timestamp > newest) newest = timestamp;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
if (!Number.isFinite(newest) || newest >= bounds.end || (bounds.start !== null && newest <= bounds.start)) return bounds;
|
|
643
|
-
return { start: bounds.start, end: newest };
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* 1D shows the latest trading session of the chart's first market listing,
|
|
648
|
-
* from its open to the newest bar, with the close before it as a reference.
|
|
649
|
-
* Null keeps the rolling day: crypto, FX and venues without known hours.
|
|
650
|
-
* The reference is the quote's previous close when the quote is from the
|
|
651
|
-
* session shown, as the legend's change is; otherwise the last bar of the
|
|
652
|
-
* session before.
|
|
653
|
-
*/
|
|
654
|
-
function latestSessionView(
|
|
655
|
-
primary: ResolvedSeries | undefined,
|
|
656
|
-
marketSeries: readonly ResolvedSeries[],
|
|
657
|
-
extendedHours: boolean,
|
|
658
|
-
quoteReference: { close: number; time: number } | undefined,
|
|
659
|
-
): { bounds: DateBounds; priorClose: number | null } | null {
|
|
660
|
-
if (!primary?.listing) return null;
|
|
661
|
-
const bars = primary.points.flatMap((point) => {
|
|
662
|
-
const close = point.close ?? point.value;
|
|
663
|
-
const time = point.date.getTime();
|
|
664
|
-
return typeof close === "number" && Number.isFinite(close) && Number.isFinite(time) ? [{ time, close }] : [];
|
|
665
|
-
});
|
|
666
|
-
const latest = bars.at(-1)?.time;
|
|
667
|
-
if (latest === undefined) return null;
|
|
668
|
-
const { symbol, exchange } = primary.listing;
|
|
669
|
-
const open = latestSessionStart(symbol, exchange, latest, { extendedHours });
|
|
670
|
-
if (open === null) return null;
|
|
671
|
-
// The axis starts at the session's first bar, which a quiet pre-market can leave well after the open.
|
|
672
|
-
const start = bars.find((bar) => bar.time >= open)?.time ?? open;
|
|
673
|
-
// Another listing on the chart can trade later than the primary one.
|
|
674
|
-
let end = latest;
|
|
675
|
-
for (const entry of marketSeries) {
|
|
676
|
-
const last = entry.points.at(-1)?.date.getTime();
|
|
677
|
-
if (last !== undefined && Number.isFinite(last) && last > end) end = last;
|
|
678
|
-
}
|
|
679
|
-
const regularOpen = latestTradingSessionOpen(symbol, exchange, latest);
|
|
680
|
-
const quoteOpen = quoteReference ? latestTradingSessionOpen(symbol, exchange, quoteReference.time) : null;
|
|
681
|
-
const priorClose = quoteReference && regularOpen !== null && quoteOpen === regularOpen
|
|
682
|
-
? quoteReference.close
|
|
683
|
-
: priorSessionClose(bars, open, symbol, exchange);
|
|
684
|
-
return { bounds: { start, end }, priorClose };
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function emptyFinancials(priceHistory: TickerFinancials["priceHistory"] = []): TickerFinancials {
|
|
688
|
-
return { annualStatements: [], quarterlyStatements: [], priceHistory };
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function chartCalculationSeriesIds(spec: ChartSpec): Set<string> {
|
|
692
|
-
const ids = activeStudyInputSeriesIds(spec.studies);
|
|
693
|
-
spec.series.filter(entry => entry.visible !== false).forEach(entry => ids.add(entry.id));
|
|
694
|
-
// Hidden primary market data still owns the shared session anchor.
|
|
695
|
-
const primary = spec.series.find(entry => entry.source.kind === "security" && isMarketFieldId(entry.source.fieldId));
|
|
696
|
-
if (primary) ids.add(primary.id);
|
|
697
|
-
return ids;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/** Select the provisional seed cadence before provider resolution support arrives. */
|
|
701
|
-
export function chartSeedResolution(spec: ChartSpec, now: Date, options: ChartResolveOptions = {}): ManualChartResolution {
|
|
702
|
-
return requestResolution(spec, requestedBounds(spec, now), chartCalculationSeriesIds(spec), options, []);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
export function seedChartResolutionResult(
|
|
706
|
-
spec: ChartSpec,
|
|
707
|
-
historyByInstrument: ReadonlyMap<string, TickerFinancials["priceHistory"]>,
|
|
708
|
-
referenceNow?: Date,
|
|
709
|
-
options: ChartResolveOptions = {},
|
|
710
|
-
): ChartResolutionResult | null {
|
|
711
|
-
let latest = Number.NEGATIVE_INFINITY;
|
|
712
|
-
for (const history of historyByInstrument.values()) {
|
|
713
|
-
for (const point of history) latest = Math.max(latest, getPricePointTimestamp(point));
|
|
714
|
-
}
|
|
715
|
-
if (!Number.isFinite(latest)) return null;
|
|
716
|
-
const now = referenceNow ?? new Date(latest);
|
|
717
|
-
const resolution = chartSeedResolution(spec, now, options);
|
|
718
|
-
// The seed is replaced by the settled chart moments later. Draw it with
|
|
719
|
-
// what that chart will know (units, served cadence, where its axis ends) so
|
|
720
|
-
// the swap changes no label and moves no plot.
|
|
721
|
-
const support: ChartResolutionSupport[][] = [];
|
|
722
|
-
const series: ResolvedSeries[] = [];
|
|
723
|
-
spec.series.forEach((seriesSpec, index) => {
|
|
724
|
-
if (seriesSpec.visible === false || seriesSpec.source.kind !== "security") return;
|
|
725
|
-
if (!isMarketFieldId(seriesSpec.source.fieldId)) return;
|
|
726
|
-
const history = historyByInstrument.get(instrumentKey(seriesSpec.source));
|
|
727
|
-
if (!history?.length) return;
|
|
728
|
-
const facts = readSeedListingFacts(seriesSpec.source.instrument);
|
|
729
|
-
const resolved = baseSecuritySeries(seriesSpec, emptyFinancials(history), index, resolution, facts?.quoteMetadata, facts?.contractUnit);
|
|
730
|
-
if (!resolved?.points.length) return;
|
|
731
|
-
series.push(resolved);
|
|
732
|
-
if (facts?.resolutionSupport?.length) support.push(facts.resolutionSupport);
|
|
733
|
-
});
|
|
734
|
-
if (series.length === 0) return null;
|
|
735
|
-
const requested = requestedBounds(spec, now);
|
|
736
|
-
const sessionView = spec.viewport.range === "1D" && !explicitBounds(spec)
|
|
737
|
-
? latestSessionView(series[0], series, spec.viewport.extendedHours === true, undefined)
|
|
738
|
-
: null;
|
|
739
|
-
const bounds = explicitBounds(spec) || runtimeRequestBounds(options)
|
|
740
|
-
? requested
|
|
741
|
-
: sessionView?.bounds ?? followLatestMarketObservation(requested, series);
|
|
742
|
-
const primary = series[0]!;
|
|
743
|
-
if (sessionView?.priorClose != null && primary.transform === "raw"
|
|
744
|
-
&& primary.unitGroup.startsWith("price") && primary.unitGroup !== "price:unknown") {
|
|
745
|
-
series[0] = { ...primary, priorClose: sessionView.priorClose };
|
|
746
|
-
}
|
|
747
|
-
const resolutionSupport = support.length === series.length ? intersectChartResolutionSupport(support) : [];
|
|
748
|
-
const priceComparison = resolvePriceComparison(spec, series, bounds, resolution);
|
|
749
|
-
const displayBounds = comparisonDisplayBounds(bounds, priceComparison);
|
|
750
|
-
const comparisonBounds = priceComparison && priceComparison.start !== null
|
|
751
|
-
? priceComparison : bounds;
|
|
752
|
-
const baseSeries = series.map((entry) => prepareBaseSeriesForStudies(entry, priceComparisonBoundsForSeries(entry, priceComparison) ?? comparisonBounds));
|
|
753
|
-
// Calculate studies from raw buffered inputs, then use the same presentation
|
|
754
|
-
// and visible-window rules as the network result.
|
|
755
|
-
const studies = resolveStudies(series, spec.studies, resolution);
|
|
756
|
-
const axisWarnings: string[] = [];
|
|
757
|
-
const bufferedSeries = assignAxes([
|
|
758
|
-
...baseSeries,
|
|
759
|
-
...applyStudyPresentationTransforms(studies.series, spec.studies, series, comparisonBounds, undefined, priceComparison, bounds),
|
|
760
|
-
], [...spec.series, ...spec.studies], axisWarnings).map((entry) => bufferPriceComparison(entry, priceComparison, bounds));
|
|
761
|
-
const visible = bufferedSeries.map((entry) => {
|
|
762
|
-
const entryBounds = presentationBounds(entry, spec.studies, priceComparison, bounds);
|
|
763
|
-
const clipped = entryBounds.start !== null && entryBounds.end !== null
|
|
764
|
-
? clipSeriesToWindow(entry, new Date(entryBounds.start), new Date(entryBounds.end))
|
|
765
|
-
: { ...entry, points: filterPoints(entry.points, entryBounds) };
|
|
766
|
-
return clipPriceComparison(limitSeriesObservations(spec, clipped), priceComparison);
|
|
767
|
-
});
|
|
768
|
-
const priceHistoryIntegrity = resolvePriceHistoryIntegrity(spec, series, visible, bounds, resolution, spec.viewport.dateWindow ?? null);
|
|
769
|
-
return {
|
|
770
|
-
series: visible,
|
|
771
|
-
...(priceHistoryIntegrity.length ? { priceHistoryIntegrity } : {}),
|
|
772
|
-
...(priceComparison ? { priceComparison } : {}),
|
|
773
|
-
...(resolutionSupport.length > 0 ? { resolutionSupport } : {}),
|
|
774
|
-
legendSeries: visible,
|
|
775
|
-
...(spec.viewport.maxPoints === undefined ? { bufferedSeries } : {}),
|
|
776
|
-
...((explicitBounds(spec) !== null || spec.viewport.maxPoints === undefined)
|
|
777
|
-
&& displayBounds.start !== null && displayBounds.end !== null
|
|
778
|
-
? { viewport: { start: new Date(displayBounds.start), end: new Date(displayBounds.end) } } : {}),
|
|
779
|
-
loading: true,
|
|
780
|
-
errors: studies.errors,
|
|
781
|
-
warnings: [...studies.warnings, ...axisWarnings, ...chartPriceHistoryIntegrityNotices(priceHistoryIntegrity), ...(priceComparison ? [priceComparison.notice] : [])],
|
|
782
|
-
resolution,
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
|
|
786
155
|
|
|
787
156
|
function chartIsPriceOnly(spec: ChartSpec, calculationSeriesIds: ReadonlySet<string>): boolean {
|
|
788
157
|
return spec.series.every((entry) => {
|
|
@@ -791,385 +160,6 @@ function chartIsPriceOnly(spec: ChartSpec, calculationSeriesIds: ReadonlySet<str
|
|
|
791
160
|
});
|
|
792
161
|
}
|
|
793
162
|
|
|
794
|
-
function isSortedPriceHistory(points: TickerFinancials["priceHistory"]): boolean {
|
|
795
|
-
let previous = Number.NEGATIVE_INFINITY;
|
|
796
|
-
for (const point of points) {
|
|
797
|
-
const timestamp = getPricePointTimestamp(point);
|
|
798
|
-
if (!Number.isFinite(timestamp)) continue;
|
|
799
|
-
if (timestamp < previous) return false;
|
|
800
|
-
previous = timestamp;
|
|
801
|
-
}
|
|
802
|
-
return true;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
export function mergePriceHistoryWindows(
|
|
806
|
-
current: TickerFinancials["priceHistory"],
|
|
807
|
-
incoming: TickerFinancials["priceHistory"],
|
|
808
|
-
resolution: ManualChartResolution,
|
|
809
|
-
): TickerFinancials["priceHistory"] {
|
|
810
|
-
let sorted: TickerFinancials["priceHistory"];
|
|
811
|
-
if (!isSortedPriceHistory(current) || !isSortedPriceHistory(incoming)) {
|
|
812
|
-
const byTimestamp = new Map<number, TickerFinancials["priceHistory"][number]>();
|
|
813
|
-
for (const point of [...current, ...incoming]) {
|
|
814
|
-
const timestamp = getPricePointTimestamp(point);
|
|
815
|
-
if (Number.isFinite(timestamp)) {
|
|
816
|
-
byTimestamp.set(
|
|
817
|
-
timestamp,
|
|
818
|
-
point.date instanceof Date ? point : { ...point, date: new Date(timestamp) },
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
sorted = [...byTimestamp.values()].sort(
|
|
823
|
-
(left, right) => getPricePointTimestamp(left) - getPricePointTimestamp(right),
|
|
824
|
-
);
|
|
825
|
-
} else {
|
|
826
|
-
sorted = [];
|
|
827
|
-
let currentIndex = 0;
|
|
828
|
-
let incomingIndex = 0;
|
|
829
|
-
// An intraday point strictly inside a later window's bar is that window's
|
|
830
|
-
// earlier observation of it, such as a trade-time final print.
|
|
831
|
-
const step = isIntradayResolution(resolution) ? CHART_RESOLUTION_STEP_MS[resolution] : 0;
|
|
832
|
-
let coveredUntil = Number.NEGATIVE_INFINITY;
|
|
833
|
-
const append = (
|
|
834
|
-
point: TickerFinancials["priceHistory"][number],
|
|
835
|
-
timestamp: number,
|
|
836
|
-
) => {
|
|
837
|
-
const normalized = point.date instanceof Date ? point : { ...point, date: new Date(timestamp) };
|
|
838
|
-
const previous = sorted.at(-1);
|
|
839
|
-
if (previous && getPricePointTimestamp(previous) === timestamp) sorted[sorted.length - 1] = normalized;
|
|
840
|
-
else sorted.push(normalized);
|
|
841
|
-
};
|
|
842
|
-
|
|
843
|
-
while (currentIndex < current.length || incomingIndex < incoming.length) {
|
|
844
|
-
const currentPoint = current[currentIndex];
|
|
845
|
-
const incomingPoint = incoming[incomingIndex];
|
|
846
|
-
const currentTimestamp = currentPoint ? getPricePointTimestamp(currentPoint) : Number.POSITIVE_INFINITY;
|
|
847
|
-
const incomingTimestamp = incomingPoint ? getPricePointTimestamp(incomingPoint) : Number.POSITIVE_INFINITY;
|
|
848
|
-
if (currentPoint && !Number.isFinite(currentTimestamp)) {
|
|
849
|
-
currentIndex += 1;
|
|
850
|
-
} else if (incomingPoint && !Number.isFinite(incomingTimestamp)) {
|
|
851
|
-
incomingIndex += 1;
|
|
852
|
-
} else if (currentPoint && currentTimestamp <= incomingTimestamp) {
|
|
853
|
-
if (!(incomingPoint && currentTimestamp < incomingTimestamp && currentTimestamp < coveredUntil)) {
|
|
854
|
-
append(currentPoint, currentTimestamp);
|
|
855
|
-
}
|
|
856
|
-
currentIndex += 1;
|
|
857
|
-
} else if (incomingPoint) {
|
|
858
|
-
append(incomingPoint, incomingTimestamp);
|
|
859
|
-
if (step) coveredUntil = incomingTimestamp + step;
|
|
860
|
-
incomingIndex += 1;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
if (isIntradayResolution(resolution)) return sorted;
|
|
866
|
-
// Windows can be served by different sources, and they stamp the same session
|
|
867
|
-
// differently: local midnight, UTC midnight, or the opening bell. Keying on the
|
|
868
|
-
// exact timestamp keeps both copies, so the chart draws every bar twice and
|
|
869
|
-
// carries twice the points through every pan. Daily and slower bars are never
|
|
870
|
-
// closer than one step apart, so anything closer is the same session; keep the
|
|
871
|
-
// copy already plotted to leave existing bars where they are.
|
|
872
|
-
const minimumGapMs = CHART_RESOLUTION_STEP_MS[resolution] * 0.8;
|
|
873
|
-
const plotted = new Set(current.map(getPricePointTimestamp));
|
|
874
|
-
const merged: TickerFinancials["priceHistory"] = [];
|
|
875
|
-
for (const point of sorted) {
|
|
876
|
-
const previous = merged.at(-1);
|
|
877
|
-
if (!previous || getPricePointTimestamp(point) - getPricePointTimestamp(previous) >= minimumGapMs) {
|
|
878
|
-
merged.push(point);
|
|
879
|
-
continue;
|
|
880
|
-
}
|
|
881
|
-
if (!plotted.has(getPricePointTimestamp(previous)) && plotted.has(getPricePointTimestamp(point))) {
|
|
882
|
-
merged[merged.length - 1] = point;
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
return merged;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function historyIntersectsBounds(
|
|
889
|
-
history: TickerFinancials["priceHistory"],
|
|
890
|
-
bounds: DateBounds,
|
|
891
|
-
): boolean {
|
|
892
|
-
if (history.length === 0) return false;
|
|
893
|
-
if (bounds.start === null || bounds.end === null) return true;
|
|
894
|
-
return history.some((point) => {
|
|
895
|
-
const timestamp = getPricePointTimestamp(point);
|
|
896
|
-
return Number.isFinite(timestamp) && timestamp >= bounds.start! && timestamp <= bounds.end!;
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
function clampHistoryBoundsToSupport(
|
|
901
|
-
bounds: DateBounds,
|
|
902
|
-
maxRange: TimeRange | null,
|
|
903
|
-
durationEnd: number | null = bounds.end,
|
|
904
|
-
): DateBounds {
|
|
905
|
-
if (bounds.start === null || bounds.end === null || !maxRange || maxRange === "ALL") {
|
|
906
|
-
return bounds;
|
|
907
|
-
}
|
|
908
|
-
const supportedStart = subtractTimeRange(new Date(durationEnd ?? bounds.end), maxRange).getTime();
|
|
909
|
-
return { start: Math.max(bounds.start, supportedStart), end: bounds.end };
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function latestQuote(snapshot: Quote | undefined, override: Quote | undefined): Quote | undefined {
|
|
913
|
-
if (!snapshot) return override;
|
|
914
|
-
if (!override) return snapshot;
|
|
915
|
-
return compareChartQuoteRecency(override, snapshot) >= 0 ? override : snapshot;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function mergeHistory(
|
|
919
|
-
financials: TickerFinancials | null,
|
|
920
|
-
history: TickerFinancials["priceHistory"],
|
|
921
|
-
quoteOverride: Quote | undefined,
|
|
922
|
-
now: number,
|
|
923
|
-
liveBarResolution?: ManualChartResolution,
|
|
924
|
-
exchange?: string,
|
|
925
|
-
appendQuote = true,
|
|
926
|
-
assetCategory?: string,
|
|
927
|
-
live?: { bars: LiveBarAccumulator; since?: number; session?: HistorySession; extendedHours?: boolean },
|
|
928
|
-
): TickerFinancials {
|
|
929
|
-
const base = financials ?? emptyFinancials();
|
|
930
|
-
const quote = latestQuote(base.quote, quoteOverride);
|
|
931
|
-
const unknownHistoryBasis = hasUnknownBondHistoryBasis(quote, assetCategory, base.quoteMetadata?.instrumentType);
|
|
932
|
-
if (!appendQuote || unknownHistoryBasis) return { ...base, quote, priceHistory: history };
|
|
933
|
-
// Market bars fold every observed quote into the forming bar; other series
|
|
934
|
-
// price their latest point from the current quote alone.
|
|
935
|
-
const priceHistory = live && liveBarResolution
|
|
936
|
-
? live.bars.apply(history, quote, { now, resolution: liveBarResolution, exchange, liveSince: live.since, session: live.session,
|
|
937
|
-
extendedHours: live.extendedHours })
|
|
938
|
-
: appendLiveQuotePoint(history, quote, liveBarResolution
|
|
939
|
-
? { now, mode: "ohlc", resolution: liveBarResolution, exchange }
|
|
940
|
-
: { now });
|
|
941
|
-
return { ...base, quote, priceHistory };
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
function retainedWarmup(points: TickerFinancials["priceHistory"], visibleStart: number | null): number {
|
|
945
|
-
return visibleStart === null ? 0 : points.filter((point) => Number.isFinite(point.close)
|
|
946
|
-
&& getPricePointTimestamp(point) < visibleStart).length;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
function recoveryCandidates(
|
|
950
|
-
error: HistoryRetentionError,
|
|
951
|
-
provider: DataProvider,
|
|
952
|
-
source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
|
|
953
|
-
): readonly HistoryRecoveryCandidate[] {
|
|
954
|
-
if (error.candidates.length) return error.candidates;
|
|
955
|
-
// A direct Cloud adapter enforces this constraint itself. Routed errors must
|
|
956
|
-
// carry the router's actual attempted-source scope, including across RPC.
|
|
957
|
-
if (provider.id !== "gloomberb-cloud") return [];
|
|
958
|
-
const target = publicListingTarget(source.instrument.symbol, source.instrument.exchange);
|
|
959
|
-
const context = requestContext(source);
|
|
960
|
-
const candidate = parseHistoryRecoveryCandidate({
|
|
961
|
-
sourceKey: `provider:${provider.id}`, retention: error.retention,
|
|
962
|
-
request: { symbol: target.symbol, exchange: target.exchange ?? "",
|
|
963
|
-
entityKey: getRouterEntityKey(source.instrument.symbol, context.instrument),
|
|
964
|
-
brokerId: context.brokerId, brokerInstanceId: context.brokerInstanceId,
|
|
965
|
-
interval: error.retention.interval, requestedStart: error.retention.requestedStart,
|
|
966
|
-
requestedEnd: error.retention.requestedEnd },
|
|
967
|
-
});
|
|
968
|
-
return candidate ? [candidate] : [];
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/** Requests for older bars one panned window may add when its source answers only the newest part. */
|
|
972
|
-
const OLDER_BAR_REQUESTS = 3;
|
|
973
|
-
/** A window may start this far before its first bar for closed sessions alone: weekends and holiday weeks. */
|
|
974
|
-
const CLOSED_SESSION_GAP_MS = 10 * DAY_MS;
|
|
975
|
-
/**
|
|
976
|
-
* An answer cut short at the source's cap holds thousands of bars. A shorter
|
|
977
|
-
* one that starts late is a young listing or a source that keeps less: it
|
|
978
|
-
* holds all there is, and an older request cannot change that.
|
|
979
|
-
*/
|
|
980
|
-
const CAPPED_ANSWER_MIN_BARS = 1_000;
|
|
981
|
-
|
|
982
|
-
function firstBarTime(points: TickerFinancials["priceHistory"]): number {
|
|
983
|
-
for (const point of points) {
|
|
984
|
-
const time = getPricePointTimestamp(point);
|
|
985
|
-
if (Number.isFinite(time) && Number.isFinite(point.close)) return time;
|
|
986
|
-
}
|
|
987
|
-
return Number.NaN;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
/**
|
|
991
|
-
* A source caps the bars one request returns and answers with the newest, so
|
|
992
|
-
* a panned window can come back starting weeks after it was asked to. The
|
|
993
|
-
* older part is asked for on its own, from the same source and session, until
|
|
994
|
-
* it is covered or the source has nothing older: then the first bar is where
|
|
995
|
-
* this listing's history at this cadence starts, and the chart stops there
|
|
996
|
-
* instead of asking again. So does an answer that starts late without being
|
|
997
|
-
* cut short, or one the source says its coverage starts with.
|
|
998
|
-
*/
|
|
999
|
-
async function withOlderBars(
|
|
1000
|
-
provider: DataProvider,
|
|
1001
|
-
source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
|
|
1002
|
-
request: PriceHistoryRequest,
|
|
1003
|
-
result: PriceHistoryResult,
|
|
1004
|
-
context: MarketDataRequestContext,
|
|
1005
|
-
): Promise<LoadedPriceHistory> {
|
|
1006
|
-
const start = request.bounds.start;
|
|
1007
|
-
const end = request.bounds.end;
|
|
1008
|
-
if (start === null || end === null || result.resolution !== request.resolution) return result;
|
|
1009
|
-
const toleranceMs = Math.max(CLOSED_SESSION_GAP_MS, (end - start) / 5);
|
|
1010
|
-
let current: LoadedPriceHistory = result;
|
|
1011
|
-
let answerBars = result.points.length;
|
|
1012
|
-
for (let attempt = 0; attempt < OLDER_BAR_REQUESTS; attempt += 1) {
|
|
1013
|
-
const first = firstBarTime(current.points);
|
|
1014
|
-
if (!Number.isFinite(first) || first - start <= toleranceMs) return current;
|
|
1015
|
-
// An answer short of the cap that still starts late is everything the source has.
|
|
1016
|
-
if (answerBars < CAPPED_ANSWER_MIN_BARS || current.coverageStart) return { ...current, historyStart: first };
|
|
1017
|
-
let older: PriceHistoryResult | null;
|
|
1018
|
-
try {
|
|
1019
|
-
older = await fetchHistoryResult(provider, source.instrument.symbol, source.instrument.exchange ?? "",
|
|
1020
|
-
{ kind: "detail", start: new Date(start), end: new Date(first), interval: request.resolution }, context);
|
|
1021
|
-
} catch (error) {
|
|
1022
|
-
// The source keeps nothing that old, by its own account; anything else may pass.
|
|
1023
|
-
return isHistoryRetentionError(error) || error instanceof ProviderMissError
|
|
1024
|
-
? { ...current, historyStart: first } : current;
|
|
1025
|
-
}
|
|
1026
|
-
if (!older || older.resolution !== current.resolution
|
|
1027
|
-
|| priceHistoryAcquisitionIdentity(older) !== priceHistoryAcquisitionIdentity(current)) return current;
|
|
1028
|
-
const earlier = older.points.filter((point) => getPricePointTimestamp(point) < first);
|
|
1029
|
-
if (!Number.isFinite(firstBarTime(earlier))) return { ...current, historyStart: first };
|
|
1030
|
-
current = { ...current, points: [...earlier, ...current.points] };
|
|
1031
|
-
answerBars = older.points.length;
|
|
1032
|
-
}
|
|
1033
|
-
return current;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
async function loadPriceHistory(
|
|
1037
|
-
provider: DataProvider,
|
|
1038
|
-
source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
|
|
1039
|
-
request: PriceHistoryRequest,
|
|
1040
|
-
): Promise<LoadedPriceHistory> {
|
|
1041
|
-
const context = { ...requestContext(source), historyRequestKey: request.historyRequestKey,
|
|
1042
|
-
...(request.historySession ? { historySession: request.historySession } : {}) };
|
|
1043
|
-
let coverageNotice: string | null = null;
|
|
1044
|
-
let retentionError: HistoryRetentionError | null = null;
|
|
1045
|
-
const observeFailure = (error: unknown) => {
|
|
1046
|
-
if (error instanceof SnapshotHistoryUnavailableError) throw error;
|
|
1047
|
-
if (error instanceof HistoryCoverageError) coverageNotice ??= error.message;
|
|
1048
|
-
if (isHistoryRetentionError(error)) retentionError ??= error;
|
|
1049
|
-
};
|
|
1050
|
-
const observeCoverage = (result: PriceHistoryResult) => {
|
|
1051
|
-
coverageNotice ??= historyCoverageNotice(result.coverageStart, request.visibleBounds.start);
|
|
1052
|
-
};
|
|
1053
|
-
const accepted = (points: TickerFinancials["priceHistory"]) => points.length > 0
|
|
1054
|
-
&& (!request.explicitWindow || historyIntersectsBounds(points, request.visibleBounds));
|
|
1055
|
-
const detailStart = request.bounds.start === null ? null : new Date(request.bounds.start);
|
|
1056
|
-
const detailEnd = exclusiveEnd(request.bounds);
|
|
1057
|
-
if (request.explicitWindow && detailStart && detailEnd && (provider.getDetailedPriceHistoryWithMetadata || provider.getDetailedPriceHistory)) {
|
|
1058
|
-
try {
|
|
1059
|
-
const result = (await fetchHistoryResult(provider, source.instrument.symbol, source.instrument.exchange ?? "",
|
|
1060
|
-
{ kind: "detail", start: detailStart, end: detailEnd, interval: request.resolution }, context))!;
|
|
1061
|
-
const { points } = result;
|
|
1062
|
-
observeCoverage(result);
|
|
1063
|
-
// Authored dates chart what their source answers; a panned view reaches for the rest.
|
|
1064
|
-
if (accepted(points)) return request.authoredWindow ? result : await withOlderBars(provider, source, request, result, context);
|
|
1065
|
-
} catch (error) { observeFailure(error); }
|
|
1066
|
-
}
|
|
1067
|
-
if (provider.getPriceHistoryForResolutionWithMetadata || provider.getPriceHistoryForResolution) {
|
|
1068
|
-
try {
|
|
1069
|
-
const result = (await fetchHistoryResult(provider, source.instrument.symbol, source.instrument.exchange ?? "",
|
|
1070
|
-
{ kind: "resolution", range: request.fallbackRange, resolution: request.resolution }, context))!;
|
|
1071
|
-
const { points } = result;
|
|
1072
|
-
observeCoverage(result);
|
|
1073
|
-
if (accepted(points)) {
|
|
1074
|
-
rememberParsedPriceHistory(parsedPriceHistoryKey(source.instrument, request.fallbackRange, request.resolution), points, result);
|
|
1075
|
-
return result;
|
|
1076
|
-
}
|
|
1077
|
-
} catch (error) { observeFailure(error); }
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// Only original source/cache exhaustion reaches recovery. Ask the selected
|
|
1081
|
-
// source for its retained window; a nominal clock multiplier cannot guarantee
|
|
1082
|
-
// enough observations across exchange closures for a study's warmup.
|
|
1083
|
-
const retainedFailure = retentionError as HistoryRetentionError | null;
|
|
1084
|
-
let recoveryAttempted = false;
|
|
1085
|
-
const retryAt = retainedFailure ? retainedFailure.retention.observedAt + HISTORY_RETENTION_MAX_AGE_MS : undefined;
|
|
1086
|
-
if (retainedFailure && !coverageNotice && (provider.getDetailedPriceHistoryWithMetadata || provider.getDetailedPriceHistory)) {
|
|
1087
|
-
const step = CHART_RESOLUTION_STEP_MS[request.resolution];
|
|
1088
|
-
const target = publicListingTarget(source.instrument.symbol, source.instrument.exchange);
|
|
1089
|
-
const candidate = recoveryCandidates(retainedFailure, provider, source).map((value) => parseHistoryRecoveryCandidate(value))
|
|
1090
|
-
.find((value) => value && value.request.symbol === target.symbol
|
|
1091
|
-
&& value.request.exchange === (target.exchange ?? "")
|
|
1092
|
-
&& value.request.interval === canonicalHistoryInterval(request.resolution)
|
|
1093
|
-
&& value.request.entityKey === getRouterEntityKey(source.instrument.symbol, context.instrument)
|
|
1094
|
-
&& value.request.brokerId === context.brokerId && value.request.brokerInstanceId === context.brokerInstanceId);
|
|
1095
|
-
if (candidate && request.visibleBounds.start !== null) {
|
|
1096
|
-
// One complete bar inside the advancing source limit absorbs acquisition
|
|
1097
|
-
// latency. Keep the source's original exclusive end at its second precision.
|
|
1098
|
-
const start = Math.ceil(candidate.retention.availableStart / step) * step + step;
|
|
1099
|
-
const end = candidate.retention.requestedEnd;
|
|
1100
|
-
const visibleEnd = request.visibleBounds.end === null ? end : Math.floor(request.visibleBounds.end / 1000) * 1000;
|
|
1101
|
-
// At an interval the user chose, a preset range or a panned or zoomed
|
|
1102
|
-
// view that reaches back past the retained bars, but still reaches them,
|
|
1103
|
-
// charts what the source keeps. Auto steps to coarser bars below instead,
|
|
1104
|
-
// and authored dates keep failing.
|
|
1105
|
-
const retainedOnly = !request.allowProviderDefaultFallback && !request.authoredWindow && visibleEnd >= start;
|
|
1106
|
-
if ((retainedOnly || request.visibleBounds.start >= start) && visibleEnd <= end && start < end) {
|
|
1107
|
-
recoveryAttempted = true;
|
|
1108
|
-
try {
|
|
1109
|
-
const result = (await fetchHistoryResult(provider, source.instrument.symbol, source.instrument.exchange ?? "",
|
|
1110
|
-
{ kind: "detail", start: new Date(start), end: new Date(end), interval: request.resolution }, { ...context, historyRecovery: candidate }))!;
|
|
1111
|
-
const { points } = result;
|
|
1112
|
-
observeCoverage(result);
|
|
1113
|
-
if (accepted(points)) return { ...result, expiresAt: retryAt, historyStart: firstBarTime(points),
|
|
1114
|
-
recovery: { sourceKey: candidate.sourceKey, start, end, requiredWarmupPoints: request.requiredWarmupPoints,
|
|
1115
|
-
usableWarmupPoints: retainedWarmup(points, request.visibleBounds.start), retention: candidate.retention } };
|
|
1116
|
-
} catch (error) { observeFailure(error); }
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
const unavailable = () => coverageNotice
|
|
1122
|
-
?? `Requested ${request.resolution} price history is unavailable for ${instrumentLabel(source)}. Choose Auto or a supported interval.`;
|
|
1123
|
-
const fail = (message: string): never => {
|
|
1124
|
-
if (retryAt !== undefined) throw new PriceHistoryAcquisitionError(message, retryAt);
|
|
1125
|
-
throw new Error(message);
|
|
1126
|
-
};
|
|
1127
|
-
if (!request.allowProviderDefaultFallback || recoveryAttempted) return fail(unavailable());
|
|
1128
|
-
|
|
1129
|
-
// After a proved retention failure, one explicit calendar fallback can retain
|
|
1130
|
-
// the familiar broader chart with known cadence. Never substitute coarser bars
|
|
1131
|
-
// for an authored financial/market period that requires finer observations.
|
|
1132
|
-
const coarser = retainedFailure && (provider.getPriceHistoryForResolutionWithMetadata || provider.getPriceHistoryForResolution)
|
|
1133
|
-
? (["1d", "1wk", "1mo"] as const).find((resolution) =>
|
|
1134
|
-
CHART_RESOLUTION_STEP_MS[resolution] > CHART_RESOLUTION_STEP_MS[request.resolution]
|
|
1135
|
-
&& isResolutionFineEnoughForMarketPeriod(resolution, source.period)
|
|
1136
|
-
&& request.support.some((entry) => entry.resolution === resolution
|
|
1137
|
-
&& TIME_RANGES.indexOf(entry.maxRange) >= TIME_RANGES.indexOf(request.fallbackRange))) : undefined;
|
|
1138
|
-
if (coarser) {
|
|
1139
|
-
try {
|
|
1140
|
-
const result = (await fetchHistoryResult(provider, source.instrument.symbol, source.instrument.exchange ?? "",
|
|
1141
|
-
{ kind: "resolution", range: request.fallbackRange, resolution: coarser }, context))!;
|
|
1142
|
-
const { points } = result;
|
|
1143
|
-
observeCoverage(result);
|
|
1144
|
-
if (accepted(points)) {
|
|
1145
|
-
rememberParsedPriceHistory(parsedPriceHistoryKey(source.instrument, request.fallbackRange, coarser), points, result);
|
|
1146
|
-
return { ...result, expiresAt: retryAt };
|
|
1147
|
-
}
|
|
1148
|
-
} catch (error) { observeFailure(error); }
|
|
1149
|
-
return fail(unavailable());
|
|
1150
|
-
}
|
|
1151
|
-
if (retainedFailure) return fail(unavailable());
|
|
1152
|
-
if (!provider.getPriceHistoryWithMetadata && source.period && source.period !== "auto" && isMarketFieldId(source.fieldId)) return fail(unavailable());
|
|
1153
|
-
let result: PriceHistoryResult;
|
|
1154
|
-
try {
|
|
1155
|
-
result = (await fetchHistoryResult(provider, source.instrument.symbol, source.instrument.exchange ?? "",
|
|
1156
|
-
{ kind: "range", range: request.fallbackRange }, context))!;
|
|
1157
|
-
if (source.period && source.period !== "auto" && isMarketFieldId(source.fieldId)
|
|
1158
|
-
&& (result.resolution === null || !isResolutionFineEnoughForMarketPeriod(result.resolution, source.period))) return fail(unavailable());
|
|
1159
|
-
} catch (error) {
|
|
1160
|
-
return fail(coverageNotice ?? (error instanceof Error ? error.message : String(error)));
|
|
1161
|
-
}
|
|
1162
|
-
const { points } = result;
|
|
1163
|
-
observeCoverage(result);
|
|
1164
|
-
if (points.length === 0 && coverageNotice) return fail(coverageNotice);
|
|
1165
|
-
if (request.explicitWindow && points.length > 0 && !historyIntersectsBounds(points, request.visibleBounds)) {
|
|
1166
|
-
return fail(coverageNotice ?? `Price history for the requested window is unavailable for ${instrumentLabel(source)}.`);
|
|
1167
|
-
}
|
|
1168
|
-
// A default-only provider remains usable, but its bare array cannot establish
|
|
1169
|
-
// an interval for live-bar merging, annualization or a requested-cadence cache.
|
|
1170
|
-
return { ...result, expiresAt: retryAt ?? Date.now() + HISTORY_RETENTION_MAX_AGE_MS };
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
163
|
/**
|
|
1174
164
|
* A US stock or fund listing, including one a broker contract routes through
|
|
1175
165
|
* SMART in dollars. Options and futures have no pre-market session.
|
|
@@ -1181,361 +171,6 @@ function tradesUsExtendedHours(exchange: string | undefined, contract: BrokerCon
|
|
|
1181
171
|
|| (!!contract && canonicalExchange(venue) === "SMART" && (!contract.currency || contract.currency.toUpperCase() === "USD"));
|
|
1182
172
|
}
|
|
1183
173
|
|
|
1184
|
-
function baseSecuritySeries(
|
|
1185
|
-
spec: ChartSeriesSpec,
|
|
1186
|
-
financials: TickerFinancials,
|
|
1187
|
-
index: number,
|
|
1188
|
-
marketResolution?: ManualChartResolution | null,
|
|
1189
|
-
quoteMetadata: QuoteMetadata | null | undefined = financials.quoteMetadata,
|
|
1190
|
-
/** What a futures contract's price counts ("USD / gallon"), from its terms. */
|
|
1191
|
-
contractUnit?: string | null,
|
|
1192
|
-
): ResolvedSeries | null {
|
|
1193
|
-
if (spec.source.kind !== "security") return null;
|
|
1194
|
-
const field = getTimeSeriesField(spec.source.fieldId);
|
|
1195
|
-
if (!field) return null;
|
|
1196
|
-
const points = extractSecuritySeries(financials, spec.source);
|
|
1197
|
-
const unreportedPeriod = points.length === 0 ? unreportedStatementPeriod(financials, spec.source) : null;
|
|
1198
|
-
const symbol = instrumentLabel(spec.source);
|
|
1199
|
-
const statementCurrency = field.id.startsWith("fundamental.") && field.unit.startsWith("currency")
|
|
1200
|
-
? reportingCurrencySeries(points, financials.financialCurrency) : null;
|
|
1201
|
-
// A broker contract names its currency even before (or without) a quote.
|
|
1202
|
-
const currency = statementCurrency ? statementCurrency.currency : financials.quote?.currency || quoteMetadata?.currency || spec.source.instrument.instrument?.currency;
|
|
1203
|
-
const assetKind = resolveAssetDisplayKind({ assetCategory: financials.quote?.instrumentType || quoteMetadata?.instrumentType });
|
|
1204
|
-
const unknownBondBasis = hasUnknownBondHistoryBasis(financials.quote,
|
|
1205
|
-
spec.source.instrument.instrument?.secType, quoteMetadata?.instrumentType);
|
|
1206
|
-
const volumeUnit = unknownBondBasis ? undefined : assetKind === "equity" ? "shares" as const : assetKind === "contract" ? "contracts" as const : undefined;
|
|
1207
|
-
// A price quote establishes its currency, but only instrument metadata can
|
|
1208
|
-
// establish a per-share or per-crypto-unit basis. This does not establish the
|
|
1209
|
-
// provider's volume denomination or a derivative's contract multiplier.
|
|
1210
|
-
const unitTemplate = field.unitGroup === "price" && isMarketFieldId(field.id)
|
|
1211
|
-
? unknownBondBasis ? "unknown" : `currency${assetKind === "equity" ? "/share" : assetKind === "crypto" ? "/unit" : ""}`
|
|
1212
|
-
: field.unit;
|
|
1213
|
-
const generic = field.unitGroup === "price"
|
|
1214
|
-
? futuresGenericListing(spec.source.instrument.symbol, spec.source.instrument.exchange) : null;
|
|
1215
|
-
// A generic future on an index, VIX or Treasuries reads in points or 32nds, and an index
|
|
1216
|
-
// future's continuous symbol or contract in points, not dollars, as its quote does.
|
|
1217
|
-
const genericBasis = field.unitGroup === "price" && isMarketFieldId(field.id)
|
|
1218
|
-
? futuresListingPriceBasis(spec.source.instrument.symbol, spec.source.instrument.exchange) : null;
|
|
1219
|
-
// An index's level is points too, never the currency its members trade in.
|
|
1220
|
-
const indexLevel = field.unitGroup === "price" && isMarketFieldId(field.id)
|
|
1221
|
-
&& isIndexLevel(financials.quote?.instrumentType || quoteMetadata?.instrumentType);
|
|
1222
|
-
const genericUnit = genericBasis === "points" || indexLevel ? INDEX_POINTS_UNIT : genericBasis === "thirty-seconds" ? "32nds" : null;
|
|
1223
|
-
// A future's price in a currency counts what its contract terms say: heating
|
|
1224
|
-
// oil is USD / gallon and crude USD / barrel, which a bare USD does not tell apart.
|
|
1225
|
-
const statedUnit = !genericUnit && unitTemplate === "currency" ? contractPriceUnit(contractUnit, currency) : null;
|
|
1226
|
-
const unit = field.id === "market.volume" ? volumeUnit ?? ""
|
|
1227
|
-
: statedUnit ?? genericUnit ?? (unitTemplate.startsWith("currency") && currency ? unitTemplate.replace("currency", currency) : unitTemplate);
|
|
1228
|
-
const currencyUnitGroup = genericUnit ? `${field.unitGroup}:${genericUnit}`
|
|
1229
|
-
: unknownBondBasis && field.unitGroup === "price" ? "price:unknown"
|
|
1230
|
-
: field.unit.startsWith("currency") && currency
|
|
1231
|
-
? `${field.unitGroup}:${currency}`
|
|
1232
|
-
: field.unitGroup;
|
|
1233
|
-
const marketExchange = spec.source.instrument.exchange
|
|
1234
|
-
|| financials.quote?.listingExchangeName
|
|
1235
|
-
|| financials.quote?.exchangeName;
|
|
1236
|
-
const marketField = isMarketFieldId(spec.source.fieldId);
|
|
1237
|
-
// A routing code such as a broker's SMART names no venue. The quote's
|
|
1238
|
-
// listing then supplies the session clock, so closed hours stay off the axis.
|
|
1239
|
-
const sessionExchange = [marketExchange, financials.quote?.listingExchangeName, quoteMetadata?.listingExchangeName, financials.quote?.exchangeName]
|
|
1240
|
-
.find((value) => resolveExchangeTimeZone(value));
|
|
1241
|
-
// A dollar listing saved with IBKR's SMART route and no other venue trades on
|
|
1242
|
-
// the US session, and so does a dollar option, which names no venue at all.
|
|
1243
|
-
const dollars = !currency || currency === "USD";
|
|
1244
|
-
const usOption = dollars && (spec.source.instrument.instrument?.secType?.toUpperCase() === "OPT"
|
|
1245
|
-
|| /^OPTION$/i.test(financials.quote?.instrumentType ?? quoteMetadata?.instrumentType ?? ""));
|
|
1246
|
-
const routedUsListing = !sessionExchange && ((canonicalExchange(marketExchange) === "SMART" && dollars) || usOption);
|
|
1247
|
-
const marketTimeZone = marketField
|
|
1248
|
-
&& canonicalExchange(marketExchange) !== "CCC"
|
|
1249
|
-
? resolveExchangeTimeZone(sessionExchange) ?? (routedUsListing ? "America/New_York" : null)
|
|
1250
|
-
: null;
|
|
1251
|
-
// A route has no hours of its own: its sessions, which the day view and
|
|
1252
|
-
// VWAP follow, are the venue's, and NYSE's for an unnamed dollar listing.
|
|
1253
|
-
const listingExchange = canonicalExchange(marketExchange) === "SMART" || usOption
|
|
1254
|
-
? canonicalExchange(sessionExchange) || (routedUsListing ? "NYSE" : marketExchange)
|
|
1255
|
-
: marketExchange;
|
|
1256
|
-
const latestChangePercent = marketField && field.unit.startsWith("currency")
|
|
1257
|
-
&& !unknownBondBasis
|
|
1258
|
-
&& financials.quote && hasValidQuoteObservationTime(financials.quote)
|
|
1259
|
-
? financials.quote.changePercent
|
|
1260
|
-
: undefined;
|
|
1261
|
-
const priceIssues = valuationPriceIssues(financials, spec.source);
|
|
1262
|
-
// A source with closes only, such as a generic future, has flat bars (open,
|
|
1263
|
-
// high and low filled from the close): nothing to draw as candles.
|
|
1264
|
-
const closesOnly = marketField && points.length > 1
|
|
1265
|
-
&& points.every((point) => [point.open, point.high, point.low].every((value) => value == null || value === point.close));
|
|
1266
|
-
return {
|
|
1267
|
-
id: spec.id,
|
|
1268
|
-
label: spec.label?.trim() || (generic ? futuresGenericCaption(generic) : `${symbol} ${field.seriesLabel ?? field.shortLabel}`),
|
|
1269
|
-
color: spec.color ?? SERIES_COLORS[index % SERIES_COLORS.length]!,
|
|
1270
|
-
unit,
|
|
1271
|
-
unitGroup: currencyUnitGroup,
|
|
1272
|
-
...(statedUnit ? { statedUnit } : {}),
|
|
1273
|
-
priceAssetCategory: marketField && field.unitGroup === "price" && !unknownBondBasis
|
|
1274
|
-
? financials.quote?.instrumentType || quoteMetadata?.instrumentType : undefined,
|
|
1275
|
-
volumeUnit,
|
|
1276
|
-
...(priceIssues.length ? { valuationPriceIssues: priceIssues } : {}),
|
|
1277
|
-
warning: field.id === "market.volume" && !volumeUnit && points.length > 0
|
|
1278
|
-
? "Volume unit unknown." : statementCurrency?.warning ?? valuationCurrencyWarning(financials, spec.source),
|
|
1279
|
-
nativeFrequency: marketField ? marketSeriesFrequency(spec.source, marketResolution)
|
|
1280
|
-
: spec.source.period && spec.source.period !== "auto" ? spec.source.period : field.nativeFrequency,
|
|
1281
|
-
...(unreportedPeriod ? { unreportedPeriod } : {}),
|
|
1282
|
-
...(marketField ? { historyResolution: marketResolution } : {}),
|
|
1283
|
-
timestampMode: spec.source.timestampMode,
|
|
1284
|
-
dataShape: field.dataShape,
|
|
1285
|
-
style: closesOnly && isOhlcSeriesStyle(spec.style ?? field.defaultStyle) ? "line" : spec.style,
|
|
1286
|
-
transform: spec.transform,
|
|
1287
|
-
axis: spec.axis === "right" ? "right" : "left",
|
|
1288
|
-
panelId: spec.panelId,
|
|
1289
|
-
interpolation: spec.interpolation,
|
|
1290
|
-
observationKind: marketField ? "market" : undefined,
|
|
1291
|
-
...(marketField ? { listing: { symbol: spec.source.instrument.symbol, exchange: listingExchange ?? "" } } : {}),
|
|
1292
|
-
timeBasis: marketTimeZone
|
|
1293
|
-
? {
|
|
1294
|
-
kind: "market",
|
|
1295
|
-
timeZone: marketTimeZone,
|
|
1296
|
-
exchange: canonicalExchange(sessionExchange) || undefined,
|
|
1297
|
-
cadenceMs: marketResolution
|
|
1298
|
-
? CHART_RESOLUTION_STEP_MS[marketResolution]
|
|
1299
|
-
: undefined,
|
|
1300
|
-
}
|
|
1301
|
-
: undefined,
|
|
1302
|
-
latestChangePercent: typeof latestChangePercent === "number" && Number.isFinite(latestChangePercent)
|
|
1303
|
-
? latestChangePercent
|
|
1304
|
-
: undefined,
|
|
1305
|
-
points,
|
|
1306
|
-
};
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
const FRED_CATALOG_UNITS: Readonly<Record<string, string>> = {
|
|
1310
|
-
"percent": "%",
|
|
1311
|
-
"millions of u.s. dollars": "million USD",
|
|
1312
|
-
"millions of dollars": "million USD",
|
|
1313
|
-
"billions of dollars": "billion USD",
|
|
1314
|
-
"billions of u.s. dollars": "billion USD",
|
|
1315
|
-
"billions of us dollars": "billion USD",
|
|
1316
|
-
"dollars per barrel": "USD/barrel",
|
|
1317
|
-
"thousands of persons": "thousand people",
|
|
1318
|
-
};
|
|
1319
|
-
|
|
1320
|
-
function fredCatalogUnit(units: string): string {
|
|
1321
|
-
return FRED_CATALOG_UNITS[units.toLowerCase()] ?? units;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
function baseEconomicSeries(
|
|
1325
|
-
spec: ChartSeriesSpec,
|
|
1326
|
-
loaded: FredSeriesLoadResult,
|
|
1327
|
-
index: number,
|
|
1328
|
-
): ResolvedSeries | null {
|
|
1329
|
-
if (spec.source.kind !== "economic") return null;
|
|
1330
|
-
const { data } = loaded;
|
|
1331
|
-
const units = data.info?.units?.trim() || "value";
|
|
1332
|
-
const catalogUnit = fredCatalogUnit(units);
|
|
1333
|
-
// "Percent Change from Year Ago" and "Percent per Annum" are percent too.
|
|
1334
|
-
const isPercent = units.toLowerCase().includes("percent");
|
|
1335
|
-
return {
|
|
1336
|
-
id: spec.id,
|
|
1337
|
-
label: spec.label?.trim() || data.info?.title?.trim() || spec.source.seriesId,
|
|
1338
|
-
color: spec.color ?? SERIES_COLORS[index % SERIES_COLORS.length]!,
|
|
1339
|
-
unit: isPercent ? "%" : catalogUnit,
|
|
1340
|
-
unitGroup: isPercent ? "percent" : `economic:${catalogUnit.toLowerCase()}`,
|
|
1341
|
-
nativeFrequency: "auto",
|
|
1342
|
-
timestampMode: "period-end",
|
|
1343
|
-
dataShape: "scalar",
|
|
1344
|
-
style: spec.style,
|
|
1345
|
-
transform: spec.transform,
|
|
1346
|
-
axis: spec.axis === "right" ? "right" : "left",
|
|
1347
|
-
panelId: spec.panelId,
|
|
1348
|
-
interpolation: spec.interpolation,
|
|
1349
|
-
points: extractFredSeries(data.observations, { providerId: "fred", timestampMode: "period-end" }),
|
|
1350
|
-
warning: "FRED vintage dates are unavailable; observations use period dates.",
|
|
1351
|
-
};
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
function baseCapabilitySeries(
|
|
1355
|
-
spec: ChartSeriesSpec,
|
|
1356
|
-
loaded: ResolvedSeries,
|
|
1357
|
-
index: number,
|
|
1358
|
-
): ResolvedSeries {
|
|
1359
|
-
const points = loaded.points.flatMap((point) => {
|
|
1360
|
-
const date = finiteDate(point.date as unknown as string | Date | undefined);
|
|
1361
|
-
const observedAt = finiteDate(point.observedAt as unknown as string | Date | undefined) ?? date;
|
|
1362
|
-
const availableAt = finiteDate(point.availableAt as unknown as string | Date | undefined) ?? undefined;
|
|
1363
|
-
return date && observedAt ? [{ ...point, date, observedAt, ...(availableAt ? { availableAt } : {}) }] : [];
|
|
1364
|
-
});
|
|
1365
|
-
return {
|
|
1366
|
-
...loaded,
|
|
1367
|
-
id: spec.id,
|
|
1368
|
-
label: spec.label?.trim() || loaded.label || (spec.source.kind === "capability" ? spec.source.seriesId : spec.id),
|
|
1369
|
-
color: spec.color ?? loaded.color ?? SERIES_COLORS[index % SERIES_COLORS.length]!,
|
|
1370
|
-
style: spec.style,
|
|
1371
|
-
transform: spec.transform,
|
|
1372
|
-
axis: spec.axis === "right" ? "right" : spec.axis === "left" ? "left" : loaded.axis,
|
|
1373
|
-
panelId: spec.panelId,
|
|
1374
|
-
interpolation: spec.interpolation,
|
|
1375
|
-
observationKind: loaded.observationKind ?? "dataset",
|
|
1376
|
-
points,
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
function staleFredWarning(loaded: FredSeriesLoadResult): string | null {
|
|
1381
|
-
if (!loaded.stale) return null;
|
|
1382
|
-
return `FRED refresh failed${loaded.refreshError ? ` (${loaded.refreshError})` : ""}; showing cached data fetched ${new Date(loaded.fetchedAt).toISOString().slice(0, 10)}.`;
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
function assignAxes(
|
|
1386
|
-
series: ResolvedSeries[],
|
|
1387
|
-
specs: readonly { id: string; axis: ChartSeriesSpec["axis"] }[],
|
|
1388
|
-
warnings: string[],
|
|
1389
|
-
): ResolvedSeries[] {
|
|
1390
|
-
const requested = new Map(specs.map((spec) => [spec.id, spec.axis] as const));
|
|
1391
|
-
const groupsByPanel = new Map<string, Partial<Record<"left" | "right", string>>>();
|
|
1392
|
-
return series.map((entry) => {
|
|
1393
|
-
const groups = groupsByPanel.get(entry.panelId) ?? {};
|
|
1394
|
-
groupsByPanel.set(entry.panelId, groups);
|
|
1395
|
-
const preferred = requested.get(entry.id);
|
|
1396
|
-
let axis: "left" | "right";
|
|
1397
|
-
if (preferred === "left" || preferred === "right") {
|
|
1398
|
-
axis = preferred;
|
|
1399
|
-
} else if (groups.left === entry.unitGroup) {
|
|
1400
|
-
axis = "left";
|
|
1401
|
-
} else if (groups.right === entry.unitGroup) {
|
|
1402
|
-
axis = "right";
|
|
1403
|
-
} else if (!groups.left) {
|
|
1404
|
-
axis = "left";
|
|
1405
|
-
} else {
|
|
1406
|
-
axis = "right";
|
|
1407
|
-
}
|
|
1408
|
-
if (groups[axis] && groups[axis] !== entry.unitGroup) {
|
|
1409
|
-
warnings.push(`${entry.label} shares the ${axis} axis with a different unit; choose an explicit panel for independent scaling.`);
|
|
1410
|
-
}
|
|
1411
|
-
groups[axis] ??= entry.unitGroup;
|
|
1412
|
-
return { ...entry, axis };
|
|
1413
|
-
});
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
/**
|
|
1417
|
-
* A series drawn in a ticker's price (a moving average of it, a band around
|
|
1418
|
-
* it) takes the units of that ticker's price series when the chart has it, so
|
|
1419
|
-
* the two share an axis and the line sits on the price instead of on a scale
|
|
1420
|
-
* of its own.
|
|
1421
|
-
*/
|
|
1422
|
-
function adoptPriceUnits(series: ResolvedSeries[], specs: readonly ChartSeriesSpec[]): ResolvedSeries[] {
|
|
1423
|
-
if (!series.some((entry) => entry.priceOf)) return series;
|
|
1424
|
-
const specsById = new Map(specs.map((spec) => [spec.id, spec]));
|
|
1425
|
-
const prices = series.filter((entry) => {
|
|
1426
|
-
const source = specsById.get(entry.id)?.source;
|
|
1427
|
-
return source?.kind === "security" && getTimeSeriesField(source.fieldId)?.unitGroup === "price"
|
|
1428
|
-
&& entry.transform === "raw";
|
|
1429
|
-
});
|
|
1430
|
-
if (prices.length === 0) return series;
|
|
1431
|
-
return series.map((entry) => {
|
|
1432
|
-
if (!entry.priceOf) return entry;
|
|
1433
|
-
const wanted = parsePublicTickerKey(entry.priceOf.toUpperCase());
|
|
1434
|
-
const key = publicTickerKey(wanted.symbol, wanted.exchange);
|
|
1435
|
-
const named = (price: ResolvedSeries) => {
|
|
1436
|
-
const source = specsById.get(price.id)?.source;
|
|
1437
|
-
if (source?.kind !== "security") return false;
|
|
1438
|
-
return publicTickerKey(source.instrument.symbol, source.instrument.exchange) === key
|
|
1439
|
-
|| (!wanted.exchange && source.instrument.symbol.toUpperCase() === wanted.symbol);
|
|
1440
|
-
};
|
|
1441
|
-
const price = prices.find((candidate) => named(candidate) && candidate.panelId === entry.panelId)
|
|
1442
|
-
?? prices.find(named);
|
|
1443
|
-
return price && entry.transform === "raw"
|
|
1444
|
-
? { ...entry, unit: price.unit, unitGroup: price.unitGroup }
|
|
1445
|
-
: entry;
|
|
1446
|
-
});
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
function prepareBaseSeriesForStudies(
|
|
1450
|
-
series: ResolvedSeries,
|
|
1451
|
-
bounds: DateBounds,
|
|
1452
|
-
clipToBounds = false,
|
|
1453
|
-
fallbackBaselineBounds?: DateBounds,
|
|
1454
|
-
): ResolvedSeries {
|
|
1455
|
-
const baselineTransform = series.transform === "percent" || series.transform === "index100";
|
|
1456
|
-
let source = series;
|
|
1457
|
-
if (clipToBounds && bounds.start !== null && bounds.end !== null) {
|
|
1458
|
-
source = clipSeriesToWindow(series, new Date(bounds.start), new Date(bounds.end));
|
|
1459
|
-
} else if (clipToBounds) {
|
|
1460
|
-
source = { ...series, points: filterPoints(series.points, bounds) };
|
|
1461
|
-
}
|
|
1462
|
-
const baseline = baselineTransform
|
|
1463
|
-
? scalarBaseline(series, bounds)
|
|
1464
|
-
?? (fallbackBaselineBounds ? scalarBaseline(series, fallbackBaselineBounds) : null)
|
|
1465
|
-
: null;
|
|
1466
|
-
return applyResolvedSeriesTransform(
|
|
1467
|
-
source,
|
|
1468
|
-
source.transform,
|
|
1469
|
-
baselineTransform ? { baseline } : undefined,
|
|
1470
|
-
);
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
function scalarBaseline(series: ResolvedSeries, bounds: DateBounds): number | null {
|
|
1474
|
-
const points = filterPoints(series.points, bounds);
|
|
1475
|
-
for (const point of points) {
|
|
1476
|
-
const value = typeof point.value === "number" && Number.isFinite(point.value)
|
|
1477
|
-
? point.value
|
|
1478
|
-
: typeof point.close === "number" && Number.isFinite(point.close)
|
|
1479
|
-
? point.close
|
|
1480
|
-
: null;
|
|
1481
|
-
if (value !== null && value !== 0) return value;
|
|
1482
|
-
}
|
|
1483
|
-
return null;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
function studyForOutput(
|
|
1487
|
-
outputId: string,
|
|
1488
|
-
studies: readonly ChartSpec["studies"][number][],
|
|
1489
|
-
): ChartSpec["studies"][number] | undefined {
|
|
1490
|
-
return studies
|
|
1491
|
-
.filter((study) => outputId === study.id || outputId.startsWith(`${study.id}:`))
|
|
1492
|
-
.sort((left, right) => right.id.length - left.id.length)[0];
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
/** Price overlays read in the price's own terms, so they take its transform. */
|
|
1496
|
-
function followsInputTransform(kind: ChartSpec["studies"][number]["kind"]): boolean {
|
|
1497
|
-
return kind === "sma" || kind === "ema" || kind === "bollinger" || kind === "vwap" || kind === "anchored-vwap";
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
function presentationBounds(
|
|
1501
|
-
series: ResolvedSeries,
|
|
1502
|
-
studies: readonly ChartSpec["studies"][number][],
|
|
1503
|
-
comparison: PriceComparison | null,
|
|
1504
|
-
fallback: DateBounds,
|
|
1505
|
-
): DateBounds {
|
|
1506
|
-
const study = studyForOutput(series.id, studies);
|
|
1507
|
-
const sourceId = study && followsInputTransform(study.kind)
|
|
1508
|
-
? study.inputSeriesIds[0] : series.id;
|
|
1509
|
-
return (sourceId && comparison?.sourceBounds?.[sourceId]) || fallback;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
function applyStudyPresentationTransforms(
|
|
1513
|
-
outputs: ResolvedSeries[],
|
|
1514
|
-
studies: readonly ChartSpec["studies"][number][],
|
|
1515
|
-
rawSeries: readonly ResolvedSeries[],
|
|
1516
|
-
visibleBounds: DateBounds,
|
|
1517
|
-
fallbackBaselineBounds?: DateBounds,
|
|
1518
|
-
priceComparison?: PriceComparison | null,
|
|
1519
|
-
view: DateBounds = visibleBounds,
|
|
1520
|
-
): ResolvedSeries[] {
|
|
1521
|
-
const rawById = new Map(rawSeries.map((series) => [series.id, series] as const));
|
|
1522
|
-
return outputs.map((output) => {
|
|
1523
|
-
const study = studyForOutput(output.id, studies);
|
|
1524
|
-
if (!study || !followsInputTransform(study.kind)) {
|
|
1525
|
-
return output;
|
|
1526
|
-
}
|
|
1527
|
-
const input = rawById.get(study.inputSeriesIds[0] ?? "");
|
|
1528
|
-
if (!input || input.transform === "raw") return output;
|
|
1529
|
-
const baseline = input.transform === "percent" || input.transform === "index100"
|
|
1530
|
-
? scalarBaseline(input, priceComparisonBoundsForSeries(input, priceComparison ?? null) ?? visibleBounds)
|
|
1531
|
-
?? (fallbackBaselineBounds ? scalarBaseline(input, fallbackBaselineBounds) : null)
|
|
1532
|
-
: undefined;
|
|
1533
|
-
const transformed = applyResolvedSeriesTransform(output, input.transform, { baseline });
|
|
1534
|
-
const inputBounds = priceComparison?.sourceBounds?.[input.id];
|
|
1535
|
-
return inputBounds ? { ...transformed, points: comparedBufferPoints(transformed.points, inputBounds, view) } : transformed;
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
174
|
/** Fewer daily or coarser bars than this make a line or two, not a chart. */
|
|
1540
175
|
const SHORT_HISTORY_MAX_BARS = 20;
|
|
1541
176
|
|
|
@@ -1822,6 +457,8 @@ async function resolveChartSpecPass(
|
|
|
1822
457
|
const served = supportBySource[index];
|
|
1823
458
|
if (served?.length) rememberSeedListingFacts(source.instrument, { resolutionSupport: served });
|
|
1824
459
|
});
|
|
460
|
+
// Futures source bars start on the clock: bars built from them must not straddle a session's open.
|
|
461
|
+
const openMinute = futuresOpenMinuteOf(activeMarketSources.map((source) => source.instrument));
|
|
1825
462
|
const initialResolution = requestResolution(
|
|
1826
463
|
spec,
|
|
1827
464
|
initialVisibleBounds,
|
|
@@ -1829,20 +466,23 @@ async function resolveChartSpecPass(
|
|
|
1829
466
|
options,
|
|
1830
467
|
sharedSupport,
|
|
1831
468
|
provisionalSupport,
|
|
469
|
+
openMinute,
|
|
1832
470
|
);
|
|
1833
|
-
|
|
1834
|
-
spec.viewport.resolution
|
|
1835
|
-
|
|
1836
|
-
) {
|
|
471
|
+
const requestedSource = spec.viewport.resolution === "auto"
|
|
472
|
+
? null : chartBarSizeSource(spec.viewport.resolution, provisionalSupport ? [] : sharedSupport, openMinute);
|
|
473
|
+
if (requestedSource !== null && initialResolution !== requestedSource) {
|
|
1837
474
|
warnings.push(
|
|
1838
475
|
`${spec.viewport.resolution.toUpperCase()} data is unavailable for this range. Auto resolution was used instead.`,
|
|
1839
476
|
);
|
|
1840
477
|
}
|
|
478
|
+
// Bars of a size the source does not serve (4h, 2d) are built from the bars it does (1h, 1d).
|
|
479
|
+
const builtBarSize = isCustomChartResolution(spec.viewport.resolution) && initialResolution === requestedSource
|
|
480
|
+
? spec.viewport.resolution : null;
|
|
1841
481
|
const requestVisibleBounds = requestBounds ?? initialVisibleBounds;
|
|
1842
482
|
// A view the user panned or zoomed to, as opposed to authored dates.
|
|
1843
483
|
const movedWindow = requestBounds !== null && !sameBounds(requestBounds, initialVisibleBounds);
|
|
1844
484
|
const hasExplicitWindow = explicitBounds(spec) !== null || movedWindow;
|
|
1845
|
-
const initialCalculationBounds = calculationBounds(spec, requestVisibleBounds, initialResolution);
|
|
485
|
+
const initialCalculationBounds = calculationBounds(spec, requestVisibleBounds, builtBarSize ?? initialResolution);
|
|
1846
486
|
|
|
1847
487
|
// Only US stocks and funds trade before and after the regular session; asking elsewhere costs a request for nothing.
|
|
1848
488
|
const extendedHoursFor = (source: Extract<ChartSeriesSpec["source"], { kind: "security" }>, resolution: ManualChartResolution) => (
|
|
@@ -1993,7 +633,7 @@ async function resolveChartSpecPass(
|
|
|
1993
633
|
}
|
|
1994
634
|
// Read per request: one recovered window answers every visible window that shares its acquisition.
|
|
1995
635
|
if (loaded.recovery && request.visibleBounds.start !== null && request.visibleBounds.start < loaded.recovery.start) {
|
|
1996
|
-
priorityWarnings.push(historyRetentionNotice(loaded.recovery.retention, loaded.resolution ?? request.resolution,
|
|
636
|
+
priorityWarnings.push(historyRetentionNotice(loaded.recovery.retention, builtBarSize ?? loaded.resolution ?? request.resolution,
|
|
1997
637
|
listingTimeZone(source.instrument.symbol, source.instrument.exchange)));
|
|
1998
638
|
retainedHistoryOnly = true;
|
|
1999
639
|
}
|
|
@@ -2036,7 +676,7 @@ async function resolveChartSpecPass(
|
|
|
2036
676
|
// A source that answered a panned view short, and then had nothing older, starts there.
|
|
2037
677
|
if (combined.historyStart !== undefined && !loaded.recovery && !combined.coverageStart && request.visibleBounds.start !== null
|
|
2038
678
|
&& request.visibleBounds.start < combined.historyStart && combined.resolution) {
|
|
2039
|
-
priorityWarnings.push(historyStartNotice(combined.historyStart, combined.resolution,
|
|
679
|
+
priorityWarnings.push(historyStartNotice(combined.historyStart, builtBarSize ?? combined.resolution,
|
|
2040
680
|
listingTimeZone(source.instrument.symbol, source.instrument.exchange), referenceNow.getTime()));
|
|
2041
681
|
}
|
|
2042
682
|
if (accumulationKey && loaded.resolution && isIntradayResolution(loaded.resolution)
|
|
@@ -2173,7 +813,7 @@ async function resolveChartSpecPass(
|
|
|
2173
813
|
}
|
|
2174
814
|
if (forwardPE && financials?.epsEstimates && !history) {
|
|
2175
815
|
history = await loadHistory(resolvedSource, true).catch((error: unknown) => {
|
|
2176
|
-
warnings.push(`${seriesSpec
|
|
816
|
+
warnings.push(`${seriesMessageName(seriesSpec)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2177
817
|
return null;
|
|
2178
818
|
});
|
|
2179
819
|
}
|
|
@@ -2225,6 +865,16 @@ async function resolveChartSpecPass(
|
|
|
2225
865
|
priceHistoryResolution: history.resolution, priceHistoryRequestKey: history.requestKey,
|
|
2226
866
|
priceHistorySession: history.session, priceHistorySourceKey: history.sourceKey };
|
|
2227
867
|
}
|
|
868
|
+
// Built after the live quote joins the source bars, so the forming bar holds it too.
|
|
869
|
+
// Captures keep the source bars, which their resolution names.
|
|
870
|
+
const sourceBars = merged;
|
|
871
|
+
const builtBars = builtBarSize && marketField && history?.resolution === initialResolution ? builtBarSize : null;
|
|
872
|
+
if (builtBars) {
|
|
873
|
+
merged = { ...merged, priceHistory: aggregatePriceBars(merged.priceHistory, builtBars, {
|
|
874
|
+
symbol: resolvedSource.instrument.symbol, exchange: resolvedSource.instrument.exchange,
|
|
875
|
+
extendedHours: history?.extendedHours === true,
|
|
876
|
+
}) };
|
|
877
|
+
}
|
|
2228
878
|
if ((source.fieldId === "fundamental.eps" || source.fieldId === "valuation.trailingPE")
|
|
2229
879
|
&& [...merged.annualStatements, ...merged.quarterlyStatements].some((row) => row.epsBasis)) {
|
|
2230
880
|
warnings.push(SEC_EPS_BASIS_NOTICE);
|
|
@@ -2233,8 +883,8 @@ async function resolveChartSpecPass(
|
|
|
2233
883
|
// Keep provider history separate from quote-extended display bars. A live
|
|
2234
884
|
// mark is not a daily closing observation, including on snapshot replay.
|
|
2235
885
|
const historical = history ? { ...merged, priceHistory: history.points } : merged;
|
|
2236
|
-
sources.onSecurityData?.(resolvedSpec, history?.session || realizedVolInputs.has(seriesSpec.id) ? historical :
|
|
2237
|
-
const
|
|
886
|
+
sources.onSecurityData?.(resolvedSpec, history?.session || realizedVolInputs.has(seriesSpec.id) ? historical : sourceBars, history !== null);
|
|
887
|
+
const base = baseSecuritySeries(
|
|
2238
888
|
resolvedSpec,
|
|
2239
889
|
merged,
|
|
2240
890
|
index,
|
|
@@ -2242,7 +892,8 @@ async function resolveChartSpecPass(
|
|
|
2242
892
|
quoteMetadata,
|
|
2243
893
|
contractUnit,
|
|
2244
894
|
);
|
|
2245
|
-
if (!
|
|
895
|
+
if (!base) throw new Error(`Unknown field ${source.fieldId}.`);
|
|
896
|
+
const result = builtBars ? withBuiltBarSize(base, builtBars) : base;
|
|
2246
897
|
if (marketField && history?.historyStart !== undefined && Number.isFinite(history.historyStart)) {
|
|
2247
898
|
result.historyStart = new Date(history.historyStart);
|
|
2248
899
|
}
|
|
@@ -2273,7 +924,7 @@ async function resolveChartSpecPass(
|
|
|
2273
924
|
return result;
|
|
2274
925
|
} catch (error) {
|
|
2275
926
|
const message = error instanceof Error ? error.message : String(error);
|
|
2276
|
-
errors.push(`${seriesSpec
|
|
927
|
+
errors.push(`${seriesMessageName(seriesSpec)}: ${message}`);
|
|
2277
928
|
return unloadableSeries(seriesSpec, index, message);
|
|
2278
929
|
}
|
|
2279
930
|
}));
|
|
@@ -2307,7 +958,8 @@ async function resolveChartSpecPass(
|
|
|
2307
958
|
const commonCadence = servedCadences.size === 1 ? [...servedCadences][0] : undefined;
|
|
2308
959
|
const resolution = commonCadence ?? undefined;
|
|
2309
960
|
const comparisonResolution = resolution ?? "auto";
|
|
2310
|
-
const priceComparison = resolvePriceComparison(spec, rawSeries, bounds, comparisonResolution, requestBounds ? null : spec.viewport.dateWindow
|
|
961
|
+
const priceComparison = resolvePriceComparison(spec, rawSeries, bounds, comparisonResolution, requestBounds ? null : spec.viewport.dateWindow,
|
|
962
|
+
{ preset: hasExplicitWindow ? null : spec.viewport.range, now: referenceNow.getTime() + Math.max(0, Date.now() - resolutionStartedAt) });
|
|
2311
963
|
const displayBounds = comparisonDisplayBounds(bounds, priceComparison);
|
|
2312
964
|
const comparisonBounds = priceComparison && priceComparison.start !== null
|
|
2313
965
|
? priceComparison : bounds;
|
|
@@ -2323,7 +975,8 @@ async function resolveChartSpecPass(
|
|
|
2323
975
|
|
|
2324
976
|
// Study outputs are appended by the pure engine before the final viewport clip.
|
|
2325
977
|
if (spec.studies.length > 0) {
|
|
2326
|
-
const studyResult = resolveStudies(calculationSeries, spec.studies, resolution, historicalPriceSeries
|
|
978
|
+
const studyResult = resolveStudies(calculationSeries, spec.studies, resolution, historicalPriceSeries,
|
|
979
|
+
studyAnchors(calculationSeries, priceComparison, comparisonBounds));
|
|
2327
980
|
resolved = [
|
|
2328
981
|
...resolved,
|
|
2329
982
|
...applyStudyPresentationTransforms(
|
|
@@ -2396,13 +1049,13 @@ async function resolveChartSpecPass(
|
|
|
2396
1049
|
}
|
|
2397
1050
|
for (const panel of spec.panels) {
|
|
2398
1051
|
if (panel.scale !== "log") continue;
|
|
2399
|
-
const
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
.filter((point) => typeof point.value === "number" && Number.isFinite(point.value) && point.value <= 0)
|
|
2403
|
-
.length;
|
|
1052
|
+
const isHidden = (point: TimeSeriesPoint) => typeof point.value === "number" && Number.isFinite(point.value) && point.value <= 0;
|
|
1053
|
+
const affected = resolved.filter((entry) => entry.panelId === panel.id && entry.points.some(isHidden));
|
|
1054
|
+
const hiddenCount = affected.reduce((total, entry) => total + entry.points.filter(isHidden).length, 0);
|
|
2404
1055
|
if (hiddenCount > 0) {
|
|
2405
|
-
|
|
1056
|
+
// A panel without a title is named by the series it holds, never by its id (`main`).
|
|
1057
|
+
const name = panel.label ?? [...new Set(affected.map((entry) => entry.label))].join(", ");
|
|
1058
|
+
warnings.push(`${name}: ${hiddenCount} non-positive observation${hiddenCount === 1 ? " is" : "s are"} hidden on the logarithmic scale.`);
|
|
2406
1059
|
}
|
|
2407
1060
|
}
|
|
2408
1061
|
|
|
@@ -2431,110 +1084,6 @@ async function resolveChartSpecPass(
|
|
|
2431
1084
|
...(delistings.size ? { delistings: [...delistings.values()] } : {}),
|
|
2432
1085
|
viewport,
|
|
2433
1086
|
resolution,
|
|
1087
|
+
...(builtBarSize && rawSeries.some((entry) => entry.barSize === builtBarSize) ? { barSize: builtBarSize } : {}),
|
|
2434
1088
|
};
|
|
2435
1089
|
}
|
|
2436
|
-
|
|
2437
|
-
/** Older tails are left to a full history refresh rather than a recent-window request. */
|
|
2438
|
-
const MAX_TAIL_RECONCILE_LAG_MS = 3 * DAY_MS;
|
|
2439
|
-
|
|
2440
|
-
function latestObservationTime(points: TickerFinancials["priceHistory"]): number {
|
|
2441
|
-
for (let index = points.length - 1; index >= 0; index -= 1) {
|
|
2442
|
-
const point = points[index]!;
|
|
2443
|
-
const time = getPricePointTimestamp(point);
|
|
2444
|
-
if (Number.isFinite(time) && Number.isFinite(point.close)) return time;
|
|
2445
|
-
}
|
|
2446
|
-
return Number.NaN;
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
function samePricePoints(left: TickerFinancials["priceHistory"], right: TickerFinancials["priceHistory"]): boolean {
|
|
2450
|
-
if (left.length !== right.length) return false;
|
|
2451
|
-
for (let index = left.length - 1; index >= 0; index -= 1) {
|
|
2452
|
-
const a = left[index]!;
|
|
2453
|
-
const b = right[index]!;
|
|
2454
|
-
if (a === b) continue;
|
|
2455
|
-
if (getPricePointTimestamp(a) !== getPricePointTimestamp(b) || a.open !== b.open || a.high !== b.high
|
|
2456
|
-
|| a.low !== b.low || a.close !== b.close || a.volume !== b.volume) return false;
|
|
2457
|
-
}
|
|
2458
|
-
return true;
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
|
-
/**
|
|
2462
|
-
* Refreshes the newest bars of each intraday history the last resolve plotted
|
|
2463
|
-
* up to the present, so bars formed from quotes settle to the provider's
|
|
2464
|
-
* version shortly after they close. Only the recent window is requested, and a
|
|
2465
|
-
* window from a different source or session contract is never mixed in.
|
|
2466
|
-
* Resolves true when any plotted bar changed.
|
|
2467
|
-
*/
|
|
2468
|
-
export function reconcileChartTail(
|
|
2469
|
-
sources: Pick<ChartResolveSources, "dataProvider">,
|
|
2470
|
-
cache: ChartResolveCache,
|
|
2471
|
-
now = Date.now(),
|
|
2472
|
-
): Promise<boolean> {
|
|
2473
|
-
if (cache.tailReconcile) return cache.tailReconcile;
|
|
2474
|
-
const provider = sources.dataProvider;
|
|
2475
|
-
if (!provider || cache.liveTails.size === 0
|
|
2476
|
-
|| !(provider.getDetailedPriceHistoryWithMetadata || provider.getDetailedPriceHistory)) {
|
|
2477
|
-
return Promise.resolve(false);
|
|
2478
|
-
}
|
|
2479
|
-
const run: Promise<boolean> = Promise.all([...cache.liveTails].map(async ([key, tail]) => {
|
|
2480
|
-
const pending = cache.priceHistoryByRequest.get(key);
|
|
2481
|
-
const loaded = pending ? await pending.catch(() => {
|
|
2482
|
-
// Intentional ignore: resolveChartSpecPass already includes this history failure in the series errors.
|
|
2483
|
-
return null;
|
|
2484
|
-
}) : null;
|
|
2485
|
-
if (!pending || !loaded || loaded.resolution !== tail.resolution) return false;
|
|
2486
|
-
const accumulated = cache.accumulatedPriceHistory.get(tail.accumulationKey);
|
|
2487
|
-
const latest = latestObservationTime(accumulated?.points ?? loaded.points);
|
|
2488
|
-
if (!Number.isFinite(latest) || now - latest > MAX_TAIL_RECONCILE_LAG_MS) return false;
|
|
2489
|
-
// The newest loaded bar may still have been forming when it was fetched.
|
|
2490
|
-
// Whole-bar starts and whole-minute ends let every chart of this history
|
|
2491
|
-
// that asks at the same boundary share one request and one cached window.
|
|
2492
|
-
const step = CHART_RESOLUTION_STEP_MS[tail.resolution];
|
|
2493
|
-
const start = Math.floor((latest - step) / step) * step;
|
|
2494
|
-
const end = Math.ceil(now / 60_000) * 60_000;
|
|
2495
|
-
const { symbol, exchange } = tail.source.instrument;
|
|
2496
|
-
const context = { ...requestContext(tail.source), ...(tail.historySession ? { historySession: tail.historySession } : {}) };
|
|
2497
|
-
const requestKey = JSON.stringify([symbol, exchange ?? "", context.brokerId ?? null,
|
|
2498
|
-
context.brokerInstanceId ?? null, context.instrument ?? null, tail.resolution, start, end, tail.historySession ?? null]);
|
|
2499
|
-
const shared = tailRequestsInFlight.get(provider) ?? new Map<string, Promise<PriceHistoryResult | null>>();
|
|
2500
|
-
tailRequestsInFlight.set(provider, shared);
|
|
2501
|
-
let request = shared.get(requestKey);
|
|
2502
|
-
if (!request) {
|
|
2503
|
-
const fetched = fetchHistoryResult(provider, symbol, exchange ?? "",
|
|
2504
|
-
{ kind: "detail", start: new Date(start), end: new Date(end), interval: tail.resolution }, context)
|
|
2505
|
-
.catch(logFailure("time-series-resolve", "Could not reconcile chart history", null))
|
|
2506
|
-
.finally(() => {
|
|
2507
|
-
if (shared.get(requestKey) === fetched) shared.delete(requestKey);
|
|
2508
|
-
});
|
|
2509
|
-
shared.set(requestKey, fetched);
|
|
2510
|
-
request = fetched;
|
|
2511
|
-
}
|
|
2512
|
-
const result = await request;
|
|
2513
|
-
if (!result || result.resolution !== loaded.resolution || !!result.extendedHours !== !!loaded.extendedHours
|
|
2514
|
-
|| priceHistoryAcquisitionIdentity(result) !== priceHistoryAcquisitionIdentity(loaded)) return false;
|
|
2515
|
-
const points = result.points.filter((point) => {
|
|
2516
|
-
const time = getPricePointTimestamp(point);
|
|
2517
|
-
return Number.isFinite(time) && time >= start && Number.isFinite(point.close);
|
|
2518
|
-
});
|
|
2519
|
-
// A full refetch that landed meanwhile supersedes this window.
|
|
2520
|
-
if (points.length === 0 || cache.priceHistoryByRequest.get(key) !== pending) return false;
|
|
2521
|
-
const refreshed = mergePriceHistoryWindows(loaded.points, points, tail.resolution);
|
|
2522
|
-
if (samePricePoints(refreshed, loaded.points)) return false;
|
|
2523
|
-
const acquisition = priceHistoryTailAcquisition(loaded, result);
|
|
2524
|
-
// The merged bars are as current as the newer acquisition says.
|
|
2525
|
-
cache.priceHistoryByRequest.set(key, Promise.resolve({ ...loaded, points: refreshed,
|
|
2526
|
-
session: acquisition.session, sourceKey: acquisition.sourceKey, savedCopyBehind: acquisition.savedCopyBehind }));
|
|
2527
|
-
if (accumulated) {
|
|
2528
|
-
const accumulatedAcquisition = priceHistoryTailAcquisition(accumulated, result);
|
|
2529
|
-
cache.accumulatedPriceHistory.set(tail.accumulationKey, { ...accumulated,
|
|
2530
|
-
points: mergePriceHistoryWindows(accumulated.points, points, tail.resolution),
|
|
2531
|
-
session: accumulatedAcquisition.session, sourceKey: accumulatedAcquisition.sourceKey });
|
|
2532
|
-
}
|
|
2533
|
-
cache.revision += 1;
|
|
2534
|
-
return true;
|
|
2535
|
-
})).then((changes) => changes.some(Boolean)).finally(() => {
|
|
2536
|
-
if (cache.tailReconcile === run) cache.tailReconcile = null;
|
|
2537
|
-
});
|
|
2538
|
-
cache.tailReconcile = run;
|
|
2539
|
-
return run;
|
|
2540
|
-
}
|