TrendEA.mq5 + Trend.mqh Explained: Line-by-Line Walkthrough (MT5) Part 1

Published: 2026/02/11 Updated: 2026/02/18 Permalink

Most EAs don’t fail because the indicator logic is wrong. They fail because the architecture is fragile.

In backtests, everything looks clean. In live trading, reality shows up: spread spikes, session drift, broker stop constraints, cooldown mistakes, loss streak spirals, and intrabar signal wobble. If your structure isn’t disciplined, your edge slowly leaks away.

This article breaks down two files line by line:

  • Strategies/Trend.mqh — the strategy module (pure signal generation)
  • Engines/TrendEA.mq5 — the execution engine (MT5 events, safety gates, risk control, order placement)

More importantly, we’ll explain why this separation matters. At 1kPips, this is the production pattern we recommend for EAs that are meant to survive years — not weeks.

The core philosophy:

  • Strategy stays pure. No order sending. No session logic. No risk sizing. Just math and decisions.
  • Engine owns reality. Spread gates, session windows, cooldowns, daily loss limits, SL/TP policy, broker validation — all centralized.
  • Closed-bar signals only. Evaluate once per completed candle. Stable backtests. Calmer live behavior. No intrabar noise.

If you’ve ever wondered:

  • “Why does my EA behave differently live vs backtest?”
  • “Why does it stop trading for no visible reason?”
  • “Why does it overtrade during bad regimes?”

The answer is usually architectural — not strategic.

Let’s walk through the implementation and see how each layer protects the system.


Table of Contents

  1. Trend.mqh (Strategy Module) Walkthrough
  2. TrendEA.mq5 (Engine) Walkthrough
  3. Architecture Notes: Why This Pattern Survives Longer

1) Trend.mqh (Strategy Module) Walkthrough

Think of Trend.mqh as a “math and decision” unit. It reads indicator values (via handles), applies filters (ATR/ADX), detects bias (EMA relationship + optional slope), and emits a simple signal: buy or sell.

1.1 Header + include guard

  • Lines 1–14: Documentation block. Key contract: closed-bar only, signal-only, portable (caller passes point).
  • Line 15: #property strict enforces stricter compilation rules (good for reliability).
  • Lines 17–18: Include guard prevents double-inclusion.

1.2 Return codes and data structures

  • Lines 20–28: enum TrendResult describes “why” the strategy didn’t signal:
    • TREND_OK means signal is valid
    • TREND_BLOCK_* means your filters blocked trading
    • TREND_ERROR_DATA means indicator data was invalid/not ready
    This is more than style: it makes optimization and troubleshooting easier because you can count blocks.
  • Lines 30–43: TrendInputs is the strategy’s input contract: RSI thresholds, ATR min/max, optional ADX band, and optional EMA slope requirement.
  • Lines 45–56: TrendSignal is the output: buy/sell flags + ATR in points, and optional debug fields for later logging/inspection.

1.3 Reading indicator buffers safely

  • Lines 58–77: Trend_ReadBuffer1()
    • Line 64: initializes outVal.
    • Lines 65–66: rejects invalid handle or shift.
    • Lines 68–71: allocates a 1-length dynamic array and sets series order.
    • Lines 72–73: reads exactly one value via CopyBuffer(). If MT5 doesn’t deliver one value, return false.
    • Lines 75–76: assigns value and returns true.
    This “read exactly 1 value and fail fast” pattern is a production habit. It prevents your EA from silently trading on garbage (zeros/uninitialized).

1.4 Core decision logic based on already-computed values

Trend_EvaluateValues() is the “purest” function: it depends only on numbers and inputs. This makes it testable and debuggable.

  • Lines 79–88: function signature + output reset.
  • Lines 89–96: clears signal, then stores debug fields into outSig.
  • Lines 97–99: rejects invalid ATR/RSI ranges. If ATR is non-positive or RSI not in 0..100, returns TREND_ERROR_DATA.
  • Lines 100–103: ATR gate:
    • If atr_min_points is set and ATR is below it, block.
    • If atr_max_points is set and ATR is above it, block.
    This is a regime filter. Trend systems often behave poorly in extreme quiet or extreme chaos.
  • Lines 104–111: Optional ADX gate:
    • Line 105: only meaningful if at least one ADX bound is set.
    • Line 107: if ADX is expected but not valid, treat as data error.
    • Lines 109–110: block if outside your allowed ADX band.
  • Lines 113–114: basic trend bias:
    • emaFast > emaSlow = up bias
    • emaFast < emaSlow = down bias
    No bias is allowed only if equal (rare) or you later filter it out.
  • Lines 116–120: optional slope requirement:
    • If enabled, up bias requires emaSlow > emaSlowPrev.
    • Down bias requires emaSlow < emaSlowPrev.
    This avoids “flat EMA” regimes where crosses are noisy.
  • Line 122: if neither up nor down, block with TREND_BLOCK_NO_BIAS.
  • Lines 124–125: entry trigger:
    • Up bias + RSI below threshold => buy
    • Down bias + RSI above threshold => sell
    Notice the intent: trend bias defines direction, RSI defines pullback.
  • Lines 127–128: if no buy/sell after rules, return TREND_BLOCK_NO_SIGNAL, else TREND_OK.

