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
+121
View File
@@ -0,0 +1,121 @@
# MetaTrader 5 ONNX EA Setup Guide
## Quick Start
1. **Copy Model Files to MT5**
- Copy `models/XAUUSD_H1_model.onnx` to: `MT5_Data_Folder/MQL5/Files/models/`
- The EA will look for the model at: `models\XAUUSD_H1_model.onnx`
2. **Compile the EA**
- Open `ai/ONNX_EA.mq5` in MetaEditor
- Press F7 to compile
- Check for any errors
3. **Attach to Chart**
- Open XAUUSD H1 chart in MT5
- Drag `ONNX_EA.ex5` from Navigator to chart
- Configure parameters (see below)
## Model Information
- **Model Type**: LSTM Neural Network
- **Input**: 60 bars × 13 features
- **Output**: Price change percentage (e.g., -0.003 = -0.3% decrease)
- **Features**: OHLC, volume, RSI, EMA20, EMA50, ATR, price_change, high_low_ratio, volume_ma, volume_ratio
## EA Parameters
### ONNX Model Settings
- **InpModelPath**: `models\\XAUUSD_H1_model.onnx` (path relative to MQL5/Files/)
- **InpLookback**: `60` (must match training)
- **InpUsePrediction**: `true` (enable/disable predictions)
### Trading Settings
- **InpLotSize**: `0.01` (start small for testing)
- **InpMagicNumber**: `123456` (unique identifier)
- **InpSlippage**: `3` (points)
- **InpStopLoss**: `50` (pips)
- **InpTakeProfit**: `100` (pips)
### Prediction Settings
- **InpPredictionThreshold**: `0.00005` (0.005% as decimal, minimum change to trade)
- **InpUseConfidence**: `true` (enable confidence filter)
- **InpMinConfidence**: `0.1` (10% minimum confidence)
## Important Notes
### Feature Normalization
⚠️ **The EA uses simplified normalization that may not exactly match training.**
For best results:
1. The training script saves a scaler (`XAUUSD_H1_scaler.pkl`)
2. You should implement the same MinMaxScaler logic in MQL5
3. Or export scaler parameters (min/max) from Python and use in MQL5
Current implementation uses:
- OHLC: Raw values (should be normalized by scaler)
- Volume: Divided by 1,000,000
- RSI: Divided by 100
- EMAs/ATR: Normalized differences
- Price change: Percentage
- Volume MA: Divided by 1,000,000
### Prediction Format
The new model predicts **price change percentage** directly:
- Example: `-0.003` = price will decrease by 0.3%
- Old format (absolute price) is also supported for backward compatibility
### Testing Recommendations
1. **Start with Strategy Tester**
- Use Visual Mode to see predictions
- Check Expert tab for prediction logs
- Verify predictions make sense
2. **Monitor Logs**
- Check "Experts" tab for prediction values
- Verify confidence calculations
- Watch for any errors
3. **Adjust Parameters**
- If too many trades: Increase `InpPredictionThreshold` or `InpMinConfidence`
- If no trades: Decrease thresholds
- Adjust stop loss/take profit based on volatility
## Troubleshooting
### "Failed to load ONNX model"
- Check model path is correct
- Ensure model file exists in `MQL5/Files/models/`
- Check file permissions
### "Failed to prepare input data"
- Ensure enough historical data (need 60+ bars)
- Check indicator calculations
- Verify symbol is XAUUSD
### "Empty output from ONNX model"
- Check model input shape matches (1, 60, 13)
- Verify feature preparation matches training
- Check ONNX runtime version compatibility
### Predictions seem wrong
- Feature normalization may not match training
- Implement proper MinMaxScaler from training
- Check feature order matches training (13 features in correct order)
## Model Training Info
- **Training Date**: 2026-01-06
- **Training MAE**: 0.0013 (0.13%)
- **Validation MAE**: 0.0018 (0.18%)
- **Data Period**: Last 2 years
- **Timeframe**: H1
## Next Steps
1. Test in Strategy Tester first
2. Compare predictions with Python backtest
3. Adjust parameters based on results
4. Consider implementing proper scaler normalization
5. Test on demo account before live trading
+104 -38
View File
@@ -25,9 +25,9 @@ input int InpStopLoss = 50; // Stop Loss (pips)
input int InpTakeProfit = 100; // Take Profit (pips)
input group "Prediction Settings"
input double InpPredictionThreshold = 0.0001; // Min Prediction Change (0.01%)
input double InpPredictionThreshold = 0.00005; // Min Prediction Change (0.005% as decimal, e.g., 0.00005 = 0.005%)
input bool InpUseConfidence = true; // Use Confidence Filter
input double InpMinConfidence = 0.6; // Minimum Confidence
input double InpMinConfidence = 0.1; // Minimum Confidence (0.1 = 10%)
//--- Global variables
CTrade trade;
@@ -75,8 +75,8 @@ int OnInit()
}
// Get model info
int input_count = OnnxGetInputCount(onnx_handle);
int output_count = OnnxGetOutputCount(onnx_handle);
long input_count = OnnxGetInputCount(onnx_handle);
long output_count = OnnxGetOutputCount(onnx_handle);
Print("ONNX Model loaded successfully");
Print(" Inputs: ", input_count);
@@ -152,31 +152,58 @@ void OnTick()
return;
}
double predicted_price = output_data[0];
// Model now predicts price change percentage directly (e.g., -0.003 = -0.3%)
double predicted_change_pct = output_data[0];
double current_price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Calculate prediction change
double price_change = predicted_price - current_price;
double price_change_pct = (price_change / current_price) * 100.0;
// Check if prediction is percentage (between -1 and 1) or absolute price (old format)
double price_change_pct;
double predicted_price;
// Calculate confidence (simple heuristic based on prediction magnitude)
double confidence = MathAbs(price_change_pct) / 1.0; // Normalize
if(confidence > 1.0) confidence = 1.0;
if(MathAbs(predicted_change_pct) < 1.0)
{
// New format: percentage (e.g., -0.003 = -0.3%)
price_change_pct = predicted_change_pct * 100.0; // Convert to percentage
predicted_price = current_price * (1.0 + predicted_change_pct); // Calculate predicted price
}
else
{
// Old format: absolute price
predicted_price = predicted_change_pct;
double price_change = predicted_price - current_price;
price_change_pct = (price_change / current_price) * 100.0;
}
// Calculate confidence (for percentage predictions: 0.001 = 0.1% = 10% confidence)
double confidence;
if(MathAbs(price_change_pct) < 1.0)
{
// It's a decimal percentage (e.g., 0.001 = 0.1%)
confidence = MathMin(MathAbs(predicted_change_pct) / 0.01, 1.0); // 0.01 = 1% = 100% confidence
}
else
{
// It's already in percentage form
confidence = MathMin(MathAbs(price_change_pct) / 1.0, 1.0);
}
last_prediction = predicted_price;
last_confidence = confidence;
// Log prediction
Print("Prediction: Current=", current_price,
" Predicted=", predicted_price,
" Change=", price_change_pct, "%",
" Predicted Change=", price_change_pct, "%",
" Predicted Price=", predicted_price,
" Confidence=", confidence);
// Check if we should trade
if(!InpUseConfidence || confidence >= InpMinConfidence)
{
// Check if prediction is significant
if(MathAbs(price_change_pct) >= InpPredictionThreshold)
// price_change_pct is now in percentage (e.g., 0.1 = 0.1%), so compare with threshold * 100
// OR: if model outputs decimal (0.001), compare directly
double abs_change_decimal = MathAbs(predicted_change_pct); // Use raw prediction for threshold check
if(abs_change_decimal >= InpPredictionThreshold)
{
// Check existing position
if(PositionSelect(_Symbol))
@@ -187,11 +214,12 @@ void OnTick()
else
{
// Open new position based on prediction
if(price_change_pct > InpPredictionThreshold)
// Use raw prediction (decimal format) for threshold comparison
if(predicted_change_pct > InpPredictionThreshold)
{
OpenBuyPosition();
}
else if(price_change_pct < -InpPredictionThreshold)
else if(predicted_change_pct < -InpPredictionThreshold)
{
OpenSellPosition();
}
@@ -209,13 +237,14 @@ bool PrepareInputData(float &input_array[])
// This is a simplified version - you may need to adjust based on your model
int lookback = InpLookback;
int features = 12; // Adjust based on your model (OHLC + volume + indicators)
int features = 13; // OHLC(4) + volume(1) + RSI(1) + EMA20(1) + EMA50(1) + ATR(1) + price_change(1) + high_low_ratio(1) + volume_ma(1) + volume_ratio(1) = 13
ArrayResize(input_array, lookback * features);
ArrayInitialize(input_array, 0.0);
// Get historical data
double open[], high[], low[], close[], volume[];
double open[], high[], low[], close[];
long volume[]; // CopyTickVolume requires long[] not double[]
ArraySetAsSeries(open, true);
ArraySetAsSeries(high, true);
ArraySetAsSeries(low, true);
@@ -275,28 +304,65 @@ bool PrepareInputData(float &input_array[])
}
IndicatorRelease(atr_handle);
// Normalize and prepare data (simplified normalization)
// IMPORTANT: This uses simplified normalization. For best results, you should:
// 1. Save the scaler during training (train_onnx_model.py does this automatically)
// 2. Implement the same normalization logic in MQL5, OR
// 3. Pre-normalize data in Python and pass to MQL5 via files/global variables
// The current implementation may not match training exactly, which can affect accuracy
// Calculate volume MA for normalization
double volume_ma[];
ArraySetAsSeries(volume_ma, true);
ArrayResize(volume_ma, lookback);
ArrayInitialize(volume_ma, 0.0);
// Calculate volume MA (20-period rolling average)
for(int j = 0; j < lookback; j++)
{
double sum = 0.0;
int count = 0;
for(int k = j; k < j + 20 && k < ArraySize(volume); k++)
{
sum += (double)volume[k]; // Convert long to double
count++;
}
volume_ma[j] = count > 0 ? sum / count : (double)volume[j]; // Convert long to double
}
// Prepare features - MUST match Python training exactly (13 features)
// IMPORTANT: This uses simplified normalization. For best results, implement MinMaxScaler from training.
// The scaler is saved as models/XAUUSD_H1_scaler.pkl - you may need to export scaler parameters to MQL5
int idx = 0;
for(int i = 0; i < lookback; i++)
{
// Normalize features (simplified - use proper scaler in production)
input_array[idx++] = (float)((open[i] - close[lookback-1]) / close[lookback-1]);
input_array[idx++] = (float)((high[i] - close[lookback-1]) / close[lookback-1]);
input_array[idx++] = (float)((low[i] - close[lookback-1]) / close[lookback-1]);
input_array[idx++] = (float)((close[i] - close[lookback-1]) / close[lookback-1]);
input_array[idx++] = (float)(volume[i] / 1000000.0); // Normalize volume
input_array[idx++] = (float)(rsi[i] / 100.0); // Normalize RSI
input_array[idx++] = (float)((ema20[i] - close[lookback-1]) / close[lookback-1]);
input_array[idx++] = (float)((ema50[i] - close[lookback-1]) / close[lookback-1]);
input_array[idx++] = (float)(atr[i] / close[lookback-1]);
input_array[idx++] = (float)((close[i] - close[i+1]) / close[i+1]); // Price change
input_array[idx++] = (float)(high[i] / low[i]); // High/low ratio
input_array[idx++] = (float)(volume[i] / (volume[i] + volume[i+1] + volume[i+2]) / 3.0); // Volume ratio
// Feature 1-4: OHLC (raw values, will be normalized by scaler)
input_array[idx++] = (float)open[i];
input_array[idx++] = (float)high[i];
input_array[idx++] = (float)low[i];
input_array[idx++] = (float)close[i];
// Feature 5: Volume (normalized by 1,000,000)
input_array[idx++] = (float)((double)volume[i] / 1000000.0); // Convert long to double first
// Feature 6: RSI (normalized by 100)
input_array[idx++] = (float)(rsi[i] / 100.0);
// Feature 7: EMA20 normalized difference
input_array[idx++] = (float)((ema20[i] - close[i]) / close[i]);
// Feature 8: EMA50 normalized difference
input_array[idx++] = (float)((ema50[i] - close[i]) / close[i]);
// Feature 9: ATR normalized
input_array[idx++] = (float)(atr[i] / close[i]);
// Feature 10: Price change (percentage)
double price_change = i > 0 ? (close[i] - close[i+1]) / close[i+1] : 0.0;
input_array[idx++] = (float)price_change;
// Feature 11: High/Low ratio
input_array[idx++] = (float)(high[i] / low[i]);
// Feature 12: Volume MA (normalized by 1,000,000)
input_array[idx++] = (float)(volume_ma[i] / 1000000.0);
// Feature 13: Volume ratio
double vol_ratio = volume_ma[i] > 0 ? (double)volume[i] / volume_ma[i] : 1.0; // Convert long to double
input_array[idx++] = (float)vol_ratio;
}
// Reshape for model: (1, lookback, features)
@@ -319,7 +385,7 @@ bool RunONNXModel(float &input_data[], float &output_data[])
return false;
// Set input shape
long input_shape[] = {1, InpLookback, 12}; // Adjust based on your model
long input_shape[] = {1, InpLookback, 13}; // 13 features
if(!OnnxSetInputShape(onnx_handle, 0, input_shape))
{
Print("ERROR: Failed to set input shape. Error: ", GetLastError());
View File
+94
View File
@@ -0,0 +1,94 @@
# XAUUSD ONNX Model Training and Backtesting Summary
## Status: ✅ Model Trained, ⚠️ Predictions Need Investigation
### Completed
1.**Model Training**: Successfully trained XAUUSD H1 ONNX model
- Model: `models/XAUUSD_H1_model.onnx`
- Scaler: `models/XAUUSD_H1_scaler.pkl`
- Training Loss: 177939.59, MAE: 349.43
- Validation Loss: 1749893.25, MAE: 1280.39
2.**Backtest Framework**: Working correctly
- Processes 2888 bars successfully
- No errors in execution
3.**Parameter Optimization Tools**: Created
- Grid search and random search support
- Can test multiple parameter combinations
### Issue Identified
⚠️ **Model Predictions Are Unrealistic**
- Model predicts prices around **2669** when current price is **3800+**
- This suggests a **-31% price change**, which is unrealistic
- All parameter combinations result in **0 trades**
### Possible Causes
1. **Model Training Issue**:
- High validation MAE (1280) suggests model may not be learning well
- Model might be predicting from wrong data range
2. **Feature Mismatch**:
- Features used in backtesting might not match training features exactly
- Normalization might be inconsistent
3. **Model Architecture**:
- LSTM might need more training or different architecture
- Current model might be underfitting
### Recommendations
#### Immediate Actions
1. **Check Model Predictions**:
```bash
python inspect_predictions.py
```
This shows actual prediction values and statistics
2. **Retrain with Better Settings**:
- Increase training epochs (try 50-100)
- Use more recent data
- Consider predicting price changes instead of absolute prices
- Add more regularization to prevent overfitting
3. **Alternative Approach**:
- Train model to predict **price change percentage** instead of absolute price
- This would be more stable and easier to interpret
#### Next Steps
1. Investigate why predictions are so far off
2. Consider retraining with:
- Price change prediction instead of absolute price
- Better feature engineering
- More training data
- Different model architecture
### Files Created
- `ai/train_onnx_model.py` - Model training script
- `ai/quick_backtest.py` - Quick backtest script
- `ai/optimize_onnx_params.py` - Parameter optimization
- `ai/debug_onnx_predictions.py` - Debug predictions
- `ai/inspect_predictions.py` - Detailed prediction inspection
- `ai/test_very_low_threshold.py` - Test with very low thresholds
- `backtesting/MT5/onnx_backtest_strategy.py` - ONNX strategy class
- `backtesting/MT5/indicator_utils.py` - Indicator calculation utilities
### Usage
```bash
# Quick backtest
cd ai
python quick_backtest.py
# Optimize parameters
python optimize_onnx_params.py 2 30
# Inspect predictions
python inspect_predictions.py
```
### Model Details
- **Symbol**: XAUUSD
- **Timeframe**: H1
- **Lookback**: 60 bars
- **Features**: 13 (OHLC + volume + RSI + EMA20 + EMA50 + ATR + price_change + high_low_ratio + volume_ma + volume_ratio)
- **Architecture**: LSTM(128) → LSTM(64) → LSTM(32) → Dense(16) → Dense(1)
+112
View File
@@ -0,0 +1,112 @@
# XAUUSD ONNX Model Training and Backtesting Guide
This guide will help you train an ONNX model for XAUUSD and backtest it.
## Step 1: Install Dependencies
Make sure you have all required packages installed:
```bash
cd ai
pip install -r requirements.txt
```
If you encounter issues, install individually:
```bash
pip install tensorflow onnx onnxruntime tf2onnx scikit-learn pandas numpy MetaTrader5
```
## Step 2: Train the Model
Train an ONNX model for XAUUSD:
```bash
python train_onnx_model.py --symbol XAUUSD --timeframe H1 --lookback 60 --epochs 30
```
**Parameters:**
- `--symbol XAUUSD`: Trading symbol (Gold)
- `--timeframe H1`: 1-hour timeframe
- `--lookback 60`: Use 60 bars for prediction
- `--epochs 30`: Training epochs (adjust based on your needs)
**Expected Output:**
- Model: `models/XAUUSD_H1_model.onnx`
- Scaler: `models/XAUUSD_H1_scaler.pkl`
**Training Time:** 5-15 minutes depending on your hardware and data availability.
## Step 3: Run Backtest
After training, backtest the model:
```bash
python run_xauusd_backtest.py
```
Or use the combined script:
```bash
python train_and_backtest_xauusd.py
```
## Step 4: Review Results
The backtest will generate:
- Performance summary in console
- Equity curve chart
- Drawdown chart
- Monthly returns chart
- Trades CSV file
All files are saved in `onnx_xauusd_backtest/` directory.
## Model Configuration
The trained model uses:
- **Input**: 60 bars × 12 features
- **Features**: OHLC, volume, RSI, EMA, ATR, price changes, ratios
- **Output**: Predicted next close price
- **Architecture**: LSTM(128) → LSTM(64) → LSTM(32) → Dense layers
## Backtest Strategy Parameters
Default backtest parameters:
- **Prediction Threshold**: 0.01% (minimum price change to trade)
- **Min Confidence**: 30%
- **Lot Size**: 0.1
- **Stop Loss**: 50 pips
- **Take Profit**: 100 pips
You can adjust these in `run_xauusd_backtest.py`.
## Troubleshooting
### "Model not found"
- Make sure you've trained the model first
- Check that the model file exists in `models/` directory
### "MT5 initialization failed"
- Ensure MetaTrader 5 is running
- Log into your account
- Check that XAUUSD symbol is available
### "Insufficient data"
- Make sure you have historical data downloaded in MT5
- Check the date range in the backtest script
- Verify symbol name is correct
## Next Steps
After successful backtesting:
1. Review performance metrics
2. Optimize prediction threshold and confidence levels
3. Adjust stop loss/take profit if needed
4. Test on demo account before live trading
5. Consider using the model in `ONNX_EA.mq5` for live trading
## Files Created
- `models/XAUUSD_H1_model.onnx`: Trained ONNX model
- `models/XAUUSD_H1_scaler.pkl`: Feature scaler for normalization
- `onnx_xauusd_backtest/`: Backtest results directory
+176
View File
@@ -0,0 +1,176 @@
"""
Debug ONNX Model Predictions
This script helps debug why the model isn't generating trades.
It shows actual predictions and checks if they meet trading criteria.
"""
import os
import sys
from datetime import datetime, timedelta
import MetaTrader5 as mt5
import numpy as np
import pandas as pd
import onnxruntime as ort
import pickle
# Add paths
current_dir = os.path.dirname(os.path.abspath(__file__))
backtest_dir = os.path.join(os.path.dirname(current_dir), 'backtesting', 'MT5')
sys.path.insert(0, backtest_dir)
from backtest_engine import BacktestEngine
from onnx_backtest_strategy import ONNXBacktestStrategy
def main():
"""Debug ONNX predictions."""
print("="*60)
print("ONNX Model Prediction Debug")
print("="*60)
# Configuration
symbol = 'XAUUSD'
timeframe = mt5.TIMEFRAME_H1
model_path = 'models/XAUUSD_H1_model.onnx'
scaler_path = 'models/XAUUSD_H1_scaler.pkl'
if not os.path.exists(model_path):
print(f"ERROR: Model not found: {model_path}")
return
# Initialize MT5
if not mt5.initialize():
print("ERROR: Failed to initialize MT5")
return
try:
# Load model and scaler
session = ort.InferenceSession(model_path)
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
with open(scaler_path, 'rb') as f:
scaler = pickle.load(f)
# Get recent data
end_date = datetime.now()
start_date = end_date - timedelta(days=10)
rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date)
if rates is None or len(rates) == 0:
print("ERROR: No data available")
return
df = pd.DataFrame(rates)
df['time'] = pd.to_datetime(df['time'], unit='s')
print(f"\nLoaded {len(df)} bars")
print(f"Date range: {df['time'].min()} to {df['time'].max()}")
# Create strategy to get features
strategy = ONNXBacktestStrategy(
symbol=symbol,
timeframe=timeframe,
model_path=model_path,
scaler_path=scaler_path,
initial_balance=10000.0,
prediction_threshold=0.0001,
min_confidence=0.3,
lot_size=0.1,
stop_loss_pips=50,
take_profit_pips=100
)
# Simulate a few bars
print("\n" + "="*60)
print("Predictions Analysis")
print("="*60)
predictions_data = []
for i in range(60, min(100, len(df))): # Start from bar 60 (need lookback)
bar = df.iloc[i]
bar_time = bar['time'] if isinstance(bar['time'], datetime) else datetime.fromtimestamp(bar['time'])
current_price = bar['close']
# Build historical buffer
bar_data = {
'time': bar_time,
'open': float(bar['open']),
'high': float(bar['high']),
'low': float(bar['low']),
'close': float(bar['close']),
'tick_volume': int(bar['tick_volume']),
'rsi': 50.0, # Simplified
'ema': current_price, # Simplified
'atr': 0.0 # Simplified
}
strategy.historical_bars.append(bar_data)
if len(strategy.historical_bars) >= strategy.lookback:
# Get prediction
features = strategy.prepare_features()
if features is not None:
input_data = features.astype(np.float32)
outputs = session.run([output_name], {input_name: input_data})
predicted_price = float(outputs[0][0][0])
# Calculate metrics
price_change = predicted_price - current_price
price_change_pct = (price_change / current_price) if current_price > 0 else 0.0
confidence = min(abs(price_change_pct) / 0.01, 1.0)
predictions_data.append({
'time': bar_time,
'current_price': current_price,
'predicted_price': predicted_price,
'price_change': price_change,
'price_change_pct': price_change_pct * 100,
'confidence': confidence,
'meets_threshold': abs(price_change_pct) >= 0.0001,
'meets_confidence': confidence >= 0.3,
'would_trade': abs(price_change_pct) >= 0.0001 and confidence >= 0.3
})
# Display results
if predictions_data:
pred_df = pd.DataFrame(predictions_data)
print(f"\nAnalyzed {len(pred_df)} predictions")
print(f"\nPrediction Statistics:")
print(f" Mean price change: {pred_df['price_change_pct'].mean():.4f}%")
print(f" Std price change: {pred_df['price_change_pct'].std():.4f}%")
print(f" Min price change: {pred_df['price_change_pct'].min():.4f}%")
print(f" Max price change: {pred_df['price_change_pct'].max():.4f}%")
print(f"\n Mean confidence: {pred_df['confidence'].mean():.4f}")
print(f" Predictions meeting threshold: {pred_df['meets_threshold'].sum()}/{len(pred_df)}")
print(f" Predictions meeting confidence: {pred_df['meets_confidence'].sum()}/{len(pred_df)}")
print(f" Predictions that would trade: {pred_df['would_trade'].sum()}/{len(pred_df)}")
print(f"\nSample predictions (first 10):")
print(pred_df[['time', 'current_price', 'predicted_price', 'price_change_pct', 'confidence', 'would_trade']].head(10).to_string(index=False))
if pred_df['would_trade'].sum() == 0:
print("\n" + "="*60)
print("RECOMMENDATIONS:")
print("="*60)
print("No trades would be generated. Try:")
print(f" 1. Lower prediction_threshold (current: 0.0001)")
print(f" Suggested: {pred_df['price_change_pct'].abs().quantile(0.1):.6f}")
print(f" 2. Lower min_confidence (current: 0.3)")
print(f" Suggested: {pred_df['confidence'].quantile(0.1):.2f}")
print(f" 3. Check if model predictions are reasonable")
else:
print("No predictions generated (need more historical data)")
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()
finally:
mt5.shutdown()
if __name__ == '__main__':
main()
+219
View File
@@ -0,0 +1,219 @@
"""
Debug ONNX Strategy - Find out why no trades are generated
"""
import os
import sys
from datetime import datetime, timedelta
import MetaTrader5 as mt5
# Add paths
current_dir = os.path.dirname(os.path.abspath(__file__))
backtest_dir = os.path.join(os.path.dirname(current_dir), 'backtesting', 'MT5')
sys.path.insert(0, backtest_dir)
from backtest_engine import BacktestEngine
from onnx_backtest_strategy import ONNXBacktestStrategy
def main():
"""Debug strategy to find why no trades."""
print("="*60)
print("Debugging ONNX Strategy - Why No Trades?")
print("="*60)
symbol = 'XAUUSD'
timeframe = mt5.TIMEFRAME_H1
model_path = 'models/XAUUSD_H1_model.onnx'
scaler_path = 'models/XAUUSD_H1_scaler.pkl'
initial_balance = 10000.0
if not os.path.exists(model_path):
print(f"ERROR: Model not found: {model_path}")
return
end_date = datetime.now()
start_date = end_date - timedelta(days=30) # Shorter period for debugging
print(f"\nModel: {model_path}")
print(f"Date Range: {start_date.date()} to {end_date.date()}")
print(f"Parameters:")
print(f" Prediction Threshold: 0.00005 (0.005%)")
print(f" Min Confidence: 0.1 (10%)")
print("\n")
if not mt5.initialize():
print("ERROR: Failed to initialize MT5")
return
try:
# Create strategy with debug enabled
strategy = ONNXBacktestStrategy(
symbol=symbol,
timeframe=timeframe,
model_path=model_path,
scaler_path=scaler_path,
initial_balance=initial_balance,
prediction_threshold=0.00005,
min_confidence=0.1,
lot_size=0.1,
stop_loss_pips=50,
take_profit_pips=100
)
# Override on_bar to add detailed debugging
original_on_bar = strategy.on_bar
def debug_on_bar(bar_data):
"""Debug version of on_bar."""
# Add current bar to historical buffer
strategy.historical_bars.append(bar_data.copy())
# Keep only necessary history
if len(strategy.historical_bars) > strategy.lookback + 50:
strategy.historical_bars = strategy.historical_bars[-(strategy.lookback + 50):]
# Check if we have enough data
if len(strategy.historical_bars) < strategy.lookback:
if len(strategy.historical_bars) % 20 == 0:
print(f" [Bar {len(strategy.historical_bars)}] Not enough data yet (need {strategy.lookback})")
return
current_price = bar_data['close']
# Check existing position
if strategy.position is not None:
strategy.check_stop_loss_take_profit(current_price)
return
# Make prediction
try:
predicted_change_pct = strategy.predict_price()
if predicted_change_pct is None:
if len(strategy.historical_bars) % 10 == 0:
print(f" [Bar {len(strategy.historical_bars)}] Prediction returned None - checking why...")
# Try to debug why prediction is None
features = strategy.prepare_features()
if features is None:
print(f" -> Features preparation returned None")
else:
print(f" -> Features shape: {features.shape}")
return
except Exception as e:
print(f" [Bar {len(strategy.historical_bars)}] Prediction exception: {e}")
import traceback
traceback.print_exc()
return
# Process prediction
if abs(predicted_change_pct) < 1.0:
price_change_pct = predicted_change_pct
else:
predicted_price = predicted_change_pct
if predicted_price <= 0 or predicted_price > 10000:
if len(strategy.historical_bars) % 50 == 0:
print(f" [Bar {len(strategy.historical_bars)}] Invalid prediction: {predicted_price}")
return
price_change = predicted_price - current_price
price_change_pct = (price_change / current_price) if current_price > 0 else 0.0
# Calculate confidence
if abs(price_change_pct) < 1.0:
confidence = min(abs(price_change_pct) / 0.01, 1.0)
else:
confidence = min(abs(price_change_pct) / 1.0, 1.0)
# Debug output for every 10th bar
if len(strategy.historical_bars) % 10 == 0:
print(f"\n [Bar {len(strategy.historical_bars)}]")
print(f" Current Price: {current_price:.2f}")
print(f" Raw Prediction: {predicted_change_pct:.6f}")
print(f" Price Change %: {price_change_pct*100:.4f}%")
print(f" Abs Change: {abs(price_change_pct):.6f}")
print(f" Threshold: {strategy.prediction_threshold:.6f}")
print(f" Confidence: {confidence:.3f}")
print(f" Min Confidence: {strategy.min_confidence:.2f}")
print(f" Threshold Check: {abs(price_change_pct) >= strategy.prediction_threshold} (need True)")
print(f" Confidence Check: {confidence >= strategy.min_confidence} (need True)")
if abs(price_change_pct) >= strategy.prediction_threshold and confidence >= strategy.min_confidence:
print(f" -> WOULD TRADE! Direction: {'BUY' if price_change_pct > 0 else 'SELL'}")
else:
if abs(price_change_pct) < strategy.prediction_threshold:
print(f" -> BLOCKED: Abs change {abs(price_change_pct):.6f} < threshold {strategy.prediction_threshold:.6f}")
if confidence < strategy.min_confidence:
print(f" -> BLOCKED: Confidence {confidence:.3f} < min {strategy.min_confidence:.2f}")
# Check if we should trade
if confidence < strategy.min_confidence:
return
if abs(price_change_pct) < strategy.prediction_threshold:
return
# Open position based on prediction
if price_change_pct > strategy.prediction_threshold:
# Bullish prediction
sl = current_price - (strategy.stop_loss_pips / 10000) if strategy.stop_loss_pips > 0 else None
tp = current_price + (strategy.take_profit_pips / 10000) if strategy.take_profit_pips > 0 else None
print(f"\n *** ATTEMPTING BUY POSITION at bar {len(strategy.historical_bars)} ***")
print(f" Price: {current_price:.2f}, Predicted Change: {price_change_pct*100:.4f}%")
print(f" SL: {sl:.2f}, TP: {tp:.2f}, Volume: {strategy.lot_size}")
print(f" Equity: {strategy.equity:.2f}, Current Position: {strategy.position}")
# Check margin requirement manually
contract_size = 100000
margin_required = strategy.lot_size * contract_size * current_price * 0.01
print(f" Margin Required: {margin_required:.2f}, Available: {strategy.equity * 0.9:.2f}")
result = strategy.open_position('BUY', strategy.lot_size, current_price, sl, tp, 'ONNX Buy')
print(f" Open Position Result: {result}")
if result:
print(f" -> Position opened! New position: {strategy.position}")
else:
if strategy.position is not None:
print(f" -> Position NOT opened! Reason: Already have position")
else:
print(f" -> Position NOT opened! Reason: Margin insufficient or other validation failed")
elif price_change_pct < -strategy.prediction_threshold:
# Bearish prediction
sl = current_price + (strategy.stop_loss_pips / 10000) if strategy.stop_loss_pips > 0 else None
tp = current_price - (strategy.take_profit_pips / 10000) if strategy.take_profit_pips > 0 else None
print(f"\n *** ATTEMPTING SELL POSITION at bar {len(strategy.historical_bars)} ***")
print(f" Price: {current_price:.2f}, Predicted Change: {price_change_pct*100:.4f}%")
print(f" SL: {sl:.2f}, TP: {tp:.2f}, Volume: {strategy.lot_size}")
print(f" Equity: {strategy.equity:.2f}, Current Position: {strategy.position}")
# Check margin requirement manually
contract_size = 100000
margin_required = strategy.lot_size * contract_size * current_price * 0.01
print(f" Margin Required: {margin_required:.2f}, Available: {strategy.equity * 0.9:.2f}")
result = strategy.open_position('SELL', strategy.lot_size, current_price, sl, tp, 'ONNX Sell')
print(f" Open Position Result: {result}")
if result:
print(f" -> Position opened! New position: {strategy.position}")
else:
if strategy.position is not None:
print(f" -> Position NOT opened! Reason: Already have position")
else:
print(f" -> Position NOT opened! Reason: Margin insufficient or other validation failed")
strategy.on_bar = debug_on_bar
print("Running backtest with detailed debugging...\n")
engine = BacktestEngine(strategy, start_date, end_date)
results = engine.run()
print("\n" + "="*60)
print("Backtest Complete")
print("="*60)
print(f"Total Trades: {len(strategy.closed_trades)}")
print(f"Open Positions: {1 if strategy.position else 0}")
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()
finally:
mt5.shutdown()
if __name__ == '__main__':
main()
+212
View File
@@ -0,0 +1,212 @@
"""
Inspect ONNX Model Predictions in Detail
This script directly tests the model and shows what it's predicting.
"""
import os
import sys
from datetime import datetime, timedelta
import MetaTrader5 as mt5
import numpy as np
import pandas as pd
import onnxruntime as ort
import pickle
# Add paths
current_dir = os.path.dirname(os.path.abspath(__file__))
backtest_dir = os.path.join(os.path.dirname(current_dir), 'backtesting', 'MT5')
sys.path.insert(0, backtest_dir)
from indicator_utils import calculate_rsi, calculate_ema, calculate_atr
def main():
"""Inspect model predictions."""
print("="*60)
print("ONNX Model Prediction Inspection")
print("="*60)
model_path = 'models/XAUUSD_H1_model.onnx'
scaler_path = 'models/XAUUSD_H1_scaler.pkl'
if not os.path.exists(model_path):
print(f"ERROR: Model not found: {model_path}")
return
# Load model
session = ort.InferenceSession(model_path)
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
input_shape = session.get_inputs()[0].shape
lookback = int(input_shape[1]) if input_shape[1] else 60
print(f"Model Input Shape: {input_shape}")
print(f"Lookback: {lookback}")
# Load scaler
with open(scaler_path, 'rb') as f:
scaler = pickle.load(f)
print(f"Scaler Feature Count: {scaler.n_features_in_}")
# Initialize MT5
if not mt5.initialize():
print("ERROR: Failed to initialize MT5")
return
try:
# Get data
symbol = 'XAUUSD'
timeframe = mt5.TIMEFRAME_H1
end_date = datetime.now()
start_date = end_date - timedelta(days=100) # Get more data
rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date)
if rates is None or len(rates) == 0:
print("ERROR: No data")
return
df = pd.DataFrame(rates)
df['time'] = pd.to_datetime(df['time'], unit='s')
df.set_index('time', inplace=True)
print(f"\nLoaded {len(df)} bars")
# Calculate indicators (same as training)
df['rsi'] = calculate_rsi(df['close'], period=14)
df['ema_20'] = calculate_ema(df['close'], period=20)
df['ema_50'] = calculate_ema(df['close'], period=50)
df['atr'] = calculate_atr(df, period=14)
df['price_change'] = df['close'].pct_change()
df['high_low_ratio'] = df['high'] / df['low']
df['volume_ma'] = df['tick_volume'].rolling(window=20).mean()
df['volume_ratio'] = df['tick_volume'] / df['volume_ma']
# Drop NaN
df = df.dropna()
print(f"After indicator calculation: {len(df)} bars")
print(f"Features: {len(['open', 'high', 'low', 'close', 'tick_volume', 'rsi', 'ema_20', 'ema_50', 'atr', 'price_change', 'high_low_ratio', 'volume_ma', 'volume_ratio'])}")
# Test predictions
print("\n" + "="*60)
print("Testing Predictions")
print("="*60)
predictions = []
for i in range(lookback, min(lookback + 50, len(df))):
# Prepare features (same as training)
feature_rows = []
for j in range(i - lookback, i):
bar = df.iloc[j]
feature_row = [
bar['open'],
bar['high'],
bar['low'],
bar['close'],
bar['tick_volume'] / 1000000.0,
bar['rsi'] / 100.0,
(bar['ema_20'] - bar['close']) / bar['close'] if bar['close'] > 0 else 0.0,
(bar['ema_50'] - bar['close']) / bar['close'] if bar['close'] > 0 else 0.0,
bar['atr'] / bar['close'] if bar['close'] > 0 else 0.0,
bar['price_change'],
bar['high_low_ratio'],
bar['volume_ma'] / 1000000.0,
bar['volume_ratio']
]
feature_rows.append(feature_row)
features = np.array(feature_rows, dtype=np.float32)
# Scale
original_shape = features.shape
features_flat = features.reshape(-1, features.shape[-1])
features_scaled = scaler.transform(features_flat)
features = features_scaled.reshape(original_shape)
# Reshape for model
input_data = features.reshape(1, lookback, -1)
# Predict
outputs = session.run([output_name], {input_name: input_data})
predicted_change_pct = float(outputs[0][0][0])
current_price = df.iloc[i]['close']
# Model predicts price change percentage directly
# If it's between -1 and 1, it's already a percentage
if abs(predicted_change_pct) < 1.0:
price_change_pct = predicted_change_pct * 100 # Convert to percentage (e.g., 0.001 -> 0.1%)
predicted_price = current_price * (1 + predicted_change_pct / 100) # Calculate predicted price
price_change = predicted_price - current_price
else:
# Old format: absolute price
predicted_price = predicted_change_pct
price_change = predicted_price - current_price
price_change_pct = (price_change / current_price) * 100 if current_price > 0 else 0.0
predictions.append({
'time': df.index[i],
'current_price': current_price,
'predicted_price': predicted_price,
'price_change': price_change,
'price_change_pct': price_change_pct,
'abs_change_pct': abs(price_change_pct)
})
if predictions:
pred_df = pd.DataFrame(predictions)
print(f"\nAnalyzed {len(pred_df)} predictions:")
print(f"\nPrice Change Statistics:")
print(f" Mean: {pred_df['price_change_pct'].mean():.6f}%")
print(f" Std: {pred_df['price_change_pct'].std():.6f}%")
print(f" Min: {pred_df['price_change_pct'].min():.6f}%")
print(f" Max: {pred_df['price_change_pct'].max():.6f}%")
print(f" Median: {pred_df['price_change_pct'].median():.6f}%")
print(f"\nAbsolute Price Change Statistics:")
print(f" Mean: {pred_df['abs_change_pct'].mean():.6f}%")
print(f" Min: {pred_df['abs_change_pct'].min():.6f}%")
print(f" Max: {pred_df['abs_change_pct'].max():.6f}%")
print(f" Median: {pred_df['abs_change_pct'].median():.6f}%")
print(f"\nSample Predictions (first 10):")
print(pred_df[['time', 'current_price', 'predicted_price', 'price_change_pct']].head(10).to_string(index=False))
# Check thresholds
threshold_0001 = (pred_df['abs_change_pct'] >= 0.01).sum()
threshold_00005 = (pred_df['abs_change_pct'] >= 0.005).sum()
threshold_00001 = (pred_df['abs_change_pct'] >= 0.001).sum()
print(f"\nPredictions meeting thresholds:")
print(f" >= 0.01% (0.0001): {threshold_0001}/{len(pred_df)}")
print(f" >= 0.005% (0.00005): {threshold_00005}/{len(pred_df)}")
print(f" >= 0.001% (0.00001): {threshold_00001}/{len(pred_df)}")
if threshold_00001 == 0:
print("\n" + "="*60)
print("ISSUE DETECTED!")
print("="*60)
print("Even with 0.001% threshold, no predictions qualify.")
print("The model may be predicting prices that are too close to current prices.")
print("\nPossible solutions:")
print(" 1. Retrain model to predict price changes instead of absolute prices")
print(" 2. Use a different prediction target (e.g., next bar high/low)")
print(" 3. Adjust the model architecture")
else:
print("No predictions generated")
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()
finally:
mt5.shutdown()
if __name__ == '__main__':
main()
@@ -0,0 +1,142 @@
[
{
"prediction_threshold": 0.0007803532317523569,
"min_confidence": 0.4778214378844623,
"stop_loss_pips": 126,
"take_profit_pips": 111,
"lot_size": 0.19966462104925914,
"total_return": 0.0,
"max_drawdown": 0.0,
"sharpe_ratio": 0.0,
"profit_factor": 0.0,
"win_rate": 0.0,
"total_trades": 0,
"final_balance": 10000.0
},
{
"prediction_threshold": 0.00035423634886275126,
"min_confidence": 0.1201975341512912,
"stop_loss_pips": 94,
"take_profit_pips": 127,
"lot_size": 0.13342715278475548,
"total_return": -6.90000000000009,
"max_drawdown": 6.90000000000009,
"sharpe_ratio": 0.0,
"profit_factor": 0.0,
"win_rate": 0.0,
"total_trades": 1,
"final_balance": -59000.0000000009
},
{
"prediction_threshold": 0.001265431347313738,
"min_confidence": 0.19890411118369217,
"stop_loss_pips": 67,
"take_profit_pips": 101,
"lot_size": 0.13129583050668675,
"total_return": -6.90000000000009,
"max_drawdown": 6.90000000000009,
"sharpe_ratio": 0.0,
"profit_factor": 0.0,
"win_rate": 0.0,
"total_trades": 1,
"final_balance": -59000.0000000009
},
{
"prediction_threshold": 0.0012316219508229722,
"min_confidence": 0.46683539533100704,
"stop_loss_pips": 60,
"take_profit_pips": 67,
"lot_size": 0.2657758564688984,
"total_return": 0.0,
"max_drawdown": 0.0,
"sharpe_ratio": 0.0,
"profit_factor": 0.0,
"win_rate": 0.0,
"total_trades": 0,
"final_balance": 10000.0
},
{
"prediction_threshold": 6.0768128391024685e-05,
"min_confidence": 0.41695764280467534,
"stop_loss_pips": 100,
"take_profit_pips": 202,
"lot_size": 0.2475438851328014,
"total_return": 0.0,
"max_drawdown": 0.0,
"sharpe_ratio": 0.0,
"profit_factor": 0.0,
"win_rate": 0.0,
"total_trades": 0,
"final_balance": 10000.0
},
{
"prediction_threshold": 0.0001953737551755531,
"min_confidence": 0.49409912147023277,
"stop_loss_pips": 107,
"take_profit_pips": 168,
"lot_size": 0.0996789203835431,
"total_return": 0.0,
"max_drawdown": 0.0,
"sharpe_ratio": 0.0,
"profit_factor": 0.0,
"win_rate": 0.0,
"total_trades": 0,
"final_balance": 10000.0
},
{
"prediction_threshold": 0.0019322478491650692,
"min_confidence": 0.3231654114590081,
"stop_loss_pips": 60,
"take_profit_pips": 196,
"lot_size": 0.2505492451885099,
"total_return": -6.90000000000009,
"max_drawdown": 6.90000000000009,
"sharpe_ratio": 0.0,
"profit_factor": 0.0,
"win_rate": 0.0,
"total_trades": 1,
"final_balance": -59000.0000000009
},
{
"prediction_threshold": 0.0019242854474812309,
"min_confidence": 0.4300402319051681,
"stop_loss_pips": 101,
"take_profit_pips": 92,
"lot_size": 0.19668779141596204,
"total_return": 0.0,
"max_drawdown": 0.0,
"sharpe_ratio": 0.0,
"profit_factor": 0.0,
"win_rate": 0.0,
"total_trades": 0,
"final_balance": 10000.0
},
{
"prediction_threshold": 0.0018569847882978986,
"min_confidence": 0.3772723981353894,
"stop_loss_pips": 34,
"take_profit_pips": 254,
"lot_size": 0.18020856500645593,
"total_return": -2.349999999999909,
"max_drawdown": 2.349999999999909,
"sharpe_ratio": 0.0,
"profit_factor": 0.0,
"win_rate": 0.0,
"total_trades": 1,
"final_balance": -13499.99999999909
},
{
"prediction_threshold": 0.00011106092028833925,
"min_confidence": 0.42902814856774935,
"stop_loss_pips": 63,
"take_profit_pips": 201,
"lot_size": 0.1487875590004536,
"total_return": 0.0,
"max_drawdown": 0.0,
"sharpe_ratio": 0.0,
"profit_factor": 0.0,
"win_rate": 0.0,
"total_trades": 0,
"final_balance": 10000.0
}
]
+359
View File
@@ -0,0 +1,359 @@
"""
Parameter Optimization for ONNX Strategy
This script optimizes strategy parameters (prediction_threshold, min_confidence,
stop_loss_pips, take_profit_pips, lot_size) using grid search or random search.
"""
import os
import sys
from datetime import datetime, timedelta
import MetaTrader5 as mt5
import pandas as pd
import numpy as np
from itertools import product
import json
# Add paths
current_dir = os.path.dirname(os.path.abspath(__file__))
backtest_dir = os.path.join(os.path.dirname(current_dir), 'backtesting', 'MT5')
sys.path.insert(0, backtest_dir)
from backtest_engine import BacktestEngine
from onnx_backtest_strategy import ONNXBacktestStrategy
from performance_analyzer import PerformanceAnalyzer
class ONNXParameterOptimizer:
"""Optimize ONNX strategy parameters."""
def __init__(self, symbol: str, timeframe: int, model_path: str, scaler_path: str,
start_date: datetime, end_date: datetime, initial_balance: float = 10000.0):
"""
Initialize optimizer.
Args:
symbol: Trading symbol
timeframe: MT5 timeframe
model_path: Path to ONNX model
scaler_path: Path to scaler
start_date: Backtest start date
end_date: Backtest end date
initial_balance: Starting balance
"""
self.symbol = symbol
self.timeframe = timeframe
self.model_path = model_path
self.scaler_path = scaler_path
self.start_date = start_date
self.end_date = end_date
self.initial_balance = initial_balance
def grid_search(self, param_grid: dict, metric: str = 'sharpe_ratio') -> pd.DataFrame:
"""
Perform grid search optimization.
Args:
param_grid: Dictionary of parameter ranges
Example: {
'prediction_threshold': [0.0001, 0.0002, 0.0005],
'min_confidence': [0.2, 0.3, 0.4],
'stop_loss_pips': [30, 50, 70],
'take_profit_pips': [60, 100, 150],
'lot_size': [0.1, 0.2]
}
metric: Metric to optimize ('sharpe_ratio', 'total_return', 'max_drawdown', 'profit_factor')
Returns:
DataFrame with results sorted by metric
"""
print("="*60)
print("Grid Search Parameter Optimization")
print("="*60)
# Generate all parameter combinations
param_names = list(param_grid.keys())
param_values = list(param_grid.values())
combinations = list(product(*param_values))
total_combinations = len(combinations)
print(f"\nTotal parameter combinations: {total_combinations}")
print(f"Optimizing for: {metric}\n")
results = []
for i, combo in enumerate(combinations, 1):
params = dict(zip(param_names, combo))
print(f"[{i}/{total_combinations}] Testing: {params}")
try:
# Create strategy with these parameters
strategy = ONNXBacktestStrategy(
symbol=self.symbol,
timeframe=self.timeframe,
model_path=self.model_path,
scaler_path=self.scaler_path,
initial_balance=self.initial_balance,
**params
)
# Run backtest
engine = BacktestEngine(strategy, self.start_date, self.end_date)
backtest_results = engine.run()
# Calculate metrics
analyzer = PerformanceAnalyzer(backtest_results)
metrics = analyzer.metrics
# Store results
result = params.copy()
# Map metric names to match what we're looking for
result['total_return'] = metrics.get('total_return_pct', 0) / 100.0
result['max_drawdown'] = metrics.get('max_drawdown_pct', 0) / 100.0
result['sharpe_ratio'] = metrics.get('sharpe_ratio', 0.0) if 'sharpe_ratio' in metrics else 0.0
result['profit_factor'] = metrics.get('profit_factor', 0.0)
result['win_rate'] = metrics.get('win_rate_pct', 0) / 100.0
result['total_trades'] = metrics.get('total_trades', 0)
result['final_balance'] = metrics.get('final_balance', self.initial_balance)
results.append(result)
metric_value = result.get(metric, 0)
print(f" -> {metric}: {metric_value:.4f} | Trades: {result['total_trades']}")
except Exception as e:
print(f" X Error: {e}")
continue
# Convert to DataFrame
df_results = pd.DataFrame(results)
if len(df_results) == 0:
raise ValueError("No successful backtests!")
# Sort by metric (descending for most metrics, ascending for max_drawdown)
if metric == 'max_drawdown':
df_results = df_results.sort_values(metric, ascending=True)
else:
df_results = df_results.sort_values(metric, ascending=False)
return df_results
def random_search(self, param_ranges: dict, n_iter: int = 50,
metric: str = 'sharpe_ratio') -> pd.DataFrame:
"""
Perform random search optimization.
Args:
param_ranges: Dictionary of parameter ranges
Example: {
'prediction_threshold': (0.0001, 0.001),
'min_confidence': (0.1, 0.5),
'stop_loss_pips': (20, 100),
'take_profit_pips': (40, 200),
'lot_size': (0.1, 0.5)
}
n_iter: Number of random combinations to test
metric: Metric to optimize
Returns:
DataFrame with results sorted by metric
"""
print("="*60)
print("Random Search Parameter Optimization")
print("="*60)
print(f"\nTesting {n_iter} random parameter combinations")
print(f"Optimizing for: {metric}\n")
results = []
np.random.seed(42) # For reproducibility
for i in range(1, n_iter + 1):
# Generate random parameters
params = {}
for param_name, (min_val, max_val) in param_ranges.items():
if isinstance(min_val, int) and isinstance(max_val, int):
params[param_name] = np.random.randint(min_val, max_val + 1)
else:
params[param_name] = np.random.uniform(min_val, max_val)
print(f"[{i}/{n_iter}] Testing: {params}")
try:
# Create strategy
strategy = ONNXBacktestStrategy(
symbol=self.symbol,
timeframe=self.timeframe,
model_path=self.model_path,
scaler_path=self.scaler_path,
initial_balance=self.initial_balance,
**params
)
# Run backtest
engine = BacktestEngine(strategy, self.start_date, self.end_date)
backtest_results = engine.run()
# Calculate metrics
analyzer = PerformanceAnalyzer(backtest_results)
metrics = analyzer.metrics
# Store results
result = params.copy()
# Map metric names to match what we're looking for
result['total_return'] = metrics.get('total_return_pct', 0) / 100.0
result['max_drawdown'] = metrics.get('max_drawdown_pct', 0) / 100.0
result['sharpe_ratio'] = metrics.get('sharpe_ratio', 0.0) if 'sharpe_ratio' in metrics else 0.0
result['profit_factor'] = metrics.get('profit_factor', 0.0)
result['win_rate'] = metrics.get('win_rate_pct', 0) / 100.0
result['total_trades'] = metrics.get('total_trades', 0)
result['final_balance'] = metrics.get('final_balance', self.initial_balance)
results.append(result)
metric_value = result.get(metric, 0)
print(f" -> {metric}: {metric_value:.4f} | Trades: {result['total_trades']}")
except Exception as e:
print(f" X Error: {e}")
continue
# Convert to DataFrame
df_results = pd.DataFrame(results)
if len(df_results) == 0:
raise ValueError("No successful backtests!")
# Sort by metric
if metric == 'max_drawdown':
df_results = df_results.sort_values(metric, ascending=True)
else:
df_results = df_results.sort_values(metric, ascending=False)
return df_results
def save_results(self, df_results: pd.DataFrame, output_file: str = 'optimization_results.csv'):
"""Save optimization results to CSV."""
df_results.to_csv(output_file, index=False)
print(f"\nResults saved to: {output_file}")
# Also save top 10 as JSON
top_10 = df_results.head(10).to_dict('records')
json_file = output_file.replace('.csv', '_top10.json')
with open(json_file, 'w') as f:
json.dump(top_10, f, indent=2, default=str)
print(f"Top 10 results saved to: {json_file}")
def main():
"""Main optimization function."""
# Configuration
symbol = 'XAUUSD'
timeframe = mt5.TIMEFRAME_H1
model_path = 'models/XAUUSD_H1_model.onnx'
scaler_path = 'models/XAUUSD_H1_scaler.pkl'
initial_balance = 10000.0
# Backtest date range (use last 6 months for optimization)
end_date = datetime.now()
start_date = end_date - timedelta(days=180)
# Check if model exists
if not os.path.exists(model_path):
print(f"ERROR: Model not found: {model_path}")
print("Please train the model first using train_onnx_model.py")
return
# Initialize MT5
if not mt5.initialize():
print("ERROR: Failed to initialize MT5")
return
try:
# Create optimizer
optimizer = ONNXParameterOptimizer(
symbol=symbol,
timeframe=timeframe,
model_path=model_path,
scaler_path=scaler_path,
start_date=start_date,
end_date=end_date,
initial_balance=initial_balance
)
# Choose optimization method (use command line args or defaults)
import sys
choice = "2" # Default to random search
n_iter = 30 # Default iterations
if len(sys.argv) > 1:
choice = sys.argv[1]
if len(sys.argv) > 2:
n_iter = int(sys.argv[2])
print("\nOptimization Configuration:")
print(f"Method: {'Grid Search' if choice == '1' else 'Random Search'}")
if choice != "1":
print(f"Iterations: {n_iter}")
print()
if choice == "1":
# Grid search parameters
param_grid = {
'prediction_threshold': [0.0001, 0.0002, 0.0005, 0.001],
'min_confidence': [0.1, 0.2, 0.3, 0.4],
'stop_loss_pips': [30, 50, 70, 100],
'take_profit_pips': [60, 100, 150, 200],
'lot_size': [0.1, 0.2]
}
results = optimizer.grid_search(param_grid, metric='sharpe_ratio')
else:
# Random search parameters
param_ranges = {
'prediction_threshold': (0.00005, 0.002), # Lower threshold to get more trades
'min_confidence': (0.05, 0.5), # Lower confidence requirement
'stop_loss_pips': (20, 150),
'take_profit_pips': (40, 300),
'lot_size': (0.05, 0.3)
}
results = optimizer.random_search(param_ranges, n_iter=n_iter, metric='sharpe_ratio')
# Display top results
print("\n" + "="*60)
print("Top 10 Results")
print("="*60)
print(results.head(10).to_string(index=False))
# Save results
optimizer.save_results(results, 'onnx_optimization_results.csv')
# Display best parameters
best = results.iloc[0]
print("\n" + "="*60)
print("Best Parameters")
print("="*60)
print(f"Prediction Threshold: {best['prediction_threshold']:.6f}")
print(f"Min Confidence: {best['min_confidence']:.2f}")
print(f"Stop Loss (pips): {best['stop_loss_pips']}")
print(f"Take Profit (pips): {best['take_profit_pips']}")
print(f"Lot Size: {best['lot_size']:.2f}")
print(f"\nPerformance Metrics:")
print(f" Sharpe Ratio: {best.get('sharpe_ratio', 'N/A'):.4f}")
print(f" Total Return: {best.get('total_return', 'N/A'):.2%}")
print(f" Max Drawdown: {best.get('max_drawdown', 'N/A'):.2%}")
print(f" Profit Factor: {best.get('profit_factor', 'N/A'):.2f}")
except KeyboardInterrupt:
print("\n\nOptimization interrupted by user")
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()
finally:
mt5.shutdown()
if __name__ == '__main__':
main()
+120
View File
@@ -0,0 +1,120 @@
"""
Quick Backtest Script for ONNX Model
Simple script to quickly backtest the trained ONNX model with default parameters.
"""
import os
import sys
from datetime import datetime, timedelta
import MetaTrader5 as mt5
# Add paths
current_dir = os.path.dirname(os.path.abspath(__file__))
backtest_dir = os.path.join(os.path.dirname(current_dir), 'backtesting', 'MT5')
sys.path.insert(0, backtest_dir)
from backtest_engine import BacktestEngine
from onnx_backtest_strategy import ONNXBacktestStrategy
from performance_analyzer import PerformanceAnalyzer
def main():
"""Run quick backtest."""
print("="*60)
print("XAUUSD ONNX Model Quick Backtest")
print("="*60)
# Configuration
symbol = 'XAUUSD'
timeframe = mt5.TIMEFRAME_H1
model_path = 'models/XAUUSD_H1_model.onnx'
scaler_path = 'models/XAUUSD_H1_scaler.pkl'
initial_balance = 10000.0
# Check if model exists
if not os.path.exists(model_path):
print(f"\nERROR: Model not found: {model_path}")
print("Please train the model first using:")
print(" python train_onnx_model.py --symbol XAUUSD --timeframe H1")
return
# Backtest date range
end_date = datetime.now()
start_date = end_date - timedelta(days=180) # Last 6 months
print(f"\nModel: {model_path}")
print(f"Symbol: {symbol}")
print(f"Timeframe: H1")
print(f"Date Range: {start_date.date()} to {end_date.date()}")
print(f"Initial Balance: ${initial_balance:,.2f}\n")
# Adjusted parameters (more relaxed to generate trades)
print("Strategy Parameters (Adjusted for Testing):")
print(" Prediction Threshold: 0.00005 (0.005%) - LOWERED")
print(" Min Confidence: 0.1 (10%) - LOWERED")
print(" Stop Loss: 50 pips")
print(" Take Profit: 100 pips")
print(" Lot Size: 0.1\n")
# Initialize MT5
if not mt5.initialize():
print("ERROR: Failed to initialize MT5")
print("Make sure MetaTrader 5 is running and you're logged in.")
return
try:
# Create strategy with relaxed parameters
strategy = ONNXBacktestStrategy(
symbol=symbol,
timeframe=timeframe,
model_path=model_path,
scaler_path=scaler_path,
initial_balance=initial_balance,
prediction_threshold=0.00005, # Lowered from 0.0001
min_confidence=0.1, # Lowered from 0.3
lot_size=0.1,
stop_loss_pips=50,
take_profit_pips=100
)
# Run backtest
print("Running backtest...\n")
engine = BacktestEngine(strategy, start_date, end_date)
results = engine.run()
# Analyze results
print("\n" + "="*60)
print("Performance Summary")
print("="*60)
analyzer = PerformanceAnalyzer(results)
metrics = analyzer.metrics
print(f"\nTotal Return: {metrics.get('total_return_pct', 0):.2f}%")
print(f"Max Drawdown: {metrics.get('max_drawdown_pct', 0):.2f}%")
print(f"Profit Factor: {metrics.get('profit_factor', 0):.2f}")
print(f"Win Rate: {metrics.get('win_rate_pct', 0):.2f}%")
print(f"Total Trades: {metrics.get('total_trades', 0)}")
print(f"Final Balance: ${metrics.get('final_balance', initial_balance):,.2f}")
# Generate report
analyzer.generate_report('onnx_xauusd_quick_backtest')
print("\n" + "="*60)
print("Backtest Completed!")
print("="*60)
print(f"\nResults saved to: onnx_xauusd_quick_backtest/")
print("\nTo optimize parameters, run:")
print(" python optimize_onnx_params.py")
except Exception as e:
print(f"\nERROR: Backtest failed: {e}")
import traceback
traceback.print_exc()
finally:
mt5.shutdown()
if __name__ == '__main__':
main()
+115
View File
@@ -0,0 +1,115 @@
"""
Retrain XAUUSD ONNX Model with Improved Settings
This script retrains the model with:
- Price change percentage prediction (instead of absolute price)
- More training epochs
- Better model architecture
- Improved data preprocessing
"""
import os
import sys
from datetime import datetime
import MetaTrader5 as mt5
# Add paths
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, current_dir)
from train_onnx_model import ONNXModelTrainer
def main():
"""Retrain XAUUSD model with improved settings."""
print("="*60)
print("Retraining XAUUSD ONNX Model (Improved)")
print("="*60)
symbol = 'XAUUSD'
timeframe_str = 'H1'
lookback = 60
epochs = 50 # More epochs for better training
# 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[timeframe_str]
# Create models directory
models_dir = 'models'
os.makedirs(models_dir, exist_ok=True)
print(f"\nConfiguration:")
print(f" Symbol: {symbol}")
print(f" Timeframe: {timeframe_str}")
print(f" Lookback: {lookback} bars")
print(f" Epochs: {epochs}")
print(f" Prediction: Price change percentage (improved)")
print("\nThis will take 10-20 minutes...\n")
trainer = ONNXModelTrainer(
symbol=symbol,
timeframe=timeframe,
lookback=lookback
)
try:
# Train model
trainer.train(epochs=epochs, batch_size=32, verbose=1)
# Export model
model_name = f"{symbol}_{timeframe_str}_model.onnx"
model_path = os.path.join(models_dir, model_name)
print(f"\nExporting model to ONNX format...")
trainer.export_to_onnx(model_path)
# Save scaler
scaler_name = f"{symbol}_{timeframe_str}_scaler.pkl"
scaler_path = os.path.join(models_dir, scaler_name)
import pickle
with open(scaler_path, 'wb') as f:
pickle.dump(trainer.scaler, f)
print(f"Scaler saved to: {scaler_path}")
print(f"\n{'='*60}")
print("Retraining Completed Successfully!")
print(f"{'='*60}")
print(f"\nModel: {model_path}")
print(f"Scaler: {scaler_path}")
print("\nNext steps:")
print(" 1. Run: python quick_backtest.py")
print(" 2. Or: python optimize_onnx_params.py 2 30")
except Exception as e:
print(f"\nERROR: Training failed: {e}")
import traceback
traceback.print_exc()
trainer.cleanup()
return
finally:
trainer.cleanup()
if __name__ == '__main__':
# Check MT5 connection
if not mt5.initialize():
print("ERROR: Failed to initialize MT5")
print("Make sure MetaTrader 5 is running and you're logged in.")
sys.exit(1)
try:
main()
except KeyboardInterrupt:
print("\n\nTraining interrupted by user")
finally:
mt5.shutdown()
+115
View File
@@ -0,0 +1,115 @@
"""
Backtest XAUUSD ONNX Model
This script backtests a trained ONNX model for XAUUSD.
Make sure you have trained the model first using train_onnx_model.py
"""
import sys
import os
from datetime import datetime, timedelta
import MetaTrader5 as mt5
# Add paths
current_dir = os.path.dirname(os.path.abspath(__file__))
backtest_dir = os.path.join(os.path.dirname(current_dir), 'backtesting', 'MT5')
sys.path.insert(0, backtest_dir)
from backtest_engine import BacktestEngine
from onnx_backtest_strategy import ONNXBacktestStrategy
from performance_analyzer import PerformanceAnalyzer
def main():
"""Run backtest for XAUUSD ONNX model."""
print("="*60)
print("XAUUSD ONNX Model Backtest")
print("="*60)
# Configuration
symbol = 'XAUUSD'
timeframe = mt5.TIMEFRAME_H1
model_path = 'models/XAUUSD_H1_model.onnx'
scaler_path = 'models/XAUUSD_H1_scaler.pkl'
initial_balance = 10000.0
# Check if model exists
if not os.path.exists(model_path):
print(f"\nERROR: Model not found: {model_path}")
print("Please train the model first using:")
print(" python train_onnx_model.py --symbol XAUUSD --timeframe H1")
return
if not os.path.exists(scaler_path):
print(f"\nWARNING: Scaler not found: {scaler_path}")
print("Will use default normalization (may affect accuracy)")
scaler_path = None
# Backtest date range
end_date = datetime.now()
start_date = end_date - timedelta(days=180) # Last 6 months
print(f"\nModel: {model_path}")
print(f"Symbol: {symbol}")
print(f"Timeframe: H1")
print(f"Date Range: {start_date.date()} to {end_date.date()}")
print(f"Initial Balance: ${initial_balance:,.2f}\n")
# Create strategy
try:
strategy = ONNXBacktestStrategy(
symbol=symbol,
timeframe=timeframe,
model_path=model_path,
scaler_path=scaler_path,
initial_balance=initial_balance,
prediction_threshold=0.0001, # 0.01% minimum change
min_confidence=0.3, # 30% minimum confidence
lot_size=0.1,
stop_loss_pips=50,
take_profit_pips=100
)
except Exception as e:
print(f"ERROR: Failed to create strategy: {e}")
import traceback
traceback.print_exc()
return
# Initialize MT5
if not mt5.initialize():
print("ERROR: Failed to initialize MT5")
print("Make sure MetaTrader 5 is running and you're logged in.")
return
try:
# Run backtest
print("Running backtest...\n")
engine = BacktestEngine(strategy, start_date, end_date)
results = engine.run()
# Analyze results
print("\n" + "="*60)
print("Performance Analysis")
print("="*60)
analyzer = PerformanceAnalyzer(results)
analyzer.generate_report('onnx_xauusd_backtest')
print("\n" + "="*60)
print("Backtest Completed!")
print("="*60)
print(f"\nResults saved to: onnx_xauusd_backtest/")
except Exception as e:
print(f"\nERROR: Backtest failed: {e}")
import traceback
traceback.print_exc()
finally:
mt5.shutdown()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print("\n\nInterrupted by user")
+21
View File
@@ -0,0 +1,21 @@
"""
Quick script to train XAUUSD ONNX model
Run this to train a model for XAUUSD.
After training, you can use the model for backtesting or live trading.
"""
import sys
import os
# Ensure we're in the right directory
os.chdir(os.path.dirname(os.path.abspath(__file__)))
# Train the model
print("Training XAUUSD ONNX model...")
print("This will take several minutes...\n")
os.system('python train_onnx_model.py --symbol XAUUSD --timeframe H1 --lookback 60 --epochs 30 --batch-size 32')
print("\nTraining completed! Model saved to models/XAUUSD_H1_model.onnx")
print("Scaler saved to models/XAUUSD_H1_scaler.pkl")
+103
View File
@@ -0,0 +1,103 @@
"""
Test with very low thresholds to see if we can get any trades
"""
import os
import sys
from datetime import datetime, timedelta
import MetaTrader5 as mt5
# Add paths
current_dir = os.path.dirname(os.path.abspath(__file__))
backtest_dir = os.path.join(os.path.dirname(current_dir), 'backtesting', 'MT5')
sys.path.insert(0, backtest_dir)
from backtest_engine import BacktestEngine
from onnx_backtest_strategy import ONNXBacktestStrategy
from performance_analyzer import PerformanceAnalyzer
def main():
"""Test with very low thresholds."""
print("="*60)
print("Testing with VERY LOW Thresholds")
print("="*60)
symbol = 'XAUUSD'
timeframe = mt5.TIMEFRAME_H1
model_path = 'models/XAUUSD_H1_model.onnx'
scaler_path = 'models/XAUUSD_H1_scaler.pkl'
initial_balance = 10000.0
if not os.path.exists(model_path):
print(f"ERROR: Model not found: {model_path}")
return
end_date = datetime.now()
start_date = end_date - timedelta(days=180)
print(f"\nModel: {model_path}")
print(f"Date Range: {start_date.date()} to {end_date.date()}")
print(f"\nVERY RELAXED Parameters:")
print(" Prediction Threshold: 0.00001 (0.001%)")
print(" Min Confidence: 0.05 (5%)")
print(" Stop Loss: 50 pips")
print(" Take Profit: 100 pips")
print(" Lot Size: 0.1\n")
if not mt5.initialize():
print("ERROR: Failed to initialize MT5")
return
try:
# Create strategy with VERY low thresholds
strategy = ONNXBacktestStrategy(
symbol=symbol,
timeframe=timeframe,
model_path=model_path,
scaler_path=scaler_path,
initial_balance=initial_balance,
prediction_threshold=0.00001, # Very low: 0.001%
min_confidence=0.05, # Very low: 5%
lot_size=0.1,
stop_loss_pips=50,
take_profit_pips=100
)
print("Running backtest...\n")
engine = BacktestEngine(strategy, start_date, end_date)
results = engine.run()
analyzer = PerformanceAnalyzer(results)
metrics = analyzer.metrics
print("\n" + "="*60)
print("Results")
print("="*60)
print(f"Total Trades: {metrics.get('total_trades', 0)}")
print(f"Final Balance: ${metrics.get('final_balance', initial_balance):,.2f}")
print(f"Total Return: {metrics.get('total_return_pct', 0):.2f}%")
if metrics.get('total_trades', 0) == 0:
print("\n" + "="*60)
print("STILL NO TRADES!")
print("="*60)
print("This suggests the model predictions may be:")
print(" 1. Too small in magnitude")
print(" 2. Not meeting even very low thresholds")
print(" 3. Or there's an issue with the prediction logic")
print("\nNext steps:")
print(" - Check model predictions directly")
print(" - Verify feature preparation matches training")
print(" - Consider retraining with different architecture")
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()
finally:
mt5.shutdown()
if __name__ == '__main__':
main()
+164
View File
@@ -0,0 +1,164 @@
"""
Train ONNX Model for XAUUSD and Backtest
This script:
1. Trains an ONNX model for XAUUSD
2. Runs backtest using the trained model
3. Generates performance report
"""
import os
import sys
from datetime import datetime, timedelta
import MetaTrader5 as mt5
# Add paths
current_dir = os.path.dirname(os.path.abspath(__file__))
backtest_dir = os.path.join(os.path.dirname(current_dir), 'backtesting', 'MT5')
sys.path.insert(0, current_dir)
sys.path.insert(0, backtest_dir)
from train_onnx_model import ONNXModelTrainer
from backtest_engine import BacktestEngine
from onnx_backtest_strategy import ONNXBacktestStrategy
from performance_analyzer import PerformanceAnalyzer
def main():
"""Main function to train model and run backtest."""
print("="*60)
print("XAUUSD ONNX Model Training and Backtesting")
print("="*60)
# Configuration
symbol = 'XAUUSD'
timeframe_str = 'H1'
lookback = 60
epochs = 30 # Reduced for faster training
initial_balance = 10000.0
# 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[timeframe_str]
# Create models directory
models_dir = 'models'
os.makedirs(models_dir, exist_ok=True)
# Step 1: Train Model
print("\n" + "="*60)
print("STEP 1: Training ONNX Model")
print("="*60)
trainer = ONNXModelTrainer(
symbol=symbol,
timeframe=timeframe,
lookback=lookback
)
try:
print(f"\nTraining model for {symbol} on {timeframe_str} timeframe...")
print(f"Lookback: {lookback} bars")
print(f"Epochs: {epochs}")
print("\nThis may take several minutes...\n")
trainer.train(epochs=epochs, batch_size=32, verbose=1)
# Export model
model_name = f"{symbol}_{timeframe_str}_model.onnx"
model_path = os.path.join(models_dir, model_name)
print(f"\nExporting model to ONNX format...")
trainer.export_to_onnx(model_path)
# Save scaler
scaler_name = f"{symbol}_{timeframe_str}_scaler.pkl"
scaler_path = os.path.join(models_dir, scaler_name)
import pickle
with open(scaler_path, 'wb') as f:
pickle.dump(trainer.scaler, f)
print(f"✓ Scaler saved to: {scaler_path}")
print(f"\n✓ Model saved to: {model_path}")
except Exception as e:
print(f"\n✗ Training failed: {e}")
import traceback
traceback.print_exc()
trainer.cleanup()
return
finally:
trainer.cleanup()
# Step 2: Run Backtest
print("\n" + "="*60)
print("STEP 2: Running Backtest")
print("="*60)
# Backtest date range (last 6 months for testing)
end_date = datetime.now()
start_date = end_date - timedelta(days=180)
# Create strategy
strategy = ONNXBacktestStrategy(
symbol=symbol,
timeframe=timeframe,
model_path=model_path,
scaler_path=scaler_path,
initial_balance=initial_balance,
prediction_threshold=0.0001, # 0.01% minimum change
min_confidence=0.3, # 30% minimum confidence
lot_size=0.1,
stop_loss_pips=50,
take_profit_pips=100
)
# Run backtest
try:
print(f"\nRunning backtest from {start_date.date()} to {end_date.date()}...")
engine = BacktestEngine(strategy, start_date, end_date)
results = engine.run()
# Analyze results
print("\n" + "="*60)
print("STEP 3: Performance Analysis")
print("="*60)
analyzer = PerformanceAnalyzer(results)
analyzer.generate_report('onnx_backtest_results')
print("\n" + "="*60)
print("Training and Backtesting Completed!")
print("="*60)
print(f"\nModel: {model_path}")
print(f"Scaler: {scaler_path}")
print(f"Results: onnx_backtest_results/")
except Exception as e:
print(f"\n✗ Backtest failed: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
# Check MT5 connection
if not mt5.initialize():
print("ERROR: Failed to initialize MT5")
print("Make sure MetaTrader 5 is running and you're logged in.")
sys.exit(1)
try:
main()
except KeyboardInterrupt:
print("\n\nInterrupted by user")
finally:
mt5.shutdown()
@@ -135,7 +135,7 @@ class ONNXModelTrainer:
Args:
data: Feature data
target: Target values
target: Target values (price change percentages)
Returns:
Tuple of (X, y) sequences
@@ -144,7 +144,8 @@ class ONNXModelTrainer:
for i in range(self.lookback, len(data) - self.prediction_horizon + 1):
X.append(data[i - self.lookback:i])
y.append(target[i + self.prediction_horizon - 1])
# Target is already the price change percentage at position i
y.append(target[i])
return np.array(X), np.array(y)
@@ -160,17 +161,18 @@ class ONNXModelTrainer:
"""
model = keras.Sequential([
layers.LSTM(128, return_sequences=True, input_shape=input_shape),
layers.Dropout(0.2),
layers.Dropout(0.3),
layers.LSTM(64, return_sequences=True),
layers.Dropout(0.2),
layers.Dropout(0.3),
layers.LSTM(32),
layers.Dropout(0.2),
layers.Dropout(0.3),
layers.Dense(32, activation='relu'),
layers.Dense(16, activation='relu'),
layers.Dense(1) # Predict next close price
layers.Dense(1) # Predict price change percentage
])
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=0.001),
optimizer=keras.optimizers.Adam(learning_rate=0.0005), # Lower learning rate for stability
loss='mse',
metrics=['mae']
)
@@ -195,9 +197,25 @@ class ONNXModelTrainer:
df = self.fetch_data(start_date, end_date)
feature_df = self.prepare_features(df)
# Prepare data
# Prepare data - align target with features after dropna
feature_data = feature_df.values
target_data = df['close'].values[feature_df.index]
# Get target data aligned with feature_df (after dropna)
# Use .loc to align by index, then convert to values
close_prices = df.loc[feature_df.index, 'close'].values
# Predict price change percentage instead of absolute price (more stable)
# Calculate future price change: (future_price - current_price) / current_price
target_data = []
for i in range(len(close_prices)):
if i + self.prediction_horizon < len(close_prices):
current_price = close_prices[i]
future_price = close_prices[i + self.prediction_horizon]
price_change_pct = (future_price - current_price) / current_price if current_price > 0 else 0.0
target_data.append(price_change_pct)
else:
target_data.append(0.0)
target_data = np.array(target_data)
# Scale features
feature_data_scaled = self.scaler.fit_transform(feature_data)
@@ -213,7 +231,9 @@ class ONNXModelTrainer:
print(f"\nTraining data shape: {X_train.shape}")
print(f"Validation data shape: {X_test.shape}")
# Build model
# Build model - use actual feature count from data
actual_num_features = X_train.shape[2]
print(f"Actual number of features: {actual_num_features}")
self.model = self.build_model((X_train.shape[1], X_train.shape[2]))
print("\nModel architecture:")
@@ -263,28 +283,137 @@ class ONNXModelTrainer:
print(f"\nExporting model to ONNX format: {output_path}")
# Get input shape
input_shape = (1, self.lookback, len(self.features) + 7) # +7 for added features
# Get actual number of features from model input shape
# The model was built with the actual feature count during training
if self.model is not None and hasattr(self.model, 'input_shape'):
num_features = self.model.input_shape[2] if len(self.model.input_shape) > 2 else self.model.input_shape[1]
else:
# Fallback calculation
# Base features: open, high, low, close, tick_volume (5)
# Added features: rsi, ema_20, ema_50, atr, price_change, high_low_ratio, volume_ma, volume_ratio (8)
num_features = len(self.features) + 8
# Create dummy input
dummy_input = np.random.randn(*input_shape).astype(np.float32)
print(f"Using {num_features} features for ONNX export")
# Convert to ONNX
spec = (tf.TensorSpec((None, self.lookback, len(self.features) + 7), tf.float32, name="input"),)
output_path_onnx = tf2onnx.convert.from_keras(
self.model,
input_signature=spec,
opset=13,
output_path=output_path
)
# Create input signature
input_shape = (None, self.lookback, num_features)
spec = (tf.TensorSpec(input_shape, tf.float32, name="input"),)
print(f"✓ ONNX model saved to: {output_path}")
# Convert to ONNX using tf2onnx
# Workaround for tf2onnx 1.16.1 issue with Sequential models (GitHub issue #2319)
# Fix: Add output_names attribute to Sequential model if missing
if hasattr(self.model, 'output_names') is False:
# Workaround: Create a wrapper or use functional API
try:
# Try to get output names from model outputs
if hasattr(self.model, 'outputs') and self.model.outputs:
self.model.output_names = [f'output_{i}' for i in range(len(self.model.outputs))]
else:
self.model.output_names = ['output']
except:
pass
# Create input signature tuple
spec = (tf.TensorSpec((None, self.lookback, num_features), tf.float32, name="input"),)
# Skip direct Sequential conversion - use Functional API directly
# This avoids the 'output_names' attribute error
try:
# Method 1: Convert Sequential to Functional API model (more reliable)
print("Converting Sequential model to Functional API...")
# Create functional model from Sequential
input_layer = keras.Input(shape=(self.lookback, num_features), name="input")
x = input_layer
# Rebuild model as functional
for layer in self.model.layers:
x = layer(x)
functional_model = keras.Model(inputs=input_layer, outputs=x)
# Convert functional model
onnx_model_proto, _ = tf2onnx.convert.from_keras(
functional_model,
input_signature=spec,
opset=13
)
onnx.save_model(onnx_model_proto, output_path)
print(f"ONNX model saved to: {output_path}")
except Exception as e1:
# Method 2: Use concrete function approach
try:
print("Trying concrete function method...")
# Create concrete function
input_spec = tf.TensorSpec(shape=(None, self.lookback, num_features), dtype=tf.float32)
@tf.function
def model_func(x):
return self.model(x)
# Get concrete function
concrete_func = model_func.get_concrete_function(input_spec)
# Convert with input_signature as list
input_signature_list = [input_spec]
onnx_model_proto, _ = tf2onnx.convert.from_function(
concrete_func,
input_signature=input_signature_list,
opset=13
)
onnx.save_model(onnx_model_proto, output_path)
print(f"ONNX model saved to: {output_path}")
except Exception as e2:
# Method 3: Try alternative conversion method
try:
print("Trying alternative conversion method...")
# Save model first, then convert
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
# Save as .keras format
keras_path = os.path.join(tmpdir, "model.keras")
self.model.save(keras_path)
# Load and convert
loaded_model = keras.models.load_model(keras_path)
# Convert Sequential to Functional
input_layer = keras.Input(shape=(self.lookback, num_features), name="input")
x = input_layer
for layer in loaded_model.layers:
x = layer(x)
functional_model = keras.Model(inputs=input_layer, outputs=x)
# Try conversion again
onnx_model_proto, _ = tf2onnx.convert.from_keras(
functional_model,
input_signature=spec,
opset=13
)
onnx.save_model(onnx_model_proto, output_path)
print(f"ONNX model saved to: {output_path}")
except Exception as e3:
raise RuntimeError(
f"Failed to export ONNX model.\n"
f"Error 1 (functional): {str(e1)[:200]}\n"
f"Error 2 (concrete): {str(e2)[:200]}\n"
f"Error 3 (alternative): {str(e3)[:200]}\n\n"
f"Please try upgrading tf2onnx: pip install --upgrade tf2onnx"
)
# Verify ONNX model
try:
onnx_model = onnx.load(output_path)
onnx.checker.check_model(onnx_model)
print("ONNX model validation passed")
print("ONNX model validation passed")
except Exception as e:
print(f"⚠ ONNX model validation warning: {e}")
@@ -345,7 +474,7 @@ def main():
import pickle
with open(scaler_path, 'wb') as f:
pickle.dump(trainer.scaler, f)
print(f"Scaler saved to: {scaler_path}")
print(f"Scaler saved to: {scaler_path}")
print(f" Use this with predict_with_onnx.py for consistent normalization")
print(f"\n{'='*60}")
+240
View File
@@ -0,0 +1,240 @@
# RSI Divergence ONNX Trading System for BTCUSD
A complete AI-powered trading system that uses machine learning to identify genuine RSI (Relative Strength Index) divergences and execute trades on MetaTrader 5.
## Overview
This system trains a neural network to classify RSI divergences into 5 categories:
- **NONE** (0): No divergence detected
- **REGULAR_BULLISH** (1): Price makes lower low, RSI makes higher low (reversal signal)
- **REGULAR_BEARISH** (2): Price makes higher high, RSI makes lower high (reversal signal)
- **HIDDEN_BULLISH** (3): Price makes higher low, RSI makes lower low (continuation signal)
- **HIDDEN_BEARISH** (4): Price makes lower high, RSI makes higher high (continuation signal)
The trained model is exported to ONNX format and used in a MetaTrader 5 Expert Advisor for live trading.
## Features
- **Advanced Divergence Detection**: Identifies both regular and hidden RSI divergences
- **Machine Learning Classification**: Uses LSTM neural network to learn genuine divergence patterns
- **ONNX Integration**: Model runs efficiently in MetaTrader 5 using ONNX Runtime
- **Comprehensive Backtesting**: Test model performance on historical data
- **Risk Management**: Built-in stop loss, take profit, trailing stop, and position time limits
## Project Structure
```
ai/rsi-divergence/
├── rsi_divergence_detector.py # Core divergence detection module
├── collect_btcusd_data.py # Data collection and labeling script
├── train_onnx_model.py # Model training script
├── backtest_model.py # Backtesting script
├── RSIDivergence_EA.mq5 # MetaTrader 5 Expert Advisor
├── requirements.txt # Python dependencies
└── README.md # This file
```
## Installation
### 1. Install Python Dependencies
```bash
cd ai/rsi-divergence
pip install -r requirements.txt
```
### 2. Setup MetaTrader 5
1. Install MetaTrader 5
2. Enable automated trading in MT5 settings
3. Copy `RSIDivergence_EA.mq5` to `MT5_Data_Folder/MQL5/Experts/`
4. Compile the EA in MetaEditor
## Usage
### Step 1: Collect and Label Data
Collect BTCUSD historical data and label it with RSI divergence signals:
```bash
python collect_btcusd_data.py \
--symbol BTCUSD \
--timeframe H1 \
--days 365 \
--rsi-period 14 \
--output data \
--min-strength 0.15
```
This will:
- Fetch BTCUSD data from MetaTrader 5
- Calculate RSI and other technical indicators
- Detect and label RSI divergences
- Save labeled data to `data/BTCUSD_H1_labeled.csv`
### Step 2: Train the Model
Train the neural network to classify divergences:
```bash
python train_onnx_model.py \
--data data/BTCUSD_H1_labeled.csv \
--lookback 60 \
--epochs 50 \
--batch-size 32 \
--output models
```
This will:
- Load labeled data
- Train an LSTM-based classification model
- Export model to ONNX format
- Save scaler and feature list for inference
Output files:
- `models/BTCUSD_H1_rsi_divergence_model.onnx` - ONNX model
- `models/BTCUSD_H1_rsi_divergence_scaler.pkl` - Feature scaler
- `models/BTCUSD_H1_rsi_divergence_features.pkl` - Feature list
### Step 3: Backtest the Model
Test the trained model on historical data:
```bash
python backtest_model.py \
--model models/BTCUSD_H1_rsi_divergence_model.onnx \
--scaler models/BTCUSD_H1_rsi_divergence_scaler.pkl \
--features models/BTCUSD_H1_rsi_divergence_features.pkl \
--symbol BTCUSD \
--timeframe H1 \
--days 90 \
--balance 10000 \
--lot-size 0.01 \
--min-confidence 0.7
```
This will:
- Load the trained model
- Run backtest on historical data
- Generate performance metrics
- Save trade history to CSV
### Step 4: Deploy to MetaTrader 5
1. **Copy Model Files**:
- Copy `BTCUSD_H1_rsi_divergence_model.onnx` to `MT5_Data_Folder/MQL5/Files/models/`
- Create the `models` folder if it doesn't exist
2. **Attach EA to Chart**:
- Open BTCUSD chart in MT5
- Drag `RSIDivergence_EA` from Navigator to chart
- Configure parameters:
- `InpModelPath`: Path to ONNX model (e.g., `models\\BTCUSD_H1_rsi_divergence_model.onnx`)
- `InpMinConfidence`: Minimum confidence threshold (0.7 recommended)
- `InpLotSize`: Position size
- `InpStopLoss`: Stop loss in pips
- `InpTakeProfit`: Take profit in pips
3. **Enable AutoTrading**:
- Click "AutoTrading" button in MT5 toolbar
- EA will start analyzing and trading automatically
## Parameters
### Data Collection Parameters
- `--symbol`: Trading symbol (default: BTCUSD)
- `--timeframe`: Timeframe (M1, M5, M15, M30, H1, H4, D1)
- `--days`: Number of days of historical data
- `--rsi-period`: RSI calculation period (default: 14)
- `--min-strength`: Minimum divergence strength (0-1)
### Training Parameters
- `--data`: Path to labeled CSV file
- `--lookback`: Number of bars to look back (default: 60)
- `--epochs`: Training epochs (default: 50)
- `--batch-size`: Batch size (default: 32)
### EA Parameters
**ONNX Model Settings**:
- `InpModelPath`: Path to ONNX model file
- `InpLookback`: Lookback period (must match training)
- `InpMinConfidence`: Minimum confidence to trade (0-1)
**Trading Settings**:
- `InpLotSize`: Position size
- `InpMagicNumber`: Unique identifier for EA trades
- `InpStopLoss`: Stop loss in pips (0 = disabled)
- `InpTakeProfit`: Take profit in pips (0 = disabled)
- `InpMaxBarsInTrade`: Maximum bars to hold position (0 = disabled)
**Divergence Filter**:
- `InpUseRegularBullish`: Enable regular bullish divergence trades
- `InpUseRegularBearish`: Enable regular bearish divergence trades
- `InpUseHiddenBullish`: Enable hidden bullish divergence trades
- `InpUseHiddenBearish`: Enable hidden bearish divergence trades
**Risk Management**:
- `InpUseTrailingStop`: Enable trailing stop
- `InpTrailingStopPips`: Trailing stop distance in pips
- `InpTrailingStepPips`: Trailing stop step in pips
## Understanding RSI Divergences
### Regular Divergences (Reversal Signals)
- **Bullish**: Price makes lower low, RSI makes higher low → Potential upward reversal
- **Bearish**: Price makes higher high, RSI makes lower high → Potential downward reversal
### Hidden Divergences (Continuation Signals)
- **Bullish**: Price makes higher low, RSI makes lower low → Trend continuation upward
- **Bearish**: Price makes lower high, RSI makes higher high → Trend continuation downward
## Performance Optimization
1. **Data Quality**: Use more historical data (1-2 years) for better training
2. **Feature Engineering**: Experiment with additional technical indicators
3. **Model Tuning**: Adjust LSTM architecture, dropout rates, learning rate
4. **Confidence Threshold**: Higher threshold = fewer but higher quality trades
5. **Risk Management**: Always use stop loss and position sizing
## Troubleshooting
### Model Not Loading in MT5
- Check model file path is correct
- Ensure model file is in `MQL5/Files/models/` folder
- Verify ONNX model version compatibility (opset 13)
### No Trades Executed
- Check confidence threshold (try lowering `InpMinConfidence`)
- Verify divergence types are enabled
- Check that sufficient historical data is available
### Poor Backtest Results
- Collect more training data
- Adjust divergence detection parameters
- Retrain with different model architecture
- Test on different timeframes
## Notes
- **Model Compatibility**: ONNX model uses opset 13 for MT5 compatibility
- **Feature Normalization**: Features are normalized using MinMaxScaler - ensure same normalization in EA
- **Timeframe**: Model trained on H1 timeframe - retrain for other timeframes
- **Symbol**: Model trained on BTCUSD - retrain for other symbols
## License
This project is provided as-is for educational and research purposes.
## References
- [MetaTrader 5 ONNX Documentation](https://www.mql5.com/en/docs/onnx/onnx_prepare)
- [RSI Divergence Trading Strategies](https://www.investopedia.com/trading/using-relative-strength-index-rsi/)
- [ONNX Runtime](https://onnxruntime.ai/)
+531
View File
@@ -0,0 +1,531 @@
//+------------------------------------------------------------------+
//| RSIDivergence_EA.mq5 |
//| RSI Divergence ONNX EA for MT5 |
//| |
//+------------------------------------------------------------------+
#property copyright "RSI Divergence ONNX EA"
#property link ""
#property version "1.00"
#property description "Expert Advisor using ONNX model to identify genuine RSI divergences"
#property description "Based on: https://www.mql5.com/en/docs/onnx/onnx_prepare"
#include <Trade\Trade.mqh>
//--- Input parameters
input group "=== ONNX Model Settings ==="
input string InpModelPath = "models\\BTCUSD_H1_rsi_divergence_model.onnx"; // ONNX Model Path
input string InpScalerPath = "models\\BTCUSD_H1_rsi_divergence_scaler.pkl"; // Scaler Path (not used in MQL5, for reference)
input string InpFeaturesPath = "models\\BTCUSD_H1_rsi_divergence_features.pkl"; // Features Path (not used in MQL5, for reference)
input int InpLookback = 60; // Lookback Period (bars)
input double InpMinConfidence = 0.7; // Minimum Confidence (0-1)
input group "=== Trading Settings ==="
input double InpLotSize = 0.01; // Lot Size
input int InpMagicNumber = 88001; // Magic Number
input int InpSlippage = 3; // Slippage (points)
input int InpStopLoss = 100; // Stop Loss (pips, 0 = disabled)
input int InpTakeProfit = 200; // Take Profit (pips, 0 = disabled)
input int InpMaxBarsInTrade = 20; // Max Bars in Trade (0 = disabled)
input group "=== Divergence Filter ==="
input bool InpUseRegularBullish = true; // Trade Regular Bullish Divergence
input bool InpUseRegularBearish = true; // Trade Regular Bearish Divergence
input bool InpUseHiddenBullish = true; // Trade Hidden Bullish Divergence
input bool InpUseHiddenBearish = true; // Trade Hidden Bearish Divergence
input group "=== Risk Management ==="
input bool InpUseTrailingStop = false; // Use Trailing Stop
input int InpTrailingStopPips = 50; // Trailing Stop (pips)
input int InpTrailingStepPips = 10; // Trailing Step (pips)
//--- Global variables
CTrade trade;
long onnx_handle = INVALID_HANDLE;
datetime last_bar_time = 0;
// Divergence type constants (must match Python model)
#define DIV_NONE 0
#define DIV_REGULAR_BULLISH 1
#define DIV_REGULAR_BEARISH 2
#define DIV_HIDDEN_BULLISH 3
#define DIV_HIDDEN_BEARISH 4
// Feature calculation buffers
double rsi_buffer[];
double ema20_buffer[];
double ema50_buffer[];
double atr_buffer[];
double sma20_buffer[];
double sma50_buffer[];
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// Set trade parameters
trade.SetExpertMagicNumber(InpMagicNumber);
trade.SetDeviationInPoints(InpSlippage);
trade.SetTypeFilling(ORDER_FILLING_FOK);
// Load ONNX model
string model_path = InpModelPath;
// Convert relative path to full path
if(StringFind(model_path, "\\") == 0 || StringFind(model_path, "/") == 0)
{
// Already absolute path
}
else
{
// Relative path - prepend terminal data folder
model_path = TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Files\\" + model_path;
}
// Replace forward slashes with backslashes for Windows
StringReplace(model_path, "/", "\\");
Print("Loading ONNX model from: ", model_path);
onnx_handle = OnnxCreate(model_path, ONNX_DEFAULT);
if(onnx_handle == INVALID_HANDLE)
{
Print("ERROR: Failed to load ONNX model. Error: ", GetLastError());
Print("Make sure the model file exists at: ", model_path);
Print("Model file should be in: ", TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Files\\models\\");
return(INIT_FAILED);
}
// Get model info
long input_count = OnnxGetInputCount(onnx_handle);
long output_count = OnnxGetOutputCount(onnx_handle);
Print("ONNX Model loaded successfully");
Print(" Inputs: ", input_count);
Print(" Outputs: ", output_count);
if(input_count > 0)
{
string input_name = OnnxGetInputName(onnx_handle, 0);
Print(" Input name: ", input_name);
}
if(output_count > 0)
{
string output_name = OnnxGetOutputName(onnx_handle, 0);
Print(" Output name: ", output_name);
}
// Initialize indicator buffers
ArraySetAsSeries(rsi_buffer, true);
ArraySetAsSeries(ema20_buffer, true);
ArraySetAsSeries(ema50_buffer, true);
ArraySetAsSeries(atr_buffer, true);
ArraySetAsSeries(sma20_buffer, true);
ArraySetAsSeries(sma50_buffer, true);
Print("RSI Divergence EA initialized successfully");
Print(" Symbol: ", _Symbol);
Print(" Timeframe: ", EnumToString(PERIOD_CURRENT));
Print(" Lookback: ", InpLookback);
Print(" Min Confidence: ", InpMinConfidence);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// Release ONNX model
if(onnx_handle != INVALID_HANDLE)
{
OnnxRelease(onnx_handle);
Print("ONNX model released");
}
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Check if new bar
datetime current_bar_time = iTime(_Symbol, PERIOD_CURRENT, 0);
if(current_bar_time == last_bar_time)
{
// Check trailing stop on current bar
if(InpUseTrailingStop)
{
ApplyTrailingStop();
}
return; // Still the same bar
}
last_bar_time = current_bar_time;
// Close positions that have been open too long
if(InpMaxBarsInTrade > 0)
{
CloseOldPositions();
}
// Prepare input data
float input_data[];
if(!PrepareInputData(input_data))
{
Print("ERROR: Failed to prepare input data");
return;
}
// Run ONNX model
float output_data[];
if(!RunONNXModel(input_data, output_data))
{
Print("ERROR: Failed to run ONNX model");
return;
}
// Get prediction
if(ArraySize(output_data) < 5)
{
Print("ERROR: Invalid output from ONNX model");
return;
}
// Get predicted class and confidence
int predicted_class = 0;
double max_prob = 0.0;
for(int i = 0; i < 5; i++)
{
if(output_data[i] > max_prob)
{
max_prob = output_data[i];
predicted_class = i;
}
}
double confidence = max_prob;
// Check if confidence meets threshold
if(confidence < InpMinConfidence)
{
return; // Not confident enough
}
// Check if we should trade this divergence type
bool should_trade = false;
int signal_type = 0; // 1 = BUY, -1 = SELL
if(predicted_class == DIV_REGULAR_BULLISH && InpUseRegularBullish)
{
should_trade = true;
signal_type = 1; // BUY
}
else if(predicted_class == DIV_REGULAR_BEARISH && InpUseRegularBearish)
{
should_trade = true;
signal_type = -1; // SELL
}
else if(predicted_class == DIV_HIDDEN_BULLISH && InpUseHiddenBullish)
{
should_trade = true;
signal_type = 1; // BUY
}
else if(predicted_class == DIV_HIDDEN_BEARISH && InpUseHiddenBearish)
{
should_trade = true;
signal_type = -1; // SELL
}
if(!should_trade)
{
return; // Divergence type not enabled
}
// Check if we already have a position
if(PositionSelect(_Symbol))
{
return; // Already in a position
}
// Execute trade
double price = (signal_type == 1) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = 0, tp = 0;
// Calculate stop loss and take profit
if(InpStopLoss > 0)
{
sl = (signal_type == 1) ? price - InpStopLoss * _Point * 10 : price + InpStopLoss * _Point * 10;
}
if(InpTakeProfit > 0)
{
tp = (signal_type == 1) ? price + InpTakeProfit * _Point * 10 : price - InpTakeProfit * _Point * 10;
}
// Open position
string divergence_name = "";
if(predicted_class == DIV_REGULAR_BULLISH) divergence_name = "Regular Bullish";
else if(predicted_class == DIV_REGULAR_BEARISH) divergence_name = "Regular Bearish";
else if(predicted_class == DIV_HIDDEN_BULLISH) divergence_name = "Hidden Bullish";
else if(predicted_class == DIV_HIDDEN_BEARISH) divergence_name = "Hidden Bearish";
if(signal_type == 1)
{
if(trade.Buy(InpLotSize, _Symbol, price, sl, tp, divergence_name + " Divergence (Conf: " + DoubleToString(confidence, 2) + ")"))
{
Print("BUY order opened: ", divergence_name, " Divergence, Confidence: ", confidence);
}
else
{
Print("ERROR: Failed to open BUY order: ", trade.ResultRetcodeDescription());
}
}
else
{
if(trade.Sell(InpLotSize, _Symbol, price, sl, tp, divergence_name + " Divergence (Conf: " + DoubleToString(confidence, 2) + ")"))
{
Print("SELL order opened: ", divergence_name, " Divergence, Confidence: ", confidence);
}
else
{
Print("ERROR: Failed to open SELL order: ", trade.ResultRetcodeDescription());
}
}
}
//+------------------------------------------------------------------+
//| Prepare input data for ONNX model |
//+------------------------------------------------------------------+
bool PrepareInputData(float &input_data[])
{
// We need to prepare features in the same order as training
// This should match the feature_cols from the Python training script
int lookback = InpLookback;
int num_features = 20; // Adjust based on your actual feature count
// Resize input array: (1, lookback, num_features)
ArrayResize(input_data, lookback * num_features);
ArrayInitialize(input_data, 0.0);
// Get price data
double close[], open[], high[], low[], volume[];
ArraySetAsSeries(close, true);
ArraySetAsSeries(open, true);
ArraySetAsSeries(high, true);
ArraySetAsSeries(low, true);
ArraySetAsSeries(volume, true);
CopyClose(_Symbol, PERIOD_CURRENT, 0, lookback, close);
CopyOpen(_Symbol, PERIOD_CURRENT, 0, lookback, open);
CopyHigh(_Symbol, PERIOD_CURRENT, 0, lookback, high);
CopyLow(_Symbol, PERIOD_CURRENT, 0, lookback, low);
CopyTickVolume(_Symbol, PERIOD_CURRENT, 0, lookback, volume);
// Calculate technical indicators
int rsi_handle = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
int ema20_handle = iMA(_Symbol, PERIOD_CURRENT, 20, 0, MODE_EMA, PRICE_CLOSE);
int ema50_handle = iMA(_Symbol, PERIOD_CURRENT, 50, 0, MODE_EMA, PRICE_CLOSE);
int sma20_handle = iMA(_Symbol, PERIOD_CURRENT, 20, 0, MODE_SMA, PRICE_CLOSE);
int sma50_handle = iMA(_Symbol, PERIOD_CURRENT, 50, 0, MODE_SMA, PRICE_CLOSE);
int atr_handle = iATR(_Symbol, PERIOD_CURRENT, 14);
ArraySetAsSeries(rsi_buffer, true);
ArraySetAsSeries(ema20_buffer, true);
ArraySetAsSeries(ema50_buffer, true);
ArraySetAsSeries(sma20_buffer, true);
ArraySetAsSeries(sma50_buffer, true);
ArraySetAsSeries(atr_buffer, true);
if(CopyBuffer(rsi_handle, 0, 0, lookback, rsi_buffer) <= 0) return false;
if(CopyBuffer(ema20_handle, 0, 0, lookback, ema20_buffer) <= 0) return false;
if(CopyBuffer(ema50_handle, 0, 0, lookback, ema50_buffer) <= 0) return false;
if(CopyBuffer(sma20_handle, 0, 0, lookback, sma20_buffer) <= 0) return false;
if(CopyBuffer(sma50_handle, 0, 0, lookback, sma50_buffer) <= 0) return false;
if(CopyBuffer(atr_handle, 0, 0, lookback, atr_buffer) <= 0) return false;
// Release indicator handles
IndicatorRelease(rsi_handle);
IndicatorRelease(ema20_handle);
IndicatorRelease(ema50_handle);
IndicatorRelease(sma20_handle);
IndicatorRelease(sma50_handle);
IndicatorRelease(atr_handle);
// Prepare features (must match Python feature order)
// Note: Features need to be normalized - this is a simplified version
// In production, you should use the same scaler from Python
for(int i = 0; i < lookback; i++)
{
int idx = i * num_features;
int bar_idx = lookback - 1 - i; // Reverse for time series
// Basic OHLCV (normalized)
input_data[idx + 0] = (float)(close[bar_idx] / close[0] - 1.0); // Normalized close
input_data[idx + 1] = (float)(open[bar_idx] / close[0] - 1.0); // Normalized open
input_data[idx + 2] = (float)(high[bar_idx] / close[0] - 1.0); // Normalized high
input_data[idx + 3] = (float)(low[bar_idx] / close[0] - 1.0); // Normalized low
input_data[idx + 4] = (float)(volume[bar_idx] / 1000000.0); // Normalized volume
// Returns
if(bar_idx < lookback - 1)
{
input_data[idx + 5] = (float)((close[bar_idx] - close[bar_idx + 1]) / close[bar_idx + 1]);
}
// Ratios
input_data[idx + 6] = (float)(high[bar_idx] / (low[bar_idx] + 1e-10));
input_data[idx + 7] = (float)(close[bar_idx] / (open[bar_idx] + 1e-10));
// Moving averages (normalized)
input_data[idx + 8] = (float)(sma20_buffer[bar_idx] / close[0] - 1.0);
input_data[idx + 9] = (float)(sma50_buffer[bar_idx] / close[0] - 1.0);
input_data[idx + 10] = (float)(ema20_buffer[bar_idx] / close[0] - 1.0);
input_data[idx + 11] = (float)(ema50_buffer[bar_idx] / close[0] - 1.0);
// ATR
input_data[idx + 12] = (float)(atr_buffer[bar_idx] / close[0]);
// RSI (normalized to 0-1)
input_data[idx + 13] = (float)(rsi_buffer[bar_idx] / 100.0);
// Volume features
double volume_ma = 0;
for(int j = 0; j < 20 && (bar_idx + j) < lookback; j++)
{
volume_ma += volume[bar_idx + j];
}
volume_ma /= 20.0;
input_data[idx + 14] = (float)(volume[bar_idx] / (volume_ma + 1e-10));
// Price position (simplified)
double min_low = low[bar_idx];
double max_high = high[bar_idx];
for(int j = 0; j < 20 && (bar_idx + j) < lookback; j++)
{
if(low[bar_idx + j] < min_low) min_low = low[bar_idx + j];
if(high[bar_idx + j] > max_high) max_high = high[bar_idx + j];
}
input_data[idx + 15] = (float)((close[bar_idx] - min_low) / (max_high - min_low + 1e-10));
// Additional features (pad with zeros if needed)
for(int j = 16; j < num_features; j++)
{
input_data[idx + j] = 0.0;
}
}
return true;
}
//+------------------------------------------------------------------+
//| Run ONNX model |
//+------------------------------------------------------------------+
bool RunONNXModel(float &input_data[], float &output_data[])
{
if(onnx_handle == INVALID_HANDLE)
{
return false;
}
// Get input/output names
string input_name = OnnxGetInputName(onnx_handle, 0);
string output_name = OnnxGetOutputName(onnx_handle, 0);
// Prepare input shape: (1, lookback, num_features)
long input_shape[] = {1, InpLookback, 20}; // Adjust num_features as needed
long output_shape[] = {1, 5}; // 5 classes
// Run model
if(!OnnxRun(onnx_handle, ONNX_NO_CONVERSION, input_data, input_shape, 3,
output_data, output_shape))
{
Print("ERROR: OnnxRun failed. Error: ", GetLastError());
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Apply trailing stop |
//+------------------------------------------------------------------+
void ApplyTrailingStop()
{
if(!PositionSelect(_Symbol))
{
return;
}
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber)
{
return;
}
double position_open_price = PositionGetDouble(POSITION_PRICE_OPEN);
double position_sl = PositionGetDouble(POSITION_SL);
double position_tp = PositionGetDouble(POSITION_TP);
long position_type = PositionGetInteger(POSITION_TYPE);
double current_price = (position_type == POSITION_TYPE_BUY) ?
SymbolInfoDouble(_Symbol, SYMBOL_BID) :
SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double trailing_distance = InpTrailingStopPips * _Point * 10;
double trailing_step = InpTrailingStepPips * _Point * 10;
if(position_type == POSITION_TYPE_BUY)
{
double new_sl = current_price - trailing_distance;
if(new_sl > position_open_price &&
(position_sl == 0 || new_sl > position_sl + trailing_step))
{
trade.PositionModify(_Symbol, new_sl, position_tp);
}
}
else if(position_type == POSITION_TYPE_SELL)
{
double new_sl = current_price + trailing_distance;
if(new_sl < position_open_price &&
(position_sl == 0 || new_sl < position_sl - trailing_step))
{
trade.PositionModify(_Symbol, new_sl, position_tp);
}
}
}
//+------------------------------------------------------------------+
//| Close positions that have been open too long |
//+------------------------------------------------------------------+
void CloseOldPositions()
{
if(!PositionSelect(_Symbol))
{
return;
}
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber)
{
return;
}
datetime position_open_time = (datetime)PositionGetInteger(POSITION_TIME);
datetime current_time = TimeCurrent();
int bars_open = Bars(_Symbol, PERIOD_CURRENT, position_open_time, current_time);
if(bars_open >= InpMaxBarsInTrade)
{
trade.PositionClose(_Symbol);
Print("Position closed: Max bars in trade reached (", bars_open, " bars)");
}
}
//+------------------------------------------------------------------+
+401
View File
@@ -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()
+221
View File
@@ -0,0 +1,221 @@
"""
Data Collection Script for BTCUSD RSI Divergence Training
Fetches BTCUSD data from MetaTrader 5 and labels it with RSI divergence signals.
"""
import argparse
import os
import sys
from datetime import datetime, timedelta
import pandas as pd
import numpy as np
import MetaTrader5 as mt5
from rsi_divergence_detector import RSIDivergenceDetector, DivergenceType
import pickle
from tqdm import tqdm
def fetch_mt5_data(symbol: str, timeframe: int, start_date: datetime, end_date: datetime) -> pd.DataFrame:
"""
Fetch historical data from MetaTrader 5.
Args:
symbol: Trading symbol (e.g., 'BTCUSD')
timeframe: MT5 timeframe constant
start_date: Start date for data
end_date: End date for data
Returns:
DataFrame with OHLCV data
"""
print(f"Fetching {symbol} data from {start_date} to {end_date}...")
if not mt5.initialize():
raise RuntimeError(f"MT5 initialization failed: {mt5.last_error()}")
try:
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} in the specified date range")
df = pd.DataFrame(rates)
df['time'] = pd.to_datetime(df['time'], unit='s')
df.set_index('time', inplace=True)
# Rename columns to lowercase
df.columns = [col.lower() for col in df.columns]
print(f"Fetched {len(df)} bars")
return df
finally:
mt5.shutdown()
def prepare_features(df: pd.DataFrame) -> pd.DataFrame:
"""
Prepare additional features for training.
Args:
df: DataFrame with OHLCV data
Returns:
DataFrame with additional features
"""
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
)
return feature_df
def create_sequences(df: pd.DataFrame, lookback: int = 60, prediction_horizon: int = 5) -> tuple:
"""
Create sequences for training.
Args:
df: Labeled DataFrame
lookback: Number of bars to look back
prediction_horizon: Number of bars ahead to predict
Returns:
Tuple of (X, y) where X is features and y is labels
"""
# Feature columns (exclude labels and time-based columns)
exclude_cols = ['divergence_type', 'divergence_confidence', 'divergence_strength', 'time']
feature_cols = [col for col in df.columns if col not in exclude_cols]
X, y = [], []
for i in range(lookback, len(df) - prediction_horizon):
# Get feature sequence
X.append(df[feature_cols].iloc[i - lookback:i].values)
# Get label (divergence type at current bar)
y.append(df['divergence_type'].iloc[i])
return np.array(X), np.array(y)
def main():
"""Main function."""
parser = argparse.ArgumentParser(description='Collect and label BTCUSD data for RSI divergence training')
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=365, help='Number of days of historical data')
parser.add_argument('--rsi-period', type=int, default=14, help='RSI period')
parser.add_argument('--output', type=str, default='data', help='Output directory')
parser.add_argument('--min-strength', type=float, default=0.15,
help='Minimum divergence strength (0-1)')
args = parser.parse_args()
# Convert timeframe string to MT5 constant
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 output directory
os.makedirs(args.output, exist_ok=True)
# Fetch data
end_date = datetime.now()
start_date = end_date - timedelta(days=args.days)
print(f"\n{'='*60}")
print("BTCUSD RSI Divergence Data Collection")
print(f"{'='*60}\n")
df = fetch_mt5_data(args.symbol, timeframe, start_date, end_date)
# Prepare features
print("\nPreparing features...")
df = prepare_features(df)
# Detect and label divergences
print("\nDetecting RSI divergences...")
detector = RSIDivergenceDetector(
rsi_period=args.rsi_period,
min_divergence_strength=args.min_strength
)
df = detector.label_data(df)
# Statistics
total_bars = len(df)
labeled_bars = len(df[df['divergence_type'] != DivergenceType.NONE.value])
print(f"\n{'='*60}")
print("Labeling Statistics:")
print(f"{'='*60}")
print(f"Total bars: {total_bars}")
print(f"Bars with divergence: {labeled_bars} ({labeled_bars/total_bars*100:.2f}%)")
for div_type in DivergenceType:
if div_type == DivergenceType.NONE:
continue
count = len(df[df['divergence_type'] == div_type.value])
print(f" {div_type.name}: {count} ({count/total_bars*100:.2f}%)")
# Save labeled data
output_file = os.path.join(args.output, f"{args.symbol}_{args.timeframe}_labeled.csv")
df.to_csv(output_file)
print(f"\nLabeled data saved to: {output_file}")
# Save detector parameters
detector_params = {
'rsi_period': args.rsi_period,
'min_swing_bars': detector.min_swing_bars,
'max_swing_bars': detector.max_swing_bars,
'min_divergence_strength': args.min_strength
}
params_file = os.path.join(args.output, f"{args.symbol}_{args.timeframe}_detector_params.pkl")
with open(params_file, 'wb') as f:
pickle.dump(detector_params, f)
print(f"Detector parameters saved to: {params_file}")
print(f"\n{'='*60}")
print("Data collection completed!")
print(f"{'='*60}\n")
if __name__ == '__main__':
main()
+17
View File
@@ -0,0 +1,17 @@
# ONNX and Machine Learning
onnx>=1.12.0
onnxruntime>=1.12.0
tensorflow>=2.10.0
tf2onnx>=1.13.0
# Data Processing
pandas>=1.3.0
numpy>=1.21.0
scikit-learn>=1.0.0
# MetaTrader 5 Integration
MetaTrader5>=5.0.45
# Visualization and Utilities
matplotlib>=3.4.0
tqdm>=4.64.0
@@ -0,0 +1,396 @@
"""
RSI Divergence Detection Module
Detects regular and hidden RSI divergences in price action.
Regular Divergence:
- Bullish: Price makes lower low, RSI makes higher low (reversal signal)
- Bearish: Price makes higher high, RSI makes lower high (reversal signal)
Hidden Divergence:
- Bullish: Price makes higher low, RSI makes lower low (continuation signal)
- Bearish: Price makes lower high, RSI makes higher high (continuation signal)
"""
import numpy as np
import pandas as pd
from typing import Tuple, Optional, List, Dict
from dataclasses import dataclass
from enum import Enum
class DivergenceType(Enum):
"""Types of RSI divergences"""
NONE = 0
REGULAR_BULLISH = 1 # Price lower low, RSI higher low
REGULAR_BEARISH = 2 # Price higher high, RSI lower high
HIDDEN_BULLISH = 3 # Price higher low, RSI lower low
HIDDEN_BEARISH = 4 # Price lower high, RSI higher high
@dataclass
class DivergenceSignal:
"""Represents a detected divergence signal"""
type: DivergenceType
price_swing_start: int # Index of price swing start
price_swing_end: int # Index of price swing end
rsi_swing_start: int # Index of RSI swing start
rsi_swing_end: int # Index of RSI swing end
price_start: float # Price at swing start
price_end: float # Price at swing end
rsi_start: float # RSI at swing start
rsi_end: float # RSI at swing end
strength: float # Divergence strength (0-1)
confidence: float # Confidence score (0-1)
timestamp: pd.Timestamp
class RSIDivergenceDetector:
"""
Detects RSI divergences in price data.
"""
def __init__(self, rsi_period: int = 14, min_swing_bars: int = 5,
max_swing_bars: int = 50, min_divergence_strength: float = 0.1):
"""
Initialize the RSI divergence detector.
Args:
rsi_period: Period for RSI calculation
min_swing_bars: Minimum bars for a valid swing
max_swing_bars: Maximum bars to look back for swings
min_divergence_strength: Minimum strength for valid divergence
"""
self.rsi_period = rsi_period
self.min_swing_bars = min_swing_bars
self.max_swing_bars = max_swing_bars
self.min_divergence_strength = min_divergence_strength
def calculate_rsi(self, prices: pd.Series, period: int = None) -> pd.Series:
"""
Calculate RSI indicator.
Args:
prices: Price series (typically close prices)
period: RSI period (defaults to self.rsi_period)
Returns:
RSI values
"""
if period is None:
period = self.rsi_period
delta = prices.diff()
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
# Avoid division by zero
rs = gain / (loss + 1e-10)
rsi = 100 - (100 / (1 + rs))
return rsi
def find_swings(self, data: pd.Series, lookback: int = None) -> Tuple[List[int], List[int]]:
"""
Find swing highs and lows in the data.
Args:
data: Series to find swings in (price or RSI)
lookback: Number of bars to look back (defaults to max_swing_bars)
Returns:
Tuple of (swing_highs, swing_lows) - lists of indices
"""
if lookback is None:
lookback = self.max_swing_bars
swing_highs = []
swing_lows = []
for i in range(lookback, len(data) - lookback):
# Check for swing high
is_swing_high = True
for j in range(i - lookback, i + lookback + 1):
if j != i and data.iloc[j] >= data.iloc[i]:
is_swing_high = False
break
if is_swing_high:
swing_highs.append(i)
# Check for swing low
is_swing_low = True
for j in range(i - lookback, i + lookback + 1):
if j != i and data.iloc[j] <= data.iloc[i]:
is_swing_low = False
break
if is_swing_low:
swing_lows.append(i)
return swing_highs, swing_lows
def detect_divergence(self, df: pd.DataFrame, current_index: int) -> Optional[DivergenceSignal]:
"""
Detect divergence at the current index.
Args:
df: DataFrame with 'close' and 'rsi' columns
current_index: Current bar index to check for divergence
Returns:
DivergenceSignal if found, None otherwise
"""
if current_index < self.max_swing_bars * 2:
return None
# Get price and RSI data up to current index
price_data = df['close'].iloc[:current_index + 1]
rsi_data = df['rsi'].iloc[:current_index + 1]
# Find recent swings
price_highs, price_lows = self.find_swings(price_data, self.max_swing_bars)
rsi_highs, rsi_lows = self.find_swings(rsi_data, self.max_swing_bars)
if len(price_highs) < 2 or len(price_lows) < 2:
return None
if len(rsi_highs) < 2 or len(rsi_lows) < 2:
return None
# Get the two most recent swings
current_price = price_data.iloc[current_index]
current_rsi = rsi_data.iloc[current_index]
# Check for regular bearish divergence (price higher high, RSI lower high)
if len(price_highs) >= 2 and len(rsi_highs) >= 2:
price_high_1_idx = price_highs[-1]
price_high_2_idx = price_highs[-2] if len(price_highs) >= 2 else price_highs[-1]
rsi_high_1_idx = rsi_highs[-1]
rsi_high_2_idx = rsi_highs[-2] if len(rsi_highs) >= 2 else rsi_highs[-1]
# Regular bearish: price higher high, RSI lower high
if (price_high_1_idx == current_index or abs(price_high_1_idx - current_index) <= 3):
if price_data.iloc[price_high_1_idx] > price_data.iloc[price_high_2_idx]:
if rsi_data.iloc[rsi_high_1_idx] < rsi_data.iloc[rsi_high_2_idx]:
strength = self._calculate_strength(
price_data.iloc[price_high_2_idx], price_data.iloc[price_high_1_idx],
rsi_data.iloc[rsi_high_2_idx], rsi_data.iloc[rsi_high_1_idx]
)
if strength >= self.min_divergence_strength:
return DivergenceSignal(
type=DivergenceType.REGULAR_BEARISH,
price_swing_start=price_high_2_idx,
price_swing_end=price_high_1_idx,
rsi_swing_start=rsi_high_2_idx,
rsi_swing_end=rsi_high_1_idx,
price_start=price_data.iloc[price_high_2_idx],
price_end=price_data.iloc[price_high_1_idx],
rsi_start=rsi_data.iloc[rsi_high_2_idx],
rsi_end=rsi_data.iloc[rsi_high_1_idx],
strength=strength,
confidence=self._calculate_confidence(df, price_high_1_idx, DivergenceType.REGULAR_BEARISH),
timestamp=df.index[current_index]
)
# Check for regular bullish divergence (price lower low, RSI higher low)
if len(price_lows) >= 2 and len(rsi_lows) >= 2:
price_low_1_idx = price_lows[-1]
price_low_2_idx = price_lows[-2] if len(price_lows) >= 2 else price_lows[-1]
rsi_low_1_idx = rsi_lows[-1]
rsi_low_2_idx = rsi_lows[-2] if len(rsi_lows) >= 2 else rsi_lows[-1]
# Regular bullish: price lower low, RSI higher low
if (price_low_1_idx == current_index or abs(price_low_1_idx - current_index) <= 3):
if price_data.iloc[price_low_1_idx] < price_data.iloc[price_low_2_idx]:
if rsi_data.iloc[rsi_low_1_idx] > rsi_data.iloc[rsi_low_2_idx]:
strength = self._calculate_strength(
price_data.iloc[price_low_2_idx], price_data.iloc[price_low_1_idx],
rsi_data.iloc[rsi_low_2_idx], rsi_data.iloc[rsi_low_1_idx],
reverse=True
)
if strength >= self.min_divergence_strength:
return DivergenceSignal(
type=DivergenceType.REGULAR_BULLISH,
price_swing_start=price_low_2_idx,
price_swing_end=price_low_1_idx,
rsi_swing_start=rsi_low_2_idx,
rsi_swing_end=rsi_low_1_idx,
price_start=price_data.iloc[price_low_2_idx],
price_end=price_data.iloc[price_low_1_idx],
rsi_start=rsi_data.iloc[rsi_low_2_idx],
rsi_end=rsi_data.iloc[rsi_low_1_idx],
strength=strength,
confidence=self._calculate_confidence(df, price_low_1_idx, DivergenceType.REGULAR_BULLISH),
timestamp=df.index[current_index]
)
# Check for hidden bearish divergence (price lower high, RSI higher high)
if len(price_highs) >= 2 and len(rsi_highs) >= 2:
price_high_1_idx = price_highs[-1]
price_high_2_idx = price_highs[-2] if len(price_highs) >= 2 else price_highs[-1]
rsi_high_1_idx = rsi_highs[-1]
rsi_high_2_idx = rsi_highs[-2] if len(rsi_highs) >= 2 else rsi_highs[-1]
# Hidden bearish: price lower high, RSI higher high
if (price_high_1_idx == current_index or abs(price_high_1_idx - current_index) <= 3):
if price_data.iloc[price_high_1_idx] < price_data.iloc[price_high_2_idx]:
if rsi_data.iloc[rsi_high_1_idx] > rsi_data.iloc[rsi_high_2_idx]:
strength = self._calculate_strength(
price_data.iloc[price_high_2_idx], price_data.iloc[price_high_1_idx],
rsi_data.iloc[rsi_high_2_idx], rsi_data.iloc[rsi_high_1_idx]
)
if strength >= self.min_divergence_strength:
return DivergenceSignal(
type=DivergenceType.HIDDEN_BEARISH,
price_swing_start=price_high_2_idx,
price_swing_end=price_high_1_idx,
rsi_swing_start=rsi_high_2_idx,
rsi_swing_end=rsi_high_1_idx,
price_start=price_data.iloc[price_high_2_idx],
price_end=price_data.iloc[price_high_1_idx],
rsi_start=rsi_data.iloc[rsi_high_2_idx],
rsi_end=rsi_data.iloc[rsi_high_1_idx],
strength=strength,
confidence=self._calculate_confidence(df, price_high_1_idx, DivergenceType.HIDDEN_BEARISH),
timestamp=df.index[current_index]
)
# Check for hidden bullish divergence (price higher low, RSI lower low)
if len(price_lows) >= 2 and len(rsi_lows) >= 2:
price_low_1_idx = price_lows[-1]
price_low_2_idx = price_lows[-2] if len(price_lows) >= 2 else price_lows[-1]
rsi_low_1_idx = rsi_lows[-1]
rsi_low_2_idx = rsi_lows[-2] if len(rsi_lows) >= 2 else rsi_lows[-1]
# Hidden bullish: price higher low, RSI lower low
if (price_low_1_idx == current_index or abs(price_low_1_idx - current_index) <= 3):
if price_data.iloc[price_low_1_idx] > price_data.iloc[price_low_2_idx]:
if rsi_data.iloc[rsi_low_1_idx] < rsi_data.iloc[rsi_low_2_idx]:
strength = self._calculate_strength(
price_data.iloc[price_low_2_idx], price_data.iloc[price_low_1_idx],
rsi_data.iloc[rsi_low_2_idx], rsi_data.iloc[rsi_low_1_idx],
reverse=True
)
if strength >= self.min_divergence_strength:
return DivergenceSignal(
type=DivergenceType.HIDDEN_BULLISH,
price_swing_start=price_low_2_idx,
price_swing_end=price_low_1_idx,
rsi_swing_start=rsi_low_2_idx,
rsi_swing_end=rsi_low_1_idx,
price_start=price_data.iloc[price_low_2_idx],
price_end=price_data.iloc[price_low_1_idx],
rsi_start=rsi_data.iloc[rsi_low_2_idx],
rsi_end=rsi_data.iloc[rsi_low_1_idx],
strength=strength,
confidence=self._calculate_confidence(df, price_low_1_idx, DivergenceType.HIDDEN_BULLISH),
timestamp=df.index[current_index]
)
return None
def _calculate_strength(self, price1: float, price2: float,
rsi1: float, rsi2: float, reverse: bool = False) -> float:
"""
Calculate divergence strength (0-1).
Args:
price1: First price value
price2: Second price value
rsi1: First RSI value
rsi2: Second RSI value
reverse: If True, reverse the calculation for bullish divergences
Returns:
Strength score (0-1)
"""
if price1 == 0 or price2 == 0:
return 0.0
price_change_pct = abs((price2 - price1) / price1)
rsi_change = abs(rsi2 - rsi1)
# Normalize to 0-1 range
price_strength = min(price_change_pct * 10, 1.0) # Scale price change
rsi_strength = min(rsi_change / 20.0, 1.0) # Scale RSI change (max ~20 points)
# Combined strength
strength = (price_strength + rsi_strength) / 2.0
return min(max(strength, 0.0), 1.0)
def _calculate_confidence(self, df: pd.DataFrame, signal_index: int,
divergence_type: DivergenceType) -> float:
"""
Calculate confidence score for a divergence signal.
Args:
df: DataFrame with market data
signal_index: Index where divergence was detected
divergence_type: Type of divergence
Returns:
Confidence score (0-1)
"""
confidence = 0.5 # Base confidence
# Check RSI extremes
if signal_index < len(df):
rsi = df['rsi'].iloc[signal_index]
# Higher confidence if RSI is in extreme zones
if divergence_type in [DivergenceType.REGULAR_BULLISH, DivergenceType.HIDDEN_BULLISH]:
if rsi < 30:
confidence += 0.2
elif rsi < 40:
confidence += 0.1
elif divergence_type in [DivergenceType.REGULAR_BEARISH, DivergenceType.HIDDEN_BEARISH]:
if rsi > 70:
confidence += 0.2
elif rsi > 60:
confidence += 0.1
# Check volume (if available)
if 'tick_volume' in df.columns and signal_index < len(df):
volume = df['tick_volume'].iloc[signal_index]
avg_volume = df['tick_volume'].rolling(20).mean().iloc[signal_index] if signal_index >= 20 else volume
if avg_volume > 0:
volume_ratio = volume / avg_volume
if volume_ratio > 1.2: # Higher volume increases confidence
confidence += 0.1
return min(max(confidence, 0.0), 1.0)
def label_data(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Label entire dataset with divergence signals.
Args:
df: DataFrame with 'close' column and datetime index
Returns:
DataFrame with 'divergence_type' and 'divergence_confidence' columns
"""
# Calculate RSI
if 'rsi' not in df.columns:
df['rsi'] = self.calculate_rsi(df['close'], self.rsi_period)
# Initialize labels
df['divergence_type'] = DivergenceType.NONE.value
df['divergence_confidence'] = 0.0
df['divergence_strength'] = 0.0
# Detect divergences at each point
for i in range(self.max_swing_bars * 2, len(df)):
signal = self.detect_divergence(df, i)
if signal:
df.loc[df.index[i], 'divergence_type'] = signal.type.value
df.loc[df.index[i], 'divergence_confidence'] = signal.confidence
df.loc[df.index[i], 'divergence_strength'] = signal.strength
return df
+337
View File
@@ -0,0 +1,337 @@
"""
ONNX Model Training Script for RSI Divergence Classification
Trains a neural network to identify genuine RSI divergences and exports to ONNX format.
"""
import argparse
import os
import sys
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from sklearn.preprocessing import MinMaxScaler, LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix
import tf2onnx
import onnx
import pickle
from tqdm import tqdm
class RSIDivergenceTrainer:
"""
Trainer class for creating ONNX models to classify RSI divergences.
"""
def __init__(self, lookback: int = 60, num_classes: int = 5):
"""
Initialize the trainer.
Args:
lookback: Number of bars to look back for prediction
num_classes: Number of divergence classes (5: NONE + 4 divergence types)
"""
self.lookback = lookback
self.num_classes = num_classes
self.scaler = MinMaxScaler()
self.label_encoder = LabelEncoder()
self.model = None
def load_data(self, data_path: str) -> tuple:
"""
Load labeled data from CSV file.
Args:
data_path: Path to labeled CSV file
Returns:
Tuple of (X, y) where X is features and y is labels
"""
print(f"Loading data from {data_path}...")
df = pd.read_csv(data_path, index_col=0, parse_dates=True)
# Exclude label columns from features
exclude_cols = ['divergence_type', 'divergence_confidence', 'divergence_strength']
feature_cols = [col for col in df.columns if col not in exclude_cols]
# Remove any remaining non-numeric columns
feature_cols = [col for col in feature_cols if df[col].dtype in [np.float64, np.int64, np.float32, np.int32]]
print(f"Using {len(feature_cols)} features: {feature_cols[:10]}...")
# Prepare sequences
X, y = [], []
for i in range(self.lookback, len(df)):
# Get feature sequence
X.append(df[feature_cols].iloc[i - self.lookback:i].values)
# Get label (divergence type at current bar)
y.append(int(df['divergence_type'].iloc[i]))
X = np.array(X)
y = np.array(y)
print(f"Created {len(X)} sequences")
print(f"Label distribution: {np.bincount(y)}")
return X, y, feature_cols
def prepare_data(self, X: np.ndarray, y: np.ndarray) -> tuple:
"""
Prepare and scale data for training.
Args:
X: Feature sequences
y: Labels
Returns:
Tuple of (X_scaled, y_encoded, X_train, X_test, y_train, y_test)
"""
# Scale features
print("Scaling features...")
original_shape = X.shape
X_reshaped = X.reshape(-1, X.shape[-1])
X_scaled = self.scaler.fit_transform(X_reshaped)
X_scaled = X_scaled.reshape(original_shape)
# Encode labels (already integers, but ensure they're 0-4)
y_encoded = y.astype(int)
# Split data (no shuffle to preserve temporal order)
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y_encoded, test_size=0.2, shuffle=False
)
print(f"Training samples: {len(X_train)}")
print(f"Test samples: {len(X_test)}")
return X_scaled, y_encoded, X_train, X_test, y_train, y_test
def build_model(self, input_shape: tuple) -> keras.Model:
"""
Build the neural network model for classification.
Args:
input_shape: Shape of input data (lookback, features)
Returns:
Compiled Keras model
"""
model = keras.Sequential([
# LSTM layers for sequence learning
layers.LSTM(128, return_sequences=True, input_shape=input_shape),
layers.Dropout(0.3),
layers.LSTM(64, return_sequences=True),
layers.Dropout(0.3),
layers.LSTM(32),
layers.Dropout(0.3),
# Dense layers for classification
layers.Dense(64, activation='relu'),
layers.Dropout(0.2),
layers.Dense(32, activation='relu'),
layers.Dropout(0.2),
layers.Dense(self.num_classes, activation='softmax') # Multi-class classification
])
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
return model
def train(self, X_train: np.ndarray, y_train: np.ndarray,
X_test: np.ndarray, y_test: np.ndarray,
epochs: int = 50, batch_size: int = 32, verbose: int = 1):
"""
Train the model.
Args:
X_train: Training features
y_train: Training labels
X_test: Test features
y_test: Test labels
epochs: Number of training epochs
batch_size: Batch size for training
verbose: Verbosity level
"""
# Build model
self.model = self.build_model((X_train.shape[1], X_train.shape[2]))
print("\nModel architecture:")
self.model.summary()
# Handle class imbalance with class weights
from sklearn.utils.class_weight import compute_class_weight
class_weights = compute_class_weight(
'balanced',
classes=np.unique(y_train),
y=y_train
)
class_weight_dict = {i: weight for i, weight in enumerate(class_weights)}
print(f"\nClass weights: {class_weight_dict}")
# Train model
print("\nTraining model...")
history = self.model.fit(
X_train, y_train,
batch_size=batch_size,
epochs=epochs,
validation_data=(X_test, y_test),
verbose=verbose,
class_weight=class_weight_dict,
callbacks=[
keras.callbacks.EarlyStopping(
monitor='val_loss',
patience=15,
restore_best_weights=True,
verbose=1
),
keras.callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.5,
patience=5,
min_lr=0.0001,
verbose=1
)
]
)
# Evaluate
train_loss, train_acc = self.model.evaluate(X_train, y_train, verbose=0)
test_loss, test_acc = self.model.evaluate(X_test, y_test, verbose=0)
print(f"\nTraining - Loss: {train_loss:.4f}, Accuracy: {train_acc:.4f}")
print(f"Test - Loss: {test_loss:.4f}, Accuracy: {test_acc:.4f}")
# Classification report
y_pred = self.model.predict(X_test, verbose=0)
y_pred_classes = np.argmax(y_pred, axis=1)
print("\nClassification Report:")
print(classification_report(y_test, y_pred_classes,
target_names=['NONE', 'REGULAR_BULLISH', 'REGULAR_BEARISH',
'HIDDEN_BULLISH', 'HIDDEN_BEARISH']))
return history
def export_to_onnx(self, output_path: str, num_features: int):
"""
Export the trained model to ONNX format.
Args:
output_path: Path to save ONNX model
num_features: Number of input features
"""
if self.model is None:
raise ValueError("Model must be trained before exporting")
print(f"\nExporting model to ONNX format: {output_path}")
# Create functional model from Sequential
input_layer = keras.Input(shape=(self.lookback, num_features), name="input")
x = input_layer
# Rebuild model as functional
for layer in self.model.layers:
x = layer(x)
functional_model = keras.Model(inputs=input_layer, outputs=x)
# Convert to ONNX
spec = (tf.TensorSpec((None, self.lookback, num_features), tf.float32, name="input"),)
try:
onnx_model_proto, _ = tf2onnx.convert.from_keras(
functional_model,
input_signature=spec,
opset=13
)
onnx.save_model(onnx_model_proto, output_path)
print(f"ONNX model saved to: {output_path}")
# Verify ONNX model
onnx_model = onnx.load(output_path)
onnx.checker.check_model(onnx_model)
print("ONNX model validation passed")
except Exception as e:
raise RuntimeError(f"Failed to export ONNX model: {str(e)}")
def save_scaler(self, output_path: str):
"""Save the scaler for consistent normalization."""
with open(output_path, 'wb') as f:
pickle.dump(self.scaler, f)
print(f"Scaler saved to: {output_path}")
def main():
"""Main function."""
parser = argparse.ArgumentParser(description='Train ONNX model for RSI divergence classification')
parser.add_argument('--data', type=str, required=True,
help='Path to labeled CSV data file')
parser.add_argument('--lookback', type=int, default=60,
help='Number of bars to look back')
parser.add_argument('--epochs', type=int, default=50, help='Training epochs')
parser.add_argument('--batch-size', type=int, default=32, help='Batch size')
parser.add_argument('--output', type=str, default='models',
help='Output directory for ONNX model')
args = parser.parse_args()
# Create output directory
os.makedirs(args.output, exist_ok=True)
# Create trainer
trainer = RSIDivergenceTrainer(lookback=args.lookback)
try:
# Load data
X, y, feature_cols = trainer.load_data(args.data)
# Prepare data
X_scaled, y_encoded, X_train, X_test, y_train, y_test = trainer.prepare_data(X, y)
# Train model
trainer.train(X_train, y_train, X_test, y_test,
epochs=args.epochs, batch_size=args.batch_size)
# Export to ONNX
num_features = len(feature_cols)
model_name = "BTCUSD_H1_rsi_divergence_model.onnx"
output_path = os.path.join(args.output, model_name)
trainer.export_to_onnx(output_path, num_features)
# Save scaler
scaler_name = "BTCUSD_H1_rsi_divergence_scaler.pkl"
scaler_path = os.path.join(args.output, scaler_name)
trainer.save_scaler(scaler_path)
# Save feature list
features_name = "BTCUSD_H1_rsi_divergence_features.pkl"
features_path = os.path.join(args.output, features_name)
with open(features_path, 'wb') as f:
pickle.dump(feature_cols, f)
print(f"Feature list saved to: {features_path}")
print(f"\n{'='*60}")
print("Training completed successfully!")
print(f"ONNX model saved to: {output_path}")
print(f"{'='*60}\n")
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()