gloomberb 0.9.9 → 0.10.0
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 +41 -3
- package/README.zh-CN.md +294 -0
- package/package.json +10 -4
- package/patches/@opentui%2Fcore@0.3.2.patch +26 -69
- package/src/api-client/auth.ts +2 -0
- package/src/api-client/index.test.ts +4 -0
- package/src/api-client/types.ts +3 -0
- package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -0
- package/src/app/pane-template-dialog-wizard.test.tsx +36 -0
- package/src/app/pane-template-dialog-wizard.tsx +10 -3
- package/src/app/runtime/desktop-deeplink.test.ts +15 -0
- package/src/app/runtime/desktop-deeplink.ts +21 -6
- package/src/app.tsx +40 -33
- package/src/capabilities/service-capabilities.ts +7 -2
- package/src/cli/commands/automation.test.ts +202 -0
- package/src/cli/commands/automation.ts +153 -36
- package/src/cli/commands/market.ts +1 -14
- package/src/cli/commands/system.ts +4 -1
- package/src/cli/context.ts +1 -1
- package/src/cli/desktop-pane-shot.ts +65 -4
- package/src/cli/entry.ts +1 -1
- package/src/cli/pane-functions/capabilities.ts +532 -0
- package/src/cli/pane-functions/catalog.ts +39 -4
- package/src/cli/pane-functions/data.test.ts +17 -0
- package/src/cli/pane-functions/data.ts +55 -13
- package/src/cli/pane-functions/discovery.ts +36 -9
- package/src/cli/pane-functions/index.test.ts +104 -1
- package/src/cli/pane-functions/index.ts +64 -5
- package/src/cli/pane-functions/options.ts +21 -6
- package/src/cli/pane-functions/report.ts +735 -72
- package/src/cli/pane-functions/resolver.ts +70 -6
- package/src/cli/pane-functions/screenshot.test.ts +431 -0
- package/src/cli/pane-functions/screenshot.ts +952 -8
- package/src/components/chart/composite/column-layout.ts +215 -0
- package/src/components/chart/composite/composite-chart.test.tsx +630 -0
- package/src/components/chart/composite/composite-chart.tsx +1148 -0
- package/src/components/chart/composite/format.test.ts +137 -0
- package/src/components/chart/composite/format.ts +135 -0
- package/src/components/chart/composite/index.ts +38 -0
- package/src/components/chart/composite/interactions.test.ts +109 -0
- package/src/components/chart/composite/interactions.ts +247 -0
- package/src/components/chart/composite/price-series.test.ts +39 -0
- package/src/components/chart/composite/price-series.ts +78 -0
- package/src/components/chart/composite/rasterizer.ts +359 -0
- package/src/components/chart/composite/renderers.test.ts +610 -0
- package/src/components/chart/composite/scene.test.ts +464 -0
- package/src/components/chart/composite/scene.ts +553 -0
- package/src/components/chart/composite/text-renderer.ts +285 -0
- package/src/components/chart/composite/time-scale.test.ts +88 -0
- package/src/components/chart/composite/time-scale.ts +217 -0
- package/src/components/chart/composite/types.ts +141 -0
- package/src/components/chart/core/data.ts +81 -5
- package/src/components/chart/core/index.test.ts +128 -43
- package/src/components/chart/core/resolution.ts +0 -6
- package/src/components/chart/native/chart-rasterizer.ts +1 -3
- package/src/components/chart/native/kitty/index.test.ts +99 -120
- package/src/components/chart/native/kitty/manager.ts +3 -0
- package/src/components/chart/native/kitty/protocol.ts +4 -1
- package/src/components/chart/native/raster/crosshair.ts +79 -46
- package/src/components/chart/native/raster/types.ts +2 -11
- package/src/components/chart/native/surface/manager.ts +48 -3
- package/src/components/chart/price-axis-labels.tsx +18 -4
- package/src/components/command-bar/commands/direct/index.ts +25 -0
- package/src/components/command-bar/commands/registry.ts +9 -0
- package/src/components/command-bar/helpers.test.ts +4 -1
- package/src/components/command-bar/list/view.tsx +18 -6
- package/src/components/command-bar/multi-select-picker.tsx +6 -5
- package/src/components/command-bar/pane-settings.test.ts +141 -0
- package/src/components/command-bar/pane-settings.ts +61 -1
- package/src/components/command-bar/panel/body.tsx +7 -6
- package/src/components/command-bar/routes/root/command-items.ts +4 -4
- package/src/components/command-bar/routes/root/results.ts +10 -1
- package/src/components/command-bar/routes/root/selection.test.ts +54 -0
- package/src/components/command-bar/routes/root/selection.ts +13 -0
- package/src/components/command-bar/routing/actions.ts +18 -2
- package/src/components/command-bar/routing/list-state.ts +2 -1
- package/src/components/command-bar/surface/index.test.tsx +36 -4
- package/src/components/command-bar/surface/pane.test.tsx +93 -0
- package/src/components/command-bar/theme-picker.tsx +2 -1
- package/src/components/command-bar/view-model.ts +19 -20
- package/src/components/command-bar/workflow/body.tsx +2 -1
- package/src/components/command-bar/workflow/field-row.tsx +3 -2
- package/src/components/command-bar/workflow/fields.ts +4 -2
- package/src/components/command-bar/workflow/ops.test.ts +161 -0
- package/src/components/command-bar/workflow/ops.ts +20 -50
- package/src/components/command-bar/workflow/route-actions.test.ts +34 -0
- package/src/components/command-bar/workflow/route-actions.ts +4 -0
- package/src/components/command-bar/workflow/types.ts +1 -0
- package/src/components/feed-data-table/stack-view.tsx +6 -5
- package/src/components/index.ts +13 -1
- package/src/components/layout/detached-pane-shell.tsx +2 -1
- package/src/components/layout/floating-pane.tsx +3 -0
- package/src/components/layout/header.tsx +15 -8
- package/src/components/layout/pane/footer/index.test.tsx +22 -3
- package/src/components/layout/pane/footer/index.tsx +3 -1
- package/src/components/layout/pane/footer/model.ts +9 -2
- package/src/components/layout/pane/header.tsx +4 -6
- package/src/components/layout/pane/index.tsx +3 -0
- package/src/components/layout/pane/sidebar.test.tsx +100 -0
- package/src/components/layout/pane/sidebar.tsx +239 -0
- package/src/components/layout/pane/title.ts +4 -3
- package/src/components/layout/shell/action-menu-overlay.tsx +5 -3
- package/src/components/layout/shell/drag/runtime.ts +2 -0
- package/src/components/layout/shell/menu.test.ts +19 -0
- package/src/components/layout/shell/menu.ts +8 -5
- package/src/components/layout/shell/native/pointer-runtime.ts +4 -1
- package/src/components/layout/shell/pane/layers.tsx +2 -2
- package/src/components/layout/status-bar.tsx +8 -7
- package/src/components/layout/window-controls.tsx +4 -3
- package/src/components/metric-treemap/index.tsx +3 -2
- package/src/components/onboarding/onboarding-steps.tsx +30 -29
- package/src/components/onboarding/onboarding-wizard.tsx +5 -4
- package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +15 -12
- package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +17 -16
- package/src/components/onboarding/portfolio-step/broker-sync-panel.tsx +6 -5
- package/src/components/onboarding/portfolio-step/choose-panel.tsx +5 -4
- package/src/components/pane-settings-dialog/desktop.tsx +25 -3
- package/src/components/pane-settings-dialog/field-dialogs.tsx +5 -4
- package/src/components/pane-settings-dialog/tui.tsx +11 -9
- package/src/components/pane-settings-dialog/value.ts +8 -5
- package/src/components/pane-settings-dialog.test.tsx +235 -0
- package/src/components/pane-settings-dialog.tsx +24 -0
- package/src/components/pane-template-wizard.tsx +6 -2
- package/src/components/ticker/list-table-view.tsx +3 -2
- package/src/components/ui/button.tsx +3 -1
- package/src/components/ui/choice-dialog.tsx +6 -1
- package/src/components/ui/confirm-dialog.tsx +3 -2
- package/src/components/ui/fields.tsx +10 -1
- package/src/components/ui/frame.tsx +4 -1
- package/src/components/ui/index.ts +5 -0
- package/src/components/ui/inline-quick-add.tsx +104 -0
- package/src/components/ui/list-view.tsx +5 -4
- package/src/components/ui/loading.tsx +2 -1
- package/src/components/ui/multi-select/dialog.tsx +188 -44
- package/src/components/ui/popover.tsx +10 -0
- package/src/components/ui/shortcut-hint.tsx +2 -0
- package/src/components/ui/status.tsx +4 -3
- package/src/components/ui/tabs.tsx +19 -8
- package/src/components/ui/toggle.tsx +44 -3
- package/src/components/ui/ui.test.tsx +67 -5
- package/src/core/app-services.ts +4 -6
- package/src/core/state/session-persistence.ts +3 -2
- package/src/data/config/chart-settings.ts +712 -0
- package/src/data/config/layout.ts +24 -5
- package/src/data/config/store/index.test.ts +377 -14
- package/src/data/config/store/normalize.ts +79 -65
- package/src/data/fred-series.ts +137 -0
- package/src/i18n/index.test.ts +114 -0
- package/src/i18n/index.ts +147 -0
- package/src/i18n/ja.ts +1013 -0
- package/src/i18n/ko.ts +1011 -0
- package/src/i18n/languages.test.ts +31 -0
- package/src/i18n/languages.ts +72 -0
- package/src/i18n/zh-cn.ts +1013 -0
- package/src/i18n/zh-tw.ts +1013 -0
- package/src/plugins/builtin/account-management/footer.ts +3 -2
- package/src/plugins/builtin/account-management/form-components.tsx +3 -2
- package/src/plugins/builtin/account-management/keyboard.ts +6 -0
- package/src/plugins/builtin/account-management/model.ts +25 -21
- package/src/plugins/builtin/account-management/navigation.test.ts +27 -0
- package/src/plugins/builtin/account-management/navigation.ts +24 -0
- package/src/plugins/builtin/account-management/pane.tsx +120 -85
- package/src/plugins/builtin/account-management/password-dialog.tsx +10 -9
- package/src/plugins/builtin/ai/agent-history.ts +139 -0
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +133 -51
- package/src/plugins/builtin/ai/ask-ai.test.tsx +83 -22
- package/src/plugins/builtin/ai/index.test.ts +302 -0
- package/src/plugins/builtin/ai/index.tsx +374 -40
- package/src/plugins/builtin/ai/pane-settings.test.ts +155 -0
- package/src/plugins/builtin/ai/pane-settings.ts +319 -0
- package/src/plugins/builtin/ai/pi/credential-store.test.ts +80 -0
- package/src/plugins/builtin/ai/pi/credential-store.ts +138 -0
- package/src/plugins/builtin/ai/pi/file-store.ts +138 -0
- package/src/plugins/builtin/ai/pi/host.test.ts +500 -0
- package/src/plugins/builtin/ai/pi/host.ts +564 -0
- package/src/plugins/builtin/ai/pi/index.ts +5 -0
- package/src/plugins/builtin/ai/pi/models-store.test.ts +44 -0
- package/src/plugins/builtin/ai/pi/models-store.ts +141 -0
- package/src/plugins/builtin/ai/pi/providers.ts +68 -0
- package/src/plugins/builtin/ai/pi/runtime.test.ts +240 -0
- package/src/plugins/builtin/ai/pi/runtime.ts +635 -0
- package/src/plugins/builtin/ai/providers.test.ts +68 -0
- package/src/plugins/builtin/ai/providers.ts +163 -38
- package/src/plugins/builtin/ai/runner-selection.test.ts +174 -0
- package/src/plugins/builtin/ai/runner-selection.ts +193 -0
- package/src/plugins/builtin/ai/runner-selector.tsx +153 -0
- package/src/plugins/builtin/ai/runner.test.ts +198 -0
- package/src/plugins/builtin/ai/runner.ts +276 -73
- package/src/plugins/builtin/ai/screener/contract.test.ts +26 -73
- package/src/plugins/builtin/ai/screener/contract.ts +14 -52
- package/src/plugins/builtin/ai/screener/editor-runtime.ts +55 -5
- package/src/plugins/builtin/ai/screener/editor.tsx +42 -36
- package/src/plugins/builtin/ai/screener/footer.ts +82 -67
- package/src/plugins/builtin/ai/screener/keyboard.ts +27 -11
- package/src/plugins/builtin/ai/screener/model.test.ts +24 -0
- package/src/plugins/builtin/ai/screener/model.ts +9 -3
- package/src/plugins/builtin/ai/screener/pane.test.tsx +142 -55
- package/src/plugins/builtin/ai/screener/pane.tsx +95 -97
- package/src/plugins/builtin/ai/screener/results-view.tsx +8 -41
- package/src/plugins/builtin/ai/screener/runner.ts +41 -25
- package/src/plugins/builtin/ai/screener/tabs-bar.tsx +2 -2
- package/src/plugins/builtin/ai/use-runtime-providers.ts +28 -0
- package/src/plugins/builtin/ai/workspace/model.test.ts +227 -0
- package/src/plugins/builtin/ai/workspace/model.ts +308 -0
- package/src/plugins/builtin/ai/workspace/pane.test.tsx +197 -0
- package/src/plugins/builtin/ai/workspace/pane.tsx +896 -0
- package/src/plugins/builtin/analytics/index.test.tsx +3 -3
- package/src/plugins/builtin/analytics/index.tsx +3 -8
- package/src/plugins/builtin/broker-manager/detail.tsx +25 -24
- package/src/plugins/builtin/broker-manager/index.test.tsx +1 -1
- package/src/plugins/builtin/broker-manager/index.tsx +9 -11
- package/src/plugins/builtin/broker-manager/model.ts +11 -10
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +22 -21
- package/src/plugins/builtin/broker-manager/table.ts +9 -8
- package/src/plugins/builtin/changelog/index.tsx +3 -8
- package/src/plugins/builtin/chart-composer/chart-spec.ts +89 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +98 -0
- package/src/plugins/builtin/chart-composer/date-window-dialog.tsx +120 -0
- package/src/plugins/builtin/chart-composer/editor.test.tsx +332 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +1107 -0
- package/src/plugins/builtin/chart-composer/index.tsx +228 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +645 -0
- package/src/plugins/builtin/chart-composer/presets.test.ts +561 -0
- package/src/plugins/builtin/chart-composer/presets.ts +759 -0
- package/src/plugins/builtin/chart-composer/quick-add.test.tsx +245 -0
- package/src/plugins/builtin/chart-composer/quick-add.tsx +409 -0
- package/src/plugins/builtin/chart-composer/semantic.ts +86 -0
- package/src/plugins/builtin/chart-composer/series-catalog.test.ts +65 -0
- package/src/plugins/builtin/chart-composer/series-catalog.ts +295 -0
- package/src/plugins/builtin/chart-composer/settings.test.ts +163 -0
- package/src/plugins/builtin/chart-composer/settings.ts +397 -0
- package/src/plugins/builtin/chart-composer/shortcuts.test.ts +44 -0
- package/src/plugins/builtin/chart-composer/shortcuts.ts +35 -0
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +103 -0
- package/src/plugins/builtin/chat/channel-labels.ts +5 -4
- package/src/plugins/builtin/chat/channels.ts +12 -11
- package/src/plugins/builtin/chat/chat.test.tsx +32 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +6 -1
- package/src/plugins/builtin/chat/content/composer.tsx +9 -8
- package/src/plugins/builtin/chat/content/index.tsx +36 -1
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +8 -7
- package/src/plugins/builtin/chat/content/transcript.tsx +9 -3
- package/src/plugins/builtin/chat/controller/utils.ts +10 -9
- package/src/plugins/builtin/chat/message/desktop.tsx +17 -10
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +122 -3
- package/src/plugins/builtin/chat/message/profile-popover.tsx +38 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +19 -10
- package/src/plugins/builtin/chat/pane-state.test.ts +31 -0
- package/src/plugins/builtin/chat/pane-state.ts +30 -0
- package/src/plugins/builtin/chat/pane.tsx +27 -4
- package/src/plugins/builtin/chat/profile-popover.ts +62 -5
- package/src/plugins/builtin/chat/sidebar.test.tsx +4 -7
- package/src/plugins/builtin/chat/sidebar.tsx +107 -168
- package/src/plugins/builtin/chat/test-harness.tsx +6 -0
- package/src/plugins/builtin/cloud/auth-actions.tsx +5 -4
- package/src/plugins/builtin/cloud/plugin.tsx +150 -127
- package/src/plugins/builtin/composite-plugins.ts +71 -0
- package/src/plugins/builtin/correlation/index.test.tsx +4 -4
- package/src/plugins/builtin/correlation/index.tsx +3 -8
- package/src/plugins/builtin/earnings/index.tsx +37 -21
- package/src/plugins/builtin/earnings/model.test.ts +52 -0
- package/src/plugins/builtin/earnings/model.ts +32 -0
- package/src/plugins/builtin/econ/detail-view.tsx +17 -4
- package/src/plugins/builtin/econ/fred-cache.test.ts +57 -11
- package/src/plugins/builtin/econ/index.tsx +16 -20
- package/src/plugins/builtin/fear-greed/index.tsx +2 -8
- package/src/plugins/builtin/fx-matrix/index.tsx +3 -8
- package/src/plugins/builtin/help/components.tsx +5 -4
- package/src/plugins/builtin/help/index.tsx +14 -17
- package/src/plugins/builtin/holders/index.tsx +2 -8
- package/src/plugins/builtin/insider/index.tsx +2 -7
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +2 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +3 -7
- package/src/plugins/builtin/kelly-sizer/test-support.tsx +3 -3
- package/src/plugins/builtin/layout-manager/index.tsx +3 -7
- package/src/plugins/builtin/market-heatmap/index.tsx +3 -8
- package/src/plugins/builtin/market-movers/index.tsx +3 -8
- package/src/plugins/builtin/news/index.tsx +12 -16
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +3 -3
- package/src/plugins/builtin/news/wire/index.tsx +83 -73
- package/src/plugins/builtin/options/index.tsx +2 -8
- package/src/plugins/builtin/plugin-module.test.ts +89 -0
- package/src/plugins/builtin/plugin-module.ts +116 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/index.test.ts +17 -0
- package/src/plugins/builtin/portfolio-list/index.tsx +3 -5
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +3 -3
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +53 -53
- package/src/plugins/builtin/portfolio-list/settings.ts +5 -3
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +4 -3
- package/src/plugins/builtin/research/index.tsx +2 -8
- package/src/plugins/builtin/sec/index.tsx +2 -8
- package/src/plugins/builtin/sectors/index.tsx +3 -8
- package/src/plugins/builtin/thirteenf/index.tsx +3 -8
- package/src/plugins/builtin/thirteenf/pane.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +49 -23
- package/src/plugins/builtin/ticker-detail/index.test.tsx +15 -136
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -93
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +6 -5
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +41 -17
- package/src/plugins/builtin/ticker-detail/pane.tsx +4 -3
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/settings.ts +3 -50
- package/src/plugins/builtin/ticker-research-backend-plugin.ts +17 -0
- package/src/plugins/builtin/ticker-research-plugin.ts +27 -0
- package/src/plugins/builtin/tv/channels.ts +27 -0
- package/src/plugins/builtin/tv/index.tsx +40 -0
- package/src/plugins/builtin/tv/pane.tsx +286 -0
- package/src/plugins/builtin/tv/youtube-stream.ts +100 -0
- package/src/plugins/builtin/world-indices/index.tsx +3 -8
- package/src/plugins/builtin/yield-curve/index.tsx +8 -8
- package/src/plugins/catalog-backend.test.ts +21 -0
- package/src/plugins/catalog-backend.ts +42 -0
- package/src/plugins/catalog-ui.ts +6 -14
- package/src/plugins/ibkr/instance-selection.test.ts +0 -1
- package/src/plugins/ownership.ts +143 -0
- package/src/plugins/prediction-markets/backend-plugin.ts +19 -0
- package/src/plugins/prediction-markets/chart.test.tsx +7 -5
- package/src/plugins/prediction-markets/chart.tsx +25 -7
- package/src/plugins/registry/index.test.ts +51 -0
- package/src/plugins/registry/index.ts +8 -0
- package/src/plugins/registry/pane-settings.ts +1 -1
- package/src/plugins/runtime/index.test.tsx +2 -2
- package/src/remote/app-host.tsx +16 -3
- package/src/remote/controller.test.ts +102 -1
- package/src/remote/controller.ts +122 -0
- package/src/remote/types.ts +11 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +123 -27
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/media.ts +15 -0
- package/src/renderers/electrobun/bun/desktop/window-style.ts +4 -1
- package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +32 -1
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +10 -0
- package/src/renderers/electrobun/bun/index.ts +5 -0
- package/src/renderers/electrobun/bun/tui-entry.ts +1 -6
- package/src/renderers/electrobun/view/ai-host.ts +124 -19
- package/src/renderers/electrobun/view/backend-rpc.ts +4 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +151 -39
- package/src/renderers/electrobun/view/desktop/popover.tsx +166 -0
- package/src/renderers/electrobun/view/dialog-host.test.ts +25 -0
- package/src/renderers/electrobun/view/dialog-host.tsx +75 -12
- package/src/renderers/electrobun/view/host/box.tsx +5 -0
- package/src/renderers/electrobun/view/host/chart-surface.tsx +10 -9
- package/src/renderers/electrobun/view/host/input.tsx +9 -19
- package/src/renderers/electrobun/view/host/media-surface.tsx +170 -0
- package/src/renderers/electrobun/view/host/mouse.ts +1 -0
- package/src/renderers/electrobun/view/host/style.ts +1 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +4 -3
- package/src/renderers/electrobun/view/main.tsx +10 -3
- package/src/renderers/electrobun/view/styles.css +17 -0
- package/src/renderers/electrobun/view/ui-host.tsx +7 -0
- package/src/renderers/opentui/chart-surface.test.tsx +52 -6
- package/src/renderers/opentui/chart-surface.tsx +95 -11
- package/src/renderers/opentui/dialog-host.tsx +36 -5
- package/src/renderers/opentui/host.tsx +39 -0
- package/src/renderers/opentui/start.tsx +30 -0
- package/src/renderers/opentui/ui-host.tsx +2 -0
- package/src/sources/provider-router/index.test.ts +43 -0
- package/src/sources/provider-router/index.ts +4 -0
- package/src/sources/provider-router/supplemental.ts +46 -0
- package/src/sources/sec-edgar.test.ts +203 -2
- package/src/sources/sec-edgar.ts +70 -15
- package/src/state/app/context/index.tsx +13 -1
- package/src/state/app/context/selector.test.tsx +62 -1
- package/src/state/session-persistence.test.ts +13 -2
- package/src/sync/controller.test.ts +190 -3
- package/src/sync/controller.ts +76 -45
- package/src/sync/core-contributors.test.ts +121 -0
- package/src/sync/core-contributors.ts +104 -27
- package/src/sync/react.ts +6 -8
- package/src/sync/types.ts +3 -0
- package/src/time-series/alignment.ts +158 -0
- package/src/time-series/economic.ts +49 -0
- package/src/time-series/field-catalog.ts +306 -0
- package/src/time-series/fundamentals.test.ts +505 -0
- package/src/time-series/fundamentals.ts +842 -0
- package/src/time-series/hooks.ts +45 -0
- package/src/time-series/index.ts +14 -0
- package/src/time-series/live-quotes.test.ts +219 -0
- package/src/time-series/live-quotes.ts +203 -0
- package/src/time-series/market-resolution.ts +53 -0
- package/src/time-series/market.ts +125 -0
- package/src/time-series/reporting.ts +98 -0
- package/src/time-series/resolve.test.ts +1281 -0
- package/src/time-series/resolve.ts +783 -0
- package/src/time-series/spec-studies.test.ts +400 -0
- package/src/time-series/spec.ts +487 -0
- package/src/time-series/studies.ts +563 -0
- package/src/time-series/transforms.test.ts +105 -0
- package/src/time-series/transforms.ts +152 -0
- package/src/time-series/types.ts +162 -0
- package/src/time-series/use-chart-resolution.ts +166 -0
- package/src/types/config.ts +9 -11
- package/src/types/desktop-window.ts +1 -0
- package/src/types/financials.ts +4 -0
- package/src/types/media.ts +17 -0
- package/src/types/plugin.ts +32 -1
- package/src/ui/context-menu.tsx +15 -3
- package/src/ui/dialog.tsx +14 -2
- package/src/ui/host.tsx +40 -1
- package/src/ui/index.tsx +9 -0
- package/src/utils/exchanges.test.ts +13 -1
- package/src/utils/exchanges.ts +10 -0
- package/src/utils/financial-statements.test.ts +138 -0
- package/src/utils/financial-statements.ts +115 -8
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +7 -0
- package/src/version.ts +1 -1
- package/src/components/chart/bar-chart-renderer.test.ts +0 -233
- package/src/components/chart/bar-chart-renderer.ts +0 -445
- package/src/components/chart/comparison/data.test.ts +0 -159
- package/src/components/chart/comparison/data.ts +0 -302
- package/src/components/chart/comparison/renderer.test.ts +0 -187
- package/src/components/chart/comparison/renderer.ts +0 -282
- package/src/components/chart/comparison/scene.ts +0 -178
- package/src/components/chart/comparison-stock-chart/axis-width.ts +0 -99
- package/src/components/chart/comparison-stock-chart/canvas.ts +0 -148
- package/src/components/chart/comparison-stock-chart/controls.ts +0 -252
- package/src/components/chart/comparison-stock-chart/helpers.ts +0 -101
- package/src/components/chart/comparison-stock-chart/interactions.ts +0 -208
- package/src/components/chart/comparison-stock-chart/layout-metrics.ts +0 -62
- package/src/components/chart/comparison-stock-chart/layout.tsx +0 -155
- package/src/components/chart/comparison-stock-chart/legend.tsx +0 -74
- package/src/components/chart/comparison-stock-chart/native-surfaces.ts +0 -89
- package/src/components/chart/comparison-stock-chart/render-data.ts +0 -227
- package/src/components/chart/comparison-stock-chart/render-output.ts +0 -140
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +0 -128
- package/src/components/chart/comparison-stock-chart/sources.ts +0 -51
- package/src/components/chart/comparison-stock-chart/surface-runtime.ts +0 -159
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +0 -117
- package/src/components/chart/comparison-stock-chart/types.ts +0 -22
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -572
- package/src/components/chart/comparison-stock-chart/viewport-runtime.ts +0 -239
- package/src/components/chart/comparison-stock-chart.tsx +0 -24
- package/src/components/chart/core/axis-measure.ts +0 -33
- package/src/components/chart/core/controller.test.ts +0 -157
- package/src/components/chart/core/controller.ts +0 -349
- package/src/components/chart/core/indicator-selector.tsx +0 -75
- package/src/components/chart/core/keyboard.ts +0 -24
- package/src/components/chart/core/pane-settings.test.ts +0 -52
- package/src/components/chart/core/pane-settings.ts +0 -141
- package/src/components/chart/core/scroll.test.ts +0 -64
- package/src/components/chart/core/scroll.ts +0 -82
- package/src/components/chart/core/window-viewport.test.ts +0 -69
- package/src/components/chart/core/window-viewport.ts +0 -162
- package/src/components/chart/display-cursor.ts +0 -153
- package/src/components/chart/indicators/bands.test.ts +0 -36
- package/src/components/chart/indicators/bands.ts +0 -29
- package/src/components/chart/indicators/moving-averages.test.ts +0 -39
- package/src/components/chart/indicators/moving-averages.ts +0 -42
- package/src/components/chart/indicators/options.test.ts +0 -32
- package/src/components/chart/indicators/options.ts +0 -107
- package/src/components/chart/indicators/oscillators.test.ts +0 -58
- package/src/components/chart/indicators/oscillators.ts +0 -92
- package/src/components/chart/native/canvas-bitmaps.ts +0 -140
- package/src/components/chart/native/raster/comparison-chart.ts +0 -162
- package/src/components/chart/native/surface/hooks.ts +0 -144
- package/src/components/chart/native/surface/rendering.ts +0 -211
- package/src/components/chart/static/bar-chart-surface.test.tsx +0 -101
- package/src/components/chart/static/bar-chart-surface.tsx +0 -292
- package/src/components/chart/stock-chart/auto.ts +0 -216
- package/src/components/chart/stock-chart/axis-width.ts +0 -146
- package/src/components/chart/stock-chart/bitmaps.ts +0 -22
- package/src/components/chart/stock-chart/canvas.ts +0 -202
- package/src/components/chart/stock-chart/controls.tsx +0 -149
- package/src/components/chart/stock-chart/cursor.ts +0 -60
- package/src/components/chart/stock-chart/data-runtime.ts +0 -128
- package/src/components/chart/stock-chart/footer.test.ts +0 -65
- package/src/components/chart/stock-chart/footer.ts +0 -303
- package/src/components/chart/stock-chart/geometry.ts +0 -160
- package/src/components/chart/stock-chart/index.test.ts +0 -532
- package/src/components/chart/stock-chart/index.tsx +0 -29
- package/src/components/chart/stock-chart/indicators.ts +0 -226
- package/src/components/chart/stock-chart/interaction-runtime.ts +0 -44
- package/src/components/chart/stock-chart/interactions.ts +0 -300
- package/src/components/chart/stock-chart/keyboard.ts +0 -373
- package/src/components/chart/stock-chart/layout-metrics.ts +0 -62
- package/src/components/chart/stock-chart/layout.tsx +0 -177
- package/src/components/chart/stock-chart/native-surfaces.ts +0 -121
- package/src/components/chart/stock-chart/presentation.ts +0 -67
- package/src/components/chart/stock-chart/renderer-switch.test.tsx +0 -263
- package/src/components/chart/stock-chart/rendering/bounds.ts +0 -116
- package/src/components/chart/stock-chart/rendering/candidates.ts +0 -145
- package/src/components/chart/stock-chart/rendering/data.ts +0 -370
- package/src/components/chart/stock-chart/rendering/invalidation.ts +0 -68
- package/src/components/chart/stock-chart/rendering/output.ts +0 -304
- package/src/components/chart/stock-chart/rendering/planning.ts +0 -122
- package/src/components/chart/stock-chart/rendering/projection.test.tsx +0 -98
- package/src/components/chart/stock-chart/rendering/projection.ts +0 -112
- package/src/components/chart/stock-chart/rendering/state.test.ts +0 -70
- package/src/components/chart/stock-chart/rendering/state.ts +0 -283
- package/src/components/chart/stock-chart/requests.ts +0 -337
- package/src/components/chart/stock-chart/resolution-support.ts +0 -96
- package/src/components/chart/stock-chart/resolved/runtime.ts +0 -289
- package/src/components/chart/stock-chart/resolved.tsx +0 -359
- package/src/components/chart/stock-chart/settings.ts +0 -81
- package/src/components/chart/stock-chart/surface-runtime.ts +0 -62
- package/src/components/chart/stock-chart/types.ts +0 -25
- package/src/components/chart/stock-chart/view.tsx +0 -155
- package/src/components/chart/stock-chart/viewport/effects.ts +0 -293
- package/src/components/chart/stock-chart/viewport/index.ts +0 -144
- package/src/components/chart/stock-chart/viewport/runtime.ts +0 -375
- package/src/components/chart/time-axis-label.tsx +0 -175
- package/src/plugins/builtin/ai/gloomberb-cli.ts +0 -71
- package/src/plugins/builtin/ai/screener/action-bar.tsx +0 -58
- package/src/plugins/builtin/comparison-chart/index.test.tsx +0 -848
- package/src/plugins/builtin/comparison-chart/index.tsx +0 -194
- package/src/plugins/builtin/econ/fred-cache.ts +0 -106
- package/src/plugins/builtin/plugin-groups.ts +0 -130
- package/src/plugins/builtin/ticker-detail/chart-pane.tsx +0 -91
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -100
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +0 -296
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/data.ts +0 -62
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +0 -608
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +0 -65
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +0 -58
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +0 -138
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +0 -220
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
import { appendLiveQuotePoint } from "../components/chart/core/data";
|
|
2
|
+
import {
|
|
3
|
+
getTimeRangeForDateWindow,
|
|
4
|
+
subtractTimeRange,
|
|
5
|
+
} from "../components/chart/core/date-window";
|
|
6
|
+
import {
|
|
7
|
+
CHART_RESOLUTION_STEP_MS,
|
|
8
|
+
clampTimeRangeToMaxRange,
|
|
9
|
+
getBestSupportedResolutionForVisibleWindow,
|
|
10
|
+
getNextBufferRange,
|
|
11
|
+
getPresetResolution,
|
|
12
|
+
getSupportMaxRange,
|
|
13
|
+
intersectChartResolutionSupport,
|
|
14
|
+
TIME_RANGE_ORDER,
|
|
15
|
+
type ChartResolutionSupport,
|
|
16
|
+
type ManualChartResolution,
|
|
17
|
+
} from "../components/chart/core/resolution";
|
|
18
|
+
import type { TimeRange } from "../components/chart/core/types";
|
|
19
|
+
import type { DataProvider, MarketDataRequestContext } from "../types/data-provider";
|
|
20
|
+
import type { Quote, TickerFinancials } from "../types/financials";
|
|
21
|
+
import type { FredSeriesLoadResult, FredSeriesRequest } from "../data/fred-series";
|
|
22
|
+
import { extractFredSeries } from "./economic";
|
|
23
|
+
import {
|
|
24
|
+
getTimeSeriesField,
|
|
25
|
+
isFundamentalFieldId,
|
|
26
|
+
isMarketFieldId,
|
|
27
|
+
} from "./field-catalog";
|
|
28
|
+
import {
|
|
29
|
+
fundamentalSeriesUsesAvailabilityFallback,
|
|
30
|
+
valuationSeriesUsesLiveQuote,
|
|
31
|
+
} from "./fundamentals";
|
|
32
|
+
import { extractSecuritySeries } from "./market";
|
|
33
|
+
import {
|
|
34
|
+
activeStudyInputSeriesIds,
|
|
35
|
+
maxStudyWarmupPoints,
|
|
36
|
+
resolveStudies,
|
|
37
|
+
} from "./studies";
|
|
38
|
+
import { isOhlcSeriesStyle } from "./spec";
|
|
39
|
+
import { applyResolvedSeriesTransform } from "./transforms";
|
|
40
|
+
import { clipSeriesToWindow } from "./alignment";
|
|
41
|
+
import { chartQuoteOverrideKeyForSource } from "./live-quotes";
|
|
42
|
+
import { resolutionForExplicitMarketPeriods } from "./market-resolution";
|
|
43
|
+
import {
|
|
44
|
+
canonicalExchange,
|
|
45
|
+
publicTickerKey,
|
|
46
|
+
resolveExchangeTimeZone,
|
|
47
|
+
} from "../utils/exchanges";
|
|
48
|
+
import type {
|
|
49
|
+
ChartResolutionResult,
|
|
50
|
+
ChartSeriesSpec,
|
|
51
|
+
ChartSpec,
|
|
52
|
+
ResolvedSeries,
|
|
53
|
+
TimeSeriesPoint,
|
|
54
|
+
} from "./types";
|
|
55
|
+
|
|
56
|
+
const SERIES_COLORS = [
|
|
57
|
+
"#4dabf7",
|
|
58
|
+
"#63e6be",
|
|
59
|
+
"#f6c85f",
|
|
60
|
+
"#b197fc",
|
|
61
|
+
"#ff8787",
|
|
62
|
+
"#ffa94d",
|
|
63
|
+
"#74c0fc",
|
|
64
|
+
"#e599f7",
|
|
65
|
+
"#8ce99a",
|
|
66
|
+
"#ffd43b",
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
export interface ChartResolveSources {
|
|
70
|
+
dataProvider: DataProvider | null;
|
|
71
|
+
loadFredSeries: (request: FredSeriesRequest) => Promise<FredSeriesLoadResult>;
|
|
72
|
+
now?: Date;
|
|
73
|
+
/** Latest streamed quote per security identity, layered over snapshot data. */
|
|
74
|
+
quoteOverrides?: ReadonlyMap<string, Quote>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ChartResolveOptions {
|
|
78
|
+
/** Runtime interaction window used only while the authored resolution is Auto. */
|
|
79
|
+
autoViewport?: { start: Date; end: Date } | null;
|
|
80
|
+
/** Approximate number of horizontal observations the current surface can use. */
|
|
81
|
+
targetPointCount?: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Raw source data retained while live quotes recompute the chart tail. */
|
|
85
|
+
export class ChartResolveCache {
|
|
86
|
+
readonly financialsByInstrument = new Map<string, Promise<TickerFinancials | null>>();
|
|
87
|
+
readonly priceHistoryByRequest = new Map<string, Promise<TickerFinancials["priceHistory"]>>();
|
|
88
|
+
readonly resolutionSupportByInstrument = new Map<string, Promise<ChartResolutionSupport[]>>();
|
|
89
|
+
readonly fredSeriesByRequest = new Map<string, Promise<FredSeriesLoadResult>>();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface DateBounds {
|
|
93
|
+
start: number | null;
|
|
94
|
+
/** Inclusive upper bound. */
|
|
95
|
+
end: number | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface PriceHistoryRequest {
|
|
99
|
+
bounds: DateBounds;
|
|
100
|
+
explicitWindow: boolean;
|
|
101
|
+
fallbackRange: TimeRange;
|
|
102
|
+
resolution: ManualChartResolution;
|
|
103
|
+
allowProviderDefaultFallback: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
107
|
+
const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
108
|
+
|
|
109
|
+
function requestContext(spec: Extract<ChartSeriesSpec["source"], { kind: "security" }>): MarketDataRequestContext {
|
|
110
|
+
return {
|
|
111
|
+
brokerId: spec.instrument.brokerId,
|
|
112
|
+
brokerInstanceId: spec.instrument.brokerInstanceId,
|
|
113
|
+
instrument: spec.instrument.instrument ?? null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function instrumentKey(spec: Extract<ChartSeriesSpec["source"], { kind: "security" }>): string {
|
|
118
|
+
return chartQuoteOverrideKeyForSource(spec);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function instrumentLabel(spec: Extract<ChartSeriesSpec["source"], { kind: "security" }>): string {
|
|
122
|
+
return publicTickerKey(spec.instrument.symbol, spec.instrument.exchange);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function finiteDate(value: string | Date | undefined): Date | null {
|
|
126
|
+
if (!value) return null;
|
|
127
|
+
const date = value instanceof Date ? new Date(value) : new Date(value);
|
|
128
|
+
return Number.isFinite(date.getTime()) ? date : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function inclusiveEndDate(value: string | Date | undefined): Date | null {
|
|
132
|
+
const parsed = finiteDate(value);
|
|
133
|
+
if (!parsed) return null;
|
|
134
|
+
return typeof value === "string" && DATE_ONLY_PATTERN.test(value.trim())
|
|
135
|
+
? new Date(parsed.getTime() + DAY_MS - 1)
|
|
136
|
+
: parsed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function dateOnly(date: Date): string {
|
|
140
|
+
return date.toISOString().slice(0, 10);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function explicitBounds(spec: ChartSpec): DateBounds | null {
|
|
144
|
+
const explicitStart = finiteDate(spec.viewport.dateWindow?.start);
|
|
145
|
+
const explicitEnd = inclusiveEndDate(spec.viewport.dateWindow?.end);
|
|
146
|
+
if (!explicitStart || !explicitEnd || explicitStart.getTime() > explicitEnd.getTime()) return null;
|
|
147
|
+
return { start: explicitStart.getTime(), end: explicitEnd.getTime() };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function requestedBounds(spec: ChartSpec, latestObservation: Date): DateBounds {
|
|
151
|
+
const explicit = explicitBounds(spec);
|
|
152
|
+
if (explicit) return explicit;
|
|
153
|
+
return {
|
|
154
|
+
start: spec.viewport.range === "ALL"
|
|
155
|
+
? null
|
|
156
|
+
: subtractTimeRange(latestObservation, spec.viewport.range).getTime(),
|
|
157
|
+
end: latestObservation.getTime(),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function runtimeAutoBounds(options: ChartResolveOptions): DateBounds | null {
|
|
162
|
+
const start = options.autoViewport?.start.getTime();
|
|
163
|
+
const end = options.autoViewport?.end.getTime();
|
|
164
|
+
return typeof start === "number"
|
|
165
|
+
&& Number.isFinite(start)
|
|
166
|
+
&& typeof end === "number"
|
|
167
|
+
&& Number.isFinite(end)
|
|
168
|
+
&& start <= end
|
|
169
|
+
? { start, end }
|
|
170
|
+
: null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function sameBounds(left: DateBounds | null, right: DateBounds | null): boolean {
|
|
174
|
+
return left?.start === right?.start && left?.end === right?.end;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function boundsRange(bounds: DateBounds): TimeRange {
|
|
178
|
+
if (bounds.start === null || bounds.end === null) return "ALL";
|
|
179
|
+
return getTimeRangeForDateWindow({
|
|
180
|
+
start: new Date(bounds.start),
|
|
181
|
+
end: new Date(bounds.end),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function requestResolution(
|
|
186
|
+
spec: ChartSpec,
|
|
187
|
+
bounds: DateBounds,
|
|
188
|
+
calculationSeriesIds: ReadonlySet<string>,
|
|
189
|
+
options: ChartResolveOptions,
|
|
190
|
+
sharedSupport: readonly ChartResolutionSupport[],
|
|
191
|
+
): ManualChartResolution {
|
|
192
|
+
if (spec.viewport.resolution !== "auto") return spec.viewport.resolution;
|
|
193
|
+
const runtimeBounds = runtimeAutoBounds(options);
|
|
194
|
+
const adaptive = runtimeBounds && runtimeBounds.start !== null && runtimeBounds.end !== null
|
|
195
|
+
? getBestSupportedResolutionForVisibleWindow(
|
|
196
|
+
{ start: new Date(runtimeBounds.start), end: new Date(runtimeBounds.end) },
|
|
197
|
+
sharedSupport,
|
|
198
|
+
options.targetPointCount ?? 120,
|
|
199
|
+
)
|
|
200
|
+
: null;
|
|
201
|
+
const preferred = adaptive
|
|
202
|
+
?? getPresetResolution(explicitBounds(spec) ? boundsRange(bounds) : spec.viewport.range);
|
|
203
|
+
const activeSeries = spec.series.filter((entry) => calculationSeriesIds.has(entry.id));
|
|
204
|
+
return resolutionForExplicitMarketPeriods(preferred, activeSeries);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function calculationBounds(
|
|
208
|
+
spec: ChartSpec,
|
|
209
|
+
visibleBounds: DateBounds,
|
|
210
|
+
resolution: ManualChartResolution,
|
|
211
|
+
): DateBounds {
|
|
212
|
+
if (visibleBounds.start === null) return visibleBounds;
|
|
213
|
+
const warmupPoints = maxStudyWarmupPoints(spec.studies);
|
|
214
|
+
|
|
215
|
+
const visibleEnd = visibleBounds.end ?? visibleBounds.start;
|
|
216
|
+
const bufferedRange = getNextBufferRange(boundsRange(visibleBounds));
|
|
217
|
+
let start = bufferedRange === "ALL"
|
|
218
|
+
? subtractTimeRange(new Date(visibleEnd), "ALL").getTime()
|
|
219
|
+
: subtractTimeRange(new Date(visibleEnd), bufferedRange).getTime();
|
|
220
|
+
|
|
221
|
+
if (warmupPoints > 0) {
|
|
222
|
+
// Calendar gaps and closed sessions mean N observations often span more
|
|
223
|
+
// than N nominal bars. The doubled point window is a bounded safety margin.
|
|
224
|
+
const pointWarmup = warmupPoints * CHART_RESOLUTION_STEP_MS[resolution] * 2;
|
|
225
|
+
start = Math.min(start, visibleBounds.start - pointWarmup);
|
|
226
|
+
}
|
|
227
|
+
return { start: Math.min(start, visibleBounds.start), end: visibleBounds.end };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function trailingRangeForStart(start: number | null, referenceDate: Date): TimeRange {
|
|
231
|
+
if (start === null) return "ALL";
|
|
232
|
+
for (const range of TIME_RANGE_ORDER) {
|
|
233
|
+
if (range === "ALL" || start >= subtractTimeRange(referenceDate, range).getTime()) return range;
|
|
234
|
+
}
|
|
235
|
+
return "ALL";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function exclusiveEnd(bounds: DateBounds): Date | null {
|
|
239
|
+
return bounds.end === null ? null : new Date(bounds.end + 1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function filterPoints(points: readonly TimeSeriesPoint[], bounds: DateBounds): TimeSeriesPoint[] {
|
|
243
|
+
return points.filter((point) => {
|
|
244
|
+
const time = point.date.getTime();
|
|
245
|
+
return Number.isFinite(time)
|
|
246
|
+
&& (bounds.start === null || time >= bounds.start)
|
|
247
|
+
&& (bounds.end === null || time <= bounds.end);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function emptyFinancials(priceHistory: TickerFinancials["priceHistory"] = []): TickerFinancials {
|
|
252
|
+
return { annualStatements: [], quarterlyStatements: [], priceHistory };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function latestQuote(snapshot: Quote | undefined, override: Quote | undefined): Quote | undefined {
|
|
256
|
+
if (!snapshot) return override;
|
|
257
|
+
if (!override) return snapshot;
|
|
258
|
+
if (override.lastUpdated !== snapshot.lastUpdated) {
|
|
259
|
+
return override.lastUpdated > snapshot.lastUpdated ? override : snapshot;
|
|
260
|
+
}
|
|
261
|
+
return (override.receivedAt ?? 0) >= (snapshot.receivedAt ?? 0) ? override : snapshot;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function mergeHistory(
|
|
265
|
+
financials: TickerFinancials | null,
|
|
266
|
+
history: TickerFinancials["priceHistory"],
|
|
267
|
+
quoteOverride: Quote | undefined,
|
|
268
|
+
now: number,
|
|
269
|
+
liveBarResolution?: ManualChartResolution,
|
|
270
|
+
): TickerFinancials {
|
|
271
|
+
const base = financials ?? emptyFinancials();
|
|
272
|
+
const quote = latestQuote(base.quote, quoteOverride);
|
|
273
|
+
const priceHistory = appendLiveQuotePoint(history, quote, liveBarResolution
|
|
274
|
+
? { now, mode: "ohlc", resolution: liveBarResolution }
|
|
275
|
+
: { now });
|
|
276
|
+
return { ...base, quote, priceHistory };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function loadPriceHistory(
|
|
280
|
+
provider: DataProvider,
|
|
281
|
+
source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
|
|
282
|
+
request: PriceHistoryRequest,
|
|
283
|
+
): Promise<TickerFinancials["priceHistory"]> {
|
|
284
|
+
const context = requestContext(source);
|
|
285
|
+
const detailStart = request.bounds.start === null ? null : new Date(request.bounds.start);
|
|
286
|
+
const detailEnd = exclusiveEnd(request.bounds);
|
|
287
|
+
if (request.explicitWindow && detailStart && detailEnd && provider.getDetailedPriceHistory) {
|
|
288
|
+
try {
|
|
289
|
+
const detailed = await provider.getDetailedPriceHistory(
|
|
290
|
+
source.instrument.symbol,
|
|
291
|
+
source.instrument.exchange ?? "",
|
|
292
|
+
detailStart,
|
|
293
|
+
detailEnd,
|
|
294
|
+
request.resolution,
|
|
295
|
+
context,
|
|
296
|
+
);
|
|
297
|
+
if (detailed.length > 0) return detailed;
|
|
298
|
+
} catch {
|
|
299
|
+
// Fall through to trailing history when a provider cannot serve the exact window.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (provider.getPriceHistoryForResolution) {
|
|
303
|
+
try {
|
|
304
|
+
const resolved = await provider.getPriceHistoryForResolution(
|
|
305
|
+
source.instrument.symbol,
|
|
306
|
+
source.instrument.exchange ?? "",
|
|
307
|
+
request.fallbackRange,
|
|
308
|
+
request.resolution,
|
|
309
|
+
context,
|
|
310
|
+
);
|
|
311
|
+
if (resolved.length > 0) return resolved;
|
|
312
|
+
} catch {
|
|
313
|
+
// Some providers expose the resolution API but only support a subset.
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (!request.allowProviderDefaultFallback) {
|
|
317
|
+
// getPriceHistory chooses its own interval, so using it here would make a
|
|
318
|
+
// manual interval label claim a granularity the provider did not honor.
|
|
319
|
+
throw new Error(
|
|
320
|
+
`Requested ${request.resolution} price history is unavailable for ${instrumentLabel(source)}. Choose Auto or a supported interval.`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return provider.getPriceHistory(
|
|
324
|
+
source.instrument.symbol,
|
|
325
|
+
source.instrument.exchange ?? "",
|
|
326
|
+
request.fallbackRange,
|
|
327
|
+
context,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function baseSecuritySeries(
|
|
332
|
+
spec: ChartSeriesSpec,
|
|
333
|
+
financials: TickerFinancials,
|
|
334
|
+
index: number,
|
|
335
|
+
marketResolution?: ManualChartResolution,
|
|
336
|
+
): ResolvedSeries | null {
|
|
337
|
+
if (spec.source.kind !== "security") return null;
|
|
338
|
+
const field = getTimeSeriesField(spec.source.fieldId);
|
|
339
|
+
if (!field) return null;
|
|
340
|
+
const points = extractSecuritySeries(financials, spec.source);
|
|
341
|
+
const symbol = instrumentLabel(spec.source);
|
|
342
|
+
const currency = financials.quote?.currency;
|
|
343
|
+
const unit = field.unit.startsWith("currency") && currency
|
|
344
|
+
? field.unit.replace("currency", currency)
|
|
345
|
+
: field.unit;
|
|
346
|
+
const currencyUnitGroup = field.unit.startsWith("currency") && currency
|
|
347
|
+
? `${field.unitGroup}:${currency}`
|
|
348
|
+
: field.unitGroup;
|
|
349
|
+
const marketExchange = spec.source.instrument.exchange
|
|
350
|
+
|| financials.quote?.listingExchangeName
|
|
351
|
+
|| financials.quote?.exchangeName;
|
|
352
|
+
const marketTimeZone = isMarketFieldId(spec.source.fieldId)
|
|
353
|
+
&& canonicalExchange(marketExchange) !== "CCC"
|
|
354
|
+
? resolveExchangeTimeZone(marketExchange)
|
|
355
|
+
: null;
|
|
356
|
+
return {
|
|
357
|
+
id: spec.id,
|
|
358
|
+
label: spec.label?.trim() || `${symbol} ${field.shortLabel}`,
|
|
359
|
+
color: spec.color ?? SERIES_COLORS[index % SERIES_COLORS.length]!,
|
|
360
|
+
unit,
|
|
361
|
+
unitGroup: currencyUnitGroup,
|
|
362
|
+
nativeFrequency: spec.source.period && spec.source.period !== "auto"
|
|
363
|
+
? spec.source.period
|
|
364
|
+
: field.nativeFrequency,
|
|
365
|
+
timestampMode: spec.source.timestampMode,
|
|
366
|
+
dataShape: field.dataShape,
|
|
367
|
+
style: spec.style,
|
|
368
|
+
transform: spec.transform,
|
|
369
|
+
axis: spec.axis === "right" ? "right" : "left",
|
|
370
|
+
panelId: spec.panelId,
|
|
371
|
+
interpolation: spec.interpolation,
|
|
372
|
+
timeBasis: marketTimeZone
|
|
373
|
+
? {
|
|
374
|
+
kind: "market",
|
|
375
|
+
timeZone: marketTimeZone,
|
|
376
|
+
cadenceMs: marketResolution
|
|
377
|
+
? CHART_RESOLUTION_STEP_MS[marketResolution]
|
|
378
|
+
: undefined,
|
|
379
|
+
}
|
|
380
|
+
: undefined,
|
|
381
|
+
points,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function baseEconomicSeries(
|
|
386
|
+
spec: ChartSeriesSpec,
|
|
387
|
+
loaded: FredSeriesLoadResult,
|
|
388
|
+
index: number,
|
|
389
|
+
): ResolvedSeries | null {
|
|
390
|
+
if (spec.source.kind !== "economic") return null;
|
|
391
|
+
const { data } = loaded;
|
|
392
|
+
const units = data.info?.units?.trim() || "value";
|
|
393
|
+
const isPercent = units.toLowerCase().includes("percent");
|
|
394
|
+
return {
|
|
395
|
+
id: spec.id,
|
|
396
|
+
label: spec.label?.trim() || data.info?.title?.trim() || spec.source.seriesId,
|
|
397
|
+
color: spec.color ?? SERIES_COLORS[index % SERIES_COLORS.length]!,
|
|
398
|
+
unit: isPercent ? "%" : units,
|
|
399
|
+
unitGroup: isPercent ? "percent" : `economic:${units.toLowerCase()}`,
|
|
400
|
+
nativeFrequency: "auto",
|
|
401
|
+
timestampMode: "period-end",
|
|
402
|
+
dataShape: "scalar",
|
|
403
|
+
style: spec.style,
|
|
404
|
+
transform: spec.transform,
|
|
405
|
+
axis: spec.axis === "right" ? "right" : "left",
|
|
406
|
+
panelId: spec.panelId,
|
|
407
|
+
interpolation: spec.interpolation,
|
|
408
|
+
points: extractFredSeries(data.observations, { providerId: "fred", timestampMode: "period-end" }),
|
|
409
|
+
warning: "FRED vintage dates are unavailable; observations use period dates.",
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function staleFredWarning(loaded: FredSeriesLoadResult): string | null {
|
|
414
|
+
if (!loaded.stale) return null;
|
|
415
|
+
return `FRED refresh failed${loaded.refreshError ? ` (${loaded.refreshError})` : ""}; showing cached data fetched ${new Date(loaded.fetchedAt).toISOString().slice(0, 10)}.`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function assignAxes(
|
|
419
|
+
series: ResolvedSeries[],
|
|
420
|
+
specs: readonly { id: string; axis: ChartSeriesSpec["axis"] }[],
|
|
421
|
+
warnings: string[],
|
|
422
|
+
): ResolvedSeries[] {
|
|
423
|
+
const requested = new Map(specs.map((spec) => [spec.id, spec.axis] as const));
|
|
424
|
+
const groupsByPanel = new Map<string, Partial<Record<"left" | "right", string>>>();
|
|
425
|
+
return series.map((entry) => {
|
|
426
|
+
const groups = groupsByPanel.get(entry.panelId) ?? {};
|
|
427
|
+
groupsByPanel.set(entry.panelId, groups);
|
|
428
|
+
const preferred = requested.get(entry.id);
|
|
429
|
+
let axis: "left" | "right";
|
|
430
|
+
if (preferred === "left" || preferred === "right") {
|
|
431
|
+
axis = preferred;
|
|
432
|
+
} else if (groups.left === entry.unitGroup) {
|
|
433
|
+
axis = "left";
|
|
434
|
+
} else if (groups.right === entry.unitGroup) {
|
|
435
|
+
axis = "right";
|
|
436
|
+
} else if (!groups.left) {
|
|
437
|
+
axis = "left";
|
|
438
|
+
} else {
|
|
439
|
+
axis = "right";
|
|
440
|
+
}
|
|
441
|
+
if (groups[axis] && groups[axis] !== entry.unitGroup) {
|
|
442
|
+
warnings.push(`${entry.label} shares the ${axis} axis with a different unit; choose an explicit panel for independent scaling.`);
|
|
443
|
+
}
|
|
444
|
+
groups[axis] ??= entry.unitGroup;
|
|
445
|
+
return { ...entry, axis };
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function prepareBaseSeriesForStudies(
|
|
450
|
+
series: ResolvedSeries,
|
|
451
|
+
bounds: DateBounds,
|
|
452
|
+
clipToBounds = false,
|
|
453
|
+
): ResolvedSeries {
|
|
454
|
+
const baselineTransform = series.transform === "percent" || series.transform === "index100";
|
|
455
|
+
let source = series;
|
|
456
|
+
if (clipToBounds && bounds.start !== null && bounds.end !== null) {
|
|
457
|
+
source = clipSeriesToWindow(series, new Date(bounds.start), new Date(bounds.end));
|
|
458
|
+
} else if (clipToBounds) {
|
|
459
|
+
source = { ...series, points: filterPoints(series.points, bounds) };
|
|
460
|
+
}
|
|
461
|
+
return applyResolvedSeriesTransform(
|
|
462
|
+
source,
|
|
463
|
+
source.transform,
|
|
464
|
+
baselineTransform ? { baseline: scalarBaseline(series, bounds) } : undefined,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function rawCalculationSeries(series: ResolvedSeries, bounds: DateBounds): ResolvedSeries {
|
|
469
|
+
if (bounds.start !== null && bounds.end !== null) {
|
|
470
|
+
return clipSeriesToWindow(series, new Date(bounds.start), new Date(bounds.end));
|
|
471
|
+
}
|
|
472
|
+
return { ...series, points: filterPoints(series.points, bounds) };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function scalarBaseline(series: ResolvedSeries, bounds: DateBounds): number | null {
|
|
476
|
+
const points = filterPoints(series.points, bounds);
|
|
477
|
+
for (const point of points) {
|
|
478
|
+
const value = typeof point.value === "number" && Number.isFinite(point.value)
|
|
479
|
+
? point.value
|
|
480
|
+
: typeof point.close === "number" && Number.isFinite(point.close)
|
|
481
|
+
? point.close
|
|
482
|
+
: null;
|
|
483
|
+
if (value !== null && value !== 0) return value;
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function studyForOutput(
|
|
489
|
+
outputId: string,
|
|
490
|
+
studies: readonly ChartSpec["studies"][number][],
|
|
491
|
+
): ChartSpec["studies"][number] | undefined {
|
|
492
|
+
return studies
|
|
493
|
+
.filter((study) => outputId === study.id || outputId.startsWith(`${study.id}:`))
|
|
494
|
+
.sort((left, right) => right.id.length - left.id.length)[0];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function applyStudyPresentationTransforms(
|
|
498
|
+
outputs: ResolvedSeries[],
|
|
499
|
+
studies: readonly ChartSpec["studies"][number][],
|
|
500
|
+
rawSeries: readonly ResolvedSeries[],
|
|
501
|
+
visibleBounds: DateBounds,
|
|
502
|
+
): ResolvedSeries[] {
|
|
503
|
+
const rawById = new Map(rawSeries.map((series) => [series.id, series] as const));
|
|
504
|
+
return outputs.map((output) => {
|
|
505
|
+
const study = studyForOutput(output.id, studies);
|
|
506
|
+
if (!study || (study.kind !== "sma" && study.kind !== "ema" && study.kind !== "bollinger")) {
|
|
507
|
+
return output;
|
|
508
|
+
}
|
|
509
|
+
const input = rawById.get(study.inputSeriesIds[0] ?? "");
|
|
510
|
+
if (!input || input.transform === "raw") return output;
|
|
511
|
+
const baseline = input.transform === "percent" || input.transform === "index100"
|
|
512
|
+
? scalarBaseline(input, visibleBounds)
|
|
513
|
+
: undefined;
|
|
514
|
+
return applyResolvedSeriesTransform(output, input.transform, { baseline });
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export async function resolveChartSpecData(
|
|
519
|
+
spec: ChartSpec,
|
|
520
|
+
sources: ChartResolveSources,
|
|
521
|
+
cache = new ChartResolveCache(),
|
|
522
|
+
options: ChartResolveOptions = {},
|
|
523
|
+
): Promise<ChartResolutionResult> {
|
|
524
|
+
const errors: string[] = [];
|
|
525
|
+
const warnings: string[] = [];
|
|
526
|
+
const priorityWarnings: string[] = [];
|
|
527
|
+
const baseSeriesIds = new Set(spec.series.map((entry) => entry.id));
|
|
528
|
+
const visibleSeriesIds = new Set(spec.series
|
|
529
|
+
.filter((entry) => entry.visible !== false)
|
|
530
|
+
.map((entry) => entry.id));
|
|
531
|
+
const calculationSeriesIds = activeStudyInputSeriesIds(spec.studies);
|
|
532
|
+
visibleSeriesIds.forEach((id) => calculationSeriesIds.add(id));
|
|
533
|
+
// Keep the first authored market series loaded as the deterministic shared
|
|
534
|
+
// session anchor even when the user temporarily hides its marks.
|
|
535
|
+
const primaryMarketSeries = spec.series.find((entry) => (
|
|
536
|
+
entry.source.kind === "security" && isMarketFieldId(entry.source.fieldId)
|
|
537
|
+
));
|
|
538
|
+
if (primaryMarketSeries) calculationSeriesIds.add(primaryMarketSeries.id);
|
|
539
|
+
if (!sources.dataProvider && spec.series.some((entry) => (
|
|
540
|
+
calculationSeriesIds.has(entry.id) && entry.source.kind === "security"
|
|
541
|
+
))) {
|
|
542
|
+
return { series: [], loading: false, errors: ["Market data is unavailable."], warnings };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const referenceNow = sources.now ?? new Date();
|
|
546
|
+
const initialVisibleBounds = requestedBounds(spec, referenceNow);
|
|
547
|
+
|
|
548
|
+
const loadFinancials = (source: Extract<ChartSeriesSpec["source"], { kind: "security" }>) => {
|
|
549
|
+
const key = instrumentKey(source);
|
|
550
|
+
let pending = cache.financialsByInstrument.get(key);
|
|
551
|
+
if (!pending) {
|
|
552
|
+
pending = sources.dataProvider!
|
|
553
|
+
.getTickerFinancials(
|
|
554
|
+
source.instrument.symbol,
|
|
555
|
+
source.instrument.exchange ?? "",
|
|
556
|
+
requestContext(source),
|
|
557
|
+
)
|
|
558
|
+
.catch(() => null);
|
|
559
|
+
cache.financialsByInstrument.set(key, pending);
|
|
560
|
+
}
|
|
561
|
+
return pending;
|
|
562
|
+
};
|
|
563
|
+
const loadResolutionSupport = (
|
|
564
|
+
source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
|
|
565
|
+
) => {
|
|
566
|
+
const provider = sources.dataProvider!;
|
|
567
|
+
if (!provider.getChartResolutionSupport) return Promise.resolve([]);
|
|
568
|
+
const key = `${provider.id}|${instrumentKey(source)}`;
|
|
569
|
+
let pending = cache.resolutionSupportByInstrument.get(key);
|
|
570
|
+
if (!pending) {
|
|
571
|
+
pending = Promise.resolve(provider.getChartResolutionSupport(
|
|
572
|
+
source.instrument.symbol,
|
|
573
|
+
source.instrument.exchange ?? "",
|
|
574
|
+
requestContext(source),
|
|
575
|
+
)).catch(() => []);
|
|
576
|
+
cache.resolutionSupportByInstrument.set(key, pending);
|
|
577
|
+
}
|
|
578
|
+
return pending;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const runtimeBounds = spec.viewport.resolution === "auto"
|
|
582
|
+
? runtimeAutoBounds(options)
|
|
583
|
+
: null;
|
|
584
|
+
const activeMarketSources = [...new Map(spec.series.flatMap((entry) => (
|
|
585
|
+
calculationSeriesIds.has(entry.id)
|
|
586
|
+
&& entry.source.kind === "security"
|
|
587
|
+
&& isMarketFieldId(entry.source.fieldId)
|
|
588
|
+
? [[instrumentKey(entry.source), entry.source] as const]
|
|
589
|
+
: []
|
|
590
|
+
))).values()];
|
|
591
|
+
const sharedSupport = runtimeBounds && activeMarketSources.length > 0
|
|
592
|
+
? intersectChartResolutionSupport(await Promise.all(
|
|
593
|
+
activeMarketSources.map((source) => loadResolutionSupport(source)),
|
|
594
|
+
))
|
|
595
|
+
: [];
|
|
596
|
+
const initialResolution = requestResolution(
|
|
597
|
+
spec,
|
|
598
|
+
initialVisibleBounds,
|
|
599
|
+
calculationSeriesIds,
|
|
600
|
+
options,
|
|
601
|
+
sharedSupport,
|
|
602
|
+
);
|
|
603
|
+
const requestVisibleBounds = runtimeBounds ?? initialVisibleBounds;
|
|
604
|
+
const initialCalculationBounds = calculationBounds(
|
|
605
|
+
spec,
|
|
606
|
+
requestVisibleBounds,
|
|
607
|
+
initialResolution,
|
|
608
|
+
);
|
|
609
|
+
const hasExplicitWindow = explicitBounds(spec) !== null
|
|
610
|
+
|| (runtimeBounds !== null && !sameBounds(runtimeBounds, initialVisibleBounds));
|
|
611
|
+
|
|
612
|
+
const loadHistory = async (
|
|
613
|
+
source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
|
|
614
|
+
all = false,
|
|
615
|
+
) => {
|
|
616
|
+
const requestedFallbackRange = all
|
|
617
|
+
? "ALL"
|
|
618
|
+
: trailingRangeForStart(initialCalculationBounds.start, referenceNow);
|
|
619
|
+
const support = await loadResolutionSupport(source);
|
|
620
|
+
const maxRange = getSupportMaxRange(support, initialResolution);
|
|
621
|
+
const fallbackRange = maxRange
|
|
622
|
+
? clampTimeRangeToMaxRange(requestedFallbackRange, maxRange)
|
|
623
|
+
: requestedFallbackRange;
|
|
624
|
+
const request: PriceHistoryRequest = {
|
|
625
|
+
bounds: initialCalculationBounds,
|
|
626
|
+
explicitWindow: hasExplicitWindow,
|
|
627
|
+
fallbackRange,
|
|
628
|
+
resolution: initialResolution,
|
|
629
|
+
allowProviderDefaultFallback: spec.viewport.resolution === "auto",
|
|
630
|
+
};
|
|
631
|
+
const key = [
|
|
632
|
+
instrumentKey(source),
|
|
633
|
+
request.resolution,
|
|
634
|
+
request.fallbackRange,
|
|
635
|
+
...(request.explicitWindow
|
|
636
|
+
? [request.bounds.start ?? "open", request.bounds.end ?? "open"]
|
|
637
|
+
: []),
|
|
638
|
+
].join("|");
|
|
639
|
+
let pending = cache.priceHistoryByRequest.get(key);
|
|
640
|
+
if (!pending) {
|
|
641
|
+
pending = loadPriceHistory(sources.dataProvider!, source, request);
|
|
642
|
+
cache.priceHistoryByRequest.set(key, pending);
|
|
643
|
+
}
|
|
644
|
+
return pending;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const loadEconomicSeries = (request: FredSeriesRequest) => {
|
|
648
|
+
const key = `${request.seriesId.trim().toUpperCase()}|${request.startDate}|${request.sortOrder}`;
|
|
649
|
+
let pending = cache.fredSeriesByRequest.get(key);
|
|
650
|
+
if (!pending) {
|
|
651
|
+
pending = sources.loadFredSeries(request);
|
|
652
|
+
cache.fredSeriesByRequest.set(key, pending);
|
|
653
|
+
}
|
|
654
|
+
return pending;
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const loaded = await Promise.all(spec.series.map(async (seriesSpec, index) => {
|
|
658
|
+
if (!calculationSeriesIds.has(seriesSpec.id)) return null;
|
|
659
|
+
try {
|
|
660
|
+
if (seriesSpec.source.kind === "economic") {
|
|
661
|
+
const request: FredSeriesRequest = {
|
|
662
|
+
seriesId: seriesSpec.source.seriesId,
|
|
663
|
+
startDate: initialCalculationBounds.start === null
|
|
664
|
+
? "1900-01-01"
|
|
665
|
+
: dateOnly(new Date(initialCalculationBounds.start)),
|
|
666
|
+
sortOrder: "asc",
|
|
667
|
+
};
|
|
668
|
+
const fred = await loadEconomicSeries(request);
|
|
669
|
+
const result = baseEconomicSeries(seriesSpec, fred, index);
|
|
670
|
+
const freshnessWarning = staleFredWarning(fred);
|
|
671
|
+
if (result && freshnessWarning) priorityWarnings.push(`${result.label}: ${freshnessWarning}`);
|
|
672
|
+
return result;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const source = seriesSpec.source;
|
|
676
|
+
const marketField = isMarketFieldId(source.fieldId);
|
|
677
|
+
const quoteDerivedValuation = valuationSeriesUsesLiveQuote(source.fieldId);
|
|
678
|
+
const needsHistory = marketField || quoteDerivedValuation;
|
|
679
|
+
const [financials, history] = await Promise.all([
|
|
680
|
+
loadFinancials(source),
|
|
681
|
+
needsHistory ? loadHistory(source, quoteDerivedValuation) : Promise.resolve(null),
|
|
682
|
+
]);
|
|
683
|
+
const quoteOverride = marketField || quoteDerivedValuation
|
|
684
|
+
? sources.quoteOverrides?.get(chartQuoteOverrideKeyForSource(source))
|
|
685
|
+
: undefined;
|
|
686
|
+
const liveBarResolution = isOhlcSeriesStyle(seriesSpec.style) ? initialResolution : undefined;
|
|
687
|
+
const merged = history
|
|
688
|
+
? mergeHistory(financials, history, quoteOverride, referenceNow.getTime(), liveBarResolution)
|
|
689
|
+
: quoteOverride && financials
|
|
690
|
+
? { ...financials, quote: latestQuote(financials.quote, quoteOverride) }
|
|
691
|
+
: financials;
|
|
692
|
+
if (!merged) throw new Error(`No financial data is available for ${instrumentLabel(source)}.`);
|
|
693
|
+
const result = baseSecuritySeries(seriesSpec, merged, index, initialResolution);
|
|
694
|
+
if (!result) throw new Error(`Unknown field ${source.fieldId}.`);
|
|
695
|
+
if (isFundamentalFieldId(source.fieldId) && fundamentalSeriesUsesAvailabilityFallback(merged, source)) {
|
|
696
|
+
result.warning = "Publication dates are unavailable for some observations; period-end dates are used as a fallback.";
|
|
697
|
+
}
|
|
698
|
+
return result;
|
|
699
|
+
} catch (error) {
|
|
700
|
+
errors.push(`${seriesSpec.label ?? seriesSpec.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
}));
|
|
704
|
+
|
|
705
|
+
const rawSeries = loaded.filter((entry): entry is ResolvedSeries => !!entry);
|
|
706
|
+
// Preset ranges describe a window ending at the requested reference time,
|
|
707
|
+
// even when the newest filing or economic observation lags that endpoint.
|
|
708
|
+
// Re-anchoring to the latest observation both mislabels the range and asks
|
|
709
|
+
// providers for a different window than the one ultimately rendered.
|
|
710
|
+
const bounds = initialVisibleBounds;
|
|
711
|
+
const resolution = initialResolution;
|
|
712
|
+
const studyBounds = initialCalculationBounds;
|
|
713
|
+
const baseSeries = rawSeries
|
|
714
|
+
.filter((entry) => visibleSeriesIds.has(entry.id))
|
|
715
|
+
.map((entry) => prepareBaseSeriesForStudies(entry, bounds));
|
|
716
|
+
const calculationSeries = rawSeries.map((entry) => rawCalculationSeries(entry, studyBounds));
|
|
717
|
+
let resolved = baseSeries;
|
|
718
|
+
|
|
719
|
+
// Study outputs are appended by the pure engine before the final viewport clip.
|
|
720
|
+
if (spec.studies.length > 0) {
|
|
721
|
+
const studyResult = resolveStudies(calculationSeries, spec.studies);
|
|
722
|
+
resolved = [
|
|
723
|
+
...resolved,
|
|
724
|
+
...applyStudyPresentationTransforms(studyResult.series, spec.studies, rawSeries, bounds),
|
|
725
|
+
];
|
|
726
|
+
warnings.push(...studyResult.warnings);
|
|
727
|
+
errors.push(...studyResult.errors);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const bufferedSeries = assignAxes(resolved, [...spec.series, ...spec.studies], warnings);
|
|
731
|
+
resolved = bufferedSeries.map((entry) => (
|
|
732
|
+
bounds.start !== null && bounds.end !== null
|
|
733
|
+
? clipSeriesToWindow(entry, new Date(bounds.start), new Date(bounds.end))
|
|
734
|
+
: { ...entry, points: filterPoints(entry.points, bounds) }
|
|
735
|
+
));
|
|
736
|
+
if (spec.viewport.maxPoints !== undefined) {
|
|
737
|
+
resolved = resolved.map((entry) => ({
|
|
738
|
+
...entry,
|
|
739
|
+
points: entry.points.slice(-spec.viewport.maxPoints!),
|
|
740
|
+
}));
|
|
741
|
+
}
|
|
742
|
+
const resolvedById = new Map(resolved.map((entry) => [entry.id, entry] as const));
|
|
743
|
+
const hiddenBaseSeries = rawSeries
|
|
744
|
+
.filter((entry) => !visibleSeriesIds.has(entry.id))
|
|
745
|
+
.map((entry) => prepareBaseSeriesForStudies(entry, bounds, true));
|
|
746
|
+
const hiddenBaseById = new Map(hiddenBaseSeries.map((entry) => [entry.id, entry] as const));
|
|
747
|
+
const legendSeries = [
|
|
748
|
+
...spec.series.flatMap((seriesSpec) => {
|
|
749
|
+
const entry = resolvedById.get(seriesSpec.id) ?? hiddenBaseById.get(seriesSpec.id);
|
|
750
|
+
return entry ? [entry] : [];
|
|
751
|
+
}),
|
|
752
|
+
...resolved.filter((entry) => !baseSeriesIds.has(entry.id)),
|
|
753
|
+
];
|
|
754
|
+
for (const entry of resolved) {
|
|
755
|
+
if (entry.warning) warnings.push(`${entry.label}: ${entry.warning}`);
|
|
756
|
+
if (entry.points.length === 0) warnings.push(`${entry.label}: no observations in the selected date range.`);
|
|
757
|
+
}
|
|
758
|
+
for (const panel of spec.panels) {
|
|
759
|
+
if (panel.scale !== "log") continue;
|
|
760
|
+
const hiddenCount = resolved
|
|
761
|
+
.filter((entry) => entry.panelId === panel.id)
|
|
762
|
+
.flatMap((entry) => entry.points)
|
|
763
|
+
.filter((point) => typeof point.value === "number" && Number.isFinite(point.value) && point.value <= 0)
|
|
764
|
+
.length;
|
|
765
|
+
if (hiddenCount > 0) {
|
|
766
|
+
warnings.push(`${panel.label ?? panel.id}: ${hiddenCount} non-positive observation${hiddenCount === 1 ? " is" : "s are"} hidden on the logarithmic scale.`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const exposeViewport = hasExplicitWindow || spec.viewport.maxPoints === undefined;
|
|
771
|
+
const viewport = exposeViewport && bounds.start !== null && bounds.end !== null
|
|
772
|
+
? { start: new Date(bounds.start), end: new Date(bounds.end) }
|
|
773
|
+
: undefined;
|
|
774
|
+
return {
|
|
775
|
+
series: resolved,
|
|
776
|
+
legendSeries,
|
|
777
|
+
...(spec.viewport.maxPoints === undefined ? { bufferedSeries } : {}),
|
|
778
|
+
loading: false,
|
|
779
|
+
errors,
|
|
780
|
+
warnings: [...new Set([...priorityWarnings, ...warnings])],
|
|
781
|
+
viewport,
|
|
782
|
+
};
|
|
783
|
+
}
|