Files
trex-core/.proofline/lines/line-0003/dcy-0003.md
T

13 KiB

id, status
id status
dcy-0003 confirmed

주문 기본 timeout 상한과 장마감 자동취소·부분체결 reconciliation

Problem

trex-core는 전략의 주문 intent가 timeout_sec를 생략하면 인스턴스의 default_order_timeout_sec를 사용하지만, 인스턴스 설정도 생략된 경우 최종 timeout이 None이 되어 주문을 무기한 기다릴 수 있다. 전략 payload와 인스턴스 설정에도 3,600초 상한이 없어, 현재 계약만으로는 모든 매수·매도 주문이 늦어도 60분 안에 취소 또는 취소 후 reconciliation 절차를 시작한다는 안전 경계를 보장하지 못한다.

timeout 후 KIS 취소 요청이 장마감 등으로 거부되더라도 취소 API의 즉시 응답은 최종 주문 상태가 아니다. KIS는 장마감 시 당일 미체결 잔량을 자동 취소할 수 있고 그 전에 일부 또는 전량이 체결될 수 있으므로, trex-core는 후속 주문조회에서 전량체결, 무체결 자동취소, 부분체결 후 잔량취소를 구분하여 슬롯과 ledger를 확정해야 한다.

현재 실행 루프와 KIS adapter는 같은 날 주문조회가 cncl_yn="Y"를 반환하면 무체결 취소와 부분체결 후 취소를 처리한다. 그러나 KIS 주문조회는 polling 시점의 오늘(KST)을 조회일로 사용하고 주문번호 row를 찾지 못하면 accepted로 간주한다. 장마감 결과를 확인하기 전에 날짜가 넘어가거나 daemon이 재시작되면 전날 주문을 다시 찾지 못해 슬롯이 PENDING_BUY 또는 PENDING_SELL에 고착될 수 있다.

이 Line은 주문 제출 후 최대 3,600초 안에 취소/reconciliation을 시작하는 실행 timeout과, KIS의 authoritative 주문조회로 체결·취소를 확인한 뒤에만 슬롯을 terminal business state로 전환하는 최종 상태 확정을 분리한다. KIS 상태가 불명확할 때 로컬 timeout만으로 슬롯을 임의 해제해서는 안 된다.

Evidence

  • GitHub issue #58 https://github.com/fourblocks2020/trex-core/issues/58은 이 문제와 현재 코드 근거를 기록한 Discovery 제안이다. 요청자는 2026-08-14에 OQ 선택을 완료한 뒤 문서 갱신과 Discovery 승인을 명시적으로 지시했다.
  • issue #58의 live 본문에서 요청자는 OQ-001 A를 선택했다. hard fallback은 3,600초이며 인스턴스 설정은 1..3600 범위에서 더 짧게 설정할 수 있어야 한다.
  • 같은 본문에서 OQ-002 A를 선택했다. 전략 payload의 timeout_sec <= 0 또는 timeout_sec > 3600은 보정하지 않고 주문 제출 전에 명시적으로 거부한다.
  • 같은 본문에서 OQ-003 A를 선택했다. KIS terminal evidence가 확보될 때까지 슬롯을 fail-closed pending/reconciliation 상태로 유지하고 CRITICAL alarm을 발생시키며, 원주문 날짜 조회와 재시작 복구를 계속하고 로컬에서 보유량 0을 추론하지 않는다.
  • 같은 본문에서 OQ-004 B를 선택했다. 기본 timeout과 3,600초 상한은 매수·매도 주문에 공통 적용한다.
  • 조사 기준 source revision은 90b72863a2258f8b8ed4f48a6c84dba32c2f74fd이며 조사 시 main과 origin/main은 동기화되어 있었다.
  • trex_core/application/intents.py의 IntentService는 default_order_timeout_sec: int | None = None을 허용하고, intent와 config에 값이 모두 없으면 슬롯에 None을 저장한다.
  • trex_core/application/messaging_handlers.py는 매수·매도 payload의 timeout_sec를 _opt_int()로 파싱하지만 1..3600 범위 계약을 적용하지 않는다.
  • trex_core/runtime/config.py의 default_order_timeout_sec 검증은 값이 존재할 때 양수인지만 확인하며 3,600초 상한과 설정 부재 시 hard fallback을 강제하지 않는다.
  • trex_core/application/execution.py::_try_timeout_cancel()은 timeout 도달 시 BrokerPort.cancel_order()를 호출하고 즉시 결과를 영속화한다. REQUESTED, REJECTED, UNKNOWN은 최종 주문 상태가 아니며 이후 _poll()의 poll_fill() 결과가 terminal authority다. 다만 slot.cancel_state가 설정된 뒤에는 추가 취소를 요청하지 않는다.
  • trex_core/adapters/broker/kis.py::cancel_order()는 KIS rt_cd != "0"을 CancelState.REJECTED로 매핑하고 msg_cd를 reason으로 보존한다. transport·5xx·malformed response는 UNKNOWN으로 구분한다.
  • KISBroker.poll_fill()은 polling 당시의 KST 날짜로 inquire-daily-ccld를 호출한다. 해당 주문 row가 없으면 order_not_visible_in_fill_inquiry reason과 함께 accepted를 반환하며 원주문 날짜를 영속화하거나 전날 주문을 조회하는 경로는 없다.
  • KISBroker._fill_status_from_row()는 cncl_yn="Y"이고 tot_ccld_qty=0이면 cancelled, 0 < tot_ccld_qty < ord_qty이면 cancelled=True인 partially_filled, 전량 체결이면 filled로 분류한다.
  • ExecutionService._on_partial_cancel()은 매수 부분체결분의 deterministic execution_id를 사용해 ledger 기록을 만들고 broker 잔고를 authority로 수량을 정한 뒤 PENDING_BUY → HOLDING으로 전환한다. 무체결 취소는 초기 매수라면 PENDING_BUY → INACTIVE, 증액 매수라면 기존 broker 보유량 기준 PENDING_BUY → HOLDING으로 복구한다. 매도도 부분체결·취소 결과를 broker 잔고에 맞춰 처리한다.
  • 종료된 issue #21은 무기한 pending 문제와 timeout 취소 기능의 선행 이력이고, 종료된 issue #9는 KIS cncl_yn 외부취소 감지의 선행 이력이다. 열린 issue #45는 장 시작 KIS REST timeout과 account snapshot fallback을 다루므로 주문별 timeout·장마감 자동취소 확정과는 범위가 다르다.
  • 기준 revision에서 uv run --extra dev pytest tests/application/test_execution_policy.py tests/application/test_execution_policy_handler.py tests/application/test_execution_cancel.py tests/adapters/broker/test_kis_cancel.py -q -o 'addopts='를 실행한 결과는 53 passed in 0.10s였다. 이는 기존 timeout 취소와 같은 날 외부취소·부분체결 회귀가 통과함을 보여줄 뿐, hard fallback, 3,600초 상한, 장마감 취소 API 오류 또는 날짜 rollover 복구가 이미 구현됐다는 증거는 아니다.

