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
|
@@ -6,6 +6,7 @@ import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../../
|
|
|
6
6
|
import { PaneFooterBar, PaneFooterProvider } from "../../../components/layout/pane/footer";
|
|
7
7
|
import {
|
|
8
8
|
AppContext,
|
|
9
|
+
type AppAction,
|
|
9
10
|
createInitialState,
|
|
10
11
|
PaneInstanceProvider,
|
|
11
12
|
} from "../../../state/app/context";
|
|
@@ -13,7 +14,7 @@ import { Box } from "../../../ui";
|
|
|
13
14
|
import { cloneLayout, createDefaultConfig } from "../../../types/config";
|
|
14
15
|
import { createTestPluginRuntime } from "../../../test-support/plugin-runtime";
|
|
15
16
|
import type { DataProvider } from "../../../types/data-provider";
|
|
16
|
-
import type { TickerFinancials } from "../../../types/financials";
|
|
17
|
+
import type { PricePoint, TickerFinancials } from "../../../types/financials";
|
|
17
18
|
import type { PluginRegistry } from "../../registry";
|
|
18
19
|
import { PluginRenderProvider, type PluginRuntimeAccess } from "../../runtime";
|
|
19
20
|
import { setSharedMarketDataForTests, setSharedRegistryForTests } from "../../registry";
|
|
@@ -30,6 +31,12 @@ let root: ReturnType<typeof createRoot> | undefined;
|
|
|
30
31
|
let sharedCoordinator: MarketDataCoordinator | null = null;
|
|
31
32
|
const actEnvironment = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean };
|
|
32
33
|
|
|
34
|
+
interface ComparisonHarnessOptions {
|
|
35
|
+
dispatch?: (action: AppAction) => void;
|
|
36
|
+
focused?: boolean;
|
|
37
|
+
focusedPaneId?: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
function makeTicker(symbol: string, currency: string): TickerRecord {
|
|
34
41
|
return {
|
|
35
42
|
metadata: {
|
|
@@ -90,6 +97,13 @@ function makeDatedFinancials(symbol: string, currency: string, closes: Array<[st
|
|
|
90
97
|
};
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
function makeDatedPriceHistory(closes: Array<[string, number]>): PricePoint[] {
|
|
101
|
+
return closes.map(([date, close]) => ({
|
|
102
|
+
date: new Date(`${date}T00:00:00Z`),
|
|
103
|
+
close,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
function createProvider(historyBySymbol: Record<string, number[]>, currencyBySymbol: Record<string, string>): DataProvider {
|
|
94
108
|
return {
|
|
95
109
|
id: "test-provider",
|
|
@@ -139,6 +153,30 @@ function createDatedProvider(financialsBySymbol: Record<string, TickerFinancials
|
|
|
139
153
|
return provider;
|
|
140
154
|
}
|
|
141
155
|
|
|
156
|
+
function createRangeAwareDatedProvider({
|
|
157
|
+
fullHistoryBySymbol,
|
|
158
|
+
shortHistoryBySymbol,
|
|
159
|
+
}: {
|
|
160
|
+
fullHistoryBySymbol: Record<string, Array<[string, number]>>;
|
|
161
|
+
shortHistoryBySymbol: Record<string, Array<[string, number]>>;
|
|
162
|
+
}): DataProvider {
|
|
163
|
+
const financialsBySymbol = Object.fromEntries(
|
|
164
|
+
Object.entries(shortHistoryBySymbol).map(([symbol, history]) => [
|
|
165
|
+
symbol,
|
|
166
|
+
makeDatedFinancials(symbol, "USD", history),
|
|
167
|
+
]),
|
|
168
|
+
);
|
|
169
|
+
const provider = createDatedProvider(financialsBySymbol);
|
|
170
|
+
const resolveHistory = (symbol: string, range: string) => makeDatedPriceHistory(
|
|
171
|
+
range === "5Y"
|
|
172
|
+
? fullHistoryBySymbol[symbol] ?? shortHistoryBySymbol[symbol] ?? []
|
|
173
|
+
: shortHistoryBySymbol[symbol] ?? [],
|
|
174
|
+
);
|
|
175
|
+
provider.getPriceHistory = async (symbol, _exchange, range) => resolveHistory(symbol, range);
|
|
176
|
+
provider.getPriceHistoryForResolution = async (symbol, _exchange, range) => resolveHistory(symbol, range);
|
|
177
|
+
return provider;
|
|
178
|
+
}
|
|
179
|
+
|
|
142
180
|
function createRegistrySpy(spy: { selected: string[]; focused: string[] }): PluginRegistry {
|
|
143
181
|
return {
|
|
144
182
|
selectTicker: (symbol: string) => { spy.selected.push(symbol); },
|
|
@@ -171,7 +209,13 @@ function createComparisonHarness(
|
|
|
171
209
|
tickers: TickerRecord[],
|
|
172
210
|
financials: Array<[string, TickerFinancials]>,
|
|
173
211
|
runtime: PluginRuntimeAccess,
|
|
212
|
+
options: ComparisonHarnessOptions = {},
|
|
174
213
|
) {
|
|
214
|
+
const {
|
|
215
|
+
dispatch = () => {},
|
|
216
|
+
focused = true,
|
|
217
|
+
focusedPaneId = focused ? TEST_PANE_ID : null,
|
|
218
|
+
} = options;
|
|
175
219
|
const config = createDefaultConfig("/tmp/gloomberb-compare");
|
|
176
220
|
const layout = {
|
|
177
221
|
dockRoot: { kind: "pane" as const, instanceId: TEST_PANE_ID },
|
|
@@ -189,7 +233,7 @@ function createComparisonHarness(
|
|
|
189
233
|
layouts: [{ name: "Default", layout: cloneLayout(layout) }],
|
|
190
234
|
};
|
|
191
235
|
const state = createInitialState(nextConfig);
|
|
192
|
-
state.focusedPaneId =
|
|
236
|
+
state.focusedPaneId = focusedPaneId;
|
|
193
237
|
state.tickers = new Map(tickers.map((ticker) => [ticker.metadata.ticker, ticker]));
|
|
194
238
|
state.financials = new Map(financials);
|
|
195
239
|
|
|
@@ -202,7 +246,7 @@ function createComparisonHarness(
|
|
|
202
246
|
}) => ReactElement;
|
|
203
247
|
|
|
204
248
|
return (
|
|
205
|
-
<AppContext value={{ state, dispatch
|
|
249
|
+
<AppContext value={{ state, dispatch }}>
|
|
206
250
|
<PaneInstanceProvider paneId={TEST_PANE_ID}>
|
|
207
251
|
<PluginRenderProvider pluginId="comparison-chart" runtime={runtime}>
|
|
208
252
|
<PaneFooterProvider>
|
|
@@ -211,11 +255,11 @@ function createComparisonHarness(
|
|
|
211
255
|
<ComparisonPane
|
|
212
256
|
paneId={TEST_PANE_ID}
|
|
213
257
|
paneType="comparison-chart"
|
|
214
|
-
focused
|
|
258
|
+
focused={focused}
|
|
215
259
|
width={120}
|
|
216
260
|
height={19}
|
|
217
261
|
/>
|
|
218
|
-
<PaneFooterBar footer={footer} focused width={120} />
|
|
262
|
+
<PaneFooterBar footer={footer} focused={focused} width={120} />
|
|
219
263
|
</Box>
|
|
220
264
|
)}
|
|
221
265
|
</PaneFooterProvider>
|
|
@@ -241,13 +285,14 @@ async function mountComparisonHarness(
|
|
|
241
285
|
tickers: TickerRecord[],
|
|
242
286
|
financials: Array<[string, TickerFinancials]>,
|
|
243
287
|
spy: { selected: string[]; focused: string[]; settings?: string[] } = { selected: [], focused: [] },
|
|
288
|
+
options: ComparisonHarnessOptions = {},
|
|
244
289
|
) {
|
|
245
290
|
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
|
|
246
291
|
testSetup = await createTestRenderer({ width: 120, height: 20 });
|
|
247
292
|
root = createRoot(testSetup.renderer);
|
|
248
293
|
await act(async () => {
|
|
249
294
|
root!.render(
|
|
250
|
-
createComparisonHarness(settings, tickers, financials, createRuntimeSpy(spy)),
|
|
295
|
+
createComparisonHarness(settings, tickers, financials, createRuntimeSpy(spy), options),
|
|
251
296
|
);
|
|
252
297
|
await Promise.resolve();
|
|
253
298
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
@@ -399,6 +444,85 @@ describe("comparisonChartPlugin", () => {
|
|
|
399
444
|
expect(frame).toMatch(/MSFT\s+0\.00%/);
|
|
400
445
|
});
|
|
401
446
|
|
|
447
|
+
test("focuses the pane when an unfocused comparison plot is clicked", async () => {
|
|
448
|
+
const dispatched: AppAction[] = [];
|
|
449
|
+
const provider = createProvider({
|
|
450
|
+
AAPL: [100, 102, 104],
|
|
451
|
+
MSFT: [200, 202, 204],
|
|
452
|
+
}, {
|
|
453
|
+
AAPL: "USD",
|
|
454
|
+
MSFT: "USD",
|
|
455
|
+
});
|
|
456
|
+
setSharedMarketDataForTests(provider);
|
|
457
|
+
sharedCoordinator = new MarketDataCoordinator(provider);
|
|
458
|
+
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
459
|
+
setSharedRegistryForTests(createRegistrySpy({ selected: [], focused: [] }));
|
|
460
|
+
|
|
461
|
+
await mountComparisonHarness({
|
|
462
|
+
axisMode: "percent",
|
|
463
|
+
symbols: ["AAPL", "MSFT"],
|
|
464
|
+
symbolsText: "AAPL, MSFT",
|
|
465
|
+
}, [
|
|
466
|
+
makeTicker("AAPL", "USD"),
|
|
467
|
+
makeTicker("MSFT", "USD"),
|
|
468
|
+
], [
|
|
469
|
+
["AAPL", makeFinancials("AAPL", "USD", [100, 102, 104])],
|
|
470
|
+
["MSFT", makeFinancials("MSFT", "USD", [200, 202, 204])],
|
|
471
|
+
], { selected: [], focused: [] }, {
|
|
472
|
+
dispatch: (action) => { dispatched.push(action); },
|
|
473
|
+
focused: false,
|
|
474
|
+
focusedPaneId: "ticker-detail:main",
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await flushFrames();
|
|
478
|
+
await act(async () => {
|
|
479
|
+
await testSetup!.mockMouse.click(20, 8);
|
|
480
|
+
await testSetup!.renderOnce();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
expect(dispatched).toContainEqual({ type: "FOCUS_PANE", paneId: TEST_PANE_ID });
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("keeps fixed return horizons independent from the selected chart range", async () => {
|
|
487
|
+
const shortHistory = {
|
|
488
|
+
AAPL: [["2025-01-02", 180], ["2026-01-02", 200]],
|
|
489
|
+
MSFT: [["2025-01-02", 300], ["2026-01-02", 330]],
|
|
490
|
+
} satisfies Record<string, Array<[string, number]>>;
|
|
491
|
+
const fullHistory = {
|
|
492
|
+
AAPL: [["2021-01-02", 100], ["2023-01-02", 150], ["2025-01-02", 180], ["2026-01-02", 200]],
|
|
493
|
+
MSFT: [["2021-01-02", 220], ["2023-01-02", 270], ["2025-01-02", 300], ["2026-01-02", 330]],
|
|
494
|
+
} satisfies Record<string, Array<[string, number]>>;
|
|
495
|
+
const provider = createRangeAwareDatedProvider({
|
|
496
|
+
fullHistoryBySymbol: fullHistory,
|
|
497
|
+
shortHistoryBySymbol: shortHistory,
|
|
498
|
+
});
|
|
499
|
+
setSharedMarketDataForTests(provider);
|
|
500
|
+
sharedCoordinator = new MarketDataCoordinator(provider);
|
|
501
|
+
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
502
|
+
setSharedRegistryForTests(createRegistrySpy({ selected: [], focused: [] }));
|
|
503
|
+
|
|
504
|
+
await mountComparisonHarness({
|
|
505
|
+
axisMode: "percent",
|
|
506
|
+
rangePreset: "1Y",
|
|
507
|
+
chartResolution: "1d",
|
|
508
|
+
symbols: ["AAPL", "MSFT"],
|
|
509
|
+
symbolsText: "AAPL, MSFT",
|
|
510
|
+
}, [
|
|
511
|
+
makeTicker("AAPL", "USD"),
|
|
512
|
+
makeTicker("MSFT", "USD"),
|
|
513
|
+
], [
|
|
514
|
+
["AAPL", makeDatedFinancials("AAPL", "USD", shortHistory.AAPL)],
|
|
515
|
+
["MSFT", makeDatedFinancials("MSFT", "USD", shortHistory.MSFT)],
|
|
516
|
+
]);
|
|
517
|
+
|
|
518
|
+
await flushFrames(8);
|
|
519
|
+
|
|
520
|
+
const frame = testSetup!.captureCharFrame();
|
|
521
|
+
expect(frame).toContain("5Y");
|
|
522
|
+
expect(frame).toMatch(/> AAPL\s+\+11\.11%[^\n]*\+100\.00%/);
|
|
523
|
+
expect(frame).toMatch(/MSFT\s+\+10\.00%[^\n]*\+50\.00%/);
|
|
524
|
+
});
|
|
525
|
+
|
|
402
526
|
test("sorts comparison summary rows by one-year return", async () => {
|
|
403
527
|
const rows = {
|
|
404
528
|
SLOW: makeDatedFinancials("SLOW", "USD", [["2025-01-02", 100], ["2026-01-02", 105]]),
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type KellyCommonAssumptions,
|
|
3
|
+
type KellySizerDraft,
|
|
4
|
+
type KellySizerModeDrafts,
|
|
5
|
+
type KellySizingMode,
|
|
6
|
+
type ScenarioKellyAssumptions,
|
|
7
|
+
} from "./model";
|
|
8
|
+
|
|
9
|
+
export interface InlineField {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
value?: number;
|
|
13
|
+
valueText?: string;
|
|
14
|
+
percent?: boolean;
|
|
15
|
+
suffix?: string;
|
|
16
|
+
allowNegative?: boolean;
|
|
17
|
+
onValue?: (value: number) => void;
|
|
18
|
+
onClear?: () => void;
|
|
19
|
+
onPress?: () => void;
|
|
20
|
+
tone?: "neutral" | "positive" | "negative";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildModeFields({
|
|
24
|
+
mode,
|
|
25
|
+
draft,
|
|
26
|
+
updateDraft,
|
|
27
|
+
}: {
|
|
28
|
+
mode: KellySizingMode;
|
|
29
|
+
draft: KellySizerDraft;
|
|
30
|
+
updateDraft: (patch: Partial<KellySizerDraft>) => void;
|
|
31
|
+
}): InlineField[] {
|
|
32
|
+
if (mode === "scenario") {
|
|
33
|
+
const scenario = draft as ScenarioKellyAssumptions;
|
|
34
|
+
const updateOutcome = (index: number, patch: Partial<ScenarioKellyAssumptions["outcomes"][number]>) => {
|
|
35
|
+
updateDraft({
|
|
36
|
+
outcomes: scenario.outcomes.map((outcome, outcomeIndex) => (
|
|
37
|
+
outcomeIndex === index ? { ...outcome, ...patch } : outcome
|
|
38
|
+
)),
|
|
39
|
+
} as Partial<KellySizerDraft>);
|
|
40
|
+
};
|
|
41
|
+
return scenario.outcomes.flatMap((outcome, index): InlineField[] => [
|
|
42
|
+
{
|
|
43
|
+
id: `${outcome.id}:p`,
|
|
44
|
+
label: `${outcome.label} p`,
|
|
45
|
+
value: outcome.probability,
|
|
46
|
+
percent: true,
|
|
47
|
+
onValue: (value) => updateOutcome(index, { probability: value }),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: `${outcome.id}:ret`,
|
|
51
|
+
label: `${outcome.label} ret`,
|
|
52
|
+
value: outcome.returnPct,
|
|
53
|
+
percent: true,
|
|
54
|
+
allowNegative: true,
|
|
55
|
+
tone: outcome.returnPct >= 0 ? "positive" : "negative",
|
|
56
|
+
onValue: (value) => updateOutcome(index, { returnPct: value }),
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (mode === "risk-budget") {
|
|
62
|
+
const risk = draft as KellySizerModeDrafts["risk-budget"];
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
id: "riskBudget",
|
|
66
|
+
label: "Risk budget",
|
|
67
|
+
value: risk.riskBudgetFraction,
|
|
68
|
+
percent: true,
|
|
69
|
+
onValue: (value) => updateDraft({ riskBudgetFraction: value } as Partial<KellySizerDraft>),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "downside",
|
|
73
|
+
label: "Downside",
|
|
74
|
+
value: risk.downsideReturn,
|
|
75
|
+
percent: true,
|
|
76
|
+
allowNegative: true,
|
|
77
|
+
tone: "negative",
|
|
78
|
+
onValue: (value) => updateDraft({ downsideReturn: value } as Partial<KellySizerDraft>),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "winProbability",
|
|
82
|
+
label: "Win p",
|
|
83
|
+
value: risk.winProbability,
|
|
84
|
+
percent: true,
|
|
85
|
+
onValue: (value) => updateDraft({ winProbability: value } as Partial<KellySizerDraft>),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "upside",
|
|
89
|
+
label: "Upside",
|
|
90
|
+
value: risk.upsideReturn,
|
|
91
|
+
percent: true,
|
|
92
|
+
tone: "positive",
|
|
93
|
+
onValue: (value) => updateDraft({ upsideReturn: value } as Partial<KellySizerDraft>),
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (mode === "prediction-market") {
|
|
99
|
+
const market = draft as KellySizerModeDrafts["prediction-market"];
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
id: "side",
|
|
103
|
+
label: "Side",
|
|
104
|
+
valueText: market.side.toUpperCase(),
|
|
105
|
+
tone: "positive",
|
|
106
|
+
onPress: () => updateDraft({ side: market.side === "yes" ? "no" : "yes" } as Partial<KellySizerDraft>),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "estimatedProbability",
|
|
110
|
+
label: "Est p",
|
|
111
|
+
value: market.estimatedProbability,
|
|
112
|
+
percent: true,
|
|
113
|
+
onValue: (value) => updateDraft({ estimatedProbability: value } as Partial<KellySizerDraft>),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "marketPrice",
|
|
117
|
+
label: "Price",
|
|
118
|
+
value: market.marketPrice,
|
|
119
|
+
percent: true,
|
|
120
|
+
onValue: (value) => updateDraft({ marketPrice: value } as Partial<KellySizerDraft>),
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (mode === "asymmetric") {
|
|
126
|
+
const asymmetric = draft as KellySizerModeDrafts["asymmetric"];
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
id: "winProbability",
|
|
130
|
+
label: "Win p",
|
|
131
|
+
value: asymmetric.winProbability,
|
|
132
|
+
percent: true,
|
|
133
|
+
onValue: (value) => updateDraft({ winProbability: value } as Partial<KellySizerDraft>),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "targetReturn",
|
|
137
|
+
label: "Target",
|
|
138
|
+
value: asymmetric.targetReturn,
|
|
139
|
+
percent: true,
|
|
140
|
+
tone: "positive",
|
|
141
|
+
onValue: (value) => updateDraft({ targetReturn: value } as Partial<KellySizerDraft>),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "maxLossReturn",
|
|
145
|
+
label: "Max loss",
|
|
146
|
+
value: asymmetric.maxLossReturn,
|
|
147
|
+
percent: true,
|
|
148
|
+
allowNegative: true,
|
|
149
|
+
tone: "negative",
|
|
150
|
+
onValue: (value) => updateDraft({ maxLossReturn: value } as Partial<KellySizerDraft>),
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const binary = draft as KellySizerModeDrafts["binary"];
|
|
156
|
+
return [
|
|
157
|
+
{
|
|
158
|
+
id: "winProbability",
|
|
159
|
+
label: "Win p",
|
|
160
|
+
value: binary.winProbability,
|
|
161
|
+
percent: true,
|
|
162
|
+
onValue: (value) => updateDraft({ winProbability: value } as Partial<KellySizerDraft>),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "upside",
|
|
166
|
+
label: "Upside",
|
|
167
|
+
value: binary.upsideReturn,
|
|
168
|
+
percent: true,
|
|
169
|
+
tone: "positive",
|
|
170
|
+
onValue: (value) => updateDraft({ upsideReturn: value } as Partial<KellySizerDraft>),
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: "downside",
|
|
174
|
+
label: "Downside",
|
|
175
|
+
value: binary.downsideReturn,
|
|
176
|
+
percent: true,
|
|
177
|
+
allowNegative: true,
|
|
178
|
+
tone: "negative",
|
|
179
|
+
onValue: (value) => updateDraft({ downsideReturn: value } as Partial<KellySizerDraft>),
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function buildCommonFields({
|
|
185
|
+
common,
|
|
186
|
+
updateCommon,
|
|
187
|
+
}: {
|
|
188
|
+
common: KellyCommonAssumptions;
|
|
189
|
+
updateCommon: (patch: Partial<KellyCommonAssumptions>) => void;
|
|
190
|
+
}): InlineField[] {
|
|
191
|
+
return [
|
|
192
|
+
{
|
|
193
|
+
id: "common:kellyFraction",
|
|
194
|
+
label: "Kelly",
|
|
195
|
+
value: common.kellyFraction,
|
|
196
|
+
percent: true,
|
|
197
|
+
onValue: (value) => updateCommon({ kellyFraction: value }),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: "common:maxLoss",
|
|
201
|
+
label: "Loss cap",
|
|
202
|
+
value: common.maxLossFraction,
|
|
203
|
+
percent: true,
|
|
204
|
+
onValue: (value) => updateCommon({ maxLossFraction: value }),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "common:maxName",
|
|
208
|
+
label: "Name cap",
|
|
209
|
+
value: common.maxNameFraction,
|
|
210
|
+
percent: true,
|
|
211
|
+
onValue: (value) => updateCommon({ maxNameFraction: value }),
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { testRender } from "../../../renderers/opentui/test-utils";
|
|
4
|
+
import { setSharedMarketDataCoordinator } from "../../../market-data/coordinator";
|
|
5
|
+
import type { TickerRecord } from "../../../types/ticker";
|
|
6
|
+
import { KellySizerHarness, createFinancials, createSizerConfig, createTicker } from "./test-support";
|
|
7
|
+
|
|
8
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
9
|
+
|
|
10
|
+
async function renderPane(props?: Parameters<typeof KellySizerHarness>[0]) {
|
|
11
|
+
await act(async () => {
|
|
12
|
+
testSetup = await testRender(<KellySizerHarness {...props} />, { width: 100, height: 30 });
|
|
13
|
+
await Promise.resolve();
|
|
14
|
+
await testSetup.renderOnce();
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function flushFrame() {
|
|
19
|
+
await act(async () => {
|
|
20
|
+
await testSetup!.renderOnce();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
setSharedMarketDataCoordinator(null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
if (testSetup) {
|
|
30
|
+
await act(async () => {
|
|
31
|
+
testSetup!.renderer.destroy();
|
|
32
|
+
});
|
|
33
|
+
testSetup = undefined;
|
|
34
|
+
}
|
|
35
|
+
setSharedMarketDataCoordinator(null);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
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
|
+
test("prefills shared caps from plugin config", async () => {
|
|
65
|
+
const config = {
|
|
66
|
+
...createSizerConfig(),
|
|
67
|
+
pluginConfig: {
|
|
68
|
+
"kelly-sizer": {
|
|
69
|
+
"commonAssumptions:v1": {
|
|
70
|
+
kellyFraction: 0.5,
|
|
71
|
+
maxLossFraction: 0.02,
|
|
72
|
+
maxNameFraction: 0.1,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
await renderPane({ config, paneState: { mode: "scenario" } });
|
|
79
|
+
await flushFrame();
|
|
80
|
+
|
|
81
|
+
const frame = testSetup!.captureCharFrame();
|
|
82
|
+
expect(frame).toContain("Scenario");
|
|
83
|
+
expect(frame).toMatch(/Kelly\s+50\.0\s+%/);
|
|
84
|
+
expect(frame).toMatch(/Loss cap\s+2\.00\s+%/);
|
|
85
|
+
expect(frame).toMatch(/Name cap\s+10\.0\s+%/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("keeps shared caps scoped to pane state after seeding", async () => {
|
|
89
|
+
const config = {
|
|
90
|
+
...createSizerConfig(),
|
|
91
|
+
pluginConfig: {
|
|
92
|
+
"kelly-sizer": {
|
|
93
|
+
"commonAssumptions:v1": {
|
|
94
|
+
kellyFraction: 0.5,
|
|
95
|
+
maxLossFraction: 0.02,
|
|
96
|
+
maxNameFraction: 0.1,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
await renderPane({
|
|
103
|
+
config,
|
|
104
|
+
paneState: {
|
|
105
|
+
mode: "scenario",
|
|
106
|
+
"commonAssumptions:v1": {
|
|
107
|
+
kellyFraction: 0.3,
|
|
108
|
+
maxLossFraction: 0.015,
|
|
109
|
+
maxNameFraction: 0.07,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
await flushFrame();
|
|
114
|
+
|
|
115
|
+
const frame = testSetup!.captureCharFrame();
|
|
116
|
+
expect(frame).toContain("Scenario");
|
|
117
|
+
expect(frame).toMatch(/Kelly\s+30\.0\s+%/);
|
|
118
|
+
expect(frame).toMatch(/Loss cap\s+1\.50\s+%/);
|
|
119
|
+
expect(frame).toMatch(/Name cap\s+7\.00\s+%/);
|
|
120
|
+
expect(frame).not.toMatch(/Kelly\s+50\.0\s+%/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("converts foreign quote current value to base currency", async () => {
|
|
124
|
+
const ticker = createTicker({
|
|
125
|
+
symbol: "SIVE",
|
|
126
|
+
currency: "SEK",
|
|
127
|
+
positions: [{
|
|
128
|
+
portfolio: "main",
|
|
129
|
+
shares: 100,
|
|
130
|
+
avgCost: 50,
|
|
131
|
+
currency: "SEK",
|
|
132
|
+
broker: "ibkr",
|
|
133
|
+
brokerInstanceId: "ibkr-flex",
|
|
134
|
+
brokerAccountId: "DU12345",
|
|
135
|
+
marketValue: 10_000,
|
|
136
|
+
unrealizedPnl: 5_000,
|
|
137
|
+
}],
|
|
138
|
+
});
|
|
139
|
+
const financials = createFinancials({ symbol: "SIVE", price: 100, currency: "SEK" });
|
|
140
|
+
|
|
141
|
+
await renderPane({
|
|
142
|
+
config: createSizerConfig("SIVE"),
|
|
143
|
+
ticker,
|
|
144
|
+
financials,
|
|
145
|
+
exchangeRates: new Map([["USD", 1], ["SEK", 0.1]]),
|
|
146
|
+
});
|
|
147
|
+
await flushFrame();
|
|
148
|
+
|
|
149
|
+
const frame = testSetup!.captureCharFrame();
|
|
150
|
+
expect(frame).toContain("SIVE · Main Portfolio");
|
|
151
|
+
expect(frame).toMatch(/Current\s+1000\s+USD/);
|
|
152
|
+
expect(frame).not.toMatch(/Current\s+10000\s+USD/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("toggles sensitivity with s", async () => {
|
|
156
|
+
await renderPane();
|
|
157
|
+
await flushFrame();
|
|
158
|
+
|
|
159
|
+
await act(async () => {
|
|
160
|
+
(testSetup!.renderer.keyInput as any).emit("keypress", {
|
|
161
|
+
name: "s",
|
|
162
|
+
sequence: "s",
|
|
163
|
+
ctrl: false,
|
|
164
|
+
meta: false,
|
|
165
|
+
shift: false,
|
|
166
|
+
});
|
|
167
|
+
await testSetup!.renderOnce();
|
|
168
|
+
});
|
|
169
|
+
await flushFrame();
|
|
170
|
+
|
|
171
|
+
const frame = testSetup!.captureCharFrame();
|
|
172
|
+
expect(frame).toContain("Sensitivity");
|
|
173
|
+
expect(frame).toContain("Win p");
|
|
174
|
+
expect(frame).toContain("Upside");
|
|
175
|
+
expect(frame).not.toContain("Kelly Curve");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("focuses ticker search with t", async () => {
|
|
179
|
+
await renderPane();
|
|
180
|
+
await flushFrame();
|
|
181
|
+
|
|
182
|
+
await act(async () => {
|
|
183
|
+
(testSetup!.renderer.keyInput as any).emit("keypress", {
|
|
184
|
+
name: "t",
|
|
185
|
+
sequence: "t",
|
|
186
|
+
ctrl: false,
|
|
187
|
+
meta: false,
|
|
188
|
+
shift: false,
|
|
189
|
+
});
|
|
190
|
+
await testSetup!.renderOnce();
|
|
191
|
+
});
|
|
192
|
+
await flushFrame();
|
|
193
|
+
|
|
194
|
+
const frame = testSetup!.captureCharFrame();
|
|
195
|
+
expect(frame).toContain("/ AAPL");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { GloomPlugin, PaneTemplateContext, PaneTemplateCreateOptions } from "../../../types/plugin";
|
|
2
|
+
import { DEFAULT_FLOATING_SIZE, KELLY_PANE_ID } from "./constants";
|
|
3
|
+
import { KellySizerPane } from "./pane";
|
|
4
|
+
|
|
5
|
+
function resolveTemplateSymbol(context: PaneTemplateContext, options?: PaneTemplateCreateOptions): string | null {
|
|
6
|
+
return options?.symbol
|
|
7
|
+
?? options?.ticker?.metadata.ticker
|
|
8
|
+
?? options?.arg?.trim().toUpperCase()
|
|
9
|
+
?? context.activeTicker
|
|
10
|
+
?? null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const kellySizerPlugin: GloomPlugin = {
|
|
14
|
+
id: KELLY_PANE_ID,
|
|
15
|
+
name: "Position Sizer",
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
description: "Size positions from Kelly, risk budget, and asymmetric payoff assumptions.",
|
|
18
|
+
toggleable: true,
|
|
19
|
+
panes: [
|
|
20
|
+
{
|
|
21
|
+
id: KELLY_PANE_ID,
|
|
22
|
+
name: "Position Sizer",
|
|
23
|
+
icon: "K",
|
|
24
|
+
component: KellySizerPane,
|
|
25
|
+
defaultPosition: "right",
|
|
26
|
+
defaultMode: "floating",
|
|
27
|
+
defaultFloatingSize: DEFAULT_FLOATING_SIZE,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
paneTemplates: [
|
|
31
|
+
{
|
|
32
|
+
id: "kelly-sizer-pane",
|
|
33
|
+
paneId: KELLY_PANE_ID,
|
|
34
|
+
label: "Position Sizer",
|
|
35
|
+
description: "Open Kelly-based position sizing for a ticker.",
|
|
36
|
+
keywords: ["kelly", "position", "sizing", "risk", "bet", "portfolio"],
|
|
37
|
+
shortcut: { prefix: "KELLY", argPlaceholder: "ticker", argKind: "ticker", argOptional: true },
|
|
38
|
+
canCreate: (context, options) => !!resolveTemplateSymbol(context, options),
|
|
39
|
+
createInstance: (context, options) => {
|
|
40
|
+
const symbol = resolveTemplateSymbol(context, options);
|
|
41
|
+
return symbol ? { params: { symbol }, placement: "floating" } : null;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default kellySizerPlugin;
|