MT5 EA 上線前的檢查:交易權限、商品規格、接手持倉、下單失敗碼

回測報告很漂亮,EA 掛到圖表上卻一張單都沒下——多半不是策略的問題。可能是演算法交易沒開,可能是商品加不進市場報價,也可能單真的送出去了、被伺服器退回來,而 EA 沒有去看退回來的原因。這篇把掛上去之前該確認的事寫成程式碼:能不能交易、商品規格、EA 重新啟動之後怎麼接回手上的倉、下單失敗怎麼分類。

三個範例檔。PreflightCheck.mq5 是腳本,在真的終端上跑一次,把權限、帳戶、要交易的商品規格全部印出來,只讀不下單。MaCrossLive.mq5 是在均線交叉 EA MaCrossTrail.mq5(EMA 10/30 交叉進場、每筆賠餘額 1%、ATR 倍數停損停利、保本與移動停損)上面補自檢、接手持倉、下單失敗分類。RetcodeDemo.mq5 故意送三張會被拒的單,用來看伺服器回什麼。三個都在我的 MT5 上編譯過,腳本在連著伺服器的終端跑、兩隻 EA 在策略測試器跑,文中的日誌是實際輸出。

先把範例跑起來

  1. PreflightCheck.mq5 放到 MQL5\Scripts\,在 MetaEditor 按 F7;回終端從「導航」把它拖到任何一張圖表上,輸出在「工具箱」的「專家」頁。腳本不跳參數視窗,要換商品清單就改 i_Symbols 的預設值重編。
  2. MaCrossLive.mq5 放到 MQL5\Experts\,按 F7;策略測試器 Ctrl+R:EA 選 MaCrossLive,EURUSD,H1,2025.01.02 到 2025.01.31,模式「每次報價」,入金 10000,杠杆 1:100,按「開始」。
  3. 看「日誌」頁,開頭會多兩行「自檢通過」和「沒有要接手的持倉」,那就是這篇加的東西。
  4. RetcodeDemo.mq5 也放 MQL5\Experts\,EA 改選它、其餘設定不動再跑一次。它只送三張錯的單、印出伺服器回的碼,而且只肯在策略測試器裡啟動

1. 先問能不能交易

//=== 1. 自檢:連上了沒、四個交易開關、這個商品能不能下 ==============
bool Preflight()
{
   if(TerminalInfoInteger(TERMINAL_CONNECTED) == 0)
      return false;                    // 還沒連上,下一個 tick 再看

   string bad = "";
   if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) == 0)
      bad += "終端的演算法交易沒開;";
   if(MQLInfoInteger(MQL_TRADE_ALLOWED) == 0)
      bad += "這張圖的 EA 沒允許演算法交易;";
   if(AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) == 0)
      bad += "帳戶不能交易;";
   if(AccountInfoInteger(ACCOUNT_TRADE_EXPERT) == 0)
      bad += "帳戶不收 EA 的單;";
   if(SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE)
      != SYMBOL_TRADE_MODE_FULL)
      bad += _Symbol + " 現在不能完整交易;";
   if(SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) <= 0 ||
      SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE_LOSS) <= 0)
      bad += _Symbol + " 規格讀不到;";

   if(bad != g_LastBad)                 // 狀況變了才印,免得洗版
   {
      if(bad == "")
         Print("自檢通過:", _Symbol, "  ",
               AccountInfoInteger(ACCOUNT_MARGIN_MODE) ==
               ACCOUNT_MARGIN_MODE_RETAIL_HEDGING ? "鎖倉" : "單邊",
               "帳戶  餘額 ", DoubleToString(
               AccountInfoDouble(ACCOUNT_BALANCE), 2));
      else
         Print("不能交易:", bad);
      g_LastBad = bad;
   }
   return bad == "";
}

四個開關,少一個 EA 就下不了單,而且它們分屬不同的地方:

  • TERMINAL_TRADE_ALLOWED:工具列那顆「演算法交易」按鈕(Ctrl+E)。最常忘的一個,關著的時候終端會印「用戶端已禁用演算法交易」。
  • MQL_TRADE_ALLOWED:這支程式自己的開關,掛上去時「專家屬性」裡的「允許演算法交易」。同一個終端可以一隻 EA 開、一隻關。
  • ACCOUNT_TRADE_ALLOWED:帳戶端。用投資者密碼登入就是只讀,看得到跑得動,就是下不了單。
  • ACCOUNT_TRADE_EXPERT:伺服器允不允許 EA 下單,關著會印「伺服器端已禁用演算法交易」。