Scope

  • AC admission은 기존 active AC를 변경하지 않고 새로운 장기 동작을 정의하는 create다. 현재 active AC-0001~AC-0008은 KIS 잔고 페이지네이션 및 GHCR 게시 경계를 소유하며 주문 timeout·취소 reconciliation을 소유하지 않는다.
  • 매수·매도 intent가 timeout_sec를 생략하거나 명시적으로 null을 전달하면 hard fallback 3600초를 적용한다. 인스턴스 default_order_timeout_sec는 1..3600초 범위에서 hard fallback보다 짧거나 같은 기본값을 제공할 수 있으며 설정 자체가 없어도 timeout을 비활성화하지 않는다.
  • 전략 payload의 timeout_sec는 1..3600초만 허용한다. timeout_sec <= 0, timeout_sec > 3600 또는 정수로 해석할 수 없는 값은 clamp하지 않고 주문 제출 전에 bad_request 계열 reason으로 거부한다.
  • 인스턴스 default_order_timeout_sec가 허용 범위를 벗어나면 daemon 시작 시 설정 오류로 실패하며 주문 실행을 시작하지 않는다.
  • effective timeout과 immutable 주문 제출 시각 또는 원주문 날짜를 영속화하여 daemon 재시작 후에도 같은 deadline과 주문일을 사용한다. 일반적인 updated_at 변경으로 timeout 기준점이나 주문 조회일이 이동해서는 안 된다.
  • timeout 도달은 슬롯의 강제 종료가 아니라 취소/reconciliation 시작 조건이다. 실행 루프는 외부 주문의 최신 상태를 먼저 조회하고 non-terminal인 경우에만 취소를 요청한다.
  • 취소 API의 REQUESTED, REJECTED, UNKNOWN은 final cancellation이 아니다. terminal 주문 상태는 원주문 날짜를 사용하는 KIS 주문조회로만 확정한다.
  • 취소 응답이 REJECTED 또는 UNKNOWN이고 terminal 상태를 즉시 확보하지 못하면 슬롯을 fail-closed pending/reconciliation 상태로 유지한다. 주문조회와 재시작 복구를 계속하고 CRITICAL alarm을 발행하며 로컬에서 취소, 미체결 또는 보유량 0을 추론하지 않는다.
  • UNKNOWN 또는 retryable 취소 결과의 재시도는 주문조회를 먼저 수행하고 broker가 주문을 여전히 open으로 확인한 경우에만 bounded backoff로 허용한다. 응답 유실 가능성이 있는 취소를 blind retry해서는 안 된다.
  • 날짜 rollover와 daemon 재시작 후에도 영속화된 원주문 날짜로 KIS 주문 상태를 조회한다. 주문 row 미가시성은 제출 직후의 bounded visibility grace와 그 이후의 unresolved 상태로 구분하며, row가 없다는 사실만으로 terminal 상태를 추론하지 않는다.
  • KIS 장마감 자동취소 결과는 다음과 같이 처리한다.
    • 무체결 초기 매수 취소: PENDING_BUY → INACTIVE
    • 무체결 증액 매수 취소: broker 기존 보유량 기준 PENDING_BUY → HOLDING
    • 매수 부분체결 후 잔량취소: 체결분 ledger exactly-once 기록, broker 잔고 기준 PENDING_BUY → HOLDING
    • 매도 무체결 또는 부분체결 후 잔량취소: broker 잔고 기준으로 HOLDING 또는 기존 terminal sell 경계를 적용
    • 취소와 전량체결 경합: 전량체결을 우선하여 정상 체결 경로 적용
  • 취소 응답 유실, 취소 API 거부, 장마감 자동취소, 전량·부분·무체결, 날짜 rollover, daemon 재시작, 주문 row 미가시성과 ledger 멱등성을 repository-local synthetic KIS fixture와 application 통합 테스트로 검증한다.
  • account, token, credential, connection string과 raw KIS payload 등 민감한 값은 로그·오류·fixture·evidence에 포함하지 않는다.

