這篇講 EA 怎麼下單:用 MT5 標準程式庫的 CTrade 類別開倉、帶停損停利、平倉,還有 EA 怎麼從一堆持倉裡認出自己的那一筆。範例是均線交叉:黃金交叉做多、死亡交叉做空,反向訊號來就先平掉再開。
整份程式在我的 MT5 上編譯過、用策略測試器跑過 2025 年 1 月的 EURUSD H1,文中日誌和數字是實際輸出。可以先下載 MaCrossTrade.mq5 跟著跑。它會真的下單,請掛在模擬帳戶上。
先把範例跑起來
- 把 MaCrossTrade.mq5 放到
MQL5\Experts\,在 MetaEditor 打開、按F7,0 errors。 - 策略測試器
Ctrl+R:EA 選 MaCrossTrade,EURUSD,H1,日期 2025.01.02 到 2025.01.31,模式「每次報價」,入金 10000,按「開始」。這隻 EA 有停損停利,最後驗證要用「每次報價」:「1分鐘OHLC」一分鐘只有開高低收四個價,「僅使用開價」一根圖表週期的 K 線只有少數幾個價(這裡一根 H1 四個),停損停利只在這些價位上檢查,碰到的時間和價格可能不一樣。像這隻只在收盤判斷、停損停利離得遠的,開發時可以先用 1分鐘OHLC 求快,定案前再用每次報價跑一次。 - 跑完看報告的「總淨盈利」「交易總計」,「日誌」頁有每一筆開倉、平倉的紀錄。
- 要掛在模擬帳戶的圖表上跑,拖上去時跳出的視窗裡「允許演算法交易」要勾,工具列的「演算法交易」(
Ctrl+E)也要開著,EA 才能下單。
1. 參數與交易物件
//=== 1. 參數與交易物件 ==============================================
#include <Trade\Trade.mqh> // 標準程式庫的交易類別
#include <Indicators\Trend.mqh> // CiMA
#include <Indicators\Oscilators.mqh> // CiATR
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; // 下單、平倉都透過它
CiMA g_Fast; // 指標物件,handle 包在裡面
CiMA g_Slow;
CiATR g_Atr;
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 手,先不管資金管理;EURUSD 1 手的合約大小是 100000 歐元,0.10 手就是 10000 歐元。
做多是先買後賣,價格漲了賺;做空是先賣後買,跌了賺。開多買在 Ask、平多賣在 Bid,開空賣在 Bid、平空買在 Ask,所以多單的停損停利由 Bid 觸發,空單由 Ask 觸發。

