Backtest

Runner (Convenience Functions)

High-level convenience functions for running backtests.

quaver.backtest.runner.run_backtest(engine_name, parameters, candles, instrument_id, initial_capital=10000.0, quantity_per_trade=1.0, ts_column='ts', allow_shorting=False, commission=None, slippage=None, sizing_fn=None, exit_rules=None)[source]

Run a single-asset walk-forward backtest.

Convenience wrapper that resolves the strategy from the registry, normalises the candle data, constructs a Portfolio and BacktestEngine, and returns the result.

Parameters:
  • engine_name (str) – Registered strategy name (e.g. "mean_reversion").

  • parameters (dict[str, object]) – Strategy-specific parameter dictionary passed to the strategy constructor.

  • candles (DataFrame) – Raw OHLCV DataFrame; normalisation is applied internally via normalise_candles().

  • instrument_id (str) – Label embedded in trade records and the result object.

  • initial_capital (float) – Starting cash balance. Defaults to 10_000.0.

  • quantity_per_trade (float) – Fixed number of units per trade. Defaults to 1.0.

  • ts_column (str) – Name of the timestamp column in candles. Defaults to "ts".

  • allow_shorting (bool) – When True, SELL signals from a flat portfolio open a short position. Defaults to False.

  • commission (CommissionConfig | None)

  • slippage (SlippageConfig | None)

  • sizing_fn (Callable[[float, float], float] | None)

  • exit_rules (ExitRules | None)

Returns:

BacktestResult containing all trades and performance metrics.

Return type:

BacktestResult

Raises:
quaver.backtest.runner.run_multi_asset_backtest(engine_name, parameters, candles_map, initial_capital=10000.0, quantity_per_trade=1.0, ts_column='ts', allow_shorting=False, timestamp_overlap_warn_threshold=0.05, commission=None, slippage=None, sizing_fn=None, exit_rules=None)[source]

Run a multi-asset walk-forward backtest.

Convenience wrapper that resolves the strategy from the registry, normalises all candle DataFrames, builds one Portfolio per instrument, constructs a MultiAssetBacktestEngine, and returns the per-instrument results.

Parameters:
  • engine_name (str) – Registered MultiAssetStrategy name.

  • parameters (dict[str, object]) – Strategy-specific parameter dictionary passed to the strategy constructor.

  • candles_map (dict[str, DataFrame]) – Mapping of instrument_id to a raw OHLCV DataFrame; normalisation is applied internally.

  • initial_capital (float) – Starting cash balance per instrument portfolio. Defaults to 10_000.0.

  • quantity_per_trade (float) – Fixed number of units per trade per instrument. Defaults to 1.0.

  • ts_column (str) – Timestamp column name shared by all DataFrames. Defaults to "ts".

  • allow_shorting (bool) – Passed through to each instrument’s portfolio engine. Defaults to False.

  • timestamp_overlap_warn_threshold (float) – If the shared timestamp intersection drops more than this fraction of any instrument’s timestamps, a WARNING is logged. Defaults to 0.05 (5%).

  • commission (CommissionConfig | None)

  • slippage (SlippageConfig | None)

  • sizing_fn (Callable[[float, float], float] | None)

  • exit_rules (ExitRules | None)

Returns:

Mapping of instrument_id to BacktestResult.

Return type:

dict[str, BacktestResult]

Raises:
  • TypeError – If engine_name resolves to a single-asset BaseStrategy; use run_backtest() instead.

  • ValueError – If instruments required by the strategy are absent from candles_map, or if any instrument has fewer candles than the strategy requires.

Engine (Single-Asset)

Single-asset walk-forward backtest engine.

class quaver.backtest.engine.BacktestEngine(strategy, portfolio, instrument_id, ts_column='ts', allow_shorting=False)[source]

Bases: object

Replays a single instrument’s candle history through a BaseStrategy.