通過之後印一行,裡面那個「鎖倉」是帳戶的持倉模式。鎖倉帳戶同一個商品可以同時有多單和空單,單邊帳戶反向下單會跟原本的倉軋掉。先平倉再開新倉那種寫法兩種帳戶都能動;直接送反向單讓它自己軋掉的寫法,換到鎖倉帳戶就會變成兩個倉。

檢查放在 OnTick 不放 OnInit,因為 OnInit 跑的時候終端可能還沒連上伺服器。這不是假設:我用啟動設定讓終端一開機就跑腳本,那支腳本讀到的持倉模式是 0——而 0 正好就是「單邊」,看起來是個正常值,其實只是還沒連線。沒連上也不會有 tick,所以 EA 不必卡著等,每個 tick 檢查一次就好,過了才把 g_Ready 打開、順手接手持倉(第 3 節)。

void OnTick()
{
   if(!g_Ready)
   {
      if(!Preflight())                 // 沒過就什麼都不做,下個 tick
         return;                       // 再檢查一次
      g_Ready = true;
      AdoptPosition();                 // 過了才看手上有沒有倉
   }
   ...

腳本沒有 tick 可以等,就自己等:

bool WaitConnected(int seconds)
{
   for(int i = 0; i < seconds * 10 && !IsStopped(); i++)
   {
      if(TerminalInfoInteger(TERMINAL_CONNECTED))
         return true;
      Sleep(100);
   }
   return false;
}

腳本在我的帳戶上印出來是這樣(帳號和餘額我遮掉了):

終端的演算法交易  是   這支程式  是
帳戶可交易        是   帳戶收 EA 單  是
帳戶 ******** @Tickmill-Live  鎖倉
  杠杆 1:1000  結算貨幣 USD  餘額 ****

2. 商品規格用問的,不要寫死

   int    digits = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
   double point  = SymbolInfoDouble(sym, SYMBOL_POINT);
   double vmin   = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
   double vstep  = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
   double vmax   = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
   double contr  = SymbolInfoDouble(sym,
                                    SYMBOL_TRADE_CONTRACT_SIZE);
   double tickSz = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
   double tickVl = SymbolInfoDouble(sym,
                                    SYMBOL_TRADE_TICK_VALUE_LOSS);
   long   stops  = SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL);
   long   freeze = SymbolInfoInteger(sym, SYMBOL_TRADE_FREEZE_LEVEL);
   long   fill   = SymbolInfoInteger(sym, SYMBOL_FILLING_MODE);
   long   trade  = SymbolInfoInteger(sym, SYMBOL_TRADE_MODE);
   long   exe    = SymbolInfoInteger(sym, SYMBOL_TRADE_EXEMODE);

前面還有一行 SymbolSelect(sym, true),加不進市場報價就是名字打錯了——經紀商的後綴五花八門,這一步先擋掉。接著每個數字都有用處:手數的最小/步進/最大決定你算出來的手數能不能送;SYMBOL_TRADE_STOPS_LEVEL 是停損停利離現價的最小距離,SYMBOL_TRADE_FREEZE_LEVEL 是「離現價多近就不准再碰這張單」(改單、平倉、刪掛單都不行);SYMBOL_FILLING_MODE 是這個商品收哪種成交方式,CTradeSetTypeFillingBySymbol() 就是照它填的。

同一個經紀商的六個商品,數字差成這樣:

商品最小 / 步進 / 最大手數合約大小tick 大小每 tick 值 (USD)
EURUSD0.01 / 0.01 / 100.001000000.000011.00
GBPUSD0.01 / 0.01 / 100.001000000.000011.00
USDJPY0.01 / 0.01 / 100.001000000.0010.64
XAUUSD0.01 / 0.01 / 100.001000.011.00
US5000.01 / 0.01 / 250.0010.010.01
BRENT1.00 / 1.00 / 10000.0010.010.01

BRENT 最小 1.00 手、步進也是 1.00 手:按風險算出來的手數只要不是整數就得捨去,算出不到 1 手就整筆做不了。把 0.01 當成天經地義寫死在程式裡,換到這種商品就一路被退。

「每 tick 值」讀的是 SYMBOL_TRADE_TICK_VALUE_LOSS,1 手跳一個 tick 虧多少錢。報價貨幣跟帳戶貨幣一樣的商品,它就是 tick 大小乘合約大小;USDJPY 那個 0.64 是日圓換成美元來的,跟著匯率每天變,所以算手數時每次都要重新問。

