Files
trex-core/docs/batches/bat-011_20260607_safety-sell-triggers/design.md
T
genichinandClaude Opus 4.8 a30efa8f3b feat(safety-sell): dcy-007 core 자율 안전 매도 — BAT-010 기반 + BAT-011 트리거
dcy-007(core 자율 안전 매도) 전체 delivery. BAT-010 기반 인프라가 미커밋
상태였고 BAT-011 트리거 레이어와 공유 파일에서 섞여 있어 한 커밋으로 묶음.

BAT-010 (REQ-034/035/036/041, release-candidate):
- WatchdogStore(application/watchdog.py) + snapshot RPC 워치독 갱신
- Slot stop_loss_price/take_profit_price 필드 + 004_add_price_thresholds.sql
- SlotService.sell() 자율 매도 내부 경로 + idempotency
- SafetySellConfig env 5종 파싱

BAT-011 (REQ-037/038/039/040/042, release-candidate):
- SafetySellService(application/safety_sell.py): forced_close 만기 / stop-loss·
  take-profit KIS polling / 장 마감 강제 청산 + trex-safety-sell 7번째 스레드
- BrokerPort.get_current_price + KIS inquire-price(TTTC8397R/VTTC8397R, stck_prpr)
  + paper stub
- SlotService.update_thresholds() + intent RPC update_thresholds 액션
  (부분 갱신/null 해제, 워치독 last_seen_at 리셋)
- baseline 3문서 정합(interface-contract/runtime-flows/project-structure)

거버넌스: REQ-034~042 Implemented 동기화, BAT-010/011 release-candidate,
dcy-007 discovery 포함.

검증: 신규 38 테스트 green, 단위 236 passed/23 skipped,
인프라 게이트(PG+RabbitMQ) 259 passed(마이그레이션 001~004 round-trip 포함),
ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:32:30 +09:00

5.8 KiB

BAT-011 Design

  • Status: confirmed
  • Batch: BAT-011

Architecture Summary

BAT-011은 BAT-010 기반 위에 자동 매도를 실제로 발동하는 트리거 레이어를 추가한다.

  1. trex-safety-sell worker 스레드 (runtime/daemon.py): 기존 6 스레드(execution/ledger/intent-rpc/snapshot-rpc/pause-rpc/snapshot-scheduler)에 7번째로 추가. _safety_sell_loop()가 단일 통합 루프에서 forced_close_at 만기(REQ-037), stop-loss·take-profit 가격 체크(REQ-038), 장 마감 청산(REQ-039)을 순차 평가한다.
  2. KIS 현재가 polling 어댑터 (adapters/broker/kis.py 확장): get_current_price(symbol) -> Decimal — TTTC8397R(실전)/VTTC8397R(모의) 시세 endpoint 호출, stck_prpr(직전 체결가) 파싱. paper 환경 stub.
  3. update_thresholds intent RPC (application/messaging_handlers.py + domain/slot/service.py): make_intent_rpc_handler에 action 라우팅 추가, SlotService.update_thresholds()로 부분 갱신 + 워치독 last_seen_at 리셋.
  4. baseline 문서 정합 (REQ-042): 구현 완료 후 3문서 갱신.

Changed Areas

  • Docs:
    • docs/interface-contract.md — update_thresholds action 계약 + intent payload stop_loss_price/take_profit_price 선택 필드 + snapshot RPC 워치독 갱신 신호 명시 (REQ-040, REQ-042)
    • docs/runtime-flows.md — 워치독 갱신·forced_close 체크·가격 체크·장 마감 청산 흐름 추가 + :114 "A-5 미구현" 주석 갱신 (REQ-042)
    • docs/project-structure.md — trex-safety-sell 스레드 + env 5종 + KIS 현재가 polling/watchdog 모듈 반영 (REQ-042)
  • Modules:
    • trex_core/runtime/daemon.py — trex-safety-sell worker 등록 + _safety_sell_loop() 구현(forced_close/가격/장 마감 통합)
    • trex_core/adapters/broker/kis.py — get_current_price(symbol) 추가(TTTC8397R/VTTC8397R, stck_prpr)
    • trex_core/application/broker_port.py — (필요 시) get_current_price 포트 시그니처 추가; paper 더블 stub 구현
    • trex_core/adapters/broker/paper.py — get_current_price stub(고정/주입 가능 현재가) — 테스트·모의용
    • trex_core/domain/slot/service.py — SlotService.update_thresholds(slot_id, stop_loss_price, take_profit_price, forced_close_at) 추가 + 트리거 조건 평가 헬퍼(또는 worker 인라인)
    • trex_core/application/messaging_handlers.py — make_intent_rpc_handler에 action: "update_thresholds" 라우팅 + holding-only 응답
  • Interfaces:
    • Broker.get_current_price(symbol: str) -> Decimal | None (신규 포트 메서드)
    • SlotService.update_thresholds(slot_id, stop_loss_price=..., take_profit_price=..., forced_close_at=...) -> str (부분 갱신, sentinel로 "미지정" vs "null 해제" 구분)
    • intent RPC action: "update_thresholds" payload/response 계약 (REQ-040)

