Most backtesting frameworks do too much. They abstract away the parts you most need to understand: how positions are sized, how fills are simulated, how slippage compounds over time. When your strategy underperforms in live trading, you want to know exactly where the backtest was optimistic. If you didn't build the backtest, you can't answer that question.
This is a minimal Python tester — around 150 lines — that covers the essentials without hiding anything important.
The core loop
A backtester is a loop over price data that simulates decisions and tracks state. The state is simple: position (long, short, flat), entry price, and equity curve. The loop is simpler than most people expect:
import pandas as pd
def backtest(prices: pd.Series, signals: pd.Series, initial_capital=10_000):
"""
prices: closing prices indexed by date
signals: +1 (long), -1 (short), 0 (flat) — aligned to prices
"""
position = 0
entry_price = 0.0
capital = initial_capital
equity = []
for date, price in prices.items():
signal = signals.get(date, 0)
# Close existing position if signal flips or goes flat
if position != 0 and signal != position:
pnl = (price - entry_price) * position
capital += pnl
position = 0
# Open new position
if signal != 0 and position == 0:
position = signal
entry_price = price
# Mark-to-market equity
unrealised = (price - entry_price) * position if position else 0
equity.append({"date": date, "equity": capital + unrealised})
return pd.DataFrame(equity).set_index("date")
Metrics that matter
An equity curve is not a result. You need summary statistics that let you compare strategies on equal footing:
def metrics(equity: pd.Series, risk_free=0.0):
returns = equity.pct_change().dropna()
total_return = (equity.iloc[-1] / equity.iloc[0]) - 1
cagr = (1 + total_return) ** (252 / len(returns)) - 1
sharpe = (returns.mean() - risk_free/252) / returns.std() * (252 ** 0.5)
roll_max = equity.cummax()
drawdown = (equity - roll_max) / roll_max
max_dd = drawdown.min()
return {"total_return": total_return, "cagr": cagr,
"sharpe": sharpe, "max_drawdown": max_dd}
Sharpe ratio alone is insufficient. A strategy with a Sharpe of 1.2 and a max drawdown of 40% is not the same as one with a Sharpe of 1.0 and a max drawdown of 8%. Know what you're optimising for before you run the first backtest.
Where simple testers lie
This tester assumes zero transaction costs, perfect fills at closing prices, no slippage, and infinite liquidity. Every one of those assumptions flatters the strategy. Add realistic costs before you trust any result: even a 0.1% round-trip cost applied to a strategy that trades daily compounds to a significant drag over a year. If the strategy's edge disappears under realistic costs, the edge was the assumption, not the strategy.