i_Magic 是魔術碼(magic number):EA 送出的每張單都帶這個號碼,之後從一堆持倉裡認出自己的就靠它。同一個帳戶跑兩隻 EA、或同一隻 EA 掛在兩個週期,魔術碼要不一樣,不然會把對方的單當成自己的平掉。手動下的單魔術碼是 0。
2. OnInit:建指標、設定交易物件
//=== 2. OnInit:建指標、設定交易物件 ================================
int OnInit()
{
if(!g_Fast.Create(_Symbol, _Period, i_FastPeriod, 0,
i_MaMethod, PRICE_CLOSE) ||
!g_Slow.Create(_Symbol, _Period, i_SlowPeriod, 0,
i_MaMethod, PRICE_CLOSE) ||
!g_Atr.Create(_Symbol, _Period, i_AtrPeriod))
{
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;
}三個指標物件在這裡 Create(),後面三行設定 g_Trade。OnDeinit 和 IsNewBar 兩段檔案裡有,這篇不貼。
SetExpertMagicNumber() 之後每張單自動帶魔術碼。SetDeviationInPoints() 是能接受的滑點(點),只有「即時執行」和更老的「執行請求」模式的商品用得到:報價差超過這個值,券商會回「重新報價」(requote)、不成交。Tickmill 的 EURUSD 是「市場執行」,券商直接照當下市價成交,這個值不起作用。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 下面 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)是全部成交,10010(TRADE_RETCODE_DONE_PARTIAL)是只成交一部分,這時也已經有倉了。MaCrossTrade.mq5 只認 10009,遇到 10010 會印「下單失敗」,但成交的部分已是帶魔術碼的持倉,FindMyPosition() 找得到:同方向訊號不再加碼,反向訊號照樣平掉。失敗時 ResultRetcodeDescription() 會給一句英文說明,印出來就知道被拒的原因。
成交後 ResultOrder() 是訂單編號、ResultPrice() 是實際成交價,RequestSL()、RequestTP() 是剛才送出去的值。在策略測試器裡 CTrade 自己還會多印一行 CTrade::OrderSend: market buy ...;掛在圖表上預設只有失敗才印。
5. OnTick:訊號來了,先平反向、再開新倉
//=== 5. OnTick:訊號來了,先平反向、再開新倉 ========================
void OnTick()
{
ShowStatus(); // 每個 tick 更新左上角
if(!IsNewBar())
return;
g_Fast.Refresh(); // 抓最新指標值,再用 Main(i) 讀
g_Slow.Refresh();
g_Atr.Refresh();
double fast1 = g_Fast.Main(1), fast2 = g_Fast.Main(2);
double slow1 = g_Slow.Main(1), slow2 = g_Slow.Main(2);
double atr1 = g_Atr.Main(1);
if(fast2 == EMPTY_VALUE || slow2 == EMPTY_VALUE ||
atr1 == EMPTY_VALUE)
{
Print("指標資料還沒準備好,錯誤碼 ", GetLastError());
g_LastBarTime = 0;
return;
}
bool goldenCross = fast2 <= slow2 && fast1 > slow1;
bool deadCross = fast2 >= slow2 && fast1 < slow1;
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, atr1);
}先 Refresh() 再用 Main(1)、Main(2) 讀已收盤的兩根,不看還在跳的 Main(0)。有訊號才去查持倉:同方向已經有一筆就不加碼;有反向的就先 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.30 USD |
一個月 14 筆什麼都證明不了,有賺運氣成分居多。看得出來的是停利 3 倍、停損 2 倍 ATR 的效果:勝率四成就打平(2 ÷ (2 + 3) = 40%),這個月 57% 所以有賺。
1 月 16 到 17 日盤整那段開的 5 筆全賠:4 筆被反向訊號平掉、賠 7.8 到 17.9 塊,1 筆碰停損。均線交叉在盤整時就是這樣,兩條線黏在一起反覆穿越。
鎖倉帳戶與單邊帳戶
MT5 帳戶有兩種模式。鎖倉(hedging)可以同一個商品同時持有多單和空單,各自是一筆持倉;單邊(netting)一個商品只有一筆持倉,反向下單直接抵銷。我的 Tickmill 帳戶是鎖倉,開模擬帳戶時的「使用鎖倉交易」選項就是這個。這篇的寫法是先平反向再開新倉,兩種模式跑出來都一樣;如果省掉平倉直接反向下單,鎖倉帳戶會變成多空各一筆、單邊帳戶會變成沒部位,寫 EA 之前要知道自己在哪一種上面。
常見的錯誤碼
- 10009
TRADE_RETCODE_DONE:全部成交 - 10004
TRADE_RETCODE_REQUOTE:重新報價(市場執行的商品不會遇到) - 10014
TRADE_RETCODE_INVALID_VOLUME:手數不合規格(低於最小手數、不是步進的倍數) - 10016
TRADE_RETCODE_INVALID_STOPS:停損停利價位不合法(太近、方向反了) - 10018
TRADE_RETCODE_MARKET_CLOSED:市場關閉 - 10019
TRADE_RETCODE_NO_MONEY:保證金不夠 - 10027
TRADE_RETCODE_CLIENT_DISABLES_AT:終端的「演算法交易」沒開
全部的代碼在 MetaEditor 按 F1 搜 Trade Server Return Codes。
本篇檔案:MaCrossTrade.mq5,放到 MQL5\Experts\ 底下,在 MetaEditor 打開按 F7 就能用。