Key Decisions (초안 — 구현 전 확정 대상)

  1. 단일 통합 worker 루프 vs. 분리 스레드: REQ-037 Notes 권고에 따라 trex-safety-sell 단일 스레드 통합 루프를 기본안으로 한다. 가격 polling 주기(TREX_STOP_LOSS_POLL_INTERVAL, 기본 30s)와 forced_close 주기(TREX_FORCED_CLOSE_CHECK_INTERVAL, 기본 60s)가 다르므로, 루프 base tick을 짧은 쪽(또는 GCD)으로 두고 각 체크에 last-run 타임스탬프로 sub-scheduling한다. (확정 필요)
  2. KIS 현재가 어댑터 배치: kis.py에 get_current_price 추가(별도 kis_price.py 분리 대신) — 토큰/세션/_account 재사용 이점. paper는 paper.py stub. (확정 필요)
  3. WatchdogStore read 동시성: BAT-010 Key Decision 3 계약 승계 — GIL 보장 단일 dict, lock 없음. worker는 is_expired()/in_grace_period() read만 수행.
  4. 장 마감 1회 발동 가드: in-memory last_liquidation_date: date | None. 재시작 시 초기화(REQ-039 Notes). 같은 거래일 재발동 방지. 발동 시각 도래 판정은 now(KST).time() >= liquidation_time and last_liquidation_date != today.
  5. update_thresholds 부분 갱신 sentinel: payload에 키가 없으면 "유지", 키가 null이면 "해제(NULL)". _UNSET sentinel로 두 경우를 구분(REQ-040 AC-4/AC-5).
  6. 루프 예외 격리: tick 단위 try/except로 polling/sell 예외가 worker 스레드를 죽이지 않게 한다. 예외는 로그 후 다음 tick 지속(REQ-024 degrade 정책 정합).

Edge Cases

  • 가격 polling 중 KIS 거부/타임아웃: 해당 슬롯 skip + 경고 로그, 다음 tick 재시도(R-4).
  • forced_close와 stop-loss가 동시 충족: 둘 다 sell() 호출하나 REQ-036 idempotency로 1회만 전이.
  • 장 마감 청산 시각이 forced_close 체크 사이에 도래: 통합 루프 tick에서 장 마감 평가가 우선 실행되도록 순서 고정.
  • update_thresholds로 forced_close_at만 갱신: 나머지 필드 유지(sentinel), last_seen_at 리셋(워치독 타이머 재시작, REQ-034).
  • non-holding 슬롯에 update_thresholds: 필드 불변 + {status: ignored, reason: "not_holding"} (REQ-040 AC-6).
  • 쿼터 상한 초과: 경고 로그만(본 batch 범위) — 동적 간격 조정은 out of scope.

Architecture Impact

  • runtime-flows: safety-sell worker 4종 흐름(워치독 갱신은 snapshot RPC 측) 추가 — REQ-042에서 문서화.
  • project-structure: 7번째 스레드 trex-safety-sell + KIS 현재가 polling 어댑터 메서드 — REQ-042에서 문서화.
  • broker_port 계약 확장(get_current_price)이 paper/kis 양 더블에 반영되어야 한다.

Reference Doc Update Plan

  • interface-contract.md: update_thresholds action + price 선택 필드 + snapshot RPC 워치독 신호 (REQ-042 AC-1/AC-2)
  • runtime-flows.md: 4종 safety-sell 흐름 + :114 A-5 주석 갱신 (REQ-042 AC-3/AC-4)
  • project-structure.md: trex-safety-sell 스레드 + env 5종 + 신규 모듈 (REQ-042 AC-5)