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.
Files changed (137) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/app/runtime/desktop-menu.ts +5 -1
  4. package/src/app/runtime/ticker-refresh.ts +4 -1
  5. package/src/app.tsx +10 -1
  6. package/src/brokers/account-cache.ts +1 -1
  7. package/src/brokers/sync-broker-instance.test.ts +107 -0
  8. package/src/brokers/sync-broker-instance.ts +22 -10
  9. package/src/cli/desktop-pane-shot.ts +2 -1
  10. package/src/cli/pane-functions/screenshot.ts +13 -1
  11. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  12. package/src/components/chart/bar-chart-renderer.ts +226 -27
  13. package/src/components/chart/core/pointer.ts +32 -6
  14. package/src/components/chart/price-axis-labels.tsx +3 -1
  15. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  16. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  17. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  18. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  19. package/src/components/chart/static/chart/surface.tsx +167 -24
  20. package/src/components/chart/stock-chart/index.test.ts +25 -0
  21. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  22. package/src/components/layout/detached-pane-shell.tsx +6 -3
  23. package/src/components/layout/floating-pane.tsx +3 -2
  24. package/src/components/layout/pane/content.tsx +20 -8
  25. package/src/components/layout/pane/footer/registration.tsx +6 -2
  26. package/src/components/layout/pane/frame.tsx +4 -4
  27. package/src/components/layout/pane/header.tsx +40 -4
  28. package/src/components/layout/pane/index.tsx +3 -1
  29. package/src/components/layout/pane/sizing.test.ts +43 -0
  30. package/src/components/layout/pane/sizing.ts +13 -4
  31. package/src/components/layout/shell/cursor-occlusion.ts +196 -0
  32. package/src/components/layout/shell/drag/runtime.ts +9 -2
  33. package/src/components/layout/shell/index.test.tsx +92 -2
  34. package/src/components/layout/shell/index.tsx +37 -2
  35. package/src/components/layout/shell/layout-state.test.ts +33 -0
  36. package/src/components/layout/shell/layout-state.ts +4 -9
  37. package/src/components/layout/shell/pane/layers.tsx +8 -2
  38. package/src/components/layout/shell/visible-layout.ts +17 -0
  39. package/src/components/layout/status-bar.test.tsx +6 -1
  40. package/src/components/layout/status-bar.tsx +5 -1
  41. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  42. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  43. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  44. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  45. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  46. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  47. package/src/components/table-view-shared.tsx +12 -4
  48. package/src/components/ui/data-table/opentui/index.tsx +25 -12
  49. package/src/components/ui/data-table/types.ts +4 -0
  50. package/src/components/ui/fields.tsx +10 -3
  51. package/src/components/ui/page-stack-view.tsx +6 -2
  52. package/src/components/ui/table-layout.test.ts +25 -0
  53. package/src/components/ui/table-layout.ts +22 -6
  54. package/src/components/ui/tabs.tsx +26 -10
  55. package/src/components/ui/ui.test.tsx +87 -0
  56. package/src/core/state/app/layout-reducer.ts +2 -2
  57. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  58. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  59. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  60. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  61. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  62. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  63. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  64. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  65. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  66. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  67. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  68. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  69. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  70. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  71. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  72. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  73. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  74. package/src/plugins/builtin/options/view.test.tsx +141 -4
  75. package/src/plugins/builtin/options/view.tsx +76 -15
  76. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  77. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  78. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  79. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
  80. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  81. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  82. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  83. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  84. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  85. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  86. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  87. package/src/plugins/catalog-ui.ts +2 -0
  88. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  89. package/src/plugins/ibkr/config.test.ts +31 -2
  90. package/src/plugins/ibkr/config.ts +9 -2
  91. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  92. package/src/plugins/ibkr/flex/index.ts +30 -0
  93. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  94. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  95. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  96. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  97. package/src/plugins/pane-manager/gridlock.ts +13 -5
  98. package/src/plugins/pane-manager/layout-state.ts +25 -0
  99. package/src/plugins/pane-manager.test.ts +33 -0
  100. package/src/plugins/pane-manager.ts +6 -1
  101. package/src/plugins/prediction-markets/chart.tsx +10 -4
  102. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  103. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  104. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  105. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  106. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  107. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  108. package/src/plugins/prediction-markets/pane.test.tsx +43 -0
  109. package/src/plugins/prediction-markets/pane.tsx +50 -22
  110. package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
  111. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  112. package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
  113. package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
  114. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  115. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  116. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  117. package/src/renderers/electrobun/view/styles.css +11 -0
  118. package/src/renderers/opentui/host.tsx +17 -1
  119. package/src/renderers/opentui/ui-host.tsx +57 -25
  120. package/src/sources/provider-router/batches.ts +16 -2
  121. package/src/sources/provider-router/financial-routes.ts +5 -0
  122. package/src/sources/provider-router/financials.ts +11 -23
  123. package/src/sources/provider-router/index.test.ts +189 -0
  124. package/src/sources/provider-router/primary.ts +3 -2
  125. package/src/sources/sec-edgar.test.ts +83 -0
  126. package/src/sources/sec-edgar.ts +231 -0
  127. package/src/sources/yahoo-finance.test.ts +51 -0
  128. package/src/sources/yahoo-finance.ts +49 -1
  129. package/src/state/app/context/index.test.tsx +5 -8
  130. package/src/types/broker.ts +1 -0
  131. package/src/types/config.ts +10 -0
  132. package/src/types/trading.ts +2 -0
  133. package/src/ui/host.tsx +16 -0
  134. package/src/ui/index.tsx +2 -0
  135. package/src/utils/financial-statements.ts +23 -0
  136. package/src/version.ts +1 -1
  137. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -0,0 +1,142 @@
