gloomberb 0.3.7 → 0.4.0
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 +19 -8
- package/package.json +1 -1
- package/src/app.tsx +29 -65
- package/src/cli/base-converter.ts +31 -0
- package/src/cli/commands/search.ts +11 -3
- package/src/cli/commands/ticker.ts +22 -11
- package/src/cli/context.ts +27 -32
- package/src/cli/helpers.ts +1 -4
- package/src/cli/index.ts +140 -123
- package/src/cli/registry.test.ts +226 -0
- package/src/cli/registry.ts +201 -0
- package/src/cli.test.ts +219 -0
- package/src/components/chart/chart-controller.ts +309 -0
- package/src/components/chart/chart-data.ts +5 -6
- package/src/components/chart/chart-pane-settings.ts +27 -0
- package/src/components/chart/chart-resolution.test.ts +56 -0
- package/src/components/chart/chart-resolution.ts +362 -0
- package/src/components/chart/chart-types.ts +12 -4
- package/src/components/chart/chart-viewport.test.ts +25 -0
- package/src/components/chart/chart-viewport.ts +28 -0
- package/src/components/chart/comparison-chart-data.ts +20 -27
- package/src/components/chart/comparison-stock-chart.tsx +374 -99
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
- package/src/components/chart/stock-chart.tsx +455 -106
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +650 -5
- package/src/components/command-bar/command-bar.tsx +359 -63
- package/src/components/command-bar/command-registry.ts +8 -0
- package/src/components/command-bar/helpers.test.ts +23 -1
- package/src/components/command-bar/helpers.ts +14 -5
- package/src/components/command-bar/pane-template-display.ts +12 -0
- package/src/components/command-bar/root-shortcuts.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +4 -2
- package/src/components/detail-feed-view.test.tsx +124 -49
- package/src/components/detail-feed-view.tsx +396 -160
- package/src/components/layout/floating-pane.tsx +1 -1
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/shell.tsx +62 -50
- package/src/components/layout/status-bar.test.tsx +66 -0
- package/src/components/layout/status-bar.tsx +26 -5
- package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
- package/src/components/onboarding/onboarding-wizard.tsx +5 -6
- package/src/components/ticker-list-table.test.tsx +83 -0
- package/src/components/ticker-list-table.tsx +174 -97
- package/src/components/ui/data-table.tsx +27 -2
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/page-stack-view.tsx +60 -0
- package/src/components/use-double-click-activation.ts +31 -0
- package/src/index.tsx +10 -10
- package/src/market-data/coordinator.test.ts +92 -4
- package/src/market-data/coordinator.ts +32 -5
- package/src/market-data/hooks.test.tsx +120 -0
- package/src/market-data/hooks.tsx +53 -16
- package/src/market-data/request-types.ts +4 -2
- package/src/market-data/selectors.ts +24 -58
- package/src/plugins/builtin/chat-controller.test.ts +370 -3
- package/src/plugins/builtin/chat-controller.ts +310 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +175 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- package/src/plugins/builtin/news.tsx +1 -11
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
- package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
- package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
- package/src/plugins/builtin/sec.tsx +1 -11
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
- package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
- package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
- package/src/plugins/catalog.ts +65 -0
- package/src/plugins/ibkr/broker-adapter.ts +28 -0
- package/src/plugins/ibkr/gateway-service.test.ts +45 -1
- package/src/plugins/ibkr/gateway-service.ts +141 -7
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +11 -3
- package/src/plugins/prediction-markets/controller.ts +141 -121
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/prediction-markets/launch.ts +51 -16
- package/src/plugins/prediction-markets/metrics.ts +9 -5
- package/src/plugins/prediction-markets/pane.test.tsx +119 -57
- package/src/plugins/prediction-markets/pane.tsx +74 -153
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/types.ts +0 -1
- package/src/sources/gloomberb-cloud.test.ts +29 -0
- package/src/sources/gloomberb-cloud.ts +89 -1
- package/src/sources/provider-router.test.ts +265 -3
- package/src/sources/provider-router.ts +239 -11
- package/src/sources/yahoo-finance.test.ts +26 -0
- package/src/sources/yahoo-finance.ts +54 -3
- package/src/state/app-context-selector.test.tsx +99 -0
- package/src/state/app-context.test.ts +18 -0
- package/src/state/app-context.tsx +170 -65
- package/src/state/selectors.ts +25 -0
- package/src/theme/colors.ts +8 -8
- package/src/theme/themes.test.ts +2 -0
- package/src/theme/themes.ts +22 -10
- package/src/types/broker.ts +21 -0
- package/src/types/config.ts +17 -1
- package/src/types/data-provider.ts +18 -0
- package/src/types/financials.ts +31 -1
- package/src/types/plugin.ts +70 -0
- package/src/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +35 -12
- package/src/utils/back-navigation.ts +27 -0
- package/src/utils/format.test.ts +14 -0
- package/src/utils/format.ts +3 -3
- package/src/utils/market-status.ts +46 -1
- package/src/utils/quote-freshness.ts +127 -0
- package/src/utils/quote-resolution.test.ts +277 -0
- package/src/utils/quote-resolution.ts +641 -0
- package/src/utils/slugify.ts +3 -0
- package/src/utils/timestamp.ts +25 -0
- package/src/version.ts +1 -1
- package/src/cli/commands/portfolio.ts +0 -210
- package/src/cli/commands/watchlist.ts +0 -210
- package/src/plugins/builtin/data-providers.ts +0 -16
- package/src/sources/exchange-rates.ts +0 -18
- package/src/utils/ascii-chart.ts +0 -172
- package/src/utils/ticker-tokenizer.test.ts +0 -48
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { TimeRange } from "../components/chart/chart-types";
|
|
2
|
+
import {
|
|
3
|
+
normalizeChartResolutionSupport,
|
|
4
|
+
type ChartResolutionSupport,
|
|
5
|
+
type ManualChartResolution,
|
|
6
|
+
} from "../components/chart/chart-resolution";
|
|
2
7
|
import type { DataProvider, MarketDataRequestContext, NewsItem, QuoteSubscriptionTarget, SearchRequestContext, SecFilingItem } from "../types/data-provider";
|
|
3
8
|
import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
|
|
4
9
|
import type { InstrumentSearchResult } from "../types/instrument";
|
|
@@ -14,6 +19,17 @@ import { normalizePriceValueByDivisor, resolveCurrencyUnit } from "../utils/curr
|
|
|
14
19
|
import { createProviderMiss } from "./provider-errors";
|
|
15
20
|
|
|
16
21
|
const providerId = "gloomberb-cloud" as const;
|
|
22
|
+
const CLOUD_RESOLUTION_SUPPORT = normalizeChartResolutionSupport([
|
|
23
|
+
{ resolution: "1m", maxRange: "1W" },
|
|
24
|
+
{ resolution: "5m", maxRange: "1M" },
|
|
25
|
+
{ resolution: "15m", maxRange: "3M" },
|
|
26
|
+
{ resolution: "30m", maxRange: "6M" },
|
|
27
|
+
{ resolution: "45m", maxRange: "6M" },
|
|
28
|
+
{ resolution: "1h", maxRange: "1Y" },
|
|
29
|
+
{ resolution: "1d", maxRange: "ALL" },
|
|
30
|
+
{ resolution: "1wk", maxRange: "ALL" },
|
|
31
|
+
{ resolution: "1mo", maxRange: "ALL" },
|
|
32
|
+
]);
|
|
17
33
|
const CLOUD_PROVIDER_MISS_PATTERNS = [
|
|
18
34
|
/data not found/i,
|
|
19
35
|
/symbol.*missing or invalid/i,
|
|
@@ -39,6 +55,8 @@ async function withCloudFallback<T>(load: () => Promise<T>, message: string): Pr
|
|
|
39
55
|
|
|
40
56
|
function mapQuote(quote: CloudQuotePayload): Quote {
|
|
41
57
|
const { currency, divisor } = resolveCurrencyUnit(quote.currency);
|
|
58
|
+
const listingExchangeName = quote.listingExchangeName ?? quote.exchangeName;
|
|
59
|
+
const listingExchangeFullName = quote.listingExchangeFullName ?? quote.fullExchangeName ?? listingExchangeName;
|
|
42
60
|
return {
|
|
43
61
|
...quote,
|
|
44
62
|
currency: currency || quote.currency,
|
|
@@ -57,6 +75,10 @@ function mapQuote(quote: CloudQuotePayload): Quote {
|
|
|
57
75
|
preMarketChange: normalizePriceValueByDivisor(quote.preMarketChange, divisor),
|
|
58
76
|
postMarketPrice: normalizePriceValueByDivisor(quote.postMarketPrice, divisor),
|
|
59
77
|
postMarketChange: normalizePriceValueByDivisor(quote.postMarketChange, divisor),
|
|
78
|
+
listingExchangeName,
|
|
79
|
+
listingExchangeFullName,
|
|
80
|
+
exchangeName: listingExchangeName,
|
|
81
|
+
fullExchangeName: listingExchangeFullName,
|
|
60
82
|
providerId,
|
|
61
83
|
};
|
|
62
84
|
}
|
|
@@ -92,7 +114,7 @@ function toCloudInterval(interval: string): string {
|
|
|
92
114
|
return "45min";
|
|
93
115
|
case "1d":
|
|
94
116
|
return "1day";
|
|
95
|
-
case "
|
|
117
|
+
case "1wk":
|
|
96
118
|
return "1week";
|
|
97
119
|
case "1mo":
|
|
98
120
|
return "1month";
|
|
@@ -118,12 +140,45 @@ function formatCloudDateTime(date: Date, includeTime: boolean): string {
|
|
|
118
140
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
119
141
|
}
|
|
120
142
|
|
|
143
|
+
function getRangeStartDate(range: TimeRange, endDate = new Date()): Date {
|
|
144
|
+
const startDate = new Date(endDate);
|
|
145
|
+
switch (range) {
|
|
146
|
+
case "1D":
|
|
147
|
+
startDate.setDate(startDate.getDate() - 1);
|
|
148
|
+
break;
|
|
149
|
+
case "1W":
|
|
150
|
+
startDate.setDate(startDate.getDate() - 7);
|
|
151
|
+
break;
|
|
152
|
+
case "1M":
|
|
153
|
+
startDate.setMonth(startDate.getMonth() - 1);
|
|
154
|
+
break;
|
|
155
|
+
case "3M":
|
|
156
|
+
startDate.setMonth(startDate.getMonth() - 3);
|
|
157
|
+
break;
|
|
158
|
+
case "6M":
|
|
159
|
+
startDate.setMonth(startDate.getMonth() - 6);
|
|
160
|
+
break;
|
|
161
|
+
case "1Y":
|
|
162
|
+
startDate.setFullYear(startDate.getFullYear() - 1);
|
|
163
|
+
break;
|
|
164
|
+
case "5Y":
|
|
165
|
+
startDate.setFullYear(startDate.getFullYear() - 5);
|
|
166
|
+
break;
|
|
167
|
+
case "ALL":
|
|
168
|
+
startDate.setFullYear(startDate.getFullYear() - 20);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
return startDate;
|
|
172
|
+
}
|
|
173
|
+
|
|
121
174
|
function toHistoryRequest(range: TimeRange): {
|
|
122
175
|
interval: string;
|
|
123
176
|
outputsize: number;
|
|
124
177
|
rangeKey: TimeRange;
|
|
125
178
|
} {
|
|
126
179
|
switch (range) {
|
|
180
|
+
case "1D":
|
|
181
|
+
return { interval: "5min", outputsize: 24 * 12, rangeKey: range };
|
|
127
182
|
case "1W":
|
|
128
183
|
return { interval: "1h", outputsize: 7 * 24, rangeKey: range };
|
|
129
184
|
case "1M":
|
|
@@ -179,6 +234,14 @@ export class GloomberbCloudProvider implements DataProvider {
|
|
|
179
234
|
readonly name = "Gloomberb Cloud";
|
|
180
235
|
readonly priority = 100;
|
|
181
236
|
|
|
237
|
+
getChartResolutionSupport(): ChartResolutionSupport[] {
|
|
238
|
+
return CLOUD_RESOLUTION_SUPPORT;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getChartResolutionCapabilities(): ManualChartResolution[] {
|
|
242
|
+
return CLOUD_RESOLUTION_SUPPORT.map((entry) => entry.resolution);
|
|
243
|
+
}
|
|
244
|
+
|
|
182
245
|
async canProvide(): Promise<boolean> {
|
|
183
246
|
return !!(await apiClient.ensureVerifiedSession());
|
|
184
247
|
}
|
|
@@ -260,6 +323,31 @@ export class GloomberbCloudProvider implements DataProvider {
|
|
|
260
323
|
.map((point) => mapPricePoint(point, divisor));
|
|
261
324
|
}
|
|
262
325
|
|
|
326
|
+
async getPriceHistoryForResolution(
|
|
327
|
+
ticker: string,
|
|
328
|
+
exchange: string,
|
|
329
|
+
bufferRange: TimeRange,
|
|
330
|
+
resolution: ManualChartResolution,
|
|
331
|
+
_context?: MarketDataRequestContext,
|
|
332
|
+
): Promise<PricePoint[]> {
|
|
333
|
+
await requireVerifiedSession();
|
|
334
|
+
const interval = toCloudInterval(resolution);
|
|
335
|
+
const endDate = new Date();
|
|
336
|
+
const startDate = getRangeStartDate(bufferRange, endDate);
|
|
337
|
+
const includeTime = /(min|h)$/i.test(interval);
|
|
338
|
+
const response = await withCloudFallback(
|
|
339
|
+
() => apiClient.getCloudHistory(ticker, exchange, {
|
|
340
|
+
interval,
|
|
341
|
+
startDate: formatCloudDateTime(startDate, includeTime),
|
|
342
|
+
endDate: formatCloudDateTime(endDate, includeTime),
|
|
343
|
+
}),
|
|
344
|
+
`Cloud chart data is unavailable for ${ticker}`,
|
|
345
|
+
);
|
|
346
|
+
const { divisor } = resolveCurrencyUnit(response.providerMeta?.currency);
|
|
347
|
+
return unwrapRequiredCloudResponse(response, `Cloud chart data is unavailable for ${ticker}`)
|
|
348
|
+
.map((point) => mapPricePoint(point, divisor));
|
|
349
|
+
}
|
|
350
|
+
|
|
263
351
|
async getDetailedPriceHistory(
|
|
264
352
|
ticker: string,
|
|
265
353
|
exchange: string,
|
|
@@ -632,7 +632,7 @@ describe("ProviderRouter", () => {
|
|
|
632
632
|
persistence.close();
|
|
633
633
|
});
|
|
634
634
|
|
|
635
|
-
test("
|
|
635
|
+
test("keeps broker price authority when a fallback quote is off by a likely 100x unit mismatch", async () => {
|
|
636
636
|
const router = new ProviderRouter({
|
|
637
637
|
...fallbackProvider,
|
|
638
638
|
async getTickerFinancials() {
|
|
@@ -642,11 +642,13 @@ describe("ProviderRouter", () => {
|
|
|
642
642
|
priceHistory: [{ date: new Date("2026-03-28T00:00:00Z"), close: 0.245 }],
|
|
643
643
|
quote: {
|
|
644
644
|
symbol: "IQE.L",
|
|
645
|
+
providerId: "yahoo",
|
|
645
646
|
price: 0.245,
|
|
646
647
|
currency: "GBP",
|
|
647
648
|
change: -0.021,
|
|
648
649
|
changePercent: -7.89,
|
|
649
650
|
lastUpdated: Date.now(),
|
|
651
|
+
dataSource: "yahoo",
|
|
650
652
|
},
|
|
651
653
|
fundamentals: { revenue: 1000 },
|
|
652
654
|
};
|
|
@@ -669,11 +671,13 @@ describe("ProviderRouter", () => {
|
|
|
669
671
|
priceHistory: [{ date: new Date("2026-03-28T00:00:00Z"), close: 24.5 }],
|
|
670
672
|
quote: {
|
|
671
673
|
symbol: "IQE",
|
|
674
|
+
providerId: "ibkr",
|
|
672
675
|
price: 24.5,
|
|
673
676
|
currency: "GBP",
|
|
674
677
|
change: -2.1,
|
|
675
678
|
changePercent: -7.89,
|
|
676
679
|
lastUpdated: Date.now(),
|
|
680
|
+
dataSource: "live",
|
|
677
681
|
},
|
|
678
682
|
fundamentals: { netIncome: 200 },
|
|
679
683
|
};
|
|
@@ -718,7 +722,8 @@ describe("ProviderRouter", () => {
|
|
|
718
722
|
brokerInstanceId: "ibkr-work",
|
|
719
723
|
});
|
|
720
724
|
|
|
721
|
-
expect(merged.quote?.price).toBe(
|
|
725
|
+
expect(merged.quote?.price).toBe(24.5);
|
|
726
|
+
expect(merged.quote?.provenance?.rejectedPriceProviders).toContain("yahoo");
|
|
722
727
|
expect(merged.priceHistory[0]?.close).toBe(0.245);
|
|
723
728
|
expect(merged.fundamentals?.netIncome).toBe(200);
|
|
724
729
|
});
|
|
@@ -1071,11 +1076,13 @@ describe("ProviderRouter", () => {
|
|
|
1071
1076
|
priceHistory: [],
|
|
1072
1077
|
quote: {
|
|
1073
1078
|
symbol: "IQE.L",
|
|
1079
|
+
providerId: "yahoo",
|
|
1074
1080
|
price: 0.245,
|
|
1075
1081
|
currency: "GBP",
|
|
1076
1082
|
change: 0,
|
|
1077
1083
|
changePercent: 0,
|
|
1078
1084
|
lastUpdated: Date.now(),
|
|
1085
|
+
dataSource: "yahoo",
|
|
1079
1086
|
},
|
|
1080
1087
|
};
|
|
1081
1088
|
},
|
|
@@ -1100,11 +1107,13 @@ describe("ProviderRouter", () => {
|
|
|
1100
1107
|
priceHistory: [],
|
|
1101
1108
|
quote: {
|
|
1102
1109
|
symbol: "IQE",
|
|
1110
|
+
providerId: "ibkr",
|
|
1103
1111
|
price: 24.5,
|
|
1104
1112
|
currency: "GBP",
|
|
1105
1113
|
change: 0,
|
|
1106
1114
|
changePercent: 0,
|
|
1107
1115
|
lastUpdated: Date.now(),
|
|
1116
|
+
dataSource: "live",
|
|
1108
1117
|
},
|
|
1109
1118
|
};
|
|
1110
1119
|
},
|
|
@@ -1157,7 +1166,7 @@ describe("ProviderRouter", () => {
|
|
|
1157
1166
|
brokerId: "ibkr",
|
|
1158
1167
|
brokerInstanceId: "ibkr-live",
|
|
1159
1168
|
});
|
|
1160
|
-
expect(live.quote?.price).toBe(
|
|
1169
|
+
expect(live.quote?.price).toBe(24.5);
|
|
1161
1170
|
|
|
1162
1171
|
const flex = await router.getTickerFinancials("IQE", "LSE", {
|
|
1163
1172
|
brokerId: "ibkr",
|
|
@@ -1281,6 +1290,209 @@ describe("ProviderRouter", () => {
|
|
|
1281
1290
|
persistence.close();
|
|
1282
1291
|
});
|
|
1283
1292
|
|
|
1293
|
+
test("drops stale cached cloud quotes while preserving cached cloud fundamentals", () => {
|
|
1294
|
+
const dbPath = createTempDbPath("stale-cloud-cached-financials");
|
|
1295
|
+
const persistence = new AppPersistence(dbPath);
|
|
1296
|
+
const now = Date.parse("2026-04-08T10:55:00Z");
|
|
1297
|
+
|
|
1298
|
+
persistence.resources.set(
|
|
1299
|
+
{
|
|
1300
|
+
namespace: "market",
|
|
1301
|
+
kind: "financials",
|
|
1302
|
+
entityKey: "HY9H",
|
|
1303
|
+
variantKey: "exchange=FWB2",
|
|
1304
|
+
sourceKey: "provider:gloomberb-cloud",
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
annualStatements: [],
|
|
1308
|
+
quarterlyStatements: [],
|
|
1309
|
+
priceHistory: [],
|
|
1310
|
+
quote: {
|
|
1311
|
+
symbol: "HY9H",
|
|
1312
|
+
price: 528,
|
|
1313
|
+
currency: "EUR",
|
|
1314
|
+
change: 28,
|
|
1315
|
+
changePercent: 5.6,
|
|
1316
|
+
lastUpdated: Date.parse("2026-04-07T19:55:00Z"),
|
|
1317
|
+
marketState: "REGULAR",
|
|
1318
|
+
exchangeName: "FWB2",
|
|
1319
|
+
listingExchangeName: "FWB2",
|
|
1320
|
+
providerId: "gloomberb-cloud",
|
|
1321
|
+
dataSource: "delayed",
|
|
1322
|
+
},
|
|
1323
|
+
fundamentals: {
|
|
1324
|
+
revenue: 1234,
|
|
1325
|
+
},
|
|
1326
|
+
profile: {
|
|
1327
|
+
description: "Cloud profile",
|
|
1328
|
+
},
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
cachePolicy: { staleMs: 60_000, expireMs: 60_000 },
|
|
1332
|
+
fetchedAt: now,
|
|
1333
|
+
},
|
|
1334
|
+
);
|
|
1335
|
+
persistence.resources.set(
|
|
1336
|
+
{
|
|
1337
|
+
namespace: "market",
|
|
1338
|
+
kind: "financials",
|
|
1339
|
+
entityKey: "HY9H",
|
|
1340
|
+
variantKey: "exchange=FWB2",
|
|
1341
|
+
sourceKey: "provider:yahoo",
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
annualStatements: [],
|
|
1345
|
+
quarterlyStatements: [],
|
|
1346
|
+
priceHistory: [],
|
|
1347
|
+
quote: {
|
|
1348
|
+
symbol: "HY9H.F",
|
|
1349
|
+
price: 596,
|
|
1350
|
+
currency: "EUR",
|
|
1351
|
+
change: 68,
|
|
1352
|
+
changePercent: 12.88,
|
|
1353
|
+
lastUpdated: now,
|
|
1354
|
+
marketState: "REGULAR",
|
|
1355
|
+
exchangeName: "FWB2",
|
|
1356
|
+
listingExchangeName: "FWB2",
|
|
1357
|
+
providerId: "yahoo",
|
|
1358
|
+
dataSource: "yahoo",
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
cachePolicy: { staleMs: 60_000, expireMs: 60_000 },
|
|
1363
|
+
fetchedAt: now,
|
|
1364
|
+
},
|
|
1365
|
+
);
|
|
1366
|
+
|
|
1367
|
+
const router = new ProviderRouter({
|
|
1368
|
+
...fallbackProvider,
|
|
1369
|
+
id: "yahoo",
|
|
1370
|
+
name: "Yahoo",
|
|
1371
|
+
async getTickerFinancials() {
|
|
1372
|
+
throw new Error("should not fetch financials");
|
|
1373
|
+
},
|
|
1374
|
+
}, [{
|
|
1375
|
+
...fallbackProvider,
|
|
1376
|
+
id: "gloomberb-cloud",
|
|
1377
|
+
name: "Cloud",
|
|
1378
|
+
priority: 100,
|
|
1379
|
+
async getTickerFinancials() {
|
|
1380
|
+
throw new Error("should not fetch financials");
|
|
1381
|
+
},
|
|
1382
|
+
}], persistence.resources);
|
|
1383
|
+
|
|
1384
|
+
const cached = router.getCachedFinancialsForTargets([{
|
|
1385
|
+
symbol: "HY9H",
|
|
1386
|
+
exchange: "FWB2",
|
|
1387
|
+
}], { allowExpired: true });
|
|
1388
|
+
|
|
1389
|
+
expect(cached.get("HY9H")?.quote?.providerId).toBe("yahoo");
|
|
1390
|
+
expect(cached.get("HY9H")?.quote?.price).toBe(596);
|
|
1391
|
+
expect(cached.get("HY9H")?.fundamentals?.revenue).toBe(1234);
|
|
1392
|
+
expect(cached.get("HY9H")?.profile?.description).toBe("Cloud profile");
|
|
1393
|
+
|
|
1394
|
+
persistence.close();
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
test("drops cached premarket cloud quotes that lack an active-session price", () => {
|
|
1398
|
+
const dbPath = createTempDbPath("stale-cloud-premarket-cache");
|
|
1399
|
+
const persistence = new AppPersistence(dbPath);
|
|
1400
|
+
const now = Date.parse("2026-04-08T10:55:00Z");
|
|
1401
|
+
|
|
1402
|
+
persistence.resources.set(
|
|
1403
|
+
{
|
|
1404
|
+
namespace: "market",
|
|
1405
|
+
kind: "financials",
|
|
1406
|
+
entityKey: "AMD",
|
|
1407
|
+
variantKey: "exchange=NASDAQ",
|
|
1408
|
+
sourceKey: "provider:gloomberb-cloud",
|
|
1409
|
+
},
|
|
1410
|
+
{
|
|
1411
|
+
annualStatements: [],
|
|
1412
|
+
quarterlyStatements: [],
|
|
1413
|
+
priceHistory: [],
|
|
1414
|
+
quote: {
|
|
1415
|
+
symbol: "AMD",
|
|
1416
|
+
price: 221.53,
|
|
1417
|
+
currency: "USD",
|
|
1418
|
+
change: 1.35,
|
|
1419
|
+
changePercent: 0.61,
|
|
1420
|
+
lastUpdated: now,
|
|
1421
|
+
marketState: "PRE",
|
|
1422
|
+
exchangeName: "NASDAQ",
|
|
1423
|
+
listingExchangeName: "NASDAQ",
|
|
1424
|
+
providerId: "gloomberb-cloud",
|
|
1425
|
+
dataSource: "delayed",
|
|
1426
|
+
},
|
|
1427
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
cachePolicy: { staleMs: 60_000, expireMs: 60_000 },
|
|
1430
|
+
fetchedAt: now,
|
|
1431
|
+
},
|
|
1432
|
+
);
|
|
1433
|
+
persistence.resources.set(
|
|
1434
|
+
{
|
|
1435
|
+
namespace: "market",
|
|
1436
|
+
kind: "financials",
|
|
1437
|
+
entityKey: "AMD",
|
|
1438
|
+
variantKey: "exchange=NASDAQ",
|
|
1439
|
+
sourceKey: "provider:yahoo",
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
annualStatements: [],
|
|
1443
|
+
quarterlyStatements: [],
|
|
1444
|
+
priceHistory: [],
|
|
1445
|
+
quote: {
|
|
1446
|
+
symbol: "AMD",
|
|
1447
|
+
price: 221.53,
|
|
1448
|
+
currency: "USD",
|
|
1449
|
+
change: 1.35,
|
|
1450
|
+
changePercent: 0.61,
|
|
1451
|
+
lastUpdated: now,
|
|
1452
|
+
marketState: "PRE",
|
|
1453
|
+
preMarketPrice: 231,
|
|
1454
|
+
preMarketChange: 9.47,
|
|
1455
|
+
preMarketChangePercent: 4.27,
|
|
1456
|
+
exchangeName: "NASDAQ",
|
|
1457
|
+
listingExchangeName: "NASDAQ",
|
|
1458
|
+
providerId: "yahoo",
|
|
1459
|
+
dataSource: "yahoo",
|
|
1460
|
+
},
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
cachePolicy: { staleMs: 60_000, expireMs: 60_000 },
|
|
1464
|
+
fetchedAt: now,
|
|
1465
|
+
},
|
|
1466
|
+
);
|
|
1467
|
+
|
|
1468
|
+
const router = new ProviderRouter({
|
|
1469
|
+
...fallbackProvider,
|
|
1470
|
+
id: "yahoo",
|
|
1471
|
+
name: "Yahoo",
|
|
1472
|
+
async getTickerFinancials() {
|
|
1473
|
+
throw new Error("should not fetch financials");
|
|
1474
|
+
},
|
|
1475
|
+
}, [{
|
|
1476
|
+
...fallbackProvider,
|
|
1477
|
+
id: "gloomberb-cloud",
|
|
1478
|
+
name: "Cloud",
|
|
1479
|
+
priority: 100,
|
|
1480
|
+
async getTickerFinancials() {
|
|
1481
|
+
throw new Error("should not fetch financials");
|
|
1482
|
+
},
|
|
1483
|
+
}], persistence.resources);
|
|
1484
|
+
|
|
1485
|
+
const cached = router.getCachedFinancialsForTargets([{
|
|
1486
|
+
symbol: "AMD",
|
|
1487
|
+
exchange: "NASDAQ",
|
|
1488
|
+
}], { allowExpired: true });
|
|
1489
|
+
|
|
1490
|
+
expect(cached.get("AMD")?.quote?.providerId).toBe("yahoo");
|
|
1491
|
+
expect(cached.get("AMD")?.quote?.preMarketPrice).toBe(231);
|
|
1492
|
+
|
|
1493
|
+
persistence.close();
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1284
1496
|
test("does not log expected provider misses for missing chart data", async () => {
|
|
1285
1497
|
const noisyProvider: DataProvider = {
|
|
1286
1498
|
...fallbackProvider,
|
|
@@ -1485,6 +1697,56 @@ describe("ProviderRouter", () => {
|
|
|
1485
1697
|
persistence.close();
|
|
1486
1698
|
});
|
|
1487
1699
|
|
|
1700
|
+
test("falls back to later providers for fixed-resolution chart history", async () => {
|
|
1701
|
+
const cloudProvider: DataProvider = {
|
|
1702
|
+
...fallbackProvider,
|
|
1703
|
+
id: "cloud",
|
|
1704
|
+
name: "Cloud",
|
|
1705
|
+
priority: 100,
|
|
1706
|
+
async getPriceHistoryForResolution() {
|
|
1707
|
+
return [];
|
|
1708
|
+
},
|
|
1709
|
+
};
|
|
1710
|
+
const yahooProvider: DataProvider = {
|
|
1711
|
+
...fallbackProvider,
|
|
1712
|
+
id: "yahoo",
|
|
1713
|
+
name: "Yahoo",
|
|
1714
|
+
priority: 1000,
|
|
1715
|
+
async getPriceHistoryForResolution() {
|
|
1716
|
+
return [{ date: new Date("2026-03-28T00:00:00Z"), close: 102 }];
|
|
1717
|
+
},
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
const router = new ProviderRouter(yahooProvider, [cloudProvider]);
|
|
1721
|
+
const history = await router.getPriceHistoryForResolution("AAPL", "NASDAQ", "1Y", "1d");
|
|
1722
|
+
|
|
1723
|
+
expect(history[0]?.close).toBe(102);
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
test("returns normalized manual chart resolution capabilities", async () => {
|
|
1727
|
+
const cloudProvider: DataProvider = {
|
|
1728
|
+
...fallbackProvider,
|
|
1729
|
+
id: "cloud",
|
|
1730
|
+
name: "Cloud",
|
|
1731
|
+
priority: 100,
|
|
1732
|
+
async getChartResolutionCapabilities() {
|
|
1733
|
+
return [];
|
|
1734
|
+
},
|
|
1735
|
+
};
|
|
1736
|
+
const yahooProvider: DataProvider = {
|
|
1737
|
+
...fallbackProvider,
|
|
1738
|
+
id: "yahoo",
|
|
1739
|
+
name: "Yahoo",
|
|
1740
|
+
priority: 1000,
|
|
1741
|
+
async getChartResolutionCapabilities() {
|
|
1742
|
+
return ["1wk", "auto", "1d", "bogus"];
|
|
1743
|
+
},
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1746
|
+
const router = new ProviderRouter(yahooProvider, [cloudProvider]);
|
|
1747
|
+
expect(await router.getChartResolutionCapabilities("AAPL", "NASDAQ")).toEqual(["1d", "1wk"]);
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1488
1750
|
test("falls back to later providers when detailed chart history is empty", async () => {
|
|
1489
1751
|
const cloudProvider: DataProvider = {
|
|
1490
1752
|
...fallbackProvider,
|