gloomberb 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +1 -0
  2. package/package.json +3 -2
  3. package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
  4. package/patches/react-reconciler@0.33.0.patch +10 -0
  5. package/src/app/runtime/desktop-menu.ts +5 -1
  6. package/src/app/runtime/ticker-refresh.ts +4 -1
  7. package/src/app.tsx +10 -1
  8. package/src/brokers/account-cache.ts +1 -1
  9. package/src/brokers/sync-broker-instance.test.ts +107 -0
  10. package/src/brokers/sync-broker-instance.ts +22 -10
  11. package/src/cli/desktop-pane-shot.ts +2 -1
  12. package/src/cli/entry.ts +8 -1
  13. package/src/cli/native-smoke.ts +5 -0
  14. package/src/cli/pane-functions/screenshot.ts +13 -1
  15. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  16. package/src/components/chart/bar-chart-renderer.ts +226 -27
  17. package/src/components/chart/core/pointer.ts +32 -6
  18. package/src/components/chart/native/bitmap-support.test.ts +27 -0
  19. package/src/components/chart/native/bitmap-support.ts +55 -0
  20. package/src/components/chart/native/kitty/index.test.ts +47 -0
  21. package/src/components/chart/native/surface/visibility.ts +8 -1
  22. package/src/components/chart/price-axis-labels.tsx +3 -1
  23. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  24. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  25. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  26. package/src/components/chart/static/chart/bitmap.ts +16 -9
  27. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  28. package/src/components/chart/static/chart/surface.tsx +167 -24
  29. package/src/components/chart/stock-chart/index.test.ts +25 -0
  30. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  31. package/src/components/command-bar/list/view.tsx +1 -1
  32. package/src/components/command-bar/theme-picker.tsx +1 -1
  33. package/src/components/data-table/view.tsx +0 -12
  34. package/src/components/layout/detached-pane-shell.tsx +6 -3
  35. package/src/components/layout/floating-pane.tsx +6 -5
  36. package/src/components/layout/pane/content.tsx +20 -8
  37. package/src/components/layout/pane/footer/registration.tsx +6 -2
  38. package/src/components/layout/pane/frame.tsx +4 -4
  39. package/src/components/layout/pane/header.tsx +45 -4
  40. package/src/components/layout/pane/index.tsx +6 -4
  41. package/src/components/layout/pane/sizing.test.ts +43 -0
  42. package/src/components/layout/pane/sizing.ts +13 -4
  43. package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
  44. package/src/components/layout/shell/cursor-occlusion.ts +204 -0
  45. package/src/components/layout/shell/drag/runtime.ts +9 -2
  46. package/src/components/layout/shell/index.test.tsx +92 -2
  47. package/src/components/layout/shell/index.tsx +45 -3
  48. package/src/components/layout/shell/layout-state.test.ts +33 -0
  49. package/src/components/layout/shell/layout-state.ts +4 -9
  50. package/src/components/layout/shell/pane/layers.tsx +10 -4
  51. package/src/components/layout/shell/visible-layout.ts +17 -0
  52. package/src/components/layout/status-bar.test.tsx +6 -1
  53. package/src/components/layout/status-bar.tsx +10 -6
  54. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  55. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  56. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  57. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  58. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  59. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  60. package/src/components/pane-settings-dialog/desktop.tsx +1 -1
  61. package/src/components/price-performance.tsx +1 -1
  62. package/src/components/price-sparkline/view.tsx +15 -14
  63. package/src/components/speedometer-gauge/terminal.tsx +18 -15
  64. package/src/components/table-view-shared.tsx +12 -14
  65. package/src/components/ticker/list-table-view.tsx +1 -7
  66. package/src/components/ticker/list-table.test.tsx +0 -4
  67. package/src/components/ui/data-table/opentui/index.tsx +29 -22
  68. package/src/components/ui/data-table/types.ts +10 -4
  69. package/src/components/ui/fields.tsx +10 -3
  70. package/src/components/ui/list-view.tsx +1 -1
  71. package/src/components/ui/page-stack-view.tsx +6 -2
  72. package/src/components/ui/table-layout.test.ts +25 -0
  73. package/src/components/ui/table-layout.ts +22 -6
  74. package/src/components/ui/tabs.tsx +27 -12
  75. package/src/components/ui/ui.test.tsx +87 -9
  76. package/src/core/state/app/layout-reducer.ts +2 -2
  77. package/src/plugins/builtin/account-management/form-components.tsx +2 -2
  78. package/src/plugins/builtin/alerts/pane.tsx +1 -1
  79. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  80. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  81. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  82. package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
  83. package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
  84. package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
  85. package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
  86. package/src/plugins/builtin/chat/sidebar.tsx +1 -1
  87. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  88. package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
  89. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
  90. package/src/plugins/builtin/debug/index.tsx +3 -4
  91. package/src/plugins/builtin/help/components.tsx +1 -1
  92. package/src/plugins/builtin/holders/treemap.tsx +1 -1
  93. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  94. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  95. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  96. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  97. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  98. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  99. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  100. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  101. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  102. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  103. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  104. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  105. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  106. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  107. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  108. package/src/plugins/builtin/options/table.ts +5 -5
  109. package/src/plugins/builtin/options/view.test.tsx +141 -4
  110. package/src/plugins/builtin/options/view.tsx +76 -15
  111. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  112. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  113. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  114. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
  115. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  116. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  117. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  118. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  119. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  120. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  121. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  122. package/src/plugins/catalog-ui.ts +2 -0
  123. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  124. package/src/plugins/ibkr/config.test.ts +31 -2
  125. package/src/plugins/ibkr/config.ts +9 -2
  126. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  127. package/src/plugins/ibkr/flex/index.ts +30 -0
  128. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  129. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  130. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  131. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  132. package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
  133. package/src/plugins/pane-manager/gridlock.ts +13 -5
  134. package/src/plugins/pane-manager/layout-state.ts +25 -0
  135. package/src/plugins/pane-manager.test.ts +33 -0
  136. package/src/plugins/pane-manager.ts +6 -1
  137. package/src/plugins/prediction-markets/chart.tsx +10 -4
  138. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  139. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  140. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  141. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  142. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  143. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  144. package/src/plugins/prediction-markets/pane.test.tsx +43 -0
  145. package/src/plugins/prediction-markets/pane.tsx +50 -22
  146. package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
  147. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  148. package/src/renderers/electrobun/view/data-table/index.tsx +19 -12
  149. package/src/renderers/electrobun/view/data-table/row.tsx +32 -18
  150. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  151. package/src/renderers/electrobun/view/host/box.tsx +6 -1
  152. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  153. package/src/renderers/electrobun/view/host/style.ts +1 -0
  154. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  155. package/src/renderers/electrobun/view/styles.css +18 -0
  156. package/src/renderers/opentui/host.tsx +17 -1
  157. package/src/renderers/opentui/native-smoke.ts +1 -0
  158. package/src/renderers/opentui/ui-host.tsx +57 -25
  159. package/src/sources/provider-router/batches.ts +16 -2
  160. package/src/sources/provider-router/financial-routes.ts +5 -0
  161. package/src/sources/provider-router/financials.ts +11 -23
  162. package/src/sources/provider-router/index.test.ts +189 -0
  163. package/src/sources/provider-router/primary.ts +3 -2
  164. package/src/sources/sec-edgar.test.ts +83 -0
  165. package/src/sources/sec-edgar.ts +231 -0
  166. package/src/sources/yahoo-finance.test.ts +51 -0
  167. package/src/sources/yahoo-finance.ts +49 -1
  168. package/src/state/app/context/index.test.tsx +5 -8
  169. package/src/types/broker.ts +1 -0
  170. package/src/types/config.ts +10 -0
  171. package/src/types/trading.ts +2 -0
  172. package/src/ui/host.tsx +16 -0
  173. package/src/ui/index.tsx +2 -0
  174. package/src/utils/financial-statements.ts +23 -0
  175. package/src/version.ts +1 -1
  176. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -0,0 +1,558 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Box, Text, TextAttributes, type InputRenderable } from "../../../ui";
