gloomberb 0.7.2 → 0.7.4

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 (167) hide show
  1. package/README.md +79 -132
  2. package/package.json +1 -1
  3. package/src/app.tsx +73 -67
  4. package/src/components/chart/chart-data.ts +7 -2
  5. package/src/components/chart/chart-pane-settings.ts +7 -4
  6. package/src/components/chart/chart-renderer.ts +158 -47
  7. package/src/components/chart/chart-types.ts +20 -2
  8. package/src/components/chart/chart.test.ts +179 -13
  9. package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
  10. package/src/components/chart/comparison-chart-renderer.ts +50 -11
  11. package/src/components/chart/comparison-stock-chart.tsx +129 -129
  12. package/src/components/chart/market-session.ts +187 -0
  13. package/src/components/chart/native/chart-rasterizer.ts +108 -19
  14. package/src/components/chart/price-axis-labels.tsx +25 -9
  15. package/src/components/chart/stock-chart-renderer-switch.test.tsx +97 -1
  16. package/src/components/chart/stock-chart.tsx +95 -49
  17. package/src/components/chart/time-axis-label.tsx +56 -2
  18. package/src/components/command-bar/command-bar.test.tsx +446 -120
  19. package/src/components/command-bar/command-bar.tsx +535 -384
  20. package/src/components/command-bar/command-registry.ts +16 -6
  21. package/src/components/command-bar/list-model.ts +8 -4
  22. package/src/components/command-bar/root-shortcuts.ts +4 -5
  23. package/src/components/command-bar/view-model.test.ts +21 -0
  24. package/src/components/command-bar/view-model.ts +17 -4
  25. package/src/components/command-bar/workflow-ops.ts +11 -32
  26. package/src/components/data-table-view.test.tsx +24 -1
  27. package/src/components/data-table-view.tsx +3 -2
  28. package/src/components/feed-data-table-stack-view.tsx +3 -2
  29. package/src/components/layout/detached-pane-shell.tsx +2 -2
  30. package/src/components/layout/floating-pane.tsx +9 -2
  31. package/src/components/layout/pane-footer.tsx +1 -1
  32. package/src/components/layout/pane-header.tsx +4 -2
  33. package/src/components/layout/pane-sizing.ts +12 -0
  34. package/src/components/layout/pane.tsx +24 -2
  35. package/src/components/layout/shell.tsx +24 -10
  36. package/src/components/layout/status-bar.test.tsx +4 -1
  37. package/src/components/layout/status-bar.tsx +4 -24
  38. package/src/components/markdown-text.tsx +53 -8
  39. package/src/components/onboarding/onboarding-wizard.tsx +7 -6
  40. package/src/components/pane-settings-dialog.tsx +9 -6
  41. package/src/components/pane-template-wizard.tsx +3 -2
  42. package/src/components/price-selector-dialog.tsx +3 -2
  43. package/src/components/price-sparkline-view.tsx +463 -0
  44. package/src/components/price-sparkline.ts +46 -0
  45. package/src/components/table-view-shared.tsx +21 -3
  46. package/src/components/ticker-list-table-view.tsx +3 -2
  47. package/src/components/ticker-list-table.tsx +15 -66
  48. package/src/components/toggle-list.tsx +3 -3
  49. package/src/components/ui/choice-dialog.tsx +3 -2
  50. package/src/components/ui/multi-select-dialog.tsx +3 -2
  51. package/src/components/ui/native-select.tsx +3 -3
  52. package/src/core/state/app-state.ts +210 -80
  53. package/src/core/state/session-persistence.ts +25 -0
  54. package/src/data/config-store-node.ts +113 -25
  55. package/src/data/config-store.test.ts +65 -3
  56. package/src/data/plugin-state-store.ts +39 -13
  57. package/src/data/sqlite-cache.test.ts +13 -0
  58. package/src/market-data/coordinator-subscriptions.test.ts +102 -18
  59. package/src/market-data/coordinator.test.ts +122 -2
  60. package/src/market-data/coordinator.ts +200 -56
  61. package/src/market-data/hooks.test.tsx +31 -0
  62. package/src/market-data/hooks.tsx +1 -0
  63. package/src/news/aggregator.test.ts +41 -0
  64. package/src/news/aggregator.ts +61 -9
  65. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
  66. package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
  67. package/src/plugins/builtin/ai/settings.ts +3 -0
  68. package/src/plugins/builtin/broker-manager/index.tsx +4 -3
  69. package/src/plugins/builtin/chat-controller.test.ts +398 -17
  70. package/src/plugins/builtin/chat-controller.ts +263 -48
  71. package/src/plugins/builtin/chat.test.tsx +312 -6
  72. package/src/plugins/builtin/chat.tsx +245 -66
  73. package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
  74. package/src/plugins/builtin/correlation/colors.test.ts +39 -0
  75. package/src/plugins/builtin/correlation/colors.ts +87 -0
  76. package/src/plugins/builtin/correlation/index.test.tsx +151 -0
  77. package/src/plugins/builtin/correlation/index.tsx +92 -21
  78. package/src/plugins/builtin/debug.tsx +4 -3
  79. package/src/plugins/builtin/econ/index.tsx +3 -2
  80. package/src/plugins/builtin/help.test.tsx +2 -2
  81. package/src/plugins/builtin/holders/index.tsx +25 -13
  82. package/src/plugins/builtin/layout-manager.tsx +1 -7
  83. package/src/plugins/builtin/market-movers/index.tsx +72 -40
  84. package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
  85. package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
  86. package/src/plugins/builtin/news-wire/index.tsx +1 -3
  87. package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
  88. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
  89. package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
  90. package/src/plugins/builtin/options.tsx +9 -17
  91. package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
  92. package/src/plugins/builtin/portfolio-list/metrics.ts +13 -2
  93. package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
  94. package/src/plugins/builtin/portfolio-list/pane.tsx +139 -23
  95. package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
  96. package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
  97. package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
  98. package/src/plugins/builtin/portfolio-list.test.tsx +311 -9
  99. package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
  100. package/src/plugins/builtin/sectors/index.tsx +275 -43
  101. package/src/plugins/builtin/sectors/sector-data.ts +41 -1
  102. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
  103. package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
  104. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
  105. package/src/plugins/builtin/ticker-detail/pane.tsx +18 -7
  106. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +445 -63
  107. package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
  108. package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
  109. package/src/plugins/builtin/world-indices/index.tsx +58 -19
  110. package/src/plugins/ibkr/trading-pane.tsx +3 -2
  111. package/src/plugins/plugin-runtime.tsx +2 -2
  112. package/src/plugins/prediction-markets/settings.ts +4 -0
  113. package/src/plugins/registry.test.ts +55 -0
  114. package/src/plugins/registry.ts +11 -4
  115. package/src/plugins/ticker-navigation.test.ts +35 -5
  116. package/src/plugins/ticker-navigation.ts +16 -22
  117. package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
  118. package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
  119. package/src/renderers/electrobun/bun/application-menu.ts +6 -1
  120. package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
  121. package/src/renderers/electrobun/bun/index.ts +52 -13
  122. package/src/renderers/electrobun/view/app-services.ts +51 -9
  123. package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
  124. package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
  125. package/src/renderers/electrobun/view/main.tsx +0 -16
  126. package/src/renderers/electrobun/view/styles.css +23 -21
  127. package/src/renderers/electrobun/view/ui-host.tsx +6 -4
  128. package/src/renderers/opentui/host.tsx +2 -1
  129. package/src/renderers/opentui/start.tsx +1 -1
  130. package/src/renderers/opentui/test-utils.tsx +6 -5
  131. package/src/sources/gloomberb-cloud.test.ts +55 -2
  132. package/src/sources/gloomberb-cloud.ts +180 -29
  133. package/src/sources/provider-router.test.ts +567 -10
  134. package/src/sources/provider-router.ts +323 -93
  135. package/src/sources/yahoo-finance.test.ts +37 -0
  136. package/src/sources/yahoo-finance.ts +48 -2
  137. package/src/state/app-bootstrap.test.ts +50 -23
  138. package/src/state/app-bootstrap.ts +29 -13
  139. package/src/state/app-context.test.ts +113 -0
  140. package/src/state/app-context.tsx +8 -4
  141. package/src/state/session-persistence.test.ts +5 -3
  142. package/src/state/use-inline-tickers.ts +3 -0
  143. package/src/state/use-quote-streaming.ts +11 -0
  144. package/src/theme/colors.ts +13 -14
  145. package/src/types/config.ts +9 -4
  146. package/src/types/data-provider.ts +5 -1
  147. package/src/types/financials.ts +1 -0
  148. package/src/types/plugin.ts +8 -1
  149. package/src/ui/context-menu.tsx +4 -4
  150. package/src/utils/api-client.test.ts +248 -0
  151. package/src/utils/api-client.ts +178 -26
  152. package/src/utils/exchanges.test.ts +16 -0
  153. package/src/utils/exchanges.ts +33 -0
  154. package/src/utils/fuzzy-search.ts +12 -1
  155. package/src/utils/keyboard.ts +31 -0
  156. package/src/utils/options.ts +1 -1
  157. package/src/utils/price-history.test.ts +28 -1
  158. package/src/utils/price-history.ts +16 -0
  159. package/src/utils/quote-freshness.test.ts +150 -0
  160. package/src/utils/quote-freshness.ts +42 -1
  161. package/src/utils/quote-resolution.ts +2 -0
  162. package/src/utils/quote-time.test.ts +34 -0
  163. package/src/utils/quote-time.ts +18 -5
  164. package/src/utils/ticker-search.test.ts +2 -2
  165. package/src/utils/ticker-search.ts +48 -1
  166. package/src/version.ts +1 -1
  167. package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
