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.
- package/README.md +1 -0
- package/package.json +3 -2
- package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
- package/patches/react-reconciler@0.33.0.patch +10 -0
- 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/entry.ts +8 -1
- package/src/cli/native-smoke.ts +5 -0
- 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/native/bitmap-support.test.ts +27 -0
- package/src/components/chart/native/bitmap-support.ts +55 -0
- package/src/components/chart/native/kitty/index.test.ts +47 -0
- package/src/components/chart/native/surface/visibility.ts +8 -1
- 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/bitmap.ts +16 -9
- 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/command-bar/list/view.tsx +1 -1
- package/src/components/command-bar/theme-picker.tsx +1 -1
- package/src/components/data-table/view.tsx +0 -12
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +6 -5
- 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 +45 -4
- package/src/components/layout/pane/index.tsx +6 -4
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
- package/src/components/layout/shell/cursor-occlusion.ts +204 -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 +45 -3
- 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 +10 -4
- 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 +10 -6
- 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/pane-settings-dialog/desktop.tsx +1 -1
- package/src/components/price-performance.tsx +1 -1
- package/src/components/price-sparkline/view.tsx +15 -14
- package/src/components/speedometer-gauge/terminal.tsx +18 -15
- package/src/components/table-view-shared.tsx +12 -14
- package/src/components/ticker/list-table-view.tsx +1 -7
- package/src/components/ticker/list-table.test.tsx +0 -4
- package/src/components/ui/data-table/opentui/index.tsx +29 -22
- package/src/components/ui/data-table/types.ts +10 -4
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/list-view.tsx +1 -1
- 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 +27 -12
- package/src/components/ui/ui.test.tsx +87 -9
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/plugins/builtin/account-management/form-components.tsx +2 -2
- package/src/plugins/builtin/alerts/pane.tsx +1 -1
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
- package/src/plugins/builtin/chat/sidebar.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
- package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
- package/src/plugins/builtin/debug/index.tsx +3 -4
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/holders/treemap.tsx +1 -1
- 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/table.ts +5 -5
- 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/ibkr/trade/ticket-panel.tsx +1 -1
- 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 -12
- package/src/renderers/electrobun/view/data-table/row.tsx +32 -18
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/box.tsx +6 -1
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/style.ts +1 -0
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/styles.css +18 -0
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/native-smoke.ts +1 -0
- 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,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
|
+
});
|