(DEFAULTS);
const advancedMode = useAdvancedMode();
const { isExpanded } = useScrollHeader('scroll-sentinel');
@@ -209,6 +213,7 @@ export function Calculator() {
taxRates.stateProfile,
advancedSettings.growthEnabled,
advancedSettings.defaultAnnualReturn,
+ inputs.collateralCostBasis,
]
);
@@ -266,11 +271,11 @@ export function Calculator() {
// Setup keyboard shortcuts (after all handlers are defined)
useKeyboardShortcuts({
- onPrint: printHandler || undefined,
- onExport: exportHandler || undefined,
- onShowHelp: () => setShowKeyboardHelp(true),
- onEscape: () => setShowKeyboardHelp(false),
- onPin: handlePinScenario,
+ onPrint: isActive ? printHandler || undefined : undefined,
+ onExport: isActive ? exportHandler || undefined : undefined,
+ onShowHelp: isActive ? () => setShowKeyboardHelp(true) : undefined,
+ onEscape: isActive ? () => setShowKeyboardHelp(false) : undefined,
+ onPin: isActive ? handlePinScenario : undefined,
});
// Input validation (warn-only, does not block calculation)
diff --git a/src/ResultsTable.tsx b/src/ResultsTable.tsx
index b5ca81a..d806ce8 100644
--- a/src/ResultsTable.tsx
+++ b/src/ResultsTable.tsx
@@ -173,7 +173,7 @@ export function ResultsTable({
// Deleverage group (only when a plan is active)
if (hasDeleverage) {
cols += 1; // Extension % headline
- if (expandDeleverage) cols += 3; // Unwind Gain, Unwind Tax, Fin. Saved
+ if (expandDeleverage) cols += 5; // Unwind Gain, ST/LT split, Unwind Tax, Fin. Saved
}
cols += 1; // Tax Savings column
@@ -445,6 +445,12 @@ export function ResultsTable({
Unwind Gain
+
+ Unwind ST
+
+
+ Unwind LT
+
Unwind Tax
@@ -560,6 +566,8 @@ export function ResultsTable({
—
—
—
+ —
+ —
>
)}
{hasDeleverage && 100% }
@@ -814,6 +822,16 @@ export function ResultsTable({
? formatCurrency(year.deleverageGainRealized)
: '—'}
+
+ {year.deleverageGainSt > 0.01
+ ? formatCurrency(year.deleverageGainSt)
+ : '—'}
+
+
+ {year.deleverageGainLt > 0.01
+ ? formatCurrency(year.deleverageGainLt)
+ : '—'}
+
{year.deleverageTax > 0.01
? `(${formatCurrency(year.deleverageTax)})`
@@ -1186,15 +1204,25 @@ function TransposedTable({
sections.push({
title: 'Gain Events',
rows: [
- { label: 'Event Amount', cell: y => moneyOrDash(y.gainEventAmount) },
- { label: 'CF Shelter Applied', cell: y => moneyOrDash(y.gainEventCfShelter) },
+ {
+ label: 'Event Amount',
+ contentKey: 'col-gain-event-amount',
+ cell: y => moneyOrDash(y.gainEventAmount),
+ },
+ {
+ label: 'CF Shelter Applied',
+ contentKey: 'col-gain-event-cf-shelter',
+ cell: y => moneyOrDash(y.gainEventCfShelter),
+ },
{
label: 'Event Tax Due',
+ contentKey: 'col-gain-event-tax',
cell: y => (y.gainEventAmount > 0 ? formatCurrency(y.gainEventTax) : '—'),
className: () => 'negative',
},
{
label: 'Tax Without Program',
+ contentKey: 'col-gain-event-tax-without-program',
cell: y => moneyOrDash(y.gainEventTaxWithoutStrategy),
},
],
@@ -1215,6 +1243,16 @@ function TransposedTable({
contentKey: 'col-deleverage-gain',
cell: y => moneyOrDash(y.deleverageGainRealized),
},
+ {
+ label: 'Unwind ST Gain',
+ contentKey: 'col-deleverage-st-gain',
+ cell: y => moneyOrDash(y.deleverageGainSt),
+ },
+ {
+ label: 'Unwind LT Gain',
+ contentKey: 'col-deleverage-lt-gain',
+ cell: y => moneyOrDash(y.deleverageGainLt),
+ },
{
label: 'Tax on Unwind',
contentKey: 'col-deleverage-tax',
diff --git a/src/components/MeetingMode/MeetingMode.tsx b/src/components/MeetingMode/MeetingMode.tsx
index 1c7aa92..5a78405 100644
--- a/src/components/MeetingMode/MeetingMode.tsx
+++ b/src/components/MeetingMode/MeetingMode.tsx
@@ -253,6 +253,8 @@ function Rail({
}) {
const projYears = advancedSettings.projectionYears;
const clampYear = (raw: string) => Math.max(1, Math.min(projYears, parseInt(raw, 10) || 1));
+ const effectiveView = useMemo(() => getEffectiveView(inputs), [inputs]);
+ const splitMode = inputs.splitAllocation?.enabled === true && effectiveView.isSplit;
const filingLabel =
FILING_STATUSES.find(f => f.value === inputs.filingStatus)
?.label.replace('Married Filing Jointly', 'MFJ')
@@ -403,10 +405,7 @@ function Rail({
}}
>
- {(() => {
- const view = getEffectiveView(inputs);
- return view.displayName;
- })()}
+ {effectiveView.displayName}
- {fmtCurrency(getEffectiveView(inputs).totalCollateral)}
+ {fmtCurrency(effectiveView.totalCollateral)}
@@ -508,116 +507,114 @@ function Rail({
Adjust scenario
- {inputs.splitAllocation?.enabled === true &&
- (() => {
- const view = getEffectiveView(inputs);
- if (!view.isSplit) return null;
- return (
+ {splitMode && (
+
+
+ Split allocation active
+
+ {effectiveView.legs.map(leg => (
-
- Split allocation active
-
- {view.legs.map(leg => (
-
-
- {leg.label}: {leg.strategy.name}
-
- {fmtCurrency(leg.amount)}
-
- ))}
-
- Edit split details in the main calculator view. Controls below are not active in
- split mode.
-
+
+ {leg.label}: {leg.strategy.name}
+
+
{fmtCurrency(leg.amount)}
- );
- })()}
-
- {/* Strategy */}
-
-
- Strategy
-
- onUpdateInput('strategyId', e.target.value)}
- style={railFieldStyle}
- >
-
- {STRATEGIES.filter(s => s.type === 'core').map(s => (
-
- {s.name}
-
- ))}
-
-
- {STRATEGIES.filter(s => s.type === 'overlay').map(s => (
-
- {s.name}
-
- ))}
-
-
-
-
- {/* Collateral */}
-
-
- Collateral
-
-
-
- $
-
-
- onUpdateInput('collateralAmount', parseFormattedNumber(e.target.value))
- }
- style={{
- ...railFieldStyle,
- paddingLeft: 22,
- fontFamily: M.mono,
- }}
- />
+ Edit split details in the Workspace before opening Meeting Mode.
+
-
+ )}
+
+ {/* Strategy */}
+ {!splitMode && (
+
+
+ Strategy
+
+ onUpdateInput('strategyId', e.target.value)}
+ style={railFieldStyle}
+ >
+
+ {STRATEGIES.filter(s => s.type === 'core').map(s => (
+
+ {s.name}
+
+ ))}
+
+
+ {STRATEGIES.filter(s => s.type === 'overlay').map(s => (
+
+ {s.name}
+
+ ))}
+
+
+
+ )}
+
+ {/* Collateral */}
+ {!splitMode && (
+
+
+ Collateral
+
+
+
+ $
+
+
+ onUpdateInput('collateralAmount', parseFormattedNumber(e.target.value))
+ }
+ style={{
+ ...railFieldStyle,
+ paddingLeft: 22,
+ fontFamily: M.mono,
+ }}
+ />
+
+
+ )}
{/* Income */}
diff --git a/src/components/ResultsSummary.tsx b/src/components/ResultsSummary.tsx
index bb3d34b..57e3ed5 100644
--- a/src/components/ResultsSummary.tsx
+++ b/src/components/ResultsSummary.tsx
@@ -215,8 +215,9 @@ export const ResultsSummary = React.memo(function ResultsSummary({
{formatCurrency(exitTaxAnalysis.embeddedGain)}
- Incl. {formatCurrency(exitTaxAnalysis.cumulativeBasisReduction)} basis reduction from
- harvesting
+ Incl. {formatCurrency(exitTaxAnalysis.cumulativeBasisReduction)} basis reduction
+ {exitTaxAnalysis.preExistingGain > 0 &&
+ ` + ${formatCurrency(exitTaxAnalysis.preExistingGain)} pre-existing gain`}
@@ -225,7 +226,7 @@ export const ResultsSummary = React.memo(function ResultsSummary({
contentKey="incremental-deferred-tax"
currentValue={formatCurrency(exitTaxAnalysis.incrementalDeferredTax)}
>
- Est. Deferred Tax (If Liquidated)
+ Est. Incremental Deferred Tax
{formatCurrency(exitTaxAnalysis.incrementalDeferredTax)}
@@ -261,9 +262,10 @@ export const ResultsSummary = React.memo(function ResultsSummary({
liquidating in year {projectionYears} would cost{' '}
{formatCurrency(Math.abs(exitTaxAnalysis.incrementalDeferredTax))} LESS than the
passive baseline, because the remaining loss carryforwards more than cover the
- strategy's embedded gain. Assumes initial basis equals initial value;
- appreciated-stock (Overlay) collateral carries additional pre-existing embedded gain
- not shown here.
+ strategy's embedded gain. Assumes initial basis equals initial value unless a
+ collateral cost basis is entered.
+ {exitTaxAnalysis.preExistingGain > 0 &&
+ ` Includes ${formatCurrency(exitTaxAnalysis.preExistingGain)} of pre-existing embedded gain in both the strategy and passive baseline, so the deferred-tax figure remains incremental to the strategy.`}
>
) : (
<>
@@ -272,8 +274,9 @@ export const ResultsSummary = React.memo(function ResultsSummary({
{formatCurrency(exitTaxAnalysis.incrementalDeferredTax)} of tax would come due on full
liquidation in year {projectionYears}. If the portfolio is instead held until death
(basis step-up) or donated, the deferred portion may become permanent. Assumes initial
- basis equals initial value; appreciated-stock (Overlay) collateral carries additional
- pre-existing embedded gain not shown here.
+ basis equals initial value unless a collateral cost basis is entered.
+ {exitTaxAnalysis.preExistingGain > 0 &&
+ ` Includes ${formatCurrency(exitTaxAnalysis.preExistingGain)} of pre-existing embedded gain in both the strategy and passive baseline, so the deferred-tax figure remains incremental to the strategy.`}
>
)}
diff --git a/src/components/StrategySelectionInputs.tsx b/src/components/StrategySelectionInputs.tsx
index 808bbfd..d9ee9f2 100644
--- a/src/components/StrategySelectionInputs.tsx
+++ b/src/components/StrategySelectionInputs.tsx
@@ -208,6 +208,36 @@ export function StrategySelectionInputs({
)}
+
+
+
Collateral Cost Basis
+
+ $
+ {
+ const raw = e.target.value.trim();
+ onUpdateInput(
+ 'collateralCostBasis',
+ raw === '' ? undefined : parseFormattedNumber(raw)
+ );
+ }}
+ />
+
+
+ Optional. Leave blank to assume basis equals today's collateral value.
+
+
+
+
Start Month
diff --git a/src/popupContent.ts b/src/popupContent.ts
index 03118cc..7f12a07 100644
--- a/src/popupContent.ts
+++ b/src/popupContent.ts
@@ -216,13 +216,13 @@ export const POPUP_CONTENT: Record
= {
definition:
'Estimated unrealized gain in the collateral portfolio at the end of the projection. ' +
'Includes both market appreciation and the basis reduction created by tax-loss ' +
- 'harvesting (each harvested loss lowers cost basis; realized LT gains raise it).',
+ 'harvesting (each harvested loss lowers cost basis; realized LT gains raise it), plus ' +
+ 'any pre-existing embedded gain implied by the collateral cost-basis input.',
formula:
- 'Embedded Gain = (Final Value − Initial Value) + Σ(ST Losses Harvested − LT Gains Realized)',
+ 'Embedded Gain = max(0, Initial Value − Cost Basis) + (Final Value − Initial Value) + Σ(ST Losses Harvested − LT Gains Realized)',
impact:
- 'This is the gain that would be taxed on full liquidation. Assumes initial basis equals ' +
- 'initial market value; Overlay collateral funded with appreciated stock carries additional ' +
- 'pre-existing embedded gain not shown.',
+ 'This is the gain that would be taxed on full liquidation. Leave cost basis blank to ' +
+ 'assume initial basis equals initial market value.',
},
'incremental-deferred-tax': {
@@ -820,6 +820,24 @@ export const POPUP_CONTENT: Record = {
'realized here step up basis, so they are NOT taxed again at the horizon exit.',
},
+ 'col-deleverage-st-gain': {
+ title: 'Unwind ST Gain',
+ definition:
+ 'Short-term-character portion of the deleveraging gain realized this year, including any modeled short-cover gain.',
+ formula: 'Short-term share of Unwind Gain',
+ impact:
+ 'This portion is netted with current-year strategy losses and capital-loss carryforwards before any remaining amount is taxed at short-term rates.',
+ },
+
+ 'col-deleverage-lt-gain': {
+ title: 'Unwind LT Gain',
+ definition:
+ 'Long-term-character portion of the deleveraging gain realized this year as seasoned extension exposure is unwound.',
+ formula: 'Long-term share of Unwind Gain',
+ impact:
+ 'This portion is netted with current-year strategy losses and capital-loss carryforwards before any remaining amount is taxed at long-term rates.',
+ },
+
'col-deleverage-tax': {
title: 'Tax on Unwind',
definition:
@@ -844,6 +862,41 @@ export const POPUP_CONTENT: Record = {
'The recurring economic benefit of deleveraging — it offsets the one-time unwind tax. ' +
'Like financing costs, it is informational: fees are netted out of portfolio growth.',
},
+
+ 'col-gain-event-amount': {
+ title: 'Gain Event Amount',
+ definition:
+ 'Planned outside capital gain realized in this year through a year override or Meeting Mode what-if.',
+ impact:
+ 'This is exogenous client activity. It can consume strategy carryforwards, but its tax is reported separately and is not charged against strategy savings.',
+ },
+
+ 'col-gain-event-cf-shelter': {
+ title: 'Carryforward Shelter Applied',
+ definition:
+ 'Amount of the planned gain event absorbed by current-year strategy losses and capital-loss carryforwards.',
+ formula: 'min(Gain Event Amount, Available Current-Year Losses + Capital Loss Carryforwards)',
+ impact:
+ 'Shows how much outside gain the strategy loss reserve sheltered before calculating event tax due.',
+ },
+
+ 'col-gain-event-tax': {
+ title: 'Gain Event Tax Due',
+ definition:
+ 'Tax due on the planned gain event after current-year strategy losses and carryforwards are applied.',
+ formula: 'Unsheltered Gain Event Amount × Applicable Gain Rate',
+ impact:
+ 'Reported separately from strategy savings so the model does not make an outside gain event look like a strategy cost.',
+ },
+
+ 'col-gain-event-tax-without-program': {
+ title: 'Tax Without Program',
+ definition:
+ 'Counterfactual tax that would have been due on the planned gain event without the strategy loss reserve.',
+ formula: 'Gain Event Amount × Applicable Gain Rate',
+ impact:
+ 'Comparing this to Event Tax Due shows the tax shelter provided by current losses and carryforwards.',
+ },
};
// Helper function to get popup content by key
diff --git a/src/test/setup.ts b/src/test/setup.ts
index 7b0828b..f813933 100644
--- a/src/test/setup.ts
+++ b/src/test/setup.ts
@@ -1 +1,43 @@
import '@testing-library/jest-dom';
+
+function installStorageShim() {
+ const store = new Map();
+ const storage: Storage = {
+ get length() {
+ return store.size;
+ },
+ clear() {
+ store.clear();
+ },
+ getItem(key: string) {
+ const normalizedKey = String(key);
+ return store.get(normalizedKey) ?? null;
+ },
+ key(index: number) {
+ return Array.from(store.keys())[index] ?? null;
+ },
+ removeItem(key: string) {
+ store.delete(String(key));
+ },
+ setItem(key: string, value: string) {
+ store.set(String(key), String(value));
+ },
+ };
+
+ Object.defineProperty(globalThis, 'localStorage', {
+ value: storage,
+ configurable: true,
+ });
+}
+
+const storage = globalThis.localStorage as Storage | undefined;
+
+if (
+ typeof storage === 'undefined' ||
+ typeof storage.getItem !== 'function' ||
+ typeof storage.setItem !== 'function' ||
+ typeof storage.removeItem !== 'function' ||
+ typeof storage.clear !== 'function'
+) {
+ installStorageShim();
+}
diff --git a/src/workspace/MeetingSession.test.tsx b/src/workspace/MeetingSession.test.tsx
index 0f6e8c9..6223355 100644
--- a/src/workspace/MeetingSession.test.tsx
+++ b/src/workspace/MeetingSession.test.tsx
@@ -131,6 +131,20 @@ describe('Meeting rail what-ifs (D-023)', () => {
expect(DEFAULT_SETTINGS.financingFeesEnabled).toBe(false);
});
+ it('defaults gain-event amount from effective split collateral', () => {
+ const inputs = createInputs({
+ collateralAmount: 100,
+ splitAllocation: {
+ enabled: true,
+ coreStrategyId: 'core-145-45',
+ coreAmount: 3000000,
+ overlayStrategyId: 'overlay-45-45',
+ overlayAmount: 7000000,
+ },
+ });
+ expect(createDefaultWhatIfs(inputs, DEFAULT_SETTINGS).gainEventAmount).toBe(5000000);
+ });
+
it('renders all four controls unchecked, labeled as resettable what-ifs', () => {
const { getByTestId, getByText } = renderSession();
fireEvent.click(getByText('Client questions'));
diff --git a/src/workspace/MeetingSession.tsx b/src/workspace/MeetingSession.tsx
index c4e5460..7244f8a 100644
--- a/src/workspace/MeetingSession.tsx
+++ b/src/workspace/MeetingSession.tsx
@@ -10,6 +10,7 @@ import {
import { getStrategy } from '../strategyData';
import { calculate, calculateWithOverrides, computeExitTaxAnalysis } from '../calculations';
import { MeetingMode, MeetingWhatIfs } from '../components/MeetingMode/MeetingMode';
+import { getEffectiveView } from '../utils/effectiveAllocation';
/**
* Shared scenario pipeline helpers — used by BOTH the Workspace results pane
@@ -78,13 +79,14 @@ export function createDefaultWhatIfs(
settings: AdvancedSettings
): MeetingWhatIfs {
const projYears = settings.projectionYears ?? 10;
+ const totalCollateral = getEffectiveView(inputs).totalCollateral;
return {
retirementEnabled: false,
retirementYear: Math.min(3, projYears),
retirementIncome: Math.round(inputs.annualIncome / 2),
gainEventEnabled: false,
gainEventYear: Math.min(3, projYears),
- gainEventAmount: Math.round(inputs.collateralAmount / 2),
+ gainEventAmount: Math.round(totalCollateral / 2),
};
}
diff --git a/src/workspace/WorkspaceTab.test.tsx b/src/workspace/WorkspaceTab.test.tsx
index 4319cf2..dc03f62 100644
--- a/src/workspace/WorkspaceTab.test.tsx
+++ b/src/workspace/WorkspaceTab.test.tsx
@@ -14,7 +14,7 @@
import { describe, it, expect, afterEach } from 'vitest';
import { render, cleanup, fireEvent } from '@testing-library/react';
import { WorkspaceTab, filterPaletteItems, PaletteItem } from './WorkspaceTab';
-import { calculate } from '../calculations';
+import { calculate, solveCollateralForTotal } from '../calculations';
import { DEFAULTS } from '../taxData';
import { CalculatorInputs, DEFAULT_SETTINGS, SplitAllocation } from '../types';
import { STORAGE_KEYS } from '../constants/storageKeys';
@@ -155,6 +155,38 @@ describe('Workspace split allocation (D-026)', () => {
expect(r.getByTestId('ws-split-total').textContent).toContain('12,000,000');
});
+ it('total-portfolio funding scales split legs proportionally', () => {
+ ackQp();
+ const r = render( );
+ fireEvent.click(r.getByLabelText('Split allocation (core + overlay)'));
+ fireEvent.click(r.getByText('Total portfolio'));
+
+ const totalInput = r.getAllByLabelText('Total available portfolio')[0];
+ fireEvent.change(totalInput, { target: { value: '20,000,000' } });
+
+ const solvedCollateral = solveCollateralForTotal(
+ 20000000,
+ { ...DEFAULTS, splitAllocation: SPLIT_ON },
+ DEFAULT_SETTINGS.qfafMultiplier,
+ DEFAULT_SETTINGS.washSaleDisallowanceRate
+ );
+ const scale = solvedCollateral / (SPLIT_ON.coreAmount + SPLIT_ON.overlayAmount);
+ const scaledSplit = {
+ ...SPLIT_ON,
+ coreAmount: SPLIT_ON.coreAmount * scale,
+ overlayAmount: SPLIT_ON.overlayAmount * scale,
+ };
+ const expected = calculate({ ...DEFAULTS, splitAllocation: scaledSplit }, DEFAULT_SETTINGS);
+
+ expect(r.getByTestId('ws-metric-total-savings').textContent).toBe(
+ formatCurrency(expected.summary.totalTaxSavings)
+ );
+ expect(r.getByTestId('ws-split-total').textContent).toContain(
+ formatCurrency(expected.sizing.totalExposure)
+ );
+ expect(Math.round(expected.sizing.totalExposure)).toBe(20000000);
+ });
+
it('deleverage + split shows the D-016 conflict warning in the rail', () => {
ackQp();
const r = render( );
diff --git a/src/workspace/WorkspaceTab.tsx b/src/workspace/WorkspaceTab.tsx
index 951d716..d1d1fb6 100644
--- a/src/workspace/WorkspaceTab.tsx
+++ b/src/workspace/WorkspaceTab.tsx
@@ -574,7 +574,11 @@ function PinComparisonStrip({
* toggle — see workspace.css. Every figure renders from the live engine
* outputs (one-engine rule); the design's numbers were placeholders.
*/
-export function WorkspaceTab() {
+interface WorkspaceTabProps {
+ isActive?: boolean;
+}
+
+export function WorkspaceTab({ isActive = true }: WorkspaceTabProps) {
const qualifiedPurchaser = useQualifiedPurchaser();
// D-026: same hook + storage as the Classic comparison panel — one shared
// pin store, so an advisor can pin here and compare there (or vice versa).
@@ -625,6 +629,7 @@ export function WorkspaceTab() {
// ⌘K / Ctrl+K opens the command palette anywhere in the Workspace.
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
+ if (!isActive) return;
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === 'k') {
if (isMeetingMode) return; // Meeting Mode keeps its own surface
e.preventDefault();
@@ -633,21 +638,33 @@ export function WorkspaceTab() {
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
- }, [isMeetingMode]);
+ }, [isActive, isMeetingMode]);
// In total-budget mode, solve for the collateral that makes
- // collateral + auto-sized QFAF equal the available portfolio.
- // Split allocation sets the total from its two leg amounts and the engine
- // ignores `collateralAmount`, so the solver has nothing to solve — total-
- // budget mode is unavailable while split is on (UI enforces + explains).
+ // collateral + auto-sized QFAF equal the available portfolio. In split mode,
+ // the two leg amounts act as allocation weights and are scaled proportionally.
const effectiveInputs = useMemo(() => {
- if (fundingMode !== 'total' || inputs.splitAllocation?.enabled === true) return inputs;
+ if (fundingMode !== 'total') return inputs;
const solved = solveCollateralForTotal(
totalAvailable,
inputs,
settings.qfafMultiplier,
settings.washSaleDisallowanceRate
);
+ const split = inputs.splitAllocation;
+ if (split?.enabled === true) {
+ const currentTotal = split.coreAmount + split.overlayAmount;
+ if (currentTotal <= 0) return inputs;
+ const scale = solved / currentTotal;
+ return {
+ ...inputs,
+ splitAllocation: {
+ ...split,
+ coreAmount: split.coreAmount * scale,
+ overlayAmount: split.overlayAmount * scale,
+ },
+ };
+ }
return { ...inputs, collateralAmount: solved };
}, [
fundingMode,
@@ -1483,7 +1500,11 @@ export function WorkspaceTab() {
icon={I.overlay}
name="Strategy"
summary={
- splitOn ? (
+ splitOn && fundingMode === 'total' ? (
+ <>
+ Split · {formatCurrencyAbbreviated(totalAvailable)} total budget
+ >
+ ) : splitOn ? (
<>
Split · {formatCurrencyAbbreviated(split.coreAmount)} core +{' '}
{formatCurrencyAbbreviated(split.overlayAmount)} overlay
@@ -1507,12 +1528,7 @@ export function WorkspaceTab() {
{
- updateSplit({ enabled: e.target.checked });
- // Total-budget solving has nothing to solve in split mode
- // (leg amounts set the total) — fall back to collateral mode.
- if (e.target.checked && fundingMode === 'total') setFundingMode('collateral');
- }}
+ onChange={e => updateSplit({ enabled: e.target.checked })}
/>
Split allocation (core + overlay)
@@ -1565,16 +1581,62 @@ export function WorkspaceTab() {
}
/>
+
+
Fund by
+
+ setFundingMode('collateral')}
+ >
+ Collateral
+
+ {
+ setTotalAvailable(Math.round(results.sizing.totalExposure));
+ setFundingMode('total');
+ }}
+ >
+ Total portfolio
+
+
+
+ {fundingMode === 'total' && (
+
+ Total available portfolio
+ setTotalAvailable(parseFormattedNumber(e.target.value))}
+ />
+
+ )}
- → Total collateral {formatCurrency(splitTotal)}
- {splitTotal > 0 && (
+ {fundingMode === 'total' ? (
<>
- {' '}
- ({formatPercent(split.coreAmount / splitTotal)} core /{' '}
- {formatPercent(split.overlayAmount / splitTotal)} overlay)
+ → Core {formatCurrency(effectiveInputs.splitAllocation?.coreAmount ?? 0)} +
+ overlay {formatCurrency(effectiveInputs.splitAllocation?.overlayAmount ?? 0)}
+ {inputs.qfafEnabled && (
+ <> + QFAF {formatCurrency(results.sizing.qfafValue)}>
+ )}
+ {' = '}
+ {formatCurrency(results.sizing.totalExposure)}
+ >
+ ) : (
+ <>
+ → Total collateral {formatCurrency(splitTotal)}
+ {splitTotal > 0 && (
+ <>
+ {' '}
+ ({formatPercent(split.coreAmount / splitTotal)} core /{' '}
+ {formatPercent(split.overlayAmount / splitTotal)} overlay)
+ >
+ )}
+ {inputs.qfafEnabled && <> · QFAF auto-sizes against the combined ST losses>}
>
)}
- {inputs.qfafEnabled && <> · QFAF auto-sizes against the combined ST losses>}
{(split.coreAmount <= 0 || split.overlayAmount <= 0) && (
@@ -1582,10 +1644,12 @@ export function WorkspaceTab() {
single-strategy inputs.
)}
-
- Total-budget funding isn't available with split allocation — the two leg
- amounts set the total.
-
+ {fundingMode === 'total' && (
+
+ The core and overlay amounts are used as allocation weights; the tool scales
+ both legs so collateral + QFAF equals the total portfolio.
+
+ )}
>
) : (
<>