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
|
@@ -31,7 +31,7 @@ const HELP_TABS = [
|
|
|
31
31
|
type HelpTabId = typeof HELP_TABS[number]["value"];
|
|
32
32
|
const GLOOMBERB_ISSUES_URL = "https://github.com/vincelwt/gloomberb/issues";
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
function HelpPane({ focused, width, height }: PaneProps) {
|
|
35
35
|
const registry = getSharedRegistry();
|
|
36
36
|
const { openCommandBar, showPane } = usePluginAppActions();
|
|
37
37
|
const [activeTabId, setActiveTabId] = useState<HelpTabId>("basics");
|
|
@@ -381,7 +381,7 @@ export function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
381
381
|
<Box flexDirection="column" width={width} height={height}>
|
|
382
382
|
<Box width={width} height={1} flexShrink={0}>
|
|
383
383
|
<Tabs
|
|
384
|
-
tabs={HELP_TABS}
|
|
384
|
+
tabs={[...HELP_TABS]}
|
|
385
385
|
activeValue={activeTabId}
|
|
386
386
|
onSelect={(value) => setActiveTabId(value as HelpTabId)}
|
|
387
387
|
focused={focused}
|
|
@@ -127,11 +127,6 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
|
|
|
127
127
|
};
|
|
128
128
|
}, [sortedRows]);
|
|
129
129
|
|
|
130
|
-
const selectIndex = useCallback((index: number) => {
|
|
131
|
-
const row = sortedRows[index];
|
|
132
|
-
if (row) setSelectedId(row.id);
|
|
133
|
-
}, [sortedRows]);
|
|
134
|
-
|
|
135
130
|
const selectedRow = activeIdx >= 0 ? sortedRows[activeIdx] ?? null : null;
|
|
136
131
|
const selectedFundMatch = selectedRow ? fundMatches.get(selectedRow.id) ?? null : null;
|
|
137
132
|
|
|
@@ -14,6 +14,7 @@ import { FeedDataTableStackView, Spinner, useExternalLinkFooter, type FeedDataTa
|
|
|
14
14
|
import { usePluginPaneState } from "../../runtime";
|
|
15
15
|
import { isUsEquityTicker } from "../../../utils/sec";
|
|
16
16
|
import { formatCompact, formatCurrency } from "../../../utils/format";
|
|
17
|
+
import { truncateWithEllipsis as truncateText } from "../../../utils/text-wrap";
|
|
17
18
|
import { parseForm4Xml, type InsiderTransaction } from "./insider-data";
|
|
18
19
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
19
20
|
import {
|
|
@@ -61,13 +62,6 @@ function buildSummary(parsed: ParsedFiling[]): string {
|
|
|
61
62
|
return parts.join(" | ");
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
function truncateText(text: string, width: number): string {
|
|
65
|
-
if (width <= 0) return "";
|
|
66
|
-
if (text.length <= width) return text;
|
|
67
|
-
if (width <= 3) return text.slice(0, width);
|
|
68
|
-
return `${text.slice(0, width - 3)}...`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
65
|
function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
|
|
72
66
|
return parsed.map(({ filing, transaction, isLoading }) => {
|
|
73
67
|
const filingMeta = [
|
|
@@ -95,7 +89,6 @@ function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
|
|
|
95
89
|
eyebrow: transaction.reportedName,
|
|
96
90
|
title: buildInsiderTransactionTitle(transaction),
|
|
97
91
|
timestamp: transaction.filingDate,
|
|
98
|
-
preview: transaction.title || undefined,
|
|
99
92
|
detailTitle: transaction.reportedName,
|
|
100
93
|
detailMeta: [
|
|
101
94
|
...(transaction.title ? [transaction.title] : []),
|
|
@@ -2,7 +2,6 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { act } from "react";
|
|
3
3
|
import { testRender } from "../../../renderers/opentui/test-utils";
|
|
4
4
|
import { setSharedMarketDataCoordinator } from "../../../market-data/coordinator";
|
|
5
|
-
import type { TickerRecord } from "../../../types/ticker";
|
|
6
5
|
import { KellySizerHarness, createFinancials, createSizerConfig, createTicker } from "./test-support";
|
|
7
6
|
|
|
8
7
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
@@ -36,31 +35,6 @@ afterEach(async () => {
|
|
|
36
35
|
});
|
|
37
36
|
|
|
38
37
|
describe("KellySizerPane", () => {
|
|
39
|
-
test("renders binary sizing context, result, and curve", async () => {
|
|
40
|
-
await renderPane();
|
|
41
|
-
await flushFrame();
|
|
42
|
-
|
|
43
|
-
const frame = testSetup!.captureCharFrame();
|
|
44
|
-
expect(frame).toContain("AAPL · Main Portfolio");
|
|
45
|
-
expect(frame).toContain("Bankroll");
|
|
46
|
-
expect(frame).toContain("100000");
|
|
47
|
-
expect(frame).toContain("Win p");
|
|
48
|
-
expect(frame).toContain("Full Kelly");
|
|
49
|
-
expect(frame).toContain("Clipped");
|
|
50
|
-
expect(frame).toContain("Target val");
|
|
51
|
-
expect(frame).toContain("Current %");
|
|
52
|
-
expect(frame).toContain("Kelly Curve");
|
|
53
|
-
expect(frame).toContain("4.0% -> 8.0%");
|
|
54
|
-
expect(frame).toContain("growth");
|
|
55
|
-
expect(frame).toContain("loss cap");
|
|
56
|
-
expect(frame).toContain("cap max loss, max name");
|
|
57
|
-
expect(frame).toContain("full");
|
|
58
|
-
expect(frame).toContain("159%");
|
|
59
|
-
expect(frame).toContain("636%");
|
|
60
|
-
expect(frame).not.toContain("Mode");
|
|
61
|
-
expect(frame).not.toContain("Sensitivity");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
38
|
test("prefills shared caps from plugin config", async () => {
|
|
65
39
|
const config = {
|
|
66
40
|
...createSizerConfig(),
|
|
@@ -51,8 +51,6 @@ function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
|
51
51
|
const selectedIdx = selectedSymbol
|
|
52
52
|
? rows.findIndex((row) => row.symbol === selectedSymbol)
|
|
53
53
|
: -1;
|
|
54
|
-
const activeIdx = selectedIdx >= 0 ? selectedIdx : (rows.length > 0 ? 0 : -1);
|
|
55
|
-
|
|
56
54
|
useEffect(() => {
|
|
57
55
|
if (selectedSymbol && selectedIdx >= 0) return;
|
|
58
56
|
const firstRow = rows[0];
|
|
@@ -3,7 +3,6 @@ import { TextAttributes } from "../../../../../ui";
|
|
|
3
3
|
import {
|
|
4
4
|
DataTableStackView,
|
|
5
5
|
TickerBadgeList,
|
|
6
|
-
activeStackIndex,
|
|
7
6
|
sortStackItems,
|
|
8
7
|
type DataTableCell,
|
|
9
8
|
type DataTableColumn,
|
|
@@ -165,23 +164,13 @@ export function NewsArticleStackView({
|
|
|
165
164
|
[articles, sortPreference],
|
|
166
165
|
);
|
|
167
166
|
const selectedIdx = sortedArticles.findIndex((article) => article.id === selectedArticleId);
|
|
168
|
-
const activeIdx = activeStackIndex(sortedArticles.length, selectedIdx);
|
|
169
167
|
const columns = useMemo(() => buildColumns(width, columnIds), [columnIds, width]);
|
|
170
168
|
|
|
171
|
-
const selectIndex = useCallback((index: number) => {
|
|
172
|
-
setSelectedArticleId(sortedArticles[index]?.id ?? null);
|
|
173
|
-
}, [setSelectedArticleId, sortedArticles]);
|
|
174
|
-
|
|
175
169
|
const openArticle = useCallback((article: MarketNewsItem) => {
|
|
176
170
|
onArticleRead?.(article.id);
|
|
177
171
|
onOpenArticle(article);
|
|
178
172
|
}, [onArticleRead, onOpenArticle]);
|
|
179
173
|
|
|
180
|
-
const openIndex = useCallback((index: number) => {
|
|
181
|
-
const article = sortedArticles[index];
|
|
182
|
-
if (article) openArticle(article);
|
|
183
|
-
}, [openArticle, sortedArticles]);
|
|
184
|
-
|
|
185
174
|
useEffect(() => {
|
|
186
175
|
if (sortedArticles.length === 0) {
|
|
187
176
|
if (selectedArticleId !== null) setSelectedArticleId(null);
|
|
@@ -123,8 +123,8 @@ function OptionsHarness({
|
|
|
123
123
|
async function renderSettled() {
|
|
124
124
|
for (let i = 0; i < 4; i += 1) {
|
|
125
125
|
await act(async () => {
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
126
127
|
await testSetup!.renderOnce();
|
|
127
|
-
await Promise.resolve();
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
}
|
|
@@ -139,43 +139,6 @@ afterEach(async () => {
|
|
|
139
139
|
setSharedMarketDataCoordinator(null);
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
-
test("renders the options chain with the shared table columns", async () => {
|
|
143
|
-
const provider = createTestDataProvider({
|
|
144
|
-
getOptionsChain: async () => makeChain(),
|
|
145
|
-
});
|
|
146
|
-
setSharedMarketDataCoordinator(new MarketDataCoordinator(provider));
|
|
147
|
-
|
|
148
|
-
await act(async () => {
|
|
149
|
-
testSetup = await testRender(<OptionsHarness ticker={makeTicker("AAPL")} />, {
|
|
150
|
-
width: 124,
|
|
151
|
-
height: 16,
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
await renderSettled();
|
|
156
|
-
|
|
157
|
-
const frame = testSetup!.captureCharFrame();
|
|
158
|
-
expect(frame).toContain("C LAST");
|
|
159
|
-
expect(frame).toContain("C IV");
|
|
160
|
-
expect(frame).toContain("STRIKE");
|
|
161
|
-
expect(frame).toContain("P IV");
|
|
162
|
-
expect(frame).toContain("P LAST");
|
|
163
|
-
expect(frame).toContain("100");
|
|
164
|
-
expect(frame).toContain("101");
|
|
165
|
-
|
|
166
|
-
const headerLine = frame.split("\n").find((line) => line.includes("C OI") && line.includes("STRIKE")) ?? "";
|
|
167
|
-
expect(headerLine.indexOf("C OI")).toBeLessThan(headerLine.indexOf("C VOL"));
|
|
168
|
-
expect(headerLine.indexOf("C VOL")).toBeLessThan(headerLine.indexOf("C LAST"));
|
|
169
|
-
expect(headerLine.indexOf("C LAST")).toBeLessThan(headerLine.indexOf("C IV"));
|
|
170
|
-
expect(headerLine.indexOf("C IV")).toBeLessThan(headerLine.indexOf("C BID"));
|
|
171
|
-
expect(headerLine.indexOf("C BID")).toBeLessThan(headerLine.indexOf("C ASK"));
|
|
172
|
-
expect(headerLine.indexOf("P BID")).toBeLessThan(headerLine.indexOf("P ASK"));
|
|
173
|
-
expect(headerLine.indexOf("P ASK")).toBeLessThan(headerLine.indexOf("P IV"));
|
|
174
|
-
expect(headerLine.indexOf("P IV")).toBeLessThan(headerLine.indexOf("P LAST"));
|
|
175
|
-
expect(headerLine.indexOf("P LAST")).toBeLessThan(headerLine.indexOf("P VOL"));
|
|
176
|
-
expect(headerLine.indexOf("P VOL")).toBeLessThan(headerLine.indexOf("P OI"));
|
|
177
|
-
});
|
|
178
|
-
|
|
179
142
|
test("defaults the table around the nearest strike to the current quote", async () => {
|
|
180
143
|
const strikes = Array.from({ length: 25 }, (_, index) => 50 + index * 5);
|
|
181
144
|
const provider = createTestDataProvider({
|
|
@@ -29,6 +29,7 @@ type PluginGroupOptions = Pick<
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
function createPluginGroup(options: PluginGroupOptions): GloomPlugin {
|
|
32
|
+
let startedPlugins: GloomPlugin[] = [];
|
|
32
33
|
return {
|
|
33
34
|
id: options.id,
|
|
34
35
|
name: options.name,
|
|
@@ -42,17 +43,30 @@ function createPluginGroup(options: PluginGroupOptions): GloomPlugin {
|
|
|
42
43
|
capabilities: options.plugins.flatMap((plugin) => plugin.capabilities ?? []),
|
|
43
44
|
|
|
44
45
|
async setup(ctx) {
|
|
46
|
+
startedPlugins = [];
|
|
45
47
|
await options.setup?.(ctx);
|
|
46
48
|
for (const plugin of options.plugins) {
|
|
49
|
+
startedPlugins.push(plugin);
|
|
47
50
|
await plugin.setup?.(ctx);
|
|
48
51
|
}
|
|
49
52
|
},
|
|
50
53
|
|
|
51
54
|
dispose() {
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
let firstError: unknown;
|
|
56
|
+
for (const plugin of startedPlugins.reverse()) {
|
|
57
|
+
try {
|
|
58
|
+
plugin.dispose?.();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
firstError ??= error;
|
|
61
|
+
}
|
|
54
62
|
}
|
|
55
|
-
|
|
63
|
+
startedPlugins = [];
|
|
64
|
+
try {
|
|
65
|
+
options.dispose?.();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
firstError ??= error;
|
|
68
|
+
}
|
|
69
|
+
if (firstError) throw firstError;
|
|
56
70
|
},
|
|
57
71
|
};
|
|
58
72
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ColumnConfig } from "../../../types/config";
|
|
2
|
-
import type { AnalystResearchData, CorporateActionsData, TickerFinancials } from "../../../types/financials";
|
|
2
|
+
import type { AnalystResearchData, CorporateActionsData, MarketState, TickerFinancials } from "../../../types/financials";
|
|
3
3
|
import type { EarningsEvent } from "../../../types/data-provider";
|
|
4
4
|
import type { TickerRecord } from "../../../types/ticker";
|
|
5
5
|
import { priceColor } from "../../../theme/colors";
|
|
@@ -13,7 +13,13 @@ import {
|
|
|
13
13
|
formatSignedMarketPrice,
|
|
14
14
|
type MarketFormatOptions,
|
|
15
15
|
} from "../../../market-data/market/format";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
getActiveQuoteDisplay,
|
|
18
|
+
marketChangeColor,
|
|
19
|
+
marketPriceColor,
|
|
20
|
+
marketStateDot,
|
|
21
|
+
type ActiveQuoteDisplay,
|
|
22
|
+
} from "../../../market-data/market/status";
|
|
17
23
|
import { formatOptionTicker } from "../../../utils/options";
|
|
18
24
|
import { PRICE_SPARKLINE_COLUMN_ID } from "../../../components/price-sparkline/view";
|
|
19
25
|
import {
|
|
@@ -159,13 +165,14 @@ function getActiveMarketValue(
|
|
|
159
165
|
export function resolvePortfolioPriceValue(
|
|
160
166
|
activeQuote: ActiveQuoteDisplay | null,
|
|
161
167
|
brokerMarkPrice: number | undefined,
|
|
162
|
-
formatOptions: MarketFormatOptions,
|
|
168
|
+
formatOptions: MarketFormatOptions = {},
|
|
163
169
|
maxWidth?: number,
|
|
170
|
+
marketState?: MarketState,
|
|
164
171
|
): { text: string; color?: string } {
|
|
165
172
|
if (activeQuote) {
|
|
166
173
|
return {
|
|
167
174
|
text: formatMarketPrice(activeQuote.price, { ...formatOptions, maxWidth }),
|
|
168
|
-
color:
|
|
175
|
+
color: marketPriceColor(activeQuote.change, marketState),
|
|
169
176
|
};
|
|
170
177
|
}
|
|
171
178
|
if (brokerMarkPrice != null) {
|
|
@@ -229,12 +236,12 @@ export function getColumnValue(
|
|
|
229
236
|
case "tags":
|
|
230
237
|
return { text: ticker.metadata.tags.length > 0 ? ticker.metadata.tags.join(",") : "—" };
|
|
231
238
|
case "price":
|
|
232
|
-
return resolvePortfolioPriceValue(activeQuote, brokerMarkPrice, formatOptions, col.width);
|
|
239
|
+
return resolvePortfolioPriceValue(activeQuote, brokerMarkPrice, formatOptions, col.width, quote?.marketState);
|
|
233
240
|
case "change":
|
|
234
241
|
if (!activeQuote) return { text: "—" };
|
|
235
242
|
return {
|
|
236
243
|
text: formatSignedMarketPrice(activeQuote.change, { ...formatOptions, maxWidth: col.width }),
|
|
237
|
-
color:
|
|
244
|
+
color: marketChangeColor(activeQuote.change, quote?.marketState),
|
|
238
245
|
};
|
|
239
246
|
case "bid":
|
|
240
247
|
return { text: quote?.bid != null ? formatMarketPrice(quote.bid, { ...formatOptions, maxWidth: col.width }) : "—" };
|
|
@@ -258,8 +265,8 @@ export function getColumnValue(
|
|
|
258
265
|
}
|
|
259
266
|
case "change_pct":
|
|
260
267
|
return activeQuote
|
|
261
|
-
? { text: formatPercentRaw(activeQuote.changePercent), color:
|
|
262
|
-
: { text: quote ? formatPercentRaw(quote.changePercent) : "—", color: quote ?
|
|
268
|
+
? { text: formatPercentRaw(activeQuote.changePercent), color: marketChangeColor(activeQuote.changePercent, quote?.marketState) }
|
|
269
|
+
: { text: quote ? formatPercentRaw(quote.changePercent) : "—", color: quote ? marketChangeColor(quote.changePercent, quote.marketState) : undefined };
|
|
263
270
|
case "volume":
|
|
264
271
|
return { text: finiteNumber(quote?.volume) ? formatCompact(quote.volume) : "—" };
|
|
265
272
|
case "dollar_volume": {
|
|
@@ -5,8 +5,8 @@ import type { BrokerAccount } from "../../../types/trading";
|
|
|
5
5
|
import type { TickerRecord } from "../../../types/ticker";
|
|
6
6
|
import type { PortfolioSummaryTotals } from "./metrics";
|
|
7
7
|
import { buildPortfolioSummarySegments } from "./summary";
|
|
8
|
-
import {
|
|
9
|
-
import { needsVisibleQuoteWatchdogRefresh, selectStreamTickers } from "./pane/data";
|
|
8
|
+
import { shouldToggleCashMarginDrawer } from ".";
|
|
9
|
+
import { needsVisibleQuoteWatchdogRefresh, selectQuoteWarmupTickers, selectStreamTickers } from "./pane/data";
|
|
10
10
|
import { buildPortfolioPaneSettingsDef, getPortfolioPaneSettings } from "./settings";
|
|
11
11
|
|
|
12
12
|
function ticker(symbol: string): TickerRecord {
|
|
@@ -134,6 +134,56 @@ describe("selectStreamTickers", () => {
|
|
|
134
134
|
});
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
describe("selectQuoteWarmupTickers", () => {
|
|
138
|
+
function financials(quoteValue: Quote | undefined): TickerFinancials {
|
|
139
|
+
return {
|
|
140
|
+
annualStatements: [],
|
|
141
|
+
quarterlyStatements: [],
|
|
142
|
+
priceHistory: [],
|
|
143
|
+
quote: quoteValue,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
test("includes hidden quote-missing rows when sorting by quote-dependent columns", () => {
|
|
148
|
+
const tickers = Array.from({ length: 30 }, (_, index) => ticker(`T${index}`));
|
|
149
|
+
const financialsMap = new Map<string, TickerFinancials>(
|
|
150
|
+
tickers.slice(0, 29).map((entry, index) => [
|
|
151
|
+
entry.metadata.ticker,
|
|
152
|
+
financials({
|
|
153
|
+
symbol: entry.metadata.ticker,
|
|
154
|
+
price: index + 1,
|
|
155
|
+
currency: "USD",
|
|
156
|
+
change: index,
|
|
157
|
+
changePercent: index,
|
|
158
|
+
lastUpdated: 1_700_000_000_000,
|
|
159
|
+
}),
|
|
160
|
+
]),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const selected = selectQuoteWarmupTickers(
|
|
164
|
+
tickers,
|
|
165
|
+
{ start: 0, end: 24 },
|
|
166
|
+
financialsMap,
|
|
167
|
+
{ columnId: "change_pct", direction: "asc" },
|
|
168
|
+
1_700_000_010_000,
|
|
169
|
+
).map((entry) => entry.metadata.ticker);
|
|
170
|
+
|
|
171
|
+
expect(selected).toContain("T29");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("does not add hidden rows for ticker-only sorting", () => {
|
|
175
|
+
const tickers = Array.from({ length: 30 }, (_, index) => ticker(`T${index}`));
|
|
176
|
+
expect(selectQuoteWarmupTickers(
|
|
177
|
+
tickers,
|
|
178
|
+
{ start: 0, end: 24 },
|
|
179
|
+
new Map(),
|
|
180
|
+
{ columnId: "ticker", direction: "asc" },
|
|
181
|
+
).map((entry) => entry.metadata.ticker)).toEqual(
|
|
182
|
+
tickers.slice(0, 24).map((entry) => entry.metadata.ticker),
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
137
187
|
describe("visible quote refresh predicates", () => {
|
|
138
188
|
function quote(overrides: Partial<Quote> = {}): Quote {
|
|
139
189
|
return {
|
|
@@ -171,29 +221,6 @@ describe("visible quote refresh predicates", () => {
|
|
|
171
221
|
});
|
|
172
222
|
});
|
|
173
223
|
|
|
174
|
-
describe("portfolio list pane templates", () => {
|
|
175
|
-
test("create panes with default all-collection settings", async () => {
|
|
176
|
-
const config = createDefaultConfig("/tmp/gloomberb-portfolio-list-template");
|
|
177
|
-
const context = {
|
|
178
|
-
config,
|
|
179
|
-
layout: config.layout,
|
|
180
|
-
focusedPaneId: "portfolio-list:main",
|
|
181
|
-
activeTicker: null,
|
|
182
|
-
activeCollectionId: "watchlist",
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
for (const templateId of ["new-collection-pane", "new-watchlist-pane"]) {
|
|
186
|
-
const template = portfolioListPlugin.paneTemplates?.find((entry) => entry.id === templateId);
|
|
187
|
-
const instance = await template?.createInstance?.(context);
|
|
188
|
-
expect(instance).toEqual({ params: { collectionId: "watchlist" } });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const portfolioTemplate = portfolioListPlugin.paneTemplates?.find((entry) => entry.id === "new-portfolio-pane");
|
|
192
|
-
const portfolioInstance = await portfolioTemplate?.createInstance?.(context);
|
|
193
|
-
expect(portfolioInstance).toEqual({ params: { collectionId: "main" } });
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
224
|
describe("portfolio list pane settings", () => {
|
|
198
225
|
test("exposes view mode only for portfolio collections", () => {
|
|
199
226
|
const config = createDefaultConfig("/tmp/gloomberb-portfolio-list-settings");
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type { CollectionSortPreference } from "../../../state/app/context";
|
|
3
3
|
import type { ColumnConfig } from "../../../types/config";
|
|
4
|
-
import type { TickerFinancials } from "../../../types/financials";
|
|
4
|
+
import type { Quote, TickerFinancials } from "../../../types/financials";
|
|
5
5
|
import type { TickerRecord } from "../../../types/ticker";
|
|
6
|
+
import { blendHex, colors } from "../../../theme/colors";
|
|
6
7
|
import {
|
|
7
8
|
calculatePortfolioSummaryTotals,
|
|
8
9
|
getColumnValue,
|
|
@@ -29,8 +30,15 @@ function createTicker(overrides: Partial<TickerRecord["metadata"]> = {}): Ticker
|
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function createFinancials(
|
|
33
|
+
function createFinancials(
|
|
34
|
+
overrides: Omit<Partial<TickerFinancials>, "quote"> & { quote?: Partial<Quote> } = {},
|
|
35
|
+
): TickerFinancials {
|
|
36
|
+
const { quote: quoteOverrides, ...financialOverrides } = overrides;
|
|
33
37
|
return {
|
|
38
|
+
annualStatements: [],
|
|
39
|
+
quarterlyStatements: [],
|
|
40
|
+
priceHistory: [],
|
|
41
|
+
...financialOverrides,
|
|
34
42
|
quote: {
|
|
35
43
|
symbol: "AAPL",
|
|
36
44
|
price: 120,
|
|
@@ -39,12 +47,8 @@ function createFinancials(overrides: Partial<TickerFinancials> = {}): TickerFina
|
|
|
39
47
|
changePercent: 4.35,
|
|
40
48
|
previousClose: 115,
|
|
41
49
|
lastUpdated: 1_700_000_000_000,
|
|
42
|
-
...
|
|
50
|
+
...quoteOverrides,
|
|
43
51
|
},
|
|
44
|
-
annualStatements: [],
|
|
45
|
-
quarterlyStatements: [],
|
|
46
|
-
priceHistory: [],
|
|
47
|
-
...overrides,
|
|
48
52
|
};
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -82,6 +86,39 @@ describe("portfolio-metrics", () => {
|
|
|
82
86
|
});
|
|
83
87
|
});
|
|
84
88
|
|
|
89
|
+
test("mutes completed-session prices and changes without discarding their direction", () => {
|
|
90
|
+
const ticker = createTicker({
|
|
91
|
+
positions: [{ portfolio: "main", shares: 10, avgCost: 100, broker: "manual" }],
|
|
92
|
+
});
|
|
93
|
+
const priceColumn: ColumnConfig = { id: "price", label: "LAST", width: 10, align: "right" };
|
|
94
|
+
const changeColumn: ColumnConfig = { id: "change", label: "CHG", width: 8, align: "right" };
|
|
95
|
+
const changePctColumn: ColumnConfig = { id: "change_pct", label: "CHG%", width: 8, align: "right" };
|
|
96
|
+
const financials = createFinancials({ quote: { marketState: "CLOSED" } });
|
|
97
|
+
const mutedPositive = blendHex(colors.positive, colors.textDim, 0.55);
|
|
98
|
+
|
|
99
|
+
expect(getColumnValue(priceColumn, ticker, financials, defaultColumnContext)).toEqual({
|
|
100
|
+
text: "120",
|
|
101
|
+
color: colors.textDim,
|
|
102
|
+
});
|
|
103
|
+
expect(getColumnValue(changeColumn, ticker, financials, defaultColumnContext)).toEqual({
|
|
104
|
+
text: "+5",
|
|
105
|
+
color: mutedPositive,
|
|
106
|
+
});
|
|
107
|
+
expect(getColumnValue(changePctColumn, ticker, financials, defaultColumnContext)).toEqual({
|
|
108
|
+
text: "+4.35%",
|
|
109
|
+
color: mutedPositive,
|
|
110
|
+
});
|
|
111
|
+
const unchanged = createFinancials({ quote: { change: 0, changePercent: 0, previousClose: 120, marketState: "CLOSED" } });
|
|
112
|
+
expect(getColumnValue(changePctColumn, ticker, unchanged, defaultColumnContext)).toEqual({
|
|
113
|
+
text: "0.00%",
|
|
114
|
+
color: colors.neutral,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const open = createFinancials({ quote: { marketState: "REGULAR" } });
|
|
118
|
+
expect(getColumnValue(priceColumn, ticker, open, defaultColumnContext).color).toBe(colors.positive);
|
|
119
|
+
expect(getColumnValue(changePctColumn, ticker, open, defaultColumnContext).color).toBe(colors.positive);
|
|
120
|
+
});
|
|
121
|
+
|
|
85
122
|
test("calculates portfolio totals from live quotes", () => {
|
|
86
123
|
const ticker = createTicker({
|
|
87
124
|
positions: [{ portfolio: "main", shares: 10, avgCost: 100, broker: "manual" }],
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Quote, TickerFinancials } from "../../../../types/financials";
|
|
3
|
+
import {
|
|
4
|
+
needsVisibleQuoteWarmup,
|
|
5
|
+
needsVisibleQuoteWatchdogRefresh,
|
|
6
|
+
VISIBLE_QUOTE_STREAM_MAX_AGE_MS,
|
|
7
|
+
} from "./data";
|
|
8
|
+
|
|
9
|
+
function quote(overrides: Partial<Quote> = {}): Quote {
|
|
10
|
+
return {
|
|
11
|
+
symbol: "AAPL",
|
|
12
|
+
price: 100,
|
|
13
|
+
currency: "USD",
|
|
14
|
+
change: 0,
|
|
15
|
+
changePercent: 0,
|
|
16
|
+
lastUpdated: Date.now(),
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function financials(quoteValue: Quote): TickerFinancials {
|
|
22
|
+
return {
|
|
23
|
+
annualStatements: [],
|
|
24
|
+
quarterlyStatements: [],
|
|
25
|
+
priceHistory: [],
|
|
26
|
+
quote: quoteValue,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("portfolio visible quote warmup", () => {
|
|
31
|
+
test("refreshes current-session quotes once the visible-row age window expires", () => {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const data = financials(quote({
|
|
34
|
+
lastUpdated: now - VISIBLE_QUOTE_STREAM_MAX_AGE_MS,
|
|
35
|
+
listingExchangeName: "FWB2",
|
|
36
|
+
marketState: "REGULAR",
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
expect(needsVisibleQuoteWarmup(data, now)).toBe(false);
|
|
40
|
+
expect(needsVisibleQuoteWatchdogRefresh(data, now)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("treats stale active-session quotes as visible warmup misses", () => {
|
|
44
|
+
const now = Date.parse("2026-07-07T12:10:30Z");
|
|
45
|
+
const data = financials(quote({
|
|
46
|
+
lastUpdated: Date.parse("2026-07-07T12:10:00Z"),
|
|
47
|
+
listingExchangeName: "NASDAQ",
|
|
48
|
+
marketState: "PRE",
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
expect(needsVisibleQuoteWarmup(data, now)).toBe(true);
|
|
52
|
+
expect(needsVisibleQuoteWatchdogRefresh(data, now)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -10,14 +10,32 @@ import { compareSortValues } from "../../../../utils/sort-values";
|
|
|
10
10
|
import { getSortValue, type ColumnContext } from "../metrics";
|
|
11
11
|
import type { ResolvedPortfolioAccountState } from "../summary";
|
|
12
12
|
|
|
13
|
-
export const VISIBLE_QUOTE_REFRESH_COOLDOWN_MS =
|
|
14
|
-
export const VISIBLE_QUOTE_STREAM_MAX_AGE_MS =
|
|
15
|
-
export const VISIBLE_QUOTE_STREAM_WATCHDOG_MS =
|
|
13
|
+
export const VISIBLE_QUOTE_REFRESH_COOLDOWN_MS = 15_000;
|
|
14
|
+
export const VISIBLE_QUOTE_STREAM_MAX_AGE_MS = 15_000;
|
|
15
|
+
export const VISIBLE_QUOTE_STREAM_WATCHDOG_MS = 5_000;
|
|
16
16
|
export const VISIBLE_SNAPSHOT_REFRESH_COOLDOWN_MS = 5 * 60_000;
|
|
17
17
|
export const VISIBLE_FINANCIAL_WARMUP_DELAY_MS = 350;
|
|
18
18
|
export const VISIBLE_SNAPSHOT_WARMUP_BATCH_LIMIT = 3;
|
|
19
|
+
export const SORT_QUOTE_WARMUP_BATCH_LIMIT = 64;
|
|
19
20
|
|
|
20
21
|
const STREAM_OVERSCAN_ROWS = 6;
|
|
22
|
+
const QUOTE_SORT_COLUMN_IDS = new Set([
|
|
23
|
+
"price",
|
|
24
|
+
"change",
|
|
25
|
+
"change_pct",
|
|
26
|
+
"volume",
|
|
27
|
+
"dollar_volume",
|
|
28
|
+
"range_52w",
|
|
29
|
+
"market_cap",
|
|
30
|
+
"latency",
|
|
31
|
+
"ext_hours",
|
|
32
|
+
"mkt_value",
|
|
33
|
+
"weight",
|
|
34
|
+
"day_pnl",
|
|
35
|
+
"pnl",
|
|
36
|
+
"pnl_pct",
|
|
37
|
+
"mark_delta",
|
|
38
|
+
]);
|
|
21
39
|
const FUNDAMENTAL_COLUMN_IDS = new Set(["pe", "forward_pe", "dividend_yield"]);
|
|
22
40
|
const PROFILE_COLUMN_IDS = new Set(["sector", "industry"]);
|
|
23
41
|
const sortValueCache = createRowValueCache<string, ReturnType<typeof getSortValue>>(5000);
|
|
@@ -81,6 +99,35 @@ export function selectStreamTickers(
|
|
|
81
99
|
return selectedTicker ? [...visible, selectedTicker] : visible;
|
|
82
100
|
}
|
|
83
101
|
|
|
102
|
+
export function sortPreferenceUsesQuote(sortPreference: CollectionSortPreference): boolean {
|
|
103
|
+
return !!sortPreference.columnId && QUOTE_SORT_COLUMN_IDS.has(sortPreference.columnId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function selectQuoteWarmupTickers(
|
|
107
|
+
sortedTickers: TickerRecord[],
|
|
108
|
+
visibleRange: { start: number; end: number },
|
|
109
|
+
financialsMap: Map<string, TickerFinancials>,
|
|
110
|
+
sortPreference: CollectionSortPreference,
|
|
111
|
+
now = Date.now(),
|
|
112
|
+
hiddenLimit = SORT_QUOTE_WARMUP_BATCH_LIMIT,
|
|
113
|
+
): TickerRecord[] {
|
|
114
|
+
const visible = sortedTickers.slice(visibleRange.start, visibleRange.end);
|
|
115
|
+
if (!sortPreferenceUsesQuote(sortPreference)) {
|
|
116
|
+
return visible;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const seen = new Set(visible.map((ticker) => ticker.metadata.ticker));
|
|
120
|
+
const hidden: TickerRecord[] = [];
|
|
121
|
+
for (const ticker of sortedTickers) {
|
|
122
|
+
if (seen.has(ticker.metadata.ticker)) continue;
|
|
123
|
+
if (!needsVisibleQuoteWarmup(financialsMap.get(ticker.metadata.ticker), now)) continue;
|
|
124
|
+
hidden.push(ticker);
|
|
125
|
+
seen.add(ticker.metadata.ticker);
|
|
126
|
+
if (hidden.length >= hiddenLimit) break;
|
|
127
|
+
}
|
|
128
|
+
return [...visible, ...hidden];
|
|
129
|
+
}
|
|
130
|
+
|
|
84
131
|
export function buildTrackedCurrencies(
|
|
85
132
|
tickers: TickerRecord[],
|
|
86
133
|
financialsMap: Map<string, TickerFinancials>,
|