Backtesting (wraquant.backtest)

The backtesting module provides event-driven and vectorized backtesting engines, strategy abstractions, position sizing, performance metrics, event tracking, and tearsheet generation.

Key components:

  • Backtest – event-driven engine with fill simulation

  • VectorizedBacktest – fast vectorized engine for signal-based strategies

  • Strategy – base class for defining trading strategies

  • PositionSizer – ATR-based, risk parity, and regime-conditional sizing

  • 15+ performance metrics beyond Sharpe: omega, Kelly, profit factor, SQN, recovery factor

  • Tearsheet generation: equity curves, drawdown tables, monthly heatmaps

Quick Example

from wraquant.backtest import Backtest, Strategy, performance_summary
from wraquant.ta import ema, crossover

class MACrossover(Strategy):
    def generate_signals(self, prices):
        fast = ema(prices, period=10)
        slow = ema(prices, period=50)
        return crossover(fast, slow).astype(float)

bt = Backtest(MACrossover())
result = bt.run(prices)

perf = performance_summary(result['returns'])
print(f"Sharpe: {perf['sharpe_ratio']:.4f}")
print(f"Max DD: {perf['max_drawdown']:.2%}")
print(f"Win rate: {perf['win_rate']:.2%}")

Tearsheet

from wraquant.backtest import generate_tearsheet, monthly_returns_table

tearsheet = generate_tearsheet(result['returns'])
monthly = monthly_returns_table(result['returns'])
print(monthly)   # year x month heatmap of returns

Walk-Forward Validation

from wraquant.backtest import walk_forward_backtest

wf = walk_forward_backtest(
    strategy=MACrossover(),
    prices=prices,
    train_period=504,
    test_period=126,
)
print(f"Walk-forward Sharpe: {wf['sharpe_ratio']:.4f}")

Regime-Conditional Sizing

from wraquant.backtest import regime_conditional_sizing

sizing = regime_conditional_sizing(
    signal=result['positions'],
    states=hmm['states'],
    sizing_map={0: 1.0, 1: 0.3},   # full in bull, 30% in bear
)

See also

API Reference

Backtesting framework for trading strategies.

Provides a full-featured backtesting infrastructure for evaluating trading strategies on historical data. Supports both fast vectorized backtesting (for signal-based strategies) and event-driven simulation (for complex order logic), with 30+ performance metrics, position sizing models, regime-aware filtering, and publication-quality tearsheets.

Key sub-modules:

  • Engine (engine) – Backtest (event-driven engine with fill simulation), VectorizedBacktest (fast signal-based engine), and walk_forward_backtest (rolling out-of-sample evaluation).

  • Strategy (strategy) – Strategy base class for defining entry/exit logic, signal generation, and position management.

  • Metrics (metrics) – 30+ performance metrics computed from an equity curve: performance_summary (one-call overview), omega_ratio, burke_ratio, ulcer_performance_index, kappa_ratio, tail_ratio, rachev_ratio, gain_to_pain_ratio, kelly_fraction (optimal bet sizing), risk_of_ruin, profit_factor, system_quality_number, expectancy, recovery_factor, and more.

  • Position sizing (position) – PositionSizer framework, risk_parity_position, regime_conditional_sizing (size based on detected regime), regime_signal_filter (suppress signals in unfavorable regimes), clip_weights, rebalance_threshold.

  • Events (events) – EventTracker for logging trades, rebalances, and drawdown events during simulation. detect_drawdown_events and detect_regime_changes identify key structural events in the equity curve.

  • Tearsheet (tearsheet) – generate_tearsheet and comprehensive_tearsheet produce multi-panel performance reports. monthly_returns_table, drawdown_table, rolling_metrics_table, strategy_comparison, and trade_analysis for detailed diagnostics.

  • Integrations – Wrappers for vectorbt, quantstats, empyrical, pyfolio, and ffn.

Example

>>> from wraquant.backtest import VectorizedBacktest, performance_summary
>>> bt = VectorizedBacktest(signal_fn=my_signal)
>>> result = bt.run(prices)
>>> perf = performance_summary(result["equity_curve"])
>>> print(f"Sharpe: {perf['sharpe']:.2f}, Max DD: {perf['max_drawdown']:.1%}")

Use wraquant.backtest for strategy evaluation and walk-forward analysis. For risk measurement on the resulting equity curve, see wraquant.risk. For parallel parameter sweeps, see wraquant.scale.parallel_backtest. For interactive tearsheet visualization, see wraquant.viz.plot_backtest_tearsheet.

class Backtest[source]

Bases: object

Vectorized backtesting engine.

Parameters:
  • strategy (Strategy) – Strategy instance.

  • initial_capital (float, default: 100000.0) – Starting portfolio value.

  • commission (float, default: 0.0) – Commission per trade as fraction (e.g., 0.001 = 10bps).

  • slippage (float, default: 0.0) – Slippage per trade as fraction.

Example

>>> from wraquant.backtest import Backtest
>>> from wraquant.backtest.strategy import BuyAndHold
>>> bt = Backtest(BuyAndHold(), initial_capital=100_000)
>>> result = bt.run(prices_df)
__init__(strategy, initial_capital=100000.0, commission=0.0, slippage=0.0)[source]
Parameters:
  • strategy (Strategy)

  • initial_capital (float, default: 100000.0)

  • commission (float, default: 0.0)

  • slippage (float, default: 0.0)

Return type:

None

run(prices)[source]

Run the backtest on historical price data.

Parameters:

prices (DataFrame) – DataFrame of asset prices (columns = assets).

Return type:

BacktestResult

Returns:

BacktestResult with portfolio value, returns, positions, metrics.

class VectorizedBacktest[source]

Bases: object

Fast vectorized backtesting engine for signal-based strategies.

Unlike the event-driven Backtest class (which requires a Strategy object), VectorizedBacktest operates directly on a pre-computed signal / weight matrix. This is ideal for research workflows where signals are generated upstream (e.g., from a machine learning model or factor pipeline) and you need fast, repeatable backtest evaluation.

The engine computes portfolio returns accounting for:

  • Transaction costs (fixed commission per unit of turnover).

  • Slippage (additional cost proportional to turnover).

  • Rebalancing frequency (skip rebalancing on off-days to reduce turnover).

Parameters:
  • initial_capital (float, default: 100000.0) – Starting portfolio value in currency units.

  • commission (float, default: 0.0) – One-way commission as a fraction of turnover (e.g., 0.001 = 10 bps).

  • slippage (float, default: 0.0) – Slippage as a fraction of turnover.

  • rebalance_frequency (int, default: 1) – Rebalance every N periods. 1 means every period (daily for daily data), 5 means weekly, etc.

Example

>>> import pandas as pd, numpy as np
>>> rng = np.random.default_rng(42)
>>> prices = pd.DataFrame(
...     100 * np.exp(np.cumsum(rng.normal(0.0005, 0.02, (100, 2)), axis=0)),
...     columns=["A", "B"],
...     index=pd.bdate_range("2023-01-01", periods=100),
... )
>>> signals = pd.DataFrame(
...     0.5, index=prices.index, columns=prices.columns
... )
>>> bt = VectorizedBacktest(commission=0.001, slippage=0.0005)
>>> result = bt.run(prices, signals)
>>> isinstance(result, BacktestResult)
True

See also

Backtest: Strategy-object-based backtesting engine. walk_forward_backtest: Walk-forward optimisation + backtest.

__init__(initial_capital=100000.0, commission=0.0, slippage=0.0, rebalance_frequency=1)[source]
Parameters:
  • initial_capital (float, default: 100000.0)

  • commission (float, default: 0.0)

  • slippage (float, default: 0.0)

  • rebalance_frequency (int, default: 1)

Return type:

None

run(prices, signals)[source]

Execute the vectorized backtest.

Parameters:
  • prices (DataFrame) – Asset price DataFrame (rows = dates, columns = assets).

  • signals (DataFrame) – Weight / signal DataFrame with the same shape as prices. Values represent desired portfolio weights (e.g., 0.5 = 50 % allocation to that asset). Signals are shifted by one period internally to avoid look-ahead bias.

Return type:

BacktestResult

Returns:

BacktestResult with equity curve, returns, positions, and metrics. Additional fields (turnover, costs, net_returns, gross_returns) are available via dict-like access on the metrics dict.

walk_forward_backtest(prices, strategy_factory, param_grid, train_size=252, test_size=63, step_size=None, metric='sharpe_ratio', initial_capital=100000.0, commission=0.0, slippage=0.0)[source]

Walk-forward optimisation and backtesting.

Walk-forward analysis is the gold standard for evaluating parameter-dependent strategies. It splits the data into rolling train/test windows, optimises strategy parameters on the training set, and evaluates on the out-of-sample test set. The result is a true out-of-sample equity curve that avoids look-ahead bias and parameter overfitting.

Algorithm:
  1. For each window starting at t: a. Train set: prices[t : t + train_size] b. Test set: prices[t + train_size : t + train_size + test_size]

  2. For each parameter combination in param_grid, backtest on the train set and record the optimisation metric.

  3. Select the best parameters and backtest on the test set.

  4. Slide forward by step_size and repeat.

  5. Concatenate all out-of-sample test returns.

Parameters:
  • prices (DataFrame) – Asset price DataFrame.

  • strategy_factory (Callable[..., Strategy]) – Callable that accepts keyword arguments (from param_grid) and returns a Strategy instance.

  • param_grid (list[dict[str, Any]]) – List of parameter dictionaries to search over.

  • train_size (int, default: 252) – Number of periods in each training window.

  • test_size (int, default: 63) – Number of periods in each test window.

  • step_size (int | None, default: None) – Number of periods to slide the window forward. Defaults to test_size (non-overlapping test windows).

  • metric (str, default: 'sharpe_ratio') – Performance metric to optimise (key from performance_summary output, e.g., "sharpe_ratio").

  • initial_capital (float, default: 100000.0) – Starting capital for each window backtest.

  • commission (float, default: 0.0) – Commission fraction.

  • slippage (float, default: 0.0) – Slippage fraction.

Returns:

  • oos_returns: Concatenated out-of-sample return series.

  • is_returns: Concatenated in-sample return series.

  • oos_equity_curve: Equity curve from OOS returns.

  • params_per_window: List of best params per window.

  • is_metrics_per_window: In-sample metrics per window.

  • oos_metrics: Overall OOS performance summary.

  • stability_ratio: Fraction of windows where OOS Sharpe > 0.

Return type:

dict[str, Any]

Example

>>> import pandas as pd, numpy as np
>>> from wraquant.backtest.strategy import MomentumStrategy
>>> rng = np.random.default_rng(42)
>>> prices = pd.DataFrame(
...     100 * np.exp(np.cumsum(rng.normal(0.0003, 0.015, (600, 2)), axis=0)),
...     columns=["A", "B"],
...     index=pd.bdate_range("2020-01-01", periods=600),
... )
>>> grid = [{"lookback": 10}, {"lookback": 20}, {"lookback": 40}]
>>> result = walk_forward_backtest(
...     prices,
...     strategy_factory=lambda **kw: MomentumStrategy(**kw),
...     param_grid=grid,
...     train_size=200,
...     test_size=50,
... )
>>> "oos_returns" in result
True

See also

Backtest: Single-pass backtesting engine. VectorizedBacktest: Signal-matrix-based backtesting.

class Strategy[source]

Bases: ABC

Abstract base class for trading strategies.

Subclasses must implement generate_signals which maps prices to position signals (-1, 0, +1 or fractional).

Example

>>> class MACrossover(Strategy):
...     def __init__(self, fast: int = 10, slow: int = 50):
...         self.fast = fast
...         self.slow = slow
...
...     def generate_signals(self, prices: pd.DataFrame) -> pd.DataFrame:
...         fast_ma = prices.rolling(self.fast).mean()
...         slow_ma = prices.rolling(self.slow).mean()
...         return (fast_ma > slow_ma).astype(float)
abstractmethod generate_signals(prices)[source]

Generate position signals from price data.

Parameters:

prices (DataFrame) – DataFrame of asset prices (columns = assets).

Return type:

DataFrame

Returns:

DataFrame of signals with same shape as prices. Values represent desired position: 1 = long, -1 = short, 0 = flat.

performance_summary(returns, risk_free=0.0, periods_per_year=252)[source]

Calculate comprehensive performance metrics.

Parameters:
  • returns (Series) – Portfolio return series.

  • risk_free (float, default: 0.0) – Annual risk-free rate.

  • periods_per_year (int, default: 252) – Number of trading periods per year.

Return type:

dict

Returns:

Dict with performance metrics.

omega_ratio(returns, threshold=0.0)[source]

Omega ratio: probability-weighted gain/loss ratio above a threshold.

The Omega ratio partitions the return distribution at a threshold L and computes the ratio of the expected gain above L to the expected loss below L. Unlike the Sharpe ratio it uses the entire distribution (all moments, not just the first two), making it more appropriate for non-normal returns.

