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
@@ -0,0 +1,30 @@
# BAT-014 Implementation
- Status: completed
- Batch: BAT-014
## Plan Summary
- REQ-053(config 2종) → REQ-052(쿨다운 로직) → REQ-054(runtime-flows). 인메모리만, DB/스레드/인터페이스 변경 없음.
## Changed Files
- 수정:
- `trex_core/runtime/config.py` — `SafetySellConfig.min_retry_interval_sec`(기본 600)·`max_retry_warn_count`(기본 3) + `from_env` 파싱(`TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`/`TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT`) (REQ-053)
- `trex_core/application/safety_sell.py` — `_last_attempt`/`_attempt_count` dict, `_in_cooldown`/`_attempt_sell`/`_prune_cooldown`, `check_forced_close`·`check_prices` 쿨다운 gate(skip→KIS poll skip 포함)·경고, `run_once` 시작 시 prune. market_close 는 미적용 (REQ-052)
- `docs/runtime-flows.md` — safety-sell 재시도 쿨다운 flow + 가격체크 tr_id(FHKST01010100) 정정 (REQ-054)
- `tests/runtime/test_safety_sell_config.py` — 쿨다운 env 기본값/파싱 2종
- `tests/application/test_safety_sell_worker.py` — TestRetryCooldown 6종(skip/만료 재시도/경고/prune/market_close 제외/트리거 해소)
## Execution Log
1. REQ-053 config 2종 + from_env → `test_safety_sell_config.py` green.
2. REQ-052 쿨다운(`_in_cooldown`/`_attempt_sell`/`_prune_cooldown` + forced_close/prices gate) → TestRetryCooldown 6 green (non-transitioning sell stub 으로 영속 거부 시뮬).
3. REQ-054 runtime-flows 쿨다운 flow 추가.
4. 전체 회귀: 단위 278 passed / 30 skipped, ruff clean.
## Validation
- `pytest tests/application/test_safety_sell_worker.py tests/runtime/test_safety_sell_config.py` → 34 passed.
- 전체 `pytest -q --deselect ...::test_sandbox_token_and_submit` → 278 passed / 30 skipped.
- `ruff check trex_core tests` → All passed.
## Remaining Risks
- R-2(재시작 시 쿨다운 초기화 → 1회 즉시 재시도): 허용(문서 명시).
- 실 운영 거부 시나리오(휴장/잔고)에서의 쿨다운 동작은 차기 거래일 실 관찰로 보강(단위는 stub 로 시뮬). 인메모리·additive 변경이라 release 위험 낮음.
@@ -0,0 +1,27 @@
# BAT-014: safety-sell 영속 거부 재시도 쿨다운 (per-slot cooldown + 경고 + env)
- Status: release-candidate
- Profile: batch-lite
- Source Discovery: dcy-010
- Included REQ: REQ-052, REQ-053, REQ-054
## Summary
- dcy-010(safety-sell 영속 거부 재시도 백오프)를 batch-lite 로 delivery 한다. `SafetySellService`에 per-slot 인메모리 쿨다운(마지막 시도 시각·시도 횟수)을 추가해 영속 거부(휴장/잔고/규제) 시 `min_retry_interval_sec`(기본 600s) 이내 재시도를 건너뛰고, `max_retry_warn_count`(기본 3) 도달 시 운영자 WARNING 을 발행한다. `SafetySellConfig` env 2종 + runtime-flows 정합. **DB/스레드/인터페이스 변경 없음**(인메모리만).
## Scope
- In Scope:
- `SafetySellService` per-slot 쿨다운 추적(`_last_attempt`/`_attempt_count`) + skip/만료 재시도 + 경고 로그 + holding 이탈 정리, market_close 제외 (REQ-052)
- `SafetySellConfig` `min_retry_interval_sec`/`max_retry_warn_count` env 2종 (REQ-053)
- `docs/runtime-flows.md` safety-sell tick 쿨다운 단계 반영 (REQ-054)
- Out of Scope:
- ExecutionService→SafetySellService 거부 사유 피드백 채널, 거부 원인별 차등 쿨다운, 쿨다운 DB 영속, 수동 해제 API, market_close 별도 쿨다운.
## Documents
- Planning: [planning.md](./planning.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- Profile **batch-lite**: 소규모(단일 service + config 2종 + 문서), 인메모리만, 구조/인터페이스/DB 변경 없음, 설계가 dcy-010 §4 에 완비 → **Design Gate: no**(design.md 불요). implementation/verification 은 해당 stage 진입 시 생성.
- OQ-1(600s)/OQ-2(3, 경고만)/OQ-3(단일 per-slot) Answered. NFR-1(인메모리)/NFR-2(무락)/NFR-4(paper reject 시뮬 테스트) 는 REQ-052 AC 로 흡수.
- 발견 근거: REL-003 Feedback / 2026-06-07 실 KIS LIVE SL/TP e2e.
@@ -0,0 +1,38 @@
# BAT-014 Planning
- Status: confirmed
- Batch: BAT-014
- Profile: batch-lite
## Included REQ
- REQ-052 (Integration): `SafetySellService` per-slot 쿨다운(`_last_attempt`/`_attempt_count`) + `_safe_sell` 전 skip / 만료 재시도 + 경고 로그 + holding 이탈 정리. market_close 제외.
- REQ-053 (Configuration): `SafetySellConfig.min_retry_interval_sec`(env `TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`, 기본 600) + `max_retry_warn_count`(env `TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT`, 기본 3).
- REQ-054 (Documentation): `docs/runtime-flows.md` safety-sell tick 쿨다운 단계.
## Delivery Plan
1. REQ-053: `SafetySellConfig` 필드 2종 + `from_env` 파싱 → `test_safety_sell_config.py` 기본값/파싱.
2. REQ-052: `SafetySellService.__init__` dict 2종 + `check_forced_close`/`check_prices` 쿨다운 gate(skip/update) + `attempt_count` 경고 + `run_once` holding 이탈 정리. SafetySellService 가 config 의 두 값을 사용. paper `reject_symbols` 시뮬 단위 테스트(skip/만료 재시도/경고/정리/market_close 제외/트리거 해소).
3. REQ-054: runtime-flows 갱신.
4. 전체 회귀 + ruff.
## Dependencies
- Internal: dcy-007/BAT-011 `SafetySellService`(변경 대상) + `SafetySellConfig`(REQ-041). paper `reject_symbols`(REQ-011/BAT-009) — 거부 시뮬.
- External: 없음.
## Design Gate
- Design Required Before Implementation: **no**
- Reason: 변경이 단일 service 인메모리 dict 추가 + config 2 필드 + 문서로 국소적이고, 구조/인터페이스/DB/스레드 영향이 없다(추가 스레드·스키마 없음, 단일 스레드 호출이라 무락). 쿨다운 알고리즘·기본값·트리거 적용 범위가 dcy-010 §4/§6(OQ-1/2/3 Answered)에 완비되어 설계 모호성이 없다 → batch-lite, design.md 생략.
## Milestones
1. M1 — REQ-053 config 2종 + 단위 green
2. M2 — REQ-052 쿨다운 로직 + paper reject 시뮬 단위 green
3. M3 — REQ-054 runtime-flows
4. M4 — 전체 회귀 + ruff
## Risks
- R-1 (Low): 쿨다운 중 트리거 해소 시 불필요 경고 — holding 이탈 정리(FR-4) + 조건 해소 시 `_safe_sell` 미호출로 완화.
- R-2 (Low): 재시작 시 쿨다운 초기화 → 1회 즉시 재시도 — 허용(문서 주의).
- R-3 (Low): paper 기본은 거부 안 함 → `reject_symbols` 시뮬로 테스트 커버.
## Notes
- batch-lite: design 생략(Design Gate no). implementation.md/verification.md 는 구현/검증 stage 진입 시 생성.
@@ -0,0 +1,39 @@
# BAT-014 Verification
- Status: verified
- Batch: BAT-014
## Acceptance Mapping
### REQ-052 (per-slot 쿨다운 + 경고)
| AC | Evidence |
|----|---------|
| AC-1: 쿨다운 이내 재시도 skip (S-1) | `test_safety_sell_worker.py::TestRetryCooldown::test_skips_retry_within_interval` (1회만) |
| AC-2: 쿨다운 만료 후 재시도 (S-2) | `test_retries_after_cooldown_expiry` (+601s → 2회) |
| AC-3: max_retry_warn_count 경고 (S-3) | `test_warning_after_max_count` (caplog WARNING + slot_id) |
| AC-4: holding 이탈 시 정리 (S-4) | `test_prune_on_non_holding` (`_last_attempt`/`_attempt_count` 제거) |
| AC-5: market_close 미적용 | `test_market_close_not_gated_by_cooldown` (쿨다운 중에도 청산) |
| AC-6: 트리거 해소 시 미증가 (R-1) | `test_trigger_resolved_no_increment` (sell 없음, count 없음) |
### REQ-053 (쿨다운 env 2종)
| AC | Evidence |
|----|---------|
| AC-1: 기본값 600/3 | `test_safety_sell_config.py::test_safety_sell_config_defaults` |
| AC-2: env 파싱(300/5) | `test_retry_cooldown_env_tuning` |
| AC-3: 미설정 회귀 없음 | 전체 278 passed (기존 safety-sell 동작 불변) |
### REQ-054 (runtime-flows 정합)
| AC | Evidence |
|----|---------|
| AC-1: 쿨다운 flow(skip/재시도/경고/정리) | `docs/runtime-flows.md` "safety-sell 재시도 쿨다운" flow |
| AC-2: market_close 제외 명시 | 동일 flow 첫 문단 + 장 마감 flow |
## Evidence
- Test: 신규 8종(쿨다운 6 + config 2) green. 전체 단위 **278 passed / 30 skipped**, `ruff` All passed.
- Manual: 없음(인메모리 로직 단위 커버; 영속 거부는 non-transitioning sell stub 로 시뮬 — dcy-010 NFR-4 의도).
- Logs: `34 passed`(대상 파일) / `278 passed`(전체).
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. 인메모리·additive(DB/스레드/인터페이스 변경 없음), default 미설정 시 기존 동작 불변. 실 운영 거부 시나리오 관찰은 차기 거래일 보강(비차단).
+1
View File
@@ -21,3 +21,4 @@
| BAT-011 | standard | release-candidate | REQ-037, REQ-038, REQ-039, REQ-040, REQ-042 | dcy-007 | [bat-011](./bat-011_20260607_safety-sell-triggers/) |
| BAT-012 | standard | release-candidate | REQ-043, REQ-044, REQ-045, REQ-046, REQ-047 | dcy-008 | [bat-012](./bat-012_20260607_kill-switch-control/) |
| BAT-013 | standard | release-candidate | REQ-048, REQ-049, REQ-050, REQ-051 | dcy-009 | [bat-013](./bat-013_20260607_multi-instance-topology/) |
| BAT-014 | batch-lite | release-candidate | REQ-052, REQ-053, REQ-054 | dcy-010 | [bat-014](./bat-014_20260608_safety-sell-cooldown/) |
@@ -4,10 +4,10 @@
- 문서 ID: dcy-010_20260607_safety-sell-persistent-rejection-backoff
- 이슈명: safety-sell 영속 거부 재시도 백오프 — 장 휴장·잔고 부족·규제 등 지속 거부 상황에서 KIS 주문 spam 방지를 위한 슬롯별 재시도 backoff/cooldown 정책
- 작성 시각(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 쿨다운, confirmed)
- 마지막 갱신 시각(KST): 2026-06-08 (draft-req: REQ-052~054 생성)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: 없음
- 생성된 REQ 참조: REQ-052, REQ-053, REQ-054
# 1. 계획 상태 요약
+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 을 즉시 청산하고 당일 전략을 차단한다.
@@ -0,0 +1,141 @@
# req-053: safety-sell 재시도 쿨다운 env 설정 2종
- Status: Implemented
- Type: Configuration
- Priority: Medium
- Owner: project maintainers
## Intent
safety-sell per-slot 쿨다운(REQ-052)의 튜닝 값을 운영 설정으로 노출한다. `SafetySellConfig`에 `min_retry_interval_sec`와 `max_retry_warn_count` 두 필드를 추가하고, 기존 safety-sell env 5종과 동일 방식으로 optional 파싱한다(dcy-010 FR-5).
## Requirement
- `SafetySellConfig`(`trex_core/runtime/config.py`)에 두 필드를 추가한다:
- `min_retry_interval_sec: int` — env `TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`, 기본값 `600`(OQ-1 Answered).
- `max_retry_warn_count: int` — env `TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT`, 기본값 `3`(OQ-2 Answered).
- `DaemonConfig.from_env()`에서 기존 safety-sell env(`TREX_WATCHDOG_TIMEOUT` 등)와 동일 패턴으로 파싱한다. 미설정 시 기본값으로 동작(회귀 없음).
- 이 값들은 `SafetySellService`(REQ-052)에 주입되어 쿨다운/경고 임계로 사용된다.
## Acceptance Criteria
1. Given `TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`/`TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT` 미설정, When `DaemonConfig.from_env()`, Then `min_retry_interval_sec=600`, `max_retry_warn_count=3` 기본값으로 구성된다.
2. Given 두 env 가 각각 `300`/`5` 로 설정, When `from_env()`, Then `SafetySellConfig`에 `300`/`5`로 파싱된다.
3. Given 두 env 미설정 + 기존 safety-sell 동작, When 전체 회귀, Then 회귀 없음(기존 5종 env 동작 불변).
## Impacted Area
- Docs:
- `docs/runtime-flows.md`/`docs/project-structure.md` — env 목록(REQ-054 / 또는 본 REQ Notes)
- Modules:
- `trex_core/runtime/config.py` — `SafetySellConfig` 필드 2종 + `from_env` 파싱
- Tests:
- `tests/runtime/test_safety_sell_config.py` — 기본값 + 파싱
## Notes
- 기존 SafetySellConfig env 5종(REQ-041)과 동일 패턴 확장. additive·optional.
- 값의 의미·동작은 REQ-052(쿨다운 로직)에서 정의. 본 REQ 는 설정 표면만.
## Change Log
### CHG-20260608-01
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- dcy-010(confirmed) → REQ-053 초안 생성.
#### Intent
- safety-sell 쿨다운 env 2종(FR-5)을 Configuration REQ 로 정규화.
#### Acceptance Criteria Delta
- Added: AC-1~3 전체
#### Impacted Area Delta
- Docs: runtime-flows.md/project-structure.md (REQ-054)
- Modules: config.py
- Tests: test_safety_sell_config.py
#### Delivery Trace Delta
- Batches: none (미할당)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: confirm-req 로 승인.
### CHG-20260608-02
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- Proposed → Approved (confirm-req).
#### Intent
- 승인 게이트 통과: 필수 필드·구체 AC·Change Log 완비. OQ-1(600s)/OQ-2(3) 기본값 확정. 기존 SafetySellConfig env 패턴 확장(additive, 회귀 없음).
#### Acceptance Criteria Delta
- (변경 없음)
#### Impacted Area Delta
- (변경 없음)
#### Delivery Trace Delta
- Batches: none (미할당)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: draft-batch 로 delivery 편성(batch-lite).
### CHG-20260608-03
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- Status Approved → Implemented (confirm-req-implemented, 근거 batch BAT-014 release-candidate).
#### Intent
- BAT-014 verification: `SafetySellConfig` min_retry_interval_sec/max_retry_warn_count 기본값(600/3) + env 파싱(300/5) 단위 green, 미설정 회귀 없음(278 passed). AC-1~3 충족.
#### Acceptance Criteria Delta
- (변경 없음)
#### Impacted Area Delta
- (변경 없음)
#### Delivery Trace Delta
- Batches: BAT-014 (release-candidate)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current (Implemented)
- Follow-up Action: draft-release 로 BAT-014 배포 편성.
@@ -0,0 +1,137 @@
# req-054: safety-sell 쿨다운 runtime-flows 정합
- Status: Implemented
- Type: Documentation
- Priority: Low
- Owner: project maintainers
## Intent
dcy-010 safety-sell 재시도 쿨다운(REQ-052/053) 구현 후 `docs/runtime-flows.md`의 safety-sell tick 흐름에 "쿨다운 체크 → 건너뜀/재시도" 단계를 반영한다(dcy-010 FR-6).
## Requirement
- `docs/runtime-flows.md`의 safety-sell 관련 흐름(forced_close / stop-loss·take-profit)에 per-slot 쿨다운 단계를 추가한다:
- `_safe_sell` 호출 전 `now - last_attempt_at < min_retry_interval_sec` 이면 건너뜀
- 쿨다운 만료 후 트리거 조건 유지 시 재시도 + `attempt_count` 증가
- `attempt_count >= max_retry_warn_count` 시 WARNING 로그
- 슬롯 holding 이탈 시 쿨다운 상태 정리
- market_close 는 쿨다운 제외(하루 1회 가드)
- (선택) `docs/project-structure.md` config.py 설명에 safety-sell env 2종 추가 반영.
## Acceptance Criteria
1. Given `docs/runtime-flows.md`의 stop-loss·take-profit / forced_close 흐름, When 읽으면, Then 쿨다운 체크(건너뜀/재시도)·경고 로그·holding 이탈 정리 단계가 서술되어 있다.
2. Given `docs/runtime-flows.md`, When market_close 흐름을 읽으면, Then 쿨다운 제외(하루 1회 가드)가 명시되어 있다.
## Impacted Area
- Docs:
- `docs/runtime-flows.md` (필수), `docs/project-structure.md`(선택)
- Modules: 없음
- Tests: 없음
## Notes
- REQ-052/053 구현 완료 후 batch verification 단계에서 수행(REQ-042/047/051 선례).
## Change Log
### CHG-20260608-01
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- dcy-010(confirmed) → REQ-054 초안 생성.
#### Intent
- safety-sell 쿨다운 runtime-flows 정합(FR-6)을 Documentation REQ 로 정규화.
#### Acceptance Criteria Delta
- Added: AC-1~2 전체
#### Impacted Area Delta
- Docs: runtime-flows.md, project-structure.md(선택)
#### Delivery Trace Delta
- Batches: none (미할당)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: confirm-req 로 승인 (REQ-052/053 구현 batch 이후 연계).
### CHG-20260608-02
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- Proposed → Approved (confirm-req).
#### Intent
- 승인 게이트 통과: 필수 필드·AC·Change Log 완비. baseline 정합 문서 REQ로 REQ-052/053 구현 후 verification 단계 수행.
#### Acceptance Criteria Delta
- (변경 없음)
#### Impacted Area Delta
- (변경 없음)
#### Delivery Trace Delta
- Batches: none (미할당)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: draft-batch 로 delivery 편성(batch-lite, REQ-052/053 이후 연계).
### CHG-20260608-03
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- Status Approved → Implemented (confirm-req-implemented, 근거 batch BAT-014 release-candidate).
#### Intent
- BAT-014 에서 `docs/runtime-flows.md` 에 "safety-sell 재시도 쿨다운" flow 추가(skip/재시도/경고/정리 + market_close 제외) + 가격체크 tr_id(FHKST01010100) 정정. AC-1~2 충족.
#### Acceptance Criteria Delta
- (변경 없음)
#### Impacted Area Delta
- (변경 없음)
#### Delivery Trace Delta
- Batches: BAT-014 (release-candidate)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current (Implemented)
- Follow-up Action: draft-release 로 BAT-014 배포 편성.
@@ -0,0 +1,147 @@
# req-052: safety-sell per-slot 재시도 쿨다운 + 영속 거부 경고
- Status: Implemented
- Type: Integration
- Priority: High
- Owner: project maintainers
## Intent
`SafetySellService`가 브로커 영속 거부(휴장/잔고부족/규제) 상황에서 동일 슬롯에 매도를 무한 재제출하는 문제(2026-06-07 실 KIS LIVE SL/TP e2e 발견, REL-003 Feedback)를 막는다. 슬롯별 마지막 시도 시각·시도 횟수를 인메모리로 추적해 `min_retry_interval_sec` 이내 재시도를 건너뛰고, 반복 거부 시 운영자 경고를 발행한다. 추가 스레드·DB 스키마 변경 없음(dcy-010 FR-1~4, NFR-1/2/4).
## Requirement
- `SafetySellService`는 슬롯별 `last_attempt_at: dict[UUID, datetime]`, `attempt_count: dict[UUID, int]`를 인메모리로 추적한다.
- 각 트리거(`forced_close`/`stop_loss`/`take_profit`)가 `_safe_sell()`을 호출하기 전, 해당 슬롯이 `now - last_attempt_at < min_retry_interval_sec`이면 **건너뛴다**(재시도 안 함). market_close 는 `_last_liquidation_date`로 이미 하루 1회 보호되므로 쿨다운 대상에서 제외(dcy-010 A-3).
- 쿨다운 만료(`>= min_retry_interval_sec`) 후 트리거 조건이 여전히 충족되면 재시도하고, 성공 여부와 무관하게 `last_attempt_at`을 갱신하고 `attempt_count`를 증가시킨다.
- `attempt_count >= max_retry_warn_count`인 슬롯은 재시도 직전 `WARNING` 로그를 발행한다(포함: `slot_id`, `attempt_count`, `reason`, `last_attempt_at`).
- 슬롯이 `_holding()` 목록에서 사라지면(CLOSED/IDLE 등) 해당 슬롯의 `last_attempt_at`/`attempt_count` 항목을 제거한다(상태 누수 방지).
- `min_retry_interval_sec`/`max_retry_warn_count`는 `SafetySellConfig`(REQ-053)에서 주입받는다. 단일 `trex-safety-sell` 스레드 호출이라 별도 락 없음.
## Acceptance Criteria
1. Given paper 브로커 `reject_symbols`로 거부 시뮬레이션 + stop-loss 트리거 슬롯, When `run_once()`를 쿨다운 간격 이내에 N회 호출, Then `_safe_sell`(또는 `SlotService.sell`) 재호출이 첫 1회 이후 발생하지 않는다(S-1).
2. Given 슬롯 `last_attempt_at`을 `min_retry_interval_sec` 이전 과거로 설정 + 트리거 조건 유지, When `run_once()`/`check_*` 호출, Then 재시도 1회 실행 + `last_attempt_at` 갱신(S-2).
3. Given `attempt_count = max_retry_warn_count - 1` 상태에서 다음 재시도, When 재시도 직전, Then `WARNING` 로그가 발행되고 `slot_id`/`attempt_count`/`reason` 포함(S-3).
4. Given 슬롯이 CLOSED 로 전환되어 `_holding()`에서 사라짐, When 다음 `run_once()`, Then `last_attempt_at`/`attempt_count`에서 해당 슬롯 항목이 제거된다(S-4).
5. Given market_close 트리거, When 쿨다운 로직 평가, Then market_close 는 쿨다운 적용 대상이 아니다(하루 1회 가드 유지).
6. Given 가격 회복 등으로 트리거 조건이 해소된 슬롯, When `check_prices()`, Then `_safe_sell` 미호출 + `attempt_count` 미증가(R-1).
## Impacted Area
- Docs:
- `docs/runtime-flows.md` — safety-sell tick 에 쿨다운 체크 단계 (REQ-054 처리)
- Modules:
- `trex_core/application/safety_sell.py` — `__init__`(`_last_attempt`/`_attempt_count`), `check_forced_close`/`check_prices`(쿨다운 check/update), `run_once`(holding 이탈 슬롯 정리), 경고 로그
- Tests:
- `tests/application/test_safety_sell_worker.py`(또는 신규) — 쿨다운 skip/만료 재시도/경고 로그/정리/market_close 제외/트리거 해소(paper reject_symbols 시뮬레이션)
## Notes
- dcy-010 OQ-3 Answered: 단일 per-slot 쿨다운(트리거별 분리 안 함).
- ExecutionService→SafetySellService 거부 사유 피드백 채널은 Out of Scope — `_holding()` 재등장으로 암묵적 거부 감지(A-2).
- 쿨다운 상태는 재시작 시 초기화(허용, R-2) — DB 영속 Out of Scope.
- NFR-1(인메모리)/NFR-2(무락)/NFR-4(paper reject 시뮬 테스트)는 본 REQ AC 로 흡수.
## Change Log
### CHG-20260608-01
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- dcy-010(confirmed) → REQ-052 초안 생성.
#### Intent
- safety-sell 영속 거부 per-slot 쿨다운 + 경고(FR-1~4)를 Integration REQ 로 정규화.
#### Acceptance Criteria Delta
- Added: AC-1~6 전체
#### Impacted Area Delta
- Docs: runtime-flows.md (REQ-054)
- Modules: safety_sell.py
- Tests: test_safety_sell_worker.py(쿨다운)
#### Delivery Trace Delta
- Batches: none (미할당)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: confirm-req 로 승인.
### CHG-20260608-02
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- Proposed → Approved (confirm-req).
#### Intent
- 승인 게이트 통과: 필수 필드·구체 AC(1~6)·Impacted Area·Change Log 완비. dcy-010(confirmed) FR-1~4/OQ 와 모순 없음. ExecutionService 피드백 채널·DB 영속은 Out of Scope(비핵심).
#### Acceptance Criteria Delta
- (변경 없음)
#### Impacted Area Delta
- (변경 없음)
#### Delivery Trace Delta
- Batches: none (미할당)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: draft-batch 로 delivery 편성(batch-lite).
### CHG-20260608-03
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- Status Approved → Implemented (confirm-req-implemented, 근거 batch BAT-014 release-candidate).
#### Intent
- BAT-014 verification 의 AC↔evidence(쿨다운 6 단위: skip/만료 재시도/경고/prune/market_close 제외/트리거 해소)로 AC-1~6 충족. 단위 278 passed, ruff clean.
#### Acceptance Criteria Delta
- (변경 없음)
#### Impacted Area Delta
- (변경 없음)
#### Delivery Trace Delta
- Batches: BAT-014 (release-candidate)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current (Implemented)
- Follow-up Action: draft-release 로 BAT-014 배포 편성. 실 운영 거부 시나리오는 차기 거래일 관찰.
+7 -4
View File
@@ -74,7 +74,7 @@
## Next Requirement ID
- Current: `REQ-052`
- Current: `REQ-055`
- Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다.
## Requirement Register
@@ -132,11 +132,14 @@
| REQ-049 | instance_key 기반 동적 큐명 + default 하위호환 | Interface | Implemented | High | project maintainers | [req-049](./Interface/req-049_instance-key-dynamic-queue-naming.md) |
| REQ-050 | trex.direct(topic) + trex.broadcast(fanout) exchange + 바인딩 | Integration | Implemented | High | project maintainers | [req-050](./Integration/req-050_direct-broadcast-exchanges-and-binding.md) |
| REQ-051 | multi-instance topology baseline 문서 정합 | Documentation | Implemented | Medium | project maintainers | [req-051](./Documentation/req-051_multi-instance-baseline-alignment.md) |
| REQ-052 | safety-sell per-slot 재시도 쿨다운 + 영속 거부 경고 | Integration | Implemented | High | project maintainers | [req-052](./Integration/req-052_safety-sell-per-slot-cooldown.md) |
| REQ-053 | safety-sell 재시도 쿨다운 env 설정 2종 | Configuration | Implemented | Medium | project maintainers | [req-053](./Configuration/req-053_safety-sell-retry-cooldown-env.md) |
| REQ-054 | safety-sell 쿨다운 runtime-flows 정합 | Documentation | Implemented | Low | project maintainers | [req-054](./Documentation/req-054_safety-sell-cooldown-baseline-alignment.md) |
## Recent Change Log Summary
- 2026-06-08 :: REQ-052~054 Approved → Implemented (confirm-req-implemented): BAT-014(batch-lite) verify 완료(신규 8 단위, 전체 278 passed, ruff clean). SafetySellService per-slot 쿨다운(skip/만료재시도/경고/prune, market_close 제외), SafetySellConfig env 2종(min_retry_interval_sec 600/max_retry_warn_count 3), runtime-flows 정합. (confirm-req-implemented)
- 2026-06-08 :: REQ-052~054 Proposed → Approved (confirm-req): dcy-010 safety-sell 쿨다운 3개 전원 승인 게이트 통과. dcy-010(confirmed) FR/OQ 와 모순 없음, 필수 필드·구체 AC 완비. 피드백 채널·DB 영속은 Out of Scope 비핵심. (confirm-req)
- 2026-06-08 :: REQ-052~054 생성 (draft-req): dcy-010(safety-sell 영속 거부 재시도 백오프, confirmed) → 3개 REQ 초안. per-slot 쿨다운+경고(052), 쿨다운 env 2종(053), runtime-flows 정합(054). OQ-1(600s)/OQ-2(3회 경고)/OQ-3(단일 per-slot) Answered. (draft-req)
- 2026-06-07 :: REQ-048~051 Approved → Implemented (confirm-req-implemented): BAT-013 verify 완료(신규 9 단위 + RabbitMQ 라우팅 2, 단위 271 passed, 인프라 게이트 301 passed(directed 격리 S-2 + broadcast dual S-3 + default 무회귀 S-4), ruff clean). per-instance TOML 로더+`--config`, instance_key 동적 큐명(default 리터럴 하위호환), trex.direct/trex.broadcast exchange+바인딩, baseline 3문서 정합. (confirm-req-implemented)
- 2026-06-07 :: REQ-048~051 Proposed → Approved (confirm-req): dcy-009 multi-instance 4개 전원 승인 게이트 통과. dcy-009(confirmed) FR/OQ 와 모순 없음, 필수 필드·구체 AC 완비. strategy publish 변경·TOML 추가필드는 비핵심 후속. (confirm-req)
- 2026-06-07 :: REQ-048~051 생성 (draft-req): dcy-009(다중 인스턴스 RabbitMQ 토폴로지, confirmed) → 4개 REQ 초안. per-instance TOML 로더+`--config` 필수(048), instance_key 동적 큐명+default 하위호환(049), trex.direct/trex.broadcast exchange+바인딩(050), baseline 정합(051). NFR-1/2/3/4 는 048/049 AC 로 흡수. (draft-req)
- 2026-06-07 :: REQ-043~047 Approved → Implemented (confirm-req-implemented): BAT-012 verify 완료(신규 26 단위 + killswitch PG 5, 단위 262 passed, 인프라 게이트 290 passed(마이그레이션 005 round-trip), ruff clean, paper 데몬 control RPC e2e PASS). `trex.control.req` dispatch, KillSwitchService(범위별 청산+이력), killswitch_exclusion store/005, day-scoped masking+resume, baseline 3문서 정합. (confirm-req-implemented)
- 2026-06-07 :: REQ-043~047 생성 (draft-req): dcy-008(비상 정지/kill-switch, confirmed) → 5개 REQ 초안. 제어 채널 `trex.control.req`+action dispatch(043), day-scoped 제외 store+마이그레이션(044), kill-switch 실행=범위별 holding 순차 sell()+이력(045), 당일 제외 masking+자동복귀+resume_kill_switch(046), baseline 정합(047). NFR-1/2/3 은 045/046 AC 로 흡수, OQ-3(pending_buy) REQ Deferred. (draft-req)