Advertisement IC Markets

Custom volume and direction indicator for MT4

Share, develop, and backtest custom MQL4/MQL5 Expert Advisors, Python data-scraping scripts, trading bots, and automated market alert systems.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Custom volume and direction indicator for MT4

Post by PTScalper »

Hi scalpers,

i would like to share my custom indicator for MetaTrader 4, because once you are scalping, you can have opened up to several hundreds of positions at that real time, so to be able to see direction and volume, plus Actual profit/loss per pair is very usefull.

Code: Select all

 #property copyright "Net Exposure Manager EA"
#property version   "2.00"
#property strict

//+------------------------------------------------------------------+
//| Inicializace                                                     |
//+------------------------------------------------------------------+
int OnInit()
  {
   EventSetTimer(1); // Aktualizace GUI každou vteřinu
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Úklid grafiky při odstranění EA z grafu                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
   ObjectsDeleteAll(0, "NET_EXP_");
  }

//+------------------------------------------------------------------+
//| Hlavní smyčky pro aktualizaci                                    |
//+------------------------------------------------------------------+
void OnTick()  { UpdateGUI(); }
void OnTimer() { UpdateGUI(); }

//+------------------------------------------------------------------+
//| Zpracování kliknutí na tlačítko                                  |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_OBJECT_CLICK)
     {
      // Pokud bylo kliknuto na naše tlačítko
      if(StringFind(sparam, "NET_EXP_BTN_") == 0)
        {
         // Z názvu tlačítka vyřízneme název instrumentu (od 12. znaku)
         string sym = StringSubstr(sparam, 12); 
         CloseAllBySymbol(sym);
         
         // Vizuální "odmáčknutí" tlačítka zpět
         ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
         UpdateGUI();
        }
     }
  }

//+------------------------------------------------------------------+
//| Logika výpočtu a vykreslení GUI                                  |
//+------------------------------------------------------------------+
void UpdateGUI()
  {
   double totalBuy = 0.0, totalSell = 0.0, totalNet = 0.0;
   double totalBuyPnL = 0.0, totalSellPnL = 0.0, totalNetPnL = 0.0;
   
   string symbols[];
   double netLots[];
   double netPnL[];
   int symbolCount = 0;
   string accCurrency = AccountCurrency();

   // 1. Agregace dat o pozicích
   for(int i = 0; i < OrdersTotal(); i++)
     {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
         if(OrderType() == OP_BUY || OrderType() == OP_SELL)
           {
            string sym = OrderSymbol();
            double lots = OrderLots();
            double pnl = OrderProfit() + OrderSwap() + OrderCommission();
            
            double calcLots = (OrderType() == OP_SELL) ? -lots : lots;
            
            totalNet += calcLots;
            totalNetPnL += pnl;
            
            if(OrderType() == OP_BUY)  { totalBuy += lots; totalBuyPnL += pnl; }
            if(OrderType() == OP_SELL) { totalSell += lots; totalSellPnL += pnl; }
            
            int foundIdx = -1;
            for(int j = 0; j < symbolCount; j++) 
              {
               if(symbols[j] == sym) { foundIdx = j; break; }
              }
              
            if(foundIdx == -1) 
              {
               ArrayResize(symbols, symbolCount + 1);
               ArrayResize(netLots, symbolCount + 1);
               ArrayResize(netPnL, symbolCount + 1);
               symbols[symbolCount] = sym;
               netLots[symbolCount] = calcLots;
               netPnL[symbolCount] = pnl;
               symbolCount++;
              } 
            else 
              {
               netLots[foundIdx] += calcLots;
               netPnL[foundIdx] += pnl;
              }
           }
        }
     }

   // 2. Vykreslení hlavičky
   int yPos = 30;
   DrawLabel("NET_EXP_HDR1", "--- AKTUÁLNÍ EXPOZICE ---", 20, yPos); yPos += 20;
   DrawLabel("NET_EXP_BUY", StringFormat("Celkový nákup: %.2f lotů  (%.2f %s)", totalBuy, totalBuyPnL, accCurrency), 20, yPos); yPos += 20;
   DrawLabel("NET_EXP_SELL", StringFormat("Celkový prodej: %.2f lotů  (%.2f %s)", totalSell, totalSellPnL, accCurrency), 20, yPos); yPos += 20;
   
   string netDir = (totalNet >= 0) ? "LONG" : "SHORT";
   DrawLabel("NET_EXP_NET", StringFormat("ČISTÁ EXPOZICE: %s %.2f lotů  (%.2f %s)", netDir, MathAbs(totalNet), totalNetPnL, accCurrency), 20, yPos); yPos += 30;
   
   DrawLabel("NET_EXP_HDR2", "--- Dle instrumentů ---", 20, yPos); yPos += 20;
   
   // 3. Vykreslení řádků s tlačítky pro jednotlivé páry
   for(int i = 0; i < symbolCount; i++) 
     {
      string dir = (netLots[i] >= 0) ? "Long" : "Short";
      string text = StringFormat("%s: %s %.2f  (%.2f %s)", symbols[i], dir, MathAbs(netLots[i]), netPnL[i], accCurrency);
      
      DrawLabel("NET_EXP_TXT_" + symbols[i], text, 20, yPos);
      DrawButton("NET_EXP_BTN_" + symbols[i], "Zavřít " + symbols[i], 300, yPos - 3);
      yPos += 25;
     }

   // 4. Úklid grafiky - smazání tlačítek párů, které už nemají otevřené pozice
   for(int obj = ObjectsTotal() - 1; obj >= 0; obj--)
     {
      string objName = ObjectName(0, obj);
      if(StringFind(objName, "NET_EXP_TXT_") == 0 || StringFind(objName, "NET_EXP_BTN_") == 0)
        {
         string sym = StringSubstr(objName, 12);
         bool isActive = false;
         for(int i = 0; i < symbolCount; i++)
           {
            if(symbols[i] == sym) { isActive = true; break; }
           }
         if(!isActive) ObjectDelete(0, objName);
        }
     }
  }

