Five Symbol-Specific Assumptions That Silently Break EAs on Non-EURUSD Instruments

Five Symbol-Specific Assumptions That Silently Break EAs on Non-EURUSD Instruments

29 June 2026, 22:53
Boris Armenteros
0
28

Most EAs I debug for cross-symbol failures share the same five hardcoded assumptions. They are all correct on EURUSD and all wrong on everything else. Here is what breaks, why, and the runtime queries that fix each one.

Digit Count and the Point * 10 Trap

The most common hardcoded assumption: using Point * 10  to convert points to pips. On 5-digit pairs like EURUSD, this works correctly. On a 3-digit pair like USDJPY, the multiplier produces values an order of magnitude too large. A 20-pip trailing stop becomes 200 pips. On a 2-digit index, it is completely meaningless.

The fix is straightforward — query the actual digit count at startup:

double PipSize()
{
   int digits = (int)SymbolInfoInteger(Symbol(), SYMBOL_DIGITS);
   if(digits == 3 || digits == 5)
   {
      return SymbolInfoDouble(Symbol(), SYMBOL_POINT) * 10;
   }
   return SymbolInfoDouble(Symbol(), SYMBOL_POINT);
}

Call this once in OnInit()  and store the result. Every pip-based calculation in the EA uses the stored value instead of a hardcoded multiplier.

Lot Constraints Vary by Symbol and Broker

Hardcoding 0.01  as the minimum lot size works on most forex accounts. On gold, many brokers set the minimum at 0.10  with a step of 0.10 . On indices the constraints vary further — 1.0 , 0.1 , or 0.01  depending on broker and instrument.

When an EA sends an order with an invalid lot size, the result depends on error handling. If the EA checks the return value of OrderSend , it gets error 131 (invalid volume). If it does not, the order silently fails and the EA opens zero trades with no explanation in the journal.

double NormalizeLot(const string symbol, double lot)
{
   double minLot  = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
   double maxLot  = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
   double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);

   if(lotStep <= 0)
   {
      printf("%s: WARNING: Invalid lot step for %s", __FUNCTION__, symbol);
      return minLot;
   }

   lot = MathMax(lot, minLot);
   lot = MathMin(lot, maxLot);
   lot = MathRound(lot / lotStep) * lotStep;

   return NormalizeDouble(lot, (int)MathCeil(-MathLog10(lotStep)));
}

This normalizes any calculated lot to the nearest valid step for the target symbol. The divide-by-zero guard on lotStep  prevents a crash if symbol data is unavailable.

Tick Value Is Not Universal

On EURUSD with a USD-denominated account, 1 pip on a 0.01 lot is roughly $0.10. Developers hardcode this ratio. On a cross like GBPJPY, the actual tick value is significantly different because it includes the GBP-to-USD conversion embedded in the JPY cross rate.

The platform provides the correct value at runtime:

double tickValue = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_VALUE);
double tickSize  = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE);

if(tickSize <= 0 || tickValue <= 0)
{
   printf("%s: WARNING: Invalid tick data for %s | TickValue=%.6f TickSize=%.6f",
          __FUNCTION__, Symbol(), tickValue, tickSize);
   return;
}

double riskPerPip = (tickValue / tickSize) * PipSize() * lotSize;

One function call replaces the hardcoded assumption. The risk-per-pip calculation is now correct regardless of the account currency or symbol.

Spread Filters Need Per-Symbol Calibration

A spread filter set to reject trades above 30 points catches extreme conditions on EURUSD where the typical spread is 10-15 points (1-1.5 pips). On gold, 30+ point spreads are normal market conditions. The EA filters 100% of trades and runs for weeks without opening a position.

The fix: treat the spread threshold as a per-symbol input, or derive it dynamically from recent spread history at startup:

int currentSpread = (int)SymbolInfoInteger(Symbol(), SYMBOL_SPREAD);
printf("%s: Current spread for %s = %d points", __FUNCTION__, Symbol(), currentSpread);

if(IN_SpreadFilter > 0 && currentSpread > IN_SpreadFilter)
{
   printf("%s: Spread filter active | Current=%d Limit=%d", __FUNCTION__, currentSpread, IN_SpreadFilter);
   return;
}

Log the spread at startup so you can see whether the filter is realistic for the attached symbol. If the current spread already exceeds the filter on a quiet market day, the filter is misconfigured — not the market.

Session Awareness Prevents Stale Data Entries

Forex trades nearly 24 hours. Metals and indices have session breaks lasting hours. An EA designed for continuous tick data does not know how to handle a gap. When the session opens, the EA evaluates conditions against the last tick from the previous day and may enter immediately on stale data.

SymbolInfoSessionTrade()  returns the trading session schedule. If the EA expects continuous data and the symbol has session breaks, log a warning at initialization:

datetime sessionStart, sessionEnd;
if(SymbolInfoSessionTrade(Symbol(), MONDAY, 0, sessionStart, sessionEnd))
{
   printf("%s: %s Monday session | Start=%s End=%s",
          __FUNCTION__, Symbol(),
          TimeToString(sessionStart, TIME_MINUTES),
          TimeToString(sessionEnd, TIME_MINUTES));
}

If the session window does not cover the hours the EA expects to trade, the EA should alert the user rather than trade on stale prices.

The Five-Query Checklist

Every production EA should run these five queries in OnInit() :

  1. SYMBOL_DIGITS  — Calculate pip multiplier from actual digits
  2. SYMBOL_VOLUME_MIN/MAX/STEP  — Normalize every lot calculation
  3. SYMBOL_TRADE_TICK_VALUE  — Use platform-provided tick value for risk math
  4. SYMBOL_SPREAD  — Log current spread and validate filter realism
  5. SymbolInfoSessionTrade()  — Check session hours against EA expectations

If any value falls outside what the EA's logic can handle, log a clear message and stop. Trading with wrong assumptions is worse than not trading.