Design invariants

  • No look-ahead bias: the window passed to compute() is candles.iloc[:i], which excludes the bar at index i (the “current” bar).

  • The input DataFrame is never mutated.

  • Exceptions raised by compute() are not caught – they propagate to the caller.

  • HOLD signals are logged at DEBUG level and treated as no-ops.

  • SELL signal routing:

    • If a long position is open -> close_long().

    • If flat and allow_shorting is True -> open_short().

    • If flat and allow_shorting is False -> logged and skipped.

  • reset() is called at the start of each run() invocation so that running the engine twice produces identical results.

Parameters:
  • strategy (BaseStrategy) – Instantiated and validated BaseStrategy.

  • portfolio (Portfolio) – Portfolio instance (will be reset automatically at the start of each run() call).

  • instrument_id (str) – Identifier embedded in TradeRecord and BacktestResult objects.

  • ts_column (str) – Name of the timestamp column in the candles DataFrame. Defaults to "ts".

  • allow_shorting (bool) – When True, SELL signals received while the portfolio is flat will open a short position. Defaults to False (suitable for most long-only mean-reversion strategies).

run(candles)[source]

Run the backtest over the full candles history.

The portfolio is reset before iteration begins. Any position that remains open at the end of the data is force-closed at the final bar’s close price with signal=None.

Parameters:

candles (DataFrame) – Normalised OHLCV DataFrame (output of normalise_candles()).

Returns:

BacktestResult summarising all trades and computed performance metrics.

Return type:

BacktestResult

Multi-Asset Engine

Multi-asset walk-forward backtest engine.

class quaver.backtest.multi_engine.MultiAssetBacktestEngine(strategy, portfolios, ts_column='ts', allow_shorting=False, timestamp_overlap_warn_threshold=0.05)[source]

Bases: object

Replays multiple instruments’ candle histories through a MultiAssetStrategy.

Alignment

All instruments are aligned to the sorted intersection of their timestamp sets. If the intersection discards more than timestamp_overlap_warn_threshold of any single instrument’s timestamps a WARNING is logged.

Atomicity

All signals emitted in a single MultiAssetStrategyOutput are applied within the same iteration step, before advancing to the next timestamp.

Position management

Each instrument has its own independent Portfolio.

Parameters:
  • strategy (MultiAssetStrategy) – Instantiated and validated MultiAssetStrategy.

  • portfolios (dict[str, Portfolio]) – Mapping of instrument_id to Portfolio, one entry per instrument.

  • ts_column (str) – Timestamp column name shared by all candle DataFrames. Defaults to "ts".

  • allow_shorting (bool) – When True, SELL signals from flat portfolios open short positions. Defaults to False.

  • timestamp_overlap_warn_threshold (float) – Fraction of timestamps that may be dropped by the intersection before a WARNING is emitted. Defaults to 0.05 (5%).

run(candles_map)[source]

Run the multi-asset backtest.

All portfolios are reset before iteration begins. Instruments are iterated over a shared timestamp intersection. Any positions that remain open at the end of the data are force-closed at each instrument’s final bar close price with signal=None.

Parameters:

candles_map (dict[str, DataFrame]) – Mapping of instrument_id to a normalised OHLCV DataFrame (output of normalise_candles()).

Returns:

Mapping of instrument_id to BacktestResult.

Return type:

dict[str, BacktestResult]

Portfolio

Portfolio: tracks cash, open positions, and closed trades.

class quaver.backtest.portfolio.CommissionConfig(fixed_per_trade=0.0, pct_of_notional=0.0)[source]

Bases: object

Commission parameters applied to every open/close event.

Parameters:
  • fixed_per_trade (float) – Flat dollar amount charged on each open and each close. Defaults to 0.0.

  • pct_of_notional (float) – Fraction of price * quantity charged on each open and each close. Defaults to 0.0.

fixed_per_trade: float = 0.0
pct_of_notional: float = 0.0
calc(price, quantity)[source]

Return commission for a single event (open or close).

Return type:

float

Parameters:
class quaver.backtest.portfolio.SlippageConfig(slippage_pct=0.0)[source]

