返回《給 CFD 交易者的 MetaTrader 5 與 MQL5》
第四部分

完整的 EA

全部收在一個 EA 裡:環境濾網、ATR 停損、風險 sizing、驗證,以及乾淨的關機。

進階閱讀時間約 15 分鐘初學 → 進階第 17 / 17 堂
在 MetaEditor 實際跑一次
完整的順勢 EA

存成 MQL5/Experts/Lesson17CompleteEA.mq5。先用模擬帳戶。日線環境濾網、EMA 交叉進場、ATR 區間、1% 風險 sizing、完整驗證,以及乾淨的關機。

//+------------------------------------------------------------------+
//| Lesson 17 - Complete trend-following EA for CFD                  |
//| WARNING: sends real orders. Test on DEMO and understand the risk |
//| lesson before any live use.                                      |
//+------------------------------------------------------------------+
#property copyright "Strategist Academy"
#property version   "1.00"
#property strict

#include <Trade/Trade.mqh>

input group "Signal"
input int FastMAPeriod   = 20;              // Fast EMA
input int SlowMAPeriod   = 50;              // Slow EMA
input int TrendMAPeriod  = 200;             // Regime EMA (higher TF)

input group "Risk"
input double RiskPercent    = 1.0;          // Risk % of equity per trade
input double AtrSLMult      = 2.0;          // Stop = ATR x
input double AtrTPMult      = 4.0;          // Target = ATR x
input int    AtrPeriod      = 14;           // ATR period
input int    MaxSpreadPoints= 30;           // Skip if spread wider than this

input group "Execution"
input ulong MagicNumber     = 17091709;     // Magic number
input int   DeviationPoints = 10;           // Slippage tolerance (points)

CTrade   trade;
int      fastHandle  = INVALID_HANDLE;
int      slowHandle  = INVALID_HANDLE;
int      trendHandle = INVALID_HANDLE;
int      atrHandle   = INVALID_HANDLE;
datetime lastBarTime = 0;

