gloomberb 0.9.7 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/auth.ts +20 -0
- package/src/api-client/index.test.ts +3 -3
- package/src/api-client/index.ts +48 -6
- package/src/api-client/request.test.ts +63 -0
- package/src/api-client/request.ts +53 -5
- package/src/api-client/types.ts +45 -2
- package/src/app/pane-runtime/layout-placement.ts +0 -1
- package/src/app/runtime/desktop-deeplink.test.ts +87 -0
- package/src/app/runtime/desktop-deeplink.ts +496 -0
- package/src/app.tsx +34 -0
- package/src/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/bar-chart-renderer.test.ts +87 -12
- package/src/components/chart/bar-chart-renderer.ts +111 -55
- 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/static/bar-chart-surface.tsx +104 -20
- package/src/components/chart/stock-chart/data-runtime.ts +0 -1
- package/src/components/chart/stock-chart/footer.test.ts +65 -0
- package/src/components/chart/stock-chart/footer.ts +68 -13
- package/src/components/chart/stock-chart/resolved.tsx +18 -4
- package/src/components/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/pane-templates/items.ts +11 -3
- package/src/components/command-bar/panel/runtime.ts +0 -2
- package/src/components/command-bar/routes/root/results.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
- package/src/components/command-bar/surface/index.test.tsx +52 -16
- package/src/components/command-bar/surface/index.tsx +0 -1
- package/src/components/command-bar/workflow/ops.ts +81 -20
- 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/index.ts +1 -0
- 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/toggle-list.tsx +29 -73
- package/src/components/ui/button.tsx +4 -1
- package/src/components/ui/checkbox.tsx +76 -0
- package/src/components/ui/choice-dialog.tsx +15 -3
- package/src/components/ui/frame.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/message-composer.tsx +4 -0
- package/src/components/ui/native-select.tsx +4 -2
- package/src/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/events.test.ts +49 -0
- package/src/market-data/coordinator/events.ts +25 -4
- package/src/market-data/coordinator/index.test.ts +223 -10
- package/src/market-data/coordinator/index.ts +41 -21
- package/src/market-data/coordinator/quotes.ts +13 -11
- package/src/market-data/coordinator/subscriptions.test.ts +1 -0
- package/src/market-data/hooks.tsx +17 -13
- package/src/market-data/market/freshness.ts +58 -1
- package/src/market-data/market/status.ts +19 -1
- package/src/market-data/query-store.ts +0 -4
- package/src/market-data/quotes/freshness.test.ts +49 -1
- package/src/market-data/quotes/freshness.ts +8 -0
- package/src/market-data/quotes/resolution.test.ts +185 -0
- package/src/market-data/quotes/resolution.ts +96 -10
- package/src/market-data/request-types.test.ts +61 -1
- package/src/market-data/request-types.ts +42 -8
- 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/footer.ts +2 -5
- package/src/plugins/builtin/account-management/form-components.tsx +175 -59
- package/src/plugins/builtin/account-management/keyboard.ts +32 -2
- package/src/plugins/builtin/account-management/model.test.ts +99 -4
- package/src/plugins/builtin/account-management/model.ts +259 -23
- package/src/plugins/builtin/account-management/pane.tsx +784 -155
- 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 +127 -1
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
- package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
- package/src/plugins/builtin/chat/content/composer.tsx +84 -1
- package/src/plugins/builtin/chat/content/index.tsx +153 -16
- package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
- package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
- package/src/plugins/builtin/chat/content/mentions.ts +88 -0
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
- package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
- package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
- 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/message/profile-popover.test.tsx +36 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
- package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
- package/src/plugins/builtin/chat/sidebar.tsx +26 -2
- package/src/plugins/builtin/chat/test-harness.tsx +22 -12
- package/src/plugins/builtin/cloud/plugin.tsx +8 -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/correlation/index.tsx +4 -1
- package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
- package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
- package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
- package/src/plugins/builtin/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 +52 -25
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
- package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
- 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 +42 -6
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- 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/gateway/market-data.ts +12 -5
- package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
- 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/context.ts +15 -0
- package/src/plugins/registry/contributions.ts +22 -11
- package/src/plugins/registry/index.test.ts +44 -0
- package/src/plugins/registry/index.ts +75 -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/bun/index.ts +42 -0
- package/src/renderers/electrobun/shared/protocol.ts +5 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
- package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
- package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
- package/src/renderers/electrobun/view/host/box.tsx +0 -2
- package/src/renderers/electrobun/view/host/mouse.ts +0 -11
- package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
- package/src/renderers/electrobun/view/host/text.tsx +1 -2
- package/src/renderers/electrobun/view/http-fetch.ts +15 -2
- package/src/renderers/electrobun/view/main.tsx +3 -0
- 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/electrobun/view/ui-host.tsx +2 -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/batches.ts +11 -3
- package/src/sources/provider-router/cached-financials.test.ts +123 -0
- package/src/sources/provider-router/financial-routes.ts +77 -5
- package/src/sources/provider-router/financials.test.ts +49 -0
- package/src/sources/provider-router/financials.ts +40 -3
- package/src/sources/provider-router/index.test.ts +314 -0
- package/src/sources/provider-router/primary.ts +17 -3
- package/src/sources/provider-router/search.ts +5 -0
- package/src/sources/provider-router/streaming.ts +6 -1
- package/src/sources/provider-router/supplemental.ts +4 -3
- package/src/sources/sec-edgar.test.ts +42 -0
- package/src/sources/sec-edgar.ts +1 -2
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/sources/yahoo-finance/mappers.test.ts +29 -0
- package/src/sources/yahoo-finance/mappers.ts +2 -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/requests.ts +2 -1
- package/src/sources/yahoo-finance/snapshots.ts +13 -2
- package/src/sources/yahoo-finance.test.ts +40 -21
- package/src/sources/yahoo-finance.ts +19 -10
- package/src/state/app/context/index.test.tsx +0 -81
- package/src/state/app/context/selector.test.tsx +6 -2
- package/src/state/hooks/quote-streaming.test.ts +14 -1
- package/src/state/hooks/quote-streaming.ts +5 -4
- package/src/sync/controller.test.ts +31 -0
- package/src/sync/controller.ts +300 -0
- package/src/sync/core-contributors.test.ts +251 -0
- package/src/sync/core-contributors.ts +400 -0
- package/src/sync/profile-analytics.ts +39 -0
- package/src/sync/react.ts +53 -0
- package/src/sync/types.ts +77 -0
- package/src/test-support/data-provider.ts +0 -1
- package/src/types/desktop-deeplink.ts +7 -0
- package/src/types/plugin.ts +3 -0
- package/src/ui/host.tsx +15 -1
- package/src/ui/index.tsx +2 -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/exchanges.test.ts +11 -1
- package/src/utils/exchanges.ts +5 -1
- 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
|
@@ -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 {
|
|
@@ -12,8 +12,10 @@ import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshnes
|
|
|
12
12
|
import { resolveTickerFinancialsQuoteState } from "../../market-data/quotes/resolution";
|
|
13
13
|
import { selectCachedResource } from "./cache";
|
|
14
14
|
import {
|
|
15
|
+
dropUnusableProviderQuote,
|
|
15
16
|
hasDeepStatementHistory,
|
|
16
17
|
hasDetailedStatementRows,
|
|
18
|
+
isProviderQuoteUsableForCurrentSession,
|
|
17
19
|
quoteWithFreshnessExchange,
|
|
18
20
|
type CachedFinancialsSelection,
|
|
19
21
|
} from "./financials";
|
|
@@ -52,8 +54,9 @@ export class ProviderRouterBatchRoutes {
|
|
|
52
54
|
const context = target.context;
|
|
53
55
|
const entityKey = this.deps.getEntityKey(target.symbol, context?.instrument);
|
|
54
56
|
const variantKeys = this.deps.getTickerVariantCandidates(target.exchange);
|
|
57
|
+
const brokerSourceKeys = this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate));
|
|
55
58
|
const sourceKeys = [
|
|
56
|
-
...
|
|
59
|
+
...brokerSourceKeys,
|
|
57
60
|
...this.deps.getProviderSourceKeys(),
|
|
58
61
|
];
|
|
59
62
|
const rawCached = selectCachedResource<Quote>(this.deps.resources, "quote", entityKey, variantKeys, sourceKeys, false);
|
|
@@ -61,6 +64,10 @@ export class ProviderRouterBatchRoutes {
|
|
|
61
64
|
? rawCached
|
|
62
65
|
: null;
|
|
63
66
|
if (cached && !forceRefresh && !cached.stale) {
|
|
67
|
+
if (brokerSourceKeys.includes(cached.sourceKey)) {
|
|
68
|
+
misses.push({ index, target });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
64
71
|
results[index] = { target, quote: cached.value };
|
|
65
72
|
return;
|
|
66
73
|
}
|
|
@@ -80,7 +87,7 @@ export class ProviderRouterBatchRoutes {
|
|
|
80
87
|
const uniqueTargets = [...providerIndexes.values()].map((bucket) => bucket[0]!.target);
|
|
81
88
|
const batchResults = await batchProvider.getQuotesBatch!(uniqueTargets, options).catch(() => []);
|
|
82
89
|
for (const item of batchResults) {
|
|
83
|
-
if (!item.quote
|
|
90
|
+
if (!isProviderQuoteUsableForCurrentSession(item.quote, item.target.exchange)) continue;
|
|
84
91
|
const key = this.quoteBatchKey(item.target);
|
|
85
92
|
const sourceKey = this.deps.providerSourceKey(batchProvider);
|
|
86
93
|
for (const entry of providerIndexes.get(key) ?? []) {
|
|
@@ -142,7 +149,8 @@ export class ProviderRouterBatchRoutes {
|
|
|
142
149
|
for (const item of batchResults) {
|
|
143
150
|
if (!item.financials) continue;
|
|
144
151
|
const key = this.cachedFinancialsBatchKey(item.target);
|
|
145
|
-
|
|
152
|
+
let value = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(item.financials));
|
|
153
|
+
if (value) value = dropUnusableProviderQuote(value, item.target.exchange);
|
|
146
154
|
if (!value) continue;
|
|
147
155
|
const sourceKey = this.deps.providerSourceKey(batchProvider);
|
|
148
156
|
for (const entry of providerIndexes.get(key) ?? []) {
|
|
@@ -405,6 +405,129 @@ describe("AssetDataRouter cached financials", () => {
|
|
|
405
405
|
persistence.close();
|
|
406
406
|
});
|
|
407
407
|
|
|
408
|
+
test("uses symbol provider quote reference for broker-linked snapshot reads", async () => {
|
|
409
|
+
const dbPath = createTempDbPath("cached-symbol-reference-for-contract-snapshot");
|
|
410
|
+
const persistence = new AppPersistence(dbPath);
|
|
411
|
+
const now = Date.now();
|
|
412
|
+
|
|
413
|
+
persistence.resources.set(
|
|
414
|
+
{
|
|
415
|
+
namespace: "market",
|
|
416
|
+
kind: "financials",
|
|
417
|
+
entityKey: "contract:275759",
|
|
418
|
+
variantKey: "exchange=NASDAQ",
|
|
419
|
+
sourceKey: "provider:gloomberb-cloud",
|
|
420
|
+
},
|
|
421
|
+
makeFinancials({
|
|
422
|
+
quote: makeQuote({
|
|
423
|
+
symbol: "VICR",
|
|
424
|
+
providerId: "gloomberb-cloud",
|
|
425
|
+
price: 292.83,
|
|
426
|
+
currency: "USD",
|
|
427
|
+
previousClose: 380.07,
|
|
428
|
+
change: -87.24,
|
|
429
|
+
changePercent: -22.95,
|
|
430
|
+
lastUpdated: now,
|
|
431
|
+
marketState: "REGULAR",
|
|
432
|
+
exchangeName: "NASDAQ",
|
|
433
|
+
listingExchangeName: "NASDAQ",
|
|
434
|
+
dataSource: "live",
|
|
435
|
+
}),
|
|
436
|
+
profile: {
|
|
437
|
+
description: "Cloud profile",
|
|
438
|
+
},
|
|
439
|
+
fundamentals: {
|
|
440
|
+
trailingPE: 94.3,
|
|
441
|
+
},
|
|
442
|
+
}),
|
|
443
|
+
{
|
|
444
|
+
cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
|
|
445
|
+
fetchedAt: now,
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
persistence.resources.set(
|
|
449
|
+
{
|
|
450
|
+
namespace: "market",
|
|
451
|
+
kind: "financials",
|
|
452
|
+
entityKey: "contract:275759",
|
|
453
|
+
variantKey: "exchange=NASDAQ",
|
|
454
|
+
sourceKey: "provider:yahoo",
|
|
455
|
+
},
|
|
456
|
+
makeFinancials({
|
|
457
|
+
annualStatements: [{ date: "2025-12-31", totalRevenue: 100 }],
|
|
458
|
+
}),
|
|
459
|
+
{
|
|
460
|
+
cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
|
|
461
|
+
fetchedAt: now + 1_000,
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
persistence.resources.set(
|
|
465
|
+
{
|
|
466
|
+
namespace: "market",
|
|
467
|
+
kind: "financials",
|
|
468
|
+
entityKey: "VICR",
|
|
469
|
+
variantKey: "exchange=NASDAQ",
|
|
470
|
+
sourceKey: "provider:yahoo",
|
|
471
|
+
},
|
|
472
|
+
makeFinancials({
|
|
473
|
+
quote: makeQuote({
|
|
474
|
+
symbol: "VICR",
|
|
475
|
+
providerId: "yahoo",
|
|
476
|
+
price: 294.39,
|
|
477
|
+
currency: "USD",
|
|
478
|
+
previousClose: 282.95,
|
|
479
|
+
change: 11.44,
|
|
480
|
+
changePercent: 4.04,
|
|
481
|
+
lastUpdated: now - 1_000,
|
|
482
|
+
marketState: "REGULAR",
|
|
483
|
+
exchangeName: "NASDAQ",
|
|
484
|
+
listingExchangeName: "NASDAQ",
|
|
485
|
+
dataSource: "delayed",
|
|
486
|
+
}),
|
|
487
|
+
}),
|
|
488
|
+
{
|
|
489
|
+
cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
|
|
490
|
+
fetchedAt: now - 1_000,
|
|
491
|
+
},
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
const router = new AssetDataRouter({
|
|
495
|
+
...fallbackProvider,
|
|
496
|
+
id: "yahoo",
|
|
497
|
+
name: "Yahoo",
|
|
498
|
+
async getTickerFinancials() {
|
|
499
|
+
throw new Error("should not fetch financials");
|
|
500
|
+
},
|
|
501
|
+
}, [{
|
|
502
|
+
...fallbackProvider,
|
|
503
|
+
id: "gloomberb-cloud",
|
|
504
|
+
name: "Cloud",
|
|
505
|
+
priority: 100,
|
|
506
|
+
async getTickerFinancials() {
|
|
507
|
+
throw new Error("should not fetch financials");
|
|
508
|
+
},
|
|
509
|
+
}], persistence.resources);
|
|
510
|
+
|
|
511
|
+
const financials = await router.getTickerFinancials("VICR", "NASDAQ", {
|
|
512
|
+
brokerId: "ibkr",
|
|
513
|
+
brokerInstanceId: "ibkr-work",
|
|
514
|
+
instrument: {
|
|
515
|
+
brokerId: "ibkr",
|
|
516
|
+
brokerInstanceId: "ibkr-work",
|
|
517
|
+
conId: 275759,
|
|
518
|
+
symbol: "VICR",
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
expect(financials.quote?.providerId).toBe("gloomberb-cloud");
|
|
523
|
+
expect(financials.quote?.price).toBe(292.83);
|
|
524
|
+
expect(financials.quote?.previousClose).toBe(282.95);
|
|
525
|
+
expect(financials.quote?.changePercent).toBeCloseTo(((292.83 - 282.95) / 282.95) * 100, 10);
|
|
526
|
+
expect(financials.quote?.provenance?.fields?.previousClose?.providerId).toBe("yahoo");
|
|
527
|
+
|
|
528
|
+
persistence.close();
|
|
529
|
+
});
|
|
530
|
+
|
|
408
531
|
test("overlays standalone quote cache on stale financial snapshots during startup priming", () => {
|
|
409
532
|
const dbPath = createTempDbPath("cached-quote-over-financial-snapshot");
|
|
410
533
|
const persistence = new AppPersistence(dbPath);
|
|
@@ -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
|
|
|
@@ -81,7 +94,9 @@ export class ProviderRouterFinancialRoutes {
|
|
|
81
94
|
quarterlyStatements: base?.quarterlyStatements ?? [],
|
|
82
95
|
priceHistory: base?.priceHistory ?? [],
|
|
83
96
|
});
|
|
84
|
-
const cached = this.readCachedMergedFinancialsSelection(ticker, exchange, context, false
|
|
97
|
+
const cached = this.readCachedMergedFinancialsSelection(ticker, exchange, context, false, {
|
|
98
|
+
includeSymbolProviderFallback: true,
|
|
99
|
+
});
|
|
85
100
|
const forceRefresh = context?.cacheMode === "refresh";
|
|
86
101
|
if (cached.value && !forceRefresh) {
|
|
87
102
|
if (isOptionTicker && !cached.value.quote) {
|
|
@@ -128,8 +143,9 @@ export class ProviderRouterFinancialRoutes {
|
|
|
128
143
|
async getQuote(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<Quote> {
|
|
129
144
|
const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
|
|
130
145
|
const variantKeys = this.deps.getTickerVariantCandidates(exchange);
|
|
146
|
+
const brokerSourceKeys = this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate));
|
|
131
147
|
const sourceKeys = [
|
|
132
|
-
...
|
|
148
|
+
...brokerSourceKeys,
|
|
133
149
|
...this.deps.getProviderSourceKeys(),
|
|
134
150
|
];
|
|
135
151
|
const rawCached = selectCachedResource<Quote>(this.deps.resources, "quote", entityKey, variantKeys, sourceKeys, false);
|
|
@@ -138,12 +154,19 @@ export class ProviderRouterFinancialRoutes {
|
|
|
138
154
|
: null;
|
|
139
155
|
const forceRefresh = context?.cacheMode === "refresh";
|
|
140
156
|
if (cached && !forceRefresh && !cached.stale) {
|
|
141
|
-
return cached.value;
|
|
157
|
+
if (!brokerSourceKeys.includes(cached.sourceKey)) return cached.value;
|
|
158
|
+
return this.mergeBrokerQuoteWithProviderReference(
|
|
159
|
+
cached.value,
|
|
160
|
+
await this.getProviderReferenceQuote(ticker, exchange, context),
|
|
161
|
+
);
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
const brokerQuote = await withBrokerTimeout(this.deps.primaryRoutes.fetchBrokerQuote(ticker, exchange, context));
|
|
145
165
|
if (brokerQuote && !isQuoteStaleForCurrentSession(quoteWithFreshnessExchange(brokerQuote.value, exchange))) {
|
|
146
|
-
return
|
|
166
|
+
return this.mergeBrokerQuoteWithProviderReference(
|
|
167
|
+
brokerQuote.value,
|
|
168
|
+
await this.getProviderReferenceQuote(ticker, exchange, context),
|
|
169
|
+
);
|
|
147
170
|
}
|
|
148
171
|
|
|
149
172
|
const providerQuote = await this.deps.primaryRoutes.fetchProviderQuote(ticker, exchange, context);
|
|
@@ -154,6 +177,54 @@ export class ProviderRouterFinancialRoutes {
|
|
|
154
177
|
throw new Error(`No quote provider available for ${ticker}`);
|
|
155
178
|
}
|
|
156
179
|
|
|
180
|
+
private async getProviderReferenceQuote(
|
|
181
|
+
ticker: string,
|
|
182
|
+
exchange?: string,
|
|
183
|
+
context?: MarketDataRequestContext,
|
|
184
|
+
): Promise<Quote | null> {
|
|
185
|
+
const cached = context?.cacheMode === "refresh"
|
|
186
|
+
? null
|
|
187
|
+
: this.readCachedProviderQuote(ticker, exchange, context);
|
|
188
|
+
if (cached) return cached;
|
|
189
|
+
const staleFallback = this.readCachedProviderQuote(ticker, exchange, context, true);
|
|
190
|
+
const providerQuote = await this.deps.primaryRoutes.fetchProviderQuote(ticker, exchange, context);
|
|
191
|
+
if (providerQuote) return providerQuote.value;
|
|
192
|
+
return staleFallback ? withoutSessionState(staleFallback) : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private readCachedProviderQuote(
|
|
196
|
+
ticker: string,
|
|
197
|
+
exchange?: string,
|
|
198
|
+
context?: MarketDataRequestContext,
|
|
199
|
+
includeStale = false,
|
|
200
|
+
): Quote | null {
|
|
201
|
+
const entityKey = this.deps.getEntityKey(ticker, context?.instrument);
|
|
202
|
+
const entityKeys = [...new Set([entityKey, normalizeTicker(ticker)])];
|
|
203
|
+
const variantKeys = this.deps.getTickerVariantCandidates(exchange);
|
|
204
|
+
const sourceKeys = this.deps.getProviderSourceKeys();
|
|
205
|
+
for (const candidateEntityKey of entityKeys) {
|
|
206
|
+
const record = selectCachedResource<Quote>(this.deps.resources, "quote", candidateEntityKey, variantKeys, sourceKeys, false);
|
|
207
|
+
if (
|
|
208
|
+
record
|
|
209
|
+
&& (includeStale || !record.stale)
|
|
210
|
+
&& !isQuoteStaleForCurrentSession(quoteWithFreshnessExchange(record.value, exchange))
|
|
211
|
+
) {
|
|
212
|
+
return record.value;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private mergeBrokerQuoteWithProviderReference(brokerQuote: Quote, providerQuote: Quote | null): Quote {
|
|
219
|
+
if (!providerQuote) return brokerQuote;
|
|
220
|
+
return resolveTickerFinancialsQuoteState({
|
|
221
|
+
quote: providerQuote,
|
|
222
|
+
annualStatements: [],
|
|
223
|
+
quarterlyStatements: [],
|
|
224
|
+
priceHistory: [],
|
|
225
|
+
}, brokerQuote)?.quote ?? brokerQuote;
|
|
226
|
+
}
|
|
227
|
+
|
|
157
228
|
readCachedMergedFinancialsSelection(
|
|
158
229
|
ticker: string,
|
|
159
230
|
exchange?: string,
|
|
@@ -171,7 +242,8 @@ export class ProviderRouterFinancialRoutes {
|
|
|
171
242
|
? { ...brokerRecord, value: sanitizeCachedFinancials(brokerRecord.value, options) }
|
|
172
243
|
: null;
|
|
173
244
|
const providerSourceKeys = this.deps.getProviderSourceKeys();
|
|
174
|
-
const
|
|
245
|
+
const includeSymbolProviderFallback = options.includeSymbolProviderFallback !== false;
|
|
246
|
+
const providerEntityKeys = includeSymbolProviderFallback
|
|
175
247
|
? [...new Set([entityKey, normalizeTicker(ticker)])]
|
|
176
248
|
: [entityKey];
|
|
177
249
|
const providerRecords = sortCachedRecords(
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
dropUnusableProviderQuote,
|
|
4
|
+
isProviderQuoteUsableForCurrentSession,
|
|
5
|
+
} from "./financials";
|
|
6
|
+
import { makeFinancials, makeQuote } from "./test-support";
|
|
7
|
+
|
|
8
|
+
describe("provider-router financial quote usability", () => {
|
|
9
|
+
test("rejects active-session labels without active-session prices", () => {
|
|
10
|
+
expect(isProviderQuoteUsableForCurrentSession(makeQuote({
|
|
11
|
+
listingExchangeName: "NASDAQ",
|
|
12
|
+
marketState: "PRE",
|
|
13
|
+
lastUpdated: Date.now(),
|
|
14
|
+
}), "NASDAQ")).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("rejects old active-session provider quotes", () => {
|
|
18
|
+
expect(isProviderQuoteUsableForCurrentSession(makeQuote({
|
|
19
|
+
listingExchangeName: "FWB2",
|
|
20
|
+
marketState: "REGULAR",
|
|
21
|
+
lastUpdated: Date.now() - 20 * 60_000,
|
|
22
|
+
}), "FWB2")).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("rejects empty zero provider quotes", () => {
|
|
26
|
+
expect(isProviderQuoteUsableForCurrentSession(makeQuote({
|
|
27
|
+
price: 0,
|
|
28
|
+
change: 0,
|
|
29
|
+
changePercent: 0,
|
|
30
|
+
listingExchangeName: "SFB",
|
|
31
|
+
lastUpdated: Date.now(),
|
|
32
|
+
}), "SFB")).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("strips unusable quotes while preserving non-quote financials", () => {
|
|
36
|
+
const value = dropUnusableProviderQuote(makeFinancials({
|
|
37
|
+
profile: { sector: "Industrials" },
|
|
38
|
+
quote: makeQuote({
|
|
39
|
+
price: 0,
|
|
40
|
+
change: 0,
|
|
41
|
+
changePercent: 0,
|
|
42
|
+
listingExchangeName: "SFB",
|
|
43
|
+
}),
|
|
44
|
+
}), "SFB");
|
|
45
|
+
|
|
46
|
+
expect(value.profile?.sector).toBe("Industrials");
|
|
47
|
+
expect(value.quote).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -77,6 +77,46 @@ export function quoteWithFreshnessExchange(quote: Quote, exchange?: string): Quo
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function finitePositiveNumber(value: unknown): value is number {
|
|
81
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ACTIVE_PROVIDER_QUOTE_MAX_AGE_MS = 10 * 60_000;
|
|
85
|
+
const ACTIVE_QUOTE_MARKET_STATES = new Set(["PRE", "REGULAR", "POST"]);
|
|
86
|
+
|
|
87
|
+
function isActiveProviderQuoteTooOld(quote: Quote, now = Date.now()): boolean {
|
|
88
|
+
if (!ACTIVE_QUOTE_MARKET_STATES.has(quote.marketState ?? "")) return false;
|
|
89
|
+
if (!Number.isFinite(quote.lastUpdated)) return false;
|
|
90
|
+
return now - quote.lastUpdated > ACTIVE_PROVIDER_QUOTE_MAX_AGE_MS;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isProviderQuoteUsableForCurrentSession(quote: Quote | null | undefined, exchange?: string): quote is Quote {
|
|
94
|
+
if (!quote) return false;
|
|
95
|
+
const normalized = quoteWithFreshnessExchange(quote, exchange);
|
|
96
|
+
if (isQuoteStaleForCurrentSession(normalized)) return false;
|
|
97
|
+
if (isActiveProviderQuoteTooOld(normalized)) return false;
|
|
98
|
+
return [
|
|
99
|
+
normalized.price,
|
|
100
|
+
normalized.preMarketPrice,
|
|
101
|
+
normalized.postMarketPrice,
|
|
102
|
+
normalized.bid,
|
|
103
|
+
normalized.ask,
|
|
104
|
+
normalized.mark,
|
|
105
|
+
].some(finitePositiveNumber);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function dropUnusableProviderQuote(value: TickerFinancials, exchange?: string): TickerFinancials {
|
|
109
|
+
if (!value.quote || isProviderQuoteUsableForCurrentSession(value.quote, exchange)) {
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
...value,
|
|
115
|
+
quote: undefined,
|
|
116
|
+
quoteContributions: undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
80
120
|
function sanitizeCachedQuote(
|
|
81
121
|
quote: Quote,
|
|
82
122
|
exchange: string | undefined,
|
|
@@ -216,13 +256,10 @@ export function mergeCachedFinancialRecords(
|
|
|
216
256
|
value: TickerFinancials | null;
|
|
217
257
|
stale: boolean;
|
|
218
258
|
} {
|
|
219
|
-
const seenSources = new Set<string>();
|
|
220
259
|
let merged: TickerFinancials | null = null;
|
|
221
260
|
let stale = false;
|
|
222
261
|
|
|
223
262
|
for (const record of records) {
|
|
224
|
-
if (seenSources.has(record.sourceKey)) continue;
|
|
225
|
-
seenSources.add(record.sourceKey);
|
|
226
263
|
merged = mergeFinancials(merged, sanitizeCachedFinancials(record.value, options));
|
|
227
264
|
stale = stale || record.stale === true;
|
|
228
265
|
}
|