Download fire_math.py from smlflg/FireTracker: direct link, hf CLI and curl.
- Browser
- Download file 5.32 kB
-
https://huggingface.co/smlflg/FireTracker/resolve/main/fire_math.py
- Command line
-
hf download hf://smlflg/FireTracker/fire_math.py
-
curl -L -o fire_math.py https://huggingface.co/smlflg/FireTracker/resolve/main/fire_math.py
5.32 kB
| from dataclasses import dataclass | |
| from datetime import date | |
| 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 | |