//+------------------------------------------------------------------+
//| Funkce pro hromadné zavření pozic dle symbolu                    |
//+------------------------------------------------------------------+
void CloseAllBySymbol(string sym)
  {
   // Smyčka musí jít vždy odzadu dopředu (OrderTotal-1 do 0)
   for(int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
         if(OrderSymbol() == sym)
           {
            int type = OrderType();
            if(type == OP_BUY || type == OP_SELL)
              {
               RefreshRates(); // Důležité pro získání nejčerstvější ceny
               double price = (type == OP_BUY) ? MarketInfo(sym, MODE_BID) : MarketInfo(sym, MODE_ASK);
               OrderClose(OrderTicket(), OrderLots(), price, 10, clrRed);
              }
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Pomocné funkce pro rychlé vykreslení GUI                         |
//+------------------------------------------------------------------+
void DrawLabel(string name, string text, int x, int y)
  {
   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, x);
      ObjectSetString(0, name, OBJPROP_FONT, "Arial");
      ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 10);
      
      // Adaptace barvy textu podle toho, zda je graf černý nebo bílý
      color textColor = (ChartGetInteger(0, CHART_COLOR_BACKGROUND) == clrBlack) ? clrWhite : clrBlack;
      ObjectSetInteger(0, name, OBJPROP_COLOR, textColor);
     }
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
  }

void DrawButton(string name, string text, int x, int y)
  {
   if(ObjectFind(0, name) < 0)
     {
      ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
      ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
      ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
      ObjectSetInteger(0, name, OBJPROP_XSIZE, 100);
      ObjectSetInteger(0, name, OBJPROP_YSIZE, 20);
      ObjectSetString(0, name, OBJPROP_FONT, "Arial");
      ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9);
      ObjectSetInteger(0, name, OBJPROP_COLOR, clrBlack);
      ObjectSetInteger(0, name, OBJPROP_BGCOLOR, clrLightGray);
     }
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
  }
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Recommended broker for automated trading & scalping IC Markets
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: Custom volume and direction indicator for MT4

Post by PTScalper »

Hi traders/scalpers :-)

I checkout this MT4 code and decided to improve it:

Key Architectural Improvements

O(n) State Caching Over ObjectsTotal() Loop: The original code parsed every single chart object backwards to clean up orphaned buttons. If you ran custom indicators (like volume profiles or pivot points) that generate thousands of objects, this would freeze the terminal on every tick. The new code caches the symbols rendered in the previous state and isolates cleanup to only those specific target names.

Structs Over Parallel Arrays: Replaced the synchronized symbols[], netLots[], and netPnL[] arrays with a unified SExposure struct. This reduces the risk of index misalignment and keeps memory allocation much cleaner.

Execution Safety: OrderClose() is notorious for failing due to requotes or busy trade contexts (Error 136, 146). The CloseAllBySymbol() function now includes a retry loop, forced Sleep(100) on failure, and error code logging via GetLastError().

