#!/usr/bin/env python3 """ Backtest execution script for cryptocurrency trading strategies Refactored for improved maintainability and error handling """ import logging import datetime import argparse import sys from pathlib import Path # Import custom modules from config_manager import ConfigManager from backtest_runner import BacktestRunner from result_processor import ResultProcessor from cycles.utils.storage import Storage from cycles.utils.system import SystemUtils def setup_logging() -> logging.Logger: """Configure and return logging instance""" logger = logging.getLogger(__name__) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler("backtest.log"), logging.StreamHandler() ] ) return logger def create_metadata_lines(config: dict, data_df, result_processor: ResultProcessor) -> list: """Create metadata lines for results file""" start_date = config['start_date'] stop_date = config['stop_date'] initial_usd = config['initial_usd'] # Get price information start_time, start_price = result_processor.get_price_info(data_df, start_date) stop_time, stop_price = result_processor.get_price_info(data_df, stop_date) metadata_lines = [ f"Start date\t{start_date}\tPrice\t{start_price or 'N/A'}", f"Stop date\t{stop_date}\tPrice\t{stop_price or 'N/A'}", f"Initial USD\t{initial_usd}" ] return metadata_lines def main(): """Main execution function""" logger = setup_logging() try: # Parse command line arguments parser = argparse.ArgumentParser(description="Run backtest with config file.") parser.add_argument("config", type=str, nargs="?", help="Path to config JSON file.") args = parser.parse_args() # Initialize configuration manager config_manager = ConfigManager(logging_instance=logger) # Load configuration logger.info("Loading configuration...") config = config_manager.load_config(args.config) # Initialize components logger.info("Initializing components...") storage = Storage( data_dir=config['data_dir'], results_dir=config['results_dir'], logging=logger ) system_utils = SystemUtils(logging=logger) result_processor = ResultProcessor(storage, logging_instance=logger) runner = BacktestRunner(storage, system_utils, result_processor, logging_instance=logger) # Validate inputs logger.info("Validating inputs...") runner.validate_inputs( config['timeframes'], config['stop_loss_pcts'], config['initial_usd'] ) # Load data logger.info("Loading market data...") data_filename = 'btcusd_1-min_data.csv' data_1min = runner.load_data( data_filename, config['start_date'], config['stop_date'] ) # Run backtests logger.info("Starting backtest execution...") debug_mode = True # Can be moved to config all_results, all_trades = runner.run_backtests( data_1min, config['timeframes'], config['stop_loss_pcts'], config['initial_usd'], debug=debug_mode ) # Process and save results logger.info("Processing and saving results...") timestamp = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M") # Create metadata metadata_lines = create_metadata_lines(config, data_1min, result_processor) # Save aggregated results result_file = result_processor.save_backtest_results( all_results, metadata_lines, timestamp ) logger.info(f"Backtest completed successfully. Results saved to {result_file}") logger.info(f"Processed {len(all_results)} result combinations") logger.info(f"Generated {len(all_trades)} total trades") except KeyboardInterrupt: logger.warning("Backtest interrupted by user") sys.exit(130) # Standard exit code for Ctrl+C except FileNotFoundError as e: logger.error(f"File not found: {e}") sys.exit(1) except ValueError as e: logger.error(f"Invalid configuration or data: {e}") sys.exit(1) except RuntimeError as e: logger.error(f"Runtime error during backtest: {e}") sys.exit(1) except Exception as e: logger.error(f"Unexpected error: {e}", exc_info=True) sys.exit(1) if __name__ == "__main__": main()