Wheel Backtest
Sell cash-secured puts → assigned → sell covered calls.
The patient income trade. Sell cash-secured puts on a stock you wouldn't mind owning. If you get assigned, flip and sell calls against the shares. Premium income drips in every cycle. Time is on your side.
The Wheel is the patient income trade. You sell cash-secured puts on a stock you wouldn't mind owning. If you get assigned, you flip and sell calls against the shares. Premium income drips in either way. Time is your edge — every cycle pulls more theta out of the chain.
Cycle premium = put credit + (assigned ? call credit + ΔS : 0)
Real Wheel Backtest bot, running on real Yahoo data when the symbol is available. Drag the params — the bot re-runs instantly.
This is the actual code the bot runs — not a re-explanation, not a simplified version. Whatever ships here is what executes when you press Run All in the workbench. Read it, copy it, fork it, build a better one.
const wheelBot: BotDef = {
id: "wheel",
name: "Wheel Backtest",
category: "options",
glyph: "○",
tagline: "Sell cash-secured puts → assigned → sell covered calls.",
formula: "Cycle premium = put credit + (assigned ? call credit + ΔS : 0)",
params: [
{ key: "putDelta", label: "Put delta", kind: "number", default: 0.3, min: 0.1, max: 0.5, step: 0.05 },
{ key: "callDelta", label: "Call delta", kind: "number", default: 0.3, min: 0.1, max: 0.5, step: 0.05 },
{ key: "dte", label: "Days to expiry", kind: "number", default: 30, min: 7, max: 60, step: 1 },
{ key: "iv", label: "IV %", kind: "number", default: 32, min: 5, max: 200, step: 0.5 },
{ key: "cycles", label: "Cycles", kind: "number", default: 12, min: 1, max: 24, step: 1 },
],
run: (ctx, p): BotResult => {
const spot = ctx.candles[ctx.candles.length - 1]?.c ?? 100;
const putDel = num(p, "putDelta", 0.3);
const callDel = num(p, "callDelta", 0.3);
const dte = num(p, "dte", 30);
const iv = num(p, "iv", 32) / 100;
const cycles = Math.round(num(p, "cycles", 12));
// Approximate strikes from delta using a coarse heuristic.
const t = dte / 365;
const sigT = iv * Math.sqrt(t);
const putStrike = spot * Math.exp(-sigT * 1 - sigT * (0.5 - putDel));
const callStrike = spot * Math.exp(sigT * 1 + sigT * (callDel - 0.5));
const putPx = priceOption(spot, putStrike, t, 0.045, iv, "P").price;
const callPx = priceOption(spot, callStrike, t, 0.045, iv, "C").price;
let cash = spot * 100; // cash to secure 1 contract
let shares = 0;
let premiums = 0;
const norm = makeNorm(7);
const equity: number[] = [cash];
for (let c = 0; c < cycles; c++) {
// simulate end-of-cycle spot
const nextSpot = spot * Math.exp((-0.5 * iv * iv) * t + iv * Math.sqrt(t) * norm());
if (shares === 0) {
// sold a put
premiums += putPx * 100;
cash += putPx * 100;
if (nextSpot < putStrike) {
// assigned 100 shares at putStrike
cash -= putStrike * 100;
shares = 100;
}
} else {
// covered call
premiums += callPx * 100;
cash += callPx * 100;
if (nextSpot > callStrike) {
cash += callStrike * 100;
shares = 0;
}
}
equity.push(cash + shares * nextSpot);
}
const finalEq = equity[equity.length - 1];
const ret = (finalEq - spot * 100) / (spot * 100);
const annual = (1 + ret) ** (12 / cycles) - 1;
return {
signals: [],
metrics: [
{ key: "putK", label: "Put strike", value: `$${putStrike.toFixed(2)}`, tone: "neutral" },
{ key: "callK", label: "Call strike", value: `$${callStrike.toFixed(2)}`, tone: "neutral" },
{ key: "premiums", label: "Premiums", value: fmtMoney(premiums), tone: "bull" },
{ key: "ret", label: `${cycles}c return`, value: fmtPct(ret), tone: ret > 0 ? "bull" : "bear" },
{ key: "annual", label: "Annualised", value: fmtPct(annual), tone: annual > 0 ? "bull" : "bear" },
],
summary: `${cycles} cycles · puts @ $${putStrike.toFixed(2)} / calls @ $${callStrike.toFixed(2)} · ${fmtPct(ret)} (${fmtPct(annual)} annualised).`,
beginner:
"The Wheel is the patient income trade. You sell puts on a stock you wouldn't mind owning. If you get assigned, you flip and sell calls against the shares. Premium income drips in either way.",
verdict: {
side: ret > 0 ? "buy" : "warn",
text: ret > 0 ? `Wheel pays ${fmtPct(annual)} annualised on this seed.` : "Wheel underperforms in this scenario — try wider deltas.",
confidence: Math.min(1, Math.abs(annual) * 2),
},
equity,
};
},
};id— unique key the workbench uses to find the bot.params— the sliders + inputs you see on the cell.run(ctx, p)— the function that gets called with candles + your params and returns the verdict.verdict— the BUY / SELL / HOLD pill at the top of the cell.metrics— the small stat boxes shown in the cell body.
- Copy the whole block above.
- On /quant, click + Import your bot in the bot library.
- Paste, hit save. It hot-loads into your workspace.
- Edit any param defaults or logic to your taste — it's now yours.
- ·Sideways markets on stocks you'd actually own (large caps, dividend payers).
- ·High-IV environments. More premium per cycle.
- ·Tax-advantaged accounts (IRA). The cycle generates short-term gains that get hammered by ordinary income tax in a regular account.
- ·Strong downtrends on the underlying. You get assigned at progressively worse prices and your basis erodes faster than the call premium replenishes.
- ·Earnings windows. Vol crush after earnings can crater your sold puts even if the stock holds.
- ·Low-IV environments. The premiums get too small to be worth the capital tie-up.
BUY when annualised return > 0%. WARN when negative or marginal. The 'premiums' metric tells you total income across cycles; 'annualised' is the rate, useful for comparing against alternatives. Win rate isn't applicable — the wheel doesn't have a win/loss event, just a cumulative P&L.