Out of Scope

  • KIS 거래소 자체의 당일주문 유효기간 또는 장마감 자동취소 정책 변경
  • strategy daemon의 매수·매도 조건, 가격 결정, slippage 또는 주문수량 정책 변경
  • portfolio-hub API·ledger schema 또는 다른 TREX 서비스의 wire/HTTP 계약 변경
  • 운영계좌를 이용한 실제 주문·취소 실행, 운영 데이터 수정 또는 기존 pending 슬롯의 사후 자동 정리
  • KIS account snapshot fallback과 장 시작 REST 장애 복구(issue #45 범위)
  • terminal broker evidence 없이 별도 유예시간만으로 슬롯을 강제 INACTIVE 또는 재사용 가능 상태로 전환하는 정책
  • timeout 범위 밖 값을 clamp하여 주문을 계속하는 호환 동작
  • 배포, release, 운영환경 설정 변경 또는 delivery/lifecycle 완료 주장

Risks and Unknowns

Risks

  • timeout 시점에 슬롯을 즉시 해제하면 취소 요청과 실제 체결이 경합하거나 KIS 응답이 유실된 경우 실제 보유량과 슬롯이 어긋날 수 있다. 요청자가 선택한 fail-closed 정책에 따라 terminal broker evidence 전에는 슬롯을 재사용 가능하게 만들지 않고 CRITICAL alarm과 reconciliation을 유지한다.
  • 원주문 날짜가 아닌 현재 날짜로 조회하면 장마감 직전 주문이나 재시작된 주문을 다음 날 찾지 못해 무기한 pending이 재발할 수 있다. immutable 주문 제출 시각 또는 원주문 날짜를 영속화하고 조회 기준으로 사용한다.
  • 주문조회 row 없음은 제출 직후 eventual consistency와 오래된 주문 누락을 모두 포함할 수 있다. bounded visibility grace 이후에도 row 없음만으로 취소·미체결을 추론하지 않고 unresolved로 유지한다.
  • UNKNOWN 취소를 맹목적으로 재시도하면 최초 취소가 실제 접수됐지만 응답만 유실된 경우 중복 요청이 발생할 수 있다. 재시도 전 authoritative 조회를 우선하고 open 주문이 확인된 경우에만 bounded backoff를 적용한다.
  • 부분체결 후 자동취소를 여러 번 관찰하면 ledger가 중복 기록될 수 있다. deterministic execution_id와 기존 outbox/ledger 멱등 경계를 보존하고 반복 polling·재시작 fixture로 검증한다.
  • KIS의 실제 장마감 error msg_cd와 자동취소 row shape가 synthetic fixture와 다를 수 있다. 특정 message code를 terminal authority로 고정하지 않고 주문조회 결과를 정본으로 사용하며, code-specific retry 분류가 필요하면 민감정보를 제거한 별도 운영 evidence로 검증한다.
  • 매수뿐 아니라 매도에도 공통 timeout을 적용하면 기존에 timeout을 생략하던 매도 전략의 동작이 바뀐다. 이 변경은 요청자가 OQ-004 B로 선택한 승인 범위이며, 양쪽 side의 기존 명시 timeout과 생략 경로를 독립 회귀 테스트로 보호한다.