Validation failed [Invalid Volume]

When uploading an EA to the market, it fails validation due to invalid volume error: 

test on EURUSD,H1 (netting) strategy tester report 22 total trades test on XAUUSD,D1 (netting) log files size exceeded 2080 MB, test terminated 2022.04.11 15:39:28 failed market sell 0.15 XAUUSD, close #47 buy 0.15 XAUUSD 1945.32 [Invalid volume] 2022.04.11 15:39:28 failed market sell 0.15 XAUUSD, close #47 buy 0.15 XAUUSD 1945.32 [Invalid volume] 2022.04.11 15:39:29 failed market sell 0.15 XAUUSD, close #47 buy 0.15 XAUUSD 1945.32 [Invalid volume]


Question: How is the EA allowed to open a position with invalid volume if it is not allowed to close the position due to invalid volume? 

I do have checks in place to prevent invalid volume, I even hardcoded lotstep of 0.1 increments on XAUUSD/GOLD if the EA is not able to retrieve the actual properties form the broker/symbol. 

Can anyone see what im missing? 

//+------------------------------------------------------------------+
//| Validate and constrain lot size with all safety checks
//+------------------------------------------------------------------+
double ValidateAndConstrainLotSize(double requestedLotSize, bool checkTotalVolume = true)
  {
// Step 1: Normalize to broker's lot step
   double lotStep = SymbolInfoDouble(symbolTraded, SYMBOL_VOLUME_STEP);
   
   // If SymbolInfoDouble fails, try ensuring symbol is selected first
   if(lotStep <= 0)
     {
      SymbolSelect(symbolTraded, true);  // Ensure symbol is in Market Watch
      Sleep(100);  // Give it a moment to load
      lotStep = SymbolInfoDouble(symbolTraded, SYMBOL_VOLUME_STEP);
     }
   
   // Last resort: use symbol-aware fallback
   if(lotStep <= 0)
     {
      if(StringFind(symbolTraded, "XAU") >= 0 || 
         StringFind(symbolTraded, "GOLD") >= 0)
        {
         lotStep = 0.1;
         Print("WARNING: Could not retrieve lot step for ", symbolTraded, " - using Gold fallback of 0.1");
        }
      else
        {
         lotStep = 0.01;  // Forex default
         Print("WARNING: Could not retrieve lot step for ", symbolTraded, " - using Forex fallback of 0.01");
        }
     }
   
   // Round to NEAREST valid step (not floor)
   double constrainedLot = MathRound(requestedLotSize / lotStep) * lotStep;
   
   // DEFENSIVE CHECK: If lot step is 0.01 but result isn't valid for 0.1 step,
   if(lotStep == 0.01)
     {
      double remainder = MathMod(constrainedLot * 100, 10);  // Check if multiple of 0.1
      if(remainder > 0.01)  // Not a clean multiple of 0.1 
        {
         constrainedLot = MathRound(constrainedLot / 0.1) * 0.1;  // Round to 0.1
        }
     }

// Step 2: Apply all maximum constraints
   constrainedLot = ApplyMaximumLotConstraints(constrainedLot);

// Step 3: Check total volume limits if requested
   if(checkTotalVolume)
     {
      constrainedLot = CheckTotalVolumeLimit(constrainedLot);
     }

// Step 4: Apply minimum constraints
   constrainedLot = ApplyMinimumLotConstraints(constrainedLot);

// Step 5: Check SYMBOL_VOLUME_LIMIT (total volume across all positions - CRITICAL for netting accounts!)
   double volumeLimit = SymbolInfoDouble(symbolTraded, SYMBOL_VOLUME_LIMIT);
   if(volumeLimit > 0)  // If limit exists 
     {
      double currentTotalVolume = getAllVolume();
      double availableVolume = volumeLimit - currentTotalVolume;
      
      if(availableVolume <= 0)
        {
         Print("Cannot open position - SYMBOL_VOLUME_LIMIT reached (", currentTotalVolume, "/", volumeLimit, ")");
         return 0;
        }
      
      if(constrainedLot > availableVolume)
        {
         Print("Reducing lot from ", constrainedLot, " to ", availableVolume, " due to SYMBOL_VOLUME_LIMIT");
         constrainedLot = availableVolume;
         
         // Re-normalize after reduction to maintain valid lot step
         constrainedLot = MathRound(constrainedLot / lotStep) * lotStep;
         
         // Re-apply defensive check for 0.1 multiples
         if(lotStep == 0.01)
           {
            double remainder = MathMod(constrainedLot * 100, 10);
            if(remainder > 0.01)
              {
               constrainedLot = MathRound(constrainedLot / 0.1) * 0.1;
              }
           }
         
         // Ensure it's still above minimum after all adjustments
         double minLot = SymbolInfoDouble(symbolTraded, SYMBOL_VOLUME_MIN);
         if(minLot <= 0) minLot = 0.01;
         if(constrainedLot < minLot)
           {
            Print("After SYMBOL_VOLUME_LIMIT adjustment, lot size too small - cannot open position");
            return 0;
           }
        }
     }

   return NormalizeDouble(constrainedLot, 2);
  }
