Skip to content

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
    pass

Parameters

  • portfolio (PortfolioContext) - Final portfolio state
  • state (dict) - Strategy state dictionary with all accumulated data
  • results (dict) - Backtest results containing:
    • initial_capital - Starting capital
    • final_equity - Ending portfolio value
    • total_return - Total return as decimal (0.15 = 15%)
    • total_return_pct - Total return as percentage
    • sharpe_ratio - Risk-adjusted return metric
    • max_drawdown - Maximum drawdown as decimal
    • max_drawdown_pct - Maximum drawdown as percentage
    • total_trades - Number of trades executed
    • cash_remaining - Final cash balance
    • positions - Final positions held
    • trades - List of all trade records
    • equity_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 results parameter contains all final performance metrics
  • Use this to validate that your strategy behaved as expected
  • This is your last chance to access the complete state dictionary

Test your trading strategies risk-free with professional backtesting.