Systematic Trading Backtester

A backtesting engine built with NumPy: momentum, mean-reversion, and pairs/cointegration strategies, with transaction-cost modeling, walk-forward validation, and an overfitting-detection parameter sweep. This includes from-scratch cointegration testing for pairs trading, along with explicit gauges of lookahead bias, transaction costs, and overfitting for realistic strategy assessment. Data comes from Yahoo Finance.

Initializing…

Strategy & parameters

560
0.53.0
0.01.0

Transaction costs

010
020
Benchmark: buy & hold SPY

Cointegration test (Engle-Granger)

Pick any two series. We fit an OLS hedge ratio and test whether the residual spread is stationary with an Augmented Dickey-Fuller test, which indicates whether the pair is cointegrated.

20120
Try EWA/EWC (cointegrated) vs. KO/PEP (no longer).

Walk-forward validation

Finds optimized parameters for each in-sample window, then judged on the out-of-sample window that follows. Use the gap between in-sample and out-of-sample Sharpe to assess how much the strategy was overfit.

26

Parameter sweep

Each cell is the Sharpe ratio for one parameter combination. A sharp peak in-sample that flattens or vanishes out-of-sample is a sign of overfitting.

0.500.85

Limitations

This backtester makes a number of simplifying assumptions, partially due to design decisions made to keep it fast and runnable entirely in-browser.

Signal Timing

The engine processes one trading day at a time and only ever looks at information available up through that day's close (it can't peek ahead). Because a day's closing price isn't known until the day is over, any order a signal generates at close[i] is filled at the next day's open (open[i+1]) rather than at that same close, so the backtest can never trade at a price it couldn't actually have gotten in real time.

for each bar i: 1. fill orders queued for open[i] (placed yesterday) 2. mark portfolio equity at close[i] 3. strategy reads history up to i -> target weights 4. size orders vs. current positions -> queue for open[i+1]

Other Limitations

Daily bars. Every decision is made once per day at the close and executed once per day at the next open, so any strategy that depends on intraday timing can't be represented here.

Simplified cost and fill model. I aimed to approximate trading frictions with commission and half-spread. None of this models order-book depth, queue position, or adverse selection from other participants, so costs here are just an estimate.

Small universe. The panel is 18 series and 3 notable pairs (SPY, sector ETFs, and cointegration candidates like EWA/EWC). This is of course a narrower scope than the entire market.

Adjusted-price accounting only. Prices are split/dividend-adjusted via yfinance, which is standard for return calculations but means the backtest doesn't model things a live account actually pays or earns (dividend cash flows, financing/borrow costs for short positions, taxes, etc.).

Only 2018-26 real data. All results are only computed over the history of the series from 2018-26. This is partially just the nature of backtesting, but a different time interval could yield different results. Also, for simplicity and speed, the walk-forward and sweep analyses are coarse and aren't an exhaustive optimization.