326 lines
12 KiB
Python
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() |