MQL5 下單與持倉管理:CTrade、魔術碼、停損停利

訊號會印了,這篇讓它真的下單。用 MT5 標準程式庫的 CTrade 類別開倉、帶停損停利、平倉,還有 EA 怎麼從一堆持倉裡認出自己的那一筆。範例沿用均線交叉:黃金交叉做多、死亡交叉做空,反向訊號來就先平掉再開。

整份程式在我的 MT5 上編譯過、用策略測試器跑過 2025 年 1 月的 EURUSD H1,文中日誌和數字是實際輸出。可以先下載 MaCrossTrade.mq5 跟著跑。它會真的下單,請掛在模擬帳號上。

先把範例跑起來

  1. 把 MaCrossTrade.mq5 放到 MQL5\Experts\,在 MetaEditor 打開、按 F7,0 errors。
  2. 策略測試器 Ctrl+R:EA 選 MaCrossTrade,EURUSD,H1,日期 2025.01.02 到 2025.01.31,模式「每個報價」,初始入金 10000,按「開始」。這隻 EA 有停損停利,一定要用「每個報價」:1 分鐘 OHLC 和只用開盤價那兩種模式,一分鐘只有開高低收四個價或更少,停損停利只在這幾個價位上檢查,碰到的時間和價格會不一樣。
  3. 跑完看報告的「總淨盈利」「交易總計」,「日誌」頁有每一筆開倉、平倉的紀錄。
  4. 要掛在模擬帳號的圖表上跑,拖上去時跳出的視窗裡「允許演算法交易」要勾,工具列的「演算法交易」(Ctrl+E)也要開著,EA 才能下單。

1. 參數與交易物件

//=== 1. 參數與交易物件 ==============================================
#include <Trade\Trade.mqh>                   // 標準程式庫的交易類別

input int            i_FastPeriod = 10;       // 快線期數
input int            i_SlowPeriod = 30;       // 慢線期數
input ENUM_MA_METHOD i_MaMethod   = MODE_EMA; // 均線種類
input int            i_AtrPeriod  = 14;       // ATR 期數
input double         i_Lots       = 0.10;     // 手數
input double         i_SlAtr      = 2.0;      // 停損 = ATR x 倍數
input double         i_TpAtr      = 3.0;      // 停利 = ATR x 倍數
input long           i_Magic      = 20250401; // 魔術碼

CTrade   g_Trade;                         // 下單、平倉都透過它
int      g_FastHandle  = INVALID_HANDLE;
int      g_SlowHandle  = INVALID_HANDLE;
int      g_AtrHandle   = INVALID_HANDLE;
datetime g_LastBarTime = 0;

#include <Trade\Trade.mqh> 把 MT5 附的 CTrade 類別拉進來。下單在 MQL5 底層是填一個 MqlTradeRequest 結構、呼叫 OrderSend()、再看回傳的結構,欄位十幾個;CTrade 把這些包起來,一行 Buy() 就搞定。宣告成全域變數 g_Trade,整隻 EA 共用一個,之後都用 g_Trade.Buy() 這種「物件.函數()」的寫法叫它做事。

停損停利(MT5 介面叫「止損」「止盈」,程式裡是 SL、TP)不用固定點數,用 ATR 的倍數:波動大,距離自動拉開。手數固定 0.10 手,先不管資金管理。

i_Magic 是魔術碼(magic number):EA 送出的每張單都帶這個號碼,之後從一堆持倉裡認出自己的就靠它。同一個帳號跑兩隻 EA、或同一隻 EA 掛在兩個週期,魔術碼要不一樣,不然會把對方的單當成自己的平掉。手動下的單魔術碼是 0。

2. OnInit:建指標、設定交易物件

//=== 2. OnInit:建指標、設定交易物件 ================================
int OnInit()
{
   g_FastHandle = iMA(_Symbol, _Period, i_FastPeriod, 0,
                      i_MaMethod, PRICE_CLOSE);
   g_SlowHandle = iMA(_Symbol, _Period, i_SlowPeriod, 0,
                      i_MaMethod, PRICE_CLOSE);
   g_AtrHandle  = iATR(_Symbol, _Period, i_AtrPeriod);
   if(g_FastHandle == INVALID_HANDLE ||
      g_SlowHandle == INVALID_HANDLE ||
      g_AtrHandle  == INVALID_HANDLE)
   {
      Print("指標建立失敗,錯誤碼 ", GetLastError());
      return INIT_FAILED;
   }

   g_Trade.SetExpertMagicNumber(i_Magic);    // 之後每張單都帶這個號碼
   g_Trade.SetDeviationInPoints(20);         // 能接受的滑點(點)
   g_Trade.SetTypeFillingBySymbol(_Symbol);  // 成交方式照商品規格
   Print("EA 啟動:", DoubleToString(i_Lots, 2), " 手,停損 ",
         i_SlAtr, " 倍 ATR,停利 ", i_TpAtr, " 倍 ATR,魔術碼 ",
         i_Magic);
   return INIT_SUCCEEDED;
}

