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,8 +1,8 @@
|
|
|
1
|
-
import { appendLiveQuotePoint } from "
|
|
1
|
+
import { appendLiveQuotePoint } from "./chart-data";
|
|
2
2
|
import {
|
|
3
3
|
getTimeRangeForDateWindow,
|
|
4
4
|
subtractTimeRange,
|
|
5
|
-
} from "
|
|
5
|
+
} from "./date-window";
|
|
6
6
|
import {
|
|
7
7
|
CHART_RESOLUTION_STEP_MS,
|
|
8
8
|
clampTimeRangeToMaxRange,
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
TIME_RANGE_ORDER,
|
|
15
15
|
type ChartResolutionSupport,
|
|
16
16
|
type ManualChartResolution,
|
|
17
|
-
} from "
|
|
18
|
-
import type { TimeRange } from "
|
|
17
|
+
} from "./resolution";
|
|
18
|
+
import type { TimeRange } from "./range";
|
|
19
19
|
import type { DataProvider, MarketDataRequestContext } from "../types/data-provider";
|
|
20
20
|
import type { Quote, TickerFinancials } from "../types/financials";
|
|
21
21
|
import type { FredSeriesLoadResult, FredSeriesRequest } from "../data/fred-series";
|
|
@@ -75,8 +75,10 @@ export interface ChartResolveSources {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export interface ChartResolveOptions {
|
|
78
|
-
/** Runtime
|
|
78
|
+
/** Runtime zoom window used only to choose an adaptive Auto resolution. */
|
|
79
79
|
autoViewport?: { start: Date; end: Date } | null;
|
|
80
|
+
/** Runtime interaction window used to load history around the visible chart. */
|
|
81
|
+
requestViewport?: { start: Date; end: Date } | null;
|
|
80
82
|
/** Approximate number of horizontal observations the current surface can use. */
|
|
81
83
|
targetPointCount?: number;
|
|
82
84
|
}
|
|
@@ -122,6 +124,24 @@ function instrumentLabel(spec: Extract<ChartSeriesSpec["source"], { kind: "secur
|
|
|
122
124
|
return publicTickerKey(spec.instrument.symbol, spec.instrument.exchange);
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
function withQuoteExchange(
|
|
128
|
+
source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
|
|
129
|
+
...quotes: Array<Quote | undefined>
|
|
130
|
+
): Extract<ChartSeriesSpec["source"], { kind: "security" }> {
|
|
131
|
+
if (source.instrument.exchange?.trim()) return source;
|
|
132
|
+
const exchange = quotes
|
|
133
|
+
.map((quote) => quote?.listingExchangeName?.trim() || quote?.exchangeName?.trim())
|
|
134
|
+
.find((value): value is string => !!value);
|
|
135
|
+
if (!exchange) return source;
|
|
136
|
+
return {
|
|
137
|
+
...source,
|
|
138
|
+
instrument: {
|
|
139
|
+
...source.instrument,
|
|
140
|
+
exchange,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
125
145
|
function finiteDate(value: string | Date | undefined): Date | null {
|
|
126
146
|
if (!value) return null;
|
|
127
147
|
const date = value instanceof Date ? new Date(value) : new Date(value);
|
|
@@ -170,6 +190,18 @@ function runtimeAutoBounds(options: ChartResolveOptions): DateBounds | null {
|
|
|
170
190
|
: null;
|
|
171
191
|
}
|
|
172
192
|
|
|
193
|
+
function runtimeRequestBounds(options: ChartResolveOptions): DateBounds | null {
|
|
194
|
+
const start = options.requestViewport?.start.getTime();
|
|
195
|
+
const end = options.requestViewport?.end.getTime();
|
|
196
|
+
return typeof start === "number"
|
|
197
|
+
&& Number.isFinite(start)
|
|
198
|
+
&& typeof end === "number"
|
|
199
|
+
&& Number.isFinite(end)
|
|
200
|
+
&& start <= end
|
|
201
|
+
? { start, end }
|
|
202
|
+
: null;
|
|
203
|
+
}
|
|
204
|
+
|
|
173
205
|
function sameBounds(left: DateBounds | null, right: DateBounds | null): boolean {
|
|
174
206
|
return left?.start === right?.start && left?.end === right?.end;
|
|
175
207
|
}
|
|
@@ -450,6 +482,7 @@ function prepareBaseSeriesForStudies(
|
|
|
450
482
|
series: ResolvedSeries,
|
|
451
483
|
bounds: DateBounds,
|
|
452
484
|
clipToBounds = false,
|
|
485
|
+
fallbackBaselineBounds?: DateBounds,
|
|
453
486
|
): ResolvedSeries {
|
|
454
487
|
const baselineTransform = series.transform === "percent" || series.transform === "index100";
|
|
455
488
|
let source = series;
|
|
@@ -458,10 +491,14 @@ function prepareBaseSeriesForStudies(
|
|
|
458
491
|
} else if (clipToBounds) {
|
|
459
492
|
source = { ...series, points: filterPoints(series.points, bounds) };
|
|
460
493
|
}
|
|
494
|
+
const baseline = baselineTransform
|
|
495
|
+
? scalarBaseline(series, bounds)
|
|
496
|
+
?? (fallbackBaselineBounds ? scalarBaseline(series, fallbackBaselineBounds) : null)
|
|
497
|
+
: null;
|
|
461
498
|
return applyResolvedSeriesTransform(
|
|
462
499
|
source,
|
|
463
500
|
source.transform,
|
|
464
|
-
baselineTransform ? { baseline
|
|
501
|
+
baselineTransform ? { baseline } : undefined,
|
|
465
502
|
);
|
|
466
503
|
}
|
|
467
504
|
|
|
@@ -499,6 +536,7 @@ function applyStudyPresentationTransforms(
|
|
|
499
536
|
studies: readonly ChartSpec["studies"][number][],
|
|
500
537
|
rawSeries: readonly ResolvedSeries[],
|
|
501
538
|
visibleBounds: DateBounds,
|
|
539
|
+
fallbackBaselineBounds?: DateBounds,
|
|
502
540
|
): ResolvedSeries[] {
|
|
503
541
|
const rawById = new Map(rawSeries.map((series) => [series.id, series] as const));
|
|
504
542
|
return outputs.map((output) => {
|
|
@@ -510,6 +548,7 @@ function applyStudyPresentationTransforms(
|
|
|
510
548
|
if (!input || input.transform === "raw") return output;
|
|
511
549
|
const baseline = input.transform === "percent" || input.transform === "index100"
|
|
512
550
|
? scalarBaseline(input, visibleBounds)
|
|
551
|
+
?? (fallbackBaselineBounds ? scalarBaseline(input, fallbackBaselineBounds) : null)
|
|
513
552
|
: undefined;
|
|
514
553
|
return applyResolvedSeriesTransform(output, input.transform, { baseline });
|
|
515
554
|
});
|
|
@@ -577,10 +616,23 @@ export async function resolveChartSpecData(
|
|
|
577
616
|
}
|
|
578
617
|
return pending;
|
|
579
618
|
};
|
|
619
|
+
const sourceWithResolvedExchange = (
|
|
620
|
+
source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
|
|
621
|
+
financials: TickerFinancials | null,
|
|
622
|
+
) => {
|
|
623
|
+
const quoteOverride = sources.quoteOverrides?.get(chartQuoteOverrideKeyForSource(source));
|
|
624
|
+
return withQuoteExchange(
|
|
625
|
+
source,
|
|
626
|
+
latestQuote(financials?.quote, quoteOverride),
|
|
627
|
+
financials?.quote,
|
|
628
|
+
quoteOverride,
|
|
629
|
+
);
|
|
630
|
+
};
|
|
580
631
|
|
|
581
|
-
const
|
|
632
|
+
const adaptiveBounds = spec.viewport.resolution === "auto"
|
|
582
633
|
? runtimeAutoBounds(options)
|
|
583
634
|
: null;
|
|
635
|
+
const requestBounds = runtimeRequestBounds(options) ?? adaptiveBounds;
|
|
584
636
|
const activeMarketSources = [...new Map(spec.series.flatMap((entry) => (
|
|
585
637
|
calculationSeriesIds.has(entry.id)
|
|
586
638
|
&& entry.source.kind === "security"
|
|
@@ -588,9 +640,16 @@ export async function resolveChartSpecData(
|
|
|
588
640
|
? [[instrumentKey(entry.source), entry.source] as const]
|
|
589
641
|
: []
|
|
590
642
|
))).values()];
|
|
591
|
-
const
|
|
643
|
+
const resolutionSupportSources = adaptiveBounds
|
|
644
|
+
? await Promise.all(activeMarketSources.map(async (source) => (
|
|
645
|
+
source.instrument.exchange?.trim()
|
|
646
|
+
? source
|
|
647
|
+
: sourceWithResolvedExchange(source, await loadFinancials(source))
|
|
648
|
+
)))
|
|
649
|
+
: activeMarketSources;
|
|
650
|
+
const sharedSupport = adaptiveBounds && activeMarketSources.length > 0
|
|
592
651
|
? intersectChartResolutionSupport(await Promise.all(
|
|
593
|
-
|
|
652
|
+
resolutionSupportSources.map((source) => loadResolutionSupport(source)),
|
|
594
653
|
))
|
|
595
654
|
: [];
|
|
596
655
|
const initialResolution = requestResolution(
|
|
@@ -600,14 +659,14 @@ export async function resolveChartSpecData(
|
|
|
600
659
|
options,
|
|
601
660
|
sharedSupport,
|
|
602
661
|
);
|
|
603
|
-
const requestVisibleBounds =
|
|
662
|
+
const requestVisibleBounds = requestBounds ?? initialVisibleBounds;
|
|
604
663
|
const initialCalculationBounds = calculationBounds(
|
|
605
664
|
spec,
|
|
606
665
|
requestVisibleBounds,
|
|
607
666
|
initialResolution,
|
|
608
667
|
);
|
|
609
668
|
const hasExplicitWindow = explicitBounds(spec) !== null
|
|
610
|
-
|| (
|
|
669
|
+
|| (requestBounds !== null && !sameBounds(requestBounds, initialVisibleBounds));
|
|
611
670
|
|
|
612
671
|
const loadHistory = async (
|
|
613
672
|
source: Extract<ChartSeriesSpec["source"], { kind: "security" }>,
|
|
@@ -676,13 +735,22 @@ export async function resolveChartSpecData(
|
|
|
676
735
|
const marketField = isMarketFieldId(source.fieldId);
|
|
677
736
|
const quoteDerivedValuation = valuationSeriesUsesLiveQuote(source.fieldId);
|
|
678
737
|
const needsHistory = marketField || quoteDerivedValuation;
|
|
679
|
-
const [financials, history] = await Promise.all([
|
|
680
|
-
loadFinancials(source),
|
|
681
|
-
needsHistory ? loadHistory(source, quoteDerivedValuation) : Promise.resolve(null),
|
|
682
|
-
]);
|
|
683
738
|
const quoteOverride = marketField || quoteDerivedValuation
|
|
684
739
|
? sources.quoteOverrides?.get(chartQuoteOverrideKeyForSource(source))
|
|
685
740
|
: undefined;
|
|
741
|
+
const financialsPromise = loadFinancials(source);
|
|
742
|
+
let financials: TickerFinancials | null;
|
|
743
|
+
let history: TickerFinancials["priceHistory"] | null;
|
|
744
|
+
if (needsHistory && !source.instrument.exchange?.trim()) {
|
|
745
|
+
financials = await financialsPromise;
|
|
746
|
+
const historySource = sourceWithResolvedExchange(source, financials);
|
|
747
|
+
history = await loadHistory(historySource, quoteDerivedValuation);
|
|
748
|
+
} else {
|
|
749
|
+
[financials, history] = await Promise.all([
|
|
750
|
+
financialsPromise,
|
|
751
|
+
needsHistory ? loadHistory(source, quoteDerivedValuation) : Promise.resolve(null),
|
|
752
|
+
]);
|
|
753
|
+
}
|
|
686
754
|
const liveBarResolution = isOhlcSeriesStyle(seriesSpec.style) ? initialResolution : undefined;
|
|
687
755
|
const merged = history
|
|
688
756
|
? mergeHistory(financials, history, quoteOverride, referenceNow.getTime(), liveBarResolution)
|
|
@@ -703,6 +771,9 @@ export async function resolveChartSpecData(
|
|
|
703
771
|
}));
|
|
704
772
|
|
|
705
773
|
const rawSeries = loaded.filter((entry): entry is ResolvedSeries => !!entry);
|
|
774
|
+
const marketTimelineSeries = primaryMarketSeries?.visible === false
|
|
775
|
+
? rawSeries.filter((entry) => entry.id === primaryMarketSeries.id)
|
|
776
|
+
: [];
|
|
706
777
|
// Preset ranges describe a window ending at the requested reference time,
|
|
707
778
|
// even when the newest filing or economic observation lags that endpoint.
|
|
708
779
|
// Re-anchoring to the latest observation both mislabels the range and asks
|
|
@@ -712,7 +783,7 @@ export async function resolveChartSpecData(
|
|
|
712
783
|
const studyBounds = initialCalculationBounds;
|
|
713
784
|
const baseSeries = rawSeries
|
|
714
785
|
.filter((entry) => visibleSeriesIds.has(entry.id))
|
|
715
|
-
.map((entry) => prepareBaseSeriesForStudies(entry, bounds));
|
|
786
|
+
.map((entry) => prepareBaseSeriesForStudies(entry, bounds, false, requestVisibleBounds));
|
|
716
787
|
const calculationSeries = rawSeries.map((entry) => rawCalculationSeries(entry, studyBounds));
|
|
717
788
|
let resolved = baseSeries;
|
|
718
789
|
|
|
@@ -721,7 +792,13 @@ export async function resolveChartSpecData(
|
|
|
721
792
|
const studyResult = resolveStudies(calculationSeries, spec.studies);
|
|
722
793
|
resolved = [
|
|
723
794
|
...resolved,
|
|
724
|
-
...applyStudyPresentationTransforms(
|
|
795
|
+
...applyStudyPresentationTransforms(
|
|
796
|
+
studyResult.series,
|
|
797
|
+
spec.studies,
|
|
798
|
+
rawSeries,
|
|
799
|
+
bounds,
|
|
800
|
+
requestVisibleBounds,
|
|
801
|
+
),
|
|
725
802
|
];
|
|
726
803
|
warnings.push(...studyResult.warnings);
|
|
727
804
|
errors.push(...studyResult.errors);
|
|
@@ -742,7 +819,7 @@ export async function resolveChartSpecData(
|
|
|
742
819
|
const resolvedById = new Map(resolved.map((entry) => [entry.id, entry] as const));
|
|
743
820
|
const hiddenBaseSeries = rawSeries
|
|
744
821
|
.filter((entry) => !visibleSeriesIds.has(entry.id))
|
|
745
|
-
.map((entry) => prepareBaseSeriesForStudies(entry, bounds, true));
|
|
822
|
+
.map((entry) => prepareBaseSeriesForStudies(entry, bounds, true, requestVisibleBounds));
|
|
746
823
|
const hiddenBaseById = new Map(hiddenBaseSeries.map((entry) => [entry.id, entry] as const));
|
|
747
824
|
const legendSeries = [
|
|
748
825
|
...spec.series.flatMap((seriesSpec) => {
|
|
@@ -775,6 +852,7 @@ export async function resolveChartSpecData(
|
|
|
775
852
|
series: resolved,
|
|
776
853
|
legendSeries,
|
|
777
854
|
...(spec.viewport.maxPoints === undefined ? { bufferedSeries } : {}),
|
|
855
|
+
...(marketTimelineSeries.length > 0 ? { timelineSeries: marketTimelineSeries } : {}),
|
|
778
856
|
loading: false,
|
|
779
857
|
errors,
|
|
780
858
|
warnings: [...new Set([...priorityWarnings, ...warnings])],
|
|
@@ -281,6 +281,21 @@ describe("study resolution", () => {
|
|
|
281
281
|
expect(maxStudyWarmupPoints(specs)).toBe(33);
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
+
test("omits empty-volume noise while preserving analytical history warnings", () => {
|
|
285
|
+
const input = resolved("a");
|
|
286
|
+
input.points = input.points.map(({ volume: _volume, ...point }) => point);
|
|
287
|
+
|
|
288
|
+
const result = resolveStudies([input], [
|
|
289
|
+
study("volume", "volume", ["a"]),
|
|
290
|
+
study("sma", "sma", ["a"], { period: 100 }),
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
expect(result.series.find(({ id }) => id === "volume")?.points).toEqual([]);
|
|
294
|
+
expect(result.warnings).toEqual([
|
|
295
|
+
"sma: not enough valid history to calculate sma.",
|
|
296
|
+
]);
|
|
297
|
+
});
|
|
298
|
+
|
|
284
299
|
test("aligns pair formulas to the latest available value even when display interpolation is off", () => {
|
|
285
300
|
const point = (date: string, value: number): TimeSeriesPoint => {
|
|
286
301
|
const availableAt = new Date(`${date}T00:00:00Z`);
|
package/src/time-series/spec.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ChartResolution, TimeRange } from "
|
|
2
|
-
import { getChartResolutionLabel } from "
|
|
1
|
+
import type { ChartResolution, TimeRange } from "./range";
|
|
2
|
+
import { getChartResolutionLabel } from "./resolution";
|
|
3
3
|
import {
|
|
4
4
|
canonicalTimeSeriesFieldId,
|
|
5
5
|
getTimeSeriesField,
|
|
@@ -156,7 +156,7 @@ function studyPeriod(spec: ChartStudySpec, fallback: number): number {
|
|
|
156
156
|
return positiveInteger(spec.parameters.period, fallback);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
function studyWarmupPoints(spec: ChartStudySpec): number {
|
|
160
160
|
if (spec.kind === "sma" || spec.kind === "ema" || spec.kind === "bollinger") {
|
|
161
161
|
return studyPeriod(spec, 20) - 1;
|
|
162
162
|
}
|
|
@@ -554,7 +554,10 @@ export function resolveStudies(
|
|
|
554
554
|
}
|
|
555
555
|
outputs = resolvePairStudy(spec, input, pairedInput, color);
|
|
556
556
|
}
|
|
557
|
-
if (
|
|
557
|
+
if (
|
|
558
|
+
spec.kind !== "volume"
|
|
559
|
+
&& (outputs.length === 0 || outputs.every((output) => output.points.length === 0))
|
|
560
|
+
) {
|
|
558
561
|
warnings.push(`${spec.id}: not enough valid history to calculate ${spec.kind}.`);
|
|
559
562
|
}
|
|
560
563
|
resolved.push(...outputs);
|
package/src/time-series/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChartResolution, TimeRange } from "
|
|
1
|
+
import type { ChartResolution, TimeRange } from "./range";
|
|
2
2
|
import type { InstrumentRef } from "../market-data/request-types";
|
|
3
3
|
|
|
4
4
|
export const CHART_SPEC_VERSION = 1 as const;
|
|
@@ -154,6 +154,8 @@ export interface ChartResolutionResult {
|
|
|
154
154
|
legendSeries?: ResolvedSeries[];
|
|
155
155
|
/** Loaded observations retained outside the visible window for interactive navigation. */
|
|
156
156
|
bufferedSeries?: ResolvedSeries[];
|
|
157
|
+
/** Hidden market data retained as a deterministic session-time anchor. */
|
|
158
|
+
timelineSeries?: ResolvedSeries[];
|
|
157
159
|
loading: boolean;
|
|
158
160
|
errors: string[];
|
|
159
161
|
warnings: string[];
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act, useState } from "react";
|
|
3
|
+
import { testRender } from "../renderers/opentui/test-utils";
|
|
4
|
+
import { createTestDataProvider } from "../test-support/data-provider";
|
|
5
|
+
import type { DataProvider } from "../types/data-provider";
|
|
6
|
+
import type { PricePoint, TickerFinancials } from "../types/financials";
|
|
7
|
+
import type { ChartResolveSources } from "./resolve";
|
|
8
|
+
import { CHART_SPEC_VERSION, type ChartSpec } from "./types";
|
|
9
|
+
import {
|
|
10
|
+
useChartResolution,
|
|
11
|
+
type UseChartResolutionResult,
|
|
12
|
+
} from "./use-chart-resolution";
|
|
13
|
+
|
|
14
|
+
type AutoViewport = NonNullable<Parameters<typeof useChartResolution>[2]["autoViewport"]>;
|
|
15
|
+
|
|
16
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
17
|
+
let setAutoViewport: ((viewport: AutoViewport | null) => void) | null = null;
|
|
18
|
+
let setRequestViewport: ((viewport: AutoViewport | null) => void) | null = null;
|
|
19
|
+
let latestResult: UseChartResolutionResult | null = null;
|
|
20
|
+
|
|
21
|
+
const EMPTY_FINANCIALS: TickerFinancials = {
|
|
22
|
+
annualStatements: [],
|
|
23
|
+
quarterlyStatements: [],
|
|
24
|
+
priceHistory: [],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const INITIAL_HISTORY: PricePoint[] = [
|
|
28
|
+
{
|
|
29
|
+
date: new Date("2025-01-02T16:00:00.000Z"),
|
|
30
|
+
open: 100,
|
|
31
|
+
high: 103,
|
|
32
|
+
low: 99,
|
|
33
|
+
close: 102,
|
|
34
|
+
volume: 1_000,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
date: new Date("2025-01-03T16:00:00.000Z"),
|
|
38
|
+
open: 102,
|
|
39
|
+
high: 105,
|
|
40
|
+
low: 101,
|
|
41
|
+
close: 104,
|
|
42
|
+
volume: 1_200,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const SPEC: ChartSpec = {
|
|
47
|
+
version: CHART_SPEC_VERSION,
|
|
48
|
+
viewport: { range: "1Y", resolution: "auto" },
|
|
49
|
+
panels: [{ id: "main" }],
|
|
50
|
+
series: [{
|
|
51
|
+
id: "price",
|
|
52
|
+
source: {
|
|
53
|
+
kind: "security",
|
|
54
|
+
instrument: { symbol: "TEST", exchange: "NASDAQ" },
|
|
55
|
+
fieldId: "market.ohlcv",
|
|
56
|
+
},
|
|
57
|
+
style: "candles",
|
|
58
|
+
transform: "raw",
|
|
59
|
+
axis: "left",
|
|
60
|
+
panelId: "main",
|
|
61
|
+
interpolation: "none",
|
|
62
|
+
}],
|
|
63
|
+
studies: [],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function deferred<T>() {
|
|
67
|
+
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
68
|
+
let reject!: (reason?: unknown) => void;
|
|
69
|
+
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
|
70
|
+
resolve = resolvePromise;
|
|
71
|
+
reject = rejectPromise;
|
|
72
|
+
});
|
|
73
|
+
return { promise, resolve, reject };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sourcesFor(dataProvider: DataProvider): ChartResolveSources {
|
|
77
|
+
return {
|
|
78
|
+
dataProvider,
|
|
79
|
+
now: new Date("2025-01-10T00:00:00.000Z"),
|
|
80
|
+
loadFredSeries: async () => {
|
|
81
|
+
throw new Error("FRED is unused in this test.");
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function ResolutionHarness({
|
|
87
|
+
sources,
|
|
88
|
+
}: {
|
|
89
|
+
sources: ChartResolveSources;
|
|
90
|
+
}) {
|
|
91
|
+
const [autoViewport, setAutoViewportState] = useState<AutoViewport | null>(null);
|
|
92
|
+
const [requestViewport, setRequestViewportState] = useState<AutoViewport | null>(null);
|
|
93
|
+
setAutoViewport = setAutoViewportState;
|
|
94
|
+
setRequestViewport = setRequestViewportState;
|
|
95
|
+
latestResult = useChartResolution(SPEC, sources, {
|
|
96
|
+
autoViewport,
|
|
97
|
+
requestViewport,
|
|
98
|
+
targetPointCount: 100,
|
|
99
|
+
liveRefreshIntervalMs: 0,
|
|
100
|
+
});
|
|
101
|
+
const pointCount = (latestResult.bufferedSeries ?? latestResult.series)
|
|
102
|
+
.reduce((total, series) => total + series.points.length, 0);
|
|
103
|
+
return <text>{`${latestResult.loading ? "loading" : "settled"}:${pointCount}`}</text>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function flushEffects(iterations = 1): Promise<void> {
|
|
107
|
+
for (let iteration = 0; iteration < iterations; iteration += 1) {
|
|
108
|
+
await act(async () => {
|
|
109
|
+
await Promise.resolve();
|
|
110
|
+
await testSetup!.renderOnce();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function waitFor(predicate: () => boolean): Promise<void> {
|
|
116
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
117
|
+
await flushEffects();
|
|
118
|
+
if (predicate()) return;
|
|
119
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
120
|
+
}
|
|
121
|
+
throw new Error("Timed out waiting for chart resolution.");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
afterEach(async () => {
|
|
125
|
+
if (testSetup) {
|
|
126
|
+
await act(async () => {
|
|
127
|
+
testSetup!.renderer.destroy();
|
|
128
|
+
});
|
|
129
|
+
testSetup = undefined;
|
|
130
|
+
}
|
|
131
|
+
setAutoViewport = null;
|
|
132
|
+
setRequestViewport = null;
|
|
133
|
+
latestResult = null;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("useChartResolution", () => {
|
|
137
|
+
test("keeps renderable data settled through empty adaptive and live refreshes", async () => {
|
|
138
|
+
const adaptiveHistory = deferred<PricePoint[]>();
|
|
139
|
+
let detailedCalls = 0;
|
|
140
|
+
let quoteHandler: Parameters<NonNullable<DataProvider["subscribeQuotes"]>>[1] | null = null;
|
|
141
|
+
let quoteTarget: Parameters<NonNullable<DataProvider["subscribeQuotes"]>>[0][number] | null = null;
|
|
142
|
+
const provider = createTestDataProvider({
|
|
143
|
+
getTickerFinancials: async () => EMPTY_FINANCIALS,
|
|
144
|
+
getChartResolutionSupport: () => [
|
|
145
|
+
{ resolution: "15m", maxRange: "1M" },
|
|
146
|
+
{ resolution: "1d", maxRange: "ALL" },
|
|
147
|
+
],
|
|
148
|
+
getPriceHistoryForResolution: async (_symbol, _exchange, _range, resolution) => (
|
|
149
|
+
resolution === "1d" ? INITIAL_HISTORY : []
|
|
150
|
+
),
|
|
151
|
+
getDetailedPriceHistory: async () => {
|
|
152
|
+
detailedCalls += 1;
|
|
153
|
+
return adaptiveHistory.promise;
|
|
154
|
+
},
|
|
155
|
+
getPriceHistory: async () => [],
|
|
156
|
+
subscribeQuotes: (targets, onQuote) => {
|
|
157
|
+
quoteTarget = targets[0] ?? null;
|
|
158
|
+
quoteHandler = onQuote;
|
|
159
|
+
return () => {};
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
const sources = sourcesFor(provider);
|
|
163
|
+
testSetup = await testRender(<ResolutionHarness sources={sources} />, {
|
|
164
|
+
width: 24,
|
|
165
|
+
height: 1,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await waitFor(() => latestResult?.loading === false && latestResult.series[0]?.points.length === 2);
|
|
169
|
+
const lastGoodSeries = latestResult!.series;
|
|
170
|
+
const lastGoodBufferedSeries = latestResult!.bufferedSeries;
|
|
171
|
+
|
|
172
|
+
await act(async () => {
|
|
173
|
+
setAutoViewport?.({
|
|
174
|
+
start: new Date("2025-01-02T00:00:00.000Z"),
|
|
175
|
+
end: new Date("2025-01-04T00:00:00.000Z"),
|
|
176
|
+
});
|
|
177
|
+
await testSetup!.renderOnce();
|
|
178
|
+
});
|
|
179
|
+
await waitFor(() => detailedCalls === 1);
|
|
180
|
+
|
|
181
|
+
expect(latestResult?.loading).toBe(false);
|
|
182
|
+
expect(latestResult?.series).toBe(lastGoodSeries);
|
|
183
|
+
expect(latestResult?.bufferedSeries).toBe(lastGoodBufferedSeries);
|
|
184
|
+
|
|
185
|
+
adaptiveHistory.resolve([]);
|
|
186
|
+
await flushEffects(3);
|
|
187
|
+
|
|
188
|
+
expect(latestResult?.loading).toBe(false);
|
|
189
|
+
expect(latestResult?.series).toBe(lastGoodSeries);
|
|
190
|
+
expect(latestResult?.bufferedSeries).toBe(lastGoodBufferedSeries);
|
|
191
|
+
expect(latestResult?.errors).toEqual([]);
|
|
192
|
+
|
|
193
|
+
await act(async () => {
|
|
194
|
+
quoteHandler!(quoteTarget!, {
|
|
195
|
+
symbol: "TEST",
|
|
196
|
+
price: 105,
|
|
197
|
+
currency: "USD",
|
|
198
|
+
change: 1,
|
|
199
|
+
changePercent: 0.96,
|
|
200
|
+
lastUpdated: Date.parse("2025-01-03T20:00:00.000Z"),
|
|
201
|
+
});
|
|
202
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
203
|
+
await testSetup!.renderOnce();
|
|
204
|
+
});
|
|
205
|
+
await flushEffects(3);
|
|
206
|
+
|
|
207
|
+
expect(latestResult?.loading).toBe(false);
|
|
208
|
+
expect(latestResult?.series).toBe(lastGoodSeries);
|
|
209
|
+
expect(latestResult?.bufferedSeries).toBe(lastGoodBufferedSeries);
|
|
210
|
+
expect(latestResult?.errors).toEqual([]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("keeps renderable data through an empty panned-history live refresh", async () => {
|
|
214
|
+
let detailedCalls = 0;
|
|
215
|
+
let fixedHistoryCalls = 0;
|
|
216
|
+
let quoteHandler: Parameters<NonNullable<DataProvider["subscribeQuotes"]>>[1] | null = null;
|
|
217
|
+
let quoteTarget: Parameters<NonNullable<DataProvider["subscribeQuotes"]>>[0][number] | null = null;
|
|
218
|
+
const provider = createTestDataProvider({
|
|
219
|
+
getTickerFinancials: async () => EMPTY_FINANCIALS,
|
|
220
|
+
getPriceHistoryForResolution: async () => {
|
|
221
|
+
fixedHistoryCalls += 1;
|
|
222
|
+
return fixedHistoryCalls === 1 ? INITIAL_HISTORY : [];
|
|
223
|
+
},
|
|
224
|
+
getDetailedPriceHistory: async () => {
|
|
225
|
+
detailedCalls += 1;
|
|
226
|
+
return [];
|
|
227
|
+
},
|
|
228
|
+
getPriceHistory: async () => [],
|
|
229
|
+
subscribeQuotes: (targets, onQuote) => {
|
|
230
|
+
quoteTarget = targets[0] ?? null;
|
|
231
|
+
quoteHandler = onQuote;
|
|
232
|
+
return () => {};
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
const sources = sourcesFor(provider);
|
|
236
|
+
testSetup = await testRender(<ResolutionHarness sources={sources} />, {
|
|
237
|
+
width: 24,
|
|
238
|
+
height: 1,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await waitFor(() => latestResult?.loading === false && latestResult.series[0]?.points.length === 2);
|
|
242
|
+
const lastGoodSeries = latestResult!.series;
|
|
243
|
+
const lastGoodBufferedSeries = latestResult!.bufferedSeries;
|
|
244
|
+
|
|
245
|
+
await act(async () => {
|
|
246
|
+
setRequestViewport?.({
|
|
247
|
+
start: new Date("2024-12-20T00:00:00.000Z"),
|
|
248
|
+
end: new Date("2024-12-27T00:00:00.000Z"),
|
|
249
|
+
});
|
|
250
|
+
await testSetup!.renderOnce();
|
|
251
|
+
});
|
|
252
|
+
await waitFor(() => detailedCalls === 1);
|
|
253
|
+
await flushEffects(3);
|
|
254
|
+
|
|
255
|
+
expect(latestResult?.series).toBe(lastGoodSeries);
|
|
256
|
+
expect(latestResult?.bufferedSeries).toBe(lastGoodBufferedSeries);
|
|
257
|
+
|
|
258
|
+
await act(async () => {
|
|
259
|
+
quoteHandler!(quoteTarget!, {
|
|
260
|
+
symbol: "TEST",
|
|
261
|
+
price: 105,
|
|
262
|
+
currency: "USD",
|
|
263
|
+
change: 1,
|
|
264
|
+
changePercent: 0.96,
|
|
265
|
+
lastUpdated: Date.parse("2025-01-03T20:00:00.000Z"),
|
|
266
|
+
});
|
|
267
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
268
|
+
await testSetup!.renderOnce();
|
|
269
|
+
});
|
|
270
|
+
await flushEffects(3);
|
|
271
|
+
|
|
272
|
+
expect(latestResult?.loading).toBe(false);
|
|
273
|
+
expect(latestResult?.series).toBe(lastGoodSeries);
|
|
274
|
+
expect(latestResult?.bufferedSeries).toBe(lastGoodBufferedSeries);
|
|
275
|
+
expect(latestResult?.errors).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("keeps renderable data when an adaptive viewport request rejects", async () => {
|
|
279
|
+
const provider = createTestDataProvider({
|
|
280
|
+
getTickerFinancials: async () => EMPTY_FINANCIALS,
|
|
281
|
+
getPriceHistoryForResolution: async () => INITIAL_HISTORY,
|
|
282
|
+
});
|
|
283
|
+
const sources = sourcesFor(provider);
|
|
284
|
+
testSetup = await testRender(<ResolutionHarness sources={sources} />, {
|
|
285
|
+
width: 24,
|
|
286
|
+
height: 1,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await waitFor(() => latestResult?.loading === false && latestResult.series[0]?.points.length === 2);
|
|
290
|
+
const lastGoodSeries = latestResult!.series;
|
|
291
|
+
const lastGoodBufferedSeries = latestResult!.bufferedSeries;
|
|
292
|
+
provider.getChartResolutionSupport = () => {
|
|
293
|
+
throw new Error("adaptive history failed");
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
await act(async () => {
|
|
297
|
+
setAutoViewport?.({
|
|
298
|
+
start: new Date("2025-01-02T00:00:00.000Z"),
|
|
299
|
+
end: new Date("2025-01-04T00:00:00.000Z"),
|
|
300
|
+
});
|
|
301
|
+
await testSetup!.renderOnce();
|
|
302
|
+
});
|
|
303
|
+
await flushEffects(3);
|
|
304
|
+
|
|
305
|
+
expect(latestResult?.loading).toBe(false);
|
|
306
|
+
expect(latestResult?.series).toBe(lastGoodSeries);
|
|
307
|
+
expect(latestResult?.bufferedSeries).toBe(lastGoodBufferedSeries);
|
|
308
|
+
expect(latestResult?.errors).toEqual([]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("keeps initial and explicit reloads blocking", async () => {
|
|
312
|
+
const initialHistory = deferred<PricePoint[]>();
|
|
313
|
+
const reloadHistory = deferred<PricePoint[]>();
|
|
314
|
+
let historyCalls = 0;
|
|
315
|
+
const provider = createTestDataProvider({
|
|
316
|
+
getTickerFinancials: async () => EMPTY_FINANCIALS,
|
|
317
|
+
getPriceHistoryForResolution: async () => {
|
|
318
|
+
historyCalls += 1;
|
|
319
|
+
return historyCalls === 1 ? initialHistory.promise : reloadHistory.promise;
|
|
320
|
+
},
|
|
321
|
+
getPriceHistory: async () => [],
|
|
322
|
+
});
|
|
323
|
+
const sources = sourcesFor(provider);
|
|
324
|
+
testSetup = await testRender(<ResolutionHarness sources={sources} />, {
|
|
325
|
+
width: 24,
|
|
326
|
+
height: 1,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await waitFor(() => historyCalls === 1);
|
|
330
|
+
expect(latestResult?.loading).toBe(true);
|
|
331
|
+
expect(latestResult?.series).toEqual([]);
|
|
332
|
+
|
|
333
|
+
await act(async () => {
|
|
334
|
+
initialHistory.resolve(INITIAL_HISTORY);
|
|
335
|
+
await testSetup!.renderOnce();
|
|
336
|
+
});
|
|
337
|
+
await waitFor(() => latestResult?.loading === false && latestResult.series[0]?.points.length === 2);
|
|
338
|
+
await act(async () => {
|
|
339
|
+
latestResult!.reload();
|
|
340
|
+
await testSetup!.renderOnce();
|
|
341
|
+
});
|
|
342
|
+
await waitFor(() => historyCalls === 2);
|
|
343
|
+
|
|
344
|
+
expect(latestResult?.loading).toBe(true);
|
|
345
|
+
|
|
346
|
+
await act(async () => {
|
|
347
|
+
reloadHistory.resolve([]);
|
|
348
|
+
await testSetup!.renderOnce();
|
|
349
|
+
});
|
|
350
|
+
await waitFor(() => (
|
|
351
|
+
latestResult?.loading === false
|
|
352
|
+
&& latestResult.series.every((series) => series.points.length === 0)
|
|
353
|
+
));
|
|
354
|
+
|
|
355
|
+
expect(latestResult?.series.every((series) => series.points.length === 0)).toBe(true);
|
|
356
|
+
expect(latestResult?.errors).toEqual([]);
|
|
357
|
+
});
|
|
358
|
+
});
|