9 / 12 · AUTO TRADING WIKI

페이퍼 주문과 가상 잔고 기록하기

실제 주문 전송 없이 주문·잔고·실행 결과를 가상으로 기록합니다.

발행
수정
문서 검토
2026-10-20
본문 버전
1.1.1-web.20261020

이번 장에서는 신호를 가상 주문으로 바꾸고, 가상 체결이 현금과 보유 수량에 미치는 영향을 기록한다. 시뮬레이션의 거래별 손익을 넘어 하나의 계정에서 자금이 실제로 모자랄 수 있다는 조건을 다룬다.

여기서 paper는 로컬 원장 시뮬레이션이다. 한국투자증권의 모의투자 서버에 접속하지 않으며 실제 계좌·주문 번호를 사용하지 않는다. 모든 코드와 금액은 auto-trading-lab의 학습 예시다.

1. 신호·주문·체결을 다른 사건으로 다루기

신호는 조건이 만족되었다는 계산 결과다. 주문은 특정 수량을 특정 조건으로 처리해 달라는 의도다. 체결은 그 주문이 가격과 수량을 가지고 실행되었다는 결과다. 이 셋을 같은 테이블의 한 행으로 처리하면 미체결과 실패를 구분하기 어려워진다.

예를 들어 T일 CCI20이 -100을 상향 돌파해도 T일에 바로 가상 매수하지 않는다. T일 종가 확정 후 다음 거래 세션의 진입 의도를 만들고, T+1의 가상 OPEN 이벤트에서만 체결을 계산한다. 주문을 만들었다고 보유 수량부터 늘리지 않는다.

객체 예제 상태 잔고에 미치는 영향
신호 DETECTED, SKIPPED 없음
주문 PENDING, FILLED, CANCELED, REJECTED 미체결 매수는 현금 예약
체결 가격·수량이 확정된 불변 기록 현금·수량을 변경
포지션 수량·평균 취득원가 체결 누적으로 계산

이번 구현은 전량 체결 또는 전량 미체결의 단순 모델이다. 부분 체결, 정정·취소의 경합, 실제 주문 접수와 거래소 체결 차이는 재현하지 않는다. 보고서에 partial_fill_supported = false를 기록한다.

2. 가상 계정과 한도를 설정하기

실제 계좌번호 대신 PAPER_DEMO_01을 사용한다. config/paper.json을 예제로 만들되, 실전 API 키가 들어 있는 설정을 함께 읽지 않도록 분리한다.

{
  "mode": "paper",
  "account_id": "PAPER_DEMO_01",
  "initial_cash": "1000000.00",
  "max_order_notional": "100000.00",
  "max_open_positions": 3,
  "same_symbol_overlap": "skip",
  "allow_short": false,
  "allow_leverage": false,
  "fill_model": "fixture_next_open_and_fifth_close",
  "external_order_transport": "disabled",
  "strategy_version": "cci20_cross_up_m100_v1"
}

매수 수량은 거래 전 사용 가능 현금, 주문당 한도, 수수료와 가격 가정을 함께 고려해 정수로 계산한다. initial_cash는 실행마다 다시 지급하는 돈이 아니다. 계정을 최초 생성할 때 한 번 기록하고, 재시작은 저장된 현금과 원장을 읽는다.

7장의 same_symbol_overlap = skip을 유지한다. 같은 의도키의 재시도라면 먼저 기존 주문과 내용을 대조하고 새 주문을 만들지 않는다. 새 진입 의도는 계정 행 잠금 안에서 검사하며, 보유 수량이 양수이거나 PENDING 매수 주문이 있는 종목은 SKIPPED/OVERLAPPING_POSITION으로 제외한다.

max_open_positions는 보유 종목과 PENDING 매수 종목의 합집합에 속한 고유 종목 수로 계산한다. 새 종목을 더했을 때 한도를 넘으면 의도를 생성하지 않는다. 취소·거절된 미체결 매수는 현금 예약과 미체결 슬롯을 해제하며, 보유 수량은 매도 체결 전까지 보유 슬롯에 남긴다. 이 검사와 주문 생성·예약은 같은 계정 잠금과 트랜잭션 안에서 처리한다.

동시에 여러 신호가 있으면 처리 순서도 고정한다. 예를 들어 (signal_date, symbol) 순서로 처리하면 재현하기 쉽지만 특정 종목 우선 효과가 생긴다. 이 정책을 문서에 남기고, 전략 성과로 오해하지 않는다.

3. 원장 구조와 고유키 정하기

최소 저장 구조는 paper_accounts, paper_orders, paper_fills, paper_cash_ledger, paper_positions다. 각 기록은 account_id, run_id, strategy_version으로 연결한다. 금액은 SQL의 DECIMAL, 수량은 허용하는 단위에 맞는 타입으로 저장한다.

