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
PortfolioandBacktestEngine, 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 vianormalise_candles().instrument_id (
str) – Label embedded in trade records and the result object.initial_capital (
float) – Starting cash balance. Defaults to10_000.0.quantity_per_trade (
float) – Fixed number of units per trade. Defaults to1.0.ts_column (
str) – Name of the timestamp column incandles. Defaults to"ts".allow_shorting (
bool) – WhenTrue,SELLsignals from a flat portfolio open a short position. Defaults toFalse.commission (CommissionConfig | None)
slippage (SlippageConfig | None)
exit_rules (ExitRules | None)
- Returns:
BacktestResultcontaining all trades and performance metrics.- Return type:
- Raises:
EngineNotFoundError – If
engine_nameis not present in the strategy registry.TypeError – If
engine_nameresolves to aMultiAssetStrategy; userun_multi_asset_backtest()instead.ValueError – If strategy parameters are invalid or if
candlescontains fewer rows than required by the strategy.
- 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
Portfolioper instrument, constructs aMultiAssetBacktestEngine, and returns the per-instrument results.- Parameters:
engine_name (
str) – RegisteredMultiAssetStrategyname.parameters (
dict[str,object]) – Strategy-specific parameter dictionary passed to the strategy constructor.candles_map (
dict[str,DataFrame]) – Mapping ofinstrument_idto a raw OHLCV DataFrame; normalisation is applied internally.initial_capital (
float) – Starting cash balance per instrument portfolio. Defaults to10_000.0.quantity_per_trade (
float) – Fixed number of units per trade per instrument. Defaults to1.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 toFalse.timestamp_overlap_warn_threshold (
float) – If the shared timestamp intersection drops more than this fraction of any instrument’s timestamps, aWARNINGis logged. Defaults to0.05(5%).commission (CommissionConfig | None)
slippage (SlippageConfig | None)
exit_rules (ExitRules | None)
- Returns:
Mapping of
instrument_idtoBacktestResult.- Return type:
- Raises:
TypeError – If
engine_nameresolves to a single-assetBaseStrategy; userun_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:
objectReplays a single instrument’s candle history through a
BaseStrategy.Design invariants
No look-ahead bias: the window passed to
compute()iscandles.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.HOLDsignals are logged atDEBUGlevel and treated as no-ops.SELLsignal routing:If a long position is open ->
close_long().If flat and
allow_shortingisTrue->open_short().If flat and
allow_shortingisFalse-> logged and skipped.
reset()is called at the start of eachrun()invocation so that running the engine twice produces identical results.
- Parameters:
strategy (
BaseStrategy) – Instantiated and validatedBaseStrategy.portfolio (
Portfolio) –Portfolioinstance (will be reset automatically at the start of eachrun()call).instrument_id (
str) – Identifier embedded inTradeRecordandBacktestResultobjects.ts_column (
str) – Name of the timestamp column in the candles DataFrame. Defaults to"ts".allow_shorting (
bool) – WhenTrue,SELLsignals received while the portfolio is flat will open a short position. Defaults toFalse(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 ofnormalise_candles()).- Returns:
BacktestResultsummarising all trades and computed performance metrics.- Return type:
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:
objectReplays 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_thresholdof any single instrument’s timestamps aWARNINGis logged.Atomicity
All signals emitted in a single
MultiAssetStrategyOutputare 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 validatedMultiAssetStrategy.portfolios (
dict[str,Portfolio]) – Mapping ofinstrument_idtoPortfolio, one entry per instrument.ts_column (
str) – Timestamp column name shared by all candle DataFrames. Defaults to"ts".allow_shorting (
bool) – WhenTrue,SELLsignals from flat portfolios open short positions. Defaults toFalse.timestamp_overlap_warn_threshold (
float) – Fraction of timestamps that may be dropped by the intersection before aWARNINGis emitted. Defaults to0.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 ofinstrument_idto a normalised OHLCV DataFrame (output ofnormalise_candles()).- Returns:
Mapping of
instrument_idtoBacktestResult.- Return type:
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:
objectCommission parameters applied to every open/close event.
- Parameters:
- class quaver.backtest.portfolio.SlippageConfig(slippage_pct=0.0)[source]
Bases:
objectSlippage 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 to0.0.
- class quaver.backtest.portfolio.ExitRules(stop_loss_pct=None, take_profit_pct=None, trailing_stop_pct=None)[source]
Bases:
objectGlobal exit rules applied to all trades.
All values are fractions (e.g.
0.02= 2%).Nonedisables the rule.- Parameters:
- 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:
objectA 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) –BUYfor a long position,SELLfor a short position.entry_signal (
SignalOutput) – TheSignalOutputthat triggered the entry.entry_commission (
float) – Commission charged on entry. Defaults to0.0.entry_slippage_cost (
float) – Dollar slippage cost on entry. Defaults to0.0.stop_loss_price (float | None)
take_profit_price (float | None)
trailing_stop_pct (float | None)
trailing_stop_extreme (float | None)
- direction: SignalDirection
- entry_signal: SignalOutput
- 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:
objectA completed round-trip trade.
P&L formula
Long:
pnl = (exit_price - entry_price) * quantityShort:
pnl = (entry_price - exit_price) * quantity
pnlreflects the ideal P&L using intended (pre-slippage) prices.net_pnlsubtracts 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 –BUYfor long,SELLfor 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, orNonewhen the position was force-closed at end-of-data.commission (
float) – Total commission (entry + exit). Defaults to0.0.slippage_cost (
float) – Total dollar slippage cost (entry + exit). Defaults to0.0.exit_reason (ExitReason | None)
- direction: SignalDirection
- entry_signal: SignalOutput
- exit_signal: SignalOutput | None
- exit_reason: ExitReason | None = None
- class quaver.backtest.portfolio.Portfolio(initial_capital, quantity_per_trade=1.0, commission=None, slippage=None, sizing_fn=None, exit_rules=None)[source]
Bases:
objectTracks cash balance, one open position at a time, and closed trades.
P&L conventions
Long:
pnl = (exit_price - entry_price) * quantityShort:
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
WARNINGis 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
WARNINGand is a no-op.- Parameters:
initial_capital (
float) – Starting cash balance.quantity_per_trade (
float) – Fixed number of units used for every trade. Defaults to1.0.commission (
CommissionConfig|None) – Commission configuration.Nonemeans zero cost.slippage (
SlippageConfig|None) – Slippage configuration.Nonemeans 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:
- 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:
- is_flat()[source]
Return
Trueif no position is currently open.- Returns:
Truewhen the portfolio holds no open position,Falseotherwise.- Return type:
- 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) – TheSignalOutputthat triggered this entry.
- Returns:
None
- Return type:
- 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:
ts (
datetime) – Timestamp of the exit bar.price (
float) – Execution (close) price.signal (
SignalOutput|None) – TheSignalOutputthat triggered the exit, orNonewhen force-closed at end-of-data.exit_reason (
ExitReason|None) – Why the position was closed.
- Returns:
The completed
TradeRecord.- Return type:
- 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) – TheSignalOutputthat triggered this entry.
- Returns:
None
- Return type:
- 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:
ts (
datetime) – Timestamp of the exit bar.price (
float) – Execution (close) price.signal (
SignalOutput|None) – TheSignalOutputthat triggered the exit, orNonewhen force-closed at end-of-data.exit_reason (
ExitReason|None) – Why the position was closed.
- Returns:
The completed
TradeRecord.- Return type:
- Raises:
RuntimeError – If no position is currently open.
Result
BacktestResult: immutable summary of a completed backtest run.
- class quaver.backtest.result.BacktestResult(instrument_id, initial_capital, final_cash, trades)[source]
Bases:
objectImmutable 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. Returns0.0when 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. Returns0.0when fewer than 2 trades are present or whenstd(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 completedTradeRecordobjects.
- trades: list[TradeRecord]
- property total_return: float
Fractional return over the full backtest period.
Computed as
(final_cash - initial_capital) / initial_capital. Returns0.0wheninitial_capitalis zero.- Returns:
Fractional total return (e.g.
0.15means +15%).- Return type:
- property winning_trades: int
Number of trades with a strictly positive P&L.
- Returns:
Count of trades where
pnl > 0.- Return type:
- property losing_trades: int
Number of trades with a zero or negative P&L.
- Returns:
Count of trades where
pnl <= 0.- Return type:
- property win_rate: float
Fraction of trades that were profitable.
Returns
0.0when no trades have been recorded.- Returns:
winning_trades / total_tradesin the range[0.0, 1.0].- Return type:
- property avg_pnl: float
Average P&L per trade.
Returns
0.0when no trades have been recorded.- Returns:
Arithmetic mean of per-trade P&L values.
- Return type:
- 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. Returns0.0when no trades are present or wheninitial_capitalis zero.- Returns:
Maximum drawdown as a non-positive fraction (e.g.
-0.12means a 12% drawdown relative to initial capital).- Return type:
- property sharpe_ratio: float
Per-trade Sharpe proxy scaled by
sqrt(252).Formula:
sharpe = mean(pnl) / std(pnl, ddof=1) * sqrt(252)
Returns
0.0when fewer than 2 trades are present or whenstd(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:
- property profit_factor: float
Ratio of gross profit to gross loss.
Formula:
profit_factor = sum(winning pnl) / abs(sum(losing pnl))
Returns
0.0when there are no trades, orfloat('inf')when all trades are profitable (no losing trades).- Returns:
Profit factor rounded to 4 decimal places,
0.0when no trades exist, orinfwhen gross loss is zero.- Return type:
- property max_consecutive_wins: int
Longest streak of consecutive trades with
pnl > 0.- Returns:
Length of the longest winning streak, or
0if no trades.- Return type:
- property max_consecutive_losses: int
Longest streak of consecutive trades with
pnl <= 0.- Returns:
Length of the longest losing streak, or
0if no trades.- Return type:
- property avg_win: float
Mean P&L of winning trades (
pnl > 0).Returns
0.0when there are no winning trades.- Returns:
Average winning P&L.
- Return type:
- property avg_loss: float
Mean P&L of losing trades (
pnl <= 0), non-positive.Returns
0.0when there are no losing trades.- Returns:
Average losing P&L (non-positive value).
- Return type:
- property recovery_factor: float
Total return divided by the absolute value of max drawdown.
Returns
0.0when there is no drawdown.- Returns:
Recovery factor.
- Return type:
- 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:
- property total_commission: float
Sum of commissions across all trades.
- Returns:
Total commission in dollars.
- Return type:
- property total_slippage: float
Sum of slippage costs across all trades.
- Returns:
Total slippage cost in dollars.
- Return type:
- classmethod from_portfolio(portfolio, candles, instrument_id)[source]
Construct a
BacktestResultfrom a completedPortfolio.- 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:
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:
Raise
ValueErrorif any required column is missing. Required columns:ts_col,open,high,low,close,volume.Cast all OHLCV columns to
float64.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.Sort ascending by the timestamp column, drop duplicate timestamp rows (keeping the last occurrence), and reset the index.
Return a copy; the input DataFrame is never mutated.
- Parameters:
- Returns:
Cleaned DataFrame with a monotonic, tz-naive UTC timestamp column and all OHLCV columns cast to
float64.- Return type:
- Raises:
ValueError – If one or more required columns are absent from
df.
- quaver.backtest.data.validate_candles(df, required, label='')[source]
Raise
ValueErrorifdfhas fewer rows thanrequired.- Parameters:
df (
DataFrame) – Normalised candles DataFrame (output ofnormalise_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:
- Raises:
ValueError – If
len(df) < required.