指標的三個 handle 照舊在這裡建。OnDeinit 釋放指標、IsNewBar 判斷新 K 線兩段檔案裡有,這篇不貼。多的是 g_Trade 的三行設定。

SetExpertMagicNumber() 之後每張單自動帶魔術碼。SetDeviationInPoints() 是能接受的滑點(點),只有「即時執行」(Instant Execution)和更老的「執行請求」模式的商品用得到:報價差超過這個值,券商會回「重新報價」(requote)、不成交。Tickmill 的 EURUSD 是「市場執行」(Market Execution),券商直接照當下市價成交,這個值不起作用。SetTypeFillingBySymbol() 依商品規格挑成交方式(FOK 或 IOC,Tickmill 是 IOC)。

3. 找自己的持倉

//=== 3. 找自己的持倉 ================================================
// 回傳 POSITION_TYPE_BUY 或 POSITION_TYPE_SELL,沒有就回 -1
// 找到的持倉會處於「已選取」狀態,ticket 由參數帶回
int FindMyPosition(ulong &ticket)
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong t = PositionGetTicket(i);          // 選取第 i 個持倉
      if(t == 0)
         continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;                             // 別的商品
      if(PositionGetInteger(POSITION_MAGIC) != i_Magic)
         continue;                             // 別的 EA 或手動單
      ticket = t;
      return (int)PositionGetInteger(POSITION_TYPE);
   }
   return -1;
}

void ShowStatus()
{
   ulong ticket = 0;
   int   have   = FindMyPosition(ticket);
   if(have < 0)
   {
      Comment("沒有持倉");
      return;
   }
   double open   = PositionGetDouble(POSITION_PRICE_OPEN);
   double sl     = PositionGetDouble(POSITION_SL);
   double tp     = PositionGetDouble(POSITION_TP);
   double profit = PositionGetDouble(POSITION_PROFIT);
   Comment(have == POSITION_TYPE_BUY ? "多單" : "空單", " #", ticket,
           "  開倉價 ", DoubleToString(open, _Digits),
           "\nSL ", DoubleToString(sl, _Digits),
           "  TP ", DoubleToString(tp, _Digits),
           "\n浮動盈虧 ", DoubleToString(profit, 2));
}

從 MT4 過來最容易搞混的地方在這裡。MT4 一張單就是一筆部位,OrderSelect() 掃過去就好。MT5 分三層:你送出的是訂單(order),成交後留下成交紀錄(deal),手上真正的部位叫持倉(position)。要知道現在手上有什麼,看持倉。

PositionsTotal() 是帳號目前的持倉數,所有商品、所有 EA、手動單全算在內。PositionGetTicket(i) 選取第 i 筆並回傳它的編號(ticket),選取之後 PositionGetString()PositionGetInteger()PositionGetDouble() 讀的都是這一筆。商品跟魔術碼都對得上,才是自己的。這隻 EA 一次只留一筆,找到就回傳。

ShowStatus() 把那筆的開倉價、SL、TP、浮動盈虧顯示在圖表左上角,每個 tick 更新。策略測試器不是視覺化模式看不到,掛在圖表上才有。

4. 開倉:市價單帶停損停利

//=== 4. 開倉:市價單帶停損停利 ======================================
bool OpenPosition(int dir, double atr)
{
   double ask     = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid     = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double slDist  = atr * i_SlAtr;
   double tpDist  = atr * i_TpAtr;
   long stops = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double minDist = stops * _Point + (ask - bid);  // 最小距離+點差
   if(slDist < minDist) slDist = minDist;
   if(tpDist < minDist) tpDist = minDist;

   bool ok;
   if(dir == POSITION_TYPE_BUY)
      ok = g_Trade.Buy(i_Lots, _Symbol, 0,    // 0 = 用現在的 Ask
                       NormalizeDouble(ask - slDist, _Digits),
                       NormalizeDouble(ask + tpDist, _Digits));
   else
      ok = g_Trade.Sell(i_Lots, _Symbol, 0,   // 0 = 用現在的 Bid
                        NormalizeDouble(bid + slDist, _Digits),
                        NormalizeDouble(bid - tpDist, _Digits));

   if(!ok || g_Trade.ResultRetcode() != TRADE_RETCODE_DONE)
   {
      Print("下單失敗 ", g_Trade.ResultRetcode(), " ",
            g_Trade.ResultRetcodeDescription());
      return false;
   }
   Print(dir == POSITION_TYPE_BUY ? "開多" : "開空",
         " #", g_Trade.ResultOrder(),
         "  ", DoubleToString(i_Lots, 2), " 手 @",
         DoubleToString(g_Trade.ResultPrice(), _Digits),
         "  SL ", DoubleToString(g_Trade.RequestSL(), _Digits),
         "  TP ", DoubleToString(g_Trade.RequestTP(), _Digits));
   return true;
}

