Skip to content

Latest commit

 

History

History
195 lines (137 loc) · 8 KB

File metadata and controls

195 lines (137 loc) · 8 KB

API reference — backtest_lab

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.

Installation

# 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.git

Python 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}))

enginesource

run(prices, strategy, start="2007-01-01", cost_per_side=0.0020, cash=None) -> BacktestResult

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.

BacktestResult

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)

DataView

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).

rebalance_dates(index, start) -> list[Timestamp]

First trading day of each month on/after start.


metricssource

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

auditsource

audit_no_lookahead(prices, strategy, start="2007-01-01", n_dates=5, seed=0, noise=0.10) -> dict

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}.


controlssource

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.

scaled(strategy, k) -> strategy

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.


significancesource

Guardrail 3. A single backtest comparison is one draw from a noisy process.

block_bootstrap_delta(a, b, metric=calmar, block_len=63, n_boot=2000, seed=0, ci=(10, 90)) -> dict

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.

leave_out(returns, start, end) -> Series

The series with one window removed (e.g. mask the 2008 crisis).

leave_out_check(a, b, start, end, metric=calmar) -> dict

Does a's edge over b survive with the window masked? A real mechanism should not live or die on one historical episode.


deflated_sharpesource

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).

probabilistic_sharpe_ratio(returns, sr_benchmark=0.0) -> float

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.

deflated_sharpe_ratio(returns, n_trials, trial_sharpes=None) -> dict

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.

expected_max_sharpe(n_trials, sr_variance) -> float

The expected best per-period Sharpe from n_trials independent unskilled strategies.

min_track_record_length(returns, sr_benchmark=0.0, prob=0.95) -> float

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.


datasource

load_prices(symbols, cache_path="data/prices.csv", refresh=False) -> DataFrame

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.


reportsource

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)

The strategy contract (what your function must do)

  1. Accept one argument, a DataView; read only view.prices / view.returns.
  2. Return {symbol: weight} with non-negative weights summing to ≤ 1 (empty dict = all cash).
  3. 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.