Dynamic Configurations: Added inputs for InpMagicNumber (allowing you to restrict management to a specific EA or -1 for global account management) and InpSlippage (replacing the hardcoded 10-point limit).

Code: Select all

#property copyright "Net Exposure Manager EA - Optimized"
#property version   "2.10"
#property strict

//--- Inputs
input int InpMagicNumber = -1; // Magic Number (-1 = All positions)
input int InpSlippage    = 30; // Max Slippage (points)
input int InpMaxRetries  = 3;  // Max Order Close Retries

//--- Data Structure
struct SExposure
  {
   string symbol;
   double netLots;
   double netPnL;
  };

//--- Global State
string lastActiveSymbols[];
int    lastSymbolCount = 0;

//+------------------------------------------------------------------+
//| Initialization                                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   EventSetTimer(1); // Update GUI every second
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Deinitialization                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
   ObjectsDeleteAll(0, "NET_EXP_");
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Main execution loops                                             |
//+------------------------------------------------------------------+
void OnTick()  { UpdateGUI(); }
void OnTimer() { UpdateGUI(); }

//+------------------------------------------------------------------+
//| Chart Event Handling (GUI Clicks)                                |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_OBJECT_CLICK)
     {
      if(StringSubstr(sparam, 0, 12) == "NET_EXP_BTN_")
        {
         string sym = StringSubstr(sparam, 12); 
         CloseAllBySymbol(sym);
         
         // Visual button reset
         ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
         UpdateGUI(); // Force immediate update after execution
        }
     }
  }

