One File Decides, Another File Acts

公開: 2026/09/16 UTC 更新: 2026/09/16 UTC この記事の URL

The EA in module 1 only decided. A trading EA also has to act — size a position, respect a daily loss limit, check the spread, send an order, manage what it opened. That is a lot of code, and almost none of it is your strategy.

Where you draw the line between the two is the most consequential architectural decision in an EA. Draw it in the wrong place and you end up unable to answer the only question that matters: is the idea bad, or is my execution bad?

Here is how the Trend EA draws it. The engine states it as a contract, in its own header:

//| Contract                                                         |
//| - This .mq5 contains ONLY MT5 event handlers + PlaceTrade()      |
//| - All utilities come from KurosawaHelpers.mqh                    |
//| - Strategy logic lives in Strategies/Trend.mqh                   |
//|                                                                  |
//| Design rules                                                     |
//| - Closed-bar signals: decide on bar close to reduce noise        |
//| - Strategy is pure: no order sending, only returns signals       |
//| - Engine owns safety: risk limits and execution protections      |

Two files. Strategies/Trend.mqh decides. Engines/TrendEA.mq5 acts. The strategy cannot place a trade even if it wanted to — it has no access to anything that could.

Two functions, one idea

The strategy module exposes its logic twice. This looks like duplication and is the whole point:

TrendResult Trend_EvaluateValues(
   const double emaFast,
   const double emaSlow,
   const double emaSlowPrev,
   const double rsi,
   const double rsiPrev,
   const double atr_points,
   const double adx,
   const TrendInputs &inps,
   TrendSignal &outSig)

Look at what that function takes: seven numbers and a settings struct. No indicator handles. No symbol. No chart. No MT5 at all.

You can call it with values you typed by hand. You can call it in a loop over a CSV of historical values. You can feed it the exact bar where your EA did something baffling last Tuesday and step through it. It has no way to know whether it is running in a terminal, a tester, or a unit test — and that is exactly what makes a strategy testable.

The second entry point is a thin adapter that fetches the numbers:

TrendResult Trend_EvaluateHandles(
   const int emaFastH,
   const int emaSlowH,
   const int rsiH,
   const int atrH,
   const int adxH,
   const int shift,
   const double point,
   const TrendInputs &inps,
   TrendSignal &outSig)

It reads each buffer, then hands the values to Trend_EvaluateValues. That is all it does.

The separation to copy is not "strategy file versus engine file". It is fetching versus deciding. Fetching touches the platform and is hard to test. Deciding is arithmetic and should be trivially testable. Most EAs weld them together, and then the only way to test a rule is to run the whole terminal.

Say why, not no

The strategy does not return a boolean. It returns a verdict:

enum TrendResult
{
   TREND_OK = 0,
   TREND_BLOCK_ATR,
   TREND_BLOCK_ADX,
   TREND_BLOCK_NO_BIAS,
   TREND_BLOCK_NO_SIGNAL,
   TREND_ERROR_DATA
};

And the engine counts every one of them:

   if(sres != TREND_OK)
   {
      if(sres == TREND_BLOCK_ATR)             g_diag.block_atr++;
      else if(sres == TREND_BLOCK_ADX)        g_diag.block_adx++;
      else if(sres == TREND_BLOCK_NO_SIGNAL)  g_diag.block_nosignal++;
      else                                    g_diag.block_indfail++;
      return;
   }

This is the difference between "the EA did not trade this week" and "the EA evaluated 480 bars, and the ATR floor rejected 471 of them." The first is a mystery. The second is a number you can act on — and in that example, the answer is that your volatility filter is set wrong, not that your idea is wrong.

Note also the distinction between TREND_BLOCK_NO_SIGNAL and TREND_ERROR_DATA. "I looked and there was nothing" and "I could not look" are different events. Collapse them into one and you will spend a weekend debugging a strategy that was never running.

The strategy depends on nothing

From the strategy module's header:

//| - No trade execution, no risk sizing, no session/spread gates     |
//| - No external includes (no KurosawaHelpers dependency)            |
//|                                                                   |
//| Notes                                                             |
//| - Uses ONLY MQL5 built-ins (CopyBuffer, ArraySetAsSeries, etc.)  |
//| - Caller passes `point` (usually _Point) so module is portable    |

Zero includes. You can copy Trend.mqh into a completely unrelated project and it compiles.

That last line is worth pausing on. The obvious thing is to use _Point inside the strategy. The module takes it as a parameter instead — and the engine, which knows what it is actually trading, passes the right one:

   // Important: use the traded symbol point size, not _Point (chart symbol may differ)
   const double pt = SymbolInfoDouble(sym, SYMBOL_POINT);

_Point is the chart's point size. Attach a USDJPY EA to a EURUSD chart and _Point is off by a factor of a hundred — so every ATR threshold in points silently means something else. A strategy module that reaches for globals inherits that bug. One that takes parameters cannot.

Enforced, not documented

Module 1's rule was: read closed bars only. Here is how the strategy module holds you to it:

   // Enforce closed-bar usage
   if(shift < 1) return TREND_ERROR_DATA;

