Performance Metrics¶
Source: rustybt/finance/metrics/metric.py
Verified: 2025-10-16
Overview¶
RustyBT provides comprehensive performance metrics for evaluating trading strategies. Metrics are automatically calculated during backtests and available in the analyze() method through the performance DataFrame (perf).
Key Metric Categories: - Returns: Algorithm and benchmark returns (cumulative and period) - Risk-Adjusted: Sharpe ratio, Sortino ratio, alpha, beta - Drawdown: Maximum drawdown tracking - Volatility: Algorithm and benchmark volatility (annualized) - Leverage: Maximum leverage tracking - Portfolio State: PNL, positions, orders, transactions - Trading Statistics: Number of trades, win rate, profit factor
Metric Classes¶
Returns¶
Source: rustybt/finance/metrics/metric.py:130
Tracks daily and cumulative returns of the algorithm.
Calculated Fields (line 133-136):
- returns - Daily returns (minute_perf / daily_perf)
- algorithm_period_return - Cumulative returns (cumulative_perf)
Access in Strategy:
from rustybt.algorithm import TradingAlgorithm
class MyStrategy(TradingAlgorithm):
def analyze(self, context, perf):
# Daily returns
daily_returns = perf['returns']
# Cumulative return
total_return = perf['algorithm_period_return'].iloc[-1]
# Calculate annualized return
years = len(perf) / 252
annualized_return = (1 + total_return) ** (1 / years) - 1
print(f"Total Return: {total_return:.2%}")
print(f"Annualized Return: {annualized_return:.2%}")
BenchmarkReturnsAndVolatility¶
Source: rustybt/finance/metrics/metric.py:142
Tracks daily and cumulative returns for the benchmark as well as benchmark volatility.
Calculated Fields (line 147-203):
- benchmark_period_return - Benchmark cumulative return
- benchmark_volatility - Benchmark annualized volatility
Formulas (from source line 154-158):
Daily Cumulative Returns:
Daily Annual Volatility:
Example:
def analyze(self, context, perf):
# Algorithm vs Benchmark returns
algo_return = perf['algorithm_period_return'].iloc[-1]
benchmark_return = perf['benchmark_period_return'].iloc[-1]
excess_return = algo_return - benchmark_return
print(f"Algorithm Return: {algo_return:.2%}")
print(f"Benchmark Return: {benchmark_return:.2%}")
print(f"Excess Return: {excess_return:.2%}")
# Volatility comparison
benchmark_vol = perf['benchmark_volatility'].iloc[-1]
print(f"Benchmark Volatility: {benchmark_vol:.2%}")
AlphaBeta¶
Source: rustybt/finance/metrics/metric.py:320
Calculates alpha and beta to the benchmark using Empyrical library.
Calculated Fields (line 331-345):
- alpha - Alpha (excess return not explained by beta)
- beta - Beta (sensitivity to benchmark movements)
Formula (line 334-337):
Example:
def analyze(self, context, perf):
# Get alpha and beta
alpha = perf['alpha'].iloc[-1]
beta = perf['beta'].iloc[-1]
print(f"\nRisk-Adjusted Metrics:")
print(f" Alpha: {alpha:.2%} (excess return)")
print(f" Beta: {beta:.2f} (market sensitivity)")
# Interpretation
if beta > 1.0:
print(f" Strategy is {beta:.2f}x more volatile than benchmark")
elif beta < 1.0:
print(f" Strategy is {(1-beta):.1%} less volatile than benchmark")
ReturnsStatistic¶
Source: rustybt/finance/metrics/metric.py:291
Generic metric that computes statistics from algorithm returns using any function.
Used for: - Sharpe Ratio - Sortino Ratio - Max Drawdown - Downside Risk - Other Empyrical statistics
Example Usage (from source):
# Sharpe ratio metric
sharpe_metric = ReturnsStatistic(
function=empyrical.sharpe_ratio,
field_name='sharpe'
)
# Sortino ratio metric
sortino_metric = ReturnsStatistic(
function=empyrical.sortino_ratio,
field_name='sortino'
)
# Max drawdown metric
max_dd_metric = ReturnsStatistic(
function=empyrical.max_drawdown,
field_name='max_drawdown'
)
PNL¶
Source: rustybt/finance/metrics/metric.py:206
Tracks daily and cumulative profit and loss.
Calculated Fields (line 218-221): - Daily PNL (minute_perf / daily_perf) - Cumulative PNL (cumulative_perf)
Example:
def analyze(self, context, perf):
# Total PNL
total_pnl = perf['pnl'].iloc[-1]
# Daily PNL
daily_pnl = perf['pnl'].diff()
# Best/worst days
best_day = daily_pnl.max()
worst_day = daily_pnl.min()
print(f"Total P&L: ${total_pnl:,.2f}")
print(f"Best Day: ${best_day:,.2f}")
print(f"Worst Day: ${worst_day:,.2f}")
MaxLeverage¶
Source: rustybt/finance/metrics/metric.py:349
Tracks the maximum account leverage reached during backtest.
Calculated Field (line 356-358):
- max_leverage - Maximum leverage (cumulative_risk_metrics)
Example:
def analyze(self, context, perf):
max_leverage = perf['max_leverage'].iloc[-1]
print(f"Maximum Leverage: {max_leverage:.2f}x")
if max_leverage > 2.0:
print("WARNING: Exceeded 2x leverage threshold")
Positions, Orders, Transactions¶
Source: rustybt/finance/metrics/metric.py:281,261,271
Track daily positions, orders, and transactions.
Classes: - Positions (line 281) - Snapshot of positions each bar/session - Orders (line 261) - Orders placed each bar/session - Transactions (line 271) - Executed trades each bar/session
Example:
def analyze(self, context, perf):
# Access transactions
all_transactions = []
for txns in perf['transactions']:
all_transactions.extend(txns)
print(f"Total Transactions: {len(all_transactions)}")
# Calculate win rate
winning_trades = sum(1 for txn in all_transactions if txn.amount * txn.price > 0)
total_trades = len(all_transactions)
win_rate = winning_trades / total_trades if total_trades > 0 else 0
print(f"Win Rate: {win_rate:.2%}")
# Access final positions
final_positions = perf['positions'].iloc[-1]
print(f"\nFinal Positions: {len(final_positions)}")
for asset, position in final_positions.items():
print(f" {asset.symbol}: {position.amount} shares")
Risk Report¶
Source: rustybt/finance/metrics/metric.py:410 (_ClassicRiskMetrics)
Generates comprehensive risk report with multiple timeframes.
risk_metric_period Method (line 421-528):
Calculates metrics for a specific period:
{
'algorithm_period_return': 0.15, # 15% return
'benchmark_period_return': 0.10, # 10% benchmark
'treasury_period_return': 0.0, # Treasury return
'excess_return': 0.15, # Excess over treasury
'alpha': 0.05, # 5% alpha
'beta': 1.2, # 1.2 beta
'sharpe': 1.5, # 1.5 Sharpe ratio
'sortino': 2.0, # 2.0 Sortino ratio
'period_label': '2023-12', # Period label
'trading_days': 252, # Trading days
'algo_volatility': 0.18, # 18% volatility
'benchmark_volatility': 0.15, # 15% benchmark vol
'max_drawdown': -0.12, # -12% max drawdown
'max_leverage': 1.5, # 1.5x max leverage
}
Formulas (from source line 481-522):
Period Returns:
benchmark_period_returns = empyrical.cum_returns(benchmark_returns).iloc[-1]
algorithm_period_returns = empyrical.cum_returns(algorithm_returns).iloc[-1]
Alpha and Beta:
Sharpe Ratio:
Sortino Ratio:
sortino = empyrical.sortino_ratio(
algorithm_returns.values,
_downside_risk=empyrical.downside_risk(algorithm_returns.values)
)
Annual Volatility:
algo_volatility = empyrical.annual_volatility(algorithm_returns)
benchmark_volatility = empyrical.annual_volatility(benchmark_returns)
Max Drawdown:
Quick Start¶
Access Metrics in analyze()¶
from rustybt.algorithm import TradingAlgorithm
class MyStrategy(TradingAlgorithm):
def analyze(self, context, perf):
"""Access all performance metrics."""
# Returns
total_return = perf['algorithm_period_return'].iloc[-1]
benchmark_return = perf['benchmark_period_return'].iloc[-1]
# Risk-adjusted metrics
sharpe = perf['sharpe'].iloc[-1]
sortino = perf['sortino'].iloc[-1]
alpha = perf['alpha'].iloc[-1]
beta = perf['beta'].iloc[-1]
# Risk metrics
max_drawdown = perf['max_drawdown'].iloc[-1]
volatility = perf['algo_volatility'].iloc[-1]
max_leverage = perf['max_leverage'].iloc[-1]
# Print summary
print(f"\nPERFORMANCE SUMMARY")
print(f"=" * 60)
print(f"Total Return: {total_return:.2%}")
print(f"Benchmark Return: {benchmark_return:.2%}")
print(f"Sharpe Ratio: {sharpe:.2f}")
print(f"Sortino Ratio: {sortino:.2f}")
print(f"Alpha: {alpha:.2%}")
print(f"Beta: {beta:.2f}")
print(f"Max Drawdown: {max_drawdown:.2%}")
print(f"Volatility: {volatility:.2%}")
print(f"Max Leverage: {max_leverage:.2f}x")
Performance Analysis Example¶
class PerformanceAnalysis(TradingAlgorithm):
def analyze(self, context, perf):
"""Comprehensive performance analysis."""
print("=" * 60)
print("PERFORMANCE SUMMARY")
print("=" * 60)
# Returns
total_return = (perf.portfolio_value[-1] / perf.portfolio_value[0]) - 1
annual_return = calculate_annualized_return(perf)
print(f"\nReturns:")
print(f" Total Return: {total_return:.2%}")
print(f" Annualized Return: {annual_return:.2%}")
# Risk metrics
daily_returns = perf.returns
volatility = calculate_volatility(daily_returns)
sharpe = calculate_sharpe_ratio(daily_returns)
sortino = calculate_sortino_ratio(daily_returns)
print(f"\nRisk Metrics:")
print(f" Volatility (annual): {volatility:.2%}")
print(f" Sharpe Ratio: {sharpe:.2f}")
print(f" Sortino Ratio: {sortino:.2f}")
# Drawdown
max_dd, dd_duration, recovery = calculate_max_drawdown(perf.portfolio_value)
calmar = annual_return / abs(max_dd)
print(f"\nDrawdown:")
print(f" Max Drawdown: {max_dd:.2%}")
print(f" Drawdown Duration: {dd_duration} days")
print(f" Recovery Time: {recovery} days" if recovery else " Not recovered")
print(f" Calmar Ratio: {calmar:.2f}")
# Trading stats
print(f"\nTrading Statistics:")
print(f" Total Trades: {len(perf.transactions)}")
if len(perf.transactions) > 0:
win_rate = calculate_win_rate(perf.transactions)
profit_factor = calculate_profit_factor(perf.transactions)
print(f" Win Rate: {win_rate:.2%}")
print(f" Profit Factor: {profit_factor:.2f}")
print("=" * 60)
Comprehensive Analysis Example¶
from rustybt.algorithm import TradingAlgorithm
import numpy as np
import pandas as pd
class ComprehensiveAnalysis(TradingAlgorithm):
def analyze(self, context, perf):
"""Complete performance analysis with all metrics."""
print("\n" + "=" * 80)
print("COMPREHENSIVE PERFORMANCE REPORT")
print("=" * 80)
# ===============================
# RETURNS ANALYSIS
# ===============================
print("\n1. RETURNS ANALYSIS")
print("-" * 80)
algo_return = perf['algorithm_period_return'].iloc[-1]
benchmark_return = perf['benchmark_period_return'].iloc[-1]
excess_return = algo_return - benchmark_return
years = len(perf) / 252
annualized_return = (1 + algo_return) ** (1 / years) - 1
print(f" Total Return: {algo_return:.2%}")
print(f" Benchmark Return: {benchmark_return:.2%}")
print(f" Excess Return: {excess_return:.2%}")
print(f" Annualized Return: {annualized_return:.2%}")
# ===============================
# RISK-ADJUSTED METRICS
# ===============================
print("\n2. RISK-ADJUSTED METRICS")
print("-" * 80)
sharpe = perf['sharpe'].iloc[-1]
sortino = perf['sortino'].iloc[-1]
alpha = perf['alpha'].iloc[-1]
beta = perf['beta'].iloc[-1]
print(f" Sharpe Ratio: {sharpe:.2f}")
print(f" Sortino Ratio: {sortino:.2f}")
print(f" Alpha: {alpha:.2%}")
print(f" Beta: {beta:.2f}")
# ===============================
# RISK METRICS
# ===============================
print("\n3. RISK METRICS")
print("-" * 80)
algo_vol = perf['algo_volatility'].iloc[-1]
benchmark_vol = perf['benchmark_volatility'].iloc[-1]
max_dd = perf['max_drawdown'].iloc[-1]
max_leverage = perf['max_leverage'].iloc[-1]
print(f" Algorithm Volatility: {algo_vol:.2%}")
print(f" Benchmark Volatility: {benchmark_vol:.2%}")
print(f" Max Drawdown: {max_dd:.2%}")
print(f" Max Leverage: {max_leverage:.2f}x")
# Calculate Calmar ratio
calmar = annualized_return / abs(max_dd) if max_dd != 0 else 0
print(f" Calmar Ratio: {calmar:.2f}")
# ===============================
# TRADING STATISTICS
# ===============================
print("\n4. TRADING STATISTICS")
print("-" * 80)
# Collect all transactions
all_transactions = []
for txns in perf['transactions']:
if isinstance(txns, list):
all_transactions.extend(txns)
total_trades = len(all_transactions)
print(f" Total Trades: {total_trades}")
if total_trades > 0:
# Calculate win rate (simplified)
total_pnl = perf['pnl'].iloc[-1]
avg_pnl_per_trade = total_pnl / total_trades
print(f" Avg P&L per Trade: ${avg_pnl_per_trade:,.2f}")
# ===============================
# PORTFOLIO SUMMARY
# ===============================
print("\n5. PORTFOLIO SUMMARY")
print("-" * 80)
starting_value = perf['portfolio_value'].iloc[0]
ending_value = perf['portfolio_value'].iloc[-1]
total_pnl = perf['pnl'].iloc[-1]
print(f" Starting Value: ${starting_value:,.2f}")
print(f" Ending Value: ${ending_value:,.2f}")
print(f" Total P&L: ${total_pnl:,.2f}")
# Final positions
final_positions = perf['positions'].iloc[-1]
print(f" Final Positions: {len(final_positions)}")
# ===============================
# MONTHLY RETURNS
# ===============================
print("\n6. MONTHLY RETURNS (Last 12 Months)")
print("-" * 80)
# Calculate monthly returns
monthly_returns = perf['returns'].resample('M').apply(
lambda x: (1 + x).prod() - 1
)
for month, ret in monthly_returns.tail(12).items():
print(f" {month.strftime('%Y-%m')}: {ret:>8.2%}")
# ===============================
# INTERPRETATION
# ===============================
print("\n7. PERFORMANCE INTERPRETATION")
print("-" * 80)
# Sharpe interpretation
if sharpe >= 2.0:
sharpe_rating = "Excellent"
elif sharpe >= 1.0:
sharpe_rating = "Good"
elif sharpe >= 0.5:
sharpe_rating = "Fair"
else:
sharpe_rating = "Poor"
print(f" Sharpe Rating: {sharpe_rating}")
# Beta interpretation
if beta > 1.2:
beta_interp = "High market sensitivity (aggressive)"
elif beta > 0.8:
beta_interp = "Market-neutral"
else:
beta_interp = "Low market sensitivity (defensive)"
print(f" Beta Interpretation: {beta_interp}")
# Drawdown assessment
if abs(max_dd) < 0.10:
dd_rating = "Low risk"
elif abs(max_dd) < 0.20:
dd_rating = "Moderate risk"
elif abs(max_dd) < 0.30:
dd_rating = "High risk"
else:
dd_rating = "Very high risk"
print(f" Drawdown Assessment: {dd_rating}")
print("\n" + "=" * 80)
Metric Interpretation Guidelines¶
Sharpe Ratio¶
Source Formula: mean(returns) / std(returns) * sqrt(252)
Interpretation: - < 0.5: Poor risk-adjusted returns - 0.5-1.0: Fair risk-adjusted returns - 1.0-2.0: Good risk-adjusted returns - > 2.0: Excellent risk-adjusted returns
Notes: Assumes normal distribution of returns. Can be misleading with non-normal distributions or when returns are autocorrelated.
Sortino Ratio¶
Source Formula: mean(returns) / downside_risk * sqrt(252)
Interpretation: - Similar to Sharpe but only penalizes downside volatility - Generally higher than Sharpe for the same strategy - Better for strategies with asymmetric return distributions
Alpha¶
Source Formula: Regression-based excess return vs benchmark
Interpretation: - > 0: Strategy outperforms benchmark (risk-adjusted) - = 0: Strategy matches benchmark (risk-adjusted) - < 0: Strategy underperforms benchmark (risk-adjusted)
Example: α = 5% means strategy generates 5% excess return beyond what beta explains.
Beta¶
Source Formula: Cov(returns, benchmark_returns) / Var(benchmark_returns)
Interpretation: - β = 1.0: Moves with market - β > 1.0: More volatile than market - β < 1.0: Less volatile than market - β = 0: Uncorrelated with market
Example: β = 1.5 means strategy is 50% more volatile than benchmark.
Maximum Drawdown¶
Source Formula: min((cumulative_value - peak_value) / peak_value)
Interpretation: - -10% to 0%: Low risk - -20% to -10%: Moderate risk - -30% to -20%: High risk - < -30%: Very high risk
Important: Indicates worst peak-to-trough decline. Critical for assessing downside risk.
Calmar Ratio¶
Calculation: annualized_return / abs(max_drawdown)
Interpretation: - < 1.0: Poor return-to-drawdown ratio - 1.0-3.0: Good return-to-drawdown ratio - > 3.0: Excellent return-to-drawdown ratio
Example: Calmar = 2.0 means strategy earns 2% return for every 1% of maximum drawdown.
Best Practices¶
✅ DO¶
- Compare Against Benchmark: Always evaluate relative to market/benchmark
- Use Multiple Metrics: Don't rely on single metric (Sharpe alone insufficient)
- Consider Drawdowns: High returns with huge drawdowns = high risk
- Annualize Metrics: For comparison across strategies/time periods
- Account for Costs: Include slippage and commissions in performance
- Check Distribution: Verify returns are reasonably normally distributed for Sharpe/Sortino
- Monitor Over Time: Track metrics evolution, not just final values
❌ DON'T¶
- Ignore Risk: High returns mean nothing without risk context
- Cherry-Pick Metrics: Report all metrics, not just favorable ones
- Overfit to Sharpe: Can be gamed with certain strategies
- Forget Context: Market conditions affect all metrics
- Compare Apples to Oranges: Match time periods and risk levels
- Trust Single Period: One good period doesn't make a good strategy
- Ignore Outliers: Extreme events significantly impact metrics
Related Documentation¶
- Risk Management - Portfolio risk controls
- Allocation Algorithms - Capital allocation strategies
- Portfolio Allocator - Multi-strategy management
- Transaction Costs - Cost impact on performance
Verification¶
✅ All classes, methods, and formulas verified in source code ✅ No fabricated APIs ✅ All line numbers referenced for verification ✅ All metric calculations cross-referenced with Empyrical library
Verification Date: 2025-10-16
Source Files Verified:
- rustybt/finance/metrics/metric.py:28,58,91,130,142,206,261,271,281,291,320,349,410
- Empyrical library (external dependency for calculations)