//+------------------------------------------------------------------+
//|                                                      ONNX_EA.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property description "Expert Advisor using ONNX model for price prediction"
#property description "Based on: https://www.mql5.com/en/docs/onnx/onnx_prepare"

#include <Trade\Trade.mqh>

//--- Resource: Embed ONNX model in EA
// Based on: https://www.mql5.com/en/docs/onnx/onnx_test
// Path is relative to MQL5 directory (not starting with \\Files\\)
#resource "Files\\XAUUSD_H1_model.onnx" as uchar ExtModel[]

//--- Input parameters
input group "ONNX Model Settings"
input string InpModelPath = "";  // ONNX Model Path (leave empty to use embedded resource)
input int    InpLookback = 60;                              // Lookback Period (bars)
input bool   InpUsePrediction = true;                        // Use Model Prediction

input group "Trading Settings"
input double InpLotSize = 0.01;                              // Lot Size
input int    InpMagicNumber = 123456;                        // Magic Number
input int    InpSlippage = 3;                                // Slippage (points)
input bool   InpUsePredictedSLTP = true;                      // Use Predicted SL/TP (based on prediction & volatility)
input int    InpStopLoss = 50;                               // Stop Loss (pips) - used if InpUsePredictedSLTP=false
input int    InpTakeProfit = 100;                            // Take Profit (pips) - used if InpUsePredictedSLTP=false
input double InpSLMultiplier = 1.5;                           // SL Multiplier (ATR-based, e.g., 1.5 = 1.5x ATR)
input double InpTPMultiplier = 2.0;                           // TP Multiplier (ATR-based, e.g., 2.0 = 2x ATR)
input double InpMinSLATR = 0.5;                              // Minimum SL (ATR multiplier)
input double InpMinTPATR = 1.0;                              // Minimum TP (ATR multiplier)

input group "Prediction Settings"
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.1;                         // Minimum Confidence (0.1 = 10%)

