gloomberb 0.9.2 → 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 +24 -2
- package/bin/gloomberb +1 -1
- 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 +20 -8
- 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/capabilities/registry.ts +1 -0
- package/src/capabilities/types.ts +23 -0
- package/src/cli/commands/api.ts +119 -0
- package/src/cli/commands/automation.ts +173 -0
- package/src/cli/commands/command-utils.ts +53 -0
- package/src/cli/commands/market.ts +527 -0
- package/src/cli/commands/overview.ts +222 -0
- package/src/cli/commands/search.ts +39 -0
- package/src/cli/commands/system.ts +553 -0
- package/src/cli/commands/ticker.ts +104 -12
- package/src/cli/context.ts +35 -2
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/entry.ts +51 -0
- package/src/cli/errors.ts +56 -5
- package/src/cli/index.ts +73 -11
- package/src/cli/options.test.ts +38 -0
- package/src/cli/options.ts +95 -0
- package/src/cli/pane-functions/index.ts +10 -1
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/cli/registry.test.ts +40 -0
- package/src/cli/registry.ts +46 -5
- package/src/cli/result.test.ts +82 -0
- package/src/cli/result.ts +149 -0
- package/src/cli/types.ts +11 -0
- package/src/cli.test.ts +11 -11
- 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/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
- package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
- package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
- package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
- package/src/components/chart/core/pane-settings.test.ts +27 -0
- package/src/components/chart/core/pane-settings.ts +60 -1
- package/src/components/chart/core/pointer.ts +34 -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/chart/stock-chart/resolved/runtime.ts +1 -1
- package/src/components/chart/stock-chart/settings.ts +7 -3
- package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
- package/src/components/command-bar/surface/pane.test.tsx +76 -0
- package/src/components/data-table/view.tsx +13 -2
- package/src/components/feed-data-table/stack-view.tsx +9 -1
- package/src/components/index.ts +1 -1
- package/src/components/input-search-bar.test.tsx +103 -0
- package/src/components/input-search-bar.tsx +16 -0
- 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/active-drag.ts +5 -1
- package/src/components/layout/shell/cursor-occlusion.ts +196 -0
- package/src/components/layout/shell/drag/runtime.ts +28 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +487 -4
- package/src/components/layout/shell/index.tsx +165 -4
- 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/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +34 -16
- package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
- package/src/components/layout/shell/shortcuts.ts +4 -0
- package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/shell/window-mode/index.ts +11 -3
- package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
- package/src/components/layout/status-bar.test.tsx +111 -1
- package/src/components/layout/status-bar.tsx +37 -7
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-edit/mode.test.ts +76 -5
- package/src/components/layout/window-edit/mode.ts +47 -2
- package/src/components/layout/window-edit/presentation.ts +2 -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/price-performance.tsx +9 -1
- 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/app-services.ts +7 -4
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/data/config/store/index.ts +1 -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/cloud-tweets/pane.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
- package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
- 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/news/wire/read-state.ts +9 -32
- 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/cli/portfolio-command.ts +23 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- 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/shared/read-state.ts +43 -0
- package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
- package/src/plugins/builtin/substack/article-detail.tsx +39 -15
- package/src/plugins/builtin/substack/article-stack.tsx +6 -2
- package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
- package/src/plugins/builtin/substack/pane.tsx +8 -3
- package/src/plugins/builtin/substack/read-state.ts +55 -0
- package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
- 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/docking.ts +9 -2
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager/queries.ts +8 -2
- 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/controller/index.ts +2 -0
- package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
- 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 +75 -0
- package/src/plugins/prediction-markets/pane.tsx +71 -23
- package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
- package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/index.ts +13 -0
- package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
- package/src/renderers/electrobun/bun/window/focus.ts +5 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- 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/ascii-text.ts +50 -0
- 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/host/text.test.ts +22 -0
- package/src/renderers/electrobun/view/host/text.tsx +24 -20
- package/src/renderers/electrobun/view/styles.css +11 -0
- package/src/renderers/electrobun/view/ui-host.tsx +1 -1
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/start.tsx +13 -5
- 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/state/app/input-capture.ts +12 -0
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/plugin.ts +28 -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/cli-output.ts +6 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- package/src/version.ts +1 -1
- package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { Box, Text, TextAttributes, type InputRenderable } from "../../../ui";
|
|
3
|
+
import type { KeyEventLike } from "../../../react/input";
|
|
4
|
+
import { colors } from "../../../theme/colors";
|
|
5
|
+
import { NumberField } from "../../../components/ui";
|
|
6
|
+
import { formatCurrency, formatNumber, formatPercentRaw } from "../../../utils/format";
|
|
7
|
+
import { isPlainKey } from "../../../utils/keyboard";
|
|
8
|
+
import type { SensitivityGrid } from "./model";
|
|
9
|
+
import type { InlineField } from "./fields";
|
|
10
|
+
export type { InlineField } from "./fields";
|
|
11
|
+
|
|
12
|
+
export function truncateText(value: string, maxWidth: number): string {
|
|
13
|
+
if (maxWidth <= 0) return "";
|
|
14
|
+
if (value.length <= maxWidth) return value;
|
|
15
|
+
if (maxWidth <= 1) return value.slice(0, maxWidth);
|
|
16
|
+
return `${value.slice(0, maxWidth - 1)}…`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatPct(value: number, decimals = 1): string {
|
|
20
|
+
return `${formatNumber(value * 100, decimals)}%`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatSignedPct(value: number): string {
|
|
24
|
+
return formatPercentRaw(value * 100);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isPlainShortcut(event: KeyEventLike, ...names: string[]): boolean {
|
|
28
|
+
if (isPlainKey(event, ...names)) return true;
|
|
29
|
+
const key = event.key?.toLowerCase();
|
|
30
|
+
return !event.ctrl && !event.meta && !event.super && !event.alt && names.includes(key);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parsePromptNumber(value: string): number | null {
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatInputNumber(value: number, percent = false): string {
|
|
39
|
+
const scaled = percent ? value * 100 : value;
|
|
40
|
+
const decimals = percent
|
|
41
|
+
? Math.abs(scaled) >= 10 ? 1 : 2
|
|
42
|
+
: Math.abs(scaled) >= 100 ? 0 : 2;
|
|
43
|
+
return formatNumber(scaled, decimals).replace(/,/g, "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function InlineFieldView({
|
|
47
|
+
field,
|
|
48
|
+
active,
|
|
49
|
+
width,
|
|
50
|
+
focused,
|
|
51
|
+
onFocus,
|
|
52
|
+
}: {
|
|
53
|
+
field: InlineField;
|
|
54
|
+
active: boolean;
|
|
55
|
+
width: number;
|
|
56
|
+
focused: boolean;
|
|
57
|
+
onFocus: () => void;
|
|
58
|
+
}) {
|
|
59
|
+
const labelWidth = Math.min(12, Math.max(7, Math.floor(width * 0.38)));
|
|
60
|
+
const suffixWidth = field.suffix ? field.suffix.length + 1 : field.percent ? 2 : 0;
|
|
61
|
+
const valueWidth = Math.max(5, width - labelWidth - suffixWidth - 2);
|
|
62
|
+
const inputNodeRef = useRef<InputRenderable | null>(null);
|
|
63
|
+
const displayValue = field.valueText ?? formatInputNumber(field.value ?? 0, field.percent);
|
|
64
|
+
const fieldRef = useRef(field);
|
|
65
|
+
const displayValueRef = useRef(displayValue);
|
|
66
|
+
const [text, setText] = useState(displayValue);
|
|
67
|
+
const latestTextRef = useRef(displayValue);
|
|
68
|
+
const committedTextRef = useRef(displayValue);
|
|
69
|
+
const wasActiveRef = useRef(active);
|
|
70
|
+
const dirtyRef = useRef(false);
|
|
71
|
+
fieldRef.current = field;
|
|
72
|
+
displayValueRef.current = displayValue;
|
|
73
|
+
const focusInput = useCallback(() => {
|
|
74
|
+
const input = inputNodeRef.current;
|
|
75
|
+
try {
|
|
76
|
+
input?.focus?.();
|
|
77
|
+
input?.setCursorOffset?.(0);
|
|
78
|
+
} catch {
|
|
79
|
+
// Renderer teardown can race queued focus attempts.
|
|
80
|
+
}
|
|
81
|
+
}, []);
|
|
82
|
+
const fg = active
|
|
83
|
+
? colors.selectedText
|
|
84
|
+
: field.tone === "positive"
|
|
85
|
+
? colors.positive
|
|
86
|
+
: field.tone === "negative"
|
|
87
|
+
? colors.negative
|
|
88
|
+
: colors.text;
|
|
89
|
+
|
|
90
|
+
const commitText = useCallback((nextText: string): string | null => {
|
|
91
|
+
const currentField = fieldRef.current;
|
|
92
|
+
if (nextText.trim() === "") {
|
|
93
|
+
currentField.onClear?.();
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const parsed = parsePromptNumber(nextText);
|
|
97
|
+
if (parsed == null) return null;
|
|
98
|
+
const nextValue = currentField.percent ? parsed / 100 : parsed;
|
|
99
|
+
currentField.onValue?.(nextValue);
|
|
100
|
+
return formatInputNumber(nextValue, currentField.percent);
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
const commitEditText = useCallback((nextText: string, fallbackText = displayValue) => {
|
|
104
|
+
latestTextRef.current = nextText;
|
|
105
|
+
const committedText = commitText(nextText);
|
|
106
|
+
committedTextRef.current = committedText ?? fallbackText;
|
|
107
|
+
setText(committedTextRef.current);
|
|
108
|
+
dirtyRef.current = false;
|
|
109
|
+
}, [commitText, displayValue]);
|
|
110
|
+
|
|
111
|
+
const getLiveInputText = useCallback(() => {
|
|
112
|
+
const input = inputNodeRef.current;
|
|
113
|
+
if (!input) return latestTextRef.current;
|
|
114
|
+
try {
|
|
115
|
+
return input.editBuffer.getText();
|
|
116
|
+
} catch {
|
|
117
|
+
return latestTextRef.current;
|
|
118
|
+
}
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const commitLiveInputText = useCallback(() => {
|
|
122
|
+
const liveText = getLiveInputText();
|
|
123
|
+
const nextText = typeof liveText === "string" ? liveText : latestTextRef.current;
|
|
124
|
+
if (!dirtyRef.current && (nextText.trim() === "" || nextText === committedTextRef.current)) return;
|
|
125
|
+
latestTextRef.current = nextText;
|
|
126
|
+
const committedText = commitText(nextText);
|
|
127
|
+
committedTextRef.current = committedText ?? displayValueRef.current;
|
|
128
|
+
dirtyRef.current = false;
|
|
129
|
+
}, [commitText, getLiveInputText]);
|
|
130
|
+
|
|
131
|
+
useLayoutEffect(() => {
|
|
132
|
+
const wasActive = wasActiveRef.current;
|
|
133
|
+
wasActiveRef.current = active;
|
|
134
|
+
if (!wasActive && active) {
|
|
135
|
+
dirtyRef.current = false;
|
|
136
|
+
latestTextRef.current = "";
|
|
137
|
+
committedTextRef.current = displayValue;
|
|
138
|
+
setText("");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (wasActive && !active) {
|
|
142
|
+
const nextText = dirtyRef.current ? latestTextRef.current : text;
|
|
143
|
+
if (dirtyRef.current) {
|
|
144
|
+
commitEditText(nextText);
|
|
145
|
+
} else {
|
|
146
|
+
committedTextRef.current = displayValue;
|
|
147
|
+
setText(displayValue);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (!active) {
|
|
152
|
+
latestTextRef.current = displayValue;
|
|
153
|
+
committedTextRef.current = displayValue;
|
|
154
|
+
setText(displayValue);
|
|
155
|
+
}
|
|
156
|
+
}, [active, commitEditText, displayValue, text]);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (!active) return;
|
|
160
|
+
let animationFrame: number | null = null;
|
|
161
|
+
const timeouts: ReturnType<typeof setTimeout>[] = [];
|
|
162
|
+
focusInput();
|
|
163
|
+
queueMicrotask(focusInput);
|
|
164
|
+
animationFrame = globalThis.requestAnimationFrame?.(focusInput) ?? null;
|
|
165
|
+
timeouts.push(setTimeout(focusInput, 0), setTimeout(focusInput, 32));
|
|
166
|
+
return () => {
|
|
167
|
+
if (animationFrame !== null) globalThis.cancelAnimationFrame?.(animationFrame);
|
|
168
|
+
for (const timeout of timeouts) clearTimeout(timeout);
|
|
169
|
+
};
|
|
170
|
+
}, [active, focusInput]);
|
|
171
|
+
|
|
172
|
+
useLayoutEffect(() => {
|
|
173
|
+
if (!active) return;
|
|
174
|
+
return () => {
|
|
175
|
+
commitLiveInputText();
|
|
176
|
+
};
|
|
177
|
+
}, [active, commitLiveInputText]);
|
|
178
|
+
|
|
179
|
+
const handleSubmit = (nextText: string) => {
|
|
180
|
+
commitEditText(nextText, nextText);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const handleBlur = (nextText: string) => {
|
|
184
|
+
if (!dirtyRef.current && (nextText.trim() === "" || nextText === committedTextRef.current)) return;
|
|
185
|
+
commitEditText(nextText, nextText);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (field.onPress && !field.onValue) {
|
|
189
|
+
return (
|
|
190
|
+
<Box
|
|
191
|
+
width={width}
|
|
192
|
+
height={1}
|
|
193
|
+
flexDirection="row"
|
|
194
|
+
backgroundColor={active ? colors.selected : colors.panel}
|
|
195
|
+
data-gloom-field-id={field.id}
|
|
196
|
+
onMouseDown={() => {
|
|
197
|
+
onFocus();
|
|
198
|
+
field.onPress?.();
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
<Text fg={active ? colors.selectedText : colors.textDim}>
|
|
202
|
+
{truncateText(field.label, labelWidth).padEnd(labelWidth)}
|
|
203
|
+
</Text>
|
|
204
|
+
<Text fg={fg} attributes={active ? TextAttributes.BOLD : 0}>
|
|
205
|
+
{truncateText(field.valueText ?? "", Math.max(1, width - labelWidth))}
|
|
206
|
+
</Text>
|
|
207
|
+
</Box>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<Box
|
|
213
|
+
width={width}
|
|
214
|
+
height={1}
|
|
215
|
+
flexDirection="row"
|
|
216
|
+
backgroundColor={active ? colors.selected : colors.panel}
|
|
217
|
+
data-gloom-field-id={field.id}
|
|
218
|
+
onMouseDown={() => {
|
|
219
|
+
onFocus();
|
|
220
|
+
focusInput();
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
<Text fg={active ? colors.selectedText : colors.textDim}>
|
|
224
|
+
{truncateText(field.label, labelWidth).padEnd(labelWidth)}
|
|
225
|
+
</Text>
|
|
226
|
+
{active ? (
|
|
227
|
+
<NumberField
|
|
228
|
+
inputRef={inputNodeRef}
|
|
229
|
+
focused={active}
|
|
230
|
+
value={text}
|
|
231
|
+
placeholder={displayValue}
|
|
232
|
+
allowNegative={field.allowNegative}
|
|
233
|
+
allowDecimal
|
|
234
|
+
width={valueWidth}
|
|
235
|
+
variant="plain"
|
|
236
|
+
backgroundColor={colors.selected}
|
|
237
|
+
textColor={fg}
|
|
238
|
+
placeholderColor={colors.textMuted}
|
|
239
|
+
onMouseDown={onFocus}
|
|
240
|
+
onChange={(nextText) => {
|
|
241
|
+
dirtyRef.current = true;
|
|
242
|
+
latestTextRef.current = nextText;
|
|
243
|
+
setText(nextText);
|
|
244
|
+
}}
|
|
245
|
+
onSubmit={handleSubmit}
|
|
246
|
+
onBlur={handleBlur}
|
|
247
|
+
/>
|
|
248
|
+
) : (
|
|
249
|
+
<Box width={valueWidth} height={1} onMouseDown={() => {
|
|
250
|
+
onFocus();
|
|
251
|
+
focusInput();
|
|
252
|
+
}}>
|
|
253
|
+
<Text fg={fg}>{truncateText(displayValue, valueWidth)}</Text>
|
|
254
|
+
</Box>
|
|
255
|
+
)}
|
|
256
|
+
{suffixWidth > 0 && (
|
|
257
|
+
<Text fg={active ? colors.selectedText : colors.textDim}>
|
|
258
|
+
{field.suffix ?? (field.percent ? "%" : "")}
|
|
259
|
+
</Text>
|
|
260
|
+
)}
|
|
261
|
+
</Box>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function MetricLine({
|
|
266
|
+
label,
|
|
267
|
+
value,
|
|
268
|
+
detail,
|
|
269
|
+
color,
|
|
270
|
+
width,
|
|
271
|
+
}: {
|
|
272
|
+
label: string;
|
|
273
|
+
value: string;
|
|
274
|
+
detail?: string;
|
|
275
|
+
color?: string;
|
|
276
|
+
width: number;
|
|
277
|
+
}) {
|
|
278
|
+
const labelWidth = Math.min(12, Math.max(8, Math.floor(width * 0.32)));
|
|
279
|
+
const valueWidth = Math.min(Math.max(8, width - labelWidth), Math.max(8, Math.floor(width * 0.42)));
|
|
280
|
+
const detailWidth = Math.max(0, width - labelWidth - valueWidth);
|
|
281
|
+
return (
|
|
282
|
+
<Box height={1} width={width} flexDirection="row" overflow="hidden">
|
|
283
|
+
<Box width={labelWidth} flexShrink={0}>
|
|
284
|
+
<Text fg={colors.textDim}>{label}</Text>
|
|
285
|
+
</Box>
|
|
286
|
+
<Box width={valueWidth} flexShrink={0}>
|
|
287
|
+
<Text fg={color ?? colors.text} attributes={TextAttributes.BOLD}>
|
|
288
|
+
{truncateText(value, valueWidth)}
|
|
289
|
+
</Text>
|
|
290
|
+
</Box>
|
|
291
|
+
{detail && detailWidth > 0 && (
|
|
292
|
+
<Text fg={colors.textDim}>{truncateText(detail, detailWidth)}</Text>
|
|
293
|
+
)}
|
|
294
|
+
</Box>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function SensitivityGridView({
|
|
299
|
+
width,
|
|
300
|
+
grid,
|
|
301
|
+
}: {
|
|
302
|
+
width: number;
|
|
303
|
+
grid: SensitivityGrid;
|
|
304
|
+
}) {
|
|
305
|
+
const labelWidth = 9;
|
|
306
|
+
const cellWidth = Math.max(8, Math.floor((width - labelWidth) / Math.max(1, grid.columns.length)));
|
|
307
|
+
return (
|
|
308
|
+
<Box flexDirection="column" paddingX={1}>
|
|
309
|
+
<Box height={1} flexDirection="row">
|
|
310
|
+
<Box width={labelWidth}>
|
|
311
|
+
<Text fg={colors.textDim}>{truncateText(grid.rowLabel, labelWidth)}</Text>
|
|
312
|
+
</Box>
|
|
313
|
+
{grid.columns.map((column) => (
|
|
314
|
+
<Box key={column} width={cellWidth}>
|
|
315
|
+
<Text fg={colors.textDim}>{truncateText(column, cellWidth - 1).padStart(cellWidth - 1)}</Text>
|
|
316
|
+
</Box>
|
|
317
|
+
))}
|
|
318
|
+
</Box>
|
|
319
|
+
{grid.rows.map((row, rowIndex) => (
|
|
320
|
+
<Box key={row} height={1} flexDirection="row">
|
|
321
|
+
<Box width={labelWidth}>
|
|
322
|
+
<Text fg={colors.textDim}>{truncateText(row, labelWidth)}</Text>
|
|
323
|
+
</Box>
|
|
324
|
+
{grid.cells[rowIndex]?.map((cell, cellIndex) => (
|
|
325
|
+
<Box key={`${row}:${cellIndex}`} width={cellWidth}>
|
|
326
|
+
<Text fg={colors.text}>{truncateText(cell.text, cellWidth - 1).padStart(cellWidth - 1)}</Text>
|
|
327
|
+
</Box>
|
|
328
|
+
))}
|
|
329
|
+
</Box>
|
|
330
|
+
))}
|
|
331
|
+
</Box>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function buildKellyCurveXAxisLabels(maxFraction: number): string[] {
|
|
336
|
+
if (!Number.isFinite(maxFraction) || maxFraction <= 0) return [];
|
|
337
|
+
return [0, 0.25, 0.5, 0.75, 1].map((ratio) => formatPct(maxFraction * ratio, 0));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function KellyCurveDecisionView({
|
|
341
|
+
width,
|
|
342
|
+
currentFraction,
|
|
343
|
+
targetFraction,
|
|
344
|
+
fullKellyFraction,
|
|
345
|
+
currentGrowth,
|
|
346
|
+
targetGrowth,
|
|
347
|
+
addTrimValue,
|
|
348
|
+
currency,
|
|
349
|
+
clipReasons,
|
|
350
|
+
}: {
|
|
351
|
+
width: number;
|
|
352
|
+
currentFraction: number;
|
|
353
|
+
targetFraction: number;
|
|
354
|
+
fullKellyFraction: number;
|
|
355
|
+
currentGrowth: number;
|
|
356
|
+
targetGrowth: number;
|
|
357
|
+
addTrimValue: number;
|
|
358
|
+
currency: string;
|
|
359
|
+
clipReasons: string[];
|
|
360
|
+
}) {
|
|
361
|
+
const moveLabel = addTrimValue >= 0 ? "add" : "trim";
|
|
362
|
+
const capLabel = clipReasons.length > 0 ? `cap ${clipReasons.join(", ")}` : "cap none";
|
|
363
|
+
const text = [
|
|
364
|
+
`${formatPct(currentFraction, 1)} -> ${formatPct(targetFraction, 1)}`,
|
|
365
|
+
`${moveLabel} ${formatCurrency(Math.abs(addTrimValue), currency)}`,
|
|
366
|
+
`growth ${formatSignedPct(currentGrowth)} -> ${formatSignedPct(targetGrowth)}`,
|
|
367
|
+
`full ${formatPct(fullKellyFraction, 0)}`,
|
|
368
|
+
capLabel,
|
|
369
|
+
].join(" ");
|
|
370
|
+
return (
|
|
371
|
+
<Box height={1} paddingX={1}>
|
|
372
|
+
<Text fg={colors.textDim}>{truncateText(text, Math.max(1, width - 2))}</Text>
|
|
373
|
+
</Box>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
@@ -105,7 +105,11 @@ export const layoutManagerPlugin: GloomPlugin = {
|
|
|
105
105
|
execute: async () => {
|
|
106
106
|
if (!getStateRef) return;
|
|
107
107
|
const { layout, termWidth, termHeight } = getStateRef();
|
|
108
|
-
persistLayout(ctx, gridlockAllPanes(
|
|
108
|
+
persistLayout(ctx, gridlockAllPanes(
|
|
109
|
+
layout,
|
|
110
|
+
{ x: 0, y: 0, width: termWidth, height: termHeight },
|
|
111
|
+
ctx.getPaneDef,
|
|
112
|
+
));
|
|
109
113
|
notifyGridlockComplete(ctx.notify, () => {
|
|
110
114
|
dispatchRef?.({ type: "UNDO_LAYOUT" });
|
|
111
115
|
});
|
|
@@ -1,55 +1,32 @@
|
|
|
1
1
|
import { useCallback, useMemo } from "react";
|
|
2
2
|
import { usePluginState } from "../../../runtime";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_MAX_READ_IDS,
|
|
5
|
+
markReadId,
|
|
6
|
+
normalizeReadIds,
|
|
7
|
+
sameStringArray,
|
|
8
|
+
} from "../../shared/read-state";
|
|
3
9
|
|
|
4
10
|
export interface NewsReadState {
|
|
5
11
|
articleIds: string[];
|
|
6
12
|
}
|
|
7
13
|
|
|
8
14
|
const NEWS_READ_STATE_SCHEMA_VERSION = 1;
|
|
9
|
-
export const MAX_READ_ARTICLE_IDS =
|
|
15
|
+
export const MAX_READ_ARTICLE_IDS = DEFAULT_MAX_READ_IDS;
|
|
10
16
|
|
|
11
17
|
const READ_STATE_KEY = "read-articles";
|
|
12
18
|
const EMPTY_READ_STATE: NewsReadState = { articleIds: [] };
|
|
13
19
|
|
|
14
|
-
function normalizeArticleId(articleId: string): string {
|
|
15
|
-
return articleId.trim();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function sameStringArray(left: string[], right: string[]): boolean {
|
|
19
|
-
if (left.length !== right.length) return false;
|
|
20
|
-
return left.every((value, index) => value === right[index]);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
20
|
export function normalizeNewsReadState(state: NewsReadState): NewsReadState {
|
|
24
|
-
|
|
25
|
-
const articleIds: string[] = [];
|
|
26
|
-
|
|
27
|
-
for (const rawId of state.articleIds) {
|
|
28
|
-
if (typeof rawId !== "string") continue;
|
|
29
|
-
const articleId = normalizeArticleId(rawId);
|
|
30
|
-
if (!articleId || seen.has(articleId)) continue;
|
|
31
|
-
seen.add(articleId);
|
|
32
|
-
articleIds.push(articleId);
|
|
33
|
-
if (articleIds.length >= MAX_READ_ARTICLE_IDS) break;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return { articleIds };
|
|
21
|
+
return { articleIds: normalizeReadIds(state.articleIds, MAX_READ_ARTICLE_IDS) };
|
|
37
22
|
}
|
|
38
23
|
|
|
39
24
|
export function markNewsArticleRead(
|
|
40
25
|
state: NewsReadState,
|
|
41
26
|
articleId: string,
|
|
42
27
|
): NewsReadState {
|
|
43
|
-
const normalizedId = normalizeArticleId(articleId);
|
|
44
28
|
const current = normalizeNewsReadState(state).articleIds;
|
|
45
|
-
|
|
46
|
-
return sameStringArray(current, state.articleIds) ? state : { articleIds: current };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const articleIds = [
|
|
50
|
-
normalizedId,
|
|
51
|
-
...current.filter((currentId) => currentId !== normalizedId),
|
|
52
|
-
].slice(0, MAX_READ_ARTICLE_IDS);
|
|
29
|
+
const articleIds = markReadId(current, articleId, MAX_READ_ARTICLE_IDS);
|
|
53
30
|
|
|
54
31
|
return sameStringArray(articleIds, state.articleIds) ? state : { articleIds };
|
|
55
32
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, expect, test } from "bun:test";
|
|
2
2
|
import { act } from "react";
|
|
3
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
3
4
|
import { testRender } from "../../../renderers/opentui/test-utils";
|
|
4
5
|
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../../market-data/coordinator";
|
|
5
6
|
import { AppContext, PaneInstanceProvider, createInitialState } from "../../../state/app/context";
|
|
@@ -7,6 +8,7 @@ import { createTestDataProvider } from "../../../test-support/data-provider";
|
|
|
7
8
|
import { cloneLayout, createDefaultConfig } from "../../../types/config";
|
|
8
9
|
import type { OptionContract, OptionsChain, TickerFinancials } from "../../../types/financials";
|
|
9
10
|
import type { TickerRecord } from "../../../types/ticker";
|
|
11
|
+
import { formatExpDate } from "../../../utils/options";
|
|
10
12
|
import { OptionsView } from "./view";
|
|
11
13
|
|
|
12
14
|
const TEST_PANE_ID = "ticker-detail:options-test";
|
|
@@ -49,10 +51,14 @@ function makeContract(strike: number, side: "C" | "P", atmStrike = 101): OptionC
|
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
function makeChain(
|
|
54
|
+
function makeChain(
|
|
55
|
+
strikes = [100, 101],
|
|
56
|
+
atmStrike = 101,
|
|
57
|
+
expirationDates = [1_782_345_600],
|
|
58
|
+
): OptionsChain {
|
|
53
59
|
return {
|
|
54
60
|
underlyingSymbol: "AAPL",
|
|
55
|
-
expirationDates
|
|
61
|
+
expirationDates,
|
|
56
62
|
calls: strikes.map((strike) => makeContract(strike, "C", atmStrike)),
|
|
57
63
|
puts: strikes.map((strike) => makeContract(strike, "P", atmStrike)),
|
|
58
64
|
};
|
|
@@ -74,7 +80,17 @@ function makeFinancials(price: number): TickerFinancials {
|
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
function OptionsHarness({
|
|
83
|
+
function OptionsHarness({
|
|
84
|
+
ticker,
|
|
85
|
+
quotePrice,
|
|
86
|
+
width = 122,
|
|
87
|
+
onCapture = () => {},
|
|
88
|
+
}: {
|
|
89
|
+
ticker: TickerRecord;
|
|
90
|
+
quotePrice?: number;
|
|
91
|
+
width?: number;
|
|
92
|
+
onCapture?: (capturing: boolean) => void;
|
|
93
|
+
}) {
|
|
78
94
|
const config = createDefaultConfig("/tmp/gloomberb-options-test");
|
|
79
95
|
config.layout = {
|
|
80
96
|
dockRoot: { kind: "pane", instanceId: TEST_PANE_ID },
|
|
@@ -98,7 +114,7 @@ function OptionsHarness({ ticker, quotePrice }: { ticker: TickerRecord; quotePri
|
|
|
98
114
|
return (
|
|
99
115
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
100
116
|
<PaneInstanceProvider paneId={TEST_PANE_ID}>
|
|
101
|
-
<OptionsView width={
|
|
117
|
+
<OptionsView width={width} height={14} focused onCapture={onCapture} />
|
|
102
118
|
</PaneInstanceProvider>
|
|
103
119
|
</AppContext>
|
|
104
120
|
);
|
|
@@ -183,3 +199,124 @@ test("defaults the table around the nearest strike to the current quote", async
|
|
|
183
199
|
expect(frame).toContain("120");
|
|
184
200
|
expect(frame).not.toContain(" 50 ");
|
|
185
201
|
});
|
|
202
|
+
|
|
203
|
+
test("lets the expiration tab row use the full available width", async () => {
|
|
204
|
+
const expirationDates = Array.from({ length: 9 }, (_, index) => (
|
|
205
|
+
Math.floor(Date.UTC(2026, index, 20) / 1000)
|
|
206
|
+
));
|
|
207
|
+
const provider = createTestDataProvider({
|
|
208
|
+
getOptionsChain: async () => makeChain([100, 101], 101, expirationDates),
|
|
209
|
+
});
|
|
210
|
+
setSharedMarketDataCoordinator(new MarketDataCoordinator(provider));
|
|
211
|
+
|
|
212
|
+
await act(async () => {
|
|
213
|
+
testSetup = await testRender(
|
|
214
|
+
<OptionsHarness ticker={makeTicker("AAPL")} width={122} />,
|
|
215
|
+
{
|
|
216
|
+
width: 124,
|
|
217
|
+
height: 16,
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await renderSettled();
|
|
223
|
+
|
|
224
|
+
const frame = testSetup!.captureCharFrame();
|
|
225
|
+
expect(frame).toContain(formatExpDate(expirationDates.at(-1)!));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("keeps expiration tabs independently scrollable from a narrow strike table", async () => {
|
|
229
|
+
const expirationDates = Array.from({ length: 12 }, (_, index) => (
|
|
230
|
+
Math.floor(Date.UTC(2026, index, 20) / 1000)
|
|
231
|
+
));
|
|
232
|
+
const requestedExpirations: Array<number | undefined> = [];
|
|
233
|
+
const provider = createTestDataProvider({
|
|
234
|
+
getOptionsChain: async (_ticker, _exchange, expirationDate) => {
|
|
235
|
+
requestedExpirations.push(expirationDate);
|
|
236
|
+
return makeChain([100, 101], 101, expirationDates);
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
setSharedMarketDataCoordinator(new MarketDataCoordinator(provider));
|
|
240
|
+
const captures: boolean[] = [];
|
|
241
|
+
|
|
242
|
+
await act(async () => {
|
|
243
|
+
testSetup = await testRender(
|
|
244
|
+
<OptionsHarness ticker={makeTicker("AAPL")} width={54} onCapture={(capturing) => captures.push(capturing)} />,
|
|
245
|
+
{
|
|
246
|
+
width: 56,
|
|
247
|
+
height: 16,
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await renderSettled();
|
|
253
|
+
const bodyScroll = testSetup!.renderer.root.findDescendantById("options-table-body-scroll") as ScrollBoxRenderable | undefined;
|
|
254
|
+
const expirationTabsScroll = testSetup!.renderer.root.findDescendantById("options-expiration-tabs-scroll") as ScrollBoxRenderable | undefined;
|
|
255
|
+
expect(bodyScroll?.horizontalScrollBar.visible).toBe(true);
|
|
256
|
+
expect(expirationTabsScroll?.horizontalScrollBar.visible).toBe(false);
|
|
257
|
+
expect(testSetup!.captureCharFrame()).not.toContain(formatExpDate(expirationDates.at(-1)!));
|
|
258
|
+
|
|
259
|
+
await act(async () => {
|
|
260
|
+
testSetup!.mockInput.pressEnter();
|
|
261
|
+
await testSetup!.renderOnce();
|
|
262
|
+
});
|
|
263
|
+
await renderSettled();
|
|
264
|
+
expect(captures).toContain(true);
|
|
265
|
+
expect(captures.at(-1)).toBe(true);
|
|
266
|
+
|
|
267
|
+
for (let index = 1; index < expirationDates.length; index += 1) {
|
|
268
|
+
await act(async () => {
|
|
269
|
+
testSetup!.mockInput.pressKey("l");
|
|
270
|
+
await testSetup!.renderOnce();
|
|
271
|
+
});
|
|
272
|
+
await renderSettled();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
expect(bodyScroll?.horizontalScrollBar.visible).toBe(true);
|
|
276
|
+
expect(bodyScroll?.scrollLeft ?? 0).toBe(0);
|
|
277
|
+
expect(expirationTabsScroll?.scrollLeft ?? 0).toBeGreaterThan(0);
|
|
278
|
+
expect(requestedExpirations).toContain(expirationDates.at(-1));
|
|
279
|
+
expect(testSetup!.captureCharFrame()).toContain(formatExpDate(expirationDates.at(-1)!));
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("clicking the option table focuses expiration tabs for arrow navigation", async () => {
|
|
283
|
+
const expirationDates = Array.from({ length: 3 }, (_, index) => (
|
|
284
|
+
Math.floor(Date.UTC(2026, index, 20) / 1000)
|
|
285
|
+
));
|
|
286
|
+
const requestedExpirations: Array<number | undefined> = [];
|
|
287
|
+
const provider = createTestDataProvider({
|
|
288
|
+
getOptionsChain: async (_ticker, _exchange, expirationDate) => {
|
|
289
|
+
requestedExpirations.push(expirationDate);
|
|
290
|
+
return makeChain([100, 101], 101, expirationDates);
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
setSharedMarketDataCoordinator(new MarketDataCoordinator(provider));
|
|
294
|
+
const captures: boolean[] = [];
|
|
295
|
+
|
|
296
|
+
await act(async () => {
|
|
297
|
+
testSetup = await testRender(
|
|
298
|
+
<OptionsHarness ticker={makeTicker("AAPL")} width={80} onCapture={(capturing) => captures.push(capturing)} />,
|
|
299
|
+
{
|
|
300
|
+
width: 82,
|
|
301
|
+
height: 16,
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await renderSettled();
|
|
307
|
+
|
|
308
|
+
await act(async () => {
|
|
309
|
+
await testSetup!.mockMouse.click(8, 4);
|
|
310
|
+
await testSetup!.renderOnce();
|
|
311
|
+
});
|
|
312
|
+
await renderSettled();
|
|
313
|
+
expect(captures.at(-1)).toBe(true);
|
|
314
|
+
|
|
315
|
+
await act(async () => {
|
|
316
|
+
testSetup!.mockInput.pressArrow("right");
|
|
317
|
+
await testSetup!.renderOnce();
|
|
318
|
+
});
|
|
319
|
+
await renderSettled();
|
|
320
|
+
|
|
321
|
+
expect(requestedExpirations).toContain(expirationDates[1]);
|
|
322
|
+
});
|