QQ 301, 4 min read

The statistics

All of these come from one function, run after the trades are accounted. Key names are the ones the API returns.

3.1 The equity curve everything is measured on

One point per bar:

equity[i] = capital + all cash changes up to bar i + unrealised P&L of open positions at close[i]

A position is marked on every bar from its entry bar up to the bar before its exit; on the exit bar its realised P&L is in cash. A bar with no usable close is marked at the last close that printed.

3.2 Headline numbers

KeyFormula as implemented
net_profitfinal_equity − capital. final_equity is cash after every trade has closed.
totalTradesNumber of trades in the ledger.
winningTradesTrades with pnl > 0.
losingTradesTrades with pnl <= 0. A trade with exactly zero P&L counts as a loss.
winRatewinningTrades / totalTrades × 100. 0 with no trades.
grossProfitSum of winning trades' P&L.
grossLossSum of losing trades' P&L (a negative number).
profitFactorgrossProfit / abs(grossLoss). If there are no losses and some profit it is infinite (sent as the marker "__+Inf__"). If there is neither, 0.
avgTradeMean P&L per trade, in account currency. This is the engine's expectancy figure; there is no separate key called "expectancy".
avgWin, avgLossgrossProfit / winningTrades, grossLoss / losingTrades.
largestWin, largestLossLargest winning P&L, most negative losing P&L.
maxConsecWins, maxConsecLossesLongest runs in ledger order. A zero-P&L trade breaks a winning run and extends a losing one.

All P&L figures are net of commission, slippage, spread, impact and financing, because those are already in the fills and the cash.

3.3 Maximum drawdown

Measured on the per-bar, marked-to-market equity curve, not at trade exits.

peak[i]        = highest equity up to bar i
maxDrawdown    = max over i of (peak[i] − equity[i])                 in account currency
maxDrawdownPct = max over i of (peak[i] − equity[i]) / peak[i] × 100

The two maxima are taken separately, so on a compounding account they can come from different dates. The source comment gives the reason for marking to market: "drawdown measured only at trade exits understates what the account actually went through".

Intrabar highs and lows are not used. A position that went far against you inside a bar and came back by the close shows only the close.

3.4 Sharpe ratio (the one in the results tiles)

r[i]          = (equity[i] − equity[i−1]) / equity[i−1]         one per bar
bars_per_year = (365.25 × 86400) / median bar spacing in seconds
sharpeRatio   = mean(r) / std(r) × sqrt(bars_per_year)

Choices the implementation makes:

  • Per-bar returns, not daily and not per-trade. Bars with no position are zero returns and are kept.
  • Sample standard deviation (divides by n − 1).
  • No risk-free rate is subtracted.
  • Annualised from the median bar spacing with a 365.25-day year. Hourly bars therefore annualise with sqrt(8766), as if the market printed a bar every hour of the year. For a market that closes at weekends or overnight there are fewer bars in a real year than that, so this figure is larger than one annualised on the bars that actually exist.
  • With fewer than two returns, or zero standard deviation, it is 0.

This is not the Sharpe the validation checks use. Those use daily returns and sqrt(252); see 4.1.

3.5 Extended statistics

Returned only when the run option extended_stats is true, under stats.extended. All are computed on the same per-bar returns r.

KeyFormula as implemented
var95, var99−percentile(r, 5) and −percentile(r, 1). Historical, per bar, as a positive loss fraction. Linear interpolation between order statistics.
cvar95, cvar99−mean(r where r <= that percentile). The average of the worst 5% (1%) of bars.
sortinomean(r) / std(negative r only) × sqrt(bars_per_year). The denominator is the sample standard deviation of the negative returns around their own mean, not the downside deviation around zero that some definitions use. 0 if there are no negative returns.
annualizedReturn((final_equity / capital) ^ (1 / years) − 1) × 100, with years = number of bars / bars_per_year. The same calendar assumption as the Sharpe applies. 0 if it cannot be computed.
annualizedVolstd(r) × sqrt(bars_per_year) × 100.
calmarannualizedReturn / maxDrawdownPct. 0 if there was no drawdown.
exposurePctTime in market: bars with at least one open position, over all bars, × 100, rounded to a whole percent. Overlapping trades count a bar once.
avgBarsHeldMean of exit bar − entry bar across trades.

VaR and CVaR here are per bar. A 1-hour bar VaR is not a daily VaR. The institutional suite's tail-risk row reports daily VaR and CVaR instead (4.9).

Source. _stats: backtest/runner.py:1588-1694 (wins and losses 1593-1594; drawdown 1598-1603; Sharpe 1605-1616; streaks 1618-1625; profit factor 1627-1632; extended 1654-1693). Equity curve and time in market: backtest/runner.py:1214-1248. Year constant: backtest/runner.py:221. Net profit: backtest/runner.py:429. Ruin flags: backtest/runner.py:421-423.