//+------------------------------------------------------------------+
//|                                                     Graph NN.mq5 |
//|                        GIT under Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/en/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "GIT under Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/johnhlomohang/"
#property version   "1.00"
#property strict

#resource "\\Files\\liquidity_gnn.onnx" as uchar ExtModel[]

#include <Trade/Trade.mqh>
#include <ONNXRuntime/ONNXRuntime.mqh>

//--- trading
CTrade trade;

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input          string            ModelFile="liquidity_gnn.onnx";
input          int               SwingWindow=3;
input          int               BarsToProcess=500;
input          int               MaxNodes=50;
input          int               MaxEdges=100;
input          double            MinLiquidityScore=0.7;
input          double            LotSize=0.1;
input          int               MagicNumber=777;
input          bool              UseStopLoss=true;
input          int               StopLossPoints=200;
input          bool              UseTakeProfit=true;
input          int               TakeProfitPoints=400;

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+

double               node_features[];
long                 edge_index[];
double               predictions[];
double               nodePrices[];
int                  swingHighs[];
int                  swingLows[];
int                  nodeCount=0;
int                  actualEdgeCount=0;
datetime             lastBarTime=0;
//--- scaler values
double               scalerMean[3]= {1950,500,0};
double               scalerStd[3]= {100,200,1};
long                 onnx_handle;
//--- Model dimensions from ONNX
long                 model_nodes = 0;
long                 model_features = 0;
long                 model_edges = 0;
long                 model_classes = 0;

//+------------------------------------------------------------------+
//| Expert initialization                                            |
//+------------------------------------------------------------------+
int OnInit()
  {
   trade.SetExpertMagicNumber(MagicNumber);

   onnx_handle = OnnxCreateFromBuffer(ExtModel, ONNX_DEFAULT);

   if(onnx_handle == INVALID_HANDLE)
     {
      Print("ONNX create failed: ", GetLastError());
      return INIT_FAILED;
     }

   Print("ONNX model loaded");

   OnnxTypeInfo type_info;

   long input_count = OnnxGetInputCount(onnx_handle);
   Print("Model inputs: ", input_count);

   for(long i = 0; i < input_count; i++)
     {
      string name = OnnxGetInputName(onnx_handle, i);
      Print("Input ", i, " name: ", name);

      if(OnnxGetInputTypeInfo(onnx_handle, i, type_info))
        {
         Print("Input ", i, " dimensions:");
         ArrayPrint(type_info.tensor.dimensions);

         //--- Store model dimensions
         if(name == "node_features")
           {
            if(ArraySize(type_info.tensor.dimensions) >= 2)
              {
               model_nodes = type_info.tensor.dimensions[0];
               model_features = type_info.tensor.dimensions[1];
               Print("Model expects node_features: [", model_nodes, ", ", model_features, "]");
              }
           }
         else
            if(name == "edge_index")
              {
               if(ArraySize(type_info.tensor.dimensions) >= 2)
                 {
                  model_edges = type_info.tensor.dimensions[1];
                  Print("Model expects edge_index: [2, ", model_edges, "]");
                 }
              }
        }
     }

   long output_count = OnnxGetOutputCount(onnx_handle);
   Print("Model outputs: ", output_count);

   for(long i = 0; i < output_count; i++)
     {
      string name = OnnxGetOutputName(onnx_handle, i);
      Print("Output ", i, " name: ", name);

      if(OnnxGetOutputTypeInfo(onnx_handle, i, type_info))
        {
         Print("Output ", i, " dimensions:");
         ArrayPrint(type_info.tensor.dimensions);

         if(ArraySize(type_info.tensor.dimensions) >= 2)
           {
            model_classes = type_info.tensor.dimensions[1];
            Print("Model output classes: ", model_classes);
           }
        }
     }

//--- Don't set fixed shapes - the model has dynamic axes
//--- Instead, we'll use the actual node count from detection

   Print("ONNX initialization complete");
   Print("Model expects: nodes=", model_nodes, ", features=", model_features,
         ", edges=", model_edges, ", classes=", model_classes);

   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Print("EA deinitializing. Reason code: ", reason);

//--- Release ONNX model session
   if(onnx_handle != INVALID_HANDLE)
     {
      OnnxRelease(onnx_handle);
      onnx_handle = INVALID_HANDLE;
      Print("ONNX session released");
     }

//--- Delete all objects created by this EA
//--- Delete liquidity objects on the chart
   int deleted = 0;
   for(int i = ObjectsTotal(0, -1, OBJ_RECTANGLE) - 1; i >= 0; i--)
     {
      string name = ObjectName(0, i);
      if(StringFind(name, "Liq_") == 0) // Objects starting with "Liq_"
        {
         ObjectDelete(0, name);
         deleted++;
        }
     }

   if(deleted > 0)
      Print("Deleted ", deleted, " rectangle objects");

//--- Optionally close all open positions (if you want this behavior)
   bool closePositions = false; // Set to true if you want to close all positions on deinit

   if(closePositions)
     {
      int closed = 0;
      for(int i = PositionsTotal() - 1; i >= 0; i--)
        {
         ulong ticket = PositionGetTicket(i);
         if(PositionSelectByTicket(ticket))
           {
            if(PositionGetInteger(POSITION_MAGIC) == MagicNumber)
              {
               trade.PositionClose(ticket);
               closed++;
              }
           }
        }

      if(closed > 0)
         Print("Closed ", closed, " positions");
     }

//--- Clear global arrays to free memory
   ArrayFree(node_features);
   ArrayFree(edge_index);
   ArrayFree(predictions);
   ArrayFree(nodePrices);
   ArrayFree(swingHighs);
   ArrayFree(swingLows);

   Print("EA deinitialization complete");
  }