3
+ import { useShortcut } from "../../../react/input";
4
+ import { colors } from "../../../theme/colors";
5
+ import {
6
+ InputSearchBar,
7
+ Tabs,
8
+ usePaneFooter,
9
+ } from "../../../components";
10
+ import type { PaneProps } from "../../../types/plugin";
11
+ import { useFxRatesMap, useTickerFinancials, useTickerFinancialsMap } from "../../../market-data/hooks";
12
+ import { formatCurrency } from "../../../utils/format";
13
+ import { selectEffectiveExchangeRates } from "../../../utils/exchange-rate-map";
14
+ import {
15
+ useAppDispatch,
16
+ getFocusedCollectionId,
17
+ getFocusedTickerSymbol,
18
+ useAppSelector,
19
+ usePaneInstance,
20
+ usePaneStateValue,
21
+ } from "../../../state/app/context";
22
+ import { selectCommandBarOpen } from "../../../state/selectors-ui";
23
+ import { usePortfolioAccountState } from "../portfolio-list/header";
24
+ import { getSharedRegistry } from "../../registry";
25
+ import { resolveTickerOpenTarget } from "../../../tickers/open-target";
26
+ import { calculatePortfolioSummaryTotals } from "../portfolio-list/metrics";
27
+ import {
28
+ buildTrackedCurrencies,
29
+ getCollectionTickersFromConfig,
30
+ } from "../portfolio-list/pane/data";
31
+ import {
32
+ DEFAULT_KELLY_DRAFTS,
33
+ KELLY_MODES,
34
+ applyKellyCommonAssumptions,
35
+ buildKellyCurvePoints,
36
+ buildSensitivityGrid,
37
+ calculateExpectedLogGrowthAtFraction,
38
+ calculateKellySizing,
39
+ cloneKellyDrafts,
40
+ getKellyCurveMaxFraction,
41
+ type KellySizerDraft,
42
+ type KellySizerModeDrafts,
43
+ type KellySizingMode,
44
+ } from "./model";
45
+ import { KELLY_PANE_ID } from "./constants";
46
+ import {
47
+ InlineFieldView,
48
+ buildKellyCurveXAxisLabels,
49
+ isPlainShortcut,
50
+ truncateText,
51
+ } from "./view";
52
+ import {
53
+ buildCommonFields,
54
+ buildModeFields,
55
+ type InlineField,
56
+ } from "./fields";
57
+ import type { StaticChartXMarker } from "../../../components/chart/static/chart/surface";
58
+ import {
59
+ KellyCurveSection,
60
+ KellyResultMetrics,
61
+ KellySensitivitySection,
62
+ } from "./sections";
63
+ import { getPortfolioPositionValue, resolveActivePortfolioId } from "./portfolio";
64
+ import { useKellyCommonAssumptions } from "./state";
65
+
66
+ export function KellySizerPane({ focused, width, height }: PaneProps) {
67
+ const paneInstance = usePaneInstance();
68
+ const dispatch = useAppDispatch();
69
+ const config = useAppSelector((state) => state.config);
70
+ const activeCollectionId = useAppSelector((state) => getFocusedCollectionId(state));
71
+ const focusedSymbol = useAppSelector((state) => getFocusedTickerSymbol(state));
72
+ const [symbolOverride, setSymbolOverride] = usePaneStateValue<string | null>("symbol", null);
73
+ const requestedSymbol = symbolOverride || paneInstance?.params?.symbol || focusedSymbol;
74
+ const ticker = useAppSelector((state) => (requestedSymbol ? state.tickers.get(requestedSymbol) ?? null : null));
75
+ const cachedFinancials = useAppSelector((state) => (requestedSymbol ? state.financials.get(requestedSymbol) ?? null : null));
76
+ const liveFinancials = useTickerFinancials(requestedSymbol ?? null, ticker);
77
+ const financials = liveFinancials ?? cachedFinancials;
78
+ const tickersBySymbol = useAppSelector((state) => state.tickers);
79
+ const cachedPortfolioFinancials = useAppSelector((state) => state.financials);
80
+ const cachedExchangeRates = useAppSelector((state) => state.exchangeRates);
81
+ const brokerAccounts = useAppSelector((state) => state.brokerAccounts);
82
+ const commandBarOpen = useAppSelector(selectCommandBarOpen);
83
+
84
+ const [mode, setMode] = usePaneStateValue<KellySizingMode>("mode", "binary");
85
+ const [drafts, setDrafts] = usePaneStateValue<KellySizerModeDrafts>("drafts", cloneKellyDrafts());
86
+ const [showSensitivity, setShowSensitivity] = usePaneStateValue<boolean>("showSensitivity", false);
87
+ const [selectedPortfolioId, setSelectedPortfolioId] = usePaneStateValue<string | null>("portfolioId", null);
88
+ const [bankrollOverride, setBankrollOverride] = usePaneStateValue<number | null>("bankrollOverride", null);
89
+ const [currentValueOverride, setCurrentValueOverride] = usePaneStateValue<number | null>("currentValueOverride", null);
90
+ const [selectedFieldIndex, setSelectedFieldIndex] = useState(0);
91
+ const [activeInputId, setActiveInputId] = useState<string | null>(null);
92
+ const tickerInputRef = useRef<InputRenderable | null>(null);
93
+ const [tickerSearchActive, setTickerSearchActive] = useState(false);
94
+ const [tickerSearchQuery, setTickerSearchQuery] = useState(requestedSymbol ?? "");
95
+ const [tickerSearchFocusToken, setTickerSearchFocusToken] = useState(0);
96
+ const [tickerSearchStatus, setTickerSearchStatus] = useState<string | null>(null);
97
+
98
+ useEffect(() => {
99
+ if (!tickerSearchActive) {
100
+ setTickerSearchQuery(requestedSymbol ?? "");
101
+ }
102
+ }, [requestedSymbol, tickerSearchActive]);
103
+
104
+ const activateInput = useCallback((inputId: string | null, fieldIndex?: number) => {
105
+ if (typeof fieldIndex === "number") setSelectedFieldIndex(fieldIndex);
106
+ setActiveInputId(inputId);
107
+ }, []);
108
+
109
+ const focusTickerSearch = useCallback(() => {
110
+ setTickerSearchActive(true);
111
+ setTickerSearchFocusToken((value) => value + 1);
112
+ activateInput(null);
113
+ }, [activateInput]);
114
+
115
+ const resolveTickerQuery = useCallback(async (query: string) => {
116
+ const normalizedQuery = query.trim().toUpperCase();
117
+ setTickerSearchQuery(normalizedQuery);
118
+ if (!normalizedQuery || normalizedQuery === requestedSymbol) {
119
+ setTickerSearchStatus(null);
120
+ return;
121
+ }
122
+
123
+ const registry = getSharedRegistry();
124
+ if (!registry) {
125
+ setTickerSearchStatus("lookup unavailable");
126
+ return;
127
+ }
128
+
129
+ setTickerSearchStatus("checking...");
130
+ const target = await resolveTickerOpenTarget({
131
+ query: normalizedQuery,
132
+ tickers: tickersBySymbol,
133
+ dataProvider: registry.marketData,
134
+ tickerRepository: registry.tickerRepository,
135
+ });
136
+
137
+ if (!target) {
138
+ setTickerSearchStatus("not found");
139
+ return;
140
+ }
141
+
142
+ dispatch({ type: "UPDATE_TICKER", ticker: target.ticker });
143
+ if (target.created) {
144
+ registry.events.emit("ticker:added", { symbol: target.symbol, ticker: target.ticker });
145
+ }
146
+ setSymbolOverride(target.symbol);
147
+ setBankrollOverride(null);
148
+ setCurrentValueOverride(null);
149
+ setTickerSearchQuery(target.symbol);
150
+ setTickerSearchStatus(null);
151
+ }, [
152
+ dispatch,
153
+ requestedSymbol,
154
+ setBankrollOverride,
155
+ setCurrentValueOverride,
156
+ setSymbolOverride,
157
+ tickersBySymbol,
158
+ ]);
159
+
160
+ const activePortfolioId = resolveActivePortfolioId({
161
+ requestedPortfolioId: selectedPortfolioId ?? paneInstance?.params?.portfolioId ?? null,
162
+ activeCollectionId,
163
+ symbol: requestedSymbol ?? null,
164
+ ticker,
165
+ config,
166
+ });
167
+ const activePortfolio = useMemo(
168
+ () => config.portfolios.find((portfolio) => portfolio.id === activePortfolioId) ?? null,
169
+ [activePortfolioId, config.portfolios],
170
+ );
171
+ const portfolioTickers = useMemo(
172
+ () => activePortfolioId ? getCollectionTickersFromConfig(config, tickersBySymbol, activePortfolioId) : [],
173
+ [activePortfolioId, config, tickersBySymbol],
174
+ );
175
+ const livePortfolioFinancials = useTickerFinancialsMap(portfolioTickers);
176
+ const portfolioFinancials = useMemo(() => {
177
+ const merged = new Map(cachedPortfolioFinancials);
178
+ for (const [symbol, value] of livePortfolioFinancials) merged.set(symbol, value);
179
+ if (requestedSymbol && financials) merged.set(requestedSymbol, financials);
180
+ return merged;
181
+ }, [cachedPortfolioFinancials, financials, livePortfolioFinancials, requestedSymbol]);
182
+ const accountState = usePortfolioAccountState(activePortfolio, { brokerAccounts, config });
183
+ const trackedCurrencies = useMemo(
184
+ () => buildTrackedCurrencies(portfolioTickers, portfolioFinancials, accountState, config.baseCurrency),
185
+ [accountState, config.baseCurrency, portfolioFinancials, portfolioTickers],
186
+ );
187
+ const fetchedExchangeRates = useFxRatesMap(trackedCurrencies);
188
+ const exchangeRates = selectEffectiveExchangeRates(fetchedExchangeRates, cachedExchangeRates);
189
+ const portfolioSummary = useMemo(
190
+ () => calculatePortfolioSummaryTotals(
191
+ portfolioTickers,
192
+ portfolioFinancials,
193
+ config.baseCurrency,
194
+ exchangeRates,
195
+ true,
196
+ activePortfolioId,
197
+ ),
198
+ [activePortfolioId, config.baseCurrency, exchangeRates, portfolioFinancials, portfolioTickers],
199
+ );
200
+ const sourceBankroll = accountState?.account.netLiquidation
201
+ ?? portfolioSummary.totalMktValue
202
+ ?? 0;
203
+ const sourceCurrentValue = getPortfolioPositionValue({
204
+ ticker,
205
+ financials,
206
+ portfolioId: activePortfolioId,
207
+ baseCurrency: config.baseCurrency,
208
+ exchangeRates,
209
+ });
210
+ const bankroll = bankrollOverride ?? sourceBankroll;
211
+ const currentValue = currentValueOverride ?? sourceCurrentValue;
212
+ const price = financials?.quote?.price ?? null;
213
+ const rawActiveDraft = drafts[mode] ?? DEFAULT_KELLY_DRAFTS[mode];
214
+ const { commonAssumptions, updateCommon } = useKellyCommonAssumptions(rawActiveDraft);
215
+ const activeDraft = useMemo(
216
+ () => applyKellyCommonAssumptions(rawActiveDraft, commonAssumptions),
217
+ [commonAssumptions, rawActiveDraft],
218
+ );
219
+
220
+ const updateDraft = useCallback((patch: Partial<KellySizerDraft>) => {
221
+ setDrafts((current) => ({
222
+ ...cloneKellyDrafts(current),
223
+ [mode]: {
224
+ ...(current[mode] ?? DEFAULT_KELLY_DRAFTS[mode]),
225
+ ...patch,
226
+ } as KellySizerDraft,
227
+ }));
228
+ }, [mode, setDrafts]);
229
+
230
+ const fields = useMemo(
231
+ () => buildModeFields({ mode, draft: activeDraft, updateDraft }),
232
+ [activeDraft, mode, updateDraft],
233
+ );
234
+ const commonFields = useMemo(
235
+ () => buildCommonFields({ common: commonAssumptions, updateCommon }),
236
+ [commonAssumptions, updateCommon],
237
+ );
238
+ const contextFields = useMemo<InlineField[]>(() => [
239
+ {
240
+ id: "context:bankroll",
241
+ label: "Bankroll",
242
+ value: bankroll,
243
+ suffix: config.baseCurrency,
244
+ onValue: (value) => setBankrollOverride(Math.max(0, value)),
245
+ onClear: () => setBankrollOverride(null),
246
+ },
247
+ {
248
+ id: "context:current",
249
+ label: "Current",
250
+ value: currentValue,
251
+ suffix: config.baseCurrency,
252
+ onValue: (value) => setCurrentValueOverride(Math.max(0, value)),
253
+ onClear: () => setCurrentValueOverride(null),
254
+ },
255
+ ], [bankroll, config.baseCurrency, currentValue, setBankrollOverride, setCurrentValueOverride]);
256
+ const editableFields = useMemo(() => [...commonFields, ...fields], [commonFields, fields]);
257
+ const safeSelectedFieldIndex = Math.min(selectedFieldIndex, Math.max(0, editableFields.length - 1));
258
+ const result = useMemo(
259
+ () => calculateKellySizing({
260
+ mode,
261
+ draft: activeDraft,
262
+ bankroll,
263
+ currentValue,
264
+ price,
265
+ }),
266
+ [activeDraft, bankroll, currentValue, mode, price],
267
+ );
268
+ const sensitivity = useMemo(() => buildSensitivityGrid(mode, activeDraft), [activeDraft, mode]);
269
+ const curveMaxFraction = useMemo(
270
+ () => getKellyCurveMaxFraction(mode, activeDraft, [
271
+ result.currentFraction,
272
+ result.clippedFraction,
273
+ result.fullKellyFraction,
274
+ ]),
275
+ [activeDraft, mode, result.clippedFraction, result.currentFraction, result.fullKellyFraction],
276
+ );
277
+ const curvePoints = useMemo(
278
+ () => buildKellyCurvePoints(mode, activeDraft, curveMaxFraction),
279
+ [activeDraft, curveMaxFraction, mode],
280
+ );
281
+ const curveXAxisLabels = useMemo(() => buildKellyCurveXAxisLabels(curveMaxFraction), [curveMaxFraction]);
282
+ const currentGrowth = useMemo(
283
+ () => calculateExpectedLogGrowthAtFraction(mode, activeDraft, result.currentFraction),
284
+ [activeDraft, mode, result.currentFraction],
285
+ );
286
+ const targetGrowth = useMemo(
287
+ () => calculateExpectedLogGrowthAtFraction(mode, activeDraft, result.clippedFraction),
288
+ [activeDraft, mode, result.clippedFraction],
289
+ );
290
+ const curveMarkers = useMemo<StaticChartXMarker[]>(() => {
291
+ if (!Number.isFinite(curveMaxFraction) || curveMaxFraction <= 0) return [];
292
+ const targetLabel = result.clipReasons[0]?.replace(/^max /, "") ?? "target";
293
+ const markers: StaticChartXMarker[] = [
294
+ {
295
+ id: "current",
296
+ xRatio: result.currentFraction / curveMaxFraction,
297
+ label: "current",
298
+ color: colors.textDim,
299
+ lineChar: "┊",
300
+ },
301
+ {
302
+ id: "target",
303
+ xRatio: result.clippedFraction / curveMaxFraction,
304
+ label: targetLabel === "target" ? "target" : `${targetLabel} cap`,
305
+ color: colors.positive,
306
+ lineChar: "┃",
307
+ },
308
+ {
309
+ id: "full",
310
+ xRatio: result.fullKellyFraction / curveMaxFraction,
311
+ label: "full",
312
+ color: colors.textMuted,
313
+ lineChar: "│",
314
+ },
315
+ ];
316
+ return markers.filter((marker) => Number.isFinite(marker.xRatio) && marker.xRatio >= 0);
317
+ }, [
318
+ curveMaxFraction,
319
+ result.clipReasons,
320
+ result.clippedFraction,
321
+ result.currentFraction,
322
+ result.fullKellyFraction,
323
+ ]);
324
+ const summaryLine = requestedSymbol
325
+ ? `${requestedSymbol}${activePortfolio ? ` · ${activePortfolio.name}` : ""}`
326
+ : "No ticker selected";
327
+
328
+ const toggleSensitivity = useCallback(() => {
329
+ setShowSensitivity((current) => !current);
330
+ }, [setShowSensitivity]);
331
+
332
+ useShortcut((event) => {
333
+ if (!focused) return;
334
+ if (commandBarOpen || event.defaultPrevented || event.propagationStopped) return;
335
+ if (event.ctrl || event.meta || event.super || event.alt || event.targetEditable) return;
336
+
337
+ if (isPlainShortcut(event, "s")) {
338
+ event.preventDefault?.();
339
+ event.stopPropagation?.();
340
+ toggleSensitivity();
341
+ return;
342
+ }
343
+ if (isPlainShortcut(event, "t")) {
344
+ event.preventDefault?.();
345
+ event.stopPropagation?.();
346
+ focusTickerSearch();
347
+ return;
348
+ }
349
+ if (isPlainShortcut(event, "e")) {
350
+ event.preventDefault?.();
351
+ event.stopPropagation?.();
352
+ activateInput(editableFields[safeSelectedFieldIndex]?.id ?? null);
353
+ }
354
+ }, { enabled: focused });
355
+
356
+ usePaneFooter(KELLY_PANE_ID, () => ({
357
+ info: result.warnings.length > 0
358
+ ? [{ id: "warning", parts: [{ text: result.warnings[0]!, tone: "warning" as const }] }]
359
+ : result.clipReasons.length > 0
360
+ ? [{ id: "clip", parts: [{ text: `clip ${result.clipReasons.join(", ")}`, tone: "muted" as const }] }]
361
+ : [],
362
+ hints: [
363
+ { id: "ticker", key: "t", label: "icker", onPress: focusTickerSearch },
364
+ { id: "sensitivity", key: "s", label: showSensitivity ? "ensitivity off" : "ensitivity", onPress: toggleSensitivity },
365
+ ],
366
+ }), [focusTickerSearch, result.clipReasons, result.warnings, showSensitivity, toggleSensitivity]);
367
+
368
+ const portfolioTabs = useMemo(
369
+ () => config.portfolios.map((portfolio) => ({ label: portfolio.name, value: portfolio.id })),
370
+ [config.portfolios],
371
+ );
372
+ const fieldColumns = width >= 92 ? 3 : 2;
373
+ const commonColumns = width >= 78 ? 3 : 2;
374
+ const commonRows = Math.max(1, Math.ceil(commonFields.length / commonColumns));
375
+ const fieldsRows = Math.max(1, Math.ceil(fields.length / fieldColumns));
376
+ const commonFieldWidth = Math.max(22, Math.floor((width - 2) / commonColumns));
377
+ const fieldWidth = Math.max(22, Math.floor((width - 2) / fieldColumns));
378
+ const contextFieldWidth = Math.max(28, Math.floor((width - 2) / 2));
379
+ const metricsRows = 6;
380
+ const curveDecisionRows = 1;
381
+ const chartHeight = showSensitivity ? 0 : Math.max(7, Math.min(10, height - commonRows - fieldsRows - metricsRows - curveDecisionRows - 8));
382
+ const showChart = !showSensitivity && chartHeight >= 6 && curvePoints.length > 0;
383
+ const leftMetricsWidth = Math.max(36, Math.floor((width - 2) * 0.52));
384
+ const rightMetricsWidth = Math.max(28, width - 2 - leftMetricsWidth);
385
+
386
+ if (!requestedSymbol || !ticker) {
387
+ return (
388
+ <Box flexDirection="column" width={width} height={height} paddingX={1} paddingY={1}>
389
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>Position Sizer</Text>
390
+ <Box height={1} />
391
+ <Text fg={colors.textMuted}>Select a ticker or open with KELLY &lt;ticker&gt;.</Text>
392
+ </Box>
393
+ );
394
+ }
395
+
396
+ return (
397
+ <Box
398
+ flexDirection="column"
399
+ width={width}
400
+ height={height}
401
+ >
402
+ <Box height={1} paddingX={1} flexDirection="row">
403
+ <Box flexGrow={1} overflow="hidden">
404
+ {tickerSearchActive ? (
405
+ <InputSearchBar
406
+ value={tickerSearchQuery}
407
+ focused={focused}
408
+ active={tickerSearchActive}
409
+ width={Math.min(36, Math.max(12, width - 18))}
410
+ focusToken={tickerSearchFocusToken}
411
+ inputRef={tickerInputRef}
412
+ placeholder="ticker"
413
+ debounceMs={500}
414
+ normalizeValue={(value) => value.trim().toUpperCase()}
415
+ onFocus={() => setTickerSearchActive(true)}
416
+ onBlur={() => setTickerSearchActive(false)}
417
+ onQueryChange={(query) => {
418
+ void resolveTickerQuery(query);
419
+ }}
420
+ />
421
+ ) : (
422
+ <Box height={1} flexDirection="row" onMouseDown={focusTickerSearch}>
423
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD}>
424
+ {truncateText(summaryLine, Math.max(8, width - 2))}
425
+ </Text>
426
+ {tickerSearchStatus ? (
427
+ <Text fg={colors.textMuted}>
428
+ {truncateText(` ${tickerSearchStatus}`, Math.max(0, width - summaryLine.length - 18))}
429
+ </Text>
430
+ ) : null}
431
+ </Box>
432
+ )}
433
+ </Box>
434
+ {price != null && (
435
+ <Text fg={colors.textDim}>
436
+ {formatCurrency(price, financials?.quote?.currency ?? ticker.metadata.currency ?? config.baseCurrency)}
437
+ </Text>
438
+ )}
439
+ </Box>
440
+
441
+ {portfolioTabs.length > 1 && (
442
+ <Box height={1} flexDirection="row">
443
+ <Box flexShrink={1} overflow="hidden">
444
+ <Tabs
445
+ tabs={portfolioTabs}
446
+ activeValue={activePortfolioId}
447
+ onSelect={(portfolioId) => {
448
+ setSelectedPortfolioId(portfolioId);
449
+ setBankrollOverride(null);
450
+ setCurrentValueOverride(null);
451
+ }}
452
+ compact
453
+ focused={focused && !activeInputId}
454
+ keyboardNavigation={false}
455
+ />
456
+ </Box>
457
+ </Box>
458
+ )}
459
+
460
+ <Box height={1} flexDirection="row" paddingX={1}>
461
+ {contextFields.map((field) => (
462
+ <InlineFieldView
463
+ key={field.id}
464
+ field={field}
465
+ active={activeInputId === field.id}
466
+ focused={focused}
467
+ width={contextFieldWidth}
468
+ onFocus={() => activateInput(field.id)}
469
+ />
470
+ ))}
471
+ {bankrollOverride != null || currentValueOverride != null ? (
472
+ <Text fg={colors.textMuted}> override</Text>
473
+ ) : null}
474
+ </Box>
475
+
476
+ <Box height={1} paddingX={1}>
477
+ <Tabs
478
+ tabs={KELLY_MODES.map((entry) => ({ label: entry.label, value: entry.id }))}
479
+ activeValue={mode}
480
+ onSelect={(nextMode) => {
481
+ setMode(nextMode as KellySizingMode);
482
+ setSelectedFieldIndex(0);
483
+ activateInput(null);
484
+ }}
485
+ compact
486
+ focused={focused && !activeInputId}
487
+ />
488
+ </Box>
489
+
490
+ <Box flexDirection="column" paddingX={1} height={commonRows}>
491
+ {Array.from({ length: commonRows }, (_, rowIndex) => (
492
+ <Box key={rowIndex} height={1} flexDirection="row">
493
+ {commonFields.slice(rowIndex * commonColumns, rowIndex * commonColumns + commonColumns).map((field, offset) => {
494
+ const index = rowIndex * commonColumns + offset;
495
+ return (
496
+ <InlineFieldView
497
+ key={field.id}
498
+ field={field}
499
+ active={activeInputId === field.id}
500
+ focused={focused}
501
+ width={commonFieldWidth}
502
+ onFocus={() => activateInput(field.id, index)}
503
+ />
504
+ );
505
+ })}
506
+ </Box>
507
+ ))}
508
+ </Box>
509
+
510
+ <Box flexDirection="column" paddingX={1} height={fieldsRows}>
511
+ {Array.from({ length: fieldsRows }, (_, rowIndex) => (
512
+ <Box key={rowIndex} height={1} flexDirection="row">
513
+ {fields.slice(rowIndex * fieldColumns, rowIndex * fieldColumns + fieldColumns).map((field, offset) => {
514
+ const index = rowIndex * fieldColumns + offset;
515
+ return (
516
+ <InlineFieldView
517
+ key={field.id}
518
+ field={field}
519
+ active={activeInputId === field.id}
520
+ focused={focused}
521
+ width={fieldWidth}
522
+ onFocus={() => activateInput(field.id, commonFields.length + index)}
523
+ />
524
+ );
525
+ })}
526
+ </Box>
527
+ ))}
528
+ </Box>
529
+
530
+ <KellyResultMetrics
531
+ result={result}
532
+ activeDraft={activeDraft}
533
+ baseCurrency={config.baseCurrency}
534
+ leftWidth={leftMetricsWidth}
535
+ rightWidth={rightMetricsWidth}
536
+ />
537
+
538
+ {showChart && (
539
+ <KellyCurveSection
540
+ width={width}
541
+ height={chartHeight}
542
+ points={curvePoints}
543
+ xAxisLabels={curveXAxisLabels}
544
+ curveMaxFraction={curveMaxFraction}
545
+ markers={curveMarkers}
546
+ result={result}
547
+ currentGrowth={currentGrowth}
548
+ targetGrowth={targetGrowth}
549
+ baseCurrency={config.baseCurrency}
550
+ />
551
+ )}
552
+
553
+ {showSensitivity && (
554
+ <KellySensitivitySection width={width} sensitivity={sensitivity} />
555
+ )}
556
+ </Box>
557
+ );
558
+ }
@@ -0,0 +1,71 @@
1
+ import type { AppConfig } from "../../../types/config";
2
+ import type { TickerFinancials } from "../../../types/financials";
3
+ import type { TickerRecord } from "../../../types/ticker";
4
+ import { getActiveQuoteDisplay } from "../../../market-data/market/status";
5
+ import { convertCurrency } from "../../../utils/format";
6
+ import { getCollectionTypeFromConfig } from "../portfolio-list/pane/data";
7
+ import { getPortfolioPositionMetrics, resolveBrokerFallbackMarketValue } from "../portfolio-list/position-metrics";
8
+
9
+ export function getPortfolioPositionValue({
10
+ ticker,
11
+ financials,
12
+ portfolioId,
13
+ baseCurrency,
14
+ exchangeRates,
15
+ }: {
16
+ ticker: TickerRecord | null;
17
+ financials: TickerFinancials | null;
18
+ portfolioId: string | null;
19
+ baseCurrency: string;
20
+ exchangeRates: Map<string, number>;
21
+ }): number {
22
+ if (!ticker) return 0;
23
+ const scopedTicker: TickerRecord = portfolioId
24
+ ? {
25
+ ...ticker,
26
+ metadata: {
27
+ ...ticker.metadata,
28
+ positions: ticker.metadata.positions.filter((position) => position.portfolio === portfolioId),
29
+ },
30
+ }
31
+ : ticker;
32
+ const quote = financials?.quote;
33
+ const activeQuote = getActiveQuoteDisplay(quote);
34
+ const quoteCurrency = quote?.currency || ticker.metadata.currency || baseCurrency;
35
+ const metrics = getPortfolioPositionMetrics(scopedTicker, undefined, quoteCurrency);
36
+
37
+ if (activeQuote && metrics.totalPriceUnits !== 0) {
38
+ return convertCurrency(Math.abs(metrics.totalPriceUnits) * activeQuote.price, quoteCurrency, baseCurrency, exchangeRates);
39
+ }
40
+
41
+ const brokerFallback = resolveBrokerFallbackMarketValue(metrics);
42
+ const positionCurrency = metrics.positionCurrency || quoteCurrency;
43
+ return convertCurrency(Math.abs(brokerFallback ?? metrics.totalCost), positionCurrency, baseCurrency, exchangeRates);
44
+ }
45
+
46
+ export function resolveActivePortfolioId({
47
+ requestedPortfolioId,
48
+ activeCollectionId,
49
+ symbol,
50
+ ticker,
51
+ config,
52
+ }: {
53
+ requestedPortfolioId?: string | null;
54
+ activeCollectionId: string | null;
55
+ symbol: string | null;
56
+ ticker: TickerRecord | null;
57
+ config: AppConfig;
58
+ }): string | null {
59
+ if (requestedPortfolioId && config.portfolios.some((portfolio) => portfolio.id === requestedPortfolioId)) {
60
+ return requestedPortfolioId;
61
+ }
62
+ if (activeCollectionId && getCollectionTypeFromConfig(config, activeCollectionId) === "portfolio") {
63
+ return activeCollectionId;
64
+ }
65
+ if (ticker) {
66
+ const positionPortfolio = ticker.metadata.positions.find((position) => position.portfolio)?.portfolio;
67
+ if (positionPortfolio) return positionPortfolio;
68
+ if (ticker.metadata.portfolios[0]) return ticker.metadata.portfolios[0];
69
+ }
70
+ return symbol ? config.portfolios[0]?.id ?? null : null;
71
+ }