買在 Ask、賣在 Bid。多單的停損放在 Ask 下面 2 倍 ATR、停利放上面 3 倍 ATR;空單用 Bid 算,方向反過來。算出來的價格用 NormalizeDouble() 修到商品的小數位數,多幾位小數券商會拒。

SYMBOL_TRADE_STOPS_LEVEL 是券商規定停損停利離現價的最小距離(點),比它近會被拒,錯誤碼 10016。多單的停損券商是拿 Bid 檢查的,我們從 Ask 算,所以最小距離再加上點差。EURUSD 在我的 Tickmill 帳號是 0,別的商品或券商不一定,所以還是擋一下。

g_Trade.Buy(手數, 商品, 價格, 停損, 停利),價格填 0 就用當下的 Ask。回傳 true 只代表請求送到伺服器,有沒有成交要看 ResultRetcode():10009(TRADE_RETCODE_DONE)才是真的成交。失敗時 ResultRetcodeDescription() 會給一句英文說明,印出來就知道被拒的原因。

成交後 ResultOrder() 是訂單編號、ResultPrice() 是實際成交價,RequestSL()RequestTP() 是剛才送出去的值。在策略測試器裡 CTrade 自己還會多印一行 CTrade::OrderSend: market buy ...;掛在圖表上預設只有失敗才印。

5. OnTick:訊號來了,先平反向、再開新倉

//=== 5. OnTick:訊號來了,先平反向、再開新倉 ========================
void OnTick()
{
   ShowStatus();                    // 每個 tick 更新左上角
   if(!IsNewBar())
      return;

   double fast[], slow[], atr[];
   ArraySetAsSeries(fast, true);
   ArraySetAsSeries(slow, true);
   ArraySetAsSeries(atr,  true);
   if(CopyBuffer(g_FastHandle, 0, 0, 3, fast) < 3 ||
      CopyBuffer(g_SlowHandle, 0, 0, 3, slow) < 3 ||
      CopyBuffer(g_AtrHandle,  0, 0, 3, atr)  < 3)
   {
      Print("指標資料還沒準備好,錯誤碼 ", GetLastError());
      g_LastBarTime = 0;
      return;
   }
   bool goldenCross = fast[2] <= slow[2] && fast[1] > slow[1];
   bool deadCross   = fast[2] >= slow[2] && fast[1] < slow[1];
   if(!goldenCross && !deadCross)
      return;
   int dir = goldenCross ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;

   ulong ticket = 0;
   int   have   = FindMyPosition(ticket);
   if(have == dir)
      return;                       // 同方向已經有了,不加碼
   if(have >= 0)                    // 反向的先平掉
   {
      double profit = PositionGetDouble(POSITION_PROFIT);
      if(!g_Trade.PositionClose(ticket) ||
         g_Trade.ResultRetcode() != TRADE_RETCODE_DONE)
      {
         Print("平倉失敗 ", g_Trade.ResultRetcode(), " ",
               g_Trade.ResultRetcodeDescription());
         return;
      }
      Print("平倉 #", ticket, "  浮動盈虧 ",
            DoubleToString(profit, 2));
   }
   OpenPosition(dir, atr[1]);
}

訊號只看已收盤的 [1] 和 [2]。有訊號才去查持倉:同方向已經有一筆就不加碼;有反向的就先 PositionClose(ticket) 平掉再開新的。CTrade 會自己送一張反向市價單把它平掉,不用算方向,成交與否一樣看 ResultRetcode()

平倉前先讀 POSITION_PROFIT,那是平倉前一刻的浮動盈虧,單位是帳戶幣別,這裡是 USD。它就是工具箱「歷史」頁盈利欄那個數:測試器裡完全一樣,實盤平倉價可能差一點滑點;手續費、隔夜利息是另外兩欄。