Bases: object

Slippage parameters applied to every fill.

Slippage shifts the fill price adversely:

  • Buys fill at price * (1 + slippage_pct)

  • Sells fill at price * (1 - slippage_pct)

Parameters:

slippage_pct (float) – Adverse price shift as a fraction. Defaults to 0.0.

slippage_pct: float = 0.0
buy_price(price)[source]

Return the adverse fill price for a buy.

Return type:

float

Parameters:

price (float)

sell_price(price)[source]

Return the adverse fill price for a sell.

Return type:

float

Parameters:

price (float)

class quaver.backtest.portfolio.ExitRules(stop_loss_pct=None, take_profit_pct=None, trailing_stop_pct=None)[source]

Bases: object

Global exit rules applied to all trades.

All values are fractions (e.g. 0.02 = 2%). None disables the rule.

Parameters:
  • stop_loss_pct (float | None) – Adverse move fraction triggering a stop-loss exit.

  • take_profit_pct (float | None) – Favorable move fraction triggering a take-profit exit.

  • trailing_stop_pct (float | None) – Trailing distance fraction for a trailing-stop exit.

stop_loss_pct: float | None = None
take_profit_pct: float | None = None
trailing_stop_pct: float | None = None
class quaver.backtest.portfolio.OpenPosition(instrument_id, entry_ts, entry_price, quantity, direction, entry_signal, entry_commission=0.0, entry_slippage_cost=0.0, stop_loss_price=None, take_profit_price=None, trailing_stop_pct=None, trailing_stop_extreme=None)[source]

Bases: object

A single open position held by the portfolio.

Parameters:
  • instrument_id (str) – Identifier of the traded instrument.

  • entry_ts (datetime) – Timestamp at which the position was opened.

  • entry_price (float) – Execution price at entry.

  • quantity (float) – Number of units held.

  • direction (SignalDirection) – BUY for a long position, SELL for a short position.

  • entry_signal (SignalOutput) – The SignalOutput that triggered the entry.

  • entry_commission (float) – Commission charged on entry. Defaults to 0.0.

  • entry_slippage_cost (float) – Dollar slippage cost on entry. Defaults to 0.0.

  • stop_loss_price (float | None)

  • take_profit_price (float | None)

  • trailing_stop_pct (float | None)

  • trailing_stop_extreme (float | None)

instrument_id: str
entry_ts: datetime
entry_price: float
quantity: float
direction: SignalDirection
entry_signal: SignalOutput
entry_commission: float = 0.0
entry_slippage_cost: float = 0.0
stop_loss_price: float | None = None
take_profit_price: float | None = None
trailing_stop_pct: float | None = None
trailing_stop_extreme: float | None = None
class quaver.backtest.portfolio.TradeRecord(instrument_id, entry_ts, exit_ts, entry_price, exit_price, quantity, direction, pnl, entry_signal, exit_signal, commission=0.0, slippage_cost=0.0, exit_reason=None)[source]

Bases: object

A completed round-trip trade.

P&L formula

  • Long: pnl = (exit_price - entry_price) * quantity

  • Short: pnl = (entry_price - exit_price) * quantity

pnl reflects the ideal P&L using intended (pre-slippage) prices. net_pnl subtracts commission and slippage costs.

Parameters:
  • instrument_id (str) – Identifier of the traded instrument.

  • entry_ts (datetime) – Timestamp at which the position was opened.

  • exit_ts (datetime) – Timestamp at which the position was closed.

  • entry_price (float) – Execution price at entry.

  • exit_price (float) – Execution price at exit.

  • quantity (float) – Number of units traded.

  • direction (SignalDirection) – Opening direction – BUY for long, SELL for short.

  • pnl (float) – Realised profit and loss for this trade (see formula above).

  • entry_signal (SignalOutput) – Signal that triggered the entry.

  • exit_signal (SignalOutput | None) – Signal that triggered the exit, or None when the position was force-closed at end-of-data.

  • commission (float) – Total commission (entry + exit). Defaults to 0.0.

  • slippage_cost (float) – Total dollar slippage cost (entry + exit). Defaults to 0.0.

  • exit_reason (ExitReason | None)

