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.
Files changed (233) hide show
  1. package/README.md +24 -2
  2. package/bin/gloomberb +1 -1
  3. package/package.json +1 -1
  4. package/src/app/runtime/desktop-menu.ts +5 -1
  5. package/src/app/runtime/ticker-refresh.ts +4 -1
  6. package/src/app.tsx +20 -8
  7. package/src/brokers/account-cache.ts +1 -1
  8. package/src/brokers/sync-broker-instance.test.ts +107 -0
  9. package/src/brokers/sync-broker-instance.ts +22 -10
  10. package/src/capabilities/registry.ts +1 -0
  11. package/src/capabilities/types.ts +23 -0
  12. package/src/cli/commands/api.ts +119 -0
  13. package/src/cli/commands/automation.ts +173 -0
  14. package/src/cli/commands/command-utils.ts +53 -0
  15. package/src/cli/commands/market.ts +527 -0
  16. package/src/cli/commands/overview.ts +222 -0
  17. package/src/cli/commands/search.ts +39 -0
  18. package/src/cli/commands/system.ts +553 -0
  19. package/src/cli/commands/ticker.ts +104 -12
  20. package/src/cli/context.ts +35 -2
  21. package/src/cli/desktop-pane-shot.ts +2 -1
  22. package/src/cli/entry.ts +51 -0
  23. package/src/cli/errors.ts +56 -5
  24. package/src/cli/index.ts +73 -11
  25. package/src/cli/options.test.ts +38 -0
  26. package/src/cli/options.ts +95 -0
  27. package/src/cli/pane-functions/index.ts +10 -1
  28. package/src/cli/pane-functions/screenshot.ts +13 -1
  29. package/src/cli/registry.test.ts +40 -0
  30. package/src/cli/registry.ts +46 -5
  31. package/src/cli/result.test.ts +82 -0
  32. package/src/cli/result.ts +149 -0
  33. package/src/cli/types.ts +11 -0
  34. package/src/cli.test.ts +11 -11
  35. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  36. package/src/components/chart/bar-chart-renderer.ts +226 -27
  37. package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
  38. package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
  39. package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
  40. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
  41. package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
  42. package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
  43. package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
  44. package/src/components/chart/core/pane-settings.test.ts +27 -0
  45. package/src/components/chart/core/pane-settings.ts +60 -1
  46. package/src/components/chart/core/pointer.ts +34 -6
  47. package/src/components/chart/price-axis-labels.tsx +3 -1
  48. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  49. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  50. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  51. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  52. package/src/components/chart/static/chart/surface.tsx +167 -24
  53. package/src/components/chart/stock-chart/index.test.ts +25 -0
  54. package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
  55. package/src/components/chart/stock-chart/settings.ts +7 -3
  56. package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
  57. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  58. package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
  59. package/src/components/command-bar/surface/pane.test.tsx +76 -0
  60. package/src/components/data-table/view.tsx +13 -2
  61. package/src/components/feed-data-table/stack-view.tsx +9 -1
  62. package/src/components/index.ts +1 -1
  63. package/src/components/input-search-bar.test.tsx +103 -0
  64. package/src/components/input-search-bar.tsx +16 -0
  65. package/src/components/layout/detached-pane-shell.tsx +6 -3
  66. package/src/components/layout/floating-pane.tsx +3 -2
  67. package/src/components/layout/pane/content.tsx +20 -8
  68. package/src/components/layout/pane/footer/registration.tsx +6 -2
  69. package/src/components/layout/pane/frame.tsx +4 -4
  70. package/src/components/layout/pane/header.tsx +40 -4
  71. package/src/components/layout/pane/index.tsx +3 -1
  72. package/src/components/layout/pane/sizing.test.ts +43 -0
  73. package/src/components/layout/pane/sizing.ts +13 -4
  74. package/src/components/layout/shell/active-drag.ts +5 -1
  75. package/src/components/layout/shell/cursor-occlusion.ts +196 -0
  76. package/src/components/layout/shell/drag/runtime.ts +28 -3
  77. package/src/components/layout/shell/fullscreen.ts +10 -0
  78. package/src/components/layout/shell/index.test.tsx +487 -4
  79. package/src/components/layout/shell/index.tsx +165 -4
  80. package/src/components/layout/shell/layout-state.test.ts +33 -0
  81. package/src/components/layout/shell/layout-state.ts +4 -9
  82. package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
  83. package/src/components/layout/shell/pane/layers.tsx +34 -16
  84. package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
  85. package/src/components/layout/shell/shortcuts.ts +4 -0
  86. package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
  87. package/src/components/layout/shell/visible-layout.ts +17 -0
  88. package/src/components/layout/shell/window-mode/index.ts +11 -3
  89. package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
  90. package/src/components/layout/status-bar.test.tsx +111 -1
  91. package/src/components/layout/status-bar.tsx +37 -7
  92. package/src/components/layout/transient-layout.tsx +57 -0
  93. package/src/components/layout/window-edit/mode.test.ts +76 -5
  94. package/src/components/layout/window-edit/mode.ts +47 -2
  95. package/src/components/layout/window-edit/presentation.ts +2 -1
  96. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  97. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  98. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  99. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  100. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  101. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  102. package/src/components/price-performance.tsx +9 -1
  103. package/src/components/table-view-shared.tsx +12 -4
  104. package/src/components/ui/data-table/opentui/index.tsx +25 -12
  105. package/src/components/ui/data-table/types.ts +4 -0
  106. package/src/components/ui/fields.tsx +10 -3
  107. package/src/components/ui/page-stack-view.tsx +6 -2
  108. package/src/components/ui/table-layout.test.ts +25 -0
  109. package/src/components/ui/table-layout.ts +22 -6
  110. package/src/components/ui/tabs.tsx +26 -10
  111. package/src/components/ui/ui.test.tsx +87 -0
  112. package/src/core/app-services.ts +7 -4
  113. package/src/core/state/app/layout-reducer.ts +2 -2
  114. package/src/data/config/store/index.ts +1 -2
  115. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  116. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  117. package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
  118. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
  119. package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
  120. package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
  121. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  122. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  123. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  124. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  125. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  126. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  127. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  128. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  129. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  130. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  131. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  132. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  133. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  134. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  135. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  136. package/src/plugins/builtin/news/wire/read-state.ts +9 -32
  137. package/src/plugins/builtin/options/view.test.tsx +141 -4
  138. package/src/plugins/builtin/options/view.tsx +76 -15
  139. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  140. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  141. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
  142. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
  143. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  144. package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
  145. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
  146. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  147. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  148. package/src/plugins/builtin/shared/read-state.ts +43 -0
  149. package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
  150. package/src/plugins/builtin/substack/article-detail.tsx +39 -15
  151. package/src/plugins/builtin/substack/article-stack.tsx +6 -2
  152. package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
  153. package/src/plugins/builtin/substack/pane.tsx +8 -3
  154. package/src/plugins/builtin/substack/read-state.ts +55 -0
  155. package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
  156. package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
  157. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  158. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  159. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  160. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  161. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  162. package/src/plugins/catalog-ui.ts +2 -0
  163. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  164. package/src/plugins/ibkr/config.test.ts +31 -2
  165. package/src/plugins/ibkr/config.ts +9 -2
  166. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  167. package/src/plugins/ibkr/flex/index.ts +30 -0
  168. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  169. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  170. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  171. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  172. package/src/plugins/pane-manager/docking.ts +9 -2
  173. package/src/plugins/pane-manager/gridlock.ts +13 -5
  174. package/src/plugins/pane-manager/layout-state.ts +25 -0
  175. package/src/plugins/pane-manager/queries.ts +8 -2
  176. package/src/plugins/pane-manager.test.ts +33 -0
  177. package/src/plugins/pane-manager.ts +6 -1
  178. package/src/plugins/prediction-markets/chart.tsx +10 -4
  179. package/src/plugins/prediction-markets/controller/index.ts +2 -0
  180. package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
  181. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  182. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  183. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  184. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  185. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  186. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  187. package/src/plugins/prediction-markets/pane.test.tsx +75 -0
  188. package/src/plugins/prediction-markets/pane.tsx +71 -23
  189. package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
  190. package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
  191. package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
  192. package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
  193. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  194. package/src/renderers/electrobun/bun/index.ts +13 -0
  195. package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
  196. package/src/renderers/electrobun/bun/window/focus.ts +5 -0
  197. package/src/renderers/electrobun/view/app-services.ts +6 -4
  198. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  199. package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
  200. package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
  201. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  202. package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
  203. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  204. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  205. package/src/renderers/electrobun/view/host/text.test.ts +22 -0
  206. package/src/renderers/electrobun/view/host/text.tsx +24 -20
  207. package/src/renderers/electrobun/view/styles.css +11 -0
  208. package/src/renderers/electrobun/view/ui-host.tsx +1 -1
  209. package/src/renderers/opentui/host.tsx +17 -1
  210. package/src/renderers/opentui/start.tsx +13 -5
  211. package/src/renderers/opentui/ui-host.tsx +57 -25
  212. package/src/sources/provider-router/batches.ts +16 -2
  213. package/src/sources/provider-router/financial-routes.ts +5 -0
  214. package/src/sources/provider-router/financials.ts +11 -23
  215. package/src/sources/provider-router/index.test.ts +189 -0
  216. package/src/sources/provider-router/primary.ts +3 -2
  217. package/src/sources/sec-edgar.test.ts +83 -0
  218. package/src/sources/sec-edgar.ts +231 -0
  219. package/src/sources/yahoo-finance.test.ts +51 -0
  220. package/src/sources/yahoo-finance.ts +49 -1
  221. package/src/state/app/context/index.test.tsx +5 -8
  222. package/src/state/app/input-capture.ts +12 -0
  223. package/src/types/broker.ts +1 -0
  224. package/src/types/config.ts +10 -0
  225. package/src/types/plugin.ts +28 -0
  226. package/src/types/trading.ts +2 -0
  227. package/src/ui/host.tsx +16 -0
  228. package/src/ui/index.tsx +2 -0
  229. package/src/utils/cli-output.ts +6 -0
  230. package/src/utils/financial-statements.ts +23 -0
  231. package/src/utils/search-focus-navigation.ts +24 -0
  232. package/src/version.ts +1 -1
  233. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -0,0 +1,580 @@
