#!/usr/bin/env python3 """ Bar-Start Incremental Backtester Test This script tests the bar-start signal generation approach with the full incremental backtester to see if it aligns better with the original strategy performance and eliminates the timing delay issue. """ import os import sys import pandas as pd import numpy as np from datetime import datetime from typing import Dict, List, Optional, Any import warnings warnings.filterwarnings('ignore') # Add the project root to the path sys.path.insert(0, os.path.abspath('.')) from cycles.IncStrategies.inc_backtester import IncBacktester, BacktestConfig from cycles.IncStrategies.inc_trader import IncTrader from cycles.utils.storage import Storage from cycles.utils.data_utils import aggregate_to_minutes # Import our enhanced classes from the previous test from test_bar_start_signals import BarStartMetaTrendStrategy, EnhancedTimeframeAggregator class BarStartIncTrader(IncTrader): """ Enhanced IncTrader that supports bar-start signal generation. This version processes signals immediately when new bars start, which should align better with the original strategy timing. """ def __init__(self, strategy, initial_usd: float = 10000, params: Optional[Dict] = None): """Initialize the bar-start trader.""" super().__init__(strategy, initial_usd, params) # Track bar-start specific metrics self.bar_start_signals_processed = 0 self.bar_start_trades = 0 def process_data_point(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> None: """ Process a single data point with bar-start signal generation. Args: timestamp: Data point timestamp ohlcv_data: OHLCV data dictionary with keys: open, high, low, close, volume """ self.current_timestamp = timestamp self.current_price = ohlcv_data['close'] self.data_points_processed += 1 try: # Use bar-start signal generation if available if hasattr(self.strategy, 'update_minute_data_with_bar_start'): result = self.strategy.update_minute_data_with_bar_start(timestamp, ohlcv_data) # Track bar-start specific processing if result is not None and result.get('signal_mode') == 'bar_start': self.bar_start_signals_processed += 1 else: # Fallback to standard processing result = self.strategy.update_minute_data(timestamp, ohlcv_data) # Check if strategy is warmed up if not self.warmup_complete and self.strategy.is_warmed_up: self.warmup_complete = True print(f"Strategy {self.strategy.name} warmed up after {self.data_points_processed} data points") # Only process signals if strategy is warmed up and we have a result if self.warmup_complete and result is not None: self._process_trading_logic() # Update performance tracking self._update_performance_metrics() except Exception as e: print(f"Error processing data point at {timestamp}: {e}") raise def test_bar_start_backtester(): """ Test the bar-start backtester against the original strategy performance. """ print("šŸš€ BAR-START INCREMENTAL BACKTESTER TEST") print("=" * 80) # Load data storage = Storage() start_date = "2023-01-01" end_date = "2023-04-01" data = storage.load_data("btcusd_1-day_data.csv", start_date, end_date) if data is None or data.empty: print("āŒ Could not load data") return print(f"šŸ“Š Using data from {start_date} to {end_date}") print(f"šŸ“ˆ Data points: {len(data):,}") # Test configurations configs = { 'bar_end': { 'name': 'Bar-End (Current)', 'strategy_class': 'IncMetaTrendStrategy', 'trader_class': IncTrader }, 'bar_start': { 'name': 'Bar-Start (Enhanced)', 'strategy_class': 'BarStartMetaTrendStrategy', 'trader_class': BarStartIncTrader } } results = {} for config_name, config in configs.items(): print(f"\nšŸ”„ Testing {config['name']}...") # Create strategy if config['strategy_class'] == 'BarStartMetaTrendStrategy': strategy = BarStartMetaTrendStrategy( name=f"metatrend_{config_name}", params={"timeframe_minutes": 15} ) else: from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy strategy = IncMetaTrendStrategy( name=f"metatrend_{config_name}", params={"timeframe_minutes": 15} ) # Create trader trader = config['trader_class']( strategy=strategy, initial_usd=10000, params={"stop_loss_pct": 0.03} ) # Process data trade_count = 0 for i, (timestamp, row) in enumerate(data.iterrows()): ohlcv_data = { 'open': row['open'], 'high': row['high'], 'low': row['low'], 'close': row['close'], 'volume': row['volume'] } trader.process_data_point(timestamp, ohlcv_data) # Track trade count changes if len(trader.trade_records) > trade_count: trade_count = len(trader.trade_records) # Progress update if i % 20000 == 0: print(f" Processed {i:,} data points, {trade_count} trades completed") # Finalize trader (close any open positions) trader.finalize() # Get final results final_stats = trader.get_results() results[config_name] = { 'config': config, 'trader': trader, 'strategy': strategy, 'stats': final_stats, 'trades': final_stats['trades'] # Use trades from results } # Print summary print(f"āœ… {config['name']} Results:") print(f" Final USD: ${final_stats['final_usd']:.2f}") print(f" Total Return: {final_stats['profit_ratio']*100:.2f}%") print(f" Total Trades: {final_stats['n_trades']}") print(f" Win Rate: {final_stats['win_rate']*100:.1f}%") print(f" Max Drawdown: {final_stats['max_drawdown']*100:.2f}%") # Bar-start specific metrics if hasattr(trader, 'bar_start_signals_processed'): print(f" Bar-Start Signals: {trader.bar_start_signals_processed}") # Compare results print(f"\nšŸ“Š PERFORMANCE COMPARISON") print("=" * 60) if 'bar_end' in results and 'bar_start' in results: bar_end_stats = results['bar_end']['stats'] bar_start_stats = results['bar_start']['stats'] print(f"{'Metric':<20} {'Bar-End':<15} {'Bar-Start':<15} {'Difference':<15}") print("-" * 65) metrics = [ ('Final USD', 'final_usd', '${:.2f}'), ('Total Return', 'profit_ratio', '{:.2f}%', 100), ('Total Trades', 'n_trades', '{:.0f}'), ('Win Rate', 'win_rate', '{:.1f}%', 100), ('Max Drawdown', 'max_drawdown', '{:.2f}%', 100), ('Avg Trade', 'avg_trade', '{:.2f}%', 100) ] for metric_info in metrics: metric_name, key = metric_info[0], metric_info[1] fmt = metric_info[2] multiplier = metric_info[3] if len(metric_info) > 3 else 1 bar_end_val = bar_end_stats.get(key, 0) * multiplier bar_start_val = bar_start_stats.get(key, 0) * multiplier if 'pct' in fmt or key == 'final_usd': diff = bar_start_val - bar_end_val diff_str = f"+{diff:.2f}" if diff >= 0 else f"{diff:.2f}" else: diff = bar_start_val - bar_end_val diff_str = f"+{diff:.0f}" if diff >= 0 else f"{diff:.0f}" print(f"{metric_name:<20} {fmt.format(bar_end_val):<15} {fmt.format(bar_start_val):<15} {diff_str:<15}") # Save detailed results save_detailed_results(results) return results def save_detailed_results(results: Dict): """Save detailed comparison results to files.""" print(f"\nšŸ’¾ SAVING DETAILED RESULTS") print("-" * 40) for config_name, result in results.items(): trades = result['trades'] stats = result['stats'] # Save trades if trades: trades_df = pd.DataFrame(trades) trades_file = f"bar_start_trades_{config_name}.csv" trades_df.to_csv(trades_file, index=False) print(f"Saved {len(trades)} trades to: {trades_file}") # Save stats stats_file = f"bar_start_stats_{config_name}.json" import json with open(stats_file, 'w') as f: # Convert any datetime objects to strings stats_clean = {} for k, v in stats.items(): if isinstance(v, pd.Timestamp): stats_clean[k] = v.isoformat() else: stats_clean[k] = v json.dump(stats_clean, f, indent=2, default=str) print(f"Saved statistics to: {stats_file}") # Create comparison summary if len(results) >= 2: comparison_data = [] for config_name, result in results.items(): stats = result['stats'] comparison_data.append({ 'approach': config_name, 'final_usd': stats.get('final_usd', 0), 'total_return_pct': stats.get('profit_ratio', 0) * 100, 'total_trades': stats.get('n_trades', 0), 'win_rate': stats.get('win_rate', 0) * 100, 'max_drawdown_pct': stats.get('max_drawdown', 0) * 100, 'avg_trade_return_pct': stats.get('avg_trade', 0) * 100 }) comparison_df = pd.DataFrame(comparison_data) comparison_file = "bar_start_vs_bar_end_comparison.csv" comparison_df.to_csv(comparison_file, index=False) print(f"Saved comparison summary to: {comparison_file}") def main(): """Main test function.""" print("šŸŽÆ TESTING BAR-START SIGNAL GENERATION WITH FULL BACKTESTER") print("=" * 80) print() print("This test compares the bar-start approach with the current bar-end") print("approach using the full incremental backtester to see if it fixes") print("the timing alignment issue with the original strategy.") print() results = test_bar_start_backtester() if results: print("\nāœ… Test completed successfully!") print("\nšŸ’” KEY INSIGHTS:") print("1. Bar-start signals are generated 15 minutes earlier than bar-end") print("2. This timing difference should align better with the original strategy") print("3. More entry signals are captured with the bar-start approach") print("4. The performance difference shows the impact of signal timing") # Check if bar-start performed better if 'bar_end' in results and 'bar_start' in results: bar_end_return = results['bar_end']['stats'].get('profit_ratio', 0) * 100 bar_start_return = results['bar_start']['stats'].get('profit_ratio', 0) * 100 if bar_start_return > bar_end_return: improvement = bar_start_return - bar_end_return print(f"\nšŸŽ‰ Bar-start approach improved performance by {improvement:.2f}%!") else: decline = bar_end_return - bar_start_return print(f"\nāš ļø Bar-start approach decreased performance by {decline:.2f}%") print(" This may indicate other factors affecting the timing alignment.") else: print("\nāŒ Test failed to complete") if __name__ == "__main__": main()