instrument_id: str
entry_ts: datetime
exit_ts: datetime
entry_price: float
exit_price: float
quantity: float
direction: SignalDirection
pnl: float
entry_signal: SignalOutput
exit_signal: SignalOutput | None
commission: float = 0.0
slippage_cost: float = 0.0
exit_reason: ExitReason | None = None
property net_pnl: float

P&L after deducting commission and slippage costs.

class quaver.backtest.portfolio.Portfolio(initial_capital, quantity_per_trade=1.0, commission=None, slippage=None, sizing_fn=None, exit_rules=None)[source]

Bases: object

Tracks cash balance, one open position at a time, and closed trades.

P&L conventions

  • Long: pnl = (exit_price - entry_price) * quantity

  • Short: pnl = (entry_price - exit_price) * quantity

Cash conventions

When commission and slippage are configured, cash accounting uses the actual (slipped) fill prices and deducts commissions.

Cash CAN go negative if sizing is misconfigured. No clamping is applied, but a WARNING is logged whenever cash drops below zero.

Only ONE open position is allowed at a time. Attempting to open a second position while one is already open logs a WARNING and is a no-op.

Parameters:
  • initial_capital (float) – Starting cash balance.

  • quantity_per_trade (float) – Fixed number of units used for every trade. Defaults to 1.0.

  • commission (CommissionConfig | None) – Commission configuration. None means zero cost.

  • slippage (SlippageConfig | None) – Slippage configuration. None means zero slippage.

  • sizing_fn (Callable[[float, float], float] | None) – Optional callable (account_value, entry_price) -> quantity. When provided, overrides quantity_per_trade.

  • exit_rules (ExitRules | None)

property cash: float

Current cash balance.

Returns:

Current cash balance, which may be negative if sizing is misconfigured.

Return type:

float

property closed_trades: list[TradeRecord]

Snapshot list of all completed trades.

Returns:

A shallow copy of the internal closed-trades list so that callers cannot mutate portfolio state.

Return type:

list[TradeRecord]

is_flat()[source]

Return True if no position is currently open.

Returns:

True when the portfolio holds no open position, False otherwise.

Return type:

bool

open_long(instrument_id, ts, price, signal)[source]

Open a long position.

No-op (with WARNING) if a position is already open.

Cash is debited at the slipped fill price plus commission.

Parameters:
  • instrument_id (str) – Identifier of the instrument to buy.

  • ts (datetime) – Timestamp of the entry bar.

  • price (float) – Execution (close) price.

  • signal (SignalOutput) – The SignalOutput that triggered this entry.

Returns:

None

Return type:

None

close_long(ts, price, signal, exit_reason=None)[source]

Close an open long position.

Cash is credited at the slipped fill price minus commission.

Parameters:
Returns:

The completed TradeRecord.

Return type:

TradeRecord

Raises:

RuntimeError – If no position is currently open.

open_short(instrument_id, ts, price, signal)[source]

Open a short position.

No-op (with WARNING) if a position is already open.

Cash is credited at the slipped fill price minus commission.

Parameters:
  • instrument_id (str) – Identifier of the instrument to sell short.

  • ts (datetime) – Timestamp of the entry bar.

  • price (float) – Execution (close) price.

  • signal (SignalOutput) – The SignalOutput that triggered this entry.

Returns:

None

Return type:

None

close_short(ts, price, signal, exit_reason=None)[source]

Close an open short position.

Cash is debited at the slipped fill price plus commission.

Parameters:
Returns:

The completed TradeRecord.

Return type:

TradeRecord

Raises:

RuntimeError – If no position is currently open.

check_exit_triggers(ts, high, low)[source]

Check whether the current bar triggers a stop-loss, trailing stop, or take-profit exit.

Priority (pessimistic): stop-loss > trailing stop > take-profit.