1.5 Evaluation using indicator handles (the “MT5 adapter”)

Trend_EvaluateHandles() bridges MT5 indicator handles to the pure value function. This keeps “platform details” out of your decision logic.

  • Lines 131–145: validates closed-bar enforcement and point value.
    • Line 143: shift < 1 is rejected so you never accidentally use the forming candle.
  • Line 146: prepares locals for indicator values.
  • Lines 148–154: reads:
    • Fast EMA at shift
    • Slow EMA at shift
    • Slow EMA at shift+1 (for slope check)
    • RSI at shift
    • ATR at shift
  • Lines 155–159: conditionally reads ADX only when filter is enabled and bounds exist.
  • Lines 161–168: calls Trend_EvaluateValues() and converts ATR from price to points by dividing by point.

Trend.mqh full listing (numbered)

001: //+------------------------------------------------------------------+
002: //| File: Strategies/Trend.mqh                                       |
003: //| Type: Strategy Module (independent)                              |
004: //|                                                                  |
005: //| Description                                                      |
006: //| SwingTrend strategy logic (signal generation only).              |
007: //| - Pure signal evaluation on CLOSED bars (shift=1 by default)     |
008: //| - No trade execution, no risk sizing, no session/spread gates    |
009: //| - No external includes (no KurosawaHelpers dependency)           |
010: //|                                                                  |
011: //| Notes                                                            |
012: //| - Uses ONLY MQL5 built-ins (CopyBuffer, ArraySetAsSeries, etc.)  |
013: //| - Caller passes `point` (usually _Point) so module is portable   |
014: //+------------------------------------------------------------------+
015: #property strict
016:
017: #ifndef KUROSAWA_STRATEGY_TREND_MQH
018: #define KUROSAWA_STRATEGY_TREND_MQH
019:
020: enum TrendResult
021: {
022:    TREND_OK = 0,
023:    TREND_BLOCK_ATR,
024:    TREND_BLOCK_ADX,
025:    TREND_BLOCK_NO_BIAS,
026:    TREND_BLOCK_NO_SIGNAL,
027:    TREND_ERROR_DATA
028: };
029:
030: struct TrendInputs
031: {
032:    double rsi_buy_below;
033:    double rsi_sell_above;
034:
035:    double atr_min_points;     // 0 disables
036:    double atr_max_points;     // 0 disables
037:
038:    bool   use_adx_filter;
039:    double adx_min_to_trade;   // 0 disables
040:    double adx_max_to_trade;   // 0 disables
041:
042:    bool   require_ema_slope;  // if true: require slow EMA rising/falling
043: };
044:
045: struct TrendSignal
046: {
047:    bool   buy;
048:    bool   sell;
049:    double atr_points;
050:
051:    // Optional debug
052:    double ema_fast;
053:    double ema_slow;
054:    double rsi;
055:    double adx;
056: };
057:
058: // Read 1 value from indicator buffer at `shift`
059: bool Trend_ReadBuffer1(const int handle,
060:                             const int buffer,
061:                             const int shift,
062:                             double &outVal)
063: {
064:    outVal = 0.0;
065:    if(handle == INVALID_HANDLE) return false;
066:    if(shift < 0) return false;
067:
068:    double v[];
069:    ArrayResize(v, 1);
070:    ArraySetAsSeries(v, true);
071:
072:    if(CopyBuffer(handle, buffer, shift, 1, v) != 1)
073:       return false;
074:
075:    outVal = v[0];
076:    return true;
077: }
078:
079: TrendResult Trend_EvaluateValues(
080:    const double emaFast,
081:    const double emaSlow,
082:    const double emaSlowPrev,
083:    const double rsi,
084:    const double atr_points,
085:    const double adx,
086:    const TrendInputs &inps,
087:    TrendSignal &outSig)
088: {
089:    outSig.buy        = false;
090:    outSig.sell       = false;
091:    outSig.atr_points = atr_points;
092:    outSig.ema_fast   = emaFast;
093:    outSig.ema_slow   = emaSlow;
094:    outSig.rsi        = rsi;
095:    outSig.adx        = adx;
096:
097:    if(atr_points <= 0.0) return TREND_ERROR_DATA;
098:    if(rsi < 0.0 || rsi > 100.0) return TREND_ERROR_DATA;
099:
100:    // ATR window gate
101:    if(inps.atr_min_points > 0.0 && atr_points < inps.atr_min_points) return TREND_BLOCK_ATR;
102:    if(inps.atr_max_points > 0.0 && atr_points > inps.atr_max_points) return TREND_BLOCK_ATR;
103:
104:    // Optional ADX gate (only meaningful if at least one bound is set)
105:    if(inps.use_adx_filter && (inps.adx_min_to_trade > 0.0 || inps.adx_max_to_trade > 0.0))
106:    {
107:       if(adx <= 0.0) return TREND_ERROR_DATA;
108:
109:       if(inps.adx_min_to_trade > 0.0 && adx < inps.adx_min_to_trade) return TREND_BLOCK_ADX;
110:       if(inps.adx_max_to_trade > 0.0 && adx > inps.adx_max_to_trade) return TREND_BLOCK_ADX;
111:    }
112:
113:    bool upTrend   = (emaFast > emaSlow);
114:    bool downTrend = (emaFast < emaSlow);
115:
116:    if(inps.require_ema_slope)
117:    {
118:       upTrend   = upTrend   && (emaSlow > emaSlowPrev);
119:       downTrend = downTrend && (emaSlow < emaSlowPrev);
120:    }
121:
122:    if(!upTrend && !downTrend) return TREND_BLOCK_NO_BIAS;
123:
124:    if(upTrend   && rsi <= inps.rsi_buy_below)  outSig.buy  = true;
125:    if(downTrend && rsi >= inps.rsi_sell_above) outSig.sell = true;
126:
127:    if(!outSig.buy && !outSig.sell) return TREND_BLOCK_NO_SIGNAL;
128:    return TREND_OK;
129: }
130:
131: TrendResult Trend_EvaluateHandles(
132:    const int emaFastH,
133:    const int emaSlowH,
134:    const int rsiH,
135:    const int atrH,
136:    const int adxH,
137:    const int shift,
138:    const double point,
139:    const TrendInputs &inps,
140:    TrendSignal &outSig)
141: {
142:    // Enforce closed-bar usage
143:    if(shift < 1) return TREND_ERROR_DATA;
144:    if(point <= 0.0) return TREND_ERROR_DATA;
145:
146:    double fast=0.0, slow=0.0, slowPrev=0.0, rsi=0.0, atr=0.0, adx=0.0;
147:
148:    if(!Trend_ReadBuffer1(emaFastH, 0, shift,   fast))     return TREND_ERROR_DATA;
149:    if(!Trend_ReadBuffer1(emaSlowH, 0, shift,   slow))     return TREND_ERROR_DATA;
150:    if(!Trend_ReadBuffer1(emaSlowH, 0, shift+1, slowPrev)) return TREND_ERROR_DATA;
151:
152:    if(!Trend_ReadBuffer1(rsiH, 0, shift, rsi)) return TREND_ERROR_DATA;
153:    if(!Trend_ReadBuffer1(atrH, 0, shift, atr)) return TREND_ERROR_DATA;
154:
155:    if(inps.use_adx_filter && (inps.adx_min_to_trade > 0.0 || inps.adx_max_to_trade > 0.0))
156:    {
157:       if(adxH == INVALID_HANDLE) return TREND_ERROR_DATA;
158:       if(!Trend_ReadBuffer1(adxH, 0, shift, adx)) return TREND_ERROR_DATA;
159:    }
160:
161:    return Trend_EvaluateValues(
162:       fast, slow, slowPrev,
163:       rsi,
164:       (atr / point),
165:       adx,
166:       inps,
167:       outSig
168:    );
169: }
170:
171: #endif

