""" Test Incremental BBRS Strategy vs Original Implementation This script validates that the incremental BBRS strategy produces equivalent results to the original batch implementation. """ import pandas as pd import numpy as np import logging from datetime import datetime import matplotlib.pyplot as plt # Import original implementation from cycles.Analysis.bb_rsi import BollingerBandsStrategy # Import incremental implementation from cycles.IncStrategies.bbrs_incremental import BBRSIncrementalState # Import storage utility from cycles.utils.storage import Storage # Import aggregation function to match original behavior from cycles.utils.data_utils import aggregate_to_minutes # Setup logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler("test_bbrs_incremental.log"), logging.StreamHandler() ] ) def load_test_data(): """Load 2023-2024 BTC data for testing.""" storage = Storage(logging=logging) # Load data for testing period start_date = "2023-01-01" end_date = "2023-01-07" # One week for faster testing data = storage.load_data("btcusd_1-min_data.csv", start_date, end_date) if data.empty: logging.error("No data loaded for testing period") return None logging.info(f"Loaded {len(data)} rows of data from {data.index[0]} to {data.index[-1]}") return data def test_bbrs_strategy_comparison(): """Test incremental BBRS vs original implementation.""" # Load test data data = load_test_data() if data is None: return # Use subset for testing test_data = data.copy() # First 5000 rows logging.info(f"Using {len(test_data)} rows for testing") # Aggregate to hourly to match original strategy hourly_data = data = aggregate_to_minutes(data, 15) # hourly_data = test_data.copy() logging.info(f"Aggregated to {len(hourly_data)} hourly data points") # Configuration config = { "bb_width": 0.05, "bb_period": 20, "rsi_period": 14, "trending": { "rsi_threshold": [30, 70], "bb_std_dev_multiplier": 2.5, }, "sideways": { "rsi_threshold": [40, 60], "bb_std_dev_multiplier": 1.8, }, "strategy_name": "MarketRegimeStrategy", "SqueezeStrategy": True } logging.info("Testing original BBRS implementation...") # Original implementation (already aggregates internally) original_strategy = BollingerBandsStrategy(config=config, logging=logging) original_result = original_strategy.run(test_data.copy(), "MarketRegimeStrategy") logging.info("Testing incremental BBRS implementation...") # Incremental implementation (use pre-aggregated data) incremental_strategy = BBRSIncrementalState(config) incremental_results = [] # Process hourly data incrementally for i, (timestamp, row) in enumerate(hourly_data.iterrows()): ohlcv_data = { 'open': row['open'], 'high': row['high'], 'low': row['low'], 'close': row['close'], 'volume': row['volume'] } result = incremental_strategy.update(ohlcv_data) result['timestamp'] = timestamp incremental_results.append(result) if i % 50 == 0: # Log every 50 hourly points logging.info(f"Processed {i+1}/{len(hourly_data)} hourly data points") # Convert incremental results to DataFrame incremental_df = pd.DataFrame(incremental_results) incremental_df.set_index('timestamp', inplace=True) logging.info("Comparing results...") # Compare key metrics after warm-up period warmup_period = max(config["bb_period"], config["rsi_period"]) + 20 # Add volume MA period if len(original_result) > warmup_period and len(incremental_df) > warmup_period: # Compare after warm-up orig_warmed = original_result.iloc[warmup_period:] inc_warmed = incremental_df.iloc[warmup_period:] # Align indices common_index = orig_warmed.index.intersection(inc_warmed.index) orig_aligned = orig_warmed.loc[common_index] inc_aligned = inc_warmed.loc[common_index] logging.info(f"Comparing {len(common_index)} aligned data points after warm-up") # Compare signals if 'BuySignal' in orig_aligned.columns and 'buy_signal' in inc_aligned.columns: buy_signal_match = (orig_aligned['BuySignal'] == inc_aligned['buy_signal']).mean() logging.info(f"Buy signal match rate: {buy_signal_match:.4f} ({buy_signal_match*100:.2f}%)") buy_signals_orig = orig_aligned['BuySignal'].sum() buy_signals_inc = inc_aligned['buy_signal'].sum() logging.info(f"Buy signals - Original: {buy_signals_orig}, Incremental: {buy_signals_inc}") if 'SellSignal' in orig_aligned.columns and 'sell_signal' in inc_aligned.columns: sell_signal_match = (orig_aligned['SellSignal'] == inc_aligned['sell_signal']).mean() logging.info(f"Sell signal match rate: {sell_signal_match:.4f} ({sell_signal_match*100:.2f}%)") sell_signals_orig = orig_aligned['SellSignal'].sum() sell_signals_inc = inc_aligned['sell_signal'].sum() logging.info(f"Sell signals - Original: {sell_signals_orig}, Incremental: {sell_signals_inc}") # Compare RSI values if 'RSI' in orig_aligned.columns and 'rsi' in inc_aligned.columns: # Filter out NaN values valid_mask = ~(orig_aligned['RSI'].isna() | inc_aligned['rsi'].isna()) if valid_mask.sum() > 0: rsi_orig = orig_aligned['RSI'][valid_mask] rsi_inc = inc_aligned['rsi'][valid_mask] rsi_diff = np.abs(rsi_orig - rsi_inc) rsi_max_diff = rsi_diff.max() rsi_mean_diff = rsi_diff.mean() logging.info(f"RSI comparison - Max diff: {rsi_max_diff:.6f}, Mean diff: {rsi_mean_diff:.6f}") # Compare Bollinger Bands bb_comparisons = [ ('UpperBand', 'upper_band'), ('LowerBand', 'lower_band'), ('SMA', 'middle_band') ] for orig_col, inc_col in bb_comparisons: if orig_col in orig_aligned.columns and inc_col in inc_aligned.columns: valid_mask = ~(orig_aligned[orig_col].isna() | inc_aligned[inc_col].isna()) if valid_mask.sum() > 0: orig_vals = orig_aligned[orig_col][valid_mask] inc_vals = inc_aligned[inc_col][valid_mask] diff = np.abs(orig_vals - inc_vals) max_diff = diff.max() mean_diff = diff.mean() logging.info(f"{orig_col} comparison - Max diff: {max_diff:.6f}, Mean diff: {mean_diff:.6f}") # Plot comparison for visual inspection plot_comparison(orig_aligned, inc_aligned) else: logging.warning("Not enough data after warm-up period for comparison") def plot_comparison(original_df, incremental_df, save_path="bbrs_strategy_comparison.png"): """Plot comparison between original and incremental BBRS strategies.""" # Plot first 1000 points for visibility plot_points = min(1000, len(original_df), len(incremental_df)) fig, axes = plt.subplots(4, 1, figsize=(15, 12)) x_range = range(plot_points) # Plot 1: Price and Bollinger Bands if all(col in original_df.columns for col in ['close', 'UpperBand', 'LowerBand', 'SMA']): axes[0].plot(x_range, original_df['close'].iloc[:plot_points], 'k-', label='Price', alpha=0.7) axes[0].plot(x_range, original_df['UpperBand'].iloc[:plot_points], 'b-', label='Original Upper BB', alpha=0.7) axes[0].plot(x_range, original_df['SMA'].iloc[:plot_points], 'g-', label='Original SMA', alpha=0.7) axes[0].plot(x_range, original_df['LowerBand'].iloc[:plot_points], 'r-', label='Original Lower BB', alpha=0.7) if all(col in incremental_df.columns for col in ['upper_band', 'lower_band', 'middle_band']): axes[0].plot(x_range, incremental_df['upper_band'].iloc[:plot_points], 'b--', label='Incremental Upper BB', alpha=0.7) axes[0].plot(x_range, incremental_df['middle_band'].iloc[:plot_points], 'g--', label='Incremental SMA', alpha=0.7) axes[0].plot(x_range, incremental_df['lower_band'].iloc[:plot_points], 'r--', label='Incremental Lower BB', alpha=0.7) axes[0].set_title('Bollinger Bands Comparison') axes[0].legend() axes[0].grid(True) # Plot 2: RSI if 'RSI' in original_df.columns and 'rsi' in incremental_df.columns: axes[1].plot(x_range, original_df['RSI'].iloc[:plot_points], 'b-', label='Original RSI', alpha=0.7) axes[1].plot(x_range, incremental_df['rsi'].iloc[:plot_points], 'r--', label='Incremental RSI', alpha=0.7) axes[1].axhline(y=70, color='gray', linestyle=':', alpha=0.5) axes[1].axhline(y=30, color='gray', linestyle=':', alpha=0.5) axes[1].set_title('RSI Comparison') axes[1].legend() axes[1].grid(True) # Plot 3: Buy/Sell Signals if 'BuySignal' in original_df.columns and 'buy_signal' in incremental_df.columns: buy_orig = original_df['BuySignal'].iloc[:plot_points] buy_inc = incremental_df['buy_signal'].iloc[:plot_points] # Plot as scatter points where signals occur buy_orig_idx = [i for i, val in enumerate(buy_orig) if val] buy_inc_idx = [i for i, val in enumerate(buy_inc) if val] axes[2].scatter(buy_orig_idx, [1]*len(buy_orig_idx), color='green', marker='^', label='Original Buy', alpha=0.7, s=30) axes[2].scatter(buy_inc_idx, [0.8]*len(buy_inc_idx), color='blue', marker='^', label='Incremental Buy', alpha=0.7, s=30) if 'SellSignal' in original_df.columns and 'sell_signal' in incremental_df.columns: sell_orig = original_df['SellSignal'].iloc[:plot_points] sell_inc = incremental_df['sell_signal'].iloc[:plot_points] sell_orig_idx = [i for i, val in enumerate(sell_orig) if val] sell_inc_idx = [i for i, val in enumerate(sell_inc) if val] axes[2].scatter(sell_orig_idx, [0.6]*len(sell_orig_idx), color='red', marker='v', label='Original Sell', alpha=0.7, s=30) axes[2].scatter(sell_inc_idx, [0.4]*len(sell_inc_idx), color='orange', marker='v', label='Incremental Sell', alpha=0.7, s=30) axes[2].set_title('Trading Signals Comparison') axes[2].legend() axes[2].grid(True) axes[2].set_ylim(0, 1.2) # Plot 4: Market Regime if 'market_regime' in incremental_df.columns: regime_numeric = [1 if regime == 'sideways' else 0 for regime in incremental_df['market_regime'].iloc[:plot_points]] axes[3].plot(x_range, regime_numeric, 'purple', label='Market Regime (1=Sideways, 0=Trending)', alpha=0.7) axes[3].set_title('Market Regime Detection') axes[3].legend() axes[3].grid(True) axes[3].set_xlabel('Time Index') plt.tight_layout() plt.savefig(save_path, dpi=300, bbox_inches='tight') logging.info(f"Comparison plot saved to {save_path}") plt.show() def main(): """Main test function.""" logging.info("Starting BBRS incremental strategy validation test") try: test_bbrs_strategy_comparison() logging.info("BBRS incremental strategy test completed successfully!") except Exception as e: logging.error(f"Test failed with error: {e}") raise if __name__ == "__main__": main()