Parameters:
  • ts (datetime) – Timestamp of the current bar.

  • high (float) – High price of the current bar.

  • low (float) – Low price of the current bar.

Return type:

tuple[ExitReason, float] | None

Returns:

(ExitReason, fill_price) if triggered, else None.

reset()[source]

Restore the portfolio to its initial state.

Important

Call this before re-running a backtest engine to ensure deterministic, reproducible results. BacktestEngine.run() calls this automatically at the start of each invocation.

Returns:

None

Return type:

None

Result

BacktestResult: immutable summary of a completed backtest run.

class quaver.backtest.result.BacktestResult(instrument_id, initial_capital, final_cash, trades)[source]

Bases: object

Immutable summary produced after BacktestEngine.run() completes.

All monetary values are expressed in the same currency unit as initial_capital.

Drawdown convention

Drawdown is computed on the cumulative P&L series (not the equity curve) and expressed as a fraction of initial_capital:

max_drawdown = (trough - peak) / initial_capital

The value is always <= 0. Returns 0.0 when fewer than 2 trades are present.

Sharpe ratio convention

Sharpe ratio uses per-trade P&L (not annualised returns), scaled by sqrt(252) as a conventional approximation. Returns 0.0 when fewer than 2 trades are present or when std(pnl) == 0.

Important

The Sharpe ratio should be interpreted only as a relative ranking metric between strategies run on the same dataset. It is not a calendar-annualised Sharpe ratio.

Parameters:
  • instrument_id (str) – Identifier of the traded instrument.

  • initial_capital (float) – Starting cash balance used in the backtest.

  • final_cash (float) – Cash balance at the end of the backtest.

  • trades (list[TradeRecord]) – Ordered list of all completed TradeRecord objects.

instrument_id: str
initial_capital: float
final_cash: float
trades: list[TradeRecord]
property total_return: float

Fractional return over the full backtest period.

Computed as (final_cash - initial_capital) / initial_capital. Returns 0.0 when initial_capital is zero.

Returns:

Fractional total return (e.g. 0.15 means +15%).

Return type:

float

property total_trades: int

Total number of completed round-trip trades.

Returns:

Count of all trades recorded in trades.

Return type:

int

property winning_trades: int

Number of trades with a strictly positive P&L.

Returns:

Count of trades where pnl > 0.

Return type:

int

property losing_trades: int

Number of trades with a zero or negative P&L.

Returns:

Count of trades where pnl <= 0.

Return type:

int

property win_rate: float

Fraction of trades that were profitable.

Returns 0.0 when no trades have been recorded.

Returns:

winning_trades / total_trades in the range [0.0, 1.0].

Return type:

float

property avg_pnl: float

Average P&L per trade.

Returns 0.0 when no trades have been recorded.

Returns:

Arithmetic mean of per-trade P&L values.

Return type:

float

property pnl_series: list[float]

Ordered list of per-trade P&L values.

Returns:

List of pnl values in trade-completion order.

Return type:

list[float]

property cumulative_pnl: list[float]

Running cumulative sum of per-trade P&L values.

Returns:

List where element i is the sum of all P&L values up to and including trade i.

Return type:

list[float]

property max_drawdown: float

Maximum peak-to-trough drop in cumulative P&L, as a fraction of initial_capital.

Formula:

max_drawdown = (trough - peak) / initial_capital

Always <= 0. Returns 0.0 when no trades are present or when initial_capital is zero.

Returns:

Maximum drawdown as a non-positive fraction (e.g. -0.12 means a 12% drawdown relative to initial capital).

Return type:

float

property sharpe_ratio: float

Per-trade Sharpe proxy scaled by sqrt(252).

Formula:

sharpe = mean(pnl) / std(pnl, ddof=1) * sqrt(252)

Returns 0.0 when fewer than 2 trades are present or when std(pnl) == 0.

Note

This is a relative comparison metric only – it is not a calendar-annualised Sharpe ratio.

Returns:

Per-trade Sharpe proxy rounded to 4 decimal places.

Return type:

float

property profit_factor: float

Ratio of gross profit to gross loss.

Formula:

profit_factor = sum(winning pnl) / abs(sum(losing pnl))

Returns 0.0 when there are no trades, or float('inf') when all trades are profitable (no losing trades).

Returns:

Profit factor rounded to 4 decimal places, 0.0 when no trades exist, or inf when gross loss is zero.

Return type:

float

property max_consecutive_wins: int

Longest streak of consecutive trades with pnl > 0.

Returns:

Length of the longest winning streak, or 0 if no trades.

Return type:

int

property max_consecutive_losses: int

Longest streak of consecutive trades with pnl <= 0.

Returns:

Length of the longest losing streak, or 0 if no trades.

Return type:

int

property avg_win: float

Mean P&L of winning trades (pnl > 0).

Returns 0.0 when there are no winning trades.

Returns:

Average winning P&L.

Return type:

float

property avg_loss: float

Mean P&L of losing trades (pnl <= 0), non-positive.

Returns 0.0 when there are no losing trades.

Returns:

Average losing P&L (non-positive value).

Return type:

float

property recovery_factor: float

Total return divided by the absolute value of max drawdown.

Returns 0.0 when there is no drawdown.

Returns:

Recovery factor.

Return type:

float

property expectancy: float

Expected P&L per trade based on win rate and average win/loss.

Formula:

expectancy = win_rate * avg_win + loss_rate * avg_loss
Returns:

Expected value per trade.

Return type:

float

property total_commission: float

Sum of commissions across all trades.

Returns:

Total commission in dollars.

Return type:

float

property total_slippage: float

Sum of slippage costs across all trades.

Returns:

Total slippage cost in dollars.

Return type:

float

summary()[source]

Return all key metrics as a flat dictionary with rounded values.

Returns:

Flat dictionary of performance metrics.

Return type:

dict[str, object]

classmethod from_portfolio(portfolio, candles, instrument_id)[source]

Construct a BacktestResult from a completed Portfolio.

Parameters:
  • portfolio (Portfolio) – Portfolio instance after all trades have been applied and any open position has been force-closed.

  • candles (DataFrame) – Normalised OHLCV DataFrame used during the backtest (currently retained for future extension; not read here).

  • instrument_id (str) – Identifier to embed in the result.

Returns:

Fully populated BacktestResult.

Return type:

BacktestResult

Data Utilities

Data normalisation and validation utilities.

quaver.backtest.data.normalise_candles(df, ts_col='ts')[source]

Validate, cast, sort, and deduplicate a candles DataFrame.

Processing steps applied in order:

  1. Raise ValueError if any required column is missing. Required columns: ts_col, open, high, low, close, volume.

  2. Cast all OHLCV columns to float64.

  3. Parse the timestamp column to datetime64[ns, UTC] – strips any existing timezone information and then re-localises as UTC to ensure consistent merging across instruments.

  4. Sort ascending by the timestamp column, drop duplicate timestamp rows (keeping the last occurrence), and reset the index.

  5. Return a copy; the input DataFrame is never mutated.

Parameters:
  • df (DataFrame) – Raw OHLCV DataFrame to be normalised.

  • ts_col (str) – Name of the timestamp column. Defaults to "ts".

Returns:

Cleaned DataFrame with a monotonic, tz-naive UTC timestamp column and all OHLCV columns cast to float64.

Return type:

DataFrame

Raises:

ValueError – If one or more required columns are absent from df.

quaver.backtest.data.validate_candles(df, required, label='')[source]

Raise ValueError if df has fewer rows than required.

Parameters:
  • df (DataFrame) – Normalised candles DataFrame (output of normalise_candles()).

  • required (int) – Minimum number of rows needed by the strategy.

  • label (str) – Optional instrument label included in the error message for easier diagnosis. Defaults to an empty string.

Returns:

None

Return type:

None

Raises:

ValueError – If len(df) < required.