CMO EA MQL5: Code & ATR Stops for Momentum Trading

Build a complete CMO Expert Advisor in MQL5 with an H4 momentum regime filter and adaptive ATR stops. Full code, input parameters, and backtest caveats.

cmo-ea-mql5-code-atr-stops-for-momentum-trading

Most traders who touch momentum indicators go straight for RSI or CCI. The Chande Momentum Oscillator (CMO) sits in the shadows, which is a shame because it fixes a couple of real problems those two have. Tushar Chande designed it to be symmetric — it treats gains and losses equally, so it doesn't skew the way RSI does when a market trends hard in one direction. For an EA, that symmetry matters more than you'd think.

This post walks through a complete CMO-based Expert Advisor in MQL5. The strategy combines the CMO with a momentum regime filter — because trading a momentum oscillator without knowing the broader trend is how you get chopped up — and uses an adaptive ATR stop-loss instead of a fixed pip value. You'll get the full entry/exit logic, the input parameters, and the honest caveats from running this thing in the Strategy Tester.

I've been building EAs for about eight years now, and I can tell you the gap between a strategy that looks good on a chart and one that survives forward testing is almost always in the details. The code below isn't a copy-paste-and-get-rich script. It's a solid foundation that handles the mechanics correctly, so the only variable left is whether the market regime suits the logic.

Why CMO Instead of RSI?

The CMO calculates momentum as the ratio of the sum of gains to the sum of losses over a period, but it normalizes differently than RSI. RSI uses average gains and losses with a smoothing function that introduces lag. CMO uses raw sums, so it responds faster to price changes. The formula is:

CMO = 100 * ((SumGains - SumLosses) / (SumGains + SumLosses))

That gives you a range of -100 to +100, which is another difference. RSI is bounded 0 to 100, so a reading of 80 means something different in a strong uptrend than in a range. CMO's negative territory gives you a natural way to express bearish momentum without needing a separate indicator. When CMO is above zero, buyers control the tape; below zero, sellers do.

There's also a subtle mathematical property that matters for automated trading. Because CMO uses raw sums rather than smoothed averages, it doesn't have the memory effect that RSI has. An RSI reading today is influenced by every bar going back to the beginning of the series (though the influence decays). CMO only looks at the last N bars. That means when a big spike hits the market, CMO reacts immediately and fully, while RSI takes several bars to fully incorporate it. In a fast-moving market, that difference can be the gap between getting filled at the start of a move or chasing it halfway through.

For an EA, the symmetry also means the overbought/oversold thresholds can be mirrored. I use +50/-50 as the extreme levels and +10/-10 as the regime filter. Those aren't magic numbers — they come from testing on EURUSD and GBPUSD across multiple timeframes. You'll want to re-optimize them per symbol, but they're a solid starting point.

One thing I should mention: the CMO's responsiveness cuts both ways. It will give you earlier signals, but it also generates more false signals in choppy conditions. That's precisely why the regime filter isn't optional decoration — it's the backbone of the whole strategy.

The Strategy Logic

The EA uses a two-step filter. First, the regime filter checks whether we're in a momentum-friendly environment. Then the CMO generates the actual entry signal. This prevents the classic mistake of buying an oversold CMO reading while price is in a freefall.

The regime filter is simple: the CMO itself, on a higher timeframe. If the H4 CMO is above +10, we only take long entries. If it's below -10, we only take shorts. Between those levels, the EA stands aside. This is a momentum regime filter — it's not trying to predict reversals, it's confirming that the trend has enough juice to justify a momentum trade.

Why H4 for the regime? It's a balance between noise and lag. M1 is useless — too much noise. D1 is too slow — by the time the daily CMO flips, the move is often over. H4 gives you a multi-day view of momentum without the whipsaw of lower timeframes. On EURUSD, an H4 CMO above +10 typically corresponds to a market that has made a clear directional commitment over the last 2-3 days.

Entries work like this:

  • Long entry: CMO crosses above +10 on the trading timeframe, and the H4 regime CMO is above +10.
  • Short entry: CMO crosses below -10 on the trading timeframe, and the H4 regime CMO is below -10.

Exits are where the ATR stop-loss comes in. Instead of a fixed stop, the EA calculates the Average True Range over a lookback period and places the stop at a multiple of that value. In a volatile week, the stop widens; in a quiet one, it tightens. That's the adaptive part — it keeps the stop outside the noise without you having to babysit the volatility regime.

I also want to clarify what this strategy is not doing. It's not a mean-reversion system. We're not buying when CMO hits -50 expecting a bounce. We're buying when CMO is already positive and then crosses a threshold — that's momentum confirmation. The regime filter ensures we're only doing that when the higher timeframe agrees. This distinction matters because the two approaches have completely different risk profiles, and mixing them up is a common source of confusion when traders look at the equity curve.

MQL5 Implementation

Let's get into the code. I'm going to show you the core logic, not a full file with every error handler, because you should understand what each piece does before you drop it into your own EA template. The full EA structure — OnInit(), OnTick(), OnDeinit() — follows the standard MQL5 skeleton, so I'll focus on the parts that are specific to this strategy.

CMO Calculation

MQL5 doesn't have a built-in CMO indicator, so you'll either write the calculation yourself or use iCustom() to call an external indicator. I prefer writing it inline for a few reasons: it's faster in the tester, there's no dependency on an external file, and you can see exactly what's happening. Here's the function:

