Quantitative Trading Backtester
A Python framework that takes a stream of trading signals (with optional Take-Profit and Stop-Loss levels) and a master OHLCV dataset, simulates the trades bar-by-bar, and reports a dozen metrics covering profitability, risk, and consistency. Built during the Summer of Quant cohort and used in a strategy that placed in the Top 10.
- Timeline
- Jun – Jul 2025 · Summer of Quant, Math & Physics Club, IIT Bombay
- Result
- Top 10 performer · Certificate of Excellence
- Language
- Python 3
- Stack
- pandas · pandas_ta · numpy · plotly · matplotlib
- Core
backtester.py(~197 LOC) —TradeType,Position,TradePair,BackTester- Capital
- $1,000 initial · 0.15% brokerage per leg · compounding or fixed-stake (
compound_flag) - Dataset
- BTC daily candles, Sept 2019 – 2023 (~1,600 bars)
- Repo
- github.com/Y-Harsha-Vardhan/SoQ-Project
What it does
Input is two CSVs: signals (timestamp, direction, optional
TP/SL) and a master OHLCV reference — for this project, daily
Bitcoin candles from Sept 2019 through 2023. The BackTester class
walks the signal stream, opens and closes Position objects, checks for
intra-bar TP/SL triggers, and collects each completed round-trip as a
TradePair. From those it computes everything from net PnL to
max drawdown to consecutive-loss streaks.
The interesting parts
1. Intra-bar TP/SL execution
Most beginner backtests fill at the candle close, which silently inflates results
because TP and SL targets are often hit somewhere inside the bar, not at
its end. check_tp_sl() walks the master data within each bar and closes
the position at the actual trigger price — high ≥ TP for longs, low ≤ TP for shorts.
That single change is the difference between a backtest that looks great and one
that's actually defensible.
2. Position-reversal signals
The signal vocabulary is {1, −1, 2, −2, 0} — open long, open short,
close-and-reverse-to-long, close-and-reverse-to-short, hold. A single bar can flip
a long into a short atomically, avoiding the gap-risk ambiguity of issuing a close
followed by a new open across two bars.
3. Lookahead-bias guard
A signal generated on the close of bar i is always executed at the
open of bar i+1 — never at the same bar's close. That single
rule makes the whole simulation honest: the strategy can never trade on a price it
wouldn't have seen yet in production. Easy to write, easy to forget, painful to
debug after the fact.
4. Realistic costs
Every trade pays a 0.15% transaction cost in the PnL formula. Small per-trade, big impact on the Sharpe number — and a reminder that "this strategy works" usually means "this strategy works after realistic frictions."
Strategy on top
The bundled strat() in main.py is a volume-spike trend
follower: a long entry on a 1.5σ-above-mean volume spike with a green close
(short on red), trailing stop at close ± 2×ATR, exit on three consecutive adverse
closes or a stop breach. Nothing exotic — the backtester was the point.
Metrics reported
- Net Profit, Win Rate, Largest Win / Loss
- Sharpe Ratio (annualised), Max & Average Drawdown
- Max / Average Holding Time, Consecutive Win/Loss Streaks
- Benchmark return (buy-and-hold)
Visualised with Plotly: a candlestick chart with green/red trade regions, and a dual-axis plot of capital evolution against the underlying price.
What I'd carry forward
The lesson wasn't the strategy — it was that most of what makes a backtest trustworthy is friction: intra-bar fills, realistic costs, lookahead checks. Without those, every chart slopes up and to the right.