Optimizer, ladder generator and simulation
Decision variables
For every candidate instrument i (issuer, type, tenor) the optimizer picks an integer number of lots k_i (default lot Rs 1,000), an on/off binary z_i, and one continuous balance L for a liquid reserve (savings account at idle_rate). Candidates are the rate card's band minimum tenors plus extra tenors that fall inside a band: each withdrawal date, each whole year and the horizon. The extra tenors let an FD mature exactly on the day money is needed.
Per-rupee coefficients (valuation.py)
Each instrument has cash flows per rupee (day, amount, interest). Interest is taxed at the effective slab rate when received (tax-free bonds are not taxed). The post-tax flow is then parked at the idle rate, taxed as well, until the horizon:
v_i = sum_j (amount_j - tax * interest_j) * (1 + idle * (1 - tax))^((H - day_j) / 365)
Model
maximise sum_i (v_i - lambda * tenor_i / 365) * k_i + v_liquid * L
subject to sum_i k_i + L = amount / lot
L >= emergency reserve (day 0)
sum_i C_i(D_m) * k_i + L >= cumulative withdrawals up to D_m + reserve for every withdrawal date D_m
sum_{i of bank b, not sovereign} k_i <= max_per_bank / lot
sum_{i of bank b, insured} g_i * k_i <= 5,00,000 * capacities / lot (if only_dicgc_insured)
k_i <= (amount / lot) * z_i, k_i >= ceil(min_amount_i / lot) * z_i
C_i(D)is the post-tax cash instrumentihas paid out by dayD. Using post-tax cash is deliberately conservative: the investor can always pay the tax out of the same pot.g_iis principal plus interest accrued at maturity per rupee (the DICGC exposure).lambdais the optionalliquidity_weight, a penalty per rupee-year of lock-in.max_per_bank_pctandmax_per_bankcombine as the tighter of the two. Sovereign paper is exempt from the per-bank cap.- The model is solved with
scipy.optimize.milp(HiGHS). BecauseLabsorbs any remainder, the only way the model is infeasible is withdrawals plus reserve exceeding the amount, which is checked up front when the plan is loaded.InfeasiblePlanremains as a guard against pathological inputs.
Worked example
Rs 10 lakh, 1-year horizon, bank A pays 7.5% and bank B 7.0%, cap Rs 4 lakh per bank. Cash after tax is higher at A, so the optimizer fills A to the cap (4,00,000), B to the cap (4,00,000) and the remaining 2,00,000 goes to the liquid reserve rather than a third bank that is not on the card. With a third bank C at 6.5% it would fill C instead of leaving cash in savings, because v_C beats the savings coefficient.
Equal-rung ladder (ladder.py)
N rungs (default 5) with tenors 365 * k days capped at the horizon, each getting (amount - reserve) / N. Each rung is filled greedily with the best post-tax instrument that covers the tenor and still has room under the per-bank cap and the DICGC limit; money that cannot be placed lands in the liquid reserve and is reported. The ladder does not know about withdrawals, so plan shows its liquidity check, which can fail where the optimizer's passes.
Reinvestment simulation (simulate.py)
compare replays each strategy day by day over the horizon:
- Cash earns the idle rate (post-tax).
- Instrument cash flows arrive; interest is taxed at slab when received (cumulative FDs at maturity or closure).
- Withdrawals are paid from cash; if cash is short, the instruments closest to maturity are closed prematurely with the card penalty. A remaining gap is reported as shortfall.
- A matured instrument is rolled into the same issuer, type and tenor at
original rate + shift, keeping back cash for the reserve and for withdrawals due before the new maturity. If the new tenor would overrun the horizon the money stays in cash.
Scenarios shift reinvestment rates once, at the first roll: flat (0), falling (-1 point) and rising (+1 point). Initial deposits keep their locked rates, so a single long FD is identical in all three scenarios. The debt fund strategy puts everything into a fund returning --debt-fund-return (default 7%) plus the shift, taxed at slab on redemption (post-April-2023 rules, no indexation), with partial redemptions taxed pro rata to the gain share.
Strategies compared: single long FD (one tenor equal to the horizon, spread to respect caps), the 5-rung ladder, the optimizer allocation, and the debt fund. IRR is computed from the invested amount, the withdrawals actually paid and the terminal post-tax value.
Why plan and compare differ slightly
plan reports the value at the horizon when each maturity is parked at the idle rate (it is a linear model, which is what lets the MILP be exact). compare rolls maturities into new deposits. Both use the same tax rules; the numbers answer different questions.
Limitations
- The optimizer's tax is slab rate times interest; the Section 80TTB deduction and TDS cash timing are applied only in the evaluation and statements.
- Rates are illustrative inputs; no live feed.
- Bank-specific day count and rounding rules differ by a few rupees.
- The simulation does not re-apply per-bank caps and DICGC limits when rolling.
- Secondary-market bond prices are not modelled; bonds are treated as bought at par and held to maturity.
- Premature closure of non-cumulative deposits, bonds and bills uses a simplified penalty formula.
- Not tax or investment advice.