Mathematical formulation:
Omega(L) = E[max(R - L, 0)] / E[max(L - R, 0)]

= sum(max(r_i - L, 0)) / sum(max(L - r_i, 0))

How to interpret:
  • Omega = 1.0: strategy breaks even relative to the threshold.

  • Omega > 1.0: gains outweigh losses (good).

  • Omega > 2.0: strong risk-adjusted performance.

  • Omega = inf: no returns below the threshold.

  • The higher the better; compare strategies at the same threshold.

When to use:

Use Omega when return distributions are skewed or fat-tailed and you want a single number that captures the full distribution. Prefer Omega over Sharpe for options-based or trend-following strategies whose returns are far from Gaussian.

Parameters:
  • returns (Series) – Simple return series (e.g., daily returns).

  • threshold (float, default: 0.0) – Minimum acceptable return per period. Default 0.

Return type:

float

Returns:

Omega ratio as a float. Returns inf if no returns fall below the threshold.

Example

>>> import pandas as pd
>>> r = pd.Series([0.01, 0.02, -0.005, 0.015, -0.01])
>>> omega_ratio(r, threshold=0.0)
3.0

See also

kappa_ratio: Generalized lower-partial-moment ratio (Kappa(1) = Omega - 1). sharpe_ratio: Mean/std ratio; only uses first two moments.

burke_ratio(returns, periods_per_year=252)[source]

Burke ratio: return per unit of drawdown severity.

The Burke ratio penalises strategies that experience many deep drawdowns by dividing the annualised excess return by the square root of the sum of squared drawdown depths. Compared to Calmar (which uses only the worst drawdown), Burke considers the entire drawdown history.

Mathematical formulation:

Burke = annualized_return / sqrt(sum(d_i^2))

where d_i are the individual drawdown depths (negative values).

How to interpret:
  • Higher is better.

  • A strategy with many small drawdowns can have a higher Burke than one with a single large drawdown, even if their Calmar ratios are identical.

  • There is no universal “good” threshold; use for relative comparison between strategies.

When to use:

Prefer Burke over Calmar when you want to penalise strategies that repeatedly draw down, not just the single worst case.

Parameters:
  • returns (Series) – Simple return series.

  • periods_per_year (int, default: 252) – Trading periods per year (252 for daily).

Return type:

float

Returns:

Burke ratio as a float. Returns 0.0 if there are no drawdowns.

Example

>>> import pandas as pd, numpy as np
>>> r = pd.Series(np.random.default_rng(42).normal(0.001, 0.01, 252))
>>> burke_ratio(r)
1.23

See also

ulcer_performance_index: Uses Ulcer Index (RMS of drawdowns) in denominator. recovery_factor: Net profit / max drawdown.

ulcer_performance_index(returns, periods_per_year=252)[source]

Ulcer Performance Index (UPI): return per unit of drawdown pain.

The UPI (also known as Martin ratio) divides the annualised excess return by the Ulcer Index, which is the root-mean-square of percentage drawdowns. The Ulcer Index captures both the depth and duration of drawdowns, making UPI a comprehensive pain-adjusted return measure.

Mathematical formulation:

Ulcer Index = sqrt(mean(d_i^2)) UPI = annualized_return / Ulcer Index

where d_i is the drawdown at each point in time.

How to interpret:
  • Higher is better.

  • UPI > 2.0 is generally considered very good.

  • UPI accounts for drawdown duration (not just depth), so a strategy that recovers quickly scores better than one that lingers in drawdown.

When to use:

Use UPI when you want a risk-adjusted measure that captures the investor’s real experience of pain (deep, prolonged drawdowns hurt more than brief dips).

Parameters:
  • returns (Series) – Simple return series.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

float

Returns:

UPI as a float. Returns 0.0 if the Ulcer Index is zero.

Example

>>> import pandas as pd, numpy as np
>>> r = pd.Series(np.random.default_rng(42).normal(0.001, 0.01, 252))
>>> ulcer_performance_index(r)
2.5

See also

burke_ratio: Uses sum of squared drawdowns instead of RMS. max_drawdown: Single worst peak-to-trough decline.

kappa_ratio(returns, order=2, threshold=0.0, periods_per_year=252)[source]

Kappa ratio: generalized Sortino using lower partial moments.

The Kappa ratio family generalises the Sortino ratio by using the n-th root of the n-th lower partial moment (LPM) as the risk measure. Special cases:

  • Kappa(1) is equivalent to (Omega - 1) when annualization is removed (first lower partial moment = expected shortfall below threshold).

  • Kappa(2) is the Sortino ratio (second lower partial moment = downside deviation).

  • Kappa(3) penalises large negative returns even more heavily.

Mathematical formulation:

LPM_n = mean(max(L - r_i, 0)^n) Kappa_n = (annualized_mean_excess) / LPM_n^(1/n)

How to interpret:
  • Higher is better (more return per unit of downside risk).

  • Higher orders penalise tail risk more severely.

  • Compare strategies using the same order.

When to use:

Use Kappa(3) when you especially fear large losses. Use Kappa(2) as a drop-in alternative to Sortino. Use Kappa(1) when you want an Omega-style measure in ratio form.

Parameters:
  • returns (Series) – Simple return series.

  • order (int, default: 2) – Moment order n (1, 2, or 3 are typical).

  • threshold (float, default: 0.0) – Minimum acceptable return per period.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

float

Returns:

Kappa ratio as a float. Returns 0.0 if LPM is zero.

Example

>>> import pandas as pd, numpy as np
>>> r = pd.Series(np.random.default_rng(42).normal(0.001, 0.01, 252))
>>> kappa_ratio(r, order=2)
1.8

See also

omega_ratio: Threshold-based gain/loss ratio. sortino_ratio: Equivalent to Kappa(2).

tail_ratio(returns, upper_pct=95.0, lower_pct=5.0)[source]

Tail ratio: magnitude of right tail vs. left tail.

The tail ratio measures how large positive outlier returns are relative to negative outlier returns. A value greater than 1 means the strategy’s best days are proportionally larger than its worst days.

Mathematical formulation:

Tail ratio = |percentile(R, upper_pct)| / |percentile(R, lower_pct)|

How to interpret:
  • Tail ratio > 1.0: right tail is fatter (upside surprises bigger than downside). Desirable.

  • Tail ratio = 1.0: symmetric tails.

  • Tail ratio < 1.0: left tail is fatter (downside risk dominates).

When to use:

Quick diagnostic to check if a strategy has favorable tail behaviour. Combine with common_sense_ratio for a sanity check.

Parameters:
  • returns (Series) – Simple return series.

  • upper_pct (float, default: 95.0) – Upper percentile (default 95).

  • lower_pct (float, default: 5.0) – Lower percentile (default 5).

Return type:

float

Returns:

Tail ratio as a float. Returns inf if the lower percentile is zero.

Example

>>> import pandas as pd
>>> r = pd.Series([0.03, 0.01, -0.01, 0.02, -0.005])
>>> tail_ratio(r)
2.0

See also

common_sense_ratio: Tail ratio multiplied by (1 + Sharpe). rachev_ratio: CVaR-based tail comparison.

common_sense_ratio(returns, risk_free=0.0, periods_per_year=252)[source]

Common Sense Ratio: quick sanity check combining Sharpe and tails.

The Common Sense Ratio multiplies the tail ratio by (1 + Sharpe). It combines a measure of tail asymmetry with overall risk-adjusted performance into a single number for rapid strategy screening.

Mathematical formulation:

CSR = tail_ratio * (1 + Sharpe)

How to interpret:
  • CSR > 1.0: strategy has both favorable tails and positive risk-adjusted return. Good.

  • CSR < 1.0: either tails are unfavorable or risk-adjusted return is poor.

  • Use for fast initial screening; not a substitute for deeper analysis.

When to use:

Use CSR as a first-pass filter when evaluating many strategies simultaneously.

Parameters:
  • returns (Series) – Simple return series.

  • risk_free (float, default: 0.0) – Annual risk-free rate.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

float

Returns:

Common Sense Ratio as a float.

Example

>>> import pandas as pd, numpy as np
>>> r = pd.Series(np.random.default_rng(42).normal(0.001, 0.01, 252))
>>> common_sense_ratio(r)
1.5

See also

tail_ratio: Upside vs downside tail magnitude. sharpe_ratio: Mean/std risk-adjusted return.

rachev_ratio(returns, alpha=0.05)[source]

Rachev ratio: CVaR of gains over CVaR of losses.

The Rachev ratio (also called the Conditional Tail Ratio) compares the expected size of extreme gains to the expected size of extreme losses, using Conditional Value at Risk (CVaR, a.k.a. Expected Shortfall). Unlike the simple tail ratio, Rachev uses the mean of the tail rather than a single percentile, making it more robust to outliers.

Mathematical formulation:

CVaR_alpha(gains) = E[R | R > VaR_{1-alpha}(R)] CVaR_alpha(losses) = E[-R | R < VaR_alpha(R)] Rachev = CVaR_alpha(gains) / CVaR_alpha(losses)

How to interpret:
  • Rachev > 1.0: expected extreme gains exceed expected extreme losses. The strategy has favorable fat-tail asymmetry.

  • Rachev = 1.0: symmetric tail risk.

  • Rachev < 1.0: tail risk is skewed to the downside.

When to use:

Use Rachev when you need a fat-tail-aware gain/loss comparison. More robust than tail_ratio because it averages over the tail rather than using a single percentile.

Parameters:
  • returns (Series) – Simple return series.

  • alpha (float, default: 0.05) – Tail probability (default 0.05 = 5 % tails).

Return type:

float

Returns:

Rachev ratio as a float. Returns inf if the loss CVaR is zero.

Example

>>> import pandas as pd, numpy as np
>>> r = pd.Series(np.random.default_rng(42).normal(0.001, 0.01, 500))
>>> rachev_ratio(r, alpha=0.05)
1.1

See also

tail_ratio: Percentile-based tail comparison. omega_ratio: Full-distribution gain/loss ratio.

gain_to_pain_ratio(returns)[source]

Gain to Pain ratio: total gains over total absolute losses.

The Gain to Pain ratio (GPR) divides the sum of all returns by the absolute sum of all negative returns. It provides a simple measure of how much total return the strategy generates per unit of pain (aggregate losses) experienced.

Mathematical formulation:

GPR = sum(r_i) / |sum(r_i where r_i < 0)|

How to interpret:
  • GPR > 1.0: total gains exceed total losses; strategy is net profitable and then some.

  • GPR = 0.5: for every dollar lost, the strategy earned 50 cents net.

  • GPR < 0: strategy loses money overall.

  • GPR > 1.5 is generally considered good for daily returns.

When to use:

Quick profitability diagnostic. Simpler and more intuitive than profit factor (which excludes zero returns and looks at gross gains vs. gross losses).

Parameters:

returns (Series) – Simple return series.

Return type:

float

Returns:

Gain to Pain ratio as a float. Returns inf if there are no negative returns.

Example

>>> import pandas as pd
>>> r = pd.Series([0.02, -0.01, 0.015, -0.005, 0.01])
>>> gain_to_pain_ratio(r)
2.67

See also

profit_factor: Gross gains / gross losses (ignores net). omega_ratio: Threshold-based gain/loss comparison.

risk_of_ruin(win_rate, payoff_ratio, ruin_pct=0.5, n_trades=1000)[source]

Probability of losing a given fraction of capital.

Estimates the risk of ruin – the probability that a strategy will lose ruin_pct of its capital – given its win rate and average payoff ratio, assuming fixed fractional position sizing and independent trades.

Mathematical formulation (simplified):

RoR = ((1 - edge) / (1 + edge)) ^ capital_units

where edge = (win_rate * payoff_ratio) - (1 - win_rate) and capital_units approximates the number of bet-units to exhaust before reaching ruin_pct.

How to interpret:
  • RoR close to 0: very unlikely to hit ruin level.

  • RoR > 0.05 (5 %): meaningful risk; reduce position size.

  • RoR > 0.20 (20 %): dangerous; the strategy may not survive.

  • A strategy with high win_rate and high payoff_ratio has very low risk of ruin.

When to use:

Use risk of ruin to decide whether a strategy is survivable over n_trades. Combine with Kelly fraction to determine appropriate sizing.

Parameters:
  • win_rate (float) – Probability of a winning trade (0 to 1).

  • payoff_ratio (float) – Average win / average loss (positive number).

  • ruin_pct (float, default: 0.5) – Fraction of capital that constitutes ruin (e.g., 0.5 = losing 50 % of capital).

  • n_trades (int, default: 1000) – Number of trades to consider in the simulation window (affects capital_units approximation).

Return type:

float

Returns:

Estimated probability of ruin (0 to 1).

Example

>>> risk_of_ruin(win_rate=0.55, payoff_ratio=1.5, ruin_pct=0.5)
0.002

See also

kelly_fraction: Optimal position sizing. expectancy: Expected value per trade.