//+------------------------------------------------------------------+
//| Expert tick                                                      |
//+------------------------------------------------------------------+
void OnTick()
  {
   datetime currentBar = iTime(_Symbol, _Period, 0);

   if(currentBar == lastBarTime)
      return;

   lastBarTime = currentBar;

   if(!DetectSwings())
      return;

   if(!BuildGraph())
      return;

   if(!RunInference())
      return;

   ExecuteTrades();
  }

//+------------------------------------------------------------------+
//| Detect swing highs/lows                                          |
//+------------------------------------------------------------------+
bool DetectSwings()
  {
   ArrayResize(swingHighs, 0);
   ArrayResize(swingLows, 0);

   double high[], low[];

   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);

   if(CopyHigh(_Symbol, _Period, 0, BarsToProcess, high) <= 0)
     {
      Print("Failed to copy high data");
      return false;
     }

   if(CopyLow(_Symbol, _Period, 0, BarsToProcess, low) <= 0)
     {
      Print("Failed to copy low data");
      return false;
     }

   for(int i = SwingWindow; i < BarsToProcess - SwingWindow; i++)
     {
      bool swingHigh = true;
      bool swingLow = true;

      for(int j = -SwingWindow; j <= SwingWindow; j++)
        {
         if(j == 0)
            continue;

         if(high[i] <= high[i + j])
            swingHigh = false;

         if(low[i] >= low[i + j])
            swingLow = false;
        }

      if(swingHigh)
        {
         int s = ArraySize(swingHighs);
         ArrayResize(swingHighs, s + 1);
         swingHighs[s] = i;
        }

      if(swingLow)
        {
         int s = ArraySize(swingLows);
         ArrayResize(swingLows, s + 1);
         swingLows[s] = i;
        }
     }

   Print("Detected ", ArraySize(swingHighs), " swing highs and ",
         ArraySize(swingLows), " swing lows");

   return true;
  }