CREATE TABLE paper_orders (
  order_id CHAR(36) PRIMARY KEY,
  account_id VARCHAR(40) NOT NULL,
  run_id VARCHAR(80) NOT NULL,
  strategy_version VARCHAR(40) NOT NULL,
  client_order_key VARCHAR(180) NOT NULL,
  symbol VARCHAR(20) NOT NULL,
  side VARCHAR(4) NOT NULL,
  quantity INT NOT NULL,
  scheduled_session DATE NOT NULL,
  scheduled_event VARCHAR(8) NOT NULL,
  status VARCHAR(20) NOT NULL,
  reserved_cash DECIMAL(18,2) NOT NULL DEFAULT 0,
  UNIQUE KEY uq_paper_intent (account_id, client_order_key)
) ENGINE=InnoDB;

교육용 키 예시는 cci20_cross_up_m100_v1|DEMO_A|2026-06-02|ENTRY다. 같은 실행을 재시도해도 이 의도 키는 바꾸지 않는다. 실행을 구별하는 run_id만 고유키로 삼으면 재시도할 때 같은 주문이 새 주문으로 생길 수 있다.

paper_orders.run_id는 주문을 최초 생성한 실행을, strategy_version은 그때의 전략 버전을 나타낸다. 재시도·복구에서는 이 최초 생성 정보를 덮어쓰지 않는다. 이후 실행의 처리 시도는 기존 order_id를 참조하는 paper_runs와 실행 로그에 별도로 남긴다. 의도의 고유 제약은 (account_id, client_order_key)로 유지하며 run_id를 포함하지 않는다.

체결 테이블에는 (order_id, fill_sequence) 고유 제약을, 현금 원장에는 체결별 정산 사건의 고유 제약을 둔다. 종료 주문은 원래 진입 포지션 식별자와 연결해, 매일 “보유 중”이라는 이유로 청산 주문이 여러 개 생기지 않도록 한다.

4. 주문을 만들 때 현금을 예약하기

예제에서 가상 현금이 1,000,000이고 10,000원 10주 매수와 수수료 50을 예상한다면 100,050을 예약한다. 총현금은 아직 1,000,000이지만 사용 가능 현금은 899,950이 된다. 다른 주문은 예약된 금액을 쓸 수 없다.

T일에는 T+1 시가를 알 수 없으므로 실제 구현의 예약액은 사전에 정한 가격 상한과 비용 여유를 이용해야 한다. T+1 가격으로 필요한 금액이 예약 한도를 넘으면 이번 단순 모델은 REJECTED/RESERVATION_LIMIT으로 처리하고 예약을 해제한다. 미래 시가를 T일에 미리 읽어 수량을 유리하게 정하지 않는다.

DB에서는 사용 가능 현금을 조회하고 주문을 삽입하고 예약을 갱신하는 과정을 한 트랜잭션으로 묶는다. 동시 실행 가능성이 있다면 계정 행을 SELECT ... FOR UPDATE로 잠그고 한도를 다시 확인한다. InnoDB 잠금 읽기의 동작은 MySQL 공식 문서를 참고한다.

고유키 충돌은 아무 오류나 무시하는 방식으로 처리하지 않는다. 기존 주문을 읽어 종목·방향·수량·예정 세션이 같은지 비교한다. 내용이 다르면 INTENT_CONFLICT로 중단한다. 같은 키에 다른 수량이 들어온 상황을 정상 재시도로 숨기지 않는다.

5. 체결과 잔고 변경을 함께 확정하기

가상 OPEN 이벤트에서 주문과 계정을 잠그고 상태가 PENDING인지 확인한다. 가격·거래량·예정 세션 조건이 맞으면 체결, 현금 차감, 수량 증가, 예약 해제, 주문 상태 변경을 하나의 트랜잭션으로 확정한다. 한 단계라도 실패하면 전체를 되돌린다.

BEGIN
  계정·주문 행 잠금
  기존 체결과 주문 상태 확인
  예정 세션·가격·수량·예약 한도 확인
  paper_fills에 가상 체결 삽입
  paper_cash_ledger에 현금 차감 삽입
  계정 현금과 paper_positions 갱신
  예약 해제 및 주문을 FILLED로 변경
COMMIT

실제 코드는 SQL 값을 문자열로 조합하지 않고 DB 드라이버의 파라미터 바인딩을 사용한다. 트랜잭션의 확정과 취소는 MySQL 공식 COMMIT·ROLLBACK 문서를 기준으로 구현한다. 로그 파일 한 줄이 성공했다는 이유만으로 DB 확정을 대신 판단하지 않는다.

청산도 같은 원칙이다. 진입 세션을 포함해 5번째 세션의 가상 CLOSE에서 보유 수량을 넘지 않는 매도만 허용한다. 매수 예약·매수 체결·매도 체결의 사건이 연결되어야 현금과 수량을 복원할 수 있다.

