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.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/app/runtime/desktop-menu.ts +5 -1
- package/src/app/runtime/ticker-refresh.ts +4 -1
- package/src/app.tsx +10 -1
- package/src/brokers/account-cache.ts +1 -1
- package/src/brokers/sync-broker-instance.test.ts +107 -0
- package/src/brokers/sync-broker-instance.ts +22 -10
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/components/chart/bar-chart-renderer.test.ts +108 -3
- package/src/components/chart/bar-chart-renderer.ts +226 -27
- package/src/components/chart/core/pointer.ts +32 -6
- package/src/components/chart/price-axis-labels.tsx +3 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
- package/src/components/chart/static/bar-chart-surface.tsx +151 -28
- package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
- package/src/components/chart/static/chart/surface.test.tsx +92 -0
- package/src/components/chart/static/chart/surface.tsx +167 -24
- package/src/components/chart/stock-chart/index.test.ts +25 -0
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/pane/content.tsx +20 -8
- package/src/components/layout/pane/footer/registration.tsx +6 -2
- package/src/components/layout/pane/frame.tsx +4 -4
- package/src/components/layout/pane/header.tsx +40 -4
- package/src/components/layout/pane/index.tsx +3 -1
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/cursor-occlusion.ts +196 -0
- package/src/components/layout/shell/drag/runtime.ts +9 -2
- package/src/components/layout/shell/index.test.tsx +92 -2
- package/src/components/layout/shell/index.tsx +37 -2
- package/src/components/layout/shell/layout-state.test.ts +33 -0
- package/src/components/layout/shell/layout-state.ts +4 -9
- package/src/components/layout/shell/pane/layers.tsx +8 -2
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/status-bar.test.tsx +6 -1
- package/src/components/layout/status-bar.tsx +5 -1
- package/src/components/onboarding/onboarding-steps.tsx +8 -28
- package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
- package/src/components/onboarding/onboarding-wizard.tsx +2 -4
- package/src/components/onboarding/portfolio-step/index.tsx +0 -12
- package/src/components/onboarding/portfolio-step/types.ts +1 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -5
- package/src/components/table-view-shared.tsx +12 -4
- package/src/components/ui/data-table/opentui/index.tsx +25 -12
- package/src/components/ui/data-table/types.ts +4 -0
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/page-stack-view.tsx +6 -2
- package/src/components/ui/table-layout.test.ts +25 -0
- package/src/components/ui/table-layout.ts +22 -6
- package/src/components/ui/tabs.tsx +26 -10
- package/src/components/ui/ui.test.tsx +87 -0
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
- package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
- package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
- package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
- package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
- package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
- package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
- package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
- package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
- package/src/plugins/builtin/layout-manager/index.tsx +5 -1
- package/src/plugins/builtin/options/view.test.tsx +141 -4
- package/src/plugins/builtin/options/view.tsx +76 -15
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
- package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/ibkr/broker-adapter.ts +19 -0
- package/src/plugins/ibkr/config.test.ts +31 -2
- package/src/plugins/ibkr/config.ts +9 -2
- package/src/plugins/ibkr/flex/index.test.ts +18 -9
- package/src/plugins/ibkr/flex/index.ts +30 -0
- package/src/plugins/ibkr/flex/statement-client.ts +68 -14
- package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
- package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
- package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager.test.ts +33 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +10 -4
- package/src/plugins/prediction-markets/detail/book.tsx +141 -96
- package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
- package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
- package/src/plugins/prediction-markets/detail.test.tsx +50 -0
- package/src/plugins/prediction-markets/pane.test.tsx +43 -0
- package/src/plugins/prediction-markets/pane.tsx +50 -22
- package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
- package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
- package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/styles.css +11 -0
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/ui-host.tsx +57 -25
- package/src/sources/provider-router/batches.ts +16 -2
- package/src/sources/provider-router/financial-routes.ts +5 -0
- package/src/sources/provider-router/financials.ts +11 -23
- package/src/sources/provider-router/index.test.ts +189 -0
- package/src/sources/provider-router/primary.ts +3 -2
- package/src/sources/sec-edgar.test.ts +83 -0
- package/src/sources/sec-edgar.ts +231 -0
- package/src/sources/yahoo-finance.test.ts +51 -0
- package/src/sources/yahoo-finance.ts +49 -1
- package/src/state/app/context/index.test.tsx +5 -8
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/trading.ts +2 -0
- package/src/ui/host.tsx +16 -0
- package/src/ui/index.tsx +2 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/version.ts +1 -1
- package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { testRender } from "../../../renderers/opentui/test-utils";
|
|
4
|
+
import { setSharedMarketDataCoordinator } from "../../../market-data/coordinator";
|
|
5
|
+
import type { TickerRecord } from "../../../types/ticker";
|
|
6
|
+
import { KellySizerHarness, createFinancials, createSizerConfig, createTicker } from "./test-support";
|
|
7
|
+
|
|
8
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
9
|
+
|
|
10
|
+
async function renderPane(props?: Parameters<typeof KellySizerHarness>[0]) {
|
|
11
|
+
await act(async () => {
|
|
12
|
+
testSetup = await testRender(<KellySizerHarness {...props} />, { width: 100, height: 30 });
|
|
13
|
+
await Promise.resolve();
|
|
14
|
+
await testSetup.renderOnce();
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function flushFrame() {
|
|
19
|
+
await act(async () => {
|
|
20
|
+
await testSetup!.renderOnce();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
setSharedMarketDataCoordinator(null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
if (testSetup) {
|
|
30
|
+
await act(async () => {
|
|
31
|
+
testSetup!.renderer.destroy();
|
|
32
|
+
});
|
|
33
|
+
testSetup = undefined;
|
|
34
|
+
}
|
|
35
|
+
setSharedMarketDataCoordinator(null);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("KellySizerPane", () => {
|
|
39
|
+
test("renders binary sizing context, result, and curve", async () => {
|
|
40
|
+
await renderPane();
|
|
41
|
+
await flushFrame();
|
|
42
|
+
|
|
43
|
+
const frame = testSetup!.captureCharFrame();
|
|
44
|
+
expect(frame).toContain("AAPL · Main Portfolio");
|
|
45
|
+
expect(frame).toContain("Bankroll");
|
|
46
|
+
expect(frame).toContain("100000");
|
|
47
|
+
expect(frame).toContain("Win p");
|
|
48
|
+
expect(frame).toContain("Full Kelly");
|
|
49
|
+
expect(frame).toContain("Clipped");
|
|
50
|
+
expect(frame).toContain("Target val");
|
|
51
|
+
expect(frame).toContain("Current %");
|
|
52
|
+
expect(frame).toContain("Kelly Curve");
|
|
53
|
+
expect(frame).toContain("4.0% -> 8.0%");
|
|
54
|
+
expect(frame).toContain("growth");
|
|
55
|
+
expect(frame).toContain("loss cap");
|
|
56
|
+
expect(frame).toContain("cap max loss, max name");
|
|
57
|
+
expect(frame).toContain("full");
|
|
58
|
+
expect(frame).toContain("159%");
|
|
59
|
+
expect(frame).toContain("636%");
|
|
60
|
+
expect(frame).not.toContain("Mode");
|
|
61
|
+
expect(frame).not.toContain("Sensitivity");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("prefills shared caps from plugin config", async () => {
|
|
65
|
+
const config = {
|
|
66
|
+
...createSizerConfig(),
|
|
67
|
+
pluginConfig: {
|
|
68
|
+
"kelly-sizer": {
|
|
69
|
+
"commonAssumptions:v1": {
|
|
70
|
+
kellyFraction: 0.5,
|
|
71
|
+
maxLossFraction: 0.02,
|
|
72
|
+
maxNameFraction: 0.1,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
await renderPane({ config, paneState: { mode: "scenario" } });
|
|
79
|
+
await flushFrame();
|
|
80
|
+
|
|
81
|
+
const frame = testSetup!.captureCharFrame();
|
|
82
|
+
expect(frame).toContain("Scenario");
|
|
83
|
+
expect(frame).toMatch(/Kelly\s+50\.0\s+%/);
|
|
84
|
+
expect(frame).toMatch(/Loss cap\s+2\.00\s+%/);
|
|
85
|
+
expect(frame).toMatch(/Name cap\s+10\.0\s+%/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("keeps shared caps scoped to pane state after seeding", async () => {
|
|
89
|
+
const config = {
|
|
90
|
+
...createSizerConfig(),
|
|
91
|
+
pluginConfig: {
|
|
92
|
+
"kelly-sizer": {
|
|
93
|
+
"commonAssumptions:v1": {
|
|
94
|
+
kellyFraction: 0.5,
|
|
95
|
+
maxLossFraction: 0.02,
|
|
96
|
+
maxNameFraction: 0.1,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
await renderPane({
|
|
103
|
+
config,
|
|
104
|
+
paneState: {
|
|
105
|
+
mode: "scenario",
|
|
106
|
+
"commonAssumptions:v1": {
|
|
107
|
+
kellyFraction: 0.3,
|
|
108
|
+
maxLossFraction: 0.015,
|
|
109
|
+
maxNameFraction: 0.07,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
await flushFrame();
|
|
114
|
+
|
|
115
|
+
const frame = testSetup!.captureCharFrame();
|
|
116
|
+
expect(frame).toContain("Scenario");
|
|
117
|
+
expect(frame).toMatch(/Kelly\s+30\.0\s+%/);
|
|
118
|
+
expect(frame).toMatch(/Loss cap\s+1\.50\s+%/);
|
|
119
|
+
expect(frame).toMatch(/Name cap\s+7\.00\s+%/);
|
|
120
|
+
expect(frame).not.toMatch(/Kelly\s+50\.0\s+%/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("converts foreign quote current value to base currency", async () => {
|
|
124
|
+
const ticker = createTicker({
|
|
125
|
+
symbol: "SIVE",
|
|
126
|
+
currency: "SEK",
|
|
127
|
+
positions: [{
|
|
128
|
+
portfolio: "main",
|
|
129
|
+
shares: 100,
|
|
130
|
+
avgCost: 50,
|
|
131
|
+
currency: "SEK",
|
|
132
|
+
broker: "ibkr",
|
|
133
|
+
brokerInstanceId: "ibkr-flex",
|
|
134
|
+
brokerAccountId: "DU12345",
|
|
135
|
+
marketValue: 10_000,
|
|
136
|
+
unrealizedPnl: 5_000,
|
|
137
|
+
}],
|
|
138
|
+
});
|
|
139
|
+
const financials = createFinancials({ symbol: "SIVE", price: 100, currency: "SEK" });
|
|
140
|
+
|
|
141
|
+
await renderPane({
|
|
142
|
+
config: createSizerConfig("SIVE"),
|
|
143
|
+
ticker,
|
|
144
|
+
financials,
|
|
145
|
+
exchangeRates: new Map([["USD", 1], ["SEK", 0.1]]),
|
|
146
|
+
});
|
|
147
|
+
await flushFrame();
|
|
148
|
+
|
|
149
|
+
const frame = testSetup!.captureCharFrame();
|
|
150
|
+
expect(frame).toContain("SIVE · Main Portfolio");
|
|
151
|
+
expect(frame).toMatch(/Current\s+1000\s+USD/);
|
|
152
|
+
expect(frame).not.toMatch(/Current\s+10000\s+USD/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("toggles sensitivity with s", async () => {
|
|
156
|
+
await renderPane();
|
|
157
|
+
await flushFrame();
|
|
158
|
+
|
|
159
|
+
await act(async () => {
|
|
160
|
+
(testSetup!.renderer.keyInput as any).emit("keypress", {
|
|
161
|
+
name: "s",
|
|
162
|
+
sequence: "s",
|
|
163
|
+
ctrl: false,
|
|
164
|
+
meta: false,
|
|
165
|
+
shift: false,
|
|
166
|
+
});
|
|
167
|
+
await testSetup!.renderOnce();
|
|
168
|
+
});
|
|
169
|
+
await flushFrame();
|
|
170
|
+
|
|
171
|
+
const frame = testSetup!.captureCharFrame();
|
|
172
|
+
expect(frame).toContain("Sensitivity");
|
|
173
|
+
expect(frame).toContain("Win p");
|
|
174
|
+
expect(frame).toContain("Upside");
|
|
175
|
+
expect(frame).not.toContain("Kelly Curve");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("focuses ticker search with t", async () => {
|
|
179
|
+
await renderPane();
|
|
180
|
+
await flushFrame();
|
|
181
|
+
|
|
182
|
+
await act(async () => {
|
|
183
|
+
(testSetup!.renderer.keyInput as any).emit("keypress", {
|
|
184
|
+
name: "t",
|
|
185
|
+
sequence: "t",
|
|
186
|
+
ctrl: false,
|
|
187
|
+
meta: false,
|
|
188
|
+
shift: false,
|
|
189
|
+
});
|
|
190
|
+
await testSetup!.renderOnce();
|
|
191
|
+
});
|
|
192
|
+
await flushFrame();
|
|
193
|
+
|
|
194
|
+
const frame = testSetup!.captureCharFrame();
|
|
195
|
+
expect(frame).toContain("/ AAPL");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { GloomPlugin, PaneTemplateContext, PaneTemplateCreateOptions } from "../../../types/plugin";
|
|
2
|
+
import { DEFAULT_FLOATING_SIZE, KELLY_PANE_ID } from "./constants";
|
|
3
|
+
import { KellySizerPane } from "./pane";
|
|
4
|
+
|
|
5
|
+
function resolveTemplateSymbol(context: PaneTemplateContext, options?: PaneTemplateCreateOptions): string | null {
|
|
6
|
+
return options?.symbol
|
|
7
|
+
?? options?.ticker?.metadata.ticker
|
|
8
|
+
?? options?.arg?.trim().toUpperCase()
|
|
9
|
+
?? context.activeTicker
|
|
10
|
+
?? null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const kellySizerPlugin: GloomPlugin = {
|
|
14
|
+
id: KELLY_PANE_ID,
|
|
15
|
+
name: "Position Sizer",
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
description: "Size positions from Kelly, risk budget, and asymmetric payoff assumptions.",
|
|
18
|
+
toggleable: true,
|
|
19
|
+
panes: [
|
|
20
|
+
{
|
|
21
|
+
id: KELLY_PANE_ID,
|
|
22
|
+
name: "Position Sizer",
|
|
23
|
+
icon: "K",
|
|
24
|
+
component: KellySizerPane,
|
|
25
|
+
defaultPosition: "right",
|
|
26
|
+
defaultMode: "floating",
|
|
27
|
+
defaultFloatingSize: DEFAULT_FLOATING_SIZE,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
paneTemplates: [
|
|
31
|
+
{
|
|
32
|
+
id: "kelly-sizer-pane",
|
|
33
|
+
paneId: KELLY_PANE_ID,
|
|
34
|
+
label: "Position Sizer",
|
|
35
|
+
description: "Open Kelly-based position sizing for a ticker.",
|
|
36
|
+
keywords: ["kelly", "position", "sizing", "risk", "bet", "portfolio"],
|
|
37
|
+
shortcut: { prefix: "KELLY", argPlaceholder: "ticker", argKind: "ticker", argOptional: true },
|
|
38
|
+
canCreate: (context, options) => !!resolveTemplateSymbol(context, options),
|
|
39
|
+
createInstance: (context, options) => {
|
|
40
|
+
const symbol = resolveTemplateSymbol(context, options);
|
|
41
|
+
return symbol ? { params: { symbol }, placement: "floating" } : null;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default kellySizerPlugin;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildBinaryOutcomes,
|
|
4
|
+
buildKellyCurvePoints,
|
|
5
|
+
buildSensitivityGrid,
|
|
6
|
+
calculateKellySizing,
|
|
7
|
+
solveKellyFraction,
|
|
8
|
+
type BinaryKellyAssumptions,
|
|
9
|
+
type PredictionMarketKellyAssumptions,
|
|
10
|
+
type ScenarioKellyAssumptions,
|
|
11
|
+
} from "./model";
|
|
12
|
+
|
|
13
|
+
function expectClose(actual: number, expected: number, precision = 4) {
|
|
14
|
+
expect(actual).toBeGreaterThan(expected - precision);
|
|
15
|
+
expect(actual).toBeLessThan(expected + precision);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("kelly sizing model", () => {
|
|
19
|
+
test("solves classic even-money Kelly sizing", () => {
|
|
20
|
+
const result = solveKellyFraction(buildBinaryOutcomes(0.6, 1, -1));
|
|
21
|
+
expectClose(result.fraction, 0.2);
|
|
22
|
+
expectClose(result.expectedReturn, 0.2);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("clips fractional binary Kelly by max name and max loss caps", () => {
|
|
26
|
+
const draft: BinaryKellyAssumptions = {
|
|
27
|
+
winProbability: 0.57,
|
|
28
|
+
upsideReturn: 0.24,
|
|
29
|
+
downsideReturn: -0.09,
|
|
30
|
+
kellyFraction: 0.25,
|
|
31
|
+
maxNameFraction: 0.08,
|
|
32
|
+
maxLossFraction: 0.01,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const result = calculateKellySizing({
|
|
36
|
+
mode: "binary",
|
|
37
|
+
draft,
|
|
38
|
+
bankroll: 100_000,
|
|
39
|
+
currentValue: 4_000,
|
|
40
|
+
price: 200,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result.fullKellyFraction).toBeGreaterThan(4);
|
|
44
|
+
expectClose(result.clippedFraction, 0.08);
|
|
45
|
+
expect(result.clipReasons).toContain("max name");
|
|
46
|
+
expectClose(result.targetValue, 8_000, 0.01);
|
|
47
|
+
expectClose(result.addTrimValue, 4_000, 0.01);
|
|
48
|
+
expectClose(result.estimatedUnits ?? 0, 20, 0.01);
|
|
49
|
+
expectClose(result.riskFraction, 0.0072, 0.0001);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("returns zero size when there is no positive edge", () => {
|
|
53
|
+
const draft: BinaryKellyAssumptions = {
|
|
54
|
+
winProbability: 0.45,
|
|
55
|
+
upsideReturn: 0.1,
|
|
56
|
+
downsideReturn: -0.1,
|
|
57
|
+
kellyFraction: 0.25,
|
|
58
|
+
maxNameFraction: 0.2,
|
|
59
|
+
maxLossFraction: 0.05,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = calculateKellySizing({
|
|
63
|
+
mode: "binary",
|
|
64
|
+
draft,
|
|
65
|
+
bankroll: 10_000,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result.clippedFraction).toBe(0);
|
|
69
|
+
expect(result.warnings).toContain("No positive Kelly edge.");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("solves scenario trees numerically", () => {
|
|
73
|
+
const draft: ScenarioKellyAssumptions = {
|
|
74
|
+
outcomes: [
|
|
75
|
+
{ id: "loss", label: "Loss", probability: 0.4, returnPct: -1 },
|
|
76
|
+
{ id: "win", label: "Win", probability: 0.6, returnPct: 1 },
|
|
77
|
+
],
|
|
78
|
+
kellyFraction: 1,
|
|
79
|
+
maxNameFraction: 1,
|
|
80
|
+
maxLossFraction: 1,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const result = calculateKellySizing({
|
|
84
|
+
mode: "scenario",
|
|
85
|
+
draft,
|
|
86
|
+
bankroll: 1_000,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expectClose(result.fullKellyFraction, 0.2);
|
|
90
|
+
expectClose(result.clippedFraction, 0.2);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("uses risk budget size before Kelly when in risk budget mode", () => {
|
|
94
|
+
const result = calculateKellySizing({
|
|
95
|
+
mode: "risk-budget",
|
|
96
|
+
draft: {
|
|
97
|
+
riskBudgetFraction: 0.01,
|
|
98
|
+
downsideReturn: -0.1,
|
|
99
|
+
winProbability: 0.6,
|
|
100
|
+
upsideReturn: 0.2,
|
|
101
|
+
kellyFraction: 0.25,
|
|
102
|
+
maxNameFraction: 1,
|
|
103
|
+
maxLossFraction: 1,
|
|
104
|
+
},
|
|
105
|
+
bankroll: 50_000,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expectClose(result.unclippedFraction, 0.1);
|
|
109
|
+
expectClose(result.clippedFraction, 0.1);
|
|
110
|
+
expectClose(result.riskValue, 500);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("sizes prediction market yes contracts from probability and price", () => {
|
|
114
|
+
const draft: PredictionMarketKellyAssumptions = {
|
|
115
|
+
estimatedProbability: 0.57,
|
|
116
|
+
marketPrice: 0.48,
|
|
117
|
+
side: "yes",
|
|
118
|
+
kellyFraction: 1,
|
|
119
|
+
maxNameFraction: 1,
|
|
120
|
+
maxLossFraction: 1,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = calculateKellySizing({
|
|
124
|
+
mode: "prediction-market",
|
|
125
|
+
draft,
|
|
126
|
+
bankroll: 1_000,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expectClose(result.fullKellyFraction, 0.173076, 0.0001);
|
|
130
|
+
expectClose(result.clippedFraction, 0.173076, 0.0001);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("builds sensitivity grid and Kelly curve data", () => {
|
|
134
|
+
const draft: BinaryKellyAssumptions = {
|
|
135
|
+
winProbability: 0.57,
|
|
136
|
+
upsideReturn: 0.24,
|
|
137
|
+
downsideReturn: -0.09,
|
|
138
|
+
kellyFraction: 0.25,
|
|
139
|
+
maxNameFraction: 0.08,
|
|
140
|
+
maxLossFraction: 0.01,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const grid = buildSensitivityGrid("binary", draft);
|
|
144
|
+
expect(grid.rows).toHaveLength(3);
|
|
145
|
+
expect(grid.columns).toHaveLength(3);
|
|
146
|
+
expect(grid.cells[1]?.[1]?.text).toBe("8.00%");
|
|
147
|
+
|
|
148
|
+
const points = buildKellyCurvePoints("binary", draft);
|
|
149
|
+
expect(points.length).toBeGreaterThan(10);
|
|
150
|
+
expect(points[0]?.close).toBe(0);
|
|
151
|
+
});
|
|
152
|
+
});
|