//+------------------------------------------------------------------+
//| Build graph                                                      |
//+------------------------------------------------------------------+
bool BuildGraph()
  {
   double close[];
   long volume[];

   ArraySetAsSeries(close, true);
   ArraySetAsSeries(volume, true);

   if(CopyClose(_Symbol, _Period, 0, BarsToProcess, close) <= 0)
     {
      Print("Failed to copy close data");
      return false;
     }

   if(CopyTickVolume(_Symbol, _Period, 0, BarsToProcess, volume) <= 0)
     {
      Print("Failed to copy volume data");
      return false;
     }

   nodeCount = 0;
   ArrayResize(nodePrices, MaxNodes);

//--- swing highs
   for(int i = 0; i < ArraySize(swingHighs) && nodeCount < MaxNodes; i++)
     {
      int bar = swingHighs[i];
      if(bar < BarsToProcess)
        {
         nodePrices[nodeCount] = close[bar];
         nodeCount++;
        }
     }

//--- swing lows
   for(int i = 0; i < ArraySize(swingLows) && nodeCount < MaxNodes; i++)
     {
      int bar = swingLows[i];
      if(bar < BarsToProcess)
        {
         nodePrices[nodeCount] = close[bar];
         nodeCount++;
        }
     }

   if(nodeCount < 2)
     {
      Print("Not enough nodes: ", nodeCount);
      return false;
     }

   Print("Building graph with ", nodeCount, " nodes");

//--- Build node features [nodes, 3]
   ArrayResize(node_features, nodeCount * 3);
   ArrayInitialize(node_features, 0);

   int f = 0;
   for(int i = 0; i < nodeCount; i++)
     {
      double price = nodePrices[i];
      double vol = 0;

      //--- Get volume for this bar
      if(i < BarsToProcess)
         vol = (double)volume[i];

      //--- Determine swing type (1 for highs, -1 for lows)
      double swingType = (i < ArraySize(swingHighs)) ? 1.0 : -1.0;

      //--- Normalize features using scaler
      node_features[f++] = (price - scalerMean[0]) / scalerStd[0];  // price
      node_features[f++] = (vol - scalerMean[1]) / scalerStd[1];    // volume
      node_features[f++] = (swingType - scalerMean[2]) / scalerStd[2]; // swing type
     }

//--- Build edges
//--- For a chain graph with bidirectional connections between consecutive nodes:
//--- Number of edges = (nodeCount - 1) * 2
   actualEdgeCount = (nodeCount - 1) * 2;

//--- Edge_index should be a flattened array of shape [2, actualEdgeCount]
//--- That means: [row0_edge0, row1_edge0, row0_edge1, row1_edge1, ...]
//--- So total array size = 2 * actualEdgeCount
   int edgeArraySize = 2 * actualEdgeCount;
   ArrayResize(edge_index, edgeArraySize);
   ArrayInitialize(edge_index, 0);

   Print("Creating ", actualEdgeCount, " edges, array size: ", edgeArraySize);

   int e = 0;
   for(int i = 0; i < nodeCount - 1; i++)
     {
      //--- Forward edge: from i to i+1
      if(e < edgeArraySize - 1)
        {
         edge_index[e] = 0;     // row 0 (source row)
         e++;
         edge_index[e] = i;      // source node index
         e++;
        }

      // Forward edge: target
      if(e < edgeArraySize - 1)
        {
         edge_index[e] = 1;      // row 1 (target row)
         e++;
         edge_index[e] = i + 1;  // target node index
         e++;
        }

      //--- Backward edge: from i+1 to i (source)
      if(e < edgeArraySize - 1)
        {
         edge_index[e] = 0;      // row 0 (source row)
         e++;
         edge_index[e] = i + 1;  // source node index
         e++;
        }

      //--- Backward edge: target
      if(e < edgeArraySize - 1)
        {
         edge_index[e] = 1;      // row 1 (target row)
         e++;
         edge_index[e] = i;      // target node index
         e++;
        }
     }

   Print("Added ", e/2, " edge entries (", e, " array elements used)");

//--- Verify we used the correct number of elements
   if(e != edgeArraySize)
     {
      Print("Warning: Expected ", edgeArraySize, " elements but used ", e);
     }

   return true;
  }


//+------------------------------------------------------------------+
//| Run ONNX inference                                               |
//+------------------------------------------------------------------+
bool RunInference()
  {
//--- Set dynamic shapes before inference
   long input0_shape[] = {nodeCount, 3};
   if(!OnnxSetInputShape(onnx_handle, 0, input0_shape))
     {
      Print("Failed to set input shape for node_features: ", GetLastError());
      return false;
     }

//--- actualEdgeCount is the number of edges (not the array size)
//--- Edge_index shape is [2, actualEdgeCount]
   long input1_shape[] = {2, actualEdgeCount};
   if(!OnnxSetInputShape(onnx_handle, 1, input1_shape))
     {
      Print("Failed to set input shape for edge_index: ", GetLastError());
      return false;
     }

   long output_shape[] = {nodeCount, 2};
   if(!OnnxSetOutputShape(onnx_handle, 0, output_shape))
     {
      Print("Failed to set output shape: ", GetLastError());
      return false;
     }

//--- Debug: Print array sizes
   Print("Running inference with: nodes=", nodeCount,
         ", edges=", actualEdgeCount,
         ", features array size=", ArraySize(node_features),
         ", edges array size=", ArraySize(edge_index));

//--- Run inference
   ArrayResize(predictions, nodeCount * 2);

   if(!OnnxRun(onnx_handle, ONNX_NO_CONVERSION, node_features, edge_index, predictions))
     {
      Print("ONNX inference failed: ", GetLastError());
      return false;
     }

   ProcessPredictions();

   return true;
  }

