From 3a2020012d43cfa30f58eb5c1b38feeb92b28274 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Sun, 7 Jun 2026 15:54:30 +0900 Subject: [PATCH] =?UTF-8?q?docs(discovery):=20dcy-010=20safety-sell=20?= =?UTF-8?q?=EC=98=81=EC=86=8D=20=EA=B1=B0=EB=B6=80=20=EC=9E=AC=EC=8B=9C?= =?UTF-8?q?=EB=8F=84=20=EB=B0=B1=EC=98=A4=ED=94=84=20=E2=80=94=20KIS=20?= =?UTF-8?q?=EC=A3=BC=EB=AC=B8=20spam=20=EB=B0=A9=EC=A7=80=20=EC=8A=AC?= =?UTF-8?q?=EB=A1=AF=EB=B3=84=20=EC=BF=A8=EB=8B=A4=EC=9A=B4=20=EC=A0=95?= =?UTF-8?q?=EC=B1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- ...afety-sell-persistent-rejection-backoff.md | 219 ++++++++++++++++++ docs/discovery/index.md | 3 +- 2 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 docs/discovery/dcy-010_20260607_safety-sell-persistent-rejection-backoff.md diff --git a/docs/discovery/dcy-010_20260607_safety-sell-persistent-rejection-backoff.md b/docs/discovery/dcy-010_20260607_safety-sell-persistent-rejection-backoff.md new file mode 100644 index 0000000..f198bf3 --- /dev/null +++ b/docs/discovery/dcy-010_20260607_safety-sell-persistent-rejection-backoff.md @@ -0,0 +1,219 @@ +# 0. 문서 상태 + +- 상태: draft +- 문서 ID: dcy-010_20260607_safety-sell-persistent-rejection-backoff +- 이슈명: safety-sell 영속 거부 재시도 백오프 — 장 휴장·잔고 부족·규제 등 지속 거부 상황에서 KIS 주문 spam 방지를 위한 슬롯별 재시도 backoff/cooldown 정책 +- 작성 시각(KST): 2026-06-07 +- 마지막 갱신 시각(KST): 2026-06-07 +- 대체됨: 없음 +- 후속 Discovery 참조: 없음 +- 생성된 REQ 참조: 없음 + +# 1. 계획 상태 요약 + +- 연계 문서 현황: + - 관련 참조 파일: trex_core/application/safety_sell.py, trex_core/application/execution.py, trex_core/domain/slot/service.py + - 관련 TODO/메모: REL-003 Feedback Handoff — Change Request Input "safety-sell 영속 거부 시 재시도 백오프/쿨다운(신규, 우선순위 ↑)"로 기록됨. 2026-06-07 실 KIS LIVE SL/TP e2e에서 직접 확인된 운영 위험. +- 해석: + - `SafetySellService`가 매도 시도(`HOLDING → PENDING_SELL`)에 성공해도, 브로커 거부로 인한 `PENDING_SELL → HOLDING` 롤백은 별도 `trex-execution` 스레드에서 일어나며 safety-sell이 이를 인지할 수 없다. 결과적으로 트리거 조건이 해소되지 않는 한 동일 슬롯에 매도를 무한 재제출한다. + - 이번 Discovery는 슬롯별 마지막 시도 시각을 `SafetySellService` 인메모리로 추적하는 단순 쿨다운을 도입해, 추가 스레드·DB 스키마 변경 없이 KIS 주문 spam을 방지하는 방법을 정의한다. + +# 2. 요구사항 판정 결과 + +- 판정: 신규 +- 근거 + - dcy-007(core 자율 안전 매도, confirmed + Implemented)은 watchdog 기반 triggered sell 메커니즘을 정의했으나, 브로커 거부 시 재시도 제한·쿨다운·백오프 정책은 전혀 포함하지 않는다. + - 현재 어떤 Discovery/REQ도 `SafetySellService`의 per-slot 재시도 상태 추적, 최소 재시도 간격, 영속 거부 운영자 경고를 다루지 않는다. + +# 3. 문제점의 요약 + +- 현재 상태 + - `trex_core/application/safety_sell.py`: `SafetySellService._safe_sell()`이 `SlotService.sell()` 호출 성공(HOLDING → PENDING_SELL) 시 즉시 `True`를 반환한다. 브로커가 해당 주문을 거부하는지는 알 수 없다. + - `trex_core/application/execution.py`: `_rollback()`이 PENDING_SELL 거부 슬롯을 `PENDING_SELL → HOLDING`으로 전환하고 `clear_dispatch()`한다. 이 사실이 `SafetySellService`에 전달될 피드백 채널이 없다. + - `SafetySellService`에는 per-slot 마지막 시도 시각, 시도 횟수, 쿨다운 만료 시각 등 재시도 상태가 전혀 없다. + - 기본 cadence: stop-loss/take-profit은 30s마다, forced_close는 60s마다 트리거를 재평가한다. 장 휴장(9시간)에서 stop-loss 트리거가 지속되면 최대 9×3600÷30 = 1,080회 KIS 매도 주문이 제출·거부된다. + - paper 브로커는 `reject_symbols` 설정 없이는 주문을 거부하지 않으므로 테스트 환경에서 이 문제가 재현되지 않았다. +- 기대 상태 + - `SafetySellService`가 슬롯별 마지막 매도 시도 시각을 인메모리로 추적한다. + - 이전 시도로부터 `min_retry_interval_sec` 이내인 슬롯에는 재시도를 건너뛴다. + - `min_retry_interval_sec` 경과 후 트리거 조건이 여전히 충족되면 자동으로 재시도한다. + - 일정 횟수 이상 재시도가 발생한 슬롯에 대해 운영자 경고 로그를 발행한다. + - DB 스키마 변경 없이 인메모리 상태만으로 구현한다. +- 이해관계자 + - 운영자(Justin Ko): KIS 쿼터 보호 및 휴장/규제 상황에서의 안전 운용 책임 + - trex-core 데몬 개발자: `SafetySellService` + `SafetySellConfig` 수정 범위 판단 + - KIS API: 분당/시간당 쿼터 제한 존재. 영속 거부 시 재제출이 쿼터를 소모해 정상 거래일 운용에 영향 가능. +- 제약사항 + - `SafetySellService`와 `ExecutionService`는 별도 스레드이며 공유 데이터 구조에 직접 접근하지 않는다. 피드백 채널 신설은 스레드 간 결합을 증가시키므로 본 Discovery 범위 밖. + - 쿨다운 상태는 데몬 재시작 시 초기화된다. 재시작 직후 1회 재시도는 허용 범위로 본다. + - `SafetySellService`는 단일 스레드에서 호출되므로 쿨다운 `dict` 접근에 별도 락 불필요. + - 브로커 거부 원인 코드(`ack.reason`)를 `SafetySellService`가 직접 수신하는 설계는 이번 Discovery 밖. + +## 주요 사용자 시나리오 + +- (휴장) KIS 장 휴장일에 stop-loss 임계값이 설정된 holding 슬롯이 있을 때 — 쿨다운 없이 30s마다 KIS에 매도 제출·거부가 반복됨. 쿨다운 도입 후: 첫 거부 후 `min_retry_interval_sec` 동안 재시도 없음, 장 재개 후 트리거 조건 재평가. +- (잔고 부족) 계좌 잔고 부족으로 KIS가 매도를 거부하는 경우 — 동일 패턴으로 반복 거부. 쿨다운 중 운영자 경고 로그가 발행되어 수동 조치 가능. +- (규제) 특정 종목 매도 규제가 일시적으로 적용된 경우 — 쿨다운이 규제 해제 시점까지 재시도를 막고, 이후 자동 재평가. +- (정상 거래일) 워치독 만료 슬롯에 stop-loss 트리거 발동 → KIS 매도 접수·체결 → 슬롯 CLOSED → `_holding()`에서 제외. 쿨다운 미발동, 동작 변화 없음. + +# 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 파싱한다. +- **운영자 경고 로그**: `attempt_count >= max_retry_warn_count`인 슬롯에 대해 `_log.warning("safety-sell repeated rejection: slot=%s count=%d reason=persistent_rejection", ...)` 경고를 발행한다. 이 경고는 쿨다운 체크 통과 후 재시도 직전에 발행한다. + +## 영향 범위 + +- 코드 영향: + - `trex_core/application/safety_sell.py`: `SafetySellService.__init__` 인스턴스 변수 추가(`_last_attempt`, `_attempt_count`). `_safe_sell()` 호출 전후에 쿨다운 check/update 로직 추가. 슬롯 추적 정리 로직 추가. + - `trex_core/runtime/config.py`: `SafetySellConfig`에 `min_retry_interval_sec: int`, `max_retry_warn_count: int` 필드 추가. `from_env()`에서 `TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`, `TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT` 파싱 추가. +- 문서 영향: + - `docs/runtime-flows.md`: safety-sell worker tick 흐름에 "쿨다운 체크 → 건너뜀 or 재시도" 단계 추가 +- 운영 영향: + - `TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`, `TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT` 두 env var 신설(전부 optional, 기본값 있음). 기존 `.env` 구조 호환. + - 데몬 재시작 시 쿨다운 상태 초기화. 재시작이 의도적이라면 허용 범위. + +# 5. 요구사항 목록 + +## 기능 요구사항 + +- FR-1: `SafetySellService`는 슬롯별 마지막 매도 시도 시각(`last_attempt_at`)과 연속 시도 횟수(`attempt_count`)를 인메모리로 추적한다. 각 트리거(`forced_close`, `stop_loss`, `take_profit`, `market_close`)가 `_safe_sell()`을 호출하기 전에 해당 슬롯의 쿨다운 상태를 확인하고, `now - last_attempt_at < min_retry_interval_sec`이면 해당 슬롯을 건너뛴다. +- 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-6: `docs/runtime-flows.md`를 갱신해 safety-sell tick 흐름에 "쿨다운 체크" 단계를 반영한다. + +## 비기능 요구사항 + +- NFR-1: 쿨다운 상태는 `SafetySellService` 인메모리 `dict`로만 유지한다. DB 스키마 변경(마이그레이션) 없음. +- NFR-2: `SafetySellService`는 단일 스레드에서 호출되므로 쿨다운 `dict` 접근에 별도 락을 두지 않는다. 다중 스레드 접근이 발생하는 구조 변경은 이번 범위 밖. +- NFR-3: `instance_key = "default"` 환경에서 기존 pytest 스위트(단위 + 회귀) 전체 통과 — 동작 회귀 없음. +- NFR-4: paper 브로커 `reject_symbols` 설정으로 거부를 시뮬레이션했을 때 쿨다운이 발동하고, 쿨다운 만료 후 재시도함을 단위 테스트로 검증한다. + +## 범위 경계 + +- In Scope + - `SafetySellService` 인메모리 per-slot 쿨다운 추적 + - `SafetySellConfig` 두 env var 확장(`min_retry_interval_sec`, `max_retry_warn_count`) + - 영속 거부 시 운영자 WARNING 로그 + - `docs/runtime-flows.md` safety-sell 흐름 갱신(FR-6) +- Out of Scope + - `ExecutionService` → `SafetySellService` 거부 사유 피드백 채널 신설 + - 브로커 거부 원인별(휴장/잔고/규제) 차등 쿨다운 정책 + - 쿨다운 상태의 DB 영속화(재시작 후 쿨다운 복원) + - 수동 쿨다운 해제 API(`resume_kill_switch` 유사) + - market_close 트리거에 대한 별도 쿨다운 분리 — market_close는 하루 1회 발동(already guarded by `_last_liquidation_date`) + +# 6. 리스크/가정 목록 + +## 리스크 + +- R-1 (Medium): 쿨다운 중 트리거 조건이 실제로 해소됐을 때(예: 슬롯 수동 청산, 가격 회복) 불필요한 경고 로그가 계속 발행될 수 있다. + - 완화 방안: 슬롯이 `_holding()`에서 사라지면 쿨다운 상태를 즉시 정리한다(FR-4). 가격 회복으로 트리거 조건 자체가 해소되면 `check_prices()`에서 `_safe_sell()`을 호출하지 않으므로 `attempt_count` 증가도 없다. +- R-2 (Low): 데몬 재시작 시 쿨다운 초기화 → 재시작 직후 영속 거부 슬롯에 1회 즉시 재시도 발생. + - 완화 방안: 재시작은 의도적 행위이므로 1회 재시도는 허용. 재시작 후 재시도가 또 거부되면 쿨다운에 즉시 재진입. 문서에 운영 주의사항으로 명시. +- R-3 (Low): paper 브로커(`reject_symbols` 미설정)에서는 쿨다운이 발동하지 않아 단위 테스트 범위가 제한될 수 있다. + - 완화 방안: paper 브로커 `reject_symbols`에 테스트 대상 종목을 설정해 거부를 시뮬레이션하는 단위 테스트를 추가(NFR-4). + +## 가정 + +- A-1: 영속 거부의 주요 원인(휴장, 잔고 부족, 규제)은 수십 분~수 시간 지속된다. 따라서 `min_retry_interval_sec` 기본값은 분 단위(예: 300s~1800s)가 적합하다. +- A-2: 슬롯이 쿨다운 중 HOLDING으로 돌아온 것은 이전 매도 시도가 브로커에서 거부됐음을 의미한다. 별도 피드백 채널 없이도 `_holding()` 재등장으로 암묵적 거부를 감지할 수 있다. +- A-3: market_close 트리거(`check_market_close`)는 하루 1회 `_last_liquidation_date`로 이미 중복 방지가 되어 있어 쿨다운 적용 대상에서 제외한다. +- A-4: `SafetySellService.run_once()`는 `trex-safety-sell` 단일 스레드에서만 호출되므로 쿨다운 `dict` 동시성 문제 없음. + +## 오픈 질문 + +- OQ-1: `min_retry_interval_sec` 기본값 — 어느 정도가 적합한가? (예: 300s / 600s / 1800s) + - 상태: Open + - 처리 방안: Justin Ko가 실 KIS 운용 경험(휴장 시 평균 거부 지속 시간)을 기준으로 결정. + - 종료 조건: confirm-discovery 또는 REQ 단계에서 Justin Ko가 값을 확정하면 Answered. +- OQ-2: `max_retry_warn_count` 기본값 및 경고 이후 동작 — 경고 로그만? 아니면 N회 초과 시 쿨다운 무기한 전환 + 수동 해제? + - 상태: Open + - 처리 방안: Justin Ko가 운용 정책(로그만으로 충분한지, 수동 해제 필요한지)을 결정. + - 종료 조건: confirm-discovery에서 확정. +- OQ-3: 트리거 종류별(`forced_close` vs `stop_loss/take_profit`) 쿨다운 분리 여부 — 단일 per-slot 쿨다운으로 통합할지, 트리거별로 독립 쿨다운을 유지할지? + - 상태: Open + - 처리 방안: 단일 쿨다운(단순)과 트리거별 분리(세밀 제어) 트레이드오프를 Justin Ko와 협의. + - 종료 조건: confirm-discovery에서 확정. + +### 오픈 질문 처리 기준 + +- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다. +- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다. + +# 7. 초기 성공 기준 + +- S-1: paper 브로커 `reject_symbols`에 테스트 종목을 설정한 상태에서 stop-loss 트리거 발동 후 `min_retry_interval_sec` 이내에 동일 슬롯 재시도가 없음을 확인한다. +- S-2: 쿨다운 만료 후 트리거 조건이 여전히 충족되면 재시도 1회 실행 및 `last_attempt_at` 갱신 확인. +- S-3: `attempt_count >= max_retry_warn_count` 도달 시 `WARNING` 로그가 발행되고 `slot_id`, `attempt_count`, `reason`이 포함됨을 확인. +- S-4: 슬롯이 CLOSED 전환 후 `_last_attempt`와 `_attempt_count`에서 해당 슬롯 항목이 제거됨을 단위 테스트로 확인. +- S-5: `instance_key = "default"` 환경에서 기존 pytest 스위트 전체 통과 — 동작 회귀 없음. + +## 측정 방식 및 데이터 출처 + +- S-1 측정 방식: `reject_symbols` 설정 + 단위 테스트에서 `run_once()` N회 호출 후 `_safe_sell` 호출 횟수 assert / 데이터 출처: pytest 단위 테스트 +- S-2 측정 방식: `last_attempt_at`을 과거 시각으로 설정 후 `run_once()` 호출 → `_safe_sell` 재호출 여부 + `last_attempt_at` 갱신 확인 / 데이터 출처: pytest 단위 테스트 +- S-3 측정 방식: `attempt_count`를 `max_retry_warn_count - 1`로 설정 후 `run_once()` → `caplog`에서 WARNING 로그 포함 여부 / 데이터 출처: pytest + `caplog` +- S-4 측정 방식: slot CLOSED 전환 후 `run_once()` → `_last_attempt[slot_id]` KeyError 또는 absent 확인 / 데이터 출처: pytest 단위 테스트 +- S-5 측정 방식: `pytest` 전체 실행 → 0 failures / 데이터 출처: pytest 출력 + +# 8. REQ로 넘기기 전 확인 체크 + +- [ ] 범위(In Scope/Out of Scope) 확정 + - 기준: OQ-1(min_retry_interval_sec 기본값), OQ-2(max_retry_warn_count 및 경고 후 동작), OQ-3(쿨다운 분리 여부) 모두 Answered 또는 Deferred +- [ ] 성공 지표의 측정 방식/데이터 출처 확정 + - 기준: S-1~S-5 각각 pytest 단위 테스트 또는 manual e2e 근거 확정 +- [ ] High 영향 리스크 우선순위 및 해소 책임자 지정 + - 기준: R-1~R-3 완화 방안 REQ 단계 인계 여부 확인 (현재 Medium/Low — High 없음) +- [ ] 모든 오픈 질문(OQ) `Open` 상태 없음 확인 + - 기준: OQ-1, OQ-2, OQ-3 모두 Answered 또는 Deferred +- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정 + - 기준: Justin Ko 확인 + +# 9. 파일 처리 결과 + +- 처리 결과: 생성 +- 생성 경로: docs/discovery/dcy-010_20260607_safety-sell-persistent-rejection-backoff.md +- 참조 문서: + - trex_core/application/safety_sell.py (SafetySellService — 변경 대상) + - trex_core/runtime/config.py (SafetySellConfig — env var 확장 대상) + - trex_core/application/execution.py (_rollback 로직 — 참조 기준, 직접 변경 없음) + - docs/runtime-flows.md (baseline 문서 — safety-sell tick 흐름 갱신 대상, FR-6) + - docs/discovery/dcy-007_20260606_core-autonomous-safety-sell.md (원래 safety-sell Discovery — 참조 기준) + - docs/releases/rel-003_20260607_core-autonomous-safety-sell.md (REL-003 Feedback Handoff — 발견 근거) + +# 10. 사용자 결정 필요 항목 요약 + +## 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). + +## CONFIRM + +- {{CONFIRM_MARKET_CLOSE_OUT_OF_SCOPE}}: `check_market_close` 트리거는 `_last_liquidation_date`로 하루 1회 이미 보호되어 별도 쿨다운 적용 대상 밖으로 확정하는지 확인. + +## DATA + +- {{DATA_REJECTION_DURATION_STATS}}: 실 KIS 운용에서 관찰된 영속 거부 지속 시간 데이터(있다면) — `min_retry_interval_sec` 기본값 결정에 필요. + +# 11. Discovery Freeze + +## 섹션 참조 + +- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록) +- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록) +- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준) +- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크) +- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약) + +## Freeze 플래그 + +- Handoff Decision: 보류 +- Handoff Rationale: Safety-sell 쿨다운 메커니즘 설계 완료. OQ-1(min_retry_interval_sec 기본값), OQ-2(경고 후 동작), OQ-3(쿨다운 분리 여부) 세 항목이 Open 상태로 confirm-discovery 전 Justin Ko 결정 필요. +- Ready for REQ Drafting: false +- Confirmed By: TBD +- Confirmed At (KST): TBD diff --git a/docs/discovery/index.md b/docs/discovery/index.md index 2c802a4..0465d59 100644 --- a/docs/discovery/index.md +++ b/docs/discovery/index.md @@ -14,4 +14,5 @@ | dcy-006_20260605_strategy-module-rabbitmq-transport | 20260605 | trex-core ↔ 전략 모듈 통신 transport — RabbitMQ 도입 (스냅샷 read + intent 제출 + 이벤트) | [Discovery](./dcy-006_20260605_strategy-module-rabbitmq-transport.md) | confirmed | | dcy-007_20260606_core-autonomous-safety-sell | 20260606 | core 자율 안전 매도 — stop-loss·시간 만기 자동 트리거 | [Discovery](./dcy-007_20260606_core-autonomous-safety-sell.md) | confirmed | | dcy-008_20260607_emergency-stop-kill-switch | 20260607 | 비상 정지/강제 청산(kill-switch) — portfolio/전역 슬롯 청산 + 당일 전략 제외 | [Discovery](./dcy-008_20260607_emergency-stop-kill-switch.md) | confirmed | -| dcy-009_20260607_multi-instance-rabbitmq-topology | 20260607 | 인스턴스별 독립 RabbitMQ 채널 토폴로지 — trex-core 다중 인스턴스 운영을 위한 Topic Exchange + Fanout Exchange per-instance 큐 분리 | [Discovery](./dcy-009_20260607_multi-instance-rabbitmq-topology.md) | confirmed | \ No newline at end of file +| dcy-009_20260607_multi-instance-rabbitmq-topology | 20260607 | 인스턴스별 독립 RabbitMQ 채널 토폴로지 — trex-core 다중 인스턴스 운영을 위한 Topic Exchange + Fanout Exchange per-instance 큐 분리 | [Discovery](./dcy-009_20260607_multi-instance-rabbitmq-topology.md) | confirmed | +| dcy-010_20260607_safety-sell-persistent-rejection-backoff | 20260607 | safety-sell 영속 거부 재시도 백오프 — 장 휴장·잔고 부족·규제 등 지속 거부 상황에서 KIS 주문 spam 방지를 위한 슬롯별 재시도 backoff/cooldown 정책 | [Discovery](./dcy-010_20260607_safety-sell-persistent-rejection-backoff.md) | draft | \ No newline at end of file