回測報告很漂亮,EA 掛到圖表上卻一張單都沒下——多半不是策略的問題。可能是演算法交易沒開,可能是商品加不進市場報價,也可能單真的送出去了、被伺服器退回來,而 EA 沒有去看退回來的原因。這篇把掛上去之前該確認的事寫成程式碼:能不能交易、商品規格、EA 重新啟動之後怎麼接回手上的倉、下單失敗怎麼分類。
三個範例檔。PreflightCheck.mq5 是腳本,在真的終端上跑一次,把權限、帳戶、要交易的商品規格全部印出來,只讀不下單。MaCrossLive.mq5 是在均線交叉 EA MaCrossTrail.mq5(EMA 10/30 交叉進場、每筆賠餘額 1%、ATR 倍數停損停利、保本與移動停損)上面補自檢、接手持倉、下單失敗分類。RetcodeDemo.mq5 故意送三張會被拒的單,用來看伺服器回什麼。三個都在我的 MT5 上編譯過,腳本在連著伺服器的終端跑、兩隻 EA 在策略測試器跑,文中的日誌是實際輸出。
先把範例跑起來
- PreflightCheck.mq5 放到
MQL5\Scripts\,在 MetaEditor 按F7;回終端從「導航」把它拖到任何一張圖表上,輸出在「工具箱」的「專家」頁。腳本不跳參數視窗,要換商品清單就改i_Symbols的預設值重編。 - MaCrossLive.mq5 放到
MQL5\Experts\,按F7;策略測試器Ctrl+R:EA 選 MaCrossLive,EURUSD,H1,2025.01.02 到 2025.01.31,模式「每次報價」,入金 10000,杠杆 1:100,按「開始」。 - 看「日誌」頁,開頭會多兩行「自檢通過」和「沒有要接手的持倉」,那就是這篇加的東西。
- 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 是這個商品收哪種成交方式,CTrade 的 SetTypeFillingBySymbol() 就是照它填的。
同一個經紀商的六個商品,數字差成這樣:
| 商品 | 最小 / 步進 / 最大手數 | 合約大小 | tick 大小 | 每 tick 值 (USD) |
|---|---|---|---|---|
| EURUSD | 0.01 / 0.01 / 100.00 | 100000 | 0.00001 | 1.00 |
| GBPUSD | 0.01 / 0.01 / 100.00 | 100000 | 0.00001 | 1.00 |
| USDJPY | 0.01 / 0.01 / 100.00 | 100000 | 0.001 | 0.64 |
| XAUUSD | 0.01 / 0.01 / 100.00 | 100 | 0.01 | 1.00 |
| US500 | 0.01 / 0.01 / 250.00 | 1 | 0.01 | 0.01 |
| BRENT | 1.00 / 1.00 / 10000.00 | 1 | 0.01 | 0.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 money100 手需要 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.mq5 和 RetcodeDemo.mq5 是 EA,放 MQL5\Experts\。在 MetaEditor 打開按 F7 就能用。