kelly_fraction(win_rate, avg_win, avg_loss)[source]

Kelly fraction: optimal bet sizing for geometric growth.

The Kelly criterion determines the fraction of capital to risk on each trade to maximise the long-run geometric growth rate. The full Kelly is aggressive; practitioners typically use fractional Kelly (e.g., half-Kelly) to reduce variance.

Mathematical formulation:

b = avg_win / avg_loss (odds ratio) f* = (b * p - q) / b

where p = win_rate, q = 1 - p.

How to interpret:
  • f* > 0: strategy has positive edge; bet this fraction.

  • f* = 0: no edge; do not bet.

  • f* < 0: negative edge (clamped to 0); avoid this strategy.

  • f* > 0.25: full Kelly is very aggressive; consider using half or quarter Kelly.

When to use:

Use Kelly to determine the theoretical maximum position size. In practice, use fractional Kelly (0.25x to 0.5x) because Kelly assumes known and constant edge, which is unrealistic.

Parameters:
  • win_rate (float) – Probability of a winning trade (0 to 1).

  • avg_win (float) – Average winning trade magnitude (positive).

  • avg_loss (float) – Average losing trade magnitude (positive, i.e., the absolute value of the average loss).

Return type:

float

Returns:

Optimal fraction of capital to risk, clamped to [0, 1].

Example

>>> kelly_fraction(win_rate=0.55, avg_win=1.5, avg_loss=1.0)
0.25

See also

risk_of_ruin: Probability of catastrophic drawdown. expectancy: Expected value per trade.

expectancy(win_rate, avg_win, avg_loss)[source]

Expectancy: expected profit per trade.

Expectancy combines win rate and payoff to give the average expected value of each trade. A strategy with positive expectancy is profitable in the long run (assuming sufficient trades and stable edge).

Mathematical formulation:

E = (win_rate * avg_win) - ((1 - win_rate) * |avg_loss|)

How to interpret:
  • E > 0: each trade is expected to be profitable on average.

  • E = 0: break-even strategy.

  • E < 0: losing strategy.

  • E > 0.10 (if avg_loss is normalised to 1.0): reasonable edge.

When to use:

Use expectancy alongside system quality number (SQN) and profit factor to evaluate a trading system. Expectancy alone does not account for variability of outcomes.

Parameters:
  • win_rate (float) – Probability of a winning trade (0 to 1).

  • avg_win (float) – Average winning trade magnitude (positive).

  • avg_loss (float) – Average losing trade magnitude (positive, absolute value of the average loss).

Return type:

float

Returns:

Expected value per trade.

Example

>>> expectancy(win_rate=0.6, avg_win=100.0, avg_loss=80.0)
28.0

See also

system_quality_number: Expectancy normalised by trade variability. kelly_fraction: Optimal sizing given expectancy.

profit_factor(returns)[source]

Profit factor: gross profit divided by gross loss.

The profit factor measures how many dollars the strategy earns for every dollar it loses. It is the simplest measure of a strategy’s profitability.

Mathematical formulation:

PF = sum(r_i where r_i > 0) / |sum(r_i where r_i < 0)|

How to interpret:
  • PF > 1.0: strategy is profitable.

  • PF = 1.0: break even.

  • PF < 1.0: strategy loses money.

  • PF > 1.5: good.

  • PF > 2.0: very good (verify not overfitting).

When to use:

Use as a quick profitability check. Pair with win rate and payoff ratio for a complete picture.

Parameters:

returns (Series) – Return or P&L series.

Return type:

float

Returns:

Profit factor as a float. Returns inf if there are no losses.

Example

>>> import pandas as pd
>>> r = pd.Series([0.02, -0.01, 0.03, -0.005])
>>> profit_factor(r)
...
3.33

See also

gain_to_pain_ratio: Net return / total losses. expectancy: Expected value per trade.

payoff_ratio(returns)[source]

Payoff ratio: average win divided by average loss.

The payoff ratio (also called reward-to-risk ratio) measures how large the average winning trade is relative to the average losing trade. Combined with win rate, it fully characterises a strategy’s return profile.

Mathematical formulation:

Payoff = mean(r_i where r_i > 0) / |mean(r_i where r_i < 0)|

How to interpret:
  • Payoff > 1.0: average wins are larger than average losses. Common in trend-following strategies.

  • Payoff = 1.0: wins and losses are the same size.

  • Payoff < 1.0: average losses exceed average wins. The strategy must have a high win rate to compensate.

  • Payoff > 2.0 with win rate > 0.40 is a strong system.

When to use:

Use alongside win rate. A low win rate with high payoff (trend-following) is as viable as high win rate with low payoff (mean-reversion).

Parameters:

returns (Series) – Return or P&L series.

Return type:

float

Returns:

Payoff ratio as a float. Returns inf if there are no losses, and 0.0 if there are no wins.

Example

>>> import pandas as pd
>>> r = pd.Series([0.02, -0.01, 0.03, -0.005])
>>> payoff_ratio(r)
...
3.33

See also

profit_factor: Gross gains / gross losses. expectancy: Combines win rate and payoff into expected value.

recovery_factor(returns)[source]

Recovery factor: net profit relative to max drawdown.

Recovery factor measures how many times over the strategy has recovered from its worst drawdown. A high recovery factor indicates that the strategy generates returns efficiently relative to the drawdown pain it inflicts.

Mathematical formulation:

RF = total_return / |max_drawdown|

How to interpret:
  • RF > 1.0: strategy has earned back more than its worst drawdown.

  • RF > 3.0: strong.

  • RF > 5.0: excellent recovery relative to risk.

  • RF < 1.0: strategy has not yet recovered from its worst drawdown.

When to use:

Use recovery factor when you want to know if the strategy’s returns justify the drawdown pain. Useful for comparing strategies with different drawdown profiles.

Parameters:

returns (Series) – Simple return series.

Return type:

float

Returns:

Recovery factor as a float. Returns inf if there is no drawdown.

Example

>>> import pandas as pd, numpy as np
>>> r = pd.Series(np.random.default_rng(42).normal(0.001, 0.01, 252))
>>> recovery_factor(r)
3.5

See also

burke_ratio: Return per sum-of-squared-drawdowns. ulcer_performance_index: Return per Ulcer Index.

system_quality_number(pnl)[source]

System Quality Number (SQN): Van Tharp’s strategy quality metric.

SQN normalises expectancy by the variability of trade outcomes and scales by the square root of the number of trades. It answers the question: “given the consistency and edge of this system, is the positive expectancy statistically significant?”

Mathematical formulation:

SQN = sqrt(N) * mean(pnl) / std(pnl)

where N is the number of trades (or periods).

How to interpret:
  • SQN < 1.6: poor; difficult to trade profitably.

  • 1.6 < SQN < 2.0: below average; marginal edge.

  • 2.0 < SQN < 2.5: average; tradeable with discipline.

  • 2.5 < SQN < 3.0: good; reliable system.

  • 3.0 < SQN < 5.0: excellent.

  • 5.0 < SQN < 7.0: superb.

  • SQN > 7.0: holy grail (verify not overfitting).

When to use:

Use SQN to evaluate whether a system’s edge is statistically meaningful given the number of observations. Particularly useful when comparing systems with different numbers of trades.

Parameters:

pnl (Series) – Per-trade or per-period P&L series.

Return type:

float

Returns:

SQN as a float. Returns 0.0 if the standard deviation is zero.

Example

>>> import pandas as pd
>>> pnl = pd.Series([100, -50, 80, -30, 120, -40, 90, -20])
>>> system_quality_number(pnl)
1.7

See also

expectancy: Mean expected value per trade. kelly_fraction: Optimal position sizing from edge.

class Event[source]

Bases: object

A single portfolio event.

Parameters:
  • timestamp (datetime) – When the event occurred.

  • event_type (EventType) – Category of the event.

  • data (dict[str, Any], default: <factory>) – Event-specific payload.

timestamp: datetime
event_type: EventType
data: dict[str, Any]
__init__(timestamp, event_type, data=<factory>)
Parameters:
Return type:

None

class EventTracker[source]

Bases: object

Track and query portfolio events during a backtest.

Maintains an ordered log of events (trades, rebalances, signals, risk events) and provides query and summary facilities.

Example

>>> tracker = EventTracker()
>>> tracker.log_trade(datetime(2024, 1, 2), "AAPL", "buy", 100, 150.0)
>>> tracker.summary()["total_events"]
1
__init__()[source]
Return type:

None

log_trade(timestamp, asset, side, quantity, price, metadata=None)[source]

Log a trade event.

Parameters:
  • timestamp (datetime) – Trade execution time.

  • asset (str) – Instrument identifier.

  • side (str) – "buy" or "sell".

  • quantity (float) – Number of units traded.

  • price (float) – Execution price per unit.

  • metadata (dict[str, Any] | None, default: None) – Additional trade metadata.

Returns:

The logged event.

Return type:

Event

log_rebalance(timestamp, old_weights, new_weights, reason='')[source]

Log a portfolio rebalance event.

Parameters:
  • timestamp (datetime) – When the rebalance occurred.

  • old_weights (dict[str, float]) – Pre-rebalance weights by asset.

  • new_weights (dict[str, float]) – Post-rebalance weights by asset.

  • reason (str, default: '') – Reason for the rebalance (e.g., "scheduled", "drift").

Returns:

The logged event.

Return type:

Event

log_signal(timestamp, signal_name, value, metadata=None)[source]

Log a signal generation event.

Parameters:
  • timestamp (datetime) – When the signal was generated.

  • signal_name (str) – Name of the signal.

  • value (float) – Signal value.

  • metadata (dict[str, Any] | None, default: None) – Additional signal metadata.

Returns:

The logged event.

Return type:

Event

log_risk_event(timestamp, event_type, details=None)[source]

Log a risk event (VaR breach, drawdown threshold, etc.).

Parameters:
  • timestamp (datetime) – When the risk event was detected.

  • event_type (str) – Type of risk event (e.g., "var_breach", "drawdown_threshold").

  • details (dict[str, Any] | None, default: None) – Event-specific details.

Returns:

The logged event.

Return type:

Event

get_events(event_type=None, start=None, end=None)[source]

Query events by type and/or time range.

Parameters:
  • event_type (EventType | str | None, default: None) – Filter to a specific event type.

  • start (datetime | None, default: None) – Inclusive lower bound on timestamp.

  • end (datetime | None, default: None) – Inclusive upper bound on timestamp.

Returns:

Matching events in chronological order.

Return type:

list[Event]

summary()[source]

Return summary statistics for all logged events.

Returns:

Dictionary with total_events and per-type counts.

Return type:

dict[str, Any]

to_dataframe()[source]

Convert the event log to a DataFrame.

Returns:

DataFrame with columns timestamp, event_type, and one column per data key.

Return type:

DataFrame

class EventType[source]

Bases: str, Enum

Enumeration of trackable event types.

TRADE = 'trade'
REBALANCE = 'rebalance'
SIGNAL = 'signal'
RISK = 'risk'
REGIME_CHANGE = 'regime_change'
DRAWDOWN = 'drawdown'
__new__(value)
detect_drawdown_events(returns, threshold=-0.05)[source]

Detect drawdown events that exceed a given threshold.

Parameters:
  • returns (Series) – Asset or portfolio return series.

  • threshold (float, default: -0.05) – Drawdown level (negative) below which events are recorded. Default -0.05 (5 % drawdown).

Returns:

DataFrame with columns start, end, trough_date, depth, and duration for each drawdown event.

Return type:

DataFrame

detect_regime_changes(returns, method='rolling_vol', window=63, threshold=1.5)[source]

Detect regime transitions in a return series.

Parameters:
  • returns (Series) – Asset or portfolio returns.

  • method (str, default: 'rolling_vol') – Detection method. Supported: "rolling_vol" (default) uses a ratio of short-term to long-term rolling volatility, "mean_shift" uses a shift in the rolling mean.

  • window (int, default: 63) – Lookback window (number of periods).

  • threshold (float, default: 1.5) – Multiplier above which a regime change is flagged.

Returns:

DataFrame with columns timestamp, regime, and indicator for each detected transition point.

Return type:

DataFrame

class PositionSizer[source]

Bases: object

Collection of position-sizing algorithms.

All methods are stateless class methods so the sizer can be used as a lightweight namespace without instantiation.

Example

>>> PositionSizer.fixed_fraction(100_000, 0.02)
2000.0
static fixed_fraction(equity, risk_pct)[source]

Fixed-fraction position sizing.

Parameters:
  • equity (float) – Current portfolio equity.

  • risk_pct (float) – Fraction of equity to risk (e.g., 0.02 for 2 %).

Returns:

Dollar amount to allocate.

Return type:

float

static kelly_criterion(win_rate, avg_win, avg_loss)[source]

Kelly criterion optimal fraction.

Parameters:
  • win_rate (float) – Probability of a winning trade (0-1).

  • avg_win (float) – Average winning trade return (positive).

  • avg_loss (float) – Average losing trade return (positive magnitude).