//+------------------------------------------------------------------+
//| Apply all maximum lot size constraints
//+------------------------------------------------------------------+
double ApplyMaximumLotConstraints(double lotSize)
  {
// Your custom max lot function
   lotSize = MathMin(lotSize, MaxLotPer1000());

// Broker's symbol maximum
   double brokerMax = SymbolInfoDouble(symbolTraded, SYMBOL_VOLUME_MAX);
   lotSize = MathMin(lotSize, brokerMax);

// Hard safety cap (especially important for volatile symbols like gold)
   double hardCap = MathMin(brokerMax, 100.0);
   lotSize = MathMin(lotSize, hardCap);

   return lotSize;
  }

//+------------------------------------------------------------------+
//| Check if lot size would exceed total volume limits
//+------------------------------------------------------------------+
double CheckTotalVolumeLimit(double requestedLotSize)
  {
   double currentTotalVolume = getAllVolume();
   double maxTotalVolume = MathMin(SymbolInfoDouble(symbolTraded, SYMBOL_VOLUME_MAX), 100.0);

   if(currentTotalVolume + requestedLotSize > maxTotalVolume)
     {
      double availableVolume = maxTotalVolume - currentTotalVolume;
      if(availableVolume <= 0)
        {
         Print("Cannot open position - total volume limit reached (",
               currentTotalVolume, "/", maxTotalVolume, ")");
         return 0; // Signal that no position can be opened
        }

      Print("Reducing lot size from ", requestedLotSize, " to ", availableVolume,
            " to stay within total volume limit");
      return availableVolume;
     }

   return requestedLotSize;
  }

//+------------------------------------------------------------------+
//| Apply minimum lot size constraints
//+------------------------------------------------------------------+
double ApplyMinimumLotConstraints(double lotSize)
  {
   double minLot = SymbolInfoDouble(symbolTraded, SYMBOL_VOLUME_MIN);
   if(minLot <= 0)
      minLot = 0.01;

   return MathMax(lotSize, minLot);
  }

//+------------------------------------------------------------------+
//| Check if lot size is valid for opening positions
//+------------------------------------------------------------------+
bool IsValidLotSize(double lotSize)
  {
   double minLot = SymbolInfoDouble(symbolTraded, SYMBOL_VOLUME_MIN);
   double maxLot = SymbolInfoDouble(symbolTraded, SYMBOL_VOLUME_MAX);

   if(lotSize <= 0)
     {
      Print("Invalid lot size: ", lotSize, " (must be positive)");
      return false;
     }

   if(lotSize < minLot)
     {
      Print("Lot size too small: ", lotSize, " (min: ", minLot, ")");
      return false;
     }

   if(lotSize > maxLot)
     {
      Print("Lot size too large: ", lotSize, " (max: ", maxLot, ")");
      return false;
     }

   return true;
  }

Two concrete problems are visible in the posted helper:

1. The "defensive" branch overrides a broker-reported step of 0.01 with 0.1 multiples. For a requested 0.05, that branch produces 0.10. Remove this forced 0.1 rounding; keep the broker-reported step.

2. CheckTotalVolumeLimit() returns zero to mean "cannot open", but ApplyMinimumLotConstraints() then raises zero to the minimum volume. Preserve the rejection instead of turning it into an order size.

Also, NormalizeDouble(constrainedLot, 2) can destroy a valid volume when the step is 0.001: 0.001 becomes 0.00. Revalidate the final volume against the actual min/max/step after every adjustment; do not guess missing properties.

These are defects in the shown sizing path, not proof of the cause of the closing error. Please show the closing code and log the selected position's symbol/volume, the outgoing request volume at full precision, and the trade result retcode/comment. The opening helper alone cannot explain that rejection.

Reference for the volume fields:
https://www.mql5.com/en/docs/constants/environment_state/marketinfoconstants

A few things stand out in your code:

1. The “defensive check” changes volumes to multiples of 0.1 even when the reported step is 0.01. Remove that assumption and use the actual symbol properties. If retrieving them fails, log the error rather than guessing a fallback.

2. Your minimum-volume adjustment can turn a zero returned by an earlier check back into a tradable volume. Keep zero as “do not send”, and validate the final volume after all adjustments.

3. For a full close, use the current position volume rather than recalculating it through your entry-sizing rules. Consider trade.PositionClose(ticket) and inspect trade.ResultRetcode() and trade.ResultRetcodeDescription().

Your log shows the failure occurs when closing, but the closing code is missing. Could you share that section, together with the symbol’s volume minimum, maximum and step at the time of failure?

If the close is retried on every tick, add a retry limit — repeated rejections fill the validation log quickly.