停損停利是券商伺服器在盯,價格碰到就平,EA 關掉、電腦關機也照樣觸發。所以 EA 只印開倉和反向平倉,停損停利觸發的紀錄是測試器自己印的。

執行結果

2025 年 1 月,14 筆交易。只列 EA 自己 Print() 的行;CTrade 的內建紀錄、測試器的時間欄和它印的成交紀錄都省略:

EA 啟動:0.10 手,停損 2.0 倍 ATR,停利 3.0 倍 ATR,魔術碼 20250401
開多 #2  0.10 手 @1.03042  SL 1.02754  TP 1.03473
開空 #4  0.10 手 @1.03545  SL 1.03934  TP 1.02961
開多 #6  0.10 手 @1.02483  SL 1.02103  TP 1.03053
開空 #8  0.10 手 @1.02889  SL 1.03173  TP 1.02463
平倉 #8  浮動盈虧 -7.80
開多 #10  0.10 手 @1.02967  SL 1.02636  TP 1.03464
平倉 #10  浮動盈虧 -12.00
開空 #12  0.10 手 @1.02847  SL 1.03008  TP 1.02606
平倉 #12  浮動盈虧 -12.40
開多 #14  0.10 手 @1.02971  SL 1.02803  TP 1.03222
開空 #16  0.10 手 @1.02864  SL 1.03168  TP 1.02408
平倉 #16  浮動盈虧 -17.90
開多 #18  0.10 手 @1.03043  SL 1.02783  TP 1.03432
開空 #20  0.10 手 @1.04039  SL 1.04251  TP 1.03721
開多 #22  0.10 手 @1.04355  SL 1.04015  TP 1.04865
開空 #24  0.10 手 @1.04372  SL 1.04719  TP 1.03852
開多 #26  0.10 手 @1.04316  SL 1.03964  TP 1.04844
開空 #28  0.10 手 @1.04074  SL 1.04508  TP 1.03422

持倉編號通常就是開倉那張訂單的編號,所以開空 #8 之後平倉也叫 #8;日誌裡跳過的奇數是平倉那些訂單的編號。開倉後沒有「平倉」行的,就是被停損停利收掉的,最後一筆 #28 是測試結束時強制平掉。

14 筆的結局:7 筆停利、2 筆停損、4 筆被反向訊號平掉、最後 1 筆測試結束時強制平倉。報告的重點:

項目數值
交易總計14
盈利交易8 (57.14%)
總淨盈利237.28 USD

一個月 14 筆什麼都證明不了,有賺運氣成分居多。看得出來的是停利 3 倍、停損 2 倍 ATR 的效果:勝率四成就打平,這個月 57% 所以有賺。

1 月 16 到 17 日盤整那段開的 5 筆全賠:4 筆被反向訊號平掉、賠 7.8 到 17.9 塊,1 筆碰停損。均線交叉在盤整時就是這樣,兩條線黏在一起反覆穿越。

鎖倉帳號與單邊帳號

MT5 帳號有兩種模式。鎖倉(hedging)可以同一個商品同時持有多單和空單,各自是一筆持倉;單邊(netting)一個商品只有一筆持倉,反向下單直接抵銷。我的 Tickmill 帳號是鎖倉,開模擬帳號時的「使用鎖倉交易」選項就是這個。這篇的寫法是先平反向再開新倉,兩種模式跑出來都一樣;如果省掉平倉直接反向下單,鎖倉帳號會變成多空各一筆、單邊帳號會變成沒部位,寫 EA 之前要知道自己在哪一種上面。

常見的錯誤碼

代碼名稱意思
10009TRADE_RETCODE_DONE成交
10004TRADE_RETCODE_REQUOTE重新報價(市場執行的商品不會遇到)
10014TRADE_RETCODE_INVALID_VOLUME手數不合規格(低於最小手數、不是步進的倍數)
10016TRADE_RETCODE_INVALID_STOPS停損停利價位不合法(太近、方向反了)
10018TRADE_RETCODE_MARKET_CLOSED市場關閉
10019TRADE_RETCODE_NO_MONEY保證金不夠
10027TRADE_RETCODE_CLIENT_DISABLES_AT終端的「演算法交易」沒開

全部的代碼在 MetaEditor 按 F1 搜 Trade Server Return Codes。

本篇檔案:MaCrossTrade.mq5,放到 MQL5\Experts\ 底下,在 MetaEditor 打開按 F7 就能用。

發佈留言

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

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