這個帳戶六個商品的停損距離下限和凍結距離都是 0,別當成通例——換一家經紀商、換一個商品就可能不是。腳本印出來的規格行長這樣,這裡貼手數差最多的兩個:

規格 EURUSD  手數 0.01/0.01/100.00  合約 100000
  停損 0 點  凍結 0 點  tick 0.00001 值 1.00  成交 IOC  執行 市場
規格 BRENT  手數 1.00/1.00/10000.00  合約 1
  停損 0 點  凍結 0 點  tick 0.01 值 0.01  成交 IOC  執行 市場

3. EA 會重來一次,持倉不會

//=== 3. 接手持倉:EA 重來一次,倉還在伺服器那邊 =====================
void AdoptPosition()
{
   ulong ticket = 0;
   int   type   = FindMyPosition(ticket);
   if(type < 0)
   {
      Print("沒有要接手的持倉");
      return;
   }
   Print("接手 #", ticket, " ",
         type == POSITION_TYPE_BUY ? "多單" : "空單", " ",
         DoubleToString(PositionGetDouble(POSITION_VOLUME), 2),
         " 手 @", DoubleToString(
         PositionGetDouble(POSITION_PRICE_OPEN), _Digits),
         "  停損 ", DoubleToString(
         PositionGetDouble(POSITION_SL), _Digits),
         "  停利 ", DoubleToString(
         PositionGetDouble(POSITION_TP), _Digits));
}

改參數、換週期、換模板、重新編譯、終端重開,OnInit 都會再跑一次,全域變數全部回到初始值;伺服器那邊的持倉不會跟著不見。所以「這筆已經保本過了」「進場時的 ATR 是多少」這種記在變數裡的東西,重來之後就對不上了。

兩條路。一條是狀態全部從持倉讀回來:搬停損那段就是這樣寫的,每次都從 POSITION_SL 和進場價現算,重來幾次都一樣。另一條是像上面這樣,自檢過了之後掃一次持倉,把 EA 要用的東西重建起來;這裡只做最小的一步——把手上的倉找出來印一行,至少日誌裡看得到它接手了什麼。

這件事回測測不到:策略測試器不會重啟 EA,這行在測試器裡永遠印「沒有要接手的持倉」。要確認,拿模擬帳戶掛上去,在有持倉的時候打開「專家屬性」把任何一個參數改一下再按確定,這樣就會重跑一次 OnInit,看日誌有沒有印出接手那行。

4. 下單失敗要分類,不是重送就好

//=== 4. 下單失敗:先分類,再決定要不要重送 ==========================
#define FAIL_RETRY   1                  // 換個價再送一次就好
#define FAIL_UNKNOWN 2                  // 送成功了沒?先查持倉
#define FAIL_STOP    3                  // 自己送錯的,重送一樣錯

int ClassifyFail(uint code)
{
   switch(code)
   {
      case TRADE_RETCODE_REQUOTE:           // 10004 重新報價
      case TRADE_RETCODE_PRICE_CHANGED:     // 10020 價格變了
      case TRADE_RETCODE_PRICE_OFF:         // 10021 沒有報價
      case TRADE_RETCODE_TOO_MANY_REQUESTS: // 10024 請求太頻繁
         return FAIL_RETRY;
      case TRADE_RETCODE_TIMEOUT:           // 10012 請求逾時
      case TRADE_RETCODE_CONNECTION:        // 10031 沒有連線
         return FAIL_UNKNOWN;
   }
   return FAIL_STOP;                        // 手數、停損、錢不夠…
}

bool SendMarket(bool isBuy, double lots, double sl, double tp)
{
   for(int n = 1; n <= i_Retry; n++)
   {
      bool ok = isBuy ? g_Trade.Buy(lots, _Symbol, 0, sl, tp)
                      : g_Trade.Sell(lots, _Symbol, 0, sl, tp);
      uint code = g_Trade.ResultRetcode();
      if(ok && (code == TRADE_RETCODE_DONE ||        // 10009 全部成交
                code == TRADE_RETCODE_DONE_PARTIAL)) // 10010 部分成交
         return true;                    // 部分成交也是有倉,別當失敗
      Print("下單失敗 ", code, " ",
            g_Trade.ResultRetcodeDescription(), "(第 ", n, " 次)");
      int what = ClassifyFail(code);
      if(what == FAIL_STOP)
         return false;
      if(what == FAIL_UNKNOWN)
      {
         Sleep(1000);                   // 給伺服器一點時間回報
         ulong t = 0;
         if(FindMyPosition(t) >= 0)
         {
            Print("  查到持倉 #", t, ",這張其實送成功了");
            return true;
         }
         Print("  結果不明又查不到持倉,這根 K 線不再送");
         return false;
      }
      Sleep(200 * n);                   // 可重送:退一步再來
   }
   return false;
}

