From 59e75c369f686d858dd64bcd786e4bd938f470ae Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 14 Aug 2026 14:46:06 +0900 Subject: [PATCH] docs(proofline): draft order timeout reconciliation discovery --- .proofline/identities.json | 2 +- .proofline/lines/line-0003/dcy-0003.md | 79 +++++++++++++++++++++++++ .proofline/lines/line-0003/line-0003.md | 3 + 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 .proofline/lines/line-0003/dcy-0003.md create mode 100644 .proofline/lines/line-0003/line-0003.md diff --git a/.proofline/identities.json b/.proofline/identities.json index 55489e2..e53e86e 100644 --- a/.proofline/identities.json +++ b/.proofline/identities.json @@ -1,5 +1,5 @@ { "schema_version": 1, - "next_line_number": 3, + "next_line_number": 4, "next_ac_number": 9 } diff --git a/.proofline/lines/line-0003/dcy-0003.md b/.proofline/lines/line-0003/dcy-0003.md new file mode 100644 index 0000000..c8a5305 --- /dev/null +++ b/.proofline/lines/line-0003/dcy-0003.md @@ -0,0 +1,79 @@ +--- +id: "dcy-0003" +status: draft +--- + +# 주문 기본 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과 생략 경로를 독립 회귀 테스트로 보호한다. diff --git a/.proofline/lines/line-0003/line-0003.md b/.proofline/lines/line-0003/line-0003.md new file mode 100644 index 0000000..27b5075 --- /dev/null +++ b/.proofline/lines/line-0003/line-0003.md @@ -0,0 +1,3 @@ +--- +id: "line-0003" +---