gloomberb 0.7.3 → 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/package.json +1 -1
- package/src/components/chart/chart-renderer.ts +109 -32
- package/src/components/chart/chart-types.ts +18 -0
- package/src/components/chart/chart.test.ts +152 -5
- 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 +45 -8
- package/src/components/chart/market-session.ts +187 -0
- package/src/components/chart/native/chart-rasterizer.ts +65 -2
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +96 -0
- package/src/components/chart/stock-chart.tsx +36 -8
- package/src/components/chart/time-axis-label.tsx +56 -2
- package/src/components/command-bar/command-bar.test.tsx +1 -1
- package/src/components/command-bar/command-bar.tsx +7 -1
- package/src/components/command-bar/list-model.ts +8 -4
- package/src/components/command-bar/view-model.test.ts +20 -0
- package/src/components/command-bar/view-model.ts +15 -3
- package/src/components/ticker-list-table.tsx +4 -15
- package/src/core/state/session-persistence.ts +25 -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 +192 -55
- package/src/market-data/hooks.test.tsx +31 -0
- package/src/market-data/hooks.tsx +1 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
- package/src/plugins/builtin/chat-controller.ts +8 -7
- package/src/plugins/builtin/chat.test.tsx +55 -0
- package/src/plugins/builtin/chat.tsx +20 -6
- 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 +0 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/pane.tsx +26 -9
- package/src/plugins/builtin/portfolio-list.test.tsx +29 -7
- package/src/plugins/builtin/sectors/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/pane.tsx +18 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -1
- package/src/plugins/builtin/world-indices/index.tsx +58 -19
- 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 +26 -10
- 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/types/data-provider.ts +5 -1
- package/src/types/financials.ts +1 -0
- package/src/utils/api-client.test.ts +120 -0
- package/src/utils/api-client.ts +66 -19
- package/src/utils/exchanges.test.ts +16 -0
- package/src/utils/exchanges.ts +33 -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/version.ts +1 -1
- package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
|
@@ -33,6 +33,43 @@ describe("YahooFinanceClient exchange aliases", () => {
|
|
|
33
33
|
expect(history[0]?.close).toBe(200);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
test("maps IBKR option symbols to Yahoo option quote marks", async () => {
|
|
37
|
+
const provider = new YahooFinanceClient() as any;
|
|
38
|
+
provider.getOptionsChain = async (ticker: string, _exchange: string, expirationDate: number) => ({
|
|
39
|
+
underlyingSymbol: ticker,
|
|
40
|
+
expirationDates: [expirationDate],
|
|
41
|
+
calls: [{
|
|
42
|
+
contractSymbol: "AMD270917C00230000",
|
|
43
|
+
strike: 230,
|
|
44
|
+
currency: "USD",
|
|
45
|
+
lastPrice: 251,
|
|
46
|
+
change: 1.5,
|
|
47
|
+
percentChange: 0.6,
|
|
48
|
+
volume: 10,
|
|
49
|
+
openInterest: 20,
|
|
50
|
+
bid: 250.25,
|
|
51
|
+
ask: 254.5,
|
|
52
|
+
impliedVolatility: 0.4,
|
|
53
|
+
inTheMoney: false,
|
|
54
|
+
expiration: expirationDate,
|
|
55
|
+
lastTradeDate: 1_800_000_000,
|
|
56
|
+
}],
|
|
57
|
+
puts: [],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const quote = await provider.getQuote("AMD 270917C00230000");
|
|
61
|
+
|
|
62
|
+
expect(quote).toMatchObject({
|
|
63
|
+
symbol: "AMD 270917C00230000",
|
|
64
|
+
price: 252.375,
|
|
65
|
+
mark: 252.375,
|
|
66
|
+
bid: 250.25,
|
|
67
|
+
ask: 254.5,
|
|
68
|
+
providerId: "yahoo",
|
|
69
|
+
});
|
|
70
|
+
expect(quote.lastUpdated).toBe(1_800_000_000_000);
|
|
71
|
+
});
|
|
72
|
+
|
|
36
73
|
test("preserves analyst rating price targets from upgrade history", async () => {
|
|
37
74
|
const provider = new YahooFinanceClient() as any;
|
|
38
75
|
let requestUrl = "";
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type ManualChartResolution,
|
|
8
8
|
} from "../components/chart/chart-resolution";
|
|
9
9
|
import type { InstrumentSearchResult } from "../types/instrument";
|
|
10
|
+
import { parseOptionSymbol } from "../utils/options";
|
|
10
11
|
import { SecEdgarClient } from "./sec-edgar";
|
|
11
12
|
|
|
12
13
|
// Exchange suffix mapping for Yahoo Finance ticker symbols
|
|
@@ -755,7 +756,7 @@ export class YahooFinanceClient implements DataProvider {
|
|
|
755
756
|
const history: PricePoint[] = [];
|
|
756
757
|
for (let i = 0; i < result.timestamp.length; i++) {
|
|
757
758
|
const close = quote.close?.[i];
|
|
758
|
-
if (close == null || Number.
|
|
759
|
+
if (close == null || !Number.isFinite(close) || close <= 0) continue;
|
|
759
760
|
history.push({
|
|
760
761
|
date: new Date((result.timestamp[i]!) * 1000),
|
|
761
762
|
open: quote.open?.[i] ?? undefined,
|
|
@@ -1047,8 +1048,53 @@ export class YahooFinanceClient implements DataProvider {
|
|
|
1047
1048
|
};
|
|
1048
1049
|
}
|
|
1049
1050
|
|
|
1051
|
+
private async getOptionQuote(ticker: string, context?: MarketDataRequestContext): Promise<Quote> {
|
|
1052
|
+
const parsed = parseOptionSymbol(ticker);
|
|
1053
|
+
if (!parsed) throw new Error(`Unsupported option symbol ${ticker}`);
|
|
1054
|
+
const chain = await this.getOptionsChain(parsed.underlying, "", parsed.expTs, context);
|
|
1055
|
+
const contracts = parsed.side === "C" ? chain.calls : chain.puts;
|
|
1056
|
+
const contract = contracts.find((candidate) =>
|
|
1057
|
+
Math.abs(candidate.strike - parsed.strike) < 0.001 &&
|
|
1058
|
+
candidate.expiration === parsed.expTs
|
|
1059
|
+
);
|
|
1060
|
+
if (!contract) throw new Error(`No option contract for ${ticker}`);
|
|
1061
|
+
|
|
1062
|
+
const mark = contract.bid > 0 && contract.ask > 0
|
|
1063
|
+
? (contract.bid + contract.ask) / 2
|
|
1064
|
+
: contract.bid > 0
|
|
1065
|
+
? contract.bid
|
|
1066
|
+
: contract.ask > 0
|
|
1067
|
+
? contract.ask
|
|
1068
|
+
: undefined;
|
|
1069
|
+
return {
|
|
1070
|
+
symbol: ticker,
|
|
1071
|
+
providerId: this.id,
|
|
1072
|
+
price: mark ?? contract.lastPrice,
|
|
1073
|
+
currency: contract.currency || "USD",
|
|
1074
|
+
change: contract.change,
|
|
1075
|
+
changePercent: contract.percentChange,
|
|
1076
|
+
volume: contract.volume,
|
|
1077
|
+
bid: contract.bid,
|
|
1078
|
+
ask: contract.ask,
|
|
1079
|
+
mark,
|
|
1080
|
+
name: contract.contractSymbol,
|
|
1081
|
+
lastUpdated: contract.lastTradeDate > 0
|
|
1082
|
+
? contract.lastTradeDate * 1000
|
|
1083
|
+
: Date.now(),
|
|
1084
|
+
exchangeName: "OPTIONS",
|
|
1085
|
+
fullExchangeName: "OPTIONS",
|
|
1086
|
+
listingExchangeName: "OPTIONS",
|
|
1087
|
+
listingExchangeFullName: "OPTIONS",
|
|
1088
|
+
dataSource: "delayed",
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1050
1092
|
/** Fetch just a quote (lighter weight) */
|
|
1051
|
-
async getQuote(ticker: string, exchange = "",
|
|
1093
|
+
async getQuote(ticker: string, exchange = "", context?: MarketDataRequestContext): Promise<Quote> {
|
|
1094
|
+
if (parseOptionSymbol(ticker)) {
|
|
1095
|
+
return this.getOptionQuote(ticker, context);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1052
1098
|
const symbolsToTry = this.getSymbolsToTry(ticker, exchange);
|
|
1053
1099
|
let lastError: any;
|
|
1054
1100
|
|
|
@@ -408,8 +408,28 @@ describe("initializeAppState", () => {
|
|
|
408
408
|
custom: {},
|
|
409
409
|
tags: [],
|
|
410
410
|
});
|
|
411
|
+
const hydrationSymbols = [
|
|
412
|
+
"AAPL",
|
|
413
|
+
...Array.from({ length: 13 }, (_, index) => `T${String(index + 1).padStart(2, "0")}`),
|
|
414
|
+
];
|
|
415
|
+
for (const symbol of hydrationSymbols.slice(1)) {
|
|
416
|
+
await tickerRepository.createTicker({
|
|
417
|
+
ticker: symbol,
|
|
418
|
+
exchange: "NASDAQ",
|
|
419
|
+
currency: "USD",
|
|
420
|
+
name: symbol,
|
|
421
|
+
portfolios: [],
|
|
422
|
+
watchlists: ["main"],
|
|
423
|
+
positions: [],
|
|
424
|
+
broker_contracts: [],
|
|
425
|
+
custom: {},
|
|
426
|
+
tags: [],
|
|
427
|
+
});
|
|
428
|
+
}
|
|
411
429
|
|
|
412
430
|
const events: string[] = [];
|
|
431
|
+
const cachedTargetRequests: string[] = [];
|
|
432
|
+
const financialRefreshes: string[] = [];
|
|
413
433
|
|
|
414
434
|
await initializeAppState({
|
|
415
435
|
config: quoteOnlyConfig,
|
|
@@ -417,23 +437,26 @@ describe("initializeAppState", () => {
|
|
|
417
437
|
dataProvider: {
|
|
418
438
|
id: "test-provider",
|
|
419
439
|
name: "Test Provider",
|
|
420
|
-
getCachedFinancialsForTargets: () =>
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
440
|
+
getCachedFinancialsForTargets: (targets: Array<{ symbol: string }>) => {
|
|
441
|
+
cachedTargetRequests.push(...targets.map((target) => target.symbol));
|
|
442
|
+
return new Map(targets.map((target) => [target.symbol, {
|
|
443
|
+
annualStatements: [],
|
|
444
|
+
quarterlyStatements: [],
|
|
445
|
+
priceHistory: [],
|
|
446
|
+
quote: {
|
|
447
|
+
symbol: target.symbol,
|
|
448
|
+
price: 200,
|
|
449
|
+
currency: "USD",
|
|
450
|
+
change: 1,
|
|
451
|
+
changePercent: 0.5,
|
|
452
|
+
marketCap: 2_000_000_000,
|
|
453
|
+
lastUpdated: Date.now(),
|
|
454
|
+
},
|
|
455
|
+
fundamentals: {
|
|
456
|
+
trailingPE: 25,
|
|
457
|
+
},
|
|
458
|
+
}]));
|
|
459
|
+
},
|
|
437
460
|
} as any,
|
|
438
461
|
sessionSnapshot: {
|
|
439
462
|
paneState: {
|
|
@@ -446,11 +469,11 @@ describe("initializeAppState", () => {
|
|
|
446
469
|
activePanel: "left",
|
|
447
470
|
statusBarVisible: true,
|
|
448
471
|
openPaneIds: ["portfolio-list:main"],
|
|
449
|
-
hydrationTargets:
|
|
450
|
-
symbol
|
|
472
|
+
hydrationTargets: hydrationSymbols.map((symbol) => ({
|
|
473
|
+
symbol,
|
|
451
474
|
exchange: "NASDAQ",
|
|
452
475
|
instrument: null,
|
|
453
|
-
}
|
|
476
|
+
})),
|
|
454
477
|
exchangeCurrencies: [],
|
|
455
478
|
savedAt: Date.now(),
|
|
456
479
|
},
|
|
@@ -458,13 +481,17 @@ describe("initializeAppState", () => {
|
|
|
458
481
|
primeCachedFinancials: (entries) => {
|
|
459
482
|
events.push(`prime:${entries.map((entry) => entry.ticker.metadata.ticker).join(",")}`);
|
|
460
483
|
},
|
|
461
|
-
refreshTicker: () => {},
|
|
484
|
+
refreshTicker: (symbol) => { financialRefreshes.push(symbol); },
|
|
462
485
|
refreshQuote: () => {},
|
|
463
486
|
autoImportBrokerPositions: async () => {},
|
|
464
487
|
});
|
|
465
488
|
|
|
466
|
-
|
|
467
|
-
expect(
|
|
489
|
+
const primeEvent = events.find((event) => event.startsWith("prime:"));
|
|
490
|
+
expect(primeEvent).toContain("AAPL");
|
|
491
|
+
expect(primeEvent).toContain("T13");
|
|
492
|
+
expect(cachedTargetRequests).toContain("T13");
|
|
493
|
+
expect(financialRefreshes).not.toContain("T13");
|
|
494
|
+
expect(events.indexOf(primeEvent!)).toBeLessThan(events.indexOf("SET_INITIALIZED"));
|
|
468
495
|
|
|
469
496
|
persistence.close();
|
|
470
497
|
});
|
|
@@ -216,26 +216,39 @@ function buildCachedFinancialTarget(ticker: TickerRecord): CachedFinancialsTarge
|
|
|
216
216
|
async function resolveCachedFinancialPrimeEntries(
|
|
217
217
|
refreshPlan: RefreshPlanEntry[],
|
|
218
218
|
sessionSnapshot: AppSessionSnapshot | null | undefined,
|
|
219
|
+
tickerMap: Map<string, TickerRecord>,
|
|
219
220
|
dataProvider: DataProvider,
|
|
220
221
|
): Promise<Array<{ ticker: TickerRecord; financials: TickerFinancials }>> {
|
|
221
222
|
if (!dataProvider.getCachedFinancialsForTargets) return [];
|
|
222
223
|
|
|
223
|
-
const
|
|
224
|
+
const targetEntriesBySymbol = new Map<string, { ticker: TickerRecord; target: CachedFinancialsTarget }>();
|
|
224
225
|
for (const target of sessionSnapshot?.hydrationTargets ?? []) {
|
|
225
|
-
|
|
226
|
+
const symbol = target.symbol.trim().toUpperCase();
|
|
227
|
+
const ticker = tickerMap.get(symbol);
|
|
228
|
+
if (ticker) {
|
|
229
|
+
targetEntriesBySymbol.set(symbol, { ticker, target });
|
|
230
|
+
}
|
|
226
231
|
}
|
|
227
232
|
|
|
228
|
-
const
|
|
229
|
-
|
|
233
|
+
for (const entry of refreshPlan.filter((entry) => entry.mode === "financials")) {
|
|
234
|
+
const symbol = entry.ticker.metadata.ticker.trim().toUpperCase();
|
|
235
|
+
if (!targetEntriesBySymbol.has(symbol)) {
|
|
236
|
+
targetEntriesBySymbol.set(symbol, {
|
|
237
|
+
ticker: entry.ticker,
|
|
238
|
+
target: buildCachedFinancialTarget(entry.ticker),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
230
242
|
|
|
243
|
+
const targetEntries = [...targetEntriesBySymbol.values()];
|
|
244
|
+
if (targetEntries.length === 0) return [];
|
|
231
245
|
const cachedFinancials = await dataProvider.getCachedFinancialsForTargets(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
)),
|
|
246
|
+
targetEntries.map((entry) => entry.target),
|
|
247
|
+
{ includeStaleQuotes: true },
|
|
235
248
|
);
|
|
236
249
|
const primedEntries: Array<{ ticker: TickerRecord; financials: TickerFinancials }> = [];
|
|
237
250
|
|
|
238
|
-
for (const entry of
|
|
251
|
+
for (const entry of targetEntries) {
|
|
239
252
|
const cached = cachedFinancials.get(entry.ticker.metadata.ticker.trim().toUpperCase());
|
|
240
253
|
if (cached) {
|
|
241
254
|
primedEntries.push({ ticker: entry.ticker, financials: cached });
|
|
@@ -347,8 +360,11 @@ export async function initializeAppState({
|
|
|
347
360
|
if (primeCachedFinancials) {
|
|
348
361
|
const cachedPrimeEntries = await measurePerfAsync(
|
|
349
362
|
"startup.resolve-cached-financial-prime",
|
|
350
|
-
() => resolveCachedFinancialPrimeEntries(refreshPlan, sessionSnapshot, dataProvider),
|
|
351
|
-
{
|
|
363
|
+
() => resolveCachedFinancialPrimeEntries(refreshPlan, sessionSnapshot, tickerMap, dataProvider),
|
|
364
|
+
{
|
|
365
|
+
financialRefreshCount: refreshPlan.filter((entry) => entry.mode === "financials").length,
|
|
366
|
+
sessionHydrationTargetCount: sessionSnapshot?.hydrationTargets.length ?? 0,
|
|
367
|
+
},
|
|
352
368
|
);
|
|
353
369
|
if (cachedPrimeEntries.length > 0) {
|
|
354
370
|
measurePerf("startup.prime-cached-financials", () => {
|
|
@@ -24,7 +24,10 @@ function createTicker(symbol: string, exchange = "NASDAQ"): TickerRecord {
|
|
|
24
24
|
describe("session persistence", () => {
|
|
25
25
|
test("builds a working-set snapshot from runtime state", () => {
|
|
26
26
|
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
27
|
-
const tickers = new Map<string, TickerRecord>([
|
|
27
|
+
const tickers = new Map<string, TickerRecord>([
|
|
28
|
+
["AAPL", createTicker("AAPL")],
|
|
29
|
+
["MSFT", createTicker("MSFT")],
|
|
30
|
+
]);
|
|
28
31
|
const financials = new Map<string, TickerFinancials>([["AAPL", {
|
|
29
32
|
annualStatements: [],
|
|
30
33
|
quarterlyStatements: [],
|
|
@@ -61,8 +64,7 @@ describe("session persistence", () => {
|
|
|
61
64
|
});
|
|
62
65
|
|
|
63
66
|
expect(snapshot.focusedPaneId).toBe("ticker-detail:main");
|
|
64
|
-
expect(snapshot.hydrationTargets).
|
|
65
|
-
expect(snapshot.hydrationTargets[0]?.symbol).toBe("AAPL");
|
|
67
|
+
expect(snapshot.hydrationTargets.map((target) => target.symbol)).toEqual(["AAPL", "MSFT"]);
|
|
66
68
|
expect(snapshot.exchangeCurrencies).toContain("JPY");
|
|
67
69
|
expect(snapshot.paneState["portfolio-list:main"]).toEqual({
|
|
68
70
|
cursorSymbol: "AAPL",
|
|
@@ -152,6 +152,9 @@ export function useInlineTickers(texts: readonly string[]): {
|
|
|
152
152
|
return {
|
|
153
153
|
symbol: ticker.metadata.ticker,
|
|
154
154
|
exchange: ticker.metadata.exchange,
|
|
155
|
+
surface: "inline" as const,
|
|
156
|
+
visible: true,
|
|
157
|
+
weight: 40,
|
|
155
158
|
context: instrument
|
|
156
159
|
? {
|
|
157
160
|
brokerId: instrument.brokerId,
|
|
@@ -22,6 +22,7 @@ export function buildQuoteStreamSubscriptionKey(target: QuoteSubscriptionTarget)
|
|
|
22
22
|
?? target.context?.instrument?.localSymbol
|
|
23
23
|
?? target.context?.instrument?.symbol
|
|
24
24
|
?? "";
|
|
25
|
+
const weight = Number.isFinite(target.weight) ? String(target.weight) : "";
|
|
25
26
|
return [
|
|
26
27
|
target.symbol,
|
|
27
28
|
target.exchange ?? "",
|
|
@@ -29,6 +30,10 @@ export function buildQuoteStreamSubscriptionKey(target: QuoteSubscriptionTarget)
|
|
|
29
30
|
target.context?.brokerInstanceId ?? "",
|
|
30
31
|
contractKey,
|
|
31
32
|
target.route ?? "auto",
|
|
33
|
+
target.surface ?? "",
|
|
34
|
+
target.visible ? "visible" : "",
|
|
35
|
+
target.selected ? "selected" : "",
|
|
36
|
+
weight,
|
|
32
37
|
].join("|");
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -68,6 +73,12 @@ export function useQuoteStreaming(targets: QuoteSubscriptionTarget[]): void {
|
|
|
68
73
|
brokerInstanceId: target.context?.brokerInstanceId,
|
|
69
74
|
instrument: target.context?.instrument ?? null,
|
|
70
75
|
},
|
|
76
|
+
priority: {
|
|
77
|
+
surface: target.surface,
|
|
78
|
+
visible: target.visible,
|
|
79
|
+
selected: target.selected,
|
|
80
|
+
weight: target.weight,
|
|
81
|
+
},
|
|
71
82
|
})));
|
|
72
83
|
return () => {
|
|
73
84
|
quoteStreamLog.info("unsubscribe", {
|
|
@@ -90,6 +90,10 @@ export interface QuoteSubscriptionTarget {
|
|
|
90
90
|
exchange?: string;
|
|
91
91
|
context?: MarketDataRequestContext;
|
|
92
92
|
route?: "auto" | "provider" | "broker";
|
|
93
|
+
surface?: "portfolio" | "watchlist" | "detail" | "monitor" | "inline" | "screener" | "unknown";
|
|
94
|
+
visible?: boolean;
|
|
95
|
+
selected?: boolean;
|
|
96
|
+
weight?: number;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
export interface QuoteBatchResult {
|
|
@@ -111,7 +115,7 @@ export interface AssetDataProvider {
|
|
|
111
115
|
readonly cachePolicy?: CachePolicyMap;
|
|
112
116
|
|
|
113
117
|
canProvide?(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<boolean> | boolean;
|
|
114
|
-
getCachedFinancialsForTargets?(targets: CachedFinancialsTarget[], options?: { allowExpired?: boolean }): Map<string, TickerFinancials> | Promise<Map<string, TickerFinancials>>;
|
|
118
|
+
getCachedFinancialsForTargets?(targets: CachedFinancialsTarget[], options?: { allowExpired?: boolean; includeStaleQuotes?: boolean }): Map<string, TickerFinancials> | Promise<Map<string, TickerFinancials>>;
|
|
115
119
|
getQuotesBatch?(targets: QuoteSubscriptionTarget[], options?: { forceRefresh?: boolean }): Promise<QuoteBatchResult[]>;
|
|
116
120
|
getTickerFinancialsBatch?(targets: CachedFinancialsTarget[], options?: { forceRefresh?: boolean }): Promise<TickerFinancialsBatchResult[]>;
|
|
117
121
|
getTickerFinancials(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<TickerFinancials>;
|
package/src/types/financials.ts
CHANGED
|
@@ -32,6 +32,7 @@ export interface Quote {
|
|
|
32
32
|
volume?: number;
|
|
33
33
|
name?: string;
|
|
34
34
|
lastUpdated: number; // timestamp ms
|
|
35
|
+
receivedAt?: number; // local receipt timestamp ms for streamed/display freshness
|
|
35
36
|
exchangeName?: string;
|
|
36
37
|
fullExchangeName?: string;
|
|
37
38
|
listingExchangeName?: string;
|
|
@@ -210,6 +210,126 @@ describe("apiClient auth cookies", () => {
|
|
|
210
210
|
|
|
211
211
|
unsubscribe();
|
|
212
212
|
});
|
|
213
|
+
|
|
214
|
+
test("opens an anonymous market websocket and sends quote priority hints", () => {
|
|
215
|
+
const sent: unknown[] = [];
|
|
216
|
+
const sockets: Array<{
|
|
217
|
+
url: string;
|
|
218
|
+
readyState: number;
|
|
219
|
+
onopen: ((event: unknown) => void) | null;
|
|
220
|
+
close: () => void;
|
|
221
|
+
}> = [];
|
|
222
|
+
|
|
223
|
+
class FakeWebSocket {
|
|
224
|
+
static OPEN = 1;
|
|
225
|
+
readyState = 1;
|
|
226
|
+
onopen: ((event: unknown) => void) | null = null;
|
|
227
|
+
onmessage: ((event: { data: string }) => void) | null = null;
|
|
228
|
+
onclose: ((event: unknown) => void) | null = null;
|
|
229
|
+
onerror: ((event: unknown) => void) | null = null;
|
|
230
|
+
|
|
231
|
+
constructor(readonly url: string) {
|
|
232
|
+
sockets.push(this);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
send(payload: string): void {
|
|
236
|
+
sent.push(JSON.parse(payload));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
close(): void {
|
|
240
|
+
this.readyState = 3;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
globalThis.WebSocket = FakeWebSocket as any;
|
|
245
|
+
|
|
246
|
+
const unsubscribe = apiClient.subscribeQuotes([{
|
|
247
|
+
symbol: "AAPL",
|
|
248
|
+
exchange: "NASDAQ",
|
|
249
|
+
surface: "portfolio",
|
|
250
|
+
visible: true,
|
|
251
|
+
selected: true,
|
|
252
|
+
weight: 100,
|
|
253
|
+
}], () => {});
|
|
254
|
+
sockets[0]!.onopen?.({});
|
|
255
|
+
|
|
256
|
+
expect(sockets).toHaveLength(1);
|
|
257
|
+
expect(sockets[0]!.url).toBe("wss://api.gloom.sh/cloud/ws");
|
|
258
|
+
expect(sent).toContainEqual({
|
|
259
|
+
type: "market.subscribe",
|
|
260
|
+
symbols: [{
|
|
261
|
+
symbol: "AAPL",
|
|
262
|
+
exchange: "NASDAQ",
|
|
263
|
+
surface: "portfolio",
|
|
264
|
+
visible: true,
|
|
265
|
+
selected: true,
|
|
266
|
+
weight: 100,
|
|
267
|
+
}],
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
unsubscribe();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("keeps an anonymous market websocket open after auth rejection", () => {
|
|
274
|
+
const seenPrices: number[] = [];
|
|
275
|
+
let closeCalls = 0;
|
|
276
|
+
const sockets: Array<{
|
|
277
|
+
readyState: number;
|
|
278
|
+
onopen: ((event: unknown) => void) | null;
|
|
279
|
+
onmessage: ((event: { data: string }) => void) | null;
|
|
280
|
+
close: () => void;
|
|
281
|
+
}> = [];
|
|
282
|
+
|
|
283
|
+
class FakeWebSocket {
|
|
284
|
+
static OPEN = 1;
|
|
285
|
+
readyState = 1;
|
|
286
|
+
onopen: ((event: unknown) => void) | null = null;
|
|
287
|
+
onmessage: ((event: { data: string }) => void) | null = null;
|
|
288
|
+
onclose: ((event: unknown) => void) | null = null;
|
|
289
|
+
onerror: ((event: unknown) => void) | null = null;
|
|
290
|
+
|
|
291
|
+
constructor(readonly _url: string) {
|
|
292
|
+
sockets.push(this);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
send(): void {}
|
|
296
|
+
|
|
297
|
+
close(): void {
|
|
298
|
+
closeCalls += 1;
|
|
299
|
+
this.readyState = 3;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
globalThis.WebSocket = FakeWebSocket as any;
|
|
304
|
+
|
|
305
|
+
const unsubscribe = apiClient.subscribeQuotes([{ symbol: "AAPL" }], (_target, quote) => {
|
|
306
|
+
seenPrices.push(quote.price);
|
|
307
|
+
});
|
|
308
|
+
sockets[0]!.onopen?.({});
|
|
309
|
+
sockets[0]!.onmessage?.({ data: JSON.stringify({ type: "auth.unverified" }) });
|
|
310
|
+
sockets[0]!.onmessage?.({
|
|
311
|
+
data: JSON.stringify({
|
|
312
|
+
type: "market.quote",
|
|
313
|
+
symbol: "AAPL",
|
|
314
|
+
exchange: "",
|
|
315
|
+
quote: {
|
|
316
|
+
symbol: "AAPL",
|
|
317
|
+
price: 123,
|
|
318
|
+
currency: "USD",
|
|
319
|
+
change: 0,
|
|
320
|
+
changePercent: 0,
|
|
321
|
+
lastUpdated: 1,
|
|
322
|
+
providerId: "gloomberb-cloud",
|
|
323
|
+
dataSource: "live",
|
|
324
|
+
},
|
|
325
|
+
}),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(closeCalls).toBe(0);
|
|
329
|
+
expect(seenPrices).toEqual([123]);
|
|
330
|
+
|
|
331
|
+
unsubscribe();
|
|
332
|
+
});
|
|
213
333
|
});
|
|
214
334
|
|
|
215
335
|
describe("apiClient chat timestamps", () => {
|