diff --git a/src/App.tsx b/src/App.tsx index 0b5af52..a988c40 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,12 +7,13 @@ import AccumulationStrategy from './components/features/AccumulationStrategy'; import TaxReference from './components/features/TaxReference'; import FireAnalysis from './components/features/FireAnalysis'; import { calculateStrategy, calculateLongevity } from './services/calculationEngine'; -import { TrendingUp, Calculator, AlertTriangle, BookOpen, Sun, Moon, PiggyBank, Settings, Flame } from 'lucide-react'; +import { TrendingUp, Calculator, AlertTriangle, BookOpen, Sun, Moon, PiggyBank, Settings, Flame, RefreshCw } from 'lucide-react'; import Footer from './components/layout/Footer'; import WizardModal from './components/features/wizard/WizardModal'; import SettingsModal from './components/features/SettingsModal'; import { db } from './services/db'; import { projectAssets } from './services/projection'; +import WhatIfAnalysis from './components/features/WhatIfAnalysis'; const INITIAL_PROFILE: UserProfile = { age: 65, // Retirement Start Age @@ -35,7 +36,7 @@ const App: React.FC = () => { const [profile, setProfile] = useState(INITIAL_PROFILE); const [strategyResult, setStrategyResult] = useState(null); const [longevityResult, setLongevityResult] = useState(null); - const [activeTab, setActiveTab] = useState<'withdrawal' | 'accumulation' | 'longevity' | 'reference' | 'fire'>('accumulation'); + const [activeTab, setActiveTab] = useState<'withdrawal' | 'accumulation' | 'longevity' | 'reference' | 'fire' | 'scenarios'>('accumulation'); const [isDarkMode, setIsDarkMode] = useState(false); const [isWizardOpen, setIsWizardOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); @@ -254,6 +255,7 @@ const App: React.FC = () => { { id: 'fire', icon: Flame, label: 'FIRE Analysis' }, { id: 'withdrawal', icon: Calculator, label: 'Withdrawal' }, { id: 'longevity', icon: TrendingUp, label: 'Longevity' }, + { id: 'scenarios', icon: RefreshCw, label: 'Scenarios' }, { id: 'reference', icon: BookOpen, label: 'Reference' } ].map(tab => ( + {isOpen && ( +
+

This tool projects your portfolio growth and withdrawal strategies to compare the long-term impact of different accumulation choices.

+ +
+
+

Total Tax Paid

+

The sum of all estimated federal taxes paid during retirement.

+
    +
  • Traditional Withdrawals: Taxed as Ordinary Income
  • +
  • Brokerage Withdrawals: Taxed as Capital Gains (assumes 100% is gain/growth, long-term rates)
  • +
  • Roth/HSA Withdrawals: Tax-Free
  • +
+
+
+

Ending Balance (Age 100)

+

The projected total portfolio value specifically at Age 100.

+
    +
  • If portfolio depletes before 100, this is $0.
  • +
  • Adjusted for inflation to show \"Present Value\" (Today's buying power) for realistic comparison.
  • +
+
+
+
+ )} + + ); + }; + + return ( +
+ + + + {/* Header / Summary */} +
+
+

+ + What-If Scenarios +

+ +
+

+ Adjust your savings strategy to see how prioritizing different accounts affects your **Total Lifetime Taxes** and **Ending Wealth**. +

+ +
+ {/* Comparison Stats */} +
+
+
Total Tax Paid
+
{formatCurrency(currentResult.totalTaxPaid)}
+ +
+ + + {formatCurrency(scenarioResult.totalTaxPaid)} + +
+
+ {scenarioResult.totalTaxPaid < currentResult.totalTaxPaid + ? `Save ${formatCurrency(currentResult.totalTaxPaid - scenarioResult.totalTaxPaid)}` + : `Pay ${formatCurrency(scenarioResult.totalTaxPaid - currentResult.totalTaxPaid)} more`} +
+
+ Ret. Start Assets: {formatCurrency(currentResult.projectionStartAssets)} +
+
+ +
+
Ending Balance (Age 100)
+
{formatCurrency(currentResult.endingBalance)}
+
+ ≈ {formatCurrency(currentResult.endingBalancePV)} (Today's Value) +
+ +
+ + currentResult.endingBalance ? 'text-green-600' : 'text-red-600'}`}> + {formatCurrency(scenarioResult.endingBalance)} + +
+
+ {scenarioResult.endingBalance > currentResult.endingBalance + ? `Gain ${formatCurrency(scenarioResult.endingBalance - currentResult.endingBalance)}` + : `Lose ${formatCurrency(currentResult.endingBalance - scenarioResult.endingBalance)}`} +
+
+ Ret. Start Assets: {formatCurrency(scenarioResult.projectionStartAssets)} +
+
+
+ + {/* Chart: Annual Tax & Balance Comparison */} +
+
+ Current Tax + Current Assets + What-If Tax + What-If Assets +
+ + + + + {/* Left Y-Axis: Taxes */} + `$${(val / 1000).toFixed(0)}k`} + tick={{ fill: isDarkMode ? '#94a3b8' : '#475569', fontSize: 10 }} + axisLine={false} + tickLine={false} + width={35} + /> + {/* Right Y-Axis: Assets */} + { + if (val >= 1000000) return `$${(val / 1000000).toFixed(1)}M`; + if (val >= 1000) return `$${(val / 1000).toFixed(0)}k`; + return `$${val}`; + }} + tick={{ fill: isDarkMode ? '#94a3b8' : '#475569', fontSize: 10 }} + axisLine={false} + tickLine={false} + width={45} + /> + { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

Age {label}

+
+
+
+
+

Current Plan

+
+
+
+ Assets: + {formatCurrency(data.currentAssets)} +
+
+ Annual Tax: + {formatCurrency(data.currentTax)} +
+
+
+
+
+
+

What-If Scenario

+
+
+
+ Assets: + {formatCurrency(data.scenarioAssets)} +
+
+ Annual Tax: + {formatCurrency(data.scenarioTax)} +
+
+
+
+
+ ); + } + return null; + }} + /> + + + + + + +
+
+
+
+
+ + {/* Controls */} +
+

Adjust Scenario Contributions

+
+
+ +
+ $ + handleContributionChange('traditionalIRA', val)} + className={`${inputClass} pl-8`} + /> +
+
+
+ +
+ $ + handleContributionChange('rothIRA', val)} + className={`${inputClass} pl-8`} + /> +
+
+
+ +
+ $ + handleContributionChange('brokerage', val)} + className={`${inputClass} pl-8`} + /> +
+
+
+ +
+ $ + handleContributionChange('hsa', val)} + className={`${inputClass} pl-8`} + /> +
+
+
+ + {/* Total Annual Savings Check */} +
+ Total Annual Savings: + + {formatCurrency(scenarioContributions.traditionalIRA + scenarioContributions.rothIRA + scenarioContributions.brokerage + scenarioContributions.hsa)} + +
+
+
+ ); +}; + +export default WhatIfAnalysis; diff --git a/src/services/calculationEngine.ts b/src/services/calculationEngine.ts index c401792..c271ac6 100644 --- a/src/services/calculationEngine.ts +++ b/src/services/calculationEngine.ts @@ -99,20 +99,44 @@ export const getWithdrawalOrder = ( } } - // 2. Taxable Brokerage (Capital Gains) - Always accessible + // 2. Rule of 55 - If age >= 55 (fills low brackets first, like standard retirement) + if (age >= 55) { + const brackets = TAX_BRACKETS[filingStatus]; + const top12Bracket = brackets.find(b => b.rate === 0.12)?.limit || 50400; + const optimalTradWithdrawal = Math.min( + assets.traditionalIRA, + stdDeductionNeed + top12Bracket + ); + + if (optimalTradWithdrawal > 0) { + order.push({ + source: 'Traditional IRA (Rule of 55)', + limit: optimalTradWithdrawal, + taxType: 'Ordinary', + penalty: false + }); + } + } + + // 3. Taxable Brokerage (Capital Gains) - Always accessible order.push({ source: 'Taxable Brokerage', limit: assets.brokerage, taxType: 'CapitalGains', penalty: false }); - // 3. Roth Contributions (Basis) - Always tax/penalty free + // 4. Roth Contributions (Basis) - Always tax/penalty free order.push({ source: 'Roth IRA (Basis)', limit: rothBasisAvailable, taxType: 'None', penalty: false }); - // 4. Rule of 55 - If age >= 55 and separated from employer + // 5. Additional Traditional IRA (Rule of 55) - If more needed beyond 12% bracket if (age >= 55) { - order.push({ - source: 'Traditional IRA (Rule of 55)', - limit: assets.traditionalIRA, - taxType: 'Ordinary', - penalty: false - }); + const brackets = TAX_BRACKETS[filingStatus]; + const top12Bracket = brackets.find(b => b.rate === 0.12)?.limit || 50400; + const alreadyIncluded = Math.min(assets.traditionalIRA, stdDeductionNeed + top12Bracket); + if (assets.traditionalIRA > alreadyIncluded) { + order.push({ + source: 'Traditional IRA (Rule of 55 - Additional)', + limit: assets.traditionalIRA - alreadyIncluded, + taxType: 'Ordinary', + penalty: false + }); + } } // 4. Traditional IRA (Penalty) - Last resort @@ -292,7 +316,7 @@ export const calculateStrategy = (profile: UserProfile): StrategyResult => { // Add to Plan const taxableAmt = (step.taxType === 'None') ? 0 : - (step.taxType === 'CapitalGains' ? pull * 0.5 : pull); // Simplifed 50% gain ratio + (step.taxType === 'CapitalGains' ? pull * 1.0 : pull); // Assumes 100% gain ratio (LTCG) withdrawalPlan.push({ source: step.source, @@ -523,6 +547,27 @@ export const calculateLongevity = (profile: UserProfile, strategy: StrategyResul const totalAssets = currentBrokerage + currentTrad + currentRoth + currentHSA; + // 4b. Calculate Estimated Tax for this Year + const ordIncome = basePension + fromTrad; // Trad Withdrawals are ordinary income + const qDivRatio = profile.income.qualifiedDividendRatio || 0.9; + const ordDividends = currentDividends * (1 - qDivRatio); + const qualDividends = currentDividends * qDivRatio; + + // Brokerage Withdrawals: Assumes 100% is gain (most conservative for long-term savings) + const brokerageGain = fromBrokerage * 1.0; + + const totalOrdinaryForTax = ordIncome + ordDividends; + const totalCapGainsForTax = qualDividends + brokerageGain; + + const taxableSS = calculateTaxableSocialSecurity(currentSS, totalOrdinaryForTax, profile.filingStatus); + const stdDeduction = STANDARD_DEDUCTION[profile.filingStatus] + + (age >= 65 ? AGE_DEDUCTION[profile.filingStatus] * (profile.filingStatus === FilingStatus.MarriedJoint ? 2 : 1) : 0); + + const estimatedTax = calculateFederalTax(totalOrdinaryForTax + taxableSS, totalCapGainsForTax, profile.filingStatus, stdDeduction); + const totalCashFlow = fromBrokerage + fromTrad + fromRoth + fromHSA + totalFixedIncome; + const effectiveTaxRate = totalCashFlow > 0 ? estimatedTax / totalCashFlow : 0; + + projection.push({ age, year: i, @@ -547,7 +592,9 @@ export const calculateLongevity = (profile: UserProfile, strategy: StrategyResul withdrawalTradSEPP: fromTradSEPP, withdrawalTradPenalty: fromTradPenalty, earlyWithdrawalPenalty: penaltyAmount, - isDepleted: totalAssets <= 0 + isDepleted: totalAssets <= 0, + estimatedTax, + effectiveTaxRate }); if (totalAssets <= 0 && !depletionAge) depletionAge = age; diff --git a/src/types/core.ts b/src/types/core.ts index 671eb25..b88e8a6 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -102,6 +102,8 @@ export interface YearProjection { withdrawalTradPenalty: number; // Penalized withdrawal amount earlyWithdrawalPenalty: number; // 10% penalty amount isDepleted: boolean; + estimatedTax: number; // New: Estimated federal tax for this year + effectiveTaxRate: number; // New: Effective tax rate } export interface LongevityResult {