double CalcCMO(const string symbol, const ENUM_TIMEFRAMES tf, const int period, const int shift)
{
   double gains = 0.0;
   double losses = 0.0;
   
   for(int i = shift + 1; i <= shift + period; i++)
   {
      double diff = iClose(symbol, tf, i) - iClose(symbol, tf, i + 1);
      if(diff > 0) gains += diff;
      else losses += -diff;
   }
   
   if(gains + losses == 0) return 0.0;
   return 100.0 * ((gains - losses) / (gains + losses));
}

That's the raw CMO. The loop iterates back through period bars, sums the gains and losses, and returns the normalized value. For the regime filter, you call this with a higher timeframe, like PERIOD_H4.

A few implementation details worth noting. First, the shift parameter — I pass 1 for the current signal bar, which means we're looking at the last closed bar. This is critical. If you use shift 0 in a live environment, the CMO value changes as the current bar forms, and your EA might enter a trade based on a value that no longer exists a minute later. In backtesting, using shift 0 introduces lookahead bias because the tester uses the final bar values. The results look fantastic and then fall apart in demo trading.

Second, the loop starts at shift + 1 and goes to shift + period. That's intentional — we need period + 1 price points to calculate period differences. If you're off by one here, your CMO values will be shifted and the cross detection will fire at the wrong time.

Third, the division by zero check. If gains + losses == 0, that means price hasn't moved at all over the period — every close is identical. That's rare in forex but can happen with illiquid symbols or during market closures. Returning 0.0 is a safe neutral value, but you could also return the previous CMO value to avoid a spurious cross signal.

ATR Stop-Loss Calculation

For the adaptive stop, use the built-in iATR() handle. The stop distance is the ATR value multiplied by a user-defined factor:

double CalcATRStop(const string symbol, const ENUM_TIMEFRAMES tf, const int atr_period, const double atr_multiplier)
{
   double atr[];
   ArraySetAsSeries(atr, true);
   
   int handle = iATR(symbol, tf, atr_period);
   if(handle == INVALID_HANDLE) return 0.0;
   
   CopyBuffer(handle, 0, 1, 1, atr);
   
   double atr_value = atr[0];
   if(atr_value == 0.0 || atr_value == EMPTY_VALUE) return 0.0;
   
   return atr_value * atr_multiplier;
}

Note the CopyBuffer call uses shift 1, not 0. That's deliberate — you want the ATR value from the closed bar, not the one currently forming. Using the current bar's ATR in a live EA introduces a lookahead bias that will make your backtest look better than reality. This is one of those subtle things that separates a credible EA from a fantasy one.

One thing I see a lot in poorly-written EAs: they create a new indicator handle on every tick. That's wasteful and can cause memory leaks over long runs. In your actual EA, create the ATR handle once in OnInit() and store it as a global variable. Then call CopyBuffer() in OnTick(). The function above is simplified for clarity, but in production code you'd want the handle created once.

Also consider which timeframe to use for the ATR. I use the trading timeframe (M15 by default) because that's where the stop will be placed. Some traders prefer a higher timeframe ATR for a more stable stop, but that creates a mismatch — your stop is based on H1 volatility while your entries are M15 signals. In my testing, using the same timeframe for both gives more consistent behavior.

Entry and Exit Logic

The entry logic checks the regime filter first, then the CMO cross. A cross is detected by comparing the current CMO value to the previous bar's value:

bool IsBuySignal()
{
   double cmo_current = CalcCMO(_Symbol, InpTradingTF, InpCMOPeriod, 1);
   double cmo_prev    = CalcCMO(_Symbol, InpTradingTF, InpCMOPeriod, 2);
   double cmo_regime  = CalcCMO(_Symbol, InpRegimeTF, InpCMOPeriod, 1);
   
   if(cmo_regime < InpRegimeLong) return false;
   if(cmo_prev <= InpEntryLevel && cmo_current > InpEntryLevel) return true;
   
   return false;
}

Same pattern for shorts, mirrored. The exit logic checks the opposite cross: a long exits when CMO crosses back below the exit level. I use the same value as the entry level for simplicity, but you can separate them — some traders like a wider exit threshold to let winners run.

There's a subtle issue with cross detection that I want to highlight. The condition cmo_prev <= InpEntryLevel && cmo_current > InpEntryLevel catches a cross from below to above. But what if CMO gaps from -20 to +30 in a single bar? That's still a valid cross — the condition catches it because cmo_prev is below the level and cmo_current is above. Good. What if CMO goes from -5 to +15? Also caught. What if it goes from +8 to +12? That's not a cross — both values are below the entry level in the first case and above in the second. The condition correctly rejects it.

But here's the edge case that trips people up: what if CMO goes from +12 to +8? That's a cross down through the level. For a long entry, we don't care. For a long exit, we do. The exit condition is the mirror: cmo_prev >= InpExitLevel && cmo_current < InpExitLevel. If you're using the same level for entry and exit, make sure you don't accidentally exit on the same bar you entered. In practice, a cross up followed by a cross down in the same bar is impossible — the bar has one close — but a cross up at bar close and a cross down at the next bar close is a one-bar trade. Whether that's acceptable depends on your trading style.

Full EA Input Parameters

Here's the input block you'd put at the top of the EA:

ParameterTypeDefaultDescription
InpCMOPeriodint14Lookback period for CMO calculation.
InpEntryLeveldouble10.0CMO cross level for entries and exits.
InpRegimeLong

Want to build your own version?

Recreate similar entry logic, risk rules, and filters in TradingBotMaker—no MQL coding. Start free.

Community

Clap for the article and open comments only when you want to read them.

0 claps0 comments

Related articles