返回资讯中心
外部精选
软件工程
#社区热议

Show HN: How long do I need to work at my salary before I can coast, or retire?

I made a financial planning tool in a couple hours that can model various stages of life and potentially guide job searching and work timelines.

Hacker Newskarmanyaahm2026/9/24 · 2026/9/2427 分钟阅读

以下正文同步自 Hacker News,版权归原站所有,已转换为易读排版。

How many years at a big salary before you can coast, retire, or retire and never shrink your nest egg.

Heads up: this is a vibe-coded FIRE tool. It was built in a conversation with Claude (an AI): someone described what they wanted, and Claude wrote the code, the tests and the design log. No financial professional has reviewed it. It is not financial advice. Use it to explore ideas, and check anything important with a real human who does this for a living.

It answers one question: how long do I need to work at a high salary before I can stop, coast, or retire?

To find out, it simulates your money day by day to age 112 in real (today's) dollars. Each day, wages arrive net of income tax, the day's expenses go out, and any surplus is invested. When there's a shortfall, it sells from the portfolio and pays the tax on that sale. Every expense is padded by a safety factor (1.1×). A bisection solver then finds the shortest stretch at the high salary (the X in your income plan) that keeps the balance at or above $0 on every single day.

Each plan is solved for three goals: retire to $0 (the money lasts until 112), coast until 60 (a job that pays your core expenses until 60), and flat from 100 (the portfolio never shrinks after 100).

uv run fire.py --plan sf_family (a shipped example: SF, one kid, a 120 → 175 → 230k ladder):

Regenerate these with uv run docs/screenshots.py (it only uses the shipped example plans).

The plans are plain Python, so Claude Code works well as the interface. Describe your life in plain English ("I make $180k, want two kids in my early 30s, might move somewhere cheaper at 35; when can I coast?") and have it write the plan in custom_plans/, run fire.py / grid.py, and put the results side by side. It's also good for questions like "what if my partner stops working?" or "does insurance really double for a couple?", for sanity-checking expense estimates, and for keeping a log of the assumptions you chose. Keep personal numbers in custom_plans/ (gitignored) so they stay out of the repo.

Requires uv. Dependencies are just rich and plotext.

uv sync # install uv run fire.py --list # plans and grids you can run uv run fire.py --plan sf_family # solve every goal: summary, what-ifs, charts uv run fire.py --plan sf_family --no-plot --no-whatif # summary and income plans only (about 1 s) uv run fire.py --plan austin_family --budget # print every expense line and exit uv run fire.py --plan sf_to_austin # SF until 35, then Austin prices and Texas taxes uv run grid.py grid_cities # sweep SF/Austin × ladders × partners × accounts

Main fire.py flags (--help for all). uv run grid.py [NAME] runs a grid (default DEFAULT_GRID).

default_plans/ holds the shipped examples. Your own plans go in custom_plans/, which is gitignored, is searched first, and wins over a same-name default.

mkdir -p custom_plans cp default_plans/sf_family.py custom_plans/my_plan.py # then edit the numbers cat > custom_plans/init.py <<'EOF' DEFAULT_PLAN = "my_plan" DEFAULT_GRID = "my_grid" EOF

custom_plans/ needs that init.py to be found. After that, a bare uv run fire.py runs your plan. A grid (e.g. custom_plans/my_grid.py) is a module with a GRID dict of axes, each {label: value}. Every combination is solved in parallel and printed as one table:

from config import NO_ACCOUNTS, TAX_ADVANTAGED from custom_plans.my_plan import PLAN from default_plans.ladders import LADDERS

GRID = { "plans": {"me": PLAN}, # required "ladders": LADDERS, # replaces plan.career "partners": {"solo": None, "+ $70k": 70_000}, # via plan.with_partner "taxes": {"CA": "CA, MFJ after wedding", "TX": "TX, MFJ after wedding"}, "accounts": {"taxable": NO_ACCOUNTS, "401k+Roth+529": TAX_ADVANTAGED}, }

SF vs Austin: taxes

From the bracket tables in config.py (2026 federal, 2025 CA; verify them): take-home pay and effective total tax rate (federal + FICA + state, incl. CA's 1.3% SDI) for wages only, one earner, standard deduction, no 401(k). "MFJ" means a $0-income spouse (the best case, as in married() in config.py).

The marginal rate on the next dollar of wages (C = CA single, t = TX single). The step down near $185k is the Social Security wage base. Above $200k, California adds roughly 10 points.

Total marginal rate (fed + FICA + state), single, one earner ┌────────────────────────────────────────────────────────────────────────────┐ 50┤ CC CA single CCCCCCCCCCCCCCCCCCCC│ │ tt TX single CCCCCCCCCCCCCCCC │ │ CCCCCCCC │ 40┤ CCCCCCCCCCCCCCCCC C │ │ CCC C CCC ttttttttttttttttttttttttttttttttttt│ │ C CCC tttttttt │ │ C tttttttttt t │ 30┤ ttttttttt t t │ │ CCCCCt tttttt │ │ C t │ 20┤ CCtttttt │ │ tt │ │ t │ 10┤CCt │ │ttt │ └──────────────┬───────────────┬──────────────┬──────────────┬──────────────┬┘ 0 100 200 300 400 500 gross wages ($k)

Holding SF prices fixed and swapping only the tax setup (uv run grid.py grid_taxes) shows what the tax code alone is worth. Cells are the stop age (portfolio when you stop):

Illustrative, rough 2026 SF prices, not anyone's real budget: sf_family is your share of a 2BR, no car, a $60k wedding at 28 and one kid at 33. Per year while at the career job, before the ×1.1 safety factor:

Other lines switch on later: an individual health plan from RETIRE to 65 (stepping up at 50 and 58), weekday groceries once the office stops feeding you, Medicare at 65, late-life care $3,000/mo from 88, UC at $45k/yr.

austin_family reuses all of this and overrides only the lines in its AUSTIN dict (rent $950, daycare $1,650, UT Austin instead of UC, cheaper health insurance, ...). It adds paid preschool at 4 (no free TK in Texas) and extra rideshare with kids, and uses TX, MFJ after wedding. That's about $45k/yr at 25.

uv run grid.py grid_profiles: different lives, same "big tech 120 → 175 → 230×X" ladder (in $k/yr):

uv run grid.py grid_cities, taxable rows only. The partner earns 55k, then 90k from 32, and puts half of their take-home into the shared pool:

The full grid's 401k+Roth+529 rows stop 0.1–1.2 years earlier than these, most in SF (the 401(k) saves CA tax too).

fire.py: the CLI (tables, charts, flags). grid.py: the sweep CLI; its docstring documents the axes. tests/: the pytest suite. DESIGN.md: the design log.

config.py: markets, taxes and safety settings. It has the federal/FICA/CA bracket tables, TAX_VARIANTS (CA/TX single or MFJ after the wedding, IL/CO MFJ; each a function of the plan), TAX_ADVANTAGED (2026 limits: 401(k) $24,500, Roth $7,500, 529 $16,000), real growth (5% until 60, then 4%), END_AGE, EXPENSE_SAFETY_FACTOR, the what-if grid, and make_config(plan, taxes, accounts).

default_plans/ (shipped examples): sf_family, austin_family (the override pattern), single_lean, three_kids, couple_with_car, sf_to_austin (a move at 35); ladders.py (LADDERS) and partners.py (PARTNERS) for grids; the grids grid_cities, grid_taxes, grid_profiles, grid_ladders; init.py sets DEFAULT_PLAN / DEFAULT_GRID.

custom_plans/ (gitignored): your plans and grids, same layout, searched first.

docs/: the README screenshots (SVG) and screenshots.py, which regenerates them.

firemodel/: the engine. schema.py (dataclasses, X/RETIRE, periods, Accounts), plan.py (LifePlan, coast salary, with_partner), tax.py (brackets, regime()), sim.py (the day-by-day simulation, accounts), solve.py (bisection, what-if, sweep), grid.py (parallel solving), loader.py (finds plans and grids).

How to customize

Expenses: Expense(name, amount, per, start, end=None, every=1, category="").

per is DAY, WEEK, MONTH, YEAR or ONCE (charged once, at start).

Ages are half-open [start, end). end=None means forever. every=10 charges it every 10 years (a car).

start or end can be RETIRE, "the day you leave the high salary": employer health insurance runs until RETIRE, and an individual plan from RETIRE to 65.

The categories kids, events and big-ticket are left out of the coast salary.

Income ladders: Income(amount, years=… | until=…, start=…) segments, laid end to end. Exactly one holds X: years=X solves for how long, amount=X (e.g. Income(X, years=12)) solves for the salary. career=[Income(120_000, years=3), Income(175_000, years=2), Income(230_000, years=X)]

alt_careers={"tag": [...]} runs extra income paths against the same spending.

Taxes: set plan.taxes to a TAX_VARIANTS name. To add one, write a function of the plan that returns regime(start_age, end_age, federal, fica, state, joint=...) segments. flat_state(rate) gives a flat-rate state.

Moving cities: SF.move_to(AUSTIN, 35) lives the first plan until 35 and the second after it. Each plan's expense lines are clipped at the move, and the tax setup switches too; career, money and events before the move come from the first plan. See default_plans/sf_to_austin.py. Compare a few move ages with a grid over plans.

Partner: plan.with_partner(70_000) (a flat salary, worked 25–55) or with_partner([Income(...), ...]). By default 50% of their take-home goes into the shared pool, and their own spending comes from the rest. For a fully joint household, pass share=1.0 and couple={line name or category: factor}: what each line costs for two adults vs one (shared rent 1.0, groceries 1.5, health insurance 2.0, ...), from together_from. Every line must be covered. Their income is on the joint return after the wedding.

Accounts are on by default (config.ACCOUNTS); --no-accounts, or the accounts grid axis, gives one taxable brokerage account. The household has one 401(k) and one Roth: each working earner (you, and a partner whose pay is pooled) adds their own yearly limit. Each account is funded only up to a need it can match. The 401(k) (then Roth) gets just what the plan's after-59½ spending needs, computed backward and frontloaded; the 529 gets the present value of the "college" expense lines, which it pays tax-free. Contributions only leave taxable while it can still cover every future need that happens before 59½ (the wedding, the bridge years, ...). Taxable pays before 59½; after it, 401(k) → Roth → taxable. Early draws pay +10%. See DESIGN.md D74.

Safety factor: EXPENSE_SAFETY_FACTOR = 1.1 in config.py. It's the biggest lever in the model.

Key assumptions and limitations

See DESIGN.md §2 (decisions) and §3 (tabled work) for the full list.

PYTHONPATH= uv run pytest -q

The PYTHONPATH= prefix clears a ROS install from PYTHONPATH. Without it, pytest loads a ROS pytest plugin and fails. The ~190 tests (a few seconds) mostly use tiny closed-form scenarios you can check by hand: zero tax, 0% growth, amounts in multiples of $365. A smoke test also checks that each shipped plan solves.

DESIGN.md is the full log: every request, every decision Claude made (and why), and the coding process step by step.

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →