Cycles/test/strategy_optimisation/strategy_parameter_optimization.py

466 lines
19 KiB
Python

#!/usr/bin/env python3
"""
Strategy Parameter Optimization Script for IncrementalTrader
This script provides comprehensive parameter optimization for trading strategies,
specifically designed for testing MetaTrend strategy with various configurations
including supertrend parameters, timeframes, and risk management settings.
Features:
- Parallel execution using multiple CPU cores
- Configurable parameter grids for strategy and risk management
- Comprehensive results analysis and reporting
- Support for custom optimization metrics
- Detailed logging and progress tracking
- Individual strategy plotting and analysis
Usage:
python tasks/strategy_parameter_optimization.py --help
"""
import os
import sys
import argparse
import logging
import json
import time
import traceback
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
from concurrent.futures import ProcessPoolExecutor, as_completed
from itertools import product
import pandas as pd
import numpy as np
from tqdm import tqdm
# Import plotting libraries for result visualization
try:
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use('default')
PLOTTING_AVAILABLE = True
except ImportError:
PLOTTING_AVAILABLE = False
# Add project root to path
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, project_root)
# Import IncrementalTrader components
from IncrementalTrader.backtester import IncBacktester, BacktestConfig
from IncrementalTrader.backtester.utils import DataLoader, SystemUtils, ResultsSaver
from IncrementalTrader.strategies import MetaTrendStrategy
from IncrementalTrader.trader import IncTrader
# Set up logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('optimization.log')
]
)
logger = logging.getLogger(__name__)
# Reduce verbosity for entry/exit logging
logging.getLogger('IncrementalTrader.strategies').setLevel(logging.WARNING)
logging.getLogger('IncrementalTrader.trader').setLevel(logging.WARNING)
class StrategyOptimizer:
"""
Advanced parameter optimization for IncrementalTrader strategies.
This class provides comprehensive parameter optimization with parallel processing,
sensitivity analysis, and detailed result reporting.
"""
def __init__(self):
"""Initialize the StrategyOptimizer."""
# Initialize utilities
self.system_utils = SystemUtils()
# Session tracking
self.session_start_time = datetime.now()
self.optimization_results = []
logger.info(f"StrategyOptimizer initialized")
logger.info(f"System info: {self.system_utils.get_system_info()}")
def generate_parameter_combinations(self, params_dict: Dict[str, List]) -> List[Dict[str, Dict]]:
"""
Generate all possible parameter combinations.
Args:
params_dict: Dictionary with strategy_params and trader_params lists
Returns:
List of parameter combinations
"""
strategy_params = params_dict.get('strategy_params', {})
trader_params = params_dict.get('trader_params', {})
# Generate all combinations
combinations = []
# Get all strategy parameter combinations
strategy_keys = list(strategy_params.keys())
strategy_values = list(strategy_params.values())
trader_keys = list(trader_params.keys())
trader_values = list(trader_params.values())
for strategy_combo in product(*strategy_values):
strategy_dict = dict(zip(strategy_keys, strategy_combo))
for trader_combo in product(*trader_values):
trader_dict = dict(zip(trader_keys, trader_combo))
combinations.append({
'strategy_params': strategy_dict,
'trader_params': trader_dict
})
return combinations
def get_quick_test_params(self) -> Dict[str, List]:
"""
Get parameters for quick testing (smaller parameter space for faster execution).
Returns:
Dictionary with parameter ranges for quick testing
"""
return {
"strategy_params": {
"supertrend_periods": [[12, 10], [10, 8]], # Only 2 period combinations
"supertrend_multipliers": [[3.0, 1.0], [2.0, 1.5]], # Only 2 multiplier combinations
"min_trend_agreement": [0.5, 0.8], # Only 2 agreement levels
"timeframe": ["5min", "15min"] # Only 2 timeframes
},
"trader_params": {
"stop_loss_pct": [0.02, 0.05], # Only 2 stop loss levels
"portfolio_percent_per_trade": [0.8, 0.9] # Only 2 position sizes
}
}
def get_comprehensive_params(self) -> Dict[str, List]:
"""
Get parameters for comprehensive optimization (larger parameter space).
Returns:
Dictionary with parameter ranges for comprehensive optimization
"""
return {
"strategy_params": {
"supertrend_periods": [
[12, 10, 11], [10, 8, 9], [14, 12, 13],
[16, 14, 15], [20, 18, 19]
],
"supertrend_multipliers": [
[3.0, 1.0, 2.0], [2.5, 1.5, 2.0], [3.5, 2.0, 2.5],
[2.0, 1.0, 1.5], [4.0, 2.5, 3.0]
],
"min_trend_agreement": [0.33, 0.5, 0.67, 0.8, 1.0],
"timeframe": ["1min", "5min", "15min", "30min", "1h"]
},
"trader_params": {
"stop_loss_pct": [0.01, 0.015, 0.02, 0.025, 0.03, 0.04, 0.05],
"portfolio_percent_per_trade": [0.1, 0.2, 0.3, 0.5, 0.8, 0.9, 1.0]
}
}
def run_single_backtest(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""
Run a single backtest with given parameters.
Args:
params: Dictionary containing all parameters for the backtest
Returns:
Dictionary with backtest results
"""
try:
start_time = time.time()
# Extract parameters
strategy_params = params['strategy_params']
trader_params = params['trader_params']
data_file = params['data_file']
start_date = params['start_date']
end_date = params['end_date']
data_dir = params['data_dir']
# Create strategy name for identification
strategy_name = f"MetaTrend_TF{strategy_params['timeframe']}_ST{len(strategy_params['supertrend_periods'])}_SL{trader_params['stop_loss_pct']}_POS{trader_params['portfolio_percent_per_trade']}"
# Create strategy
strategy = MetaTrendStrategy(name="metatrend", params=strategy_params)
# Create backtest config (only with BacktestConfig-supported parameters)
config = BacktestConfig(
data_file=data_file,
start_date=start_date,
end_date=end_date,
initial_usd=10000,
data_dir=data_dir,
stop_loss_pct=trader_params.get('stop_loss_pct', 0.0)
)
# Create backtester
backtester = IncBacktester(config)
# Run backtest with trader-specific parameters
results = backtester.run_single_strategy(strategy, trader_params)
# Calculate additional metrics
end_time = time.time()
backtest_duration = end_time - start_time
# Format results
formatted_results = {
"success": True,
"strategy_name": strategy_name,
"strategy_params": strategy_params,
"trader_params": trader_params,
"initial_usd": results["initial_usd"],
"final_usd": results["final_usd"],
"profit_ratio": results["profit_ratio"],
"n_trades": results["n_trades"],
"win_rate": results["win_rate"],
"max_drawdown": results["max_drawdown"],
"avg_trade": results["avg_trade"],
"total_fees_usd": results["total_fees_usd"],
"backtest_duration_seconds": backtest_duration,
"data_points_processed": results.get("data_points", 0),
"warmup_complete": results.get("warmup_complete", False),
"trades": results.get("trades", [])
}
return formatted_results
except Exception as e:
logger.error(f"Error in backtest {params.get('strategy_params', {}).get('timeframe', 'unknown')}: {e}")
return {
"success": False,
"error": str(e),
"strategy_name": strategy_name if 'strategy_name' in locals() else "Unknown",
"strategy_params": params.get('strategy_params', {}),
"trader_params": params.get('trader_params', {}),
"traceback": traceback.format_exc()
}
def optimize_parallel(self, params_dict: Dict[str, List],
data_file: str, start_date: str, end_date: str,
data_dir: str = "data", max_workers: Optional[int] = None) -> List[Dict[str, Any]]:
"""
Run parameter optimization using parallel processing with progress tracking.
Args:
params_dict: Dictionary with parameter ranges
data_file: Data file for backtesting
start_date: Start date for backtesting
end_date: End date for backtesting
data_dir: Directory containing data files
max_workers: Maximum number of worker processes
Returns:
List of backtest results
"""
# Generate parameter combinations
param_combinations = self.generate_parameter_combinations(params_dict)
total_combinations = len(param_combinations)
logger.info(f"Starting optimization with {total_combinations} parameter combinations")
logger.info(f"Using {max_workers or self.system_utils.get_optimal_workers()} worker processes")
# Prepare jobs
jobs = []
for combo in param_combinations:
job_params = {
'strategy_params': combo['strategy_params'],
'trader_params': combo['trader_params'],
'data_file': data_file,
'start_date': start_date,
'end_date': end_date,
'data_dir': data_dir
}
jobs.append(job_params)
# Run parallel optimization with progress bar
results = []
failed_jobs = []
max_workers = max_workers or self.system_utils.get_optimal_workers()
with ProcessPoolExecutor(max_workers=max_workers) as executor:
# Submit all jobs
future_to_params = {executor.submit(self.run_single_backtest, job): job for job in jobs}
# Process results with progress bar
with tqdm(total=total_combinations, desc="Optimizing strategies", unit="strategy") as pbar:
for future in as_completed(future_to_params):
try:
result = future.result(timeout=300) # 5 minute timeout per job
results.append(result)
if result['success']:
pbar.set_postfix({
'Success': f"{len([r for r in results if r['success']])}/{len(results)}",
'Best Profit': f"{max([r.get('profit_ratio', 0) for r in results if r['success']], default=0):.1%}"
})
else:
failed_jobs.append(future_to_params[future])
except Exception as e:
logger.error(f"Job failed with exception: {e}")
failed_jobs.append(future_to_params[future])
results.append({
"success": False,
"error": f"Job exception: {e}",
"strategy_name": "Failed",
"strategy_params": future_to_params[future].get('strategy_params', {}),
"trader_params": future_to_params[future].get('trader_params', {})
})
pbar.update(1)
# Log summary
successful_results = [r for r in results if r['success']]
logger.info(f"Optimization completed: {len(successful_results)}/{total_combinations} successful")
if failed_jobs:
logger.warning(f"{len(failed_jobs)} jobs failed")
return results
def main():
"""Main function for running parameter optimization."""
parser = argparse.ArgumentParser(description="Strategy Parameter Optimization")
parser.add_argument("--data-file", type=str, default="btcusd_1-min_data.csv",
help="Data file for backtesting")
parser.add_argument("--data-dir", type=str, default="data",
help="Directory containing data files")
parser.add_argument("--results-dir", type=str, default="results",
help="Directory for saving results")
parser.add_argument("--start-date", type=str, default="2023-01-01",
help="Start date for backtesting (YYYY-MM-DD)")
parser.add_argument("--end-date", type=str, default="2023-01-31",
help="End date for backtesting (YYYY-MM-DD)")
parser.add_argument("--max-workers", type=int, default=None,
help="Maximum number of worker processes")
parser.add_argument("--quick-test", action="store_true",
help="Run quick test with smaller parameter space")
parser.add_argument("--custom-params", type=str, default=None,
help="Path to custom parameter configuration JSON file")
args = parser.parse_args()
# Adjust dates for quick test - use only 3 days for very fast testing
if args.quick_test:
args.start_date = "2023-01-01"
args.end_date = "2023-01-03" # Only 3 days for quick test
logger.info("Quick test mode: Using shortened time period (2023-01-01 to 2023-01-03)")
# Create optimizer
optimizer = StrategyOptimizer()
# Determine parameter configuration
if args.custom_params:
# Load custom parameters from JSON file
if not os.path.exists(args.custom_params):
logger.error(f"Custom parameter file not found: {args.custom_params}")
return
with open(args.custom_params, 'r') as f:
params_dict = json.load(f)
logger.info(f"Using custom parameters from: {args.custom_params}")
elif args.quick_test:
# Quick test parameters
params_dict = optimizer.get_quick_test_params()
logger.info("Using quick test parameter configuration")
else:
# Comprehensive optimization parameters
params_dict = optimizer.get_comprehensive_params()
logger.info("Using comprehensive optimization parameter configuration")
# Log optimization details
total_combinations = len(optimizer.generate_parameter_combinations(params_dict))
logger.info(f"Total parameter combinations: {total_combinations}")
logger.info(f"Data file: {args.data_file}")
logger.info(f"Date range: {args.start_date} to {args.end_date}")
logger.info(f"Results directory: {args.results_dir}")
# Check if data file exists
data_path = os.path.join(args.data_dir, args.data_file)
if not os.path.exists(data_path):
logger.error(f"Data file not found: {data_path}")
return
# Create results directory
os.makedirs(args.results_dir, exist_ok=True)
try:
# Run optimization
session_start_time = datetime.now()
logger.info("Starting parameter optimization...")
results = optimizer.optimize_parallel(
params_dict=params_dict,
data_file=args.data_file,
start_date=args.start_date,
end_date=args.end_date,
data_dir=args.data_dir,
max_workers=args.max_workers
)
# Save results
saver = ResultsSaver(args.results_dir)
# Generate base filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
test_type = "quick_test" if args.quick_test else "comprehensive"
base_filename = f"metatrend_optimization_{test_type}"
# Save comprehensive results
saver.save_comprehensive_results(
results=results,
base_filename=base_filename,
session_start_time=session_start_time
)
# Calculate and display summary statistics
successful_results = [r for r in results if r['success']]
if successful_results:
# Sort by profit ratio
sorted_results = sorted(successful_results, key=lambda x: x['profit_ratio'], reverse=True)
print(f"\nOptimization Summary:")
print(f" Successful runs: {len(successful_results)}/{len(results)}")
print(f" Total duration: {(datetime.now() - session_start_time).total_seconds():.1f} seconds")
print(f"\nTop 5 Strategies:")
for i, result in enumerate(sorted_results[:5], 1):
print(f" {i}. {result['strategy_name']}")
print(f" Profit: {result['profit_ratio']:.1%} (${result['final_usd']:.2f})")
print(f" Trades: {result['n_trades']} | Win Rate: {result['win_rate']:.1%}")
print(f" Max DD: {result['max_drawdown']:.1%}")
else:
print(f"\nNo successful optimization runs completed")
logger.error("All optimization runs failed")
print(f"\nFull results saved to: {args.results_dir}/")
except KeyboardInterrupt:
logger.info("Optimization interrupted by user")
except Exception as e:
logger.error(f"Optimization failed: {e}")
traceback.print_exc()
if __name__ == "__main__":
main()