From 8bb635efbddd630249e8d15de628539ad790c8dc Mon Sep 17 00:00:00 2001 From: HouseofTyrell Date: Sun, 28 Jun 2026 08:29:18 -0600 Subject: [PATCH] Fix calculator state and split allocation flows --- src/App.tsx | 14 +- src/Calculator.tsx | 17 +- src/ResultsTable.tsx | 44 ++++- src/components/MeetingMode/MeetingMode.tsx | 211 ++++++++++----------- src/components/ResultsSummary.tsx | 19 +- src/components/StrategySelectionInputs.tsx | 30 +++ src/popupContent.ts | 63 +++++- src/test/setup.ts | 42 ++++ src/workspace/MeetingSession.test.tsx | 14 ++ src/workspace/MeetingSession.tsx | 4 +- src/workspace/WorkspaceTab.test.tsx | 34 +++- src/workspace/WorkspaceTab.tsx | 112 ++++++++--- 12 files changed, 447 insertions(+), 157 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 2a19ba8..50dec15 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -60,8 +60,18 @@ export function App() {
- {activeView === 'calculator' && } - {activeView === 'workspace' && } + + {activeView === 'qfaf-test' && }
diff --git a/src/Calculator.tsx b/src/Calculator.tsx index 41e1172..18ad532 100644 --- a/src/Calculator.tsx +++ b/src/Calculator.tsx @@ -63,7 +63,11 @@ const generateDefaultOverrides = (baseIncome: number): YearOverride[] => { })); }; -export function Calculator() { +interface CalculatorProps { + isActive?: boolean; +} + +export function Calculator({ isActive = true }: CalculatorProps) { const [inputs, setInputs] = useState(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 */} -
- - -
- - {/* 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 && ( +
+ + +
+ )} + + {/* Collateral */} + {!splitMode && ( +
+ +
+ + $ + + + 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({ )} +
+
+ +
+ $ + { + const raw = e.target.value.trim(); + onUpdateInput( + 'collateralCostBasis', + raw === '' ? undefined : parseFormattedNumber(raw) + ); + }} + /> +
+ + Optional. Leave blank to assume basis equals today's collateral value. + +
+
+
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 +
+ + +
+
+ {fundingMode === 'total' && ( + + )}

- → 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. +

+ )} ) : ( <>