This commit is contained in:
zhutoutoutousan
2026-02-13 08:03:25 +01:00
parent 09c2f54c71
commit 98a87a69ca
134 changed files with 20003 additions and 253 deletions
+67 -75
View File
@@ -10,6 +10,7 @@ import MetaTrader5 as mt5
import pandas as pd
import numpy as np
from base_strategy import BaseStrategy
from indicator_utils import calculate_rsi, calculate_ema, calculate_sma, calculate_atr, calculate_macd
class BacktestEngine:
@@ -34,91 +35,84 @@ class BacktestEngine:
if not mt5.initialize():
raise RuntimeError(f"MT5 initialization failed: {mt5.last_error()}")
# Indicator handles
self.indicator_handles = {}
self.setup_indicators()
# Store required indicators config (we'll calculate them from data)
self.required_indicators = self.strategy.get_required_indicators()
def setup_indicators(self):
"""Setup all required indicators for the strategy."""
required_indicators = self.strategy.get_required_indicators()
# Pre-calculate indicators from historical data
self.indicator_data = {}
self._precalculate_indicators()
for indicator_name, params in required_indicators.items():
handle = None
def _precalculate_indicators(self):
"""Pre-calculate all indicators from historical data."""
# Fetch all historical data first
rates = mt5.copy_rates_range(
self.strategy.symbol,
self.strategy.timeframe,
self.start_date - timedelta(days=100), # Extra data for indicator calculation
self.end_date
)
if rates is None or len(rates) == 0:
print("Warning: Could not fetch historical data for indicators")
return
# Convert to DataFrame
df = pd.DataFrame(rates)
df['time'] = pd.to_datetime(df['time'], unit='s')
df.set_index('time', inplace=True)
# Calculate indicators
for indicator_name, params in self.required_indicators.items():
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)
)
period = params.get('period', 14)
self.indicator_data['rsi'] = calculate_rsi(df['close'], period)
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)
)
period = params.get('period', 50)
self.indicator_data['ema'] = calculate_ema(df['close'], period)
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)
)
period = params.get('period', 50)
self.indicator_data['sma'] = calculate_sma(df['close'], period)
elif indicator_name.lower() == 'atr':
handle = mt5.iATR(
self.strategy.symbol,
self.strategy.timeframe,
params.get('period', 14)
)
period = params.get('period', 14)
self.indicator_data['atr'] = calculate_atr(df, period)
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")
fast = params.get('fast', 12)
slow = params.get('slow', 26)
signal = params.get('signal', 9)
macd_df = calculate_macd(df['close'], fast, slow, signal)
self.indicator_data['macd'] = macd_df['macd']
self.indicator_data['macd_signal'] = macd_df['signal']
self.indicator_data['macd_histogram'] = macd_df['histogram']
def get_indicator_values(self, indicator_name: str, count: int = 1) -> Optional[np.ndarray]:
def get_indicator_value(self, indicator_name: str, time: datetime) -> Optional[float]:
"""
Get indicator values.
Get indicator value for a specific time.
Args:
indicator_name: Name of the indicator
count: Number of values to retrieve
time: Bar time
Returns:
Array of indicator values or None
Indicator value or None
"""
if indicator_name not in self.indicator_handles:
if indicator_name.lower() not in self.indicator_data:
return None
handle = self.indicator_handles[indicator_name]
buffer = np.zeros(count, dtype=float)
series = self.indicator_data[indicator_name.lower()]
if time in series.index:
value = series.loc[time]
return float(value) if not pd.isna(value) else None
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)
# Try to find closest time
try:
closest_time = series.index[series.index <= time][-1] if len(series.index[series.index <= time]) > 0 else None
if closest_time:
value = series.loc[closest_time]
return float(value) if not pd.isna(value) else None
except:
pass
return None
def get_bar_data(self, time: datetime) -> Optional[Dict[str, Any]]:
"""
@@ -160,12 +154,12 @@ class BacktestEngine:
}
# 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]
for indicator_name in self.required_indicators.keys():
value = self.get_indicator_value(indicator_name, bar_data['time'])
if value is not None:
bar_data['indicators'][indicator_name] = value
# Also add to top level for convenience
bar_data[indicator_name.lower()] = values[0]
bar_data[indicator_name.lower()] = value
return bar_data
@@ -251,7 +245,5 @@ class BacktestEngine:
}
def cleanup(self):
"""Clean up indicator handles and MT5 connection."""
for handle in self.indicator_handles.values():
mt5.indicator_release(handle)
"""Clean up MT5 connection."""
mt5.shutdown()
+11 -3
View File
@@ -120,9 +120,17 @@ class BaseStrategy(ABC):
# Validate volume
volume = max(self.min_lot_size, min(volume, self.max_lot_size))
# Calculate margin requirement (simplified)
contract_size = 100000 # Standard lot size
margin_required = volume * contract_size * price * 0.01 # 1% margin (adjust as needed)
# Calculate margin requirement
# For XAUUSD (Gold): 1 lot = 100 oz, typical margin 1-2% of contract value
# For Forex pairs: 1 lot = 100,000 units, typical margin 1-2%
if 'XAU' in self.symbol or 'GOLD' in self.symbol:
contract_size = 100 # 1 lot = 100 oz for gold
margin_percent = 0.02 # 2% margin for gold (more volatile)
else:
contract_size = 100000 # Standard forex lot size
margin_percent = 0.01 # 1% margin for forex
margin_required = volume * contract_size * price * margin_percent
if margin_required > self.equity * 0.9: # Don't use more than 90% of equity
return False
+54
View File
@@ -0,0 +1,54 @@
"""
Indicator calculation utilities for backtesting.
These functions calculate indicators directly from price data,
without requiring MT5 indicator handles.
"""
import numpy as np
import pandas as pd
def calculate_rsi(prices: pd.Series, period: int = 14) -> pd.Series:
"""Calculate RSI indicator."""
delta = prices.diff()
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
return rsi
def calculate_ema(prices: pd.Series, period: int = 50) -> pd.Series:
"""Calculate EMA indicator."""
return prices.ewm(span=period, adjust=False).mean()
def calculate_sma(prices: pd.Series, period: int = 50) -> pd.Series:
"""Calculate SMA indicator."""
return prices.rolling(window=period).mean()
def calculate_atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
"""Calculate ATR indicator."""
high_low = df['high'] - df['low']
high_close = np.abs(df['high'] - df['close'].shift())
low_close = np.abs(df['low'] - df['close'].shift())
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
atr = tr.rolling(window=period).mean()
return atr
def calculate_macd(prices: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9) -> pd.DataFrame:
"""Calculate MACD indicator."""
ema_fast = prices.ewm(span=fast, adjust=False).mean()
ema_slow = prices.ewm(span=slow, adjust=False).mean()
macd = ema_fast - ema_slow
signal_line = macd.ewm(span=signal, adjust=False).mean()
histogram = macd - signal_line
return pd.DataFrame({
'macd': macd,
'signal': signal_line,
'histogram': histogram
})
+274
View File
@@ -0,0 +1,274 @@
"""
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
}
+220
View File
@@ -0,0 +1,220 @@
"""
ONNX-based Trading Strategy for Backtesting
This strategy uses a trained ONNX model to make price predictions and trade based on those predictions.
"""
from datetime import datetime
from typing import Dict, Any, Optional
import numpy as np
import MetaTrader5 as mt5
import onnxruntime as ort
import pickle
import os
from base_strategy import BaseStrategy
class ONNXStrategy(BaseStrategy):
"""
Trading strategy that uses ONNX model predictions for trading decisions.
"""
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 strategy.
Args:
symbol: Trading symbol
timeframe: MT5 timeframe
model_path: Path to ONNX model file
scaler_path: Path to saved scaler (optional)
initial_balance: Starting balance
prediction_threshold: Minimum price change % to trade (0.0001 = 0.01%)
min_confidence: Minimum confidence level (0.0-1.0)
lot_size: Position size
stop_loss_pips: Stop loss in pips
take_profit_pips: Take profit in pips
"""
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 from model shape
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
print("Warning: No scaler provided. Will use default normalization.")
# Track previous prediction for comparison
self.prev_prediction = None
self.prev_price = None
def get_required_indicators(self) -> Dict[str, Dict[str, Any]]:
"""ONNX model doesn't use traditional indicators, but we need RSI, EMA, ATR for features."""
return {
'rsi': {'period': 14, 'applied_price': mt5.PRICE_CLOSE},
'ema': {'period': 50, 'applied_price': mt5.PRICE_CLOSE},
'atr': {'period': 14}
}
def prepare_features(self, bar_data: Dict[str, Any], historical_bars: list) -> np.ndarray:
"""
Prepare features for ONNX model input.
Args:
bar_data: Current bar data
historical_bars: List of historical bar data dictionaries
Returns:
Prepared feature array
"""
features = []
for bar in historical_bars[-self.lookback:]:
feature_row = []
# OHLC
feature_row.append(bar['open'])
feature_row.append(bar['high'])
feature_row.append(bar['low'])
feature_row.append(bar['close'])
# Volume (normalized)
feature_row.append(bar.get('tick_volume', 0) / 1000000.0)
# RSI (if available)
rsi = bar.get('rsi', 50.0)
feature_row.append(rsi / 100.0)
# EMA (if available)
ema = bar.get('ema', bar['close'])
feature_row.append((ema - bar['close']) / bar['close'])
# ATR (if available)
atr = bar.get('atr', 0.0)
feature_row.append(atr / bar['close'])
# Price change
if len(features) > 0:
prev_close = historical_bars[historical_bars.index(bar) - 1]['close']
price_change = (bar['close'] - prev_close) / prev_close
else:
price_change = 0.0
feature_row.append(price_change)
# High/Low ratio
feature_row.append(bar['high'] / bar['low'])
# Volume ratio (simplified)
if len(features) > 0:
prev_volume = historical_bars[historical_bars.index(bar) - 1].get('tick_volume', 1)
volume_ratio = bar.get('tick_volume', 1) / max(prev_volume, 1)
else:
volume_ratio = 1.0
feature_row.append(volume_ratio)
features.append(feature_row)
# Pad if needed
while len(features) < self.lookback:
features.insert(0, features[0] if features else [0.0] * 12)
features = np.array(features[-self.lookback:], dtype=np.float32)
# Normalize if scaler available
if self.scaler is not None:
# Reshape for scaler (flatten, scale, reshape)
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
features = (features - features.mean(axis=0)) / (features.std(axis=0) + 1e-8)
# Reshape for model: (1, lookback, features)
features = features.reshape(1, self.lookback, -1)
return features
def predict_price(self, bar_data: Dict[str, Any], historical_bars: list) -> float:
"""
Make price prediction using ONNX model.
Args:
bar_data: Current bar data
historical_bars: Historical bar data
Returns:
Predicted price
"""
# Prepare input
input_data = self.prepare_features(bar_data, historical_bars)
# Run model
outputs = self.session.run([self.output_name], {self.input_name: input_data})
prediction = outputs[0][0][0]
return float(prediction)
def on_bar(self, bar_data: Dict[str, Any]) -> None:
"""
Trading logic based on ONNX predictions.
"""
current_price = bar_data['close']
# We need historical bars for prediction
# For now, we'll use a simplified approach
# In a real implementation, you'd maintain a buffer of historical bars
# Check if we have a position
if self.position is not None:
# Check stop loss/take profit
self.check_stop_loss_take_profit(current_price)
return
# For backtesting, we need to get historical data
# This is a simplified version - in practice, you'd maintain a buffer
# For now, we'll skip prediction if we don't have enough data
# The backtest engine should provide historical context
# Simple prediction-based logic (simplified for backtesting)
# In production, use the full ONNX prediction pipeline
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
}