feat(safety-sell): dcy-010 BAT-014 — 영속 거부 재시도 쿨다운 (REQ-052~054)

dcy-010(safety-sell 영속 거부 재시도 백오프) 전체 SDLC: draft-req → confirm-req →
draft-batch(BAT-014 batch-lite) → run-batch-delivery → confirm-req-implemented.
2026-06-07 실 KIS LIVE SL/TP e2e에서 발견한 KIS 주문 spam 위험(영속 거부 시
매도 무한 재제출) 차단.

- REQ-052 (Integration): SafetySellService per-slot 인메모리 쿨다운
  (_in_cooldown/_attempt_sell/_prune_cooldown) — forced_close·stop-loss·take-profit
  트리거에 min_retry_interval_sec(600s) 이내 재시도 skip(가격 poll 포함) +
  max_retry_warn_count(3) 도달 시 WARNING + holding 이탈 정리. market_close 제외.
- REQ-053 (Configuration): SafetySellConfig min_retry_interval_sec/max_retry_warn_count
  + TREX_SAFETY_SELL_MIN_RETRY_INTERVAL/MAX_RETRY_WARN_COUNT env (additive)
- REQ-054 (Documentation): runtime-flows 쿨다운 flow + 가격체크 tr_id(FHKST01010100) 정정

인메모리 only(DB/스레드/인터페이스 변경 없음), default 미설정 시 기존 동작 불변.
검증: 신규 8 단위(쿨다운 6 + config 2), 전체 278 passed, ruff clean.
REQ-052~054 Implemented, BAT-014 release-candidate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 00:35:11 +09:00
co-authored by Claude Opus 4.8
parent 5eebe9c4e8
commit 565af6792c
15 changed files with 739 additions and 11 deletions
+12 -2
View File
@@ -4,7 +4,7 @@
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
- Status: draft
- Owner: project maintainers
- Last Updated (KST): 2026-06-07 (BAT-013)
- Last Updated (KST): 2026-06-08 (BAT-014)
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); 데몬/HTTP 흐름 = dcy-002 / BAT-002
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
@@ -132,7 +132,7 @@
`trex-safety-sell` worker 가 `TREX_STOP_LOSS_POLL_INTERVAL` 주기로 수행.
1. `holding` + 워치독 만료 + (`stop_loss_price` 또는 `take_profit_price` 설정) 슬롯만 대상(워치독 활성 슬롯은 KIS 호출조차 안 함).
2. `BrokerPort.get_current_price(symbol)` → KIS `inquire-price`(`TTTC8397R` 실전 / `VTTC8397R` 모의)의 `stck_prpr`. paper 는 프로그램된 가격(미설정 시 None).
2. `BrokerPort.get_current_price(symbol)` → KIS 주식현재가 시세 `inquire-price`(tr `FHKST01010100`, 실전/모의 공용, BAT-011 CHG-04 정정)의 `stck_prpr`. paper 는 프로그램된 가격(미설정 시 None).
3. `현재가 <= stop_loss_price` → 손절(`reason="stop_loss"`), `현재가 >= take_profit_price` → 익절(`reason="take_profit"`)으로 `SlotService.sell()`. 가격 미수신(None) 시 skip(오탐 방지, 다음 tick 재시도).
4. 쿼터 가드: 시간당 호출 상한 `N × (3600 / interval)` 추정이 임계 초과 시 경고 로그(REQ-038 AC-8).
@@ -144,6 +144,16 @@
2. 발동 시 **워치독·grace 무관** 모든 `holding` 슬롯에 `SlotService.sell(reason="market_close_liquidation")`. 이미 `pending_sell` 은 idempotent.
3. 미설정이면 동작하지 않음. in-memory 가드라 재시작 시 같은 날 재발동 가능(설계상 허용 — REQ-039 Notes).
## Flow: `safety-sell 재시도 쿨다운` (BAT-014 / REQ-052)
영속 거부(휴장/잔고/규제)로 동일 슬롯에 매도가 무한 재제출되는 것을 막는 per-slot 쿨다운. `forced_close`/`stop_loss`/`take_profit` 트리거에 적용(`market_close` 제외 — `_last_liquidation_date`로 하루 1회 보호).
1. `run_once` tick 시작 시 `_holding()`에 없는 슬롯의 쿨다운 상태(`_last_attempt`/`_attempt_count`)를 정리(FR-4).
2. 트리거가 `_safe_sell` 호출 직전, 슬롯이 `now - last_attempt_at < TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`(기본 600s)이면 **건너뜀**(stop-loss/take-profit 은 KIS 시세 poll 도 skip).
3. 쿨다운 만료 후 트리거 조건이 여전히 충족되면 재시도 + `last_attempt_at` 갱신 + `attempt_count` 증가(성공 여부 무관).
4. `attempt_count >= TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT`(기본 3) 도달 시 재시도 직전 운영자 `WARNING` 로그(slot_id/count/reason/last_attempt_at).
5. 트리거 조건 해소(가격 회복 등)·체결 완료(슬롯 holding 이탈) 시 재시도/카운트 증가 없음. 데몬 재시작 시 쿨다운 초기화(재시작 직후 1회 재시도 허용).
## Flow: `kill-switch 비상 정지` (BAT-012 / REQ-043/045/046)
운영자가 `trex.control.req` 로 비상 정지를 발동해 범위별 holding 을 즉시 청산하고 당일 전략을 차단한다.