//+------------------------------------------------------------------+
//| 39_MT5_Level2_DOM_Monitor_v1_00.mq5                             |
//| Shows Level 1 quotes and audits broker-provided Depth of Market. |
//+------------------------------------------------------------------+
#property copyright "FX Omoshiro Lab"
#property link      "https://fx-omoshiro-lab.com/"
#property version   "1.00"
#property strict
#property indicator_chart_window
#property indicator_plots   0
#property indicator_buffers 14

input int  InpLevelsToShow = 5;
input bool InpShowPanel    = true;

#define OBJECT_PREFIX "MQL39_DOM_"

double g_ready[];
double g_max_depth[];
double g_subscribed_buffer[];
double g_subscribe_error_buffer[];
double g_entries[];
double g_bid_levels[];
double g_ask_levels[];
double g_buy_market_levels[];
double g_sell_market_levels[];
double g_level1_bid[];
double g_level1_ask[];
double g_spread_points[];
double g_book_events_buffer[];
double g_snapshot_success_buffer[];

bool g_subscription_attempted = false;
bool g_subscribed = false;
int g_subscribe_error = 0;
ulong g_book_events = 0;
bool g_snapshot_success = false;
MqlBookInfo g_book[];
MqlTick g_tick;

bool RefreshSnapshot();
void AnalyzeBook(const MqlBookInfo &book[], int &bid_count, int &ask_count,
                 int &buy_market_count, int &sell_market_count,
                 double &bid_prices[], double &bid_volumes[],
                 double &ask_prices[], double &ask_volumes[]);
void AddLevel(double &prices[], double &volumes[], const double price, const double volume);
void SortLevels(double &prices[], double &volumes[], const bool descending);
void UpdateBuffers();
void DrawPanel();
void SetPanelLine(const int row, const string text, const color text_color, const int font_size = 10);
void DeleteObjects();
string BookStatusText();

int OnInit()
{
   if(InpLevelsToShow < 1 || InpLevelsToShow > 10)
   {
      Print("Level II monitor: InpLevelsToShow must be between 1 and 10");
      return INIT_PARAMETERS_INCORRECT;
   }

   SetIndexBuffer(0, g_ready, INDICATOR_CALCULATIONS);
   SetIndexBuffer(1, g_max_depth, INDICATOR_CALCULATIONS);
   SetIndexBuffer(2, g_subscribed_buffer, INDICATOR_CALCULATIONS);
   SetIndexBuffer(3, g_subscribe_error_buffer, INDICATOR_CALCULATIONS);
   SetIndexBuffer(4, g_entries, INDICATOR_CALCULATIONS);
   SetIndexBuffer(5, g_bid_levels, INDICATOR_CALCULATIONS);
   SetIndexBuffer(6, g_ask_levels, INDICATOR_CALCULATIONS);
   SetIndexBuffer(7, g_buy_market_levels, INDICATOR_CALCULATIONS);
   SetIndexBuffer(8, g_sell_market_levels, INDICATOR_CALCULATIONS);
   SetIndexBuffer(9, g_level1_bid, INDICATOR_CALCULATIONS);
   SetIndexBuffer(10, g_level1_ask, INDICATOR_CALCULATIONS);
   SetIndexBuffer(11, g_spread_points, INDICATOR_CALCULATIONS);
   SetIndexBuffer(12, g_book_events_buffer, INDICATOR_CALCULATIONS);
   SetIndexBuffer(13, g_snapshot_success_buffer, INDICATOR_CALCULATIONS);

   ArraySetAsSeries(g_ready, true);
   ArraySetAsSeries(g_max_depth, true);
   ArraySetAsSeries(g_subscribed_buffer, true);
   ArraySetAsSeries(g_subscribe_error_buffer, true);
   ArraySetAsSeries(g_entries, true);
   ArraySetAsSeries(g_bid_levels, true);
   ArraySetAsSeries(g_ask_levels, true);
   ArraySetAsSeries(g_buy_market_levels, true);
   ArraySetAsSeries(g_sell_market_levels, true);
   ArraySetAsSeries(g_level1_bid, true);
   ArraySetAsSeries(g_level1_ask, true);
   ArraySetAsSeries(g_spread_points, true);
   ArraySetAsSeries(g_book_events_buffer, true);
   ArraySetAsSeries(g_snapshot_success_buffer, true);

   IndicatorSetString(INDICATOR_SHORTNAME, "MT5 Level II DOM Monitor");
   ResetLastError();
   g_subscribed = MarketBookAdd(_Symbol);
   g_subscribe_error = g_subscribed ? 0 : GetLastError();
   g_subscription_attempted = true;
   PrintFormat("Level II monitor: MarketBookAdd symbol=%s subscribed=%s max_depth=%d error=%d",
               _Symbol, g_subscribed ? "true" : "false",
               (int)SymbolInfoInteger(_Symbol, SYMBOL_TICKS_BOOKDEPTH), g_subscribe_error);
   RefreshSnapshot();
   EventSetTimer(1);
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   EventKillTimer();
   if(g_subscribed)
   {
      ResetLastError();
      if(!MarketBookRelease(_Symbol))
         PrintFormat("Level II monitor: MarketBookRelease failed symbol=%s error=%d",
                     _Symbol, GetLastError());
      g_subscribed = false;
   }
   DeleteObjects();
}

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   if(rates_total < 1)
      return 0;
   RefreshSnapshot();
   UpdateBuffers();
   if(InpShowPanel)
      DrawPanel();
   return rates_total;
}

