Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/dhir1007/nanoARB/llms.txt

Use this file to discover all available pages before exploring further.

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);

Key Performance Indicators

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 11-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, (&timestamp, &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

MetricGoodAcceptablePoor
Profit Factor>2.01.5-2.0<1.0
Sharpe Ratio>2.01.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.01.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()?;

Build docs developers (and LLMs) love