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
package/src/theme/colors.test.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
applyTheme,
|
|
4
4
|
clearTransientThemePreview,
|
|
5
5
|
getCurrentThemeId,
|
|
6
|
+
getThemeColors,
|
|
6
7
|
previewTheme,
|
|
7
8
|
syncTheme,
|
|
8
9
|
} from "./colors";
|
|
@@ -43,6 +44,15 @@ afterEach(() => {
|
|
|
43
44
|
});
|
|
44
45
|
|
|
45
46
|
describe("theme colors", () => {
|
|
47
|
+
test("provides immutable palette snapshots while the compatibility facade follows the active theme", () => {
|
|
48
|
+
const amber = getThemeColors(DEFAULT_THEME);
|
|
49
|
+
const midnight = getThemeColors("midnight");
|
|
50
|
+
|
|
51
|
+
expect(Object.isFrozen(amber)).toBe(true);
|
|
52
|
+
expect(Object.isFrozen(midnight)).toBe(true);
|
|
53
|
+
expect(amber).not.toBe(midnight);
|
|
54
|
+
});
|
|
55
|
+
|
|
46
56
|
test("syncs CSS variables when a theme is applied", () => {
|
|
47
57
|
const values = installDocumentStyleMock();
|
|
48
58
|
const theme = getTheme("midnight");
|
package/src/theme/colors.ts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { blendForContrast, blendHex, higherContrast } from "./color-utils";
|
|
2
2
|
import { getTheme, DEFAULT_THEME, type Theme } from "./themes";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export
|
|
4
|
+
export type ThemeColors = Readonly<Omit<Theme, "name" | "description">>;
|
|
5
|
+
|
|
6
|
+
export function getThemeColors(id: string): ThemeColors {
|
|
7
|
+
const { name: _name, description: _description, ...palette } = getTheme(id);
|
|
8
|
+
return Object.freeze(palette);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let currentColors = getThemeColors(DEFAULT_THEME);
|
|
12
|
+
|
|
13
|
+
/** Compatibility facade for non-React formatting and rendering helpers. */
|
|
14
|
+
export const colors = new Proxy({ ...currentColors } as ThemeColors, {
|
|
15
|
+
get: (_target, property) => currentColors[property as keyof ThemeColors],
|
|
16
|
+
getOwnPropertyDescriptor: (_target, property) => ({
|
|
17
|
+
configurable: true,
|
|
18
|
+
enumerable: true,
|
|
19
|
+
value: currentColors[property as keyof ThemeColors],
|
|
20
|
+
}),
|
|
21
|
+
}) as ThemeColors;
|
|
7
22
|
|
|
8
23
|
type ColorKey = keyof typeof colors;
|
|
9
24
|
|
|
@@ -38,10 +53,8 @@ export function getCurrentThemeId(): string {
|
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
export function applyTheme(id: string): void {
|
|
41
|
-
const theme = getTheme(id);
|
|
42
56
|
currentThemeId = id;
|
|
43
|
-
|
|
44
|
-
Object.assign(colors, theme);
|
|
57
|
+
currentColors = getThemeColors(id);
|
|
45
58
|
syncThemeCssVariables();
|
|
46
59
|
}
|
|
47
60
|
|
|
@@ -73,22 +86,6 @@ export function syncTheme(id: string): void {
|
|
|
73
86
|
|
|
74
87
|
export { blendHex } from "./color-utils";
|
|
75
88
|
|
|
76
|
-
export function getComparisonSeriesColor(index: number): string {
|
|
77
|
-
const seriesColors = [
|
|
78
|
-
colors.borderFocused,
|
|
79
|
-
colors.warning,
|
|
80
|
-
colors.positive,
|
|
81
|
-
colors.negative,
|
|
82
|
-
colors.textBright,
|
|
83
|
-
colors.neutral,
|
|
84
|
-
blendHex(colors.borderFocused, colors.positive, 0.45),
|
|
85
|
-
blendHex(colors.warning, colors.negative, 0.35),
|
|
86
|
-
blendHex(colors.textBright, colors.positive, 0.35),
|
|
87
|
-
blendHex(colors.borderFocused, colors.negative, 0.35),
|
|
88
|
-
];
|
|
89
|
-
return seriesColors[((index % seriesColors.length) + seriesColors.length) % seriesColors.length]!;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
89
|
export function getChartIndicatorColor(index: number): string {
|
|
93
90
|
const accent = higherContrast(colors.warning, colors.borderFocused, colors.bg);
|
|
94
91
|
const indicatorColors = [
|
|
@@ -112,8 +109,8 @@ export function getChartIndicatorColor(index: number): string {
|
|
|
112
109
|
}
|
|
113
110
|
|
|
114
111
|
/** Returns a hover background color derived from bg and selected */
|
|
115
|
-
export function hoverBg(): string {
|
|
116
|
-
return blendHex(
|
|
112
|
+
export function hoverBg(palette: ThemeColors = colors): string {
|
|
113
|
+
return blendHex(palette.bg, palette.selected, 0.5);
|
|
117
114
|
}
|
|
118
115
|
|
|
119
116
|
function syncThemeCssVariables(): void {
|
|
@@ -126,62 +123,62 @@ function syncThemeCssVariables(): void {
|
|
|
126
123
|
cssThemeId = currentThemeId;
|
|
127
124
|
cssThemeDocument = documentLike;
|
|
128
125
|
for (const [key, name] of THEME_CSS_VARIABLES) {
|
|
129
|
-
style.setProperty(name,
|
|
126
|
+
style.setProperty(name, currentColors[key]);
|
|
130
127
|
}
|
|
131
128
|
style.setProperty("--gloom-hover-bg", hoverBg());
|
|
132
129
|
}
|
|
133
130
|
|
|
134
|
-
export function commandBarBg(): string {
|
|
135
|
-
const base = higherContrast(
|
|
136
|
-
const accent = higherContrast(
|
|
137
|
-
return blendForContrast(base,
|
|
131
|
+
export function commandBarBg(palette: ThemeColors = colors): string {
|
|
132
|
+
const base = higherContrast(palette.commandBg, palette.panel, palette.bg);
|
|
133
|
+
const accent = higherContrast(palette.textBright, palette.borderFocused, base);
|
|
134
|
+
return blendForContrast(base, palette.bg, accent, 1.45);
|
|
138
135
|
}
|
|
139
136
|
|
|
140
|
-
export function commandBarPanelBg(): string {
|
|
141
|
-
return blendHex(commandBarBg(),
|
|
137
|
+
export function commandBarPanelBg(palette: ThemeColors = colors): string {
|
|
138
|
+
return blendHex(commandBarBg(palette), palette.panel, 0.28);
|
|
142
139
|
}
|
|
143
140
|
|
|
144
|
-
export function commandBarInputBg(): string {
|
|
145
|
-
return blendHex(commandBarPanelBg(),
|
|
141
|
+
export function commandBarInputBg(palette: ThemeColors = colors): string {
|
|
142
|
+
return blendHex(commandBarPanelBg(palette), palette.bg, 0.22);
|
|
146
143
|
}
|
|
147
144
|
|
|
148
|
-
export function commandBarSelectedBg(): string {
|
|
149
|
-
const base = commandBarBg();
|
|
150
|
-
const accent = higherContrast(
|
|
151
|
-
return blendForContrast(
|
|
145
|
+
export function commandBarSelectedBg(palette: ThemeColors = colors): string {
|
|
146
|
+
const base = commandBarBg(palette);
|
|
147
|
+
const accent = higherContrast(palette.selectedText, palette.textBright, palette.selected);
|
|
148
|
+
return blendForContrast(palette.selected, base, accent, 1.45);
|
|
152
149
|
}
|
|
153
150
|
|
|
154
|
-
export function commandBarHoverBg(): string {
|
|
155
|
-
return blendHex(commandBarBg(), commandBarSelectedBg(), 0.45);
|
|
151
|
+
export function commandBarHoverBg(palette: ThemeColors = colors): string {
|
|
152
|
+
return blendHex(commandBarBg(palette), commandBarSelectedBg(palette), 0.45);
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
export function commandBarText(): string {
|
|
159
|
-
const base = commandBarBg();
|
|
160
|
-
const fallback = higherContrast(
|
|
161
|
-
return blendForContrast(
|
|
155
|
+
export function commandBarText(palette: ThemeColors = colors): string {
|
|
156
|
+
const base = commandBarBg(palette);
|
|
157
|
+
const fallback = higherContrast(palette.textBright, "#f2f2f2", base);
|
|
158
|
+
return blendForContrast(palette.text, base, fallback, 5.4);
|
|
162
159
|
}
|
|
163
160
|
|
|
164
|
-
export function commandBarHeadingText(): string {
|
|
165
|
-
const base = commandBarBg();
|
|
161
|
+
export function commandBarHeadingText(palette: ThemeColors = colors): string {
|
|
162
|
+
const base = commandBarBg(palette);
|
|
166
163
|
const fallback = higherContrast(
|
|
167
|
-
higherContrast(
|
|
168
|
-
higherContrast(
|
|
164
|
+
higherContrast(palette.textDim, palette.text, base),
|
|
165
|
+
higherContrast(palette.textBright, palette.selectedText, base),
|
|
169
166
|
base,
|
|
170
167
|
);
|
|
171
|
-
return blendForContrast(
|
|
168
|
+
return blendForContrast(palette.textMuted, base, fallback, 3.6);
|
|
172
169
|
}
|
|
173
170
|
|
|
174
|
-
export function commandBarSubtleText(): string {
|
|
175
|
-
const base = commandBarBg();
|
|
176
|
-
const fallback = higherContrast(commandBarText(), "#d8d8d8", base);
|
|
177
|
-
return blendForContrast(
|
|
171
|
+
export function commandBarSubtleText(palette: ThemeColors = colors): string {
|
|
172
|
+
const base = commandBarBg(palette);
|
|
173
|
+
const fallback = higherContrast(commandBarText(palette), "#d8d8d8", base);
|
|
174
|
+
return blendForContrast(palette.textDim, base, fallback, 4.1);
|
|
178
175
|
}
|
|
179
176
|
|
|
180
|
-
export function commandBarSelectedText(): string {
|
|
181
|
-
const base = commandBarSelectedBg();
|
|
182
|
-
const preferred = higherContrast(
|
|
177
|
+
export function commandBarSelectedText(palette: ThemeColors = colors): string {
|
|
178
|
+
const base = commandBarSelectedBg(palette);
|
|
179
|
+
const preferred = higherContrast(palette.selectedText, palette.text, base);
|
|
183
180
|
const fallback = higherContrast(
|
|
184
|
-
higherContrast(
|
|
181
|
+
higherContrast(palette.textBright, palette.text, base),
|
|
185
182
|
higherContrast("#ffffff", "#000000", base),
|
|
186
183
|
base,
|
|
187
184
|
);
|
|
@@ -189,44 +186,44 @@ export function commandBarSelectedText(): string {
|
|
|
189
186
|
}
|
|
190
187
|
|
|
191
188
|
/** Background for docked pane bodies */
|
|
192
|
-
export function paneBg(focused: boolean): string {
|
|
193
|
-
if (focused) return blendHex(
|
|
194
|
-
return blendHex(
|
|
189
|
+
export function paneBg(focused: boolean, palette: ThemeColors = colors): string {
|
|
190
|
+
if (focused) return blendHex(palette.bg, palette.borderFocused, 0.06);
|
|
191
|
+
return blendHex(palette.panel, palette.border, 0.08);
|
|
195
192
|
}
|
|
196
193
|
|
|
197
194
|
/** Background for floating pane bodies — elevated above docked panes */
|
|
198
|
-
export function floatingPaneBg(focused: boolean): string {
|
|
199
|
-
if (focused) return blendHex(
|
|
200
|
-
return blendHex(
|
|
195
|
+
export function floatingPaneBg(focused: boolean, palette: ThemeColors = colors): string {
|
|
196
|
+
if (focused) return blendHex(palette.bg, palette.borderFocused, 0.08);
|
|
197
|
+
return blendHex(palette.panel, palette.border, 0.18);
|
|
201
198
|
}
|
|
202
199
|
|
|
203
200
|
/** Background for pane title bars */
|
|
204
|
-
export function paneTitleBg(focused: boolean): string {
|
|
205
|
-
if (focused) return blendHex(
|
|
206
|
-
return blendHex(
|
|
201
|
+
export function paneTitleBg(focused: boolean, palette: ThemeColors = colors): string {
|
|
202
|
+
if (focused) return blendHex(palette.bg, palette.borderFocused, 0.22);
|
|
203
|
+
return blendHex(palette.panel, palette.border, 0.15);
|
|
207
204
|
}
|
|
208
205
|
|
|
209
206
|
/** Background for floating pane title bars */
|
|
210
|
-
export function floatingPaneTitleBg(focused: boolean): string {
|
|
211
|
-
if (focused) return blendHex(
|
|
212
|
-
return blendHex(
|
|
207
|
+
export function floatingPaneTitleBg(focused: boolean, palette: ThemeColors = colors): string {
|
|
208
|
+
if (focused) return blendHex(palette.bg, palette.borderFocused, 0.25);
|
|
209
|
+
return blendHex(palette.panel, palette.border, 0.25);
|
|
213
210
|
}
|
|
214
211
|
|
|
215
212
|
/** Title text color for panes */
|
|
216
|
-
export function paneTitleText(focused: boolean, floating = false): string {
|
|
217
|
-
const background = floating ? floatingPaneTitleBg(focused) : paneTitleBg(focused);
|
|
213
|
+
export function paneTitleText(focused: boolean, floating = false, palette: ThemeColors = colors): string {
|
|
214
|
+
const background = floating ? floatingPaneTitleBg(focused, palette) : paneTitleBg(focused, palette);
|
|
218
215
|
const preferred = focused
|
|
219
|
-
? higherContrast(
|
|
220
|
-
:
|
|
216
|
+
? higherContrast(palette.textBright, palette.headerText, background)
|
|
217
|
+
: palette.textDim;
|
|
221
218
|
const fallback = focused
|
|
222
|
-
? higherContrast(
|
|
223
|
-
: higherContrast(
|
|
219
|
+
? higherContrast(palette.text, "#f2f2f2", background)
|
|
220
|
+
: higherContrast(palette.text, palette.textBright, background);
|
|
224
221
|
return blendForContrast(preferred, background, fallback, focused ? 5.2 : 4.5);
|
|
225
222
|
}
|
|
226
223
|
|
|
227
224
|
/** Returns green for positive, red for negative, neutral for zero */
|
|
228
|
-
export function priceColor(value: number): string {
|
|
229
|
-
if (value > 0) return
|
|
230
|
-
if (value < 0) return
|
|
231
|
-
return
|
|
225
|
+
export function priceColor(value: number, palette: ThemeColors = colors): string {
|
|
226
|
+
if (value > 0) return palette.positive;
|
|
227
|
+
if (value < 0) return palette.negative;
|
|
228
|
+
return palette.neutral;
|
|
232
229
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createTestRenderer } from "@opentui/core/testing";
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createOpenTuiTestRoot as createRoot } from "../renderers/opentui/test-utils";
|
|
5
|
+
import { commandBarBg, getThemeColors, paneBg, syncTheme } from "./colors";
|
|
6
|
+
import { ThemeProvider, useThemeColors } from "./theme-context";
|
|
7
|
+
import { DEFAULT_THEME } from "./themes";
|
|
8
|
+
|
|
9
|
+
let testSetup: Awaited<ReturnType<typeof createTestRenderer>> | undefined;
|
|
10
|
+
let root: ReturnType<typeof createRoot> | undefined;
|
|
11
|
+
const actEnvironment = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean };
|
|
12
|
+
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
|
|
13
|
+
|
|
14
|
+
function ThemeProbe() {
|
|
15
|
+
const colors = useThemeColors();
|
|
16
|
+
return <text>{`${colors.bg}|${commandBarBg(colors)}|${paneBg(true, colors)}`}</text>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
if (root) {
|
|
21
|
+
await act(async () => {
|
|
22
|
+
root?.unmount();
|
|
23
|
+
await Promise.resolve();
|
|
24
|
+
});
|
|
25
|
+
root = undefined;
|
|
26
|
+
}
|
|
27
|
+
testSetup?.renderer.destroy();
|
|
28
|
+
testSetup = undefined;
|
|
29
|
+
syncTheme(DEFAULT_THEME);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("ThemeProvider", () => {
|
|
33
|
+
test("render helpers switch with the context palette in the same render", async () => {
|
|
34
|
+
testSetup = await createTestRenderer({ width: 120, height: 2 });
|
|
35
|
+
root = createRoot(testSetup.renderer);
|
|
36
|
+
|
|
37
|
+
act(() => {
|
|
38
|
+
root?.render(
|
|
39
|
+
<ThemeProvider themeId={DEFAULT_THEME}>
|
|
40
|
+
<ThemeProbe />
|
|
41
|
+
</ThemeProvider>,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
await act(async () => {
|
|
45
|
+
await testSetup?.renderOnce();
|
|
46
|
+
await testSetup?.renderOnce();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const nextThemeId = "midnight";
|
|
50
|
+
const nextColors = getThemeColors(nextThemeId);
|
|
51
|
+
act(() => {
|
|
52
|
+
root?.render(
|
|
53
|
+
<ThemeProvider themeId={nextThemeId}>
|
|
54
|
+
<ThemeProbe />
|
|
55
|
+
</ThemeProvider>,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
await act(async () => {
|
|
59
|
+
await testSetup?.renderOnce();
|
|
60
|
+
await testSetup?.renderOnce();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(testSetup.captureCharFrame()).toContain(
|
|
64
|
+
`${nextColors.bg}|${commandBarBg(nextColors)}|${paneBg(true, nextColors)}`,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import {
|
|
3
|
+
colors,
|
|
4
|
+
getCurrentThemeId,
|
|
5
|
+
getThemeColors,
|
|
6
|
+
syncTheme,
|
|
7
|
+
type ThemeColors,
|
|
8
|
+
} from "./colors";
|
|
6
9
|
|
|
7
10
|
interface ThemeContextValue {
|
|
8
11
|
themeId: string;
|
|
@@ -13,7 +16,8 @@ const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
|
13
16
|
|
|
14
17
|
export function ThemeProvider({ themeId, children }: { themeId: string; children: ReactNode }) {
|
|
15
18
|
syncTheme(themeId);
|
|
16
|
-
const
|
|
19
|
+
const themeColors = useMemo(() => getThemeColors(themeId), [themeId]);
|
|
20
|
+
const value = useMemo(() => ({ themeId, colors: themeColors }), [themeColors, themeId]);
|
|
17
21
|
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AppTickerRepositoryPort } from "../core/app-service-ports";
|
|
2
2
|
import type { SearchRequestContext, DataProvider } from "../types/data-provider";
|
|
3
3
|
import type { TickerRecord } from "../types/ticker";
|
|
4
4
|
import {
|
|
@@ -21,7 +21,7 @@ export async function resolveTickerOpenTarget({
|
|
|
21
21
|
query: string;
|
|
22
22
|
tickers: ReadonlyMap<string, TickerRecord>;
|
|
23
23
|
dataProvider: DataProvider;
|
|
24
|
-
tickerRepository:
|
|
24
|
+
tickerRepository: AppTickerRepositoryPort;
|
|
25
25
|
searchContext?: SearchRequestContext;
|
|
26
26
|
}): Promise<TickerOpenTarget | null> {
|
|
27
27
|
const symbol = normalizeTickerInput(null, query);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { InstrumentSearchResult } from "../../types/instrument";
|
|
2
2
|
import type { TickerRecord, TickerMetadata } from "../../types/ticker";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AppTickerRepositoryPort } from "../../core/app-service-ports";
|
|
4
4
|
import {
|
|
5
5
|
classifyInstrumentKind,
|
|
6
6
|
} from "./ranking";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./result";
|
|
11
11
|
|
|
12
12
|
export async function upsertTickerFromSearchResult(
|
|
13
|
-
tickerRepository:
|
|
13
|
+
tickerRepository: AppTickerRepositoryPort,
|
|
14
14
|
result: InstrumentSearchResult,
|
|
15
15
|
): Promise<{ ticker: TickerRecord; created: boolean }> {
|
|
16
16
|
const symbol = getSearchResultSymbol(result);
|
|
@@ -127,13 +127,6 @@ export function alignTimeSeries(
|
|
|
127
127
|
return rows;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
export function firstCommonValueDate(series: readonly ResolvedSeries[]): Date | null {
|
|
131
|
-
const row = alignTimeSeries(series, { mode: "intersection" }).find((entry) => (
|
|
132
|
-
series.every(({ id }) => finiteNumber(entry.values[id]?.value))
|
|
133
|
-
));
|
|
134
|
-
return row ? new Date(row.date) : null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
130
|
/** Returns visible points and optionally the prior step anchor needed to draw continuity. */
|
|
138
131
|
export function clipSeriesToWindow(
|
|
139
132
|
series: ResolvedSeries,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { PricePoint, Quote } from "../types/financials";
|
|
2
|
+
import { isQuoteStaleForCurrentSession } from "../market-data/quotes/freshness";
|
|
3
|
+
import { hasLikelyQuoteUnitMismatch } from "../utils/currency-units";
|
|
4
|
+
import {
|
|
5
|
+
CHART_RESOLUTION_STEP_MS,
|
|
6
|
+
type ManualChartResolution,
|
|
7
|
+
} from "./resolution";
|
|
8
|
+
|
|
9
|
+
const MAX_LIVE_QUOTE_TAIL_AGE_MS = 7 * 24 * 60 * 60_000;
|
|
10
|
+
const MAX_LIVE_QUOTE_CLOCK_SKEW_MS = 5 * 60_000;
|
|
11
|
+
const MAX_INTRADAY_BAR_INTERVAL_MS = 6 * 60 * 60_000;
|
|
12
|
+
const MIN_LIVE_QUOTE_TAIL_GAP_MS = 5 * 60_000;
|
|
13
|
+
|
|
14
|
+
export type AppendLiveQuotePointOptions =
|
|
15
|
+
| {
|
|
16
|
+
now?: number;
|
|
17
|
+
mode?: "scalar";
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
now?: number;
|
|
21
|
+
mode: "ohlc";
|
|
22
|
+
resolution: ManualChartResolution;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function coerceDate(value: Date | string | number): Date {
|
|
26
|
+
return value instanceof Date ? value : new Date(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getPointTime(point: Pick<PricePoint, "date">): number {
|
|
30
|
+
return coerceDate(point.date as Date | string | number).getTime();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getActiveQuotePrice(quote: Quote): number {
|
|
34
|
+
if ((quote.marketState === "PRE" || quote.marketState === "PREPRE") && quote.preMarketPrice != null) {
|
|
35
|
+
return quote.preMarketPrice;
|
|
36
|
+
}
|
|
37
|
+
if ((quote.marketState === "POST" || quote.marketState === "POSTPOST") && quote.postMarketPrice != null) {
|
|
38
|
+
return quote.postMarketPrice;
|
|
39
|
+
}
|
|
40
|
+
return quote.price;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function quoteBelongsToLatestBar(
|
|
44
|
+
latestTime: number,
|
|
45
|
+
quoteTime: number,
|
|
46
|
+
resolution: ManualChartResolution,
|
|
47
|
+
): boolean {
|
|
48
|
+
if (quoteTime < latestTime) return false;
|
|
49
|
+
if (resolution === "1mo") {
|
|
50
|
+
const latestDate = new Date(latestTime);
|
|
51
|
+
const quoteDate = new Date(quoteTime);
|
|
52
|
+
return latestDate.getUTCFullYear() === quoteDate.getUTCFullYear()
|
|
53
|
+
&& latestDate.getUTCMonth() === quoteDate.getUTCMonth();
|
|
54
|
+
}
|
|
55
|
+
return quoteTime - latestTime < CHART_RESOLUTION_STEP_MS[resolution];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function finiteOrFallback(value: number | undefined, fallback: number): number {
|
|
59
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mergeQuoteIntoLatestBar(latest: PricePoint, quotePrice: number): PricePoint {
|
|
63
|
+
const open = finiteOrFallback(latest.open, latest.close);
|
|
64
|
+
const high = finiteOrFallback(latest.high, Math.max(open, latest.close));
|
|
65
|
+
const low = finiteOrFallback(latest.low, Math.min(open, latest.close));
|
|
66
|
+
return {
|
|
67
|
+
...latest,
|
|
68
|
+
open,
|
|
69
|
+
high: Math.max(high, open, latest.close, quotePrice),
|
|
70
|
+
low: Math.min(low, open, latest.close, quotePrice),
|
|
71
|
+
close: quotePrice,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function appendLiveQuotePoint(
|
|
76
|
+
points: PricePoint[],
|
|
77
|
+
quote: Quote | null | undefined,
|
|
78
|
+
options: AppendLiveQuotePointOptions = {},
|
|
79
|
+
): PricePoint[] {
|
|
80
|
+
const now = options.now ?? Date.now();
|
|
81
|
+
if (!quote || isQuoteStaleForCurrentSession(quote, now)) return points;
|
|
82
|
+
|
|
83
|
+
const quoteTime = quote.lastUpdated;
|
|
84
|
+
const quotePrice = getActiveQuotePrice(quote);
|
|
85
|
+
if (
|
|
86
|
+
!Number.isFinite(quoteTime)
|
|
87
|
+
|| !Number.isFinite(quotePrice)
|
|
88
|
+
|| quotePrice <= 0
|
|
89
|
+
|| quoteTime > now + MAX_LIVE_QUOTE_CLOCK_SKEW_MS
|
|
90
|
+
|| now - quoteTime > MAX_LIVE_QUOTE_TAIL_AGE_MS
|
|
91
|
+
) {
|
|
92
|
+
return points;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const latest = points.at(-1);
|
|
96
|
+
if (!latest) return points;
|
|
97
|
+
|
|
98
|
+
const latestTime = getPointTime(latest);
|
|
99
|
+
if (!Number.isFinite(latestTime) || quoteTime < latestTime) return points;
|
|
100
|
+
|
|
101
|
+
const previous = points.at(-2);
|
|
102
|
+
const latestInterval = previous ? latestTime - getPointTime(previous) : Number.NaN;
|
|
103
|
+
if (
|
|
104
|
+
latestInterval > 0
|
|
105
|
+
&& latestInterval <= MAX_INTRADAY_BAR_INTERVAL_MS
|
|
106
|
+
&& quoteTime - latestTime > Math.max(MIN_LIVE_QUOTE_TAIL_GAP_MS, latestInterval * 3)
|
|
107
|
+
) {
|
|
108
|
+
return points;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const latestClose = latest.close;
|
|
112
|
+
if (hasLikelyQuoteUnitMismatch(
|
|
113
|
+
{ currency: quote.currency, price: latestClose },
|
|
114
|
+
{ currency: quote.currency, price: quotePrice },
|
|
115
|
+
)) {
|
|
116
|
+
return points;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (options.mode === "ohlc") {
|
|
120
|
+
if (quoteBelongsToLatestBar(latestTime, quoteTime, options.resolution)) {
|
|
121
|
+
const merged = mergeQuoteIntoLatestBar(latest, quotePrice);
|
|
122
|
+
return [...points.slice(0, -1), merged];
|
|
123
|
+
}
|
|
124
|
+
return [
|
|
125
|
+
...points,
|
|
126
|
+
{
|
|
127
|
+
date: new Date(quoteTime),
|
|
128
|
+
open: quotePrice,
|
|
129
|
+
high: quotePrice,
|
|
130
|
+
low: quotePrice,
|
|
131
|
+
close: quotePrice,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (quoteTime === latestTime) return points;
|
|
137
|
+
|
|
138
|
+
return [
|
|
139
|
+
...points,
|
|
140
|
+
{
|
|
141
|
+
date: new Date(quoteTime),
|
|
142
|
+
close: quotePrice,
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { TIME_RANGES, type ChartDateWindow, type TimeRange } from "./range";
|
|
2
|
+
|
|
3
|
+
export interface DateWindowRange extends ChartDateWindow {}
|
|
4
|
+
|
|
5
|
+
export function subtractTimeRange(endDate: Date, range: TimeRange): Date {
|
|
6
|
+
const startDate = new Date(endDate);
|
|
7
|
+
switch (range) {
|
|
8
|
+
case "1D":
|
|
9
|
+
startDate.setDate(startDate.getDate() - 1);
|
|
10
|
+
break;
|
|
11
|
+
case "1W":
|
|
12
|
+
startDate.setDate(startDate.getDate() - 7);
|
|
13
|
+
break;
|
|
14
|
+
case "1M":
|
|
15
|
+
startDate.setMonth(startDate.getMonth() - 1);
|
|
16
|
+
break;
|
|
17
|
+
case "3M":
|
|
18
|
+
startDate.setMonth(startDate.getMonth() - 3);
|
|
19
|
+
break;
|
|
20
|
+
case "6M":
|
|
21
|
+
startDate.setMonth(startDate.getMonth() - 6);
|
|
22
|
+
break;
|
|
23
|
+
case "1Y":
|
|
24
|
+
startDate.setFullYear(startDate.getFullYear() - 1);
|
|
25
|
+
break;
|
|
26
|
+
case "5Y":
|
|
27
|
+
startDate.setFullYear(startDate.getFullYear() - 5);
|
|
28
|
+
break;
|
|
29
|
+
case "ALL":
|
|
30
|
+
startDate.setFullYear(startDate.getFullYear() - 50);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
return startDate;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isDateWindowWithinTimeRange(startDate: Date, endDate: Date, maxRange: TimeRange): boolean {
|
|
37
|
+
if (maxRange === "ALL") return true;
|
|
38
|
+
return startDate.getTime() >= subtractTimeRange(endDate, maxRange).getTime();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getTimeRangeForDateWindow(window: DateWindowRange | null): TimeRange {
|
|
42
|
+
if (!window?.start || !window.end) return "ALL";
|
|
43
|
+
return TIME_RANGES.find((candidate) => isDateWindowWithinTimeRange(window.start!, window.end!, candidate)) ?? "ALL";
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { subtractTimeRange } from "./date-window";
|
|
2
|
+
import type { TimeRange } from "./range";
|
|
3
|
+
import type { PricePoint } from "../types/financials";
|
|
4
|
+
import { getPricePointTimestamp } from "../utils/price-history";
|
|
5
|
+
|
|
6
|
+
export function clipPriceHistoryToRange(points: PricePoint[], range: TimeRange): PricePoint[] {
|
|
7
|
+
if (range === "ALL" || points.length === 0) return points;
|
|
8
|
+
const dated = points.flatMap((point) => {
|
|
9
|
+
const timestamp = getPricePointTimestamp(point);
|
|
10
|
+
return Number.isFinite(timestamp) ? [{ point, timestamp }] : [];
|
|
11
|
+
});
|
|
12
|
+
const endTimestamp = dated.reduce<number | null>((latest, entry) => (
|
|
13
|
+
latest == null || entry.timestamp > latest ? entry.timestamp : latest
|
|
14
|
+
), null);
|
|
15
|
+
if (endTimestamp == null) return [];
|
|
16
|
+
const end = new Date(endTimestamp);
|
|
17
|
+
const start = subtractTimeRange(end, range);
|
|
18
|
+
return dated
|
|
19
|
+
.filter(({ timestamp }) => timestamp >= start.getTime() && timestamp <= endTimestamp)
|
|
20
|
+
.map(({ point }) => point);
|
|
21
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildComparisonChartPreset } from "../plugins/builtin/chart-composer/presets";
|
|
3
|
+
import type { TickerRecord } from "../types/ticker";
|
|
4
|
+
import { hydrateChartSpecInstruments } from "./hooks";
|
|
5
|
+
|
|
6
|
+
function ticker(symbol: string, exchange: string): TickerRecord {
|
|
7
|
+
return {
|
|
8
|
+
metadata: {
|
|
9
|
+
ticker: symbol,
|
|
10
|
+
exchange,
|
|
11
|
+
currency: "USD",
|
|
12
|
+
name: symbol,
|
|
13
|
+
portfolios: [],
|
|
14
|
+
watchlists: [],
|
|
15
|
+
positions: [],
|
|
16
|
+
custom: {},
|
|
17
|
+
tags: [],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("hydrateChartSpecInstruments", () => {
|
|
23
|
+
test("fills missing exchanges without replacing explicit chart identities", () => {
|
|
24
|
+
const initial = buildComparisonChartPreset(["SNDK", "SAP:XETR"]);
|
|
25
|
+
const spec = {
|
|
26
|
+
...initial,
|
|
27
|
+
series: initial.series.map((entry, index) => (
|
|
28
|
+
index === 0 && entry.source.kind === "security"
|
|
29
|
+
? {
|
|
30
|
+
...entry,
|
|
31
|
+
source: {
|
|
32
|
+
...entry.source,
|
|
33
|
+
instrument: { ...entry.source.instrument, exchange: "" },
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
: entry
|
|
37
|
+
)),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const hydrated = hydrateChartSpecInstruments(spec, new Map([
|
|
41
|
+
["SNDK", ticker("SNDK", "NASDAQ")],
|
|
42
|
+
["SAP", ticker("SAP", "JSE")],
|
|
43
|
+
]));
|
|
44
|
+
|
|
45
|
+
expect(hydrated.series[0]?.source).toMatchObject({
|
|
46
|
+
kind: "security",
|
|
47
|
+
instrument: { symbol: "SNDK", exchange: "NASDAQ" },
|
|
48
|
+
});
|
|
49
|
+
expect(hydrated.series[1]?.source).toMatchObject({
|
|
50
|
+
kind: "security",
|
|
51
|
+
instrument: { symbol: "SAP", exchange: "XETRA" },
|
|
52
|
+
});
|
|
53
|
+
expect(spec.series[0]?.source).toMatchObject({
|
|
54
|
+
kind: "security",
|
|
55
|
+
instrument: { symbol: "SNDK", exchange: "" },
|
|
56
|
+
});
|
|
57
|
+
expect(hydrateChartSpecInstruments(hydrated, new Map())).toBe(hydrated);
|
|
58
|
+
});
|
|
59
|
+
});
|