void OnTimer()
{
   RefreshSnapshot();
   UpdateBuffers();
   if(InpShowPanel)
      DrawPanel();
}

void OnBookEvent(const string &symbol)
{
   if(!g_subscribed || symbol != _Symbol)
      return;
   ++g_book_events;
   RefreshSnapshot();
   UpdateBuffers();
   if(InpShowPanel)
      DrawPanel();
}

bool RefreshSnapshot()
{
   ResetLastError();
   if(!SymbolInfoTick(_Symbol, g_tick))
   {
      PrintFormat("Level II monitor: SymbolInfoTick failed symbol=%s error=%d",
                  _Symbol, GetLastError());
      return false;
   }

   ArrayResize(g_book, 0);
   g_snapshot_success = false;
   if(!g_subscribed)
      return true;

   ResetLastError();
   if(!MarketBookGet(_Symbol, g_book))
   {
      const int error = GetLastError();
      if(error != 0)
         PrintFormat("Level II monitor: MarketBookGet failed symbol=%s error=%d", _Symbol, error);
      ArrayResize(g_book, 0);
      return true;
   }
   g_snapshot_success = true;
   return true;
}

void AnalyzeBook(const MqlBookInfo &book[], int &bid_count, int &ask_count,
                 int &buy_market_count, int &sell_market_count,
                 double &bid_prices[], double &bid_volumes[],
                 double &ask_prices[], double &ask_volumes[])
{
   bid_count = 0;
   ask_count = 0;
   buy_market_count = 0;
   sell_market_count = 0;
   ArrayResize(bid_prices, 0);
   ArrayResize(bid_volumes, 0);
   ArrayResize(ask_prices, 0);
   ArrayResize(ask_volumes, 0);

   const int total = ArraySize(book);
   for(int index = 0; index < total; ++index)
   {
      if(book[index].type == BOOK_TYPE_BUY)
      {
         ++bid_count;
         AddLevel(bid_prices, bid_volumes, book[index].price, book[index].volume_real);
      }
      else if(book[index].type == BOOK_TYPE_SELL)
      {
         ++ask_count;
         AddLevel(ask_prices, ask_volumes, book[index].price, book[index].volume_real);
      }
      else if(book[index].type == BOOK_TYPE_BUY_MARKET)
         ++buy_market_count;
      else if(book[index].type == BOOK_TYPE_SELL_MARKET)
         ++sell_market_count;
   }
   SortLevels(bid_prices, bid_volumes, true);
   SortLevels(ask_prices, ask_volumes, false);
}

