Cycles/test/simple_strategy_comparison_2025.py

465 lines
19 KiB
Python

#!/usr/bin/env python3
"""
Simple Strategy Comparison for 2025 Data
This script runs both the original and incremental strategies on the same 2025 timeframe
and creates side-by-side comparison plots.
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import logging
from typing import Dict, List, Tuple
import os
import sys
from datetime import datetime
import json
# Add project root to path
sys.path.insert(0, os.path.abspath('..'))
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
from cycles.IncStrategies.inc_backtester import IncBacktester, BacktestConfig
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 SimpleStrategyComparison:
"""Simple comparison between original and incremental strategies for 2025 data."""
def __init__(self, start_date: str = "2025-01-01", end_date: str = "2025-05-01"):
"""Initialize the comparison."""
self.start_date = start_date
self.end_date = end_date
self.storage = Storage(logging=logger)
# Results storage
self.original_results = None
self.incremental_results = None
self.test_data = None
def load_data(self) -> pd.DataFrame:
"""Load test data for the specified date range."""
logger.info(f"Loading data from {self.start_date} to {self.end_date}")
try:
# Load data directly from CSV file
data_file = "../data/btcusd_1-min_data.csv"
logger.info(f"Loading data from: {data_file}")
# Read CSV file
df = pd.read_csv(data_file)
# Convert timestamp column
df['timestamp'] = pd.to_datetime(df['Timestamp'], unit='s')
# Rename columns to match expected format
df = df.rename(columns={
'Open': 'open',
'High': 'high',
'Low': 'low',
'Close': 'close',
'Volume': 'volume'
})
# Filter by date range
start_dt = pd.to_datetime(self.start_date)
end_dt = pd.to_datetime(self.end_date)
df = df[(df['timestamp'] >= start_dt) & (df['timestamp'] < end_dt)]
if df.empty:
raise ValueError(f"No data found for the specified date range: {self.start_date} to {self.end_date}")
# Keep only required columns
df = df[['timestamp', 'open', 'high', 'low', 'close', 'volume']]
self.test_data = df
logger.info(f"Loaded {len(df)} data points")
logger.info(f"Date range: {df['timestamp'].min()} to {df['timestamp'].max()}")
logger.info(f"Price range: ${df['close'].min():.0f} - ${df['close'].max():.0f}")
return df
except Exception as e:
logger.error(f"Failed to load test data: {e}")
import traceback
traceback.print_exc()
raise
def load_original_results(self) -> Dict:
"""Load original strategy results from existing CSV file."""
logger.info("📂 Loading Original Strategy results from CSV...")
try:
# Load the original trades file
original_file = "../results/trades_15min(15min)_ST3pct.csv"
if not os.path.exists(original_file):
logger.warning(f"Original trades file not found: {original_file}")
return None
df = pd.read_csv(original_file)
df['entry_time'] = pd.to_datetime(df['entry_time'])
df['exit_time'] = pd.to_datetime(df['exit_time'], errors='coerce')
# Calculate performance metrics
buy_signals = df[df['type'] == 'BUY']
sell_signals = df[df['type'] != 'BUY']
# Calculate final value using compounding logic
initial_usd = 10000
final_usd = initial_usd
for _, trade in sell_signals.iterrows():
profit_pct = trade['profit_pct']
final_usd *= (1 + profit_pct)
total_return = (final_usd - initial_usd) / initial_usd * 100
# Convert to standardized format
trades = []
for _, row in df.iterrows():
trades.append({
'timestamp': row['entry_time'],
'type': row['type'],
'price': row.get('entry_price', row.get('exit_price')),
'exit_time': row['exit_time'],
'exit_price': row.get('exit_price'),
'profit_pct': row.get('profit_pct', 0),
'source': 'original'
})
performance = {
'strategy_name': 'Original Strategy',
'initial_value': initial_usd,
'final_value': final_usd,
'total_return': total_return,
'num_trades': len(sell_signals),
'trades': trades
}
logger.info(f"✅ Original strategy loaded: {len(sell_signals)} trades, {total_return:.2f}% return")
self.original_results = performance
return performance
except Exception as e:
logger.error(f"❌ Error loading original strategy: {e}")
import traceback
traceback.print_exc()
return None
def run_incremental_strategy(self, initial_usd: float = 10000) -> Dict:
"""Run the incremental strategy using the backtester."""
logger.info("🔄 Running Incremental Strategy...")
try:
# Create strategy instance
strategy = IncMetaTrendStrategy("metatrend", weight=1.0, params={
"timeframe": "1min",
"enable_logging": False
})
# Save our data to a temporary CSV file for the backtester
temp_data_file = "../data/temp_2025_data.csv"
# Prepare data in the format expected by Storage class
temp_df = self.test_data.copy()
temp_df['Timestamp'] = temp_df['timestamp'].astype('int64') // 10**9 # Convert to Unix timestamp
temp_df = temp_df.rename(columns={
'open': 'Open',
'high': 'High',
'low': 'Low',
'close': 'Close',
'volume': 'Volume'
})
temp_df = temp_df[['Timestamp', 'Open', 'High', 'Low', 'Close', 'Volume']]
temp_df.to_csv(temp_data_file, index=False)
# Create backtest configuration with correct parameters
config = BacktestConfig(
data_file="temp_2025_data.csv",
start_date=self.start_date,
end_date=self.end_date,
initial_usd=initial_usd,
stop_loss_pct=0.03,
take_profit_pct=0.0
)
# Create backtester
backtester = IncBacktester(config)
# Run backtest
results = backtester.run_single_strategy(strategy)
# Clean up temporary file
if os.path.exists(temp_data_file):
os.remove(temp_data_file)
# Extract results
trades = results.get('trades', [])
# Convert trades to standardized format
standardized_trades = []
for trade in trades:
standardized_trades.append({
'timestamp': trade.entry_time,
'type': 'BUY',
'price': trade.entry_price,
'exit_time': trade.exit_time,
'exit_price': trade.exit_price,
'profit_pct': trade.profit_pct,
'source': 'incremental'
})
# Add sell signal
if trade.exit_time:
standardized_trades.append({
'timestamp': trade.exit_time,
'type': 'SELL',
'price': trade.exit_price,
'exit_time': trade.exit_time,
'exit_price': trade.exit_price,
'profit_pct': trade.profit_pct,
'source': 'incremental'
})
# Calculate performance metrics
final_value = results.get('final_usd', initial_usd)
total_return = (final_value - initial_usd) / initial_usd * 100
performance = {
'strategy_name': 'Incremental MetaTrend',
'initial_value': initial_usd,
'final_value': final_value,
'total_return': total_return,
'num_trades': results.get('n_trades', 0),
'trades': standardized_trades
}
logger.info(f"✅ Incremental strategy completed: {results.get('n_trades', 0)} trades, {total_return:.2f}% return")
self.incremental_results = performance
return performance
except Exception as e:
logger.error(f"❌ Error running incremental strategy: {e}")
import traceback
traceback.print_exc()
return None
def create_side_by_side_comparison(self, save_path: str = "../results/strategy_comparison_2025_simple.png"):
"""Create side-by-side comparison plots."""
logger.info("📊 Creating side-by-side comparison plots...")
# Create figure with subplots
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(20, 16))
# Plot 1: Original Strategy Signals
self._plot_strategy_signals(ax1, self.original_results, "Original Strategy", 'blue')
# Plot 2: Incremental Strategy Signals
self._plot_strategy_signals(ax2, self.incremental_results, "Incremental Strategy", 'red')
# Plot 3: Performance Comparison
self._plot_performance_comparison(ax3)
# Plot 4: Trade Statistics
self._plot_trade_statistics(ax4)
# Overall title
fig.suptitle(f'Strategy Comparison: {self.start_date} to {self.end_date}',
fontsize=20, fontweight='bold', y=0.98)
# Adjust layout and save
plt.tight_layout()
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.show()
logger.info(f"📈 Comparison plot saved to: {save_path}")
def _plot_strategy_signals(self, ax, results: Dict, title: str, color: str):
"""Plot price data with trading signals for a single strategy."""
if not results:
ax.text(0.5, 0.5, f"No data for {title}", ha='center', va='center', transform=ax.transAxes)
return
# Plot price data
ax.plot(self.test_data['timestamp'], self.test_data['close'],
color='black', linewidth=1, alpha=0.7, label='BTC Price')
# Plot trading signals
trades = results['trades']
buy_signals = [t for t in trades if t['type'] == 'BUY']
sell_signals = [t for t in trades if t['type'] == 'SELL' or t['type'] != 'BUY']
if buy_signals:
buy_times = [t['timestamp'] for t in buy_signals]
buy_prices = [t['price'] for t in buy_signals]
ax.scatter(buy_times, buy_prices, color='green', marker='^',
s=80, label=f'Buy ({len(buy_signals)})', zorder=5, alpha=0.8)
if sell_signals:
# Separate profitable and losing sells
profitable_sells = [t for t in sell_signals if t.get('profit_pct', 0) > 0]
losing_sells = [t for t in sell_signals if t.get('profit_pct', 0) <= 0]
if profitable_sells:
profit_times = [t['timestamp'] for t in profitable_sells]
profit_prices = [t['price'] for t in profitable_sells]
ax.scatter(profit_times, profit_prices, color='blue', marker='v',
s=80, label=f'Profitable Sell ({len(profitable_sells)})', zorder=5, alpha=0.8)
if losing_sells:
loss_times = [t['timestamp'] for t in losing_sells]
loss_prices = [t['price'] for t in losing_sells]
ax.scatter(loss_times, loss_prices, color='red', marker='v',
s=80, label=f'Loss Sell ({len(losing_sells)})', zorder=5, alpha=0.8)
ax.set_title(title, fontsize=14, fontweight='bold')
ax.set_ylabel('Price (USD)', fontsize=12)
ax.legend(loc='upper left', fontsize=10)
ax.grid(True, alpha=0.3)
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:,.0f}'))
# Format x-axis
ax.xaxis.set_major_locator(mdates.DayLocator(interval=7))
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d'))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
def _plot_performance_comparison(self, ax):
"""Plot performance comparison bar chart."""
if not self.original_results or not self.incremental_results:
ax.text(0.5, 0.5, "Performance data not available", ha='center', va='center',
transform=ax.transAxes, fontsize=14)
return
strategies = ['Original', 'Incremental']
returns = [self.original_results['total_return'], self.incremental_results['total_return']]
colors = ['blue', 'red']
bars = ax.bar(strategies, returns, color=colors, alpha=0.7)
# Add value labels on bars
for bar, return_val in zip(bars, returns):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height + (1 if height >= 0 else -3),
f'{return_val:.1f}%', ha='center', va='bottom' if height >= 0 else 'top',
fontweight='bold', fontsize=12)
ax.set_title('Total Return Comparison', fontsize=14, fontweight='bold')
ax.set_ylabel('Return (%)', fontsize=12)
ax.grid(True, alpha=0.3, axis='y')
ax.axhline(y=0, color='black', linestyle='-', alpha=0.5)
def _plot_trade_statistics(self, ax):
"""Create trade statistics table."""
ax.axis('off')
if not self.original_results or not self.incremental_results:
ax.text(0.5, 0.5, "Trade data not available", ha='center', va='center',
transform=ax.transAxes, fontsize=14)
return
# Create comparison table
orig = self.original_results
incr = self.incremental_results
comparison_text = f"""
STRATEGY COMPARISON SUMMARY
{'='*50}
{'Metric':<20} {'Original':<15} {'Incremental':<15} {'Difference':<15}
{'-'*65}
{'Initial Value':<20} ${orig['initial_value']:>10,.0f} ${incr['initial_value']:>12,.0f} ${incr['initial_value'] - orig['initial_value']:>12,.0f}
{'Final Value':<20} ${orig['final_value']:>10,.0f} ${incr['final_value']:>12,.0f} ${incr['final_value'] - orig['final_value']:>12,.0f}
{'Total Return':<20} {orig['total_return']:>10.1f}% {incr['total_return']:>12.1f}% {incr['total_return'] - orig['total_return']:>12.1f}%
{'Number of Trades':<20} {orig['num_trades']:>10} {incr['num_trades']:>12} {incr['num_trades'] - orig['num_trades']:>12}
TIMEFRAME: {self.start_date} to {self.end_date}
DATA POINTS: {len(self.test_data):,} minute bars
PRICE RANGE: ${self.test_data['close'].min():,.0f} - ${self.test_data['close'].max():,.0f}
Both strategies use MetaTrend logic with 3% stop loss.
Differences indicate implementation variations.
"""
ax.text(0.05, 0.95, comparison_text, transform=ax.transAxes, fontsize=10,
verticalalignment='top', fontfamily='monospace',
bbox=dict(boxstyle="round,pad=0.5", facecolor="lightgray", alpha=0.9))
def save_results(self, output_dir: str = "../results"):
"""Save detailed results to files."""
logger.info("💾 Saving detailed results...")
os.makedirs(output_dir, exist_ok=True)
# Save performance summary
summary = {
'timeframe': f"{self.start_date} to {self.end_date}",
'data_points': len(self.test_data) if self.test_data is not None else 0,
'original_strategy': self.original_results,
'incremental_strategy': self.incremental_results,
'comparison_timestamp': datetime.now().isoformat()
}
summary_file = f"{output_dir}/strategy_comparison_2025_simple.json"
with open(summary_file, 'w') as f:
json.dump(summary, f, indent=2, default=str)
logger.info(f"Performance summary saved to: {summary_file}")
def run_full_comparison(self, initial_usd: float = 10000):
"""Run the complete comparison workflow."""
logger.info("🚀 Starting Simple Strategy Comparison for 2025")
logger.info("=" * 60)
try:
# Load data
self.load_data()
# Load original results and run incremental strategy
self.load_original_results()
self.run_incremental_strategy(initial_usd)
# Create comparison plots
self.create_side_by_side_comparison()
# Save results
self.save_results()
# Print summary
if self.original_results and self.incremental_results:
logger.info("\n📊 COMPARISON SUMMARY:")
logger.info(f"Original Strategy: ${self.original_results['final_value']:,.0f} ({self.original_results['total_return']:+.2f}%)")
logger.info(f"Incremental Strategy: ${self.incremental_results['final_value']:,.0f} ({self.incremental_results['total_return']:+.2f}%)")
logger.info(f"Difference: ${self.incremental_results['final_value'] - self.original_results['final_value']:,.0f} ({self.incremental_results['total_return'] - self.original_results['total_return']:+.2f}%)")
logger.info("✅ Simple comparison completed successfully!")
except Exception as e:
logger.error(f"❌ Error during comparison: {e}")
import traceback
traceback.print_exc()
def main():
"""Main function to run the strategy comparison."""
# Create comparison instance
comparison = SimpleStrategyComparison(
start_date="2025-01-01",
end_date="2025-05-01"
)
# Run full comparison
comparison.run_full_comparison(initial_usd=10000)
if __name__ == "__main__":
main()