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.
- package/README.md +79 -132
- package/package.json +1 -1
- package/src/app.tsx +73 -67
- package/src/components/chart/chart-data.ts +7 -2
- package/src/components/chart/chart-pane-settings.ts +7 -4
- package/src/components/chart/chart-renderer.ts +158 -47
- package/src/components/chart/chart-types.ts +20 -2
- package/src/components/chart/chart.test.ts +179 -13
- package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
- package/src/components/chart/comparison-chart-renderer.ts +50 -11
- package/src/components/chart/comparison-stock-chart.tsx +129 -129
- package/src/components/chart/market-session.ts +187 -0
- package/src/components/chart/native/chart-rasterizer.ts +108 -19
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +97 -1
- package/src/components/chart/stock-chart.tsx +95 -49
- package/src/components/chart/time-axis-label.tsx +56 -2
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +535 -384
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/list-model.ts +8 -4
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +21 -0
- package/src/components/command-bar/view-model.ts +17 -4
- package/src/components/command-bar/workflow-ops.ts +11 -32
- package/src/components/data-table-view.test.tsx +24 -1
- package/src/components/data-table-view.tsx +3 -2
- package/src/components/feed-data-table-stack-view.tsx +3 -2
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/floating-pane.tsx +9 -2
- package/src/components/layout/pane-footer.tsx +1 -1
- package/src/components/layout/pane-header.tsx +4 -2
- package/src/components/layout/pane-sizing.ts +12 -0
- package/src/components/layout/pane.tsx +24 -2
- package/src/components/layout/shell.tsx +24 -10
- package/src/components/layout/status-bar.test.tsx +4 -1
- package/src/components/layout/status-bar.tsx +4 -24
- package/src/components/markdown-text.tsx +53 -8
- package/src/components/onboarding/onboarding-wizard.tsx +7 -6
- package/src/components/pane-settings-dialog.tsx +9 -6
- package/src/components/pane-template-wizard.tsx +3 -2
- package/src/components/price-selector-dialog.tsx +3 -2
- package/src/components/price-sparkline-view.tsx +463 -0
- package/src/components/price-sparkline.ts +46 -0
- package/src/components/table-view-shared.tsx +21 -3
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +15 -66
- package/src/components/toggle-list.tsx +3 -3
- package/src/components/ui/choice-dialog.tsx +3 -2
- package/src/components/ui/multi-select-dialog.tsx +3 -2
- package/src/components/ui/native-select.tsx +3 -3
- package/src/core/state/app-state.ts +210 -80
- package/src/core/state/session-persistence.ts +25 -0
- package/src/data/config-store-node.ts +113 -25
- package/src/data/config-store.test.ts +65 -3
- package/src/data/plugin-state-store.ts +39 -13
- package/src/data/sqlite-cache.test.ts +13 -0
- package/src/market-data/coordinator-subscriptions.test.ts +102 -18
- package/src/market-data/coordinator.test.ts +122 -2
- package/src/market-data/coordinator.ts +200 -56
- package/src/market-data/hooks.test.tsx +31 -0
- package/src/market-data/hooks.tsx +1 -0
- package/src/news/aggregator.test.ts +41 -0
- package/src/news/aggregator.ts +61 -9
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
- package/src/plugins/builtin/ai/settings.ts +3 -0
- package/src/plugins/builtin/broker-manager/index.tsx +4 -3
- package/src/plugins/builtin/chat-controller.test.ts +398 -17
- package/src/plugins/builtin/chat-controller.ts +263 -48
- package/src/plugins/builtin/chat.test.tsx +312 -6
- package/src/plugins/builtin/chat.tsx +245 -66
- package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
- package/src/plugins/builtin/correlation/colors.test.ts +39 -0
- package/src/plugins/builtin/correlation/colors.ts +87 -0
- package/src/plugins/builtin/correlation/index.test.tsx +151 -0
- package/src/plugins/builtin/correlation/index.tsx +92 -21
- package/src/plugins/builtin/debug.tsx +4 -3
- package/src/plugins/builtin/econ/index.tsx +3 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +25 -13
- package/src/plugins/builtin/layout-manager.tsx +1 -7
- package/src/plugins/builtin/market-movers/index.tsx +72 -40
- package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
- package/src/plugins/builtin/news-wire/index.tsx +1 -3
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
- package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
- package/src/plugins/builtin/options.tsx +9 -17
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
- package/src/plugins/builtin/portfolio-list/metrics.ts +13 -2
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +139 -23
- package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
- package/src/plugins/builtin/portfolio-list.test.tsx +311 -9
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +275 -43
- package/src/plugins/builtin/sectors/sector-data.ts +41 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +18 -7
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +445 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- package/src/plugins/builtin/world-indices/index.tsx +58 -19
- package/src/plugins/ibkr/trading-pane.tsx +3 -2
- package/src/plugins/plugin-runtime.tsx +2 -2
- package/src/plugins/prediction-markets/settings.ts +4 -0
- package/src/plugins/registry.test.ts +55 -0
- package/src/plugins/registry.ts +11 -4
- package/src/plugins/ticker-navigation.test.ts +35 -5
- package/src/plugins/ticker-navigation.ts +16 -22
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
- package/src/renderers/electrobun/bun/application-menu.ts +6 -1
- package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
- package/src/renderers/electrobun/bun/index.ts +52 -13
- package/src/renderers/electrobun/view/app-services.ts +51 -9
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
- package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
- package/src/renderers/electrobun/view/main.tsx +0 -16
- package/src/renderers/electrobun/view/styles.css +23 -21
- package/src/renderers/electrobun/view/ui-host.tsx +6 -4
- package/src/renderers/opentui/host.tsx +2 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/opentui/test-utils.tsx +6 -5
- package/src/sources/gloomberb-cloud.test.ts +55 -2
- package/src/sources/gloomberb-cloud.ts +180 -29
- package/src/sources/provider-router.test.ts +567 -10
- package/src/sources/provider-router.ts +323 -93
- package/src/sources/yahoo-finance.test.ts +37 -0
- package/src/sources/yahoo-finance.ts +48 -2
- package/src/state/app-bootstrap.test.ts +50 -23
- package/src/state/app-bootstrap.ts +29 -13
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- package/src/state/session-persistence.test.ts +5 -3
- package/src/state/use-inline-tickers.ts +3 -0
- package/src/state/use-quote-streaming.ts +11 -0
- package/src/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +1 -0
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/utils/api-client.test.ts +248 -0
- package/src/utils/api-client.ts +178 -26
- package/src/utils/exchanges.test.ts +16 -0
- package/src/utils/exchanges.ts +33 -0
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -0
- package/src/utils/options.ts +1 -1
- package/src/utils/price-history.test.ts +28 -1
- package/src/utils/price-history.ts +16 -0
- package/src/utils/quote-freshness.test.ts +150 -0
- package/src/utils/quote-freshness.ts +42 -1
- package/src/utils/quote-resolution.ts +2 -0
- package/src/utils/quote-time.test.ts +34 -0
- package/src/utils/quote-time.ts +18 -5
- package/src/utils/ticker-search.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -1
- package/src/version.ts +1 -1
- 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
|
|
87
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
631
|
+
const merged = mergeFinancials(
|
|
506
632
|
brokerResult?.value ?? cached.brokerRecord?.value ?? null,
|
|
507
633
|
providerResult?.value ?? cached.providerValue ?? null,
|
|
508
|
-
)
|
|
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
|
|
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
|
|
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:${
|
|
551
|
-
await this.revalidateExchangeRate(
|
|
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(
|
|
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 ${
|
|
699
|
+
throw new Error(`No exchange rate provider available for ${normalizedCurrency}`);
|
|
562
700
|
}
|
|
563
|
-
this.cacheExchangeRate(
|
|
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
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
613
|
-
|
|
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
|
|
1069
|
-
|
|
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
|
-
|
|
1265
|
+
providerEntityKey,
|
|
1072
1266
|
variantKeys,
|
|
1073
|
-
|
|
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:
|
|
1081
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
|
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(
|
|
2114
|
+
const rate = await provider.getExchangeRate(normalizedCurrency);
|
|
1885
2115
|
return { provider, rate };
|
|
1886
2116
|
});
|
|
1887
2117
|
if (!result) return;
|
|
1888
|
-
this.cacheExchangeRate(
|
|
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> {
|