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
|
@@ -3,6 +3,7 @@ import { existsSync, rmSync } from "fs";
|
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { act, useReducer } from "react";
|
|
6
|
+
import { RGBA, TextAttributes } from "@opentui/core";
|
|
6
7
|
import { testRender } from "../../renderers/opentui/test-utils";
|
|
7
8
|
import { AppPersistence } from "../../data/app-persistence";
|
|
8
9
|
import { TickerRepository } from "../../data/ticker-repository";
|
|
@@ -18,6 +19,7 @@ import { createTestPluginRuntime } from "../../test-support/plugin-runtime";
|
|
|
18
19
|
import { PluginRenderProvider, type PluginRuntimeAccess } from "../plugin-runtime";
|
|
19
20
|
import { PluginRegistry, setSharedMarketDataForTests, setSharedRegistryForTests } from "../registry";
|
|
20
21
|
import type { BrokerAdapter } from "../../types/broker";
|
|
22
|
+
import { colors } from "../../theme/colors";
|
|
21
23
|
import { portfolioListPlugin } from "./portfolio-list";
|
|
22
24
|
|
|
23
25
|
const TEST_PANE_ID = "portfolio-list:test";
|
|
@@ -316,6 +318,10 @@ async function flushFrame() {
|
|
|
316
318
|
});
|
|
317
319
|
}
|
|
318
320
|
|
|
321
|
+
function lineText(line: { spans: Array<{ text: string }> }) {
|
|
322
|
+
return line.spans.map((span) => span.text).join("");
|
|
323
|
+
}
|
|
324
|
+
|
|
319
325
|
afterEach(async () => {
|
|
320
326
|
if (testSetup) {
|
|
321
327
|
testSetup.renderer.destroy();
|
|
@@ -1122,10 +1128,11 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
1122
1128
|
quote: makeQuote({
|
|
1123
1129
|
symbol: "AAPL",
|
|
1124
1130
|
price: 125,
|
|
1125
|
-
marketCap:
|
|
1131
|
+
marketCap: 2_000_000_000,
|
|
1126
1132
|
}),
|
|
1127
1133
|
fundamentals: {
|
|
1128
1134
|
trailingPE: 25,
|
|
1135
|
+
forwardPE: 22,
|
|
1129
1136
|
},
|
|
1130
1137
|
profile: {
|
|
1131
1138
|
sector: "Technology",
|
|
@@ -1437,7 +1444,7 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
1437
1444
|
expect(frame).toContain("+5.41%");
|
|
1438
1445
|
});
|
|
1439
1446
|
|
|
1440
|
-
test("quote flash
|
|
1447
|
+
test("quote flash dims cells without changing quote sign color or row background", async () => {
|
|
1441
1448
|
const config = createPortfolioConfigWithColumns(
|
|
1442
1449
|
"broker:ibkr-flex:DU12345",
|
|
1443
1450
|
["ticker", "price", "change", "latency"],
|
|
@@ -1454,7 +1461,18 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
1454
1461
|
["MSFT", makeTicker({ ticker: "MSFT", name: "Microsoft" })],
|
|
1455
1462
|
]);
|
|
1456
1463
|
state.financials = new Map([
|
|
1457
|
-
["AAPL", {
|
|
1464
|
+
["AAPL", {
|
|
1465
|
+
annualStatements: [],
|
|
1466
|
+
quarterlyStatements: [],
|
|
1467
|
+
priceHistory: [],
|
|
1468
|
+
quote: makeQuote({
|
|
1469
|
+
price: 125,
|
|
1470
|
+
bid: 124.95,
|
|
1471
|
+
ask: 125.05,
|
|
1472
|
+
change: -5,
|
|
1473
|
+
changePercent: -4.17,
|
|
1474
|
+
}),
|
|
1475
|
+
}],
|
|
1458
1476
|
["MSFT", {
|
|
1459
1477
|
annualStatements: [],
|
|
1460
1478
|
quarterlyStatements: [],
|
|
@@ -1483,7 +1501,7 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
1483
1501
|
|
|
1484
1502
|
await flushFrame();
|
|
1485
1503
|
const beforeFrame = testSetup.captureSpans();
|
|
1486
|
-
const beforeLine = beforeFrame.lines.find((line) => line
|
|
1504
|
+
const beforeLine = beforeFrame.lines.find((line) => lineText(line).includes("AAPL"));
|
|
1487
1505
|
expect(beforeLine).toBeDefined();
|
|
1488
1506
|
|
|
1489
1507
|
await act(async () => {
|
|
@@ -1494,8 +1512,8 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
1494
1512
|
price: 126,
|
|
1495
1513
|
bid: 125.95,
|
|
1496
1514
|
ask: 126.05,
|
|
1497
|
-
change:
|
|
1498
|
-
changePercent:
|
|
1515
|
+
change: -4,
|
|
1516
|
+
changePercent: -3.08,
|
|
1499
1517
|
lastUpdated: Date.now() + 1_000,
|
|
1500
1518
|
}),
|
|
1501
1519
|
});
|
|
@@ -1503,11 +1521,15 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
1503
1521
|
await flushFrame();
|
|
1504
1522
|
|
|
1505
1523
|
const frame = testSetup.captureSpans();
|
|
1506
|
-
const aaplLine = frame.lines.find((line) => line
|
|
1524
|
+
const aaplLine = frame.lines.find((line) => lineText(line).includes("AAPL"));
|
|
1507
1525
|
expect(aaplLine).toBeDefined();
|
|
1508
1526
|
|
|
1509
1527
|
const beforeBackgrounds = beforeLine!.spans.map((span) => span.bg.toInts().join(","));
|
|
1510
1528
|
const afterBackgrounds = aaplLine!.spans.map((span) => span.bg.toInts().join(","));
|
|
1511
1529
|
expect(afterBackgrounds).toEqual(beforeBackgrounds);
|
|
1530
|
+
const priceSpan = aaplLine!.spans.find((span) => span.text.includes("126"));
|
|
1531
|
+
expect(priceSpan).toBeDefined();
|
|
1532
|
+
expect(priceSpan!.fg.toInts().join(",")).toBe(RGBA.fromHex(colors.negative).toInts().join(","));
|
|
1533
|
+
expect(priceSpan!.attributes & TextAttributes.DIM).not.toBe(0);
|
|
1512
1534
|
});
|
|
1513
1535
|
});
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
|
2
2
|
import { Box } from "../../../ui";
|
|
3
3
|
import { DataTableView, Tabs, usePaneFooter, type DataTableCell, type DataTableColumn, type DataTableKeyEvent } from "../../../components";
|
|
4
4
|
import type { GloomPlugin, PaneProps } from "../../../types/plugin";
|
|
5
|
-
import type { PricePoint } from "../../../types/financials";
|
|
5
|
+
import type { PricePoint, Quote } from "../../../types/financials";
|
|
6
6
|
import { colors, priceColor } from "../../../theme/colors";
|
|
7
7
|
import { formatCurrency, formatPercentRaw } from "../../../utils/format";
|
|
8
8
|
import { useAssetData, useDebouncedPluginPaneState, usePluginPaneState, usePluginTickerActions } from "../../plugin-runtime";
|
|
@@ -300,14 +300,35 @@ function SectorPerformancePane({ focused, width, height }: PaneProps) {
|
|
|
300
300
|
rows.map((row) => ({ ...row, loading: true }))
|
|
301
301
|
)));
|
|
302
302
|
|
|
303
|
-
const
|
|
303
|
+
const loadQuotes = async (): Promise<Map<string, Quote | null>> => {
|
|
304
|
+
const quotes = new Map<string, Quote | null>();
|
|
305
|
+
if (dataProvider.getQuotesBatch) {
|
|
306
|
+
const results = await dataProvider.getQuotesBatch(
|
|
307
|
+
sectorDefs.map((sector) => ({ symbol: sector.etf, exchange: "" })),
|
|
308
|
+
).catch(() => []);
|
|
309
|
+
for (const result of results) {
|
|
310
|
+
quotes.set(result.target.symbol, result.quote ?? null);
|
|
311
|
+
}
|
|
312
|
+
return quotes;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await Promise.all(sectorDefs.map(async (sector) => {
|
|
316
|
+
try {
|
|
317
|
+
quotes.set(sector.etf, await dataProvider.getQuote(sector.etf, ""));
|
|
318
|
+
} catch {
|
|
319
|
+
quotes.set(sector.etf, null);
|
|
320
|
+
}
|
|
321
|
+
}));
|
|
322
|
+
return quotes;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const fetches = loadQuotes().then((quotesByEtf) => Promise.allSettled(sectorDefs.map(async (sector) => {
|
|
304
326
|
try {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
]);
|
|
327
|
+
const historyResult = await dataProvider.getPriceHistory(sector.etf, "", "1Y")
|
|
328
|
+
.then((value) => ({ status: "fulfilled" as const, value }))
|
|
329
|
+
.catch(() => ({ status: "rejected" as const }));
|
|
309
330
|
if (fetchGenRef.current !== gen) return;
|
|
310
|
-
const quote =
|
|
331
|
+
const quote = quotesByEtf.get(sector.etf) ?? null;
|
|
311
332
|
const history = historyResult.status === "fulfilled" ? historyResult.value : [];
|
|
312
333
|
const historyClose = latestHistoryClose(history);
|
|
313
334
|
const price = quote?.price ?? historyClose;
|
|
@@ -336,9 +357,9 @@ function SectorPerformancePane({ focused, width, height }: PaneProps) {
|
|
|
336
357
|
)
|
|
337
358
|
)));
|
|
338
359
|
}
|
|
339
|
-
});
|
|
360
|
+
})));
|
|
340
361
|
|
|
341
|
-
|
|
362
|
+
fetches.then(() => {
|
|
342
363
|
if (fetchGenRef.current === gen) {
|
|
343
364
|
setLastRefreshByCollection((prev) => ({
|
|
344
365
|
...prev,
|
|
@@ -62,7 +62,24 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
|
|
|
62
62
|
const paneInstance = usePaneInstance();
|
|
63
63
|
const { symbol, ticker, financials } = usePaneTicker();
|
|
64
64
|
const streamingTarget = quoteSubscriptionTargetFromTicker(ticker, ticker?.metadata.ticker, "provider");
|
|
65
|
-
const streamingTargets = useMemo(() => (
|
|
65
|
+
const streamingTargets = useMemo(() => (
|
|
66
|
+
streamingTarget
|
|
67
|
+
? [{
|
|
68
|
+
...streamingTarget,
|
|
69
|
+
surface: "detail" as const,
|
|
70
|
+
visible: true,
|
|
71
|
+
selected: true,
|
|
72
|
+
weight: 100,
|
|
73
|
+
}]
|
|
74
|
+
: []
|
|
75
|
+
), [
|
|
76
|
+
streamingTarget?.symbol,
|
|
77
|
+
streamingTarget?.exchange,
|
|
78
|
+
streamingTarget?.route,
|
|
79
|
+
streamingTarget?.context?.brokerId,
|
|
80
|
+
streamingTarget?.context?.brokerInstanceId,
|
|
81
|
+
streamingTarget?.context?.instrument,
|
|
82
|
+
]);
|
|
66
83
|
useQuoteStreaming(streamingTargets);
|
|
67
84
|
|
|
68
85
|
const { collectionId } = usePaneCollection();
|
|
@@ -371,7 +371,10 @@ export function QuoteMonitorPane({ paneId, focused, width, height }: PaneProps)
|
|
|
371
371
|
symbols
|
|
372
372
|
.map((symbol) => {
|
|
373
373
|
const ticker = tickersBySymbol.get(symbol) ?? (fallbackTicker?.metadata.ticker === symbol ? fallbackTicker : null);
|
|
374
|
-
|
|
374
|
+
const target = quoteSubscriptionTargetFromTicker(ticker, symbol, "provider");
|
|
375
|
+
return target
|
|
376
|
+
? { ...target, surface: "monitor" as const, visible: true, selected: symbol === fallbackSymbol, weight: symbol === fallbackSymbol ? 100 : 90 }
|
|
377
|
+
: null;
|
|
375
378
|
})
|
|
376
379
|
.filter((target): target is QuoteSubscriptionTarget => target != null)
|
|
377
380
|
), [fallbackTicker, symbols, tickersBySymbol]);
|
|
@@ -183,31 +183,70 @@ function WorldIndicesPane({ focused, width, height }: PaneProps) {
|
|
|
183
183
|
fetchGenRef.current += 1;
|
|
184
184
|
const gen = fetchGenRef.current;
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
setQuotes((prev) => {
|
|
187
|
+
const next = new Map(prev);
|
|
188
|
+
for (const entry of WORLD_INDICES) {
|
|
189
189
|
const existing = next.get(entry.symbol);
|
|
190
190
|
next.set(entry.symbol, { quote: existing?.quote ?? null, loading: true, error: null });
|
|
191
|
+
}
|
|
192
|
+
return next;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const loadQuotes = async (): Promise<QuoteMap> => {
|
|
196
|
+
const next = new Map<string, IndexQuoteState>();
|
|
197
|
+
if (dataProvider.getQuotesBatch) {
|
|
198
|
+
const results = await dataProvider.getQuotesBatch(
|
|
199
|
+
WORLD_INDICES.map((entry) => ({ symbol: entry.symbol, exchange: "" })),
|
|
200
|
+
);
|
|
201
|
+
const bySymbol = new Map(results.map((result) => [result.target.symbol, result]));
|
|
202
|
+
for (const entry of WORLD_INDICES) {
|
|
203
|
+
const result = bySymbol.get(entry.symbol);
|
|
204
|
+
next.set(entry.symbol, {
|
|
205
|
+
quote: result?.quote ?? null,
|
|
206
|
+
loading: false,
|
|
207
|
+
error: result?.error
|
|
208
|
+
? result.error instanceof Error ? result.error.message : String(result.error)
|
|
209
|
+
: null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
191
212
|
return next;
|
|
192
|
-
}
|
|
213
|
+
}
|
|
193
214
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const next = new Map(prev);
|
|
215
|
+
await Promise.all(WORLD_INDICES.map(async (entry) => {
|
|
216
|
+
try {
|
|
217
|
+
const quote = await dataProvider.getQuote(entry.symbol, "");
|
|
198
218
|
next.set(entry.symbol, { quote, loading: false, error: null });
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
219
|
+
} catch (err: unknown) {
|
|
220
|
+
next.set(entry.symbol, {
|
|
221
|
+
quote: null,
|
|
222
|
+
loading: false,
|
|
223
|
+
error: err instanceof Error ? err.message : String(err),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}));
|
|
227
|
+
return next;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
loadQuotes().then((nextQuotes) => {
|
|
231
|
+
if (fetchGenRef.current !== gen) return;
|
|
232
|
+
setQuotes((prev) => {
|
|
233
|
+
const next = new Map(prev);
|
|
234
|
+
for (const [symbol, state] of nextQuotes) {
|
|
235
|
+
next.set(symbol, state);
|
|
236
|
+
}
|
|
237
|
+
return next;
|
|
209
238
|
});
|
|
210
|
-
}
|
|
239
|
+
}).catch((err: unknown) => {
|
|
240
|
+
if (fetchGenRef.current !== gen) return;
|
|
241
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
242
|
+
setQuotes((prev) => {
|
|
243
|
+
const next = new Map(prev);
|
|
244
|
+
for (const entry of WORLD_INDICES) {
|
|
245
|
+
next.set(entry.symbol, { quote: null, loading: false, error: message });
|
|
246
|
+
}
|
|
247
|
+
return next;
|
|
248
|
+
});
|
|
249
|
+
});
|
|
211
250
|
}, [dataProvider]);
|
|
212
251
|
|
|
213
252
|
useEffect(() => {
|
|
@@ -20,6 +20,7 @@ const originalGetCloudQuote = apiClient.getCloudQuote.bind(apiClient);
|
|
|
20
20
|
const originalGetCloudHolders = apiClient.getCloudHolders.bind(apiClient);
|
|
21
21
|
const originalGetCloudAnalystResearch = apiClient.getCloudAnalystResearch.bind(apiClient);
|
|
22
22
|
const originalGetCloudCorporateActions = apiClient.getCloudCorporateActions.bind(apiClient);
|
|
23
|
+
const originalGetCloudOptionsChain = apiClient.getCloudOptionsChain.bind(apiClient);
|
|
23
24
|
const originalGetCloudNews = apiClient.getCloudNews.bind(apiClient);
|
|
24
25
|
const originalGetCloudNewsStory = apiClient.getCloudNewsStory.bind(apiClient);
|
|
25
26
|
const originalSubscribeQuotes = apiClient.subscribeQuotes.bind(apiClient);
|
|
@@ -78,6 +79,7 @@ afterEach(() => {
|
|
|
78
79
|
apiClient.getCloudHolders = originalGetCloudHolders;
|
|
79
80
|
apiClient.getCloudAnalystResearch = originalGetCloudAnalystResearch;
|
|
80
81
|
apiClient.getCloudCorporateActions = originalGetCloudCorporateActions;
|
|
82
|
+
apiClient.getCloudOptionsChain = originalGetCloudOptionsChain;
|
|
81
83
|
apiClient.getCloudNews = originalGetCloudNews;
|
|
82
84
|
apiClient.getCloudNewsStory = originalGetCloudNewsStory;
|
|
83
85
|
apiClient.subscribeQuotes = originalSubscribeQuotes;
|
|
@@ -103,8 +105,8 @@ describe("GloomberbCloudProvider", () => {
|
|
|
103
105
|
const history = await provider.getDetailedPriceHistory(
|
|
104
106
|
"AAPL",
|
|
105
107
|
"NASDAQ",
|
|
106
|
-
new Date(2026
|
|
107
|
-
new Date(2026
|
|
108
|
+
new Date("2026-03-27T14:00:00Z"),
|
|
109
|
+
new Date("2026-03-27T16:00:00Z"),
|
|
108
110
|
"15m",
|
|
109
111
|
);
|
|
110
112
|
|
|
@@ -119,6 +121,7 @@ describe("GloomberbCloudProvider", () => {
|
|
|
119
121
|
});
|
|
120
122
|
expect(history).toHaveLength(1);
|
|
121
123
|
expect(history[0]?.close).toBe(250.12);
|
|
124
|
+
expect(history[0]?.date.toISOString()).toBe("2026-03-27T14:15:00.000Z");
|
|
122
125
|
});
|
|
123
126
|
|
|
124
127
|
test("normalizes daily detailed history requests to 1day", async () => {
|
|
@@ -173,6 +176,11 @@ describe("GloomberbCloudProvider", () => {
|
|
|
173
176
|
expect(history[0]?.close).toBe(250.12);
|
|
174
177
|
});
|
|
175
178
|
|
|
179
|
+
test("does not advertise unsupported 45 minute chart resolution", () => {
|
|
180
|
+
const provider = new GloomberbCloudProvider();
|
|
181
|
+
expect(provider.getChartResolutionCapabilities()).not.toContain("45m");
|
|
182
|
+
});
|
|
183
|
+
|
|
176
184
|
test("normalizes sub-unit cloud quotes to their main currency", async () => {
|
|
177
185
|
apiClient.ensureVerifiedSession = async () => verifiedUser;
|
|
178
186
|
apiClient.getCloudQuote = async () => ({
|
|
@@ -319,6 +327,51 @@ describe("GloomberbCloudProvider", () => {
|
|
|
319
327
|
expect(actions.earnings[0]?.surprisePercent).toBe(6.37);
|
|
320
328
|
});
|
|
321
329
|
|
|
330
|
+
test("fetches options chains from the cloud market endpoint", async () => {
|
|
331
|
+
apiClient.ensureVerifiedSession = async () => verifiedUser;
|
|
332
|
+
|
|
333
|
+
const requestArgs: { current: { symbol: string; exchange?: string; expirationDate?: number } | null } = { current: null };
|
|
334
|
+
apiClient.getCloudOptionsChain = async (symbol, exchange, expirationDate) => {
|
|
335
|
+
requestArgs.current = { symbol, exchange, expirationDate };
|
|
336
|
+
return {
|
|
337
|
+
status: "success",
|
|
338
|
+
data: {
|
|
339
|
+
providerId: "gloomberb-cloud",
|
|
340
|
+
underlyingSymbol: "AMD",
|
|
341
|
+
expirationDates: [1_800_000_000],
|
|
342
|
+
calls: [{
|
|
343
|
+
contractSymbol: "AMD270917C00230000",
|
|
344
|
+
strike: 230,
|
|
345
|
+
currency: "USD",
|
|
346
|
+
lastPrice: 11,
|
|
347
|
+
change: 1,
|
|
348
|
+
percentChange: 10,
|
|
349
|
+
volume: 10,
|
|
350
|
+
openInterest: 20,
|
|
351
|
+
bid: 10,
|
|
352
|
+
ask: 12,
|
|
353
|
+
impliedVolatility: 0.4,
|
|
354
|
+
inTheMoney: false,
|
|
355
|
+
expiration: 1_800_000_000,
|
|
356
|
+
lastTradeDate: 1_799_000_000,
|
|
357
|
+
}],
|
|
358
|
+
puts: [],
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const provider = new GloomberbCloudProvider();
|
|
364
|
+
const chain = await provider.getOptionsChain("AMD", "NASDAQ", 1_800_000_000);
|
|
365
|
+
|
|
366
|
+
expect(requestArgs.current).toEqual({
|
|
367
|
+
symbol: "AMD",
|
|
368
|
+
exchange: "NASDAQ",
|
|
369
|
+
expirationDate: 1_800_000_000,
|
|
370
|
+
});
|
|
371
|
+
expect(chain.underlyingSymbol).toBe("AMD");
|
|
372
|
+
expect(chain.calls[0]?.contractSymbol).toBe("AMD270917C00230000");
|
|
373
|
+
});
|
|
374
|
+
|
|
322
375
|
test("preserves original target context when streaming quotes", () => {
|
|
323
376
|
let unsubscribeCalled = false;
|
|
324
377
|
const seenQuotes: Array<{ price: number; currency: string }> = [];
|