gloomberb 0.9.3 → 0.9.5

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 (176) hide show
  1. package/README.md +1 -0
  2. package/package.json +3 -2
  3. package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
  4. package/patches/react-reconciler@0.33.0.patch +10 -0
  5. package/src/app/runtime/desktop-menu.ts +5 -1
  6. package/src/app/runtime/ticker-refresh.ts +4 -1
  7. package/src/app.tsx +10 -1
  8. package/src/brokers/account-cache.ts +1 -1
  9. package/src/brokers/sync-broker-instance.test.ts +107 -0
  10. package/src/brokers/sync-broker-instance.ts +22 -10
  11. package/src/cli/desktop-pane-shot.ts +2 -1
  12. package/src/cli/entry.ts +8 -1
  13. package/src/cli/native-smoke.ts +5 -0
  14. package/src/cli/pane-functions/screenshot.ts +13 -1
  15. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  16. package/src/components/chart/bar-chart-renderer.ts +226 -27
  17. package/src/components/chart/core/pointer.ts +32 -6
  18. package/src/components/chart/native/bitmap-support.test.ts +27 -0
  19. package/src/components/chart/native/bitmap-support.ts +55 -0
  20. package/src/components/chart/native/kitty/index.test.ts +47 -0
  21. package/src/components/chart/native/surface/visibility.ts +8 -1
  22. package/src/components/chart/price-axis-labels.tsx +3 -1
  23. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  24. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  25. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  26. package/src/components/chart/static/chart/bitmap.ts +16 -9
  27. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  28. package/src/components/chart/static/chart/surface.tsx +167 -24
  29. package/src/components/chart/stock-chart/index.test.ts +25 -0
  30. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  31. package/src/components/command-bar/list/view.tsx +1 -1
  32. package/src/components/command-bar/theme-picker.tsx +1 -1
  33. package/src/components/data-table/view.tsx +0 -12
  34. package/src/components/layout/detached-pane-shell.tsx +6 -3
  35. package/src/components/layout/floating-pane.tsx +6 -5
  36. package/src/components/layout/pane/content.tsx +20 -8
  37. package/src/components/layout/pane/footer/registration.tsx +6 -2
  38. package/src/components/layout/pane/frame.tsx +4 -4
  39. package/src/components/layout/pane/header.tsx +45 -4
  40. package/src/components/layout/pane/index.tsx +6 -4
  41. package/src/components/layout/pane/sizing.test.ts +43 -0
  42. package/src/components/layout/pane/sizing.ts +13 -4
  43. package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
  44. package/src/components/layout/shell/cursor-occlusion.ts +204 -0
  45. package/src/components/layout/shell/drag/runtime.ts +9 -2
  46. package/src/components/layout/shell/index.test.tsx +92 -2
  47. package/src/components/layout/shell/index.tsx +45 -3
  48. package/src/components/layout/shell/layout-state.test.ts +33 -0
  49. package/src/components/layout/shell/layout-state.ts +4 -9
  50. package/src/components/layout/shell/pane/layers.tsx +10 -4
  51. package/src/components/layout/shell/visible-layout.ts +17 -0
  52. package/src/components/layout/status-bar.test.tsx +6 -1
  53. package/src/components/layout/status-bar.tsx +10 -6
  54. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  55. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  56. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  57. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  58. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  59. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  60. package/src/components/pane-settings-dialog/desktop.tsx +1 -1
  61. package/src/components/price-performance.tsx +1 -1
  62. package/src/components/price-sparkline/view.tsx +15 -14
  63. package/src/components/speedometer-gauge/terminal.tsx +18 -15
  64. package/src/components/table-view-shared.tsx +12 -14
  65. package/src/components/ticker/list-table-view.tsx +1 -7
  66. package/src/components/ticker/list-table.test.tsx +0 -4
  67. package/src/components/ui/data-table/opentui/index.tsx +29 -22
  68. package/src/components/ui/data-table/types.ts +10 -4
  69. package/src/components/ui/fields.tsx +10 -3
  70. package/src/components/ui/list-view.tsx +1 -1
  71. package/src/components/ui/page-stack-view.tsx +6 -2
  72. package/src/components/ui/table-layout.test.ts +25 -0
  73. package/src/components/ui/table-layout.ts +22 -6
  74. package/src/components/ui/tabs.tsx +27 -12
  75. package/src/components/ui/ui.test.tsx +87 -9
  76. package/src/core/state/app/layout-reducer.ts +2 -2
  77. package/src/plugins/builtin/account-management/form-components.tsx +2 -2
  78. package/src/plugins/builtin/alerts/pane.tsx +1 -1
  79. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  80. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  81. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  82. package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
  83. package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
  84. package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
  85. package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
  86. package/src/plugins/builtin/chat/sidebar.tsx +1 -1
  87. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  88. package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
  89. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
  90. package/src/plugins/builtin/debug/index.tsx +3 -4
  91. package/src/plugins/builtin/help/components.tsx +1 -1
  92. package/src/plugins/builtin/holders/treemap.tsx +1 -1
  93. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  94. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  95. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  96. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  97. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  98. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  99. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  100. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  101. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  102. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  103. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  104. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  105. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  106. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  107. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  108. package/src/plugins/builtin/options/table.ts +5 -5
  109. package/src/plugins/builtin/options/view.test.tsx +141 -4
  110. package/src/plugins/builtin/options/view.tsx +76 -15
  111. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  112. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  113. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  114. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
  115. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  116. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  117. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  118. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  119. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  120. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  121. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  122. package/src/plugins/catalog-ui.ts +2 -0
  123. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  124. package/src/plugins/ibkr/config.test.ts +31 -2
  125. package/src/plugins/ibkr/config.ts +9 -2
  126. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  127. package/src/plugins/ibkr/flex/index.ts +30 -0
  128. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  129. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  130. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  131. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  132. package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
  133. package/src/plugins/pane-manager/gridlock.ts +13 -5
  134. package/src/plugins/pane-manager/layout-state.ts +25 -0
  135. package/src/plugins/pane-manager.test.ts +33 -0
  136. package/src/plugins/pane-manager.ts +6 -1
  137. package/src/plugins/prediction-markets/chart.tsx +10 -4
  138. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  139. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  140. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  141. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  142. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  143. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  144. package/src/plugins/prediction-markets/pane.test.tsx +43 -0
  145. package/src/plugins/prediction-markets/pane.tsx +50 -22
  146. package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
  147. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  148. package/src/renderers/electrobun/view/data-table/index.tsx +19 -12
  149. package/src/renderers/electrobun/view/data-table/row.tsx +32 -18
  150. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  151. package/src/renderers/electrobun/view/host/box.tsx +6 -1
  152. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  153. package/src/renderers/electrobun/view/host/style.ts +1 -0
  154. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  155. package/src/renderers/electrobun/view/styles.css +18 -0
  156. package/src/renderers/opentui/host.tsx +17 -1
  157. package/src/renderers/opentui/native-smoke.ts +1 -0
  158. package/src/renderers/opentui/ui-host.tsx +57 -25
  159. package/src/sources/provider-router/batches.ts +16 -2
  160. package/src/sources/provider-router/financial-routes.ts +5 -0
  161. package/src/sources/provider-router/financials.ts +11 -23
  162. package/src/sources/provider-router/index.test.ts +189 -0
  163. package/src/sources/provider-router/primary.ts +3 -2
  164. package/src/sources/sec-edgar.test.ts +83 -0
  165. package/src/sources/sec-edgar.ts +231 -0
  166. package/src/sources/yahoo-finance.test.ts +51 -0
  167. package/src/sources/yahoo-finance.ts +49 -1
  168. package/src/state/app/context/index.test.tsx +5 -8
  169. package/src/types/broker.ts +1 -0
  170. package/src/types/config.ts +10 -0
  171. package/src/types/trading.ts +2 -0
  172. package/src/ui/host.tsx +16 -0
  173. package/src/ui/index.tsx +2 -0
  174. package/src/utils/financial-statements.ts +23 -0
  175. package/src/version.ts +1 -1
  176. 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
+ });