//+------------------------------------------------------------------+
//| Calculation and GUI Rendering                                    |
//+------------------------------------------------------------------+
void UpdateGUI()
  {
   double totalBuy = 0.0, totalSell = 0.0, totalNet = 0.0;
   double totalBuyPnL = 0.0, totalSellPnL = 0.0, totalNetPnL = 0.0;
   
   SExposure exposures[];
   int symbolCount = 0;
   string accCurrency = AccountCurrency();

   // 1. Data Aggregation
   for(int i = 0; i < OrdersTotal(); i++)
     {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
         if((OrderType() == OP_BUY || OrderType() == OP_SELL) && 
            (InpMagicNumber == -1 || OrderMagicNumber() == InpMagicNumber))
           {
            string sym = OrderSymbol();
            double lots = OrderLots();
            double pnl = OrderProfit() + OrderSwap() + OrderCommission();
            double calcLots = (OrderType() == OP_SELL) ? -lots : lots;
            
            totalNet += calcLots;
            totalNetPnL += pnl;
            
            if(OrderType() == OP_BUY)  { totalBuy += lots; totalBuyPnL += pnl; }
            if(OrderType() == OP_SELL) { totalSell += lots; totalSellPnL += pnl; }
            
            int foundIdx = -1;
            for(int j = 0; j < symbolCount; j++) 
              {
               if(exposures[j].symbol == sym) { foundIdx = j; break; }
              }
              
            if(foundIdx == -1) 
              {
               ArrayResize(exposures, symbolCount + 1);
               exposures[symbolCount].symbol = sym;
               exposures[symbolCount].netLots = calcLots;
               exposures[symbolCount].netPnL = pnl;
               symbolCount++;
              } 
            else 
              {
               exposures[foundIdx].netLots += calcLots;
               exposures[foundIdx].netPnL += pnl;
              }
           }
        }
     }

   // 2. Render Header
   int yPos = 30;
   DrawLabel("NET_EXP_HDR1", "--- AKTUÁLNÍ EXPOZICE ---", 20, yPos); yPos += 20;
   DrawLabel("NET_EXP_BUY", StringFormat("Celkový nákup: %.2f lotů  (%.2f %s)", totalBuy, totalBuyPnL, accCurrency), 20, yPos); yPos += 20;
   DrawLabel("NET_EXP_SELL", StringFormat("Celkový prodej: %.2f lotů  (%.2f %s)", totalSell, totalSellPnL, accCurrency), 20, yPos); yPos += 20;
   
   string netDir = (totalNet >= 0) ? "LONG" : "SHORT";
   DrawLabel("NET_EXP_NET", StringFormat("ČISTÁ EXPOZICE: %s %.2f lotů  (%.2f %s)", netDir, MathAbs(totalNet), totalNetPnL, accCurrency), 20, yPos); yPos += 30;
   
   DrawLabel("NET_EXP_HDR2", "--- Dle instrumentů ---", 20, yPos); yPos += 20;
   
   // 3. Render Symbol Buttons & Rows
   for(int i = 0; i < symbolCount; i++) 
     {
      string dir = (exposures[i].netLots >= 0) ? "Long" : "Short";
      string text = StringFormat("%s: %s %.2f  (%.2f %s)", exposures[i].symbol, dir, MathAbs(exposures[i].netLots), exposures[i].netPnL, accCurrency);
      
      DrawLabel("NET_EXP_TXT_" + exposures[i].symbol, text, 20, yPos);
      DrawButton("NET_EXP_BTN_" + exposures[i].symbol, "Zavřít " + exposures[i].symbol, 300, yPos - 3);
      yPos += 25;
     }

   // 4. Optimized Graphic Cleanup (O(n) instead of querying all chart objects)
   for(int i = 0; i < lastSymbolCount; i++)
     {
      bool isActive = false;
      for(int j = 0; j < symbolCount; j++)
        {
         if(lastActiveSymbols[i] == exposures[j].symbol) { isActive = true; break; }
        }
      
      if(!isActive)
        {
         ObjectDelete(0, "NET_EXP_TXT_" + lastActiveSymbols[i]);
         ObjectDelete(0, "NET_EXP_BTN_" + lastActiveSymbols[i]);
        }
     }

   // 5. Cache current state for the next tick's cleanup
   lastSymbolCount = symbolCount;
   ArrayResize(lastActiveSymbols, lastSymbolCount);
   for(int i = 0; i < symbolCount; i++)
     {
      lastActiveSymbols[i] = exposures[i].symbol;
     }
     
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Close Position Logic                                             |
//+------------------------------------------------------------------+
void CloseAllBySymbol(string sym)
  {
   for(int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
         if(OrderSymbol() == sym && (InpMagicNumber == -1 || OrderMagicNumber() == InpMagicNumber))
           {
            int type = OrderType();
            if(type == OP_BUY || type == OP_SELL)
              {
               int ticket = OrderTicket();
               double lots = OrderLots();
               
               for(int retry = 0; retry < InpMaxRetries; retry++)
                 {
                  RefreshRates(); 
                  double price = (type == OP_BUY) ? MarketInfo(sym, MODE_BID) : MarketInfo(sym, MODE_ASK);
                  
                  ResetLastError();
                  if(OrderClose(ticket, lots, price, InpSlippage, clrRed))
                    {
                     PrintFormat("Order %d closed successfully.", ticket);
                     break; 
                    }
                  else
                    {
                     int err = GetLastError();
                     PrintFormat("Failed to close order %d. Error: %d. Retrying...", ticket, err);
                     Sleep(100); 
                    }
                 }
              }
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| GUI Helpers                                                      |
//+------------------------------------------------------------------+
void DrawLabel(string name, string text, int x, int y)
  {
   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, x);
      ObjectSetString(0, name, OBJPROP_FONT, "Arial");
      ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 10);
      
      color textColor = (ChartGetInteger(0, CHART_COLOR_BACKGROUND) == clrBlack) ? clrWhite : clrBlack;
      ObjectSetInteger(0, name, OBJPROP_COLOR, textColor);
     }
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
  }

void DrawButton(string name, string text, int x, int y)
  {
   if(ObjectFind(0, name) < 0)
     {
      ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
      ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
      ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
      ObjectSetInteger(0, name, OBJPROP_XSIZE, 100);
      ObjectSetInteger(0, name, OBJPROP_YSIZE, 20);
      ObjectSetString(0, name, OBJPROP_FONT, "Arial");
      ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9);
      ObjectSetInteger(0, name, OBJPROP_COLOR, clrBlack);
      ObjectSetInteger(0, name, OBJPROP_BGCOLOR, clrLightGray);
     }
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
  }
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: Custom volume and direction indicator for MT4

Post by PTScalper »

Because MQL4 lacks a native asynchronous execution function (unlike OrderSendAsync in MQL5), closing a massive grid of high-volume positions synchronously in a single for loop will freeze the terminal GUI and potentially trigger context busyness errors (Error 146).

To solve this, we must shift the architecture to a Tick-Driven State Machine (Queue). Instead of executing the entire loop at once, the button click adds the symbol to a queue. The OnTick() and OnTimer() events will then process the closures one chunk at a time, allowing the terminal to "breathe" and process incoming market data between requests.

