MT5 EA 상태 복구는 메모리가 아니라 계좌 사실에서 시작합니다
이번 포스트의 핵심 내용 3줄요약
- EA가 다시 시작되면
lastBarTime = 0같은 메모리 값은 사라집니다. 반면 서버의 position, pending order, deal은 남을 수 있으므로 서버 상태를 먼저 읽고 신규 주문은 나중에 허용해야 합니다. 처리 완료한 봉과전송을 시작했지만 결과가 모호한 봉을 구분해 디스크에 남기면, 응답을 받기 직전에 꺼진 EA가 같은 신호를 자동 재전송하는 일을 막을 수 있습니다.- Magic Number는 조회 범위를 나누는 식별자일 뿐 중복 실행 잠금이 아닙니다. 로컬 PC와 VPS 가운데 한 실행 주체만 활성화하고, 상태가 설명되지 않으면 fail-closed로 멈춥니다.
이전 글인 MT5 EA 데모 계좌 포워드 테스트 체크리스트에서는 재시작과 재연결 뒤 기존 포지션을 중복 진입 없이 다시 인식해야 한다고 정리했습니다. 이번 글은 그 요구를 EA 코드의 상태 모델로 옮깁니다.
여기서 목표는 “어떤 상황에서도 자동으로 다시 거래한다”가 아닙니다. 더 안전한 목표는 현재 계좌 상태를 설명할 수 있을 때만 신규 신호 처리를 재개하고, 설명할 수 없으면 사람의 검토 전까지 멈추는 것입니다.
재시작 뒤에는 서로 다른 네 상태를 맞춰야 합니다
EA 파일의 전역 변수와 서버 계좌 상태는 수명이 다릅니다. 터미널이나 EA가 다시 로드되면 프로그램 전역 변수는 초기화되지만, 이미 접수된 주문과 체결, 열린 포지션은 거래 서버에 남을 수 있습니다. 네트워크만 끊겼다가 복구된 경우에는 EA 프로세스가 계속 살아 있어도 오프라인 동안 서버 상태가 변했을 수 있습니다.
| 상태 원천 | 예시 | 재시작 뒤 신뢰 방식 |
|---|---|---|
| 프로세스 메모리 | lastBarTime, requestPending, 로컬 배열 | 그대로 신뢰하지 않고 다시 만듦 |
| 현재 계좌 상태 | 열린 position, pending order, SL/TP | 서버의 현재 사실로 다시 조회 |
| 거래 이력 | order, deal, retcode와 position identifier | 모호한 전송 결과를 재조정할 때 조회 |
| 영속 마커 | 처리한 bar time, in-flight signal, 런타임 소유권 | 범위를 좁힌 키로 저장하고 계좌 사실과 대조 |
복구의 우선순위는 계좌 현재 상태 → 거래 이력 → 영속 마커 → 새 메모리 캐시입니다. 영속 마커도 서버보다 앞선 진실은 아닙니다. 마커에는 “요청을 시작했다”만 남고 실제 주문은 서버에 도착하지 않았을 수 있기 때문입니다.
MQL5의 position 속성 문서 새 탭에 따르면 POSITION_IDENTIFIER는 position의 생애 동안 유지되며 관련 order와 deal의 ORDER_POSITION_ID, DEAL_POSITION_ID에 연결됩니다. Netting 계좌에서 반전으로 ticket이 바뀔 수 있으므로 장기 추적에는 ticket 하나보다 identifier와 이력을 함께 쓰는 편이 낫습니다.
신규 주문보다 먼저 fail-closed 상태 머신을 둡니다
상태 복구가 필요한 EA는 단순한 켜짐/꺼짐 대신 최소 세 상태를 가져야 합니다.
| 런타임 상태 | 의미 | 신규 주문 |
|---|---|---|
RECOVERING | 연결·권한·포지션·주문·마커를 대조하는 중 | 금지 |
READY | 사전 정의한 불변식이 모두 맞고 모호한 요청이 없음 | 신호 경계에서만 허용 |
LOCKED | 중복 실행, 알 수 없는 노출, 미해결 in-flight 요청 등 발견 | 사람의 원인 확인 전까지 금지 |
핵심 흐름은 다음처럼 단순해야 합니다.
OnInit
-> RECOVERING으로 시작
-> Magic Number와 실행 환경을 기록
OnTick
-> 서버 미연결이면 RECOVERING으로 전환하고 즉시 return
-> 연결 복구를 처음 관찰하면 계좌 상태를 재조정하고 return
-> 재조정 성공이면 READY, 불일치면 LOCKED
-> READY가 아니면 어떤 신호도 주문으로 바꾸지 않음
-> READY일 때만 새 봉과 영속 마커를 확인한 뒤 한 번 평가
OnTradeTransaction
-> 짧게 식별자와 결과만 기록
-> needsReconciliation = true로 표시
-> 다음 안전한 이벤트에서 전체 계좌 상태를 다시 조회연결 확인에는 TerminalInfoInteger(TERMINAL_CONNECTED)를 사용할 수 있습니다. 공식 터미널 속성 표 새 탭는 이 값과 TERMINAL_TRADE_ALLOWED를 구분합니다. 계좌 쪽의 ACCOUNT_TRADE_ALLOWED, ACCOUNT_TRADE_EXPERT도 별도이므로 “연결됨”을 “EA 주문 가능”과 같은 뜻으로 처리하면 안 됩니다.
연결이 돌아온 첫 tick에서 곧바로 주문하지 않는 것도 중요합니다. 그 tick은 RECOVERING에서 현재 상태를 읽는 데만 사용하고 return합니다. 다음 tick부터 READY인지 다시 확인하면 복구와 신규 진입의 경계를 로그에서 분명히 볼 수 있습니다.
열린 position과 pending order를 모두 재조정합니다
PositionSelect(_Symbol) 하나만 호출하면 충분하지 않습니다. Hedging 계좌에는 같은 symbol의 position이 여러 개 있을 수 있고, pending order는 position 목록에 포함되지 않습니다. 공식 trade functions 목록 새 탭에 따라 PositionsTotal()과 PositionGetTicket(), OrdersTotal()과 OrderGetTicket()을 각각 순회해야 합니다.
다음 코드는 주문을 보내는 EA가 아니라 현재 노출을 읽기 위한 축약 예시입니다.
struct ExposureSnapshot
{
int ownedPositions;
int ownedPendingOrders;
double ownedVolume;
bool missingStopLoss;
bool foreignPositionOnSymbol;
};
bool ReadExposure(ExposureSnapshot &snapshot)
{
ZeroMemory(snapshot);
for(int i=0; i<PositionsTotal(); i++)
{
ulong ticket=PositionGetTicket(i);
if(ticket==0)
return false;
string symbol=PositionGetString(POSITION_SYMBOL);
long magic=PositionGetInteger(POSITION_MAGIC);
if(symbol!=_Symbol)
continue;
if(magic!=(long)MagicNumber)
{
snapshot.foreignPositionOnSymbol=true;
continue;
}
snapshot.ownedPositions++;
snapshot.ownedVolume+=PositionGetDouble(POSITION_VOLUME);
if(PositionGetDouble(POSITION_SL)<=0.0)
snapshot.missingStopLoss=true;
}
for(int i=0; i<OrdersTotal(); i++)
{
ulong ticket=OrderGetTicket(i);
if(ticket==0)
return false;
if(OrderGetString(ORDER_SYMBOL)==_Symbol &&
OrderGetInteger(ORDER_MAGIC)==(long)MagicNumber)
snapshot.ownedPendingOrders++;
}
return true;
}조회가 끝났다고 복구가 끝난 것은 아닙니다. 전략의 설계 불변식과 비교해야 합니다. 예를 들어 이 시리즈의 학습용 시장가 EA라면 다음과 같이 보수적으로 판정할 수 있습니다.
- 소유 position은 0개 또는 1개여야 한다.
- 학습용 EA는 pending order를 만들지 않으므로 하나라도 있으면
LOCKED다. - 열린 position의 volume, 방향, SL/TP가 허용 범위와 맞아야 한다.
- Netting 계좌에서 같은 symbol에 다른 Magic Number의 position이 있으면 소유권을 자동 추론하지 않는다.
- in-flight 마커가 남아 있으면 현재 목록뿐 아니라 거래 이력까지 확인한다.
Magic Number가 다르다는 이유로 같은 symbol의 position을 항상 무시해도 되는 것은 아닙니다. Netting 계좌는 symbol당 하나의 position만 유지하며 여러 deal의 결과가 한 position에 합쳐질 수 있습니다. 공식 계좌 속성 문서 새 탭의 ACCOUNT_MARGIN_MODE를 확인하고, 여러 전략이 같은 netting symbol을 공동 소유하지 않도록 계좌·symbol 경계를 설계하는 편이 명확합니다.
복구 코드는 알 수 없는 position을 자동 청산하거나 SL을 임의로 다시 쓰지 않아야 합니다. 불일치를 발견한 순간의 ticket, identifier, volume, SL/TP, Magic Number를 남기고 신규 주문만 잠그는 것이 원인 조사에 더 유용합니다.
처리한 봉과 전송 중인 봉을 따로 영속화합니다
기존 학습용 EA의 datetime lastBarTime = 0;은 프로세스 메모리에만 있습니다. 새 봉에서 주문한 직후 EA를 재시작하면 값이 다시 0이 되고, 같은 현재 봉을 새 신호 경계로 오인할 수 있습니다.
현재 position이 존재하면 중복 진입을 막을 수 있지만 그것만으로는 부족합니다. 첫 주문이 같은 봉 안에서 이미 청산됐거나, 서버 응답을 받기 전에 연결이 끊겼거나, pending order만 남은 경우에는 position 없음이 이 신호를 처리하지 않음을 뜻하지 않습니다.
최소한 두 마커를 구분합니다.
| 마커 | 의미 | 재시작 뒤 동작 |
|---|---|---|
last_processed_bar | 계좌·이력 재조정까지 끝난 신호 경계 | 같거나 과거인 bar는 다시 평가하지 않음 |
inflight_bar | 주문 전송을 시작했지만 최종 상태 확정 전 | 자동 재전송 금지, 이력 재조정 또는 LOCKED |
클라이언트 터미널 global variable은 일반 프로그램 전역 변수와 다릅니다. 공식 global variables 문서 새 탭는 이 값이 터미널에서 유지되고 같은 터미널의 MQL5 프로그램들이 공유한다고 설명합니다. 아래는 저장 키와 첫 실행 정책을 보여 주는 축약 코드입니다.
datetime lastProcessedBar=0;
string processedKey;
string inflightKey;
string StatePrefix()
{
return StringFormat("Permact.%I64d.%s.%s.%d.%I64u",
AccountInfoInteger(ACCOUNT_LOGIN),
AccountInfoString(ACCOUNT_SERVER),
_Symbol,
(int)_Period,
MagicNumber);
}
bool LoadProcessedBar()
{
processedKey=StatePrefix()+".last_processed_bar";
inflightKey=StatePrefix()+".inflight_bar";
if(!GlobalVariableCheck(processedKey))
{
datetime currentBar=iTime(_Symbol,_Period,0);
if(currentBar==0 || GlobalVariableSet(processedKey,(double)currentBar)==0)
return false;
GlobalVariablesFlush();
lastProcessedBar=currentBar; // 첫 부착 시 현재 봉은 소비한 것으로 간주
return true;
}
double stored=0.0;
if(!GlobalVariableGet(processedKey,stored))
return false;
lastProcessedBar=(datetime)(long)stored;
return true;
}키에는 최소한 account login, server, symbol, timeframe, Magic Number를 포함합니다. 같은 Magic Number라도 서버나 timeframe이 다르면 별도 상태여야 합니다. 계좌번호 전체가 로그나 캡처에 노출되지 않도록 출력 로그에서는 마스킹하거나 별도 observation ID로 바꿉니다.
첫 부착에서 현재 봉을 이미 처리한 것으로 저장하는 이유는 안전한 기본값을 선택하기 위해서입니다. EA를 차트에 붙이자마자 이전에 시작된 봉의 신호를 뒤늦게 주문하는 대신 다음 새 봉까지 기다립니다.
주문 직전에는 inflight_bar를 먼저 저장하고 GlobalVariablesFlush()로 디스크 반영을 요청합니다. 주문 요청의 최종 상태를 계좌와 이력에서 확인한 뒤에만 last_processed_bar를 갱신하고 in-flight 마커를 삭제합니다. 공식 flush 문서 새 탭는 갑작스러운 종료 때 손실될 수 있는 global variable을 강제로 디스크에 쓰는 용도를 설명합니다.
이 순서가 중요한 이유는 장애 시점마다 의미가 달라지기 때문입니다.
| 장애 시점 | 남을 수 있는 상태 | 복구 정책 |
|---|---|---|
| in-flight 저장 전 | 서버 요청 없음 | 다음 정상 신호 평가 가능 |
| in-flight 저장 후, 전송 전 | 주문이 없지만 모호한 마커 존재 | 자동 재전송보다 LOCKED가 안전 |
| 전송 후, 응답 전 | 서버에 주문·deal·position이 있을 수 있음 | 현재 목록과 이력 재조정 |
| 서버 처리 후, 완료 마커 전 | 체결됐지만 in-flight만 남을 수 있음 | deal과 position identifier로 완료 여부 확인 |
| 완료 마커와 in-flight 삭제 후 | 처리 완료 | 같은 bar 재평가 금지 |
클라이언트 파일과 서버 주문을 하나의 원자적 transaction으로 묶을 수는 없습니다. 그래서 이 설계는 일부 장애에서 신호를 놓칠 수는 있어도, 모호한 요청을 자동 재전송해 중복 노출을 만드는 쪽을 피합니다. 정확히 한 번 처리라는 표현보다 at-most-once 전송과 명시적 재조정이라고 설명하는 편이 정직합니다.
OnTradeTransaction은 증거를 남기고 짧게 끝냅니다
OrderSend()의 즉시 반환만으로 전체 수명 주기를 확정할 수 없습니다. 주문 요청 하나가 order 생성, deal 추가, history 이동, position 변경 등 여러 거래 이벤트를 만들 수 있습니다.
공식 OnTradeTransaction 문서 새 탭는 이벤트 도착 순서가 보장되지 않고, handler가 오래 걸리면 길이 1024의 거래 transaction queue에서 이전 이벤트가 밀려날 수 있다고 설명합니다. 따라서 handler 안에서 복잡한 복구를 끝내려 하지 말고 식별자를 남긴 뒤 전체 재조정을 예약합니다.
bool needsReconciliation=false;
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
PrintFormat("trade_tx type=%s order=%I64u deal=%I64u position=%I64u symbol=%s",
EnumToString(trans.type),
trans.order,
trans.deal,
trans.position,
trans.symbol);
if(trans.type==TRADE_TRANSACTION_REQUEST)
PrintFormat("trade_request magic=%I64u symbol=%s retcode=%u order=%I64u deal=%I64u",
request.magic,
request.symbol,
result.retcode,
result.order,
result.deal);
if(trans.symbol==_Symbol ||
(trans.type==TRADE_TRANSACTION_REQUEST && request.symbol==_Symbol))
needsReconciliation=true;
}request와 result의 추가 정보는 TRADE_TRANSACTION_REQUEST일 때만 해석합니다. 이후 안전한 OnTick 또는 OnTimer 경계에서 현재 position과 order를 다시 읽습니다. 이벤트 순서를 조립한 메모리 상태보다 최종 서버 snapshot을 우선합니다.
in-flight 마커가 남았는데 현재 position이나 pending order가 없다면 거래 이력을 확인합니다. 공식 HistorySelect 문서 새 탭에 따라 요청 전 시각부터 현재 서버 시각까지 order와 deal 목록을 만들고, symbol, Magic Number, 시간, volume, DEAL_POSITION_ID를 함께 좁힙니다. deal 속성 표 새 탭의 DEAL_MAGIC과 DEAL_POSITION_ID는 이 연결에 사용할 수 있습니다.
단, “같은 시간대에 같은 Magic Number의 deal 하나가 있다”만으로 특정 신호와 일치한다고 단정하지 않습니다. 요청마다 고유한 correlation ID를 comment나 별도 ledger에 남기는 설계가 더 강합니다. 브로커가 comment를 보존·변경하는 방식도 다를 수 있으므로 ticket과 server history를 함께 기록합니다.
Magic Number는 잠금이 아니며 실행 주체는 하나여야 합니다
같은 EA를 로컬 터미널과 VPS에 동시에 켜면 두 프로그램 모두 같은 Magic Number로 같은 신호를 평가할 수 있습니다. Magic Number는 주문과 position의 출처를 분류하는 값이지, 다른 프로세스의 전송을 막는 mutex가 아닙니다.
같은 클라이언트 터미널 안에서는 GlobalVariableSetOnCondition()으로 원자적 compare-and-set을 구현할 수 있습니다. 공식 문서 새 탭도 여러 EA가 한 터미널 안에서 상호 배제를 구현하는 용도를 명시합니다. 그러나 로컬 PC와 원격 VPS는 서로 다른 터미널 저장소를 사용하므로 이 잠금을 공유하지 않습니다.
따라서 단일 실행 주체 원칙을 운영 절차로 고정합니다.
- 기존 실행 주체에서 신규 진입을 먼저 차단합니다.
- 열린 position과 pending order, 마지막 처리 bar, in-flight 상태를 기록합니다.
- 기존 터미널의 EA가 실제로 중지됐는지 로그에서 확인합니다.
- 대상 VPS 또는 새 터미널에서
RECOVERING으로 시작합니다. - 동일한 계좌·server·symbol·timeframe·Inputs·Magic Number인지 대조합니다.
- 재조정이 성공한 뒤에만
READY로 전환하고 첫 복구 tick에는 주문하지 않습니다.
고가용성을 위해 두 터미널을 상시 대기시키려면 터미널 밖의 lease, fencing token, 만료와 clock 정책까지 필요합니다. 단순한 heartbeat 파일이나 같은 Magic Number만으로는 split-brain 중복 주문을 막을 수 없습니다.
복구 판정표를 코드와 함께 고정합니다
복구 함수의 반환값만 true/false로 남기면 나중에 왜 잠겼는지 알기 어렵습니다. 판정 항목과 증거를 구조화합니다.
| 판정 항목 | READY 조건 | LOCKED 예시 |
|---|---|---|
| 연결·권한 | server 연결, terminal·account EA 권한 확인 | 연결 불안정, 거래 권한 불일치 |
| 런타임 소유권 | 활성 실행 주체 1개 | 로컬과 VPS 동시 heartbeat |
| 현재 position | 전략 불변식 안의 개수·방향·volume·SL/TP | 알 수 없는 position, 필수 SL 누락 |
| pending order | 전략이 예상한 종류와 개수 | 시장가 전략에 남은 pending order |
| 처리 bar | 현재 bar보다 미래가 아니며 키 범위 일치 | 다른 server/timeframe 마커 혼용 |
| in-flight | 없음 또는 이력으로 결과 확정 | 요청 결과를 찾을 수 없음 |
| 거래 이력 | order·deal·identifier 연결 가능 | volume 또는 소유권이 설명되지 않음 |
복구 로그에는 비밀번호나 전체 계좌번호 없이 다음을 남깁니다.
observation_id / EA version / source commit
terminal build / server alias / account mode
symbol / timeframe / Magic Number / Inputs hash
runtime state before -> after
owned positions / pending orders / volume / SL/TP
last_processed_bar / inflight_bar
matched order / deal / position identifier
lock reason / operator decision / next replay test코드 변경으로 불변식이나 영속 키 구조가 달라지면 state schema version을 키에 포함합니다. 예전 버전 마커를 새 코드가 조용히 읽는 대신 migration 규칙을 만들거나 LOCKED로 보내야 합니다.
데모에서 장애 창을 직접 재현합니다
정상 시작 한 번으로 상태 복구를 검증할 수 없습니다. MT5 Strategy Tester 결과 읽기와 별도로, 데모 환경에서 다음 장애 창을 observation ID별로 재현합니다.
| 시험 | 주입 시점 | 통과 증거 |
|---|---|---|
| EA 재부착 | position 없는 현재 봉 중간 | 현재 봉을 재주문하지 않고 다음 봉까지 대기 |
| 열린 position 중 재시작 | 보호 주문이 붙은 뒤 | 같은 ticket/identifier를 인식하고 중복 진입 없음 |
| 서버 연결 복구 | 시세 단절 뒤 첫 tick | RECOVERING 로그 뒤 한 tick 이상 주문 보류 |
| 모호한 요청 | in-flight 저장 뒤 강제 종료 | 자동 재전송 없이 order/deal/history 재조정 |
| pending order 잔존 | 주문 접수 뒤 재시작 | position과 별도로 order 목록에서 발견 |
| 로컬→VPS 이전 | 기존 실행 중지 후 대상 시작 | 겹치는 활성 구간 없이 소유권 이전 로그 존재 |
| 이중 실행 오배치 | 같은 EA를 두 차트에 부착 | 두 번째 실행이 READY에 진입하지 못함 |
처음에는 position이 없는 데모 구간에서 시작하고, 열린 position을 포함한 시험은 EA의 보호 주문과 중단 절차를 이해한 뒤 진행합니다. 네트워크 차단이나 강제 종료를 실계좌에서 장애 시험으로 사용하지 않습니다.
통과 기준은 손익이 아닙니다. 각 신호에 대해 평가 횟수, 전송 횟수, order, deal, position 변화, 완료 마커가 한 줄로 연결되고 설명되지 않는 노출이 없어야 합니다.
자주 묻는 질문
lastBarTime만 파일에 저장하면 충분한가요?
아닙니다. 주문 전 저장하면 장애 때 신호를 놓칠 수 있고, 주문 후 저장하면 응답 전 장애 때 중복 전송할 수 있습니다. 처리 완료와 in-flight를 구분하고 서버 상태와 거래 이력을 재조정해야 합니다.
Magic Number가 같으면 재시작 전 position을 찾을 수 있나요?
조회 범위를 좁히는 데 도움이 되지만 소유권 전체를 증명하지는 않습니다. symbol, account mode, order/deal 이력, position identifier, EA 버전과 Inputs를 함께 확인해야 합니다. 특히 netting symbol을 여러 전략이 공유하면 Magic Number만으로 분리가 모호할 수 있습니다.
연결이 돌아오면 OnInit이 다시 호출되나요?
터미널이나 EA가 다시 로드되는 상황과 단순 네트워크 재연결은 같은 사건이 아닙니다. 특정 callback 재호출을 전제로 하지 말고, 매 event 경계에서 연결 전환을 감지해 RECOVERING으로 보내는 편이 명확합니다.
in-flight가 남았는데 이력에도 아무것도 없으면 다시 주문해도 되나요?
자동으로 단정하지 않습니다. 전송 전 종료였을 수도 있지만 이력 동기화 지연이나 조회 구간 오류일 수도 있습니다. observation을 LOCKED로 남기고 원인을 확인한 뒤 새 테스트 ID로 재개합니다.
로컬과 VPS 중 하나가 멈추면 다른 쪽이 자동 승계해도 되나요?
외부 lease와 fencing이 없는 자동 승계는 두 실행 주체가 동시에 자신을 활성이라고 판단하는 split-brain 위험이 있습니다. 이 글의 범위에서는 통제된 수동 이전과 단일 실행 주체를 사용합니다.
위험 설명
상태 복구 로직은 중복 주문과 운영 결함의 가능성을 낮추기 위한 소프트웨어 통제입니다. 네트워크, 브로커 서버, 유동성, 부분 체결, 거래소 또는 OTC 실행 규칙, 운영체제 장애를 제거하지 않으며 정확히 한 번 체결을 보장하지 않습니다.
이 글의 코드는 설계 설명을 위한 축약 예시이며 완성된 EA가 아닙니다. 특정 브로커, 심볼, 파라미터 또는 거래를 권유하지 않습니다. 레버리지 상품은 큰 손실을 빠르게 만들 수 있으므로 Strategy Tester와 격리된 데모 계좌에서 불변식, 로그, 중단 절차를 먼저 검증하세요.
다음 포스팅 예고
다음 글인 AI로 MT5 EA 만들기: Codex·Claude에 전달할 MQL5 전략 명세와 프롬프트에서는 AI에게 막연히 “수익 나는 전략을 만들어 달라”고 맡기는 대신, 사람이 시장 가설과 거래 대상·timeframe, 진입·청산 규칙, position sizing과 손실 한도, 금지 조건, 상태 복구·로그 요구사항, 백테스트와 데모 포워드 테스트의 합격·중단 기준을 먼저 명세하는 과정을 정리합니다.
그 명세를 바탕으로 AI가 MQL5 구현, 테스트 시나리오 작성, 실패 사례와 과최적화 위험 검토를 맡고, 사람이 재현 가능한 증거로 전략의 채택·수정·폐기를 판단하는 협업 흐름까지 살펴봅니다.