Files
profitable-expert-advisor/backtesting/MT5/backtest_engine.py
T
zhutoutoutousan 5b44e14211 Update
2026-01-05 05:37:33 +01:00

258 lines
9.0 KiB
Python

"""
Backtesting Engine for MetaTrader5
This module provides the core backtesting functionality using MT5 historical data.
"""
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
import MetaTrader5 as mt5
import pandas as pd
import numpy as np
from base_strategy import BaseStrategy
class BacktestEngine:
"""
Main backtesting engine that runs strategies on historical data.
"""
def __init__(self, strategy: BaseStrategy, start_date: datetime, end_date: datetime):
"""
Initialize the backtesting engine.
Args:
strategy: Strategy instance to backtest
start_date: Start date for backtesting
end_date: End date for backtesting
"""
self.strategy = strategy
self.start_date = start_date
self.end_date = end_date
# Initialize MT5 connection
if not mt5.initialize():
raise RuntimeError(f"MT5 initialization failed: {mt5.last_error()}")
# Indicator handles
self.indicator_handles = {}
self.setup_indicators()
def setup_indicators(self):
"""Setup all required indicators for the strategy."""
required_indicators = self.strategy.get_required_indicators()
for indicator_name, params in required_indicators.items():
handle = None
if indicator_name.lower() == 'rsi':
handle = mt5.iRSI(
self.strategy.symbol,
self.strategy.timeframe,
params.get('period', 14),
params.get('applied_price', mt5.PRICE_CLOSE)
)
elif indicator_name.lower() == 'ema':
handle = mt5.iMA(
self.strategy.symbol,
self.strategy.timeframe,
params.get('period', 50),
0, # shift
mt5.MODE_EMA,
params.get('applied_price', mt5.PRICE_CLOSE)
)
elif indicator_name.lower() == 'sma':
handle = mt5.iMA(
self.strategy.symbol,
self.strategy.timeframe,
params.get('period', 50),
0, # shift
mt5.MODE_SMA,
params.get('applied_price', mt5.PRICE_CLOSE)
)
elif indicator_name.lower() == 'atr':
handle = mt5.iATR(
self.strategy.symbol,
self.strategy.timeframe,
params.get('period', 14)
)
elif indicator_name.lower() == 'macd':
handle = mt5.iMACD(
self.strategy.symbol,
self.strategy.timeframe,
params.get('fast', 12),
params.get('slow', 26),
params.get('signal', 9),
params.get('applied_price', mt5.PRICE_CLOSE)
)
if handle is not None and handle != mt5.INVALID_HANDLE:
self.indicator_handles[indicator_name] = handle
else:
print(f"Warning: Failed to create {indicator_name} indicator")
def get_indicator_values(self, indicator_name: str, count: int = 1) -> Optional[np.ndarray]:
"""
Get indicator values.
Args:
indicator_name: Name of the indicator
count: Number of values to retrieve
Returns:
Array of indicator values or None
"""
if indicator_name not in self.indicator_handles:
return None
handle = self.indicator_handles[indicator_name]
buffer = np.zeros(count, dtype=float)
if indicator_name.lower() == 'macd':
# MACD returns 3 buffers
result = mt5.copy_buffer(handle, 0, 0, count) # Main line
if result is None:
return None
return np.array(result)
else:
result = mt5.copy_buffer(handle, 0, 0, count)
if result is None:
return None
return np.array(result)
def get_bar_data(self, time: datetime) -> Optional[Dict[str, Any]]:
"""
Get bar data and indicator values for a specific time.
Args:
time: Bar time
Returns:
Dictionary with bar data and indicators
"""
# Get rates
rates = mt5.copy_rates_from(
self.strategy.symbol,
self.strategy.timeframe,
time,
1
)
if rates is None or len(rates) == 0:
return None
rate = rates[0]
# Get spread
symbol_info = mt5.symbol_info(self.strategy.symbol)
spread = symbol_info.spread if symbol_info else 0
# Build bar data
bar_data = {
'time': datetime.fromtimestamp(rate['time']),
'open': float(rate['open']),
'high': float(rate['high']),
'low': float(rate['low']),
'close': float(rate['close']),
'tick_volume': int(rate['tick_volume']),
'spread': spread,
'indicators': {}
}
# Get indicator values
for indicator_name in self.indicator_handles.keys():
values = self.get_indicator_values(indicator_name, 2)
if values is not None and len(values) >= 1:
bar_data['indicators'][indicator_name] = values[0]
# Also add to top level for convenience
bar_data[indicator_name.lower()] = values[0]
return bar_data
def run(self) -> Dict[str, Any]:
"""
Run the backtest.
Returns:
Dictionary with backtest results and performance metrics
"""
print(f"Starting backtest from {self.start_date} to {self.end_date}")
print(f"Symbol: {self.strategy.symbol}, Timeframe: {self.strategy.timeframe}")
# Get all bars in the date range
rates = mt5.copy_rates_range(
self.strategy.symbol,
self.strategy.timeframe,
self.start_date,
self.end_date
)
if rates is None or len(rates) == 0:
raise ValueError(f"No data available for {self.strategy.symbol} in the specified date range")
print(f"Processing {len(rates)} bars...")
# Process each bar
processed_bars = 0
for i, rate in enumerate(rates):
bar_time = datetime.fromtimestamp(rate['time'])
# Get full bar data with indicators
bar_data = self.get_bar_data(bar_time)
if bar_data is None:
continue
# Check stop loss/take profit on current position
if self.strategy.position is not None:
self.strategy.check_stop_loss_take_profit(bar_data['close'])
# Call strategy on_bar method
try:
self.strategy.on_bar(bar_data)
except Exception as e:
print(f"Error in strategy on_bar at {bar_time}: {e}")
continue
# Update equity (unrealized P&L)
if self.strategy.position is not None:
if self.strategy.position['type'] == 'BUY':
unrealized_pnl = (bar_data['close'] - self.strategy.position['open_price']) * \
self.strategy.position['volume'] * 10000 * 10
else:
unrealized_pnl = (self.strategy.position['open_price'] - bar_data['close']) * \
self.strategy.position['volume'] * 10000 * 10
self.strategy.equity = self.strategy.current_balance + unrealized_pnl
else:
self.strategy.equity = self.strategy.current_balance
processed_bars += 1
if processed_bars % 100 == 0:
print(f"Processed {processed_bars}/{len(rates)} bars...")
# Close any open position at the end
if self.strategy.position is not None:
last_bar = rates[-1]
last_price = float(last_bar['close'])
self.strategy.close_position(last_price)
print(f"Backtest completed. Processed {processed_bars} bars.")
# Get performance metrics
metrics = self.strategy.get_performance_metrics()
# Cleanup
self.cleanup()
return {
'metrics': metrics,
'trades': self.strategy.closed_trades,
'strategy_name': self.strategy.__class__.__name__
}
def cleanup(self):
"""Clean up indicator handles and MT5 connection."""
for handle in self.indicator_handles.values():
mt5.indicator_release(handle)
mt5.shutdown()