Files
trex-core/docs/batches/bat-011_20260607_safety-sell-triggers/planning.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

6.7 KiB
Raw Blame History

BAT-011 Planning

  • Status: confirmed
  • Batch: BAT-011
  • Profile: standard

Included REQ

  • REQ-037 (Integration): forced_close_at 만기 자동 매도 루프 — holding + 워치독 만료 + forced_close_at <= now(KST) 슬롯에 SlotService.sell(). TREX_FORCED_CLOSE_CHECK_INTERVAL 주기. KIS 호출 없음(DB + in-memory watchdog 비교).
  • REQ-038 (Integration): stop-loss·take-profit KIS 현재가 polling + 트리거 — TTTC8397R(실전)/VTTC8397R(모의) stck_prpr 기준, 현재가 <= stop_loss_price(손절)/현재가 >= take_profit_price(익절) 시 sell(). TREX_STOP_LOSS_POLL_INTERVAL 주기. 쿼터 상한 N × (3600/interval) 경고.
  • REQ-039 (Integration): 장 마감 강제 청산 — TREX_MARKET_CLOSE_LIQUIDATION_TIME(HH:MM KST) 도래 시 워치독 무관 전체 holding 청산. 거래일 1회 발동 가드(last_liquidation_date).
  • REQ-040 (Interface): update_thresholds intent RPC 액션 — {action, slot_id, stop_loss_price?, take_profit_price?, forced_close_at?} 부분 갱신 + 워치독 last_seen_at 리셋 + holding-only. reply_to 시 {status: updated|ignored, slot_id} 응답.
  • REQ-042 (Documentation): baseline 3문서 정합 — interface-contract.md(update_thresholds + price 필드), runtime-flows.md(워치독 갱신·forced_close·가격 체크·장 마감 흐름 + :114 A-5 주석 갱신), project-structure.md(trex-safety-sell 스레드 + env 5종 + KIS polling/watchdog 모듈).

Delivery Plan

  1. REQ-038 KIS 현재가 polling 어댑터: get_current_price(symbol) (TTTC8397R/VTTC8397R, stck_prpr 파싱, 실전/모의 분기). paper 환경 stub. 어댑터 단위 테스트.
  2. REQ-037 + REQ-038 + REQ-039 통합 worker: daemon.py에 trex-safety-sell 스레드 + _safety_sell_loop() 추가. 루프 1 tick에서 (a) forced_close_at 만기, (b) stop-loss/take-profit 가격 체크, (c) 장 마감 청산 시각 평가를 순차 수행. 각 경로 SlotService.sell() 호출. mock 기반 단위 테스트(만기/손절/익절/미충족/워치독 보호/idempotency/장 마감 전체·중복가드).
  3. REQ-040 update_thresholds: SlotService.update_thresholds(...) + make_intent_rpc_handler action 라우팅 + last_seen_at 리셋 + holding-only 응답. handler 단위 테스트(부분 갱신/null 해제/non-holding ignored/last_seen_at 리셋).
  4. REQ-039 config: TREX_MARKET_CLOSE_LIQUIDATION_TIME 활용 확인(REQ-041에서 파싱 완료 — 본 batch는 소비).
  5. REQ-042 baseline 문서 3종 정합 — 구현 완료 상태 반영.
  6. 전체 회귀 green + ruff lint clean + BAT-010 잔여 R-2(실 PG 마이그레이션 round-trip) 검증.

Dependencies

  • Internal:
    • BAT-010/REQ-034: WatchdogStore.is_expired() / in_grace_period() — 트리거 루프의 워치독 만료 판정 계약(read 측).
    • BAT-010/REQ-035: stop_loss_price/take_profit_price 도메인 필드 — 가격 체크 기준값.
    • BAT-010/REQ-036: SlotService.sell(slot_id, actor, reason) — 모든 트리거의 매도 제출 경로.
    • BAT-010/REQ-041: safety-sell env vars 5종 파싱(DaemonConfig) — 본 batch가 값을 소비.
    • BAT-007/REQ-022: make_intent_rpc_handler — update_thresholds action 확장 대상.
    • BAT-004/REQ-012: KISBroker(kis.py) — 현재가 polling 어댑터 추가/확장 대상.
  • External:
    • KIS Open API 현재가 시세 endpoint(TTTC8397R/VTTC8397R). 실 호출은 운영(broker=kis) 환경; 테스트는 mock/stub.

