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
- Trend.mqh (Strategy Module) Walkthrough
- TrendEA.mq5 (Engine) Walkthrough
- 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 strictenforces 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 TrendResultdescribes “why” the strategy didn’t signal:TREND_OKmeans signal is validTREND_BLOCK_*means your filters blocked tradingTREND_ERROR_DATAmeans indicator data was invalid/not ready
- Lines 30–43:
TrendInputsis the strategy’s input contract: RSI thresholds, ATR min/max, optional ADX band, and optional EMA slope requirement. - Lines 45–56:
TrendSignalis 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.
- Line 64: initializes
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_pointsis set and ATR is below it, block. - If
atr_max_pointsis set and ATR is above it, block.
- If
- 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 biasemaFast < emaSlow= down bias
- Lines 116–120: optional slope requirement:
- If enabled, up bias requires
emaSlow > emaSlowPrev. - Down bias requires
emaSlow < emaSlowPrev.
- If enabled, up bias requires
- 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
- Lines 127–128: if no buy/sell after rules, return
TREND_BLOCK_NO_SIGNAL, elseTREND_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 < 1is rejected so you never accidentally use the forming candle.
- Line 143:
- 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
- Fast EMA at
- 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 bypoint.
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
TrendResultso 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.