gloomberb 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/app/runtime/desktop-menu.ts +5 -1
- package/src/app/runtime/ticker-refresh.ts +4 -1
- package/src/app.tsx +10 -1
- package/src/brokers/account-cache.ts +1 -1
- package/src/brokers/sync-broker-instance.test.ts +107 -0
- package/src/brokers/sync-broker-instance.ts +22 -10
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/components/chart/bar-chart-renderer.test.ts +108 -3
- package/src/components/chart/bar-chart-renderer.ts +226 -27
- package/src/components/chart/core/pointer.ts +32 -6
- package/src/components/chart/price-axis-labels.tsx +3 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
- package/src/components/chart/static/bar-chart-surface.tsx +151 -28
- package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
- package/src/components/chart/static/chart/surface.test.tsx +92 -0
- package/src/components/chart/static/chart/surface.tsx +167 -24
- package/src/components/chart/stock-chart/index.test.ts +25 -0
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/pane/content.tsx +20 -8
- package/src/components/layout/pane/footer/registration.tsx +6 -2
- package/src/components/layout/pane/frame.tsx +4 -4
- package/src/components/layout/pane/header.tsx +40 -4
- package/src/components/layout/pane/index.tsx +3 -1
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/cursor-occlusion.ts +196 -0
- package/src/components/layout/shell/drag/runtime.ts +9 -2
- package/src/components/layout/shell/index.test.tsx +92 -2
- package/src/components/layout/shell/index.tsx +37 -2
- package/src/components/layout/shell/layout-state.test.ts +33 -0
- package/src/components/layout/shell/layout-state.ts +4 -9
- package/src/components/layout/shell/pane/layers.tsx +8 -2
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/status-bar.test.tsx +6 -1
- package/src/components/layout/status-bar.tsx +5 -1
- package/src/components/onboarding/onboarding-steps.tsx +8 -28
- package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
- package/src/components/onboarding/onboarding-wizard.tsx +2 -4
- package/src/components/onboarding/portfolio-step/index.tsx +0 -12
- package/src/components/onboarding/portfolio-step/types.ts +1 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -5
- package/src/components/table-view-shared.tsx +12 -4
- package/src/components/ui/data-table/opentui/index.tsx +25 -12
- package/src/components/ui/data-table/types.ts +4 -0
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/page-stack-view.tsx +6 -2
- package/src/components/ui/table-layout.test.ts +25 -0
- package/src/components/ui/table-layout.ts +22 -6
- package/src/components/ui/tabs.tsx +26 -10
- package/src/components/ui/ui.test.tsx +87 -0
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
- package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
- package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
- package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
- package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
- package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
- package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
- package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
- package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
- package/src/plugins/builtin/layout-manager/index.tsx +5 -1
- package/src/plugins/builtin/options/view.test.tsx +141 -4
- package/src/plugins/builtin/options/view.tsx +76 -15
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
- package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/ibkr/broker-adapter.ts +19 -0
- package/src/plugins/ibkr/config.test.ts +31 -2
- package/src/plugins/ibkr/config.ts +9 -2
- package/src/plugins/ibkr/flex/index.test.ts +18 -9
- package/src/plugins/ibkr/flex/index.ts +30 -0
- package/src/plugins/ibkr/flex/statement-client.ts +68 -14
- package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
- package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
- package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager.test.ts +33 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +10 -4
- package/src/plugins/prediction-markets/detail/book.tsx +141 -96
- package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
- package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
- package/src/plugins/prediction-markets/detail.test.tsx +50 -0
- package/src/plugins/prediction-markets/pane.test.tsx +43 -0
- package/src/plugins/prediction-markets/pane.tsx +50 -22
- package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
- package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
- package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/styles.css +11 -0
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/ui-host.tsx +57 -25
- package/src/sources/provider-router/batches.ts +16 -2
- package/src/sources/provider-router/financial-routes.ts +5 -0
- package/src/sources/provider-router/financials.ts +11 -23
- package/src/sources/provider-router/index.test.ts +189 -0
- package/src/sources/provider-router/primary.ts +3 -2
- package/src/sources/sec-edgar.test.ts +83 -0
- package/src/sources/sec-edgar.ts +231 -0
- package/src/sources/yahoo-finance.test.ts +51 -0
- package/src/sources/yahoo-finance.ts +49 -1
- package/src/state/app/context/index.test.tsx +5 -8
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/trading.ts +2 -0
- package/src/ui/host.tsx +16 -0
- package/src/ui/index.tsx +2 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/version.ts +1 -1
- package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
|
@@ -48,4 +48,54 @@ describe("prediction markets detail views", () => {
|
|
|
48
48
|
expect(frame).not.toContain("Kalshi");
|
|
49
49
|
expect(frame).not.toContain("targets");
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
test("keeps grouped outcomes in place while selected detail is loading", async () => {
|
|
53
|
+
testSetup = await testRender(<GroupedDetailHarness loading />, {
|
|
54
|
+
width: 64,
|
|
55
|
+
height: 24,
|
|
56
|
+
});
|
|
57
|
+
await flushFrames(testSetup);
|
|
58
|
+
|
|
59
|
+
const frame = testSetup.captureCharFrame();
|
|
60
|
+
expect(frame).toContain("Outcomes");
|
|
61
|
+
expect(frame).toContain("Above 4.25%");
|
|
62
|
+
expect(frame).toContain("Loading chart...");
|
|
63
|
+
expect(frame).not.toContain("No chart history.");
|
|
64
|
+
expect(frame).not.toContain("Loading market detail...");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("renders book levels in the shared data table", async () => {
|
|
68
|
+
testSetup = await testRender(<GroupedDetailHarness detailTab="book" />, {
|
|
69
|
+
width: 64,
|
|
70
|
+
height: 24,
|
|
71
|
+
});
|
|
72
|
+
await flushFrames(testSetup);
|
|
73
|
+
|
|
74
|
+
const frame = testSetup.captureCharFrame();
|
|
75
|
+
expect(frame).toContain("OUT");
|
|
76
|
+
expect(frame).toContain("SIDE");
|
|
77
|
+
expect(frame).toContain("PRICE");
|
|
78
|
+
expect(frame).toContain("SIZE");
|
|
79
|
+
expect(frame).toContain("YES");
|
|
80
|
+
expect(frame).toContain("BID");
|
|
81
|
+
expect(frame).toContain("47c");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("renders recent trades in the shared data table", async () => {
|
|
85
|
+
testSetup = await testRender(<GroupedDetailHarness detailTab="trades" />, {
|
|
86
|
+
width: 64,
|
|
87
|
+
height: 24,
|
|
88
|
+
});
|
|
89
|
+
await flushFrames(testSetup);
|
|
90
|
+
|
|
91
|
+
const frame = testSetup.captureCharFrame();
|
|
92
|
+
expect(frame).toContain("TIME");
|
|
93
|
+
expect(frame).toContain("SIDE");
|
|
94
|
+
expect(frame).toContain("OUT");
|
|
95
|
+
expect(frame).toContain("PRICE");
|
|
96
|
+
expect(frame).toContain("SIZE");
|
|
97
|
+
expect(frame).toContain("BUY");
|
|
98
|
+
expect(frame).toContain("YES");
|
|
99
|
+
expect(frame).toContain("48c");
|
|
100
|
+
});
|
|
51
101
|
});
|
|
@@ -130,6 +130,32 @@ describe("prediction markets pane interactions", () => {
|
|
|
130
130
|
expect(frame).not.toContain("Was there a typo in the url or port?");
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
test("shows loading instead of an empty state while empty catalogs are pending", async () => {
|
|
134
|
+
attachPredictionMarketsPersistence(new MemoryPersistence());
|
|
135
|
+
|
|
136
|
+
const releaseFetches: Array<() => void> = [];
|
|
137
|
+
globalThis.fetch = (async (input: Request | string | URL) => {
|
|
138
|
+
await new Promise<void>((resolve) => {
|
|
139
|
+
releaseFetches.push(resolve);
|
|
140
|
+
});
|
|
141
|
+
const url = String(input);
|
|
142
|
+
if (url.includes("/trade-api/v2/events?")) {
|
|
143
|
+
return new Response(JSON.stringify({ events: [] }), { status: 200 });
|
|
144
|
+
}
|
|
145
|
+
return new Response(JSON.stringify([]), { status: 200 });
|
|
146
|
+
}) as unknown as typeof fetch;
|
|
147
|
+
|
|
148
|
+
testSetup = await testRender(<Harness />, { width: 120, height: 34 });
|
|
149
|
+
await flushFrames(testSetup);
|
|
150
|
+
|
|
151
|
+
const frame = testSetup.captureCharFrame();
|
|
152
|
+
expect(frame).toContain("Loading markets...");
|
|
153
|
+
expect(frame).not.toContain("No markets matched.");
|
|
154
|
+
|
|
155
|
+
for (const release of releaseFetches) release();
|
|
156
|
+
await flushFrames(testSetup);
|
|
157
|
+
});
|
|
158
|
+
|
|
133
159
|
test("selects a market on single click and opens detail on double click", async () => {
|
|
134
160
|
installPredictionMarketMocks();
|
|
135
161
|
|
|
@@ -172,6 +198,23 @@ describe("prediction markets pane interactions", () => {
|
|
|
172
198
|
|
|
173
199
|
frame = testSetup.captureCharFrame();
|
|
174
200
|
expect(frame).toContain("Kalshi primary rule");
|
|
201
|
+
expect(frame).toContain("\u2190 Back Will the Fed cut rates?");
|
|
202
|
+
expect(frame.match(/Will the Fed cut rates\?/g) ?? []).toHaveLength(1);
|
|
203
|
+
expect(frame).not.toContain("[/]search");
|
|
204
|
+
expect(frame).not.toContain("[w]atch");
|
|
205
|
+
expect(frame).not.toContain("[1-4]browse");
|
|
206
|
+
|
|
207
|
+
const metricsHeader = frame
|
|
208
|
+
.split("\n")
|
|
209
|
+
.find((line) =>
|
|
210
|
+
line.includes("YES") &&
|
|
211
|
+
line.includes("NO") &&
|
|
212
|
+
line.includes("24H VOL"),
|
|
213
|
+
);
|
|
214
|
+
expect(metricsHeader).toContain("TOTAL VOL");
|
|
215
|
+
expect(metricsHeader).toContain("OI");
|
|
216
|
+
expect(metricsHeader).toContain("SPREAD");
|
|
217
|
+
expect(metricsHeader).toContain("LAST");
|
|
175
218
|
});
|
|
176
219
|
|
|
177
220
|
test("focuses the pane when a market row is clicked", async () => {
|
|
@@ -2,6 +2,7 @@ import { Box, Input, Text } from "../../ui";
|
|
|
2
2
|
import { useCallback, useMemo, useRef } from "react";
|
|
3
3
|
import {
|
|
4
4
|
DataTableStackView,
|
|
5
|
+
Spinner,
|
|
5
6
|
Tabs,
|
|
6
7
|
usePaneFooter,
|
|
7
8
|
type DataTableKeyEvent,
|
|
@@ -13,6 +14,7 @@ import { colors } from "../../theme/colors";
|
|
|
13
14
|
import { PREDICTION_CATEGORY_OPTIONS } from "./categories";
|
|
14
15
|
import { usePredictionMarketsController } from "./controller";
|
|
15
16
|
import { PredictionMarketDetailPane } from "./detail/pane";
|
|
17
|
+
import { resolvePredictionDetailTitle } from "./detail/shared";
|
|
16
18
|
import { getPredictionColumnValue } from "./metrics";
|
|
17
19
|
import { BROWSE_TABS, VENUE_TABS } from "./navigation";
|
|
18
20
|
import { isPlainArrowUp, stopSearchFocusNavigation } from "../../utils/search-focus-navigation";
|
|
@@ -73,33 +75,45 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
|
|
|
73
75
|
controller.catalogStatus?.tone === "danger"
|
|
74
76
|
? colors.negative
|
|
75
77
|
: colors.borderFocused;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
id: "
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
78
|
+
const rowsLoading =
|
|
79
|
+
controller.visibleRows.length === 0 &&
|
|
80
|
+
(controller.catalogLoadCount > 0 || controller.searchLoading);
|
|
81
|
+
const detailTitle = resolvePredictionDetailTitle({
|
|
82
|
+
detail: controller.detail,
|
|
83
|
+
selectedRow: controller.selectedRow,
|
|
84
|
+
selectedSummary: controller.selectedSummary,
|
|
85
|
+
});
|
|
86
|
+
usePaneFooter("prediction-markets", () => {
|
|
87
|
+
if (controller.detailOpen) return null;
|
|
88
|
+
return {
|
|
89
|
+
info: [
|
|
90
|
+
...(controller.searchQuery.trim() ? [{ id: "search", parts: [{ text: `search: ${controller.searchQuery.trim()}`, tone: "value" as const }] }] : []),
|
|
91
|
+
...(controller.searchLoading ? [{ id: "search-loading", parts: [{ text: "searching", tone: "muted" as const }] }] : []),
|
|
92
|
+
...(controller.catalogStatus ? [{
|
|
93
|
+
id: "catalog",
|
|
94
|
+
parts: [{ text: controller.catalogStatus.message, tone: controller.catalogStatus.tone === "danger" ? "warning" as const : "muted" as const, color: catalogStatusColor }],
|
|
95
|
+
}] : []),
|
|
96
|
+
],
|
|
97
|
+
hints: [
|
|
98
|
+
{ id: "search", key: "/", label: "search", onPress: controller.actions.focusSearch },
|
|
99
|
+
{ id: "watch", key: "w", label: "atch", onPress: controller.selectedRow ? () => controller.actions.toggleWatchlist(controller.selectedRow!) : undefined, disabled: !controller.selectedRow },
|
|
100
|
+
{
|
|
101
|
+
id: "browse",
|
|
102
|
+
key: "1-4",
|
|
103
|
+
label: "browse",
|
|
104
|
+
onPress: () => {
|
|
105
|
+
const index = BROWSE_TABS.findIndex((tab) => tab.value === controller.browseTab);
|
|
106
|
+
controller.actions.selectBrowseTab(BROWSE_TABS[(index + 1) % BROWSE_TABS.length]!.value as PredictionBrowseTab);
|
|
107
|
+
},
|
|
95
108
|
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}, [
|
|
99
112
|
catalogStatusColor,
|
|
100
113
|
controller.browseTab,
|
|
101
114
|
controller.catalogStatus?.message,
|
|
102
115
|
controller.catalogStatus?.tone,
|
|
116
|
+
controller.detailOpen,
|
|
103
117
|
controller.searchLoading,
|
|
104
118
|
controller.searchQuery,
|
|
105
119
|
controller.selectedRow,
|
|
@@ -275,6 +289,7 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
|
|
|
275
289
|
detailOpen={controller.detailOpen && !!controller.selectedSummary}
|
|
276
290
|
onBack={controller.actions.closeDetail}
|
|
277
291
|
detailContent={detailContent}
|
|
292
|
+
detailTitle={detailTitle}
|
|
278
293
|
rootBefore={browseControls}
|
|
279
294
|
rootWidth={width}
|
|
280
295
|
rootHeight={height}
|
|
@@ -301,6 +316,19 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
|
|
|
301
316
|
getItemKey={(row) => row.key}
|
|
302
317
|
virtualize
|
|
303
318
|
renderCell={renderCell}
|
|
319
|
+
emptyContent={
|
|
320
|
+
rowsLoading ? (
|
|
321
|
+
<Box width="100%" paddingX={1} paddingY={1}>
|
|
322
|
+
<Spinner
|
|
323
|
+
label={
|
|
324
|
+
controller.searchQuery.trim().length > 0
|
|
325
|
+
? "Searching markets..."
|
|
326
|
+
: "Loading markets..."
|
|
327
|
+
}
|
|
328
|
+
/>
|
|
329
|
+
</Box>
|
|
330
|
+
) : undefined
|
|
331
|
+
}
|
|
304
332
|
emptyStateTitle="No markets matched."
|
|
305
333
|
emptyStateHint="Change the venue, browse tab, or search query."
|
|
306
334
|
/>
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
attachPredictionMarketsPersistence,
|
|
25
25
|
} from "./services/fetch";
|
|
26
26
|
import { normalizeKalshiMarket } from "./services/kalshi/adapter";
|
|
27
|
+
import type { PredictionDetailTab } from "./types";
|
|
27
28
|
|
|
28
29
|
export const TEST_PANE_ID = "prediction-markets:main";
|
|
29
30
|
const originalFetch = globalThis.fetch;
|
|
@@ -408,7 +409,13 @@ export function ChartHarness({
|
|
|
408
409
|
);
|
|
409
410
|
}
|
|
410
411
|
|
|
411
|
-
export function GroupedDetailHarness(
|
|
412
|
+
export function GroupedDetailHarness({
|
|
413
|
+
detailTab = "overview",
|
|
414
|
+
loading = false,
|
|
415
|
+
}: {
|
|
416
|
+
detailTab?: PredictionDetailTab;
|
|
417
|
+
loading?: boolean;
|
|
418
|
+
} = {}) {
|
|
412
419
|
const [state, dispatch] = useReducer(
|
|
413
420
|
appReducer,
|
|
414
421
|
(() => {
|
|
@@ -475,29 +482,40 @@ export function GroupedDetailHarness() {
|
|
|
475
482
|
<AppContext value={{ state, dispatch }}>
|
|
476
483
|
<PaneInstanceProvider paneId={TEST_PANE_ID}>
|
|
477
484
|
<PredictionMarketDetailPane
|
|
478
|
-
detail={
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
485
|
+
detail={
|
|
486
|
+
loading
|
|
487
|
+
? null
|
|
488
|
+
: {
|
|
489
|
+
summary,
|
|
490
|
+
siblings: selectedRow!.markets.map((market) => ({
|
|
491
|
+
key: market.key,
|
|
492
|
+
marketId: market.marketId,
|
|
493
|
+
label: market.marketLabel,
|
|
494
|
+
yesPrice: market.yesPrice,
|
|
495
|
+
volume24h: market.volume24h,
|
|
496
|
+
})),
|
|
497
|
+
rules: [],
|
|
498
|
+
history: [],
|
|
499
|
+
book: {
|
|
500
|
+
yesBids: [{ price: 0.47, size: 120 }],
|
|
501
|
+
yesAsks: [{ price: 0.49, size: 95 }],
|
|
502
|
+
noBids: [{ price: 0.51, size: 80 }],
|
|
503
|
+
noAsks: [{ price: 0.53, size: 105 }],
|
|
504
|
+
lastTradePrice: null,
|
|
505
|
+
},
|
|
506
|
+
trades: [{
|
|
507
|
+
id: "trade-1",
|
|
508
|
+
timestamp: Date.parse("2026-04-01T12:34:56Z"),
|
|
509
|
+
side: "buy",
|
|
510
|
+
outcome: "yes",
|
|
511
|
+
price: 0.48,
|
|
512
|
+
size: 50,
|
|
513
|
+
}],
|
|
514
|
+
}
|
|
515
|
+
}
|
|
498
516
|
detailError={null}
|
|
499
|
-
detailLoadCount={0}
|
|
500
|
-
detailTab=
|
|
517
|
+
detailLoadCount={loading ? 1 : 0}
|
|
518
|
+
detailTab={detailTab}
|
|
501
519
|
detailWidth={58}
|
|
502
520
|
focused
|
|
503
521
|
height={24}
|
|
@@ -20,7 +20,7 @@ import { resolvePaneBodyFrame } from "../../../components/layout/pane/sizing";
|
|
|
20
20
|
import { getPaneDisplayTitle } from "../../../components/layout/pane/title";
|
|
21
21
|
import type { AppConfig } from "../../../types/config";
|
|
22
22
|
import type { CachedFinancialsTarget, DataProvider, QuoteSubscriptionTarget } from "../../../types/data-provider";
|
|
23
|
-
import type { TickerFinancials } from "../../../types/financials";
|
|
23
|
+
import type { OptionsChain, TickerFinancials } from "../../../types/financials";
|
|
24
24
|
import type { TickerRecord } from "../../../types/ticker";
|
|
25
25
|
import type { AppState, PaneRuntimeState } from "../../../core/state/app/state";
|
|
26
26
|
import type { PaneDef } from "../../../types/plugin";
|
|
@@ -32,6 +32,7 @@ interface CliPaneShotPayload {
|
|
|
32
32
|
heightCells: number;
|
|
33
33
|
tickers: TickerRecord[];
|
|
34
34
|
financials: Array<[string, TickerFinancials]>;
|
|
35
|
+
optionsChains: Array<[string, OptionsChain]>;
|
|
35
36
|
paneState: Record<string, PaneRuntimeState>;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -56,6 +57,7 @@ const TRACKED_RESPONSE_METHODS = new Set<PropertyKey>([
|
|
|
56
57
|
|
|
57
58
|
let pendingShotWork = 0;
|
|
58
59
|
let didInstallShotFetchTracker = false;
|
|
60
|
+
let shotDataProvider: DataProvider | null = null;
|
|
59
61
|
|
|
60
62
|
const rendererHost: RendererHost = {
|
|
61
63
|
requestExit() {},
|
|
@@ -152,6 +154,7 @@ function waitForShotReadiness(): () => void {
|
|
|
152
154
|
|
|
153
155
|
function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
|
|
154
156
|
const financials = new Map(payload.financials.map(([symbol, data]) => [normalizeSymbol(symbol), data]));
|
|
157
|
+
const optionsChains = new Map((payload.optionsChains ?? []).map(([symbol, data]) => [normalizeSymbol(symbol), data]));
|
|
155
158
|
|
|
156
159
|
const getFinancials = (symbol: string) => {
|
|
157
160
|
const data = financials.get(normalizeSymbol(symbol));
|
|
@@ -187,6 +190,13 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
|
|
|
187
190
|
getExchangeRate() {
|
|
188
191
|
return resolveShotWork(1);
|
|
189
192
|
},
|
|
193
|
+
getOptionsChain(ticker) {
|
|
194
|
+
return trackShotWork(Promise.resolve().then(() => {
|
|
195
|
+
const chain = optionsChains.get(normalizeSymbol(ticker));
|
|
196
|
+
if (!chain) throw new Error(`No screenshot options data available for ${ticker}.`);
|
|
197
|
+
return chain;
|
|
198
|
+
}));
|
|
199
|
+
},
|
|
190
200
|
search(query) {
|
|
191
201
|
const normalized = normalizeSymbol(query);
|
|
192
202
|
return resolveShotWork(payload.tickers
|
|
@@ -217,6 +227,7 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
|
|
|
217
227
|
|
|
218
228
|
function installShotMarketData(payload: CliPaneShotPayload): void {
|
|
219
229
|
const provider = createShotDataProvider(payload);
|
|
230
|
+
shotDataProvider = provider;
|
|
220
231
|
const coordinator = new MarketDataCoordinator(provider);
|
|
221
232
|
coordinator.primeCachedFinancials(payload.tickers.flatMap((ticker) => {
|
|
222
233
|
const financials = payload.financials.find(([symbol]) => normalizeSymbol(symbol) === normalizeSymbol(ticker.metadata.ticker))?.[1];
|
|
@@ -236,7 +247,7 @@ function createRuntime(payload: CliPaneShotPayload): PluginRuntimeAccess {
|
|
|
236
247
|
return (payload.config.pluginConfig[pluginId]?.[key] as T | undefined) ?? null;
|
|
237
248
|
}
|
|
238
249
|
return {
|
|
239
|
-
getMarketData: () =>
|
|
250
|
+
getMarketData: () => shotDataProvider,
|
|
240
251
|
getCapability: () => null,
|
|
241
252
|
getBrokerAdapter: () => null,
|
|
242
253
|
connectBrokerInstance: async () => {},
|
|
@@ -59,6 +59,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
59
59
|
isSelected,
|
|
60
60
|
onSelect,
|
|
61
61
|
onActivate,
|
|
62
|
+
onTableMouseDown,
|
|
62
63
|
onRowMouseDown,
|
|
63
64
|
onRowContextMenu,
|
|
64
65
|
rowContextMenuSurface = false,
|
|
@@ -71,6 +72,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
71
72
|
emptyStateHint,
|
|
72
73
|
virtualize = true,
|
|
73
74
|
overscan = 3,
|
|
75
|
+
columnGap = 1,
|
|
76
|
+
horizontalPadding = 1,
|
|
77
|
+
fillAvailableWidth = true,
|
|
74
78
|
showHorizontalScrollbar = true,
|
|
75
79
|
scrollToIndex,
|
|
76
80
|
scrollToIndexAlign = "nearest",
|
|
@@ -85,10 +89,13 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
85
89
|
const bodyVertical = useScrollbarState(true);
|
|
86
90
|
const [viewportWidth, setViewportWidth] = useState(0);
|
|
87
91
|
const [scrollbarActive, markScrollbarActive] = useScrollbarActivity();
|
|
88
|
-
const tableWidth = useMemo(
|
|
92
|
+
const tableWidth = useMemo(
|
|
93
|
+
() => getTableWidth(columns, columnGap, horizontalPadding),
|
|
94
|
+
[columnGap, columns, horizontalPadding],
|
|
95
|
+
);
|
|
89
96
|
const gridTemplateColumns = useMemo(
|
|
90
|
-
() => buildTableGridTemplateColumns(columns),
|
|
91
|
-
[columns],
|
|
97
|
+
() => buildTableGridTemplateColumns(columns, fillAvailableWidth),
|
|
98
|
+
[columns, fillAvailableWidth],
|
|
92
99
|
);
|
|
93
100
|
const selectRow = useCallback((item: T, index: number) => {
|
|
94
101
|
onSelect(item, index);
|
|
@@ -136,8 +143,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
136
143
|
: WEB_CELL_HEIGHT + items.length * WEB_CELL_HEIGHT;
|
|
137
144
|
const bodyAfterHeight = bodyAfter ? WEB_CELL_HEIGHT * 6 : 0;
|
|
138
145
|
const horizontalScrollEnabled = showHorizontalScrollbar
|
|
139
|
-
&& hasMeaningfulTableHorizontalOverflow(tableWidth, viewportWidth);
|
|
146
|
+
&& hasMeaningfulTableHorizontalOverflow(tableWidth, viewportWidth, columnGap);
|
|
140
147
|
const scrollContentWidth = horizontalScrollEnabled
|
|
148
|
+
|| !fillAvailableWidth
|
|
141
149
|
? Math.max(1, tableWidth * WEB_CELL_WIDTH)
|
|
142
150
|
: "100%";
|
|
143
151
|
const measureViewportWidth = useCallback(() => {
|
|
@@ -247,6 +255,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
247
255
|
style={bodyScrollerStyle}
|
|
248
256
|
onMouseDown={() => {
|
|
249
257
|
focusPane();
|
|
258
|
+
onTableMouseDown?.({});
|
|
250
259
|
}}
|
|
251
260
|
onMouseLeave={() => {
|
|
252
261
|
if (hoveredIdx !== null) setHoveredIdx(null);
|
|
@@ -270,7 +279,10 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
270
279
|
>
|
|
271
280
|
<WebDataTableHeader
|
|
272
281
|
columns={columns}
|
|
282
|
+
columnGap={columnGap}
|
|
283
|
+
horizontalPadding={horizontalPadding}
|
|
273
284
|
focusPane={focusPane}
|
|
285
|
+
onTableMouseDown={onTableMouseDown}
|
|
274
286
|
gridTemplateColumns={gridTemplateColumns}
|
|
275
287
|
onHeaderClick={onHeaderClick}
|
|
276
288
|
sortColumnId={sortColumnId}
|
|
@@ -314,7 +326,10 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
314
326
|
item={item}
|
|
315
327
|
itemKey={itemKey}
|
|
316
328
|
columns={columns}
|
|
329
|
+
columnGap={columnGap}
|
|
330
|
+
horizontalPadding={horizontalPadding}
|
|
317
331
|
focusPane={focusPane}
|
|
332
|
+
onTableMouseDown={onTableMouseDown}
|
|
318
333
|
gridTemplateColumns={gridTemplateColumns}
|
|
319
334
|
onActivateRow={onActivate ? activateRow : undefined}
|
|
320
335
|
onRowContextMenu={onRowContextMenu}
|
|
@@ -44,16 +44,30 @@ function contentJustifyForAlign(align: string | undefined): CSSProperties["justi
|
|
|
44
44
|
return "flex-start";
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function columnGapCss(columnGap: number): CSSProperties["columnGap"] {
|
|
48
|
+
return columnGap === 0 ? 0 : `calc(${columnGap} * var(--cell-w))`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inlinePaddingPx(horizontalPadding: number): number {
|
|
52
|
+
return TABLE_INLINE_PADDING_PX * horizontalPadding;
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
export function WebDataTableHeader<C extends DataTableColumn>({
|
|
48
56
|
columns,
|
|
57
|
+
columnGap,
|
|
58
|
+
horizontalPadding,
|
|
49
59
|
focusPane,
|
|
60
|
+
onTableMouseDown,
|
|
50
61
|
gridTemplateColumns,
|
|
51
62
|
onHeaderClick,
|
|
52
63
|
sortColumnId,
|
|
53
64
|
sortDirection,
|
|
54
65
|
}: {
|
|
55
66
|
columns: C[];
|
|
67
|
+
columnGap: number;
|
|
68
|
+
horizontalPadding: number;
|
|
56
69
|
focusPane: () => void;
|
|
70
|
+
onTableMouseDown?: (event: any) => void;
|
|
57
71
|
gridTemplateColumns: string;
|
|
58
72
|
onHeaderClick: (columnId: string) => void;
|
|
59
73
|
sortColumnId: string | null;
|
|
@@ -69,13 +83,13 @@ export function WebDataTableHeader<C extends DataTableColumn>({
|
|
|
69
83
|
zIndex: 2,
|
|
70
84
|
display: "grid",
|
|
71
85
|
gridTemplateColumns,
|
|
72
|
-
columnGap:
|
|
86
|
+
columnGap: columnGapCss(columnGap),
|
|
73
87
|
alignItems: "center",
|
|
74
88
|
width: "100%",
|
|
75
89
|
minWidth: 0,
|
|
76
90
|
height: WEB_CELL_HEIGHT,
|
|
77
|
-
paddingLeft:
|
|
78
|
-
paddingRight:
|
|
91
|
+
paddingLeft: inlinePaddingPx(horizontalPadding),
|
|
92
|
+
paddingRight: inlinePaddingPx(horizontalPadding),
|
|
79
93
|
boxSizing: "border-box",
|
|
80
94
|
backgroundColor: CSS_PANEL,
|
|
81
95
|
}}
|
|
@@ -99,6 +113,7 @@ export function WebDataTableHeader<C extends DataTableColumn>({
|
|
|
99
113
|
}}
|
|
100
114
|
onMouseDown={(event) => {
|
|
101
115
|
focusPane();
|
|
116
|
+
onTableMouseDown?.(event);
|
|
102
117
|
event.preventDefault();
|
|
103
118
|
onHeaderClick(column.id);
|
|
104
119
|
}}
|
|
@@ -128,7 +143,10 @@ function WebDataTableRowInner<
|
|
|
128
143
|
C extends DataTableColumn,
|
|
129
144
|
>({
|
|
130
145
|
columns,
|
|
146
|
+
columnGap,
|
|
147
|
+
horizontalPadding,
|
|
131
148
|
focusPane,
|
|
149
|
+
onTableMouseDown,
|
|
132
150
|
onActivateRow,
|
|
133
151
|
onRowContextMenu,
|
|
134
152
|
onRowMouseDown,
|
|
@@ -148,7 +166,10 @@ function WebDataTableRowInner<
|
|
|
148
166
|
setHoveredIdx,
|
|
149
167
|
}: {
|
|
150
168
|
columns: C[];
|
|
169
|
+
columnGap: number;
|
|
170
|
+
horizontalPadding: number;
|
|
151
171
|
focusPane: () => void;
|
|
172
|
+
onTableMouseDown?: (event: any) => void;
|
|
152
173
|
onActivateRow?: (item: T, index: number) => void;
|
|
153
174
|
onRowContextMenu?: DataTableProps<T, C>["onRowContextMenu"];
|
|
154
175
|
onRowMouseDown?: DataTableProps<T, C>["onRowMouseDown"];
|
|
@@ -177,13 +198,13 @@ function WebDataTableRowInner<
|
|
|
177
198
|
transform: `translateY(${rowStart}px)`,
|
|
178
199
|
display: "grid",
|
|
179
200
|
gridTemplateColumns,
|
|
180
|
-
columnGap:
|
|
201
|
+
columnGap: columnGapCss(columnGap),
|
|
181
202
|
alignItems: "center",
|
|
182
203
|
width: "100%",
|
|
183
204
|
minWidth: 0,
|
|
184
205
|
height: rowSize,
|
|
185
|
-
paddingLeft:
|
|
186
|
-
paddingRight:
|
|
206
|
+
paddingLeft: inlinePaddingPx(horizontalPadding),
|
|
207
|
+
paddingRight: inlinePaddingPx(horizontalPadding),
|
|
187
208
|
boxSizing: "border-box",
|
|
188
209
|
lineHeight: "var(--cell-h)",
|
|
189
210
|
};
|
|
@@ -199,6 +220,7 @@ function WebDataTableRowInner<
|
|
|
199
220
|
}}
|
|
200
221
|
onMouseDown={(event) => {
|
|
201
222
|
focusPane();
|
|
223
|
+
onTableMouseDown?.(event);
|
|
202
224
|
event.preventDefault();
|
|
203
225
|
}}
|
|
204
226
|
>
|
|
@@ -243,6 +265,7 @@ function WebDataTableRowInner<
|
|
|
243
265
|
}}
|
|
244
266
|
onMouseDown={(event) => {
|
|
245
267
|
focusPane();
|
|
268
|
+
onTableMouseDown?.(event);
|
|
246
269
|
if (onRowMouseDown?.(item, index, eventWithCellCoordinates(event)) === true) {
|
|
247
270
|
return;
|
|
248
271
|
}
|
|
@@ -274,6 +297,7 @@ function WebDataTableRowInner<
|
|
|
274
297
|
}}
|
|
275
298
|
onMouseDown={(event) => {
|
|
276
299
|
focusPane();
|
|
300
|
+
onTableMouseDown?.(event);
|
|
277
301
|
if (cell.onMouseDown) {
|
|
278
302
|
cell.onMouseDown(eventWithCellCoordinates(event));
|
|
279
303
|
return;
|
|
@@ -84,6 +84,7 @@ export function WebTextField({
|
|
|
84
84
|
inputRef,
|
|
85
85
|
onChange,
|
|
86
86
|
onSubmit,
|
|
87
|
+
onBlur,
|
|
87
88
|
hint,
|
|
88
89
|
type = "text",
|
|
89
90
|
variant = "default",
|
|
@@ -155,7 +156,12 @@ export function WebTextField({
|
|
|
155
156
|
}}
|
|
156
157
|
onInput={onChange}
|
|
157
158
|
onChange={onChange}
|
|
158
|
-
onSubmit={() => onSubmit?.(
|
|
159
|
+
onSubmit={(nextValue?: string) => onSubmit?.(
|
|
160
|
+
typeof nextValue === "string" ? nextValue : resolvedInputRef.current?.editBuffer.getText() ?? value ?? "",
|
|
161
|
+
)}
|
|
162
|
+
onBlur={(nextValue?: string) => onBlur?.(
|
|
163
|
+
typeof nextValue === "string" ? nextValue : resolvedInputRef.current?.editBuffer.getText() ?? value ?? "",
|
|
164
|
+
)}
|
|
159
165
|
/>
|
|
160
166
|
</Box>
|
|
161
167
|
{hint && (
|
|
@@ -347,14 +353,14 @@ export function WebPageStackView({
|
|
|
347
353
|
|
|
348
354
|
if (!detailOpen) {
|
|
349
355
|
return (
|
|
350
|
-
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
356
|
+
<Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
|
|
351
357
|
{rootContent}
|
|
352
358
|
</Box>
|
|
353
359
|
);
|
|
354
360
|
}
|
|
355
361
|
|
|
356
362
|
return (
|
|
357
|
-
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
363
|
+
<Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
|
|
358
364
|
<Box
|
|
359
365
|
flexDirection="row"
|
|
360
366
|
alignItems="flex-start"
|
|
@@ -370,7 +376,7 @@ export function WebPageStackView({
|
|
|
370
376
|
flexDirection="row"
|
|
371
377
|
alignItems="center"
|
|
372
378
|
backgroundColor={panelFill()}
|
|
373
|
-
onMouseDown={(event) => {
|
|
379
|
+
onMouseDown={(event: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
374
380
|
event.preventDefault?.();
|
|
375
381
|
event.stopPropagation?.();
|
|
376
382
|
onBack();
|
|
@@ -413,7 +419,7 @@ export function WebPageStackView({
|
|
|
413
419
|
</Text>
|
|
414
420
|
) : null}
|
|
415
421
|
</Box>
|
|
416
|
-
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
422
|
+
<Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
|
|
417
423
|
{detailContent}
|
|
418
424
|
</Box>
|
|
419
425
|
</Box>
|