Additionally, we must implement Volume Chunking. This ensures the closure respects the broker's MODE_MAXLOT limits and normalizes the partial size using MODE_LOTSTEP to prevent Error 131 (Invalid Trade Volume).

Here are the structural replacements for your script.

1. Add New Inputs and Global State

Add these at the top of your script. InpMaxChunkLots allows you to deliberately slice massive positions (e.g., closing 500 lots in chunks of 50 to minimize slippage).

Code: Select all

//--- Async & Volume Inputs
input double InpMaxChunkLots = 50.0; // Max lots per closure chunk (0 = use broker MaxLot)
input bool   InpUseAsyncLoop = true; // Use Tick-Driven async closing

//--- Global Queue State
string g_closingQueueSymbol = ""; // Symbol currently being liquidated
2. Update the Event Handlers

Modify your events so that OnTick and OnTimer handle the background processing, and OnChartEvent simply hands off the symbol to the queue.

Code: Select all

void OnTick()  
  { 
   ProcessQueue();
   UpdateGUI(); 
  }
  
void OnTimer() 
  { 
   ProcessQueue();
   UpdateGUI(); 
  }

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_OBJECT_CLICK)
     {
      if(StringSubstr(sparam, 0, 12) == "NET_EXP_BTN_")
        {
         string sym = StringSubstr(sparam, 12); 
         
         if(InpUseAsyncLoop)
           {
            g_closingQueueSymbol = sym; // Hand off to the tick/timer loop
           }
         else
           {
            CloseAllBySymbol(sym, false); // Synchronous fallback
           }
           
         ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
         UpdateGUI();
        }
     }
  }

//+------------------------------------------------------------------+
//| Background Queue Processor                                       |
//+------------------------------------------------------------------+
void ProcessQueue()
  {
   if(g_closingQueueSymbol != "")
     {
      // Process one order chunk. If false is returned, the queue is finished.
      bool hasMore = CloseAllBySymbol(g_closingQueueSymbol, true);
      if(!hasMore) 
        {
         g_closingQueueSymbol = ""; // Clear queue when done
        }
     }
  }
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: Custom volume and direction indicator for MT4

Post by PTScalper »

3. The New Chunking Engine

Replace the previous CloseAllBySymbol function with this updated version.

When a partial close occurs in MT4, the remaining volume is assigned a completely new ticket number. Because this function exits after processing one chunk when asyncMode is true, the next tick simply catches the new ticket and continues the process seamlessly.

Code: Select all

//+------------------------------------------------------------------+
//| Asynchronous Partial Closure Engine                              |
//+------------------------------------------------------------------+
bool CloseAllBySymbol(string sym, bool asyncMode = true)
  {
   bool ordersRemaining = false;

   for(int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
        {
         if(OrderSymbol() == sym && (InpMagicNumber == -1 || OrderMagicNumber() == InpMagicNumber))
           {
            int type = OrderType();
            if(type == OP_BUY || type == OP_SELL)
              {
               ordersRemaining = true; // We found an open order
               
               int ticket = OrderTicket();
               double currentLots = OrderLots();
               
               // 1. Determine broker limits
               double maxLot   = MarketInfo(sym, MODE_MAXLOT);
               double minLot   = MarketInfo(sym, MODE_MINLOT);
               double lotStep  = MarketInfo(sym, MODE_LOTSTEP);
               
               // 2. Calculate chunk size
               double closeLots = currentLots;
               if(closeLots > maxLot) closeLots = maxLot;
               if(InpMaxChunkLots > 0 && closeLots > InpMaxChunkLots) closeLots = InpMaxChunkLots;
               
               // 3. Normalize to prevent Error 131 (Invalid Trade Volume)
               closeLots = MathFloor(closeLots / lotStep) * lotStep;
               if(closeLots < minLot) closeLots = currentLots; // Failsafe for rounding issues
               
               RefreshRates(); 
               double price = (type == OP_BUY) ? MarketInfo(sym, MODE_BID) : MarketInfo(sym, MODE_ASK);
               
               ResetLastError();
               if(OrderClose(ticket, closeLots, price, InpSlippage, clrRed))
                 {
                  PrintFormat("Closed %.2f lots of ticket %d. Remaining: %.2f", closeLots, ticket, currentLots - closeLots);
                  
                  // In async mode, we yield execution back to the terminal.
                  // The next OnTick() will pick up the remaining volume (which gets a new ticket ID).
                  if(asyncMode) return true; 
                 }
               else
                 {
                  int err = GetLastError();
                  PrintFormat("Failed to close ticket %d. Error: %d.", ticket, err);
                  
                  // Yield on failure to prevent rapid-fire error loops freezing the EA
                  if(asyncMode) return true; 
                 }
              }
           }
        }
     }
     
   return ordersRemaining;
  }
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: Custom volume and direction indicator for MT4

