-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfour_percent_rule.py
More file actions
139 lines (118 loc) · 3.8 KB
/
four_percent_rule.py
File metadata and controls
139 lines (118 loc) · 3.8 KB
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
#!/usr/bin/env python3
import argparse
import re
def parse_balance(value: str) -> float:
cleaned = value.strip().replace(",", "").replace("$", "")
match = re.fullmatch(r"(.+?)\s*([kKmM])", cleaned)
if match:
try:
amount = float(match.group(1))
except ValueError as exc:
raise argparse.ArgumentTypeError(
"Balance must be a number like 1500000, 1e6, 800k, or 1.5M."
) from exc
suffix = match.group(2).lower()
multiplier = {"k": 1_000, "m": 1_000_000}[suffix]
return amount * multiplier
try:
return float(cleaned)
except ValueError as exc:
raise argparse.ArgumentTypeError(
"Balance must be a number like 1500000, 1e6, 800k, or 1.5M."
) from exc
def parse_args():
parser = argparse.ArgumentParser(
description=(
"Retirement projection with optional overrides for starting balance, "
"starting age, growth rate, withdrawal rate, and inflation."
)
)
parser.add_argument(
"--age",
"-a",
type=int,
default=59,
help="Starting age (default: 59)",
)
parser.add_argument(
"--balance",
"-b",
type=parse_balance,
default=1_500_000,
help="Starting balance in dollars, for example 1500000, 1e6, 800k, or 1.5M (default: 1500000)",
)
parser.add_argument(
"--growth",
"-g",
type=float,
default=3.2,
help="Annual growth rate in percent (default: 3.2)",
)
parser.add_argument(
"--inflation",
"-i",
type=float,
default=3.0,
help="Annual inflation rate in percent for increasing withdrawals (default: 3.0)",
)
parser.add_argument(
"--withdrawal",
"-w",
type=float,
default=4.0,
help="Annual withdrawal rate in percent of starting balance (default: 4.0)",
)
parser.add_argument(
"--years",
"-y",
type=int,
default=10,
help="Number of years to project (default: 10)",
)
return parser.parse_args()
def money(value: float) -> str:
return f"${value:,.2f}"
def main():
args = parse_args()
starting_balance = args.balance
starting_age = args.age
growth_rate = args.growth / 100.0
withdrawal_rate = args.withdrawal / 100.0
inflation_rate = args.inflation / 100.0
years = args.years
first_year_withdrawal = starting_balance * withdrawal_rate
balance = starting_balance
current_withdrawal = first_year_withdrawal
print()
print("Retirement Projection")
print("---------------------")
print(f"Starting balance: {money(starting_balance)}")
print(f"Starting age: {starting_age}")
print(f"Growth rate: {args.growth:.2f}%")
print(f"Withdrawal rate: {args.withdrawal:.2f}%")
print(f"Inflation rate: {args.inflation:.2f}%")
print(f"First-year withdrawal: {money(first_year_withdrawal)}")
print(f"Years: {years}")
print()
header = (
f"{'Year':>4} {'Age':>3} {'Start Balance':>15} "
f"{'Growth':>12} {'Withdrawal':>12} {'End Balance':>15}"
)
print(header)
print("-" * len(header))
for year in range(1, years + 1):
age = starting_age + year - 1
start_balance = balance
growth = start_balance * growth_rate
end_balance = start_balance + growth - current_withdrawal
print(
f"{year:>4} {age:>3} {money(start_balance):>15} "
f"{money(growth):>12} {money(current_withdrawal):>12} "
f"{money(end_balance):>15}"
)
balance = end_balance
current_withdrawal *= (1 + inflation_rate)
print()
print(f"Ending balance after {years} years: {money(balance)}")
if __name__ == "__main__":
main()