Design Gate

  • Design Required Before Implementation: yes
  • Reason: 본 batch는 (a) **신규 daemon worker 스레드(trex-safety-sell)**를 추가하고 BAT-010의 WatchdogStore를 새로운 스레드에서 read하므로 동시성 계약(GIL 의존 read vs. snapshot RPC handler write)을 명시해야 한다. (b) 신규 외부 I/O(KIS 현재가 polling) 어댑터의 배치(kis.py 확장 vs. 신규 kis_price.py), 실전/모의 endpoint 분기, paper stub 전략, 쿼터 상한 산정을 설계에서 확정해야 한다. (c) forced_close·가격 체크·장 마감을 단일 통합 루프로 묶는 구조와 장 마감 1회 발동 가드(last_liquidation_date) 상태 모델을 설계에서 고정해야 구현·검증 drift가 없다. → design.md를 동일 턴에 scaffold한다.

Milestones

  1. M1 — REQ-038 KIS 현재가 polling 어댑터 + 단위 테스트 green (실전/모의 분기, stck_prpr 파싱)
  2. M2 — trex-safety-sell worker + forced_close_at 만기 루프(REQ-037) + 단위 테스트 green
  3. M3 — stop-loss·take-profit 가격 체크(REQ-038) worker 통합 + mock 단위 테스트 green(손절/익절/미충족/워치독 보호/쿼터 산정)
  4. M4 — 장 마감 강제 청산(REQ-039) + 거래일 1회 가드 + 단위 테스트 green
  5. M5 — update_thresholds RPC(REQ-040) + handler 단위 테스트 green
  6. M6 — baseline 3문서 정합(REQ-042) + 전체 회귀 + ruff clean + BAT-010 R-2 검증

Risks

  • R-1 (Medium): KIS 현재가 polling 쿼터 — 워치독 만료 슬롯 N개 × (3600/TREX_STOP_LOSS_POLL_INTERVAL) 호출이 KIS 분당/일일 쿼터를 초과할 수 있음. 대응: REQ-038 AC-8 상한 산정 + 임계 초과 시 경고 로그. 폴링 대상은 "워치독 만료" 슬롯으로 한정해 평시 호출 최소화.
  • R-2 (Medium): WatchdogStore cross-thread read — safety-sell worker가 snapshot RPC handler thread의 write와 동시 접근. 대응: BAT-010 Key Decision 3(GIL 보장 단일 dict, lock 없음) 계약을 그대로 따르고 read-only 판정만 수행. 단위 테스트로 만료 판정 고정.
  • R-3 (Medium): 장 마감 청산 발동 정밀도/중복 — worker 루프 주기에 의존하는 시각 평가 + 재시작 시 in-memory last_liquidation_date 초기화로 동일 거래일 재발동 위험. 대응: 발동 가드 + (필요 시) 발동 시각 경계 처리 설계에서 확정.
  • R-4 (Low): KIS 거부/네트워크 예외 시 루프 안정성 — polling 또는 sell 제출 중 예외가 worker 스레드를 죽이면 안 됨. 대응: tick 단위 try/except + 로그 후 다음 tick 지속(REQ-024 degrade 정책 정합).
  • R-5 (Low, 이월): BAT-010 잔여 R-2 — 004_add_price_thresholds.sql 실 PostgreSQL round-trip 미검증. 대응: 본 batch verification에서 실 PG 적용 + round-trip 확인.

Notes

  • safety-sell worker는 REQ-037 Notes의 권고(forced_close·stop-loss/take-profit·장 마감을 하나의 통합 루프로 처리)를 따른다. 루프 주기와 가격 체크 주기가 다를 경우(TREX_FORCED_CLOSE_CHECK_INTERVAL vs TREX_STOP_LOSS_POLL_INTERVAL) tick 내 sub-scheduling 방식은 design에서 확정한다.
  • REQ-040 OQ-6(non-holding 슬롯 처리)는 REQ-040 AC-6에서 {status: ignored, reason: "not_holding"} 응답으로 이미 결정됨 — 구현은 이를 따른다.