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
|
@@ -7,7 +7,7 @@ import { formatCompact, formatNumber, formatPercentRaw } from "../../../utils/fo
|
|
|
7
7
|
import { formatRelativeAge } from "../../../utils/relative-time";
|
|
8
8
|
import { instrumentFromTicker, type ChartRequest } from "../../../market-data/request-types";
|
|
9
9
|
import { buildChartKey } from "../../../market-data/selectors";
|
|
10
|
-
import { resolvePortfolioAccountMetrics } from "../portfolio-list/account-metrics";
|
|
10
|
+
import { resolvePortfolioAccountMetrics, resolvePortfolioMarketValue } from "../portfolio-list/account-metrics";
|
|
11
11
|
import type { ColumnContext, PortfolioSummaryTotals } from "../portfolio-list/metrics";
|
|
12
12
|
import type { ResolvedPortfolioAccountState } from "../portfolio-list/summary";
|
|
13
13
|
import { performancePointValue } from "./broker-performance";
|
|
@@ -38,6 +38,31 @@ export type ChartEntryLookup = Map<string, {
|
|
|
38
38
|
lastGoodData?: PricePoint[] | null;
|
|
39
39
|
} | undefined>;
|
|
40
40
|
|
|
41
|
+
function formatIsoDateMonthDay(value: string): string {
|
|
42
|
+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
43
|
+
if (!match) return value;
|
|
44
|
+
const [, year, month, day] = match;
|
|
45
|
+
return new Date(Number(year), Number(month) - 1, Number(day)).toLocaleDateString("en-US", {
|
|
46
|
+
month: "short",
|
|
47
|
+
day: "numeric",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatAccountFreshness(account: ResolvedPortfolioAccountState["account"] | undefined): string | null {
|
|
52
|
+
if (!account) return null;
|
|
53
|
+
if (account.asOfDate) return formatIsoDateMonthDay(account.asOfDate);
|
|
54
|
+
return account.updatedAt ? formatRelativeAge(account.updatedAt) : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function finiteNumber(value: unknown): value is number {
|
|
58
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatMarginLeverage(netLiquidation: number | undefined, totalMarketValue: number): string | null {
|
|
62
|
+
if (!finiteNumber(netLiquidation) || !finiteNumber(totalMarketValue) || totalMarketValue <= 0) return null;
|
|
63
|
+
return `${(netLiquidation / totalMarketValue).toFixed(1)}x`;
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
export function buildPortfolioChartTargets(portfolioTickers: TickerRecord[]): PortfolioChartTarget[] {
|
|
42
67
|
return portfolioTickers.flatMap((ticker) => {
|
|
43
68
|
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
|
|
@@ -96,7 +121,6 @@ export function buildBenchmarkReturnSeries(
|
|
|
96
121
|
|
|
97
122
|
export function buildAnalyticsSummaryRows({
|
|
98
123
|
accountState,
|
|
99
|
-
activePortfolio,
|
|
100
124
|
brokerPerformance,
|
|
101
125
|
portfolioStats,
|
|
102
126
|
}: {
|
|
@@ -108,7 +132,8 @@ export function buildAnalyticsSummaryRows({
|
|
|
108
132
|
const rows: AnalyticsMetricRow[] = [];
|
|
109
133
|
const account = accountState?.account;
|
|
110
134
|
const accountMetrics = resolvePortfolioAccountMetrics(portfolioStats, account);
|
|
111
|
-
const
|
|
135
|
+
const accountFreshness = formatAccountFreshness(account);
|
|
136
|
+
const totalMarketValue = resolvePortfolioMarketValue(portfolioStats, account);
|
|
112
137
|
|
|
113
138
|
if (account?.netLiquidation != null) {
|
|
114
139
|
rows.push({
|
|
@@ -122,10 +147,20 @@ export function buildAnalyticsSummaryRows({
|
|
|
122
147
|
rows.push({
|
|
123
148
|
id: "total-value",
|
|
124
149
|
label: "Val",
|
|
125
|
-
value: formatCompact(
|
|
150
|
+
value: formatCompact(totalMarketValue),
|
|
126
151
|
color: colors.text,
|
|
127
152
|
});
|
|
128
153
|
|
|
154
|
+
const marginLeverage = formatMarginLeverage(account?.netLiquidation, totalMarketValue);
|
|
155
|
+
if (marginLeverage) {
|
|
156
|
+
rows.push({
|
|
157
|
+
id: "margin-leverage",
|
|
158
|
+
label: "Margin Lev",
|
|
159
|
+
value: marginLeverage,
|
|
160
|
+
color: colors.text,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
129
164
|
if (account?.totalCashValue != null) {
|
|
130
165
|
rows.push({
|
|
131
166
|
id: "cash",
|
|
@@ -202,11 +237,11 @@ export function buildAnalyticsSummaryRows({
|
|
|
202
237
|
});
|
|
203
238
|
}
|
|
204
239
|
if (accountState) {
|
|
205
|
-
if (
|
|
240
|
+
if (accountFreshness) {
|
|
206
241
|
rows.push({
|
|
207
|
-
id: "
|
|
208
|
-
label: "
|
|
209
|
-
value:
|
|
242
|
+
id: "account-freshness",
|
|
243
|
+
label: "As Of",
|
|
244
|
+
value: accountFreshness,
|
|
210
245
|
color: colors.textDim,
|
|
211
246
|
});
|
|
212
247
|
}
|
|
@@ -26,7 +26,7 @@ interface BuildoutCellContext {
|
|
|
26
26
|
export function renderBuildoutCell(
|
|
27
27
|
row: BuildoutRow,
|
|
28
28
|
column: BuildoutColumn,
|
|
29
|
-
rowState: { selected: boolean
|
|
29
|
+
rowState: { selected: boolean },
|
|
30
30
|
context: BuildoutCellContext,
|
|
31
31
|
): DataTableCell {
|
|
32
32
|
const selectedColor = rowState.selected ? colors.selectedText : undefined;
|
|
@@ -314,7 +314,7 @@ export function BuildoutPane({ focused, width, height }: PaneProps) {
|
|
|
314
314
|
row: BuildoutRow,
|
|
315
315
|
column: BuildoutColumn,
|
|
316
316
|
_index: number,
|
|
317
|
-
rowState: { selected: boolean
|
|
317
|
+
rowState: { selected: boolean },
|
|
318
318
|
) => renderBuildoutCell(row, column, rowState, {
|
|
319
319
|
favoriteBusyKey,
|
|
320
320
|
toggleFavorite,
|
|
@@ -97,7 +97,7 @@ export const DesktopChatMessage = memo(function DesktopChatMessage({
|
|
|
97
97
|
<Text
|
|
98
98
|
fg={state.authorColor}
|
|
99
99
|
attributes={state.authorAttributes}
|
|
100
|
-
|
|
100
|
+
onMouseOver={() => onUserHover(msg.user)}
|
|
101
101
|
onMouseOut={onUserHoverEnd}
|
|
102
102
|
style={{ cursor: "default" }}
|
|
103
103
|
>
|
|
@@ -44,7 +44,7 @@ export function TerminalChatMessage({
|
|
|
44
44
|
const messageRowProps = {
|
|
45
45
|
width: contentWidth,
|
|
46
46
|
backgroundColor: state.bgColor,
|
|
47
|
-
|
|
47
|
+
onMouseOver: setHovered,
|
|
48
48
|
onMouseOut: clearHovered,
|
|
49
49
|
};
|
|
50
50
|
|
|
@@ -80,7 +80,7 @@ export function TerminalChatMessage({
|
|
|
80
80
|
<Text
|
|
81
81
|
fg={state.authorColor}
|
|
82
82
|
attributes={state.authorAttributes}
|
|
83
|
-
|
|
83
|
+
onMouseOver={() => onUserHover(msg.user)}
|
|
84
84
|
onMouseOut={onUserHoverEnd}
|
|
85
85
|
>
|
|
86
86
|
{msg.user.username ?? "anon"}
|
|
@@ -239,7 +239,7 @@ export function ChannelSidebar({
|
|
|
239
239
|
flexDirection="row"
|
|
240
240
|
backgroundColor={bg}
|
|
241
241
|
onMouseDown={selectChannel}
|
|
242
|
-
|
|
242
|
+
onMouseOver={() => setHoveredChannelId((current) => (current === channel.id ? current : channel.id))}
|
|
243
243
|
onMouseOut={() => setHoveredChannelId((current) => (current === channel.id ? null : current))}
|
|
244
244
|
style={{ cursor: "pointer" }}
|
|
245
245
|
>
|
|
@@ -98,7 +98,7 @@ export function ChatStatusWidget({ controller = chatController }: ChatStatusWidg
|
|
|
98
98
|
<Box
|
|
99
99
|
flexDirection="row"
|
|
100
100
|
backgroundColor={hovered ? hoverBg() : undefined}
|
|
101
|
-
|
|
101
|
+
onMouseOver={() => setHovered((current) => (current ? current : true))}
|
|
102
102
|
onMouseOut={() => setHovered((current) => (current ? false : current))}
|
|
103
103
|
onMouseDown={openChat}
|
|
104
104
|
>
|
|
@@ -22,7 +22,7 @@ export function InlineAuthActions({ showSignup = true }: { showSignup?: boolean
|
|
|
22
22
|
<Box flexDirection="row">
|
|
23
23
|
<Box
|
|
24
24
|
backgroundColor={hoveredAction === "login" ? hoverBg() : undefined}
|
|
25
|
-
|
|
25
|
+
onMouseOver={() => setHoveredAction((current) => (current === "login" ? current : "login"))}
|
|
26
26
|
onMouseOut={() => setHoveredAction((current) => (current === "login" ? null : current))}
|
|
27
27
|
onMouseDown={(event: any) => openAuthCommand(openCommandBar, "Log In", event)}
|
|
28
28
|
>
|
|
@@ -33,7 +33,7 @@ export function InlineAuthActions({ showSignup = true }: { showSignup?: boolean
|
|
|
33
33
|
<Text fg={colors.textDim}>/</Text>
|
|
34
34
|
<Box
|
|
35
35
|
backgroundColor={hoveredAction === "signup" ? hoverBg() : undefined}
|
|
36
|
-
|
|
36
|
+
onMouseOver={() => setHoveredAction((current) => (current === "signup" ? current : "signup"))}
|
|
37
37
|
onMouseOut={() => setHoveredAction((current) => (current === "signup" ? null : current))}
|
|
38
38
|
onMouseDown={(event: any) => openAuthCommand(openCommandBar, "Sign Up", event)}
|
|
39
39
|
>
|
|
@@ -41,7 +41,7 @@ export function SymbolLabelCell({
|
|
|
41
41
|
overflow="hidden"
|
|
42
42
|
backgroundColor={hovered ? hoverBg() : undefined}
|
|
43
43
|
style={{ cursor: "pointer" }}
|
|
44
|
-
|
|
44
|
+
onMouseOver={() => onHover(symbol)}
|
|
45
45
|
onMouseOut={() => onLeave(symbol)}
|
|
46
46
|
onMouseDown={(event: any) => handleSymbolMouseDown(event, openSymbol)}
|
|
47
47
|
>
|
|
@@ -64,7 +64,6 @@ function DebugPane({ focused, width, height }: PaneProps) {
|
|
|
64
64
|
const [filterSource, setFilterSource] = useState<string | null>(null);
|
|
65
65
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
66
66
|
const [selectedIdx, setSelectedIdx] = useState(-1);
|
|
67
|
-
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
68
67
|
const [showDetail, setShowDetail] = useState(false);
|
|
69
68
|
const sourcesRef = useRef<string[]>(debugLog.getSources());
|
|
70
69
|
|
|
@@ -261,8 +260,8 @@ function DebugPane({ focused, width, height }: PaneProps) {
|
|
|
261
260
|
{visibleEntries.map((entry, i) => {
|
|
262
261
|
const globalIdx = viewStart + i;
|
|
263
262
|
const isSelected = globalIdx === selectedIdx;
|
|
264
|
-
const
|
|
265
|
-
const
|
|
263
|
+
const bgColor = isSelected ? colors.selected : undefined;
|
|
264
|
+
const rowHoverBg = isSelected ? undefined : hoverBg();
|
|
266
265
|
const ts = formatTimestamp(entry.timestamp);
|
|
267
266
|
const lvl = LEVEL_LABELS[entry.level];
|
|
268
267
|
const sourceTag = entry.source;
|
|
@@ -278,7 +277,7 @@ function DebugPane({ focused, width, height }: PaneProps) {
|
|
|
278
277
|
width={contentWidth}
|
|
279
278
|
flexDirection="row"
|
|
280
279
|
backgroundColor={bgColor}
|
|
281
|
-
|
|
280
|
+
hoverBackgroundColor={rowHoverBg}
|
|
282
281
|
onMouseDown={() => { setSelectedIdx(globalIdx); setAutoScroll(false); }}
|
|
283
282
|
>
|
|
284
283
|
<Text fg={colors.textMuted}> {ts} </Text>
|
|
@@ -63,7 +63,7 @@ export function ActionButton({
|
|
|
63
63
|
return (
|
|
64
64
|
<Box
|
|
65
65
|
backgroundColor={hovered ? hoverBg() : colors.panel}
|
|
66
|
-
|
|
66
|
+
onMouseOver={() => onHover(id)}
|
|
67
67
|
onMouseOut={() => onHover(null)}
|
|
68
68
|
onMouseDown={(event: any) => {
|
|
69
69
|
event.stopPropagation?.();
|
|
@@ -165,7 +165,7 @@ function DesktopTile({ tile, chartWidth, chartHeight, selected, hovered, currenc
|
|
|
165
165
|
onSelect();
|
|
166
166
|
}}
|
|
167
167
|
onDoubleClick={onActivate}
|
|
168
|
-
|
|
168
|
+
onMouseOver={() => onHover(true)}
|
|
169
169
|
onMouseOut={() => onHover(false)}
|
|
170
170
|
>
|
|
171
171
|
{canShowLabel && (
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type KellyCommonAssumptions,
|
|
3
|
+
type KellySizerDraft,
|
|
4
|
+
type KellySizerModeDrafts,
|
|
5
|
+
type KellySizingMode,
|
|
6
|
+
type ScenarioKellyAssumptions,
|
|
7
|
+
} from "./model";
|
|
8
|
+
|
|
9
|
+
export interface InlineField {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
value?: number;
|
|
13
|
+
valueText?: string;
|
|
14
|
+
percent?: boolean;
|
|
15
|
+
suffix?: string;
|
|
16
|
+
allowNegative?: boolean;
|
|
17
|
+
onValue?: (value: number) => void;
|
|
18
|
+
onClear?: () => void;
|
|
19
|
+
onPress?: () => void;
|
|
20
|
+
tone?: "neutral" | "positive" | "negative";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildModeFields({
|
|
24
|
+
mode,
|
|
25
|
+
draft,
|
|
26
|
+
updateDraft,
|
|
27
|
+
}: {
|
|
28
|
+
mode: KellySizingMode;
|
|
29
|
+
draft: KellySizerDraft;
|
|
30
|
+
updateDraft: (patch: Partial<KellySizerDraft>) => void;
|
|
31
|
+
}): InlineField[] {
|
|
32
|
+
if (mode === "scenario") {
|
|
33
|
+
const scenario = draft as ScenarioKellyAssumptions;
|
|
34
|
+
const updateOutcome = (index: number, patch: Partial<ScenarioKellyAssumptions["outcomes"][number]>) => {
|
|
35
|
+
updateDraft({
|
|
36
|
+
outcomes: scenario.outcomes.map((outcome, outcomeIndex) => (
|
|
37
|
+
outcomeIndex === index ? { ...outcome, ...patch } : outcome
|
|
38
|
+
)),
|
|
39
|
+
} as Partial<KellySizerDraft>);
|
|
40
|
+
};
|
|
41
|
+
return scenario.outcomes.flatMap((outcome, index): InlineField[] => [
|
|
42
|
+
{
|
|
43
|
+
id: `${outcome.id}:p`,
|
|
44
|
+
label: `${outcome.label} p`,
|
|
45
|
+
value: outcome.probability,
|
|
46
|
+
percent: true,
|
|
47
|
+
onValue: (value) => updateOutcome(index, { probability: value }),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: `${outcome.id}:ret`,
|
|
51
|
+
label: `${outcome.label} ret`,
|
|
52
|
+
value: outcome.returnPct,
|
|
53
|
+
percent: true,
|
|
54
|
+
allowNegative: true,
|
|
55
|
+
tone: outcome.returnPct >= 0 ? "positive" : "negative",
|
|
56
|
+
onValue: (value) => updateOutcome(index, { returnPct: value }),
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (mode === "risk-budget") {
|
|
62
|
+
const risk = draft as KellySizerModeDrafts["risk-budget"];
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
id: "riskBudget",
|
|
66
|
+
label: "Risk budget",
|
|
67
|
+
value: risk.riskBudgetFraction,
|
|
68
|
+
percent: true,
|
|
69
|
+
onValue: (value) => updateDraft({ riskBudgetFraction: value } as Partial<KellySizerDraft>),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "downside",
|
|
73
|
+
label: "Downside",
|
|
74
|
+
value: risk.downsideReturn,
|
|
75
|
+
percent: true,
|
|
76
|
+
allowNegative: true,
|
|
77
|
+
tone: "negative",
|
|
78
|
+
onValue: (value) => updateDraft({ downsideReturn: value } as Partial<KellySizerDraft>),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "winProbability",
|
|
82
|
+
label: "Win p",
|
|
83
|
+
value: risk.winProbability,
|
|
84
|
+
percent: true,
|
|
85
|
+
onValue: (value) => updateDraft({ winProbability: value } as Partial<KellySizerDraft>),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "upside",
|
|
89
|
+
label: "Upside",
|
|
90
|
+
value: risk.upsideReturn,
|
|
91
|
+
percent: true,
|
|
92
|
+
tone: "positive",
|
|
93
|
+
onValue: (value) => updateDraft({ upsideReturn: value } as Partial<KellySizerDraft>),
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (mode === "prediction-market") {
|
|
99
|
+
const market = draft as KellySizerModeDrafts["prediction-market"];
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
id: "side",
|
|
103
|
+
label: "Side",
|
|
104
|
+
valueText: market.side.toUpperCase(),
|
|
105
|
+
tone: "positive",
|
|
106
|
+
onPress: () => updateDraft({ side: market.side === "yes" ? "no" : "yes" } as Partial<KellySizerDraft>),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "estimatedProbability",
|
|
110
|
+
label: "Est p",
|
|
111
|
+
value: market.estimatedProbability,
|
|
112
|
+
percent: true,
|
|
113
|
+
onValue: (value) => updateDraft({ estimatedProbability: value } as Partial<KellySizerDraft>),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "marketPrice",
|
|
117
|
+
label: "Price",
|
|
118
|
+
value: market.marketPrice,
|
|
119
|
+
percent: true,
|
|
120
|
+
onValue: (value) => updateDraft({ marketPrice: value } as Partial<KellySizerDraft>),
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (mode === "asymmetric") {
|
|
126
|
+
const asymmetric = draft as KellySizerModeDrafts["asymmetric"];
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
id: "winProbability",
|
|
130
|
+
label: "Win p",
|
|
131
|
+
value: asymmetric.winProbability,
|
|
132
|
+
percent: true,
|
|
133
|
+
onValue: (value) => updateDraft({ winProbability: value } as Partial<KellySizerDraft>),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "targetReturn",
|
|
137
|
+
label: "Target",
|
|
138
|
+
value: asymmetric.targetReturn,
|
|
139
|
+
percent: true,
|
|
140
|
+
tone: "positive",
|
|
141
|
+
onValue: (value) => updateDraft({ targetReturn: value } as Partial<KellySizerDraft>),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "maxLossReturn",
|
|
145
|
+
label: "Max loss",
|
|
146
|
+
value: asymmetric.maxLossReturn,
|
|
147
|
+
percent: true,
|
|
148
|
+
allowNegative: true,
|
|
149
|
+
tone: "negative",
|
|
150
|
+
onValue: (value) => updateDraft({ maxLossReturn: value } as Partial<KellySizerDraft>),
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const binary = draft as KellySizerModeDrafts["binary"];
|
|
156
|
+
return [
|
|
157
|
+
{
|
|
158
|
+
id: "winProbability",
|
|
159
|
+
label: "Win p",
|
|
160
|
+
value: binary.winProbability,
|
|
161
|
+
percent: true,
|
|
162
|
+
onValue: (value) => updateDraft({ winProbability: value } as Partial<KellySizerDraft>),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "upside",
|
|
166
|
+
label: "Upside",
|
|
167
|
+
value: binary.upsideReturn,
|
|
168
|
+
percent: true,
|
|
169
|
+
tone: "positive",
|
|
170
|
+
onValue: (value) => updateDraft({ upsideReturn: value } as Partial<KellySizerDraft>),
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: "downside",
|
|
174
|
+
label: "Downside",
|
|
175
|
+
value: binary.downsideReturn,
|
|
176
|
+
percent: true,
|
|
177
|
+
allowNegative: true,
|
|
178
|
+
tone: "negative",
|
|
179
|
+
onValue: (value) => updateDraft({ downsideReturn: value } as Partial<KellySizerDraft>),
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function buildCommonFields({
|
|
185
|
+
common,
|
|
186
|
+
updateCommon,
|
|
187
|
+
}: {
|
|
188
|
+
common: KellyCommonAssumptions;
|
|
189
|
+
updateCommon: (patch: Partial<KellyCommonAssumptions>) => void;
|
|
190
|
+
}): InlineField[] {
|
|
191
|
+
return [
|
|
192
|
+
{
|
|
193
|
+
id: "common:kellyFraction",
|
|
194
|
+
label: "Kelly",
|
|
195
|
+
value: common.kellyFraction,
|
|
196
|
+
percent: true,
|
|
197
|
+
onValue: (value) => updateCommon({ kellyFraction: value }),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: "common:maxLoss",
|
|
201
|
+
label: "Loss cap",
|
|
202
|
+
value: common.maxLossFraction,
|
|
203
|
+
percent: true,
|
|
204
|
+
onValue: (value) => updateCommon({ maxLossFraction: value }),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "common:maxName",
|
|
208
|
+
label: "Name cap",
|
|
209
|
+
value: common.maxNameFraction,
|
|
210
|
+
percent: true,
|
|
211
|
+
onValue: (value) => updateCommon({ maxNameFraction: value }),
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
}
|
|
@@ -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
|
+
});
|