Appearance
on_strategy_end
Called once when the backtest completes. Use this function for final cleanup, performance summary, or result analysis.
Signature
python
def on_strategy_end(portfolio, state, results):
# Final cleanup and summary logic
passParameters
- portfolio (
PortfolioContext) - Final portfolio state - state (
dict) - Strategy state dictionary with all accumulated data - results (
dict) - Backtest results containing:initial_capital- Starting capitalfinal_equity- Ending portfolio valuetotal_return- Total return as decimal (0.15 = 15%)total_return_pct- Total return as percentagesharpe_ratio- Risk-adjusted return metricmax_drawdown- Maximum drawdown as decimalmax_drawdown_pct- Maximum drawdown as percentagetotal_trades- Number of trades executedcash_remaining- Final cash balancepositions- Final positions heldtrades- List of all trade recordsequity_curve- Portfolio value over time
Usage
This function is optional but very useful for strategy analysis and debugging.
Common Use Cases
- Log final performance summary
- Generate custom analytics
- Export data for external analysis
- Validate strategy behavior
- Clean up temporary state variables
Examples
Basic Performance Summary
python
def on_strategy_end(portfolio, state, results):
"""Print comprehensive strategy summary"""
print("=" * 50)
print("BACKTEST COMPLETE - FINAL SUMMARY")
print("=" * 50)
# Performance metrics
print(f"Initial Capital: ${results['initial_capital']:,.2f}")
print(f"Final Equity: ${results['final_equity']:,.2f}")
print(f"Total Return: {results['total_return_pct']:.2f}%")
print(f"Sharpe Ratio: {results['sharpe_ratio']:.2f}")
print(f"Max Drawdown: {results['max_drawdown_pct']:.2f}%")
print(f"Total Trades: {results['total_trades']}")
# Custom metrics from state
if 'win_days' in state and 'loss_days' in state:
total_days = state['win_days'] + state['loss_days']
win_rate = state['win_days'] / total_days if total_days > 0 else 0
print(f"Winning Days: {win_rate*100:.1f}% ({state['win_days']}/{total_days})")
# Final positions
if results['positions']:
print(f"Final Positions: {results['positions']}")
else:
print("No final positions (all cash)")Detailed Analytics
python
def on_strategy_end(portfolio, state, results):
"""Generate detailed performance analytics"""
# Trade analysis
trades = results['trades']
if trades:
winning_trades = [t for t in trades if t.get('pnl', 0) > 0]
losing_trades = [t for t in trades if t.get('pnl', 0) < 0]
print(f"Trade Analysis:")
print(f" Total Trades: {len(trades)}")
print(f" Winning Trades: {len(winning_trades)} ({len(winning_trades)/len(trades)*100:.1f}%)")
print(f" Losing Trades: {len(losing_trades)} ({len(losing_trades)/len(trades)*100:.1f}%)")
if winning_trades:
avg_win = sum(t.get('pnl', 0) for t in winning_trades) / len(winning_trades)
print(f" Average Win: ${avg_win:.2f}")
if losing_trades:
avg_loss = sum(t.get('pnl', 0) for t in losing_trades) / len(losing_trades)
print(f" Average Loss: ${avg_loss:.2f}")
# Equity curve analysis
equity_curve = results.get('equity_curve', [])
if len(equity_curve) > 1:
# Calculate maximum consecutive losing days
daily_returns = []
for i in range(1, len(equity_curve)):
prev_equity = equity_curve[i-1]['equity']
curr_equity = equity_curve[i]['equity']
daily_return = (curr_equity - prev_equity) / prev_equity
daily_returns.append(daily_return)
# Find longest losing streak
max_losing_streak = 0
current_streak = 0
for ret in daily_returns:
if ret < 0:
current_streak += 1
max_losing_streak = max(max_losing_streak, current_streak)
else:
current_streak = 0
print(f"Risk Metrics:")
print(f" Longest Losing Streak: {max_losing_streak} periods")
print(f" Volatility: {(sum(r**2 for r in daily_returns)/len(daily_returns))**0.5 * 100:.2f}% per period")Export Data for Analysis
python
def on_strategy_end(portfolio, state, results):
"""Export strategy data for external analysis"""
import json
from datetime import datetime
# Prepare export data
export_data = {
'strategy_name': state.get('strategy_name', 'Unknown'),
'backtest_date': datetime.now().isoformat(),
'parameters': {
'initial_capital': results['initial_capital'],
'symbols': state.get('symbols_traded', []),
'start_date': state.get('start_date'),
'end_date': state.get('end_date')
},
'performance': {
'total_return': results['total_return_pct'],
'sharpe_ratio': results['sharpe_ratio'],
'max_drawdown': results['max_drawdown_pct'],
'total_trades': results['total_trades']
},
'trades': results['trades'],
'equity_curve': results.get('equity_curve', []),
'custom_metrics': {
'win_rate': state.get('win_rate'),
'max_consecutive_losses': state.get('max_consecutive_losses'),
'custom_indicators': state.get('custom_indicators', {})
}
}
# Save to file (in real implementation)
print("Strategy data prepared for export")
print(f"Export contains {len(export_data['trades'])} trades")
print(f"Export contains {len(export_data['equity_curve'])} equity points")Strategy Validation
python
def on_strategy_end(portfolio, state, results):
"""Validate strategy behavior and flag potential issues"""
warnings = []
# Check for overfitting signs
if results['total_trades'] < 10:
warnings.append("Very few trades - strategy may be too restrictive")
if results['sharpe_ratio'] > 3:
warnings.append("Extremely high Sharpe ratio - check for look-ahead bias")
# Check for unrealistic performance
annual_return = results['total_return_pct'] # Assuming 1-year backtest
if annual_return > 100:
warnings.append("Very high returns - validate strategy logic")
# Check state usage
if not state:
warnings.append("Strategy state not used - missing optimization opportunities")
# Check for proper risk management
if results['max_drawdown_pct'] > 50:
warnings.append("High drawdown - consider adding risk management")
# Report validation results
if warnings:
print("\n⚠️ STRATEGY VALIDATION WARNINGS:")
for i, warning in enumerate(warnings, 1):
print(f" {i}. {warning}")
else:
print("\n✅ Strategy validation passed - no major issues detected")Custom Reporting
python
def on_strategy_end(portfolio, state, results):
"""Generate custom strategy report"""
# Calculate custom metrics
total_days = len(results.get('equity_curve', []))
if total_days > 0:
# Annualized metrics
years = total_days / 252 # Assuming 252 trading days per year
annualized_return = (results['final_equity'] / results['initial_capital']) ** (1/years) - 1
print(f"\nCustom Performance Report:")
print(f"Trading Days: {total_days}")
print(f"Years Simulated: {years:.2f}")
print(f"Annualized Return: {annualized_return*100:.2f}%")
# Risk-adjusted metrics
if years > 0:
annual_volatility = (results.get('volatility', 0) * (252**0.5)) if results.get('volatility') else 0
print(f"Annualized Volatility: {annual_volatility*100:.2f}%")
if annual_volatility > 0:
calmar_ratio = annualized_return / abs(results['max_drawdown'])
print(f"Calmar Ratio: {calmar_ratio:.2f}")
# Strategy-specific metrics
print(f"\nStrategy Behavior:")
print(f"Average Holding Period: {state.get('avg_holding_period', 'N/A')}")
print(f"Maximum Position Size: {state.get('max_position_size', 'N/A')}")
print(f"Risk Management Triggers: {state.get('risk_triggers', 0)}")Error Handling
Since this function runs at the very end, errors here won't break your backtest, but they also won't be easily caught:
python
def on_strategy_end(portfolio, state, results):
"""Safe strategy end with error handling"""
try:
# Your analysis code here
generate_final_report(portfolio, state, results)
except Exception as e:
print(f"Warning: Error in strategy end analysis: {e}")
# Still print basic summary
print(f"Final Equity: ${portfolio.equity:,.2f}")
print(f"Total Return: {results['total_return_pct']:.2f}%")Notes
- This function runs exactly once at the end of the backtest
- Errors in this function won't affect your backtest results
- Perfect place for generating reports or exporting data
- The
resultsparameter contains all final performance metrics - Use this to validate that your strategy behaved as expected
- This is your last chance to access the complete
statedictionary