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을 개별적으로 핸들링한다
- 트레일 수정은 프리즈 레벨을 확인한 후에 한다
- 우선순위 체크리스트
- 정리
- FAQ
- 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의 "호가 → 종목 우클릭 → 사양"을 열어 다음 2가지 항목을 확인하세요.
스톱 레벨 (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 측에서 주문 전에 클램프(clamp)한다(아래 코드 참고)
- 아무래도 타이트한 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와 포인트의 혼동도 대표적인 사례입니다. 5자리 브로커(예: USDJPY가 3자리·EURUSD가 5자리 표시)에서는 1 pip = 10포인트입니다. "SL 50"이 pips 기준인지 포인트 기준인지에 따라 거리가 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를 붙이는 2단계 방식으로 변경한다 - 이 방식에서는 "주문은 성공했지만 SL 설정에 실패한" 순간이 생기므로, SL 설정이 통과할 때까지 재시도하고, 규정 횟수 실패하면 즉시 청산하는 보호 장치를 반드시 넣는다(SL 없이 방치하는 것이 최악의 결과)
- 체결 방식은
SymbolInfoInteger(_Symbol, SYMBOL_TRADE_EXEMODE)로 확인할 수 있다
⑥ 종목 고유의 특성(금·지수는 스톱 레벨이 큼)
증상: 같은 EA가 EURUSD에서는 작동하는데, XAUUSD(금)나 주가지수 CFD에 올리는 순간 Invalid stops가 연발함.
원인: 스톱 레벨은 종목별로 별도 설정되며, 금·지수·이색 통화는 메이저 통화보다 크게 설정되어 있는 것이 일반적입니다. 메이저 FX용으로 조정한 타이트한 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;
}
포인트는 3가지입니다.
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의 2단계 방식 |
| 🛠 개발 | 종목 사양을 동적으로 취득하고 있는가 | 위의 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) 해설 글에 정리되어 있습니다.
FAQ
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()로 설정하는 2단계 방식으로 변경하세요. 다만 SL 설정에 실패했을 경우의 재시도와, 계속 실패했을 경우의 즉시 청산 보호 장치를 반드시 넣으세요.
Q: EURUSD에서는 작동하는 EA가, 금에서는 Invalid stops가 연발합니다.
금·주가지수는 스톱 레벨이 메이저 통화보다 크게 설정되어 있는 것이 일반적이며, 자릿수도 다릅니다. SL/트레일 폭을 종목 사양에 맞춰 넓히거나, ATR 기준의 변동성 연동 폭으로 변경하세요.
관련 기사
📧 가격 인상 사전 알림 + 무료 5일 이메일 강좌
모든 EA는 현재 출시 가격이며 판매 수량에 따라 단계적으로 인상됩니다. 인상 전 사전 알림과 함께 자동매매의 본질, 백테스트 해석법, 브로커 선택 요령을 매일 한 통씩 보내드립니다.
※ 개인정보는 엄격히 보호. 언제든 구독 해지 가능.