gloomberb 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -2
- package/bin/gloomberb +1 -1
- package/package.json +1 -1
- package/src/app/runtime/desktop-menu.ts +5 -1
- package/src/app/runtime/ticker-refresh.ts +4 -1
- package/src/app.tsx +20 -8
- 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/capabilities/registry.ts +1 -0
- package/src/capabilities/types.ts +23 -0
- package/src/cli/commands/api.ts +119 -0
- package/src/cli/commands/automation.ts +173 -0
- package/src/cli/commands/command-utils.ts +53 -0
- package/src/cli/commands/market.ts +527 -0
- package/src/cli/commands/overview.ts +222 -0
- package/src/cli/commands/search.ts +39 -0
- package/src/cli/commands/system.ts +553 -0
- package/src/cli/commands/ticker.ts +104 -12
- package/src/cli/context.ts +35 -2
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/entry.ts +51 -0
- package/src/cli/errors.ts +56 -5
- package/src/cli/index.ts +73 -11
- package/src/cli/options.test.ts +38 -0
- package/src/cli/options.ts +95 -0
- package/src/cli/pane-functions/index.ts +10 -1
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/cli/registry.test.ts +40 -0
- package/src/cli/registry.ts +46 -5
- package/src/cli/result.test.ts +82 -0
- package/src/cli/result.ts +149 -0
- package/src/cli/types.ts +11 -0
- package/src/cli.test.ts +11 -11
- 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/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
- package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
- package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
- package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
- package/src/components/chart/core/pane-settings.test.ts +27 -0
- package/src/components/chart/core/pane-settings.ts +60 -1
- package/src/components/chart/core/pointer.ts +34 -6
- package/src/components/chart/price-axis-labels.tsx +3 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
- package/src/components/chart/static/bar-chart-surface.tsx +151 -28
- package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
- package/src/components/chart/static/chart/surface.test.tsx +92 -0
- package/src/components/chart/static/chart/surface.tsx +167 -24
- package/src/components/chart/stock-chart/index.test.ts +25 -0
- package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
- package/src/components/chart/stock-chart/settings.ts +7 -3
- package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
- package/src/components/command-bar/surface/pane.test.tsx +76 -0
- package/src/components/data-table/view.tsx +13 -2
- package/src/components/feed-data-table/stack-view.tsx +9 -1
- package/src/components/index.ts +1 -1
- package/src/components/input-search-bar.test.tsx +103 -0
- package/src/components/input-search-bar.tsx +16 -0
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/pane/content.tsx +20 -8
- package/src/components/layout/pane/footer/registration.tsx +6 -2
- package/src/components/layout/pane/frame.tsx +4 -4
- package/src/components/layout/pane/header.tsx +40 -4
- package/src/components/layout/pane/index.tsx +3 -1
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/active-drag.ts +5 -1
- package/src/components/layout/shell/cursor-occlusion.ts +196 -0
- package/src/components/layout/shell/drag/runtime.ts +28 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +487 -4
- package/src/components/layout/shell/index.tsx +165 -4
- 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/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +34 -16
- package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
- package/src/components/layout/shell/shortcuts.ts +4 -0
- package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/shell/window-mode/index.ts +11 -3
- package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
- package/src/components/layout/status-bar.test.tsx +111 -1
- package/src/components/layout/status-bar.tsx +37 -7
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-edit/mode.test.ts +76 -5
- package/src/components/layout/window-edit/mode.ts +47 -2
- package/src/components/layout/window-edit/presentation.ts +2 -1
- package/src/components/onboarding/onboarding-steps.tsx +8 -28
- package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
- package/src/components/onboarding/onboarding-wizard.tsx +2 -4
- package/src/components/onboarding/portfolio-step/index.tsx +0 -12
- package/src/components/onboarding/portfolio-step/types.ts +1 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -5
- package/src/components/price-performance.tsx +9 -1
- package/src/components/table-view-shared.tsx +12 -4
- package/src/components/ui/data-table/opentui/index.tsx +25 -12
- package/src/components/ui/data-table/types.ts +4 -0
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/page-stack-view.tsx +6 -2
- package/src/components/ui/table-layout.test.ts +25 -0
- package/src/components/ui/table-layout.ts +22 -6
- package/src/components/ui/tabs.tsx +26 -10
- package/src/components/ui/ui.test.tsx +87 -0
- package/src/core/app-services.ts +7 -4
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/data/config/store/index.ts +1 -2
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
- package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
- 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/news/wire/read-state.ts +9 -32
- 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/cli/portfolio-command.ts +23 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- 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/shared/read-state.ts +43 -0
- package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
- package/src/plugins/builtin/substack/article-detail.tsx +39 -15
- package/src/plugins/builtin/substack/article-stack.tsx +6 -2
- package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
- package/src/plugins/builtin/substack/pane.tsx +8 -3
- package/src/plugins/builtin/substack/read-state.ts +55 -0
- package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/ibkr/broker-adapter.ts +19 -0
- package/src/plugins/ibkr/config.test.ts +31 -2
- package/src/plugins/ibkr/config.ts +9 -2
- package/src/plugins/ibkr/flex/index.test.ts +18 -9
- package/src/plugins/ibkr/flex/index.ts +30 -0
- package/src/plugins/ibkr/flex/statement-client.ts +68 -14
- package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
- package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
- package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
- package/src/plugins/pane-manager/docking.ts +9 -2
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager/queries.ts +8 -2
- 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/controller/index.ts +2 -0
- package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
- 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 +75 -0
- package/src/plugins/prediction-markets/pane.tsx +71 -23
- package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
- package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/index.ts +13 -0
- package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
- package/src/renderers/electrobun/bun/window/focus.ts +5 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
- package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
- package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/host/text.test.ts +22 -0
- package/src/renderers/electrobun/view/host/text.tsx +24 -20
- package/src/renderers/electrobun/view/styles.css +11 -0
- package/src/renderers/electrobun/view/ui-host.tsx +1 -1
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/start.tsx +13 -5
- 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/state/app/input-capture.ts +12 -0
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/plugin.ts +28 -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/cli-output.ts +6 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- package/src/version.ts +1 -1
- 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 <ticker>.</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
|
+
}
|