6. 가상 원장을 손으로 대조하기

7장의 가상 거래와 같은 가격·비용을 사용하면 다음 결과가 나와야 한다. 초기 예제와 대조할 때만 매수 가격 10,000, 매도 가격 10,200, 수량 10, 슬리피지 0으로 고정한다.

시점 총현금 예약현금 보유 수량
초기 1,000,000 0 0
매수 의도 생성 1,000,000 100,050 0
매수 체결 899,950 0 10
매도 체결 1,001,695 0 0

매도 정산 유입은 102,000에서 가정 수수료 51과 가정 세금 204를 뺀 101,745다. 이 숫자가 현금 원장과 계정 현금에 모두 반영되어야 한다. 보유 중 자산 평가는 평가 가격과 평가 시각도 함께 표시하고 실현손익과 구분한다.

대조식은 현재 현금 = 최초 입금 + 현금 원장 증감 합계와 현재 수량 = 매수 체결 수량 - 매도 체결 수량이다. 반올림 정책까지 동일하게 적용했는데도 차이가 있으면 다음 실행을 막는다.

7. 과거 재생과 앞으로의 관찰을 구분하기

일봉만으로 학습할 때는 가상 시계를 두고 과거의 CLOSE → 다음 OPEN → CLOSE 이벤트를 순서대로 재생한다. T+1 시가를 이미 갖고 있어도 T일 의도 생성 함수에는 전달하지 않는다. 전체 데이터가 메모리에 있다는 이유로 미래 열을 읽지 않도록 입력 경계를 나눈다.

앞으로의 데이터를 매일 관찰하는 방식이라면 아직 오지 않은 시가나 종가를 체결로 기록할 수 없다. 일봉이 확정된 뒤 뒤늦게 체결을 재구성한다면 replayed_after_close와 원본 가격 확정 시각을 표시한다. 이는 실시간 시장에서 그 가격에 주문이 체결되었다는 뜻이 아니다.

장중 고가·저가를 이용한 손절·익절은 이 모델에 추가하지 않는다. 한 일봉에서 두 조건이 모두 충족될 때 선후관계를 알 수 없기 때문이다. 원장의 정확성과 시장 체결의 현실성은 별도 문제로 평가한다.

8. Codex에 작은 원장 시험을 요청하기

외부 네트워크 주문 기능이 없는 로컬 paper 원장을 구현해줘.
config/paper.json을 읽고 계정 초기 입금은 한 번만 기록해.
T일 종가 신호로 PENDING 의도를 만들고 T+1 OPEN 이벤트에서만 가상 체결해.
진입일 포함 5번째 CLOSE 청산을 포지션 식별자와 연결해.
계정 잠금·고유 의도키·체결키·현금 예약·원자적 정산을 적용해.
같은 종목 보유·PENDING 매수의 중복 진입을 막고 보유·미체결 종목의 합집합으로 보유 한도를 검사해.
주문의 최초 run_id·strategy_version은 보존하고 재시도는 기존 order_id와 새 실행 로그로 연결해.
실제 계좌번호와 API 키를 읽지 말고 가상 fixture만 사용해.
매수 후 현금 899950, 청산 후 1001695가 되는 예제를 확인해.
같은 이벤트 재생·현금 부족·가격 누락·트랜잭션 중단도 확인하고 보고해.

정상 로그는 intent_created, fill_committed, reconciliation_passed처럼 사건을 구분한다. 가상 체결 번호에는 PAPER- 접두사를 넣어 실제 주문 번호와 혼동하지 않게 한다. 민감한 설정 전체를 로그로 출력하지 않는다.

9. 완료 기준과 문제 해결

완료 기준은 주문과 체결이 구분되고, 재실행해도 초기 자금·주문·체결이 늘지 않으며, 현금·수량 대조 차이가 0인 것이다. 가격이 없는 이벤트는 미체결로 남거나 사유와 함께 종료되어야 하며, 임의의 가격으로 채우지 않는다.

증상 먼저 확인할 것
재시작마다 현금이 늘어남 초기 입금 사건의 고유키
주문만 만들었는데 수량이 증가 신호·주문·체결의 갱신 경계
현금 부족인데 여러 주문이 생성 계정 잠금과 예약액 대조
같은 체결이 두 번 반영 체결키와 정산 사건 고유키
가격 누락인데 FILLED 상태 가상 체결 전 데이터 검증

다음 장에서는 이 원장을 하루씩 반복 실행하고, 중간 실패 후 어디까지 확정되었는지 확인하며 복구하는 절차를 만든다.

학습의 다음 단계를 남겨 두세요

공유 링크를 복사해 이어서 읽을 수 있습니다.