void AddLevel(double &prices[], double &volumes[], const double price, const double volume)
{
   const int size = ArraySize(prices);
   if(ArrayResize(prices, size + 1) != size + 1 ||
      ArrayResize(volumes, size + 1) != size + 1)
   {
      Print("Level II monitor: could not resize level arrays");
      return;
   }
   prices[size] = price;
   volumes[size] = volume;
}

void SortLevels(double &prices[], double &volumes[], const bool descending)
{
   const int size = ArraySize(prices);
   if(size != ArraySize(volumes))
      return;
   for(int left = 0; left < size - 1; ++left)
   {
      for(int right = left + 1; right < size; ++right)
      {
         const bool swap_needed = descending ? prices[right] > prices[left]
                                             : prices[right] < prices[left];
         if(!swap_needed)
            continue;
         const double price = prices[left];
         const double volume = volumes[left];
         prices[left] = prices[right];
         volumes[left] = volumes[right];
         prices[right] = price;
         volumes[right] = volume;
      }
   }
}

void UpdateBuffers()
{
   if(ArraySize(g_ready) < 1)
      return;

   int bids = 0;
   int asks = 0;
   int buy_market = 0;
   int sell_market = 0;
   double bid_prices[];
   double bid_volumes[];
   double ask_prices[];
   double ask_volumes[];
   AnalyzeBook(g_book, bids, asks, buy_market, sell_market,
               bid_prices, bid_volumes, ask_prices, ask_volumes);

   const double spread = g_tick.ask >= g_tick.bid && _Point > 0.0
                         ? (g_tick.ask - g_tick.bid) / _Point : 0.0;
   g_ready[0] = g_subscription_attempted && g_tick.bid > 0.0 && g_tick.ask > 0.0 ? 1.0 : 0.0;
   g_max_depth[0] = (double)SymbolInfoInteger(_Symbol, SYMBOL_TICKS_BOOKDEPTH);
   g_subscribed_buffer[0] = g_subscribed ? 1.0 : 0.0;
   g_subscribe_error_buffer[0] = (double)g_subscribe_error;
   g_entries[0] = (double)ArraySize(g_book);
   g_bid_levels[0] = (double)bids;
   g_ask_levels[0] = (double)asks;
   g_buy_market_levels[0] = (double)buy_market;
   g_sell_market_levels[0] = (double)sell_market;
   g_level1_bid[0] = g_tick.bid;
   g_level1_ask[0] = g_tick.ask;
   g_spread_points[0] = spread;
   g_book_events_buffer[0] = (double)g_book_events;
   g_snapshot_success_buffer[0] = g_snapshot_success ? 1.0 : 0.0;
}

