市價單是用現在的價格馬上成交;掛單是先把價格、停損停利交給券商,價格到了才成交。這篇把通道突破改成用掛單進場:每根 K 線在通道上緣(前 20 根的最高點)掛一張 Buy Stop、下緣(最低點)掛一張 Sell Stop,一張成交就刪掉另一張。
範例 EA:ChannelStop.mq5,送單、找單、改單、刪單都在這一個檔;參數、手數(每筆賠餘額 1% 倒推)這些在檔案開頭,文中不貼。文中的日誌都是策略測試器的實際輸出(EURUSD H1,2025 年 1 月,每次報價)。
1. 四種掛單,各看哪個價
掛在現價上方的有兩種:Buy Stop 漲上來就買,追突破;Sell Limit 漲上來才賣,等反彈。現價下方也兩種:Sell Stop 跌下去就賣,追跌破;Buy Limit 跌下來才買,等回檔。另有 Buy Stop Limit、Sell Stop Limit(到價後改掛一張限價單),本文不用。
Stop 這類,MT5 繁中介面叫「止損單」;Limit 這類叫「限價單」。為了不跟保護持倉的停損搞混,本文把 Stop 這類叫停止單。

跟市價單一樣,買單看 Ask、賣單看 Bid。MT5 圖表的 K 線是用 Bid 畫的,所以 K 線最高點還沒到掛單價,Buy Stop 就可能已經成交:
15:19:40 order [#193 buy stop 0.57 EURUSD at 1.03119] triggered
15:19:40 買入停止單成交 @1.03119 當時 Bid 1.03118那根 H1 K 線(1 月 10 日 15:00)最高只到 1.03118,差 1 點沒碰到掛單價;Ask 比 Bid 高一個點差,先到了。換日時點差會放大,差得更多:1 月 14 日 00:02 那張 Buy Stop 掛 1.02469,成交那一刻 Bid 還在掛單價下方 80 點,只是點差撐開就買進了。
價格一口氣跳過掛單價時,停止單用當時的價格成交,會比掛單價差;限價單則成交在掛單價或更好。另寫一隻測試 EA,在現價下方 30 點各掛一張 Buy Limit、Sell Stop;1 月 10 日 15:30:16 的一個 tick 同時越過兩張的價位:Buy Limit 掛 1.03009,成交在 1.03007,好 2 點;Sell Stop 掛 1.03008,成交在 1.03006,差 2 點。每次報價模式的 tick 是從 1 分鐘 K 線模擬的,跳幾點不代表實盤。
掛單存在券商的伺服器上:EA 停了、電腦關了,價格到了照樣成交。
2. 送出掛單:價格、停損停利、到期時間
//=== 2. 送出掛單:價格、停損停利、到期時間 ==========================
bool Done() // 伺服器收了這個請求
{
uint code = g_Trade.ResultRetcode();
return code == TRADE_RETCODE_DONE || code == TRADE_RETCODE_PLACED;
}
ENUM_ORDER_TYPE_TIME Life(datetime &expiry)
{
expiry = 0;
long can = SymbolInfoInteger(_Symbol, SYMBOL_EXPIRATION_MODE);
if(i_ExpireBars <= 0 || (can & SYMBOL_EXPIRATION_SPECIFIED) == 0)
return ORDER_TIME_GTC; // 一直有效,直到成交或刪掉
expiry = iTime(_Symbol, _Period, 0) // 這根的開盤時間
+ i_ExpireBars * PeriodSeconds(); // 再往後 N 根
return ORDER_TIME_SPECIFIED; // 到 expiry 就失效
}
bool SendStop(ENUM_ORDER_TYPE type, double price, double sl,
double tp, double lots)
{
datetime expiry;
ENUM_ORDER_TYPE_TIME life = Life(expiry);
bool ok = (type == ORDER_TYPE_BUY_STOP)
? g_Trade.BuyStop(lots, price, _Symbol, sl, tp, life, expiry)
: g_Trade.SellStop(lots, price, _Symbol, sl, tp, life, expiry);
if(!ok || !Done())
{
Print("掛單失敗 ", g_Trade.ResultRetcode(), " ",
g_Trade.ResultRetcodeDescription());
return false;
}
return true;
}CTrade 的 BuyStop、SellStop 依序是手數、掛單價、商品、停損、停利、期限種類、到期時間。停損停利跟著掛單走,成交後就是這筆持倉的停損停利。回 true 只代表請求送出去了,Done() 再看回傳碼:10009 是完成,10008 是單子已放上,都算收了。
期限種類常用三種:ORDER_TIME_GTC 一直有效,直到成交或刪掉;ORDER_TIME_DAY 到當天結束;ORDER_TIME_SPECIFIED 到指定的到期時間。
券商收哪幾種,看 SYMBOL_EXPIRATION_MODE。它是幾個旗標加起來的數,都是 SYMBOL_EXPIRATION_ 開頭:GTC 1、DAY 2、SPECIFIED 4、SPECIFIED_DAY 8(到指定那天結束);實測 Tickmill 的 EURUSD 是 15=1+2+4+8,四種都收。can & SYMBOL_EXPIRATION_SPECIFIED 的 & 是位元運算,取出 4 那一位,結果 0 就是不收,跟參數 datetime &expiry 的 &(傳參考)是兩回事。查的時候用 SYMBOL_EXPIRATION_*,送單才用 ORDER_TIME_*,兩組數值不同,不能混用。
範例的 i_ExpireBars 預設 2:到期時間設在兩根 K 線後。EA 正常時每根 K 線都會改單或刪掉重掛,到期時間跟著往後推;EA 當掉或斷線,單子最多再掛兩根 K 線就自己消失(前提是券商收指定到期時間;不收的話 Life() 退回 GTC,就沒有這層保護)。EA 不在的這段時間,沒人會在一張成交後刪另一張,兩張都可能成交,所以別掛太久。
掛錯邊(Buy Stop 掛在 Ask 下方)會被拒,回 10015;到期時間在過去或離現在太近,回 10022。下面兩行是另一隻測試 EA 故意送錯的結果:
failed buy stop 0.1 EURUSD at 1.03595 [Invalid price]
failed buy stop 0.1 EURUSD at 1.06600 [Invalid expiration]在測試器裡實測,到期時間設在 90 秒後被拒,120 秒後才收。
3. 找自己的掛單
//=== 3. 找自己的掛單 ================================================
ulong FindMyOrder(ENUM_ORDER_TYPE type)
{
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong t = OrderGetTicket(i); // 順便選取這張,下面才讀得到
if(t == 0)
continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol ||
OrderGetInteger(ORDER_MAGIC) != i_Magic)
continue;
if(OrderGetInteger(ORDER_TYPE) == type)
return t;
}
return 0;
}
bool DeleteMyOrder(ENUM_ORDER_TYPE type, string why)
{
ulong t = FindMyOrder(type);
if(t == 0)
return true; // 本來就沒有
if(g_Trade.OrderDelete(t) && Done())
{
Print("刪掉 #", t, ":", why);
return true;
}
Print("刪單失敗 ", g_Trade.ResultRetcode(), " ",
g_Trade.ResultRetcodeDescription());
return false;
}掛單還沒成交就不是持倉,PositionsTotal 找不到,要用 OrdersTotal 和 OrderGetTicket。OrderGetTicket 回傳訂單編號(ticket),同時選取這張單,之後 OrderGetString、OrderGetInteger、OrderGetDouble 讀的都是它。跟找持倉一樣,用商品和魔術碼認出自己的單,再看類型。
刪單是 OrderDelete(ticket),測試器日誌會印 order canceled。DeleteMyOrder 回傳有沒有刪成功(沒有要刪的也算成功),下一節會用到。
4. 每根新 K 線:改價,或刪掉重掛
//=== 4. 每根新 K 線:改價,或刪掉重掛 ===============================
void PlaceOrMove(ENUM_ORDER_TYPE type, double price, double atr)
{
bool isBuy = (type == ORDER_TYPE_BUY_STOP);
double now = SymbolInfoDouble(_Symbol,
isBuy ? SYMBOL_ASK : SYMBOL_BID);
long stops = SymbolInfoInteger(_Symbol,
SYMBOL_TRADE_STOPS_LEVEL);
// 停止單要掛在現價外面,而且至少隔 StopsLevel
if((isBuy ? price - now : now - price) <= stops * _Point)
{
DeleteMyOrder(type, "現價已經過了通道線");
return;
}
double dir = isBuy ? 1 : -1;
double sl = NormalizeDouble(price - dir * atr * i_SlAtr, _Digits);
double tp = NormalizeDouble(price + dir * atr * i_TpAtr, _Digits);
double lots = CalcLots(MathAbs(price - sl));
ulong t = FindMyOrder(type);
if(t > 0 && MathAbs(OrderGetDouble(ORDER_VOLUME_CURRENT) - lots)
< 1e-8)
{
// 手數沒變:改價、停損停利、到期時間
datetime expiry;
ENUM_ORDER_TYPE_TIME life = Life(expiry);
if(!g_Trade.OrderModify(t, price, sl, tp, life, expiry) ||
!Done())
Print("改單失敗 ", g_Trade.ResultRetcode(), " ",
g_Trade.ResultRetcodeDescription());
return;
}
if(!DeleteMyOrder(type, "手數要換")) // OrderModify 改不了手數
return; // 舊的刪不掉,就別再掛一張
if(lots > 0)
SendStop(type, price, sl, tp, lots);
}
void OnTick()
{
datetime bar = iTime(_Symbol, _Period, 0);
if(bar == g_LastBar)
return;
g_LastBar = bar;
if(HavePosition()) // 有倉就不掛,交給停損停利
{
DeleteMyOrder(ORDER_TYPE_BUY_STOP, "已經有持倉"); // 平常早就
DeleteMyOrder(ORDER_TYPE_SELL_STOP, "已經有持倉"); // 刪掉了
return;
}
g_High.Refresh();
g_Low.Refresh();
g_Atr.Refresh();
int at; // MaxValue 順便回傳位置,不用
double top = g_High.MaxValue(1, i_Channel, at); // 前 N 根的
double bot = g_Low.MinValue(1, i_Channel, at); // 最高、最低
double atr = g_Atr.Main(1);
if(top == EMPTY_VALUE || bot == EMPTY_VALUE || atr == EMPTY_VALUE)
{
g_LastBar = 0; // 資料還沒好,下個 tick 重來
return;
}
PlaceOrMove(ORDER_TYPE_BUY_STOP, top, atr);
PlaceOrMove(ORDER_TYPE_SELL_STOP, bot, atr);
}通道線每根 K 線都可能移動;ATR 一變,停損距離就變,照每筆風險 1% 算出來的手數也跟著變。
OrderModify 能改掛單價、停損停利、到期時間,改不了手數:CTrade 的 OrderModify 沒有手數參數。另寫測試 EA,把一張 0.1 手的掛單用 OrderSend 直接送改單請求(TRADE_ACTION_MODIFY)、手數寫 0.20:測試器裡回 10009 成功,價格改了,手數還是 0.1,不報錯。
所以手數沒變就改單,變了就刪掉重掛;舊的刪不掉就先不掛,免得多出一張。這個月改單 124 次、刪掉重掛 318 次。
停止單要掛在現價外面,還要隔商品的 StopsLevel 點(券商規定掛單價離現價至少幾點,Tickmill 是 0)。新 K 線一開盤就已經在通道外,這一邊就不掛,舊單也刪掉,不去追已經跑掉的價。
有持倉就不掛新單,交給停損停利出場。
5. 一張成交,就刪掉另一張
//=== 5. 一張成交,就刪掉另一張 ======================================
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
if(trans.type != TRADE_TRANSACTION_DEAL_ADD)
return; // 只看「多了一筆成交」
ulong deal = trans.deal;
if(!HistoryDealSelect(deal) ||
HistoryDealGetInteger(deal, DEAL_MAGIC) != i_Magic ||
HistoryDealGetString(deal, DEAL_SYMBOL) != _Symbol ||
HistoryDealGetInteger(deal, DEAL_ENTRY) != DEAL_ENTRY_IN)
return; // 不是這隻 EA 的進場
bool isBuy = (HistoryDealGetInteger(deal, DEAL_TYPE)
== DEAL_TYPE_BUY);
Print(isBuy ? "買入停止單" : "賣出停止單", "成交 @",
DoubleToString(HistoryDealGetDouble(deal, DEAL_PRICE),
_Digits), " 當時 Bid ", DoubleToString(
SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits));
DeleteMyOrder(isBuy ? ORDER_TYPE_SELL_STOP : ORDER_TYPE_BUY_STOP,
"另一邊成交了");
}MT5 的單子類型裡沒有「一張成交就取消另一張」(OCO)這種,要 EA 自己刪,而且要馬上刪:等到下一根 K 線才刪,價格掉頭碰到通道另一邊,另一張就可能也成交。
OnTradeTransaction 是交易有變化時 MT5 叫的函數。收到 TRADE_TRANSACTION_DEAL_ADD(多了一筆成交),用 HistoryDealSelect 選出這筆成交,確認是這隻 EA 的進場,就刪掉另一邊:
00:05:04 order [#3 sell stop 0.32 EURUSD at 1.03440] triggered
00:05:04 賣出停止單成交 @1.03433 當時 Bid 1.03433
00:05:04 order canceled [#2 buy stop 0.32 EURUSD at 1.04245]
00:05:04 刪掉 #2:另一邊成交了6. 在測試器跑一個月
測試設定:EURUSD H1,2025.01.02 到 2025.01.31,每次報價,入金 10000,杠杆 1:100。整個月 34 張停止單成交:21 張正好在掛單價,13 張比掛單價差、最多差 10 點,沒有一張更好。
過期的單,測試器要等下一根 K 線開盤才刪。另一隻測試 EA 送一張 00:32 到期的單:測試週期選 H1,01:00 才刪;選 M15,00:45 就刪。「幾分鐘後失效」這種邏輯,測試器驗不準。
週末也一樣:週五 23:00 改單,到期時間是週六 01:00,週一第一個 tick 才刪:
01.10 23:00:00 order modified [#206 buy stop 0.22 EURUSD at 1.03119]
01.10 23:00:00 order modified [#207 sell stop 0.22 EURUSD at 1.02109]
01.13 00:05:00 order expired [#206 buy stop 0.22 EURUSD at 1.03119]
01.13 00:05:00 order expired [#207 sell stop 0.22 EURUSD at 1.02109]EA 在同一個 tick 重新掛上新的兩張。
餘額 10000 跑到 9088.53:掛單只決定怎麼進場,這個通道突破在這個月是賠的。
本篇檔案:ChannelStop.mq5 放 MQL5\Experts\,在 MetaEditor 打開按 F7。