DEV Community

Validating Expert Advisor inputs in OnInit so bad settings never reach the market

Most Expert Advisors trust their inputs completely. Whatever the user types in the settings dialog goes straight into the trading logic. A risk of 10 instead of 1.0, a stop loss of 0, a fast moving average longer than the slow one: the EA starts, runs and trades on settings that make no sense.

MQL5 gives you a clean place to stop that. OnInit() runs once when the EA is attached, when inputs change and at the start of every Strategy Tester pass. Its return value decides whether the EA is allowed to run at all. This tutorial shows how to check inputs there, how to choose the right return code, and how the same checks speed up optimization.

The return codes

OnInit() returns an int, normally one of these values from ENUM_INIT_RETCODE:

  • INIT_SUCCEEDED means everything is fine and the EA starts.
  • INIT_FAILED means initialization failed for a reason outside the inputs, such as an indicator handle that could not be created.
  • INIT_PARAMETERS_INCORRECT means the inputs themselves are wrong.
  • INIT_AGENT_NOT_SUITABLE is for tester agents that cannot run the pass, and rarely matters in normal EAs.

Any value other than INIT_SUCCEEDED stops the EA. On a live chart it is removed, and OnDeinit() runs with the reason REASON_INITFAILED. The user sees nothing unless you tell them, so always log why.

A small rejection helper

Keeping the message and the return code in one place makes the checks short and consistent.

int Reject(const string why)
  {
   PrintFormat("Input error, EA not started: %s", why);
   if(!MQLInfoInteger(MQL_TESTER))
      Alert("EA not started: ", why);
   return INIT_PARAMETERS_INCORRECT;
  }

The Alert is skipped in the tester, because a popup for every rejected optimization pass would be unbearable.

Checking the inputs themselves

These are rules that hold on any account and any symbol. Write them as plainly as you can.

input double InpRiskPercent = 1.0; // risk per trade, percent of balance
input int InpStopPoints = 300;     // stop loss distance in points
input int InpTakePoints = 600;     // take profit distance in points
input int InpFastPeriod = 12;      // fast moving average period
input int InpSlowPeriod = 26;      // slow moving average period
input int InpStartHour = 8;        // trading window start, server time
input int InpEndHour = 20;         // trading window end, server time
input long InpMagic = 240101;      // magic number

int OnInit()
  {
   if(InpRiskPercent <= 0.0 || InpRiskPercent > 5.0)
      return Reject("InpRiskPercent must be above 0 and no more than 5");
   if(InpStopPoints <= 0)
      return Reject("InpStopPoints must be positive, the EA never trades without a stop");
   if(InpTakePoints < 0)
      return Reject("InpTakePoints cannot be negative, use 0 for no take profit");
   if(InpFastPeriod < 1 || InpSlowPeriod < 1)
      return Reject("moving average periods must be at least 1");
   if(InpFastPeriod >= InpSlowPeriod)
      return Reject("InpFastPeriod must be shorter than InpSlowPeriod");
   if(InpStartHour < 0 || InpStartHour > 23 || InpEndHour < 0 || InpEndHour > 23)
      return Reject("trading hours must be between 0 and 23");
   if(InpMagic <= 0)
      return Reject("InpMagic must be a positive number");
   return CheckEnvironment();
  }

The upper limit on risk is a deliberate guard against typing 10 when you meant 1.0. Pick a ceiling that suits the strategy and say so in the message.

Checking inputs against the symbol

Some inputs are only valid on certain symbols or accounts. A stop of 30 points is fine on one broker and below the minimum distance on another. These checks use symbol properties, so they belong in a second function.

int CheckEnvironment()
  {
   long stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   if(stopsLevel > 0 && InpStopPoints <= stopsLevel)
      return Reject(StringFormat("InpStopPoints %d is inside the broker minimum of %d points on %s", InpStopPoints, (int)stopsLevel, _Symbol));
   if(InpTakePoints > 0 && stopsLevel > 0 && InpTakePoints <= stopsLevel)
      return Reject(StringFormat("InpTakePoints %d is inside the broker minimum of %d points on %s", InpTakePoints, (int)stopsLevel, _Symbol));
   double minVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   if(minVol <= 0.0)
     {
      PrintFormat("Could not read volume limits for %s", _Symbol);
      return INIT_FAILED;
     }
   return INIT_SUCCEEDED;
  }

Notice the different return code for the volume check. If the terminal cannot give us symbol data, the inputs are not the problem, so INIT_FAILED is the honest answer.

A stops level of 0 usually means the broker uses a floating minimum distance, so the startup check cannot catch everything. Keep checking the distance again just before each order or modify.

Why this matters in optimization

Optimization is where input validation pays for itself. Suppose you optimize the fast period from 5 to 50 and the slow period from 10 to 100. A large share of those combinations has the fast period equal to or longer than the slow one.

Without validation, every one of them runs a full test that produces nonsense. With the check above, those passes return INIT_PARAMETERS_INCORRECT and stop immediately. The tester marks them in the results instead of simulating them, so the optimization finishes faster and the results table only ranks combinations that make sense.

Use enums instead of magic integers

The cheapest validation is the one the user cannot get wrong. If an input chooses between modes, declare an enum and the settings dialog will show a drop down list instead of a free number field.

enum ENUM_EXIT_MODE
  {
   EXIT_FIXED_TP = 0, // Fixed take profit
   EXIT_TRAILING = 1, // Trailing stop
   EXIT_OPPOSITE = 2  // Opposite signal
  };

input ENUM_EXIT_MODE InpExitMode = EXIT_FIXED_TP; // exit method

The comment after each enum value is what the user sees in the list, which doubles as documentation.

Practical notes

  • Input variables are read only inside the EA. If you need to adjust a value, for example rounding a lot size to the symbol step, copy it into a global variable in OnInit() and use the copy.
  • Print the final accepted settings once in OnInit(). When someone sends you a log, you will know exactly what the EA was running with.
  • Write the reason in the message, not just the input name. "InpFastPeriod must be shorter than InpSlowPeriod" saves a support question. "Invalid input" creates one.
  • Validation in OnInit() runs again whenever the user changes inputs on a running EA, so the same rules protect live edits too.

An EA that refuses to start with a clear message is far better than one that starts happily and trades on a typo.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.