1
+ import { Box, Text, TextAttributes } from "../../../ui";
2
+ import { StaticChartSurface } from "../../../components";
3
+ import { resolveChartPalette } from "../../../components/chart/core/renderer";
4
+ import type { StaticChartXMarker } from "../../../components/chart/static/chart/surface";
5
+ import { colors, priceColor } from "../../../theme/colors";
6
+ import { formatCompact, formatCurrency, formatNumber } from "../../../utils/format";
7
+ import type { ProjectedChartPoint } from "../../../components/chart/core/data";
8
+ import type { KellySizerDraft, KellySizingResult, SensitivityGrid } from "./model";
9
+ import {
10
+ KellyCurveDecisionView,
11
+ MetricLine,
12
+ SensitivityGridView,
13
+ formatPct,
14
+ formatSignedPct,
15
+ } from "./view";
16
+
17
+ export function KellyResultMetrics({
18
+ result,
19
+ activeDraft,
20
+ baseCurrency,
21
+ leftWidth,
22
+ rightWidth,
23
+ }: {
24
+ result: KellySizingResult;
25
+ activeDraft: KellySizerDraft;
26
+ baseCurrency: string;
27
+ leftWidth: number;
28
+ rightWidth: number;
29
+ }) {
30
+ return (
31
+ <Box flexDirection="row" paddingX={1}>
32
+ <Box flexDirection="column" width={leftWidth}>
33
+ <MetricLine width={leftWidth} label="Full Kelly" value={formatPct(result.fullKellyFraction, 1)} />
34
+ <MetricLine width={leftWidth} label="Fractional" value={formatPct(result.fractionalKellyFraction, 1)} detail={formatPct(activeDraft.kellyFraction, 0)} />
35
+ <MetricLine
36
+ width={leftWidth}
37
+ label="Clipped"
38
+ value={formatPct(result.clippedFraction, 2)}
39
+ detail={result.clipReasons.length > 0 ? result.clipReasons.join(", ") : undefined}
40
+ color={result.clipReasons.length > 0 ? colors.positive : colors.text}
41
+ />
42
+ <MetricLine width={leftWidth} label="Target val" value={formatCurrency(result.targetValue, baseCurrency)} />
43
+ <MetricLine
44
+ width={leftWidth}
45
+ label="Add / trim"
46
+ value={formatCurrency(result.addTrimValue, baseCurrency)}
47
+ color={priceColor(result.addTrimValue)}
48
+ />
49
+ <MetricLine
50
+ width={leftWidth}
51
+ label="Units"
52
+ value={result.estimatedUnits == null ? "—" : formatNumber(result.estimatedUnits, 1)}
53
+ />
54
+ </Box>
55
+ <Box flexDirection="column" width={rightWidth}>
56
+ <MetricLine width={rightWidth} label="Risk" value={formatCurrency(result.riskValue, baseCurrency)} detail={formatPct(result.riskFraction, 2)} color={colors.negative} />
57
+ <MetricLine width={rightWidth} label="Worst loss" value={formatPct(result.downsideLossFraction, 1)} />
58
+ <MetricLine width={rightWidth} label="Current %" value={formatPct(result.currentFraction, 1)} />
59
+ <MetricLine width={rightWidth} label="Exp return" value={formatSignedPct(result.expectedReturn)} color={priceColor(result.expectedReturn)} />
60
+ <MetricLine width={rightWidth} label="Log growth" value={formatCompact(result.expectedLogGrowth)} />
61
+ </Box>
62
+ </Box>
63
+ );
64
+ }
65
+
66
+ export function KellyCurveSection({
67
+ width,
68
+ height,
69
+ points,
70
+ xAxisLabels,
71
+ curveMaxFraction,
72
+ markers,
73
+ result,
74
+ currentGrowth,
75
+ targetGrowth,
76
+ baseCurrency,
77
+ }: {
78
+ width: number;
79
+ height: number;
80
+ points: ProjectedChartPoint[];
81
+ xAxisLabels: string[];
82
+ curveMaxFraction: number;
83
+ markers: StaticChartXMarker[];
84
+ result: KellySizingResult;
85
+ currentGrowth: number;
86
+ targetGrowth: number;
87
+ baseCurrency: string;
88
+ }) {
89
+ return (
90
+ <>
91
+ <Box height={1} paddingX={1}>
92
+ <Text fg={colors.textDim} attributes={TextAttributes.BOLD}>Kelly Curve</Text>
93
+ </Box>
94
+ <Box paddingX={1} height={height}>
95
+ <StaticChartSurface
96
+ points={points}
97
+ width={Math.max(10, width - 2)}
98
+ height={height}
99
+ mode="line"
100
+ axisMode="percent"
101
+ colors={resolveChartPalette(colors, "positive")}
102
+ yAxisLabel="Expected log growth"
103
+ yAxisColor={colors.textDim}
104
+ formatYAxisValue={(value) => formatSignedPct(value)}
105
+ xAxisLabels={xAxisLabels}
106
+ xAxisColor={colors.textDim}
107
+ formatXAxisCursorValue={(ratio) => formatPct(curveMaxFraction * ratio, 1)}
108
+ xMarkers={markers}
109
+ />
110
+ </Box>
111
+ <KellyCurveDecisionView
112
+ width={Math.max(10, width - 2)}
113
+ currentFraction={result.currentFraction}
114
+ targetFraction={result.clippedFraction}
115
+ fullKellyFraction={result.fullKellyFraction}
116
+ currentGrowth={currentGrowth}
117
+ targetGrowth={targetGrowth}
118
+ addTrimValue={result.addTrimValue}
119
+ currency={baseCurrency}
120
+ clipReasons={result.clipReasons}
121
+ />
122
+ </>
123
+ );
124
+ }
125
+
126
+ export function KellySensitivitySection({
127
+ width,
128
+ sensitivity,
129
+ }: {
130
+ width: number;
131
+ sensitivity: SensitivityGrid;
132
+ }) {
133
+ return (
134
+ <>
135
+ <Box height={1} paddingX={1} flexDirection="row">
136
+ <Text fg={colors.textDim} attributes={TextAttributes.BOLD}>Sensitivity</Text>
137
+ <Text fg={colors.textDim}>{` ${sensitivity.columnLabel}`}</Text>
138
+ </Box>
139
+ <SensitivityGridView width={Math.max(10, width - 2)} grid={sensitivity} />
140
+ </>
141
+ );
142
+ }
@@ -0,0 +1,67 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { usePaneStateValue } from "../../../state/app/context";
3
+ import { usePluginConfigState } from "../../runtime";
4
+ import { COMMON_ASSUMPTIONS_STATE_KEY } from "./constants";
5
+ import {
6
+ DEFAULT_KELLY_COMMON_ASSUMPTIONS,
7
+ normalizeKellyCommonAssumptions,
8
+ type KellyCommonAssumptions,
9
+ type KellySizerDraft,
10
+ } from "./model";
11
+
12
+ export function useKellyCommonAssumptions(rawActiveDraft: KellySizerDraft): {
13
+ commonAssumptions: KellyCommonAssumptions;
14
+ updateCommon: (patch: Partial<KellyCommonAssumptions>) => void;
15
+ } {
16
+ const commonFallback = useMemo(
17
+ () => normalizeKellyCommonAssumptions(rawActiveDraft, DEFAULT_KELLY_COMMON_ASSUMPTIONS),
18
+ [rawActiveDraft],
19
+ );
20
+ const [storedCommonAssumptions, setStoredCommonAssumptions] = usePluginConfigState<KellyCommonAssumptions>(
21
+ COMMON_ASSUMPTIONS_STATE_KEY,
22
+ commonFallback,
23
+ );
24
+ const [paneCommonAssumptions, setPaneCommonAssumptions] = usePaneStateValue<KellyCommonAssumptions | null>(
25
+ COMMON_ASSUMPTIONS_STATE_KEY,
26
+ null,
27
+ );
28
+ const seededCommonAssumptions = useMemo(
29
+ () => normalizeKellyCommonAssumptions(storedCommonAssumptions, commonFallback),
30
+ [commonFallback, storedCommonAssumptions],
31
+ );
32
+ const [localCommonAssumptions, setLocalCommonAssumptions] = useState<KellyCommonAssumptions | null>(null);
33
+
34
+ useEffect(() => {
35
+ if (paneCommonAssumptions === null) {
36
+ setPaneCommonAssumptions(seededCommonAssumptions);
37
+ }
38
+ }, [paneCommonAssumptions, seededCommonAssumptions, setPaneCommonAssumptions]);
39
+
40
+ useEffect(() => {
41
+ if (localCommonAssumptions !== null) return;
42
+ setLocalCommonAssumptions(normalizeKellyCommonAssumptions(
43
+ paneCommonAssumptions ?? seededCommonAssumptions,
44
+ seededCommonAssumptions,
45
+ ));
46
+ }, [localCommonAssumptions, paneCommonAssumptions, seededCommonAssumptions]);
47
+
48
+ const commonAssumptions = useMemo(
49
+ () => normalizeKellyCommonAssumptions(
50
+ localCommonAssumptions ?? paneCommonAssumptions ?? seededCommonAssumptions,
51
+ seededCommonAssumptions,
52
+ ),
53
+ [localCommonAssumptions, paneCommonAssumptions, seededCommonAssumptions],
54
+ );
55
+
56
+ const updateCommon = useCallback((patch: Partial<KellyCommonAssumptions>) => {
57
+ const next = normalizeKellyCommonAssumptions({
58
+ ...commonAssumptions,
59
+ ...patch,
60
+ }, commonAssumptions);
61
+ setLocalCommonAssumptions(next);
62
+ setPaneCommonAssumptions(next);
63
+ setStoredCommonAssumptions(next);
64
+ }, [commonAssumptions, setPaneCommonAssumptions, setStoredCommonAssumptions]);
65
+
66
+ return { commonAssumptions, updateCommon };
67
+ }
@@ -0,0 +1,174 @@
1
+ import { useReducer, type ReactElement } from "react";
2
+ import {
3
+ AppContext,
4
+ appReducer,
5
+ createInitialState,
6
+ PaneInstanceProvider,
7
+ } from "../../../state/app/context";
8
+ import { createTestPluginRuntime } from "../../../test-support/plugin-runtime";
9
+ import { cloneLayout, createDefaultConfig, type AppConfig } from "../../../types/config";
10
+ import type { TickerFinancials } from "../../../types/financials";
11
+ import type { TickerRecord } from "../../../types/ticker";
12
+ import type { BrokerAccount } from "../../../types/trading";
13
+ import { PluginRenderProvider } from "../../runtime";
14
+ import { kellySizerPlugin } from "./index";
15
+
16
+ export const TEST_PANE_ID = "kelly-sizer:test";
17
+
18
+ const KellySizerPane = kellySizerPlugin.panes![0]!.component as (props: {
19
+ paneId: string;
20
+ paneType: string;
21
+ focused: boolean;
22
+ width: number;
23
+ height: number;
24
+ }) => ReactElement;
25
+
26
+ export function createSizerConfig(symbol = "AAPL"): AppConfig {
27
+ const baseConfig = createDefaultConfig("/tmp/gloomberb-kelly-sizer");
28
+ const layout: AppConfig["layout"] = {
29
+ dockRoot: { kind: "pane", instanceId: TEST_PANE_ID },
30
+ instances: [{
31
+ instanceId: TEST_PANE_ID,
32
+ paneId: "kelly-sizer",
33
+ binding: { kind: "none" },
34
+ params: { symbol },
35
+ }],
36
+ floating: [],
37
+ detached: [],
38
+ };
39
+
40
+ return {
41
+ ...baseConfig,
42
+ brokerInstances: [{
43
+ id: "ibkr-flex",
44
+ brokerType: "ibkr",
45
+ label: "IBKR Flex",
46
+ config: {},
47
+ }],
48
+ portfolios: [{
49
+ id: "main",
50
+ name: "Main Portfolio",
51
+ currency: "USD",
52
+ brokerId: "ibkr",
53
+ brokerInstanceId: "ibkr-flex",
54
+ brokerAccountId: "DU12345",
55
+ }],
56
+ layout,
57
+ layouts: [{ name: "Default", layout: cloneLayout(layout) }],
58
+ };
59
+ }
60
+
61
+ export function createTicker({
62
+ symbol = "AAPL",
63
+ currency = "USD",
64
+ positions,
65
+ }: {
66
+ symbol?: string;
67
+ currency?: string;
68
+ positions?: TickerRecord["metadata"]["positions"];
69
+ } = {}): TickerRecord {
70
+ return {
71
+ metadata: {
72
+ ticker: symbol,
73
+ exchange: "NASDAQ",
74
+ currency,
75
+ name: symbol === "AAPL" ? "Apple" : symbol,
76
+ sector: "Technology",
77
+ portfolios: ["main"],
78
+ watchlists: [],
79
+ positions: positions ?? [{
80
+ portfolio: "main",
81
+ shares: 20,
82
+ avgCost: 180,
83
+ currency,
84
+ broker: "ibkr",
85
+ brokerInstanceId: "ibkr-flex",
86
+ brokerAccountId: "DU12345",
87
+ marketValue: 4_000,
88
+ unrealizedPnl: 400,
89
+ }],
90
+ custom: {},
91
+ tags: [],
92
+ },
93
+ };
94
+ }
95
+
96
+ export function createFinancials({
97
+ symbol = "AAPL",
98
+ price = 200,
99
+ currency = "USD",
100
+ }: {
101
+ symbol?: string;
102
+ price?: number;
103
+ currency?: string;
104
+ } = {}): TickerFinancials {
105
+ return {
106
+ annualStatements: [],
107
+ quarterlyStatements: [],
108
+ priceHistory: [],
109
+ quote: {
110
+ symbol,
111
+ price,
112
+ currency,
113
+ change: price * 0.01,
114
+ changePercent: 1,
115
+ previousClose: price * 0.99,
116
+ lastUpdated: Date.now(),
117
+ },
118
+ };
119
+ }
120
+
121
+ function createBrokerAccount(): BrokerAccount {
122
+ return {
123
+ accountId: "DU12345",
124
+ name: "DU12345",
125
+ currency: "USD",
126
+ source: "flex",
127
+ updatedAt: Date.now(),
128
+ totalCashValue: 20_000,
129
+ netLiquidation: 100_000,
130
+ dailyPnl: 0,
131
+ unrealizedPnl: 400,
132
+ cashBalances: [],
133
+ };
134
+ }
135
+
136
+ export function KellySizerHarness({
137
+ config = createSizerConfig(),
138
+ ticker = createTicker(),
139
+ financials = createFinancials({ symbol: ticker.metadata.ticker, currency: ticker.metadata.currency }),
140
+ exchangeRates,
141
+ paneState,
142
+ }: {
143
+ config?: AppConfig;
144
+ ticker?: TickerRecord;
145
+ financials?: TickerFinancials;
146
+ exchangeRates?: Map<string, number>;
147
+ paneState?: Record<string, unknown>;
148
+ }) {
149
+ const initialState = createInitialState(config);
150
+ initialState.focusedPaneId = TEST_PANE_ID;
151
+ if (paneState) initialState.paneState[TEST_PANE_ID] = paneState;
152
+ initialState.tickers = new Map([[ticker.metadata.ticker, ticker]]);
153
+ initialState.financials = new Map([[ticker.metadata.ticker, financials]]);
154
+ if (exchangeRates) initialState.exchangeRates = exchangeRates;
155
+ initialState.brokerAccounts = { "ibkr-flex": [createBrokerAccount()] };
156
+
157
+ const [state, dispatch] = useReducer(appReducer, initialState);
158
+
159
+ return (
160
+ <AppContext value={{ state, dispatch }}>
161
+ <PaneInstanceProvider paneId={TEST_PANE_ID}>
162
+ <PluginRenderProvider pluginId={kellySizerPlugin.id} runtime={createTestPluginRuntime()}>
163
+ <KellySizerPane
164
+ paneId={TEST_PANE_ID}
165
+ paneType="kelly-sizer"
166
+ focused
167
+ width={100}
168
+ height={30}
169
+ />
170
+ </PluginRenderProvider>
171
+ </PaneInstanceProvider>
172
+ </AppContext>
173
+ );
174
+ }
@@ -0,0 +1,156 @@
1
+ export type KellySizingMode =
2
+ | "binary"
3
+ | "scenario"
4
+ | "risk-budget"
5
+ | "prediction-market"
6
+ | "asymmetric";
7
+
8
+ export interface KellyCommonAssumptions {
9
+ kellyFraction: number;
10
+ maxNameFraction: number;
11
+ maxLossFraction: number;
12
+ }
13
+
14
+ export interface BinaryKellyAssumptions extends KellyCommonAssumptions {
15
+ winProbability: number;
16
+ upsideReturn: number;
17
+ downsideReturn: number;
18
+ }
19
+
20
+ export interface ScenarioKellyOutcome {
21
+ id: string;
22
+ label: string;
23
+ probability: number;
24
+ returnPct: number;
25
+ }
26
+
27
+ export interface ScenarioKellyAssumptions extends KellyCommonAssumptions {
28
+ outcomes: ScenarioKellyOutcome[];
29
+ }
30
+
31
+ export interface RiskBudgetKellyAssumptions extends KellyCommonAssumptions {
32
+ riskBudgetFraction: number;
33
+ downsideReturn: number;
34
+ winProbability: number;
35
+ upsideReturn: number;
36
+ }
37
+
38
+ export interface PredictionMarketKellyAssumptions extends KellyCommonAssumptions {
39
+ estimatedProbability: number;
40
+ marketPrice: number;
41
+ side: "yes" | "no";
42
+ }
43
+
44
+ export interface AsymmetricKellyAssumptions extends KellyCommonAssumptions {
45
+ winProbability: number;
46
+ targetReturn: number;
47
+ maxLossReturn: number;
48
+ }
49
+
50
+ export interface KellySizerModeDrafts {
51
+ binary: BinaryKellyAssumptions;
52
+ scenario: ScenarioKellyAssumptions;
53
+ "risk-budget": RiskBudgetKellyAssumptions;
54
+ "prediction-market": PredictionMarketKellyAssumptions;
55
+ asymmetric: AsymmetricKellyAssumptions;
56
+ }
57
+
58
+ export type KellySizerDraft = KellySizerModeDrafts[KellySizingMode];
59
+
60
+ export interface KellyOutcome {
61
+ probability: number;
62
+ returnPct: number;
63
+ }
64
+
65
+ export interface KellySolveResult {
66
+ fraction: number;
67
+ expectedReturn: number;
68
+ expectedLogGrowth: number;
69
+ warning?: string;
70
+ }
71
+
72
+ export interface KellySizingResult {
73
+ mode: KellySizingMode;
74
+ bankroll: number;
75
+ currentValue: number;
76
+ currentFraction: number;
77
+ price: number | null;
78
+ fullKellyFraction: number;
79
+ fractionalKellyFraction: number;
80
+ unclippedFraction: number;
81
+ clippedFraction: number;
82
+ targetValue: number;
83
+ addTrimValue: number;
84
+ estimatedUnits: number | null;
85
+ downsideLossFraction: number;
86
+ riskValue: number;
87
+ riskFraction: number;
88
+ expectedReturn: number;
89
+ expectedLogGrowth: number;
90
+ warnings: string[];
91
+ clipReasons: string[];
92
+ recommendationLabel: string;
93
+ }
94
+
95
+ export interface SensitivityGridCell {
96
+ fraction: number | null;
97
+ text: string;
98
+ }
99
+
100
+ export interface SensitivityGrid {
101
+ rowLabel: string;
102
+ columnLabel: string;
103
+ rows: string[];
104
+ columns: string[];
105
+ cells: SensitivityGridCell[][];
106
+ }
107
+
108
+ export const KELLY_MODES: Array<{ id: KellySizingMode; label: string }> = [
109
+ { id: "binary", label: "Binary" },
110
+ { id: "scenario", label: "Scenario" },
111
+ { id: "risk-budget", label: "Risk" },
112
+ { id: "prediction-market", label: "Market" },
113
+ { id: "asymmetric", label: "Asym" },
114
+ ];
115
+
116
+ export const DEFAULT_KELLY_COMMON_ASSUMPTIONS: KellyCommonAssumptions = {
117
+ kellyFraction: 0.25,
118
+ maxNameFraction: 0.08,
119
+ maxLossFraction: 0.01,
120
+ };
121
+
122
+ export const DEFAULT_KELLY_DRAFTS: KellySizerModeDrafts = {
123
+ binary: {
124
+ winProbability: 0.57,
125
+ upsideReturn: 0.24,
126
+ downsideReturn: -0.09,
127
+ ...DEFAULT_KELLY_COMMON_ASSUMPTIONS,
128
+ },
129
+ scenario: {
130
+ outcomes: [
131
+ { id: "bear", label: "Bear", probability: 0.25, returnPct: -0.12 },
132
+ { id: "base", label: "Base", probability: 0.45, returnPct: 0.08 },
133
+ { id: "bull", label: "Bull", probability: 0.30, returnPct: 0.28 },
134
+ ],
135
+ ...DEFAULT_KELLY_COMMON_ASSUMPTIONS,
136
+ },
137
+ "risk-budget": {
138
+ riskBudgetFraction: 0.01,
139
+ downsideReturn: -0.09,
140
+ winProbability: 0.57,
141
+ upsideReturn: 0.24,
142
+ ...DEFAULT_KELLY_COMMON_ASSUMPTIONS,
143
+ },
144
+ "prediction-market": {
145
+ estimatedProbability: 0.57,
146
+ marketPrice: 0.48,
147
+ side: "yes",
148
+ ...DEFAULT_KELLY_COMMON_ASSUMPTIONS,
149
+ },
150
+ asymmetric: {
151
+ winProbability: 0.42,
152
+ targetReturn: 1.5,
153
+ maxLossReturn: -1,
154
+ ...DEFAULT_KELLY_COMMON_ASSUMPTIONS,
155
+ },
156
+ };
@@ -0,0 +1,77 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act, useState, type Dispatch, type SetStateAction } from "react";
3
+ import { testRender } from "../../../renderers/opentui/test-utils";
4
+ import { InlineFieldView } from "./view";
5
+
6
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
7
+ let setFieldActive: Dispatch<SetStateAction<boolean>> | null = null;
8
+
9
+ afterEach(async () => {
10
+ setFieldActive = null;
11
+ if (!testSetup) return;
12
+ await act(async () => {
13
+ testSetup!.renderer.destroy();
14
+ });
15
+ testSetup = undefined;
16
+ });
17
+
18
+ function InlineFieldHarness({ commits }: { commits: number[] }) {
19
+ const [active, setActive] = useState(false);
20
+ const [value, setValue] = useState(0.01);
21
+ setFieldActive = setActive;
22
+
23
+ return (
24
+ <InlineFieldView
25
+ field={{
26
+ id: "loss-cap",
27
+ label: "Loss cap",
28
+ value,
29
+ percent: true,
30
+ onValue: (nextValue) => {
31
+ commits.push(nextValue);
32
+ setValue(nextValue);
33
+ },
34
+ }}
35
+ active={active}
36
+ focused
37
+ width={30}
38
+ onFocus={() => setActive(true)}
39
+ />
40
+ );
41
+ }
42
+
43
+ describe("InlineFieldView", () => {
44
+ test("replaces the formatted value and commits when focus leaves the active field", async () => {
45
+ const commits: number[] = [];
46
+ testSetup = await testRender(<InlineFieldHarness commits={commits} />, { width: 36, height: 4 });
47
+
48
+ await act(async () => {
49
+ await testSetup!.renderOnce();
50
+ await testSetup!.renderOnce();
51
+ });
52
+
53
+ await act(async () => {
54
+ setFieldActive?.(true);
55
+ await testSetup!.renderOnce();
56
+ await testSetup!.renderOnce();
57
+ });
58
+
59
+ await act(async () => {
60
+ await testSetup!.mockInput.typeText("2");
61
+ await testSetup!.renderOnce();
62
+ });
63
+
64
+ await act(async () => {
65
+ setFieldActive?.(false);
66
+ await testSetup!.renderOnce();
67
+ });
68
+
69
+ await act(async () => {
70
+ await testSetup!.renderOnce();
71
+ await testSetup!.renderOnce();
72
+ });
73
+
74
+ expect(commits.at(-1)).toBeCloseTo(0.02);
75
+ expect(testSetup.captureCharFrame()).toContain("2.00");
76
+ });
77
+ });