//+------------------------------------------------------------------+
int OnInit()
{
   if(FastMAPeriod >= SlowMAPeriod)
   {
      Print("FastMA must be shorter than SlowMA");
      return(INIT_FAILED);
   }

   // Create handles ONCE. Doing this in OnTick leaks one per tick.
   fastHandle  = iMA(_Symbol, _Period, FastMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   slowHandle  = iMA(_Symbol, _Period, SlowMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   trendHandle = iMA(_Symbol, PERIOD_D1, TrendMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
   atrHandle   = iATR(_Symbol, _Period, AtrPeriod);

   if(fastHandle == INVALID_HANDLE || slowHandle == INVALID_HANDLE ||
      trendHandle == INVALID_HANDLE || atrHandle == INVALID_HANDLE)
   {
      Print("Indicator handle creation failed. Error: ", GetLastError());
      return(INIT_FAILED);
   }

   trade.SetExpertMagicNumber(MagicNumber);
   trade.SetDeviationInPoints(DeviationPoints);
   trade.SetTypeFillingBySymbol(_Symbol);

   Print("Complete EA ready on ", _Symbol, " / ", EnumToString((ENUM_TIMEFRAMES)_Period));
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   if(fastHandle  != INVALID_HANDLE) IndicatorRelease(fastHandle);
   if(slowHandle  != INVALID_HANDLE) IndicatorRelease(slowHandle);
   if(trendHandle != INVALID_HANDLE) IndicatorRelease(trendHandle);
   if(atrHandle   != INVALID_HANDLE) IndicatorRelease(atrHandle);
}

//+------------------------------------------------------------------+
//| Read one value from a handle at a given shift                    |
//+------------------------------------------------------------------+
bool ReadAt(const int handle, const int shift, double &out)
{
   double buf[];
   ArraySetAsSeries(buf, true);
   if(CopyBuffer(handle, 0, 0, shift + 3, buf) < shift + 3)
      return false;
   double v = buf[shift];
   if(v == EMPTY_VALUE || v <= 0.0)
      return false;
   out = v;
   return true;
}

//+------------------------------------------------------------------+
//| Volume rounded DOWN onto the broker's step                       |
//+------------------------------------------------------------------+
double NormalizeVolumeDown(const string sym, double vol)
{
   double vMin  = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
   double vMax  = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
   double vStep = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
   vol = MathMax(vMin, MathMin(vMax, vol));
   if(vStep > 0)
      vol = vMin + MathFloor((vol - vMin) / vStep + 0.0000001) * vStep;
   return NormalizeDouble(vol, 2);
}

//+------------------------------------------------------------------+
//| Lots so a stop of stopPoints costs at most riskMoney             |
//+------------------------------------------------------------------+
double CalcLots(const string sym, const double riskMoney, const double stopPoints)
{
   double tickValue = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
   double tickSize  = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
   if(tickValue <= 0 || tickSize <= 0 || stopPoints <= 0)
      return 0.0;
   double stopTicks  = (stopPoints * _Point) / tickSize;
   double riskPerLot = stopTicks * tickValue;
   if(riskPerLot <= 0)
      return 0.0;
   return NormalizeVolumeDown(sym, riskMoney / riskPerLot);
}

//+------------------------------------------------------------------+
void OnTick()
{
   // --- cheap checks first -------------------------------------------
   datetime t = iTime(_Symbol, _Period, 0);
   if(t == lastBarTime) return;
   lastBarTime = t;

   if(PositionSelect(_Symbol) &&
      (ulong)PositionGetInteger(POSITION_MAGIC) == MagicNumber)
      return;                                    // already in our position

   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);

   if((ask - bid) / _Point > MaxSpreadPoints)
   {
      Print("Spread too wide - skipping. Spread=",
            DoubleToString((ask - bid) / _Point, 1), " points");
      return;
   }

   // --- read indicators (shift 1 = last closed bar) -------------------
   double fast[1], slow[1], fastPrev[1], slowPrev[1], trend[1], atr[1];

   if(!ReadAt(fastHandle, 1, fast[0]) || !ReadAt(slowHandle, 1, slow[0]) ||
      !ReadAt(fastHandle, 2, fastPrev[0]) || !ReadAt(slowHandle, 2, slowPrev[0]) ||
      !ReadAt(trendHandle, 1, trend[0]) || !ReadAt(atrHandle, 1, atr[0]))
      return;                                    // not enough data yet

   // --- the signal ----------------------------------------------------
   bool bullRegime = (bid > trend[0]);           // daily trend agrees
   bool crossUp    = (fastPrev[0] <= slowPrev[0] && fast[0] > slow[0]);

   if(!(bullRegime && crossUp))
      return;

   // --- levels --------------------------------------------------------
   double atrVal    = atr[0];
   double stopPrice = NormalizeDouble(bid - atrVal * AtrSLMult, _Digits);
   double tpPrice   = NormalizeDouble(ask + atrVal * AtrTPMult, _Digits);
   double stopPts   = (bid - stopPrice) / _Point;

   long stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   if(stopPts < (double)stopsLevel)
   {
      Print("Stop ", DoubleToString(stopPts, 0), " pts is inside broker minimum ",
            stopsLevel, " - skipping rather than widening the stop.");
      return;
   }

   // --- sizing --------------------------------------------------------
   double riskMoney = AccountInfoDouble(ACCOUNT_EQUITY) * RiskPercent / 100.0;
   double lots      = CalcLots(_Symbol, riskMoney, stopPts);

   if(lots <= 0)
   {
      Print("Calculated size is zero - symbol limits make this unriskable at ",
            DoubleToString(riskMoney, 2));
      return;
   }

   double margin = 0.0;
   if(OrderCalcMargin(ORDER_TYPE_BUY, _Symbol, lots, ask, margin))
   {
      if(margin > AccountInfoDouble(ACCOUNT_MARGIN_FREE))
      {
         Print("Not enough free margin. Need ", DoubleToString(margin, 2),
               " have ", DoubleToString(AccountInfoDouble(ACCOUNT_MARGIN_FREE), 2));
         return;
      }
   }

   // --- send ----------------------------------------------------------
   bool ok = trade.Buy(lots, _Symbol, 0.0, stopPrice, tpPrice, "lesson17");

   Print("BUY ", ok ? "sent" : "FAILED",
         "  lots=", DoubleToString(lots, 2),
         "  sl=", DoubleToString(stopPrice, _Digits),
         "  tp=", DoubleToString(tpPrice, _Digits),
         "  retcode=", trade.ResultRetcode(),
         " (", trade.ResultRetcodeDescription(), ")");
}
如何執行
  1. 1. 從 MT5 開啟 MetaEditor(按 F4), 選擇這支程式該放的資料夾——ExpertsIndicators Scripts
  2. 2. 開新檔案、把這段程式貼上去,然後按 F7 編譯。Errors 分頁顯示的錯誤 都修掉。
  3. 3. 回到 MT5,把它拖到圖表上——如果是 EA,就打開 Strategy Tester

我們在組裝什麼

一支給 CFD 商品用的純多單順勢 EA。一條日線 EMA 定義環境,工作週期上的 EMA 交叉觸發進場,ATR 設定停損與目標,部位規模來自一個固定的風險百分比,而每一張單在離開這棟建築之前,都會拿券商自己的限制驗證過。

執行順序很重要

注意程式碼裡的順序。便宜的檢查先做——新 K 線、已存在部位、價差。昂貴的檢查次之——讀指標。然後才是驗證——stops level、成交量、保證金。最後才送出。這個順序讓最常見的情況(什麼都不做)幾乎不花代價,而當你在每個 tick 上跑的時候,那很重要。

每一道防護都在,是因為有東西壞過

  • 新 K 線閘門——因為少了它,那個 EA 會在每個 tick 上開倉。
  • 帶 magic 的部位檢查——因為一個去管理別的 EA 交易的 EA 是一場災難。
  • 價差濾網——因為在價差暴衝時進場純粹是成本。
  • stops level 驗證——因為「無效停損」是最常見的拒絕,而且除非你記錄它,否則它是靜默的。
  • 成交量正規化——因為 0.137 手不是一張合法的單。
  • 保證金檢查——因為正確的規模不一定是負擔得起的規模。
  • handle 釋放——因為洩漏會拖垮測試器,最後拖垮終端機。

刻意不做什麼

沒有馬丁格爾、沒有網格、沒有向下攤平、沒有 recovery zone。那些都是一邊製造平滑的權益曲線、一邊建立一個早晚會殺掉帳戶的部位的方法。如果一個系統需要它們,那個系統就是不行。

在上線之前

在你的資料裡的每一種環境上測試——趨勢、盤整、危機。跑一次 walk-forward。確認隔夜利息與手續費是真實的。在模擬帳戶上前瞻測試數週,不是數小時。並且把部位規模維持在小到「即使這一切你都判斷錯了,也還活得下來」的程度。

你現在知道什麼

你能讀也能寫 MQL5:指標、EA、腳本、handle、事件、訂單管理,以及風險導向的 sizing。這個 EA 裡的每一行都是第一到第三部分涵蓋過的東西。你接下來寫的系統,都會是這個模板的變體——而現在你知道該保留哪些防護。

你剛完成了什麼

給 CFD 交易者的 MetaTrader 5 與 MQL5》的第 17 / 17 堂課。跑完範例或讀完這段之後, 把它勾起來,然後進下一堂課。