Post by PTScalper »

The transition from MQL4 to MQL5 completely changes the architectural paradigm here. Because OrderSendAsync pushes the trade request to a background terminal thread rather than blocking the main execution thread, we no longer need a tick-driven state machine to protect the GUI.

You can instantly fire off dozens of chunked closure requests in a single for loop. The MT5 terminal will queue them internally and stream them to the trade server asynchronously without freezing your chart.

Here is the native MQL5 implementation tailored for high-volume execution on a Hedging account (which allows multiple positions per symbol, standard for modern scalping).

The MQL5 Asynchronous Closure Engine

Code: Select all

//--- Async & Volume Inputs
input double InpMaxChunkLots = 50.0; // Max lots per closure chunk (0 = use broker MaxLot)
input int    InpMagicNumber  = -1;   // Magic Number (-1 = All positions)
input ulong  InpSlippage     = 30;   // Max Slippage (points)

//+------------------------------------------------------------------+
//| Asynchronous Partial Closure Engine (MQL5)                       |
//+------------------------------------------------------------------+
void CloseAllBySymbolAsync(string sym)
  {
   // In MQL5, we iterate over open positions, not orders
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      string posSym = PositionGetSymbol(i);
      
      if(posSym == sym)
        {
         ulong  ticket = PositionGetInteger(POSITION_TICKET);
         ulong  magic  = PositionGetInteger(POSITION_MAGIC);
         long   type   = PositionGetInteger(POSITION_TYPE);
         double totalVolume = PositionGetDouble(POSITION_VOLUME);
         
         if(InpMagicNumber != -1 && magic != InpMagicNumber) continue;

         // 1. Determine broker volume limits
         double maxLot  = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
         double minLot  = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
         double stepLot = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
         
         // 2. Calculate effective max chunk limit
         double chunkLimit = maxLot;
         if(InpMaxChunkLots > 0 && InpMaxChunkLots < maxLot) 
           {
            chunkLimit = InpMaxChunkLots;
           }

         double remainingVolume = totalVolume;

         // 3. Slice the position and dispatch async requests instantly
         while(remainingVolume >= minLot)
           {
            double closeVol = remainingVolume;
            if(closeVol > chunkLimit) closeVol = chunkLimit;
            
            // Normalize volume to prevent Invalid Volume errors
            closeVol = MathFloor(closeVol / stepLot) * stepLot;
            if(closeVol < minLot) break; // Failsafe
            
            // Prepare MQL5 Trade Request
            MqlTradeRequest request = {};
            MqlTradeResult  result  = {};
            
            request.action    = TRADE_ACTION_DEAL;
            request.position  = ticket; // Explicitly targets this specific position (Hedging)
            request.symbol    = sym;
            request.volume    = closeVol;
            request.deviation = InpSlippage;
            request.magic     = (InpMagicNumber == -1) ? 0 : InpMagicNumber;
            
            // MQL5 requires sending the opposite order type to close
            if(type == POSITION_TYPE_BUY)
              {
               request.type  = ORDER_TYPE_SELL;
               request.price = SymbolInfoDouble(sym, SYMBOL_BID);
              }
            else
              {
               request.type  = ORDER_TYPE_BUY;
               request.price = SymbolInfoDouble(sym, SYMBOL_ASK);
              }
              
            // Broker specific filling mode (IOC is usually safest for high-volume partials)
            // request.type_filling = ORDER_FILLING_IOC; 

            // 4. Fire and forget
            if(OrderSendAsync(request, result))
              {
               PrintFormat("Async close dispatched for Ticket %I64u | Volume: %.2f", ticket, closeVol);
              }
            else
              {
               PrintFormat("Failed to dispatch async request for Ticket %I64u | Error: %d", ticket, GetLastError());
              }
              
            remainingVolume -= closeVol;
           }
        }
     }
  }
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: Custom volume and direction indicator for MT4

Post by PTScalper »

Key Differences in this Implementation

No Tick Yielding: The while(remainingVolume >= minLot) loop mathematically slices a massive position (e.g., 5,000 lots) into optimal chunks and fires 100 OrderSendAsync requests in a few milliseconds.

