Everything you need to test a strategy lives in eight small modules. This page documents the public functions; each section links the source file, which is short enough to read whole.
# full repo (examples, tests, case studies) — recommended
git clone https://github.com/gulat70984-del/Algorithm-tester.git
cd Algorithm-tester
pip install -e .
# or the library alone, straight from GitHub
pip install git+https://github.com/gulat70984-del/Algorithm-tester.gitPython 3.10+. After install, import backtest_lab works from anywhere.
Minimal end-to-end example:
from backtest_lab.data import load_prices
from backtest_lab.engine import run
from backtest_lab.audit import audit_no_lookahead
from backtest_lab import metrics as M
def my_strategy(view):
px = view.prices # history strictly BEFORE the rebalance date
if len(px) < 127:
return {}
momentum = px.iloc[-1] / px.iloc[-127] - 1.0
return {momentum.drop("SHY").idxmax(): 1.0}
prices = load_prices(["SPY", "EFA", "GLD", "SHY"])
assert audit_no_lookahead(prices, my_strategy)["passed"]
result = run(prices, my_strategy, cost_per_side=0.0020, cash="SHY")
print(M.table({"mine": result.returns}))engine — source
Backtest a strategy with monthly rebalancing.
| Parameter | Meaning |
|---|---|
prices |
wide DataFrame of adjusted daily closes (columns = symbols) |
strategy |
callable (view) -> dict[symbol, weight]; long-only, weights sum ≤ 1 |
start |
first rebalance on/after this date (earlier history is signal warm-up) |
cost_per_side |
proportional cost per unit turnover (0.0020 = 20 bps) |
cash |
optional symbol that absorbs residual weight (e.g. "SHY") |
Raises ValueError on negative weights, weights summing above 1, or unknown symbols.
The initial entry is charged — a backtest that gets its first positions free is lying.
| Attribute | Contents |
|---|---|
.returns |
daily net return series |
.weights |
daily weight matrix (including the cash column) |
.gross |
daily gross risk exposure (excludes cash) |
.turnover |
daily sum of absolute weight changes |
.equity |
growth-of-$1 series (property) |
What a strategy receives. .prices / .returns return history strictly before .date —
the same information a live trader has. This is enforced structurally, and audited (below).
First trading day of each month on/after start.
metrics — source
All annualisation assumes daily data (252 days/year).
| Function | Returns |
|---|---|
cagr(r) |
compound annual growth rate |
sharpe(r) |
annualised Sharpe ratio (0.0 for zero-vol series) |
sortino(r) |
Sharpe using downside deviation only |
max_drawdown(r) |
worst peak-to-trough loss (negative number) |
calmar(r) |
CAGR / |max drawdown| |
ann_vol(r) |
annualised volatility |
worst_month(r) |
worst calendar-month return |
summarize(r) |
all of the above as a dict |
span(r, start, end) |
slice a return series to a date window |
table(named, spans) |
fixed-width comparison table for any strategies × any windows |
audit — source
Guardrail 1. For sampled rebalance dates, corrupt every price on/after the date with random
noise and re-ask the strategy for weights. Any change proves the strategy read the future.
Returns {"passed": bool, "checked": [...], "failures": [...], "verdict": str}.
controls — source
Guardrail 2. Any filter that lowers average exposure shows a smaller drawdown even with zero timing skill — so filters are compared against a matched-exposure twin, not the base.
Wrap a strategy so every risk weight is multiplied by k ∈ [0, 1] (residual to cash).
matched_exposure_control(prices, base_strategy, filter_result, base_result, **run_kwargs) -> (BacktestResult, k)
Build and run the twin: k = mean(filter gross) / mean(base gross), held constantly.
significance — source
Guardrail 3. A single backtest comparison is one draw from a noisy process.
Paired circular block bootstrap of metric(a) − metric(b) on aligned daily return series.
Blocks preserve autocorrelation (drawdown metrics need it); pairing resamples identical dates
for both series. Returns point delta, bootstrap mean, CI, p_delta_gt_0, and a verdict —
a delta only counts if the CI excludes zero.
The series with one window removed (e.g. mask the 2008 crisis).
Does a's edge over b survive with the window masked? A real mechanism should not live or
die on one historical episode.
deflated_sharpe — source
Guardrail 4. The multiple-testing correction the others miss: is a Sharpe still special once you account for how many variants were tried? All Sharpes are per-period (non-annualized).
P(true Sharpe > sr_benchmark) given the observed Sharpe, track length, and skew/kurtosis
(fat tails and negative skew lower confidence). > 0.95 is the usual significance bar.
PSR against a benchmark set to the expected maximum Sharpe that n_trials skill-less searches
would produce by luck. Count n_trials honestly (every variant, not just the winner). Returns
dsr, sr0 (the deflated bar), significant, and a verdict. trial_sharpes (the per-period
Sharpes of all trials) gives the exact deflation variance; omitted, a documented fallback is used.
The expected best per-period Sharpe from n_trials independent unskilled strategies.
Minimum observations for the Sharpe to be significant at prob. If it exceeds len(returns),
the record is too short to trust. inf when the Sharpe is below the benchmark.
Reference: Bailey & López de Prado, "The Deflated Sharpe Ratio," J. of Portfolio Management 40(5), 2014 — https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2460551.
data — source
Adjusted daily closes, downloaded once (Yahoo Finance via yfinance) then served from a local
CSV cache — so results reproduce offline and numbers can't silently drift between runs.
report — source
| Function | Output |
|---|---|
equity_chart(named, path, title, subtitle="", log_ticks=None) |
growth-of-$1 PNG, log scale, readable dollar ticks |
drawdown_chart(named, path, title) |
filled drawdown comparison PNG |
write_artifact(text, path, header="") |
UTF-8 text artifact (every published number should trace to one) |
- Accept one argument, a
DataView; read onlyview.prices/view.returns. - Return
{symbol: weight}with non-negative weights summing to ≤ 1 (empty dict = all cash). - Be deterministic given the same view (required for reproducibility and the audit).
Anything satisfying this contract — momentum rankings, trend-filtered allocations, seasonal switches — plugs into the full vetting pipeline unmodified. Out of scope by design: shorting, leverage, intraday data, and per-trade stop/entry systems.