465 lines
19 KiB
Python
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()
|