//--- Global variables
CTrade trade;
long onnx_handle = INVALID_HANDLE;
datetime last_bar_time = 0;
double last_prediction = 0.0;
double last_confidence = 0.0;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    // Set trade parameters
    trade.SetExpertMagicNumber(InpMagicNumber);
    trade.SetDeviationInPoints(InpSlippage);
    trade.SetTypeFilling(ORDER_FILLING_FOK);
    
    // Load ONNX model
    // Based on: https://www.mql5.com/en/docs/onnx/onnx_test
    Print("Loading ONNX model from embedded resource...");
    
    // Create model from resource buffer
    onnx_handle = OnnxCreateFromBuffer(ExtModel, ONNX_DEBUG_LOGS);
    
    if(onnx_handle == INVALID_HANDLE)
    {
        int error = GetLastError();
        Print("ERROR: Failed to create ONNX model from resource. Error: ", error);
        Print("Make sure the model file exists at: MQL5\\Files\\XAUUSD_H1_model.onnx");
        Print("Then recompile the EA to embed it as a resource.");
        return(INIT_FAILED);
    }
    
    // Set input shape - per MQL5 documentation
    const long ExtInputShape[] = {1, InpLookback, 13}; // batch=1, lookback bars, 13 features
    if(!OnnxSetInputShape(onnx_handle, 0, ExtInputShape))
    {
        Print("OnnxSetInputShape failed, error ", GetLastError());
        OnnxRelease(onnx_handle);
        return(INIT_FAILED);
    }
    
    // Set output shape - per MQL5 documentation
    const long ExtOutputShape[] = {1, 1}; // batch=1, single output value
    if(!OnnxSetOutputShape(onnx_handle, 0, ExtOutputShape))
    {
        Print("OnnxSetOutputShape failed, error ", GetLastError());
        OnnxRelease(onnx_handle);
        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);
    }
    
    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)
    {
        return; // Still the same bar
    }
    last_bar_time = current_bar_time;
    
    // Check if we should use prediction
    if(!InpUsePrediction)
    {
        return;
    }
    
    // Prepare input data
    float input_data[];
    if(!PrepareInputData(input_data))
    {
        Print("ERROR: Failed to prepare input data");
        return;
    }
    
    // Check if input data is valid
    if(ArraySize(input_data) != InpLookback * 13)
    {
        Print("ERROR: Input data size mismatch. Expected: ", InpLookback * 13, ", Got: ", ArraySize(input_data));
        return;
    }
    
    // Convert flat array to matrixf for OnnxRun
    // Shape: [lookback, features] = [60, 13] - batch dimension is added automatically
    matrixf input_matrix;
    input_matrix.Resize(InpLookback, 13);
    
    // Fill matrix from flat array
    int idx = 0;
    for(int i = 0; i < InpLookback; i++)
    {
        for(int j = 0; j < 13; j++)
        {
            if(idx >= ArraySize(input_data))
            {
                Print("ERROR: Index out of bounds when filling matrix. idx=", idx, ", array size=", ArraySize(input_data));
                return;
            }
            input_matrix[i][j] = input_data[idx++];
        }
    }
    
    // Verify matrix is not empty
    if(input_matrix.Rows() == 0 || input_matrix.Cols() == 0)
    {
        Print("ERROR: Input matrix is empty. Rows: ", input_matrix.Rows(), ", Cols: ", input_matrix.Cols());
        return;
    }
    
    Print("Input matrix prepared: Rows=", input_matrix.Rows(), ", Cols=", input_matrix.Cols());
    
    // Run ONNX model - use matrixf and vectorf per MQL5 documentation
    vectorf output_vector(1);
    if(!RunONNXModel(input_matrix, output_vector))
    {
        Print("ERROR: Failed to run ONNX model");
        return;
    }
    
    // Get prediction
    if(output_vector.Size() == 0)
    {
        Print("ERROR: Empty output from ONNX model");
        return;
    }
    
    // Model now predicts price change percentage directly (e.g., -0.003 = -0.3%)
    double predicted_change_pct = output_vector[0];
    double current_price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    
    // Check if prediction is percentage (between -1 and 1) or absolute price (old format)
    double price_change_pct;
    double predicted_price;
    
    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;
    
    // Calculate ATR for dynamic SL/TP
    double atr_value = 0.0;
    double atr_array[];
    ArraySetAsSeries(atr_array, true);
    int atr_handle = iATR(_Symbol, PERIOD_CURRENT, 14);
    if(atr_handle != INVALID_HANDLE)
    {
        if(CopyBuffer(atr_handle, 0, 0, 1, atr_array) > 0)
        {
            atr_value = atr_array[0];
        }
        IndicatorRelease(atr_handle);
    }
    
    // Calculate predicted SL/TP based on prediction, confidence, and volatility
    double predicted_sl = 0.0;
    double predicted_tp = 0.0;
    if(InpUsePredictedSLTP && atr_value > 0)
    {
        // Calculate SL/TP based on ATR, prediction, and confidence
        double predicted_move = MathAbs(predicted_price - current_price);
        
        // SL: Based on ATR and confidence
        // Higher confidence = tighter SL, lower confidence = wider SL
        double sl_atr_mult = InpSLMultiplier / MathMax(confidence, 0.1);
        sl_atr_mult = MathMax(sl_atr_mult, InpMinSLATR);
        predicted_sl = atr_value * sl_atr_mult;
        
        // TP: Use a fraction of predicted move (not the full move)
        // Take 30-50% of predicted move as TP, but ensure minimum
        double tp_fraction = 0.3 + (confidence * 0.2); // 30-50% based on confidence
        double tp_from_prediction = predicted_move * tp_fraction;
        
        // Also calculate TP from ATR multiplier
        double tp_from_atr = atr_value * InpTPMultiplier;
        
        // Use the smaller of the two (more conservative)
        predicted_tp = MathMin(tp_from_prediction, tp_from_atr);
        predicted_tp = MathMax(predicted_tp, atr_value * InpMinTPATR); // Minimum TP
        
        // Ensure TP is at least 1.5x SL for risk/reward
        if(predicted_tp < predicted_sl * 1.5)
        {
            predicted_tp = predicted_sl * 1.5;
        }
        
        // Cap TP at maximum 80% of predicted move (don't be too greedy)
        double max_tp = predicted_move * 0.8;
        if(predicted_tp > max_tp)
        {
            predicted_tp = max_tp;
        }
    }
    
    // Log prediction
    Print("Prediction: Current=", current_price, 
          " Predicted Change=", price_change_pct, "%",
          " Predicted Price=", predicted_price,
          " Confidence=", confidence,
          " ATR=", atr_value);
    if(InpUsePredictedSLTP && predicted_sl > 0 && predicted_tp > 0)
    {
        Print("  Predicted SL=", predicted_sl, " (", predicted_sl/current_price*100, "%)",
              " Predicted TP=", predicted_tp, " (", predicted_tp/current_price*100, "%)");
    }
    
    // Check if we should trade
    if(!InpUseConfidence || confidence >= InpMinConfidence)
    {
        // Check if prediction is significant
        // price_change_pct is in percentage (e.g., 5.72 = 5.72%)
        // InpPredictionThreshold is in decimal (e.g., 0.00005 = 0.005%)
        // Convert threshold to percentage for comparison
        double threshold_pct = InpPredictionThreshold * 100.0;
        double abs_change_pct = MathAbs(price_change_pct); // Already in percentage
        
        Print("Trade Check: Change=", price_change_pct, "% Threshold=", threshold_pct, "% Confidence=", confidence);
        
        if(abs_change_pct >= threshold_pct)
        {
            // Check existing position
            if(PositionSelect(_Symbol))
            {
                // Manage existing position
                ManagePosition(predicted_price, price_change_pct);
            }
            else
            {
                // Open new position based on prediction
                if(price_change_pct > threshold_pct)
                {
                    Print(">>> Opening BUY position: Change=", price_change_pct, "% Threshold=", threshold_pct, "%");
                    OpenBuyPosition(predicted_sl, predicted_tp);
                }
                else if(price_change_pct < -threshold_pct)
                {
                    Print(">>> Opening SELL position: Change=", price_change_pct, "% Threshold=", threshold_pct, "%");
                    OpenSellPosition(predicted_sl, predicted_tp);
                }
            }
        }
        else
        {
            Print("Prediction below threshold: Change=", price_change_pct, "% < Threshold=", threshold_pct, "%");
        }
    }
    else
    {
        Print("Confidence too low: ", confidence, " < ", InpMinConfidence);
    }
}

