gloomberb 0.9.8 → 0.9.10
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/index.test.ts +3 -3
- package/src/api-client/index.ts +0 -6
- package/src/api-client/request.test.ts +63 -0
- package/src/api-client/request.ts +53 -5
- package/src/api-client/types.ts +0 -2
- package/src/app/pane-runtime/layout-placement.ts +0 -1
- package/src/app.tsx +1 -0
- package/src/brokers/account-cache.ts +2 -10
- package/src/brokers/broker-portfolio-sync.ts +0 -1
- package/src/brokers/broker-ticker-sync.ts +20 -16
- package/src/brokers/remote-broker-adapter.ts +3 -0
- package/src/capabilities/registry.test.ts +25 -0
- package/src/capabilities/registry.ts +22 -5
- package/src/cli/commands/automation.ts +1 -3
- package/src/cli/commands/command-utils.ts +0 -7
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/plugins.ts +34 -17
- package/src/cli/index.ts +2 -2
- package/src/cli.test.ts +0 -157
- package/src/components/chart/comparison/renderer.ts +0 -1
- package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
- package/src/components/chart/core/index.test.ts +0 -24
- package/src/components/chart/core/renderer.ts +0 -4
- package/src/components/chart/core/resolution.test.ts +0 -17
- package/src/components/chart/core/resolution.ts +0 -26
- package/src/components/chart/core/scroll.test.ts +0 -6
- package/src/components/chart/core/scroll.ts +0 -22
- package/src/components/chart/core/viewport.test.ts +2 -23
- package/src/components/chart/core/viewport.ts +0 -27
- package/src/components/chart/market-session.ts +1 -1
- package/src/components/chart/native/surface/hooks.ts +10 -3
- package/src/components/chart/stock-chart/data-runtime.ts +0 -1
- package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
- package/src/components/command-bar/helpers.test.ts +1 -80
- package/src/components/command-bar/helpers.ts +1 -36
- package/src/components/command-bar/pane-settings.ts +1 -1
- package/src/components/command-bar/panel/runtime.ts +0 -2
- package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
- package/src/components/command-bar/surface/index.test.tsx +0 -16
- package/src/components/command-bar/surface/index.tsx +0 -1
- package/src/components/feed-data-table/stack-view.test.tsx +36 -130
- package/src/components/feed-data-table/stack-view.tsx +0 -1
- package/src/components/layout/shell/index.test.tsx +2 -113
- package/src/components/layout/shell/index.tsx +0 -1
- package/src/components/layout/shell/menu.ts +0 -1
- package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
- package/src/components/onboarding/onboarding-wizard.tsx +3 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -23
- package/src/components/price-selector-dialog.tsx +3 -3
- package/src/components/ticker/badge/text.tsx +0 -1
- package/src/components/ui/page-stack-view.tsx +1 -1
- package/src/components/ui/ui.test.tsx +0 -78
- package/src/core/state/app/layout-reducer.ts +0 -1
- package/src/data/config/layout.ts +3 -3
- package/src/data/resource-store.ts +6 -0
- package/src/data/sqlite/cache.test.ts +5 -0
- package/src/data/ticker-store.ts +0 -7
- package/src/market-data/coordinator/auxiliary.ts +1 -44
- package/src/market-data/coordinator/entries.ts +0 -1
- package/src/market-data/coordinator/index.test.ts +0 -10
- package/src/market-data/coordinator/index.ts +1 -18
- package/src/market-data/coordinator/quotes.ts +0 -6
- package/src/market-data/coordinator/subscriptions.test.ts +1 -0
- package/src/market-data/market/status.ts +19 -1
- package/src/market-data/query-store.ts +0 -4
- package/src/market-data/quotes/resolution.ts +2 -2
- package/src/market-data/request-types.ts +0 -5
- package/src/market-data/result-types.ts +0 -11
- package/src/market-data/selectors.ts +1 -5
- package/src/news/aggregator.ts +5 -1
- package/src/pane-settings.test.ts +0 -18
- package/src/plugins/builtin/account-management/form-components.tsx +0 -120
- package/src/plugins/builtin/account-management/model.ts +3 -27
- package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
- package/src/plugins/builtin/buildout/format.ts +2 -11
- package/src/plugins/builtin/chat/chat.test.tsx +2 -1
- package/src/plugins/builtin/chat/content/index.tsx +1 -2
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -1
- package/src/plugins/builtin/chat/controller/index.ts +0 -10
- package/src/plugins/builtin/chat/controller.test.ts +5 -5
- package/src/plugins/builtin/chat/sidebar.test.tsx +15 -5
- package/src/plugins/builtin/chat/test-harness.tsx +2 -2
- package/src/plugins/builtin/cloud/plugin.tsx +1 -1
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
- package/src/plugins/builtin/congress-trades/model.ts +2 -7
- package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
- package/src/plugins/builtin/earnings/index.tsx +0 -9
- package/src/plugins/builtin/econ/index.tsx +1 -5
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/pane.tsx +0 -5
- package/src/plugins/builtin/insider/index.tsx +1 -8
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
- package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -2
- package/src/plugins/builtin/news/index.tsx +0 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
- package/src/plugins/builtin/options/view.test.tsx +1 -38
- package/src/plugins/builtin/plugin-groups.ts +17 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
- package/src/plugins/builtin/portfolio-list/index.test.ts +1 -24
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
- package/src/plugins/builtin/sec/filing-documents.ts +0 -11
- package/src/plugins/builtin/sec/index.tsx +0 -2
- package/src/plugins/builtin/substack/article-detail.tsx +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +0 -15
- package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
- package/src/plugins/builtin/world-indices/index.tsx +0 -4
- package/src/plugins/builtin/world-indices/model.ts +0 -7
- package/src/plugins/ibkr/account-cache.ts +1 -1
- package/src/plugins/ibkr/portfolio-performance.ts +1 -1
- package/src/plugins/ibkr/trade/utils.ts +1 -7
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/prediction-markets/detail.test.tsx +0 -34
- package/src/plugins/registry/contributions.ts +22 -11
- package/src/plugins/registry/index.test.ts +44 -0
- package/src/plugins/registry/index.ts +44 -26
- package/src/remote/controller.ts +1 -7
- package/src/remote/json-patch.ts +15 -1
- package/src/remote/server.ts +16 -6
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
- package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
- package/src/renderers/electrobun/view/host/mouse.ts +0 -11
- package/src/renderers/electrobun/view/http-fetch.ts +15 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
- package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
- package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
- package/src/renderers/opentui/host.tsx +11 -3
- package/src/renderers/opentui/input-host.tsx +5 -1
- package/src/sources/gloomberb-cloud/index.test.ts +0 -72
- package/src/sources/gloomberb-cloud/index.ts +1 -30
- package/src/sources/gloomberb-cloud/news.ts +0 -12
- package/src/sources/provider-router/financial-routes.ts +25 -3
- package/src/sources/provider-router/index.test.ts +167 -0
- package/src/sources/provider-router/search.ts +5 -0
- package/src/sources/provider-router/supplemental.ts +4 -3
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/sources/yahoo-finance/options.ts +59 -14
- package/src/sources/yahoo-finance/quote-summary.ts +4 -1
- package/src/sources/yahoo-finance.test.ts +40 -21
- package/src/sources/yahoo-finance.ts +13 -8
- package/src/state/app/context/index.test.tsx +0 -81
- package/src/state/app/context/selector.test.tsx +6 -2
- package/src/sync/controller.test.ts +151 -0
- package/src/sync/controller.ts +92 -57
- package/src/sync/core-contributors.ts +45 -21
- package/src/sync/react.ts +6 -8
- package/src/sync/types.ts +3 -0
- package/src/test-support/data-provider.ts +0 -1
- package/src/ui/host.tsx +2 -1
- package/src/ui/index.tsx +1 -0
- package/src/updater.test.ts +0 -21
- package/src/updater.ts +20 -16
- package/src/utils/async-deadline.ts +26 -0
- package/src/utils/selection-clipboard.ts +2 -2
- package/src/version.ts +1 -1
- package/src/components/layout/pane/header.test.tsx +0 -39
- package/src/plugins/builtin/chat/channels.test.ts +0 -28
- package/src/plugins/builtin/chat/index.tsx +0 -4
- package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
- package/src/plugins/builtin/earnings/index.test.ts +0 -12
- package/src/plugins/builtin/help/index.test.tsx +0 -242
- package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
- package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
- package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
- package/src/renderers/electrobun/view/host/text.test.ts +0 -22
- package/src/renderers/opentui/dialog-host.test.tsx +0 -44
- /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
|
@@ -22,6 +22,7 @@ export function toKeyEventLike(event: {
|
|
|
22
22
|
shift?: boolean;
|
|
23
23
|
alt?: boolean;
|
|
24
24
|
meta?: boolean;
|
|
25
|
+
option?: boolean;
|
|
25
26
|
preventDefault?: () => void;
|
|
26
27
|
stopPropagation?: () => void;
|
|
27
28
|
defaultPrevented?: boolean;
|
|
@@ -37,9 +38,12 @@ export function toKeyEventLike(event: {
|
|
|
37
38
|
sequence: event.sequence,
|
|
38
39
|
ctrl: event.ctrl ?? false,
|
|
39
40
|
shift: event.shift ?? false,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// OpenTUI follows terminal naming: `meta` is Alt/Option, while the
|
|
42
|
+
// kitty keyboard protocol exposes the Command/Windows modifier as
|
|
43
|
+
// `super`. Keep the shared input model aligned with browser semantics.
|
|
44
|
+
alt: event.alt === true || event.meta === true || event.option === true,
|
|
45
|
+
meta: event.super === true,
|
|
46
|
+
super: event.super === true,
|
|
43
47
|
get defaultPrevented() {
|
|
44
48
|
return event.defaultPrevented ?? false;
|
|
45
49
|
},
|
|
@@ -101,6 +105,9 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
|
|
|
101
105
|
await proc.exited;
|
|
102
106
|
},
|
|
103
107
|
async copyText(text) {
|
|
108
|
+
if (!text) return;
|
|
109
|
+
if (renderer.copyToClipboardOSC52(text)) return;
|
|
110
|
+
if (process.platform !== "darwin") return;
|
|
104
111
|
const proc = Bun.spawn(["pbcopy"], {
|
|
105
112
|
stdin: "pipe",
|
|
106
113
|
stdout: "ignore",
|
|
@@ -111,6 +118,7 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
|
|
|
111
118
|
await proc.exited;
|
|
112
119
|
},
|
|
113
120
|
async readText() {
|
|
121
|
+
if (process.platform !== "darwin") return "";
|
|
114
122
|
const proc = Bun.spawn(["pbpaste"], {
|
|
115
123
|
stdout: "pipe",
|
|
116
124
|
stderr: "ignore",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useLayoutEffect, useMemo, useRef, type ReactNode } from "react";
|
|
2
|
+
import { useNativeRenderer } from "../../ui";
|
|
2
3
|
import {
|
|
3
4
|
InputHostProvider,
|
|
4
5
|
shouldDeliverShortcut,
|
|
@@ -19,6 +20,7 @@ interface ShortcutEntry {
|
|
|
19
20
|
let nextShortcutOrder = 1;
|
|
20
21
|
|
|
21
22
|
export function OpenTuiInputHostProvider({ children }: { children: ReactNode }) {
|
|
23
|
+
const renderer = useNativeRenderer();
|
|
22
24
|
const shortcutsRef = useRef<ShortcutEntry[]>([]);
|
|
23
25
|
const dispatchShortcut = (event: KeyEventLike) => {
|
|
24
26
|
const shortcuts = shortcutsRef.current;
|
|
@@ -34,7 +36,9 @@ export function OpenTuiInputHostProvider({ children }: { children: ReactNode })
|
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
useKeyboard((event) => {
|
|
37
|
-
|
|
39
|
+
const shortcutEvent = toKeyEventLike(event);
|
|
40
|
+
shortcutEvent.targetEditable = renderer.currentFocusedEditor != null;
|
|
41
|
+
dispatchShortcut(shortcutEvent);
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
const host = useMemo<InputHost>(() => ({
|
|
@@ -450,78 +450,6 @@ describe("GloomberbCloudProvider", () => {
|
|
|
450
450
|
expect(unsubscribeCalled).toBe(true);
|
|
451
451
|
});
|
|
452
452
|
|
|
453
|
-
test("maps backend news stories without requiring verified market-data access", async () => {
|
|
454
|
-
type CloudNewsRequest = NonNullable<Parameters<typeof apiClient.getCloudNews>[0]>;
|
|
455
|
-
let verifyCalled = false;
|
|
456
|
-
const requestArgs: { current: CloudNewsRequest | null } = { current: null };
|
|
457
|
-
apiClient.ensureVerifiedSession = async () => {
|
|
458
|
-
verifyCalled = true;
|
|
459
|
-
return verifiedUser;
|
|
460
|
-
};
|
|
461
|
-
apiClient.getCloudNews = async (params = {}) => {
|
|
462
|
-
requestArgs.current = params;
|
|
463
|
-
return {
|
|
464
|
-
items: [{
|
|
465
|
-
id: "story-1",
|
|
466
|
-
headline: "Apple raises guidance",
|
|
467
|
-
summary: "Apple lifted its outlook after stronger iPhone demand.",
|
|
468
|
-
topic: "guidance",
|
|
469
|
-
topics: ["guidance"],
|
|
470
|
-
category: "guidance",
|
|
471
|
-
sentiment: "positive",
|
|
472
|
-
sectors: ["information_technology"],
|
|
473
|
-
firstPublishedAt: "2026-04-01T10:00:00.000Z",
|
|
474
|
-
lastPublishedAt: "2026-04-01T10:05:00.000Z",
|
|
475
|
-
firstSeenAt: "2026-04-01T10:00:10.000Z",
|
|
476
|
-
lastSeenAt: "2026-04-01T10:05:10.000Z",
|
|
477
|
-
primaryUrl: "https://example.com/aapl-guidance",
|
|
478
|
-
primarySource: "example-wire",
|
|
479
|
-
scores: {
|
|
480
|
-
importance: 91,
|
|
481
|
-
urgency: 74,
|
|
482
|
-
marketImpact: 88,
|
|
483
|
-
novelty: 86,
|
|
484
|
-
confidence: 95,
|
|
485
|
-
},
|
|
486
|
-
flags: {
|
|
487
|
-
breaking: true,
|
|
488
|
-
developing: false,
|
|
489
|
-
stale: false,
|
|
490
|
-
},
|
|
491
|
-
variantCount: 1,
|
|
492
|
-
sourceCount: 1,
|
|
493
|
-
sources: ["example-wire"],
|
|
494
|
-
entities: [],
|
|
495
|
-
tickerLinks: [{
|
|
496
|
-
symbol: "AAPL",
|
|
497
|
-
exchange: "XNAS",
|
|
498
|
-
canonicalTicker: "AAPL:XNAS",
|
|
499
|
-
relationType: "direct",
|
|
500
|
-
displayTier: "primary",
|
|
501
|
-
confidence: 0.98,
|
|
502
|
-
relevanceScore: 95,
|
|
503
|
-
impactScore: 93,
|
|
504
|
-
sentiment: "positive",
|
|
505
|
-
}],
|
|
506
|
-
}],
|
|
507
|
-
nextCursor: null,
|
|
508
|
-
};
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
const provider = new GloomberbCloudProvider();
|
|
512
|
-
const news = await provider.getNews("AAPL", 10, "NASDAQ");
|
|
513
|
-
|
|
514
|
-
expect(verifyCalled).toBe(false);
|
|
515
|
-
expect(requestArgs.current).toEqual({ feed: "ticker", ticker: "AAPL", exchange: "XNAS", tickerTier: "primary", limit: 10 });
|
|
516
|
-
expect(news).toEqual([{
|
|
517
|
-
title: "Apple raises guidance",
|
|
518
|
-
url: "https://example.com/aapl-guidance",
|
|
519
|
-
source: "example-wire",
|
|
520
|
-
publishedAt: new Date("2026-04-01T10:05:00.000Z"),
|
|
521
|
-
summary: "Apple lifted its outlook after stronger iPhone demand.",
|
|
522
|
-
}]);
|
|
523
|
-
});
|
|
524
|
-
|
|
525
453
|
test("maps news ticker labels from validated story links only", async () => {
|
|
526
454
|
apiClient.getCloudNews = async () => ({
|
|
527
455
|
items: [{
|
|
@@ -9,11 +9,9 @@ import type {
|
|
|
9
9
|
AssetDataProvider,
|
|
10
10
|
CachedFinancialsTarget,
|
|
11
11
|
MarketDataRequestContext,
|
|
12
|
-
NewsItem,
|
|
13
12
|
QuoteBatchResult,
|
|
14
13
|
QuoteSubscriptionTarget,
|
|
15
14
|
SearchRequestContext,
|
|
16
|
-
SecFilingItem,
|
|
17
15
|
TickerFinancialsBatchResult,
|
|
18
16
|
} from "../../types/data-provider";
|
|
19
17
|
import type { AnalystResearchData, CorporateActionsData, HolderData, OptionsChain, PricePoint, Quote, TickerFinancials } from "../../types/financials";
|
|
@@ -27,12 +25,11 @@ import {
|
|
|
27
25
|
} from "../../api-client";
|
|
28
26
|
import type { NewsArticle, NewsQuery } from "../../types/news-source";
|
|
29
27
|
import { resolvePriceHistoryCurrencyUnit } from "../../utils/currency-units";
|
|
30
|
-
import { canonicalTickerKey
|
|
28
|
+
import { canonicalTickerKey } from "../../utils/exchanges";
|
|
31
29
|
import { createProviderMiss } from "../provider-errors";
|
|
32
30
|
import {
|
|
33
31
|
cloudNewsParams,
|
|
34
32
|
mapCloudNewsArticle,
|
|
35
|
-
mapCloudNewsItem,
|
|
36
33
|
} from "./news";
|
|
37
34
|
import {
|
|
38
35
|
GLOOMBERB_CLOUD_PROVIDER_ID,
|
|
@@ -237,20 +234,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
237
234
|
);
|
|
238
235
|
}
|
|
239
236
|
|
|
240
|
-
async getNews(ticker: string, count?: number, exchange?: string, _context?: MarketDataRequestContext): Promise<NewsItem[]> {
|
|
241
|
-
const response = await withCloudFallback(
|
|
242
|
-
() => apiClient.getCloudNews({
|
|
243
|
-
feed: "ticker",
|
|
244
|
-
ticker,
|
|
245
|
-
exchange: exchange ? publicExchange(exchange) : undefined,
|
|
246
|
-
tickerTier: "primary",
|
|
247
|
-
limit: count,
|
|
248
|
-
}),
|
|
249
|
-
`Cloud news is unavailable for ${ticker}`,
|
|
250
|
-
);
|
|
251
|
-
return response.items.map(mapCloudNewsItem);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
237
|
async getHolders(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<HolderData> {
|
|
255
238
|
await requireVerifiedSession();
|
|
256
239
|
return withCloudFallback(async () => {
|
|
@@ -275,18 +258,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
275
258
|
}, `Cloud corporate actions are unavailable for ${ticker}`);
|
|
276
259
|
}
|
|
277
260
|
|
|
278
|
-
async getSecFilings(_ticker: string, _count?: number, _exchange?: string, _context?: MarketDataRequestContext): Promise<SecFilingItem[]> {
|
|
279
|
-
throw createProviderMiss("Cloud SEC filings are not available");
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async getSecFilingDocuments(_filing: SecFilingItem) {
|
|
283
|
-
throw createProviderMiss("Cloud filing documents are not available");
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async getSecFilingContent(_filing: SecFilingItem): Promise<string | null> {
|
|
287
|
-
throw createProviderMiss("Cloud filing content is not available");
|
|
288
|
-
}
|
|
289
|
-
|
|
290
261
|
async getArticleSummary(_url: string): Promise<string | null> {
|
|
291
262
|
throw createProviderMiss("Cloud article summaries are not available");
|
|
292
263
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { collectNewsDisplayTickers } from "../../news/ticker-symbols";
|
|
2
|
-
import type { NewsItem } from "../../types/data-provider";
|
|
3
2
|
import type { NewsArticle, NewsQuery, NewsStoryItem } from "../../types/news-source";
|
|
4
3
|
import type {
|
|
5
4
|
CloudNewsPayload,
|
|
@@ -90,17 +89,6 @@ export function mapCloudNewsArticle(
|
|
|
90
89
|
};
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
export function mapCloudNewsItem(item: CloudNewsPayload): NewsItem {
|
|
94
|
-
const article = mapCloudNewsArticle(item);
|
|
95
|
-
return {
|
|
96
|
-
title: article.title,
|
|
97
|
-
url: article.url,
|
|
98
|
-
source: article.source,
|
|
99
|
-
publishedAt: article.publishedAt,
|
|
100
|
-
summary: article.summary,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
92
|
export function cloudNewsParams(query: NewsQuery): CloudNewsParams {
|
|
105
93
|
const feed = query.feed ?? (query.scope === "ticker" ? "ticker" : "latest");
|
|
106
94
|
return {
|
|
@@ -43,6 +43,19 @@ export interface ProviderRouterFinancialRouteDeps extends Pick<
|
|
|
43
43
|
primaryRoutes: ProviderRouterPrimaryRoutes;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function withoutSessionState(quote: Quote): Quote {
|
|
47
|
+
const fallback = { ...quote };
|
|
48
|
+
delete fallback.marketState;
|
|
49
|
+
delete fallback.sessionConfidence;
|
|
50
|
+
delete fallback.preMarketPrice;
|
|
51
|
+
delete fallback.preMarketChange;
|
|
52
|
+
delete fallback.preMarketChangePercent;
|
|
53
|
+
delete fallback.postMarketPrice;
|
|
54
|
+
delete fallback.postMarketChange;
|
|
55
|
+
delete fallback.postMarketChangePercent;
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
export class ProviderRouterFinancialRoutes {
|
|
47
60
|
constructor(private readonly deps: ProviderRouterFinancialRouteDeps) {}
|
|
48
61
|
|
|
@@ -169,16 +182,21 @@ export class ProviderRouterFinancialRoutes {
|
|
|
169
182
|
exchange?: string,
|
|
170
183
|
context?: MarketDataRequestContext,
|
|
171
184
|
): Promise<Quote | null> {
|
|
172
|
-
const cached =
|
|
185
|
+
const cached = context?.cacheMode === "refresh"
|
|
186
|
+
? null
|
|
187
|
+
: this.readCachedProviderQuote(ticker, exchange, context);
|
|
173
188
|
if (cached) return cached;
|
|
189
|
+
const staleFallback = this.readCachedProviderQuote(ticker, exchange, context, true);
|
|
174
190
|
const providerQuote = await this.deps.primaryRoutes.fetchProviderQuote(ticker, exchange, context);
|
|
175
|
-
return providerQuote
|
|
191
|
+
if (providerQuote) return providerQuote.value;
|
|
192
|
+
return staleFallback ? withoutSessionState(staleFallback) : null;
|
|
176
193
|
}
|
|
177
194
|
|
|
178
195
|
private readCachedProviderQuote(
|
|
179
196
|
ticker: string,
|
|
180
197
|
exchange?: string,
|
|
181
198
|
context?: MarketDataRequestContext,
|
|
199
|
+
includeStale = false,
|
|
182
200
|
): Quote | null {
|
|
183
201
|
const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
|
|
184
202
|
const entityKeys = [...new Set([entityKey, normalizeTicker(ticker)])];
|
|
@@ -186,7 +204,11 @@ export class ProviderRouterFinancialRoutes {
|
|
|
186
204
|
const sourceKeys = this.deps.getProviderSourceKeys();
|
|
187
205
|
for (const candidateEntityKey of entityKeys) {
|
|
188
206
|
const record = selectCachedResource<Quote>(this.deps.resources, "quote", candidateEntityKey, variantKeys, sourceKeys, false);
|
|
189
|
-
if (
|
|
207
|
+
if (
|
|
208
|
+
record
|
|
209
|
+
&& (includeStale || !record.stale)
|
|
210
|
+
&& !isQuoteStaleForCurrentSession(quoteWithFreshnessExchange(record.value, exchange))
|
|
211
|
+
) {
|
|
190
212
|
return record.value;
|
|
191
213
|
}
|
|
192
214
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { CloudApiRequestTransport } from "../../api-client/request";
|
|
2
3
|
import { AppPersistence } from "../../data/app-persistence";
|
|
3
4
|
import { AssetDataRouter } from "./index";
|
|
4
5
|
import { assetDataProvider } from "../../capabilities";
|
|
@@ -26,6 +27,59 @@ afterEach(() => {
|
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
describe("AssetDataRouter", () => {
|
|
30
|
+
test("falls through to Yahoo quotes and history when Cloud market requests never settle", async () => {
|
|
31
|
+
const cloudCalls = { quote: 0, history: 0 };
|
|
32
|
+
const yahooCalls = { quote: 0, history: 0 };
|
|
33
|
+
const cloudTransport = new CloudApiRequestTransport({
|
|
34
|
+
marketRequestTimeoutMs: 10,
|
|
35
|
+
fetchTransport: async () => new Promise<Response>(() => {}),
|
|
36
|
+
});
|
|
37
|
+
const cloudProvider: DataProvider = {
|
|
38
|
+
...fallbackProvider,
|
|
39
|
+
id: "gloomberb-cloud",
|
|
40
|
+
name: "Cloud",
|
|
41
|
+
priority: 100,
|
|
42
|
+
async getQuote() {
|
|
43
|
+
cloudCalls.quote += 1;
|
|
44
|
+
await cloudTransport.request("/market/quote?symbol=AAPL");
|
|
45
|
+
throw new Error("unreachable");
|
|
46
|
+
},
|
|
47
|
+
async getPriceHistory() {
|
|
48
|
+
cloudCalls.history += 1;
|
|
49
|
+
await cloudTransport.request("/market/history?symbol=AAPL&range=1Y");
|
|
50
|
+
throw new Error("unreachable");
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const yahooProvider: DataProvider = {
|
|
54
|
+
...fallbackProvider,
|
|
55
|
+
id: "yahoo",
|
|
56
|
+
name: "Yahoo",
|
|
57
|
+
priority: 1000,
|
|
58
|
+
async getQuote() {
|
|
59
|
+
yahooCalls.quote += 1;
|
|
60
|
+
return makeQuote({ providerId: "yahoo", price: 212.5 });
|
|
61
|
+
},
|
|
62
|
+
async getPriceHistory() {
|
|
63
|
+
yahooCalls.history += 1;
|
|
64
|
+
return [
|
|
65
|
+
{ date: new Date("2026-07-10T00:00:00Z"), close: 210 },
|
|
66
|
+
{ date: new Date("2026-07-11T00:00:00Z"), close: 212.5 },
|
|
67
|
+
];
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
const router = new AssetDataRouter(yahooProvider, [cloudProvider]);
|
|
71
|
+
console.error = () => {};
|
|
72
|
+
|
|
73
|
+
const quote = await router.getQuote("AAPL", "NASDAQ");
|
|
74
|
+
const history = await router.getPriceHistory("AAPL", "NASDAQ", "1Y");
|
|
75
|
+
|
|
76
|
+
expect(quote.providerId).toBe("yahoo");
|
|
77
|
+
expect(quote.price).toBe(212.5);
|
|
78
|
+
expect(history.map((point) => point.close)).toEqual([210, 212.5]);
|
|
79
|
+
expect(cloudCalls).toEqual({ quote: 1, history: 1 });
|
|
80
|
+
expect(yahooCalls).toEqual({ quote: 1, history: 1 });
|
|
81
|
+
});
|
|
82
|
+
|
|
29
83
|
test("serves USD exchange rate locally without provider revalidation", async () => {
|
|
30
84
|
let providerCalls = 0;
|
|
31
85
|
const router = new AssetDataRouter({
|
|
@@ -367,6 +421,119 @@ describe("AssetDataRouter", () => {
|
|
|
367
421
|
expect(quote.provenance?.fields?.previousClose?.providerId).toBe("yahoo");
|
|
368
422
|
});
|
|
369
423
|
|
|
424
|
+
test("refreshes broker option session references across the market close", async () => {
|
|
425
|
+
const dbPath = createTempDbPath("option-session-transition");
|
|
426
|
+
const persistence = new AppPersistence(dbPath);
|
|
427
|
+
const optionSymbol = "IBIT 281215C00030000";
|
|
428
|
+
const entityKey = `contract:${optionSymbol}`;
|
|
429
|
+
const providerSourceKey = "provider:yahoo";
|
|
430
|
+
const cacheKey = {
|
|
431
|
+
namespace: "market",
|
|
432
|
+
kind: "quote",
|
|
433
|
+
entityKey,
|
|
434
|
+
variantKey: "",
|
|
435
|
+
sourceKey: providerSourceKey,
|
|
436
|
+
};
|
|
437
|
+
const regularReference = makeQuote({
|
|
438
|
+
symbol: optionSymbol,
|
|
439
|
+
providerId: "yahoo",
|
|
440
|
+
price: 15.775,
|
|
441
|
+
change: 0.29,
|
|
442
|
+
changePercent: 1.87,
|
|
443
|
+
previousClose: 15.485,
|
|
444
|
+
listingExchangeName: "OPTIONS",
|
|
445
|
+
marketState: "REGULAR",
|
|
446
|
+
sessionConfidence: "derived",
|
|
447
|
+
});
|
|
448
|
+
let providerCalls = 0;
|
|
449
|
+
let providerAvailable = true;
|
|
450
|
+
const provider: DataProvider = {
|
|
451
|
+
...fallbackProvider,
|
|
452
|
+
id: "yahoo",
|
|
453
|
+
name: "Yahoo",
|
|
454
|
+
async getQuote() {
|
|
455
|
+
providerCalls += 1;
|
|
456
|
+
if (!providerAvailable) throw new Error("temporary provider outage");
|
|
457
|
+
return {
|
|
458
|
+
...regularReference,
|
|
459
|
+
marketState: "CLOSED",
|
|
460
|
+
lastUpdated: Date.now(),
|
|
461
|
+
};
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
const broker: BrokerAdapter = {
|
|
465
|
+
id: "ibkr",
|
|
466
|
+
name: "IBKR",
|
|
467
|
+
configSchema: [],
|
|
468
|
+
async validate() {
|
|
469
|
+
return true;
|
|
470
|
+
},
|
|
471
|
+
async importPositions() {
|
|
472
|
+
return [];
|
|
473
|
+
},
|
|
474
|
+
async getQuote() {
|
|
475
|
+
return {
|
|
476
|
+
...regularReference,
|
|
477
|
+
providerId: "ibkr",
|
|
478
|
+
previousClose: 99,
|
|
479
|
+
marketState: undefined,
|
|
480
|
+
sessionConfidence: "unknown",
|
|
481
|
+
dataSource: "live",
|
|
482
|
+
lastUpdated: Date.now(),
|
|
483
|
+
};
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
persistence.resources.set(cacheKey, regularReference, {
|
|
489
|
+
cachePolicy: { staleMs: 60_000, expireMs: 24 * 60 * 60_000 },
|
|
490
|
+
fetchedAt: Date.now() - 2 * 60_000,
|
|
491
|
+
});
|
|
492
|
+
const router = new AssetDataRouter(provider, [], persistence.resources);
|
|
493
|
+
attachTestRegistry(router, { brokers: [["ibkr", broker]] });
|
|
494
|
+
setBrokerInstances(router, [brokerInstance()]);
|
|
495
|
+
const context = {
|
|
496
|
+
brokerId: "ibkr",
|
|
497
|
+
brokerInstanceId: "ibkr-work",
|
|
498
|
+
instrument: {
|
|
499
|
+
brokerId: "ibkr",
|
|
500
|
+
brokerInstanceId: "ibkr-work",
|
|
501
|
+
symbol: "IBIT",
|
|
502
|
+
localSymbol: optionSymbol,
|
|
503
|
+
secType: "OPT",
|
|
504
|
+
},
|
|
505
|
+
} as const;
|
|
506
|
+
|
|
507
|
+
const staleTransition = await router.getQuote(optionSymbol, "", context);
|
|
508
|
+
expect(staleTransition.marketState).toBe("CLOSED");
|
|
509
|
+
expect(providerCalls).toBe(1);
|
|
510
|
+
|
|
511
|
+
persistence.resources.set(cacheKey, regularReference, {
|
|
512
|
+
cachePolicy: { staleMs: 60_000, expireMs: 24 * 60 * 60_000 },
|
|
513
|
+
fetchedAt: Date.now(),
|
|
514
|
+
});
|
|
515
|
+
const forcedTransition = await router.getQuote(optionSymbol, "", {
|
|
516
|
+
...context,
|
|
517
|
+
cacheMode: "refresh",
|
|
518
|
+
});
|
|
519
|
+
expect(forcedTransition.marketState).toBe("CLOSED");
|
|
520
|
+
expect(providerCalls).toBe(2);
|
|
521
|
+
|
|
522
|
+
providerAvailable = false;
|
|
523
|
+
persistence.resources.set(cacheKey, regularReference, {
|
|
524
|
+
cachePolicy: { staleMs: 60_000, expireMs: 24 * 60 * 60_000 },
|
|
525
|
+
fetchedAt: Date.now() - 2 * 60_000,
|
|
526
|
+
});
|
|
527
|
+
const outageFallback = await router.getQuote(optionSymbol, "", context);
|
|
528
|
+
expect(outageFallback.previousClose).toBe(15.485);
|
|
529
|
+
expect(outageFallback.provenance?.fields?.previousClose?.providerId).toBe("yahoo");
|
|
530
|
+
expect(outageFallback.marketState).toBeUndefined();
|
|
531
|
+
expect(providerCalls).toBe(3);
|
|
532
|
+
} finally {
|
|
533
|
+
persistence.close();
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
370
537
|
test("prefers broker options chains for broker-context targets", async () => {
|
|
371
538
|
const chain = {
|
|
372
539
|
underlyingSymbol: "AAPL",
|
|
@@ -4,6 +4,7 @@ import type { BrokerCandidate } from "./brokers";
|
|
|
4
4
|
import { shouldLogProviderError } from "../provider-errors";
|
|
5
5
|
|
|
6
6
|
const SEARCH_CACHE_TTL_MS = 30_000;
|
|
7
|
+
const SEARCH_CACHE_MAX_ENTRIES = 100;
|
|
7
8
|
|
|
8
9
|
const SEARCH_PROVIDER_TIMEOUT_MS = 5_000;
|
|
9
10
|
|
|
@@ -88,6 +89,7 @@ export class ProviderRouterSearchRoutes {
|
|
|
88
89
|
const cacheKey = buildSearchCacheKey(query, context);
|
|
89
90
|
const cached = this.searchCache.get(cacheKey);
|
|
90
91
|
if (cached && cached.expiresAt > Date.now()) return cached.results;
|
|
92
|
+
if (cached) this.searchCache.delete(cacheKey);
|
|
91
93
|
const inFlight = this.searchInFlight.get(cacheKey);
|
|
92
94
|
if (inFlight) return inFlight;
|
|
93
95
|
|
|
@@ -111,6 +113,9 @@ export class ProviderRouterSearchRoutes {
|
|
|
111
113
|
expiresAt: Date.now() + ttl,
|
|
112
114
|
results,
|
|
113
115
|
});
|
|
116
|
+
if (this.searchCache.size > SEARCH_CACHE_MAX_ENTRIES) {
|
|
117
|
+
this.searchCache.delete(this.searchCache.keys().next().value!);
|
|
118
|
+
}
|
|
114
119
|
};
|
|
115
120
|
const push = (items: InstrumentSearchResult[]) => {
|
|
116
121
|
mergeSearchResults(results, resultIndexByKey, items, context);
|
|
@@ -57,7 +57,7 @@ export class ProviderRouterSupplementalRoutes {
|
|
|
57
57
|
|
|
58
58
|
const result = await firstProviderResult(this.deps, async (provider) => {
|
|
59
59
|
const rate = await provider.getExchangeRate(normalizedCurrency);
|
|
60
|
-
return { provider, rate };
|
|
60
|
+
return Number.isFinite(rate) && rate > 0 ? { provider, rate } : null;
|
|
61
61
|
});
|
|
62
62
|
if (!result) {
|
|
63
63
|
throw new Error(`No exchange rate provider available for ${normalizedCurrency}`);
|
|
@@ -164,7 +164,8 @@ export class ProviderRouterSupplementalRoutes {
|
|
|
164
164
|
if (normalizedCurrency === "USD") return 1;
|
|
165
165
|
const entityKey = `${normalizedCurrency}/USD`;
|
|
166
166
|
const cached = selectCachedResource<{ rate: number }>(this.deps.resources, "exchange-rate", entityKey, [""], this.deps.getProviderSourceKeys(), allowExpired);
|
|
167
|
-
|
|
167
|
+
const rate = cached?.value.rate;
|
|
168
|
+
return typeof rate === "number" && Number.isFinite(rate) && rate > 0 ? rate : null;
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
private cacheExchangeRate(currency: string, rate: number, provider: DataProvider): void {
|
|
@@ -341,7 +342,7 @@ export class ProviderRouterSupplementalRoutes {
|
|
|
341
342
|
|
|
342
343
|
const result = await firstProviderResult(this.deps, async (provider) => {
|
|
343
344
|
const rate = await provider.getExchangeRate(normalizedCurrency);
|
|
344
|
-
return { provider, rate };
|
|
345
|
+
return Number.isFinite(rate) && rate > 0 ? { provider, rate } : null;
|
|
345
346
|
});
|
|
346
347
|
if (!result) return;
|
|
347
348
|
this.cacheExchangeRate(normalizedCurrency, result.value.rate, result.value.provider);
|
|
@@ -33,7 +33,11 @@ type YahooChartFetcher = (
|
|
|
33
33
|
symbol: string,
|
|
34
34
|
range: string,
|
|
35
35
|
interval: ManualChartResolution,
|
|
36
|
-
) => Promise<
|
|
36
|
+
) => Promise<{
|
|
37
|
+
meta: NonNullable<ChartResult["meta"]>;
|
|
38
|
+
history: PricePoint[];
|
|
39
|
+
events?: ChartResult["events"];
|
|
40
|
+
}>;
|
|
37
41
|
|
|
38
42
|
export function getYahooChartResolutionSupport(): ChartResolutionSupport[] {
|
|
39
43
|
return YAHOO_RESOLUTION_SUPPORT;
|