Wrap-up: What Trend.mqh Gives the Engine

At this point, Trend.mqh has done its job. It turns indicator data into a stable, closed-bar decision:

  • Direction bias from EMA fast vs slow (optionally confirmed by slope)
  • Timing trigger from RSI pullback thresholds
  • Regime filtering from ATR (and optionally ADX) windows
  • Clean visibility via TrendResult so the engine can count “why no trade”

Notice what it still does not do: it doesn’t send orders, doesn’t size risk, doesn’t check spread/session, and doesn’t manage positions. That’s intentional.

In production, you want strategy code to be easy to change and easy to test — and you want execution code to be boring, centralized, and hard to accidentally break. That’s exactly what this module enables.

In the next part, we’ll switch to the engine file TrendEA.mq5 and walk through the layer that makes this strategy tradable: indicator handle creation, closed-bar orchestration, safety gates, position sizing, broker constraint checks, and position management.

If this helped your EA work, share it.
X Facebook LinkedIn

Keisuke Kurosawa
Hello
Share
https://1kpips.com/en/blog/trend-ea-line-by-line-walkthrough
Categories
EA Development & Programming
Tags
MQL5 Code Walkthrough,Trend EA,MT5 EA Architecture,EA Execution Engine,Maintainable EA

Related Articles

Next step
Save this idea into your EA: add a session filter, then backtest with and without it to see the difference.