Opposite Order Logic: Unlike MQL4's OrderClose(), MQL5 requires you to explicitly send a TRADE_ACTION_DEAL request with the opposite trade direction (ORDER_TYPE_SELL to close a Long), bound to the specific request.position ticket.

No RefreshRates(): MQL5 automatically pulls the absolute latest tick data directly from the terminal memory via SymbolInfoDouble(), bypassing the need to refresh market environment variables mid-loop.

Handling Rejections: Because requests are async, the OrderSendAsync boolean only confirms the request reached the terminal successfully. If you need to track execution failures (like requotes or liquidity rejections at the server level), you will need to catch the server responses inside the OnTradeTransaction() event handler using the request_id generated in the MqlTradeResult.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: Custom volume and direction indicator for MT4

Post by PTScalper »

In cTrader (C# / cAlgo.API), the architecture for high-volume partial closures shifts again.

Unlike MT4 (which generates a new ticket on a partial close) or MT5 (which uses opposite orders), cTrader retains the exact same Position ID when partially closed; it just mutates the volume.

If you fire a while loop of asynchronous partial close requests at the exact same Position ID (like we did in MT5), the cTrader server will likely reject the subsequent requests with "Position Modified" or "Invalid Volume" errors because the previous chunk hasn't been processed by the matching engine yet.

To solve this, we must use a Recursive Asynchronous Callback. We fire the first chunk, wait for the server to return the TradeResult, and if the position still has remaining volume, the callback recursively fires the next chunk.

Here is the professional C# implementation for cTrader:

Code: Select all

using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class HighVolumeExposureManager : Robot
    {
        [Parameter("Max Chunk (Lots)", DefaultValue = 50.0, MinValue = 0.01)]
        public double MaxChunkLots { get; set; }

        [Parameter("Target Label (Empty = All)", DefaultValue = "")]
        public string TargetLabel { get; set; }

        //+------------------------------------------------------------------+
        //| Public trigger to initiate the liquidation of a symbol           |
        //+------------------------------------------------------------------+
        public void InitiateClosure(string symbolName)
        {
            // Filter by label if one is provided
            var positionsToClose = string.IsNullOrEmpty(TargetLabel) 
                ? Positions.Where(p => p.SymbolName == symbolName).ToArray()
                : Positions.FindAll(TargetLabel, symbolName);

            foreach (var position in positionsToClose)
            {
                ProcessChunk(position);
            }
        }

        //+------------------------------------------------------------------+
        //| Core engine: Calculates volume and dispatches the async request  |
        //+------------------------------------------------------------------+
        private void ProcessChunk(Position position)
        {
            var symbol = Symbols.GetSymbol(position.SymbolName);
            
            // 1. Convert Lots to raw VolumeInUnits
            double maxChunkVolume = symbol.QuantityToVolumeInUnits(MaxChunkLots);
            
            // 2. Determine broker limits
            double brokerMax = symbol.VolumeInUnitsMax;
            double brokerMin = symbol.VolumeInUnitsMin;
            double stepVol   = symbol.VolumeInUnitsStep;

            double chunkLimit = maxChunkVolume > 0 ? Math.Min(maxChunkVolume, brokerMax) : brokerMax;
            double remainingVolume = position.VolumeInUnits;
            
            if (remainingVolume < brokerMin) 
                return;

            // 3. Slice and normalize volume
            double closeVol = Math.Min(remainingVolume, chunkLimit);
            closeVol = Math.Floor(closeVol / stepVol) * stepVol;

            if (closeVol < brokerMin)
                return; // Failsafe

            Print("Dispatching async close: {0} units for Position {1}", closeVol, position.Id);

            // 4. Fire async request and pass the state to the lambda callback
            ClosePositionAsync(position, closeVol, (result) => OnChunkProcessed(result, position.Id, closeVol));
        }

        //+------------------------------------------------------------------+
        //| Server Callback: Handles the recursive loop or error states      |
        //+------------------------------------------------------------------+
        private void OnChunkProcessed(TradeResult result, int positionId, double attemptedVolume)
        {
            if (result.IsSuccessful)
            {
                Print("Successfully closed {0} units of Position {1}.", attemptedVolume, positionId);
                
                // Fetch the mutated position from the server state using the same ID
                var remainingPosition = Positions.FirstOrDefault(p => p.Id == positionId);
                
                if (remainingPosition != null && remainingPosition.VolumeInUnits > 0)
                {
                    // Recursively process the next chunk
                    ProcessChunk(remainingPosition);
                }
                else
                {
                    Print("Position {0} fully closed.", positionId);
                }
            }
            else
            {
                Print("Failed to close chunk of Position {0}. Error: {1}", positionId, result.Error);
                
                // Add optional retry logic here if dealing with requotes or liquidity rejections
            }
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: Custom volume and direction indicator for MT4

Post by PTScalper »

Key Architectural Differences in cTrader

QuantityToVolumeInUnits: cTrader's API operates natively in raw units, not lots. The ProcessChunk method translates your MaxChunkLots parameter into the exact unit count required by your specific broker's symbol specifications.

Lambda State Capture: The ClosePositionAsync method uses a lambda expression (result) => OnChunkProcessed(...) to pass the position.Id into the callback. This is critical because the TradeResult.Position object might be null if the partial close resulted in the final liquidation of the position.

No GUI Thread Blocking: Because this relies entirely on the FIX API callback from the broker server (TradeResult), your cBot will not drop a single tick or freeze the UI, even if you are liquidating billions in volume across hundreds of positions.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: Custom volume and direction indicator for MT4

Post by PTScalper »

TradingView’s Pine Script operates on a completely different architectural paradigm than MT4/MT5 or cTrader.

Pine Script is a cloud-based synchronous engine. It does not connect directly to a broker's FIX API, nor does it have native asynchronous execution. Instead, Pine Script evaluates your logic bar-by-bar and fires Webhooks (Alerts) to a third-party bridge (like your own cloud server) which then forwards the execution to the broker.

Because TradingView executes a while loop instantly within a single bar's calculation, you cannot force Pine Script to "wait" between chunks. What you can do is mathematically slice your massive position inside Pine, and fire a sequence of chunked JSON payloads to your webhook receiver.

Here is the Pine Script v5 implementation to handle high-volume partial slicing:

Code: Select all

//@version=5
strategy("High-Volume Exposure Manager", overlay=true, process_orders_on_close=true)

// --- Inputs ---
maxChunkLots = input.float(50.0, title="Max Chunk (Lots)", minval=0.1)
triggerClose = input.bool(false, title="Trigger Close (Checkbox for testing)")

// --- Demo Position ---
// Entering a massive position on the first bar so we have something to slice
if barstate.isfirst
    strategy.entry("Long", strategy.long, qty=235.5)

// --- Chunking & Execution Engine ---
// We use a custom function to handle the slicing to keep the main scope clean
f_liquidate_position(id, max_chunk) =>
    float currentQty = strategy.position_size
    
    // Only execute if we have a position
    if currentQty != 0
        float remainingQty = math.abs(currentQty)
        int step = 1
        
        // Loop until the entire position is sliced
        while remainingQty > 0
            // Determine chunk size
            float closeQty = math.min(remainingQty, max_chunk)
            
            // Create a unique identifier for each chunk
            string chunkId = "Close_" + id + "_Part" + str.tostring(step)
            
            // Build the JSON payload to send to your webhook receiver
            // We pass the chunk size, not the total size
            string alertJson = '{"action": "close", "symbol": "' + syminfo.ticker + '", "qty": ' + str.tostring(closeQty) + '}'
            
            // Dispatch the strategy close command with the specific quantity and alert
            strategy.close(id, qty=closeQty, comment=chunkId, alert_message=alertJson)
            
            // Deduct the chunk from the remaining volume
            remainingQty -= closeQty
            step += 1

// --- Trigger Logic ---
// Replace `triggerClose` with your actual algorithmic exit conditions
if triggerClose and barstate.isconfirmed
    f_liquidate_position("Long", maxChunkLots)
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: Custom volume and direction indicator for MT4

Post by PTScalper »

Critical Execution Caveats for Pine

Instantaneous Firing: When this script triggers, TradingView will fire all chunked alert_message JSON payloads in the exact same millisecond.

Broker Rate Limits: If your bridge forwards all these requests to the broker simultaneously, the broker will reject them (e.g., "Too Many Requests" or "Position Modified").

The Real Queue: To make this work safely with high-volume scalping, the asynchronous queue we built for cTrader/MT5 must actually live on your webhook receiver server. Pine Script's job is simply to say, "Close 235.5 lots." Your server should catch that single alert, slice it into 50-lot chunks, and dispatch them to the broker sequentially via the broker's API.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply