gloomberb 0.3.7 → 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.
Files changed (133) hide show
  1. package/README.md +19 -8
  2. package/package.json +1 -1
  3. package/src/app.tsx +29 -65
  4. package/src/cli/base-converter.ts +31 -0
  5. package/src/cli/commands/search.ts +11 -3
  6. package/src/cli/commands/ticker.ts +22 -11
  7. package/src/cli/context.ts +27 -32
  8. package/src/cli/helpers.ts +1 -4
  9. package/src/cli/index.ts +140 -123
  10. package/src/cli/registry.test.ts +226 -0
  11. package/src/cli/registry.ts +201 -0
  12. package/src/cli.test.ts +219 -0
  13. package/src/components/chart/chart-controller.ts +309 -0
  14. package/src/components/chart/chart-data.ts +5 -6
  15. package/src/components/chart/chart-pane-settings.ts +27 -0
  16. package/src/components/chart/chart-resolution.test.ts +56 -0
  17. package/src/components/chart/chart-resolution.ts +362 -0
  18. package/src/components/chart/chart-types.ts +12 -4
  19. package/src/components/chart/chart-viewport.test.ts +25 -0
  20. package/src/components/chart/chart-viewport.ts +28 -0
  21. package/src/components/chart/comparison-chart-data.ts +20 -27
  22. package/src/components/chart/comparison-stock-chart.tsx +374 -99
  23. package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
  24. package/src/components/chart/stock-chart.tsx +455 -106
  25. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  26. package/src/components/command-bar/command-bar.test.tsx +650 -5
  27. package/src/components/command-bar/command-bar.tsx +359 -63
  28. package/src/components/command-bar/command-registry.ts +8 -0
  29. package/src/components/command-bar/helpers.test.ts +23 -1
  30. package/src/components/command-bar/helpers.ts +14 -5
  31. package/src/components/command-bar/pane-template-display.ts +12 -0
  32. package/src/components/command-bar/root-shortcuts.ts +2 -1
  33. package/src/components/command-bar/workflow-ops.ts +4 -2
  34. package/src/components/detail-feed-view.test.tsx +124 -49
  35. package/src/components/detail-feed-view.tsx +396 -160
  36. package/src/components/layout/floating-pane.tsx +1 -1
  37. package/src/components/layout/pane-header.tsx +26 -2
  38. package/src/components/layout/shell.tsx +62 -50
  39. package/src/components/layout/status-bar.test.tsx +66 -0
  40. package/src/components/layout/status-bar.tsx +26 -5
  41. package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
  42. package/src/components/onboarding/onboarding-wizard.tsx +5 -6
  43. package/src/components/ticker-list-table.test.tsx +83 -0
  44. package/src/components/ticker-list-table.tsx +174 -97
  45. package/src/components/ui/data-table.tsx +27 -2
  46. package/src/components/ui/index.ts +1 -0
  47. package/src/components/ui/page-stack-view.tsx +60 -0
  48. package/src/components/use-double-click-activation.ts +31 -0
  49. package/src/index.tsx +10 -10
  50. package/src/market-data/coordinator.test.ts +92 -4
  51. package/src/market-data/coordinator.ts +32 -5
  52. package/src/market-data/hooks.test.tsx +120 -0
  53. package/src/market-data/hooks.tsx +53 -16
  54. package/src/market-data/request-types.ts +4 -2
  55. package/src/market-data/selectors.ts +24 -58
  56. package/src/plugins/builtin/chat-controller.test.ts +370 -3
  57. package/src/plugins/builtin/chat-controller.ts +310 -35
  58. package/src/plugins/builtin/chat.test.tsx +118 -6
  59. package/src/plugins/builtin/chat.tsx +175 -116
  60. package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
  61. package/src/plugins/builtin/comparison-chart.tsx +21 -1
  62. package/src/plugins/builtin/news.tsx +1 -11
  63. package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
  64. package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
  65. package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
  66. package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
  67. package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
  68. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  69. package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
  70. package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
  71. package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
  72. package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
  73. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
  74. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
  75. package/src/plugins/builtin/sec.tsx +1 -11
  76. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
  77. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
  78. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
  79. package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
  80. package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
  81. package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
  82. package/src/plugins/catalog.ts +65 -0
  83. package/src/plugins/ibkr/broker-adapter.ts +28 -0
  84. package/src/plugins/ibkr/gateway-service.test.ts +45 -1
  85. package/src/plugins/ibkr/gateway-service.ts +141 -7
  86. package/src/plugins/pane-manager.test.ts +1 -1
  87. package/src/plugins/plugin-runtime.tsx +27 -7
  88. package/src/plugins/prediction-markets/cli.ts +28 -0
  89. package/src/plugins/prediction-markets/controller-data.ts +11 -3
  90. package/src/plugins/prediction-markets/controller.ts +141 -121
  91. package/src/plugins/prediction-markets/index.tsx +2 -0
  92. package/src/plugins/prediction-markets/launch.ts +51 -16
  93. package/src/plugins/prediction-markets/metrics.ts +9 -5
  94. package/src/plugins/prediction-markets/pane.test.tsx +119 -57
  95. package/src/plugins/prediction-markets/pane.tsx +74 -153
  96. package/src/plugins/prediction-markets/table.tsx +6 -3
  97. package/src/plugins/prediction-markets/types.ts +0 -1
  98. package/src/sources/gloomberb-cloud.test.ts +29 -0
  99. package/src/sources/gloomberb-cloud.ts +89 -1
  100. package/src/sources/provider-router.test.ts +265 -3
  101. package/src/sources/provider-router.ts +239 -11
  102. package/src/sources/yahoo-finance.test.ts +26 -0
  103. package/src/sources/yahoo-finance.ts +54 -3
  104. package/src/state/app-context-selector.test.tsx +99 -0
  105. package/src/state/app-context.test.ts +18 -0
  106. package/src/state/app-context.tsx +170 -65
  107. package/src/state/selectors.ts +25 -0
  108. package/src/theme/colors.ts +8 -8
  109. package/src/theme/themes.test.ts +2 -0
  110. package/src/theme/themes.ts +22 -10
  111. package/src/types/broker.ts +21 -0
  112. package/src/types/config.ts +17 -1
  113. package/src/types/data-provider.ts +18 -0
  114. package/src/types/financials.ts +31 -1
  115. package/src/types/plugin.ts +70 -0
  116. package/src/utils/api-client.test.ts +57 -0
  117. package/src/utils/api-client.ts +35 -12
  118. package/src/utils/back-navigation.ts +27 -0
  119. package/src/utils/format.test.ts +14 -0
  120. package/src/utils/format.ts +3 -3
  121. package/src/utils/market-status.ts +46 -1
  122. package/src/utils/quote-freshness.ts +127 -0
  123. package/src/utils/quote-resolution.test.ts +277 -0
  124. package/src/utils/quote-resolution.ts +641 -0
  125. package/src/utils/slugify.ts +3 -0
  126. package/src/utils/timestamp.ts +25 -0
  127. package/src/version.ts +1 -1
  128. package/src/cli/commands/portfolio.ts +0 -210
  129. package/src/cli/commands/watchlist.ts +0 -210
  130. package/src/plugins/builtin/data-providers.ts +0 -16
  131. package/src/sources/exchange-rates.ts +0 -18
  132. package/src/utils/ascii-chart.ts +0 -172
  133. 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