Not a comment. Not a convention. The function refuses. Someone six months from now who passes 0 to "see the live value" gets an error return rather than a subtly wrong EA.

When you find a rule that matters, look for somewhere to encode it. A rule in a comment is a suggestion.

The trap: NaN slips past every gate

This one is worth internalising because it is invisible:

   // Reject non-finite inputs up front. NaN/Inf slip past every < / > gate
   // below (comparisons with NaN are always false), so guard explicitly.
   if(!MathIsValidNumber(emaFast) || !MathIsValidNumber(emaSlow) ||
      !MathIsValidNumber(emaSlowPrev) || !MathIsValidNumber(rsi) ||
      !MathIsValidNumber(atr_points) || !MathIsValidNumber(adx))
      return TREND_ERROR_DATA;

An indicator buffer that has not computed yet can hand you NaN. And every comparison involving NaN is falseNaN < 20 is false, and so is NaN >= 20. So a NaN does not fail your ATR floor. It passes straight through every filter you wrote, because each one was checking whether to block, and NaN never satisfies a block condition.

The buffer reader guards too, so it cannot leak in that way either:

   outVal = v[0];
   if(!MathIsValidNumber(outVal)) return false;   // reject NaN/Inf from indicator buffer

What the split caught: a state pretending to be an event

Here is the payoff. This comment sits in the strategy module, and it documents a real bug found in this code:

   // Without require_rsi_reclaim this is a STATE, not an event: "RSI <= 44 in
   // an uptrend" stays true for many consecutive bars while RSI is still
   // FALLING, so the EA buys into the decline and keeps qualifying on every
   // later bar. What limited re-entry was the cooldown and the one-position
   // rule, not the signal.

Read that carefully, because it is the most common unforced error in EA design. "RSI below 44" is a condition that persists. Writing it as an entry trigger means the EA does not buy the pullback — it buys every single bar of the pullback, all the way down, and only stops because some unrelated mechanism (a cooldown, a one-position-at-a-time rule) happens to be in the way.

The fix turns the state into an event:

      buySignal  = (rsiPrev <= inps.rsi_buy_below  && rsi >  inps.rsi_buy_below);
      sellSignal = (rsiPrev >= inps.rsi_sell_above && rsi <  inps.rsi_sell_above);

RSI had to be inside the zone on the previous closed bar and has now crossed back out of it. The threshold stops meaning "buy here" and starts meaning "how deep the pullback must go". The signal fires once per pullback instead of once per bar.

Ask this of every entry rule you write: is this a state or an event? If it is a state, something else in your EA is silently deciding your trade frequency.

Where the line actually falls

Stop-loss distance is calculated from ATR, which the strategy already read. So does SL belong to the strategy? The engine says no:

   // 5) Convert ATR signal into SL/TP distances in POINTS.
   //    We keep SL/TP distance logic in engine because it is execution policy.

The rule is worth stating generally. The strategy answers "is this a setup?" The engine answers "what do we do about it?" Stop distance, position size, trailing, time-stops, whether we are even allowed to trade right now — all execution policy. Change any of them and the strategy's answer does not change; only the outcome does.

That is also why the same engine shape carries every strategy in the suite. Swap the include and the input schema and you have a different EA.

What this was worth: the Trend strategy failed

Trend was rejected. Not softened, not quietly retired — rejected, with the numbers left up.

Screening stopped after three of ten pairs: EURUSD, GBPUSD and USDJPY, 846 trades on real ticks, profit factors of 0.92, 0.85 and 0.93. The same signature on all three — the stop was hit about two and a half times as often as the target, and the target was reached on 21–26% of the trades that ended at one or the other, against the 33% it needed just to break even at 2.2R. A session probe in the New York window came out worse. An H1 run, to test whether the losses were spread-bound, came back at 0.53.

You can read all of it, including the runs, on the preset's page.

So the architecture in this module produced a losing strategy. That is not an embarrassment; it is the point. Because the strategy was pure, the block counters could say which gate was eating the bars. Because the verdict was an enum, "no setup" and "could not read the indicator" never got confused. And because the engine knew nothing about Trend, killing Trend cost nothing else: two other strategies run on the same engine shape today with real money on them, and the Tokyo fix is so far the only family in the suite to clear the promotion gate.

An architecture that lets you abandon an idea cheaply is worth more than one that makes any single idea work.

What you should have now

A clear line between deciding and acting, a strategy function you can call with plain numbers, a verdict that explains itself, and a habit of asking whether a rule is a state or an event.

Next module: the input schema — why every tunable lives in its own header, and why a preset file is the difference between a result you can reproduce and a number you once saw.

EA づくりの参考になったら、ぜひ共有してください。
X Facebook LinkedIn

Keisuke Kurosawa
Hello

コメント

0
まだコメントはありません。

コメントするにはログインしてください。
共有
https://1kpips.com/ja/blog/strategy-and-engine
カテゴリ
Learn
タグ
MQL5, MT5, Expert Advisor, EA development, architecture, testing, tutorial, course

関連記事

次にやること
手を動かすなら、時間帯フィルタを 1 つ足してみてください。入れた場合と外した場合でバックテストを回せば、効いているかどうかがそのまま数字に出ます。