gloomberb 0.16.2 → 0.16.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/package.json +4 -1
- package/src/api-client/askg.ts +43 -0
- package/src/api-client/auth.ts +15 -0
- package/src/api-client/beneficial-owners.ts +4 -0
- package/src/api-client/chat.ts +19 -0
- package/src/api-client/company-kpis.ts +14 -2
- package/src/api-client/data.ts +34 -16
- package/src/api-client/index.ts +12 -0
- package/src/api-client/normalizers.ts +27 -0
- package/src/api-client/paths.ts +88 -23
- package/src/api-client/request.ts +2 -1
- package/src/api-client/research-activity.ts +31 -0
- package/src/api-client/socket.ts +39 -8
- package/src/api-client/types.ts +57 -1
- package/src/api-client/upgrade-experiment.ts +90 -0
- package/src/app/global-shortcuts.ts +11 -1
- package/src/app/keybindings/actions.ts +10 -0
- package/src/app/pane-runtime/index.tsx +8 -0
- package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
- package/src/app/pane-runtime/plugin-bindings.ts +65 -25
- package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
- package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
- package/src/app/runtime/desktop-deeplink.ts +5 -0
- package/src/app/star-prompt/model.ts +98 -0
- package/src/app/star-prompt/runtime.ts +41 -0
- package/src/app/star-prompt/status-line.tsx +190 -0
- package/src/app.tsx +13 -7
- package/src/architecture/core-builtin-allowlist.json +70 -0
- package/src/brokers/account-cache.ts +45 -0
- package/src/brokers/signed-in/adapter.ts +33 -2
- package/src/brokers/signed-in/client.ts +10 -0
- package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
- package/src/brokers/signed-in/sign-in.ts +62 -5
- package/src/capabilities/registry.ts +8 -1
- package/src/capabilities/types.ts +2 -0
- package/src/cli/command-options.ts +74 -0
- package/src/cli/commands/command-utils.ts +29 -0
- package/src/cli/commands/crypto-hints.ts +12 -4
- package/src/cli/commands/market.ts +524 -148
- package/src/cli/commands/options-chain.ts +172 -0
- package/src/cli/commands/options-leaps.ts +422 -0
- package/src/cli/commands/overview.ts +223 -42
- package/src/cli/commands/search.ts +29 -8
- package/src/cli/commands/system.ts +99 -19
- package/src/cli/commands/themes.ts +64 -0
- package/src/cli/commands/ticker.ts +200 -73
- package/src/cli/desktop-pane-shot.ts +100 -16
- package/src/cli/errors.ts +25 -2
- package/src/cli/freshness.ts +7 -4
- package/src/cli/fx-pair.ts +57 -0
- package/src/cli/help.ts +40 -7
- package/src/cli/history-rows.ts +187 -15
- package/src/cli/index-roots.ts +51 -0
- package/src/cli/index.ts +21 -13
- package/src/cli/listing-arg.ts +211 -3
- package/src/cli/not-a-ticker.ts +37 -0
- package/src/cli/options.ts +55 -7
- package/src/cli/pane-functions/capabilities.ts +13 -2
- package/src/cli/pane-functions/catalog.ts +13 -2
- package/src/cli/pane-functions/data.ts +36 -5
- package/src/cli/pane-functions/dom.ts +89 -28
- package/src/cli/pane-functions/freshness.ts +205 -26
- package/src/cli/pane-functions/function-help.ts +71 -24
- package/src/cli/pane-functions/headless.ts +168 -35
- package/src/cli/pane-functions/index.ts +12 -2
- package/src/cli/pane-functions/market-session.ts +133 -0
- package/src/cli/pane-functions/options.ts +15 -4
- package/src/cli/pane-functions/report-notices.ts +63 -0
- package/src/cli/pane-functions/report.ts +12 -1
- package/src/cli/pane-functions/resolver.ts +32 -8
- package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
- package/src/cli/pane-functions/screenshot.ts +109 -23
- package/src/cli/registry.ts +8 -4
- package/src/cli/report-tables.ts +460 -0
- package/src/cli/result.ts +63 -16
- package/src/cli/row-window.ts +76 -0
- package/src/cli/scroll-truncation.ts +34 -0
- package/src/components/chart/composite/composite-chart.tsx +43 -4
- package/src/components/chart/composite/legend.tsx +39 -6
- package/src/components/chart/composite/rasterizer.ts +64 -4
- package/src/components/chart/composite/series-cues.ts +71 -0
- package/src/components/chart/composite/text-renderer.ts +16 -8
- package/src/components/chart/composite/types.ts +3 -0
- package/src/components/command-bar/commands/direct/index.ts +9 -1
- package/src/components/command-bar/commands/plugin/items.ts +3 -1
- package/src/components/command-bar/commands/registry.ts +7 -0
- package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
- package/src/components/command-bar/pane-templates/workflow.ts +5 -0
- package/src/components/command-bar/routes/root/command-items.ts +2 -0
- package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
- package/src/components/command-bar/routes/root/results.ts +40 -10
- package/src/components/command-bar/routes/root/runtime.ts +8 -0
- package/src/components/command-bar/routes/root/search-providers.ts +97 -14
- package/src/components/command-bar/surface/index.tsx +18 -4
- package/src/components/command-bar/theme-picker.tsx +48 -13
- package/src/components/command-bar/workflow/ops.ts +5 -3
- package/src/components/data-table/export.ts +6 -51
- package/src/components/data-table/view.tsx +31 -10
- package/src/components/layout/floating-pane.tsx +6 -2
- package/src/components/layout/market-summary.ts +23 -9
- package/src/components/layout/pane/content.tsx +46 -29
- package/src/components/layout/pane/cycle-order.ts +2 -11
- package/src/components/layout/pane/header.tsx +2 -1
- package/src/components/layout/pane/title.ts +1 -1
- package/src/components/layout/presentation.ts +22 -0
- package/src/components/layout/shell/active-drag.ts +67 -35
- package/src/components/layout/shell/cursor-occlusion.ts +6 -5
- package/src/components/layout/shell/drag/index.ts +16 -7
- package/src/components/layout/shell/drag/live.ts +193 -0
- package/src/components/layout/shell/drag/overlays.tsx +179 -35
- package/src/components/layout/shell/drag/runtime.ts +62 -50
- package/src/components/layout/shell/drag/slide.ts +10 -0
- package/src/components/layout/shell/index.tsx +84 -57
- package/src/components/layout/shell/layout-state.ts +5 -11
- package/src/components/layout/shell/menu.ts +28 -1
- package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
- package/src/components/layout/shell/native/surfaces.ts +30 -14
- package/src/components/layout/shell/pane/actions.ts +2 -1
- package/src/components/layout/shell/pane/layers.tsx +461 -180
- package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
- package/src/components/layout/shell/pane/new-layout.ts +116 -0
- package/src/components/layout/shell/shortcuts.ts +5 -2
- package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
- package/src/components/layout/shell/visible-layout.ts +44 -1
- package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-text.tsx +9 -5
- package/src/components/metric-treemap/heat-surface.tsx +27 -8
- package/src/components/metric-treemap/heat-tiles.ts +48 -17
- package/src/components/onboarding/onboarding-frame.tsx +33 -10
- package/src/components/onboarding/onboarding-wizard.tsx +49 -40
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
- package/src/components/onboarding/pro-step-layout.ts +86 -0
- package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
- package/src/components/ui/button.tsx +10 -0
- package/src/components/ui/data-table/index.tsx +38 -0
- package/src/components/ui/data-table/types.ts +8 -1
- package/src/components/ui/data-table/visible-range.ts +2 -0
- package/src/components/ui/icon.tsx +11 -2
- package/src/components/ui/index.ts +1 -1
- package/src/core/connection-health.ts +1 -1
- package/src/core/state/app/layout-reducer.ts +89 -3
- package/src/core/state/app/state.ts +8 -5
- package/src/core/state/app/types.ts +13 -2
- package/src/data/config/layout.ts +55 -21
- package/src/data/config/store/fold-panes.ts +149 -0
- package/src/data/config/store/migrations.ts +7 -0
- package/src/data/config/store/node.ts +27 -6
- package/src/data/config/store/normalize.ts +37 -3
- package/src/data/json-persistence.ts +17 -3
- package/src/data/json-storage.ts +2 -0
- package/src/data/memory-resource-store.ts +46 -0
- package/src/i18n/es.ts +14 -1
- package/src/i18n/ja.ts +14 -1
- package/src/i18n/ko.ts +14 -1
- package/src/i18n/zh-cn.ts +14 -1
- package/src/i18n/zh-tw.ts +14 -1
- package/src/layout/desks.ts +1 -2
- package/src/layout/pane-layout-move.ts +264 -0
- package/src/layout/pane-manager/hidden-panes.ts +160 -0
- package/src/layout/pane-manager/index.ts +1 -0
- package/src/market-data/coordinator/entries.ts +7 -1
- package/src/market-data/coordinator/quotes.ts +2 -2
- package/src/market-data/market/freshness.ts +156 -11
- package/src/market-data/market/status.ts +40 -0
- package/src/market-data/options-alternatives.ts +69 -0
- package/src/market-data/quotes/contributions.ts +19 -2
- package/src/market-data/quotes/freshness.ts +5 -0
- package/src/market-data/quotes/metadata.ts +16 -0
- package/src/market-data/quotes/screener-live-quotes.ts +56 -5
- package/src/plugins/builtin/account-management/keyboard.ts +3 -0
- package/src/plugins/builtin/account-management/model.ts +1 -0
- package/src/plugins/builtin/account-management/navigation.ts +1 -1
- package/src/plugins/builtin/account-management/pane.tsx +31 -4
- package/src/plugins/builtin/alerts/quotes.ts +3 -0
- package/src/plugins/builtin/analytics/pane-model.ts +10 -6
- package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
- package/src/plugins/builtin/analytics/risk-model.ts +37 -8
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
- package/src/plugins/builtin/attention/pane.tsx +1 -1
- package/src/plugins/builtin/backtest/pane.tsx +1 -1
- package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
- package/src/plugins/builtin/broker-manager/index.tsx +7 -3
- package/src/plugins/builtin/broker-manager/model.ts +8 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
- package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
- package/src/plugins/builtin/broker-manager/table.ts +5 -0
- package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
- package/src/plugins/builtin/catalysts/detail.tsx +1 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
- package/src/plugins/builtin/chat/channels.ts +34 -36
- package/src/plugins/builtin/chat/command-bar.ts +138 -0
- package/src/plugins/builtin/chat/composer-commands.ts +21 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
- package/src/plugins/builtin/chat/content/footer.tsx +5 -3
- package/src/plugins/builtin/chat/content/index.tsx +21 -1
- package/src/plugins/builtin/chat/content/mentions.ts +2 -1
- package/src/plugins/builtin/chat/content/user-map.ts +3 -1
- package/src/plugins/builtin/chat/controller/index.ts +48 -0
- package/src/plugins/builtin/chat/conversations.ts +256 -0
- package/src/plugins/builtin/chat/discord-command.ts +91 -0
- package/src/plugins/builtin/chat/edit-window.ts +12 -6
- package/src/plugins/builtin/chat/ghost-user.ts +14 -0
- package/src/plugins/builtin/chat/index.tsx +40 -2
- package/src/plugins/builtin/chat/layout.ts +2 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/message/header.tsx +24 -15
- package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/pane-requests.ts +60 -0
- package/src/plugins/builtin/chat/pane-state.ts +32 -2
- package/src/plugins/builtin/chat/pane.tsx +8 -0
- package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
- package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
- package/src/plugins/builtin/cloud/askg/model.ts +129 -70
- package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
- package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
- package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
- package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
- package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
- package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
- package/src/plugins/builtin/cloud/plugin.tsx +36 -1
- package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
- package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
- package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
- package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
- package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
- package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
- package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
- package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
- package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
- package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
- package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
- package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
- package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
- package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
- package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
- package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
- package/src/plugins/builtin/comment-letters/client.ts +1 -3
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/company-kpis/client.ts +23 -8
- package/src/plugins/builtin/company-kpis/headless.ts +3 -1
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
- package/src/plugins/builtin/composite-plugins.ts +107 -64
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
- package/src/plugins/builtin/credit-boards/index.tsx +21 -21
- package/src/plugins/builtin/credit-boards/model.ts +0 -1
- package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
- package/src/plugins/builtin/debt-maturities/client.ts +37 -9
- package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
- package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
- package/src/plugins/builtin/earnings-calls/data.ts +28 -7
- package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
- package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
- package/src/plugins/builtin/econ/calendar-model.ts +42 -17
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/index.tsx +68 -22
- package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
- package/src/plugins/builtin/econ-statistics/view.ts +17 -0
- package/src/plugins/builtin/executives/data.ts +17 -6
- package/src/plugins/builtin/executives/pane.tsx +16 -7
- package/src/plugins/builtin/filing-events/pane.tsx +22 -5
- package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
- package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
- package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
- package/src/plugins/builtin/gpu/headless.ts +9 -3
- package/src/plugins/builtin/gpu/model.ts +14 -5
- package/src/plugins/builtin/help/index.tsx +1 -0
- package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
- package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
- package/src/plugins/builtin/holders/client.ts +3 -6
- package/src/plugins/builtin/holders/format.ts +31 -1
- package/src/plugins/builtin/holders/headless.ts +32 -6
- package/src/plugins/builtin/holders/pane.tsx +25 -13
- package/src/plugins/builtin/holders/report-header.ts +46 -5
- package/src/plugins/builtin/insider/index.tsx +4 -4
- package/src/plugins/builtin/iv-history/charts.tsx +21 -7
- package/src/plugins/builtin/iv-history/client.ts +12 -2
- package/src/plugins/builtin/iv-history/format.ts +33 -2
- package/src/plugins/builtin/iv-history/headless.ts +22 -10
- package/src/plugins/builtin/iv-history/index.tsx +3 -3
- package/src/plugins/builtin/iv-history/model.ts +61 -13
- package/src/plugins/builtin/iv-history/pane.tsx +11 -3
- package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
- package/src/plugins/builtin/jobs/pane.tsx +1 -1
- package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
- package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +121 -45
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
- package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
- package/src/plugins/builtin/layout-manager/index.tsx +9 -1
- package/src/plugins/builtin/macro-day/client.ts +24 -0
- package/src/plugins/builtin/macro-day/headless.ts +11 -8
- package/src/plugins/builtin/macro-day/index.tsx +3 -0
- package/src/plugins/builtin/macro-day/model.ts +11 -6
- package/src/plugins/builtin/macro-day/pane.tsx +12 -8
- package/src/plugins/builtin/macro-day/release-table.ts +40 -0
- package/src/plugins/builtin/macro-day/releases.ts +62 -44
- package/src/plugins/builtin/market-heatmap/data.ts +25 -5
- package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
- package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
- package/src/plugins/builtin/market-heatmap/model.ts +94 -3
- package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
- package/src/plugins/builtin/market-movers/headless.ts +31 -4
- package/src/plugins/builtin/market-movers/index.tsx +8 -1
- package/src/plugins/builtin/market-movers/model.ts +71 -17
- package/src/plugins/builtin/market-movers/table.ts +50 -23
- package/src/plugins/builtin/members/pane.tsx +10 -0
- package/src/plugins/builtin/news/headless.ts +9 -2
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
- package/src/plugins/builtin/openfda/client.ts +1 -1
- package/src/plugins/builtin/options/carry.ts +95 -0
- package/src/plugins/builtin/options/index.tsx +58 -5
- package/src/plugins/builtin/options/strike-window.ts +183 -0
- package/src/plugins/builtin/options/table.ts +55 -3
- package/src/plugins/builtin/options/types.ts +8 -1
- package/src/plugins/builtin/options/view.tsx +161 -12
- package/src/plugins/builtin/options-positioning/headless.ts +4 -2
- package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
- package/src/plugins/builtin/options-scenario/client.ts +73 -17
- package/src/plugins/builtin/options-scenario/headless.ts +71 -24
- package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
- package/src/plugins/builtin/options-scenario/index.tsx +9 -2
- package/src/plugins/builtin/options-scenario/model.ts +33 -0
- package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
- package/src/plugins/builtin/pe-band/model.ts +2 -2
- package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
- package/src/plugins/builtin/perps/equity-row.tsx +8 -5
- package/src/plugins/builtin/perps/model.ts +16 -5
- package/src/plugins/builtin/perps/pane.tsx +1 -1
- package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
- package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
- package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
- package/src/plugins/builtin/plugin-module.ts +87 -4
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
- package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
- package/src/plugins/builtin/portfolio-list/cli/render.ts +89 -18
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
- package/src/plugins/builtin/portfolio-list/headless.ts +40 -7
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
- package/src/plugins/builtin/realized-vol/headless.ts +3 -0
- package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
- package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
- package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
- package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
- package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
- package/src/plugins/builtin/risk-factors/data.ts +23 -9
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
- package/src/plugins/builtin/seasonality/pane.tsx +1 -1
- package/src/plugins/builtin/sec/index.tsx +5 -5
- package/src/plugins/builtin/sectors/client.ts +10 -2
- package/src/plugins/builtin/sectors/headless.ts +35 -6
- package/src/plugins/builtin/sectors/index.tsx +18 -1
- package/src/plugins/builtin/sectors/sector-model.ts +150 -41
- package/src/plugins/builtin/shared/command-tab.ts +51 -0
- package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
- package/src/plugins/builtin/shared/members-menu.tsx +260 -0
- package/src/plugins/builtin/shared/report-freshness.ts +50 -2
- package/src/plugins/builtin/shared/ticker-request.ts +25 -1
- package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
- package/src/plugins/builtin/shared/volatility/options.ts +1 -46
- package/src/plugins/builtin/short-interest/index.tsx +19 -4
- package/src/plugins/builtin/short-interest/surface.tsx +4 -4
- package/src/plugins/builtin/short-volume/index.tsx +1 -10
- package/src/plugins/builtin/short-volume/pane.tsx +1 -1
- package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/model.ts +1 -1
- package/src/plugins/builtin/themes/pane.tsx +3 -0
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
- package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/headless.ts +63 -25
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
- package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
- package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
- package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
- package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
- package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
- package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
- package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
- package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
- package/src/plugins/builtin/vol-surface/client.ts +6 -2
- package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
- package/src/plugins/builtin/vol-surface/headless.ts +124 -20
- package/src/plugins/builtin/vol-surface/model.ts +23 -11
- package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
- package/src/plugins/builtin/vol-surface/tables.ts +124 -0
- package/src/plugins/builtin/world-indices/client.ts +22 -4
- package/src/plugins/builtin/world-indices/headless.ts +54 -27
- package/src/plugins/builtin/world-indices/model.ts +4 -2
- package/src/plugins/builtin/world-indices/table.tsx +13 -2
- package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
- package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
- package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
- package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
- package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -6
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
- package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
- package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
- package/src/plugins/builtin/world-venue-map/model.ts +16 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
- package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/builtin-editorial.ts +126 -0
- package/src/plugins/catalog-backend.ts +16 -9
- package/src/plugins/catalog-browser.ts +127 -63
- package/src/plugins/catalog-manifest.ts +73 -40
- package/src/plugins/catalog-ui.ts +26 -6
- package/src/plugins/extracted-plugins.ts +7 -1
- package/src/plugins/ownership.ts +150 -7
- package/src/plugins/plugin-functions.ts +54 -0
- package/src/plugins/plugin-off.ts +36 -0
- package/src/plugins/registry/host-actions.ts +16 -0
- package/src/plugins/registry/index.ts +84 -24
- package/src/plugins/registry/plugin-state.ts +25 -1
- package/src/plugins/runtime/context.tsx +29 -0
- package/src/react/input.ts +48 -1
- package/src/remote/app-host.tsx +5 -0
- package/src/remote/controller.ts +3 -53
- package/src/remote/matching.ts +69 -0
- package/src/remote/schema.ts +1 -1
- package/src/remote/semantic-tree.tsx +14 -0
- package/src/remote/types.ts +2 -0
- package/src/renderers/browser/app-services.ts +3 -1
- package/src/renderers/dom/data-table/dom.ts +31 -0
- package/src/renderers/dom/data-table/index.tsx +5 -0
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/query-bar.tsx +59 -32
- package/src/renderers/dom/host/box.tsx +113 -17
- package/src/renderers/dom/host/drag-shield.ts +35 -0
- package/src/renderers/dom/host/live-frame.ts +62 -0
- package/src/renderers/dom/host/mouse.ts +8 -1
- package/src/renderers/dom/host/scroll-box.tsx +6 -5
- package/src/renderers/dom/host/tabs.tsx +3 -2
- package/src/renderers/dom/input-host.tsx +6 -1
- package/src/renderers/dom/styles.css +26 -5
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +15 -1
- package/src/renderers/electrobun/shared/protocol.ts +12 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +4 -1
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
- package/src/renderers/electrobun/view/host/native.ts +0 -10
- package/src/renderers/electrobun/view/host/window-move.ts +28 -0
- package/src/renderers/electrobun/view/main.tsx +4 -1
- package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
- package/src/renderers/electrobun/view/ui-host.tsx +2 -5
- package/src/renderers/opentui/input-host.tsx +6 -1
- package/src/renderers/opentui/start.tsx +9 -0
- package/src/renderers/opentui/ui-host.tsx +7 -1
- package/src/shares/portable-layout.ts +16 -6
- package/src/sources/gloomberb-cloud/index.ts +19 -10
- package/src/sources/gloomberb-cloud/normalizers.ts +20 -21
- package/src/sources/listing-symbols.ts +11 -5
- package/src/sources/provider-errors.ts +88 -3
- package/src/sources/provider-router/batches.ts +3 -3
- package/src/sources/provider-router/cached-routes.ts +8 -4
- package/src/sources/provider-router/financial-routes.ts +13 -7
- package/src/sources/provider-router/financials.ts +106 -16
- package/src/sources/provider-router/history.ts +27 -11
- package/src/sources/provider-router/index.ts +1 -1
- package/src/sources/provider-router/primary.ts +24 -6
- package/src/sources/sec-edgar.ts +68 -8
- package/src/state/pane-scroll-registry.tsx +67 -35
- package/src/sync/core-contributors.ts +4 -3
- package/src/test-support/cli-context.ts +2 -1
- package/src/test-support/data-provider.ts +34 -0
- package/src/test-support/margin-account.ts +108 -0
- package/src/test-support/shell-pane-keys.tsx +1 -0
- package/src/theme/series-colors.ts +46 -0
- package/src/theme/themes.ts +149 -0
- package/src/tickers/search/index.ts +22 -2
- package/src/tickers/search/ranking.ts +45 -1
- package/src/tickers/search/types.ts +3 -1
- package/src/time-series/resolution.ts +15 -0
- package/src/time-series/resolve.ts +1 -14
- package/src/types/config.ts +61 -1
- package/src/types/financials.ts +32 -3
- package/src/types/headless.ts +11 -0
- package/src/types/instrument.ts +8 -0
- package/src/types/plugin.ts +49 -3
- package/src/ui/host.tsx +16 -0
- package/src/ui/index.tsx +10 -3
- package/src/ui/scroll-box-axis.ts +26 -0
- package/src/utils/cli-output.ts +14 -1
- package/src/utils/currency-units.ts +8 -3
- package/src/utils/depositary-receipt.ts +54 -10
- package/src/utils/display-number.ts +63 -0
- package/src/utils/exchanges.ts +13 -0
- package/src/utils/format.ts +2 -1
- package/src/utils/fundamentals.ts +17 -1
- package/src/utils/fx-market-hours.ts +34 -0
- package/src/utils/option-expiry.ts +126 -0
- package/src/utils/price-history-integrity.ts +19 -6
- package/src/utils/price-history.ts +39 -6
- package/src/utils/rpc-timeout-error.ts +6 -2
- package/src/utils/sec-eps-basis.ts +159 -30
- package/src/utils/sec.ts +38 -34
- package/src/utils/throttled-fetch.ts +51 -22
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
const PROVIDER_MISS_BRAND = Symbol("provider-miss");
|
|
2
2
|
const EXPECTED_PROVIDER_MISS = /No data found|symbol may be delisted|"code":"Not Found"|No history for /i;
|
|
3
3
|
|
|
4
|
+
const MAX_REASON_LENGTH = 200;
|
|
5
|
+
// Whole ANSI sequences go first, so their printable tail does not outlive the escape byte.
|
|
6
|
+
const ANSI_SEQUENCE = /\u001b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\u0007\u001b]*(?:\u0007|\u001b\\)?)/g;
|
|
7
|
+
const LINE_BREAKS = /[\r\n\t\v\f\u0085\u2028\u2029]+/g;
|
|
8
|
+
// Remaining controls, and the bidirectional overrides that can reorder text in a terminal.
|
|
9
|
+
const CONTROL_CHARACTERS = /[\p{Cc}\u200e\u200f\u202a-\u202e\u2066-\u2069]/gu;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A reason a data service wrote for an empty answer, made safe to show: one
|
|
13
|
+
* line, no control characters, at most 200 characters. Anything that is not a
|
|
14
|
+
* non-empty string has no reason.
|
|
15
|
+
*/
|
|
16
|
+
export function cleanProviderReason(value: unknown): string | undefined {
|
|
17
|
+
if (typeof value !== "string") return undefined;
|
|
18
|
+
const line = value.replace(ANSI_SEQUENCE, "").replace(LINE_BREAKS, " ").replace(CONTROL_CHARACTERS, "").replace(/\s+/g, " ").trim();
|
|
19
|
+
if (!line) return undefined;
|
|
20
|
+
const characters = Array.from(line);
|
|
21
|
+
return characters.length > MAX_REASON_LENGTH
|
|
22
|
+
? `${characters.slice(0, MAX_REASON_LENGTH - 1).join("").trimEnd()}…`
|
|
23
|
+
: line;
|
|
24
|
+
}
|
|
25
|
+
|
|
4
26
|
export class ProviderMissError extends Error {
|
|
5
27
|
readonly [PROVIDER_MISS_BRAND] = true;
|
|
28
|
+
/** Why the service has nothing, in its own words, when it said. */
|
|
29
|
+
readonly reason?: string;
|
|
30
|
+
/** The service found no listing for the symbol: it is not a ticker, which differs from a ticker it has no data for. */
|
|
31
|
+
readonly notFound: boolean;
|
|
6
32
|
|
|
7
|
-
constructor(message = "Provider could not satisfy request") {
|
|
33
|
+
constructor(message = "Provider could not satisfy request", reason?: unknown, options: { notFound?: boolean } = {}) {
|
|
8
34
|
super(message);
|
|
9
35
|
this.name = "ProviderMissError";
|
|
36
|
+
this.reason = cleanProviderReason(reason);
|
|
37
|
+
this.notFound = options.notFound === true;
|
|
10
38
|
}
|
|
11
39
|
}
|
|
12
40
|
|
|
13
|
-
export function createProviderMiss(message?: string): ProviderMissError {
|
|
14
|
-
return new ProviderMissError(message);
|
|
41
|
+
export function createProviderMiss(message?: string, reason?: unknown, options?: { notFound?: boolean }): ProviderMissError {
|
|
42
|
+
return new ProviderMissError(message, reason, options);
|
|
15
43
|
}
|
|
16
44
|
|
|
17
45
|
function isProviderMiss(error: unknown): error is ProviderMissError {
|
|
@@ -19,6 +47,63 @@ function isProviderMiss(error: unknown): error is ProviderMissError {
|
|
|
19
47
|
|| (!!error && typeof error === "object" && PROVIDER_MISS_BRAND in error);
|
|
20
48
|
}
|
|
21
49
|
|
|
50
|
+
/** The reason a provider gave for a miss, or undefined when it gave none. */
|
|
51
|
+
export function providerMissReason(error: unknown): string | undefined {
|
|
52
|
+
return isProviderMiss(error) ? error.reason : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Collects the latest reason across the providers of one request. */
|
|
56
|
+
export interface ProviderMissNote {
|
|
57
|
+
reason?: string;
|
|
58
|
+
/** True while every provider that failed said it found no listing; one that failed another way (a timeout) clears it. */
|
|
59
|
+
notFound?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function noteProviderMiss(note: ProviderMissNote | undefined, error: unknown): void {
|
|
63
|
+
if (!note) return;
|
|
64
|
+
const reason = providerMissReason(error);
|
|
65
|
+
if (reason) note.reason = reason;
|
|
66
|
+
note.notFound = note.notFound !== false && isProviderMiss(error) && error.notFound === true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A provider answered with data the router could not use: whatever else is missing, the symbol exists. */
|
|
70
|
+
export function noteProviderAnswer(note: ProviderMissNote | undefined): void {
|
|
71
|
+
if (note) note.notFound = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const NOT_A_TICKER = /^Not a ticker: (.+)\.$/;
|
|
75
|
+
|
|
76
|
+
/** "Not a ticker: APPLE.", what a request for a symbol no listing carries ends in. */
|
|
77
|
+
function notATickerMessage(symbol: string): string {
|
|
78
|
+
return `Not a ticker: ${symbol}.`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The symbol of a "Not a ticker: APPLE." message, or null for any other text. */
|
|
82
|
+
export function notATickerSymbol(message: string): string | null {
|
|
83
|
+
return NOT_A_TICKER.exec(message.split("\n")[0] ?? "")?.[1] ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether a message is the router's answer that no source serves a symbol: "No history provider
|
|
88
|
+
* available for 2222", or "Not a ticker: 2222." when the service found no listing for it.
|
|
89
|
+
* A timeout or any other failure is not.
|
|
90
|
+
*/
|
|
91
|
+
export function isNoProviderMessage(message: string): boolean {
|
|
92
|
+
return /\bprovider available for\b/i.test(message) || notATickerSymbol(message) !== null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The router's final error when no provider answered. A reason a provider gave
|
|
97
|
+
* replaces the generic wording, and so does the service's answer that no
|
|
98
|
+
* listing exists for `symbol` ("Not a ticker"); a timeout or any other failure
|
|
99
|
+
* keeps the generic message.
|
|
100
|
+
*/
|
|
101
|
+
export function noProviderError(message: string, note: ProviderMissNote | undefined, symbol?: string): Error {
|
|
102
|
+
if (note?.reason) return createProviderMiss(note.reason, note.reason);
|
|
103
|
+
if (note?.notFound === true && symbol) return createProviderMiss(notATickerMessage(symbol), undefined, { notFound: true });
|
|
104
|
+
return new Error(message);
|
|
105
|
+
}
|
|
106
|
+
|
|
22
107
|
export function shouldLogProviderError(error: unknown): boolean {
|
|
23
108
|
if (isProviderMiss(error)) return false;
|
|
24
109
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -66,7 +66,7 @@ export class ProviderRouterBatchRoutes {
|
|
|
66
66
|
const rawCached = selectCachedResource<Quote>(this.deps.resources, "quote", entityKey, variantKeys, sourceKeys, false);
|
|
67
67
|
const cached = rawCached && (brokerSourceKeys.includes(rawCached.sourceKey)
|
|
68
68
|
? !isQuoteStaleForCurrentSession(quoteWithFreshnessExchange(rawCached.value, target.exchange))
|
|
69
|
-
: isProviderQuoteUsableForCurrentSession(rawCached.value, target.exchange, target.symbol))
|
|
69
|
+
: isProviderQuoteUsableForCurrentSession(rawCached.value, target.exchange, target.symbol, { recentAnswer: !rawCached.stale }))
|
|
70
70
|
? rawCached
|
|
71
71
|
: null;
|
|
72
72
|
if (cached && !forceRefresh && !cached.stale) {
|
|
@@ -93,7 +93,7 @@ export class ProviderRouterBatchRoutes {
|
|
|
93
93
|
const uniqueTargets = [...providerIndexes.values()].map((bucket) => bucket[0]!.target);
|
|
94
94
|
const batchResults = await batchProvider.getQuotesBatch!(uniqueTargets, options).catch(() => []);
|
|
95
95
|
for (const item of batchResults) {
|
|
96
|
-
if (!isProviderQuoteUsableForCurrentSession(item.quote, item.target.exchange, item.target.symbol)) continue;
|
|
96
|
+
if (!isProviderQuoteUsableForCurrentSession(item.quote, item.target.exchange, item.target.symbol, { recentAnswer: true })) continue;
|
|
97
97
|
const key = this.quoteBatchKey(item.target);
|
|
98
98
|
const sourceKey = this.deps.providerSourceKey(batchProvider);
|
|
99
99
|
for (const entry of providerIndexes.get(key) ?? []) {
|
|
@@ -162,7 +162,7 @@ export class ProviderRouterBatchRoutes {
|
|
|
162
162
|
const key = this.cachedFinancialsBatchKey(item.target);
|
|
163
163
|
let value = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(item.financials));
|
|
164
164
|
if (value && !item.target.instrument && !providerFinancialsMatchTarget(value, item.target.symbol, item.target.exchange)) continue;
|
|
165
|
-
if (value) value = dropUnusableProviderQuote(value, item.target.exchange);
|
|
165
|
+
if (value) value = dropUnusableProviderQuote(value, item.target.exchange, { recentAnswer: true });
|
|
166
166
|
if (!value) continue;
|
|
167
167
|
const sourceKey = this.deps.providerSourceKey(batchProvider);
|
|
168
168
|
for (const entry of providerIndexes.get(key) ?? []) {
|
|
@@ -4,7 +4,7 @@ import type { CachedAssetArgs, CachedAssetMethod, CachedAssetValue, DataProvider
|
|
|
4
4
|
import type { AnalystResearchData, CorporateActionsData, HolderData } from "../../types/financials";
|
|
5
5
|
import { canonicalExchange } from "../../utils/exchanges";
|
|
6
6
|
import { nonUsSecListingVenue } from "../../utils/sec";
|
|
7
|
-
import { shouldLogProviderError } from "../provider-errors";
|
|
7
|
+
import { noProviderError, noteProviderMiss, shouldLogProviderError, type ProviderMissNote } from "../provider-errors";
|
|
8
8
|
import { hasBrokerContext, withBrokerTimeout } from "./brokers";
|
|
9
9
|
import { publicListingExchange } from "../listing-target";
|
|
10
10
|
import { buildVariantKey, compactUrl, listCachedResources, resolveCachePolicy, type ProviderRouterCachePolicyKey } from "./cache";
|
|
@@ -18,6 +18,8 @@ interface Route {
|
|
|
18
18
|
variants: string[];
|
|
19
19
|
request: (provider: DataProvider, force: boolean) => Promise<unknown> | undefined;
|
|
20
20
|
error: string;
|
|
21
|
+
/** The symbol the request is for, so an answer that no listing exists can name it. */
|
|
22
|
+
symbol?: string;
|
|
21
23
|
rank?: (value: any, fetchedAt?: number) => number;
|
|
22
24
|
acceptCached?: (value: any) => boolean;
|
|
23
25
|
decode?: (value: any) => unknown;
|
|
@@ -111,6 +113,7 @@ export class ProviderRouterCachedRoutes {
|
|
|
111
113
|
let best: CachedValue<unknown> | null = null;
|
|
112
114
|
let bestRank = -1;
|
|
113
115
|
let lastError: unknown;
|
|
116
|
+
const misses: ProviderMissNote = {};
|
|
114
117
|
for (const provider of providers) {
|
|
115
118
|
try {
|
|
116
119
|
const request = route.request(provider, force);
|
|
@@ -123,6 +126,7 @@ export class ProviderRouterCachedRoutes {
|
|
|
123
126
|
if (rank > bestRank) { best = result; bestRank = rank; }
|
|
124
127
|
} catch (error) {
|
|
125
128
|
lastError = error;
|
|
129
|
+
noteProviderMiss(misses, error);
|
|
126
130
|
if (shouldLogProviderError(error)) this.deps.logProviderError(`${provider.id} failed: ${error}`);
|
|
127
131
|
}
|
|
128
132
|
}
|
|
@@ -133,7 +137,7 @@ export class ProviderRouterCachedRoutes {
|
|
|
133
137
|
const policy = resolveCachePolicy(undefined, route.policy);
|
|
134
138
|
return { value: route.empty(), fetchedAt, staleAt: fetchedAt + policy.staleMs, expiresAt: fetchedAt + policy.expireMs, source: "" };
|
|
135
139
|
}
|
|
136
|
-
throw
|
|
140
|
+
throw noProviderError(route.error, misses, route.symbol);
|
|
137
141
|
},
|
|
138
142
|
});
|
|
139
143
|
this.queries.set(key, query);
|
|
@@ -175,8 +179,8 @@ export class ProviderRouterCachedRoutes {
|
|
|
175
179
|
&& !hasBrokerContext(instrumentContext) && !instrumentContext?.instrument) {
|
|
176
180
|
exchange = publicListingExchange(ticker, exchange);
|
|
177
181
|
}
|
|
178
|
-
const tickerRoute = (kind: string, policy: ProviderRouterCachePolicyKey): Pick<Route, "kind" | "policy" | "entityKey" | "variants"> => ({
|
|
179
|
-
kind, policy, entityKey: this.deps.getEntityKey(ticker, instrumentContext?.instrument),
|
|
182
|
+
const tickerRoute = (kind: string, policy: ProviderRouterCachePolicyKey): Pick<Route, "kind" | "policy" | "entityKey" | "variants" | "symbol"> => ({
|
|
183
|
+
kind, policy, symbol: ticker, entityKey: this.deps.getEntityKey(ticker, instrumentContext?.instrument),
|
|
180
184
|
variants: this.deps.getTickerVariantCandidates(exchange),
|
|
181
185
|
});
|
|
182
186
|
switch (method) {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
selectCachedResource,
|
|
16
16
|
sortCachedRecords,
|
|
17
17
|
} from "./cache";
|
|
18
|
+
import { noProviderError, type ProviderMissNote } from "../provider-errors";
|
|
18
19
|
import { withBrokerTimeout } from "./brokers";
|
|
19
20
|
import {
|
|
20
21
|
hasMeaningfulProfile,
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
type CachedFinancialsReadOptions,
|
|
32
33
|
type CachedFinancialsSelection,
|
|
33
34
|
} from "./financials";
|
|
34
|
-
import type { ProviderRouterPrimaryRoutes } from "./primary";
|
|
35
|
+
import type { ProviderQuoteMissNote, ProviderRouterPrimaryRoutes } from "./primary";
|
|
35
36
|
import type { ProviderRouterCoreDeps, SourceResult } from "./route-types";
|
|
36
37
|
|
|
37
38
|
export interface ProviderRouterFinancialRouteDeps extends Pick<
|
|
@@ -178,13 +179,14 @@ export class ProviderRouterFinancialRoutes {
|
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
const brokerResult = await withBrokerTimeout(this.deps.primaryRoutes.fetchBrokerFinancials(ticker, exchange, context));
|
|
181
|
-
const
|
|
182
|
+
const misses: ProviderMissNote = {};
|
|
183
|
+
const fallback = await this.deps.primaryRoutes.fetchProviderFinancials(ticker, exchange, context, misses);
|
|
182
184
|
const merged = mergeFinancials(brokerResult?.value ?? null, fallback?.value ?? null);
|
|
183
185
|
if (isOptionTicker && !merged?.quote) {
|
|
184
186
|
return quoteOnlyFinancials(merged);
|
|
185
187
|
}
|
|
186
188
|
if (!merged) {
|
|
187
|
-
throw
|
|
189
|
+
throw noProviderError(`No provider available for ${ticker}`, misses, ticker);
|
|
188
190
|
}
|
|
189
191
|
return merged;
|
|
190
192
|
}
|
|
@@ -200,7 +202,7 @@ export class ProviderRouterFinancialRoutes {
|
|
|
200
202
|
const rawCached = selectCachedResource<Quote>(this.deps.resources, "quote", entityKey, variantKeys, sourceKeys, false);
|
|
201
203
|
const cachedIsStale = rawCached && (brokerSourceKeys.includes(rawCached.sourceKey)
|
|
202
204
|
? isQuoteContributionStaleForCurrentSession(quoteWithFreshnessExchange(rawCached.value, exchange))
|
|
203
|
-
: !isProviderQuoteUsableForCurrentSession(rawCached.value, exchange, ticker));
|
|
205
|
+
: !isProviderQuoteUsableForCurrentSession(rawCached.value, exchange, ticker, { recentAnswer: !rawCached.stale }));
|
|
204
206
|
const cached = rawCached && !cachedIsStale
|
|
205
207
|
? rawCached
|
|
206
208
|
: null;
|
|
@@ -221,12 +223,16 @@ export class ProviderRouterFinancialRoutes {
|
|
|
221
223
|
);
|
|
222
224
|
}
|
|
223
225
|
|
|
224
|
-
const
|
|
226
|
+
const misses: ProviderQuoteMissNote = {};
|
|
227
|
+
const providerQuote = await this.deps.primaryRoutes.fetchProviderQuote(ticker, exchange, context, misses);
|
|
225
228
|
if (providerQuote) {
|
|
226
229
|
return providerQuote.value;
|
|
227
230
|
}
|
|
228
231
|
if (cached) return cached.value;
|
|
229
|
-
|
|
232
|
+
// No source has a current quote: the last price one answered with, flagged
|
|
233
|
+
// stale and dated by its own time, rather than none.
|
|
234
|
+
if (misses.lastKnownQuote) return misses.lastKnownQuote;
|
|
235
|
+
throw noProviderError(`No quote provider available for ${ticker}`, misses, ticker);
|
|
230
236
|
}
|
|
231
237
|
|
|
232
238
|
private async getProviderReferenceQuote(
|
|
@@ -259,7 +265,7 @@ export class ProviderRouterFinancialRoutes {
|
|
|
259
265
|
if (
|
|
260
266
|
record
|
|
261
267
|
&& (includeStale || !record.stale)
|
|
262
|
-
&& isProviderQuoteUsableForCurrentSession(record.value, exchange, ticker)
|
|
268
|
+
&& isProviderQuoteUsableForCurrentSession(record.value, exchange, ticker, { recentAnswer: !record.stale })
|
|
263
269
|
) {
|
|
264
270
|
return record.value;
|
|
265
271
|
}
|
|
@@ -4,10 +4,19 @@ import { hasLikelyQuoteUnitMismatch } from "../../utils/currency-units";
|
|
|
4
4
|
import { coalesceFinancialPeriodAliases, mergeFinancialStatementRows } from "../../utils/financial-statements";
|
|
5
5
|
import { normalizePriceHistory, normalizeTickerFinancialsPriceHistory } from "../../utils/price-history";
|
|
6
6
|
import { redactUnavailableFundamentals, RETRACTABLE_VALUATION_FIELDS } from "../../utils/fundamentals";
|
|
7
|
-
import { isExtendedHoursExchange, isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
|
|
7
|
+
import { hasValidQuoteObservationTime, isExtendedHoursExchange, isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
|
|
8
8
|
import { mergeQuoteMetadata, quoteMetadataFromQuote, quoteMetadataMatchesTarget } from "../../market-data/quotes/metadata";
|
|
9
|
-
import { parsePublicTickerKey } from "../../utils/exchanges";
|
|
10
|
-
import {
|
|
9
|
+
import { canonicalExchange, parsePublicTickerKey } from "../../utils/exchanges";
|
|
10
|
+
import {
|
|
11
|
+
activeUsMarketSession,
|
|
12
|
+
delayedFeedLagMs,
|
|
13
|
+
isMiddayBreakPrint,
|
|
14
|
+
isRegularSessionTime,
|
|
15
|
+
isUsExtendedHoursSessionPrint,
|
|
16
|
+
isUsPriorSessionPremarketQuote,
|
|
17
|
+
isUsRegularCloseQuoteInPostSession,
|
|
18
|
+
latestRegularSessionOpen,
|
|
19
|
+
} from "../../market-data/market/freshness";
|
|
11
20
|
import {
|
|
12
21
|
mergeQuoteContributionMaps,
|
|
13
22
|
isQuoteContributionStaleForCurrentSession,
|
|
@@ -122,7 +131,50 @@ function isQuoteInActiveSession(quote: Quote, now: number): boolean {
|
|
|
122
131
|
return false;
|
|
123
132
|
}
|
|
124
133
|
|
|
125
|
-
|
|
134
|
+
// An opening auction can print before the continuous open (Hong Kong's at 09:20).
|
|
135
|
+
const OPENING_AUCTION_MS = 15 * 60_000;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Whether Gloom Cloud vouches that a delayed quote is current, however long
|
|
139
|
+
* ago its last trade was. The service judges more than that trade's age: it
|
|
140
|
+
* knows when it asked for the quote and how often the listing trades, so a
|
|
141
|
+
* thinly traded listing's hour-old trade can be the current price while its
|
|
142
|
+
* venue trades. Its verdict stands in for the age bound when:
|
|
143
|
+
* - the service answered on this request, or a cache entry still inside its
|
|
144
|
+
* TTL holds that answer (`recentAnswer`); a copy past the TTL is not vouched for;
|
|
145
|
+
* - the answer says `stale: false`. One without the flag vouches for nothing,
|
|
146
|
+
* and one flagged stale never gets this far;
|
|
147
|
+
* - the quote is delayed. The service bounds a delayed quote's feed age itself
|
|
148
|
+
* but not a real-time one's, so real-time quotes keep the age bound;
|
|
149
|
+
* - it reports the regular session, the venue's session is in progress as
|
|
150
|
+
* the delayed feed shows it, and the trade belongs to that session; a
|
|
151
|
+
* currency pair's session is the service's to judge; or
|
|
152
|
+
* - it reports a US extended-hours session and the trade is no older than
|
|
153
|
+
* the regular session that session extends: after the close, today's
|
|
154
|
+
* regular session or its after-hours; before the open, the previous
|
|
155
|
+
* regular session or anything since. Whether the quote carries the
|
|
156
|
+
* session's price is the freshness rule's call, made before this one.
|
|
157
|
+
* The answer says nothing about how often a listing trades, so a liquid
|
|
158
|
+
* listing the service wrongly vouches for is accepted too. The service's own
|
|
159
|
+
* bound for liquid listings, about twenty minutes, is tighter than this one.
|
|
160
|
+
*/
|
|
161
|
+
function isVouchedCurrentByService(quote: Quote, exchange: string | undefined, now: number): boolean {
|
|
162
|
+
if (quote.providerId !== "gloomberb-cloud" || quote.stale !== false || quote.dataSource !== "delayed") return false;
|
|
163
|
+
if (quote.marketState === "PRE" || quote.marketState === "POST") {
|
|
164
|
+
return isUsExtendedHoursSessionPrint(quote.lastUpdated, exchange, quote.marketState, now);
|
|
165
|
+
}
|
|
166
|
+
if (quote.marketState !== "REGULAR") return false;
|
|
167
|
+
// A currency pair trades around the clock through the week, and the service
|
|
168
|
+
// measures its silence against how often that pair prints: a pegged or
|
|
169
|
+
// exotic pair such as SAR=X goes hours without a print while its feed is fine.
|
|
170
|
+
if (canonicalExchange(exchange) === "CCY") return true;
|
|
171
|
+
const feedTime = now - delayedFeedLagMs(exchange);
|
|
172
|
+
if (isRegularSessionTime(exchange, feedTime) !== true) return false;
|
|
173
|
+
const open = latestRegularSessionOpen(exchange, feedTime);
|
|
174
|
+
return open != null && quote.lastUpdated >= open - OPENING_AUCTION_MS;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isActiveProviderQuoteTooOld(quote: Quote, now = Date.now(), recentAnswer = false): boolean {
|
|
126
178
|
// The shared freshness predicate already validates and dates a daily NAV.
|
|
127
179
|
if (quote.priceObservation === "nav") return false;
|
|
128
180
|
if (!isQuoteInActiveSession(quote, now)) return false;
|
|
@@ -132,12 +184,17 @@ function isActiveProviderQuoteTooOld(quote: Quote, now = Date.now()): boolean {
|
|
|
132
184
|
if (isUsPriorSessionPremarketQuote(quote.lastUpdated, exchange, quote.marketState, now)) {
|
|
133
185
|
return false;
|
|
134
186
|
}
|
|
187
|
+
// Likewise today's regular close before any after-hours trade.
|
|
188
|
+
if (quote.postMarketPrice == null && isUsRegularCloseQuoteInPostSession(quote.lastUpdated, exchange, quote.marketState, now)) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
135
191
|
if (!Number.isFinite(quote.lastUpdated)) return false;
|
|
136
192
|
const delayed = quote.dataSource === "delayed";
|
|
137
193
|
const maxAge = delayed ? ACTIVE_DELAYED_PROVIDER_QUOTE_MAX_AGE_MS : ACTIVE_PROVIDER_QUOTE_MAX_AGE_MS;
|
|
138
194
|
if (now - quote.lastUpdated <= maxAge) return false;
|
|
139
195
|
// While the venue pauses at midday, the morning's last print is the current price.
|
|
140
|
-
|
|
196
|
+
if (isMiddayBreakPrint(quote.lastUpdated, exchange, now, delayed ? delayedFeedLagMs(exchange) : 0)) return false;
|
|
197
|
+
return !(recentAnswer && isVouchedCurrentByService(quote, exchange, now));
|
|
141
198
|
}
|
|
142
199
|
|
|
143
200
|
export function providerQuoteMatchesTarget(quote: Quote | null | undefined, symbol?: string, exchange?: string): quote is Quote {
|
|
@@ -157,26 +214,55 @@ export function providerQuoteMatchesTarget(quote: Quote | null | undefined, symb
|
|
|
157
214
|
return true;
|
|
158
215
|
}
|
|
159
216
|
|
|
160
|
-
|
|
217
|
+
/**
|
|
218
|
+
* `recentAnswer`: the source answered with this quote on this request, or a
|
|
219
|
+
* cache entry still inside its TTL holds it. Only then can the source's own
|
|
220
|
+
* verdict on a delayed quote stand in for the age bound.
|
|
221
|
+
*/
|
|
222
|
+
export function isProviderQuoteUsableForCurrentSession(
|
|
223
|
+
quote: Quote | null | undefined,
|
|
224
|
+
exchange?: string,
|
|
225
|
+
symbol?: string,
|
|
226
|
+
options: { recentAnswer?: boolean } = {},
|
|
227
|
+
): quote is Quote {
|
|
161
228
|
if (!providerQuoteMatchesTarget(quote, symbol, exchange)) return false;
|
|
162
229
|
const normalized = quoteWithFreshnessExchange(quote, exchange);
|
|
163
230
|
if (isQuoteStaleForCurrentSession(normalized)) return false;
|
|
164
|
-
if (isActiveProviderQuoteTooOld(normalized)) return false;
|
|
231
|
+
if (isActiveProviderQuoteTooOld(normalized, Date.now(), options.recentAnswer === true)) return false;
|
|
232
|
+
return hasQuotedPrice(normalized);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function hasQuotedPrice(quote: Quote): boolean {
|
|
165
236
|
// Futures may trade or settle at zero or negative prices. Require explicit
|
|
166
237
|
// source type metadata; an alias alone cannot establish the price domain.
|
|
167
|
-
const futures = ["FUT", "FUTURE", "FUTURES"].includes(
|
|
238
|
+
const futures = ["FUT", "FUTURE", "FUTURES"].includes(quote.instrumentType?.trim().toUpperCase() ?? "");
|
|
168
239
|
return [
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
240
|
+
quote.price,
|
|
241
|
+
quote.preMarketPrice,
|
|
242
|
+
quote.postMarketPrice,
|
|
243
|
+
quote.bid,
|
|
244
|
+
quote.ask,
|
|
245
|
+
quote.mark,
|
|
175
246
|
].some((value) => futures
|
|
176
247
|
? typeof value === "number" && Number.isFinite(value)
|
|
177
248
|
: finitePositiveNumber(value));
|
|
178
249
|
}
|
|
179
250
|
|
|
251
|
+
/**
|
|
252
|
+
* A source's answer for the target that is priced and dated but not current
|
|
253
|
+
* for its session, flagged stale: the last price there is, for when no source
|
|
254
|
+
* has a current one. It keeps its own observation time, which dates it
|
|
255
|
+
* wherever it is shown, and the flag keeps it out of anything that needs a
|
|
256
|
+
* current price. Null for an answer that names another listing, carries no
|
|
257
|
+
* price or no valid time.
|
|
258
|
+
*/
|
|
259
|
+
export function lastKnownProviderQuote(quote: Quote | null | undefined, exchange?: string, symbol?: string): Quote | null {
|
|
260
|
+
if (!providerQuoteMatchesTarget(quote, symbol, exchange)) return null;
|
|
261
|
+
const normalized = quoteWithFreshnessExchange(quote, exchange);
|
|
262
|
+
if (!hasValidQuoteObservationTime(normalized) || !hasQuotedPrice(normalized)) return null;
|
|
263
|
+
return { ...quote, stale: true };
|
|
264
|
+
}
|
|
265
|
+
|
|
180
266
|
export function providerFinancialsMatchTarget(value: TickerFinancials, symbol: string, exchange?: string): boolean {
|
|
181
267
|
if (value.quote && !providerQuoteMatchesTarget(value.quote, symbol, exchange)) return false;
|
|
182
268
|
if (Object.values(value.quoteContributions ?? {}).some((quote) => !providerQuoteMatchesTarget(quote, symbol, exchange))) return false;
|
|
@@ -188,8 +274,12 @@ export function providerFinancialsMatchTarget(value: TickerFinancials, symbol: s
|
|
|
188
274
|
return quoteMetadataMatchesTarget(metadata, hasListing ? symbol : parsePublicTickerKey(symbol).symbol, hasListing ? exchange : undefined);
|
|
189
275
|
}
|
|
190
276
|
|
|
191
|
-
export function dropUnusableProviderQuote(
|
|
192
|
-
|
|
277
|
+
export function dropUnusableProviderQuote(
|
|
278
|
+
value: TickerFinancials,
|
|
279
|
+
exchange?: string,
|
|
280
|
+
options: { recentAnswer?: boolean } = {},
|
|
281
|
+
): TickerFinancials {
|
|
282
|
+
if (!value.quote || isProviderQuoteUsableForCurrentSession(value.quote, exchange, undefined, options)) {
|
|
193
283
|
return value;
|
|
194
284
|
}
|
|
195
285
|
|
|
@@ -23,7 +23,7 @@ import { isRoundTheClockCoin } from "../../utils/crypto-pair";
|
|
|
23
23
|
import { zonedDateKey } from "../../utils/zoned-date-time";
|
|
24
24
|
import { resolvePriceHistoryCurrencyUnit } from "../../utils/currency-units";
|
|
25
25
|
import { calendarHistoryFetchState, calendarHistoryLastBarDate, dropLeadingPlaceholderBars, getPricePointTimestamp, hasUsablePriceHistory, preservePriceHistoryGaps, isPriceHistoryStaleForCurrentWindow, normalizePriceHistory, priceHistoryIntervalMs, type CalendarHistoryFetchState } from "../../utils/price-history";
|
|
26
|
-
import { shouldLogProviderError } from "../provider-errors";
|
|
26
|
+
import { noProviderError, noteProviderAnswer, noteProviderMiss, shouldLogProviderError, type ProviderMissNote } from "../provider-errors";
|
|
27
27
|
import { HistoryCoverageError } from "../history-coverage";
|
|
28
28
|
import {
|
|
29
29
|
buildVariantKey,
|
|
@@ -66,6 +66,8 @@ interface HistoryAttempts {
|
|
|
66
66
|
outcomes: Map<string, HistorySourceOutcome>;
|
|
67
67
|
candidates: Map<string, HistoryRecoveryCandidate>;
|
|
68
68
|
pending: Set<string>;
|
|
69
|
+
/** Whether every provider that failed said it found no listing for the symbol. */
|
|
70
|
+
misses: ProviderMissNote;
|
|
69
71
|
coverageError?: HistoryCoverageError;
|
|
70
72
|
retention?: HistoryRetention;
|
|
71
73
|
}
|
|
@@ -105,6 +107,7 @@ function candidateForRequest(request: HistoryRequestDescriptor, sourceKey: strin
|
|
|
105
107
|
|
|
106
108
|
function recordHistoryError(attempts: HistoryAttempts | undefined, request: HistoryRequestDescriptor, sourceKey: string, error: unknown): void {
|
|
107
109
|
if (!attempts) return;
|
|
110
|
+
noteProviderMiss(attempts.misses, error);
|
|
108
111
|
if (error instanceof InvalidHistoryResultError) {
|
|
109
112
|
recordHistoryOutcome(attempts, sourceKey, "malformed");
|
|
110
113
|
} else if (isHistoryRetentionError(error)) {
|
|
@@ -286,8 +289,13 @@ function calendarRecheckMarkerKey(request: HistoryRequestDescriptor) {
|
|
|
286
289
|
variantKey: request.identity.variantKey, sourceKey: "router" };
|
|
287
290
|
}
|
|
288
291
|
|
|
292
|
+
interface HistoryRouteDeps extends ProviderRouterCoreDeps {
|
|
293
|
+
/** False in a short-lived process that must await refreshes of stale copies. */
|
|
294
|
+
revalidatesInBackground?(): boolean;
|
|
295
|
+
}
|
|
296
|
+
|
|
289
297
|
export class ProviderRouterHistoryRoutes {
|
|
290
|
-
constructor(private readonly deps:
|
|
298
|
+
constructor(private readonly deps: HistoryRouteDeps) {}
|
|
291
299
|
private readonly historyRefreshInFlight = new Map<string, Promise<unknown>>();
|
|
292
300
|
private readonly calendarRecheckAt = new Map<string, number>();
|
|
293
301
|
|
|
@@ -557,8 +565,9 @@ export class ProviderRouterHistoryRoutes {
|
|
|
557
565
|
|| hasRangeBarSize(record.value.points, request.requestedRange));
|
|
558
566
|
// A background revalidation cannot correct bars from before a close in
|
|
559
567
|
// time: a one-shot CLI exits first, and this caller keeps the old bars.
|
|
560
|
-
//
|
|
561
|
-
//
|
|
568
|
+
// That holds for daily bars and for intraday bars that stop short of the
|
|
569
|
+
// close. Broader variants answer the same way. A current copy under
|
|
570
|
+
// another key (such as the refetch of this range) answers first. Next come copies
|
|
562
571
|
// fetched after the latest settled close that are behind, due a re-check
|
|
563
572
|
// or not, the one reaching furthest first. A copy fetched before that close
|
|
564
573
|
// answers only when no later copy exists: its latest bar may be the session
|
|
@@ -606,12 +615,16 @@ export class ProviderRouterHistoryRoutes {
|
|
|
606
615
|
if (cachedBeforeClose) this.markCalendarChecked(request, now);
|
|
607
616
|
const usableCached = hasUsablePriceHistory(cachedValue.points) && cached && !cached.expired && !cachedHistoryStale
|
|
608
617
|
&& !cachedBeforeClose;
|
|
609
|
-
|
|
618
|
+
// A copy behind or before the latest session is re-checked on the paced
|
|
619
|
+
// schedule above, not on its short TTL: the source is likely to answer
|
|
620
|
+
// the same. A short-lived process (the CLI) closes its store before a
|
|
621
|
+
// background refresh lands, so it refetches a copy past its TTL before
|
|
622
|
+
// answering and falls back to that copy.
|
|
623
|
+
const refreshDue = !!cached?.stale && servedState === "current";
|
|
624
|
+
const awaitRefresh = refreshDue && !(this.deps.revalidatesInBackground?.() ?? true);
|
|
625
|
+
if (usableCached && !forceRefresh && !awaitRefresh) {
|
|
610
626
|
const exactHit = request.exactCacheVariantKeys.includes(cached.variantKey);
|
|
611
|
-
|
|
612
|
-
// paced schedule above, not on its short TTL: the source is likely to
|
|
613
|
-
// answer the same.
|
|
614
|
-
if (cached.stale && servedState === "current") {
|
|
627
|
+
if (refreshDue) {
|
|
615
628
|
scheduleRouterRevalidation(this.historyRefreshInFlight, request.identity.revalidationKey, () => this.refreshHistory(request));
|
|
616
629
|
}
|
|
617
630
|
return exactHit || !request.requestedRange
|
|
@@ -623,7 +636,7 @@ export class ProviderRouterHistoryRoutes {
|
|
|
623
636
|
const onUnavailable = (sourceKey: string, value: PriceHistoryResult) => {
|
|
624
637
|
if (value.points.length && !hasUsablePriceHistory(value.points)) supersededCacheSources.add(sourceKey);
|
|
625
638
|
};
|
|
626
|
-
const attempts: HistoryAttempts = { outcomes: new Map(), candidates: new Map(), pending: new Set() };
|
|
639
|
+
const attempts: HistoryAttempts = { outcomes: new Map(), candidates: new Map(), pending: new Set(), misses: {} };
|
|
627
640
|
const brokerResult = await withBrokerTimeout(this.fetchBrokerHistory(request, brokerCandidates, onUnavailable, attempts));
|
|
628
641
|
for (const sourceKey of [...attempts.pending]) recordHistoryOutcome(attempts, sourceKey, "timeout");
|
|
629
642
|
if (brokerResult && hasUsablePriceHistory(brokerResult.value.points)) return withReportedGaps(brokerResult.value);
|
|
@@ -662,7 +675,9 @@ export class ProviderRouterHistoryRoutes {
|
|
|
662
675
|
// A source answered a coin's history but it stops short of the present: say so, not that nothing answered.
|
|
663
676
|
const behind = isRoundTheClockCoin(target.symbol, target.exchange || request.target.exchange)
|
|
664
677
|
&& [...attempts.outcomes.values()].some((outcome) => outcome.outcome === "stale");
|
|
665
|
-
throw new Error(
|
|
678
|
+
if (behind) throw new Error(`Latest history for ${target.symbol} is behind`);
|
|
679
|
+
// The service has no listing for the symbol: say it is not a ticker, not that no source answered.
|
|
680
|
+
throw noProviderError(request.missingProviderError, { notFound: attempts.misses.notFound }, request.target.symbol);
|
|
666
681
|
}
|
|
667
682
|
return withReportedGaps(providerResult?.value ?? { points: [], resolution: historyResolutionForInterval(request.interval) });
|
|
668
683
|
}
|
|
@@ -739,6 +754,7 @@ export class ProviderRouterHistoryRoutes {
|
|
|
739
754
|
attempts?.pending.add(sourceKey);
|
|
740
755
|
try {
|
|
741
756
|
const fetched = await request.fetchProvider(provider);
|
|
757
|
+
noteProviderAnswer(attempts?.misses);
|
|
742
758
|
if (fetched === null) { recordHistoryOutcome(attempts, sourceKey, "empty"); return null; }
|
|
743
759
|
if (!Array.isArray(fetched.points)) { recordHistoryOutcome(attempts, sourceKey, "malformed"); return null; }
|
|
744
760
|
const value = normalizeRequestResult({ ...fetched, sourceKey }, request);
|
|
@@ -109,7 +109,7 @@ export class AssetDataRouter implements DataProvider {
|
|
|
109
109
|
getQuote: (ticker, exchange, context) => this.financialRoutes.getQuote(ticker, exchange, context),
|
|
110
110
|
getTickerFinancials: (ticker, exchange, context) => this.financialRoutes.getTickerFinancials(ticker, exchange, context),
|
|
111
111
|
});
|
|
112
|
-
this.historyRoutes = new ProviderRouterHistoryRoutes(routeDeps);
|
|
112
|
+
this.historyRoutes = new ProviderRouterHistoryRoutes({ ...routeDeps, revalidatesInBackground: () => this.revalidateInBackground });
|
|
113
113
|
this.newsRoutes = new ProviderRouterNewsRoutes({
|
|
114
114
|
newsSourcesInPriorityOrder: () => this.newsSourcesInPriorityOrder(),
|
|
115
115
|
logProviderError: (message) => providerLog.error(message),
|
|
@@ -4,7 +4,7 @@ import type { MarketDataRequestContext } from "../../types/data-provider";
|
|
|
4
4
|
import type { Quote, TickerFinancials } from "../../types/financials";
|
|
5
5
|
import { normalizeTickerFinancialsPriceHistory } from "../../utils/price-history";
|
|
6
6
|
import { resolveTickerFinancialsQuoteState } from "../../market-data/quotes/resolution";
|
|
7
|
-
import { shouldLogProviderError } from "../provider-errors";
|
|
7
|
+
import { noteProviderAnswer, noteProviderMiss, shouldLogProviderError, type ProviderMissNote } from "../provider-errors";
|
|
8
8
|
import { quoteMetadataFromQuote } from "../../market-data/quotes/metadata";
|
|
9
9
|
import {
|
|
10
10
|
dropUnusableProviderQuote,
|
|
@@ -14,12 +14,19 @@ import {
|
|
|
14
14
|
needsFinancialProfile,
|
|
15
15
|
profileForSameListing,
|
|
16
16
|
isProviderQuoteUsableForCurrentSession,
|
|
17
|
+
lastKnownProviderQuote,
|
|
17
18
|
providerFinancialsMatchTarget,
|
|
18
19
|
mergeMissingStatementArrays,
|
|
19
20
|
mergeFinancials,
|
|
20
21
|
} from "./financials";
|
|
21
22
|
import type { ProviderRouterCoreDeps, SourceResult } from "./route-types";
|
|
22
23
|
|
|
24
|
+
/** What the providers of one quote request said when none had a current quote. */
|
|
25
|
+
export interface ProviderQuoteMissNote extends ProviderMissNote {
|
|
26
|
+
/** The newest priced answer for the target that was not current for its session, flagged stale. */
|
|
27
|
+
lastKnownQuote?: Quote;
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
export class ProviderRouterPrimaryRoutes {
|
|
24
31
|
constructor(private readonly options: ProviderRouterCoreDeps) {}
|
|
25
32
|
|
|
@@ -61,6 +68,7 @@ export class ProviderRouterPrimaryRoutes {
|
|
|
61
68
|
ticker: string,
|
|
62
69
|
exchange?: string,
|
|
63
70
|
context?: MarketDataRequestContext,
|
|
71
|
+
misses?: ProviderMissNote,
|
|
64
72
|
): Promise<SourceResult<TickerFinancials> | null> {
|
|
65
73
|
const entityKey = this.options.getEntityKey(ticker, context?.instrument);
|
|
66
74
|
const variantKey = financialHistoryVariants(this.options.getTickerVariantCandidates(exchange), context)[0] ?? "";
|
|
@@ -70,11 +78,11 @@ export class ProviderRouterPrimaryRoutes {
|
|
|
70
78
|
for (const provider of this.options.providersInPriorityOrder()) {
|
|
71
79
|
try {
|
|
72
80
|
const rawValue = await provider.getTickerFinancials(ticker, exchange, context);
|
|
73
|
-
if (rawValue && !context?.instrument && !providerFinancialsMatchTarget(rawValue, ticker, exchange)) continue;
|
|
81
|
+
if (rawValue && !context?.instrument && !providerFinancialsMatchTarget(rawValue, ticker, exchange)) { noteProviderAnswer(misses); continue; }
|
|
74
82
|
const resolvedValue = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(rawValue));
|
|
75
|
-
if (resolvedValue && !context?.instrument && !providerFinancialsMatchTarget(resolvedValue, ticker, exchange)) continue;
|
|
76
|
-
let value = resolvedValue ? dropUnusableProviderQuote(resolvedValue, exchange) : null;
|
|
77
|
-
if (!value) continue;
|
|
83
|
+
if (resolvedValue && !context?.instrument && !providerFinancialsMatchTarget(resolvedValue, ticker, exchange)) { noteProviderAnswer(misses); continue; }
|
|
84
|
+
let value = resolvedValue ? dropUnusableProviderQuote(resolvedValue, exchange, { recentAnswer: true }) : null;
|
|
85
|
+
if (!value) { noteProviderAnswer(misses); continue; }
|
|
78
86
|
const sourceKey = this.options.providerSourceKey(provider);
|
|
79
87
|
if (context?.statementHistory === "extended" && !hasReusableExtendedHistory(value)) {
|
|
80
88
|
const previous = selectCachedResource<TickerFinancials>(this.options.resources, "financials", entityKey, [variantKey], [sourceKey], true);
|
|
@@ -131,6 +139,7 @@ export class ProviderRouterPrimaryRoutes {
|
|
|
131
139
|
if (!needsFinancialProfile(primaryResult.value) && (context?.statementHistory === "extended" ? primaryResult.value.statementHistory?.status === "available" : hasDetailedStatementRows(primaryResult.value) && hasDeepStatementHistory(primaryResult.value))) return primaryResult;
|
|
132
140
|
}
|
|
133
141
|
} catch (error) {
|
|
142
|
+
noteProviderMiss(misses, error);
|
|
134
143
|
if (shouldLogProviderError(error)) {
|
|
135
144
|
this.options.logProviderError(`${provider.id} failed: ${error}`);
|
|
136
145
|
}
|
|
@@ -181,13 +190,21 @@ export class ProviderRouterPrimaryRoutes {
|
|
|
181
190
|
ticker: string,
|
|
182
191
|
exchange?: string,
|
|
183
192
|
context?: MarketDataRequestContext,
|
|
193
|
+
misses?: ProviderQuoteMissNote,
|
|
184
194
|
): Promise<SourceResult<Quote> | null> {
|
|
185
195
|
const entityKey = this.options.getEntityKey(ticker, context?.instrument);
|
|
186
196
|
const variantKey = this.options.getTickerVariantCandidates(exchange)[0] ?? "";
|
|
187
197
|
for (const provider of this.options.providersInPriorityOrder()) {
|
|
188
198
|
try {
|
|
189
199
|
const quote = await provider.getQuote(ticker, exchange, context);
|
|
190
|
-
if (!isProviderQuoteUsableForCurrentSession(quote, exchange, ticker))
|
|
200
|
+
if (!isProviderQuoteUsableForCurrentSession(quote, exchange, ticker, { recentAnswer: true })) {
|
|
201
|
+
noteProviderAnswer(misses);
|
|
202
|
+
const lastKnown = misses && lastKnownProviderQuote(quote, exchange, ticker);
|
|
203
|
+
if (lastKnown && (misses.lastKnownQuote?.lastUpdated ?? -Infinity) < lastKnown.lastUpdated) {
|
|
204
|
+
misses.lastKnownQuote = lastKnown;
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
191
208
|
const sourceKey = this.options.providerSourceKey(provider);
|
|
192
209
|
this.options.cacheResource(
|
|
193
210
|
"quote",
|
|
@@ -199,6 +216,7 @@ export class ProviderRouterPrimaryRoutes {
|
|
|
199
216
|
);
|
|
200
217
|
return { sourceKey, value: quote };
|
|
201
218
|
} catch (error) {
|
|
219
|
+
noteProviderMiss(misses, error);
|
|
202
220
|
if (shouldLogProviderError(error)) {
|
|
203
221
|
this.options.logProviderError(`${provider.id} failed: ${error}`);
|
|
204
222
|
}
|