Files
zhutoutoutousan 98a87a69ca Update
2026-02-13 08:03:25 +01:00

275 lines
12 KiB
Python

"""
Enhanced ONNX Strategy for Backtesting with Historical Data Buffer
This version maintains a buffer of historical bars for proper ONNX predictions.
"""
from datetime import datetime
from typing import Dict, Any, Optional, List
import numpy as np
import MetaTrader5 as mt5
import onnxruntime as ort
import pickle
import os
from base_strategy import BaseStrategy
class ONNXBacktestStrategy(BaseStrategy):
"""
ONNX strategy with historical data buffer for backtesting.
"""
def __init__(self, symbol: str, timeframe: int, model_path: str,
scaler_path: Optional[str] = None, initial_balance: float = 10000.0,
prediction_threshold: float = 0.0001, min_confidence: float = 0.0,
lot_size: float = 0.1, stop_loss_pips: int = 50, take_profit_pips: int = 100):
"""
Initialize the ONNX backtest strategy.
"""
super().__init__(symbol, timeframe, initial_balance)
self.model_path = model_path
self.scaler_path = scaler_path
self.prediction_threshold = prediction_threshold
self.min_confidence = min_confidence
self.lot_size = lot_size
self.stop_loss_pips = stop_loss_pips
self.take_profit_pips = take_profit_pips
# Load ONNX model
if not os.path.exists(model_path):
raise FileNotFoundError(f"ONNX model not found: {model_path}")
self.session = ort.InferenceSession(model_path)
self.input_name = self.session.get_inputs()[0].name
self.output_name = self.session.get_outputs()[0].name
self.input_shape = self.session.get_inputs()[0].shape
# Determine lookback
if self.input_shape and len(self.input_shape) >= 2:
self.lookback = int(self.input_shape[1]) if self.input_shape[1] else 60
else:
self.lookback = 60
# Load scaler
if scaler_path and os.path.exists(scaler_path):
with open(scaler_path, 'rb') as f:
self.scaler = pickle.load(f)
else:
self.scaler = None
# Historical data buffer
self.historical_bars: List[Dict[str, Any]] = []
def get_required_indicators(self) -> Dict[str, Dict[str, Any]]:
"""Required indicators for feature preparation."""
# MT5 uses PRICE_CLOSE constant, but if not available, use 0 (close price)
price_close = getattr(mt5, 'PRICE_CLOSE', 0)
return {
'rsi': {'period': 14, 'applied_price': price_close},
'ema': {'period': 50, 'applied_price': price_close},
'atr': {'period': 14}
}
def prepare_features(self) -> np.ndarray:
"""Prepare features from historical buffer - must match training features (13 total)."""
if len(self.historical_bars) < self.lookback:
return None
features = []
bars_to_use = self.historical_bars[-self.lookback:]
# Calculate EMA20 and volume MA for all bars first
closes = [bar['close'] for bar in bars_to_use]
volumes = [bar.get('tick_volume', 0) for bar in bars_to_use]
# Calculate EMA20 (using pandas-like ewm)
import pandas as pd
closes_series = pd.Series(closes)
ema20_values = closes_series.ewm(span=20, adjust=False).mean().tolist()
# Calculate volume MA
volumes_series = pd.Series(volumes)
volume_ma_values = volumes_series.rolling(window=20, min_periods=1).mean().tolist()
for i, bar in enumerate(bars_to_use):
feature_row = []
# OHLC (4 features)
feature_row.append(bar['open'])
feature_row.append(bar['high'])
feature_row.append(bar['low'])
feature_row.append(bar['close'])
# Volume (1 feature)
volume = bar.get('tick_volume', 0)
feature_row.append(volume / 1000000.0)
# RSI (1 feature)
rsi = bar.get('rsi', 50.0)
feature_row.append(rsi / 100.0)
# EMA20 (1 feature) - normalized difference
ema20 = ema20_values[i] if i < len(ema20_values) else bar['close']
feature_row.append((ema20 - bar['close']) / bar['close'] if bar['close'] > 0 else 0.0)
# EMA50 (1 feature) - normalized difference
ema50 = bar.get('ema', bar['close'])
feature_row.append((ema50 - bar['close']) / bar['close'] if bar['close'] > 0 else 0.0)
# ATR (1 feature)
atr = bar.get('atr', 0.0)
feature_row.append(atr / bar['close'] if bar['close'] > 0 else 0.0)
# Price change (1 feature)
if i > 0:
prev_close = bars_to_use[i-1]['close']
price_change = (bar['close'] - prev_close) / prev_close if prev_close > 0 else 0.0
else:
price_change = 0.0
feature_row.append(price_change)
# High/Low ratio (1 feature)
feature_row.append(bar['high'] / bar['low'] if bar['low'] > 0 else 1.0)
# Volume MA and ratio (2 features)
volume_ma = volume_ma_values[i] if i < len(volume_ma_values) else max(volume, 1)
volume_ratio = volume / max(volume_ma, 1) if volume_ma > 0 else 1.0
feature_row.append(volume_ma / 1000000.0) # Normalized volume MA
feature_row.append(volume_ratio)
features.append(feature_row)
features = np.array(features, dtype=np.float32)
# Normalize
if self.scaler is not None:
original_shape = features.shape
features_flat = features.reshape(-1, features.shape[-1])
features_scaled = self.scaler.transform(features_flat)
features = features_scaled.reshape(original_shape)
else:
# Simple normalization
mean = features.mean(axis=0)
std = features.std(axis=0) + 1e-8
features = (features - mean) / std
# Reshape for model: (1, lookback, features)
features = features.reshape(1, self.lookback, -1)
return features
def predict_price(self) -> Optional[float]:
"""Make prediction using ONNX model."""
if len(self.historical_bars) < self.lookback:
return None
input_data = self.prepare_features()
if input_data is None:
return None
try:
outputs = self.session.run([self.output_name], {self.input_name: input_data})
prediction = outputs[0][0][0]
# Model now predicts price change percentage (e.g., -0.003 = -0.3%)
# These values should be between -1 and 1 (or slightly outside for extreme cases)
# Don't filter based on absolute price range anymore
return float(prediction)
except Exception as e:
print(f"Prediction error: {e}")
import traceback
traceback.print_exc()
return None
def on_bar(self, bar_data: Dict[str, Any]) -> None:
"""Trading logic based on ONNX predictions."""
# Add current bar to historical buffer
self.historical_bars.append(bar_data.copy())
# Keep only necessary history
if len(self.historical_bars) > self.lookback + 50:
self.historical_bars = self.historical_bars[-(self.lookback + 50):]
# Check if we have enough data
if len(self.historical_bars) < self.lookback:
return
current_price = bar_data['close']
# Check existing position
if self.position is not None:
self.check_stop_loss_take_profit(current_price)
return
# Make prediction
# Model now predicts price change percentage directly (e.g., 0.001 = 0.1%)
predicted_change_pct = self.predict_price()
if predicted_change_pct is None:
return
# Model predicts price change percentage directly
# Check if it's a percentage (between -1 and 1) or absolute price
if abs(predicted_change_pct) < 1.0:
# It's already a percentage (e.g., 0.001 = 0.1%)
price_change_pct = predicted_change_pct
else:
# It's an absolute price (old model format), convert to percentage
predicted_price = predicted_change_pct
if predicted_price <= 0 or predicted_price > 10000:
return # Invalid prediction
price_change = predicted_price - current_price
price_change_pct = (price_change / current_price) if current_price > 0 else 0.0
# Calculate confidence (simple heuristic)
# For percentage predictions (0.001 = 0.1%), normalize to 0-1
# If price_change_pct is already a percentage (e.g., 0.001), use it directly
# If it's a large number, it's already in percentage form
if abs(price_change_pct) < 1.0:
# It's a decimal percentage (e.g., 0.001 = 0.1%)
confidence = min(abs(price_change_pct) / 0.01, 1.0) # Normalize: 0.01 = 1% = 100% confidence
else:
# It's already in percentage form (e.g., 0.1 = 0.1%)
confidence = min(abs(price_change_pct) / 1.0, 1.0) # Normalize: 1% = 100% confidence
# Debug: Print first few predictions (only for debugging)
if len(self.historical_bars) % 100 == 0:
predicted_price_val = current_price * (1 + price_change_pct) if abs(price_change_pct) < 1.0 else current_price * (1 + price_change_pct / 100)
print(f" Debug - Bar {len(self.historical_bars)}, Price: {current_price:.2f}, "
f"Predicted Change: {price_change_pct*100:.4f}%, Abs: {abs(price_change_pct):.6f}, "
f"Confidence: {confidence:.3f}, Threshold: {self.prediction_threshold:.6f}, "
f"MinConf: {self.min_confidence:.2f}, WillTrade: {abs(price_change_pct) >= self.prediction_threshold and confidence >= self.min_confidence}")
# Check if we should trade
if confidence < self.min_confidence:
return
if abs(price_change_pct) < self.prediction_threshold:
return
# Open position based on prediction
if price_change_pct > self.prediction_threshold:
# Bullish prediction
sl = current_price - (self.stop_loss_pips / 10000) if self.stop_loss_pips > 0 else None
tp = current_price + (self.take_profit_pips / 10000) if self.take_profit_pips > 0 else None
self.open_position('BUY', self.lot_size, current_price, sl, tp, 'ONNX Buy')
elif price_change_pct < -self.prediction_threshold:
# Bearish prediction
sl = current_price + (self.stop_loss_pips / 10000) if self.stop_loss_pips > 0 else None
tp = current_price - (self.take_profit_pips / 10000) if self.take_profit_pips > 0 else None
self.open_position('SELL', self.lot_size, current_price, sl, tp, 'ONNX Sell')
def get_parameters(self) -> Dict[str, Any]:
"""Return strategy parameters."""
return {
'model_path': self.model_path,
'lookback': self.lookback,
'prediction_threshold': self.prediction_threshold,
'min_confidence': self.min_confidence,
'lot_size': self.lot_size,
'stop_loss_pips': self.stop_loss_pips,
'take_profit_pips': self.take_profit_pips
}