KIS 계좌에서 직접 매도 주문을 취소하면 poll_fill 이 cncl_yn 을 파싱하지 않아 슬롯이 pending_sell 에 영구히 갇히고 죽은 주문을 무한 폴링하던 버그(issue #9) 정정. 거버넌스: 전부 change-req (신규 REQ 없음, dcy-013 OQ-3). - REQ-012: KIS poll_fill cncl_yn 파싱 → 외부 취소 감지. FillStatus.cancelled /is_partial_cancel 추가. 체결 0 취소→cancelled, 부분 체결 후 취소→partial+cancelled - REQ-013: 집행 _on_partial_cancel — 체결분 원장 1회 + qty 차감(매도 잔여/매수 체결분) + holding 복귀 (OQ-1 A안). 전량 취소는 기존 롤백 경로 재사용 - REQ-018: 부분 체결분 원장 1회 기록 (_record_ledger 재사용) - REQ-031: 잔여 포지션 정합 연계 (slot.qty ↔ 실 잔고, 과매도 방지) - REQ-014: pending stale 워치독 Alert-only (TREX_PENDING_STALE_THRESHOLD, WARN+카운터, 상태 미변경) — 취소 감지 누락 backstop - REQ-015: interface-contract/runtime-flows 정합 전량 취소는 holding 자동 복구(limit_price clear→재매도 시장가, dcy-012 연계). KIS cncl_yn 실 규약은 운영 검증(미지 값 보수 처리 + 워치독 backstop). 검증: 단위 294 passed/32 skipped, 인프라 게이트(PG+RabbitMQ) 326 passed, ruff clean. 신규 test_kis_cancel(httpx stub) + test_execution_cancel(programmable broker). BAT-017 release-candidate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.0 KiB
4.0 KiB
BAT-017 Design
- Status: confirmed
- Batch: BAT-017
설계 목표
- KIS 외부 취소를 종료 상태로 정확히 매핑해 pending_sell 영구 갇힘(issue #9)을 자동 복구한다.
- 부분 체결 후 취소를 정확 회계(과매도·이중기록 방지)하고, 미지 원인 갇힘을 Alert-only 워치독으로 가시화한다.
1. FillStatus 계약 확장 (REQ-012)
FillStatus.cancelled: bool = False추가.is_partial_cancel프로퍼티 =cancelled and filled_qty > 0.- 매핑 규칙:
- 체결 0 + 외부 취소 →
state="cancelled"(기존is_terminal_rejectTrue). - 부분 체결(0<ccld<ord) + 외부 취소 →
state="partially_filled"+cancelled=True(is_terminal_reject False, is_partial_cancel True) + execution_id/fee/avg_price 운반. - 전량 체결 →
state="filled"(취소 플래그 무시).
- 체결 0 + 외부 취소 →
2. KIS poll_fill (REQ-012)
inquire-daily-ccldrow 의cncl_yn을 파싱:str(cncl_yn).strip().upper() == "Y". 미지/부재 → 비취소(보수적, R-1; stale 워치독이 backstop).- ccld_qty<=0: cancelled 면
cancelled반환, 아니면 기존accepted. - 0<ccld<ord: cancelled 면 execution_id(
kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{ccld_qty})+fee 합성 후 partial+cancelled 반환, 아니면 기존 partially_filled. - 전량 체결: 기존 filled 경로 불변.
3. 집행 처리 (REQ-013/018, OQ-1 A안)
_poll분기 순서:is_filled→is_partial_cancel→is_terminal_reject→ else(대기+stale 체크).- 전량 취소(
cancelled, ccld 0): 기존_rollback재사용 — 매도pending_sell→holding+clear_dispatch, 매수pending_buy→idle. holding 진입 시 limit_price clear(dcy-012) → 재매도 시장가(A-1). - 부분 체결 후 취소
_on_partial_cancel:- 매도(PENDING_SELL):
remaining = slot.qty - filled;transit_slot(PENDING_SELL→HOLDING, qty=remaining)(avg_price=None → cost basis 유지). 체결분(filled, 체결가) 원장 1회(_record_ledger재사용). - 매수(PENDING_BUY):
transit_slot(PENDING_BUY→HOLDING, qty=filled, avg_price=fill). 체결분 원장 1회. - 공통: clear_dispatch + event emit.
r.filled += 1,_record_ledger가r.ledger_enqueued += 1.
- 매도(PENDING_SELL):
- 멱등:
SlotStateMismatchError시 조용히 반환(다음 tick reconcile).
4. pending stale 워치독 (REQ-014, OQ-2 Alert-only)
- ExecutionService
stale_pending_threshold_sec(<=0/None=비활성)._check_stale_pending(slot, now, r):age = now - slot.updated_at;age >= threshold면r.stale_pending += 1+ WARN. 상태 불변. slot.updated_at출처: mark_dispatched/transit 가 갱신 → "마지막 쓰기 이후 경과"가 곧 "pending 체류 시간" 근사.- config:
DaemonConfig.pending_stale_threshold_sec(envTREX_PENDING_STALE_THRESHOLD, default 0). daemon 이 ExecutionService 에 주입.
5. 포지션 정합 연계 (REQ-031)
_on_partial_cancel이 slot.qty 를 잔여 held 와 일치시켜 reconcile_positions 의 입력 정합을 유지. reconcile 코드 자체 변경 없음(연계 change-req).
6. baseline 문서 (REQ-015)
- interface-contract: FillStatus.cancelled/is_partial_cancel + state별 집행 처리 + KIS cncl_yn 매핑.
- runtime-flows: poll 단계 외부 취소(전량→롤백 / 부분→회계) + Alert-only stale 워치독 + limit_price clear 재매도.
자동 복구 성립 근거 (A-2)
is_terminal_reject가 이미cancelled포함 +_rollback이 pending_sell→holding 수행 → KIS 어댑터가 cancelled 만 정확히 반환하면 application 경로 재사용으로 복구(전량 취소 케이스는 코드 추가 없이 성립).
테스트 설계
tests/adapters/broker/test_kis_cancel.py(httpx stub): full cancel→cancelled, zero-fill non-cancel→accepted, partial+cancel→is_partial_cancel(execution_id 확인), full fill 회귀.tests/application/test_execution_cancel.py(programmable broker): 전량 취소→holding 복귀+clear, 부분 취소→원장 1건+qty 차감+cost basis 유지, stale 워치독 WARN(상태 불변), 비활성 default.