""" Signal Comparison Test This test compares the exact signals generated by: 1. Original DefaultStrategy 2. Incremental IncMetaTrendStrategy Focus is on signal timing, type, and accuracy. """ import pandas as pd import numpy as np import logging from typing import Dict, List, Tuple import os import sys # Add project root to path sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from cycles.strategies.default_strategy import DefaultStrategy from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy from cycles.utils.storage import Storage # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class SignalComparisonTest: """Test to compare signals between original and incremental strategies.""" def __init__(self): """Initialize the signal comparison test.""" self.storage = Storage(logging=logger) self.test_data = None self.original_signals = [] self.incremental_signals = [] def load_test_data(self, limit: int = 500) -> pd.DataFrame: """Load a small dataset for signal testing.""" logger.info(f"Loading test data (limit: {limit} points)") try: # Load recent data filename = "btcusd_1-min_data.csv" start_date = pd.to_datetime("2022-12-31") end_date = pd.to_datetime("2023-01-01") df = self.storage.load_data(filename, start_date, end_date) if len(df) > limit: df = df.tail(limit) logger.info(f"Limited data to last {limit} points") # Reset index to get timestamp as column df_with_timestamp = df.reset_index() self.test_data = df_with_timestamp logger.info(f"Loaded {len(df_with_timestamp)} data points") logger.info(f"Date range: {df_with_timestamp['timestamp'].min()} to {df_with_timestamp['timestamp'].max()}") return df_with_timestamp except Exception as e: logger.error(f"Failed to load test data: {e}") raise def test_original_strategy_signals(self) -> List[Dict]: """Test original DefaultStrategy and extract all signals.""" logger.info("Testing Original DefaultStrategy signals...") # Create indexed DataFrame for original strategy indexed_data = self.test_data.set_index('timestamp') # Limit to 200 points like original strategy does if len(indexed_data) > 200: original_data_used = indexed_data.tail(200) data_start_index = len(self.test_data) - 200 else: original_data_used = indexed_data data_start_index = 0 # Create mock backtester class MockBacktester: def __init__(self, df): self.original_df = df self.min1_df = df self.strategies = {} backtester = MockBacktester(original_data_used) # Initialize original strategy strategy = DefaultStrategy(weight=1.0, params={ "stop_loss_pct": 0.03, "timeframe": "1min" }) strategy.initialize(backtester) # Extract signals by simulating the strategy step by step signals = [] for i in range(len(original_data_used)): # Get entry signal entry_signal = strategy.get_entry_signal(backtester, i) if entry_signal.signal_type == "ENTRY": signals.append({ 'index': i, 'global_index': data_start_index + i, 'timestamp': original_data_used.index[i], 'close': original_data_used.iloc[i]['close'], 'signal_type': 'ENTRY', 'confidence': entry_signal.confidence, 'metadata': entry_signal.metadata, 'source': 'original' }) # Get exit signal exit_signal = strategy.get_exit_signal(backtester, i) if exit_signal.signal_type == "EXIT": signals.append({ 'index': i, 'global_index': data_start_index + i, 'timestamp': original_data_used.index[i], 'close': original_data_used.iloc[i]['close'], 'signal_type': 'EXIT', 'confidence': exit_signal.confidence, 'metadata': exit_signal.metadata, 'source': 'original' }) self.original_signals = signals logger.info(f"Original strategy generated {len(signals)} signals") return signals def test_incremental_strategy_signals(self) -> List[Dict]: """Test incremental IncMetaTrendStrategy and extract all signals.""" logger.info("Testing Incremental IncMetaTrendStrategy signals...") # Create strategy instance strategy = IncMetaTrendStrategy("metatrend", weight=1.0, params={ "timeframe": "1min", "enable_logging": False }) # Determine data range to match original strategy if len(self.test_data) > 200: test_data_subset = self.test_data.tail(200) data_start_index = len(self.test_data) - 200 else: test_data_subset = self.test_data data_start_index = 0 # Process data incrementally and collect signals signals = [] for idx, (_, row) in enumerate(test_data_subset.iterrows()): ohlc = { 'open': row['open'], 'high': row['high'], 'low': row['low'], 'close': row['close'] } # Update strategy with new data point strategy.calculate_on_data(ohlc, row['timestamp']) # Check for entry signal entry_signal = strategy.get_entry_signal() if entry_signal.signal_type == "ENTRY": signals.append({ 'index': idx, 'global_index': data_start_index + idx, 'timestamp': row['timestamp'], 'close': row['close'], 'signal_type': 'ENTRY', 'confidence': entry_signal.confidence, 'metadata': entry_signal.metadata, 'source': 'incremental' }) # Check for exit signal exit_signal = strategy.get_exit_signal() if exit_signal.signal_type == "EXIT": signals.append({ 'index': idx, 'global_index': data_start_index + idx, 'timestamp': row['timestamp'], 'close': row['close'], 'signal_type': 'EXIT', 'confidence': exit_signal.confidence, 'metadata': exit_signal.metadata, 'source': 'incremental' }) self.incremental_signals = signals logger.info(f"Incremental strategy generated {len(signals)} signals") return signals def compare_signals(self) -> Dict: """Compare signals between original and incremental strategies.""" logger.info("Comparing signals between strategies...") if not self.original_signals or not self.incremental_signals: raise ValueError("Must run both signal tests before comparison") # Separate by signal type orig_entry = [s for s in self.original_signals if s['signal_type'] == 'ENTRY'] orig_exit = [s for s in self.original_signals if s['signal_type'] == 'EXIT'] inc_entry = [s for s in self.incremental_signals if s['signal_type'] == 'ENTRY'] inc_exit = [s for s in self.incremental_signals if s['signal_type'] == 'EXIT'] # Compare counts comparison = { 'original_total': len(self.original_signals), 'incremental_total': len(self.incremental_signals), 'original_entry_count': len(orig_entry), 'original_exit_count': len(orig_exit), 'incremental_entry_count': len(inc_entry), 'incremental_exit_count': len(inc_exit), 'entry_count_match': len(orig_entry) == len(inc_entry), 'exit_count_match': len(orig_exit) == len(inc_exit), 'total_count_match': len(self.original_signals) == len(self.incremental_signals) } # Compare signal timing (by index) orig_entry_indices = set(s['index'] for s in orig_entry) orig_exit_indices = set(s['index'] for s in orig_exit) inc_entry_indices = set(s['index'] for s in inc_entry) inc_exit_indices = set(s['index'] for s in inc_exit) comparison.update({ 'entry_indices_match': orig_entry_indices == inc_entry_indices, 'exit_indices_match': orig_exit_indices == inc_exit_indices, 'entry_index_diff': orig_entry_indices.symmetric_difference(inc_entry_indices), 'exit_index_diff': orig_exit_indices.symmetric_difference(inc_exit_indices) }) return comparison def print_signal_details(self): """Print detailed signal information for analysis.""" print("\n" + "="*80) print("DETAILED SIGNAL COMPARISON") print("="*80) # Original signals print(f"\nšŸ“Š ORIGINAL STRATEGY SIGNALS ({len(self.original_signals)} total)") print("-" * 60) for signal in self.original_signals: print(f"Index {signal['index']:3d} | {signal['timestamp']} | " f"{signal['signal_type']:5s} | Price: {signal['close']:8.2f} | " f"Conf: {signal['confidence']:.2f}") # Incremental signals print(f"\nšŸ“Š INCREMENTAL STRATEGY SIGNALS ({len(self.incremental_signals)} total)") print("-" * 60) for signal in self.incremental_signals: print(f"Index {signal['index']:3d} | {signal['timestamp']} | " f"{signal['signal_type']:5s} | Price: {signal['close']:8.2f} | " f"Conf: {signal['confidence']:.2f}") # Side-by-side comparison print(f"\nšŸ”„ SIDE-BY-SIDE COMPARISON") print("-" * 80) print(f"{'Index':<6} {'Original':<20} {'Incremental':<20} {'Match':<8}") print("-" * 80) # Get all unique indices all_indices = set() for signal in self.original_signals + self.incremental_signals: all_indices.add(signal['index']) for idx in sorted(all_indices): orig_signal = next((s for s in self.original_signals if s['index'] == idx), None) inc_signal = next((s for s in self.incremental_signals if s['index'] == idx), None) orig_str = f"{orig_signal['signal_type']}" if orig_signal else "---" inc_str = f"{inc_signal['signal_type']}" if inc_signal else "---" match_str = "āœ…" if orig_str == inc_str else "āŒ" print(f"{idx:<6} {orig_str:<20} {inc_str:<20} {match_str:<8}") def save_signal_comparison(self, filename: str = "signal_comparison.csv"): """Save detailed signal comparison to CSV.""" all_signals = [] # Add original signals for signal in self.original_signals: all_signals.append({ 'index': signal['index'], 'timestamp': signal['timestamp'], 'close': signal['close'], 'original_signal': signal['signal_type'], 'original_confidence': signal['confidence'], 'incremental_signal': '', 'incremental_confidence': '', 'match': False }) # Add incremental signals for signal in self.incremental_signals: # Find if there's already a row for this index existing = next((s for s in all_signals if s['index'] == signal['index']), None) if existing: existing['incremental_signal'] = signal['signal_type'] existing['incremental_confidence'] = signal['confidence'] existing['match'] = existing['original_signal'] == signal['signal_type'] else: all_signals.append({ 'index': signal['index'], 'timestamp': signal['timestamp'], 'close': signal['close'], 'original_signal': '', 'original_confidence': '', 'incremental_signal': signal['signal_type'], 'incremental_confidence': signal['confidence'], 'match': False }) # Sort by index all_signals.sort(key=lambda x: x['index']) # Save to CSV os.makedirs("results", exist_ok=True) df = pd.DataFrame(all_signals) filepath = os.path.join("results", filename) df.to_csv(filepath, index=False) logger.info(f"Signal comparison saved to {filepath}") def run_signal_test(self, limit: int = 500) -> bool: """Run the complete signal comparison test.""" logger.info("="*80) logger.info("STARTING SIGNAL COMPARISON TEST") logger.info("="*80) try: # Load test data self.load_test_data(limit) # Test both strategies self.test_original_strategy_signals() self.test_incremental_strategy_signals() # Compare results comparison = self.compare_signals() # Print results print("\n" + "="*80) print("SIGNAL COMPARISON RESULTS") print("="*80) print(f"\nšŸ“Š SIGNAL COUNTS:") print(f"Original Strategy: {comparison['original_entry_count']} entries, {comparison['original_exit_count']} exits") print(f"Incremental Strategy: {comparison['incremental_entry_count']} entries, {comparison['incremental_exit_count']} exits") print(f"\nāœ… MATCHES:") print(f"Entry count match: {'āœ… YES' if comparison['entry_count_match'] else 'āŒ NO'}") print(f"Exit count match: {'āœ… YES' if comparison['exit_count_match'] else 'āŒ NO'}") print(f"Entry timing match: {'āœ… YES' if comparison['entry_indices_match'] else 'āŒ NO'}") print(f"Exit timing match: {'āœ… YES' if comparison['exit_indices_match'] else 'āŒ NO'}") if comparison['entry_index_diff']: print(f"\nāŒ Entry signal differences at indices: {sorted(comparison['entry_index_diff'])}") if comparison['exit_index_diff']: print(f"āŒ Exit signal differences at indices: {sorted(comparison['exit_index_diff'])}") # Print detailed signals self.print_signal_details() # Save comparison self.save_signal_comparison() # Overall result overall_match = (comparison['entry_count_match'] and comparison['exit_count_match'] and comparison['entry_indices_match'] and comparison['exit_indices_match']) print(f"\nšŸ† OVERALL RESULT: {'āœ… SIGNALS MATCH PERFECTLY' if overall_match else 'āŒ SIGNALS DIFFER'}") return overall_match except Exception as e: logger.error(f"Signal test failed: {e}") import traceback traceback.print_exc() return False def main(): """Run the signal comparison test.""" test = SignalComparisonTest() # Run test with 500 data points success = test.run_signal_test(limit=500) return success if __name__ == "__main__": success = main() sys.exit(0 if success else 1)