gloomberb 0.10.0 → 0.10.2
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 +8 -1
- package/src/api-client/auth.ts +1 -0
- package/src/api-client/index.test.ts +359 -108
- package/src/api-client/index.ts +24 -13
- package/src/api-client/socket.ts +70 -35
- package/src/api-client/types.ts +1 -1
- package/src/app/language-observer.test.tsx +82 -0
- package/src/app/language-observer.tsx +13 -0
- package/src/app/pane-runtime/index.tsx +2 -2
- package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
- package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
- package/src/app/pane-runtime/plugin-bindings.ts +2 -2
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
- package/src/app/runtime/broker-import.ts +2 -2
- package/src/app/runtime/plugin-bindings.ts +2 -2
- package/src/app/runtime/startup.ts +2 -2
- package/src/app.tsx +16 -7
- package/src/brokers/account-cache.ts +6 -6
- package/src/brokers/broker-ticker-sync.ts +3 -3
- package/src/brokers/remote-broker-adapter.ts +2 -2
- package/src/brokers/sync-broker-instance.ts +5 -6
- package/src/cli/commands/api.ts +3 -6
- package/src/cli/commands/automation.ts +7 -15
- package/src/cli/commands/market.ts +26 -61
- package/src/cli/commands/overview.ts +34 -45
- package/src/cli/commands/search.ts +25 -27
- package/src/cli/commands/system.ts +27 -57
- package/src/cli/commands/ticker.ts +62 -63
- package/src/cli/context.test.ts +63 -0
- package/src/cli/context.ts +15 -1
- package/src/cli/index.ts +1 -1
- package/src/cli/pane-functions/data.ts +2 -19
- package/src/cli/pane-functions/index.ts +26 -25
- package/src/cli/pane-functions/report.ts +1 -1
- package/src/cli/pane-functions/screenshot.ts +3 -3
- package/src/cli/scoped-context.ts +41 -0
- package/src/components/chart/composite/composite-chart.test.tsx +615 -18
- package/src/components/chart/composite/composite-chart.tsx +179 -79
- package/src/components/chart/composite/format.test.ts +20 -7
- package/src/components/chart/composite/index.ts +0 -21
- package/src/components/chart/composite/interactions.test.ts +90 -3
- package/src/components/chart/composite/interactions.ts +189 -24
- package/src/components/chart/composite/price-series.ts +0 -2
- package/src/components/chart/composite/rasterizer.ts +10 -9
- package/src/components/chart/composite/renderers.test.ts +108 -2
- package/src/components/chart/composite/scene.test.ts +36 -0
- package/src/components/chart/composite/scene.ts +5 -2
- package/src/components/chart/composite/text-renderer.ts +37 -25
- package/src/components/chart/composite/time-axis.test.ts +243 -0
- package/src/components/chart/composite/time-axis.ts +608 -0
- package/src/components/chart/composite/types.ts +12 -1
- package/src/components/chart/core/data.ts +2 -159
- package/src/components/chart/core/date-window.ts +6 -225
- package/src/components/chart/core/palette.ts +1 -12
- package/src/components/chart/core/pointer.ts +0 -142
- package/src/components/chart/core/types.ts +1 -58
- package/src/components/chart/cursor-motion.ts +2 -4
- package/src/components/chart/indicators/types.ts +0 -9
- package/src/components/chart/market-session.ts +0 -11
- package/src/components/chart/native/kitty/protocol.ts +1 -1
- package/src/components/chart/native/raster/layout.ts +3 -10
- package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
- package/src/components/chart/static/chart/surface.tsx +1 -2
- package/src/components/command-bar/action-runtime.ts +2 -2
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/panel/body.tsx +2 -2
- package/src/components/command-bar/panel/index.tsx +11 -9
- package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
- package/src/components/command-bar/surface/index.tsx +2 -2
- package/src/components/command-bar/workflow/body.tsx +3 -3
- package/src/components/command-bar/workflow/collection-actions.ts +2 -2
- package/src/components/command-bar/workflow/coordinator.ts +2 -2
- package/src/components/command-bar/workflow/field-row.tsx +25 -11
- package/src/components/command-bar/workflow/tickers.ts +2 -2
- package/src/components/feed-data-table/stack-view.test.tsx +28 -36
- package/src/components/feed-data-table/stack-view.tsx +3 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -6
- package/src/components/layout/header.tsx +13 -9
- package/src/components/layout/pane/content.test.tsx +42 -3
- package/src/components/layout/pane/content.tsx +3 -1
- package/src/components/layout/pane/footer/index.test.tsx +45 -2
- package/src/components/layout/pane/footer/registration.tsx +3 -1
- package/src/components/layout/shell/index.tsx +1 -2
- package/src/components/layout/shell/native/surfaces.ts +1 -1
- package/src/components/layout/shell/native/window-state.ts +1 -1
- package/src/components/layout/status-bar.tsx +12 -8
- package/src/components/layout/window-controls.tsx +8 -6
- package/src/components/onboarding/onboarding-wizard.tsx +3 -1
- package/src/components/pane-settings-dialog/desktop.tsx +0 -2
- package/src/components/pane-settings-dialog.tsx +0 -2
- package/src/components/pane-template-wizard.tsx +10 -9
- package/src/components/price-performance.tsx +2 -113
- package/src/components/price-sparkline/model.ts +15 -17
- package/src/components/price-sparkline/view.tsx +8 -7
- package/src/components/speedometer-gauge/desktop.tsx +2 -1
- package/src/components/speedometer-gauge/index.tsx +0 -2
- package/src/components/speedometer-gauge/terminal.tsx +24 -5
- package/src/components/ticker/badge/index.tsx +1 -18
- package/src/components/ui/data-table/opentui/index.tsx +3 -3
- package/src/components/ui/frame.tsx +1 -2
- package/src/components/ui/page-stack-view.test.tsx +126 -0
- package/src/components/ui/page-stack-view.tsx +24 -12
- package/src/components/ui/tabs.tsx +7 -3
- package/src/components/ui/ui.test.tsx +23 -2
- package/src/components/use-external-link-footer.ts +4 -2
- package/src/core/app-service-ports.ts +46 -0
- package/src/core/app-services.test.ts +66 -0
- package/src/core/app-services.ts +17 -15
- package/src/data/config/chart-settings.ts +3 -10
- package/src/data/config/store/index.test.ts +99 -2
- package/src/data/config/store/migrations.ts +278 -0
- package/src/data/config/store/normalize.ts +44 -186
- package/src/data/resource-store.ts +73 -12
- package/src/data/sqlite/cache.test.ts +104 -15
- package/src/i18n/index.test.ts +17 -0
- package/src/i18n/index.ts +23 -6
- package/src/i18n/ja.ts +13 -0
- package/src/i18n/ko.ts +13 -0
- package/src/i18n/react.ts +6 -0
- package/src/i18n/zh-cn.ts +13 -0
- package/src/i18n/zh-tw.ts +13 -0
- package/src/market-data/coordinator/auxiliary.ts +7 -3
- package/src/market-data/coordinator/chart.ts +1 -1
- package/src/market-data/coordinator/index.test.ts +26 -0
- package/src/market-data/coordinator/index.ts +11 -5
- package/src/market-data/coordinator/quotes.ts +4 -40
- package/src/market-data/coordinator/subscriptions.test.ts +89 -1
- package/src/market-data/hooks.tsx +47 -1
- package/src/market-data/market/status.ts +6 -6
- package/src/market-data/performance.ts +1 -1
- package/src/market-data/quote-subscription-target.test.ts +37 -0
- package/src/market-data/quote-subscription-target.ts +60 -0
- package/src/market-data/quotes/freshness.ts +1 -0
- package/src/market-data/quotes/resolution.ts +4 -0
- package/src/market-data/request-types.ts +3 -3
- package/src/news/aggregator.test.ts +97 -1
- package/src/news/aggregator.ts +100 -57
- package/src/news/hooks.test.tsx +64 -0
- package/src/news/hooks.ts +18 -25
- package/src/plugins/builtin/account-management/footer.ts +4 -2
- package/src/plugins/builtin/account-management/pane.tsx +9 -4
- package/src/plugins/builtin/ai/agent-history.ts +11 -7
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
- package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
- package/src/plugins/builtin/ai/index.test.ts +2 -2
- package/src/plugins/builtin/ai/index.tsx +10 -11
- package/src/plugins/builtin/ai/pane-settings.ts +1 -1
- package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
- package/src/plugins/builtin/ai/providers.ts +1 -9
- package/src/plugins/builtin/ai/runner.ts +0 -6
- package/src/plugins/builtin/ai/screener/footer.ts +4 -1
- package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
- package/src/plugins/builtin/ai/workspace/model.ts +2 -27
- package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
- package/src/plugins/builtin/analytics/metrics.ts +1 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
- package/src/plugins/builtin/broker-manager/index.tsx +4 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
- package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
- package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
- package/src/plugins/builtin/chart-composer/index.tsx +0 -6
- package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
- package/src/plugins/builtin/chart-composer/presets.ts +1 -19
- package/src/plugins/builtin/chart-composer/settings.ts +1 -1
- package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
- package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
- package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
- package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
- package/src/plugins/builtin/chat/controller/session.ts +2 -0
- package/src/plugins/builtin/chat/controller/state.ts +7 -0
- package/src/plugins/builtin/chat/controller.test.ts +73 -20
- package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
- package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
- package/src/plugins/builtin/correlation/compute.ts +4 -10
- package/src/plugins/builtin/econ/index.tsx +17 -19
- package/src/plugins/builtin/holders/types.ts +0 -1
- package/src/plugins/builtin/insider/index.tsx +6 -31
- package/src/plugins/builtin/news/index.tsx +5 -7
- package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
- package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
- package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
- package/src/plugins/builtin/news/wire/read-state.ts +18 -28
- package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
- package/src/plugins/builtin/options/footer.test.ts +115 -0
- package/src/plugins/builtin/options/footer.ts +110 -0
- package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
- package/src/plugins/builtin/options/live-quotes.ts +147 -0
- package/src/plugins/builtin/options/view.test.tsx +71 -1
- package/src/plugins/builtin/options/view.tsx +39 -24
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
- package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
- package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
- package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
- package/src/plugins/builtin/sec/filing-content.ts +94 -0
- package/src/plugins/builtin/sec/index.tsx +21 -104
- package/src/plugins/builtin/sectors/sector-model.ts +1 -7
- package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
- package/src/plugins/builtin/shared/pane-footer.ts +82 -0
- package/src/plugins/builtin/shared/read-state.ts +64 -3
- package/src/plugins/builtin/substack/read-state.ts +16 -41
- package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
- package/src/plugins/builtin/yield-curve/index.tsx +11 -9
- package/src/plugins/ibkr/dialogs.tsx +3 -3
- package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
- package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
- package/src/plugins/ibkr/gateway/history.ts +2 -2
- package/src/plugins/ibkr/gateway/selection.ts +2 -2
- package/src/plugins/ibkr/gateway/service/index.ts +2 -2
- package/src/plugins/ibkr/gateway/service/native.ts +2 -2
- package/src/plugins/plugin-persistence.ts +3 -4
- package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
- package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
- package/src/plugins/prediction-markets/detail/book.tsx +2 -2
- package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
- package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
- package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
- package/src/plugins/registry/context.ts +3 -4
- package/src/plugins/registry/index.ts +5 -6
- package/src/react/input.test.ts +126 -3
- package/src/react/input.ts +120 -1
- package/src/remote/app-host.tsx +2 -13
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
- package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
- package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
- package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
- package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
- package/src/renderers/electrobun/bun/index.ts +123 -83
- package/src/renderers/electrobun/shared/protocol.ts +146 -7
- package/src/renderers/electrobun/view/app-services.ts +5 -8
- package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
- package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
- package/src/renderers/electrobun/view/config-host.ts +1 -1
- package/src/renderers/electrobun/view/context-menu.ts +1 -17
- package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
- package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
- package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
- package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
- package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
- package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
- package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
- package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
- package/src/renderers/electrobun/view/host/box.tsx +0 -1
- package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/input.tsx +0 -1
- package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
- package/src/renderers/electrobun/view/host/native.ts +0 -1
- package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
- package/src/renderers/electrobun/view/host/text.tsx +0 -1
- package/src/renderers/electrobun/view/http-fetch.ts +3 -11
- package/src/renderers/electrobun/view/input-host.tsx +10 -53
- package/src/renderers/electrobun/view/key-event.ts +0 -1
- package/src/renderers/electrobun/view/main.tsx +3 -3
- package/src/renderers/electrobun/view/native-renderer.ts +0 -1
- package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
- package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
- package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
- package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
- package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
- package/src/renderers/electrobun/view/toast-host.tsx +0 -1
- package/src/renderers/electrobun/view/ui-host.tsx +1 -2
- package/src/renderers/electrobun/view/update-host.ts +1 -1
- package/src/renderers/opentui/chart-surface.tsx +2 -1
- package/src/renderers/opentui/dialog-host.tsx +20 -4
- package/src/renderers/opentui/image/loader.ts +12 -2
- package/src/renderers/opentui/input-host.tsx +7 -49
- package/src/renderers/opentui/start.tsx +2 -0
- package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
- package/src/sources/gloomberb-cloud/index.test.ts +173 -0
- package/src/sources/gloomberb-cloud/index.ts +42 -20
- package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
- package/src/sources/provider-router/cache.ts +1 -1
- package/src/sources/provider-router/document-routes.ts +10 -5
- package/src/sources/provider-router/financials.test.ts +14 -0
- package/src/sources/provider-router/financials.ts +6 -1
- package/src/sources/provider-router/history.test.ts +163 -1
- package/src/sources/provider-router/history.ts +278 -347
- package/src/sources/provider-router/index.ts +2 -2
- package/src/sources/provider-router/routing.test.ts +39 -0
- package/src/sources/provider-router/routing.ts +24 -11
- package/src/sources/provider-router/supplemental.ts +37 -17
- package/src/sources/sec-edgar/content.ts +2 -12
- package/src/sources/sec-edgar.ts +2 -13
- package/src/sources/yahoo-finance/history.ts +2 -2
- package/src/sources/yahoo-finance/options.ts +5 -0
- package/src/sources/yahoo-finance.ts +2 -2
- package/src/state/app/bootstrap.ts +2 -2
- package/src/state/app/context/index.tsx +2 -2
- package/src/state/hooks/quote-streaming.ts +66 -1
- package/src/state/hooks/session-snapshot.ts +2 -2
- package/src/state/selectors.ts +0 -9
- package/src/sync/controller.test.ts +98 -0
- package/src/sync/controller.ts +29 -5
- package/src/sync/react.ts +2 -2
- package/src/sync/types.ts +2 -2
- package/src/theme/colors.test.ts +10 -0
- package/src/theme/colors.ts +75 -78
- package/src/theme/theme-context.test.tsx +67 -0
- package/src/theme/theme-context.tsx +9 -5
- package/src/tickers/open-target.ts +2 -2
- package/src/tickers/search/upsert.ts +2 -2
- package/src/time-series/alignment.ts +0 -7
- package/src/time-series/chart-data.ts +145 -0
- package/src/time-series/date-window.ts +44 -0
- package/src/time-series/history-window.ts +21 -0
- package/src/time-series/hooks.test.ts +59 -0
- package/src/time-series/hooks.ts +33 -12
- package/src/time-series/market-resolution.ts +2 -2
- package/src/time-series/market.ts +1 -1
- package/src/time-series/range.ts +9 -0
- package/src/{components/chart/core → time-series}/resolution.ts +3 -102
- package/src/time-series/resolve.test.ts +203 -1
- package/src/time-series/resolve.ts +96 -18
- package/src/time-series/spec-studies.test.ts +15 -0
- package/src/time-series/spec.ts +2 -2
- package/src/time-series/studies.ts +5 -2
- package/src/time-series/types.ts +3 -1
- package/src/time-series/use-chart-resolution.test.tsx +358 -0
- package/src/time-series/use-chart-resolution.ts +46 -4
- package/src/types/broker.ts +2 -2
- package/src/types/data-provider.ts +3 -3
- package/src/types/financials.ts +12 -0
- package/src/types/plugin.ts +2 -2
- package/src/ui/context-menu.tsx +3 -2
- package/src/ui/dialog.tsx +2 -2
- package/src/utils/html-entities.ts +3 -5
- package/src/utils/main-thread-monitor.ts +1 -1
- package/src/utils/price-history.ts +5 -5
- package/src/version.ts +3 -1
- package/src/components/chart/core/viewport.test.ts +0 -8
- package/src/components/chart/core/viewport.ts +0 -24
- package/src/time-series/index.ts +0 -14
- /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { createGloomberbCloudCapabilities, GloomberbCloudProvider } from "./index";
|
|
3
|
+
import { getRangeStartDate, toHistoryRequest } from "./normalizers";
|
|
3
4
|
import type { NewsCapability } from "../../capabilities";
|
|
4
5
|
import { apiClient, type AuthUser, type CloudNewsPayload } from "../../api-client";
|
|
5
6
|
|
|
@@ -25,6 +26,17 @@ const originalGetCloudNews = apiClient.getCloudNews.bind(apiClient);
|
|
|
25
26
|
const originalGetCloudNewsStory = apiClient.getCloudNewsStory.bind(apiClient);
|
|
26
27
|
const originalSubscribeQuotes = apiClient.subscribeQuotes.bind(apiClient);
|
|
27
28
|
|
|
29
|
+
test("requests fifty years of cloud history for the ALL range", () => {
|
|
30
|
+
const endDate = new Date("2026-07-30T20:00:00.000Z");
|
|
31
|
+
|
|
32
|
+
expect(getRangeStartDate("ALL", endDate).toISOString()).toBe("1976-07-30T20:00:00.000Z");
|
|
33
|
+
expect(toHistoryRequest("ALL")).toEqual({
|
|
34
|
+
interval: "1month",
|
|
35
|
+
outputsize: 600,
|
|
36
|
+
rangeKey: "ALL",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
28
40
|
function makeCloudNewsPayload(overrides: Partial<CloudNewsPayload> = {}): CloudNewsPayload {
|
|
29
41
|
return {
|
|
30
42
|
id: "story-1",
|
|
@@ -124,6 +136,154 @@ describe("GloomberbCloudProvider", () => {
|
|
|
124
136
|
expect(history[0]?.date.toISOString()).toBe("2026-03-27T14:15:00.000Z");
|
|
125
137
|
});
|
|
126
138
|
|
|
139
|
+
test("rejects isolated cloud OHLC spikes so another history source can be used", async () => {
|
|
140
|
+
apiClient.ensureVerifiedSession = async () => verifiedUser;
|
|
141
|
+
apiClient.getCloudHistory = async () => ({
|
|
142
|
+
status: "success",
|
|
143
|
+
data: [
|
|
144
|
+
{
|
|
145
|
+
date: "2026-07-29T20:46:00Z",
|
|
146
|
+
open: 728.4,
|
|
147
|
+
high: 728.7,
|
|
148
|
+
low: 728.3,
|
|
149
|
+
close: 728.5,
|
|
150
|
+
volume: 0,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
date: "2026-07-29T20:47:00Z",
|
|
154
|
+
open: 686.98,
|
|
155
|
+
high: 728.67,
|
|
156
|
+
low: 686.98,
|
|
157
|
+
close: 728.6,
|
|
158
|
+
volume: 0,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
date: "2026-07-29T20:48:00Z",
|
|
162
|
+
open: 728.6,
|
|
163
|
+
high: 728.7,
|
|
164
|
+
low: 728.1,
|
|
165
|
+
close: 728.3,
|
|
166
|
+
volume: 0,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const provider = new GloomberbCloudProvider();
|
|
172
|
+
|
|
173
|
+
await expect(
|
|
174
|
+
provider.getPriceHistoryForResolution("SPY", "NYSEARCA", "1W", "1m"),
|
|
175
|
+
).rejects.toThrow("failed OHLC validation");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("validates an outlier after normalizing descending cloud history", async () => {
|
|
179
|
+
apiClient.ensureVerifiedSession = async () => verifiedUser;
|
|
180
|
+
apiClient.getCloudHistory = async () => ({
|
|
181
|
+
status: "success",
|
|
182
|
+
data: [
|
|
183
|
+
{
|
|
184
|
+
date: "2026-07-29T20:48:00Z",
|
|
185
|
+
open: 728.6,
|
|
186
|
+
high: 728.7,
|
|
187
|
+
low: 728.1,
|
|
188
|
+
close: 728.3,
|
|
189
|
+
volume: 0,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
date: "2026-07-29T20:47:00Z",
|
|
193
|
+
open: 686.98,
|
|
194
|
+
high: 728.67,
|
|
195
|
+
low: 686.98,
|
|
196
|
+
close: 728.6,
|
|
197
|
+
volume: 0,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
date: "2026-07-29T20:46:00Z",
|
|
201
|
+
open: 728.4,
|
|
202
|
+
high: 728.7,
|
|
203
|
+
low: 728.3,
|
|
204
|
+
close: 728.5,
|
|
205
|
+
volume: 0,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const provider = new GloomberbCloudProvider();
|
|
211
|
+
|
|
212
|
+
await expect(
|
|
213
|
+
provider.getPriceHistoryForResolution("SPY", "NYSEARCA", "1W", "1m"),
|
|
214
|
+
).rejects.toThrow("failed OHLC validation");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("preserves a wide cloud bar during a continuing price move", async () => {
|
|
218
|
+
apiClient.ensureVerifiedSession = async () => verifiedUser;
|
|
219
|
+
apiClient.getCloudHistory = async () => ({
|
|
220
|
+
status: "success",
|
|
221
|
+
data: [
|
|
222
|
+
{
|
|
223
|
+
date: "2026-07-29T20:10:00Z",
|
|
224
|
+
open: 561.1,
|
|
225
|
+
high: 561.3,
|
|
226
|
+
low: 560.7,
|
|
227
|
+
close: 560.8994,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
date: "2026-07-29T20:15:00Z",
|
|
231
|
+
open: 560.11,
|
|
232
|
+
high: 585.61,
|
|
233
|
+
low: 553.41,
|
|
234
|
+
close: 555.4,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
date: "2026-07-29T20:20:00Z",
|
|
238
|
+
open: 555.2,
|
|
239
|
+
high: 556,
|
|
240
|
+
low: 548.9,
|
|
241
|
+
close: 549.01,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const provider = new GloomberbCloudProvider();
|
|
247
|
+
const history = await provider.getPriceHistoryForResolution(
|
|
248
|
+
"META",
|
|
249
|
+
"NASDAQ",
|
|
250
|
+
"1W",
|
|
251
|
+
"5m",
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(history).toHaveLength(3);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("does not reject an explicitly Yahoo-backed cloud history response", async () => {
|
|
258
|
+
apiClient.ensureVerifiedSession = async () => verifiedUser;
|
|
259
|
+
apiClient.getCloudHistory = async () => ({
|
|
260
|
+
status: "success",
|
|
261
|
+
providerMeta: { provider: "yahoo" },
|
|
262
|
+
data: [
|
|
263
|
+
{ date: "2026-07-29T20:46:00Z", close: 728.5 },
|
|
264
|
+
{
|
|
265
|
+
date: "2026-07-29T20:47:00Z",
|
|
266
|
+
open: 686.98,
|
|
267
|
+
high: 728.67,
|
|
268
|
+
low: 686.98,
|
|
269
|
+
close: 728.6,
|
|
270
|
+
volume: 0,
|
|
271
|
+
},
|
|
272
|
+
{ date: "2026-07-29T20:48:00Z", close: 728.3 },
|
|
273
|
+
],
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const provider = new GloomberbCloudProvider();
|
|
277
|
+
const history = await provider.getPriceHistoryForResolution(
|
|
278
|
+
"SPY",
|
|
279
|
+
"NYSEARCA",
|
|
280
|
+
"1W",
|
|
281
|
+
"1m",
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
expect(history[1]?.low).toBe(686.98);
|
|
285
|
+
});
|
|
286
|
+
|
|
127
287
|
test("normalizes daily detailed history requests to 1day", async () => {
|
|
128
288
|
apiClient.ensureVerifiedSession = async () => verifiedUser;
|
|
129
289
|
|
|
@@ -362,6 +522,11 @@ describe("GloomberbCloudProvider", () => {
|
|
|
362
522
|
providerId: "gloomberb-cloud",
|
|
363
523
|
underlyingSymbol: "AMD",
|
|
364
524
|
expirationDates: [1_800_000_000],
|
|
525
|
+
dataSource: "live",
|
|
526
|
+
feed: "opra",
|
|
527
|
+
delayMinutes: 0,
|
|
528
|
+
realtimeEligible: true,
|
|
529
|
+
asOf: "2027-01-15T14:30:00.000Z",
|
|
365
530
|
calls: [{
|
|
366
531
|
contractSymbol: "AMD270917C00230000",
|
|
367
532
|
strike: 230,
|
|
@@ -393,6 +558,14 @@ describe("GloomberbCloudProvider", () => {
|
|
|
393
558
|
});
|
|
394
559
|
expect(chain.underlyingSymbol).toBe("AMD");
|
|
395
560
|
expect(chain.calls[0]?.contractSymbol).toBe("AMD270917C00230000");
|
|
561
|
+
expect(chain).toMatchObject({
|
|
562
|
+
providerId: "gloomberb-cloud",
|
|
563
|
+
dataSource: "live",
|
|
564
|
+
feed: "opra",
|
|
565
|
+
delayMinutes: 0,
|
|
566
|
+
realtimeEligible: true,
|
|
567
|
+
asOf: "2027-01-15T14:30:00.000Z",
|
|
568
|
+
});
|
|
396
569
|
});
|
|
397
570
|
|
|
398
571
|
test("preserves original target context when streaming quotes", () => {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { TimeRange } from "../../
|
|
1
|
+
import type { TimeRange } from "../../time-series/range";
|
|
2
2
|
import {
|
|
3
3
|
normalizeChartResolutionSupport,
|
|
4
4
|
type ChartResolutionSupport,
|
|
5
5
|
type ManualChartResolution,
|
|
6
|
-
} from "../../
|
|
6
|
+
} from "../../time-series/resolution";
|
|
7
7
|
import { assetDataProvider, newsProvider, type PluginCapability } from "../../capabilities";
|
|
8
8
|
import type {
|
|
9
9
|
AssetDataProvider,
|
|
@@ -22,11 +22,14 @@ import {
|
|
|
22
22
|
type CloudCorporateActionsPayload,
|
|
23
23
|
type CloudHoldersPayload,
|
|
24
24
|
type CloudMarketResponse,
|
|
25
|
+
type CloudPricePointPayload,
|
|
25
26
|
} from "../../api-client";
|
|
26
27
|
import type { NewsArticle, NewsQuery } from "../../types/news-source";
|
|
27
28
|
import { resolvePriceHistoryCurrencyUnit } from "../../utils/currency-units";
|
|
28
29
|
import { canonicalTickerKey } from "../../utils/exchanges";
|
|
30
|
+
import { normalizePriceHistory } from "../../utils/price-history";
|
|
29
31
|
import { createProviderMiss } from "../provider-errors";
|
|
32
|
+
import { hasMalformedCloudIntradayHistory } from "./history-quality";
|
|
30
33
|
import {
|
|
31
34
|
cloudNewsParams,
|
|
32
35
|
mapCloudNewsArticle,
|
|
@@ -83,6 +86,40 @@ function isStaleCloudResponse(response: CloudMarketResponse<unknown>): boolean {
|
|
|
83
86
|
return response.stale === true || response.providerMeta?.stale === true;
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
function mapCloudPriceHistory(
|
|
90
|
+
response: CloudMarketResponse<CloudPricePointPayload[]>,
|
|
91
|
+
ticker: string,
|
|
92
|
+
exchange: string,
|
|
93
|
+
interval: string,
|
|
94
|
+
): PricePoint[] {
|
|
95
|
+
if (isStaleCloudResponse(response)) {
|
|
96
|
+
throw createProviderMiss(`Cloud chart data is stale for ${ticker}`);
|
|
97
|
+
}
|
|
98
|
+
const { divisor } = resolvePriceHistoryCurrencyUnit(
|
|
99
|
+
response.currency ?? response.providerMeta?.currency,
|
|
100
|
+
exchange,
|
|
101
|
+
);
|
|
102
|
+
const points = normalizePriceHistory(
|
|
103
|
+
unwrapRequiredCloudResponse(
|
|
104
|
+
response,
|
|
105
|
+
`Cloud chart data is unavailable for ${ticker}`,
|
|
106
|
+
).map((point) => mapPricePoint(point, divisor, exchange)),
|
|
107
|
+
);
|
|
108
|
+
const upstream = (
|
|
109
|
+
response.providerMeta?.provider
|
|
110
|
+
?? response.providerMeta?.upstream
|
|
111
|
+
?? ""
|
|
112
|
+
).trim().toLowerCase();
|
|
113
|
+
if (
|
|
114
|
+
/(min|h)$/i.test(interval)
|
|
115
|
+
&& upstream !== "yahoo"
|
|
116
|
+
&& hasMalformedCloudIntradayHistory(points)
|
|
117
|
+
) {
|
|
118
|
+
throw createProviderMiss(`Cloud chart data failed OHLC validation for ${ticker}`);
|
|
119
|
+
}
|
|
120
|
+
return points;
|
|
121
|
+
}
|
|
122
|
+
|
|
86
123
|
function quoteTargetKey(symbol: string, exchange?: string): string {
|
|
87
124
|
return canonicalTickerKey(symbol, exchange);
|
|
88
125
|
}
|
|
@@ -269,12 +306,7 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
269
306
|
() => apiClient.getCloudHistory(ticker, exchange, request),
|
|
270
307
|
`Cloud chart data is unavailable for ${ticker}`,
|
|
271
308
|
);
|
|
272
|
-
|
|
273
|
-
throw createProviderMiss(`Cloud chart data is stale for ${ticker}`);
|
|
274
|
-
}
|
|
275
|
-
const { divisor } = resolvePriceHistoryCurrencyUnit(response.currency ?? response.providerMeta?.currency, exchange);
|
|
276
|
-
return unwrapRequiredCloudResponse(response, `Cloud chart data is unavailable for ${ticker}`)
|
|
277
|
-
.map((point) => mapPricePoint(point, divisor, exchange));
|
|
309
|
+
return mapCloudPriceHistory(response, ticker, exchange, request.interval);
|
|
278
310
|
}
|
|
279
311
|
|
|
280
312
|
async getPriceHistoryForResolution(
|
|
@@ -297,12 +329,7 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
297
329
|
}),
|
|
298
330
|
`Cloud chart data is unavailable for ${ticker}`,
|
|
299
331
|
);
|
|
300
|
-
|
|
301
|
-
throw createProviderMiss(`Cloud chart data is stale for ${ticker}`);
|
|
302
|
-
}
|
|
303
|
-
const { divisor } = resolvePriceHistoryCurrencyUnit(response.currency ?? response.providerMeta?.currency, exchange);
|
|
304
|
-
return unwrapRequiredCloudResponse(response, `Cloud chart data is unavailable for ${ticker}`)
|
|
305
|
-
.map((point) => mapPricePoint(point, divisor, exchange));
|
|
332
|
+
return mapCloudPriceHistory(response, ticker, exchange, interval);
|
|
306
333
|
}
|
|
307
334
|
|
|
308
335
|
async getDetailedPriceHistory(
|
|
@@ -324,12 +351,7 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
324
351
|
}),
|
|
325
352
|
`Cloud detailed chart history is unavailable for ${ticker}`,
|
|
326
353
|
);
|
|
327
|
-
|
|
328
|
-
throw createProviderMiss(`Cloud detailed chart history is stale for ${ticker}`);
|
|
329
|
-
}
|
|
330
|
-
const { divisor } = resolvePriceHistoryCurrencyUnit(response.currency ?? response.providerMeta?.currency, exchange);
|
|
331
|
-
return unwrapRequiredCloudResponse(response, `Cloud detailed chart history is unavailable for ${ticker}`)
|
|
332
|
-
.map((point) => mapPricePoint(point, divisor, exchange));
|
|
354
|
+
return mapCloudPriceHistory(response, ticker, exchange, interval);
|
|
333
355
|
}
|
|
334
356
|
|
|
335
357
|
async getOptionsChain(ticker: string, exchange?: string, expirationDate?: number, _context?: MarketDataRequestContext): Promise<OptionsChain> {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TimeRange } from "../../
|
|
1
|
+
import type { TimeRange } from "../../time-series/range";
|
|
2
2
|
import type {
|
|
3
3
|
OptionsChain,
|
|
4
4
|
PricePoint,
|
|
@@ -217,6 +217,12 @@ export function mapOptionsChain(
|
|
|
217
217
|
expirationDates: chain.expirationDates ?? [],
|
|
218
218
|
calls: chain.calls ?? [],
|
|
219
219
|
puts: chain.puts ?? [],
|
|
220
|
+
providerId: chain.providerId,
|
|
221
|
+
dataSource: chain.dataSource,
|
|
222
|
+
feed: chain.feed,
|
|
223
|
+
delayMinutes: chain.delayMinutes,
|
|
224
|
+
realtimeEligible: chain.realtimeEligible,
|
|
225
|
+
asOf: chain.asOf,
|
|
220
226
|
};
|
|
221
227
|
}
|
|
222
228
|
|
|
@@ -320,7 +326,7 @@ export function getRangeStartDate(
|
|
|
320
326
|
startDate.setFullYear(startDate.getFullYear() - 5);
|
|
321
327
|
break;
|
|
322
328
|
case "ALL":
|
|
323
|
-
startDate.setFullYear(startDate.getFullYear() -
|
|
329
|
+
startDate.setFullYear(startDate.getFullYear() - 50);
|
|
324
330
|
break;
|
|
325
331
|
}
|
|
326
332
|
return startDate;
|
|
@@ -347,7 +353,7 @@ export function toHistoryRequest(range: TimeRange): {
|
|
|
347
353
|
case "5Y":
|
|
348
354
|
return { interval: "1week", outputsize: 261, rangeKey: range };
|
|
349
355
|
case "ALL":
|
|
350
|
-
return { interval: "1month", outputsize:
|
|
356
|
+
return { interval: "1month", outputsize: 600, rangeKey: range };
|
|
351
357
|
default:
|
|
352
358
|
return { interval: "1day", outputsize: 366, rangeKey: range };
|
|
353
359
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CachedResourceRecord, ResourceStore } from "../../data/resource-store";
|
|
2
|
-
import type { TimeRange } from "../../
|
|
2
|
+
import type { TimeRange } from "../../time-series/range";
|
|
3
3
|
import type { BrokerContractRef } from "../../types/instrument";
|
|
4
4
|
import type { PricePoint } from "../../types/financials";
|
|
5
5
|
import type { CachePolicy, CachePolicyMap } from "../../types/persistence";
|
|
@@ -13,7 +13,7 @@ import { shouldLogProviderError } from "../provider-errors";
|
|
|
13
13
|
import type { ProviderRouterCoreDeps, SourceResult } from "./route-types";
|
|
14
14
|
import {
|
|
15
15
|
firstProviderResult,
|
|
16
|
-
|
|
16
|
+
makeRouterRequestIdentity,
|
|
17
17
|
resolveProviderBySourceKey,
|
|
18
18
|
scheduleRouterRevalidation,
|
|
19
19
|
} from "./routing";
|
|
@@ -24,15 +24,20 @@ export class ProviderRouterDocumentRoutes {
|
|
|
24
24
|
constructor(private readonly deps: ProviderRouterCoreDeps) {}
|
|
25
25
|
|
|
26
26
|
async getSecFilings(ticker: string, count = 15, exchange?: string, context?: MarketDataRequestContext): Promise<SecFilingItem[]> {
|
|
27
|
-
const
|
|
27
|
+
const identity = makeRouterRequestIdentity(this.deps, {
|
|
28
|
+
kind: "sec-filings",
|
|
29
|
+
ticker,
|
|
30
|
+
context,
|
|
31
|
+
variantParts: [["exchange", canonicalExchange(exchange)], ["count", count]],
|
|
32
|
+
});
|
|
28
33
|
const variantKeys = [
|
|
29
|
-
|
|
34
|
+
identity.variantKey,
|
|
30
35
|
buildVariantKey([["count", count]]),
|
|
31
36
|
"",
|
|
32
37
|
];
|
|
33
|
-
const cached = selectCachedResource<SecFilingItem[]>(this.deps.resources,
|
|
38
|
+
const cached = selectCachedResource<SecFilingItem[]>(this.deps.resources, identity.kind, identity.entityKey, variantKeys, this.deps.getProviderSourceKeys(), false);
|
|
34
39
|
if (cached) {
|
|
35
|
-
scheduleRouterRevalidation(this.revalidationInFlight,
|
|
40
|
+
scheduleRouterRevalidation(this.revalidationInFlight, identity.revalidationKey, async () => {
|
|
36
41
|
await this.revalidateSecFilings(ticker, count, exchange, context);
|
|
37
42
|
});
|
|
38
43
|
return cached.value;
|
|
@@ -22,6 +22,20 @@ describe("provider-router financial quote usability", () => {
|
|
|
22
22
|
}), "FWB2")).toBe(false);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
test("accepts a streamed 15-minute delayed quote during the active session", () => {
|
|
26
|
+
expect(
|
|
27
|
+
isProviderQuoteUsableForCurrentSession(
|
|
28
|
+
makeQuote({
|
|
29
|
+
dataSource: "delayed",
|
|
30
|
+
listingExchangeName: "NASDAQ",
|
|
31
|
+
marketState: "REGULAR",
|
|
32
|
+
lastUpdated: Date.now() - 15 * 60_000,
|
|
33
|
+
}),
|
|
34
|
+
"NASDAQ",
|
|
35
|
+
),
|
|
36
|
+
).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
25
39
|
test("rejects empty zero provider quotes", () => {
|
|
26
40
|
expect(isProviderQuoteUsableForCurrentSession(makeQuote({
|
|
27
41
|
price: 0,
|
|
@@ -82,12 +82,17 @@ function finitePositiveNumber(value: unknown): value is number {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
const ACTIVE_PROVIDER_QUOTE_MAX_AGE_MS = 10 * 60_000;
|
|
85
|
+
const ACTIVE_DELAYED_PROVIDER_QUOTE_MAX_AGE_MS = 20 * 60_000;
|
|
85
86
|
const ACTIVE_QUOTE_MARKET_STATES = new Set(["PRE", "REGULAR", "POST"]);
|
|
86
87
|
|
|
87
88
|
function isActiveProviderQuoteTooOld(quote: Quote, now = Date.now()): boolean {
|
|
88
89
|
if (!ACTIVE_QUOTE_MARKET_STATES.has(quote.marketState ?? "")) return false;
|
|
89
90
|
if (!Number.isFinite(quote.lastUpdated)) return false;
|
|
90
|
-
|
|
91
|
+
const maxAge =
|
|
92
|
+
quote.dataSource === "delayed"
|
|
93
|
+
? ACTIVE_DELAYED_PROVIDER_QUOTE_MAX_AGE_MS
|
|
94
|
+
: ACTIVE_PROVIDER_QUOTE_MAX_AGE_MS;
|
|
95
|
+
return now - quote.lastUpdated > maxAge;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
export function isProviderQuoteUsableForCurrentSession(quote: Quote | null | undefined, exchange?: string): quote is Quote {
|
|
@@ -135,6 +135,59 @@ describe("AssetDataRouter chart history", () => {
|
|
|
135
135
|
persistence.close();
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
test("ignores the previous chart history cache generation", async () => {
|
|
139
|
+
const dbPath = createTempDbPath("previous-chart-cache-generation");
|
|
140
|
+
const persistence = new AppPersistence(dbPath);
|
|
141
|
+
const latestDate = new Date(Date.now() - 60_000);
|
|
142
|
+
const previousDate = new Date(latestDate.getTime() - 5 * 60_000);
|
|
143
|
+
|
|
144
|
+
persistence.resources.set(
|
|
145
|
+
{
|
|
146
|
+
namespace: "market",
|
|
147
|
+
kind: "price-history",
|
|
148
|
+
entityKey: "META",
|
|
149
|
+
variantKey: "exchange=NASDAQ;range=1M;resolution=5m;version=2",
|
|
150
|
+
sourceKey: "provider:gloomberb-cloud",
|
|
151
|
+
},
|
|
152
|
+
[
|
|
153
|
+
{ date: previousDate, close: 620 },
|
|
154
|
+
{ date: latestDate, close: 680 },
|
|
155
|
+
],
|
|
156
|
+
{
|
|
157
|
+
cachePolicy: { staleMs: 60_000, expireMs: 60_000 },
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
let providerCalls = 0;
|
|
162
|
+
const router = new AssetDataRouter({
|
|
163
|
+
...fallbackProvider,
|
|
164
|
+
id: "gloomberb-cloud",
|
|
165
|
+
name: "Gloomberb Cloud",
|
|
166
|
+
async getPriceHistoryForResolution() {
|
|
167
|
+
providerCalls += 1;
|
|
168
|
+
return [
|
|
169
|
+
{ date: previousDate, close: 620 },
|
|
170
|
+
{ date: latestDate, close: 560 },
|
|
171
|
+
];
|
|
172
|
+
},
|
|
173
|
+
}, [], persistence.resources);
|
|
174
|
+
|
|
175
|
+
const history = await router.getPriceHistoryForResolution("META", "NASDAQ", "1M", "5m");
|
|
176
|
+
|
|
177
|
+
expect(providerCalls).toBe(1);
|
|
178
|
+
expect(history.map((point) => point.close)).toEqual([620, 560]);
|
|
179
|
+
|
|
180
|
+
const cachedRows = persistence.database.connection
|
|
181
|
+
.query("SELECT variant_key FROM resource_cache WHERE namespace = ? AND kind = ? AND entity_key = ? ORDER BY variant_key")
|
|
182
|
+
.all("market", "price-history", "META") as Array<{ variant_key: string }>;
|
|
183
|
+
expect(cachedRows.map((row) => row.variant_key)).toEqual([
|
|
184
|
+
"exchange=NASDAQ;range=1M;resolution=5m;version=2",
|
|
185
|
+
"exchange=NASDAQ;range=1M;resolution=5m;version=3",
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
persistence.close();
|
|
189
|
+
});
|
|
190
|
+
|
|
138
191
|
test("ignores legacy unversioned sub-unit chart history caches", async () => {
|
|
139
192
|
const dbPath = createTempDbPath("subunit-chart-cache");
|
|
140
193
|
const persistence = new AppPersistence(dbPath);
|
|
@@ -180,7 +233,7 @@ describe("AssetDataRouter chart history", () => {
|
|
|
180
233
|
.all("market", "price-history", "FTC") as Array<{ variant_key: string }>;
|
|
181
234
|
expect(cachedRows.map((row) => row.variant_key)).toEqual([
|
|
182
235
|
"exchange=LSE;range=ALL;resolution=1wk",
|
|
183
|
-
"exchange=LSE;range=ALL;resolution=1wk;unit=GBP",
|
|
236
|
+
"exchange=LSE;range=ALL;resolution=1wk;version=3;unit=GBP",
|
|
184
237
|
]);
|
|
185
238
|
|
|
186
239
|
persistence.close();
|
|
@@ -349,6 +402,115 @@ describe("AssetDataRouter chart history", () => {
|
|
|
349
402
|
expect(await router.getChartResolutionCapabilities("AAPL", "NASDAQ")).toEqual(["1d", "1wk"]);
|
|
350
403
|
});
|
|
351
404
|
|
|
405
|
+
test("skips unavailable providers when resolving chart resolution support", async () => {
|
|
406
|
+
let cloudSupportCalls = 0;
|
|
407
|
+
const cloudProvider: DataProvider = {
|
|
408
|
+
...fallbackProvider,
|
|
409
|
+
id: "cloud",
|
|
410
|
+
name: "Cloud",
|
|
411
|
+
priority: 100,
|
|
412
|
+
async canProvide() {
|
|
413
|
+
return false;
|
|
414
|
+
},
|
|
415
|
+
getChartResolutionSupport() {
|
|
416
|
+
cloudSupportCalls += 1;
|
|
417
|
+
return [{ resolution: "1m", maxRange: "1W" }];
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
const yahooProvider: DataProvider = {
|
|
421
|
+
...fallbackProvider,
|
|
422
|
+
id: "yahoo",
|
|
423
|
+
name: "Yahoo",
|
|
424
|
+
priority: 1000,
|
|
425
|
+
getChartResolutionSupport() {
|
|
426
|
+
return [{ resolution: "1d", maxRange: "5Y" }];
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const router = new AssetDataRouter(yahooProvider, [cloudProvider]);
|
|
431
|
+
|
|
432
|
+
expect(await router.getChartResolutionSupport("AAPL", "NASDAQ")).toEqual([
|
|
433
|
+
{ resolution: "1d", maxRange: "5Y" },
|
|
434
|
+
]);
|
|
435
|
+
expect(cloudSupportCalls).toBe(0);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("refreshes each history request type before falling back to its cached value", async () => {
|
|
439
|
+
const dbPath = createTempDbPath("forced-history-refresh");
|
|
440
|
+
const persistence = new AppPersistence(dbPath);
|
|
441
|
+
const startDate = new Date("2026-03-28T09:30:00Z");
|
|
442
|
+
const endDate = new Date("2026-03-28T16:00:00Z");
|
|
443
|
+
const seedProvider: DataProvider = {
|
|
444
|
+
...fallbackProvider,
|
|
445
|
+
async getPriceHistory() {
|
|
446
|
+
return [{ date: endDate, close: 101 }];
|
|
447
|
+
},
|
|
448
|
+
async getPriceHistoryForResolution() {
|
|
449
|
+
return [{ date: endDate, close: 102 }];
|
|
450
|
+
},
|
|
451
|
+
async getDetailedPriceHistory() {
|
|
452
|
+
return [{ date: endDate, close: 103 }];
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
const seedRouter = new AssetDataRouter(seedProvider, [], persistence.resources);
|
|
456
|
+
await seedRouter.getPriceHistory("AAPL", "NASDAQ", "1Y");
|
|
457
|
+
await seedRouter.getPriceHistoryForResolution("AAPL", "NASDAQ", "1Y", "1d");
|
|
458
|
+
await seedRouter.getDetailedPriceHistory("AAPL", "NASDAQ", startDate, endDate, "15m");
|
|
459
|
+
|
|
460
|
+
const calls = { range: 0, resolution: 0, detailed: 0 };
|
|
461
|
+
const refreshProvider: DataProvider = {
|
|
462
|
+
...fallbackProvider,
|
|
463
|
+
async getPriceHistory() {
|
|
464
|
+
calls.range += 1;
|
|
465
|
+
return [];
|
|
466
|
+
},
|
|
467
|
+
async getPriceHistoryForResolution() {
|
|
468
|
+
calls.resolution += 1;
|
|
469
|
+
return [];
|
|
470
|
+
},
|
|
471
|
+
async getDetailedPriceHistory() {
|
|
472
|
+
calls.detailed += 1;
|
|
473
|
+
return [];
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
const refreshRouter = new AssetDataRouter(refreshProvider, [], persistence.resources);
|
|
477
|
+
|
|
478
|
+
const range = await refreshRouter.getPriceHistory("AAPL", "NASDAQ", "1Y", { cacheMode: "refresh" });
|
|
479
|
+
const resolution = await refreshRouter.getPriceHistoryForResolution(
|
|
480
|
+
"AAPL",
|
|
481
|
+
"NASDAQ",
|
|
482
|
+
"1Y",
|
|
483
|
+
"1d",
|
|
484
|
+
{ cacheMode: "refresh" },
|
|
485
|
+
);
|
|
486
|
+
const detailed = await refreshRouter.getDetailedPriceHistory(
|
|
487
|
+
"AAPL",
|
|
488
|
+
"NASDAQ",
|
|
489
|
+
startDate,
|
|
490
|
+
endDate,
|
|
491
|
+
"15m",
|
|
492
|
+
{ cacheMode: "refresh" },
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
expect(calls).toEqual({ range: 1, resolution: 1, detailed: 1 });
|
|
496
|
+
expect([range[0]?.close, resolution[0]?.close, detailed[0]?.close]).toEqual([101, 102, 103]);
|
|
497
|
+
|
|
498
|
+
persistence.close();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("preserves each history request type's missing-provider result", async () => {
|
|
502
|
+
const router = new AssetDataRouter(null);
|
|
503
|
+
const startDate = new Date("2026-03-28T09:30:00Z");
|
|
504
|
+
const endDate = new Date("2026-03-28T16:00:00Z");
|
|
505
|
+
|
|
506
|
+
await expect(router.getPriceHistory("AAPL", "NASDAQ", "1Y"))
|
|
507
|
+
.rejects.toThrow("No history provider available for AAPL");
|
|
508
|
+
await expect(router.getPriceHistoryForResolution("AAPL", "NASDAQ", "1Y", "1d"))
|
|
509
|
+
.rejects.toThrow("No resolution-aware history provider available for AAPL");
|
|
510
|
+
await expect(router.getDetailedPriceHistory("AAPL", "NASDAQ", startDate, endDate, "15m"))
|
|
511
|
+
.resolves.toEqual([]);
|
|
512
|
+
});
|
|
513
|
+
|
|
352
514
|
test("falls back to later providers when detailed chart history is empty", async () => {
|
|
353
515
|
const cloudProvider: DataProvider = {
|
|
354
516
|
...fallbackProvider,
|