Having Claude Code produce this as I've been idly interested in it on and off so this is a chance to whip something up. I also saw Entire drop which seems to be $60M wrapped in a bunch of commit hooks so thought it might be interesting to check out.
Still a work in progress as interest waxes and wanes. Don't expect to get rich but OTOH who knows?
Requires Python 3.12+.
uv syncThat installs all dependencies and the backtest package itself as an editable install.
# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Install as editable package (includes all dependencies)
pip install -e .uv run python trade_analysis.py SPYThis is the primary entry point. For the given ticker it:
- Downloads OHLCV history (2020 to present, adjusted prices).
- Runs every built-in strategy over the full period for a descriptive comparison (this table is in-sample and used only for context — it does not gate anything).
- Runs Walk-Forward Analysis on every strategy with a parameter grid defined in
WFA_PARAM_GRIDS(14 at the time of writing). Strategies are ranked by their WFA out-of-sample Sharpe ratio, not the in-sample comparison. - For each candidate, also backtests its fixed
best_params_overallover the same out-of-sample span, so the card records how the deployed parameter set performed — the WFA summary itself is earned by adaptive per-window parameters. - Writes
output/<ticker>_report.html(full report) andoutput/<ticker>_card.json(the machine-readable strategy card consumed by the daily cron / reconciler).
Run settings live as constants at the top of the script:
| Constant | Default | Meaning |
|---|---|---|
START_CAPITAL |
10,000 | Starting capital for every simulation |
COMMISSION_FIXED |
3.0 | Flat USD fee charged per fill (each buy and each sell) |
COMMISSION_RATE |
0.0 | Percentage-of-value commission per fill |
SLIPPAGE_PCT |
0.0005 | 5 bps per fill: buys fill above the open, sells below |
MIN_TRADES |
3 | Training windows with fewer trades score -inf in WFA |
Costs are configured on PortfolioConfig and applied by the runner to every simulation
(including WFA training and test windows, and parallel workers):
from backtest.config import ConfigFactory, set_config
config = ConfigFactory.create_default()
config.portfolio.commission_fixed = 3.0 # flat fee per fill
config.portfolio.commission_rate = 0.0 # plus a percentage of trade value
config.portfolio.slippage_pct = 0.0005 # buys fill higher, sells fill lower
set_config(config)Position sizing accounts for costs (shares are sized so cost + commission fits the
budget), and trade pnl is net of both sides' commissions.
python examples/basic_backtest.pyRSI(14) vs Buy & Hold, SPY, and DCA on AAPL over 5 years. Prints a metric summary and final portfolio value.
python examples/compare_strategies.py # defaults: AAPL 2020–2024
python examples/compare_strategies.py MSFT 2018-01-01 2024-01-01Runs every built-in strategy on the same data and prints a table ranked by total return.
python examples/parameter_sensitivity.py
python examples/parameter_sensitivity.py SPY 2018-01-01 2024-01-01Sweeps RSI period from 5 to 30, printing Sharpe, Sortino, Calmar, and trade count at each value. Helps identify stable parameter regions before optimising.
python examples/walk_forward_analysis.py
python examples/walk_forward_analysis.py SPY 2016-01-01 2024-01-01
python examples/walk_forward_analysis.py AAPL 2016-01-01 2024-01-01 -1 # all CPUsWFA repeatedly optimises RSI parameters on a 1-year training window and validates on the
following 3-month test window. Prints per-window results and overall out-of-sample metrics.
Saves three plots to output/: chained equity curve, parameter stability, and per-window
metric bars. Pass a 4th argument to control parallel candidate evaluation (-1 = all CPUs,
2 = 2 workers, 1 = sequential).
python examples/ensemble_strategy.py
python examples/ensemble_strategy.py MSFT 2019-01-01 2024-01-01Combines RSI, MACD, and Bollinger Bands into an ensemble that signals only when at least 2 of 3 strategies agree. Compares the ensemble against each constituent and Buy & Hold.
uv run python examples/multi_asset_portfolio.pyRuns two pre-optimised strategies on separate assets (SPY breakout, COIN MACD) and blends their equity curves into a capital-weighted portfolio. Illustrative of the blending mechanics only — it predates the cost model and uses fixed in-sample parameters.
The following metrics can be passed as the objective parameter to WalkForwardOptimizer,
or called directly from backtest.metrics:
from backtest.metrics import METRICS
print(list(METRICS.keys()))
# ['total_return', 'cagr', 'sharpe_ratio', 'sortino_ratio', 'calmar_ratio',
# 'max_drawdown', 'ulcer_index', 'profit_factor', 'win_rate', 'expectancy',
# 'recovery_factor']You can also pass any callable with the signature (portfolio_history, trades) -> float
as a custom objective.
Metric conventions worth knowing:
- Sharpe and Sortino measure excess return over the configured annual
risk_free_rate(BacktestConfig, default 2%), applied asrf / 252daily. - Degenerate windows return
-inf: zero-volatility Sharpe, or Sortino with no negative returns, can never win the optimizer. - Higher is always better; drawdown-style metrics are negated accordingly.
| Class | Description |
|---|---|
ConsecutiveDaysStrategy |
Buy after N consecutive down days, sell after N consecutive up days |
MovingAverageCrossoverStrategy |
Golden/death cross on short- and long-term moving average crossovers |
RSIStrategy |
Buy when RSI is oversold, sell when overbought |
MACDStrategy |
Buy/sell on MACD line crossovers with the signal line |
BollingerBandsStrategy |
Mean reversion on Bollinger Band touches |
ParabolicSARStrategy |
Trend-following with Parabolic SAR (requires High/Low columns) |
| Class | Description |
|---|---|
BreakoutStrategy |
Enter on N-day high breakout or low breakdown |
GapStrategy |
Gap trading on overnight price discontinuities (requires Open column) |
FibonacciRetracementStrategy |
Support/resistance at 38.2%, 50%, 61.8% retracement levels (requires High/Low) |
| Class | Description |
|---|---|
MeanReversionStrategy |
Combines RSI and Bollinger Bands for mean reversion signals |
MomentumStrategy |
Rate-of-Change (ROC) momentum — buy/sell on threshold crossovers |
VolatilityStrategy |
ATR-based volatility breakout detection (requires High/Low) |
EnsembleStrategy |
Wraps multiple sub-strategies, aggregates signals via majority voting |
RegimeFilteredStrategy |
Wraps any strategy; vetoes buys in unfavourable regimes (ADX trend strength or SMA bull/bear filter) |
All strategies shift signals by one bar and the runner executes at the next open, so a signal computed from Monday's close trades at Tuesday's open.
WalkForwardOptimizer optimises any strategy's parameters over a rolling or expanding train
window and evaluates out-of-sample on the following test window. Results include a stitched
equity curve, per-window metrics, and the most stable parameter set across all windows.
Methodology details:
- Full-context signals: signals for each window are generated on all history up to the window's end, then sliced — lookback indicators are already warm at the window's first bar, so no out-of-sample bars are discarded for warmup.
- Position carry: if the strategy's last entry event fired before a window started, a buy is forced on the window's first bar. Event-based strategies (crossovers) would otherwise idle in cash through windows whose entry signal predates them.
- Cost-aware: commissions and slippage from the global config apply in training and test simulations alike, including parallel workers.
RandomSearchsamples unique combinations (returns the whole grid whenncovers it).
Pass n_jobs=-1 to evaluate parameter candidates in parallel using all available CPUs.
Custom callable objectives fall back to sequential automatically.
backtest.reporting turns a WalkForwardResult into three plotly figures:
from backtest.reporting import plot_equity_curve, plot_parameter_stability, plot_metrics_by_window, save_wfa_report
fig = plot_equity_curve(result, chain=True, start_capital=10_000)
fig.show()
# Or save all three plots at once:
save_wfa_report(result, output_dir="output", prefix="my_run")trade_analysis.py writes output/<ticker>_card.json — the single artifact that crosses
from analysis to deployment. Per candidate it records:
params: the modal best parameter set across WFA windows (best_params_overall) — what the daily cron actually trades.wfa_baseline: out-of-sample metrics earned by adaptive per-window parameters.fixed_params_baseline: metrics from a plain backtest ofparamsover the same out-of-sample span — the honest estimate for the deployed configuration. If this is much worse thanwfa_baseline, the strategy's edge depends on re-optimisation.
recommended is the candidate with the highest WFA out-of-sample Sharpe. Sanity-check it
against param_stability and the fixed baseline before deploying — the best of 14 noisy
out-of-sample curves still overstates its true edge (winner's curse).
# Run the full test suite
uv run pytest
# With coverage report
uv run pytest --cov=backtest -v- Prices come from yfinance with
auto_adjust=True. History is back-adjusted by later dividends/splits, so the price (and signal) shown for a historical date can drift from what a live run saw on that date — expect small discrepancies when reconciling. - Fills are modelled at the open plus/minus slippage; there is no volume or spread model.
- Single ticker at a time: picking the ticker is itself a selection decision the framework cannot de-bias.
- All strategies operate on single-asset OHLCV data. Cross-asset strategies (pairs trading, relative momentum) are out of scope until the runner supports multi-asset event loops.
- Volatility strategies use ATR from price data only. Options-based or VIX-based approaches require a different data model and are out of scope.
- Momentum strategies use single-asset Rate-of-Change. Cross-sectional momentum (ranking multiple assets) requires runner changes and is out of scope.
MIT License