//+------------------------------------------------------------------+
//| Prepare input data for ONNX model                                |
//+------------------------------------------------------------------+
bool PrepareInputData(float &input_array[])
{
    // We need to prepare data similar to training
    // This is a simplified version - you may need to adjust based on your model
    
    int lookback = InpLookback;
    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[];
    long volume[];  // CopyTickVolume requires long[] not double[]
    ArraySetAsSeries(open, true);
    ArraySetAsSeries(high, true);
    ArraySetAsSeries(low, true);
    ArraySetAsSeries(close, true);
    ArraySetAsSeries(volume, true);
    
    int copied_open = CopyOpen(_Symbol, PERIOD_CURRENT, 0, lookback + 50, open);
    if(copied_open < lookback)
    {
        Print("ERROR: CopyOpen failed. Got ", copied_open, " bars, need ", lookback);
        return false;
    }
    int copied_high = CopyHigh(_Symbol, PERIOD_CURRENT, 0, lookback + 50, high);
    if(copied_high < lookback)
    {
        Print("ERROR: CopyHigh failed. Got ", copied_high, " bars, need ", lookback);
        return false;
    }
    int copied_low = CopyLow(_Symbol, PERIOD_CURRENT, 0, lookback + 50, low);
    if(copied_low < lookback)
    {
        Print("ERROR: CopyLow failed. Got ", copied_low, " bars, need ", lookback);
        return false;
    }
    int copied_close = CopyClose(_Symbol, PERIOD_CURRENT, 0, lookback + 50, close);
    if(copied_close < lookback)
    {
        Print("ERROR: CopyClose failed. Got ", copied_close, " bars, need ", lookback);
        return false;
    }
    int copied_volume = CopyTickVolume(_Symbol, PERIOD_CURRENT, 0, lookback + 50, volume);
    if(copied_volume < lookback)
    {
        Print("ERROR: CopyTickVolume failed. Got ", copied_volume, " bars, need ", lookback);
        return false;
    }
    
    // Calculate indicators (simplified - you may need to match training exactly)
    double rsi[], ema20[], ema50[], atr[];
    ArraySetAsSeries(rsi, true);
    ArraySetAsSeries(ema20, true);
    ArraySetAsSeries(ema50, true);
    ArraySetAsSeries(atr, true);
    
    // Calculate RSI
    int rsi_handle = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
    if(rsi_handle == INVALID_HANDLE) return false;
    if(CopyBuffer(rsi_handle, 0, 0, lookback + 50, rsi) < lookback)
    {
        IndicatorRelease(rsi_handle);
        return false;
    }
    IndicatorRelease(rsi_handle);
    
    // Calculate EMAs
    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);
    if(ema20_handle == INVALID_HANDLE || ema50_handle == INVALID_HANDLE) return false;
    
    if(CopyBuffer(ema20_handle, 0, 0, lookback + 50, ema20) < lookback ||
       CopyBuffer(ema50_handle, 0, 0, lookback + 50, ema50) < lookback)
    {
        IndicatorRelease(ema20_handle);
        IndicatorRelease(ema50_handle);
        return false;
    }
    IndicatorRelease(ema20_handle);
    IndicatorRelease(ema50_handle);
    
    // Calculate ATR
    int atr_handle = iATR(_Symbol, PERIOD_CURRENT, 14);
    if(atr_handle == INVALID_HANDLE) return false;
    if(CopyBuffer(atr_handle, 0, 0, lookback + 50, atr) < lookback)
    {
        IndicatorRelease(atr_handle);
        return false;
    }
    IndicatorRelease(atr_handle);
    
    // 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++)
    {
        // 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)
    // ONNX expects shape [1, lookback, features]
    float reshaped[];
    ArrayResize(reshaped, 1 * lookback * features);
    ArrayCopy(reshaped, input_array);
    
    ArrayCopy(input_array, reshaped);
    
    return true;
}

