docs(discovery): dcy-010 OQ-3 Answered — 단일 per-slot 쿨다운 확정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 17:21:16 +09:00
co-authored by Claude Sonnet 4.6
parent cc063afdb9
commit b0539e752c
@@ -4,7 +4,7 @@
- 문서 ID: dcy-010_20260607_safety-sell-persistent-rejection-backoff
- 이슈명: safety-sell 영속 거부 재시도 백오프 — 장 휴장·잔고 부족·규제 등 지속 거부 상황에서 KIS 주문 spam 방지를 위한 슬롯별 재시도 backoff/cooldown 정책
- 작성 시각(KST): 2026-06-07
- 마지막 갱신 시각(KST): 2026-06-07
- 마지막 갱신 시각(KST): 2026-06-07 (OQ-1 Answered: min_retry_interval_sec = 600s, OQ-2 Answered: max_retry_warn_count = 3 로그만 발행, OQ-3 Answered: 단일 per-slot 쿨다운)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: 없음
@@ -59,7 +59,7 @@
# 4. 이번에 정의할 변경
- **`SafetySellService` per-slot 쿨다운 추적 도입**: `_last_attempt: dict[UUID, datetime]`와 `_attempt_count: dict[UUID, int]`를 추가한다. `_safe_sell()` 호출 직전 쿨다운 체크를 수행하고, 시도 후 `last_attempt`와 `attempt_count`를 갱신한다. 슬롯이 CLOSED 또는 IDLE로 전환되면(= `_holding()`에서 사라지면) 해당 슬롯의 쿨다운 상태를 자동 정리한다.
- **`SafetySellConfig` 확장**: `min_retry_interval_sec`(기본값 TBD), `max_retry_warn_count`(기본값 TBD) 두 env var을 추가한다. 기존 5종 env var(`TREX_WATCHDOG_TIMEOUT` 등)과 같은 방식으로 optional 파싱한다.
- **`SafetySellConfig` 확장**: `min_retry_interval_sec`(기본값 `600`, OQ-1 Answered), `max_retry_warn_count`(기본값 `3`, OQ-2 Answered) 두 env var을 추가한다. 기존 5종 env var(`TREX_WATCHDOG_TIMEOUT` 등)과 같은 방식으로 optional 파싱한다.
- **운영자 경고 로그**: `attempt_count >= max_retry_warn_count`인 슬롯에 대해 `_log.warning("safety-sell repeated rejection: slot=%s count=%d reason=persistent_rejection", ...)` 경고를 발행한다. 이 경고는 쿨다운 체크 통과 후 재시도 직전에 발행한다.
## 영향 범위
@@ -81,7 +81,7 @@
- FR-2: 쿨다운 만료(`now - last_attempt_at >= min_retry_interval_sec`) 후 트리거 조건이 여전히 충족되면 재시도한다. 재시도 성공 여부와 무관하게 `last_attempt_at`을 갱신한다.
- FR-3: `attempt_count >= max_retry_warn_count`인 슬롯에 대해 재시도 직전에 `WARNING` 로그를 발행한다. 로그에는 `slot_id`, `attempt_count`, `reason`, `last_attempt_at`을 포함한다.
- FR-4: 슬롯이 HOLDING 상태를 벗어나 `_holding()` 목록에서 사라지면(CLOSED, IDLE 등) 해당 슬롯의 `last_attempt_at`과 `attempt_count` 항목을 제거한다.
- FR-5: `SafetySellConfig`에 `min_retry_interval_sec`(`TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`, optional)과 `max_retry_warn_count`(`TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT`, optional) 두 env var를 추가한다. 미설정 시 기본값으로 동작한다.
- FR-5: `SafetySellConfig`에 `min_retry_interval_sec`(`TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`, optional, 기본값 `600`)과 `max_retry_warn_count`(`TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT`, optional, 기본값 `3`) 두 env var를 추가한다. 미설정 시 기본값으로 동작한다.
- FR-6: `docs/runtime-flows.md`를 갱신해 safety-sell tick 흐름에 "쿨다운 체크" 단계를 반영한다.
## 비기능 요구사항
@@ -118,7 +118,7 @@
## 가정
- A-1: 영속 거부의 주요 원인(휴장, 잔고 부족, 규제)은 수십 분~수 시간 지속된다. 따라서 `min_retry_interval_sec` 기본값은 분 단위(예: 300s~1800s)가 적합하다.
- A-1: 영속 거부의 주요 원인(휴장, 잔고 부족, 규제)은 수십 분~수 시간 지속된다. `min_retry_interval_sec = 600s`(기본값)는 일시 거부 원인이 해소되기를 기다리는 중간값으로 적합하다(OQ-1 Answered).
- A-2: 슬롯이 쿨다운 중 HOLDING으로 돌아온 것은 이전 매도 시도가 브로커에서 거부됐음을 의미한다. 별도 피드백 채널 없이도 `_holding()` 재등장으로 암묵적 거부를 감지할 수 있다.
- A-3: market_close 트리거(`check_market_close`)는 하루 1회 `_last_liquidation_date`로 이미 중복 방지가 되어 있어 쿨다운 적용 대상에서 제외한다.
- A-4: `SafetySellService.run_once()`는 `trex-safety-sell` 단일 스레드에서만 호출되므로 쿨다운 `dict` 동시성 문제 없음.
@@ -126,17 +126,17 @@
## 오픈 질문
- OQ-1: `min_retry_interval_sec` 기본값 — 어느 정도가 적합한가? (예: 300s / 600s / 1800s)
- 상태: Open
- 처리 방안: Justin Ko가 실 KIS 운용 경험(휴장 시 평균 거부 지속 시간)을 기준으로 결정.
- 종료 조건: confirm-discovery 또는 REQ 단계에서 Justin Ko가 값을 확정하면 Answered.
- 상태: Answered
- 결정: `600s` (10분). 대부분의 일시 거부 원인(잔고 부족, 일시 규제)이 해소되기를 기다리는 중간값. 휴장 9시간 기준 최대 54회 재시도로 쿼터 소모를 수용 가능한 수준으로 제한.
- 반영 위치: FR-5 env var 기본값, A-1
- OQ-2: `max_retry_warn_count` 기본값 및 경고 이후 동작 — 경고 로그만? 아니면 N회 초과 시 쿨다운 무기한 전환 + 수동 해제?
- 상태: Open
- 처리 방안: Justin Ko가 운용 정책(로그만으로 충분한지, 수동 해제 필요한지)을 결정.
- 종료 조건: confirm-discovery에서 확정.
- 상태: Answered
- 결정: `max_retry_warn_count = 3`, 경고 로그만 발행. 무기한 차단 없음. 3회(600s × 3 = 30분 지속 거부) 시점에서 WARNING 로그 발행으로 운영자 인지. 재시도는 쿨다운 만료 후 계속 허용.
- 반영 위치: FR-3, FR-5 env var 기본값
- OQ-3: 트리거 종류별(`forced_close` vs `stop_loss/take_profit`) 쿨다운 분리 여부 — 단일 per-slot 쿨다운으로 통합할지, 트리거별로 독립 쿨다운을 유지할지?
- 상태: Open
- 처리 방안: 단일 쿨다운(단순)과 트리거별 분리(세밀 제어) 트레이드오프를 Justin Ko와 협의.
- 종료 조건: confirm-discovery에서 확정.
- 상태: Answered
- 결정: 단일 per-slot 쿨다운. 거부 원인(휴장/잔고/규제)은 트리거 종류와 무관하게 슬롯·시장 상태에 종속적이므로 트리거별 분리는 복잡도만 늘린다. `_last_attempt: dict[UUID, datetime]` 단일 구조로 구현.
- 반영 위치: FR-1, Section 4 쿨다운 추적 설계
### 오픈 질문 처리 기준
@@ -188,9 +188,9 @@
## DECIDE
- {{DECIDE_MIN_RETRY_INTERVAL}}: `min_retry_interval_sec` 기본값 결정 — 300s / 600s / 1800s 중 선택, 또는 다른 값. 실 KIS 운용에서 휴장·잔고 부족 등 영속 거부의 평균 지속 시간 기준으로 판단(OQ-1).
- {{DECIDE_MAX_RETRY_WARN_AND_ACTION}}: `max_retry_warn_count` 기본값 및 경고 이후 동작 결정 — 로그만 발행할지, N회 초과 시 쿨다운 무기한 전환 + 수동 해제 방식으로 전환할지(OQ-2).
- {{DECIDE_COOLDOWN_GRANULARITY}}: 단일 per-slot 쿨다운(simple) vs 트리거 종류별(`forced_close`/`stop_loss`/`take_profit`) 독립 쿨다운(granular) 결정(OQ-3).
- DECIDE_MIN_RETRY_INTERVAL: 확정됨. `min_retry_interval_sec = 600s` (OQ-1 Answered).
- DECIDE_MAX_RETRY_WARN_AND_ACTION: 확정됨. `max_retry_warn_count = 3`, 경고 로그만 발행 (OQ-2 Answered).
- DECIDE_COOLDOWN_GRANULARITY: 확정됨. 단일 per-slot 쿨다운 (OQ-3 Answered).
## CONFIRM
@@ -213,7 +213,7 @@
## Freeze 플래그
- Handoff Decision: 보류
- Handoff Rationale: Safety-sell 쿨다운 메커니즘 설계 완료. OQ-1(min_retry_interval_sec 기본값), OQ-2(경고 후 동작), OQ-3(쿨다운 분리 여부) 세 항목이 Open 상태로 confirm-discovery 전 Justin Ko 결정 필요.
- Handoff Rationale: Safety-sell 쿨다운 메커니즘 설계 완료. OQ-1(600s), OQ-2(3회 경고 로그), OQ-3(단일 per-slot) 모두 Answered. confirm-discovery 전 Justin Ko 최종 승인 필요.
- Ready for REQ Drafting: false
- Confirmed By: TBD
- Confirmed At (KST): TBD