Cycles/test/test_bar_start_backtester.py

326 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Bar-Start Incremental Backtester Test
This script tests the bar-start signal generation approach with the full
incremental backtester to see if it aligns better with the original strategy
performance and eliminates the timing delay issue.
"""
import os
import sys
import pandas as pd
import numpy as np
from datetime import datetime
from typing import Dict, List, Optional, Any
import warnings
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.inc_trader import IncTrader
from cycles.utils.storage import Storage
from cycles.utils.data_utils import aggregate_to_minutes
# Import our enhanced classes from the previous test
from test_bar_start_signals import BarStartMetaTrendStrategy, EnhancedTimeframeAggregator
class BarStartIncTrader(IncTrader):
"""
Enhanced IncTrader that supports bar-start signal generation.
This version processes signals immediately when new bars start,
which should align better with the original strategy timing.
"""
def __init__(self, strategy, initial_usd: float = 10000, params: Optional[Dict] = None):
"""Initialize the bar-start trader."""
super().__init__(strategy, initial_usd, params)
# Track bar-start specific metrics
self.bar_start_signals_processed = 0
self.bar_start_trades = 0
def process_data_point(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> None:
"""
Process a single data point with bar-start signal generation.
Args:
timestamp: Data point timestamp
ohlcv_data: OHLCV data dictionary with keys: open, high, low, close, volume
"""
self.current_timestamp = timestamp
self.current_price = ohlcv_data['close']
self.data_points_processed += 1
try:
# Use bar-start signal generation if available
if hasattr(self.strategy, 'update_minute_data_with_bar_start'):
result = self.strategy.update_minute_data_with_bar_start(timestamp, ohlcv_data)
# Track bar-start specific processing
if result is not None and result.get('signal_mode') == 'bar_start':
self.bar_start_signals_processed += 1
else:
# Fallback to standard processing
result = self.strategy.update_minute_data(timestamp, ohlcv_data)
# Check if strategy is warmed up
if not self.warmup_complete and self.strategy.is_warmed_up:
self.warmup_complete = True
print(f"Strategy {self.strategy.name} warmed up after {self.data_points_processed} data points")
# Only process signals if strategy is warmed up and we have a result
if self.warmup_complete and result is not None:
self._process_trading_logic()
# Update performance tracking
self._update_performance_metrics()
except Exception as e:
print(f"Error processing data point at {timestamp}: {e}")
raise
def test_bar_start_backtester():
"""
Test the bar-start backtester against the original strategy performance.
"""
print("🚀 BAR-START INCREMENTAL BACKTESTER TEST")
print("=" * 80)
# Load data
storage = Storage()
start_date = "2023-01-01"
end_date = "2023-04-01"
data = storage.load_data("btcusd_1-day_data.csv", start_date, end_date)
if data is None or data.empty:
print("❌ Could not load data")
return
print(f"📊 Using data from {start_date} to {end_date}")
print(f"📈 Data points: {len(data):,}")
# Test configurations
configs = {
'bar_end': {
'name': 'Bar-End (Current)',
'strategy_class': 'IncMetaTrendStrategy',
'trader_class': IncTrader
},
'bar_start': {
'name': 'Bar-Start (Enhanced)',
'strategy_class': 'BarStartMetaTrendStrategy',
'trader_class': BarStartIncTrader
}
}
results = {}
for config_name, config in configs.items():
print(f"\n🔄 Testing {config['name']}...")
# Create strategy
if config['strategy_class'] == 'BarStartMetaTrendStrategy':
strategy = BarStartMetaTrendStrategy(
name=f"metatrend_{config_name}",
params={"timeframe_minutes": 15}
)
else:
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
strategy = IncMetaTrendStrategy(
name=f"metatrend_{config_name}",
params={"timeframe_minutes": 15}
)
# Create trader
trader = config['trader_class'](
strategy=strategy,
initial_usd=10000,
params={"stop_loss_pct": 0.03}
)
# Process data
trade_count = 0
for i, (timestamp, row) in enumerate(data.iterrows()):
ohlcv_data = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
trader.process_data_point(timestamp, ohlcv_data)
# Track trade count changes
if len(trader.trade_records) > trade_count:
trade_count = len(trader.trade_records)
# Progress update
if i % 20000 == 0:
print(f" Processed {i:,} data points, {trade_count} trades completed")
# Finalize trader (close any open positions)
trader.finalize()
# Get final results
final_stats = trader.get_results()
results[config_name] = {
'config': config,
'trader': trader,
'strategy': strategy,
'stats': final_stats,
'trades': final_stats['trades'] # Use trades from results
}
# Print summary
print(f"{config['name']} Results:")
print(f" Final USD: ${final_stats['final_usd']:.2f}")
print(f" Total Return: {final_stats['profit_ratio']*100:.2f}%")
print(f" Total Trades: {final_stats['n_trades']}")
print(f" Win Rate: {final_stats['win_rate']*100:.1f}%")
print(f" Max Drawdown: {final_stats['max_drawdown']*100:.2f}%")
# Bar-start specific metrics
if hasattr(trader, 'bar_start_signals_processed'):
print(f" Bar-Start Signals: {trader.bar_start_signals_processed}")
# Compare results
print(f"\n📊 PERFORMANCE COMPARISON")
print("=" * 60)
if 'bar_end' in results and 'bar_start' in results:
bar_end_stats = results['bar_end']['stats']
bar_start_stats = results['bar_start']['stats']
print(f"{'Metric':<20} {'Bar-End':<15} {'Bar-Start':<15} {'Difference':<15}")
print("-" * 65)
metrics = [
('Final USD', 'final_usd', '${:.2f}'),
('Total Return', 'profit_ratio', '{:.2f}%', 100),
('Total Trades', 'n_trades', '{:.0f}'),
('Win Rate', 'win_rate', '{:.1f}%', 100),
('Max Drawdown', 'max_drawdown', '{:.2f}%', 100),
('Avg Trade', 'avg_trade', '{:.2f}%', 100)
]
for metric_info in metrics:
metric_name, key = metric_info[0], metric_info[1]
fmt = metric_info[2]
multiplier = metric_info[3] if len(metric_info) > 3 else 1
bar_end_val = bar_end_stats.get(key, 0) * multiplier
bar_start_val = bar_start_stats.get(key, 0) * multiplier
if 'pct' in fmt or key == 'final_usd':
diff = bar_start_val - bar_end_val
diff_str = f"+{diff:.2f}" if diff >= 0 else f"{diff:.2f}"
else:
diff = bar_start_val - bar_end_val
diff_str = f"+{diff:.0f}" if diff >= 0 else f"{diff:.0f}"
print(f"{metric_name:<20} {fmt.format(bar_end_val):<15} {fmt.format(bar_start_val):<15} {diff_str:<15}")
# Save detailed results
save_detailed_results(results)
return results
def save_detailed_results(results: Dict):
"""Save detailed comparison results to files."""
print(f"\n💾 SAVING DETAILED RESULTS")
print("-" * 40)
for config_name, result in results.items():
trades = result['trades']
stats = result['stats']
# Save trades
if trades:
trades_df = pd.DataFrame(trades)
trades_file = f"bar_start_trades_{config_name}.csv"
trades_df.to_csv(trades_file, index=False)
print(f"Saved {len(trades)} trades to: {trades_file}")
# Save stats
stats_file = f"bar_start_stats_{config_name}.json"
import json
with open(stats_file, 'w') as f:
# Convert any datetime objects to strings
stats_clean = {}
for k, v in stats.items():
if isinstance(v, pd.Timestamp):
stats_clean[k] = v.isoformat()
else:
stats_clean[k] = v
json.dump(stats_clean, f, indent=2, default=str)
print(f"Saved statistics to: {stats_file}")
# Create comparison summary
if len(results) >= 2:
comparison_data = []
for config_name, result in results.items():
stats = result['stats']
comparison_data.append({
'approach': config_name,
'final_usd': stats.get('final_usd', 0),
'total_return_pct': stats.get('profit_ratio', 0) * 100,
'total_trades': stats.get('n_trades', 0),
'win_rate': stats.get('win_rate', 0) * 100,
'max_drawdown_pct': stats.get('max_drawdown', 0) * 100,
'avg_trade_return_pct': stats.get('avg_trade', 0) * 100
})
comparison_df = pd.DataFrame(comparison_data)
comparison_file = "bar_start_vs_bar_end_comparison.csv"
comparison_df.to_csv(comparison_file, index=False)
print(f"Saved comparison summary to: {comparison_file}")
def main():
"""Main test function."""
print("🎯 TESTING BAR-START SIGNAL GENERATION WITH FULL BACKTESTER")
print("=" * 80)
print()
print("This test compares the bar-start approach with the current bar-end")
print("approach using the full incremental backtester to see if it fixes")
print("the timing alignment issue with the original strategy.")
print()
results = test_bar_start_backtester()
if results:
print("\n✅ Test completed successfully!")
print("\n💡 KEY INSIGHTS:")
print("1. Bar-start signals are generated 15 minutes earlier than bar-end")
print("2. This timing difference should align better with the original strategy")
print("3. More entry signals are captured with the bar-start approach")
print("4. The performance difference shows the impact of signal timing")
# Check if bar-start performed better
if 'bar_end' in results and 'bar_start' in results:
bar_end_return = results['bar_end']['stats'].get('profit_ratio', 0) * 100
bar_start_return = results['bar_start']['stats'].get('profit_ratio', 0) * 100
if bar_start_return > bar_end_return:
improvement = bar_start_return - bar_end_return
print(f"\n🎉 Bar-start approach improved performance by {improvement:.2f}%!")
else:
decline = bar_end_return - bar_start_return
print(f"\n⚠️ Bar-start approach decreased performance by {decline:.2f}%")
print(" This may indicate other factors affecting the timing alignment.")
else:
print("\n❌ Test failed to complete")
if __name__ == "__main__":
main()