@@ -30,7 +30,8 @@ import {
30
30
  import { hasLikelyQuoteUnitMismatch } from "../utils/currency-units";
31
31
  import { debugLog } from "../utils/debug-log";
32
32
  import { canonicalExchange } from "../utils/exchanges";
33
- import { normalizePriceHistory, normalizeTickerFinancialsPriceHistory } from "../utils/price-history";
33
+ import { isPriceHistoryStaleForCurrentWindow, normalizePriceHistory, normalizeTickerFinancialsPriceHistory } from "../utils/price-history";
34
+ import { parseOptionSymbol } from "../utils/options";
34
35
  import { isQuoteStaleForCurrentSession } from "../utils/quote-freshness";
35
36
  import {
36
37
  mergeQuoteContributionMaps,
@@ -45,6 +46,8 @@ const providerLog = debugLog.createLogger("asset-data-router");
45
46
  const BROKER_ATTEMPT_TIMEOUT = 10_000;
46
47
  const EXPECTED_PROVIDER_MISS = /No data found|symbol may be delisted|"code":"Not Found"|No history for /i;
47
48
  const MARKET_NAMESPACE = "market";
49
+ const SEARCH_CACHE_TTL_MS = 30_000;
50
+ const SEARCH_PROVIDER_TIMEOUT_MS = 5_000;
48
51
 
49
52
  const DEFAULT_CACHE_POLICIES: Record<string, CachePolicy> = {
50
53
  brokerQuote: { staleMs: 15_000, expireMs: 15 * 60_000 },
@@ -73,6 +76,16 @@ function withBrokerTimeout<T>(promise: Promise<T>): Promise<T | null> {
73
76
  });
74
77
  }
75
78
 
79
+ function withSearchTimeout<T>(promise: Promise<T>): Promise<T | null> {
80
+ return new Promise<T | null>((resolve) => {
81
+ const timer = setTimeout(() => resolve(null), SEARCH_PROVIDER_TIMEOUT_MS);
82
+ promise.then(
83
+ (value) => { clearTimeout(timer); resolve(value); },
84
+ () => { clearTimeout(timer); resolve(null); },
85
+ );
86
+ });
87
+ }
88
+
76
89
  function shouldLogProviderError(error: unknown): boolean {
77
90
  if (isProviderMiss(error)) return false;
78
91
  const message = error instanceof Error ? error.message : String(error);
@@ -83,15 +96,66 @@ function normalizeTicker(ticker: string): string {
83
96
  return ticker.trim().toUpperCase();
84
97
  }
85
98
 
86
- function sanitizeCachedFinancials(financials: TickerFinancials): TickerFinancials {
87
- if (!isQuoteStaleForCurrentSession(financials.quote)) return financials;
99
+ function deriveMarketCapFromShares(
100
+ financials: TickerFinancials,
101
+ options: { replaceExisting?: boolean } = {},
102
+ ): TickerFinancials {
103
+ const quote = financials.quote;
104
+ const sharesOutstanding = financials.fundamentals?.sharesOutstanding;
105
+ if (
106
+ !quote
107
+ || (quote.marketCap != null && !options.replaceExisting)
108
+ || !Number.isFinite(quote.price)
109
+ || !Number.isFinite(sharesOutstanding)
110
+ || quote.price <= 0
111
+ || !sharesOutstanding
112
+ || sharesOutstanding <= 0
113
+ ) {
114
+ return financials;
115
+ }
116
+
88
117
  return {
89
118
  ...financials,
119
+ quote: {
120
+ ...quote,
121
+ marketCap: quote.price * sharesOutstanding,
122
+ },
123
+ };
124
+ }
125
+
126
+ function sanitizeCachedFinancials(
127
+ financials: TickerFinancials,
128
+ options: { includeStaleQuotes?: boolean } = {},
129
+ ): TickerFinancials {
130
+ const enriched = deriveMarketCapFromShares(financials);
131
+ if (options.includeStaleQuotes || !isQuoteStaleForCurrentSession(enriched.quote)) return enriched;
132
+ return {
133
+ ...enriched,
90
134
  quote: undefined,
91
135
  quoteContributions: undefined,
92
136
  };
93
137
  }
94
138
 
139
+ function sanitizeCachedQuote(
140
+ quote: Quote,
141
+ exchange: string | undefined,
142
+ options: { includeStaleQuotes?: boolean } = {},
143
+ ): Quote | null {
144
+ const normalized = quoteWithFreshnessExchange(quote, exchange);
145
+ return options.includeStaleQuotes || !isQuoteStaleForCurrentSession(normalized)
146
+ ? normalized
147
+ : null;
148
+ }
149
+
150
+ function quoteWithFreshnessExchange(quote: Quote, exchange?: string): Quote {
151
+ if (!exchange || quote.listingExchangeName || quote.exchangeName) return quote;
152
+ return {
153
+ ...quote,
154
+ listingExchangeName: exchange,
155
+ exchangeName: exchange,
156
+ };
157
+ }
158
+
95
159
  function compactUrl(url: string): string {
96
160
  return url.trim();
97
161
  }
@@ -136,6 +200,16 @@ function isIntradayRange(range: TimeRange): boolean {
136
200
  return range === "1D" || range === "1W" || range === "1M" || range === "3M";
137
201
  }
138
202
 
203
+ function isStaleIntradayHistory(points: PricePoint[], enabled: boolean): boolean {
204
+ return enabled && isPriceHistoryStaleForCurrentWindow(points);
205
+ }
206
+
207
+ function isCurrentHistoryWindow(endDate?: Date): boolean {
208
+ if (!endDate) return true;
209
+ const endMs = endDate.getTime();
210
+ return Number.isFinite(endMs) && Date.now() - endMs < 60 * 60_000;
211
+ }
212
+
139
213
  function hasMeaningfulProfile(data: TickerFinancials | null | undefined): boolean {
140
214
  return !!data && !!(
141
215
  data.profile?.description
@@ -191,7 +265,8 @@ function mergeDefinedObject<T extends object>(preferred: T | null | undefined, f
191
265
  function mergeFinancials(primary: TickerFinancials | null, fallback: TickerFinancials | null): TickerFinancials | null {
192
266
  if (!primary || !fallback) {
193
267
  const single = primary ?? fallback;
194
- return single ? resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(single)) : null;
268
+ const resolved = single ? resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(single)) : null;
269
+ return resolved ? deriveMarketCapFromShares(resolved) : null;
195
270
  }
196
271
 
197
272
  const preferFallbackPriceData = hasLikelyPriceUnitMismatch(primary, fallback);
@@ -203,7 +278,7 @@ function mergeFinancials(primary: TickerFinancials | null, fallback: TickerFinan
203
278
  );
204
279
  const resolvedQuote = resolveCanonicalQuote(quoteContributions).quote;
205
280
 
206
- return {
281
+ return deriveMarketCapFromShares({
207
282
  ...fallback,
208
283
  ...primary,
209
284
  quote: resolvedQuote,
@@ -213,10 +288,13 @@ function mergeFinancials(primary: TickerFinancials | null, fallback: TickerFinan
213
288
  priceHistory: normalizePriceHistory(dominant.priceHistory.length > 0 ? dominant.priceHistory : secondary.priceHistory),
214
289
  annualStatements: primary.annualStatements.length > 0 ? primary.annualStatements : fallback.annualStatements,
215
290
  quarterlyStatements: primary.quarterlyStatements.length > 0 ? primary.quarterlyStatements : fallback.quarterlyStatements,
216
- };
291
+ });
217
292
  }
218
293
 
219
- function mergeCachedFinancialRecords(records: CachedResourceRecord<TickerFinancials>[]): {
294
+ function mergeCachedFinancialRecords(
295
+ records: CachedResourceRecord<TickerFinancials>[],
296
+ options: { includeStaleQuotes?: boolean } = {},
297
+ ): {
220
298
  value: TickerFinancials | null;
221
299
  stale: boolean;
222
300
  } {
@@ -227,13 +305,29 @@ function mergeCachedFinancialRecords(records: CachedResourceRecord<TickerFinanci
227
305
  for (const record of records) {
228
306
  if (seenSources.has(record.sourceKey)) continue;
229
307
  seenSources.add(record.sourceKey);
230
- merged = mergeFinancials(merged, sanitizeCachedFinancials(record.value));
231
- stale = stale || record.stale;
308
+ merged = mergeFinancials(merged, sanitizeCachedFinancials(record.value, options));
309
+ stale = stale || record.stale === true;
232
310
  }
233
311
 
234
312
  return { value: merged, stale };
235
313
  }
236
314
 
315
+ function selectCachedQuoteRecord(
316
+ records: CachedResourceRecord<Quote>[],
317
+ exchange: string | undefined,
318
+ options: { includeStaleQuotes?: boolean } = {},
319
+ ): CachedQuoteSelection {
320
+ let stale = false;
321
+
322
+ for (const record of records) {
323
+ stale ||= record.stale === true;
324
+ const quote = sanitizeCachedQuote(record.value, exchange, options);
325
+ if (quote) return { quote, stale };
326
+ }
327
+
328
+ return { quote: null, stale };
329
+ }
330
+
237
331
  interface CachedFinancialsSelection {
238
332
  brokerRecord: CachedResourceRecord<TickerFinancials> | null;
239
333
  providerValue: TickerFinancials | null;
@@ -241,6 +335,16 @@ interface CachedFinancialsSelection {
241
335
  stale: boolean;
242
336
  }
243
337
 
338
+ interface CachedFinancialsReadOptions {
339
+ includeStaleQuotes?: boolean;
340
+ includeSymbolProviderFallback?: boolean;
341
+ }
342
+
343
+ interface CachedQuoteSelection {
344
+ quote: Quote | null;
345
+ stale: boolean;
346
+ }
347
+
244
348
  interface BrokerCandidate {
245
349
  brokerId: string;
246
350
  brokerInstanceId: string;
@@ -299,6 +403,8 @@ export class AssetDataRouter implements DataProvider {
299
403
 
300
404
  private registry: PluginRegistry | null = null;
301
405
  private readonly revalidationInFlight = new Map<string, Promise<unknown>>();
406
+ private readonly searchCache = new Map<string, { expiresAt: number; results: InstrumentSearchResult[] }>();
407
+ private readonly searchInFlight = new Map<string, Promise<InstrumentSearchResult[]>>();
302
408
  private getConfigFn: () => AppConfig = () => createDefaultConfig("");
303
409
  private readonly fallbackSource: CapabilityRouteSource | null;
304
410
  private readonly extraSources: CapabilityRouteSource[];
@@ -335,14 +441,20 @@ export class AssetDataRouter implements DataProvider {
335
441
  return false;
336
442
  }
337
443
 
338
- getCachedFinancialsForTargets(targets: CachedFinancialsTarget[], options: { allowExpired?: boolean } = {}): Map<string, TickerFinancials> {
444
+ getCachedFinancialsForTargets(
445
+ targets: CachedFinancialsTarget[],
446
+ options: { allowExpired?: boolean; includeStaleQuotes?: boolean } = {},
447
+ ): Map<string, TickerFinancials> {
339
448
  const results = new Map<string, TickerFinancials>();
340
449
  for (const target of targets) {
341
450
  const cached = this.readCachedMergedFinancials(target.symbol, target.exchange, {
342
451
  brokerId: target.brokerId,
343
452
  brokerInstanceId: target.brokerInstanceId,
344
453
  instrument: target.instrument ?? undefined,
345
- }, options.allowExpired ?? true);
454
+ }, options.allowExpired ?? true, {
455
+ includeStaleQuotes: options.includeStaleQuotes,
456
+ includeSymbolProviderFallback: true,
457
+ });
346
458
  if (cached) results.set(target.symbol.toUpperCase(), cached);
347
459
  }
348
460
  return results;
@@ -374,7 +486,7 @@ export class AssetDataRouter implements DataProvider {
374
486
  ...this.getProviderSourceKeys(),
375
487
  ];
376
488
  const rawCached = this.selectCachedResource<Quote>("quote", entityKey, variantKeys, sourceKeys, false);
377
- const cached = rawCached && !isQuoteStaleForCurrentSession(rawCached.value)
489
+ const cached = rawCached && !isQuoteStaleForCurrentSession(quoteWithFreshnessExchange(rawCached.value, target.exchange))
378
490
  ? rawCached
379
491
  : null;
380
492
  if (cached && !forceRefresh && !cached.stale) {
@@ -397,7 +509,7 @@ export class AssetDataRouter implements DataProvider {
397
509
  const uniqueTargets = [...providerIndexes.values()].map((bucket) => bucket[0]!.target);
398
510
  const batchResults = await batchProvider.getQuotesBatch!(uniqueTargets, options).catch(() => []);
399
511
  for (const item of batchResults) {
400
- if (!item.quote) continue;
512
+ if (!item.quote || isQuoteStaleForCurrentSession(item.quote)) continue;
401
513
  const key = this.quoteBatchKey(item.target);
402
514
  const sourceKey = this.providerSourceKey(batchProvider);
403
515
  for (const entry of providerIndexes.get(key) ?? []) {
@@ -487,9 +599,23 @@ export class AssetDataRouter implements DataProvider {
487
599
  }
488
600
 
489
601
  async getTickerFinancials(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<TickerFinancials> {
602
+ const isOptionTicker =
603
+ parseOptionSymbol(ticker) != null ||
604
+ parseOptionSymbol(context?.instrument?.localSymbol ?? "") != null ||
605
+ context?.instrument?.secType === "OPT";
606
+ const quoteOnlyFinancials = async (base?: TickerFinancials | null): Promise<TickerFinancials> => ({
607
+ ...base,
608
+ quote: await this.getQuote(ticker, exchange, context),
609
+ annualStatements: base?.annualStatements ?? [],
610
+ quarterlyStatements: base?.quarterlyStatements ?? [],
611
+ priceHistory: base?.priceHistory ?? [],
612
+ });
490
613
  const cached = this.readCachedMergedFinancialsSelection(ticker, exchange, context, false);
491
614
  const forceRefresh = context?.cacheMode === "refresh";
492
615
  if (cached.value && !forceRefresh) {
616
+ if (isOptionTicker && !cached.value.quote) {
617
+ return quoteOnlyFinancials(cached.value);
618
+ }
493
619
  if (!cached.stale && hasMeaningfulProfile(cached.value)) {
494
620
  return cached.value;
495
621
  }
@@ -502,15 +628,22 @@ export class AssetDataRouter implements DataProvider {
502
628
  if (cached.value) {
503
629
  const brokerResult = await withBrokerTimeout(this.fetchBrokerFinancials(ticker, exchange, context));
504
630
  const providerResult = await this.fetchProviderFinancials(ticker, exchange, context);
505
- return mergeFinancials(
631
+ const merged = mergeFinancials(
506
632
  brokerResult?.value ?? cached.brokerRecord?.value ?? null,
507
633
  providerResult?.value ?? cached.providerValue ?? null,
508
- ) ?? cached.value;
634
+ );
635
+ if (isOptionTicker && !merged?.quote) {
636
+ return quoteOnlyFinancials(merged ?? cached.value);
637
+ }
638
+ return merged ?? cached.value;
509
639
  }
510
640
 
511
641
  const brokerResult = await withBrokerTimeout(this.fetchBrokerFinancials(ticker, exchange, context));
512
642
  const fallback = await this.fetchProviderFinancials(ticker, exchange, context);
513
643
  const merged = mergeFinancials(brokerResult?.value ?? null, fallback?.value ?? null);
644
+ if (isOptionTicker && !merged?.quote) {
645
+ return quoteOnlyFinancials(merged);
646
+ }
514
647
  if (!merged) {
515
648
  throw new Error(`No provider available for ${ticker}`);
516
649
  }
@@ -525,7 +658,7 @@ export class AssetDataRouter implements DataProvider {
525
658
  ...this.getProviderSourceKeys(),
526
659
  ];
527
660
  const rawCached = this.selectCachedResource<Quote>("quote", entityKey, variantKeys, sourceKeys, false);
528
- const cached = rawCached && !isQuoteStaleForCurrentSession(rawCached.value)
661
+ const cached = rawCached && !isQuoteStaleForCurrentSession(quoteWithFreshnessExchange(rawCached.value, exchange))
529
662
  ? rawCached
530
663
  : null;
531
664
  const forceRefresh = context?.cacheMode === "refresh";
@@ -534,7 +667,9 @@ export class AssetDataRouter implements DataProvider {
534
667
  }
535
668
 
536
669
  const brokerQuote = await withBrokerTimeout(this.fetchBrokerQuote(ticker, exchange, context));
537
- if (brokerQuote) return brokerQuote.value;
670
+ if (brokerQuote && !isQuoteStaleForCurrentSession(quoteWithFreshnessExchange(brokerQuote.value, exchange))) {
671
+ return brokerQuote.value;
672
+ }
538
673
 
539
674
  const providerQuote = await this.fetchProviderQuote(ticker, exchange, context);
540
675
  if (providerQuote) {
@@ -545,73 +680,114 @@ export class AssetDataRouter implements DataProvider {
545
680
  }
546
681
 
547
682
  async getExchangeRate(fromCurrency: string): Promise<number> {
548
- const cached = this.readCachedExchangeRate(fromCurrency, false);
683
+ const normalizedCurrency = fromCurrency.trim().toUpperCase();
684
+ if (normalizedCurrency === "USD") return 1;
685
+
686
+ const cached = this.readCachedExchangeRate(normalizedCurrency, false);
549
687
  if (cached != null) {
550
- this.scheduleRevalidation(`exchange-rate:${fromCurrency.toUpperCase()}`, async () => {
551
- await this.revalidateExchangeRate(fromCurrency);
688
+ this.scheduleRevalidation(`exchange-rate:${normalizedCurrency}`, async () => {
689
+ await this.revalidateExchangeRate(normalizedCurrency);
552
690
  });
553
691
  return cached;
554
692
  }
555
693
 
556
694
  const result = await this.firstProvider(async (provider) => {
557
- const rate = await provider.getExchangeRate(fromCurrency);
695
+ const rate = await provider.getExchangeRate(normalizedCurrency);
558
696
  return { provider, rate };
559
697
  });
560
698
  if (!result) {
561
- throw new Error(`No exchange rate provider available for ${fromCurrency}`);
699
+ throw new Error(`No exchange rate provider available for ${normalizedCurrency}`);
562
700
  }
563
- this.cacheExchangeRate(fromCurrency, result.value.rate, result.value.provider);
701
+ this.cacheExchangeRate(normalizedCurrency, result.value.rate, result.value.provider);
564
702
  return result.value.rate;
565
703
  }
566
704
 
567
705
  async search(query: string, context?: SearchRequestContext): Promise<InstrumentSearchResult[]> {
568
- const results: InstrumentSearchResult[] = [];
569
- const resultIndexByKey = new Map<string, number>();
570
-
571
- const push = (items: InstrumentSearchResult[]) => {
572
- for (const item of items) {
573
- const key = buildSearchResultKey(item);
574
- const existingIndex = resultIndexByKey.get(key);
575
- if (existingIndex == null) {
576
- resultIndexByKey.set(key, results.length);
577
- results.push(item);
578
- continue;
579
- }
706
+ const cacheKey = JSON.stringify([
707
+ query.trim().toUpperCase(),
708
+ context?.preferBroker ?? true,
709
+ context?.brokerInstanceId ?? "",
710
+ context?.brokerId ?? "",
711
+ ]);
712
+ const cached = this.searchCache.get(cacheKey);
713
+ if (cached && cached.expiresAt > Date.now()) return cached.results;
714
+ const inFlight = this.searchInFlight.get(cacheKey);
715
+ if (inFlight) return inFlight;
716
+
717
+ const task = (async () => {
718
+ const results: InstrumentSearchResult[] = [];
719
+ const resultIndexByKey = new Map<string, number>();
720
+
721
+ const push = (items: InstrumentSearchResult[]) => {
722
+ for (const item of items) {
723
+ const key = buildSearchResultKey(item);
724
+ const existingIndex = resultIndexByKey.get(key);
725
+ if (existingIndex == null) {
726
+ resultIndexByKey.set(key, results.length);
727
+ results.push(item);
728
+ continue;
729
+ }
580
730
 
581
- const existing = results[existingIndex]!;
582
- if (getSearchResultRichness(item, context) > getSearchResultRichness(existing, context)) {
583
- results[existingIndex] = item;
731
+ const existing = results[existingIndex]!;
732
+ if (getSearchResultRichness(item, context) > getSearchResultRichness(existing, context)) {
733
+ results[existingIndex] = item;
734
+ }
735
+ }
736
+ };
737
+
738
+ if (context?.preferBroker !== false) {
739
+ for (const candidate of this.getBrokerCandidates(
740
+ context?.brokerInstanceId,
741
+ context?.brokerId,
742
+ )) {
743
+ if (!candidate.broker.searchInstruments) continue;
744
+ try {
745
+ const items = await withSearchTimeout(candidate.broker.searchInstruments(query, candidate.instance));
746
+ if (!items) continue;
747
+ push(this.annotateSearchResults(items, candidate));
748
+ if (results.length > 0) {
749
+ this.searchCache.set(cacheKey, {
750
+ expiresAt: Date.now() + SEARCH_CACHE_TTL_MS,
751
+ results,
752
+ });
753
+ return results;
754
+ }
755
+ } catch {
756
+ // continue through broker candidates
757
+ }
584
758
  }
585
759
  }
586
- };
587
760
 
588
- const searchPromises: Promise<void>[] = [];
589
-
590
- if (context?.preferBroker !== false) {
591
- for (const candidate of this.getBrokerCandidates(
592
- context?.brokerInstanceId,
593
- context?.brokerId,
594
- )) {
595
- if (!candidate.broker.searchInstruments) continue;
596
- searchPromises.push(
597
- candidate.broker.searchInstruments(query, candidate.instance)
598
- .then((items) => push(this.annotateSearchResults(items, candidate)))
599
- .catch(() => {}),
600
- );
761
+ for (const provider of this.providersInPriorityOrder()) {
762
+ try {
763
+ const items = await withSearchTimeout(provider.search(query, context));
764
+ if (!items) continue;
765
+ push(items);
766
+ if (results.length > 0) {
767
+ this.searchCache.set(cacheKey, {
768
+ expiresAt: Date.now() + SEARCH_CACHE_TTL_MS,
769
+ results,
770
+ });
771
+ return results;
772
+ }
773
+ } catch (error) {
774
+ if (shouldLogProviderError(error)) {
775
+ providerLog.error(`${provider.id} failed: ${error}`);
776
+ }
777
+ }
601
778
  }
602
- }
603
779
 
604
- for (const provider of this.providersInPriorityOrder()) {
605
- searchPromises.push(
606
- provider.search(query, context)
607
- .then((items) => push(items))
608
- .catch(() => {}),
609
- );
610
- }
780
+ this.searchCache.set(cacheKey, {
781
+ expiresAt: Date.now() + Math.min(SEARCH_CACHE_TTL_MS, 5_000),
782
+ results,
783
+ });
784
+ return results;
785
+ })().finally(() => {
786
+ this.searchInFlight.delete(cacheKey);
787
+ });
611
788
 
612
- const deadline = new Promise<void>((resolve) => setTimeout(resolve, 5_000));
613
- await Promise.race([Promise.all(searchPromises), deadline]);
614
- return results;
789
+ this.searchInFlight.set(cacheKey, task);
790
+ return task;
615
791
  }
616
792
 
617
793
  async getNews(query: NewsQuery): Promise<NewsArticle[]> {
@@ -764,8 +940,9 @@ export class AssetDataRouter implements DataProvider {
764
940
  ];
765
941
  const cached = this.selectCachedArrayResource<PricePoint>("price-history", entityKey, variantKeys, sourceKeys, false);
766
942
  const cachedValue = cached ? normalizePriceHistory(cached.value) : [];
943
+ const cachedHistoryStale = isStaleIntradayHistory(cachedValue, isIntradayRange(range));
767
944
  const forceRefresh = context?.cacheMode === "refresh";
768
- if (cachedValue.length > 0 && !forceRefresh && cached && !cached.stale) {
945
+ if (cachedValue.length > 0 && !forceRefresh && cached && !cached.stale && !cachedHistoryStale) {
769
946
  return cachedValue;
770
947
  }
771
948
 
@@ -776,7 +953,7 @@ export class AssetDataRouter implements DataProvider {
776
953
  if (providerHistory && providerHistory.value.length > 0) {
777
954
  return providerHistory.value;
778
955
  }
779
- if (cachedValue.length > 0) return cachedValue;
956
+ if (cachedValue.length > 0 && !cachedHistoryStale) return cachedValue;
780
957
  if (!providerHistory) {
781
958
  throw new Error(`No history provider available for ${ticker}`);
782
959
  }
@@ -801,8 +978,9 @@ export class AssetDataRouter implements DataProvider {
801
978
  ];
802
979
  const cached = this.selectCachedArrayResource<PricePoint>("price-history", entityKey, variantKeys, sourceKeys, false);
803
980
  const cachedValue = cached ? normalizePriceHistory(cached.value) : [];
981
+ const cachedHistoryStale = isStaleIntradayHistory(cachedValue, isIntradayResolution(resolution));
804
982
  const forceRefresh = context?.cacheMode === "refresh";
805
- if (cachedValue.length > 0 && !forceRefresh && cached && !cached.stale) {
983
+ if (cachedValue.length > 0 && !forceRefresh && cached && !cached.stale && !cachedHistoryStale) {
806
984
  return cachedValue;
807
985
  }
808
986
 
@@ -813,7 +991,7 @@ export class AssetDataRouter implements DataProvider {
813
991
  if (providerHistory && providerHistory.value.length > 0) {
814
992
  return providerHistory.value;
815
993
  }
816
- if (cachedValue.length > 0) return cachedValue;
994
+ if (cachedValue.length > 0 && !cachedHistoryStale) return cachedValue;
817
995
  if (!providerHistory) {
818
996
  throw new Error(`No resolution-aware history provider available for ${ticker}`);
819
997
  }
@@ -861,8 +1039,10 @@ export class AssetDataRouter implements DataProvider {
861
1039
  ];
862
1040
  const cached = this.selectCachedArrayResource<PricePoint>("detailed-price-history", entityKey, variantKeys, sourceKeys, false);
863
1041
  const cachedValue = cached ? normalizePriceHistory(cached.value) : [];
1042
+ const isCurrentWindow = isCurrentHistoryWindow(endDate);
1043
+ const cachedHistoryStale = isCurrentWindow && isPriceHistoryStaleForCurrentWindow(cachedValue);
864
1044
  const forceRefresh = context?.cacheMode === "refresh";
865
- if (cachedValue.length > 0 && !forceRefresh && cached && !cached.stale) {
1045
+ if (cachedValue.length > 0 && !forceRefresh && cached && !cached.stale && !cachedHistoryStale) {
866
1046
  return cachedValue;
867
1047
  }
868
1048
 
@@ -873,7 +1053,7 @@ export class AssetDataRouter implements DataProvider {
873
1053
  if (providerResult && providerResult.value.length > 0) {
874
1054
  return providerResult.value;
875
1055
  }
876
- return cachedValue.length > 0 ? cachedValue : (providerResult?.value ?? []);
1056
+ return cachedValue.length > 0 && !cachedHistoryStale ? cachedValue : (providerResult?.value ?? []);
877
1057
  }
878
1058
 
879
1059
  async getOptionsChain(ticker: string, exchange?: string, expirationDate?: number, context?: MarketDataRequestContext): Promise<OptionsChain> {
@@ -997,6 +1177,14 @@ export class AssetDataRouter implements DataProvider {
997
1177
  });
998
1178
  if (records.length === 0) return [];
999
1179
 
1180
+ return this.sortCachedRecords(records, variantKeys, sourceKeys);
1181
+ }
1182
+
1183
+ private sortCachedRecords<T>(
1184
+ records: CachedResourceRecord<T>[],
1185
+ variantKeys: string[],
1186
+ sourceKeys: string[],
1187
+ ): CachedResourceRecord<T>[] {
1000
1188
  const sourceRank = new Map(sourceKeys.map((sourceKey, index) => [sourceKey, index]));
1001
1189
  const variantRank = new Map(variantKeys.map((variantKey, index) => [variantKey, index]));
1002
1190
  return [...records].sort((a, b) => {
@@ -1046,8 +1234,9 @@ export class AssetDataRouter implements DataProvider {
1046
1234
  exchange?: string,
1047
1235
  context?: MarketDataRequestContext,
1048
1236
  allowExpired = false,
1237
+ options: CachedFinancialsReadOptions = {},
1049
1238
  ): TickerFinancials | null {
1050
- return this.readCachedMergedFinancialsSelection(ticker, exchange, context, allowExpired).value;
1239
+ return this.readCachedMergedFinancialsSelection(ticker, exchange, context, allowExpired, options).value;
1051
1240
  }
1052
1241
 
1053
1242
  private readCachedMergedFinancialsSelection(
@@ -1055,6 +1244,7 @@ export class AssetDataRouter implements DataProvider {
1055
1244
  exchange?: string,
1056
1245
  context?: MarketDataRequestContext,
1057
1246
  allowExpired = false,
1247
+ options: CachedFinancialsReadOptions = {},
1058
1248
  ): CachedFinancialsSelection {
1059
1249
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1060
1250
  const variantKeys = this.getTickerVariantCandidates(exchange);
@@ -1063,22 +1253,51 @@ export class AssetDataRouter implements DataProvider {
1063
1253
  ? this.selectCachedResource<TickerFinancials>("financials", entityKey, variantKeys, brokerSourceKeys, allowExpired)
1064
1254
  : null;
1065
1255
  const sanitizedBrokerRecord = brokerRecord
1066
- ? { ...brokerRecord, value: sanitizeCachedFinancials(brokerRecord.value) }
1256
+ ? { ...brokerRecord, value: sanitizeCachedFinancials(brokerRecord.value, options) }
1067
1257
  : null;
1068
- const providerSelection = mergeCachedFinancialRecords(
1069
- this.listCachedResources<TickerFinancials>(
1258
+ const providerSourceKeys = this.getProviderSourceKeys();
1259
+ const providerEntityKeys = options.includeSymbolProviderFallback
1260
+ ? [...new Set([entityKey, normalizeTicker(ticker)])]
1261
+ : [entityKey];
1262
+ const providerRecords = this.sortCachedRecords(
1263
+ providerEntityKeys.flatMap((providerEntityKey) => this.listCachedResources<TickerFinancials>(
1070
1264
  "financials",
1071
- entityKey,
1265
+ providerEntityKey,
1072
1266
  variantKeys,
1073
- this.getProviderSourceKeys(),
1267
+ providerSourceKeys,
1074
1268
  allowExpired,
1075
- ),
1269
+ )),
1270
+ variantKeys,
1271
+ providerSourceKeys,
1272
+ );
1273
+ const providerSelection = mergeCachedFinancialRecords(
1274
+ providerRecords,
1275
+ options,
1276
+ );
1277
+ const quoteSourceKeys = [...brokerSourceKeys, ...providerSourceKeys];
1278
+ const quoteRecords = this.sortCachedRecords(
1279
+ providerEntityKeys.flatMap((quoteEntityKey) => this.listCachedResources<Quote>(
1280
+ "quote",
1281
+ quoteEntityKey,
1282
+ variantKeys,
1283
+ quoteSourceKeys,
1284
+ allowExpired,
1285
+ )),
1286
+ variantKeys,
1287
+ quoteSourceKeys,
1076
1288
  );
1289
+ const quoteSelection = selectCachedQuoteRecord(quoteRecords, exchange, options);
1290
+ const mergedValue = mergeFinancials(sanitizedBrokerRecord?.value ?? null, providerSelection.value);
1291
+ const value = quoteSelection.quote
1292
+ ? resolveTickerFinancialsQuoteState(mergedValue, quoteSelection.quote)
1293
+ : mergedValue;
1077
1294
  return {
1078
1295
  brokerRecord: sanitizedBrokerRecord,
1079
1296
  providerValue: providerSelection.value,
1080
- value: mergeFinancials(sanitizedBrokerRecord?.value ?? null, providerSelection.value),
1081
- stale: (sanitizedBrokerRecord?.stale ?? false) || providerSelection.stale,
1297
+ value: value
1298
+ ? deriveMarketCapFromShares(value, { replaceExisting: !!quoteSelection.quote && quoteSelection.quote.marketCap == null })
1299
+ : null,
1300
+ stale: (sanitizedBrokerRecord?.stale ?? false) || providerSelection.stale || quoteSelection.stale,
1082
1301
  };
1083
1302
  }
1084
1303
 
@@ -1308,8 +1527,6 @@ export class AssetDataRouter implements DataProvider {
1308
1527
  ): Promise<SourceResult<TickerFinancials> | null> {
1309
1528
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1310
1529
  const variantKey = this.getTickerVariantCandidates(exchange)[0] ?? "";
1311
- let merged: TickerFinancials | null = null;
1312
- let firstSourceKey: string | null = null;
1313
1530
 
1314
1531
  for (const provider of this.providersInPriorityOrder()) {
1315
1532
  try {
@@ -1325,8 +1542,7 @@ export class AssetDataRouter implements DataProvider {
1325
1542
  value,
1326
1543
  this.resolveProviderPolicy("financials", provider),
1327
1544
  );
1328
- merged = mergeFinancials(merged, value);
1329
- firstSourceKey ??= sourceKey;
1545
+ return { sourceKey, value };
1330
1546
  } catch (error) {
1331
1547
  if (shouldLogProviderError(error)) {
1332
1548
  providerLog.error(`${provider.id} failed: ${error}`);
@@ -1334,9 +1550,7 @@ export class AssetDataRouter implements DataProvider {
1334
1550
  }
1335
1551
  }
1336
1552
 
1337
- return merged && firstSourceKey
1338
- ? { sourceKey: firstSourceKey, value: merged }
1339
- : null;
1553
+ return null;
1340
1554
  }
1341
1555
 
1342
1556
  private async fetchBrokerQuote(
@@ -1378,13 +1592,20 @@ export class AssetDataRouter implements DataProvider {
1378
1592
  ): Promise<SourceResult<Quote> | null> {
1379
1593
  const entityKey = this.getEntityKey(ticker, exchange, context?.instrument);
1380
1594
  const variantKey = this.getTickerVariantCandidates(exchange)[0] ?? "";
1381
- const result = await this.firstProvider((provider) => provider.getQuote(ticker, exchange, context));
1382
- if (!result) return null;
1383
- const provider = this.resolveProviderBySourceKey(result.sourceKey);
1384
- if (provider) {
1385
- this.cacheResource("quote", entityKey, variantKey, result.sourceKey, result.value, this.resolveProviderPolicy("quote", provider));
1595
+ for (const provider of this.providersInPriorityOrder()) {
1596
+ try {
1597
+ const quote = await provider.getQuote(ticker, exchange, context);
1598
+ if (quote == null || isQuoteStaleForCurrentSession(quote)) continue;
1599
+ const sourceKey = this.providerSourceKey(provider);
1600
+ this.cacheResource("quote", entityKey, variantKey, sourceKey, quote, this.resolveProviderPolicy("quote", provider));
1601
+ return { sourceKey, value: quote };
1602
+ } catch (err) {
1603
+ if (shouldLogProviderError(err)) {
1604
+ providerLog.error(`${provider.id} failed: ${err}`);
1605
+ }
1606
+ }
1386
1607
  }
1387
- return result;
1608
+ return null;
1388
1609
  }
1389
1610
 
1390
1611
  private async fetchBrokerPriceHistory(
@@ -1405,6 +1626,7 @@ export class AssetDataRouter implements DataProvider {
1405
1626
  range,
1406
1627
  context?.instrument ?? null,
1407
1628
  ));
1629
+ if (isStaleIntradayHistory(result, isIntradayRange(range))) continue;
1408
1630
  this.cacheResource(
1409
1631
  "price-history",
1410
1632
  entityKey,
@@ -1441,6 +1663,7 @@ export class AssetDataRouter implements DataProvider {
1441
1663
  resolution,
1442
1664
  context?.instrument ?? null,
1443
1665
  ));
1666
+ if (isStaleIntradayHistory(result, isIntradayResolution(resolution))) continue;
1444
1667
  this.cacheResource(
1445
1668
  "price-history",
1446
1669
  entityKey,
@@ -1470,6 +1693,7 @@ export class AssetDataRouter implements DataProvider {
1470
1693
  for (const provider of this.providersInPriorityOrder()) {
1471
1694
  try {
1472
1695
  const value = normalizePriceHistory(await provider.getPriceHistory(ticker, exchange, range, context));
1696
+ if (isStaleIntradayHistory(value, isIntradayRange(range))) continue;
1473
1697
  const sourceKey = this.providerSourceKey(provider);
1474
1698
  this.cacheResource(
1475
1699
  "price-history",
@@ -1508,6 +1732,7 @@ export class AssetDataRouter implements DataProvider {
1508
1732
  if (!provider.getPriceHistoryForResolution) continue;
1509
1733
  try {
1510
1734
  const value = normalizePriceHistory(await provider.getPriceHistoryForResolution(ticker, exchange, bufferRange, resolution, context));
1735
+ if (isStaleIntradayHistory(value, isIntradayResolution(resolution))) continue;
1511
1736
  const sourceKey = this.providerSourceKey(provider);
1512
1737
  this.cacheResource(
1513
1738
  "price-history",
@@ -1610,6 +1835,7 @@ export class AssetDataRouter implements DataProvider {
1610
1835
  barSize,
1611
1836
  context?.instrument ?? null,
1612
1837
  ));
1838
+ if (isCurrentHistoryWindow(endDate) && isPriceHistoryStaleForCurrentWindow(result)) continue;
1613
1839
  this.cacheResource(
1614
1840
  "detailed-price-history",
1615
1841
  entityKey,
@@ -1642,6 +1868,7 @@ export class AssetDataRouter implements DataProvider {
1642
1868
  if (!provider.getDetailedPriceHistory) continue;
1643
1869
  try {
1644
1870
  const value = normalizePriceHistory(await provider.getDetailedPriceHistory(ticker, exchange, startDate, endDate, barSize, context));
1871
+ if (isCurrentHistoryWindow(endDate) && isPriceHistoryStaleForCurrentWindow(value)) continue;
1645
1872
  const sourceKey = this.providerSourceKey(provider);
1646
1873
  this.cacheResource(
1647
1874
  "detailed-price-history",
@@ -1880,12 +2107,15 @@ export class AssetDataRouter implements DataProvider {
1880
2107
  }
1881
2108
 
1882
2109
  private async revalidateExchangeRate(fromCurrency: string): Promise<void> {
2110
+ const normalizedCurrency = fromCurrency.trim().toUpperCase();
2111
+ if (normalizedCurrency === "USD") return;
2112
+
1883
2113
  const result = await this.firstProvider(async (provider) => {
1884
- const rate = await provider.getExchangeRate(fromCurrency);
2114
+ const rate = await provider.getExchangeRate(normalizedCurrency);
1885
2115
  return { provider, rate };
1886
2116
  });
1887
2117
  if (!result) return;
1888
- this.cacheExchangeRate(fromCurrency, result.value.rate, result.value.provider);
2118
+ this.cacheExchangeRate(normalizedCurrency, result.value.rate, result.value.provider);
1889
2119
  }
1890
2120
 
1891
2121
  private async revalidateSecFilings(ticker: string, count = 15, exchange?: string, context?: MarketDataRequestContext): Promise<void> {