//+------------------------------------------------------------------+
//| Run ONNX model                                                   |
//+------------------------------------------------------------------+
bool RunONNXModel(matrixf &input_matrix, vectorf &output_vector)
{
    if(onnx_handle == INVALID_HANDLE)
        return false;
    
    // Run model - shapes are already set in OnInit per MQL5 documentation
    // Based on: https://www.mql5.com/en/docs/onnx/onnx_test
    // OnnxRun expects matrixf and vectorf, not flat arrays
    if(!OnnxRun(onnx_handle, ONNX_DEBUG_LOGS | ONNX_NO_CONVERSION, input_matrix, output_vector))
    {
        Print("ERROR: Failed to run ONNX model. Error: ", GetLastError());
        return false;
    }
    
    return true;
}

//+------------------------------------------------------------------+
//| Open buy position                                                |
//+------------------------------------------------------------------+
void OpenBuyPosition(double predicted_sl = 0.0, double predicted_tp = 0.0)
{
    double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    double sl = 0.0;
    double tp = 0.0;
    
    if(InpUsePredictedSLTP && predicted_sl > 0 && predicted_tp > 0)
    {
        // Use predicted SL/TP
        sl = price - predicted_sl;
        tp = price + predicted_tp;
        Print("Using predicted SL/TP: SL=", sl, " TP=", tp);
    }
    else
    {
        // Use fixed SL/TP from input parameters
        sl = InpStopLoss > 0 ? price - InpStopLoss * _Point * 10 : 0;
        tp = InpTakeProfit > 0 ? price + InpTakeProfit * _Point * 10 : 0;
    }
    
    if(trade.Buy(InpLotSize, _Symbol, price, sl, tp, "ONNX Buy Signal"))
    {
        Print("Buy order opened. Ticket: ", trade.ResultOrder(), " Price: ", price, " SL: ", sl, " TP: ", tp);
    }
    else
    {
        Print("Failed to open buy order. Error: ", trade.ResultRetcodeDescription());
    }
}

//+------------------------------------------------------------------+
//| Open sell position                                               |
//+------------------------------------------------------------------+
void OpenSellPosition(double predicted_sl = 0.0, double predicted_tp = 0.0)
{
    double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    double sl = 0.0;
    double tp = 0.0;
    
    if(InpUsePredictedSLTP && predicted_sl > 0 && predicted_tp > 0)
    {
        // Use predicted SL/TP
        sl = price + predicted_sl;
        tp = price - predicted_tp;
        Print("Using predicted SL/TP: SL=", sl, " TP=", tp);
    }
    else
    {
        // Use fixed SL/TP from input parameters
        sl = InpStopLoss > 0 ? price + InpStopLoss * _Point * 10 : 0;
        tp = InpTakeProfit > 0 ? price - InpTakeProfit * _Point * 10 : 0;
    }
    
    if(trade.Sell(InpLotSize, _Symbol, price, sl, tp, "ONNX Sell Signal"))
    {
        Print("Sell order opened. Ticket: ", trade.ResultOrder(), " Price: ", price, " SL: ", sl, " TP: ", tp);
    }
    else
    {
        Print("Failed to open sell order. Error: ", trade.ResultRetcodeDescription());
    }
}

//+------------------------------------------------------------------+
//| Manage existing position                                          |
//+------------------------------------------------------------------+
void ManagePosition(double predicted_price, double price_change_pct)
{
    if(!PositionSelect(_Symbol))
        return;
    
    long position_type = PositionGetInteger(POSITION_TYPE);
    double position_open_price = PositionGetDouble(POSITION_PRICE_OPEN);
    double current_profit = PositionGetDouble(POSITION_PROFIT);
    
    // Simple management: close if prediction reverses
    if(position_type == POSITION_TYPE_BUY && price_change_pct < -InpPredictionThreshold)
    {
        // Prediction turned bearish, close long
        if(trade.PositionClose(_Symbol))
        {
            Print("Closed long position due to bearish prediction");
        }
    }
    else if(position_type == POSITION_TYPE_SELL && price_change_pct > InpPredictionThreshold)
    {
        // Prediction turned bullish, close short
        if(trade.PositionClose(_Symbol))
        {
            Print("Closed short position due to bullish prediction");
        }
    }
}