Returns:

Optimal fraction of capital to risk. Clamped to [0, 1].

Return type:

float

static volatility_targeting(returns, target_vol, lookback=20)[source]

Volatility-targeting position scalar.

Computes the leverage / de-leverage factor so that the portfolio’s annualised volatility approximates target_vol.

Parameters:
  • returns (Series) – Recent asset returns.

  • target_vol (float) – Desired annualised volatility (e.g., 0.10 for 10 %).

  • lookback (int, default: 20) – Number of recent periods for vol estimation.

Returns:

Scalar multiplier for the position size.

Return type:

float

static risk_parity_weights(cov_matrix)[source]

Risk-parity portfolio weights.

Each asset contributes equally to total portfolio risk. Uses an inverse-volatility heuristic that provides a good approximation for portfolios with moderate correlations.

Parameters:

cov_matrix (DataFrame | ndarray[tuple[Any, ...], dtype[floating]]) – Covariance matrix of asset returns (n x n).

Returns:

Weight vector summing to 1.

Return type:

ndarray[tuple[Any, ...], dtype[floating]]

static equal_risk_contribution(cov_matrix)[source]

Equal Risk Contribution (ERC) portfolio weights.

Solves for weights such that each asset’s marginal contribution to total portfolio variance is identical.

Parameters:

cov_matrix (DataFrame | ndarray[tuple[Any, ...], dtype[floating]]) – Covariance matrix of asset returns (n x n).

Returns:

Weight vector summing to 1.

Return type:

ndarray[tuple[Any, ...], dtype[floating]]

clip_weights(weights, min_w=0.0, max_w=1.0)[source]

Clip portfolio weights and re-normalise to sum to 1.

Parameters:
Returns:

Clipped and re-normalised weights.

Return type:

Series | ndarray[tuple[Any, ...], dtype[floating]]

invert_signal(signal)[source]

Flip long/short signals (multiply by -1).

Parameters:

signal (Series | DataFrame | ndarray[tuple[Any, ...], dtype[floating]]) – Signal values where positive = long, negative = short.

Returns:

Inverted signal.

Return type:

Series | DataFrame | ndarray[tuple[Any, ...], dtype[floating]]

rebalance_threshold(current_weights, target_weights, threshold=0.05)[source]

Check whether portfolio drift exceeds a rebalance threshold.

Parameters:
Returns:

True if any weight has drifted beyond threshold and a rebalance is recommended.

Return type:

bool

risk_parity_position(cov_matrix, target_vol=None)[source]

Position sizing using risk parity (equal risk contribution).

Computes portfolio weights such that each asset contributes equally to total portfolio risk, then optionally scales the weights so that the portfolio’s annualised volatility matches target_vol.

This is a convenience wrapper around PositionSizer.equal_risk_contribution that adds volatility targeting and returns a clean weight vector.

Mathematical formulation:
For each asset i, the risk contribution is:

RC_i = w_i * (Cov @ w)_i

We solve for w such that RC_i = RC_j for all i, j, subject to sum(w) = 1.

If target_vol is provided, the weights are scaled by target_vol / portfolio_vol.

How to interpret:
  • Weights will be higher for lower-volatility assets and lower for higher-volatility assets.

  • In a diagonal covariance matrix, risk parity reduces to inverse volatility weighting.

  • With non-zero correlations, the optimiser also accounts for diversification benefit.

When to use:

Use risk parity when you want a balanced portfolio where no single asset dominates the risk budget. Particularly popular for multi-asset and all-weather portfolios.

Parameters:
  • cov_matrix (DataFrame | ndarray[tuple[Any, ...], dtype[floating]]) – Covariance matrix of asset returns (n x n). Can be a pandas DataFrame or numpy array.

  • target_vol (float | None, default: None) – Target annualised portfolio volatility (e.g., 0.10 for 10 %). If None, weights sum to 1 without vol scaling.

Return type:

ndarray[tuple[Any, ...], dtype[floating]]

Returns:

Weight array. Sums to 1 if target_vol is None; otherwise scaled to achieve the target volatility.

Example

>>> import numpy as np
>>> cov = np.array([[0.04, 0.01], [0.01, 0.09]])
>>> w = risk_parity_position(cov)
>>> abs(w.sum() - 1.0) < 1e-6
True
>>> w[0] > w[1]  # lower-vol asset gets more weight
True

See also

PositionSizer.equal_risk_contribution: Core ERC optimiser. PositionSizer.risk_parity_weights: Simpler inverse-vol heuristic. regime_conditional_sizing: Adjust weights based on market regime.

regime_conditional_sizing(base_weights, regime_probabilities, risk_multipliers)[source]

Adjust position sizes based on current regime probabilities.

Scales base portfolio weights by a regime-dependent risk multiplier. The effective multiplier is a probability-weighted average of the per-regime multipliers, ensuring smooth transitions between regimes.

Mathematical formulation:

effective_multiplier = sum(P(regime_i) * multiplier_i) adjusted_weights = base_weights * effective_multiplier

How to interpret:
  • If the current regime is “high_vol” with probability 0.8 and the risk multiplier for “high_vol” is 0.5, the weights will be scaled down significantly.

  • If the regime is “normal” (multiplier = 1.0), weights remain unchanged.

  • Multipliers > 1.0 increase exposure (e.g., during low-vol regimes).

When to use:

Use regime-conditional sizing when your strategy should adjust leverage or exposure based on market conditions. Pair with regime detection (HMM, rolling volatility, etc.) to automatically reduce risk during turbulent markets.

Parameters:
  • base_weights (ndarray[tuple[Any, ...], dtype[floating]] | Series) – Base portfolio weights (array or Series).

  • regime_probabilities (dict[str, float]) – Mapping of regime name to probability (e.g., {"normal": 0.3, "high_vol": 0.7}). Probabilities should sum to 1 but are not strictly enforced.

  • risk_multipliers (dict[str, float]) – Mapping of regime name to risk multiplier (e.g., {"normal": 1.0, "high_vol": 0.5, "low_vol": 1.5}). Regimes in regime_probabilities that are missing from risk_multipliers default to a multiplier of 1.0.

Return type:

ndarray[tuple[Any, ...], dtype[floating]]

Returns:

Adjusted weight array. May not sum to 1 (the multiplier acts as a leverage/de-leverage factor).

Example

>>> import numpy as np
>>> base = np.array([0.5, 0.3, 0.2])
>>> probs = {"normal": 0.3, "high_vol": 0.7}
>>> mults = {"normal": 1.0, "high_vol": 0.5}
>>> adj = regime_conditional_sizing(base, probs, mults)
>>> adj.sum() < base.sum()  # scaled down due to high_vol
True

See also

risk_parity_position: Risk-parity-based weight computation. PositionSizer.volatility_targeting: Vol-targeting scalar.

regime_signal_filter(signals, regime_probs, active_regime=0, min_prob=0.6)[source]

Filter trading signals to only trade in favorable regimes.

Zeros out signals when the probability of the active regime is below min_prob. Useful for avoiding trades during crisis regimes or only trading during trending markets.

Parameters:
  • signals (Series | ndarray) – Raw trading signals (1=long, -1=short, 0=flat).

  • regime_probs (ndarray) – Regime probability matrix (T, K) from detect_regimes().

  • active_regime (int, default: 0) – Which regime to trade in (0=low vol by convention).

  • min_prob (float, default: 0.6) – Minimum probability threshold to allow trading.

Return type:

Series

Returns:

Filtered signals (same shape, zeros where regime inactive).

Example

>>> from wraquant.regimes import detect_regimes
>>> from wraquant.backtest.position import regime_signal_filter
>>> regimes = detect_regimes(returns, method="hmm", n_regimes=2)
>>> filtered = regime_signal_filter(signals, regimes.probabilities,
...                                 active_regime=0, min_prob=0.6)
comprehensive_tearsheet(returns, benchmark=None, risk_free=0.0, periods_per_year=252, trades_df=None, regime_states=None)[source]

Generate a complete performance report combining all available metrics.

This is the “kitchen sink” tearsheet — it computes every metric in the backtesting module and organises them into a nested dictionary that can feed directly into dashboards and visualisation tools.

The returned dictionary contains the following top-level keys:

  • summary: Core risk/return metrics from generate_tearsheet.

  • extended_metrics: All metrics from backtest.metrics (Omega, Burke, UPI, Kappa, tail, Rachev, SQN, etc.).

  • monthly_returns: Monthly return table (years x months).

  • yearly_returns: Annual compounded returns.

  • drawdown_analysis: Top 5 drawdowns with dates and durations.

  • rolling_metrics: Rolling 3m/6m/12m Sharpe, vol, and return.

  • trade_analysis: Per-trade statistics (if trades_df is provided).

  • regime_performance: Performance broken out by regime state (if regime_states is provided).

Parameters:
  • returns (Series) – Simple return series with a DatetimeIndex.

  • benchmark (Series | None, default: None) – Benchmark return series for relative metrics.

  • risk_free (float, default: 0.0) – Annual risk-free rate.

  • periods_per_year (int, default: 252) – Trading periods per year (252 for daily).

  • trades_df (DataFrame | None, default: None) – Trade log DataFrame with a pnl column. If provided, trade-level analysis is included.

  • regime_states (Series | None, default: None) – Series of regime labels (e.g., “bull”, “bear”, “normal”) aligned with returns. If provided, the tearsheet includes per-regime performance breakdowns.

Return type:

dict[str, Any]

Returns:

Nested dictionary with all metrics and analysis tables.

Example

>>> import pandas as pd, numpy as np
>>> rng = np.random.default_rng(42)
>>> rets = pd.Series(
...     rng.normal(0.0004, 0.01, 504),
...     index=pd.bdate_range("2022-01-03", periods=504),
... )
>>> ts = comprehensive_tearsheet(rets)
>>> "summary" in ts and "extended_metrics" in ts
True

See also

generate_tearsheet: Core performance metrics only. strategy_comparison: Side-by-side comparison of multiple strategies.

drawdown_table(returns, top_n=5)[source]

Return the top N drawdown periods with metadata.

Parameters:
  • returns (Series) – Portfolio return series.

  • top_n (int, default: 5) – Number of worst drawdowns to report.

Returns:

Columns: peak_date, trough_date, recovery_date, depth, duration (periods from peak to recovery or end).

Return type:

DataFrame

generate_tearsheet(returns, benchmark=None, risk_free=0.0, periods_per_year=252)[source]

Generate a comprehensive performance tearsheet dictionary.

Parameters:
  • returns (Series) – Portfolio return series (simple, not log).

  • benchmark (Series | None, default: None) – Benchmark return series for relative metrics.

  • risk_free (float, default: 0.0) – Annualised risk-free rate.

  • periods_per_year (int, default: 252) – Trading periods per year (252 for daily).

Returns:

Dictionary containing absolute and (optionally) relative performance metrics.

Return type:

dict[str, Any]

monthly_returns_table(returns)[source]

Compute a table of monthly returns suitable for heatmap display.

Parameters:

returns (Series) – Daily (or intraday) return series with a DatetimeIndex.

Returns:

Rows = years, columns = months (1-12). Values are total returns for that month.

Return type:

DataFrame

rolling_metrics_table(returns, windows=None, periods_per_year=252)[source]

Compute rolling Sharpe, volatility, and return at multiple windows.

Parameters:
  • returns (Series) – Portfolio return series.

  • windows (list[int] | None, default: None) – Rolling window sizes in periods. Default [21, 63, 126, 252].

  • periods_per_year (int, default: 252) – Trading periods per year.

Returns:

MultiIndex columns: (window, metric) with metrics rolling_return, rolling_vol, rolling_sharpe.

Return type:

DataFrame

strategy_comparison(strategies, risk_free=0.0, periods_per_year=252)[source]

Compare multiple strategies side-by-side on all metrics.

Computes a comprehensive set of performance metrics for each strategy and returns them in a single DataFrame where columns are strategy names and rows are metric names. The best and worst values for each metric are easy to spot by sorting.

Parameters:
  • strategies (dict[str, Series]) – Mapping of {strategy_name: returns_series}.

  • risk_free (float, default: 0.0) – Annual risk-free rate.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

DataFrame

Returns:

DataFrame with metric names as the index and strategy names as columns. Contains all core and extended metrics.

Example

>>> import pandas as pd, numpy as np
>>> rng = np.random.default_rng(42)
>>> strats = {
...     "momentum": pd.Series(rng.normal(0.0005, 0.012, 252)),
...     "mean_rev": pd.Series(rng.normal(0.0003, 0.008, 252)),
... }
>>> comp = strategy_comparison(strats)
>>> "momentum" in comp.columns and "mean_rev" in comp.columns
True

See also

comprehensive_tearsheet: Full report for a single strategy. generate_tearsheet: Core metrics for a single strategy.

trade_analysis(trades_df)[source]

Analyse trade-level performance.

Parameters:

trades_df (DataFrame) – Must contain a pnl column with per-trade profit/loss values. Optionally includes entry_price, exit_price, side, etc.

Returns:

Dictionary with win_rate, avg_pnl, avg_win, avg_loss, profit_factor, expectancy, max_win, max_loss, n_trades.

Return type:

dict[str, float]

vectorbt_backtest(prices, entries, exits, **kwargs)[source]

Run a vectorised backtest using vectorbt.

Parameters:
  • prices (Series | DataFrame) – Price data aligned with the entry/exit signals.

  • entries (Series | DataFrame) – Boolean series/DataFrame indicating entry signals.

  • exits (Series | DataFrame) – Boolean series/DataFrame indicating exit signals.

  • **kwargs (Any) – Additional keyword arguments forwarded to vectorbt.Portfolio.from_signals.

Returns:

Dictionary containing:

  • total_return – total portfolio return.

  • sharpe_ratio – annualised Sharpe ratio.

  • max_drawdown – maximum drawdown.

  • total_trades – number of trades executed.

  • win_rate – fraction of winning trades.

  • portfolio – the raw vectorbt.Portfolio object.

Return type:

dict[str, Any]

quantstats_report(returns, benchmark=None, output=None)[source]

Generate performance analytics using quantstats.

Parameters:
  • returns (Series) – Strategy return series (simple returns).

  • benchmark (Series | None, default: None) – Benchmark return series for comparison.

  • output (str | None, default: None) – File path for the HTML report. When None, no file is written.

Returns:

Dictionary containing:

  • sharpe – annualised Sharpe ratio.

  • sortino – annualised Sortino ratio.

  • max_drawdown – maximum drawdown.

  • cagr – compound annual growth rate.

  • volatility – annualised volatility.

  • calmar – Calmar ratio.

Return type:

dict[str, Any]

empyrical_metrics(returns)[source]

Compute a comprehensive set of metrics using empyrical.

Parameters:

returns (Series) – Simple return series.

Returns:

Dictionary containing:

  • annual_return – annualised return.

  • annual_volatility – annualised volatility.

  • sharpe_ratio – annualised Sharpe ratio.

  • sortino_ratio – annualised Sortino ratio.

  • max_drawdown – maximum drawdown.

  • calmar_ratio – Calmar ratio.

  • omega_ratio – Omega ratio.

  • tail_ratio – tail ratio (95th / 5th percentile).

  • stability – R-squared of cumulative log returns.

Return type:

dict[str, float]

pyfolio_tearsheet_data(returns, positions=None, transactions=None)[source]

Prepare data in the format expected by pyfolio tearsheets.

This function does not render plots but returns the intermediate data structures that pyfolio uses internally, making it possible to inspect results programmatically.

Parameters:
  • returns (Series) – Strategy return series with a DatetimeIndex.

  • positions (DataFrame | None, default: None) – Position sizes over time. Columns are asset names, values are dollar positions.

  • transactions (DataFrame | None, default: None) – Trade log. Expected columns: amount, price, symbol.

Returns:

Dictionary containing:

  • returns – the input return series.

  • cum_returns – cumulative return series.

  • drawdown – drawdown series.

  • positions – positions DataFrame (or None).

  • transactions – transactions DataFrame (or None).

Return type:

dict[str, Any]

ffn_stats(prices)[source]

Compute performance statistics using ffn.

Parameters:

prices (Series | DataFrame) – Price series or multi-asset price DataFrame.

Returns:

Dictionary containing key performance metrics:

  • total_return – total return over the period.

  • cagr – compound annual growth rate.

  • daily_sharpe – daily Sharpe ratio.

  • max_drawdown – maximum drawdown.

  • avg_drawdown – average drawdown.

  • monthly_sharpe – monthly Sharpe ratio.

  • stats_object – the raw ffn.PerformanceStats object.

Return type:

dict[str, Any]

Engine

Vectorized backtesting engine.

class Backtest[source]

Bases: object

Vectorized backtesting engine.

Parameters:
  • strategy (Strategy) – Strategy instance.

  • initial_capital (float, default: 100000.0) – Starting portfolio value.

  • commission (float, default: 0.0) – Commission per trade as fraction (e.g., 0.001 = 10bps).

  • slippage (float, default: 0.0) – Slippage per trade as fraction.

Example

>>> from wraquant.backtest import Backtest
>>> from wraquant.backtest.strategy import BuyAndHold
>>> bt = Backtest(BuyAndHold(), initial_capital=100_000)
>>> result = bt.run(prices_df)
__init__(strategy, initial_capital=100000.0, commission=0.0, slippage=0.0)[source]
Parameters:
  • strategy (Strategy)

  • initial_capital (float, default: 100000.0)

  • commission (float, default: 0.0)

  • slippage (float, default: 0.0)

Return type:

None

run(prices)[source]

Run the backtest on historical price data.

Parameters:

prices (DataFrame) – DataFrame of asset prices (columns = assets).

Return type:

BacktestResult

Returns:

BacktestResult with portfolio value, returns, positions, metrics.

class VectorizedBacktest[source]

Bases: object

Fast vectorized backtesting engine for signal-based strategies.

Unlike the event-driven Backtest class (which requires a Strategy object), VectorizedBacktest operates directly on a pre-computed signal / weight matrix. This is ideal for research workflows where signals are generated upstream (e.g., from a machine learning model or factor pipeline) and you need fast, repeatable backtest evaluation.

The engine computes portfolio returns accounting for:

  • Transaction costs (fixed commission per unit of turnover).

  • Slippage (additional cost proportional to turnover).

  • Rebalancing frequency (skip rebalancing on off-days to reduce turnover).

Parameters:
  • initial_capital (float, default: 100000.0) – Starting portfolio value in currency units.

  • commission (float, default: 0.0) – One-way commission as a fraction of turnover (e.g., 0.001 = 10 bps).

  • slippage (float, default: 0.0) – Slippage as a fraction of turnover.

  • rebalance_frequency (int, default: 1) – Rebalance every N periods. 1 means every period (daily for daily data), 5 means weekly, etc.

Example

>>> import pandas as pd, numpy as np
>>> rng = np.random.default_rng(42)
>>> prices = pd.DataFrame(
...     100 * np.exp(np.cumsum(rng.normal(0.0005, 0.02, (100, 2)), axis=0)),
...     columns=["A", "B"],
...     index=pd.bdate_range("2023-01-01", periods=100),
... )
>>> signals = pd.DataFrame(
...     0.5, index=prices.index, columns=prices.columns
... )
>>> bt = VectorizedBacktest(commission=0.001, slippage=0.0005)
>>> result = bt.run(prices, signals)
>>> isinstance(result, BacktestResult)
True

See also

Backtest: Strategy-object-based backtesting engine. walk_forward_backtest: Walk-forward optimisation + backtest.

__init__(initial_capital=100000.0, commission=0.0, slippage=0.0, rebalance_frequency=1)[source]
Parameters:
  • initial_capital (float, default: 100000.0)

  • commission (float, default: 0.0)

  • slippage (float, default: 0.0)

  • rebalance_frequency (int, default: 1)

Return type:

None

run(prices, signals)[source]

Execute the vectorized backtest.

Parameters:
  • prices (DataFrame) – Asset price DataFrame (rows = dates, columns = assets).

  • signals (DataFrame) – Weight / signal DataFrame with the same shape as prices. Values represent desired portfolio weights (e.g., 0.5 = 50 % allocation to that asset). Signals are shifted by one period internally to avoid look-ahead bias.

Return type:

BacktestResult

Returns:

BacktestResult with equity curve, returns, positions, and metrics. Additional fields (turnover, costs, net_returns, gross_returns) are available via dict-like access on the metrics dict.

walk_forward_backtest(prices, strategy_factory, param_grid, train_size=252, test_size=63, step_size=None, metric='sharpe_ratio', initial_capital=100000.0, commission=0.0, slippage=0.0)[source]

Walk-forward optimisation and backtesting.

Walk-forward analysis is the gold standard for evaluating parameter-dependent strategies. It splits the data into rolling train/test windows, optimises strategy parameters on the training set, and evaluates on the out-of-sample test set. The result is a true out-of-sample equity curve that avoids look-ahead bias and parameter overfitting.

Algorithm:
  1. For each window starting at t: a. Train set: prices[t : t + train_size] b. Test set: prices[t + train_size : t + train_size + test_size]

  2. For each parameter combination in param_grid, backtest on the train set and record the optimisation metric.

  3. Select the best parameters and backtest on the test set.

  4. Slide forward by step_size and repeat.

  5. Concatenate all out-of-sample test returns.

Parameters:
  • prices (DataFrame) – Asset price DataFrame.

  • strategy_factory (Callable[..., Strategy]) – Callable that accepts keyword arguments (from param_grid) and returns a Strategy instance.

  • param_grid (list[dict[str, Any]]) – List of parameter dictionaries to search over.

  • train_size (int, default: 252) – Number of periods in each training window.

  • test_size (int, default: 63) – Number of periods in each test window.

  • step_size (int | None, default: None) – Number of periods to slide the window forward. Defaults to test_size (non-overlapping test windows).

  • metric (str, default: 'sharpe_ratio') – Performance metric to optimise (key from performance_summary output, e.g., "sharpe_ratio").

  • initial_capital (float, default: 100000.0) – Starting capital for each window backtest.

  • commission (float, default: 0.0) – Commission fraction.

  • slippage (float, default: 0.0) – Slippage fraction.

Returns:

  • oos_returns: Concatenated out-of-sample return series.

  • is_returns: Concatenated in-sample return series.

  • oos_equity_curve: Equity curve from OOS returns.

  • params_per_window: List of best params per window.

  • is_metrics_per_window: In-sample metrics per window.

  • oos_metrics: Overall OOS performance summary.

  • stability_ratio: Fraction of windows where OOS Sharpe > 0.

Return type:

dict[str, Any]

Example

>>> import pandas as pd, numpy as np
>>> from wraquant.backtest.strategy import MomentumStrategy
>>> rng = np.random.default_rng(42)
>>> prices = pd.DataFrame(
...     100 * np.exp(np.cumsum(rng.normal(0.0003, 0.015, (600, 2)), axis=0)),
...     columns=["A", "B"],
...     index=pd.bdate_range("2020-01-01", periods=600),
... )
>>> grid = [{"lookback": 10}, {"lookback": 20}, {"lookback": 40}]
>>> result = walk_forward_backtest(
...     prices,
...     strategy_factory=lambda **kw: MomentumStrategy(**kw),
...     param_grid=grid,
...     train_size=200,
...     test_size=50,
... )
>>> "oos_returns" in result
True

See also

Backtest: Single-pass backtesting engine. VectorizedBacktest: Signal-matrix-based backtesting.

Strategy

Strategy abstract base class and common strategies.

class Strategy[source]

Bases: ABC

Abstract base class for trading strategies.

Subclasses must implement generate_signals which maps prices to position signals (-1, 0, +1 or fractional).

Example

>>> class MACrossover(Strategy):
...     def __init__(self, fast: int = 10, slow: int = 50):
...         self.fast = fast
...         self.slow = slow
...
...     def generate_signals(self, prices: pd.DataFrame) -> pd.DataFrame:
...         fast_ma = prices.rolling(self.fast).mean()
...         slow_ma = prices.rolling(self.slow).mean()
...         return (fast_ma > slow_ma).astype(float)
abstractmethod generate_signals(prices)[source]

Generate position signals from price data.

Parameters:

prices (DataFrame) – DataFrame of asset prices (columns = assets).

Return type:

DataFrame

Returns:

DataFrame of signals with same shape as prices. Values represent desired position: 1 = long, -1 = short, 0 = flat.

class BuyAndHold[source]

Bases: Strategy

Buy and hold strategy — always fully invested.

generate_signals(prices)[source]

Generate position signals from price data.

Parameters:

prices (DataFrame) – DataFrame of asset prices (columns = assets).

Return type:

DataFrame

Returns:

DataFrame of signals with same shape as prices. Values represent desired position: 1 = long, -1 = short, 0 = flat.

class MomentumStrategy[source]

Bases: Strategy

Simple momentum strategy based on lookback returns.

Parameters:
  • lookback (int, default: 20) – Number of periods for momentum calculation.

  • top_n (int | None, default: None) – Number of top assets to go long.

__init__(lookback=20, top_n=None)[source]
Parameters:
  • lookback (int, default: 20)

  • top_n (int | None, default: None)

Return type:

None

generate_signals(prices)[source]

Generate position signals from price data.

Parameters:

prices (DataFrame) – DataFrame of asset prices (columns = assets).

Return type:

DataFrame

Returns:

DataFrame of signals with same shape as prices. Values represent desired position: 1 = long, -1 = short, 0 = flat.

class MeanReversionStrategy[source]

Bases: Strategy

Mean reversion strategy using z-score.

Parameters:
  • window (int, default: 20) – Lookback window for mean/std.

  • entry_z (float, default: 2.0) – Z-score threshold to enter position.

  • exit_z (float, default: 0.5) – Z-score threshold to exit position.

