Update
This commit is contained in:
@@ -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
|
||||
@@ -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());
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
@@ -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/)
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
Binary file not shown.
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user