Files
trex-core/docs/batches/bat-017_20260608_external-cancel-detection-stale-watchdog/design.md
T
genichinandClaude Opus 4.8 4746f74aae fix(broker): dcy-013 KIS 외부 취소 감지 + 부분 체결 회계 + stale 워치독 (BAT-017)
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>
2026-06-08 14:11:49 +09:00

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_reject True).
    • 부분 체결(0<ccld<ord) + 외부 취소 → state="partially_filled" + cancelled=True(is_terminal_reject False, is_partial_cancel True) + execution_id/fee/avg_price 운반.
    • 전량 체결 → state="filled"(취소 플래그 무시).

2. KIS poll_fill (REQ-012)

  • inquire-daily-ccld row 의 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.
  • 멱등: 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(env TREX_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.