The Roth Conversion Math: A Linear Program

This is the “show your work” post in the series. If you came for the FIRE strategy you can happily skip it; if you want the Roth conversion optimization written out as an actual linear program — decision variables, constraints, objective — this is for you. It mirrors the Excel + OpenSolver model I described in part 2.

This is a 4-part series on optimizing IRA-to-Roth conversions in early retirement:

The model is a multi-year plan. Each row is one year, from your current age out to the end of the IRS RMD table. Everything below is “per year” unless I say otherwise.

Decision variables

There are two sets of things the solver gets to choose each year:

  • How much to convert from the IRA, split across the ordinary-income tax brackets. In the spreadsheet this is one variable per bracket per year (the 0%/standard-deduction band, then 10%, 12%, 22%, 24%, 32%, 35%, 37%). Splitting by bracket is what lets the objective “see” the marginal rate on each slice.
  • How much long-term capital gain to realize in the taxable account, also split across the capital-gains brackets (0% / 15% / 20%). This lets the model harvest gains in cheap years.

All of these are continuous and non-negative, which is what keeps the whole thing a linear program rather than something nastier.

Constraints

The constraints are where the tax code and reality get encoded:

  • Bracket capacity. In each year, the total income landing in a bracket (other income + conversions + realized gains) can’t exceed that bracket’s width. Fill a bracket and the next dollar spills into the one above — exactly how progressive rates work.
  • RMD floor. Once RMDs begin, the total pulled out of the IRA that year must be at least the required minimum distribution (balance ÷ the IRS life-expectancy factor).
  • Non-negative IRA balance. You can’t convert or withdraw more than you have; the IRA balance has to stay at or above zero every year.
  • Fund your life. Each year the plan has to cover living expenses from some combination of income, realized gains, and withdrawals.
  • Account bookkeeping. Balances roll forward year to year: grow at the expected return, subtract what comes out, and the converted money lands in the Roth.

The objective — the part I keep going back and forth on

Full disclosure: I’ve changed this objective several times across versions of the spreadsheet, and I’m still not completely settled. There are three candidates, and each has a bias worth understanding.

  • Minimize the present value of total tax paid. This is what my current file does, and honestly it’s a reasonable choice — it’s simple, interpretable, and it’s what a lot of conversion calculators optimize. The catch is that, on its own, minimizing tax doesn’t credit the tax-free growth you get by moving money into the Roth early; and once you discount future taxes, it can even lean toward paying later, which means converting less now.
  • Maximize the ending Roth balance. This is what I thought I was doing. It has the opposite bias: it will happily pay a 37% rate to cram one more dollar into the Roth, because a bigger Roth always scores higher — even when that’s a bad trade.
  • Maximize total after-tax wealth at the horizon — the Roth, plus the taxable account, plus the traditional IRA net of the tax still owed on it, all discounted to today. This is the theoretically clean one: it values early tax-free growth without pretending the Roth is the only account that matters.

Here’s where I’ve landed for now, and it’s a bit of a both/and. If I add a constraint that draws the IRA down to a target by a certain age, then the total amount I’ll convert is essentially pinned, and the only thing left to optimize is the timing and how I pack the brackets. For that job, minimizing the present value of tax is exactly right — and simpler to explain. If instead I leave the total amount to convert free, minimizing tax under-converts and I’d switch to maximizing after-tax wealth. Same model, two honest objectives, depending on whether the constraints already decide how much comes out of the IRA. That’s the call I’m still making for the next version.

The model, written as a linear program

Here is the same model written out as a linear program. It’s a faithful statement of the spreadsheet’s logic, lightly simplified — the live file approximates the discounting and carries a few extra bookkeeping rows, but this is the heart of it.

Indices
  t = 0 … T      years, from your start age to your end age (≤ 120)
  b = 0 … 7      ordinary-income brackets (0% std deduction, 10, 12, 22, 24, 32, 35, 37%)
  g = 0, 1, 2    long-term capital-gains bands (0, 15, 20%)

Data
  r[b], W[b]     rate and width of ordinary bracket b
  ρ[g], Ω[t,g]   rate and remaining room of LTCG band g in year t
  o[t,b]         your other income already filling bracket b in year t
  y[t]           total other income in year t   (= Σ_b o[t,b])
  e[t]           living expenses in year t
  π              expected (real) return
  δ              discount rate
  f[t]           IRS life-expectancy factor used for the RMD
  B0, R0         starting traditional-IRA and Roth balances

Decision variables   (all ≥ 0)
  c[t,b]         IRA dollars converted in year t, taxed in bracket b
  h[t,g]         long-term gains realized in year t, taxed in band g
                 C[t] = Σ_b c[t,b]   is the total converted in year t

Objective
  minimize   Σ_(t=0..T)   Tax[t] / (1 + δ)^t

        where   Tax[t] = Σ_b r[b]·( o[t,b] + c[t,b] )  +  Σ_g ρ[g]·h[t,g]

Subject to, for every year t:
  (1)  o[t,b] + c[t,b]  ≤  W[b]            fill a bracket before spilling up (top bracket uncapped)
  (2)  h[t,g]  ≤  Ω[t,g]                   realized gains fit their bands
  (3)  C[t]  ≥  B[t] / f[t]                take at least the RMD, once RMDs start
  (4)  B[t]  =  ( B[t-1] − C[t-1] )·(1+π)   IRA rolls forward,  B[t] ≥ 0,  B[0] = B0
  (5)  R[t]  =  R[t-1]·(1+π) + C[t-1]       Roth rolls forward,  R[0] = R0
  (6)  y[t] + C[t] + Σ_g h[t,g] − Tax[t]  ≥  e[t]    the year has to fund your life
  (7)  c[t,b] ≥ 0,   h[t,g] ≥ 0

Constraint (1) is the progressive-bracket rule; (3) is the RMD floor; (4) keeps you from converting money you don’t have; and (6) is what forces the plan to actually pay for your retirement rather than just chase a low tax bill. Swap the objective line for “maximize after-tax wealth at year T” and everything else stays the same — which is exactly why this is easy to change later.

Why it stays linear

Everything above — bracket splits, RMD floors, balance roll-forwards, the objective — is linear in the decision variables. That means a standard LP/MILP solver handles it quickly. The Excel version uses OpenSolver with the CBC engine; because it’s a small, clean linear model, it ports almost directly to Python with PuLP — which is exactly the plan for the web tool.

This is the third optimization model I’m walking through publicly. If you like this kind of thing, the earlier two — a staff scheduler and a vehicle-routing solver — went from Excel to live web apps the same way, and I wrote up that whole journey in the OR-tools series on this blog.

One disclaimer, because this is money and taxes: I am an operations research analyst, not a CPA or a financial advisor, and nothing here is tax or investment advice. This is just how I think about my own situation. Tax rules change and your situation is different from mine, so check the primary sources and talk to a professional before you actually move money.

Tradeline Supply
Things that I use, like, and am affiliated with:
Mint Mobile offers great cell phone service for $15 flat, get $15 off using the link. Get discounted phones with service activation and no contract.
I never spend money before I check Mr Rebates or Rakuten to get cashbacks, rebates, discounts, coupons or cheaper gift cards.

Leave a Reply