A lightweight, dependency-minimal Python backtesting utility for cryptocurrency strategies. Built with zero-lookahead execution, realistic friction modeling (maker/taker fees + slippage), and institutional-grade risk metrics out of the box.
- Public Data Loader (
backtester/data_loader.py):- Load historical OHLCV data from local CSV or Parquet files.
- Fetch live or historical candlestick bars directly from Binance's public REST API (
/api/v3/klines) without requiring an API key or third-party SDK.
- Event-Driven Simulation Loop (
backtester/engine.py):- Sequential bar-by-bar state machine tracking free cash, position size, collateral, unrealized/realized PnL, and trade-by-trade execution logs.
- Supports long-only (
allow_short=False) or long/short (allow_short=True) strategies.
- Realistic Friction Modeling:
- Deducts configurable maker/taker transaction fees (e.g.,
0.05%per trade side) and applies adverse execution slippage (e.g.,0.02%/ 2 bps per fill) on every entry and exit.
- Deducts configurable maker/taker transaction fees (e.g.,
- Statistical Reporting (
backtester/metrics.py):- Total & Annualized Return (benchmarked against Buy & Hold)
- Sharpe Ratio & Sortino Ratio (annualized for 24/7 crypto markets)
- Maximum Drawdown (MDD)
- Win Rate, Profit Factor, Average Trade PnL, and Total Fees Paid
blockchainli-python-backtester/
├── README.md # Quickstart guide + explanation of lookahead prevention
├── LICENSE # MIT License
├── requirements.txt # pandas, numpy (minimal dependencies)
├── data/
│ └── sample_btc_1h.csv # Bundled 30-day 1h BTCUSDT dataset for instant zero-setup testing
├── backtester/
│ ├── __init__.py
│ ├── engine.py # Event-driven Backtester class & BacktestConfig
│ ├── metrics.py # Standalone Sharpe, Sortino, MDD, Win Rate & Profit Factor calculators
│ └── data_loader.py # CSV / Parquet / Public Binance REST fetch helper
└── run_backtest.py # Entrypoint script demonstrating the workflow
Create and activate a virtual environment:
python3 -m venv ./venv
source ./venv/bin/activateInstall the required dependencies:
python3 -m pip install -r requirements.txtpython run_backtest.pypython run_backtest.py --binance --symbol BTCUSDT --interval 1h --limit 720 --fee 0.0005 --slippage 0.0002from backtester import Backtester, load_csv
# 1. Load standardized OHLCV data
df = load_csv("data/sample_btc_1h.csv")
# 2. Define a strategy function returning target signals: 1 (Long), 0 (Flat), -1 (Short)
def dual_ema_strategy(data):
fast = data["close"].ewm(span=12, adjust=False).mean()
slow = data["close"].ewm(span=36, adjust=False).mean()
return (fast > slow).astype(int)
# 3. Configure realistic trading friction (0.05% fee + 0.02% slippage per fill)
engine = Backtester(
initial_capital=10_000.0,
fee_rate=0.0005,
slippage_rate=0.0002,
allow_short=False,
)
# 4. Run simulation and print report
result = engine.run(df, strategy=dual_ema_strategy)
print(result.summary())One of the most common pitfalls in public backtesting scripts is lookahead bias (peeking into the future):
-
The Classic Bug: A strategy calculates an indicator using bar
$t$ closing price (close[t]) and immediately executes the buy/sell order atclose[t]. In live trading,close[t]is only known at the exact instant bar$t$ finishes, at which point you can only fill at bar$t+1$ opening price (open[t+1]) or later. -
Our Execution Model:
- At bar
$t$ , your strategy evaluates indicators using data available up toclose[t]and emits a desired target state (signal[t]). - Inside
Backtester.run(), target signals are shifted forward by one bar (executable_target = signals.shift(1)). - Consequently, a signal generated at the close of bar
$t-1$ is executed at the open of bar$t$ (open[t]).
- At bar
-
Adverse Slippage & Fee Deduction:
-
Buy Fills: Executed at
Fill Price = open[t] * (1 + slippage_rate) -
Sell Fills: Executed at
Fill Price = open[t] * (1 - slippage_rate) -
Transaction Fees: Exchange fees (
Notional * fee_rate) are deducted from cash on both entry and exit.
-
Buy Fills: Executed at
Bar t-1 Close Bar t Open Bar t Close
───────────── ────────── ───────────
Strategy computes Order fills at: Position marked-to-market
signal[t-1] using ────────► open[t] * (1 ± slippage_rate) ──► at close[t] to update
data <= t-1 minus maker/taker fee bar-by-bar equity curve
Released under the MIT License.