__init__(window=20, entry_z=2.0, exit_z=0.5)[source]
Parameters:
  • window (int, default: 20)

  • entry_z (float, default: 2.0)

  • exit_z (float, default: 0.5)

Return type:

None

generate_signals(prices)[source]

Generate position signals from price data.

Parameters:

prices (DataFrame) – DataFrame of asset prices (columns = assets).

Return type:

DataFrame

Returns:

DataFrame of signals with same shape as prices. Values represent desired position: 1 = long, -1 = short, 0 = flat.

Position Sizing

Enhanced position sizing and weight management utilities.

Provides position sizing algorithms (fixed-fraction, Kelly, vol-targeting, risk parity, equal risk contribution), signal inversion, weight clipping, and rebalance threshold logic.

class PositionSizer[source]

Bases: object

Collection of position-sizing algorithms.

All methods are stateless class methods so the sizer can be used as a lightweight namespace without instantiation.

Example

>>> PositionSizer.fixed_fraction(100_000, 0.02)
2000.0
static fixed_fraction(equity, risk_pct)[source]

Fixed-fraction position sizing.

Parameters:
  • equity (float) – Current portfolio equity.

  • risk_pct (float) – Fraction of equity to risk (e.g., 0.02 for 2 %).

Returns:

Dollar amount to allocate.

Return type:

float

static kelly_criterion(win_rate, avg_win, avg_loss)[source]

Kelly criterion optimal fraction.

Parameters:
  • win_rate (float) – Probability of a winning trade (0-1).

  • avg_win (float) – Average winning trade return (positive).

  • avg_loss (float) – Average losing trade return (positive magnitude).

Returns:

Optimal fraction of capital to risk. Clamped to [0, 1].

Return type:

float

static volatility_targeting(returns, target_vol, lookback=20)[source]

Volatility-targeting position scalar.

Computes the leverage / de-leverage factor so that the portfolio’s annualised volatility approximates target_vol.

Parameters:
  • returns (Series) – Recent asset returns.

  • target_vol (float) – Desired annualised volatility (e.g., 0.10 for 10 %).

  • lookback (int, default: 20) – Number of recent periods for vol estimation.

Returns:

Scalar multiplier for the position size.

Return type:

float

static risk_parity_weights(cov_matrix)[source]

Risk-parity portfolio weights.

Each asset contributes equally to total portfolio risk. Uses an inverse-volatility heuristic that provides a good approximation for portfolios with moderate correlations.

Parameters:

cov_matrix (DataFrame | ndarray[tuple[Any, ...], dtype[floating]]) – Covariance matrix of asset returns (n x n).

Returns:

Weight vector summing to 1.

Return type:

ndarray[tuple[Any, ...], dtype[floating]]

static equal_risk_contribution(cov_matrix)[source]

Equal Risk Contribution (ERC) portfolio weights.

Solves for weights such that each asset’s marginal contribution to total portfolio variance is identical.

Parameters:

cov_matrix (DataFrame | ndarray[tuple[Any, ...], dtype[floating]]) – Covariance matrix of asset returns (n x n).

Returns:

Weight vector summing to 1.

Return type:

ndarray[tuple[Any, ...], dtype[floating]]

invert_signal(signal)[source]

Flip long/short signals (multiply by -1).

Parameters:

signal (Series | DataFrame | ndarray[tuple[Any, ...], dtype[floating]]) – Signal values where positive = long, negative = short.

Returns:

Inverted signal.

Return type:

Series | DataFrame | ndarray[tuple[Any, ...], dtype[floating]]

clip_weights(weights, min_w=0.0, max_w=1.0)[source]

Clip portfolio weights and re-normalise to sum to 1.

Parameters:
Returns:

Clipped and re-normalised weights.

Return type:

Series | ndarray[tuple[Any, ...], dtype[floating]]

rebalance_threshold(current_weights, target_weights, threshold=0.05)[source]

Check whether portfolio drift exceeds a rebalance threshold.

Parameters:
Returns:

True if any weight has drifted beyond threshold and a rebalance is recommended.

Return type:

bool

regime_signal_filter(signals, regime_probs, active_regime=0, min_prob=0.6)[source]

Filter trading signals to only trade in favorable regimes.

Zeros out signals when the probability of the active regime is below min_prob. Useful for avoiding trades during crisis regimes or only trading during trending markets.

Parameters:
  • signals (Series | ndarray) – Raw trading signals (1=long, -1=short, 0=flat).

  • regime_probs (ndarray) – Regime probability matrix (T, K) from detect_regimes().

  • active_regime (int, default: 0) – Which regime to trade in (0=low vol by convention).

  • min_prob (float, default: 0.6) – Minimum probability threshold to allow trading.

Return type:

Series

Returns:

Filtered signals (same shape, zeros where regime inactive).

Example

>>> from wraquant.regimes import detect_regimes
>>> from wraquant.backtest.position import regime_signal_filter
>>> regimes = detect_regimes(returns, method="hmm", n_regimes=2)
>>> filtered = regime_signal_filter(signals, regimes.probabilities,
...                                 active_regime=0, min_prob=0.6)
volatility_target_sizing(returns, target_vol=0.15, method='ewma', span=30)[source]

Size positions to target a specific annualised volatility level.

Computes a scalar multiplier so that the portfolio’s expected annualised volatility equals target_vol. The current volatility is estimated via EWMA (wraquant.vol.models.ewma_volatility), providing a responsive, GARCH-inspired estimate that reacts quickly to changing market conditions.

Mathematical formulation:

scalar = target_vol / current_vol

If current_vol is zero or negative (constant returns), the function returns 1.0 (no scaling).

When to use:

Use volatility targeting when you want your strategy to maintain a consistent risk profile regardless of market conditions. This is the foundation of most institutional risk-parity and managed-futures strategies.

Parameters:
  • returns (Series) – Recent asset return series (at least 10 observations).

  • target_vol (float, default: 0.15) – Desired annualised volatility (e.g., 0.15 for 15%).

  • method (str, default: 'ewma') – Volatility estimation method. Currently only "ewma" is supported.

  • span (int, default: 30) – EWMA span parameter (default 30). Higher values produce smoother estimates; lower values react faster to shocks.

Return type:

float

Returns:

Scalar multiplier for position size. Values > 1 indicate the current vol is below target (increase exposure); values < 1 indicate it is above target (reduce exposure).

Example

>>> import pandas as pd, numpy as np
>>> np.random.seed(42)
>>> returns = pd.Series(np.random.normal(0, 0.01, 100))
>>> scalar = volatility_target_sizing(returns, target_vol=0.10)
>>> scalar > 0
True

See also

PositionSizer.volatility_targeting: Simple std-based vol targeting. regime_conditional_sizing: Regime-aware position scaling.

Events

Event tracking system for portfolio backtesting.

Provides structured logging and querying of portfolio events including trades, rebalances, signals, risk events, regime changes, and drawdowns.

class EventType[source]

Bases: str, Enum

Enumeration of trackable event types.

TRADE = 'trade'
REBALANCE = 'rebalance'
SIGNAL = 'signal'
RISK = 'risk'
REGIME_CHANGE = 'regime_change'
DRAWDOWN = 'drawdown'
__new__(value)
class Event[source]

Bases: object

A single portfolio event.

Parameters:
  • timestamp (datetime) – When the event occurred.

  • event_type (EventType) – Category of the event.

  • data (dict[str, Any], default: <factory>) – Event-specific payload.

timestamp: datetime
event_type: EventType
data: dict[str, Any]
__init__(timestamp, event_type, data=<factory>)
Parameters:
Return type:

None

class EventTracker[source]

Bases: object

Track and query portfolio events during a backtest.

Maintains an ordered log of events (trades, rebalances, signals, risk events) and provides query and summary facilities.

Example

>>> tracker = EventTracker()
>>> tracker.log_trade(datetime(2024, 1, 2), "AAPL", "buy", 100, 150.0)
>>> tracker.summary()["total_events"]
1
__init__()[source]
Return type:

None

log_trade(timestamp, asset, side, quantity, price, metadata=None)[source]

Log a trade event.

Parameters:
  • timestamp (datetime) – Trade execution time.

  • asset (str) – Instrument identifier.

  • side (str) – "buy" or "sell".

  • quantity (float) – Number of units traded.

  • price (float) – Execution price per unit.

  • metadata (dict[str, Any] | None, default: None) – Additional trade metadata.

Returns:

The logged event.

Return type:

Event

log_rebalance(timestamp, old_weights, new_weights, reason='')[source]

Log a portfolio rebalance event.

Parameters:
  • timestamp (datetime) – When the rebalance occurred.

  • old_weights (dict[str, float]) – Pre-rebalance weights by asset.

  • new_weights (dict[str, float]) – Post-rebalance weights by asset.

  • reason (str, default: '') – Reason for the rebalance (e.g., "scheduled", "drift").

Returns:

The logged event.

Return type:

Event

log_signal(timestamp, signal_name, value, metadata=None)[source]

Log a signal generation event.

Parameters:
  • timestamp (datetime) – When the signal was generated.

  • signal_name (str) – Name of the signal.

  • value (float) – Signal value.

  • metadata (dict[str, Any] | None, default: None) – Additional signal metadata.

Returns:

The logged event.

Return type:

Event

log_risk_event(timestamp, event_type, details=None)[source]

Log a risk event (VaR breach, drawdown threshold, etc.).

Parameters:
  • timestamp (datetime) – When the risk event was detected.

  • event_type (str) – Type of risk event (e.g., "var_breach", "drawdown_threshold").

  • details (dict[str, Any] | None, default: None) – Event-specific details.

Returns:

The logged event.

Return type:

Event

get_events(event_type=None, start=None, end=None)[source]

Query events by type and/or time range.

Parameters:
  • event_type (EventType | str | None, default: None) – Filter to a specific event type.

  • start (datetime | None, default: None) – Inclusive lower bound on timestamp.

  • end (datetime | None, default: None) – Inclusive upper bound on timestamp.

Returns:

Matching events in chronological order.

Return type:

list[Event]

summary()[source]

Return summary statistics for all logged events.

Returns:

Dictionary with total_events and per-type counts.

Return type:

dict[str, Any]

to_dataframe()[source]

Convert the event log to a DataFrame.

Returns:

DataFrame with columns timestamp, event_type, and one column per data key.

Return type:

DataFrame

detect_regime_changes(returns, method='rolling_vol', window=63, threshold=1.5)[source]

Detect regime transitions in a return series.

Parameters:
  • returns (Series) – Asset or portfolio returns.

  • method (str, default: 'rolling_vol') – Detection method. Supported: "rolling_vol" (default) uses a ratio of short-term to long-term rolling volatility, "mean_shift" uses a shift in the rolling mean.

  • window (int, default: 63) – Lookback window (number of periods).

  • threshold (float, default: 1.5) – Multiplier above which a regime change is flagged.

Returns:

DataFrame with columns timestamp, regime, and indicator for each detected transition point.

Return type:

DataFrame

detect_drawdown_events(returns, threshold=-0.05)[source]

Detect drawdown events that exceed a given threshold.

Parameters:
  • returns (Series) – Asset or portfolio return series.

  • threshold (float, default: -0.05) – Drawdown level (negative) below which events are recorded. Default -0.05 (5 % drawdown).

Returns:

DataFrame with columns start, end, trough_date, depth, and duration for each drawdown event.

Return type:

DataFrame

Metrics

Backtesting performance metrics.

performance_summary(returns, risk_free=0.0, periods_per_year=252)[source]

Calculate comprehensive performance metrics.

Parameters:
  • returns (Series) – Portfolio return series.

  • risk_free (float, default: 0.0) – Annual risk-free rate.

  • periods_per_year (int, default: 252) – Number of trading periods per year.

Return type:

dict

Returns:

Dict with performance metrics.

omega_ratio(returns, threshold=0.0)[source]

Omega ratio: probability-weighted gain/loss ratio above a threshold.

The Omega ratio partitions the return distribution at a threshold L and computes the ratio of the expected gain above L to the expected loss below L. Unlike the Sharpe ratio it uses the entire distribution (all moments, not just the first two), making it more appropriate for non-normal returns.

Mathematical formulation:
Omega(L) = E[max(R - L, 0)] / E[max(L - R, 0)]

= sum(max(r_i - L, 0)) / sum(max(L - r_i, 0))

How to interpret:
  • Omega = 1.0: strategy breaks even relative to the threshold.

  • Omega > 1.0: gains outweigh losses (good).

  • Omega > 2.0: strong risk-adjusted performance.

  • Omega = inf: no returns below the threshold.

  • The higher the better; compare strategies at the same threshold.

When to use:

Use Omega when return distributions are skewed or fat-tailed and you want a single number that captures the full distribution. Prefer Omega over Sharpe for options-based or trend-following strategies whose returns are far from Gaussian.

