#!/usr/bin/env python3 """ Enhanced test script for incremental backtester using real BTC data with comprehensive visualization and analysis features. ENHANCED FEATURES: - Stop Loss/Take Profit Visualization: Different colors and markers for exit types * Green triangles (^): Buy entries * Blue triangles (v): Strategy exits * Dark red X: Stop loss exits (prominent markers) * Gold stars (*): Take profit exits * Gray squares: End-of-day exits - Portfolio Tracking: Combined USD + BTC value calculation * Real-time portfolio value based on current BTC price * Separate tracking of USD balance and BTC holdings * Portfolio composition visualization - Three-Panel Analysis: 1. Price chart with trading signals and exit types 2. Portfolio value over time with profit/loss zones 3. Portfolio composition (USD vs BTC value breakdown) - Comprehensive Data Export: * CSV: Individual trades with entry/exit details * JSON: Complete performance statistics * CSV: Portfolio value tracking over time * PNG: Multi-panel visualization charts - Performance Analysis: * Exit type breakdown and performance * Win/loss distribution analysis * Best/worst trade identification * Detailed trade-by-trade logging """ import os import sys import pandas as pd import numpy as np import matplotlib.pyplot as plt import matplotlib.dates as mdates from datetime import datetime from typing import Dict, List import warnings import json 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.random_strategy import IncRandomStrategy from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy from cycles.utils.storage import Storage from cycles.utils.data_utils import aggregate_to_minutes def save_trades_to_csv(trades: List[Dict], filename: str) -> None: """Save trades to CSV file in the same format as existing trades file.""" if not trades: print("No trades to save") return # Convert trades to the exact format of the existing file formatted_trades = [] for trade in trades: # Create entry row (buy signal) entry_row = { 'entry_time': trade['entry_time'], 'exit_time': '', # Empty for entry row 'entry_price': trade['entry'], 'exit_price': '', # Empty for entry row 'profit_pct': 0.0, # 0 for entry 'type': 'BUY', 'fee_usd': trade.get('entry_fee_usd', 10.0) # Default fee if not available } formatted_trades.append(entry_row) # Create exit row (sell signal) exit_type = trade.get('type', 'META_TREND_EXIT_SIGNAL') if exit_type == 'STRATEGY_EXIT': exit_type = 'META_TREND_EXIT_SIGNAL' elif exit_type == 'STOP_LOSS': exit_type = 'STOP_LOSS' elif exit_type == 'TAKE_PROFIT': exit_type = 'TAKE_PROFIT' elif exit_type == 'EOD': exit_type = 'EOD' exit_row = { 'entry_time': trade['entry_time'], 'exit_time': trade['exit_time'], 'entry_price': trade['entry'], 'exit_price': trade['exit'], 'profit_pct': trade['profit_pct'], 'type': exit_type, 'fee_usd': trade.get('exit_fee_usd', trade.get('total_fees_usd', 10.0)) } formatted_trades.append(exit_row) # Convert to DataFrame and save trades_df = pd.DataFrame(formatted_trades) # Ensure the columns are in the exact same order column_order = ['entry_time', 'exit_time', 'entry_price', 'exit_price', 'profit_pct', 'type', 'fee_usd'] trades_df = trades_df[column_order] # Save with same formatting trades_df.to_csv(filename, index=False) print(f"Saved {len(formatted_trades)} trade signals ({len(trades)} complete trades) to: {filename}") # Print summary for comparison buy_signals = len([t for t in formatted_trades if t['type'] == 'BUY']) sell_signals = len(formatted_trades) - buy_signals print(f" - Buy signals: {buy_signals}") print(f" - Sell signals: {sell_signals}") # Show exit type breakdown exit_types = {} for trade in formatted_trades: if trade['type'] != 'BUY': exit_type = trade['type'] exit_types[exit_type] = exit_types.get(exit_type, 0) + 1 if exit_types: print(f" - Exit types: {exit_types}") def save_stats_to_json(stats: Dict, filename: str) -> None: """Save statistics to JSON file.""" # Convert any datetime objects to strings for JSON serialization stats_copy = stats.copy() for key, value in stats_copy.items(): if isinstance(value, pd.Timestamp): stats_copy[key] = value.isoformat() elif isinstance(value, dict): for k, v in value.items(): if isinstance(v, pd.Timestamp): value[k] = v.isoformat() with open(filename, 'w') as f: json.dump(stats_copy, f, indent=2, default=str) print(f"Saved statistics to: {filename}") def calculate_portfolio_over_time(data: pd.DataFrame, trades: List[Dict], initial_usd: float, debug: bool = False) -> pd.DataFrame: """Calculate portfolio value over time with proper USD + BTC tracking.""" print("Calculating portfolio value over time...") # Create portfolio tracking with detailed state portfolio_data = data[['close']].copy() portfolio_data['portfolio_value'] = initial_usd portfolio_data['usd_balance'] = initial_usd portfolio_data['btc_balance'] = 0.0 portfolio_data['position'] = 0 # 0 = cash, 1 = in position if not trades: return portfolio_data # Initialize state current_usd = initial_usd current_btc = 0.0 in_position = False # Sort trades by entry time sorted_trades = sorted(trades, key=lambda x: x['entry_time']) trade_idx = 0 print(f"Processing {len(sorted_trades)} trades across {len(portfolio_data)} data points...") for i, (timestamp, row) in enumerate(portfolio_data.iterrows()): current_price = row['close'] # Check if we need to execute any trades at this timestamp while trade_idx < len(sorted_trades): trade = sorted_trades[trade_idx] # Check for entry if trade['entry_time'] <= timestamp and not in_position: # Execute buy order entry_price = trade['entry'] current_btc = current_usd / entry_price current_usd = 0.0 in_position = True if debug: print(f"Entry {trade_idx + 1}: Buy at ${entry_price:.2f}, BTC: {current_btc:.6f}") break # Check for exit elif trade['exit_time'] <= timestamp and in_position: # Execute sell order exit_price = trade['exit'] current_usd = current_btc * exit_price current_btc = 0.0 in_position = False exit_type = trade.get('type', 'STRATEGY_EXIT') if debug: print(f"Exit {trade_idx + 1}: {exit_type} at ${exit_price:.2f}, USD: ${current_usd:.2f}") trade_idx += 1 break else: break # Calculate total portfolio value (USD + BTC value) btc_value = current_btc * current_price total_value = current_usd + btc_value # Update portfolio data portfolio_data.iloc[i, portfolio_data.columns.get_loc('portfolio_value')] = total_value portfolio_data.iloc[i, portfolio_data.columns.get_loc('usd_balance')] = current_usd portfolio_data.iloc[i, portfolio_data.columns.get_loc('btc_balance')] = current_btc portfolio_data.iloc[i, portfolio_data.columns.get_loc('position')] = 1 if in_position else 0 return portfolio_data def create_comprehensive_plot(data: pd.DataFrame, trades: List[Dict], portfolio_data: pd.DataFrame, strategy_name: str, save_path: str) -> None: """Create comprehensive plot with price, trades, and portfolio value.""" print(f"Creating comprehensive plot with {len(data)} data points and {len(trades)} trades...") # Create figure with subplots fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(16, 16), gridspec_kw={'height_ratios': [2, 1, 1]}) # Plot 1: Price action with trades ax1.plot(data.index, data['close'], label='BTC Price', color='black', linewidth=1.5) # Plot trades with different markers for different exit types if trades: entry_times = [trade['entry_time'] for trade in trades] entry_prices = [trade['entry'] for trade in trades] # Separate exits by type strategy_exits = [] stop_loss_exits = [] take_profit_exits = [] eod_exits = [] for trade in trades: exit_type = trade.get('type', 'STRATEGY_EXIT') exit_data = (trade['exit_time'], trade['exit']) if exit_type == 'STOP_LOSS': stop_loss_exits.append(exit_data) elif exit_type == 'TAKE_PROFIT': take_profit_exits.append(exit_data) elif exit_type == 'EOD': eod_exits.append(exit_data) else: strategy_exits.append(exit_data) # Plot entry points (green triangles) ax1.scatter(entry_times, entry_prices, color='darkgreen', marker='^', s=100, label=f'Buy ({len(entry_times)})', zorder=6, alpha=0.9, edgecolors='white', linewidth=1) # Plot different types of exits with distinct styling if strategy_exits: exit_times, exit_prices = zip(*strategy_exits) ax1.scatter(exit_times, exit_prices, color='blue', marker='v', s=100, label=f'Strategy Exit ({len(strategy_exits)})', zorder=5, alpha=0.8, edgecolors='white', linewidth=1) if stop_loss_exits: exit_times, exit_prices = zip(*stop_loss_exits) ax1.scatter(exit_times, exit_prices, color='darkred', marker='X', s=150, label=f'Stop Loss ({len(stop_loss_exits)})', zorder=7, alpha=1.0, edgecolors='white', linewidth=2) if take_profit_exits: exit_times, exit_prices = zip(*take_profit_exits) ax1.scatter(exit_times, exit_prices, color='gold', marker='*', s=150, label=f'Take Profit ({len(take_profit_exits)})', zorder=6, alpha=0.9, edgecolors='black', linewidth=1) if eod_exits: exit_times, exit_prices = zip(*eod_exits) ax1.scatter(exit_times, exit_prices, color='gray', marker='s', s=80, label=f'End of Day ({len(eod_exits)})', zorder=5, alpha=0.8, edgecolors='white', linewidth=1) # Print exit type summary print(f"Exit types: Strategy={len(strategy_exits)}, Stop Loss={len(stop_loss_exits)}, " f"Take Profit={len(take_profit_exits)}, EOD={len(eod_exits)}") ax1.set_title(f'{strategy_name} - BTC Trading Signals (Q1 2023)', fontsize=16, fontweight='bold') ax1.set_ylabel('Price (USD)', fontsize=12) ax1.legend(loc='upper left', fontsize=10) ax1.grid(True, alpha=0.3) # Plot 2: Portfolio value over time ax2.plot(portfolio_data.index, portfolio_data['portfolio_value'], label='Total Portfolio Value', color='blue', linewidth=2) ax2.axhline(y=portfolio_data['portfolio_value'].iloc[0], color='gray', linestyle='--', alpha=0.7, label='Initial Value') # Add profit/loss shading initial_value = portfolio_data['portfolio_value'].iloc[0] profit_mask = portfolio_data['portfolio_value'] > initial_value loss_mask = portfolio_data['portfolio_value'] < initial_value ax2.fill_between(portfolio_data.index, portfolio_data['portfolio_value'], initial_value, where=profit_mask, color='green', alpha=0.2, label='Profit Zone') ax2.fill_between(portfolio_data.index, portfolio_data['portfolio_value'], initial_value, where=loss_mask, color='red', alpha=0.2, label='Loss Zone') ax2.set_title('Portfolio Value Over Time (USD + BTC)', fontsize=14, fontweight='bold') ax2.set_ylabel('Portfolio Value (USD)', fontsize=12) ax2.legend(loc='upper left', fontsize=10) ax2.grid(True, alpha=0.3) # Plot 3: Portfolio composition (USD vs BTC value) usd_values = portfolio_data['usd_balance'] btc_values = portfolio_data['btc_balance'] * portfolio_data['close'] ax3.fill_between(portfolio_data.index, 0, usd_values, color='green', alpha=0.6, label='USD Balance') ax3.fill_between(portfolio_data.index, usd_values, usd_values + btc_values, color='orange', alpha=0.6, label='BTC Value') # Mark position periods position_mask = portfolio_data['position'] == 1 if position_mask.any(): ax3.fill_between(portfolio_data.index, 0, portfolio_data['portfolio_value'], where=position_mask, color='orange', alpha=0.2, label='In Position') ax3.set_title('Portfolio Composition (USD vs BTC)', fontsize=14, fontweight='bold') ax3.set_ylabel('Value (USD)', fontsize=12) ax3.set_xlabel('Date', fontsize=12) ax3.legend(loc='upper left', fontsize=10) ax3.grid(True, alpha=0.3) # Format x-axis for all plots for ax in [ax1, ax2, ax3]: ax.xaxis.set_major_locator(mdates.WeekdayLocator()) ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d')) plt.setp(ax.xaxis.get_majorticklabels(), rotation=45) # Save plot plt.tight_layout() plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.close() print(f"Comprehensive plot saved to: {save_path}") def compare_with_existing_trades(new_trades_file: str, existing_trades_file: str = "results/trades_15min(15min)_ST3pct.csv") -> None: """Compare the new incremental trades with existing strategy trades.""" try: if not os.path.exists(existing_trades_file): print(f"Existing trades file not found: {existing_trades_file}") return print(f"\nšŸ“Š COMPARING WITH EXISTING STRATEGY:") # Load both files new_df = pd.read_csv(new_trades_file) existing_df = pd.read_csv(existing_trades_file) # Count signals new_buy_signals = len(new_df[new_df['type'] == 'BUY']) new_sell_signals = len(new_df[new_df['type'] != 'BUY']) existing_buy_signals = len(existing_df[existing_df['type'] == 'BUY']) existing_sell_signals = len(existing_df[existing_df['type'] != 'BUY']) print(f"šŸ“ˆ SIGNAL COMPARISON:") print(f" Incremental Strategy:") print(f" - Buy signals: {new_buy_signals}") print(f" - Sell signals: {new_sell_signals}") print(f" Existing Strategy:") print(f" - Buy signals: {existing_buy_signals}") print(f" - Sell signals: {existing_sell_signals}") # Compare exit types new_exit_types = new_df[new_df['type'] != 'BUY']['type'].value_counts().to_dict() existing_exit_types = existing_df[existing_df['type'] != 'BUY']['type'].value_counts().to_dict() print(f"\nšŸŽÆ EXIT TYPE COMPARISON:") print(f" Incremental Strategy: {new_exit_types}") print(f" Existing Strategy: {existing_exit_types}") # Calculate profit comparison new_profits = new_df[new_df['type'] != 'BUY']['profit_pct'].sum() existing_profits = existing_df[existing_df['type'] != 'BUY']['profit_pct'].sum() print(f"\nšŸ’° PROFIT COMPARISON:") print(f" Incremental Strategy: {new_profits*100:.2f}% total") print(f" Existing Strategy: {existing_profits*100:.2f}% total") print(f" Difference: {(new_profits - existing_profits)*100:.2f}%") except Exception as e: print(f"Error comparing trades: {e}") def test_single_strategy(): """Test a single strategy and create comprehensive analysis.""" print("\n" + "="*60) print("TESTING SINGLE STRATEGY") print("="*60) # Create storage instance storage = Storage() # Create backtester configuration using 3 months of data config = BacktestConfig( data_file="btcusd_1-min_data.csv", start_date="2025-01-01", end_date="2025-05-01", initial_usd=10000, stop_loss_pct=0.03, # 3% stop loss to match existing take_profit_pct=0.0 ) # Create strategy strategy = IncMetaTrendStrategy( name="metatrend", weight=1.0, params={ "timeframe": "15min", "enable_logging": False } ) print(f"Testing strategy: {strategy.name}") print(f"Strategy timeframe: {strategy.params.get('timeframe', '15min')}") print(f"Stop loss: {config.stop_loss_pct*100:.1f}%") print(f"Date range: {config.start_date} to {config.end_date}") # Run backtest print(f"\nšŸš€ Running backtest...") backtester = IncBacktester(config, storage) result = backtester.run_single_strategy(strategy) # Print results print(f"\nšŸ“Š RESULTS:") print(f"Strategy: {strategy.__class__.__name__}") profit = result['final_usd'] - result['initial_usd'] print(f"Total Profit: ${profit:.2f} ({result['profit_ratio']*100:.2f}%)") print(f"Total Trades: {result['n_trades']}") print(f"Win Rate: {result['win_rate']*100:.2f}%") print(f"Max Drawdown: {result['max_drawdown']*100:.2f}%") print(f"Average Trade: {result['avg_trade']*100:.2f}%") print(f"Total Fees: ${result['total_fees_usd']:.2f}") # Create results directory os.makedirs("results", exist_ok=True) # Save trades in the same format as existing file if result['trades']: # Create filename matching the existing format timeframe = strategy.params.get('timeframe', '15min') stop_loss_pct = int(config.stop_loss_pct * 100) trades_filename = f"results/trades_incremental_{timeframe}({timeframe})_ST{stop_loss_pct}pct.csv" save_trades_to_csv(result['trades'], trades_filename) # Compare with existing trades compare_with_existing_trades(trades_filename) # Save statistics to JSON stats_filename = f"results/incremental_stats_{config.start_date}_{config.end_date}.json" save_stats_to_json(result, stats_filename) # Load and aggregate data for plotting print(f"\nšŸ“ˆ CREATING COMPREHENSIVE ANALYSIS...") data = storage.load_data("btcusd_1-min_data.csv", config.start_date, config.end_date) print(f"Loaded {len(data)} minute-level data points") # Aggregate to strategy timeframe using existing data_utils timeframe_minutes = 15 # Match strategy timeframe print(f"Aggregating to {timeframe_minutes}-minute bars using data_utils...") aggregated_data = aggregate_to_minutes(data, timeframe_minutes) print(f"Aggregated to {len(aggregated_data)} bars") # Calculate portfolio value over time portfolio_data = calculate_portfolio_over_time(aggregated_data, result['trades'], config.initial_usd, debug=False) # Save portfolio data to CSV portfolio_filename = f"results/incremental_portfolio_{config.start_date}_{config.end_date}.csv" portfolio_data.to_csv(portfolio_filename) print(f"Saved portfolio data to: {portfolio_filename}") # Create comprehensive plot plot_path = f"results/incremental_comprehensive_{config.start_date}_{config.end_date}.png" create_comprehensive_plot(aggregated_data, result['trades'], portfolio_data, "Incremental MetaTrend Strategy", plot_path) return result def main(): """Main test function.""" print("šŸš€ Starting Comprehensive Incremental Backtester Test (Q1 2023)") print("=" * 80) try: # Test single strategy result = test_single_strategy() print("\n" + "="*80) print("āœ… TEST COMPLETED SUCCESSFULLY!") print("="*80) print(f"šŸ“ Check the 'results/' directory for:") print(f" - Trading plot: incremental_comprehensive_q1_2023.png") print(f" - Trades data: trades_incremental_15min(15min)_ST3pct.csv") print(f" - Statistics: incremental_stats_2025-01-01_2025-05-01.json") print(f" - Portfolio data: incremental_portfolio_2025-01-01_2025-05-01.csv") print(f"šŸ“Š Strategy processed {result['data_points_processed']} data points") print(f"šŸŽÆ Strategy warmup: {'āœ… Complete' if result['warmup_complete'] else 'āŒ Incomplete'}") # Show some trade details if result['n_trades'] > 0: print(f"\nšŸ“ˆ DETAILED TRADE ANALYSIS:") print(f"First trade: {result.get('first_trade', {}).get('entry_time', 'N/A')}") print(f"Last trade: {result.get('last_trade', {}).get('exit_time', 'N/A')}") # Analyze trades by exit type trades = result['trades'] # Group trades by exit type exit_types = {} for trade in trades: exit_type = trade.get('type', 'STRATEGY_EXIT') if exit_type not in exit_types: exit_types[exit_type] = [] exit_types[exit_type].append(trade) print(f"\nšŸ“Š EXIT TYPE ANALYSIS:") for exit_type, type_trades in exit_types.items(): profits = [trade['profit_pct'] for trade in type_trades] avg_profit = np.mean(profits) * 100 win_rate = len([p for p in profits if p > 0]) / len(profits) * 100 print(f" {exit_type}:") print(f" Count: {len(type_trades)}") print(f" Avg Profit: {avg_profit:.2f}%") print(f" Win Rate: {win_rate:.1f}%") if exit_type == 'STOP_LOSS': avg_loss = np.mean([p for p in profits if p <= 0]) * 100 print(f" Avg Loss: {avg_loss:.2f}%") # Overall profit distribution all_profits = [trade['profit_pct'] for trade in trades] winning_trades = [p for p in all_profits if p > 0] losing_trades = [p for p in all_profits if p <= 0] print(f"\nšŸ“ˆ OVERALL PROFIT DISTRIBUTION:") if winning_trades: print(f"Winning trades: {len(winning_trades)} (avg: {np.mean(winning_trades)*100:.2f}%)") print(f"Best trade: {max(winning_trades)*100:.2f}%") if losing_trades: print(f"Losing trades: {len(losing_trades)} (avg: {np.mean(losing_trades)*100:.2f}%)") print(f"Worst trade: {min(losing_trades)*100:.2f}%") return True except Exception as e: print(f"\nāŒ Error during testing: {e}") import traceback traceback.print_exc() return False if __name__ == "__main__": success = main() sys.exit(0 if success else 1)