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