Parameters:
  • returns (Series) – Simple return series (e.g., daily returns).

  • threshold (float, default: 0.0) – Minimum acceptable return per period. Default 0.

Return type:

float

Returns:

Omega ratio as a float. Returns inf if no returns fall below the threshold.

Example

>>> import pandas as pd
>>> r = pd.Series([0.01, 0.02, -0.005, 0.015, -0.01])
>>> omega_ratio(r, threshold=0.0)
3.0

See also

kappa_ratio: Generalized lower-partial-moment ratio (Kappa(1) = Omega - 1). sharpe_ratio: Mean/std ratio; only uses first two moments.

burke_ratio(returns, periods_per_year=252)[source]

Burke ratio: return per unit of drawdown severity.

The Burke ratio penalises strategies that experience many deep drawdowns by dividing the annualised excess return by the square root of the sum of squared drawdown depths. Compared to Calmar (which uses only the worst drawdown), Burke considers the entire drawdown history.

Mathematical formulation:

Burke = annualized_return / sqrt(sum(d_i^2))

where d_i are the individual drawdown depths (negative values).

How to interpret:
  • Higher is better.

  • A strategy with many small drawdowns can have a higher Burke than one with a single large drawdown, even if their Calmar ratios are identical.

  • There is no universal “good” threshold; use for relative comparison between strategies.

When to use:

Prefer Burke over Calmar when you want to penalise strategies that repeatedly draw down, not just the single worst case.

Parameters:
  • returns (Series) – Simple return series.

  • periods_per_year (int, default: 252) – Trading periods per year (252 for daily).

Return type:

float

Returns:

Burke ratio as a float. Returns 0.0 if there are no drawdowns.

Example

>>> import pandas as pd, numpy as np
>>> r = pd.Series(np.random.default_rng(42).normal(0.001, 0.01, 252))
>>> burke_ratio(r)
1.23

See also

ulcer_performance_index: Uses Ulcer Index (RMS of drawdowns) in denominator. recovery_factor: Net profit / max drawdown.

ulcer_performance_index(returns, periods_per_year=252)[source]

Ulcer Performance Index (UPI): return per unit of drawdown pain.

The UPI (also known as Martin ratio) divides the annualised excess return by the Ulcer Index, which is the root-mean-square of percentage drawdowns. The Ulcer Index captures both the depth and duration of drawdowns, making UPI a comprehensive pain-adjusted return measure.

Mathematical formulation:

Ulcer Index = sqrt(mean(d_i^2)) UPI = annualized_return / Ulcer Index

where d_i is the drawdown at each point in time.

How to interpret:
  • Higher is better.

  • UPI > 2.0 is generally considered very good.

  • UPI accounts for drawdown duration (not just depth), so a strategy that recovers quickly scores better than one that lingers in drawdown.

When to use:

Use UPI when you want a risk-adjusted measure that captures the investor’s real experience of pain (deep, prolonged drawdowns hurt more than brief dips).

Parameters:
  • returns (Series) – Simple return series.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

float

Returns:

UPI as a float. Returns 0.0 if the Ulcer Index is zero.

Example

>>> import pandas as pd, numpy as np
>>> r = pd.Series(np.random.default_rng(42).normal(0.001, 0.01, 252))
>>> ulcer_performance_index(r)
2.5

See also

burke_ratio: Uses sum of squared drawdowns instead of RMS. max_drawdown: Single worst peak-to-trough decline.

kappa_ratio(returns, order=2, threshold=0.0, periods_per_year=252)[source]

Kappa ratio: generalized Sortino using lower partial moments.

The Kappa ratio family generalises the Sortino ratio by using the n-th root of the n-th lower partial moment (LPM) as the risk measure. Special cases:

  • Kappa(1) is equivalent to (Omega - 1) when annualization is removed (first lower partial moment = expected shortfall below threshold).

  • Kappa(2) is the Sortino ratio (second lower partial moment = downside deviation).

  • Kappa(3) penalises large negative returns even more heavily.

Mathematical formulation:

LPM_n = mean(max(L - r_i, 0)^n) Kappa_n = (annualized_mean_excess) / LPM_n^(1/n)

How to interpret:
  • Higher is better (more return per unit of downside risk).

  • Higher orders penalise tail risk more severely.

  • Compare strategies using the same order.

When to use:

Use Kappa(3) when you especially fear large losses. Use Kappa(2) as a drop-in alternative to Sortino. Use Kappa(1) when you want an Omega-style measure in ratio form.

Parameters:
  • returns (Series) – Simple return series.

  • order (int, default: 2) – Moment order n (1, 2, or 3 are typical).

  • threshold (float, default: 0.0) – Minimum acceptable return per period.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

float

Returns:

Kappa ratio as a float. Returns 0.0 if LPM is zero.

Example

>>> import pandas as pd, numpy as np
>>> r = pd.Series(np.random.default_rng(42).normal(0.001, 0.01, 252))
>>> kappa_ratio(r, order=2)
1.8

See also

omega_ratio: Threshold-based gain/loss ratio. sortino_ratio: Equivalent to Kappa(2).

tail_ratio(returns, upper_pct=95.0, lower_pct=5.0)[source]

Tail ratio: magnitude of right tail vs. left tail.

The tail ratio measures how large positive outlier returns are relative to negative outlier returns. A value greater than 1 means the strategy’s best days are proportionally larger than its worst days.

Mathematical formulation:

Tail ratio = |percentile(R, upper_pct)| / |percentile(R, lower_pct)|

How to interpret:
  • Tail ratio > 1.0: right tail is fatter (upside surprises bigger than downside). Desirable.

  • Tail ratio = 1.0: symmetric tails.

  • Tail ratio < 1.0: left tail is fatter (downside risk dominates).

When to use:

Quick diagnostic to check if a strategy has favorable tail behaviour. Combine with common_sense_ratio for a sanity check.

Parameters:
  • returns (Series) – Simple return series.

  • upper_pct (float, default: 95.0) – Upper percentile (default 95).

  • lower_pct (float, default: 5.0) – Lower percentile (default 5).

Return type:

float

Returns:

Tail ratio as a float. Returns inf if the lower percentile is zero.

Example

>>> import pandas as pd
>>> r = pd.Series([0.03, 0.01, -0.01, 0.02, -0.005])
>>> tail_ratio(r)
2.0

See also

common_sense_ratio: Tail ratio multiplied by (1 + Sharpe). rachev_ratio: CVaR-based tail comparison.

common_sense_ratio(returns, risk_free=0.0, periods_per_year=252)[source]

Common Sense Ratio: quick sanity check combining Sharpe and tails.

The Common Sense Ratio multiplies the tail ratio by (1 + Sharpe). It combines a measure of tail asymmetry with overall risk-adjusted performance into a single number for rapid strategy screening.

Mathematical formulation:

CSR = tail_ratio * (1 + Sharpe)

How to interpret:
  • CSR > 1.0: strategy has both favorable tails and positive risk-adjusted return. Good.

  • CSR < 1.0: either tails are unfavorable or risk-adjusted return is poor.

  • Use for fast initial screening; not a substitute for deeper analysis.

When to use:

Use CSR as a first-pass filter when evaluating many strategies simultaneously.

Parameters:
  • returns (Series) – Simple return series.

  • risk_free (float, default: 0.0) – Annual risk-free rate.

  • periods_per_year (int, default: 252) – Trading periods per year.

Return type:

float

Returns:

Common Sense Ratio as a float.

Example

>>> import pandas as pd, numpy as np
>>> r = pd.Series(np.random.default_rng(42).normal(0.001, 0.01, 252))
>>> common_sense_ratio(r)
1.5

See also

tail_ratio: Upside vs downside tail magnitude. sharpe_ratio: Mean/std risk-adjusted return.

rachev_ratio(returns, alpha=0.05)[source]

Rachev ratio: CVaR of gains over CVaR of losses.

The Rachev ratio (also called the Conditional Tail Ratio) compares the expected size of extreme gains to the expected size of extreme losses, using Conditional Value at Risk (CVaR, a.k.a. Expected Shortfall). Unlike the simple tail ratio, Rachev uses the mean of the tail rather than a single percentile, making it more robust to outliers.

Mathematical formulation:

CVaR_alpha(gains) = E[R | R > VaR_{1-alpha}(R)] CVaR_alpha(losses) = E[-R | R < VaR_alpha(R)] Rachev = CVaR_alpha(gains) / CVaR_alpha(losses)

How to interpret:
  • Rachev > 1.0: expected extreme gains exceed expected extreme losses. The strategy has favorable fat-tail asymmetry.

  • Rachev = 1.0: symmetric tail risk.

  • Rachev < 1.0: tail risk is skewed to the downside.

When to use:

Use Rachev when you need a fat-tail-aware gain/loss comparison. More robust than tail_ratio because it averages over the tail rather than using a single percentile.

Parameters:
  • returns (Series) – Simple return series.

  • alpha (float, default: 0.05) – Tail probability (default 0.05 = 5 % tails).

Return type:

float

Returns:

Rachev ratio as a float. Returns inf if the loss CVaR is zero.

Example

>>> import pandas as pd, numpy as np
>>> r = pd.Series(np.random.default_rng(42).normal(0.001, 0.01, 500))
>>> rachev_ratio(r, alpha=0.05)
1.1

See also

tail_ratio: Percentile-based tail comparison. omega_ratio: Full-distribution gain/loss ratio.

gain_to_pain_ratio(returns)[source]

Gain to Pain ratio: total gains over total absolute losses.

The Gain to Pain ratio (GPR) divides the sum of all returns by the absolute sum of all negative returns. It provides a simple measure of how much total return the strategy generates per unit of pain (aggregate losses) experienced.

Mathematical formulation:

GPR = sum(r_i) / |sum(r_i where r_i < 0)|

How to interpret:
  • GPR > 1.0: total gains exceed total losses; strategy is net profitable and then some.

  • GPR = 0.5: for every dollar lost, the strategy earned 50 cents net.

  • GPR < 0: strategy loses money overall.

  • GPR > 1.5 is generally considered good for daily returns.

When to use:

Quick profitability diagnostic. Simpler and more intuitive than profit factor (which excludes zero returns and looks at gross gains vs. gross losses).

Parameters:

returns (Series) – Simple return series.

Return type:

float

Returns:

Gain to Pain ratio as a float. Returns inf if there are no negative returns.

Example

>>> import pandas as pd
>>> r = pd.Series([0.02, -0.01, 0.015, -0.005, 0.01])
>>> gain_to_pain_ratio(r)
2.67

See also

profit_factor: Gross gains / gross losses (ignores net). omega_ratio: Threshold-based gain/loss comparison.

risk_of_ruin(win_rate, payoff_ratio, ruin_pct=0.5, n_trades=1000)[source]

Probability of losing a given fraction of capital.

Estimates the risk of ruin – the probability that a strategy will lose ruin_pct of its capital – given its win rate and average payoff ratio, assuming fixed fractional position sizing and independent trades.

Mathematical formulation (simplified):

RoR = ((1 - edge) / (1 + edge)) ^ capital_units

where edge = (win_rate * payoff_ratio) - (1 - win_rate) and capital_units approximates the number of bet-units to exhaust before reaching ruin_pct.

How to interpret:
  • RoR close to 0: very unlikely to hit ruin level.

  • RoR > 0.05 (5 %): meaningful risk; reduce position size.

  • RoR > 0.20 (20 %): dangerous; the strategy may not survive.

  • A strategy with high win_rate and high payoff_ratio has very low risk of ruin.

When to use:

Use risk of ruin to decide whether a strategy is survivable over n_trades. Combine with Kelly fraction to determine appropriate sizing.

Parameters:
  • win_rate (float) – Probability of a winning trade (0 to 1).

  • payoff_ratio (float) – Average win / average loss (positive number).

  • ruin_pct (float, default: 0.5) – Fraction of capital that constitutes ruin (e.g., 0.5 = losing 50 % of capital).

  • n_trades (int, default: 1000) – Number of trades to consider in the simulation window (affects capital_units approximation).

Return type:

float

Returns:

Estimated probability of ruin (0 to 1).

Example

>>> risk_of_ruin(win_rate=0.55, payoff_ratio=1.5, ruin_pct=0.5)
0.002

See also

kelly_fraction: Optimal position sizing. expectancy: Expected value per trade.

kelly_fraction(win_rate, avg_win, avg_loss)[source]

Kelly fraction: optimal bet sizing for geometric growth.

The Kelly criterion determines the fraction of capital to risk on each trade to maximise the long-run geometric growth rate. The full Kelly is aggressive; practitioners typically use fractional Kelly (e.g., half-Kelly) to reduce variance.

Mathematical formulation:

b = avg_win / avg_loss (odds ratio) f* = (b * p - q) / b

where p = win_rate, q = 1 - p.

How to interpret:
  • f* > 0: strategy has positive edge; bet this fraction.

  • f* = 0: no edge; do not bet.

  • f* < 0: negative edge (clamped to 0); avoid this strategy.

  • f* > 0.25: full Kelly is very aggressive; consider using half or quarter Kelly.