1
+ import type { ProjectedChartPoint } from "../../../components/chart/core/data";
2
+ import {
3
+ DEFAULT_KELLY_COMMON_ASSUMPTIONS,
4
+ DEFAULT_KELLY_DRAFTS,
5
+ type AsymmetricKellyAssumptions,
6
+ type BinaryKellyAssumptions,
7
+ type KellyCommonAssumptions,
8
+ type KellyOutcome,
9
+ type KellySizerDraft,
10
+ type KellySizerModeDrafts,
11
+ type KellySizingMode,
12
+ type KellySizingResult,
13
+ type KellySolveResult,
14
+ type PredictionMarketKellyAssumptions,
15
+ type RiskBudgetKellyAssumptions,
16
+ type ScenarioKellyAssumptions,
17
+ type SensitivityGrid,
18
+ type SensitivityGridCell,
19
+ } from "./types";
20
+ export {
21
+ DEFAULT_KELLY_COMMON_ASSUMPTIONS,
22
+ DEFAULT_KELLY_DRAFTS,
23
+ KELLY_MODES,
24
+ } from "./types";
25
+ export type {
26
+ AsymmetricKellyAssumptions,
27
+ BinaryKellyAssumptions,
28
+ KellyCommonAssumptions,
29
+ KellyOutcome,
30
+ KellySizerDraft,
31
+ KellySizerModeDrafts,
32
+ KellySizingMode,
33
+ KellySizingResult,
34
+ KellySolveResult,
35
+ PredictionMarketKellyAssumptions,
36
+ RiskBudgetKellyAssumptions,
37
+ ScenarioKellyAssumptions,
38
+ ScenarioKellyOutcome,
39
+ SensitivityGrid,
40
+ SensitivityGridCell,
41
+ } from "./types";
42
+
43
+ const MAX_NUMERIC_KELLY_FRACTION = 10;
44
+ const SOLVER_ITERATIONS = 80;
45
+
46
+ function finite(value: number): boolean {
47
+ return Number.isFinite(value);
48
+ }
49
+
50
+ function clamp(value: number, min: number, max: number): number {
51
+ return Math.max(min, Math.min(max, value));
52
+ }
53
+
54
+ function sanitizeFraction(value: number, fallback: number, min = 0, max = 1): number {
55
+ return finite(value) ? clamp(value, min, max) : fallback;
56
+ }
57
+
58
+ function normalizeProbability(value: number): number {
59
+ return sanitizeFraction(value, 0, 0, 1);
60
+ }
61
+
62
+ function normalizeOutcomes(outcomes: KellyOutcome[]): KellyOutcome[] {
63
+ const valid = outcomes.filter((outcome) => (
64
+ finite(outcome.probability)
65
+ && outcome.probability > 0
66
+ && finite(outcome.returnPct)
67
+ && outcome.returnPct >= -1
68
+ ));
69
+ const totalProbability = valid.reduce((sum, outcome) => sum + outcome.probability, 0);
70
+ if (totalProbability <= 0) return [];
71
+ return valid.map((outcome) => ({
72
+ probability: outcome.probability / totalProbability,
73
+ returnPct: outcome.returnPct,
74
+ }));
75
+ }
76
+
77
+ function expectedReturn(outcomes: KellyOutcome[]): number {
78
+ return outcomes.reduce((sum, outcome) => sum + outcome.probability * outcome.returnPct, 0);
79
+ }
80
+
81
+ export function computeExpectedLogGrowth(fraction: number, outcomes: KellyOutcome[]): number {
82
+ if (!finite(fraction) || fraction < 0) return Number.NEGATIVE_INFINITY;
83
+ let growth = 0;
84
+ for (const outcome of outcomes) {
85
+ const terminalValue = 1 + fraction * outcome.returnPct;
86
+ if (terminalValue <= 0) return Number.NEGATIVE_INFINITY;
87
+ growth += outcome.probability * Math.log(terminalValue);
88
+ }
89
+ return growth;
90
+ }
91
+
92
+ export function calculateExpectedLogGrowthAtFraction(
93
+ mode: KellySizingMode,
94
+ draft: KellySizerDraft,
95
+ fraction: number,
96
+ ): number {
97
+ if (!finite(fraction) || fraction < 0) return 0;
98
+ const { outcomes } = getModeOutcomes(mode, draft);
99
+ const normalizedOutcomes = normalizeOutcomes(outcomes);
100
+ if (normalizedOutcomes.length === 0) return 0;
101
+ const growth = computeExpectedLogGrowth(fraction, normalizedOutcomes);
102
+ return finite(growth) ? growth : 0;
103
+ }
104
+
105
+ function derivativeAt(fraction: number, outcomes: KellyOutcome[]): number {
106
+ let derivative = 0;
107
+ for (const outcome of outcomes) {
108
+ derivative += outcome.probability * outcome.returnPct / (1 + fraction * outcome.returnPct);
109
+ }
110
+ return derivative;
111
+ }
112
+
113
+ export function solveKellyFraction(rawOutcomes: KellyOutcome[]): KellySolveResult {
114
+ const outcomes = normalizeOutcomes(rawOutcomes);
115
+ if (outcomes.length === 0) {
116
+ return {
117
+ fraction: 0,
118
+ expectedReturn: 0,
119
+ expectedLogGrowth: 0,
120
+ warning: "No valid payoff outcomes.",
121
+ };
122
+ }
123
+
124
+ const edge = expectedReturn(outcomes);
125
+ if (edge <= 0) {
126
+ return {
127
+ fraction: 0,
128
+ expectedReturn: edge,
129
+ expectedLogGrowth: 0,
130
+ };
131
+ }
132
+
133
+ const negativeReturns = outcomes.map((outcome) => outcome.returnPct).filter((value) => value < 0);
134
+ if (negativeReturns.length === 0) {
135
+ return {
136
+ fraction: MAX_NUMERIC_KELLY_FRACTION,
137
+ expectedReturn: edge,
138
+ expectedLogGrowth: computeExpectedLogGrowth(MAX_NUMERIC_KELLY_FRACTION, outcomes),
139
+ warning: "No downside outcome; Kelly is unbounded.",
140
+ };
141
+ }
142
+
143
+ const worstReturn = Math.min(...negativeReturns);
144
+ let low = 0;
145
+ let high = Math.min(MAX_NUMERIC_KELLY_FRACTION, (-1 / worstReturn) * 0.999999);
146
+ if (!finite(high) || high <= 0) {
147
+ return {
148
+ fraction: 0,
149
+ expectedReturn: edge,
150
+ expectedLogGrowth: 0,
151
+ warning: "Invalid downside domain.",
152
+ };
153
+ }
154
+
155
+ if (derivativeAt(low, outcomes) <= 0) {
156
+ return {
157
+ fraction: 0,
158
+ expectedReturn: edge,
159
+ expectedLogGrowth: 0,
160
+ };
161
+ }
162
+
163
+ const highDerivative = derivativeAt(high, outcomes);
164
+ if (highDerivative > 0) {
165
+ return {
166
+ fraction: high,
167
+ expectedReturn: edge,
168
+ expectedLogGrowth: computeExpectedLogGrowth(high, outcomes),
169
+ warning: "Optimum is beyond the pane's solver cap.",
170
+ };
171
+ }
172
+
173
+ for (let i = 0; i < SOLVER_ITERATIONS; i++) {
174
+ const mid = (low + high) / 2;
175
+ if (derivativeAt(mid, outcomes) > 0) {
176
+ low = mid;
177
+ } else {
178
+ high = mid;
179
+ }
180
+ }
181
+
182
+ const fraction = (low + high) / 2;
183
+ return {
184
+ fraction,
185
+ expectedReturn: edge,
186
+ expectedLogGrowth: computeExpectedLogGrowth(fraction, outcomes),
187
+ };
188
+ }
189
+
190
+ export function buildBinaryOutcomes(
191
+ winProbability: number,
192
+ upsideReturn: number,
193
+ downsideReturn: number,
194
+ ): KellyOutcome[] {
195
+ const probability = normalizeProbability(winProbability);
196
+ return [
197
+ { probability, returnPct: Math.max(0, upsideReturn) },
198
+ { probability: 1 - probability, returnPct: Math.min(0, downsideReturn) },
199
+ ];
200
+ }
201
+
202
+ function getModeOutcomes(
203
+ mode: KellySizingMode,
204
+ draft: KellySizerDraft,
205
+ ): { outcomes: KellyOutcome[]; recommendationLabel: string; warnings: string[] } {
206
+ if (mode === "binary") {
207
+ const binary = draft as BinaryKellyAssumptions;
208
+ return {
209
+ outcomes: buildBinaryOutcomes(binary.winProbability, binary.upsideReturn, binary.downsideReturn),
210
+ recommendationLabel: "Binary thesis",
211
+ warnings: [],
212
+ };
213
+ }
214
+
215
+ if (mode === "scenario") {
216
+ const scenario = draft as ScenarioKellyAssumptions;
217
+ return {
218
+ outcomes: scenario.outcomes.map((outcome) => ({
219
+ probability: outcome.probability,
220
+ returnPct: outcome.returnPct,
221
+ })),
222
+ recommendationLabel: "Scenario tree",
223
+ warnings: [],
224
+ };
225
+ }
226
+
227
+ if (mode === "risk-budget") {
228
+ const risk = draft as RiskBudgetKellyAssumptions;
229
+ return {
230
+ outcomes: buildBinaryOutcomes(risk.winProbability, risk.upsideReturn, risk.downsideReturn),
231
+ recommendationLabel: "Risk budget",
232
+ warnings: [],
233
+ };
234
+ }
235
+
236
+ if (mode === "prediction-market") {
237
+ const market = draft as PredictionMarketKellyAssumptions;
238
+ const price = sanitizeFraction(market.side === "yes" ? market.marketPrice : 1 - market.marketPrice, 0, 0.0001, 0.9999);
239
+ const winProbability = market.side === "yes"
240
+ ? normalizeProbability(market.estimatedProbability)
241
+ : 1 - normalizeProbability(market.estimatedProbability);
242
+ return {
243
+ outcomes: buildBinaryOutcomes(winProbability, (1 - price) / price, -1),
244
+ recommendationLabel: `${market.side.toUpperCase()} contract`,
245
+ warnings: [],
246
+ };
247
+ }
248
+
249
+ const asymmetric = draft as AsymmetricKellyAssumptions;
250
+ return {
251
+ outcomes: buildBinaryOutcomes(
252
+ asymmetric.winProbability,
253
+ asymmetric.targetReturn,
254
+ asymmetric.maxLossReturn,
255
+ ),
256
+ recommendationLabel: "Asymmetric payoff",
257
+ warnings: [],
258
+ };
259
+ }
260
+
261
+ function getCommonAssumptions(draft: KellySizerDraft): KellyCommonAssumptions {
262
+ return normalizeKellyCommonAssumptions(draft);
263
+ }
264
+
265
+ export function normalizeKellyCommonAssumptions(
266
+ value: Partial<KellyCommonAssumptions> | null | undefined,
267
+ fallback: KellyCommonAssumptions = DEFAULT_KELLY_COMMON_ASSUMPTIONS,
268
+ ): KellyCommonAssumptions {
269
+ return {
270
+ kellyFraction: sanitizeFraction(value?.kellyFraction ?? fallback.kellyFraction, fallback.kellyFraction, 0, 1),
271
+ maxNameFraction: sanitizeFraction(value?.maxNameFraction ?? fallback.maxNameFraction, fallback.maxNameFraction, 0, 1),
272
+ maxLossFraction: sanitizeFraction(value?.maxLossFraction ?? fallback.maxLossFraction, fallback.maxLossFraction, 0, 1),
273
+ };
274
+ }
275
+
276
+ export function applyKellyCommonAssumptions<T extends KellySizerDraft>(
277
+ draft: T,
278
+ common: KellyCommonAssumptions,
279
+ ): T {
280
+ return {
281
+ ...draft,
282
+ ...normalizeKellyCommonAssumptions(common),
283
+ };
284
+ }
285
+
286
+ function getDownsideLossFraction(mode: KellySizingMode, draft: KellySizerDraft, outcomes: KellyOutcome[]): number {
287
+ if (mode === "risk-budget") {
288
+ return Math.abs(Math.min(0, (draft as RiskBudgetKellyAssumptions).downsideReturn));
289
+ }
290
+ const worstReturn = Math.min(...outcomes.map((outcome) => outcome.returnPct), 0);
291
+ return Math.abs(worstReturn);
292
+ }
293
+
294
+ export function calculateKellySizing({
295
+ mode,
296
+ draft,
297
+ bankroll,
298
+ currentValue = 0,
299
+ price = null,
300
+ }: {
301
+ mode: KellySizingMode;
302
+ draft: KellySizerDraft;
303
+ bankroll: number;
304
+ currentValue?: number;
305
+ price?: number | null;
306
+ }): KellySizingResult {
307
+ const warnings: string[] = [];
308
+ if (!finite(bankroll) || bankroll <= 0) {
309
+ return {
310
+ mode,
311
+ bankroll: 0,
312
+ currentValue: 0,
313
+ currentFraction: 0,
314
+ price: null,
315
+ fullKellyFraction: 0,
316
+ fractionalKellyFraction: 0,
317
+ unclippedFraction: 0,
318
+ clippedFraction: 0,
319
+ targetValue: 0,
320
+ addTrimValue: 0,
321
+ estimatedUnits: null,
322
+ downsideLossFraction: 0,
323
+ riskValue: 0,
324
+ riskFraction: 0,
325
+ expectedReturn: 0,
326
+ expectedLogGrowth: 0,
327
+ warnings: ["No positive bankroll available."],
328
+ clipReasons: [],
329
+ recommendationLabel: "Unavailable",
330
+ };
331
+ }
332
+
333
+ const { outcomes, recommendationLabel, warnings: outcomeWarnings } = getModeOutcomes(mode, draft);
334
+ warnings.push(...outcomeWarnings);
335
+ const normalizedOutcomes = normalizeOutcomes(outcomes);
336
+ const solve = solveKellyFraction(normalizedOutcomes);
337
+ if (solve.warning) warnings.push(solve.warning);
338
+
339
+ const common = getCommonAssumptions(draft);
340
+ const downsideLossFraction = getDownsideLossFraction(mode, draft, normalizedOutcomes);
341
+ const fullKellyFraction = Math.max(0, solve.fraction);
342
+ const fractionalKellyFraction = fullKellyFraction * common.kellyFraction;
343
+ const riskBudgetFraction = mode === "risk-budget"
344
+ ? (draft as RiskBudgetKellyAssumptions).riskBudgetFraction
345
+ : null;
346
+ const riskBudgetSize = riskBudgetFraction != null && downsideLossFraction > 0
347
+ ? Math.max(0, riskBudgetFraction) / downsideLossFraction
348
+ : null;
349
+ const unclippedFraction = riskBudgetSize ?? fractionalKellyFraction;
350
+ let clippedFraction = Math.max(0, unclippedFraction);
351
+ const clipReasons: string[] = [];
352
+
353
+ if (common.maxLossFraction > 0 && downsideLossFraction > 0) {
354
+ const lossCapFraction = common.maxLossFraction / downsideLossFraction;
355
+ if (clippedFraction > lossCapFraction) {
356
+ clippedFraction = lossCapFraction;
357
+ clipReasons.push("max loss");
358
+ }
359
+ }
360
+
361
+ if (common.maxNameFraction > 0 && clippedFraction > common.maxNameFraction) {
362
+ clippedFraction = common.maxNameFraction;
363
+ clipReasons.push("max name");
364
+ }
365
+
366
+ const safeCurrentValue = finite(currentValue) ? Math.max(0, currentValue) : 0;
367
+ const safePrice = price != null && finite(price) && price > 0 ? price : null;
368
+ const targetValue = clippedFraction * bankroll;
369
+ const addTrimValue = targetValue - safeCurrentValue;
370
+ const estimatedUnits = safePrice ? addTrimValue / safePrice : null;
371
+ const riskFraction = clippedFraction * downsideLossFraction;
372
+ const riskValue = riskFraction * bankroll;
373
+
374
+ if (fullKellyFraction === 0 && solve.expectedReturn <= 0) warnings.push("No positive Kelly edge.");
375
+ if (downsideLossFraction <= 0) warnings.push("No downside assumption; risk caps cannot bind.");
376
+
377
+ return {
378
+ mode,
379
+ bankroll,
380
+ currentValue: safeCurrentValue,
381
+ currentFraction: safeCurrentValue / bankroll,
382
+ price: safePrice,
383
+ fullKellyFraction,
384
+ fractionalKellyFraction,
385
+ unclippedFraction,
386
+ clippedFraction,
387
+ targetValue,
388
+ addTrimValue,
389
+ estimatedUnits,
390
+ downsideLossFraction,
391
+ riskValue,
392
+ riskFraction,
393
+ expectedReturn: solve.expectedReturn,
394
+ expectedLogGrowth: solve.expectedLogGrowth,
395
+ warnings,
396
+ clipReasons,
397
+ recommendationLabel,
398
+ };
399
+ }
400
+
401
+ function formatPercentCell(value: number | null): string {
402
+ if (value == null || !finite(value)) return "—";
403
+ return `${(value * 100).toFixed(Math.abs(value) >= 0.1 ? 1 : 2)}%`;
404
+ }
405
+
406
+ function resultForSensitivity(mode: KellySizingMode, draft: KellySizerDraft): SensitivityGridCell {
407
+ const result = calculateKellySizing({
408
+ mode,
409
+ draft,
410
+ bankroll: 100,
411
+ currentValue: 0,
412
+ price: 1,
413
+ });
414
+ return {
415
+ fraction: result.clippedFraction,
416
+ text: formatPercentCell(result.clippedFraction),
417
+ };
418
+ }
419
+
420
+ function applyCommon<T extends KellyCommonAssumptions>(base: T, patch: Partial<T>): T {
421
+ return { ...base, ...patch };
422
+ }
423
+
424
+ function probabilityLabels(center: number): number[] {
425
+ return [-0.05, 0, 0.05].map((offset) => clamp(center + offset, 0.01, 0.99));
426
+ }
427
+
428
+ export function buildSensitivityGrid(mode: KellySizingMode, draft: KellySizerDraft): SensitivityGrid {
429
+ if (mode === "prediction-market") {
430
+ const market = draft as PredictionMarketKellyAssumptions;
431
+ const rows = probabilityLabels(market.estimatedProbability);
432
+ const columns = [-0.05, 0, 0.05].map((offset) => clamp(market.marketPrice + offset, 0.01, 0.99));
433
+ return {
434
+ rowLabel: "Est p",
435
+ columnLabel: "Price",
436
+ rows: rows.map(formatPercentCell),
437
+ columns: columns.map(formatPercentCell),
438
+ cells: rows.map((estimatedProbability) => columns.map((marketPrice) => (
439
+ resultForSensitivity(mode, applyCommon(market, { estimatedProbability, marketPrice }))
440
+ ))),
441
+ };
442
+ }
443
+
444
+ if (mode === "scenario") {
445
+ const scenario = draft as ScenarioKellyAssumptions;
446
+ const bear = scenario.outcomes[0] ?? DEFAULT_KELLY_DRAFTS.scenario.outcomes[0]!;
447
+ const base = scenario.outcomes[1] ?? DEFAULT_KELLY_DRAFTS.scenario.outcomes[1]!;
448
+ const bull = scenario.outcomes[2] ?? DEFAULT_KELLY_DRAFTS.scenario.outcomes[2]!;
449
+ const bearProbabilities = probabilityLabels(bear.probability);
450
+ const bullReturns = [-0.06, 0, 0.06].map((offset) => bull.returnPct + offset);
451
+ return {
452
+ rowLabel: "Bear p",
453
+ columnLabel: "Bull ret",
454
+ rows: bearProbabilities.map(formatPercentCell),
455
+ columns: bullReturns.map(formatPercentCell),
456
+ cells: bearProbabilities.map((bearProbability) => bullReturns.map((bullReturn) => {
457
+ const bullProbability = bull.probability;
458
+ const baseProbability = Math.max(0.01, 1 - bearProbability - bullProbability);
459
+ return resultForSensitivity(mode, {
460
+ ...scenario,
461
+ outcomes: [
462
+ { ...bear, probability: bearProbability },
463
+ { ...base, probability: baseProbability },
464
+ { ...bull, returnPct: bullReturn },
465
+ ],
466
+ });
467
+ })),
468
+ };
469
+ }
470
+
471
+ const twoOutcome = mode === "risk-budget"
472
+ ? draft as RiskBudgetKellyAssumptions
473
+ : mode === "asymmetric"
474
+ ? draft as AsymmetricKellyAssumptions
475
+ : draft as BinaryKellyAssumptions;
476
+ const winProbability = "winProbability" in twoOutcome ? twoOutcome.winProbability : 0.5;
477
+ const upside = "upsideReturn" in twoOutcome
478
+ ? twoOutcome.upsideReturn
479
+ : (twoOutcome as AsymmetricKellyAssumptions).targetReturn;
480
+ const rows = probabilityLabels(winProbability);
481
+ const columns = [-0.06, 0, 0.06].map((offset) => Math.max(0.01, upside + offset));
482
+
483
+ return {
484
+ rowLabel: "Win p",
485
+ columnLabel: "Upside",
486
+ rows: rows.map(formatPercentCell),
487
+ columns: columns.map(formatPercentCell),
488
+ cells: rows.map((probability) => columns.map((upsideReturn) => {
489
+ if (mode === "risk-budget") {
490
+ return resultForSensitivity(mode, applyCommon(draft as RiskBudgetKellyAssumptions, {
491
+ winProbability: probability,
492
+ upsideReturn,
493
+ }));
494
+ }
495
+ if (mode === "asymmetric") {
496
+ return resultForSensitivity(mode, applyCommon(draft as AsymmetricKellyAssumptions, {
497
+ winProbability: probability,
498
+ targetReturn: upsideReturn,
499
+ }));
500
+ }
501
+ return resultForSensitivity(mode, applyCommon(draft as BinaryKellyAssumptions, {
502
+ winProbability: probability,
503
+ upsideReturn,
504
+ }));
505
+ })),
506
+ };
507
+ }
508
+
509
+ export function buildKellyCurvePoints(
510
+ mode: KellySizingMode,
511
+ draft: KellySizerDraft,
512
+ maxFractionOverride?: number,
513
+ ): ProjectedChartPoint[] {
514
+ const { outcomes } = getModeOutcomes(mode, draft);
515
+ const normalizedOutcomes = normalizeOutcomes(outcomes);
516
+ if (normalizedOutcomes.length === 0) return [];
517
+ const maxFraction = maxFractionOverride ?? getKellyCurveMaxFraction(mode, draft);
518
+ if (maxFraction <= 0) return [];
519
+
520
+ return Array.from({ length: 32 }, (_, index) => {
521
+ const fraction = (maxFraction * index) / 31;
522
+ const growth = computeExpectedLogGrowth(fraction, normalizedOutcomes);
523
+ const safeGrowth = finite(growth) ? growth : 0;
524
+ return {
525
+ date: new Date(Date.UTC(2026, 0, 1 + index)),
526
+ open: safeGrowth,
527
+ high: safeGrowth,
528
+ low: safeGrowth,
529
+ close: safeGrowth,
530
+ volume: 0,
531
+ };
532
+ });
533
+ }
534
+
535
+ export function getKellyCurveMaxFraction(
536
+ mode: KellySizingMode,
537
+ draft: KellySizerDraft,
538
+ focusFractions: number[] = [],
539
+ ): number {
540
+ const { outcomes } = getModeOutcomes(mode, draft);
541
+ const normalizedOutcomes = normalizeOutcomes(outcomes);
542
+ if (normalizedOutcomes.length === 0) return 0;
543
+
544
+ const result = calculateKellySizing({
545
+ mode,
546
+ draft,
547
+ bankroll: 100,
548
+ currentValue: 0,
549
+ price: 1,
550
+ });
551
+ const worstReturn = Math.min(...normalizedOutcomes.map((outcome) => outcome.returnPct), 0);
552
+ const domainLimit = worstReturn < 0 ? (-1 / worstReturn) * 0.95 : MAX_NUMERIC_KELLY_FRACTION;
553
+ const focusMax = Math.max(0, ...focusFractions.filter((value) => finite(value) && value >= 0));
554
+ const maxFraction = clamp(
555
+ Math.max(result.fullKellyFraction * 1.4, result.clippedFraction * 2, focusMax * 1.15, 0.2),
556
+ 0.05,
557
+ Math.min(MAX_NUMERIC_KELLY_FRACTION, domainLimit),
558
+ );
559
+ return maxFraction;
560
+ }
561
+
562
+ export function cloneKellyDrafts(drafts: Partial<KellySizerModeDrafts> = DEFAULT_KELLY_DRAFTS): KellySizerModeDrafts {
563
+ const binary = { ...DEFAULT_KELLY_DRAFTS.binary, ...drafts.binary };
564
+ const scenario = {
565
+ ...DEFAULT_KELLY_DRAFTS.scenario,
566
+ ...drafts.scenario,
567
+ outcomes: (drafts.scenario?.outcomes ?? DEFAULT_KELLY_DRAFTS.scenario.outcomes).map((outcome) => ({ ...outcome })),
568
+ };
569
+ const riskBudget = { ...DEFAULT_KELLY_DRAFTS["risk-budget"], ...drafts["risk-budget"] };
570
+ const predictionMarket = { ...DEFAULT_KELLY_DRAFTS["prediction-market"], ...drafts["prediction-market"] };
571
+ const asymmetric = { ...DEFAULT_KELLY_DRAFTS.asymmetric, ...drafts.asymmetric };
572
+
573
+ return {
574
+ binary,
575
+ scenario,
576
+ "risk-budget": riskBudget,
577
+ "prediction-market": predictionMarket,
578
+ asymmetric,
579
+ };
580
+ }