Update
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
"""
|
||||
Backtesting Script for RSI Divergence ONNX Model
|
||||
Tests the trained model on historical data and evaluates trading performance.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import MetaTrader5 as mt5
|
||||
from datetime import datetime, timedelta
|
||||
import onnxruntime as ort
|
||||
import pickle
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class RSIDivergenceBacktester:
|
||||
"""
|
||||
Backtests the RSI divergence ONNX model.
|
||||
"""
|
||||
|
||||
def __init__(self, model_path: str, scaler_path: str, features_path: str, lookback: int = 60):
|
||||
"""
|
||||
Initialize the backtester.
|
||||
|
||||
Args:
|
||||
model_path: Path to ONNX model file
|
||||
scaler_path: Path to scaler pickle file
|
||||
features_path: Path to features list pickle file
|
||||
lookback: Number of bars to look back
|
||||
"""
|
||||
self.lookback = lookback
|
||||
|
||||
# Load ONNX model
|
||||
print(f"Loading ONNX model from {model_path}...")
|
||||
self.session = ort.InferenceSession(model_path)
|
||||
print("ONNX model loaded successfully")
|
||||
|
||||
# Load scaler
|
||||
print(f"Loading scaler from {scaler_path}...")
|
||||
with open(scaler_path, 'rb') as f:
|
||||
self.scaler = pickle.load(f)
|
||||
print("Scaler loaded successfully")
|
||||
|
||||
# Load feature list
|
||||
print(f"Loading features from {features_path}...")
|
||||
with open(features_path, 'rb') as f:
|
||||
self.feature_cols = pickle.load(f)
|
||||
print(f"Using {len(self.feature_cols)} features")
|
||||
|
||||
# Divergence type mapping
|
||||
self.divergence_types = {
|
||||
0: 'NONE',
|
||||
1: 'REGULAR_BULLISH',
|
||||
2: 'REGULAR_BEARISH',
|
||||
3: 'HIDDEN_BULLISH',
|
||||
4: 'HIDDEN_BEARISH'
|
||||
}
|
||||
|
||||
def prepare_features(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Prepare features from raw data (same as in collect_btcusd_data.py)."""
|
||||
feature_df = df.copy()
|
||||
|
||||
# Price-based features
|
||||
feature_df['returns'] = feature_df['close'].pct_change()
|
||||
feature_df['high_low_ratio'] = feature_df['high'] / (feature_df['low'] + 1e-10)
|
||||
feature_df['close_open_ratio'] = feature_df['close'] / (feature_df['open'] + 1e-10)
|
||||
|
||||
# Moving averages
|
||||
feature_df['sma_20'] = feature_df['close'].rolling(window=20).mean()
|
||||
feature_df['sma_50'] = feature_df['close'].rolling(window=50).mean()
|
||||
feature_df['ema_20'] = feature_df['close'].ewm(span=20).mean()
|
||||
feature_df['ema_50'] = feature_df['close'].ewm(span=50).mean()
|
||||
|
||||
# ATR
|
||||
high_low = feature_df['high'] - feature_df['low']
|
||||
high_close = np.abs(feature_df['high'] - feature_df['close'].shift())
|
||||
low_close = np.abs(feature_df['low'] - feature_df['close'].shift())
|
||||
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
|
||||
feature_df['atr'] = tr.rolling(window=14).mean()
|
||||
feature_df['atr_pct'] = feature_df['atr'] / (feature_df['close'] + 1e-10)
|
||||
|
||||
# Volume features
|
||||
if 'tick_volume' in feature_df.columns:
|
||||
feature_df['volume_ma'] = feature_df['tick_volume'].rolling(window=20).mean()
|
||||
feature_df['volume_ratio'] = feature_df['tick_volume'] / (feature_df['volume_ma'] + 1e-10)
|
||||
|
||||
# Price position relative to range
|
||||
feature_df['price_position'] = (feature_df['close'] - feature_df['low'].rolling(20).min()) / (
|
||||
feature_df['high'].rolling(20).max() - feature_df['low'].rolling(20).min() + 1e-10
|
||||
)
|
||||
|
||||
# Calculate RSI
|
||||
from rsi_divergence_detector import RSIDivergenceDetector
|
||||
detector = RSIDivergenceDetector()
|
||||
feature_df['rsi'] = detector.calculate_rsi(feature_df['close'])
|
||||
|
||||
return feature_df
|
||||
|
||||
def predict(self, df: pd.DataFrame, index: int) -> tuple:
|
||||
"""
|
||||
Make prediction at given index.
|
||||
|
||||
Args:
|
||||
df: DataFrame with features
|
||||
index: Current bar index
|
||||
|
||||
Returns:
|
||||
Tuple of (predicted_class, confidence)
|
||||
"""
|
||||
if index < self.lookback:
|
||||
return 0, 0.0
|
||||
|
||||
# Get feature sequence
|
||||
feature_data = df[self.feature_cols].iloc[index - self.lookback:index].values
|
||||
|
||||
# Scale features
|
||||
feature_data_scaled = self.scaler.transform(feature_data)
|
||||
|
||||
# Reshape for model input (1, lookback, features)
|
||||
feature_data_scaled = feature_data_scaled.reshape(1, self.lookback, -1)
|
||||
|
||||
# Run ONNX model
|
||||
input_name = self.session.get_inputs()[0].name
|
||||
output_name = self.session.get_outputs()[0].name
|
||||
|
||||
result = self.session.run([output_name], {input_name: feature_data_scaled.astype(np.float32)})
|
||||
|
||||
# Get prediction
|
||||
probabilities = result[0][0]
|
||||
predicted_class = int(np.argmax(probabilities))
|
||||
confidence = float(np.max(probabilities))
|
||||
|
||||
return predicted_class, confidence
|
||||
|
||||
def backtest(self, symbol: str, timeframe: int, start_date: datetime,
|
||||
end_date: datetime, initial_balance: float = 10000.0,
|
||||
lot_size: float = 0.01, min_confidence: float = 0.7) -> dict:
|
||||
"""
|
||||
Run backtest on historical data.
|
||||
|
||||
Args:
|
||||
symbol: Trading symbol
|
||||
timeframe: MT5 timeframe constant
|
||||
start_date: Start date
|
||||
end_date: End date
|
||||
initial_balance: Starting balance
|
||||
lot_size: Lot size per trade
|
||||
min_confidence: Minimum confidence to take a trade
|
||||
|
||||
Returns:
|
||||
Dictionary with backtest results
|
||||
"""
|
||||
print(f"\n{'='*60}")
|
||||
print("RSI Divergence Model Backtest")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# Fetch data
|
||||
if not mt5.initialize():
|
||||
raise RuntimeError(f"MT5 initialization failed: {mt5.last_error()}")
|
||||
|
||||
try:
|
||||
print(f"Fetching {symbol} data from {start_date} to {end_date}...")
|
||||
rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date)
|
||||
|
||||
if rates is None or len(rates) == 0:
|
||||
raise ValueError(f"No data available for {symbol}")
|
||||
|
||||
df = pd.DataFrame(rates)
|
||||
df['time'] = pd.to_datetime(df['time'], unit='s')
|
||||
df.set_index('time', inplace=True)
|
||||
df.columns = [col.lower() for col in df.columns]
|
||||
|
||||
print(f"Fetched {len(df)} bars")
|
||||
|
||||
# Prepare features
|
||||
print("Preparing features...")
|
||||
df = self.prepare_features(df)
|
||||
df = df.dropna()
|
||||
|
||||
print(f"Data ready: {len(df)} bars after feature preparation")
|
||||
|
||||
# Backtest simulation
|
||||
balance = initial_balance
|
||||
equity = initial_balance
|
||||
position = None # (type: 'BUY' or 'SELL', entry_price, entry_index, size)
|
||||
trades = []
|
||||
equity_curve = [initial_balance]
|
||||
|
||||
print("\nRunning backtest...")
|
||||
for i in tqdm(range(self.lookback, len(df))):
|
||||
current_price = df['close'].iloc[i]
|
||||
current_time = df.index[i]
|
||||
|
||||
# Make prediction
|
||||
predicted_class, confidence = self.predict(df, i)
|
||||
divergence_type = self.divergence_types[predicted_class]
|
||||
|
||||
# Close position if needed
|
||||
if position is not None:
|
||||
# Simple exit: close after 10 bars or on opposite signal
|
||||
bars_in_trade = i - position[2]
|
||||
|
||||
if bars_in_trade >= 10:
|
||||
# Close position
|
||||
if position[0] == 'BUY':
|
||||
pnl = (current_price - position[1]) * position[3]
|
||||
else:
|
||||
pnl = (position[1] - current_price) * position[3]
|
||||
|
||||
balance += pnl
|
||||
equity = balance
|
||||
|
||||
trades.append({
|
||||
'entry_time': df.index[position[2]],
|
||||
'exit_time': current_time,
|
||||
'type': position[0],
|
||||
'entry_price': position[1],
|
||||
'exit_price': current_price,
|
||||
'size': position[3],
|
||||
'pnl': pnl,
|
||||
'bars_held': bars_in_trade
|
||||
})
|
||||
|
||||
position = None
|
||||
|
||||
# Open new position based on prediction
|
||||
if position is None and confidence >= min_confidence:
|
||||
if divergence_type == 'REGULAR_BULLISH' or divergence_type == 'HIDDEN_BULLISH':
|
||||
# Buy signal
|
||||
position = ('BUY', current_price, i, lot_size)
|
||||
elif divergence_type == 'REGULAR_BEARISH' or divergence_type == 'HIDDEN_BEARISH':
|
||||
# Sell signal
|
||||
position = ('SELL', current_price, i, lot_size)
|
||||
|
||||
# Update equity (with unrealized PnL)
|
||||
if position is not None:
|
||||
if position[0] == 'BUY':
|
||||
unrealized_pnl = (current_price - position[1]) * position[3]
|
||||
else:
|
||||
unrealized_pnl = (position[1] - current_price) * position[3]
|
||||
equity = balance + unrealized_pnl
|
||||
else:
|
||||
equity = balance
|
||||
|
||||
equity_curve.append(equity)
|
||||
|
||||
# Close any remaining position
|
||||
if position is not None:
|
||||
final_price = df['close'].iloc[-1]
|
||||
if position[0] == 'BUY':
|
||||
pnl = (final_price - position[1]) * position[3]
|
||||
else:
|
||||
pnl = (position[1] - final_price) * position[3]
|
||||
|
||||
balance += pnl
|
||||
trades.append({
|
||||
'entry_time': df.index[position[2]],
|
||||
'exit_time': df.index[-1],
|
||||
'type': position[0],
|
||||
'entry_price': position[1],
|
||||
'exit_price': final_price,
|
||||
'size': position[3],
|
||||
'pnl': pnl,
|
||||
'bars_held': len(df) - position[2]
|
||||
})
|
||||
|
||||
# Calculate metrics
|
||||
trades_df = pd.DataFrame(trades)
|
||||
|
||||
if len(trades) > 0:
|
||||
total_trades = len(trades)
|
||||
winning_trades = len(trades_df[trades_df['pnl'] > 0])
|
||||
losing_trades = len(trades_df[trades_df['pnl'] <= 0])
|
||||
win_rate = winning_trades / total_trades * 100
|
||||
|
||||
total_pnl = trades_df['pnl'].sum()
|
||||
avg_win = trades_df[trades_df['pnl'] > 0]['pnl'].mean() if winning_trades > 0 else 0
|
||||
avg_loss = trades_df[trades_df['pnl'] <= 0]['pnl'].mean() if losing_trades > 0 else 0
|
||||
|
||||
profit_factor = abs(avg_win * winning_trades / (avg_loss * losing_trades)) if losing_trades > 0 and avg_loss != 0 else float('inf')
|
||||
|
||||
final_balance = balance
|
||||
total_return = (final_balance - initial_balance) / initial_balance * 100
|
||||
|
||||
# Drawdown
|
||||
equity_series = pd.Series(equity_curve)
|
||||
running_max = equity_series.expanding().max()
|
||||
drawdown = (equity_series - running_max) / running_max * 100
|
||||
max_drawdown = drawdown.min()
|
||||
else:
|
||||
total_trades = 0
|
||||
winning_trades = 0
|
||||
losing_trades = 0
|
||||
win_rate = 0
|
||||
total_pnl = 0
|
||||
avg_win = 0
|
||||
avg_loss = 0
|
||||
profit_factor = 0
|
||||
final_balance = initial_balance
|
||||
total_return = 0
|
||||
max_drawdown = 0
|
||||
|
||||
results = {
|
||||
'initial_balance': initial_balance,
|
||||
'final_balance': final_balance,
|
||||
'total_return_pct': total_return,
|
||||
'total_trades': total_trades,
|
||||
'winning_trades': winning_trades,
|
||||
'losing_trades': losing_trades,
|
||||
'win_rate': win_rate,
|
||||
'total_pnl': total_pnl,
|
||||
'avg_win': avg_win,
|
||||
'avg_loss': avg_loss,
|
||||
'profit_factor': profit_factor,
|
||||
'max_drawdown_pct': max_drawdown,
|
||||
'trades': trades_df
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
finally:
|
||||
mt5.shutdown()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function."""
|
||||
parser = argparse.ArgumentParser(description='Backtest RSI divergence ONNX model')
|
||||
parser.add_argument('--model', type=str, required=True, help='Path to ONNX model file')
|
||||
parser.add_argument('--scaler', type=str, required=True, help='Path to scaler pickle file')
|
||||
parser.add_argument('--features', type=str, required=True, help='Path to features list pickle file')
|
||||
parser.add_argument('--symbol', type=str, default='BTCUSD', help='Trading symbol')
|
||||
parser.add_argument('--timeframe', type=str, default='H1',
|
||||
choices=['M1', 'M5', 'M15', 'M30', 'H1', 'H4', 'D1'],
|
||||
help='Timeframe')
|
||||
parser.add_argument('--days', type=int, default=90, help='Number of days to backtest')
|
||||
parser.add_argument('--balance', type=float, default=10000.0, help='Initial balance')
|
||||
parser.add_argument('--lot-size', type=float, default=0.01, help='Lot size per trade')
|
||||
parser.add_argument('--min-confidence', type=float, default=0.7,
|
||||
help='Minimum confidence to take a trade')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Convert timeframe
|
||||
timeframe_map = {
|
||||
'M1': mt5.TIMEFRAME_M1,
|
||||
'M5': mt5.TIMEFRAME_M5,
|
||||
'M15': mt5.TIMEFRAME_M15,
|
||||
'M30': mt5.TIMEFRAME_M30,
|
||||
'H1': mt5.TIMEFRAME_H1,
|
||||
'H4': mt5.TIMEFRAME_H4,
|
||||
'D1': mt5.TIMEFRAME_D1
|
||||
}
|
||||
timeframe = timeframe_map[args.timeframe]
|
||||
|
||||
# Create backtester
|
||||
backtester = RSIDivergenceBacktester(
|
||||
args.model, args.scaler, args.features, lookback=60
|
||||
)
|
||||
|
||||
# Run backtest
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=args.days)
|
||||
|
||||
results = backtester.backtest(
|
||||
args.symbol, timeframe, start_date, end_date,
|
||||
initial_balance=args.balance,
|
||||
lot_size=args.lot_size,
|
||||
min_confidence=args.min_confidence
|
||||
)
|
||||
|
||||
# Print results
|
||||
print(f"\n{'='*60}")
|
||||
print("Backtest Results")
|
||||
print(f"{'='*60}")
|
||||
print(f"Initial Balance: ${results['initial_balance']:,.2f}")
|
||||
print(f"Final Balance: ${results['final_balance']:,.2f}")
|
||||
print(f"Total Return: {results['total_return_pct']:.2f}%")
|
||||
print(f"Max Drawdown: {results['max_drawdown_pct']:.2f}%")
|
||||
print(f"\nTrades:")
|
||||
print(f" Total: {results['total_trades']}")
|
||||
print(f" Winning: {results['winning_trades']}")
|
||||
print(f" Losing: {results['losing_trades']}")
|
||||
print(f" Win Rate: {results['win_rate']:.2f}%")
|
||||
print(f"\nPerformance:")
|
||||
print(f" Total P&L: ${results['total_pnl']:,.2f}")
|
||||
print(f" Avg Win: ${results['avg_win']:,.2f}")
|
||||
print(f" Avg Loss: ${results['avg_loss']:,.2f}")
|
||||
print(f" Profit Factor: {results['profit_factor']:.2f}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# Save trades to CSV
|
||||
if len(results['trades']) > 0:
|
||||
output_file = f"backtest_trades_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||
results['trades'].to_csv(output_file, index=False)
|
||||
print(f"Trades saved to: {output_file}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user