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,7 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { act, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { TestDialogProvider, testRender } from "../../renderers/opentui/test-utils";
|
|
4
|
-
import { TextAttributes } from "../../ui";
|
|
5
4
|
import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
|
|
6
5
|
import { ChoiceDialog } from "./choice-dialog";
|
|
7
6
|
import { DataTable, type DataTableColumn } from "./data-table";
|
|
@@ -171,37 +170,6 @@ function DataTableHorizontalScrollHarness({
|
|
|
171
170
|
);
|
|
172
171
|
}
|
|
173
172
|
|
|
174
|
-
function DataTableColumnGapHarness() {
|
|
175
|
-
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
176
|
-
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
177
|
-
const items = [{ id: "row-0", left: "AA", right: "BB" }];
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
<DataTableView
|
|
181
|
-
rootWidth={8}
|
|
182
|
-
rootHeight={4}
|
|
183
|
-
selection={{ kind: "none" }}
|
|
184
|
-
columns={[
|
|
185
|
-
{ id: "left", label: "L", width: 2, align: "left" },
|
|
186
|
-
{ id: "right", label: "R", width: 2, align: "left" },
|
|
187
|
-
]}
|
|
188
|
-
items={items}
|
|
189
|
-
sortColumnId={null}
|
|
190
|
-
sortDirection="asc"
|
|
191
|
-
onHeaderClick={() => {}}
|
|
192
|
-
headerScrollRef={headerScrollRef}
|
|
193
|
-
scrollRef={scrollRef}
|
|
194
|
-
getItemKey={(row) => row.id}
|
|
195
|
-
renderCell={(row, column) => ({ text: column.id === "left" ? row.left : row.right })}
|
|
196
|
-
emptyStateTitle="No rows."
|
|
197
|
-
columnGap={0}
|
|
198
|
-
horizontalPadding={0}
|
|
199
|
-
fillAvailableWidth={false}
|
|
200
|
-
showHorizontalScrollbar={false}
|
|
201
|
-
/>
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
173
|
function DataTableVirtualizationHarness() {
|
|
206
174
|
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
207
175
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
@@ -392,31 +360,6 @@ describe("shared UI kit", () => {
|
|
|
392
360
|
expect(lastSelected).toBe("chart");
|
|
393
361
|
});
|
|
394
362
|
|
|
395
|
-
test("marks the active bare tab when it has keyboard focus", async () => {
|
|
396
|
-
testSetup = await testRender(
|
|
397
|
-
<Tabs
|
|
398
|
-
tabs={[
|
|
399
|
-
{ label: "One", value: "one" },
|
|
400
|
-
{ label: "Two", value: "two" },
|
|
401
|
-
]}
|
|
402
|
-
activeValue="one"
|
|
403
|
-
onSelect={() => {}}
|
|
404
|
-
variant="bare"
|
|
405
|
-
compact
|
|
406
|
-
focused
|
|
407
|
-
/>,
|
|
408
|
-
{ width: 24, height: 2 },
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
await act(async () => {
|
|
412
|
-
await testSetup!.renderOnce();
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
const activeSpan = testSetup.captureSpans().lines[0]?.spans.find((span) => span.text.includes("One"));
|
|
416
|
-
expect(activeSpan).toBeDefined();
|
|
417
|
-
expect((activeSpan!.attributes & TextAttributes.UNDERLINE)).toBe(TextAttributes.UNDERLINE);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
363
|
test("selects tabs by clicking their text labels", async () => {
|
|
421
364
|
let lastSelected = "overview";
|
|
422
365
|
const selectedValues: string[] = [];
|
|
@@ -794,27 +737,6 @@ describe("shared UI kit", () => {
|
|
|
794
737
|
expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(false);
|
|
795
738
|
});
|
|
796
739
|
|
|
797
|
-
test("renders data table columns without spacer cells when column gap is zero", async () => {
|
|
798
|
-
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
799
|
-
testSetup = await testRender(
|
|
800
|
-
<AppContext value={{ state, dispatch: () => {} }}>
|
|
801
|
-
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
802
|
-
<DataTableColumnGapHarness />
|
|
803
|
-
</PaneInstanceProvider>
|
|
804
|
-
</AppContext>,
|
|
805
|
-
{ width: 8, height: 4 },
|
|
806
|
-
);
|
|
807
|
-
|
|
808
|
-
await act(async () => {
|
|
809
|
-
await testSetup!.renderOnce();
|
|
810
|
-
await testSetup!.renderOnce();
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
const frame = testSetup.captureCharFrame();
|
|
814
|
-
expect(frame).toContain("AABB");
|
|
815
|
-
expect(frame).not.toContain("AA BB");
|
|
816
|
-
});
|
|
817
|
-
|
|
818
740
|
test("hides data table horizontal scrolling when content fits", async () => {
|
|
819
741
|
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
820
742
|
tableScrollBoxForTest = null;
|
|
@@ -60,9 +60,9 @@ function sanitizePlacementMemory(value: unknown): PanePlacementMemory | undefine
|
|
|
60
60
|
const docked = (() => {
|
|
61
61
|
const raw = (value as PanePlacementMemory).docked;
|
|
62
62
|
if (!raw || typeof raw !== "object") return undefined;
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
const rawPath = (raw as { path?: unknown }).path;
|
|
64
|
+
const path = Array.isArray(rawPath)
|
|
65
|
+
? rawPath.filter((segment): segment is 0 | 1 => segment === 0 || segment === 1)
|
|
66
66
|
: undefined;
|
|
67
67
|
const anchorInstanceId = typeof (raw as { anchorInstanceId?: unknown }).anchorInstanceId === "string"
|
|
68
68
|
? (raw as { anchorInstanceId: string }).anchorInstanceId
|
|
@@ -291,6 +291,12 @@ export class ResourceStore {
|
|
|
291
291
|
}
|
|
292
292
|
records.push(record);
|
|
293
293
|
}
|
|
294
|
+
if (options.touch !== false) {
|
|
295
|
+
for (const record of records) {
|
|
296
|
+
const touchedAt = this.touch(record);
|
|
297
|
+
if (touchedAt != null) record.lastAccessedAt = touchedAt;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
294
300
|
return records;
|
|
295
301
|
}
|
|
296
302
|
|
|
@@ -35,6 +35,7 @@ describe("AppPersistence", () => {
|
|
|
35
35
|
}, {
|
|
36
36
|
cachePolicy: { staleMs: 60_000, expireMs: 120_000 },
|
|
37
37
|
schemaVersion: 1,
|
|
38
|
+
fetchedAt: 1,
|
|
38
39
|
});
|
|
39
40
|
|
|
40
41
|
expect(persistence.resources.get<{ price: number }>({
|
|
@@ -43,6 +44,10 @@ describe("AppPersistence", () => {
|
|
|
43
44
|
entityKey: "AAPL",
|
|
44
45
|
sourceKey: "provider:yahoo",
|
|
45
46
|
}, { allowExpired: true })?.value.price).toBe(123);
|
|
47
|
+
const listed = persistence.resources.list<{ price: number }>({
|
|
48
|
+
namespace: "market", kind: "quote", entityKey: "AAPL",
|
|
49
|
+
}, { allowExpired: true });
|
|
50
|
+
expect(listed[0]?.lastAccessedAt).toBeGreaterThan(1);
|
|
46
51
|
persistence.close();
|
|
47
52
|
});
|
|
48
53
|
|
package/src/data/ticker-store.ts
CHANGED
|
@@ -47,11 +47,4 @@ export class TickerStore {
|
|
|
47
47
|
this.db.query("DELETE FROM tickers WHERE symbol = ?").run(symbol);
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
count(): number {
|
|
52
|
-
const row = withSqliteBusyRetry("count tickers", () => (
|
|
53
|
-
this.db.query<{ count: number }, []>("SELECT COUNT(*) as count FROM tickers").get()
|
|
54
|
-
));
|
|
55
|
-
return row?.count ?? 0;
|
|
56
|
-
}
|
|
57
50
|
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import type { DataProvider, SecFilingDocument, SecFilingItem } from "../../types/data-provider";
|
|
2
|
-
import type { NewsArticle } from "../../news/types";
|
|
3
2
|
import type { OptionsChain } from "../../types/financials";
|
|
4
|
-
import type {
|
|
3
|
+
import type { OptionsRequest, SecFilingsRequest } from "../request-types";
|
|
5
4
|
import type { QueryEntry } from "../result-types";
|
|
6
5
|
import { QueryStore } from "../query-store";
|
|
7
6
|
import {
|
|
8
7
|
ARTICLE_SUMMARY_CACHE_TTL_MS,
|
|
9
8
|
EXPECTED_EMPTY,
|
|
10
9
|
FX_CACHE_TTL_MS,
|
|
11
|
-
NEWS_CACHE_TTL_MS,
|
|
12
10
|
OPTIONS_CACHE_TTL_MS,
|
|
13
11
|
SEC_CONTENT_CACHE_TTL_MS,
|
|
14
12
|
SEC_FILINGS_CACHE_TTL_MS,
|
|
@@ -23,7 +21,6 @@ import {
|
|
|
23
21
|
import {
|
|
24
22
|
buildArticleSummaryKey,
|
|
25
23
|
buildFxKey,
|
|
26
|
-
buildNewsKey,
|
|
27
24
|
buildOptionsKey,
|
|
28
25
|
buildSecContentKey,
|
|
29
26
|
buildSecDocumentsKey,
|
|
@@ -42,46 +39,6 @@ interface AuxiliaryLoaderOptions<T> {
|
|
|
42
39
|
load: (startedAt: number) => Promise<QueryEntry<T>>;
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
interface NewsDataProvider extends DataProvider {
|
|
46
|
-
getNews?: (query: {
|
|
47
|
-
feed: "ticker";
|
|
48
|
-
ticker: string;
|
|
49
|
-
exchange?: string;
|
|
50
|
-
tickerTier: "primary";
|
|
51
|
-
limit?: number;
|
|
52
|
-
}) => Promise<NewsArticle[]>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function loadNewsEntry(options: {
|
|
56
|
-
dataProvider: DataProvider;
|
|
57
|
-
request: NewsRequest;
|
|
58
|
-
store: QueryStore<NewsArticle[]>;
|
|
59
|
-
runSingleFlight: RunSingleFlight;
|
|
60
|
-
}): Promise<QueryEntry<NewsArticle[]>> {
|
|
61
|
-
const { dataProvider, request, store, runSingleFlight } = options;
|
|
62
|
-
const key = buildNewsKey(request);
|
|
63
|
-
return loadAuxiliaryEntry({
|
|
64
|
-
dataProvider,
|
|
65
|
-
key,
|
|
66
|
-
store,
|
|
67
|
-
ttlMs: NEWS_CACHE_TTL_MS,
|
|
68
|
-
runSingleFlight,
|
|
69
|
-
load: async (startedAt) => {
|
|
70
|
-
const newsProvider = dataProvider as NewsDataProvider;
|
|
71
|
-
if (!newsProvider.getNews) throw new Error("No news provider available");
|
|
72
|
-
const data = await newsProvider.getNews({
|
|
73
|
-
feed: "ticker",
|
|
74
|
-
ticker: request.instrument.symbol,
|
|
75
|
-
exchange: request.instrument.exchange ?? "",
|
|
76
|
-
tickerTier: "primary",
|
|
77
|
-
limit: request.count ?? 50,
|
|
78
|
-
});
|
|
79
|
-
const attempts = [createAttempt(dataProvider.id, startedAt, data.length > 0 ? "success" : "empty", data.length === 0 ? "NO_DATA" : undefined)];
|
|
80
|
-
return store.update(key, (current) => readyEntry(current, data.length > 0 ? data : null, dataProvider.id, attempts, { keepLastGoodOnEmpty: true }));
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
42
|
export function loadOptionsEntry(options: {
|
|
86
43
|
dataProvider: DataProvider;
|
|
87
44
|
request: OptionsRequest;
|
|
@@ -8,7 +8,6 @@ const EMPTY_MESSAGE = "No data available";
|
|
|
8
8
|
export const EXPECTED_EMPTY = /no data|not found|delisted|unavailable|unsupported/i;
|
|
9
9
|
export const SNAPSHOT_CACHE_TTL_MS = 5 * 60_000;
|
|
10
10
|
export const CHART_CACHE_TTL_MS = 10 * 60_000;
|
|
11
|
-
export const NEWS_CACHE_TTL_MS = 2 * 60_000;
|
|
12
11
|
export const OPTIONS_CACHE_TTL_MS = 10 * 60_000;
|
|
13
12
|
export const SEC_FILINGS_CACHE_TTL_MS = 10 * 60_000;
|
|
14
13
|
export const SEC_CONTENT_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { MarketDataCoordinatorEvents } from "./events";
|
|
3
|
+
|
|
4
|
+
const waitMicrotask = () => Promise.resolve();
|
|
5
|
+
const waitTimer = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
6
|
+
|
|
7
|
+
describe("MarketDataCoordinatorEvents", () => {
|
|
8
|
+
test("coalesces version bumps before notifying external-store listeners", async () => {
|
|
9
|
+
const events = new MarketDataCoordinatorEvents();
|
|
10
|
+
const calls: number[] = [];
|
|
11
|
+
events.subscribeKeys(["quote:AMD"], () => {
|
|
12
|
+
calls.push(events.getKeysVersion(["quote:AMD"]));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
events.bump("quote:AMD");
|
|
16
|
+
events.bump("quote:AMD");
|
|
17
|
+
|
|
18
|
+
expect(events.getKeysVersion(["quote:AMD"])).toBe(0);
|
|
19
|
+
expect(calls).toEqual([]);
|
|
20
|
+
|
|
21
|
+
await waitMicrotask();
|
|
22
|
+
expect(events.getKeysVersion(["quote:AMD"])).toBe(1);
|
|
23
|
+
expect(calls).toEqual([]);
|
|
24
|
+
|
|
25
|
+
await waitTimer();
|
|
26
|
+
expect(calls).toEqual([1]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("delivers bumps scheduled during notification in a later notification pass", async () => {
|
|
30
|
+
const events = new MarketDataCoordinatorEvents();
|
|
31
|
+
const order: string[] = [];
|
|
32
|
+
events.subscribeKeys(["quote:AMD"], () => {
|
|
33
|
+
order.push("AMD");
|
|
34
|
+
events.bump("quote:NVDA");
|
|
35
|
+
});
|
|
36
|
+
events.subscribeKeys(["quote:NVDA"], () => {
|
|
37
|
+
order.push("NVDA");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
events.bump("quote:AMD");
|
|
41
|
+
await waitMicrotask();
|
|
42
|
+
await waitTimer();
|
|
43
|
+
expect(order).toEqual(["AMD"]);
|
|
44
|
+
|
|
45
|
+
await waitMicrotask();
|
|
46
|
+
await waitTimer();
|
|
47
|
+
expect(order).toEqual(["AMD", "NVDA"]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -4,6 +4,8 @@ export class MarketDataCoordinatorEvents {
|
|
|
4
4
|
private version = 0;
|
|
5
5
|
private pendingVersionBump = false;
|
|
6
6
|
private pendingChangedKeys = new Set<string>();
|
|
7
|
+
private pendingNotify = false;
|
|
8
|
+
private pendingListeners = new Set<() => void>();
|
|
7
9
|
private readonly listeners = new Set<() => void>();
|
|
8
10
|
private readonly keyListeners = new Map<string, Set<() => void>>();
|
|
9
11
|
private readonly keyVersions = new Map<string, number>();
|
|
@@ -60,15 +62,34 @@ export class MarketDataCoordinatorEvents {
|
|
|
60
62
|
for (const key of changedKeys) {
|
|
61
63
|
this.keyVersions.set(key, (this.keyVersions.get(key) ?? 0) + 1);
|
|
62
64
|
}
|
|
63
|
-
const
|
|
65
|
+
for (const listener of this.listeners) {
|
|
66
|
+
this.pendingListeners.add(listener);
|
|
67
|
+
}
|
|
64
68
|
for (const key of changedKeys) {
|
|
65
69
|
for (const listener of this.keyListeners.get(key) ?? []) {
|
|
66
|
-
|
|
70
|
+
this.pendingListeners.add(listener);
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
for (const listener of keyListeners) listener();
|
|
74
|
+
this.scheduleNotify();
|
|
72
75
|
}, { changedKeyCount: changedKeys.size });
|
|
73
76
|
}
|
|
77
|
+
|
|
78
|
+
private scheduleNotify(): void {
|
|
79
|
+
if (this.pendingNotify) return;
|
|
80
|
+
this.pendingNotify = true;
|
|
81
|
+
setTimeout(() => this.flushNotify(), 0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private flushNotify(): void {
|
|
85
|
+
this.pendingNotify = false;
|
|
86
|
+
const listeners = [...this.pendingListeners];
|
|
87
|
+
this.pendingListeners.clear();
|
|
88
|
+
for (const listener of listeners) {
|
|
89
|
+
listener();
|
|
90
|
+
}
|
|
91
|
+
if (this.pendingListeners.size > 0) {
|
|
92
|
+
this.scheduleNotify();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
74
95
|
}
|
|
@@ -155,13 +155,8 @@ describe("MarketDataCoordinator", () => {
|
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
it("reuses fresh empty tab query results instead of refetching on reopen", async () => {
|
|
158
|
-
let newsCalls = 0;
|
|
159
158
|
let optionsCalls = 0;
|
|
160
159
|
const provider = createProvider({
|
|
161
|
-
getNews: async () => {
|
|
162
|
-
newsCalls += 1;
|
|
163
|
-
return [];
|
|
164
|
-
},
|
|
165
160
|
getOptionsChain: async () => {
|
|
166
161
|
optionsCalls += 1;
|
|
167
162
|
return {
|
|
@@ -175,16 +170,11 @@ describe("MarketDataCoordinator", () => {
|
|
|
175
170
|
const coordinator = new MarketDataCoordinator(provider);
|
|
176
171
|
const instrument = { symbol: "AAPL", exchange: "NASDAQ" };
|
|
177
172
|
|
|
178
|
-
const firstNews = await coordinator.loadNews({ instrument, count: 20 });
|
|
179
|
-
const secondNews = await coordinator.loadNews({ instrument, count: 20 });
|
|
180
173
|
const firstOptions = await coordinator.loadOptions({ instrument });
|
|
181
174
|
const secondOptions = await coordinator.loadOptions({ instrument });
|
|
182
175
|
|
|
183
|
-
expect(firstNews.error?.reasonCode).toBe("NO_DATA");
|
|
184
|
-
expect(secondNews.error?.reasonCode).toBe("NO_DATA");
|
|
185
176
|
expect(firstOptions.error?.reasonCode).toBe("NO_DATA");
|
|
186
177
|
expect(secondOptions.error?.reasonCode).toBe("NO_DATA");
|
|
187
|
-
expect(newsCalls).toBe(1);
|
|
188
178
|
expect(optionsCalls).toBe(1);
|
|
189
179
|
});
|
|
190
180
|
|
|
@@ -347,6 +337,229 @@ describe("MarketDataCoordinator", () => {
|
|
|
347
337
|
expect(coordinator.getQuoteEntry(instrument).data?.price).toBe(412.5);
|
|
348
338
|
});
|
|
349
339
|
|
|
340
|
+
it("reconciles stream quotes with cached provider day references before a snapshot loads", () => {
|
|
341
|
+
let streamed: ((target: QuoteSubscriptionTarget, quote: Quote) => void) | null = null;
|
|
342
|
+
const provider = createProvider({
|
|
343
|
+
getCachedFinancialsForTargets: () => new Map([[
|
|
344
|
+
"VICR",
|
|
345
|
+
{
|
|
346
|
+
quote: {
|
|
347
|
+
symbol: "VICR",
|
|
348
|
+
providerId: "gloomberb-cloud",
|
|
349
|
+
price: 292.83,
|
|
350
|
+
currency: "USD",
|
|
351
|
+
previousClose: 282.95,
|
|
352
|
+
change: 9.88,
|
|
353
|
+
changePercent: 3.49,
|
|
354
|
+
lastUpdated: Date.parse("2026-07-06T16:54:30Z"),
|
|
355
|
+
marketState: "REGULAR",
|
|
356
|
+
dataSource: "live",
|
|
357
|
+
},
|
|
358
|
+
quoteContributions: {
|
|
359
|
+
"gloomberb-cloud": {
|
|
360
|
+
symbol: "VICR",
|
|
361
|
+
providerId: "gloomberb-cloud",
|
|
362
|
+
price: 292.83,
|
|
363
|
+
currency: "USD",
|
|
364
|
+
previousClose: 380.07,
|
|
365
|
+
change: -87.24,
|
|
366
|
+
changePercent: -22.95,
|
|
367
|
+
lastUpdated: Date.parse("2026-07-06T16:54:30Z"),
|
|
368
|
+
marketState: "REGULAR",
|
|
369
|
+
dataSource: "live",
|
|
370
|
+
},
|
|
371
|
+
yahoo: {
|
|
372
|
+
symbol: "VICR",
|
|
373
|
+
providerId: "yahoo",
|
|
374
|
+
price: 294.39,
|
|
375
|
+
currency: "USD",
|
|
376
|
+
previousClose: 282.95,
|
|
377
|
+
change: 11.44,
|
|
378
|
+
changePercent: 4.04,
|
|
379
|
+
lastUpdated: Date.parse("2026-07-06T16:45:37Z"),
|
|
380
|
+
marketState: "REGULAR",
|
|
381
|
+
dataSource: "delayed",
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
annualStatements: [],
|
|
385
|
+
quarterlyStatements: [],
|
|
386
|
+
priceHistory: [],
|
|
387
|
+
},
|
|
388
|
+
]]),
|
|
389
|
+
subscribeQuotes: (_targets, onQuote) => {
|
|
390
|
+
streamed = onQuote as typeof streamed;
|
|
391
|
+
return () => {};
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
395
|
+
const instrument = {
|
|
396
|
+
symbol: "VICR",
|
|
397
|
+
exchange: "NASDAQ",
|
|
398
|
+
brokerId: "ibkr",
|
|
399
|
+
brokerInstanceId: "ibkr-work",
|
|
400
|
+
instrument: {
|
|
401
|
+
brokerId: "ibkr",
|
|
402
|
+
brokerInstanceId: "ibkr-work",
|
|
403
|
+
conId: 275759,
|
|
404
|
+
symbol: "VICR",
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
coordinator.subscribeQuotes([{ instrument }]);
|
|
409
|
+
const onStreamed = streamed as ((target: QuoteSubscriptionTarget, quote: Quote) => void) | null;
|
|
410
|
+
if (!onStreamed) throw new Error("expected streaming callback");
|
|
411
|
+
onStreamed(
|
|
412
|
+
{
|
|
413
|
+
symbol: "VICR",
|
|
414
|
+
exchange: "NASDAQ",
|
|
415
|
+
context: {
|
|
416
|
+
brokerId: "ibkr",
|
|
417
|
+
brokerInstanceId: "ibkr-work",
|
|
418
|
+
instrument: instrument.instrument,
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
symbol: "VICR",
|
|
423
|
+
providerId: "gloomberb-cloud",
|
|
424
|
+
price: 293.07,
|
|
425
|
+
currency: "USD",
|
|
426
|
+
previousClose: 380.07,
|
|
427
|
+
change: -87,
|
|
428
|
+
changePercent: -22.89,
|
|
429
|
+
lastUpdated: Date.parse("2026-07-06T17:17:00Z"),
|
|
430
|
+
marketState: "REGULAR",
|
|
431
|
+
dataSource: "live",
|
|
432
|
+
},
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const quote = coordinator.getQuoteEntry(instrument).data;
|
|
436
|
+
expect(quote?.price).toBe(293.07);
|
|
437
|
+
expect(quote?.previousClose).toBe(282.95);
|
|
438
|
+
expect(quote?.changePercent).toBeCloseTo(((293.07 - 282.95) / 282.95) * 100, 10);
|
|
439
|
+
expect(quote?.provenance?.fields?.previousClose?.providerId).toBe("yahoo");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("reconciles batch quote loads with cached provider day references before a snapshot loads", async () => {
|
|
443
|
+
const provider = createProvider({
|
|
444
|
+
getCachedFinancialsForTargets: () => new Map([[
|
|
445
|
+
"VICR",
|
|
446
|
+
{
|
|
447
|
+
quote: {
|
|
448
|
+
symbol: "VICR",
|
|
449
|
+
providerId: "gloomberb-cloud",
|
|
450
|
+
price: 292.83,
|
|
451
|
+
currency: "USD",
|
|
452
|
+
previousClose: 282.95,
|
|
453
|
+
change: 9.88,
|
|
454
|
+
changePercent: 3.49,
|
|
455
|
+
lastUpdated: Date.parse("2026-07-06T16:54:30Z"),
|
|
456
|
+
marketState: "REGULAR",
|
|
457
|
+
dataSource: "live",
|
|
458
|
+
},
|
|
459
|
+
quoteContributions: {
|
|
460
|
+
"gloomberb-cloud": {
|
|
461
|
+
symbol: "VICR",
|
|
462
|
+
providerId: "gloomberb-cloud",
|
|
463
|
+
price: 292.83,
|
|
464
|
+
currency: "USD",
|
|
465
|
+
previousClose: 380.07,
|
|
466
|
+
change: -87.24,
|
|
467
|
+
changePercent: -22.95,
|
|
468
|
+
lastUpdated: Date.parse("2026-07-06T16:54:30Z"),
|
|
469
|
+
marketState: "REGULAR",
|
|
470
|
+
dataSource: "live",
|
|
471
|
+
},
|
|
472
|
+
yahoo: {
|
|
473
|
+
symbol: "VICR",
|
|
474
|
+
providerId: "yahoo",
|
|
475
|
+
price: 294.39,
|
|
476
|
+
currency: "USD",
|
|
477
|
+
previousClose: 282.95,
|
|
478
|
+
change: 11.44,
|
|
479
|
+
changePercent: 4.04,
|
|
480
|
+
lastUpdated: Date.parse("2026-07-06T16:45:37Z"),
|
|
481
|
+
marketState: "REGULAR",
|
|
482
|
+
dataSource: "delayed",
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
annualStatements: [],
|
|
486
|
+
quarterlyStatements: [],
|
|
487
|
+
priceHistory: [],
|
|
488
|
+
},
|
|
489
|
+
]]),
|
|
490
|
+
getQuotesBatch: async (targets) => targets.map((target) => ({
|
|
491
|
+
target,
|
|
492
|
+
quote: {
|
|
493
|
+
symbol: target.symbol,
|
|
494
|
+
providerId: "gloomberb-cloud",
|
|
495
|
+
price: 293.07,
|
|
496
|
+
currency: "USD",
|
|
497
|
+
previousClose: 380.07,
|
|
498
|
+
change: -87,
|
|
499
|
+
changePercent: -22.89,
|
|
500
|
+
lastUpdated: Date.parse("2026-07-06T17:17:00Z"),
|
|
501
|
+
marketState: "REGULAR",
|
|
502
|
+
dataSource: "live",
|
|
503
|
+
},
|
|
504
|
+
})),
|
|
505
|
+
});
|
|
506
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
507
|
+
const instrument = {
|
|
508
|
+
symbol: "VICR",
|
|
509
|
+
exchange: "NASDAQ",
|
|
510
|
+
brokerId: "ibkr",
|
|
511
|
+
brokerInstanceId: "ibkr-work",
|
|
512
|
+
instrument: {
|
|
513
|
+
brokerId: "ibkr",
|
|
514
|
+
brokerInstanceId: "ibkr-work",
|
|
515
|
+
conId: 275759,
|
|
516
|
+
symbol: "VICR",
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
await coordinator.loadQuotesBatch([instrument], { forceRefresh: true });
|
|
521
|
+
|
|
522
|
+
const quote = coordinator.getQuoteEntry(instrument).data;
|
|
523
|
+
expect(quote?.price).toBe(293.07);
|
|
524
|
+
expect(quote?.previousClose).toBe(282.95);
|
|
525
|
+
expect(quote?.changePercent).toBeCloseTo(((293.07 - 282.95) / 282.95) * 100, 10);
|
|
526
|
+
expect(quote?.provenance?.fields?.previousClose?.providerId).toBe("yahoo");
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("preserves requested quote stream routes", () => {
|
|
530
|
+
let subscribedTargets: QuoteSubscriptionTarget[] = [];
|
|
531
|
+
const provider = createProvider({
|
|
532
|
+
subscribeQuotes: (targets) => {
|
|
533
|
+
subscribedTargets = targets;
|
|
534
|
+
return () => {};
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
538
|
+
|
|
539
|
+
coordinator.subscribeQuotes([{
|
|
540
|
+
instrument: {
|
|
541
|
+
symbol: "AAPL",
|
|
542
|
+
exchange: "NASDAQ",
|
|
543
|
+
brokerId: "ibkr",
|
|
544
|
+
brokerInstanceId: "ibkr-work",
|
|
545
|
+
},
|
|
546
|
+
priority: { route: "provider", surface: "portfolio", visible: true },
|
|
547
|
+
}]);
|
|
548
|
+
|
|
549
|
+
expect(subscribedTargets).toEqual([{
|
|
550
|
+
symbol: "AAPL",
|
|
551
|
+
exchange: "NASDAQ",
|
|
552
|
+
route: "provider",
|
|
553
|
+
context: {
|
|
554
|
+
brokerId: "ibkr",
|
|
555
|
+
brokerInstanceId: "ibkr-work",
|
|
556
|
+
instrument: null,
|
|
557
|
+
},
|
|
558
|
+
surface: "portfolio",
|
|
559
|
+
visible: true,
|
|
560
|
+
}]);
|
|
561
|
+
});
|
|
562
|
+
|
|
350
563
|
it("routes manual-resolution chart requests through the resolution-aware provider path", async () => {
|
|
351
564
|
let requested: { range: string; resolution: string } | null = null;
|
|
352
565
|
const provider = createProvider({
|