gloomberb 0.9.7 → 0.9.9
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 +1 -1
- package/src/api-client/auth.ts +20 -0
- package/src/api-client/index.test.ts +3 -3
- package/src/api-client/index.ts +48 -6
- package/src/api-client/request.test.ts +63 -0
- package/src/api-client/request.ts +53 -5
- package/src/api-client/types.ts +45 -2
- package/src/app/pane-runtime/layout-placement.ts +0 -1
- package/src/app/runtime/desktop-deeplink.test.ts +87 -0
- package/src/app/runtime/desktop-deeplink.ts +496 -0
- package/src/app.tsx +34 -0
- package/src/brokers/account-cache.ts +2 -10
- package/src/brokers/broker-portfolio-sync.ts +0 -1
- package/src/brokers/broker-ticker-sync.ts +20 -16
- package/src/brokers/remote-broker-adapter.ts +3 -0
- package/src/capabilities/registry.test.ts +25 -0
- package/src/capabilities/registry.ts +22 -5
- package/src/cli/commands/automation.ts +1 -3
- package/src/cli/commands/command-utils.ts +0 -7
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/plugins.ts +34 -17
- package/src/cli/index.ts +2 -2
- package/src/cli.test.ts +0 -157
- package/src/components/chart/bar-chart-renderer.test.ts +87 -12
- package/src/components/chart/bar-chart-renderer.ts +111 -55
- package/src/components/chart/comparison/renderer.ts +0 -1
- package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
- package/src/components/chart/core/index.test.ts +0 -24
- package/src/components/chart/core/renderer.ts +0 -4
- package/src/components/chart/core/resolution.test.ts +0 -17
- package/src/components/chart/core/resolution.ts +0 -26
- package/src/components/chart/core/scroll.test.ts +0 -6
- package/src/components/chart/core/scroll.ts +0 -22
- package/src/components/chart/core/viewport.test.ts +2 -23
- package/src/components/chart/core/viewport.ts +0 -27
- package/src/components/chart/market-session.ts +1 -1
- package/src/components/chart/native/surface/hooks.ts +10 -3
- package/src/components/chart/static/bar-chart-surface.tsx +104 -20
- package/src/components/chart/stock-chart/data-runtime.ts +0 -1
- package/src/components/chart/stock-chart/footer.test.ts +65 -0
- package/src/components/chart/stock-chart/footer.ts +68 -13
- package/src/components/chart/stock-chart/resolved.tsx +18 -4
- package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
- package/src/components/command-bar/helpers.test.ts +1 -80
- package/src/components/command-bar/helpers.ts +1 -36
- package/src/components/command-bar/pane-settings.ts +1 -1
- package/src/components/command-bar/pane-templates/items.ts +11 -3
- package/src/components/command-bar/panel/runtime.ts +0 -2
- package/src/components/command-bar/routes/root/results.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
- package/src/components/command-bar/surface/index.test.tsx +52 -16
- package/src/components/command-bar/surface/index.tsx +0 -1
- package/src/components/command-bar/workflow/ops.ts +81 -20
- package/src/components/feed-data-table/stack-view.test.tsx +36 -130
- package/src/components/feed-data-table/stack-view.tsx +0 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/shell/index.test.tsx +2 -113
- package/src/components/layout/shell/index.tsx +0 -1
- package/src/components/layout/shell/menu.ts +0 -1
- package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
- package/src/components/onboarding/onboarding-wizard.tsx +3 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -23
- package/src/components/price-selector-dialog.tsx +3 -3
- package/src/components/ticker/badge/text.tsx +0 -1
- package/src/components/toggle-list.tsx +29 -73
- package/src/components/ui/button.tsx +4 -1
- package/src/components/ui/checkbox.tsx +76 -0
- package/src/components/ui/choice-dialog.tsx +15 -3
- package/src/components/ui/frame.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/message-composer.tsx +4 -0
- package/src/components/ui/native-select.tsx +4 -2
- package/src/components/ui/page-stack-view.tsx +1 -1
- package/src/components/ui/ui.test.tsx +0 -78
- package/src/core/state/app/layout-reducer.ts +0 -1
- package/src/data/config/layout.ts +3 -3
- package/src/data/resource-store.ts +6 -0
- package/src/data/sqlite/cache.test.ts +5 -0
- package/src/data/ticker-store.ts +0 -7
- package/src/market-data/coordinator/auxiliary.ts +1 -44
- package/src/market-data/coordinator/entries.ts +0 -1
- package/src/market-data/coordinator/events.test.ts +49 -0
- package/src/market-data/coordinator/events.ts +25 -4
- package/src/market-data/coordinator/index.test.ts +223 -10
- package/src/market-data/coordinator/index.ts +41 -21
- package/src/market-data/coordinator/quotes.ts +13 -11
- package/src/market-data/coordinator/subscriptions.test.ts +1 -0
- package/src/market-data/hooks.tsx +17 -13
- package/src/market-data/market/freshness.ts +58 -1
- package/src/market-data/market/status.ts +19 -1
- package/src/market-data/query-store.ts +0 -4
- package/src/market-data/quotes/freshness.test.ts +49 -1
- package/src/market-data/quotes/freshness.ts +8 -0
- package/src/market-data/quotes/resolution.test.ts +185 -0
- package/src/market-data/quotes/resolution.ts +96 -10
- package/src/market-data/request-types.test.ts +61 -1
- package/src/market-data/request-types.ts +42 -8
- package/src/market-data/result-types.ts +0 -11
- package/src/market-data/selectors.ts +1 -5
- package/src/news/aggregator.ts +5 -1
- package/src/pane-settings.test.ts +0 -18
- package/src/plugins/builtin/account-management/footer.ts +2 -5
- package/src/plugins/builtin/account-management/form-components.tsx +175 -59
- package/src/plugins/builtin/account-management/keyboard.ts +32 -2
- package/src/plugins/builtin/account-management/model.test.ts +99 -4
- package/src/plugins/builtin/account-management/model.ts +259 -23
- package/src/plugins/builtin/account-management/pane.tsx +784 -155
- package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
- package/src/plugins/builtin/buildout/format.ts +2 -11
- package/src/plugins/builtin/chat/chat.test.tsx +127 -1
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
- package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
- package/src/plugins/builtin/chat/content/composer.tsx +84 -1
- package/src/plugins/builtin/chat/content/index.tsx +153 -16
- package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
- package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
- package/src/plugins/builtin/chat/content/mentions.ts +88 -0
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
- package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
- package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
- package/src/plugins/builtin/chat/controller/index.ts +0 -10
- package/src/plugins/builtin/chat/controller.test.ts +5 -5
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
- package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
- package/src/plugins/builtin/chat/sidebar.tsx +26 -2
- package/src/plugins/builtin/chat/test-harness.tsx +22 -12
- package/src/plugins/builtin/cloud/plugin.tsx +8 -1
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
- package/src/plugins/builtin/congress-trades/model.ts +2 -7
- package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
- package/src/plugins/builtin/correlation/index.tsx +4 -1
- package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
- package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
- package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
- package/src/plugins/builtin/earnings/index.tsx +0 -9
- package/src/plugins/builtin/econ/index.tsx +1 -5
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/pane.tsx +0 -5
- package/src/plugins/builtin/insider/index.tsx +1 -8
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
- package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -2
- package/src/plugins/builtin/news/index.tsx +0 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
- package/src/plugins/builtin/options/view.test.tsx +1 -38
- package/src/plugins/builtin/plugin-groups.ts +17 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
- package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
- package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
- package/src/plugins/builtin/sec/filing-documents.ts +0 -11
- package/src/plugins/builtin/sec/index.tsx +0 -2
- package/src/plugins/builtin/substack/article-detail.tsx +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +0 -15
- package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
- package/src/plugins/builtin/world-indices/index.tsx +0 -4
- package/src/plugins/builtin/world-indices/model.ts +0 -7
- package/src/plugins/ibkr/account-cache.ts +1 -1
- package/src/plugins/ibkr/gateway/market-data.ts +12 -5
- package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
- package/src/plugins/ibkr/portfolio-performance.ts +1 -1
- package/src/plugins/ibkr/trade/utils.ts +1 -7
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/prediction-markets/detail.test.tsx +0 -34
- package/src/plugins/registry/context.ts +15 -0
- package/src/plugins/registry/contributions.ts +22 -11
- package/src/plugins/registry/index.test.ts +44 -0
- package/src/plugins/registry/index.ts +75 -26
- package/src/remote/controller.ts +1 -7
- package/src/remote/json-patch.ts +15 -1
- package/src/remote/server.ts +16 -6
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
- package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +42 -0
- package/src/renderers/electrobun/shared/protocol.ts +5 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
- package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
- package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
- package/src/renderers/electrobun/view/host/box.tsx +0 -2
- package/src/renderers/electrobun/view/host/mouse.ts +0 -11
- package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
- package/src/renderers/electrobun/view/host/text.tsx +1 -2
- package/src/renderers/electrobun/view/http-fetch.ts +15 -2
- package/src/renderers/electrobun/view/main.tsx +3 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
- package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
- package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
- package/src/renderers/electrobun/view/ui-host.tsx +2 -0
- package/src/renderers/opentui/host.tsx +11 -3
- package/src/renderers/opentui/input-host.tsx +5 -1
- package/src/sources/gloomberb-cloud/index.test.ts +0 -72
- package/src/sources/gloomberb-cloud/index.ts +1 -30
- package/src/sources/gloomberb-cloud/news.ts +0 -12
- package/src/sources/provider-router/batches.ts +11 -3
- package/src/sources/provider-router/cached-financials.test.ts +123 -0
- package/src/sources/provider-router/financial-routes.ts +77 -5
- package/src/sources/provider-router/financials.test.ts +49 -0
- package/src/sources/provider-router/financials.ts +40 -3
- package/src/sources/provider-router/index.test.ts +314 -0
- package/src/sources/provider-router/primary.ts +17 -3
- package/src/sources/provider-router/search.ts +5 -0
- package/src/sources/provider-router/streaming.ts +6 -1
- package/src/sources/provider-router/supplemental.ts +4 -3
- package/src/sources/sec-edgar.test.ts +42 -0
- package/src/sources/sec-edgar.ts +1 -2
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/sources/yahoo-finance/mappers.test.ts +29 -0
- package/src/sources/yahoo-finance/mappers.ts +2 -1
- package/src/sources/yahoo-finance/options.ts +59 -14
- package/src/sources/yahoo-finance/quote-summary.ts +4 -1
- package/src/sources/yahoo-finance/requests.ts +2 -1
- package/src/sources/yahoo-finance/snapshots.ts +13 -2
- package/src/sources/yahoo-finance.test.ts +40 -21
- package/src/sources/yahoo-finance.ts +19 -10
- package/src/state/app/context/index.test.tsx +0 -81
- package/src/state/app/context/selector.test.tsx +6 -2
- package/src/state/hooks/quote-streaming.test.ts +14 -1
- package/src/state/hooks/quote-streaming.ts +5 -4
- package/src/sync/controller.test.ts +31 -0
- package/src/sync/controller.ts +300 -0
- package/src/sync/core-contributors.test.ts +251 -0
- package/src/sync/core-contributors.ts +400 -0
- package/src/sync/profile-analytics.ts +39 -0
- package/src/sync/react.ts +53 -0
- package/src/sync/types.ts +77 -0
- package/src/test-support/data-provider.ts +0 -1
- package/src/types/desktop-deeplink.ts +7 -0
- package/src/types/plugin.ts +3 -0
- package/src/ui/host.tsx +15 -1
- package/src/ui/index.tsx +2 -0
- package/src/updater.test.ts +0 -21
- package/src/updater.ts +20 -16
- package/src/utils/async-deadline.ts +26 -0
- package/src/utils/exchanges.test.ts +11 -1
- package/src/utils/exchanges.ts +5 -1
- package/src/utils/selection-clipboard.ts +2 -2
- package/src/version.ts +1 -1
- package/src/components/layout/pane/header.test.tsx +0 -39
- package/src/plugins/builtin/chat/channels.test.ts +0 -28
- package/src/plugins/builtin/chat/index.tsx +0 -4
- package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
- package/src/plugins/builtin/earnings/index.test.ts +0 -12
- package/src/plugins/builtin/help/index.test.tsx +0 -242
- package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
- package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
- package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
- package/src/renderers/electrobun/view/host/text.test.ts +0 -22
- package/src/renderers/opentui/dialog-host.test.tsx +0 -44
- /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { DataProvider, SecFilingDocument, SecFilingItem } from "../../types/data-provider";
|
|
2
|
-
import type { NewsArticle } from "../../news/types";
|
|
3
2
|
import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../../types/financials";
|
|
4
|
-
import type { ChartRequest, InstrumentRef,
|
|
3
|
+
import type { ChartRequest, InstrumentRef, OptionsRequest, SecFilingsRequest } from "../request-types";
|
|
5
4
|
import { QueryStore } from "../query-store";
|
|
6
5
|
import type { QueryEntry } from "../result-types";
|
|
7
6
|
import {
|
|
@@ -9,7 +8,6 @@ import {
|
|
|
9
8
|
buildChartKey,
|
|
10
9
|
buildFxKey,
|
|
11
10
|
buildTickerFinancialsSnapshot,
|
|
12
|
-
buildNewsKey,
|
|
13
11
|
buildOptionsKey,
|
|
14
12
|
buildQuoteKey,
|
|
15
13
|
buildSecContentKey,
|
|
@@ -19,6 +17,8 @@ import {
|
|
|
19
17
|
resolveEntryData,
|
|
20
18
|
toMarketDataContext,
|
|
21
19
|
} from "../selectors";
|
|
20
|
+
import { resolveTickerFinancialsQuoteState } from "../quotes/resolution";
|
|
21
|
+
import { hasLikelyQuoteUnitMismatch } from "../../utils/currency-units";
|
|
22
22
|
import { normalizePriceHistory } from "../../utils/price-history";
|
|
23
23
|
import {
|
|
24
24
|
createBaselineChartRequest,
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
import {
|
|
40
40
|
loadArticleSummaryEntry,
|
|
41
41
|
loadFxRateEntry,
|
|
42
|
-
loadNewsEntry,
|
|
43
42
|
loadOptionsEntry,
|
|
44
43
|
loadSecFilingContentEntry,
|
|
45
44
|
loadSecFilingDocumentsEntry,
|
|
@@ -71,7 +70,6 @@ export class MarketDataCoordinator {
|
|
|
71
70
|
private readonly fundamentalsStore = new QueryStore<TickerFinancials["fundamentals"]>((key) => this.events.bump(key));
|
|
72
71
|
private readonly statementsStore = new QueryStore<Pick<TickerFinancials, "annualStatements" | "quarterlyStatements">>((key) => this.events.bump(key));
|
|
73
72
|
private readonly chartStore = new QueryStore<PricePoint[]>((key) => this.events.bump(key));
|
|
74
|
-
private readonly newsStore = new QueryStore<NewsArticle[]>((key) => this.events.bump(key));
|
|
75
73
|
private readonly optionsStore = new QueryStore<OptionsChain>((key) => this.events.bump(key));
|
|
76
74
|
private readonly secFilingsStore = new QueryStore<SecFilingItem[]>((key) => this.events.bump(key));
|
|
77
75
|
private readonly secDocumentsStore = new QueryStore<SecFilingDocument[]>((key) => this.events.bump(key));
|
|
@@ -122,10 +120,6 @@ export class MarketDataCoordinator {
|
|
|
122
120
|
return this.chartStore.get(buildChartKey(request));
|
|
123
121
|
}
|
|
124
122
|
|
|
125
|
-
getNewsEntry(request: NewsRequest): QueryEntry<NewsArticle[]> {
|
|
126
|
-
return this.newsStore.get(buildNewsKey(request));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
123
|
getOptionsEntry(request: OptionsRequest): QueryEntry<OptionsChain> {
|
|
130
124
|
return this.optionsStore.get(buildOptionsKey(request));
|
|
131
125
|
}
|
|
@@ -218,6 +212,7 @@ export class MarketDataCoordinator {
|
|
|
218
212
|
options,
|
|
219
213
|
quoteStore: this.quoteStore,
|
|
220
214
|
runSingleFlight: (key, task) => this.runSingleFlight(key, task),
|
|
215
|
+
resolveQuote: (targetInstrument, quote) => this.resolveIncomingQuote(targetInstrument, quote),
|
|
221
216
|
});
|
|
222
217
|
}
|
|
223
218
|
|
|
@@ -231,6 +226,7 @@ export class MarketDataCoordinator {
|
|
|
231
226
|
options,
|
|
232
227
|
quoteStore: this.quoteStore,
|
|
233
228
|
runSingleFlight: (key, task) => this.runSingleFlight(key, task),
|
|
229
|
+
resolveQuote: (targetInstrument, quote) => this.resolveIncomingQuote(targetInstrument, quote),
|
|
234
230
|
});
|
|
235
231
|
}
|
|
236
232
|
|
|
@@ -307,15 +303,6 @@ export class MarketDataCoordinator {
|
|
|
307
303
|
});
|
|
308
304
|
}
|
|
309
305
|
|
|
310
|
-
async loadNews(request: NewsRequest): Promise<QueryEntry<NewsArticle[]>> {
|
|
311
|
-
return loadNewsEntry({
|
|
312
|
-
dataProvider: this.dataProvider,
|
|
313
|
-
request,
|
|
314
|
-
store: this.newsStore,
|
|
315
|
-
runSingleFlight: (key, task) => this.runSingleFlight(key, task),
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
306
|
async loadOptions(request: OptionsRequest): Promise<QueryEntry<OptionsChain>> {
|
|
320
307
|
return loadOptionsEntry({
|
|
321
308
|
dataProvider: this.dataProvider,
|
|
@@ -377,10 +364,43 @@ export class MarketDataCoordinator {
|
|
|
377
364
|
private applyStreamQuote(instrument: InstrumentRef, quote: Quote): void {
|
|
378
365
|
const key = buildQuoteKey(instrument);
|
|
379
366
|
const current = this.quoteStore.get(key);
|
|
380
|
-
|
|
367
|
+
const resolvedQuote = this.resolveIncomingQuote(instrument, quote);
|
|
368
|
+
if (areStreamQuotesEquivalent(current.data ?? current.lastGoodData, resolvedQuote)) return;
|
|
381
369
|
const receivedAt = Date.now();
|
|
382
|
-
const attempts = [createAttempt(
|
|
383
|
-
this.quoteStore.set(key, readyQuoteEntry(current, { ...
|
|
370
|
+
const attempts = [createAttempt(resolvedQuote.providerId ?? this.dataProvider.id, receivedAt, "success")];
|
|
371
|
+
this.quoteStore.set(key, readyQuoteEntry(current, { ...resolvedQuote, receivedAt }, resolvedQuote.providerId ?? this.dataProvider.id, attempts));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private resolveIncomingQuote(instrument: InstrumentRef, quote: Quote): Quote {
|
|
375
|
+
const snapshot = resolveEntryData(this.snapshotStore.get(buildSnapshotKey(instrument)));
|
|
376
|
+
if (snapshot?.quote) {
|
|
377
|
+
if (hasLikelyQuoteUnitMismatch(snapshot.quote, quote)) return quote;
|
|
378
|
+
if (
|
|
379
|
+
typeof snapshot.quote.lastUpdated === "number"
|
|
380
|
+
&& typeof quote.lastUpdated === "number"
|
|
381
|
+
&& quote.lastUpdated < snapshot.quote.lastUpdated
|
|
382
|
+
) {
|
|
383
|
+
return quote;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const cachedFinancials = snapshot ?? this.readCachedFinancialsForInstrument(instrument);
|
|
387
|
+
return resolveTickerFinancialsQuoteState(cachedFinancials, quote)?.quote ?? quote;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private readCachedFinancialsForInstrument(instrument: InstrumentRef): TickerFinancials | null {
|
|
391
|
+
if (!this.dataProvider.getCachedFinancialsForTargets) return null;
|
|
392
|
+
const result = this.dataProvider.getCachedFinancialsForTargets([{
|
|
393
|
+
symbol: instrument.symbol,
|
|
394
|
+
exchange: instrument.exchange,
|
|
395
|
+
brokerId: instrument.brokerId,
|
|
396
|
+
brokerInstanceId: instrument.brokerInstanceId,
|
|
397
|
+
instrument: instrument.instrument ?? null,
|
|
398
|
+
}], {
|
|
399
|
+
allowExpired: true,
|
|
400
|
+
includeStaleQuotes: true,
|
|
401
|
+
});
|
|
402
|
+
if (result instanceof Promise) return null;
|
|
403
|
+
return result.get(instrument.symbol.trim().toUpperCase()) ?? null;
|
|
384
404
|
}
|
|
385
405
|
}
|
|
386
406
|
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
readyQuoteEntry,
|
|
16
16
|
} from "./entries";
|
|
17
17
|
|
|
18
|
-
export type QuoteSubscriptionPriority = Pick<QuoteSubscriptionTarget, "surface" | "visible" | "selected" | "weight">;
|
|
18
|
+
export type QuoteSubscriptionPriority = Pick<QuoteSubscriptionTarget, "route" | "surface" | "visible" | "selected" | "weight">;
|
|
19
19
|
|
|
20
20
|
export interface QuoteSubscriptionEntry {
|
|
21
21
|
target: QuoteSubscriptionTarget;
|
|
@@ -123,6 +123,7 @@ interface LoadQuoteEntryOptions {
|
|
|
123
123
|
options?: CoordinatorLoadOptions;
|
|
124
124
|
quoteStore: QueryStore<Quote>;
|
|
125
125
|
runSingleFlight: CoordinatorSingleFlight;
|
|
126
|
+
resolveQuote?: (instrument: InstrumentRef, quote: Quote) => Quote;
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
export async function loadQuoteEntry({
|
|
@@ -131,6 +132,7 @@ export async function loadQuoteEntry({
|
|
|
131
132
|
options = {},
|
|
132
133
|
quoteStore,
|
|
133
134
|
runSingleFlight,
|
|
135
|
+
resolveQuote,
|
|
134
136
|
}: LoadQuoteEntryOptions): Promise<QueryEntry<Quote>> {
|
|
135
137
|
const key = buildQuoteKey(instrument);
|
|
136
138
|
const flightKey = options.forceRefresh ? `${key}|refresh` : key;
|
|
@@ -146,9 +148,10 @@ export async function loadQuoteEntry({
|
|
|
146
148
|
cacheMode: options.forceRefresh ? "refresh" : "default",
|
|
147
149
|
},
|
|
148
150
|
);
|
|
149
|
-
const
|
|
151
|
+
const resolvedQuote = resolveQuote?.(instrument, quote) ?? quote;
|
|
152
|
+
const source = resolvedQuote.providerId ?? dataProvider.id;
|
|
150
153
|
const attempts = [createAttempt(source, startedAt, "success")];
|
|
151
|
-
return quoteStore.update(key, (current) => readyQuoteEntry(current,
|
|
154
|
+
return quoteStore.update(key, (current) => readyQuoteEntry(current, resolvedQuote, source, attempts));
|
|
152
155
|
} catch (error) {
|
|
153
156
|
const classified = classifyError(error);
|
|
154
157
|
const attempt = createAttempt(dataProvider.id, startedAt, EXPECTED_EMPTY.test(classified.message) ? "empty" : "fatal_error", classified.reasonCode, classified.message);
|
|
@@ -163,6 +166,7 @@ interface LoadQuoteBatchEntriesOptions {
|
|
|
163
166
|
options?: CoordinatorLoadOptions;
|
|
164
167
|
quoteStore: QueryStore<Quote>;
|
|
165
168
|
runSingleFlight: CoordinatorSingleFlight;
|
|
169
|
+
resolveQuote?: (instrument: InstrumentRef, quote: Quote) => Quote;
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
export async function loadQuoteBatchEntries({
|
|
@@ -171,6 +175,7 @@ export async function loadQuoteBatchEntries({
|
|
|
171
175
|
options = {},
|
|
172
176
|
quoteStore,
|
|
173
177
|
runSingleFlight,
|
|
178
|
+
resolveQuote,
|
|
174
179
|
}: LoadQuoteBatchEntriesOptions): Promise<QueryEntry<Quote>[]> {
|
|
175
180
|
const uniqueInstruments = [...new Map(instruments.map((instrument) => [buildQuoteKey(instrument), instrument] as const)).values()];
|
|
176
181
|
const results = new Map<string, QueryEntry<Quote>>();
|
|
@@ -195,9 +200,10 @@ export async function loadQuoteBatchEntries({
|
|
|
195
200
|
const instrument = misses[index];
|
|
196
201
|
if (!instrument || !item.quote) return;
|
|
197
202
|
const key = buildQuoteKey(instrument);
|
|
198
|
-
const
|
|
203
|
+
const quote = resolveQuote?.(instrument, item.quote) ?? item.quote;
|
|
204
|
+
const source = quote.providerId ?? dataProvider.id;
|
|
199
205
|
const attempts = [createAttempt(source, Date.now(), "success")];
|
|
200
|
-
results.set(key, quoteStore.update(key, (current) => readyQuoteEntry(current,
|
|
206
|
+
results.set(key, quoteStore.update(key, (current) => readyQuoteEntry(current, quote, source, attempts)));
|
|
201
207
|
});
|
|
202
208
|
}
|
|
203
209
|
|
|
@@ -210,6 +216,7 @@ export async function loadQuoteBatchEntries({
|
|
|
210
216
|
options,
|
|
211
217
|
quoteStore,
|
|
212
218
|
runSingleFlight,
|
|
219
|
+
resolveQuote,
|
|
213
220
|
}));
|
|
214
221
|
}));
|
|
215
222
|
|
|
@@ -221,7 +228,6 @@ export class QuoteSubscriptionManager {
|
|
|
221
228
|
private nextQuoteSubscriptionId = 1;
|
|
222
229
|
private quoteSubscriptionDispose: (() => void) | null = null;
|
|
223
230
|
private quoteSubscriptionSignature = "";
|
|
224
|
-
private pendingQuoteSubscriptionKeys = new Set<string>();
|
|
225
231
|
|
|
226
232
|
constructor(
|
|
227
233
|
private readonly dataProvider: DataProvider,
|
|
@@ -243,7 +249,6 @@ export class QuoteSubscriptionManager {
|
|
|
243
249
|
if (existing) {
|
|
244
250
|
existing.targets.set(subscriptionId, target);
|
|
245
251
|
existing.target = mergeQuoteSubscriptionTargets(existing.targets.values()) ?? target;
|
|
246
|
-
this.pendingQuoteSubscriptionKeys.add(key);
|
|
247
252
|
if (existing.removeTimer) {
|
|
248
253
|
clearTimeout(existing.removeTimer);
|
|
249
254
|
existing.removeTimer = null;
|
|
@@ -255,7 +260,6 @@ export class QuoteSubscriptionManager {
|
|
|
255
260
|
targets: new Map([[subscriptionId, target]]),
|
|
256
261
|
removeTimer: null,
|
|
257
262
|
});
|
|
258
|
-
this.pendingQuoteSubscriptionKeys.add(key);
|
|
259
263
|
}
|
|
260
264
|
this.flush();
|
|
261
265
|
|
|
@@ -267,7 +271,6 @@ export class QuoteSubscriptionManager {
|
|
|
267
271
|
const mergedTarget = mergeQuoteSubscriptionTargets(existing.targets.values());
|
|
268
272
|
if (mergedTarget) {
|
|
269
273
|
existing.target = mergedTarget;
|
|
270
|
-
this.pendingQuoteSubscriptionKeys.add(key);
|
|
271
274
|
this.flush();
|
|
272
275
|
continue;
|
|
273
276
|
}
|
|
@@ -276,7 +279,6 @@ export class QuoteSubscriptionManager {
|
|
|
276
279
|
const current = this.quoteSubscriptions.get(key);
|
|
277
280
|
if (!current || current.targets.size > 0) return;
|
|
278
281
|
this.quoteSubscriptions.delete(key);
|
|
279
|
-
this.pendingQuoteSubscriptionKeys.delete(key);
|
|
280
282
|
this.flush();
|
|
281
283
|
}, QUOTE_SUBSCRIPTION_REMOVE_GRACE_MS);
|
|
282
284
|
}
|
|
@@ -285,13 +287,13 @@ export class QuoteSubscriptionManager {
|
|
|
285
287
|
|
|
286
288
|
private flush(): void {
|
|
287
289
|
if (!this.dataProvider.subscribeQuotes) return;
|
|
288
|
-
this.pendingQuoteSubscriptionKeys = new Set();
|
|
289
290
|
|
|
290
291
|
const activeEntries = [...this.quoteSubscriptions.entries()]
|
|
291
292
|
.filter(([, entry]) => entry.targets.size > 0 || entry.removeTimer)
|
|
292
293
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
293
294
|
const nextSignature = activeEntries.map(([key, entry]) => [
|
|
294
295
|
key,
|
|
296
|
+
entry.target.route ?? "",
|
|
295
297
|
entry.target.surface ?? "",
|
|
296
298
|
entry.target.visible ? "visible" : "",
|
|
297
299
|
entry.target.selected ? "selected" : "",
|
|
@@ -40,6 +40,7 @@ function quote(symbol: string, price: number, overrides: Partial<Quote> = {}): Q
|
|
|
40
40
|
|
|
41
41
|
async function flushCoordinator(): Promise<void> {
|
|
42
42
|
await Promise.resolve();
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
describe("MarketDataCoordinator key subscriptions", () => {
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "r
|
|
|
2
2
|
import type { SecFilingDocument, SecFilingItem } from "../types/data-provider";
|
|
3
3
|
import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
|
|
4
4
|
import type { TickerRecord } from "../types/ticker";
|
|
5
|
-
import type { ChartRequest, InstrumentRef, OptionsRequest, SecFilingsRequest } from "./request-types";
|
|
5
|
+
import type { ChartRequest, InstrumentRef, OptionsRequest, SecFilingsRequest, TickerInstrumentOptions } from "./request-types";
|
|
6
6
|
import { instrumentFromTicker } from "./request-types";
|
|
7
7
|
import { useAppActive } from "../state/app/activity";
|
|
8
8
|
import {
|
|
@@ -74,17 +74,17 @@ function stableCurrencyList(currencies: Array<string | null | undefined>): strin
|
|
|
74
74
|
)].sort((left, right) => left.localeCompare(right));
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function buildTickerFinancialsMapKey(tickers: TickerRecord[]): string {
|
|
77
|
+
function buildTickerFinancialsMapKey(tickers: TickerRecord[], options: TickerInstrumentOptions = {}): string {
|
|
78
78
|
return tickers.map((ticker) => {
|
|
79
|
-
const instrument = ticker.metadata.
|
|
79
|
+
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker, options);
|
|
80
80
|
return [
|
|
81
81
|
ticker.metadata.ticker,
|
|
82
82
|
ticker.metadata.exchange ?? "",
|
|
83
83
|
instrument?.brokerId ?? "",
|
|
84
84
|
instrument?.brokerInstanceId ?? "",
|
|
85
|
-
instrument?.conId ?? "",
|
|
86
|
-
instrument?.localSymbol ?? "",
|
|
87
|
-
instrument?.symbol ?? "",
|
|
85
|
+
instrument?.instrument?.conId ?? "",
|
|
86
|
+
instrument?.instrument?.localSymbol ?? "",
|
|
87
|
+
instrument?.instrument?.symbol ?? "",
|
|
88
88
|
].join("|");
|
|
89
89
|
}).join("::");
|
|
90
90
|
}
|
|
@@ -122,10 +122,10 @@ export function useTickerFinancials(symbol: string | null | undefined, ticker: T
|
|
|
122
122
|
return financials;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function buildTickerFinancialsKeys(tickers: TickerRecord[]): string[] {
|
|
125
|
+
function buildTickerFinancialsKeys(tickers: TickerRecord[], options: TickerInstrumentOptions = {}): string[] {
|
|
126
126
|
const keys: string[] = [];
|
|
127
127
|
for (const ticker of tickers) {
|
|
128
|
-
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
|
|
128
|
+
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker, options);
|
|
129
129
|
if (!instrument) continue;
|
|
130
130
|
keys.push(
|
|
131
131
|
buildSnapshotKey(instrument),
|
|
@@ -136,17 +136,21 @@ function buildTickerFinancialsKeys(tickers: TickerRecord[]): string[] {
|
|
|
136
136
|
return keys;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
export function useTickerFinancialsMap(
|
|
139
|
+
export function useTickerFinancialsMap(
|
|
140
|
+
tickers: TickerRecord[],
|
|
141
|
+
options: TickerInstrumentOptions = {},
|
|
142
|
+
): Map<string, TickerFinancials> {
|
|
140
143
|
const coordinator = getSharedMarketDataCoordinator();
|
|
141
|
-
const
|
|
142
|
-
const
|
|
144
|
+
const optionsKey = options.portfolioId ?? "";
|
|
145
|
+
const tickerKey = buildTickerFinancialsMapKey(tickers, options);
|
|
146
|
+
const subscriptionKeys = useMemo(() => buildTickerFinancialsKeys(tickers, options), [optionsKey, tickerKey]);
|
|
143
147
|
const keysVersion = useCoordinatorKeysVersion(subscriptionKeys);
|
|
144
148
|
|
|
145
149
|
return useMemo(() => {
|
|
146
150
|
if (!coordinator) return new Map<string, TickerFinancials>();
|
|
147
151
|
const result = new Map<string, TickerFinancials>();
|
|
148
152
|
for (const ticker of tickers) {
|
|
149
|
-
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
|
|
153
|
+
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker, options);
|
|
150
154
|
if (!instrument) continue;
|
|
151
155
|
const financials = coordinator.getTickerFinancialsSync(instrument);
|
|
152
156
|
if (financials) {
|
|
@@ -154,7 +158,7 @@ export function useTickerFinancialsMap(tickers: TickerRecord[]): Map<string, Tic
|
|
|
154
158
|
}
|
|
155
159
|
}
|
|
156
160
|
return result;
|
|
157
|
-
}, [coordinator, keysVersion, tickerKey, subscriptionKeys]);
|
|
161
|
+
}, [coordinator, keysVersion, optionsKey, tickerKey, subscriptionKeys]);
|
|
158
162
|
}
|
|
159
163
|
|
|
160
164
|
export function useQuoteEntry(symbol: string | null | undefined, ticker: TickerRecord | null | undefined): QueryEntry<Quote> | null {
|
|
@@ -5,7 +5,33 @@ const US_EXTENDED_HOURS_EXCHANGES = new Set(["NASDAQ", "NYSE", "AMEX", "ARCA", "
|
|
|
5
5
|
const ALWAYS_OPEN_EXCHANGES = new Set(["CCC"]);
|
|
6
6
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
7
7
|
const OVERNIGHT_CLOSE_MAX_AGE_MS = 20 * 60 * 60 * 1000;
|
|
8
|
+
const REGULAR_OPEN_MINUTES: Record<string, number> = {
|
|
9
|
+
NASDAQ: 9 * 60 + 30,
|
|
10
|
+
NYSE: 9 * 60 + 30,
|
|
11
|
+
ARCA: 9 * 60 + 30,
|
|
12
|
+
AMEX: 9 * 60 + 30,
|
|
13
|
+
BATS: 9 * 60 + 30,
|
|
14
|
+
TSX: 9 * 60 + 30,
|
|
15
|
+
TSXV: 9 * 60 + 30,
|
|
16
|
+
CSE: 9 * 60 + 30,
|
|
17
|
+
FWB2: 8 * 60,
|
|
18
|
+
XETRA: 8 * 60,
|
|
19
|
+
LSE: 8 * 60,
|
|
20
|
+
EPA: 9 * 60,
|
|
21
|
+
AMS: 9 * 60,
|
|
22
|
+
BRU: 9 * 60,
|
|
23
|
+
LIS: 8 * 60,
|
|
24
|
+
BIT: 9 * 60,
|
|
25
|
+
SFB: 9 * 60,
|
|
26
|
+
HEL: 10 * 60,
|
|
27
|
+
CPH: 9 * 60,
|
|
28
|
+
JPX: 9 * 60,
|
|
29
|
+
HKEX: 9 * 60 + 30,
|
|
30
|
+
TWSE: 9 * 60,
|
|
31
|
+
NSE: 9 * 60 + 15,
|
|
32
|
+
};
|
|
8
33
|
const exchangeLocalDateFormatters = new Map<string, Intl.DateTimeFormat>();
|
|
34
|
+
const exchangeLocalTimeFormatters = new Map<string, Intl.DateTimeFormat>();
|
|
9
35
|
const usSessionFormatter = new Intl.DateTimeFormat("en-US", {
|
|
10
36
|
timeZone: "America/New_York",
|
|
11
37
|
weekday: "short",
|
|
@@ -34,6 +60,20 @@ function getExchangeLocalDateFormatter(timeZone: string): Intl.DateTimeFormat {
|
|
|
34
60
|
return formatter;
|
|
35
61
|
}
|
|
36
62
|
|
|
63
|
+
function getExchangeLocalTimeFormatter(timeZone: string): Intl.DateTimeFormat {
|
|
64
|
+
let formatter = exchangeLocalTimeFormatters.get(timeZone);
|
|
65
|
+
if (!formatter) {
|
|
66
|
+
formatter = new Intl.DateTimeFormat("en-US", {
|
|
67
|
+
timeZone,
|
|
68
|
+
hour: "2-digit",
|
|
69
|
+
minute: "2-digit",
|
|
70
|
+
hourCycle: "h23",
|
|
71
|
+
});
|
|
72
|
+
exchangeLocalTimeFormatters.set(timeZone, formatter);
|
|
73
|
+
}
|
|
74
|
+
return formatter;
|
|
75
|
+
}
|
|
76
|
+
|
|
37
77
|
function exchangeLocalDate(exchange: string, timestampMs: number): string | null {
|
|
38
78
|
const timeZone = EXCHANGE_TIME_ZONES[canonicalExchange(exchange)];
|
|
39
79
|
if (!timeZone) return null;
|
|
@@ -46,6 +86,23 @@ function exchangeLocalDate(exchange: string, timestampMs: number): string | null
|
|
|
46
86
|
return `${year}-${month}-${day}`;
|
|
47
87
|
}
|
|
48
88
|
|
|
89
|
+
function exchangeLocalMinuteOfDay(exchange: string, timestampMs: number): number | null {
|
|
90
|
+
const timeZone = EXCHANGE_TIME_ZONES[canonicalExchange(exchange)];
|
|
91
|
+
if (!timeZone) return null;
|
|
92
|
+
|
|
93
|
+
const parts = getExchangeLocalTimeFormatter(timeZone).formatToParts(new Date(timestampMs));
|
|
94
|
+
const hour = Number(parts.find((part) => part.type === "hour")?.value);
|
|
95
|
+
const minute = Number(parts.find((part) => part.type === "minute")?.value);
|
|
96
|
+
return Number.isFinite(hour) && Number.isFinite(minute) ? hour * 60 + minute : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isBeforeKnownRegularOpen(exchange: string, timestampMs: number): boolean {
|
|
100
|
+
const canonical = canonicalExchange(exchange);
|
|
101
|
+
const openMinute = REGULAR_OPEN_MINUTES[canonical];
|
|
102
|
+
const localMinute = exchangeLocalMinuteOfDay(canonical, timestampMs);
|
|
103
|
+
return openMinute != null && localMinute != null && localMinute < openMinute;
|
|
104
|
+
}
|
|
105
|
+
|
|
49
106
|
function isoLocalDateToUtcDay(date: string): number | null {
|
|
50
107
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
|
51
108
|
if (!match) return null;
|
|
@@ -122,7 +179,7 @@ function isTimestampStaleForExchangeSessionUnsafe(
|
|
|
122
179
|
if (!timestampDate || !currentDate || timestampDate === currentDate) return false;
|
|
123
180
|
|
|
124
181
|
if (ALWAYS_OPEN_EXCHANGES.has(canonical)) return true;
|
|
125
|
-
if (marketState === "REGULAR") return true;
|
|
182
|
+
if (marketState === "REGULAR" && !isBeforeKnownRegularOpen(canonical, now)) return true;
|
|
126
183
|
|
|
127
184
|
if (isUsExtendedHoursExchange(canonical)) {
|
|
128
185
|
const session = usSessionState(now);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { MarketState, Quote, QuoteFieldProvenance } from "../../types/financials";
|
|
2
|
-
import { colors } from "../../theme/colors";
|
|
2
|
+
import { blendHex, colors, priceColor } from "../../theme/colors";
|
|
3
|
+
|
|
4
|
+
const CLOSED_CHANGE_MUTING_RATIO = 0.55;
|
|
3
5
|
|
|
4
6
|
export interface ActiveQuoteDisplay {
|
|
5
7
|
price: number;
|
|
@@ -45,6 +47,22 @@ export function marketStateDot(state?: MarketState): string {
|
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
export function isClosedMarketState(state?: MarketState): boolean {
|
|
51
|
+
return state === "CLOSED" || state === "PREPRE" || state === "POSTPOST";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Closed prices are final snapshots, so they should not look live or directional. */
|
|
55
|
+
export function marketPriceColor(change: number, state?: MarketState): string {
|
|
56
|
+
return isClosedMarketState(state) ? colors.textDim : priceColor(change);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Preserve a closed session's direction while visually distinguishing it from a live move. */
|
|
60
|
+
export function marketChangeColor(change: number, state?: MarketState): string {
|
|
61
|
+
const directionalColor = priceColor(change);
|
|
62
|
+
if (!isClosedMarketState(state) || change === 0) return directionalColor;
|
|
63
|
+
return blendHex(directionalColor, colors.textDim, CLOSED_CHANGE_MUTING_RATIO);
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
/** Short exchange display name */
|
|
49
67
|
export function exchangeShortName(exchangeName?: string, fullExchangeName?: string): string {
|
|
50
68
|
if (!exchangeName && !fullExchangeName) return "";
|
|
@@ -132,7 +132,55 @@ describe("quote freshness", () => {
|
|
|
132
132
|
).toBe(false);
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
test("
|
|
135
|
+
test("allows prior-day regular-session quotes before the local exchange reopens", () => {
|
|
136
|
+
expect(
|
|
137
|
+
isQuoteStaleForCurrentSession(
|
|
138
|
+
quote({
|
|
139
|
+
symbol: "NEO.TO",
|
|
140
|
+
currency: "CAD",
|
|
141
|
+
lastUpdated: Date.parse("2026-07-06T20:00:00Z"),
|
|
142
|
+
listingExchangeName: "TOR",
|
|
143
|
+
exchangeName: "TOR",
|
|
144
|
+
marketState: "REGULAR",
|
|
145
|
+
}),
|
|
146
|
+
Date.parse("2026-07-07T12:10:00Z"),
|
|
147
|
+
),
|
|
148
|
+
).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("allows non-US premarket labels without active-session prices", () => {
|
|
152
|
+
expect(
|
|
153
|
+
isQuoteStaleForCurrentSession(
|
|
154
|
+
quote({
|
|
155
|
+
symbol: "NEO.TO",
|
|
156
|
+
currency: "CAD",
|
|
157
|
+
lastUpdated: Date.parse("2026-07-07T12:10:00Z"),
|
|
158
|
+
listingExchangeName: "TOR",
|
|
159
|
+
exchangeName: "TOR",
|
|
160
|
+
marketState: "PRE",
|
|
161
|
+
}),
|
|
162
|
+
Date.parse("2026-07-07T12:10:30Z"),
|
|
163
|
+
),
|
|
164
|
+
).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("rejects US premarket labels without active-session prices", () => {
|
|
168
|
+
expect(
|
|
169
|
+
isQuoteStaleForCurrentSession(
|
|
170
|
+
quote({
|
|
171
|
+
symbol: "ALAB",
|
|
172
|
+
currency: "USD",
|
|
173
|
+
lastUpdated: Date.parse("2026-07-07T12:10:00Z"),
|
|
174
|
+
listingExchangeName: "NASDAQ",
|
|
175
|
+
exchangeName: "NASDAQ",
|
|
176
|
+
marketState: "PRE",
|
|
177
|
+
}),
|
|
178
|
+
Date.parse("2026-07-07T12:10:30Z"),
|
|
179
|
+
),
|
|
180
|
+
).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("treats prior-date regular-session quotes as stale after the local exchange reopens", () => {
|
|
136
184
|
expect(
|
|
137
185
|
isQuoteStaleForCurrentSession(
|
|
138
186
|
quote({
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { Quote } from "../../types/financials";
|
|
2
|
+
import { canonicalExchange } from "../../utils/exchanges";
|
|
2
3
|
import { isTimestampStaleForExchangeSession } from "../market/freshness";
|
|
3
4
|
|
|
5
|
+
const EXTENDED_HOURS_EXCHANGES = new Set(["NASDAQ", "NYSE", "AMEX", "ARCA", "BATS"]);
|
|
6
|
+
|
|
7
|
+
function isExtendedHoursExchange(quote: Quote): boolean {
|
|
8
|
+
return EXTENDED_HOURS_EXCHANGES.has(canonicalExchange(quote.listingExchangeName || quote.exchangeName));
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
function isQuoteMissingActiveSessionPrice(quote: Quote): boolean {
|
|
12
|
+
if (!isExtendedHoursExchange(quote)) return false;
|
|
5
13
|
if ((quote.marketState === "PRE" || quote.marketState === "PREPRE") && quote.preMarketPrice == null) {
|
|
6
14
|
return true;
|
|
7
15
|
}
|