The backtesting engine provides comprehensive performance metrics and statistics to evaluate strategy quality.
BacktestMetrics
The primary metrics structure is defined in nano-backtest/src/metrics.rs:10-52.
Core Metrics
pub struct BacktestMetrics {
pub total_pnl: f64, // Total profit/loss
pub realized_pnl: f64, // Realized P&L from closed trades
pub unrealized_pnl: f64, // Mark-to-market unrealized P&L
pub total_fees: f64, // Total fees paid
pub num_trades: u32, // Number of round-trip trades
pub winning_trades: u32, // Number of profitable trades
pub losing_trades: u32, // Number of losing trades
pub gross_profit: f64, // Sum of all winning trades
pub gross_loss: f64, // Sum of all losing trades
pub max_drawdown_pct: f64, // Maximum drawdown percentage
pub max_drawdown_abs: f64, // Maximum drawdown in dollars
pub peak_pnl: f64, // Peak P&L reached
pub total_volume: u64, // Total contracts traded
// ... additional metrics
}
Accessing Metrics
From engine.rs:325-329:
let metrics = engine.metrics();
println!("Total P&L: ${:.2}", metrics.total_pnl);
println!("Realized P&L: ${:.2}", metrics.realized_pnl);
println!("Total Fees: ${:.2}", metrics.total_fees);
println!("Net P&L: ${:.2}", metrics.total_pnl - metrics.total_fees);
Win Rate
Percentage of profitable trades (metrics.rs:62-68):
let win_rate = metrics.win_rate();
println!("Win Rate: {:.1}%", win_rate * 100.0);
// Calculation
win_rate = winning_trades / total_trades
Interpretation:
- 50%+: Strategy has edge (for equal-sized trades)
- 40-50%: Acceptable if winners > losers
- <40%: Likely unprofitable unless avg winner >> avg loser
Profit Factor
Ratio of gross profit to gross loss (metrics.rs:71-77):
let profit_factor = metrics.profit_factor();
println!("Profit Factor: {:.2}", profit_factor);
// Calculation
profit_factor = gross_profit / abs(gross_loss)
Interpretation:
- >2.0: Excellent strategy
- 1.5-2.0: Good strategy
- 1.0-1.5: Marginal, needs improvement
- <1.0: Unprofitable
Average Trade P&L
Mean profit per trade (metrics.rs:80-86):
let avg_pnl = metrics.avg_trade_pnl();
let avg_winner = metrics.avg_winning_trade();
let avg_loser = metrics.avg_losing_trade();
println!("Avg Trade: ${:.2}", avg_pnl);
println!("Avg Winner: ${:.2}", avg_winner);
println!("Avg Loser: ${:.2}", avg_loser);
Interpretation:
- Average trade should be significantly positive after fees
- For HFT: Even 1−5 per round-trip can be profitable at scale
- Avg winner should typically be larger than avg loser
Maker Ratio
Percentage of fills that added liquidity (metrics.rs:107-114):
let maker_ratio = metrics.maker_ratio();
println!("Maker Ratio: {:.1}%", maker_ratio * 100.0);
// Calculation
maker_ratio = maker_fills / (maker_fills + taker_fills)
Interpretation:
- >80%: Excellent for fee optimization
- 60-80%: Good maker-taker balance
- <50%: High taker fees may erode profits
For CME futures, maker rebates vs taker fees can be $0.60+ difference per contract.
Maximum Drawdown
Drawdown Calculation
From metrics.rs:146-167:
// Percentage drawdown
let max_dd_pct = metrics.max_drawdown_pct;
println!("Max Drawdown: {:.2}%", max_dd_pct * 100.0);
// Absolute drawdown
let max_dd_abs = metrics.max_drawdown_abs;
println!("Max Drawdown: ${:.2}", max_dd_abs);
// Calculation
drawdown = (peak_pnl - current_pnl) / peak_pnl
Drawdown Tracking
The engine continuously updates drawdown (metrics.rs:156-166):
if total_pnl > peak_pnl {
peak_pnl = total_pnl; // New peak
}
let drawdown = peak_pnl - total_pnl;
let drawdown_pct = drawdown / peak_pnl;
if drawdown_pct > max_drawdown_pct {
max_drawdown_pct = drawdown_pct; // New max drawdown
}
Interpretation:
- <5%: Excellent risk management
- 5-10%: Good for HFT strategies
- 10-20%: Acceptable for lower-frequency strategies
- >20%: High risk, review strategy and risk limits
High drawdowns can trigger the kill switch if max_drawdown_pct is exceeded in the risk configuration.
Advanced Statistics
The PerformanceStats struct (metrics.rs:186-208) provides deeper analytics.
Sharpe Ratio
Risk-adjusted return metric (metrics.rs:260-275):
let sharpe = stats.sharpe_ratio;
println!("Sharpe Ratio: {:.2}", sharpe);
// Calculation (annualized)
mean_daily_return = mean(daily_returns)
std_daily_return = std_dev(daily_returns)
sharpe = (mean / std) * sqrt(252) // 252 trading days
Interpretation:
- >3.0: Excellent (rare for real strategies)
- 2.0-3.0: Very good
- 1.0-2.0: Good
- <1.0: Poor risk-adjusted returns
HFT strategies often achieve Sharpe ratios of 2-4 due to low volatility and consistent returns.
Sortino Ratio
Downside risk-adjusted return (metrics.rs:278-299):
let sortino = stats.sortino_ratio;
println!("Sortino Ratio: {:.2}", sortino);
// Calculation (annualized)
mean_return = mean(daily_returns)
downside_std = std_dev(negative_returns_only)
sortino = (mean / downside_std) * sqrt(252)
Interpretation:
- Similar to Sharpe, but only penalizes downside volatility
- Better metric for asymmetric return distributions
- Higher Sortino than Sharpe indicates positive skew
Calmar Ratio
Return relative to maximum drawdown (metrics.rs:301-309):
let calmar = stats.calmar_ratio;
println!("Calmar Ratio: {:.2}", calmar);
// Calculation
annual_return = mean(daily_returns) * 252
calmar = annual_return / max_drawdown_pct
Interpretation:
- >3.0: Excellent return/drawdown profile
- 1.0-3.0: Good
- <1.0: Returns don’t justify drawdown risk
Consecutive Wins/Losses
Track winning and losing streaks (metrics.rs:311-332):
let max_wins = stats.max_consecutive_wins;
let max_losses = stats.max_consecutive_losses;
println!("Max Consecutive Wins: {}", max_wins);
println!("Max Consecutive Losses: {}", max_losses);
Interpretation:
- Long losing streaks indicate strategy may need adjustment
- Very long winning streaks may indicate overfitting
- Ratio should be reasonable (e.g., max_wins/max_losses ≈ 2-3)
Recovery Factor
Ability to recover from drawdowns (metrics.rs:334-342):
let recovery = stats.recovery_factor;
println!("Recovery Factor: {:.2}", recovery);
// Calculation
total_return_pct = total_pnl / initial_capital
recovery_factor = total_return_pct / max_drawdown_pct
Interpretation:
- >5.0: Strong recovery capability
- 2.0-5.0: Moderate recovery
- <2.0: Slow recovery from drawdowns
Equity Curve Analysis
The equity curve tracks cumulative P&L over time (metrics.rs:235-238):
let equity_curve = stats.equity_curve;
let timestamps = stats.equity_timestamps;
// Plot or analyze equity progression
for (i, (×tamp, &pnl)) in timestamps.iter().zip(equity_curve.iter()).enumerate() {
println!("[{}] {}: ${:.2}", i, timestamp, pnl);
}
Equity Curve Characteristics
Smooth Upward Trend:
Ideal - consistent positive returns with low volatility.
Volatile but Positive:
Positive but risky - high variance in returns.
Drawdown Pattern:
Periods of losses - analyze what caused drawdown.
Volume and Fill Analysis
From metrics.rs:36-50:
// Trading volume
let total_volume = metrics.total_volume;
let buy_fills = metrics.buy_fills;
let sell_fills = metrics.sell_fills;
println!("Total Volume: {} contracts", total_volume);
println!("Buy Fills: {}", buy_fills);
println!("Sell Fills: {}", sell_fills);
// Liquidity provision
let maker_fills = metrics.maker_fills;
let taker_fills = metrics.taker_fills;
println!("Maker Fills: {}", maker_fills);
println!("Taker Fills: {}", taker_fills);
println!("Maker Ratio: {:.1}%", metrics.maker_ratio() * 100.0);
Volume Analysis:
- High volume strategies can profit with small edge per trade
- Unbalanced buy/sell may indicate directional bias
- High maker ratio reduces total trading costs
Time-Based Metrics
From metrics.rs:170-182:
// Backtest duration
let duration_secs = metrics.duration_secs();
let duration_hours = duration_secs / 3600.0;
let duration_days = duration_hours / 24.0;
println!("Backtest Duration: {:.2} hours", duration_hours);
// Trades per hour
let trades_per_hour = metrics.num_trades as f64 / duration_hours;
println!("Trade Frequency: {:.1} trades/hour", trades_per_hour);
// Average P&L per hour
let pnl_per_hour = metrics.total_pnl / duration_hours;
println!("P&L Rate: ${:.2}/hour", pnl_per_hour);
Rolling Statistics
The RollingStats calculator (metrics.rs:346-442) tracks windowed metrics:
use nano_backtest::metrics::RollingStats;
// Create 100-trade rolling window
let mut rolling = RollingStats::new(100);
for trade_pnl in trade_pnls {
rolling.add(trade_pnl);
if rolling.is_full() {
let mean = rolling.mean();
let std = rolling.std_dev();
let sharpe = rolling.sharpe();
println!("Rolling Sharpe (100 trades): {:.2}", sharpe);
}
}
Use Cases:
- Detect strategy degradation over time
- Monitor consistency of returns
- Adaptive risk management based on recent performance
Interpreting Results
Example Good Strategy
Total P&L: $127,450.00
Realized P&L: $125,320.00
Total Fees: $18,250.00
Net P&L: $109,200.00
Num Trades: 1,247
Winning Trades: 789 (63.3%)
Losing Trades: 458 (36.7%)
Profit Factor: 2.31
Avg Trade: $87.56
Avg Winner: $203.45
Avg Loser: -$89.32
Max Drawdown: 3.24%
Sharpe Ratio: 2.87
Sortino Ratio: 3.45
Calmar Ratio: 4.12
Maker Ratio: 76.3%
Total Volume: 12,470 contracts
Analysis:
- Strong win rate (63%)
- Excellent profit factor (2.31)
- Low drawdown (3.24%)
- High Sharpe ratio (2.87)
- Good maker ratio (76%)
- Avg winner > Avg loser
Example Problematic Strategy
Total P&L: $12,340.00
Realized P&L: $18,250.00
Total Fees: $22,100.00
Net P&L: -$9,760.00 ⚠️
Num Trades: 3,142
Winning Trades: 1,257 (40.0%) ⚠️
Losing Trades: 1,885 (60.0%)
Profit Factor: 0.87 ⚠️
Avg Trade: -$3.11
Avg Winner: $45.23
Avg Loser: -$68.92 ⚠️
Max Drawdown: 18.45% ⚠️
Sharpe Ratio: 0.34 ⚠️
Sortino Ratio: 0.42
Maker Ratio: 23.1% ⚠️
Issues:
- Unprofitable after fees (profit factor < 1.0)
- Low win rate with larger losers
- High drawdown (18%)
- Poor Sharpe ratio (0.34)
- Too many taker fills (expensive)
- Over-trading (fees > gross profit)
Metrics Summary Table
| Metric | Good | Acceptable | Poor |
|---|
| Profit Factor | >2.0 | 1.5-2.0 | <1.0 |
| Sharpe Ratio | >2.0 | 1.0-2.0 | <1.0 |
| Win Rate | >55% | 45-55% | <40% |
| Max Drawdown | <5% | 5-10% | >15% |
| Maker Ratio (HFT) | >75% | 60-75% | <50% |
| Calmar Ratio | >3.0 | 1.5-3.0 | <1.0 |
Exporting Results
use serde_json;
use std::fs::File;
// Export metrics to JSON
let metrics_json = serde_json::to_string_pretty(&metrics)?;
std::fs::write("backtest_metrics.json", metrics_json)?;
// Export equity curve to CSV
let mut writer = csv::Writer::from_path("equity_curve.csv")?;
writer.write_record(&["timestamp", "pnl"])?;
for (&ts, &pnl) in stats.equity_timestamps.iter().zip(stats.equity_curve.iter()) {
writer.write_record(&[ts.to_string(), pnl.to_string()])?;
}
writer.flush()?;