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