자동매매에서 실계좌 오주문을 막는 안전장치 설계

투자 분석 서비스에 주문 기능을 추가할 때 가장 먼저 정한 원칙은 “정상 동작보다 잘못된 주문을 막는 것이 우선”이라는 점이었습니다. API 호출 한 번, 환경변수 한 줄, 테스트 코드의 실수만으로 실계좌 주문이 나가서는 안 됩니다. 그래서 하나의 조건이 아니라 서로 독립적인 여러 차단 장치를 통과해야 주문 계층에 도달하도록 설계했습니다.

분석과 주문 의도를 분리

전략 분석 결과가 곧바로 주문 API를 호출하지 않습니다. 먼저 종목, 방향, 수량, 예상 가격, 판단 근거를 담은 OrderIntent를 생성합니다. 이 객체는 “주문 후보”일 뿐이며, 별도의 검증기와 실행 서비스를 지나기 전에는 외부 주문 요청이 발생하지 않습니다.

1. 기본값은 DRY_RUN

DRY_RUN=true에서는 키움 주문 API를 호출하지 않고 paper order만 SQLite에 기록합니다. 분석 API의 execute 기본값도 false입니다. 즉, 설정을 아무것도 바꾸지 않은 상태에서는 분석과 주문 후보 생성까지만 진행됩니다.

2. 거래 모드를 명시적으로 구분

  • DRY_RUN: 외부 주문 없이 기록만 생성
  • MOCK: 키움 모의투자 환경에서만 주문
  • REAL: 별도 실계좌 허용 조건과 수동 확인 필요

모의투자 주문도 TRADING_ENABLED=true, TRADING_MODE=MOCK, DRY_RUN=false, KIWOOM_USE_MOCK=true, REAL_TRADING_ENABLED=false가 모두 맞아야 실행됩니다. 일부 설정만 바뀐 어중간한 상태는 실패하도록 만들었습니다.

3. 종목·수량·금액을 다시 검증

OrderValidatorRiskGuard는 허용 종목 목록, 주문 수량, 일일 주문 횟수, 누적 주문 금액 등을 확인합니다. 전략 코드에서 이미 계산한 값이라도 실행 직전에 다시 검증합니다. 분석 로직의 버그가 주문 계층까지 그대로 전달되지 않도록 경계를 하나 더 둔 것입니다.

4. 실계좌는 Pending 상태와 수동 승인

실계좌 후보는 바로 실행하지 않고 pending_orders에 저장합니다. 웹 콘솔에도 주문 실행 화면을 넣지 않았습니다. 현재 단계의 수동 확인은 상태 변경과 검토 기록에 집중하며, 실제 주문 실행 경로와 분리되어 있습니다.

5. Kill Switch

Kill Switch가 켜져 있으면 다른 설정이 모두 정상이어도 주문을 차단합니다. 웹 콘솔에서 해제할 때는 로그인 세션만으로 부족하고 TOTP 또는 일회용 복구 코드를 다시 확인합니다. 긴급 상황에서는 Bearer API를 통해 즉시 활성화할 수도 있습니다.

재시작과 중복 주문도 고려

주문 요청 전후 상태를 SQLite에 남기고, 실제 계좌의 주문·체결 내역과 로컬 기록을 대조하는 reconciliation 계층을 두었습니다. 네트워크 타임아웃이 발생했을 때 무조건 같은 주문을 다시 보내는 대신, 외부 상태를 먼저 확인할 수 있도록 하기 위함입니다.

테스트에서 확인한 핵심 시나리오

  • DRY_RUN에서 외부 주문 클라이언트가 호출되지 않는지
  • 허용 목록 밖 종목과 0 이하 수량이 차단되는지
  • Kill Switch가 모든 거래 모드보다 우선하는지
  • 실계좌 관련 환경변수 하나가 빠져도 실패하는지
  • 일일 한도를 초과한 주문이 거절되고 기록되는지

마무리

자동화에서 안전은 하나의 완벽한 검사로 얻기 어렵습니다. 기본 비활성화, 모드 분리, 입력 검증, 한도, 수동 승인, Kill Switch, 이력 대조처럼 성격이 다른 장치를 겹쳐야 실수 한 번이 실제 손실로 이어질 가능성을 낮출 수 있습니다. 아직 이 프로젝트의 기본 목적은 투자 판단 보조이며 실계좌 자동 주문은 기본적으로 비활성 상태입니다.

이 글은 소프트웨어 안전 설계에 관한 기록이며 투자 조언이 아닙니다.

관련 글