Summer of Quant · Math & Physics Club, IIT Bombay

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

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.

Source & documentation Backtester, sample strategy, BTC dataset
View on GitHub