Invalid stops(10016/130) 완전 해결 — MT5/MT4에서 SL
목차
Invalid stops(10016/130) 완전 해결
EA를 운용하다가 Expert 탭에 Invalid stops나 OrderSend error 130이 뜨면 "SL 값이 이상한가? 계산은 맞을 텐데…"라며 혼란스러워지기 쉽지만, 이 오류의 대부분은 SL/TP 계산 실수가 아니라 "브로커가 정한 최소 거리 규칙" 위반입니다. 값 자체가 정확해도 현재가에 너무 가깝거나, 방향이 반대이거나, 수정 금지 구간에 들어가 있으면 거부됩니다.
영상으로 보기: "Invalid S/L or T/P" 거부부터 수정·체결까지 60초 (실제 MT5 화면, 영어 자막) — YouTube
이 글은 MT5/MT4에서 EA를 사용하는 사람과 MQL5로 EA를 작성하는 사람 모두를 위해, Invalid stops의 정체·6가지 원인·30초 진단법·코드 단에서의 근본적인 방지책까지 하나로 정리한 결정판입니다. 오류 코드 전반의 목록은 MQL5 / MT5 오류 코드 대처법 종합 가이드를 참고하세요.
이 글은 2026년 7월 기준 MT5(build 4xxx 계열)를 전제로 합니다. 스톱 레벨의 구체적인 값은 브로커·종목마다 다릅니다.
우선 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));
추천 브로커 비교 →
🎁 무료 EA 다운로드📧 무료 이메일 강좌(총 8통) + 구독자 전용 MT5 지표 13종 팩
※ 개인정보는 엄격히 보호. 언제든 구독 해지 가능.
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(스톱)의 위치" 문제입니다.
원인과 대처(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 기준의 변동성 연동 폭으로 변경하세요.
관련 기사
📧 무료 이메일 강좌(총 8통) + 구독자 전용 MT5 지표 13종 팩
※ 개인정보는 엄격히 보호. 언제든 구독 해지 가능.
EA나 FX에 대해 궁금한 점이 있나요?
Nattzy 트레이딩 포럼에 질문을 올릴 수 있습니다(무료 가입). 이 글의 내용이 아니어도 EA 설정이나 브로커 선택 등 어떤 질문이든 환영합니다.
Nattzy에서 질문하기 ↗