- range: "5Y",
34
- granularity: "daily",
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) => readyEntry(current, quote, source, attempts, { keepLastGoodOnEmpty: true }));
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.range,
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, readyEntry(current, quote, quote.providerId ?? this.dataProvider.id, attempts, { keepLastGoodOnEmpty: true }));
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
- return useCoordinatorSelector((coordinator) => {
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
- }, new Map<string, TickerFinancials>());
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 normalizedCurrencies = useMemo(() => stableCurrencyList(currencies), [currencies]);
207
- const entries = useCoordinatorSelector((coordinator) => (
208
- normalizedCurrencies.map((currency) => [currency, coordinator.getFxEntry(currency)] as const)
209
- ), [] as Array<readonly [string, QueryEntry<number>]>);
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
- }, [normalizedCurrencies.join("|")]);
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 = "daily" | "detail";
15
+ export type ChartGranularity = "range" | "detail" | "resolution";
15
16
 
16
17
  export interface ChartRequest {
17
18
  instrument: InstrumentRef;
18
- range: TimeRange;
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.range,
55
- request.granularity ?? "daily",
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
- const resolvedQuote = snapshot?.quote && quote && hasLikelyPriceUnitMismatch(snapshot.quote, quote)
156
- ? snapshot.quote
157
- : (projectExtendedHoursQuote(snapshot?.quote, quote) ?? snapshot?.quote);
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
- quote: resolvedQuote,
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
  }