//+------------------------------------------------------------------+
//| Process predictions                                              |
//+------------------------------------------------------------------+
void ProcessPredictions()
  {
   int liquidityCount = 0;

   for(int i = 0; i < nodeCount; i++)
     {
      double p0 = predictions[i * 2];
      double p1 = predictions[i * 2 + 1];

      //--- Apply softmax manually
      double maxv = MathMax(p0, p1);
      double e0 = MathExp(p0 - maxv);
      double e1 = MathExp(p1 - maxv);
      double prob = e1 / (e0 + e1);  // Probability of class 1 (liquidity)

      if(prob > MinLiquidityScore)
        {
         liquidityCount++;
         DrawLiquidityZone(i, nodePrices[i], prob);
        }
     }

   if(liquidityCount > 0)
      Print("Found ", liquidityCount, " liquidity zones");
  }

//+------------------------------------------------------------------+
//| Execute trades                                                   |
//+------------------------------------------------------------------+
void ExecuteTrades()
  {
   if(CountPositions() > 0)
      return;

   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

   for(int i = 0; i < nodeCount; i++)
     {
      if(MathAbs(nodePrices[i] - ask) < 10 * _Point)
        {
         if(ask < nodePrices[i])
            OpenTrade(ORDER_TYPE_BUY);
         else
            OpenTrade(ORDER_TYPE_SELL);
         break;
        }
     }
  }

//+------------------------------------------------------------------+
//| Open trade                                                       |
//+------------------------------------------------------------------+
void OpenTrade(ENUM_ORDER_TYPE type)
  {
   double price, sl = 0, tp = 0;

   if(type == ORDER_TYPE_BUY)
      price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   else
      price = SymbolInfoDouble(_Symbol, SYMBOL_BID);

   if(UseStopLoss)
     {
      if(type == ORDER_TYPE_BUY)
         sl = price - StopLossPoints * _Point;
      else
         sl = price + StopLossPoints * _Point;
     }

   if(UseTakeProfit)
     {
      if(type == ORDER_TYPE_BUY)
         tp = price + TakeProfitPoints * _Point;
      else
         tp = price - TakeProfitPoints * _Point;
     }

   if(type == ORDER_TYPE_BUY)
      trade.Buy(LotSize, _Symbol, price, sl, tp);
   else
      trade.Sell(LotSize, _Symbol, price, sl, tp);
  }

//+------------------------------------------------------------------+
//| Draw zone                                                        |
//+------------------------------------------------------------------+
void DrawLiquidityZone(int id, double price, double probability)
  {
   string name = "Liq_" + IntegerToString(id) + "_" + IntegerToString(TimeCurrent());

   datetime t1 = iTime(_Symbol, _Period, 0);
   datetime t2 = t1 + PeriodSeconds(_Period) * 5;

   if(!ObjectCreate(0, name, OBJ_RECTANGLE, 0,
                    t1, price - 5 * _Point,
                    t2, price + 5 * _Point))
     {
      Print("Failed to create rectangle object");
      return;
     }

   ObjectSetInteger(0, name, OBJPROP_COLOR, clrGold);
   ObjectSetInteger(0, name, OBJPROP_FILL, true);
   ObjectSetString(0, name, OBJPROP_TEXT, "Liquidity: " + DoubleToString(probability * 100, 1) + "%");
  }


//+------------------------------------------------------------------+
//| Count positions                                                  |
//+------------------------------------------------------------------+
int CountPositions()
  {
   int count = 0;

   for(int i = 0; i < PositionsTotal(); i++)
     {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
        {
         if(PositionGetInteger(POSITION_MAGIC) == MagicNumber)
            count++;
        }
     }

   return count;
  }

//+------------------------------------------------------------------+
