gloomberb 0.9.6 → 0.9.8
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/api-client/auth.ts +20 -0
- package/src/api-client/index.ts +48 -0
- package/src/api-client/types.ts +45 -0
- package/src/app/runtime/desktop-deeplink.test.ts +87 -0
- package/src/app/runtime/desktop-deeplink.ts +496 -0
- package/src/app.tsx +34 -0
- package/src/components/chart/bar-chart-renderer.test.ts +87 -12
- package/src/components/chart/bar-chart-renderer.ts +111 -55
- package/src/components/chart/static/bar-chart-surface.tsx +104 -20
- package/src/components/chart/stock-chart/footer.test.ts +65 -0
- package/src/components/chart/stock-chart/footer.ts +68 -13
- package/src/components/chart/stock-chart/resolved.tsx +18 -4
- package/src/components/command-bar/pane-templates/items.ts +11 -3
- package/src/components/command-bar/routes/root/results.ts +1 -1
- package/src/components/command-bar/surface/index.test.tsx +52 -0
- package/src/components/command-bar/workflow/ops.ts +81 -20
- package/src/components/index.ts +1 -0
- package/src/components/toggle-list.tsx +29 -73
- package/src/components/ui/button.tsx +4 -1
- package/src/components/ui/checkbox.tsx +76 -0
- package/src/components/ui/choice-dialog.tsx +15 -3
- package/src/components/ui/frame.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/message-composer.tsx +4 -0
- package/src/components/ui/native-select.tsx +4 -2
- package/src/market-data/coordinator/events.test.ts +49 -0
- package/src/market-data/coordinator/events.ts +25 -4
- package/src/market-data/coordinator/index.test.ts +223 -0
- package/src/market-data/coordinator/index.ts +40 -3
- package/src/market-data/coordinator/quotes.ts +13 -5
- package/src/market-data/hooks.tsx +17 -13
- package/src/market-data/market/freshness.ts +58 -1
- package/src/market-data/quotes/freshness.test.ts +49 -1
- package/src/market-data/quotes/freshness.ts +8 -0
- package/src/market-data/quotes/resolution.test.ts +185 -0
- package/src/market-data/quotes/resolution.ts +94 -8
- package/src/market-data/request-types.test.ts +61 -1
- package/src/market-data/request-types.ts +42 -3
- package/src/pane-settings.test.ts +1 -0
- package/src/plugins/builtin/account-management/footer.ts +2 -5
- package/src/plugins/builtin/account-management/form-components.tsx +260 -24
- package/src/plugins/builtin/account-management/keyboard.ts +32 -2
- package/src/plugins/builtin/account-management/model.test.ts +99 -4
- package/src/plugins/builtin/account-management/model.ts +263 -3
- package/src/plugins/builtin/account-management/pane.tsx +784 -155
- package/src/plugins/builtin/chat/chat.test.tsx +125 -0
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
- package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
- package/src/plugins/builtin/chat/content/composer.tsx +84 -1
- package/src/plugins/builtin/chat/content/index.tsx +154 -16
- package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
- package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
- package/src/plugins/builtin/chat/content/mentions.ts +88 -0
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
- package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
- package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -4
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
- package/src/plugins/builtin/chat/sidebar.test.tsx +69 -2
- package/src/plugins/builtin/chat/sidebar.tsx +26 -2
- package/src/plugins/builtin/chat/test-harness.tsx +21 -11
- package/src/plugins/builtin/cloud/plugin.tsx +7 -0
- package/src/plugins/builtin/correlation/index.tsx +4 -1
- package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
- package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
- package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
- package/src/plugins/builtin/portfolio-list/index.test.ts +102 -1
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +62 -1
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +414 -8
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +24 -32
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +131 -22
- package/src/plugins/builtin/portfolio-list/settings.ts +36 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +43 -7
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- package/src/plugins/ibkr/gateway/market-data.ts +12 -5
- package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
- package/src/plugins/registry/context.ts +15 -0
- package/src/plugins/registry/index.ts +31 -0
- package/src/renderers/electrobun/bun/index.ts +42 -0
- package/src/renderers/electrobun/shared/protocol.ts +5 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
- package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
- package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
- package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
- package/src/renderers/electrobun/view/host/box.tsx +0 -2
- package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
- package/src/renderers/electrobun/view/host/text.tsx +1 -2
- package/src/renderers/electrobun/view/main.tsx +3 -0
- package/src/renderers/electrobun/view/ui-host.tsx +2 -0
- package/src/sources/provider-router/batches.ts +11 -3
- package/src/sources/provider-router/cached-financials.test.ts +123 -0
- package/src/sources/provider-router/financial-routes.ts +55 -5
- package/src/sources/provider-router/financials.test.ts +49 -0
- package/src/sources/provider-router/financials.ts +40 -3
- package/src/sources/provider-router/index.test.ts +147 -0
- package/src/sources/provider-router/primary.ts +17 -3
- package/src/sources/provider-router/streaming.ts +6 -1
- package/src/sources/sec-edgar.test.ts +42 -0
- package/src/sources/sec-edgar.ts +1 -2
- package/src/sources/yahoo-finance/mappers.test.ts +29 -0
- package/src/sources/yahoo-finance/mappers.ts +2 -1
- package/src/sources/yahoo-finance/requests.ts +2 -1
- package/src/sources/yahoo-finance/snapshots.ts +13 -2
- package/src/sources/yahoo-finance.ts +6 -2
- package/src/state/hooks/quote-streaming.test.ts +14 -1
- package/src/state/hooks/quote-streaming.ts +5 -4
- package/src/sync/controller.ts +296 -0
- package/src/sync/core-contributors.test.ts +251 -0
- package/src/sync/core-contributors.ts +400 -0
- package/src/sync/profile-analytics.ts +39 -0
- package/src/sync/react.ts +53 -0
- package/src/sync/types.ts +77 -0
- package/src/types/config.ts +1 -0
- package/src/types/desktop-deeplink.ts +7 -0
- package/src/types/plugin.ts +3 -0
- package/src/ui/host.tsx +14 -1
- package/src/ui/index.tsx +1 -0
- package/src/utils/exchanges.test.ts +11 -1
- package/src/utils/exchanges.ts +5 -1
- package/src/version.ts +1 -1
|
@@ -312,6 +312,61 @@ describe("AssetDataRouter", () => {
|
|
|
312
312
|
expect(quote.price).toBe(123.45);
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
+
test("reconciles broker quote price with provider day reference fields", async () => {
|
|
316
|
+
const router = new AssetDataRouter({
|
|
317
|
+
...fallbackProvider,
|
|
318
|
+
id: "yahoo",
|
|
319
|
+
async getQuote() {
|
|
320
|
+
return {
|
|
321
|
+
symbol: "VICR",
|
|
322
|
+
providerId: "yahoo",
|
|
323
|
+
price: 299.74,
|
|
324
|
+
currency: "USD",
|
|
325
|
+
previousClose: 282.95,
|
|
326
|
+
change: 16.79,
|
|
327
|
+
changePercent: 5.93,
|
|
328
|
+
lastUpdated: Date.now(),
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
const broker: BrokerAdapter = {
|
|
333
|
+
id: "ibkr",
|
|
334
|
+
name: "IBKR",
|
|
335
|
+
configSchema: [],
|
|
336
|
+
async validate() {
|
|
337
|
+
return true;
|
|
338
|
+
},
|
|
339
|
+
async importPositions() {
|
|
340
|
+
return [];
|
|
341
|
+
},
|
|
342
|
+
async getQuote() {
|
|
343
|
+
return {
|
|
344
|
+
symbol: "VICR",
|
|
345
|
+
providerId: "ibkr",
|
|
346
|
+
price: 299.8,
|
|
347
|
+
currency: "USD",
|
|
348
|
+
previousClose: 380.1,
|
|
349
|
+
change: -80.3,
|
|
350
|
+
changePercent: -21.12,
|
|
351
|
+
lastUpdated: Date.now(),
|
|
352
|
+
dataSource: "live",
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
attachTestRegistry(router, { brokers: [["ibkr", broker]] });
|
|
358
|
+
setBrokerInstances(router, [brokerInstance()]);
|
|
359
|
+
|
|
360
|
+
const quote = await router.getQuote("VICR", "NASDAQ", { brokerId: "ibkr", brokerInstanceId: "ibkr-work" });
|
|
361
|
+
expect(quote.providerId).toBe("ibkr");
|
|
362
|
+
expect(quote.price).toBe(299.8);
|
|
363
|
+
expect(quote.previousClose).toBe(282.95);
|
|
364
|
+
expect(quote.change).toBeCloseTo(16.85, 5);
|
|
365
|
+
expect(quote.changePercent).toBeCloseTo(5.955, 3);
|
|
366
|
+
expect(quote.provenance?.price?.providerId).toBe("ibkr");
|
|
367
|
+
expect(quote.provenance?.fields?.previousClose?.providerId).toBe("yahoo");
|
|
368
|
+
});
|
|
369
|
+
|
|
315
370
|
test("prefers broker options chains for broker-context targets", async () => {
|
|
316
371
|
const chain = {
|
|
317
372
|
underlyingSymbol: "AAPL",
|
|
@@ -820,6 +875,48 @@ describe("AssetDataRouter", () => {
|
|
|
820
875
|
unsubscribe();
|
|
821
876
|
});
|
|
822
877
|
|
|
878
|
+
test("drops empty zero provider stream quotes", () => {
|
|
879
|
+
const providerTargets: QuoteSubscriptionTarget[] = [];
|
|
880
|
+
const seenQuotes: Array<{ symbol: string; price: number }> = [];
|
|
881
|
+
const streamingProvider: DataProvider = {
|
|
882
|
+
...fallbackProvider,
|
|
883
|
+
id: "cloud",
|
|
884
|
+
name: "Cloud",
|
|
885
|
+
subscribeQuotes(targets, onQuote) {
|
|
886
|
+
providerTargets.push(...targets);
|
|
887
|
+
onQuote(targets[0]!, {
|
|
888
|
+
symbol: "HEXA B",
|
|
889
|
+
price: 0,
|
|
890
|
+
currency: "SEK",
|
|
891
|
+
change: 0,
|
|
892
|
+
changePercent: 0,
|
|
893
|
+
lastUpdated: Date.now(),
|
|
894
|
+
listingExchangeName: "SFB",
|
|
895
|
+
dataSource: "delayed",
|
|
896
|
+
});
|
|
897
|
+
return () => {};
|
|
898
|
+
},
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
const router = new AssetDataRouter(fallbackProvider, [streamingProvider]);
|
|
902
|
+
const unsubscribe = router.subscribeQuotes([{
|
|
903
|
+
symbol: "HEXA B",
|
|
904
|
+
exchange: "SFB",
|
|
905
|
+
route: "provider",
|
|
906
|
+
}], (_target, quote) => {
|
|
907
|
+
seenQuotes.push({ symbol: quote.symbol, price: quote.price });
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
expect(providerTargets).toEqual([{
|
|
911
|
+
symbol: "HEXA B",
|
|
912
|
+
exchange: "SFB",
|
|
913
|
+
route: "provider",
|
|
914
|
+
}]);
|
|
915
|
+
expect(seenQuotes).toEqual([]);
|
|
916
|
+
|
|
917
|
+
unsubscribe();
|
|
918
|
+
});
|
|
919
|
+
|
|
823
920
|
test("returns quote-only financials for option tickers when snapshots are unavailable", async () => {
|
|
824
921
|
const router = new AssetDataRouter({
|
|
825
922
|
...fallbackProvider,
|
|
@@ -1308,6 +1405,56 @@ describe("AssetDataRouter", () => {
|
|
|
1308
1405
|
expect(yahooCalls).toBe(1);
|
|
1309
1406
|
});
|
|
1310
1407
|
|
|
1408
|
+
test("fills unusable preferred provider financial quotes from a later provider", async () => {
|
|
1409
|
+
const cloudProvider: DataProvider = {
|
|
1410
|
+
...fallbackProvider,
|
|
1411
|
+
id: "cloud",
|
|
1412
|
+
name: "Cloud",
|
|
1413
|
+
priority: 100,
|
|
1414
|
+
async getTickerFinancials() {
|
|
1415
|
+
return makeFinancials({
|
|
1416
|
+
profile: { sector: "Industrials" },
|
|
1417
|
+
quote: makeQuote({
|
|
1418
|
+
symbol: "HY9H",
|
|
1419
|
+
price: 1295,
|
|
1420
|
+
change: -95,
|
|
1421
|
+
changePercent: -6.83,
|
|
1422
|
+
lastUpdated: Date.now() - 20 * 60_000,
|
|
1423
|
+
listingExchangeName: "FWB2",
|
|
1424
|
+
marketState: "REGULAR",
|
|
1425
|
+
}),
|
|
1426
|
+
});
|
|
1427
|
+
},
|
|
1428
|
+
};
|
|
1429
|
+
const yahooProvider: DataProvider = {
|
|
1430
|
+
...fallbackProvider,
|
|
1431
|
+
id: "yahoo",
|
|
1432
|
+
name: "Yahoo",
|
|
1433
|
+
priority: 1000,
|
|
1434
|
+
async getTickerFinancials() {
|
|
1435
|
+
return makeFinancials({
|
|
1436
|
+
quote: makeQuote({
|
|
1437
|
+
symbol: "HY9H.F",
|
|
1438
|
+
price: 1305,
|
|
1439
|
+
change: -85,
|
|
1440
|
+
changePercent: -6.12,
|
|
1441
|
+
lastUpdated: Date.now(),
|
|
1442
|
+
listingExchangeName: "FWB2",
|
|
1443
|
+
marketState: "REGULAR",
|
|
1444
|
+
}),
|
|
1445
|
+
});
|
|
1446
|
+
},
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
const router = new AssetDataRouter(yahooProvider, [cloudProvider]);
|
|
1450
|
+
const merged = await router.getTickerFinancials("HY9H", "FWB2", { cacheMode: "refresh" });
|
|
1451
|
+
|
|
1452
|
+
expect(merged.profile?.sector).toBe("Industrials");
|
|
1453
|
+
expect(merged.quote?.symbol).toBe("HY9H.F");
|
|
1454
|
+
expect(merged.quote?.price).toBe(1305);
|
|
1455
|
+
expect(merged.quote?.changePercent).toBe(-6.12);
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1311
1458
|
test("caches the preferred provider financial snapshot without merging fallback snapshots", async () => {
|
|
1312
1459
|
const dbPath = createTempDbPath("cached-provider-financial-preferred");
|
|
1313
1460
|
const persistence = new AppPersistence(dbPath);
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { MarketDataRequestContext } from "../../types/data-provider";
|
|
2
2
|
import type { Quote, TickerFinancials } from "../../types/financials";
|
|
3
3
|
import { normalizeTickerFinancialsPriceHistory } from "../../utils/price-history";
|
|
4
|
-
import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
|
|
5
4
|
import { resolveTickerFinancialsQuoteState } from "../../market-data/quotes/resolution";
|
|
6
5
|
import { shouldLogProviderError } from "../provider-errors";
|
|
7
6
|
import {
|
|
7
|
+
dropUnusableProviderQuote,
|
|
8
8
|
hasDetailedStatementRows,
|
|
9
9
|
hasDeepStatementHistory,
|
|
10
10
|
hasStatementRows,
|
|
11
|
+
isProviderQuoteUsableForCurrentSession,
|
|
11
12
|
mergeMissingStatementArrays,
|
|
13
|
+
mergeFinancials,
|
|
12
14
|
} from "./financials";
|
|
13
15
|
import type { ProviderRouterCoreDeps, SourceResult } from "./route-types";
|
|
14
16
|
|
|
@@ -61,7 +63,8 @@ export class ProviderRouterPrimaryRoutes {
|
|
|
61
63
|
for (const provider of this.options.providersInPriorityOrder()) {
|
|
62
64
|
try {
|
|
63
65
|
const rawValue = await provider.getTickerFinancials(ticker, exchange, context);
|
|
64
|
-
const
|
|
66
|
+
const resolvedValue = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(rawValue));
|
|
67
|
+
const value = resolvedValue ? dropUnusableProviderQuote(resolvedValue, exchange) : null;
|
|
65
68
|
if (!value) continue;
|
|
66
69
|
const sourceKey = this.options.providerSourceKey(provider);
|
|
67
70
|
const cacheValue = primaryResult
|
|
@@ -84,6 +87,17 @@ export class ProviderRouterPrimaryRoutes {
|
|
|
84
87
|
if (hasDetailedStatementRows(value) && hasDeepStatementHistory(value)) return primaryResult;
|
|
85
88
|
continue;
|
|
86
89
|
}
|
|
90
|
+
if (!primaryResult.value.quote && value.quote) {
|
|
91
|
+
primaryResult = {
|
|
92
|
+
sourceKey: primaryResult.sourceKey,
|
|
93
|
+
value: mergeFinancials(primaryResult.value, {
|
|
94
|
+
annualStatements: [],
|
|
95
|
+
quarterlyStatements: [],
|
|
96
|
+
priceHistory: [],
|
|
97
|
+
quote: value.quote,
|
|
98
|
+
}) ?? primaryResult.value,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
87
101
|
if (hasStatementRows(value)) {
|
|
88
102
|
primaryResult = {
|
|
89
103
|
sourceKey: primaryResult.sourceKey,
|
|
@@ -143,7 +157,7 @@ export class ProviderRouterPrimaryRoutes {
|
|
|
143
157
|
for (const provider of this.options.providersInPriorityOrder()) {
|
|
144
158
|
try {
|
|
145
159
|
const quote = await provider.getQuote(ticker, exchange, context);
|
|
146
|
-
if (quote
|
|
160
|
+
if (!isProviderQuoteUsableForCurrentSession(quote, exchange)) continue;
|
|
147
161
|
const sourceKey = this.options.providerSourceKey(provider);
|
|
148
162
|
this.options.cacheResource(
|
|
149
163
|
"quote",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DataProvider, MarketDataRequestContext, QuoteSubscriptionTarget } from "../../types/data-provider";
|
|
2
2
|
import type { Quote } from "../../types/financials";
|
|
3
3
|
import type { BrokerCandidate } from "./brokers";
|
|
4
|
+
import { isProviderQuoteUsableForCurrentSession } from "./financials";
|
|
4
5
|
|
|
5
6
|
export interface ProviderRouterStreamingDeps {
|
|
6
7
|
providersInPriorityOrder(): DataProvider[];
|
|
@@ -67,7 +68,11 @@ export class ProviderRouterStreamingRoutes {
|
|
|
67
68
|
providerId: streamingProvider.id,
|
|
68
69
|
targetCount: providerTargets.length,
|
|
69
70
|
});
|
|
70
|
-
unsubscribers.push(streamingProvider.subscribeQuotes(providerTargets,
|
|
71
|
+
unsubscribers.push(streamingProvider.subscribeQuotes(providerTargets, (target, quote) => {
|
|
72
|
+
if (isProviderQuoteUsableForCurrentSession(quote, target.exchange)) {
|
|
73
|
+
onQuote(target, quote);
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
if (unsubscribers.length === 0) {
|
|
@@ -200,6 +200,47 @@ describe("parseCompanyFactsFinancialStatements", () => {
|
|
|
200
200
|
},
|
|
201
201
|
]);
|
|
202
202
|
});
|
|
203
|
+
|
|
204
|
+
test("merges statement values from all configured tags for a field", () => {
|
|
205
|
+
const fact = (tag: string, unit: string, rows: unknown[]) => ({
|
|
206
|
+
[tag]: {
|
|
207
|
+
units: {
|
|
208
|
+
[unit]: rows,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
const quarterly = (end: string, val: number, frame: string) => ({
|
|
213
|
+
start: `${end.slice(0, 4)}-01-01`,
|
|
214
|
+
end,
|
|
215
|
+
val,
|
|
216
|
+
filed: end,
|
|
217
|
+
form: "10-Q",
|
|
218
|
+
fp: "Q1",
|
|
219
|
+
frame,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const statements = parseCompanyFactsFinancialStatements({
|
|
223
|
+
facts: {
|
|
224
|
+
"us-gaap": {
|
|
225
|
+
...fact("RevenueFromContractWithCustomerExcludingAssessedTax", "USD", [
|
|
226
|
+
quarterly("2026-03-31", 200, "CY2026Q1"),
|
|
227
|
+
]),
|
|
228
|
+
...fact("Revenues", "USD", [
|
|
229
|
+
quarterly("2025-03-31", 100, "CY2025Q1"),
|
|
230
|
+
]),
|
|
231
|
+
...fact("GrossProfit", "USD", [
|
|
232
|
+
quarterly("2025-03-31", 60, "CY2025Q1"),
|
|
233
|
+
quarterly("2026-03-31", 120, "CY2026Q1"),
|
|
234
|
+
]),
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(statements.quarterlyStatements).toEqual([
|
|
240
|
+
{ date: "2025-03-31", totalRevenue: 100, grossProfit: 60 },
|
|
241
|
+
{ date: "2026-03-31", totalRevenue: 200, grossProfit: 120 },
|
|
242
|
+
]);
|
|
243
|
+
});
|
|
203
244
|
});
|
|
204
245
|
|
|
205
246
|
describe("SecEdgarClient", () => {
|
|
@@ -403,6 +444,7 @@ describe("SecEdgarClient", () => {
|
|
|
403
444
|
|
|
404
445
|
const client = new SecEdgarClient();
|
|
405
446
|
const content = await client.getFilingContent({
|
|
447
|
+
form: "10-Q",
|
|
406
448
|
filingUrl: "https://www.sec.gov/Archives/edgar/data/320193/index.htm",
|
|
407
449
|
primaryDocumentUrl: "https://www.sec.gov/Archives/edgar/data/320193/primary-doc.htm",
|
|
408
450
|
});
|
package/src/sources/sec-edgar.ts
CHANGED
|
@@ -451,7 +451,7 @@ function setCompanyFactValue(
|
|
|
451
451
|
const selectionKey = `${date}:${String(field)}`;
|
|
452
452
|
if (compareCompanyFactsEntries(entry, selectedFacts.get(selectionKey)) < 0) return;
|
|
453
453
|
const row = rows.get(date) ?? { date };
|
|
454
|
-
(row as Record<string, unknown>)[field] = value;
|
|
454
|
+
(row as unknown as Record<string, unknown>)[field] = value;
|
|
455
455
|
rows.set(date, row);
|
|
456
456
|
selectedFacts.set(selectionKey, entry);
|
|
457
457
|
}
|
|
@@ -506,7 +506,6 @@ export function parseCompanyFactsFinancialStatements(payload: unknown): SecCompa
|
|
|
506
506
|
if (entries.length === 0) continue;
|
|
507
507
|
fillCompanyFactsStatementRows(annualRows, annualSelectedFacts, entries, field, "annual");
|
|
508
508
|
fillCompanyFactsStatementRows(quarterlyRows, quarterlySelectedFacts, entries, field, "quarterly");
|
|
509
|
-
break;
|
|
510
509
|
}
|
|
511
510
|
}
|
|
512
511
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { extractExtendedHoursPrices } from "./mappers";
|
|
3
|
+
import type { ChartResult } from "./types";
|
|
4
|
+
|
|
5
|
+
describe("Yahoo mappers", () => {
|
|
6
|
+
test("derives premarket change from the prior regular close", () => {
|
|
7
|
+
const meta: NonNullable<ChartResult["meta"]> = {
|
|
8
|
+
regularMarketPrice: 39.47,
|
|
9
|
+
chartPreviousClose: 29,
|
|
10
|
+
currentTradingPeriod: {
|
|
11
|
+
pre: { start: 100, end: 200 },
|
|
12
|
+
regular: { start: 300, end: 400 },
|
|
13
|
+
post: { start: 500, end: 600 },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const result = extractExtendedHoursPrices(
|
|
18
|
+
meta,
|
|
19
|
+
[100, 200],
|
|
20
|
+
[39, 39.47],
|
|
21
|
+
"PRE",
|
|
22
|
+
38,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(result.preMarketPrice).toBe(39.47);
|
|
26
|
+
expect(result.preMarketChange).toBeCloseTo(1.47, 8);
|
|
27
|
+
expect(result.preMarketChangePercent).toBeCloseTo(3.8684210526, 8);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -286,13 +286,14 @@ export function extractExtendedHoursPrices(
|
|
|
286
286
|
timestamps: number[],
|
|
287
287
|
closes: (number | null)[],
|
|
288
288
|
marketState: MarketState,
|
|
289
|
+
regularCloseOverride?: number,
|
|
289
290
|
): ExtendedHoursData {
|
|
290
291
|
const ctp = meta.currentTradingPeriod;
|
|
291
292
|
if (!ctp || !timestamps.length) return {};
|
|
292
293
|
|
|
293
294
|
const regStart = ctp.regular?.start ?? 0;
|
|
294
295
|
const regEnd = ctp.regular?.end ?? Infinity;
|
|
295
|
-
const regularClose = meta.regularMarketPrice ?? meta.chartPreviousClose;
|
|
296
|
+
const regularClose = regularCloseOverride ?? meta.regularMarketPrice ?? meta.chartPreviousClose;
|
|
296
297
|
|
|
297
298
|
if (marketState === "PRE") {
|
|
298
299
|
for (let i = timestamps.length - 1; i >= 0; i--) {
|
|
@@ -65,6 +65,7 @@ export async function fetchYahooExtendedHoursData(
|
|
|
65
65
|
http: YahooHttpClient,
|
|
66
66
|
symbol: string,
|
|
67
67
|
meta: NonNullable<ChartResult["meta"]>,
|
|
68
|
+
regularClose?: number,
|
|
68
69
|
): Promise<ExtendedHoursData> {
|
|
69
70
|
const marketState = deriveMarketState(meta);
|
|
70
71
|
if (marketState !== "PRE" && marketState !== "POST") return {};
|
|
@@ -80,7 +81,7 @@ export async function fetchYahooExtendedHoursData(
|
|
|
80
81
|
const result = data.chart?.result?.[0];
|
|
81
82
|
if (!result?.timestamp?.length) return {};
|
|
82
83
|
const closes = result.indicators?.quote?.[0]?.close || [];
|
|
83
|
-
return extractExtendedHoursPrices(meta, result.timestamp, closes, marketState);
|
|
84
|
+
return extractExtendedHoursPrices(meta, result.timestamp, closes, marketState, regularClose);
|
|
84
85
|
} catch {
|
|
85
86
|
return {};
|
|
86
87
|
}
|
|
@@ -42,6 +42,7 @@ interface YahooSnapshotLoaders {
|
|
|
42
42
|
fetchExtendedHoursData: (
|
|
43
43
|
symbol: string,
|
|
44
44
|
meta: NonNullable<ChartResult["meta"]>,
|
|
45
|
+
regularClose?: number,
|
|
45
46
|
) => Promise<ExtendedHoursData>;
|
|
46
47
|
fetchQuoteSupplement: (
|
|
47
48
|
symbol: string,
|
|
@@ -132,7 +133,12 @@ export async function loadYahooTickerFinancials(
|
|
|
132
133
|
const changePct = prev ? (change / prev) * 100 : 0;
|
|
133
134
|
|
|
134
135
|
const marketState = deriveMarketState(meta);
|
|
135
|
-
const
|
|
136
|
+
const extendedHoursBase = quoteSupplement.previousClose ?? prev;
|
|
137
|
+
const extHours = await loaders.fetchExtendedHoursData(
|
|
138
|
+
symbol,
|
|
139
|
+
meta,
|
|
140
|
+
extendedHoursBase == null ? undefined : extendedHoursBase * currencyDivisor,
|
|
141
|
+
);
|
|
136
142
|
if (currencyDivisor !== 1) {
|
|
137
143
|
normalizeExtendedHoursPrices(extHours, currencyDivisor);
|
|
138
144
|
}
|
|
@@ -207,7 +213,12 @@ export async function loadYahooQuote(
|
|
|
207
213
|
const change = prev != null ? price - prev : 0;
|
|
208
214
|
|
|
209
215
|
const marketState = deriveMarketState(meta);
|
|
210
|
-
const
|
|
216
|
+
const extendedHoursBase = quoteSupplement.previousClose ?? prev;
|
|
217
|
+
const extHours = await loaders.fetchExtendedHoursData(
|
|
218
|
+
symbol,
|
|
219
|
+
meta,
|
|
220
|
+
extendedHoursBase == null ? undefined : extendedHoursBase * currencyDivisor,
|
|
221
|
+
);
|
|
211
222
|
if (currencyDivisor !== 1) {
|
|
212
223
|
normalizeExtendedHoursPrices(extHours, currencyDivisor);
|
|
213
224
|
}
|
|
@@ -103,8 +103,12 @@ export class YahooFinanceClient implements DataProvider {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/** Fetch extended hours data using 1d intraday chart with pre/post market included */
|
|
106
|
-
private async fetchExtendedHoursData(
|
|
107
|
-
|
|
106
|
+
private async fetchExtendedHoursData(
|
|
107
|
+
symbol: string,
|
|
108
|
+
meta: NonNullable<ChartResult["meta"]>,
|
|
109
|
+
regularClose?: number,
|
|
110
|
+
) {
|
|
111
|
+
return fetchYahooExtendedHoursData(this.http, symbol, meta, regularClose);
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
private async fetchTimeseries(symbol: string, types: string[], period1 = "2010-01-01") {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
buildQuoteStreamSubscriptionKey,
|
|
4
|
+
normalizeQuoteStreamSubscriptionTarget,
|
|
5
|
+
} from "./quote-streaming";
|
|
3
6
|
|
|
4
7
|
describe("buildQuoteStreamSubscriptionKey", () => {
|
|
5
8
|
test("includes broker context so identical symbols can stream independently", () => {
|
|
@@ -28,4 +31,14 @@ describe("buildQuoteStreamSubscriptionKey", () => {
|
|
|
28
31
|
expect(workKey).not.toBe(personalKey);
|
|
29
32
|
});
|
|
30
33
|
|
|
34
|
+
test("keeps saved exchange aliases in stream targets", () => {
|
|
35
|
+
const target = normalizeQuoteStreamSubscriptionTarget({
|
|
36
|
+
symbol: "lpk",
|
|
37
|
+
exchange: "ibis",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(target?.symbol).toBe("LPK");
|
|
41
|
+
expect(target?.exchange).toBe("IBIS");
|
|
42
|
+
expect(buildQuoteStreamSubscriptionKey(target!)).toContain("LPK|IBIS");
|
|
43
|
+
});
|
|
31
44
|
});
|
|
@@ -2,18 +2,18 @@ import { useEffect } from "react";
|
|
|
2
2
|
import { useAppActive } from "../app/activity";
|
|
3
3
|
import type { QuoteSubscriptionTarget } from "../../types/data-provider";
|
|
4
4
|
import { debugLog } from "../../utils/debug-log";
|
|
5
|
-
import {
|
|
5
|
+
import { normalizeSymbol } from "../../utils/exchanges";
|
|
6
6
|
import { getSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
7
7
|
|
|
8
8
|
const quoteStreamLog = debugLog.createLogger("quote-stream");
|
|
9
9
|
|
|
10
|
-
function
|
|
10
|
+
export function normalizeQuoteStreamSubscriptionTarget(target: QuoteSubscriptionTarget): QuoteSubscriptionTarget | null {
|
|
11
11
|
const symbol = normalizeSymbol(target.symbol);
|
|
12
12
|
if (!symbol) return null;
|
|
13
13
|
return {
|
|
14
14
|
...target,
|
|
15
15
|
symbol,
|
|
16
|
-
exchange:
|
|
16
|
+
exchange: target.exchange?.trim().toUpperCase() ?? "",
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -43,7 +43,7 @@ export function useQuoteStreaming(targets: QuoteSubscriptionTarget[]): void {
|
|
|
43
43
|
|
|
44
44
|
const normalizedEntries = new Map<string, QuoteSubscriptionTarget>();
|
|
45
45
|
for (const target of targets) {
|
|
46
|
-
const normalized =
|
|
46
|
+
const normalized = normalizeQuoteStreamSubscriptionTarget(target);
|
|
47
47
|
if (!normalized) continue;
|
|
48
48
|
const key = buildQuoteStreamSubscriptionKey(normalized);
|
|
49
49
|
normalizedEntries.set(key, normalized);
|
|
@@ -74,6 +74,7 @@ export function useQuoteStreaming(targets: QuoteSubscriptionTarget[]): void {
|
|
|
74
74
|
instrument: target.context?.instrument ?? null,
|
|
75
75
|
},
|
|
76
76
|
priority: {
|
|
77
|
+
route: target.route,
|
|
77
78
|
surface: target.surface,
|
|
78
79
|
visible: target.visible,
|
|
79
80
|
selected: target.selected,
|