When to use:

Use Kelly to determine the theoretical maximum position size. In practice, use fractional Kelly (0.25x to 0.5x) because Kelly assumes known and constant edge, which is unrealistic.

Parameters:
  • win_rate (float) – Probability of a winning trade (0 to 1).

  • avg_win (float) – Average winning trade magnitude (positive).

  • avg_loss (float) – Average losing trade magnitude (positive, i.e., the absolute value of the average loss).

Return type:

float

Returns:

Optimal fraction of capital to risk, clamped to [0, 1].

Example

>>> kelly_fraction(win_rate=0.55, avg_win=1.5, avg_loss=1.0)
0.25

See also

risk_of_ruin: Probability of catastrophic drawdown. expectancy: Expected value per trade.

expectancy(win_rate, avg_win, avg_loss)[source]

Expectancy: expected profit per trade.

Expectancy combines win rate and payoff to give the average expected value of each trade. A strategy with positive expectancy is profitable in the long run (assuming sufficient trades and stable edge).

Mathematical formulation:

E = (win_rate * avg_win) - ((1 - win_rate) * |avg_loss|)

How to interpret:
  • E > 0: each trade is expected to be profitable on average.

  • E = 0: break-even strategy.

  • E < 0: losing strategy.

  • E > 0.10 (if avg_loss is normalised to 1.0): reasonable edge.

When to use:

Use expectancy alongside system quality number (SQN) and profit factor to evaluate a trading system. Expectancy alone does not account for variability of outcomes.

Parameters:
  • win_rate (float) – Probability of a winning trade (0 to 1).

  • avg_win (float) – Average winning trade magnitude (positive).

  • avg_loss (float) – Average losing trade magnitude (positive, absolute value of the average loss).

Return type:

float

Returns:

Expected value per trade.

Example

>>> expectancy(win_rate=0.6, avg_win=100.0, avg_loss=80.0)
28.0

See also

system_quality_number: Expectancy normalised by trade variability. kelly_fraction: Optimal sizing given expectancy.

profit_factor(returns)[source]

Profit factor: gross profit divided by gross loss.

The profit factor measures how many dollars the strategy earns for every dollar it loses. It is the simplest measure of a strategy’s profitability.

Mathematical formulation:

PF = sum(r_i where r_i > 0) / |sum(r_i where r_i < 0)|

How to interpret:
  • PF > 1.0: strategy is profitable.

  • PF = 1.0: break even.

  • PF < 1.0: strategy loses money.

  • PF > 1.5: good.

  • PF > 2.0: very good (verify not overfitting).

When to use:

Use as a quick profitability check. Pair with win rate and payoff ratio for a complete picture.

Parameters:

returns (Series) – Return or P&L series.

Return type:

float

Returns:

Profit factor as a float. Returns inf if there are no losses.

Example

>>> import pandas as pd
>>> r = pd.Series([0.02, -0.01, 0.03, -0.005])
>>> profit_factor(r)
...
3.33

See also

gain_to_pain_ratio: Net return / total losses. expectancy: Expected value per trade.

payoff_ratio(returns)[source]

Payoff ratio: average win divided by average loss.

The payoff ratio (also called reward-to-risk ratio) measures how large the average winning trade is relative to the average losing trade. Combined with win rate, it fully characterises a strategy’s return profile.

Mathematical formulation:

Payoff = mean(r_i where r_i > 0) / |mean(r_i where r_i < 0)|

How to interpret:
  • Payoff > 1.0: average wins are larger than average losses. Common in trend-following strategies.

  • Payoff = 1.0: wins and losses are the same size.

  • Payoff < 1.0: average losses exceed average wins. The strategy must have a high win rate to compensate.

  • Payoff > 2.0 with win rate > 0.40 is a strong system.

When to use:

Use alongside win rate. A low win rate with high payoff (trend-following) is as viable as high win rate with low payoff (mean-reversion).

Parameters:

returns (Series) – Return or P&L series.

Return type:

float

Returns:

Payoff ratio as a float. Returns inf if there are no losses, and 0.0 if there are no wins.

Example

>>> import pandas as pd
>>> r = pd.Series([0.02, -0.01, 0.03, -0.005])
>>> payoff_ratio(r)
...
3.33

See also

profit_factor: Gross gains / gross losses. expectancy: Combines win rate and payoff into expected value.

recovery_factor(returns)[source]

Recovery factor: net profit relative to max drawdown.

Recovery factor measures how many times over the strategy has recovered from its worst drawdown. A high recovery factor indicates that the strategy generates returns efficiently relative to the drawdown pain it inflicts.

Mathematical formulation:

RF = total_return / |max_drawdown|

How to interpret:
  • RF > 1.0: strategy has earned back more than its worst drawdown.

  • RF > 3.0: strong.

  • RF > 5.0: excellent recovery relative to risk.

  • RF < 1.0: strategy has not yet recovered from its worst drawdown.

When to use:

Use recovery factor when you want to know if the strategy’s returns justify the drawdown pain. Useful for comparing strategies with different drawdown profiles.

Parameters:

returns (Series) – Simple return series.

Return type:

float

Returns:

Recovery factor as a float. Returns inf if there is no drawdown.

Example

>>> import pandas as pd, numpy as np
>>> r = pd.Series(np.random.default_rng(42).normal(0.001, 0.01, 252))
>>> recovery_factor(r)
3.5

See also

burke_ratio: Return per sum-of-squared-drawdowns. ulcer_performance_index: Return per Ulcer Index.

system_quality_number(pnl)[source]

System Quality Number (SQN): Van Tharp’s strategy quality metric.

SQN normalises expectancy by the variability of trade outcomes and scales by the square root of the number of trades. It answers the question: “given the consistency and edge of this system, is the positive expectancy statistically significant?”

Mathematical formulation:

SQN = sqrt(N) * mean(pnl) / std(pnl)

where N is the number of trades (or periods).

How to interpret:
  • SQN < 1.6: poor; difficult to trade profitably.

  • 1.6 < SQN < 2.0: below average; marginal edge.

  • 2.0 < SQN < 2.5: average; tradeable with discipline.

  • 2.5 < SQN < 3.0: good; reliable system.

  • 3.0 < SQN < 5.0: excellent.

  • 5.0 < SQN < 7.0: superb.

  • SQN > 7.0: holy grail (verify not overfitting).

When to use:

Use SQN to evaluate whether a system’s edge is statistically meaningful given the number of observations. Particularly useful when comparing systems with different numbers of trades.

Parameters:

pnl (Series) – Per-trade or per-period P&L series.

Return type:

float

Returns:

SQN as a float. Returns 0.0 if the standard deviation is zero.

Example

>>> import pandas as pd
>>> pnl = pd.Series([100, -50, 80, -30, 120, -40, 90, -20])
>>> system_quality_number(pnl)
1.7

See also

expectancy: Mean expected value per trade. kelly_fraction: Optimal position sizing from edge.

Tearsheet

Enhanced tearsheet / reporting utilities for backtests.

Generates comprehensive performance summaries, monthly return tables, drawdown analysis, rolling metrics, and trade-level analytics.

generate_tearsheet(returns, benchmark=None, risk_free=0.0, periods_per_year=252)[source]

Generate a comprehensive performance tearsheet dictionary.

Parameters:
  • returns (Series) – Portfolio return series (simple, not log).

  • benchmark (Series | None, default: None) – Benchmark return series for relative metrics.

  • risk_free (float, default: 0.0) – Annualised risk-free rate.

  • periods_per_year (int, default: 252) – Trading periods per year (252 for daily).

Returns:

Dictionary containing absolute and (optionally) relative performance metrics.

Return type:

dict[str, Any]

monthly_returns_table(returns)[source]

Compute a table of monthly returns suitable for heatmap display.

Parameters:

returns (Series) – Daily (or intraday) return series with a DatetimeIndex.

Returns:

Rows = years, columns = months (1-12). Values are total returns for that month.

Return type:

DataFrame

drawdown_table(returns, top_n=5)[source]

Return the top N drawdown periods with metadata.

Parameters:
  • returns (Series) – Portfolio return series.

  • top_n (int, default: 5) – Number of worst drawdowns to report.

Returns:

Columns: peak_date, trough_date, recovery_date, depth, duration (periods from peak to recovery or end).

Return type:

DataFrame

rolling_metrics_table(returns, windows=None, periods_per_year=252)[source]

Compute rolling Sharpe, volatility, and return at multiple windows.

Parameters:
  • returns (Series) – Portfolio return series.

  • windows (list[int] | None, default: None) – Rolling window sizes in periods. Default [21, 63, 126, 252].

  • periods_per_year (int, default: 252) – Trading periods per year.

Returns:

MultiIndex columns: (window, metric) with metrics rolling_return, rolling_vol, rolling_sharpe.

Return type:

DataFrame

trade_analysis(trades_df)[source]

Analyse trade-level performance.

Parameters:

trades_df (DataFrame) – Must contain a pnl column with per-trade profit/loss values. Optionally includes entry_price, exit_price, side, etc.

Returns:

Dictionary with win_rate, avg_pnl, avg_win, avg_loss, profit_factor, expectancy, max_win, max_loss, n_trades.

Return type:

dict[str, float]

Integrations

Advanced backtesting integrations using optional packages.

Provides wrappers around vectorbt, quantstats, empyrical, pyfolio, and ffn for backtesting, reporting, and performance analytics.

vectorbt_backtest(prices, entries, exits, **kwargs)[source]

Run a vectorised backtest using vectorbt.

Parameters:
  • prices (Series | DataFrame) – Price data aligned with the entry/exit signals.

  • entries (Series | DataFrame) – Boolean series/DataFrame indicating entry signals.

  • exits (Series | DataFrame) – Boolean series/DataFrame indicating exit signals.

  • **kwargs (Any) – Additional keyword arguments forwarded to vectorbt.Portfolio.from_signals.

Returns:

Dictionary containing:

  • total_return – total portfolio return.

  • sharpe_ratio – annualised Sharpe ratio.

  • max_drawdown – maximum drawdown.

  • total_trades – number of trades executed.

  • win_rate – fraction of winning trades.

  • portfolio – the raw vectorbt.Portfolio object.

Return type:

dict[str, Any]

quantstats_report(returns, benchmark=None, output=None)[source]

Generate performance analytics using quantstats.

Parameters:
  • returns (Series) – Strategy return series (simple returns).

  • benchmark (Series | None, default: None) – Benchmark return series for comparison.

  • output (str | None, default: None) – File path for the HTML report. When None, no file is written.

Returns:

Dictionary containing:

  • sharpe – annualised Sharpe ratio.

  • sortino – annualised Sortino ratio.

  • max_drawdown – maximum drawdown.

  • cagr – compound annual growth rate.

  • volatility – annualised volatility.

  • calmar – Calmar ratio.

Return type:

dict[str, Any]

empyrical_metrics(returns)[source]

Compute a comprehensive set of metrics using empyrical.

Parameters:

returns (Series) – Simple return series.

Returns:

Dictionary containing:

  • annual_return – annualised return.

  • annual_volatility – annualised volatility.

  • sharpe_ratio – annualised Sharpe ratio.

  • sortino_ratio – annualised Sortino ratio.

  • max_drawdown – maximum drawdown.

  • calmar_ratio – Calmar ratio.

  • omega_ratio – Omega ratio.

  • tail_ratio – tail ratio (95th / 5th percentile).

  • stability – R-squared of cumulative log returns.

Return type:

dict[str, float]

pyfolio_tearsheet_data(returns, positions=None, transactions=None)[source]

Prepare data in the format expected by pyfolio tearsheets.

This function does not render plots but returns the intermediate data structures that pyfolio uses internally, making it possible to inspect results programmatically.

Parameters:
  • returns (Series) – Strategy return series with a DatetimeIndex.

  • positions (DataFrame | None, default: None) – Position sizes over time. Columns are asset names, values are dollar positions.

  • transactions (DataFrame | None, default: None) – Trade log. Expected columns: amount, price, symbol.

Returns:

Dictionary containing:

  • returns – the input return series.

  • cum_returns – cumulative return series.

  • drawdown – drawdown series.

  • positions – positions DataFrame (or None).

  • transactions – transactions DataFrame (or None).

Return type:

dict[str, Any]

ffn_stats(prices)[source]

Compute performance statistics using ffn.

Parameters:

prices (Series | DataFrame) – Price series or multi-asset price DataFrame.

Returns:

Dictionary containing key performance metrics:

  • total_return – total return over the period.

  • cagr – compound annual growth rate.

  • daily_sharpe – daily Sharpe ratio.

  • max_drawdown – maximum drawdown.

  • avg_drawdown – average drawdown.

  • monthly_sharpe – monthly Sharpe ratio.

  • stats_object – the raw ffn.PerformanceStats object.

Return type:

dict[str, Any]