徹底解決 Invalid stops(10016/130)— MT5/MT4的SL設定錯誤
目錄
- 何謂 Invalid stops(10016 與 130 的差異)
- ① TRADE_RETCODE_INVALID_STOPS = 10016(MT5 / `OrderSend()` 的返回代碼)
- ② ERR_INVALID_STOPS = 130(MT4 / `GetLastError()`)
- 先花30秒排查
- 成因與對策(6種模式)
- ① SL/TP太接近當前價格(未達止損位)
- ② SL/TP方向相反(BUY/SELL搞混)
- ③ 在凍結位範圍內修改訂單或持倉
- ④ 價格與點數(距離)混淆
- ⑤ 市價執行券商無法在進場時同時設定SL/TP
- ⑥ 商品特性差異(黃金、指數的止損位較大)
- 各券商的差異(注意事項)
- 用MQL5程式碼防範此錯誤(EA開發者適用)
- 下單前驗證並限幅SL/TP
- 個別處理 retcode 10016
- 移動止損修改前先確認凍結位
- 優先順序檢查清單
- 總結
- 常見問題
- Q: SL/TP的計算應該是對的,卻仍出現 Invalid stops,為什麼?
- Q: 10016 與 130 有什麼不同?
- Q: 平常不會出現,卻只在指標公布時出現 Invalid stops。
- Q: 不帶SL/TP能成交,帶SL/TP就被拒絕。
- Q: 在EURUSD上正常運作的EA,換到黃金就頻繁出現 Invalid stops。
徹底解決 Invalid stops(10016/130)
在運行EA時,若Expert頁籤出現 Invalid stops 或 OrderSend error 130,很容易讓人困惑:「難道SL的數值有問題?但計算應該是對的……」。然而,這個錯誤的大部分原因並非SL/TP計算錯誤,而是違反了「券商制定的最小距離規則」。即使數值本身正確,只要太接近當前價格、方向相反、或落在禁止修改的區域內,都會被拒絕。
本文同時面向使用MT5/MT4運行EA的用戶,以及用MQL5開發EA的開發者,將 Invalid stops 的真面目、6大成因、30秒排查法,到程式碼層面的永久防範整理成一篇完整指南。錯誤代碼總覽請參閱 MQL5 / MT5 錯誤代碼處理總指南。
本文以2026年7月當時的MT5(build 4xxx系列)為前提撰寫。止損位的具體數值因券商與商品而異。
何謂 Invalid stops(10016 與 130 的差異)
代表「不合法止損」的數值,在MT5與MT4(世代)中共有2種。只要區分清楚是在哪個平台、哪個階段出現,就能大幅加快原因排查的速度。
① TRADE_RETCODE_INVALID_STOPS = 10016(MT5 / OrderSend() 的返回代碼)
MQL5的 OrderSend() 執行結果會存入 MqlTradeResult.retcode。若請求中的SL/TP(或掛單價格之間的關係)不符合伺服器規則,便會以 10016(TRADE_RETCODE_INVALID_STOPS) 被拒絕。這是交易伺服器端發出的拒絕通知。
意義: 請求中的止損(SL/TP)不合法(Invalid stops in the request)
常數: TRADE_RETCODE_INVALID_STOPS
數值: 10016
// 日誌中可見的典型輸出範例
2026.07.07 09:15:32.441 EA_NAME XAUUSD,M5: OrderSend error: retcode=10016 (invalid stops)
② ERR_INVALID_STOPS = 130(MT4 / GetLastError())
在MT4(MQL4)世代中,當 OrderSend() / OrderModify() 失敗後,GetLastError() 會返回 130(ERR_INVALID_STOPS)。Expert頁籤中的 OrderSend error 130 指的就是這個。若同時使用MT4版EA,就會遇到此錯誤。
意義: 不合法的止損(invalid stops)
常數: ERR_INVALID_STOPS
數值: 130
實務上的區分:
| 平台 | 取得來源 | 數值 | 出現場合 |
|---|---|---|---|
| MT5 | MqlTradeResult.retcode | 10016(TRADE_RETCODE_INVALID_STOPS) | OrderSend / PositionModify 被伺服器拒絕 |
| MT5 | CTrade.ResultRetcode() | 10016 | 透過CTrade下單、修改被拒絕 |
| MT4 | GetLastError() | 130(ERR_INVALID_STOPS) | OrderSend / OrderModify 失敗後 |
雖然代碼不同,但意義與成因幾乎相同,對策也是共通的。順帶一提,MT5中常被混淆的 10015(TRADE_RETCODE_INVALID_PRICE)指的是「訂單價格本身不合法」,是另一回事。10016純粹是「SL/TP(止損)位置」的問題。
先花30秒排查
打開MT5的「報價 → 右鍵點擊商品 → 規格」,查看以下兩項:
止損位 (Stops level) : SL/TP 應與當前價格保持的最小距離(點數)
凍結位 (Freeze level) : 即將觸發的訂單,禁止修改/取消的距離(點數)
接著,在錯誤發生當下的日誌中,比對打算送出的SL/TP數值與當時的Bid/Ask。
- SL或TP與當前價格的距離小於止損位 → 這幾乎就是原因所在(原因①)。
- BUY時SL卻高於Bid/TP低於Bid(SELL則相反) → 方向搞反了(原因②)。
- 只有修改持倉時失敗 → 可能是凍結位限制,或SL/TP需後補的規格(原因③・⑤)。
- SL的數值是「50」這類明顯不像價格的數字 → 價格與點數混淆(原因④)。
若要從程式碼中確認,一行即可:
Print("StopsLevel=", SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL),
" FreezeLevel=", SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL));
成因與對策(6種模式)
① SL/TP太接近當前價格(未達止損位)
症狀: 常見於將SL設得很緊的剝頭皮型EA,或移動止損幅度很小的EA。手動操作時,若試圖將SL設在「價格附近」,下單按鈕也可能無法按下/遭拒絕。
原因: 券商會針對每個商品設定 SYMBOL_TRADE_STOPS_LEVEL(以點數為單位的最小止損距離),距當前價格未達此距離的SL/TP、掛單價格,一律會被伺服器拒絕。驗證的基準價格,BUY倉位的SL/TP以Bid為準,SELL則以Ask為準。點差擴大時Bid與Ask的間隔會拉開,因此平時能通過的距離,在指標公布時或清晨時段可能突然被拒絕。
對策:
- 在規格視窗確認止損位,將EA的SL/TP、移動止損幅度設定得更寬
- 在EA端於下單前先做限幅處理(見後述程式碼)
- 若確實需要極緊的SL,可考慮止損位較小的券商或帳戶類型
② SL/TP方向相反(BUY/SELL搞混)
症狀: 特定方向(僅買或僅賣)必定出現 Invalid stops。這是自製EA首次測試時最常見的模式。
原因: 規則很單純,BUY的SL須低於當前價格(Bid),TP須高於;SELL的SL須高於當前價格(Ask),TP須低於。將BUY用的計算式複製給SELL卻忘了改正負號、把 price - sl 與 price + sl 搞混,都是典型錯誤,伺服器會立即返回10016/130。
對策:
- 出錯時用
PrintFormat("type=%s sl=%.5f tp=%.5f bid=%.5f ask=%.5f", ...)印出實際數值,目視確認方向 - 將SL/TP計算改為BUY/SELL共用函式,把正負號的分支統一收攏在一處(消除複製貼上分支)
③ 在凍結位範圍內修改訂單或持倉
症狀: 新開倉沒問題,但僅在觸及TP前、SL前、掛單即將觸發前的修改/取消操作遭拒絕。
原因: 對設有 SYMBOL_TRADE_FREEZE_LEVEL 的商品,當觸發價格(TP/SL/掛單的觸發價)與當前價格接近到一定距離時,該訂單的修改與取消會被凍結。這是為防止成交處理與修改請求發生衝突的伺服器規格,常表現為EA的移動止損「想在接近TP時再更新一段,卻被拒絕」。
對策:
- 修改前先讀取
SYMBOL_TRADE_FREEZE_LEVEL,若距觸發價的距離小於等於凍結位,該次修改直接跳過 - 拉長移動止損的更新間隔與更新幅度,減少接近觸發前的無謂修改請求
- 被拒絕並非致命問題(接近觸發=即將成交),因此可設計成吞下錯誤、僅留日誌記錄即可
④ 價格與點數(距離)混淆
症狀: SL被直接填入「50」或「0.0050」這類「本意是距離」的數值。日誌中出現的 sl=50.00000 明顯不是價格。
原因: MqlTradeRequest.sl / .tp 要填入的是絕對價格(並非「距進場價下方50點」)。以距離管理的EA,必須先換算成 entry ± distance * _Point 後再傳入。反過來,若延續MT4時代某些函式的用法,在應填絕對價格的地方傳入距離,價格便無法成立,導致10016/130。
此外,pips與點數(point)的混淆也是常見問題。5位報價的券商(例如USDJPY顯示3位小數、EURUSD顯示5位小數)中,1 pip = 10點。「SL 50」究竟是以pips還是point為單位,會使距離相差10倍,可能因此落在止損位以下而遭拒絕。
對策:
- SL/TP務必以
NormalizeDouble(price ± dist * _Point, _Digits)的形式組合 - 在輸入參數的單位(pips / points)上以註解明確標示,並將
_Point換算邏輯統一收攏在一處
⑤ 市價執行券商無法在進場時同時設定SL/TP
症狀: 不帶SL/TP時能成交,但帶SL/TP的新單就出現Invalid stops。特別常見於ECN/市價執行(Market Execution)類型的帳戶。
原因: 市價執行下,「請求時的價格」與「實際成交價格」會有落差,因此部分伺服器不接受新單請求內附帶SL/TP,要求成交後再透過修改持倉來設定(這是MT4時代ECN帳戶頻繁出現error 130的經典規格,MT5中仍有部分伺服器保留相同行為)。
對策:
- 改為先以不帶SL/TP的方式下單 → 確認成交後再用
PositionModify()(若用CTrade則為trade.PositionModify())補上SL/TP的兩段式流程 - 此方式會出現「下單成功但SL設定失敗」的瞬間,因此務必加入SL設定失敗時重試,達到規定次數仍失敗則立即平倉的保護機制(放任無SL部位是最壞的結果)
- 可用
SymbolInfoInteger(_Symbol, SYMBOL_TRADE_EXEMODE)確認執行方式
⑥ 商品特性差異(黃金、指數的止損位較大)
症狀: 同一個EA在EURUSD上運作正常,一換到XAUUSD(黃金)或股指CFD就頻繁出現Invalid stops。
原因: 止損位是依商品個別設定的,黃金、指數、非主流貨幣對通常設定得比主要貨幣對更大。若直接沿用為主要外匯調校的緊縮SL/移動止損幅度,便會達不到該商品的最小距離而遭拒絕。位數也因商品而異(例如黃金常為2〜3位小數),若程式碼中硬編碼 _Digits,同樣會出錯。
對策:
- 更換商品時務必在規格視窗確認止損位與位數
- 將SL/TP幅度改為以ATR等波動性基準設定,而非固定點數,可提升跨商品的穩健性
- 程式碼中應始終動態取得
_Point/_Digits/SYMBOL_TRADE_STOPS_LEVEL(禁止硬編碼)
各券商的差異(注意事項)
止損位、凍結位因券商與商品的組合而完全不同。即使是同一個EA、同一組設定,也可能出現「券商A從未出現的錯誤,在券商B每天都發生」的情況。
更需留意的是,止損位顯示為「0」的券商。0通常並非代表「無限制」,而是「動態判定」,平時無論SL多近都能通過,但只有在指標公布或清晨等點差擴大的瞬間才會遭拒絕。「偶爾才出現的Invalid stops」,真面目多半就是這個。
務必在實際帳戶中進行確認。
long stops = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); // 點數
long freeze = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL); // 點數
具體數值依各券商的帳戶類型與商品規格而異,本文不列出。在自己的帳戶上執行上述程式碼所得的數值,才是唯一正解。
用MQL5程式碼防範此錯誤(EA開發者適用)
正確的設計思路不是「出錯後再修」,而是在下單前就將SL/TP限幅至券商規定的最小距離內,從根本上避免觸發10016。
下單前驗證並限幅SL/TP
// 將 SL/TP 限幅至止損位以上的距離後再下單
// 回傳值 false = 方向相反(設計錯誤),此時不下單
bool ClampStops(ENUM_ORDER_TYPE type, double &sl, double &tp)
{
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
long stopsPt = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double spread = SymbolInfoDouble(_Symbol, SYMBOL_ASK)
- SymbolInfoDouble(_Symbol, SYMBOL_BID);
// 止損位 + 點差的緩衝空間(因應 stops=0 動態判定的券商)
double minDist = stopsPt * point + spread;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(type == ORDER_TYPE_BUY)
{
// BUY 的 SL/TP 以 Bid 為基準驗證
if(sl > 0 && sl >= bid) return false; // 方向相反
if(tp > 0 && tp <= bid) return false;
if(sl > 0 && (bid - sl) < minDist) sl = bid - minDist;
if(tp > 0 && (tp - bid) < minDist) tp = bid + minDist;
}
else if(type == ORDER_TYPE_SELL)
{
// SELL 的 SL/TP 以 Ask 為基準驗證
if(sl > 0 && sl <= ask) return false; // 方向相反
if(tp > 0 && tp >= ask) return false;
if(sl > 0 && (sl - ask) < minDist) sl = ask + minDist;
if(tp > 0 && (ask - tp) < minDist) tp = ask - minDist;
}
sl = NormalizeDouble(sl, digits);
tp = NormalizeDouble(tp, digits);
return true;
}
重點有三:
- 每次都動態取得
SYMBOL_TRADE_STOPS_LEVEL與SYMBOL_POINT(不受商品、券商限制) - 加上點差的緩衝空間(即使是止損位為0的動態判定券商,也更容易通過)
- 最後務必以
NormalizeDouble(價格, _Digits)對齊位數(多餘的小數位也可能是遭拒的原因)
個別處理 retcode 10016
被拒絕時,若能記錄「送出了什麼、當時距離是多少」的日誌,就能一眼看出是原因①〜⑥中的哪一個。
MqlTradeRequest req; MqlTradeResult res;
ZeroMemory(req); ZeroMemory(res);
// ... 組裝 req(sl/tp 已經過 ClampStops 處理) ...
if(!OrderSend(req, res) || res.retcode != TRADE_RETCODE_DONE)
{
if(res.retcode == TRADE_RETCODE_INVALID_STOPS) // 10016
PrintFormat("Invalid stops: sl=%s tp=%s bid=%s stopsLevel=%d",
DoubleToString(req.sl, _Digits),
DoubleToString(req.tp, _Digits),
DoubleToString(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits),
(int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL));
else
PrintFormat("OrderSend failed: retcode=%d, lastError=%d", res.retcode, GetLastError());
}
移動止損修改前先確認凍結位
// 修改持倉前,確認觸發價格是否落入凍結區
bool CanModify(double triggerPrice)
{
long freeze = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
if(freeze <= 0) return true;
double dist = MathAbs(SymbolInfoDouble(_Symbol, SYMBOL_BID) - triggerPrice);
return (dist > freeze * SymbolInfoDouble(_Symbol, SYMBOL_POINT));
}
MT4版EA可用 MarketInfo(Symbol(), MODE_STOPLEVEL) / MODE_FREEZELEVEL 取得相同數值,概念完全一致。
FXEA365提供的EA,標準內建了此下單前SL/TP驗證、止損位限幅、跨商品動態取得的設計,因此即使更換券商或商品,也不會因Invalid stops而卡住。
優先順序檢查清單
| 優先度 | 確認事項 | 對策 |
|---|---|---|
| 🚨 首先 | SL/TP與當前價格的距離是否 < 止損位 | 拉寬SL/TP幅度/實作限幅處理 |
| 🚨 首先 | BUY/SELL的SL/TP方向是否相反 | 印出日誌中的實際數值,目視確認 |
| ⚠️ 其次 | 僅修改失敗 → 是否落在凍結位範圍內 | 跳過觸發前的修改操作 |
| ⚠️ 其次 | sl/tp是否誤填了距離(點數) | 換算為絕對價格 price ± dist*_Point |
| ✅ 確認 | 是否為需要市價執行後補SL/TP的帳戶 | 改為下單→PositionModify的兩段式流程 |
| 🛠 開發 | 是否動態取得商品規格 | 實作上述的 ClampStops |
總結
Invalid stops在 MT5為10016(TRADE_RETCODE_INVALID_STOPS),MT4為130(ERR_INVALID_STOPS)。代碼不同但意義與對策相通。- 成因大多不是計算錯誤,而是未達止損位、方向相反、凍結位限制、價格與點數混淆、SL/TP後補規格、商品特性差異這6種。
- 開發者可透過「下單前讀取
SYMBOL_TRADE_STOPS_LEVEL進行限幅+NormalizeDouble」做到永久性防範。即使是止損位為0的券商,也能靠加上點差緩衝空間來吸收。
錯誤代碼總覽請參閱 MQL5 / MT5 錯誤代碼處理總指南。同樣屬於常見下單被拒錯誤的「資金不足」,可參閱 ERR_NO_MONEY(134/10019)解說文章。
常見問題
Q: SL/TP的計算應該是對的,卻仍出現 Invalid stops,為什麼?
問題很可能不在於數值是否正確,而是與當前價格的距離遭拒。未達券商最小止損距離(止損位)的SL/TP,即使數值精確,也會一律被拒絕。請在規格視窗或透過 SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) 確認最小距離。
Q: 10016 與 130 有什麼不同?
10016(TRADE_RETCODE_INVALID_STOPS)是MT5的 OrderSend() / PositionModify() 返回的結果代碼,130(ERR_INVALID_STOPS)則是MT4的 GetLastError() 所返回的數值。兩者僅平台不同,意義(SL/TP位置不合法)與對策是相同的。
Q: 平常不會出現,卻只在指標公布時出現 Invalid stops。
原因是點差擴大。由於BUY的SL/TP以Bid為基準、SELL以Ask為基準驗證,點差擴大的瞬間就可能造成距離不足。即使是止損位為0的券商,遇到急劇變動時也可能出現動態拒絕的行為。請在SL/TP距離上加入點差緩衝空間(參見本文程式碼)。
Q: 不帶SL/TP能成交,帶SL/TP就被拒絕。
這可能是市價執行(Market Execution)類帳戶,伺服器不接受新單請求內附帶SL/TP。請改為先不帶SL/TP下單,成交後再用 PositionModify() 設定的兩段式流程。但務必加入SL設定失敗時的重試機制,以及持續失敗時立即平倉的保護措施。
Q: 在EURUSD上正常運作的EA,換到黃金就頻繁出現 Invalid stops。
黃金、股指的止損位通常比主要貨幣對設定得更大,位數也不同。請依商品規格拉寬SL/移動止損幅度,或改為採用ATR基準的波動連動幅度。
相關文章
📧 漲價預告 + 免費5天郵件課程
所有EA目前均為首發價,並將隨銷量階梯式上調。在每次漲價前收到通知,另有每日一封郵件:自動交易本質、正確解讀回測、券商選擇技巧。
※ 嚴格保護隱私。可隨時取消訂閱。