FireTracker / fire_math.py
smlflg's picture
Initial public upload from Projekte/FireTracker
f91bd05 verified
Raw History Blame Contribute Delete
5.32 kB
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