File size: 5,324 Bytes
f91bd05 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | from dataclasses import dataclass
from datetime import date
@dataclass
class FireResult:
fi_number: float # Annual expenses / SWR
lean_fire: float # FI number at 80% expenses
fat_fire: float # FI number at 120% expenses
coast_fire: float # Amount needed now for compound growth to reach FI
total_portfolio: float # Sum of all assets
fi_percentage: float # portfolio / fi_number * 100
savings_rate: float # (income - expenses) / income * 100
monthly_savings: float # income - expenses
years_to_fi: float | None # compound growth projection, None if already FI
fi_date: date | None # projected FI date
real_return: float # nominal return - inflation
def calculate_fire(
monthly_income: float,
monthly_expenses: float,
total_portfolio: float,
birth_year: int,
swr: float = 0.04,
expected_return: float = 0.07,
inflation: float = 0.02,
target_retirement_age: int = 67,
) -> FireResult:
"""Main FIRE calculation. All the core metrics."""
annual_expenses = monthly_expenses * 12
annual_income = monthly_income * 12
monthly_savings = monthly_income - monthly_expenses
annual_savings = monthly_savings * 12
fi_number = annual_expenses / swr
lean_fire = (annual_expenses * 0.8) / swr
fat_fire = (annual_expenses * 1.2) / swr
real_return = expected_return - inflation
# Coast FIRE: how much you need NOW so compounding alone reaches FI by target retirement age
current_age = date.today().year - birth_year
years_to_retirement = max(target_retirement_age - current_age, 0)
coast_fire = fi_number / ((1 + real_return) ** years_to_retirement) if years_to_retirement > 0 else fi_number
fi_percentage = (total_portfolio / fi_number * 100) if fi_number > 0 else 100
savings_rate = ((annual_income - annual_expenses) / annual_income * 100) if annual_income > 0 else 0
# Time to FI via compound growth projection
years_to_fi = calculate_years_to_fi(total_portfolio, annual_savings, fi_number, real_return)
fi_date = None
if years_to_fi is not None:
total_months = date.today().month + int((years_to_fi % 1) * 12)
extra_years = (total_months - 1) // 12
final_month = ((total_months - 1) % 12) + 1
fi_date = date(date.today().year + int(years_to_fi) + extra_years, final_month, 1)
return FireResult(
fi_number=round(fi_number, 2),
lean_fire=round(lean_fire, 2),
fat_fire=round(fat_fire, 2),
coast_fire=round(coast_fire, 2),
total_portfolio=total_portfolio,
fi_percentage=round(fi_percentage, 1),
savings_rate=round(savings_rate, 1),
monthly_savings=round(monthly_savings, 2),
years_to_fi=round(years_to_fi, 1) if years_to_fi is not None else None,
fi_date=fi_date,
real_return=real_return,
)
def calculate_years_to_fi(
current_portfolio: float,
annual_savings: float,
target: float,
annual_return: float,
) -> float | None:
"""Calculate years to reach FI number with compound growth + contributions."""
if current_portfolio >= target:
return 0.0
if annual_savings <= 0 and annual_return <= 0:
return None # Will never reach FI
# Iterative calculation (month by month for accuracy)
portfolio = current_portfolio
monthly_return = (1 + annual_return) ** (1 / 12) - 1
monthly_contribution = annual_savings / 12
months = 0
max_months = 12 * 100 # Cap at 100 years
while portfolio < target and months < max_months:
portfolio = portfolio * (1 + monthly_return) + monthly_contribution
months += 1
if months >= max_months:
return None
return months / 12
def calculate_scenario(
base: FireResult,
monthly_income: float,
monthly_expenses: float,
total_portfolio: float,
birth_year: int,
extra_monthly_savings: float = 0,
adjusted_expenses: float | None = None,
swr: float = 0.04,
expected_return: float = 0.07,
inflation: float = 0.02,
target_retirement_age: int = 67,
) -> FireResult:
"""What-if scenario: adjust savings or expenses and recalculate."""
new_expenses = adjusted_expenses if adjusted_expenses is not None else monthly_expenses
new_income = monthly_income + extra_monthly_savings
return calculate_fire(
monthly_income=new_income,
monthly_expenses=new_expenses,
total_portfolio=total_portfolio,
birth_year=birth_year,
swr=swr,
expected_return=expected_return,
inflation=inflation,
target_retirement_age=target_retirement_age,
)
def projection_timeline(
total_portfolio: float,
monthly_savings: float,
annual_return: float,
years: int = 30,
) -> list[dict]:
"""Generate year-by-year portfolio projection for charts."""
timeline = []
portfolio = total_portfolio
monthly_return = (1 + annual_return) ** (1 / 12) - 1
for year in range(years + 1):
timeline.append({
"year": date.today().year + year,
"portfolio": round(portfolio, 2),
})
for _ in range(12):
portfolio = portfolio * (1 + monthly_return) + monthly_savings
return timeline
|