Quantitative Research · Intraday Options

A disciplined backtest that reaches an honest conclusion

A fully vectorized backtest of a daily 09:20 BankNifty short-strangle strategy over one year of minute-level option data — ~9.4M bars processed in ~18 seconds with no per-row loops. It is gross-positive, but the edge lives almost entirely on expiry day, and realistic costs likely erase it at one-lot scale. The point of the project is the method and the verdict, not a headline return.

Vectorized pandas First-passage stop-loss Look-ahead safe Hand-verified tests Cost-honest
One trading day · 09:15 → 15:30 IST
09:15 09:20 entry 15:20 exit 15:30 stop-loss scan · High ≥ 1.5 × entry, each minute
09:20 — short the CE & PE nearest ₹50  ·  scan every minute for a 50% stop  ·  15:20 — close whatever survives
~18s
Full-year runtime
9.4M bars · no loops
494
Legs traded
247 sessions · 2/day
56%
P&L from expiry
from 21% of days
−3.11%
Max drawdown
stop-capped losses
5/5
Unit tests pass
hand-computed
The Strategy

Sell time, cap the tail, go home flat

A short strangle sells a call and a put at once, collecting two premiums and profiting when the index stays range-bound and the options decay. The whole rulebook fits in three lines.

Entry — 09:20

Among all calls, pick the one whose 09:20 one-minute close is nearest ₹50; do the same for puts. Short one lot (15) of each. The ₹50 target is a model-free proxy for constant moneyness-in-vol-units.

Risk — per leg

Buy a leg back if its price hits 1.5 × entry (a 50% loss on a short), watched on each bar's High — the only in-bar evidence of the intrabar maximum.

Exit — 15:20

Any leg not stopped out is bought back at the 15:20 close. On expiry day this is usually pennies; the position is always flat overnight.

Why sub-50% wins profit

Losses cap at half the premium; expiry-day winners decay toward the full premium. Breakeven win rate is ℓ/(w+ℓ) ≈ 1/3 — the strategy can lose more often than it wins and still make money.

Results

One year, 247 sessions, gross of costs

Headline figures on a fixed one-lot book with a ₹100,000 accounting base. Every number below is reproducible from the source; nothing is tuned after the fact.

MetricValueReading
Gross P&L₹8,259on 1 lot (15 qty), no compounding
CAGR8.27%on the accounting base — a convention, not return-on-margin
Max drawdown−3.11%shallow: per-leg losses are stop-capped
Sharpe (gross)≈1.21daily mean ₹33.4 / sd ₹433.4
Win rate49.6%245 winners / 249 losers
Exit mix244 / 250stop-loss / time exits
Est. annual costs₹12–25kbrokerage + STT + exchange/SEBI/stamp

Where the money actually comes from

Expiry Wednesdays56% of gross P&L
from just 21% of trading days · ₹89/day
All other days44% of gross P&L
from 79% of trading days · ₹18.6/day

The asymmetry is exactly what theory predicts: theta decay accelerates as time-to-expiry → 0 (Θ ∝ τ−1/2), so the edge concentrates on the day the options expire.

The honest verdict

Gross-positive, almost certainly net-negative at one-lot scale. Estimated annual costs (₹12–25k) exceed the ₹8,259 gross profit, and the entire surviving economic core is expiry-day theta. So as specified, this is an existence proof of an expiry-day variance premium — not a deployable system.

The disciplined thing to do next is not to deploy it but to test it: a cost-inclusive re-run, a parameter-sensitivity sweep to confirm the result is a plateau rather than a lucky point, and an out-of-sample year in a different volatility regime.

Engineering

Vectorized, fast, and paranoid about the data

Nine-plus million rows means per-row Python loops are off the table. Every stage is a whole-table operation that runs in compiled C.

Strike selection

A single groupby(["Date","Type"]).idxmin() on premium-distance picks the nearest-₹50 option per day and side.

Stop as first-passage

Mask High ≥ 1.5·entry, sort, groupby.first() — the earliest in-window touch, τ* = inf{t > t₀ : Hₜ ≥ 1.5Pₑ}.

Fast parsing

Dates and tickers are parsed on unique values and broadcast back; pyarrow CSV engine with a pandas fallback.

Defensive loading

Auto-detects two spot schemas; drops exact duplicate rows but treats price-conflicting keys as a fatal error, never a silent keep-first.

Reproduce it

bash
$pip install -r requirements.txt
$python src/backtest.py # → 3-sheet workbook + equity/drawdown chart
$pytest -q # 5 hand-computed tests · no market data needed
Bias Audit

What the backtest controls for — and what it doesn't

The interesting part of any backtest is what it admits it isn't handling. Stated plainly.

ConcernStatusHow, or why not
Look-ahead bias✓ handledSelection uses only the 09:20 close; the stop scan starts the bar after entry (that bar's own High is pre-entry information).
Data leakage✓ handledNo future bar informs any decision; the breach is the earliest in-window touch only.
Survivorship✓ mostlyDaily universe is whatever was quoted at 09:20; no hindsight liquidity filter; last-bar fallback avoids dropping thin legs.
Transaction costs✗ excludedPer spec — but quantified (₹12–25k/yr) and shown likely to exceed gross P&L.
Slippage✗ excludedStops fill at the exact level; the direction of the bias (optimistic) is documented.
Rebalancing— n/aIntraday, flat overnight, fixed daily size — nothing to rebalance.
Corporate actions— n/aCash-settled index options; no splits or dividends on the index level.

One more caveat worth stating: the specification fixes Wednesday as expiry day, but real BankNifty weeklies expired on Thursdays until September 2023 — so the expiry flag is mislabelled for part of the year. A production version needs a date-keyed expiry calendar. Full treatment, including the derivations and a production-upgrade path, is in the methodology write-up.