An event-driven backtesting engine for trying out trading strategies against historical OHLCV data. You feed it a strategy and a CSV/Parquet file of bars, and it replays that data bar-by-bar through a strategy, a risk manager, and a simulated execution layer, tracking a portfolio's cash, positions, and equity along the way.
This started as a follow-up to an earlier project of mine, a limit order book matching engine. After building that, I got curious about the other side of the problem: given a matching/execution layer, how do you actually evaluate whether a trading strategy is any good before risking real money on it? That question is what backtesting engines are for, so I read up on how event-driven backtesters are usually structured, picked a stack (Python, pandas), and built this as a personal project to understand the pieces myself.
It's not a research-grade or production-ready system, there's no live trading, no real broker integration, and the execution model (fixed slippage in basis points, flat per-share commission) is a simplification, not a faithful market microstructure model. It's a working, tested implementation of the classic architecture, built to learn how the pieces fit together.
Everything flows through a single event loop, one historical bar at a time:
flowchart LR
Feed["Data Feed\n(CSV / Parquet)"] -- MarketEvent --> Strategy
Strategy -- SignalEvent --> Risk["Risk Manager"]
Risk -- OrderEvent --> Exec["Execution Simulator"]
Exec -- FillEvent --> Portfolio
Feed -- MarketEvent --> Portfolio
Portfolio --> Analytics["Performance Analytics"]
- Data Feed reads OHLCV bars from a CSV or Parquet file and emits them in order as
MarketEvents. - Strategy looks at each bar and decides whether to go long or exit, emitting a
SignalEvent. - Risk Manager turns a signal into a sized
OrderEvent, capping position size and risking a fixed fraction of cash per trade. - Execution Simulator "fills" the order against the bar's price, applying slippage and commission, producing a
FillEvent. - Portfolio applies fills to update cash and positions, and marks the position to the latest close every bar to build an equity curve.
- Performance Analytics computes Sharpe ratio, max drawdown, total return, and win rate from that equity curve, once the run is done.
Here's the same loop for a single bar, in more detail:
sequenceDiagram
participant Feed as Data Feed
participant Engine
participant Strategy
participant Risk as Risk Manager
participant Exec as Execution Simulator
participant Port as Portfolio
Engine->>Feed: update_bars()
Feed-->>Engine: MarketEvent
Engine->>Port: update_market_value(event)
Engine->>Strategy: on_market_event(event)
Strategy-->>Engine: SignalEvent(s)
loop for each signal
Engine->>Risk: size_order(signal, cash, price)
Risk-->>Engine: OrderEvent
Engine->>Exec: execute_order(order, price)
Exec-->>Engine: FillEvent
Engine->>Port: update_fill(fill)
end
- Moving average crossover — goes long when a short moving average crosses above a longer one, exits on the reverse cross.
- Mean reversion — goes long when price drops well below its rolling mean (by a z-score threshold), exits once it reverts.
- VWAP — goes long when price trades meaningfully below the volume-weighted average price, exits once it moves back above it.
- TWAP — the same idea as VWAP, but against a plain time-weighted average instead of a volume-weighted one.
Each one implements the same small Strategy interface, so writing a new one just means implementing on_market_event(event) -> list[SignalEvent].
pip install -r requirements.txt
PYTHONPATH=. python examples/run_backtest.pyThat runs a moving average crossover strategy over the sample data in examples/data/sample_ohlcv.csv and prints final equity, total return, Sharpe ratio, max drawdown, and win rate.
To wire things up yourself:
from backtester.data.csv_feed import CSVDataFeed
from backtester.strategy.moving_average import MovingAverageCrossoverStrategy
from backtester.portfolio.portfolio import Portfolio
from backtester.risk.risk_manager import RiskManager
from backtester.execution.simulator import ExecutionSimulator
from backtester.engine import BacktestEngine
from backtester.analytics.performance import sharpe_ratio, max_drawdown
engine = BacktestEngine(
data_handler=CSVDataFeed("path/to/data.csv", symbol="AAPL"),
strategy=MovingAverageCrossoverStrategy(short_window=10, long_window=50),
portfolio=Portfolio(initial_cash=100_000),
risk_manager=RiskManager(max_position_size=100, risk_per_trade=0.02),
execution_simulator=ExecutionSimulator(commission_per_share=0.005, slippage_bps=5.0),
)
portfolio = engine.run()
print(portfolio.total_equity())
print(sharpe_ratio(portfolio.equity_curve))
print(max_drawdown(portfolio.equity_curve))python -m pytest tests/ -qbacktester/
events.py # MarketEvent, SignalEvent, OrderEvent, FillEvent
data/ # CSV and Parquet feeds
strategy/ # Strategy interface + moving average, mean reversion, VWAP, TWAP
risk/ # position sizing
execution/ # slippage/commission fill simulation
portfolio/ # cash, positions, equity curve
analytics/ # Sharpe, drawdown, total return, win rate
engine.py # wires it all together
examples/
run_backtest.py # runnable example against sample data
tests/ # one test file per module above