void DrawPanel()
{
   int bids = 0;
   int asks = 0;
   int buy_market = 0;
   int sell_market = 0;
   double bid_prices[];
   double bid_volumes[];
   double ask_prices[];
   double ask_volumes[];
   AnalyzeBook(g_book, bids, asks, buy_market, sell_market,
               bid_prices, bid_volumes, ask_prices, ask_volumes);

   const string background = OBJECT_PREFIX + "Panel";
   if(ObjectFind(0, background) < 0)
      ObjectCreate(0, background, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, background, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, background, OBJPROP_XDISTANCE, 24);
   ObjectSetInteger(0, background, OBJPROP_YDISTANCE, 42);
   ObjectSetInteger(0, background, OBJPROP_XSIZE, 760);
   ObjectSetInteger(0, background, OBJPROP_YSIZE, 410);
   ObjectSetInteger(0, background, OBJPROP_BGCOLOR, C'6,18,12');
   ObjectSetInteger(0, background, OBJPROP_BORDER_COLOR, clrDarkGreen);
   ObjectSetInteger(0, background, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, background, OBJPROP_HIDDEN, true);

   const int max_depth = (int)SymbolInfoInteger(_Symbol, SYMBOL_TICKS_BOOKDEPTH);
   const double spread = g_tick.ask >= g_tick.bid && _Point > 0.0
                         ? (g_tick.ask - g_tick.bid) / _Point : 0.0;
   SetPanelLine(0, "MT5 LEVEL II / DEPTH OF MARKET MONITOR", clrLime, 14);
   SetPanelLine(1, StringFormat("Symbol=%s   provider max depth=%d", _Symbol, max_depth), clrWhite);
   SetPanelLine(2, StringFormat("LEVEL 1   Bid=%.*f   Ask=%.*f   spread=%.1f points",
                               _Digits, g_tick.bid, _Digits, g_tick.ask, spread), clrAqua);
   SetPanelLine(3, StringFormat("Subscription=%s   error=%d   BookEvents=%I64u",
                               g_subscribed ? "OPEN" : "FAILED",
                               g_subscribe_error, g_book_events),
                g_subscribed ? clrLime : clrYellow);
   SetPanelLine(4, StringFormat("Snapshot=%s   entries=%d   bids=%d   asks=%d   market=%d/%d",
                               g_snapshot_success ? "OK" : "NONE", ArraySize(g_book),
                               bids, asks, buy_market, sell_market), clrWhite);
   SetPanelLine(5, BookStatusText(), ArraySize(g_book) > 0 ? clrLime : clrYellow, 11);

   int row = 6;
   if(ArraySize(g_book) > 0)
   {
      SetPanelLine(row++, "ASK LEVELS (best first)              BID LEVELS (best first)", clrSilver, 9);
      const int rows = MathMin(InpLevelsToShow,
                               MathMax(ArraySize(ask_prices), ArraySize(bid_prices)));
      for(int index = 0; index < rows; ++index)
      {
         const string ask_text = index < ArraySize(ask_prices)
                                 ? StringFormat("A%d  %.*f  vol=%.2f", index + 1, _Digits,
                                                ask_prices[index], ask_volumes[index])
                                 : "";
         const string bid_text = index < ArraySize(bid_prices)
                                 ? StringFormat("B%d  %.*f  vol=%.2f", index + 1, _Digits,
                                                bid_prices[index], bid_volumes[index])
                                 : "";
         SetPanelLine(row++, StringFormat("%-34s %s", ask_text, bid_text),
                      index < ArraySize(ask_prices) ? clrTomato : clrLime, 9);
      }
   }
   else
   {
      SetPanelLine(row++, "No order-book rows were delivered for this symbol/account.", clrWhite, 10);
      SetPanelLine(row++, "No DOM rows does NOT mean no market liquidity.", clrYellow, 10);
      SetPanelLine(row++, "It only describes the data exposed by the connected broker.", clrSilver, 10);
   }
   SetPanelLine(12, "MONITOR ONLY / NO ORDER ENTRY", clrAqua, 10);
   ChartRedraw(0);
}

void SetPanelLine(const int row, const string text, const color text_color, const int font_size)
{
   const string name = OBJECT_PREFIX + "Line_" + IntegerToString(row);
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 44);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, 58 + row * 28);
   ObjectSetInteger(0, name, OBJPROP_COLOR, text_color);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, font_size);
   ObjectSetString(0, name, OBJPROP_FONT, "Consolas");
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
}

void DeleteObjects()
{
   ObjectsDeleteAll(0, OBJECT_PREFIX);
}

string BookStatusText()
{
   if(ArraySize(g_book) > 0)
      return "LEVEL 2 AVAILABLE - BROKER IS DELIVERING ORDER-BOOK ROWS";
   if((int)SymbolInfoInteger(_Symbol, SYMBOL_TICKS_BOOKDEPTH) <= 0)
      return "LEVEL 1 ONLY - PROVIDER DECLARES NO ORDER-BOOK QUEUE";
   if(!g_subscribed)
      return "LEVEL 2 UNAVAILABLE - SUBSCRIPTION WAS REJECTED";
   return "LEVEL 2 SUBSCRIBED - WAITING FOR A BOOK SNAPSHOT";
}
