gloomberb 0.3.3 → 0.4.0
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/README.md +27 -16
- package/package.json +1 -1
- package/src/app.tsx +29 -65
- package/src/cli/base-converter.ts +31 -0
- package/src/cli/commands/search.ts +11 -3
- package/src/cli/commands/ticker.ts +22 -11
- package/src/cli/context.ts +27 -32
- package/src/cli/helpers.ts +1 -4
- package/src/cli/index.ts +140 -123
- package/src/cli/registry.test.ts +226 -0
- package/src/cli/registry.ts +201 -0
- package/src/cli.test.ts +219 -0
- package/src/components/chart/chart-controller.ts +309 -0
- package/src/components/chart/chart-data.ts +5 -6
- package/src/components/chart/chart-pane-settings.ts +27 -0
- package/src/components/chart/chart-renderer.ts +7 -2
- package/src/components/chart/chart-resolution.test.ts +56 -0
- package/src/components/chart/chart-resolution.ts +362 -0
- package/src/components/chart/chart-types.ts +12 -4
- package/src/components/chart/chart-viewport.test.ts +25 -0
- package/src/components/chart/chart-viewport.ts +28 -0
- package/src/components/chart/chart.test.ts +26 -0
- package/src/components/chart/comparison-chart-data.ts +20 -27
- package/src/components/chart/comparison-stock-chart.tsx +374 -99
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
- package/src/components/chart/stock-chart.tsx +484 -112
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +650 -5
- package/src/components/command-bar/command-bar.tsx +359 -63
- package/src/components/command-bar/command-registry.ts +8 -0
- package/src/components/command-bar/helpers.test.ts +23 -1
- package/src/components/command-bar/helpers.ts +14 -5
- package/src/components/command-bar/pane-template-display.ts +12 -0
- package/src/components/command-bar/root-shortcuts.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +4 -2
- package/src/components/detail-feed-view.test.tsx +124 -49
- package/src/components/detail-feed-view.tsx +396 -160
- package/src/components/layout/floating-pane.tsx +1 -1
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/shell.tsx +62 -50
- package/src/components/layout/status-bar.test.tsx +66 -0
- package/src/components/layout/status-bar.tsx +26 -5
- package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
- package/src/components/onboarding/onboarding-wizard.tsx +5 -6
- package/src/components/ticker-list-table.test.tsx +83 -0
- package/src/components/ticker-list-table.tsx +174 -97
- package/src/components/ui/data-table.tsx +27 -2
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/page-stack-view.tsx +60 -0
- package/src/components/use-double-click-activation.ts +31 -0
- package/src/index.tsx +10 -10
- package/src/market-data/coordinator.test.ts +92 -4
- package/src/market-data/coordinator.ts +32 -5
- package/src/market-data/hooks.test.tsx +120 -0
- package/src/market-data/hooks.tsx +53 -16
- package/src/market-data/request-types.ts +4 -2
- package/src/market-data/selectors.ts +24 -58
- package/src/plugins/builtin/chat-controller.test.ts +370 -3
- package/src/plugins/builtin/chat-controller.ts +310 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +175 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- package/src/plugins/builtin/news.tsx +1 -11
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
- package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
- package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
- package/src/plugins/builtin/sec.tsx +1 -11
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
- package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
- package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
- package/src/plugins/catalog.ts +65 -0
- package/src/plugins/ibkr/broker-adapter.ts +28 -0
- package/src/plugins/ibkr/gateway-service.test.ts +75 -1
- package/src/plugins/ibkr/gateway-service.ts +179 -9
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/chart.test.tsx +139 -0
- package/src/plugins/prediction-markets/chart.tsx +65 -436
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +17 -5
- package/src/plugins/prediction-markets/controller.ts +141 -121
- package/src/plugins/prediction-markets/detail/outcomes.tsx +2 -6
- package/src/plugins/prediction-markets/detail/pane.tsx +4 -25
- package/src/plugins/prediction-markets/detail/shared.tsx +0 -1
- package/src/plugins/prediction-markets/detail.test.tsx +4 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/prediction-markets/launch.ts +51 -16
- package/src/plugins/prediction-markets/metrics.ts +9 -5
- package/src/plugins/prediction-markets/pane.test.tsx +160 -58
- package/src/plugins/prediction-markets/pane.tsx +74 -183
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/types.ts +0 -1
- package/src/sources/gloomberb-cloud.test.ts +29 -0
- package/src/sources/gloomberb-cloud.ts +89 -1
- package/src/sources/provider-router.test.ts +308 -3
- package/src/sources/provider-router.ts +247 -17
- package/src/sources/yahoo-finance.test.ts +26 -0
- package/src/sources/yahoo-finance.ts +54 -3
- package/src/state/app-context-selector.test.tsx +99 -0
- package/src/state/app-context.test.ts +18 -0
- package/src/state/app-context.tsx +170 -65
- package/src/state/selectors.ts +25 -0
- package/src/theme/colors.ts +8 -8
- package/src/theme/themes.test.ts +2 -0
- package/src/theme/themes.ts +22 -10
- package/src/types/broker.ts +21 -0
- package/src/types/config.ts +17 -1
- package/src/types/data-provider.ts +18 -0
- package/src/types/financials.ts +31 -1
- package/src/types/plugin.ts +70 -0
- package/src/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +35 -12
- package/src/utils/back-navigation.ts +27 -0
- package/src/utils/format.test.ts +14 -0
- package/src/utils/format.ts +3 -3
- package/src/utils/market-status.ts +46 -1
- package/src/utils/price-history.test.ts +25 -0
- package/src/utils/price-history.ts +29 -5
- package/src/utils/quote-freshness.ts +127 -0
- package/src/utils/quote-resolution.test.ts +277 -0
- package/src/utils/quote-resolution.ts +641 -0
- package/src/utils/slugify.ts +3 -0
- package/src/utils/timestamp.ts +25 -0
- package/src/version.ts +1 -1
- package/src/cli/commands/portfolio.ts +0 -210
- package/src/cli/commands/watchlist.ts +0 -210
- package/src/plugins/builtin/data-providers.ts +0 -16
- package/src/sources/exchange-rates.ts +0 -18
- package/src/utils/ascii-chart.ts +0 -172
- package/src/utils/ticker-tokenizer.test.ts +0 -48
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "./selectors";
|
|
24
24
|
import { traceMarketData } from "./trace";
|
|
25
25
|
import { normalizePriceHistory, normalizeTickerFinancialsPriceHistory } from "../utils/price-history";
|
|
26
|
+
import { hasFreshQuoteForCurrentSession, isQuoteStaleForCurrentSession } from "../utils/quote-freshness";
|
|
26
27
|
|
|
27
28
|
const EMPTY_MESSAGE = "No data available";
|
|
28
29
|
const EXPECTED_EMPTY = /no data|not found|delisted|unavailable|unsupported/i;
|
|
@@ -30,8 +31,8 @@ const EXPECTED_EMPTY = /no data|not found|delisted|unavailable|unsupported/i;
|
|
|
30
31
|
function createBaselineChartRequest(instrument: InstrumentRef): ChartRequest {
|
|
31
32
|
return {
|
|
32
33
|
instrument,
|
|
33
|
-
|
|
34
|
-
granularity: "
|
|
34
|
+
bufferRange: "5Y",
|
|
35
|
+
granularity: "range",
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -92,6 +93,21 @@ function readyEntry<T>(
|
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
function readyQuoteEntry(
|
|
97
|
+
current: QueryEntry<Quote>,
|
|
98
|
+
quote: Quote,
|
|
99
|
+
source: string,
|
|
100
|
+
attempts: ProviderAttempt[],
|
|
101
|
+
): QueryEntry<Quote> {
|
|
102
|
+
if (
|
|
103
|
+
isQuoteStaleForCurrentSession(quote)
|
|
104
|
+
&& hasFreshQuoteForCurrentSession([current.data, current.lastGoodData])
|
|
105
|
+
) {
|
|
106
|
+
return readyEntry(current, null, current.source ?? source, attempts, { keepLastGoodOnEmpty: true });
|
|
107
|
+
}
|
|
108
|
+
return readyEntry(current, quote, source, attempts, { keepLastGoodOnEmpty: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
95
111
|
function errorEntry<T>(current: QueryEntry<T>, attempt: ProviderAttempt): QueryEntry<T> {
|
|
96
112
|
return {
|
|
97
113
|
...current,
|
|
@@ -349,7 +365,7 @@ export class MarketDataCoordinator {
|
|
|
349
365
|
);
|
|
350
366
|
const source = quote.providerId ?? this.dataProvider.id;
|
|
351
367
|
const attempts = [createAttempt(source, startedAt, "success")];
|
|
352
|
-
return this.quoteStore.update(key, (current) =>
|
|
368
|
+
return this.quoteStore.update(key, (current) => readyQuoteEntry(current, quote, source, attempts));
|
|
353
369
|
} catch (error) {
|
|
354
370
|
const classified = classifyError(error);
|
|
355
371
|
const attempt = createAttempt(this.dataProvider.id, startedAt, EXPECTED_EMPTY.test(classified.message) ? "empty" : "fatal_error", classified.reasonCode, classified.message);
|
|
@@ -381,10 +397,21 @@ export class MarketDataCoordinator {
|
|
|
381
397
|
cacheMode: options.forceRefresh ? "refresh" : "default",
|
|
382
398
|
},
|
|
383
399
|
)
|
|
400
|
+
: request.granularity === "resolution" && request.resolution && this.dataProvider.getPriceHistoryForResolution
|
|
401
|
+
? await this.dataProvider.getPriceHistoryForResolution(
|
|
402
|
+
request.instrument.symbol,
|
|
403
|
+
request.instrument.exchange ?? "",
|
|
404
|
+
request.bufferRange,
|
|
405
|
+
request.resolution,
|
|
406
|
+
{
|
|
407
|
+
...toMarketDataContext(request.instrument),
|
|
408
|
+
cacheMode: options.forceRefresh ? "refresh" : "default",
|
|
409
|
+
},
|
|
410
|
+
)
|
|
384
411
|
: await this.dataProvider.getPriceHistory(
|
|
385
412
|
request.instrument.symbol,
|
|
386
413
|
request.instrument.exchange ?? "",
|
|
387
|
-
request.
|
|
414
|
+
request.bufferRange,
|
|
388
415
|
{
|
|
389
416
|
...toMarketDataContext(request.instrument),
|
|
390
417
|
cacheMode: options.forceRefresh ? "refresh" : "default",
|
|
@@ -555,7 +582,7 @@ export class MarketDataCoordinator {
|
|
|
555
582
|
const key = buildQuoteKey(instrument);
|
|
556
583
|
const current = this.quoteStore.get(key);
|
|
557
584
|
const attempts = [createAttempt(quote.providerId ?? this.dataProvider.id, Date.now(), "success")];
|
|
558
|
-
this.quoteStore.set(key,
|
|
585
|
+
this.quoteStore.set(key, readyQuoteEntry(current, quote, quote.providerId ?? this.dataProvider.id, attempts));
|
|
559
586
|
});
|
|
560
587
|
}
|
|
561
588
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act, useState } from "react";
|
|
3
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import type { TickerFinancials } from "../types/financials";
|
|
5
|
+
import type { TickerRecord } from "../types/ticker";
|
|
6
|
+
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "./coordinator";
|
|
7
|
+
import { useFxRatesMap, useTickerFinancialsMap } from "./hooks";
|
|
8
|
+
|
|
9
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
10
|
+
let bumpHarness: (() => void) | null = null;
|
|
11
|
+
let latestFxRates: Map<string, number> | null = null;
|
|
12
|
+
let latestFinancialsMap: Map<string, TickerFinancials> | null = null;
|
|
13
|
+
|
|
14
|
+
const readyUsdEntry = {
|
|
15
|
+
phase: "ready" as const,
|
|
16
|
+
data: 1,
|
|
17
|
+
lastGoodData: 1,
|
|
18
|
+
source: "test",
|
|
19
|
+
fetchedAt: 1,
|
|
20
|
+
staleAt: null,
|
|
21
|
+
error: null,
|
|
22
|
+
attempts: [],
|
|
23
|
+
};
|
|
24
|
+
const readyEurEntry = {
|
|
25
|
+
phase: "ready" as const,
|
|
26
|
+
data: 1.08,
|
|
27
|
+
lastGoodData: 1.08,
|
|
28
|
+
source: "test",
|
|
29
|
+
fetchedAt: 1,
|
|
30
|
+
staleAt: null,
|
|
31
|
+
error: null,
|
|
32
|
+
attempts: [],
|
|
33
|
+
};
|
|
34
|
+
const sampleFinancials: TickerFinancials = {
|
|
35
|
+
annualStatements: [],
|
|
36
|
+
quarterlyStatements: [],
|
|
37
|
+
priceHistory: [],
|
|
38
|
+
quote: {
|
|
39
|
+
symbol: "SAP",
|
|
40
|
+
price: 250,
|
|
41
|
+
currency: "EUR",
|
|
42
|
+
change: 2,
|
|
43
|
+
changePercent: 0.8,
|
|
44
|
+
lastUpdated: 1,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const tickers: TickerRecord[] = [
|
|
48
|
+
{
|
|
49
|
+
metadata: {
|
|
50
|
+
ticker: "SAP",
|
|
51
|
+
exchange: "XETRA",
|
|
52
|
+
currency: "EUR",
|
|
53
|
+
name: "SAP",
|
|
54
|
+
portfolios: [],
|
|
55
|
+
watchlists: [],
|
|
56
|
+
positions: [],
|
|
57
|
+
custom: {},
|
|
58
|
+
tags: [],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function HooksHarness() {
|
|
64
|
+
const [tick, setTick] = useState(0);
|
|
65
|
+
bumpHarness = () => setTick((current) => current + 1);
|
|
66
|
+
|
|
67
|
+
latestFxRates = useFxRatesMap(["USD", "EUR"]);
|
|
68
|
+
latestFinancialsMap = useTickerFinancialsMap(tickers);
|
|
69
|
+
|
|
70
|
+
return <text>{String(tick)}</text>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
afterEach(async () => {
|
|
74
|
+
if (testSetup) {
|
|
75
|
+
await act(async () => {
|
|
76
|
+
testSetup!.renderer.destroy();
|
|
77
|
+
});
|
|
78
|
+
testSetup = undefined;
|
|
79
|
+
}
|
|
80
|
+
bumpHarness = null;
|
|
81
|
+
latestFxRates = null;
|
|
82
|
+
latestFinancialsMap = null;
|
|
83
|
+
setSharedMarketDataCoordinator(null);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("market-data hooks", () => {
|
|
87
|
+
test("preserve derived map instances across unrelated rerenders", async () => {
|
|
88
|
+
const coordinator = {
|
|
89
|
+
subscribe: () => () => {},
|
|
90
|
+
getVersion: () => 1,
|
|
91
|
+
getFxEntry: (currency: string) => (currency === "EUR" ? readyEurEntry : readyUsdEntry),
|
|
92
|
+
loadFxRate: async () => {},
|
|
93
|
+
getTickerFinancialsSync: () => sampleFinancials,
|
|
94
|
+
};
|
|
95
|
+
setSharedMarketDataCoordinator(coordinator as unknown as MarketDataCoordinator);
|
|
96
|
+
|
|
97
|
+
testSetup = await testRender(<HooksHarness />, {
|
|
98
|
+
width: 20,
|
|
99
|
+
height: 1,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await act(async () => {
|
|
103
|
+
await testSetup!.renderOnce();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const initialFxRates = latestFxRates;
|
|
107
|
+
const initialFinancialsMap = latestFinancialsMap;
|
|
108
|
+
|
|
109
|
+
await act(async () => {
|
|
110
|
+
bumpHarness?.();
|
|
111
|
+
await Promise.resolve();
|
|
112
|
+
});
|
|
113
|
+
await act(async () => {
|
|
114
|
+
await testSetup!.renderOnce();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(latestFxRates).toBe(initialFxRates);
|
|
118
|
+
expect(latestFinancialsMap).toBe(initialFinancialsMap);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveEntryValue,
|
|
10
10
|
} from "./coordinator";
|
|
11
11
|
import type { QueryEntry } from "./result-types";
|
|
12
|
+
import { buildChartKey } from "./selectors";
|
|
12
13
|
|
|
13
14
|
function useCoordinatorVersion(): number {
|
|
14
15
|
const coordinator = getSharedMarketDataCoordinator();
|
|
@@ -33,6 +34,21 @@ function stableCurrencyList(currencies: Array<string | null | undefined>): strin
|
|
|
33
34
|
)].sort((left, right) => left.localeCompare(right));
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function buildTickerFinancialsMapKey(tickers: TickerRecord[]): string {
|
|
38
|
+
return tickers.map((ticker) => {
|
|
39
|
+
const instrument = ticker.metadata.broker_contracts?.[0];
|
|
40
|
+
return [
|
|
41
|
+
ticker.metadata.ticker,
|
|
42
|
+
ticker.metadata.exchange ?? "",
|
|
43
|
+
instrument?.brokerId ?? "",
|
|
44
|
+
instrument?.brokerInstanceId ?? "",
|
|
45
|
+
instrument?.conId ?? "",
|
|
46
|
+
instrument?.localSymbol ?? "",
|
|
47
|
+
instrument?.symbol ?? "",
|
|
48
|
+
].join("|");
|
|
49
|
+
}).join("::");
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
export function useTickerInstrument(symbol: string | null | undefined, ticker: TickerRecord | null | undefined): InstrumentRef | null {
|
|
37
53
|
return useMemo(() => instrumentFromTicker(ticker, symbol ?? null), [symbol, ticker]);
|
|
38
54
|
}
|
|
@@ -54,7 +70,12 @@ export function useTickerFinancials(symbol: string | null | undefined, ticker: T
|
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
export function useTickerFinancialsMap(tickers: TickerRecord[]): Map<string, TickerFinancials> {
|
|
57
|
-
|
|
73
|
+
const coordinator = getSharedMarketDataCoordinator();
|
|
74
|
+
const version = useCoordinatorVersion();
|
|
75
|
+
const tickerKey = buildTickerFinancialsMapKey(tickers);
|
|
76
|
+
|
|
77
|
+
return useMemo(() => {
|
|
78
|
+
if (!coordinator) return new Map<string, TickerFinancials>();
|
|
58
79
|
const result = new Map<string, TickerFinancials>();
|
|
59
80
|
for (const ticker of tickers) {
|
|
60
81
|
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
|
|
@@ -65,7 +86,7 @@ export function useTickerFinancialsMap(tickers: TickerRecord[]): Map<string, Tic
|
|
|
65
86
|
}
|
|
66
87
|
}
|
|
67
88
|
return result;
|
|
68
|
-
},
|
|
89
|
+
}, [coordinator, tickerKey, version]);
|
|
69
90
|
}
|
|
70
91
|
|
|
71
92
|
export function useQuoteEntry(symbol: string | null | undefined, ticker: TickerRecord | null | undefined): QueryEntry<Quote> | null {
|
|
@@ -85,15 +106,7 @@ export function useQuoteEntry(symbol: string | null | undefined, ticker: TickerR
|
|
|
85
106
|
}
|
|
86
107
|
|
|
87
108
|
export function useChartQuery(request: ChartRequest | null | undefined): QueryEntry<PricePoint[]> | null {
|
|
88
|
-
const key = request ?
|
|
89
|
-
request.instrument.symbol,
|
|
90
|
-
request.instrument.exchange ?? "",
|
|
91
|
-
request.range,
|
|
92
|
-
request.granularity ?? "daily",
|
|
93
|
-
request.startDate?.toISOString() ?? "",
|
|
94
|
-
request.endDate?.toISOString() ?? "",
|
|
95
|
-
request.barSize ?? "",
|
|
96
|
-
].join("|") : null;
|
|
109
|
+
const key = request ? buildChartKey(request) : null;
|
|
97
110
|
const entry = useCoordinatorSelector(
|
|
98
111
|
(coordinator) => (request ? coordinator.getChartEntry(request) : null),
|
|
99
112
|
null,
|
|
@@ -108,6 +121,23 @@ export function useChartQuery(request: ChartRequest | null | undefined): QueryEn
|
|
|
108
121
|
return entry;
|
|
109
122
|
}
|
|
110
123
|
|
|
124
|
+
export function useChartQueries(requests: readonly ChartRequest[]): Map<string, QueryEntry<PricePoint[]>> {
|
|
125
|
+
const requestKey = requests.map((request) => buildChartKey(request)).join(",");
|
|
126
|
+
const entries = useCoordinatorSelector((coordinator) => (
|
|
127
|
+
requests.map((request) => [buildChartKey(request), coordinator.getChartEntry(request)] as const)
|
|
128
|
+
), [] as Array<readonly [string, QueryEntry<PricePoint[]>]>);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const coordinator = getSharedMarketDataCoordinator();
|
|
132
|
+
if (!coordinator) return;
|
|
133
|
+
for (const request of requests) {
|
|
134
|
+
void coordinator.loadChart(request);
|
|
135
|
+
}
|
|
136
|
+
}, [requestKey]);
|
|
137
|
+
|
|
138
|
+
return useMemo(() => new Map(entries), [entries]);
|
|
139
|
+
}
|
|
140
|
+
|
|
111
141
|
export function useNewsQuery(request: NewsRequest | null | undefined): QueryEntry<NewsItem[]> | null {
|
|
112
142
|
const key = request ? `${request.instrument.symbol}|${request.instrument.exchange ?? ""}|${request.count ?? 50}` : null;
|
|
113
143
|
const entry = useCoordinatorSelector(
|
|
@@ -203,10 +233,17 @@ export function useFxRate(currency: string | null | undefined): QueryEntry<numbe
|
|
|
203
233
|
}
|
|
204
234
|
|
|
205
235
|
export function useFxRatesMap(currencies: Array<string | null | undefined>): Map<string, number> {
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
236
|
+
const coordinator = getSharedMarketDataCoordinator();
|
|
237
|
+
const version = useCoordinatorVersion();
|
|
238
|
+
const normalizedCurrencyKey = stableCurrencyList(currencies).join("|");
|
|
239
|
+
const normalizedCurrencies = useMemo(
|
|
240
|
+
() => (normalizedCurrencyKey ? normalizedCurrencyKey.split("|") : []),
|
|
241
|
+
[normalizedCurrencyKey],
|
|
242
|
+
);
|
|
243
|
+
const entries = useMemo(() => {
|
|
244
|
+
if (!coordinator) return [] as Array<readonly [string, QueryEntry<number>]>;
|
|
245
|
+
return normalizedCurrencies.map((currency) => [currency, coordinator.getFxEntry(currency)] as const);
|
|
246
|
+
}, [coordinator, normalizedCurrencies, version]);
|
|
210
247
|
|
|
211
248
|
useEffect(() => {
|
|
212
249
|
const coordinator = getSharedMarketDataCoordinator();
|
|
@@ -214,7 +251,7 @@ export function useFxRatesMap(currencies: Array<string | null | undefined>): Map
|
|
|
214
251
|
for (const currency of normalizedCurrencies) {
|
|
215
252
|
void coordinator.loadFxRate(currency);
|
|
216
253
|
}
|
|
217
|
-
}, [
|
|
254
|
+
}, [normalizedCurrencyKey]);
|
|
218
255
|
|
|
219
256
|
return useMemo(() => {
|
|
220
257
|
const rates = new Map<string, number>();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TimeRange } from "../components/chart/chart-types";
|
|
2
|
+
import type { ManualChartResolution } from "../components/chart/chart-resolution";
|
|
2
3
|
import type { QuoteSubscriptionTarget } from "../types/data-provider";
|
|
3
4
|
import type { BrokerContractRef } from "../types/instrument";
|
|
4
5
|
import type { TickerRecord } from "../types/ticker";
|
|
@@ -11,12 +12,13 @@ export interface InstrumentRef {
|
|
|
11
12
|
instrument?: BrokerContractRef | null;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export type ChartGranularity = "
|
|
15
|
+
export type ChartGranularity = "range" | "detail" | "resolution";
|
|
15
16
|
|
|
16
17
|
export interface ChartRequest {
|
|
17
18
|
instrument: InstrumentRef;
|
|
18
|
-
|
|
19
|
+
bufferRange: TimeRange;
|
|
19
20
|
granularity?: ChartGranularity;
|
|
21
|
+
resolution?: ManualChartResolution;
|
|
20
22
|
startDate?: Date;
|
|
21
23
|
endDate?: Date;
|
|
22
24
|
barSize?: string;
|
|
@@ -4,6 +4,7 @@ import type { InstrumentRef, NewsRequest, OptionsRequest, SecFilingsRequest, Cha
|
|
|
4
4
|
import type { QueryEntry } from "./result-types";
|
|
5
5
|
import { hasLikelyQuoteUnitMismatch } from "../utils/currency-units";
|
|
6
6
|
import { normalizePriceHistory } from "../utils/price-history";
|
|
7
|
+
import { resolveTickerFinancialsQuoteState } from "../utils/quote-resolution";
|
|
7
8
|
|
|
8
9
|
export function buildInstrumentKey(instrument: InstrumentRef): string {
|
|
9
10
|
const contractKey = instrument.instrument?.conId
|
|
@@ -51,8 +52,9 @@ export function buildChartKey(request: ChartRequest): string {
|
|
|
51
52
|
return [
|
|
52
53
|
"chart",
|
|
53
54
|
buildInstrumentKey(request.instrument),
|
|
54
|
-
request.
|
|
55
|
-
request.granularity ?? "
|
|
55
|
+
request.bufferRange,
|
|
56
|
+
request.granularity ?? "range",
|
|
57
|
+
request.resolution ?? "",
|
|
56
58
|
request.startDate ? request.startDate.toISOString() : "",
|
|
57
59
|
request.endDate ? request.endDate.toISOString() : "",
|
|
58
60
|
request.barSize ?? "",
|
|
@@ -95,54 +97,6 @@ export function hasLikelyPriceUnitMismatch(
|
|
|
95
97
|
return hasLikelyQuoteUnitMismatch(left, right);
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
function mergeDefinedQuoteFields(
|
|
99
|
-
preferred: Quote | null | undefined,
|
|
100
|
-
fallback: Quote | null | undefined,
|
|
101
|
-
): Quote | undefined {
|
|
102
|
-
const mergedEntries: Array<[string, unknown]> = [];
|
|
103
|
-
for (const source of [fallback, preferred]) {
|
|
104
|
-
if (!source) continue;
|
|
105
|
-
for (const [key, value] of Object.entries(source)) {
|
|
106
|
-
if (value !== undefined) {
|
|
107
|
-
mergedEntries.push([key, value]);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (mergedEntries.length === 0) return undefined;
|
|
112
|
-
return Object.fromEntries(mergedEntries) as Quote;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function projectExtendedHoursQuote(
|
|
116
|
-
snapshotQuote: Quote | null | undefined,
|
|
117
|
-
liveQuote: Quote | null | undefined,
|
|
118
|
-
): Quote | undefined {
|
|
119
|
-
const merged = mergeDefinedQuoteFields(liveQuote, snapshotQuote);
|
|
120
|
-
if (!merged) return undefined;
|
|
121
|
-
if (!snapshotQuote || !liveQuote) return merged;
|
|
122
|
-
|
|
123
|
-
const snapshotState = snapshotQuote.marketState;
|
|
124
|
-
if (snapshotState !== "PRE" && snapshotState !== "POST") return merged;
|
|
125
|
-
if (liveQuote.marketState === "PRE" || liveQuote.marketState === "POST") return merged;
|
|
126
|
-
|
|
127
|
-
if (snapshotState === "PRE") {
|
|
128
|
-
return {
|
|
129
|
-
...merged,
|
|
130
|
-
marketState: "PRE",
|
|
131
|
-
preMarketPrice: liveQuote.price,
|
|
132
|
-
preMarketChange: liveQuote.change,
|
|
133
|
-
preMarketChangePercent: liveQuote.changePercent,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
...merged,
|
|
139
|
-
marketState: "POST",
|
|
140
|
-
postMarketPrice: liveQuote.price,
|
|
141
|
-
postMarketChange: liveQuote.change,
|
|
142
|
-
postMarketChangePercent: liveQuote.changePercent,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
100
|
export function buildTickerFinancialsSnapshot(
|
|
147
101
|
snapshotEntry: QueryEntry<TickerFinancials>,
|
|
148
102
|
quoteEntry?: QueryEntry<TickerFinancials["quote"]>,
|
|
@@ -152,15 +106,27 @@ export function buildTickerFinancialsSnapshot(
|
|
|
152
106
|
const quote = resolveEntryData(quoteEntry);
|
|
153
107
|
const priceHistory = resolveEntryData(chartEntry);
|
|
154
108
|
if (!snapshot && !quote && !priceHistory) return null;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
109
|
+
if (snapshot?.quote && quote && hasLikelyPriceUnitMismatch(snapshot.quote, quote)) {
|
|
110
|
+
return {
|
|
111
|
+
...(resolveTickerFinancialsQuoteState(snapshot) ?? {
|
|
112
|
+
annualStatements: snapshot?.annualStatements ?? [],
|
|
113
|
+
quarterlyStatements: snapshot?.quarterlyStatements ?? [],
|
|
114
|
+
priceHistory: snapshot?.priceHistory ?? [],
|
|
115
|
+
}),
|
|
116
|
+
priceHistory: normalizePriceHistory(priceHistory ?? snapshot?.priceHistory ?? []),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const resolved = resolveTickerFinancialsQuoteState(snapshot ?? {
|
|
121
|
+
annualStatements: [],
|
|
122
|
+
quarterlyStatements: [],
|
|
123
|
+
priceHistory: [],
|
|
124
|
+
}, quote);
|
|
125
|
+
|
|
126
|
+
if (!resolved) return null;
|
|
127
|
+
|
|
158
128
|
return {
|
|
159
|
-
|
|
160
|
-
fundamentals: snapshot?.fundamentals,
|
|
161
|
-
profile: snapshot?.profile,
|
|
162
|
-
annualStatements: snapshot?.annualStatements ?? [],
|
|
163
|
-
quarterlyStatements: snapshot?.quarterlyStatements ?? [],
|
|
129
|
+
...resolved,
|
|
164
130
|
priceHistory: normalizePriceHistory(priceHistory ?? snapshot?.priceHistory ?? []),
|
|
165
131
|
};
|
|
166
132
|
}
|