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
|
@@ -83,6 +83,23 @@ function makeArticle(id: string): NewsArticle {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
describe("AssetDataRouter", () => {
|
|
86
|
+
test("serves USD exchange rate locally without provider revalidation", async () => {
|
|
87
|
+
let providerCalls = 0;
|
|
88
|
+
const router = new AssetDataRouter({
|
|
89
|
+
...fallbackProvider,
|
|
90
|
+
async getExchangeRate() {
|
|
91
|
+
providerCalls += 1;
|
|
92
|
+
return 1;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const rate = await router.getExchangeRate("usd");
|
|
97
|
+
await Promise.resolve();
|
|
98
|
+
|
|
99
|
+
expect(rate).toBe(1);
|
|
100
|
+
expect(providerCalls).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
86
103
|
test("routes market calls only through sources with market capability", async () => {
|
|
87
104
|
const newsOnlySource: CapabilityRouteSource = {
|
|
88
105
|
id: "news-only",
|
|
@@ -378,6 +395,96 @@ describe("AssetDataRouter", () => {
|
|
|
378
395
|
expect(quote.price).toBe(123.45);
|
|
379
396
|
});
|
|
380
397
|
|
|
398
|
+
test("falls back to provider quotes when a broker mark is stale", async () => {
|
|
399
|
+
const fixedNow = Date.parse("2026-05-13T21:00:00Z");
|
|
400
|
+
const realDateNow = Date.now;
|
|
401
|
+
Date.now = () => fixedNow;
|
|
402
|
+
try {
|
|
403
|
+
const yahooProvider: DataProvider = {
|
|
404
|
+
...fallbackProvider,
|
|
405
|
+
id: "yahoo",
|
|
406
|
+
name: "Yahoo",
|
|
407
|
+
async getQuote(symbol) {
|
|
408
|
+
return {
|
|
409
|
+
symbol,
|
|
410
|
+
providerId: "yahoo",
|
|
411
|
+
price: 50_500,
|
|
412
|
+
currency: "JPY",
|
|
413
|
+
change: 4400,
|
|
414
|
+
changePercent: 9.54,
|
|
415
|
+
lastUpdated: Date.parse("2026-05-13T06:24:00Z"),
|
|
416
|
+
listingExchangeName: "JPX",
|
|
417
|
+
marketState: "CLOSED",
|
|
418
|
+
dataSource: "delayed",
|
|
419
|
+
};
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
const router = new AssetDataRouter(yahooProvider);
|
|
423
|
+
const broker: BrokerAdapter = {
|
|
424
|
+
id: "ibkr",
|
|
425
|
+
name: "IBKR",
|
|
426
|
+
configSchema: [],
|
|
427
|
+
async validate() {
|
|
428
|
+
return true;
|
|
429
|
+
},
|
|
430
|
+
async importPositions() {
|
|
431
|
+
return [];
|
|
432
|
+
},
|
|
433
|
+
async getQuote() {
|
|
434
|
+
return {
|
|
435
|
+
symbol: "285A.T",
|
|
436
|
+
providerId: "ibkr",
|
|
437
|
+
price: 46_100,
|
|
438
|
+
currency: "JPY",
|
|
439
|
+
change: 0,
|
|
440
|
+
changePercent: 0,
|
|
441
|
+
lastUpdated: Date.parse("2026-05-08T06:00:00Z"),
|
|
442
|
+
dataSource: "delayed",
|
|
443
|
+
};
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
router.attachRegistry({
|
|
448
|
+
brokers: new Map([["ibkr", broker]]),
|
|
449
|
+
getEnabledCapabilities: () => [],
|
|
450
|
+
} as any);
|
|
451
|
+
router.setConfigAccessor(() => ({
|
|
452
|
+
dataDir: "",
|
|
453
|
+
configVersion: CURRENT_CONFIG_VERSION,
|
|
454
|
+
baseCurrency: "USD",
|
|
455
|
+
refreshIntervalMinutes: 30,
|
|
456
|
+
portfolios: [],
|
|
457
|
+
watchlists: [],
|
|
458
|
+
columns: [],
|
|
459
|
+
layout: cloneLayout(DEFAULT_LAYOUT),
|
|
460
|
+
layouts: [{ name: "Default", layout: cloneLayout(DEFAULT_LAYOUT) }],
|
|
461
|
+
activeLayoutIndex: 0,
|
|
462
|
+
brokerInstances: [{
|
|
463
|
+
id: "ibkr-work",
|
|
464
|
+
brokerType: "ibkr",
|
|
465
|
+
label: "Work",
|
|
466
|
+
connectionMode: "gateway",
|
|
467
|
+
config: { connectionMode: "gateway", gateway: { host: "127.0.0.1", port: 4002, clientId: 1 } },
|
|
468
|
+
enabled: true,
|
|
469
|
+
}],
|
|
470
|
+
plugins: [],
|
|
471
|
+
disabledPlugins: [],
|
|
472
|
+
theme: "amber",
|
|
473
|
+
chartPreferences: {
|
|
474
|
+
defaultRenderMode: "area",
|
|
475
|
+
renderer: "auto",
|
|
476
|
+
},
|
|
477
|
+
recentTickers: [],
|
|
478
|
+
}));
|
|
479
|
+
|
|
480
|
+
const quote = await router.getQuote("285A.T", "TSEJ", { brokerId: "ibkr", brokerInstanceId: "ibkr-work" });
|
|
481
|
+
expect(quote.providerId).toBe("yahoo");
|
|
482
|
+
expect(quote.price).toBe(50_500);
|
|
483
|
+
} finally {
|
|
484
|
+
Date.now = realDateNow;
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
381
488
|
test("prefers the requested broker instance in search results", async () => {
|
|
382
489
|
const router = new AssetDataRouter(fallbackProvider);
|
|
383
490
|
const broker: BrokerAdapter = {
|
|
@@ -551,6 +658,38 @@ describe("AssetDataRouter", () => {
|
|
|
551
658
|
expect(quote.providerId).toBeUndefined();
|
|
552
659
|
});
|
|
553
660
|
|
|
661
|
+
test("does not search fallback providers when the preferred provider returns results", async () => {
|
|
662
|
+
let cloudCalls = 0;
|
|
663
|
+
let yahooCalls = 0;
|
|
664
|
+
const cloudProvider: DataProvider = {
|
|
665
|
+
...fallbackProvider,
|
|
666
|
+
id: "cloud",
|
|
667
|
+
name: "Cloud",
|
|
668
|
+
priority: 100,
|
|
669
|
+
async search() {
|
|
670
|
+
cloudCalls += 1;
|
|
671
|
+
return [{ providerId: "cloud", symbol: "SEC0", name: "iShares ETF", exchange: "XETRA", type: "ETF" }];
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
const yahooProvider: DataProvider = {
|
|
675
|
+
...fallbackProvider,
|
|
676
|
+
id: "yahoo",
|
|
677
|
+
name: "Yahoo",
|
|
678
|
+
priority: 1000,
|
|
679
|
+
async search() {
|
|
680
|
+
yahooCalls += 1;
|
|
681
|
+
return [{ providerId: "yahoo", symbol: "SEC0", name: "iShares ETF", exchange: "XETRA", type: "ETF" }];
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const router = new AssetDataRouter(yahooProvider, [cloudProvider]);
|
|
686
|
+
const results = await router.search("SEC0");
|
|
687
|
+
|
|
688
|
+
expect(results[0]?.providerId).toBe("cloud");
|
|
689
|
+
expect(cloudCalls).toBe(1);
|
|
690
|
+
expect(yahooCalls).toBe(0);
|
|
691
|
+
});
|
|
692
|
+
|
|
554
693
|
test("routes through registered asset-data capabilities", async () => {
|
|
555
694
|
const cloudProvider: DataProvider = {
|
|
556
695
|
...fallbackProvider,
|
|
@@ -582,6 +721,81 @@ describe("AssetDataRouter", () => {
|
|
|
582
721
|
expect(quote.providerId).toBe("cloud");
|
|
583
722
|
});
|
|
584
723
|
|
|
724
|
+
test("falls back when the preferred provider returns a stale quote", async () => {
|
|
725
|
+
const fixedNow = Date.parse("2026-05-13T21:00:00Z");
|
|
726
|
+
const realDateNow = Date.now;
|
|
727
|
+
Date.now = () => fixedNow;
|
|
728
|
+
try {
|
|
729
|
+
const yahooProvider: DataProvider = {
|
|
730
|
+
...fallbackProvider,
|
|
731
|
+
id: "yahoo",
|
|
732
|
+
name: "Yahoo",
|
|
733
|
+
async getQuote(symbol) {
|
|
734
|
+
return {
|
|
735
|
+
symbol,
|
|
736
|
+
providerId: "yahoo",
|
|
737
|
+
price: 168,
|
|
738
|
+
currency: "TWD",
|
|
739
|
+
change: 1,
|
|
740
|
+
changePercent: 0.6,
|
|
741
|
+
lastUpdated: Date.parse("2026-05-13T05:30:00Z"),
|
|
742
|
+
listingExchangeName: "TWSE",
|
|
743
|
+
marketState: "CLOSED",
|
|
744
|
+
dataSource: "delayed",
|
|
745
|
+
};
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
const cloudProvider: DataProvider = {
|
|
749
|
+
...fallbackProvider,
|
|
750
|
+
id: "gloomberb-cloud",
|
|
751
|
+
name: "Cloud",
|
|
752
|
+
priority: 100,
|
|
753
|
+
async getQuote(symbol) {
|
|
754
|
+
return {
|
|
755
|
+
symbol,
|
|
756
|
+
providerId: "gloomberb-cloud",
|
|
757
|
+
price: 150,
|
|
758
|
+
currency: "TWD",
|
|
759
|
+
change: 0,
|
|
760
|
+
changePercent: 0,
|
|
761
|
+
lastUpdated: Date.parse("2026-05-08T06:00:00Z"),
|
|
762
|
+
listingExchangeName: "TWSE",
|
|
763
|
+
marketState: "CLOSED",
|
|
764
|
+
dataSource: "delayed",
|
|
765
|
+
};
|
|
766
|
+
},
|
|
767
|
+
async getQuotesBatch(targets) {
|
|
768
|
+
return targets.map((target) => ({
|
|
769
|
+
target,
|
|
770
|
+
quote: {
|
|
771
|
+
symbol: target.symbol,
|
|
772
|
+
providerId: "gloomberb-cloud",
|
|
773
|
+
price: 150,
|
|
774
|
+
currency: "TWD",
|
|
775
|
+
change: 0,
|
|
776
|
+
changePercent: 0,
|
|
777
|
+
lastUpdated: Date.parse("2026-05-08T06:00:00Z"),
|
|
778
|
+
listingExchangeName: "TWSE",
|
|
779
|
+
marketState: "CLOSED",
|
|
780
|
+
dataSource: "delayed",
|
|
781
|
+
},
|
|
782
|
+
}));
|
|
783
|
+
},
|
|
784
|
+
};
|
|
785
|
+
const router = new AssetDataRouter(yahooProvider, [cloudProvider]);
|
|
786
|
+
|
|
787
|
+
const quote = await router.getQuote("2337", "TWSE");
|
|
788
|
+
expect(quote.providerId).toBe("yahoo");
|
|
789
|
+
expect(quote.price).toBe(168);
|
|
790
|
+
|
|
791
|
+
const [batch] = await router.getQuotesBatch([{ symbol: "2337", exchange: "TWSE" }], { forceRefresh: true });
|
|
792
|
+
expect(batch?.quote?.providerId).toBe("yahoo");
|
|
793
|
+
expect(batch?.quote?.price).toBe(168);
|
|
794
|
+
} finally {
|
|
795
|
+
Date.now = realDateNow;
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
585
799
|
test("prefers provider quote streams when one is available", () => {
|
|
586
800
|
const brokerTargets: QuoteSubscriptionTarget[] = [];
|
|
587
801
|
const providerTargets: QuoteSubscriptionTarget[] = [];
|
|
@@ -794,6 +1008,37 @@ describe("AssetDataRouter", () => {
|
|
|
794
1008
|
unsubscribe();
|
|
795
1009
|
});
|
|
796
1010
|
|
|
1011
|
+
test("returns quote-only financials for option tickers when snapshots are unavailable", async () => {
|
|
1012
|
+
const router = new AssetDataRouter({
|
|
1013
|
+
...fallbackProvider,
|
|
1014
|
+
async getTickerFinancials() {
|
|
1015
|
+
return { annualStatements: [], quarterlyStatements: [], priceHistory: [] };
|
|
1016
|
+
},
|
|
1017
|
+
async getQuote(ticker) {
|
|
1018
|
+
return {
|
|
1019
|
+
symbol: ticker,
|
|
1020
|
+
price: 252.375,
|
|
1021
|
+
currency: "USD",
|
|
1022
|
+
change: 1.5,
|
|
1023
|
+
changePercent: 0.6,
|
|
1024
|
+
bid: 250.25,
|
|
1025
|
+
ask: 254.5,
|
|
1026
|
+
mark: 252.375,
|
|
1027
|
+
lastUpdated: Date.now(),
|
|
1028
|
+
providerId: "gloomberb-cloud",
|
|
1029
|
+
dataSource: "delayed",
|
|
1030
|
+
};
|
|
1031
|
+
},
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
const financials = await router.getTickerFinancials("AMD 270917C00230000");
|
|
1035
|
+
|
|
1036
|
+
expect(financials.quote?.symbol).toBe("AMD 270917C00230000");
|
|
1037
|
+
expect(financials.quote?.mark).toBe(252.375);
|
|
1038
|
+
expect(financials.annualStatements).toEqual([]);
|
|
1039
|
+
expect(financials.priceHistory).toEqual([]);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
797
1042
|
test("merges cached broker financials with cached fallback fundamentals", async () => {
|
|
798
1043
|
const dbPath = createTempDbPath("cache-merge");
|
|
799
1044
|
const persistence = new AppPersistence(dbPath);
|
|
@@ -1066,7 +1311,7 @@ describe("AssetDataRouter", () => {
|
|
|
1066
1311
|
expect(merged.profile?.sector).toBe("Technology");
|
|
1067
1312
|
});
|
|
1068
1313
|
|
|
1069
|
-
test("
|
|
1314
|
+
test("uses the preferred provider financials without merging fallback providers", async () => {
|
|
1070
1315
|
const cloudProvider: DataProvider = {
|
|
1071
1316
|
...fallbackProvider,
|
|
1072
1317
|
id: "cloud",
|
|
@@ -1091,12 +1336,14 @@ describe("AssetDataRouter", () => {
|
|
|
1091
1336
|
};
|
|
1092
1337
|
},
|
|
1093
1338
|
};
|
|
1339
|
+
let yahooCalls = 0;
|
|
1094
1340
|
const yahooProvider: DataProvider = {
|
|
1095
1341
|
...fallbackProvider,
|
|
1096
1342
|
id: "yahoo",
|
|
1097
1343
|
name: "Yahoo",
|
|
1098
1344
|
priority: 1000,
|
|
1099
1345
|
async getTickerFinancials() {
|
|
1346
|
+
yahooCalls += 1;
|
|
1100
1347
|
return {
|
|
1101
1348
|
annualStatements: [],
|
|
1102
1349
|
quarterlyStatements: [],
|
|
@@ -1121,14 +1368,15 @@ describe("AssetDataRouter", () => {
|
|
|
1121
1368
|
const merged = await router.getTickerFinancials("AAPL", "NASDAQ");
|
|
1122
1369
|
|
|
1123
1370
|
expect(merged.quote?.price).toBe(125);
|
|
1124
|
-
expect(merged.quote?.marketCap).
|
|
1371
|
+
expect(merged.quote?.marketCap).toBeUndefined();
|
|
1125
1372
|
expect(merged.fundamentals?.trailingPE).toBe(25);
|
|
1126
|
-
expect(merged.fundamentals?.forwardPE).
|
|
1127
|
-
expect(merged.priceHistory
|
|
1373
|
+
expect(merged.fundamentals?.forwardPE).toBeUndefined();
|
|
1374
|
+
expect(merged.priceHistory).toEqual([]);
|
|
1375
|
+
expect(yahooCalls).toBe(0);
|
|
1128
1376
|
});
|
|
1129
1377
|
|
|
1130
|
-
test("
|
|
1131
|
-
const dbPath = createTempDbPath("cached-provider-financial-
|
|
1378
|
+
test("caches the preferred provider financial snapshot without merging fallback snapshots", async () => {
|
|
1379
|
+
const dbPath = createTempDbPath("cached-provider-financial-preferred");
|
|
1132
1380
|
const persistence = new AppPersistence(dbPath);
|
|
1133
1381
|
|
|
1134
1382
|
const cloudProvider: DataProvider = {
|
|
@@ -1186,7 +1434,8 @@ describe("AssetDataRouter", () => {
|
|
|
1186
1434
|
|
|
1187
1435
|
const seedRouter = new AssetDataRouter(yahooProvider, [cloudProvider], persistence.resources);
|
|
1188
1436
|
const seeded = await seedRouter.getTickerFinancials("AAPL", "NASDAQ");
|
|
1189
|
-
expect(seeded.quote?.
|
|
1437
|
+
expect(seeded.quote?.price).toBe(125);
|
|
1438
|
+
expect(seeded.quote?.marketCap).toBeUndefined();
|
|
1190
1439
|
|
|
1191
1440
|
let cloudCalls = 0;
|
|
1192
1441
|
let yahooCalls = 0;
|
|
@@ -1209,11 +1458,11 @@ describe("AssetDataRouter", () => {
|
|
|
1209
1458
|
expect(cloudCalls).toBe(0);
|
|
1210
1459
|
expect(yahooCalls).toBe(0);
|
|
1211
1460
|
expect(cached.quote?.price).toBe(125);
|
|
1212
|
-
expect(cached.quote?.marketCap).
|
|
1461
|
+
expect(cached.quote?.marketCap).toBeUndefined();
|
|
1213
1462
|
expect(cached.fundamentals?.trailingPE).toBe(25);
|
|
1214
|
-
expect(cached.fundamentals?.forwardPE).
|
|
1463
|
+
expect(cached.fundamentals?.forwardPE).toBeUndefined();
|
|
1215
1464
|
expect(cached.profile?.sector).toBe("Technology");
|
|
1216
|
-
expect(cached.priceHistory
|
|
1465
|
+
expect(cached.priceHistory).toEqual([]);
|
|
1217
1466
|
|
|
1218
1467
|
persistence.close();
|
|
1219
1468
|
});
|
|
@@ -1755,6 +2004,314 @@ describe("AssetDataRouter", () => {
|
|
|
1755
2004
|
persistence.close();
|
|
1756
2005
|
});
|
|
1757
2006
|
|
|
2007
|
+
test("keeps stale cached quotes only for startup cache priming", () => {
|
|
2008
|
+
const dbPath = createTempDbPath("startup-stale-quote-prime");
|
|
2009
|
+
const persistence = new AppPersistence(dbPath);
|
|
2010
|
+
const now = Date.now();
|
|
2011
|
+
|
|
2012
|
+
persistence.resources.set(
|
|
2013
|
+
{
|
|
2014
|
+
namespace: "market",
|
|
2015
|
+
kind: "financials",
|
|
2016
|
+
entityKey: "OLD",
|
|
2017
|
+
variantKey: "exchange=NASDAQ",
|
|
2018
|
+
sourceKey: "provider:gloomberb-cloud",
|
|
2019
|
+
},
|
|
2020
|
+
{
|
|
2021
|
+
annualStatements: [],
|
|
2022
|
+
quarterlyStatements: [],
|
|
2023
|
+
priceHistory: [],
|
|
2024
|
+
quote: {
|
|
2025
|
+
symbol: "OLD",
|
|
2026
|
+
price: 42,
|
|
2027
|
+
currency: "USD",
|
|
2028
|
+
change: -1,
|
|
2029
|
+
changePercent: -2.33,
|
|
2030
|
+
lastUpdated: Date.UTC(2020, 0, 2),
|
|
2031
|
+
marketState: "REGULAR",
|
|
2032
|
+
exchangeName: "NASDAQ",
|
|
2033
|
+
listingExchangeName: "NASDAQ",
|
|
2034
|
+
providerId: "gloomberb-cloud",
|
|
2035
|
+
dataSource: "delayed",
|
|
2036
|
+
},
|
|
2037
|
+
fundamentals: {
|
|
2038
|
+
trailingPE: 12,
|
|
2039
|
+
},
|
|
2040
|
+
},
|
|
2041
|
+
{
|
|
2042
|
+
cachePolicy: { staleMs: 60_000, expireMs: 60_000 },
|
|
2043
|
+
fetchedAt: now,
|
|
2044
|
+
},
|
|
2045
|
+
);
|
|
2046
|
+
|
|
2047
|
+
const router = new AssetDataRouter({
|
|
2048
|
+
...fallbackProvider,
|
|
2049
|
+
id: "gloomberb-cloud",
|
|
2050
|
+
name: "Cloud",
|
|
2051
|
+
async getTickerFinancials() {
|
|
2052
|
+
throw new Error("should not fetch financials");
|
|
2053
|
+
},
|
|
2054
|
+
}, [], persistence.resources);
|
|
2055
|
+
|
|
2056
|
+
const normal = router.getCachedFinancialsForTargets([{ symbol: "OLD", exchange: "NASDAQ" }], { allowExpired: true });
|
|
2057
|
+
expect(normal.get("OLD")?.quote).toBeUndefined();
|
|
2058
|
+
expect(normal.get("OLD")?.fundamentals?.trailingPE).toBe(12);
|
|
2059
|
+
|
|
2060
|
+
const startup = router.getCachedFinancialsForTargets(
|
|
2061
|
+
[{ symbol: "OLD", exchange: "NASDAQ" }],
|
|
2062
|
+
{ allowExpired: true, includeStaleQuotes: true },
|
|
2063
|
+
);
|
|
2064
|
+
expect(startup.get("OLD")?.quote?.changePercent).toBe(-2.33);
|
|
2065
|
+
expect(startup.get("OLD")?.fundamentals?.trailingPE).toBe(12);
|
|
2066
|
+
|
|
2067
|
+
persistence.close();
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
test("derives cached market cap from quote price and shares outstanding", () => {
|
|
2071
|
+
const dbPath = createTempDbPath("cached-derived-market-cap");
|
|
2072
|
+
const persistence = new AppPersistence(dbPath);
|
|
2073
|
+
const now = Date.now();
|
|
2074
|
+
|
|
2075
|
+
persistence.resources.set(
|
|
2076
|
+
{
|
|
2077
|
+
namespace: "market",
|
|
2078
|
+
kind: "financials",
|
|
2079
|
+
entityKey: "AMD",
|
|
2080
|
+
variantKey: "exchange=NASDAQ",
|
|
2081
|
+
sourceKey: "provider:gloomberb-cloud",
|
|
2082
|
+
},
|
|
2083
|
+
{
|
|
2084
|
+
annualStatements: [],
|
|
2085
|
+
quarterlyStatements: [],
|
|
2086
|
+
priceHistory: [],
|
|
2087
|
+
quote: {
|
|
2088
|
+
symbol: "AMD",
|
|
2089
|
+
price: 445.38,
|
|
2090
|
+
currency: "USD",
|
|
2091
|
+
change: -2.91,
|
|
2092
|
+
changePercent: -0.65,
|
|
2093
|
+
lastUpdated: now,
|
|
2094
|
+
marketState: "REGULAR",
|
|
2095
|
+
exchangeName: "NASDAQ",
|
|
2096
|
+
listingExchangeName: "NASDAQ",
|
|
2097
|
+
providerId: "gloomberb-cloud",
|
|
2098
|
+
dataSource: "delayed",
|
|
2099
|
+
},
|
|
2100
|
+
fundamentals: {
|
|
2101
|
+
sharesOutstanding: 1_630_000_000,
|
|
2102
|
+
},
|
|
2103
|
+
},
|
|
2104
|
+
{
|
|
2105
|
+
cachePolicy: { staleMs: 60_000, expireMs: 60_000 },
|
|
2106
|
+
fetchedAt: now,
|
|
2107
|
+
},
|
|
2108
|
+
);
|
|
2109
|
+
|
|
2110
|
+
const router = new AssetDataRouter({
|
|
2111
|
+
...fallbackProvider,
|
|
2112
|
+
id: "gloomberb-cloud",
|
|
2113
|
+
name: "Cloud",
|
|
2114
|
+
async getTickerFinancials() {
|
|
2115
|
+
throw new Error("should not fetch financials");
|
|
2116
|
+
},
|
|
2117
|
+
}, [], persistence.resources);
|
|
2118
|
+
|
|
2119
|
+
const cached = router.getCachedFinancialsForTargets([{ symbol: "AMD", exchange: "NASDAQ" }], {
|
|
2120
|
+
allowExpired: true,
|
|
2121
|
+
includeStaleQuotes: true,
|
|
2122
|
+
});
|
|
2123
|
+
expect(cached.get("AMD")?.quote?.marketCap).toBeCloseTo(725_969_400_000);
|
|
2124
|
+
|
|
2125
|
+
persistence.close();
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
test("uses symbol provider cache for broker-linked startup targets", () => {
|
|
2129
|
+
const dbPath = createTempDbPath("cached-symbol-fallback-for-contract");
|
|
2130
|
+
const persistence = new AppPersistence(dbPath);
|
|
2131
|
+
const now = Date.now();
|
|
2132
|
+
const old = now - 3 * 24 * 60 * 60_000;
|
|
2133
|
+
|
|
2134
|
+
persistence.resources.set(
|
|
2135
|
+
{
|
|
2136
|
+
namespace: "market",
|
|
2137
|
+
kind: "financials",
|
|
2138
|
+
entityKey: "contract:14015423",
|
|
2139
|
+
variantKey: "exchange=JPX",
|
|
2140
|
+
sourceKey: "provider:yahoo",
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
annualStatements: [],
|
|
2144
|
+
quarterlyStatements: [],
|
|
2145
|
+
priceHistory: [],
|
|
2146
|
+
quote: {
|
|
2147
|
+
symbol: "4092.T",
|
|
2148
|
+
providerId: "yahoo",
|
|
2149
|
+
price: 3770,
|
|
2150
|
+
currency: "JPY",
|
|
2151
|
+
change: 145,
|
|
2152
|
+
changePercent: 4,
|
|
2153
|
+
lastUpdated: old,
|
|
2154
|
+
marketState: "CLOSED",
|
|
2155
|
+
exchangeName: "JPX",
|
|
2156
|
+
listingExchangeName: "JPX",
|
|
2157
|
+
},
|
|
2158
|
+
},
|
|
2159
|
+
{
|
|
2160
|
+
cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
|
|
2161
|
+
fetchedAt: old,
|
|
2162
|
+
},
|
|
2163
|
+
);
|
|
2164
|
+
persistence.resources.set(
|
|
2165
|
+
{
|
|
2166
|
+
namespace: "market",
|
|
2167
|
+
kind: "financials",
|
|
2168
|
+
entityKey: "4092.T",
|
|
2169
|
+
variantKey: "exchange=JPX",
|
|
2170
|
+
sourceKey: "provider:yahoo",
|
|
2171
|
+
},
|
|
2172
|
+
{
|
|
2173
|
+
annualStatements: [],
|
|
2174
|
+
quarterlyStatements: [],
|
|
2175
|
+
priceHistory: [],
|
|
2176
|
+
quote: {
|
|
2177
|
+
symbol: "4092.T",
|
|
2178
|
+
providerId: "yahoo",
|
|
2179
|
+
price: 3925,
|
|
2180
|
+
currency: "JPY",
|
|
2181
|
+
change: 20,
|
|
2182
|
+
changePercent: 0.51,
|
|
2183
|
+
lastUpdated: now,
|
|
2184
|
+
marketState: "CLOSED",
|
|
2185
|
+
exchangeName: "JPX",
|
|
2186
|
+
listingExchangeName: "JPX",
|
|
2187
|
+
},
|
|
2188
|
+
},
|
|
2189
|
+
{
|
|
2190
|
+
cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
|
|
2191
|
+
fetchedAt: now,
|
|
2192
|
+
},
|
|
2193
|
+
);
|
|
2194
|
+
|
|
2195
|
+
const router = new AssetDataRouter({
|
|
2196
|
+
...fallbackProvider,
|
|
2197
|
+
id: "yahoo",
|
|
2198
|
+
name: "Yahoo",
|
|
2199
|
+
async getTickerFinancials() {
|
|
2200
|
+
throw new Error("should not fetch financials");
|
|
2201
|
+
},
|
|
2202
|
+
}, [], persistence.resources);
|
|
2203
|
+
|
|
2204
|
+
const cached = router.getCachedFinancialsForTargets([{
|
|
2205
|
+
symbol: "4092.T",
|
|
2206
|
+
exchange: "JPX",
|
|
2207
|
+
instrument: {
|
|
2208
|
+
brokerId: "ibkr",
|
|
2209
|
+
brokerInstanceId: "ibkr-live",
|
|
2210
|
+
conId: 14015423,
|
|
2211
|
+
symbol: "4092.T",
|
|
2212
|
+
},
|
|
2213
|
+
}], { allowExpired: true, includeStaleQuotes: true });
|
|
2214
|
+
|
|
2215
|
+
expect(cached.get("4092.T")?.quote?.price).toBe(3925);
|
|
2216
|
+
expect(cached.get("4092.T")?.quote?.changePercent).toBe(0.51);
|
|
2217
|
+
|
|
2218
|
+
persistence.close();
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2221
|
+
test("overlays standalone quote cache on stale financial snapshots during startup priming", () => {
|
|
2222
|
+
const dbPath = createTempDbPath("cached-quote-over-financial-snapshot");
|
|
2223
|
+
const persistence = new AppPersistence(dbPath);
|
|
2224
|
+
const now = Date.parse("2026-05-13T21:00:00Z");
|
|
2225
|
+
const old = Date.parse("2026-05-09T14:13:30Z");
|
|
2226
|
+
|
|
2227
|
+
persistence.resources.set(
|
|
2228
|
+
{
|
|
2229
|
+
namespace: "market",
|
|
2230
|
+
kind: "financials",
|
|
2231
|
+
entityKey: "contract:14016494",
|
|
2232
|
+
variantKey: "exchange=JPX",
|
|
2233
|
+
sourceKey: "provider:yahoo",
|
|
2234
|
+
},
|
|
2235
|
+
{
|
|
2236
|
+
annualStatements: [],
|
|
2237
|
+
quarterlyStatements: [],
|
|
2238
|
+
priceHistory: [],
|
|
2239
|
+
quote: {
|
|
2240
|
+
symbol: "6315.T",
|
|
2241
|
+
providerId: "yahoo",
|
|
2242
|
+
price: 3380,
|
|
2243
|
+
currency: "JPY",
|
|
2244
|
+
change: 75,
|
|
2245
|
+
changePercent: 2.27,
|
|
2246
|
+
lastUpdated: old,
|
|
2247
|
+
marketState: "CLOSED",
|
|
2248
|
+
exchangeName: "JPX",
|
|
2249
|
+
listingExchangeName: "JPX",
|
|
2250
|
+
},
|
|
2251
|
+
fundamentals: {
|
|
2252
|
+
sharesOutstanding: 75_000_462,
|
|
2253
|
+
trailingPE: 37.2,
|
|
2254
|
+
},
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
|
|
2258
|
+
fetchedAt: old,
|
|
2259
|
+
},
|
|
2260
|
+
);
|
|
2261
|
+
persistence.resources.set(
|
|
2262
|
+
{
|
|
2263
|
+
namespace: "market",
|
|
2264
|
+
kind: "quote",
|
|
2265
|
+
entityKey: "contract:14016494",
|
|
2266
|
+
variantKey: "exchange=JPX",
|
|
2267
|
+
sourceKey: "provider:yahoo",
|
|
2268
|
+
},
|
|
2269
|
+
{
|
|
2270
|
+
symbol: "6315.T",
|
|
2271
|
+
providerId: "yahoo",
|
|
2272
|
+
price: 2688,
|
|
2273
|
+
currency: "JPY",
|
|
2274
|
+
change: 13,
|
|
2275
|
+
changePercent: 0.49,
|
|
2276
|
+
lastUpdated: now,
|
|
2277
|
+
marketState: "CLOSED",
|
|
2278
|
+
exchangeName: "JPX",
|
|
2279
|
+
listingExchangeName: "JPX",
|
|
2280
|
+
},
|
|
2281
|
+
{
|
|
2282
|
+
cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
|
|
2283
|
+
fetchedAt: now,
|
|
2284
|
+
},
|
|
2285
|
+
);
|
|
2286
|
+
|
|
2287
|
+
const router = new AssetDataRouter({
|
|
2288
|
+
...fallbackProvider,
|
|
2289
|
+
id: "yahoo",
|
|
2290
|
+
name: "Yahoo",
|
|
2291
|
+
async getTickerFinancials() {
|
|
2292
|
+
throw new Error("should not fetch financials");
|
|
2293
|
+
},
|
|
2294
|
+
}, [], persistence.resources);
|
|
2295
|
+
|
|
2296
|
+
const cached = router.getCachedFinancialsForTargets([{
|
|
2297
|
+
symbol: "6315.T",
|
|
2298
|
+
exchange: "JPX",
|
|
2299
|
+
instrument: {
|
|
2300
|
+
brokerId: "ibkr",
|
|
2301
|
+
brokerInstanceId: "ibkr-live",
|
|
2302
|
+
conId: 14016494,
|
|
2303
|
+
symbol: "6315.T",
|
|
2304
|
+
},
|
|
2305
|
+
}], { allowExpired: true, includeStaleQuotes: true });
|
|
2306
|
+
|
|
2307
|
+
expect(cached.get("6315.T")?.quote?.price).toBe(2688);
|
|
2308
|
+
expect(cached.get("6315.T")?.quote?.changePercent).toBe(0.49);
|
|
2309
|
+
expect(cached.get("6315.T")?.quote?.marketCap).toBe(201_601_241_856);
|
|
2310
|
+
expect(cached.get("6315.T")?.fundamentals?.trailingPE).toBe(37.2);
|
|
2311
|
+
|
|
2312
|
+
persistence.close();
|
|
2313
|
+
});
|
|
2314
|
+
|
|
1758
2315
|
test("does not log expected provider misses for missing chart data", async () => {
|
|
1759
2316
|
const noisyProvider: DataProvider = {
|
|
1760
2317
|
...fallbackProvider,
|