Lesson 6 — Backtesting & analysis (Jupyter)
Use Jupyter to replay historical market data, simulate order execution and compute P/L and performance metrics. We’ll build small, testable backtest components you can evolve into more advanced systems.
Prerequisites
- Lessons 1–5 completed (env, auth, market lookup, orders, webhooks)
- Python environment with Jupyter / JupyterLab and pandas installed
- Historical market data (CSV/Parquet) or a delayed API data dump
Overview
We’ll cover: loading tick/candlestick data, a simple event-driven backtester loop, execution simulation (slippage/partial fills), logging trades, computing P/L and common metrics (win rate, expectancy, Sharpe-like ratio).
1) Jupyter setup
Install Jupyter and essentials if you haven’t already:
# install Jupyter and common libs
pip install jupyterlab pandas numpy matplotlib
Open a notebook: jupyter lab or jupyter notebook and create a new Python 3 notebook for the backtest.
2) Load historical data (example)
Load market ticks or candles into a pandas DataFrame. Use Parquet when possible for performance.
import pandas as pd
# CSV example: columns: timestamp, marketId, selectionId, price, size
df = pd.read_csv('market_ticks.csv', parse_dates=['timestamp'])
df = df.sort_values(by='timestamp')
df.head()
Tip: Normalize column names and data types; store microsecond timestamps if you need finer replay granularity.
3) Simple event-driven backtest loop
Below is a minimal backtester that replays rows and calls a strategy decide() function for signals. Implement your own strategy object with on_tick / on_bar methods.
from collections import deque
class BacktestEngine:
def __init__(self, data, strategy, starting_cash=100.0):
self.data = data # pandas DataFrame iterator
self.strategy = strategy
self.cash = starting_cash
self.positions = {} # key: selectionId -> position size/avg price
self.trades = [] # list of trade dicts
def run(self):
for _, row in self.data.iterrows():
# convert row to a tick object or dict
tick = row.to_dict()
# strategy inspects tick and may return orders
orders = self.strategy.on_tick(tick)
for order in orders or []:
# simulate execution
exec_report = self.execute(order, tick)
self.trades.append(exec_report)
# update P/L / positions
self._apply_exec(exec_report)
return self.trades
def execute(self, order, tick):
# naive execution: if price crosses desired price, fill; else partial or no-fill
executed = {'order': order, 'filled_size': 0.0, 'avg_price': None, 'status': 'NO_FILL'}
# Example simplistic fill logic (replace with more realistic sim)
market_price = tick.get('price')
if order['side'] == 'BACK' and market_price <= order['price']:
executed['filled_size'] = order['size']
executed['avg_price'] = order['price']
executed['status'] = 'FILLED'
elif order['side'] == 'LAY' and market_price >= order['price']:
executed['filled_size'] = order['size']
executed['avg_price'] = order['price']
executed['status'] = 'FILLED'
return executed
def _apply_exec(self, exec_report):
# update positions/cash
order = exec_report['order']
size = exec_report['filled_size']
price = exec_report['avg_price']
if size and price:
side = order['side']
if side == 'BACK':
# buy exposure
self.positions.setdefault(order['selectionId'], {'size':0.0,'avg_price':0.0})
pos = self.positions[order['selectionId']]
new_size = pos['size'] + size
pos['avg_price'] = ((pos['avg_price']*pos['size']) + price*size)/new_size if pos['size'] else price
pos['size'] = new_size
self.cash -= price * size
else: # LAY simulation (simplified)
# LAY increases liability; track separately in real systems
pass
Note: This engine is intentionally minimal. Replace execution logic with slippage models, partial fills and available liquidity limits for more fidelity.
4) Example SMA strategy (notebook cell)
Compute moving averages in the notebook and generate simple cross signals.
import pandas as pd
# df: DataFrame with 'timestamp' and 'price' columns
df['ma_short'] = df['price'].rolling(window=5).mean()
df['ma_long'] = df['price'].rolling(window=20).mean()
class SMAStrategy:
def __init__(self):
self.last_signal = None
def on_tick(self, tick):
# In notebook, you may use row-by-row iteration
if tick.get('ma_short') and tick.get('ma_long'):
if tick['ma_short'] > tick['ma_long'] and self.last_signal != 'BUY':
self.last_signal = 'BUY'
return [{'selectionId': tick['selectionId'], 'side':'BACK', 'size':1.0, 'price': tick['price']}]
elif tick['ma_short'] < tick['ma_long'] and self.last_signal != 'SELL':
self.last_signal = 'SELL'
return [{'selectionId': tick['selectionId'], 'side':'LAY', 'size':1.0, 'price': tick['price']}]
return []
This strategy is intentionally simple — use it to validate your pipeline, then add risk sizing, stop logic and realistic fees.
5) Compute P/L and performance metrics
After running trades, compute basic metrics: total P/L, number of trades, win rate, average P/L per trade, max drawdown.
import pandas as pd
import numpy as np
# trades: list of exec reports with filled_size, avg_price, order side, timestamp
trades_df = pd.DataFrame(trades)
# Example P/L computation (simplified)
def compute_pnl(trades_df):
pnl_list = []
for _, t in trades_df.iterrows():
if t['order']['side'] == 'BACK':
pnl = (t.get('exit_price', t['avg_price']) - t['avg_price']) * t['filled_size']
else:
pnl = (t['avg_price'] - t.get('exit_price', t['avg_price'])) * t['filled_size']
pnl_list.append(pnl)
pnl_series = pd.Series(pnl_list)
total_pnl = pnl_series.sum()
win_rate = (pnl_series > 0).mean()
avg_pnl = pnl_series.mean()
return {'total_pnl': total_pnl, 'win_rate': win_rate, 'avg_pnl': avg_pnl}
metrics = compute_pnl(trades_df)
Tip: Include fees/commission/slippage in the P/L calculation. Keep detailed logs to reproduce any surprising results.
6) Replay fidelity & common pitfalls
- Tick vs candle resolution: candles hide intra-bar price moves — prefer tick-level data for execution-sensitive strategies.
- Survivorship bias: ensure event lists reflect the historical universe at the time, not the current one.
- Lookahead bias: don’t use future information in your decision logic (e.g., next-bar close to decide current order).
- Transaction costs: include commission, exchange fees and latency slippage models.
What you'll build next
Lesson 7 will focus on risk, hedging and deployment: green‑up hedges, idempotent deployment patterns, logging and Docker/VPS deployment guidance.