迴圈最多送 i_Retry 次(預設 3),成功、或碰到不該再送的就直接出來。市價單成功的碼有兩個:10009 全部成交,10010 部分成交。IOC 這種成交方式本來就允許只成交一部分,這時候伺服器上已經有倉了,要是只當成失敗回去,日誌會少一行開倉、狀態也對不上。平倉那條路一樣,部分平倉之後手上還有剩,別急著開反手單。

失敗分三類,差別在「這張單到底送成功了沒」。價格變了、重新報價、沒有報價、請求太頻繁,這些是伺服器明確拒絕,沒有成交,退一步再送一次就好。手數不對、停損不對、錢不夠,是自己送錯的,重送一百次也一樣。

麻煩的是逾時和沒有連線:這兩個代表你不知道結果。請求可能已經到了伺服器並且成交,只是回覆沒傳回來,這時候盲目重送就會變成兩張單。所以先等一下去查持倉,查到就當它成功了。要留意查的是終端本地那份持倉清單,真的斷線時它也更新不了——查不到就別再送,等連上之後的 tick 再說。

故意送錯的三張單,測試器回的碼:

最小 0.01 步進 0.01 最大 100.00 可用保證金 10000.00
手數 0.01333333 不是步進的倍數 -> 10014 invalid volume
多單的停損掛在現價之上 -> 10016 invalid stops
手數 100.00 保證金不夠 -> 10019 not enough money

100 手需要 205128.00 的保證金而帳戶只有 10000.00——這個數字是測試器按商品的保證金規格算的,不是拿帳戶杠杆乘一乘就會得到。三張都沒有成交,餘額一毛沒動。

常用的幾個碼,號碼是終端自己印出來的:

重新報價 10004  價格變了 10020  沒有報價 10021  請求太頻繁 10024
請求逾時 10012  沒有連線 10031  市場關閉 10018  禁止交易 10017
全部成交 10009  部分成交 10010  終端禁用 10027  伺服器禁用 10026
回傳碼意思怎麼辦
10009全部成交正常
10010部分成交有倉了,照成交量繼續
10004重新報價換個價再送
10020價格變了換個價再送
10021沒有報價換個價再送
10024請求太頻繁退一步再送
10012請求逾時結果未知,先查持倉
10031沒有連線結果未知,先查持倉
10014手數無效照規格修手數
10016停損停利無效照 StopsLevel 修距離
10019錢不夠縮手數或別做
10018市場關閉等開市
10017禁止交易查權限那四項
10027用戶端禁用 EA按 Ctrl+E 或查專家屬性
10026伺服器端禁用 EA問經紀商

執行結果

MaCrossLive 在 EURUSD 2025 年 1 月跑一趟,日誌開頭多了兩行:

EA 啟動:保本 1.0 ATR +10 點,移動停損 2.0 ATR,停利 3.0 ATR
自檢通過:EURUSD  鎖倉帳戶  餘額 10000.00
沒有要接手的持倉

後面 EA 自己印的 78 行開倉、保本、移動、平倉,跟同一套訊號但沒有這些檢查的 MaCrossTrail 逐字相同:14 筆交易、總淨盈利 416.21。這是應該的——自檢通過之後那條路徑跟原本一模一樣,檢查只在「不該下單的時候」才會改變行為。

上線前的清單

把上面四件事排成順序,掛上去之前走一遍:

  • 跑一次 PreflightCheck,四個開關都要是「是」,要交易的商品規格印得出來。
  • 手數、停損距離、合約大小全部從 SymbolInfo 問,不要寫死在程式裡。
  • EA 重來一次之後,狀態要能從持倉重建。
  • 下單失敗一定要看回傳碼,分成「換個價再送」「結果未知去查持倉」「別再送了」三類,成功別漏掉部分成交。
  • 先用模擬帳戶掛一段時間:斷線、重啟、重新報價、請求逾時、真實滑價,這些回測裡都不會發生。

自檢是起飛前檢查,不是全程監看——g_Ready 過了就不再檢查。想要盤中有人按掉「演算法交易」也能發現,就別把它閂死,讓它每根 K 線再跑一次。

本篇檔案:PreflightCheck.mq5 是腳本,放 MQL5\Scripts\MaCrossLive.mq5RetcodeDemo.mq5 是 EA,放 MQL5\Experts\。在 MetaEditor 打開按 F7 就能用。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料