feat(safety-sell): dcy-017 워치독 소유권 가시화 — snapshot 상태 + 발동 로깅 (BAT-021)

전략 이탈로 슬롯이 내부 청산 루틴(워치독) 소유로 넘어간 상태가 무성이라
운영자가 인지할 수 없던 문제(issue #12)를 가시화한다.

거버넌스: 전부 change-req (신규 REQ 없음, dcy-017 OQ-3; Redis 미채택).
- REQ-034/021: WatchdogStore.last_seen + snapshot RPC 응답에 holding 슬롯
  워치독 상태(expired/last_seen) read-only 동봉(best-effort)
- REQ-032: trex-core slots 가 expired 슬롯을 WATCHDOG-OWNED(전략 이탈) 표시
- REQ-037/038/039: safety-sell 워치독 소유 진입(WARN)/이탈(INFO) + 발동
  (forced_close·stop_loss=WARN, take_profit·market_close=INFO) 로그 +
  in-process 카운터(SafetySellService.counters), 구조화 필드(slot_id/
  portfolio_id/reason/phase/instance_key). per-tick 스킵은 카운터만
- REQ-042: runtime-flows/interface-contract 정합

best-effort 격리(NFR-1): 모든 가시화는 예외 격리 — 워치독 판정·청산 불변.
dcy-016 억제 읽기로 관측이 워치독을 리셋하지 않음(비침습).

검증: 단위 335 passed/34 skipped, 인프라 게이트(PG+RabbitMQ) 369 passed, ruff clean.
신규 test_safety_sell_visibility(6) + snapshot watchdog(1) + CLI(1). BAT-021 rc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 17:10:26 +09:00
co-authored by Claude Opus 4.8
parent d09b93195f
commit 6d7bca1460
26 changed files with 1022 additions and 10 deletions
@@ -0,0 +1,41 @@
# BAT-021 Design
- Status: confirmed
- Batch: BAT-021
## 설계 목표
- 전략 이탈로 슬롯이 워치독(안전망) 소유로 넘어간 상태와 safety-sell 발동을 무성에서 가시화한다(issue #12). 가시화는 best-effort — 판정·청산을 절대 막지 않는다(NFR-1).
## 1. 워치독 상태 노출 (REQ-034/021)
- `WatchdogStore.last_seen(slot_id) -> Optional[datetime]` read-only getter 추가(판정 로직 불변).
- `make_snapshot_rpc_handler(..., watchdog_timeout_sec=120)` + `_attach_watchdog(payload, slot_repo, day, watchdog_store, timeout)`:
- holding 슬롯마다 `{slot_id, portfolio_id, symbol, expired(is_expired), last_seen}` 산출 → `payload["watchdog"]`.
- try/except 로 감싸 실패 시 응답 불변(best-effort). envelope·stub 두 경로 모두 attach.
- additive top-level 필드라 REQ-028 strict asset shape(no-price) 불변.
## 2. safety-sell 가시화 (REQ-037/038/039, OQ-2)
- `SafetySellService(instance_key="default")` + `self._owned: set` + `self.counters: dict`.
- `_log_fire(slot, reason)`: `_WARN_REASONS={forced_close_at, stop_loss}` → WARN, 그 외(take_profit, market_close_liquidation) → INFO. 구조화 필드(phase/slot_id/portfolio_id/reason/instance_key) + `counters["fire:{reason}"]++`. check_forced_close/check_prices/check_market_close 의 발동 지점에서 호출.
- `_update_ownership(now)`: grace 중이면 skip. `current = {holding ∧ watchdog_expired}`; `current - _owned` → WARN(owned:enter), `_owned - current` → INFO(owned:exit); `_owned = current`. run_once 매 tick 호출. 예외 격리.
- per-tick 스킵/no-fire 평가는 로그 X, 카운터만(30초 로그 스팸 방지). 영속 거부 반복 경고는 REQ-052 별도.
## 3. CLI 표시 (REQ-032)
- `_cmd_slots`: snapshot 응답의 `watchdog` 필드에서 `expired=true` 인 portfolio_id 집합 → 자산 출력에 `[WATCHDOG-OWNED: strategy absent]` 태그 + `watchdog_owned=N` 요약. `--json` 은 원응답 그대로.
## 4. 배선
- daemon: snapshot handler 에 `watchdog_timeout_sec=config.safety_sell.watchdog_timeout_sec`; SafetySellService 에 `instance_key=config.instance_key`.
## 5. baseline 문서 (REQ-042)
- runtime-flows: 워치독 갱신 flow 에 상태 노출(5) + `safety-sell 가시화` flow 신규(소유 진입/이탈·발동 차등·카운터·best-effort).
- interface-contract: snapshot 응답 `watchdog[]` 필드.
## best-effort 격리 (NFR-1)
- `_attach_watchdog`·`_update_ownership` 예외 무시(no-op), `_log_fire` 는 순수 로그/카운터. 워치독 판정(is_expired)·청산(`SlotService.sell`) source of truth 불변.
## 재시작 (R-3)
- WatchdogStore·`_owned`·counters in-memory → 재시작 시 소유 상태 소실(폴링 재개 시 재구성). 발동 로그는 외부 로그 싱크에 이력 유지. 영속 승격은 Out of Scope.
## 테스트 설계
- `tests/application/test_safety_sell_visibility.py`: 소유 진입 WARN+카운터, 이탈 INFO+카운터, 안정 시 churn 없음, forced_close WARN, take_profit INFO, grace 억제(6건). instance_key 로그 포함 단언.
- `tests/application/test_messaging_snapshot_consumer.py`: 워치독 만료 holding → watchdog 필드 expired/last_seen 노출.
- `tests/runtime/test_cli_operator.py`: slots 가 expired 슬롯을 WATCHDOG-OWNED 로 표시.
@@ -0,0 +1,39 @@
# BAT-021 Implementation
- Status: completed
- Batch: BAT-021
## 구현 요약
- dcy-017 워치독 소유권 가시화(snapshot 워치독 상태 + safety-sell 발동 로깅)를 전부 change-req 로 구현(신규 REQ 없음, Redis 미채택).
## 변경 내역
### REQ-034/021 — 워치독 상태 노출
- `trex_core/application/watchdog.py`: `last_seen(slot_id) -> Optional[datetime]` read-only getter.
- `trex_core/application/messaging_handlers.py`: `make_snapshot_rpc_handler(..., watchdog_timeout_sec=120)` + `_attach_watchdog(payload, ...)` — holding 슬롯 `{slot_id, portfolio_id, symbol, expired, last_seen}` 를 `payload["watchdog"]` 에 best-effort 동봉(envelope·stub 두 경로). try/except 격리.
- `trex_core/runtime/daemon.py`: handler 에 `watchdog_timeout_sec=config.safety_sell.watchdog_timeout_sec` 배선.
### REQ-037/038/039 — safety-sell 발동·소유 가시화
- `trex_core/application/safety_sell.py`:
- `SafetySellService(instance_key="default")` + `self._owned: set` + `self.counters: dict` + `_count`.
- `_log_fire(slot, reason)`: WARN(`forced_close_at`/`stop_loss`)·INFO(`take_profit`/`market_close_liquidation`) + `fire:{reason}` 카운터, 구조화 필드.
- `_update_ownership(now)`: grace 밖에서 holding∧expired 집합 diff → 진입 WARN(`owned:enter`)/이탈 INFO(`owned:exit`). 예외 격리.
- check_forced_close/check_prices/check_market_close 발동 지점에서 `_log_fire`; run_once 가 매 tick `_update_ownership`.
- `trex_core/runtime/daemon.py`: SafetySellService 에 `instance_key=config.instance_key`.
### REQ-032 — CLI 표시
- `trex_core/runtime/cli.py::_cmd_slots`: snapshot 응답 `watchdog` 에서 `expired` 슬롯 → `[WATCHDOG-OWNED: strategy absent]` 태그 + `watchdog_owned=N` 요약.
### REQ-042 — baseline 문서
- `docs/runtime-flows.md`: 워치독 갱신 flow 에 상태 노출(5) + `safety-sell 가시화` flow 신규. Last Updated → BAT-021.
- `docs/interface-contract.md`: snapshot 응답 `watchdog[]` 필드. Last Updated → BAT-021.
## 테스트
- 신규 `tests/application/test_safety_sell_visibility.py`: 6건 — 소유 진입 WARN+카운터(instance_key 포함), 이탈 INFO+카운터, 안정 시 churn 없음, forced_close WARN, take_profit INFO, grace 억제.
- `tests/application/test_messaging_snapshot_consumer.py` 확장: 워치독 만료 holding → watchdog 필드 expired/last_seen 노출(operator 비침습).
- `tests/runtime/test_cli_operator.py` 확장: slots 가 expired 슬롯 WATCHDOG-OWNED 표시.
## 검증 실행
- 단위: `TREX_BROKER=paper .venv/bin/python -m pytest -q` → `335 passed / 34 skipped`(`::test_sandbox_token_and_submit` deselect — LIVE `.env` 환경 의존, 본 batch 무관).
- 인프라 게이트: `TREX_POSTGRESQL_TEST_DSN=...(trex_core_test) TREX_RABBITMQ_TEST_URL=... pytest -q` → `369 passed`.
- `ruff check trex_core tests` → All checks passed.
@@ -0,0 +1,34 @@
# BAT-021: 워치독 소유권 가시화 — 스냅샷 워치독 상태 + safety-sell 발동 로깅 (dcy-017)
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-017
- Included REQ: REQ-034 (CHG-20260608-02), REQ-021 (CHG-20260608-03), REQ-037 (CHG-20260608-01), REQ-038 (CHG-20260608-01), REQ-039 (CHG-20260608-01), REQ-032 (CHG-20260608-02), REQ-042 (CHG-20260608-01)
## Summary
- dcy-017(전략 이탈로 슬롯이 내부 청산 루틴(워치독) 소유로 넘어간 상태를 가시화, issue #12)을 단일 standard batch 로 delivery 한다. 거버넌스는 dcy-017 OQ-3 결정대로 **전부 change-req, 신규 REQ 없음**.
- 핵심: snapshot RPC 응답에 holding 슬롯 워치독 상태(`expired`/`last_seen`)를 read-only 동봉(REQ-034/021)하고 `trex-core slots` 가 WATCHDOG-OWNED(전략 이탈)를 표시(REQ-032). safety-sell 의 워치독 소유 진입(WARN)/이탈(INFO)·발동(forced_close·stop_loss=WARN, take_profit·market_close=INFO)을 카운터+구조화 로그로 가시화(REQ-037/038/039). 모든 가시화는 best-effort(청산을 막지 않음). Redis projection·push 알림 미채택(OQ-1 (a)).
## Scope
- In Scope:
- snapshot RPC 응답 워치독 상태 필드(best-effort) + `WatchdogStore.last_seen` (REQ-034/021)
- `trex-core slots` 워치독 소유 표시 (REQ-032)
- safety-sell 워치독 소유 진입/이탈 + 발동(phase 차등 WARN/INFO) 로그 + in-process 카운터 (REQ-037/038/039)
- runtime-flows/interface-contract 정합 (REQ-042)
- Out of Scope:
- 워치독 판정 로직·timeout 기준 변경(가시화만)
- WatchdogStore in-memory → 영속 store 승격(정본 변경)
- Redis projection·pub/sub push 알림(OQ-1 (a) 미채택) — push 하드 요구 시 후속 분리
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- Source discovery 단일(dcy-017). OQ-1((a) CLI/RPC 노출, Redis 미채택)·OQ-2(차등 매트릭스)·OQ-3(전부 change-req) Answered.
- dcy-016 으로 관측 함정(조회가 워치독 리셋) 이미 해소 → 비침습 on-demand 관측으로 issue #12 충족(A-1).
- best-effort 격리(NFR-1): _attach_watchdog·_update_ownership·_log_fire 모두 예외 격리, source of truth(in-memory 워치독)·청산 동작 불변.
- 메트릭은 in-process 카운터(`SafetySellService.counters`) — 기존 REQ 범위(신규 인프라 없음).
- WatchdogStore in-memory → 소유 *상태* 는 재시작 시 소실(폴링 재개 시 재구성), 발동 *로그* 는 이력 유지(R-3).
@@ -0,0 +1,50 @@
# BAT-021 Planning
- Status: confirmed
- Batch: BAT-021
- Profile: standard
## Included REQ
- REQ-034 (Interface, change-req): `WatchdogStore.last_seen` + snapshot 워치독 상태 노출.
- REQ-021 (Integration, change-req): snapshot 응답 `watchdog[]` 필드.
- REQ-037 (Integration, change-req): 워치독 소유 진입/이탈 + forced_close 발동 가시화.
- REQ-038 (Integration, change-req): stop_loss(WARN)/take_profit(INFO) 발동 가시화.
- REQ-039 (Integration, change-req): market_close 발동 가시화.
- REQ-032 (Interface, change-req): `trex-core slots` 워치독 소유 표시.
- REQ-042 (Documentation, change-req): runtime-flows/interface-contract 정합.
## Delivery Plan
1. REQ-034/021(서버): `watchdog.py` `last_seen` getter; `messaging_handlers.py` `_attach_watchdog`(best-effort) + `watchdog_timeout_sec` 파라미터; daemon 배선.
2. REQ-037/038/039(가시화): `safety_sell.py` `instance_key`/`counters`/`_log_fire`(phase 차등)/`_update_ownership`(소유 진입·이탈) + check_*·run_once 배선; daemon instance_key 배선.
3. REQ-032(CLI): `cli.py` `_cmd_slots` 가 watchdog 필드로 WATCHDOG-OWNED 표시.
4. REQ-042(문서): runtime-flows(워치독 노출 + safety-sell 가시화 flow), interface-contract(snapshot watchdog 필드).
5. 테스트: safety-sell 가시화 + snapshot watchdog 필드 + CLI 표시 + 전체 회귀 + ruff + 인프라 게이트.
## Dependencies
- Internal:
- dcy-007/BAT-010·011 safety-sell/워치독(REQ-034/037/038/039) — 가시화 대상.
- dcy-016/BAT-020 운영자 CLI `slots`·워치독 억제 읽기(REQ-032/021) — 비침습 관측 기반(A-1).
- dcy-011/dcy-013 의 WARN+카운터 가시화 패턴 정합(NFR-3).
- External:
- 없음(Redis 미채택). in-process 카운터.
## Design Gate
- Design Required Before Implementation: yes
- Reason: (a) 가시화 부수효과가 워치독 판정·safety-sell 청산을 절대 막으면 안 됨(R-1/NFR-1) — best-effort 격리 경계를 설계에서 못박아야 안전 기능 회귀를 막는다. (b) 워치독 소유 진입/이탈 diff 의 grace 처리·상태 보관(재시작 소실, R-3)·phase별 로그 레벨 매트릭스(OQ-2)를 설계에서 고정. (c) snapshot 응답에 watchdog 필드 추가가 REQ-028 strict envelope 와 충돌하지 않도록 additive top-level 필드 경계 확정. → design.md 동일 턴 scaffold.
## Milestones
1. M1 — REQ-034/021 watchdog last_seen + snapshot 필드, 핸들러 테스트 green
2. M2 — REQ-037/038/039 safety-sell 소유/발동 가시화 + 카운터, 가시화 테스트 green
3. M3 — REQ-032 CLI 표시
4. M4 — REQ-042 문서 정합
5. M5 — 전체 회귀 + ruff + 인프라 게이트
## Risks
- R-1 (High): 가시화가 실패/지연해 워치독 판정·청산을 막음. 대응: _attach_watchdog/_update_ownership/_log_fire 예외 격리, source of truth(in-memory) 불변. (NFR-1)
- R-2 (Low, OQ-1): 메커니즘 과잉. 대응: Redis 미채택, CLI/RPC 노출 + 로깅으로 한정.
- R-3 (Low): WatchdogStore in-memory → 재시작 시 소유 상태 소실. 대응: live 반영임을 문서화, 발동 로그는 이력 유지.
## Notes
- 1차 게이트: 전체 회귀 green(워치독 판정·safety-sell 트리거 무회귀 — 관측만 추가).
- 가시화 테스트는 caplog(WARN/INFO) + counters 단언으로 결정론.
- snapshot watchdog 필드는 operator(touch_watchdog=false) 요청으로 비침습 검증(폴링이 last_seen 갱신 안 함).
@@ -0,0 +1,71 @@
# BAT-021 Verification
- Status: verified
- Batch: BAT-021
## Acceptance Mapping
### REQ-034 (워치독 상태 노출, change-req)
| AC | Evidence |
|----|----------|
| snapshot 응답 holding 슬롯 expired/last_seen 노출 | tests/application/test_messaging_snapshot_consumer.py::test_snapshot_exposes_watchdog_state_for_holding_slots |
| 관측이 워치독 리셋 안 함(비침습) | ::test_operator_query_suppresses_watchdog_touch (dcy-016 회귀) |
| 판정 동작 불변 | tests/application/test_safety_sell_worker.py 전체 회귀 |
### REQ-021 (snapshot watchdog 필드, change-req)
| AC | Evidence |
|----|----------|
| 응답 watchdog[] shape | test_messaging_snapshot_consumer.py::test_snapshot_exposes_watchdog_state_for_holding_slots |
### REQ-037 (소유 진입/이탈 + forced_close, change-req)
| AC | Evidence |
|----|----------|
| 소유 진입 WARN + 카운터 | tests/application/test_safety_sell_visibility.py::test_watchdog_ownership_entry_warns |
| 소유 이탈 INFO + 카운터 | ::test_watchdog_ownership_exit_on_return |
| 안정 시 churn 없음 | ::test_no_ownership_churn_when_stable |
| forced_close 발동 WARN + 카운터 | ::test_forced_close_fire_logs_warn |
| grace 중 억제 | ::test_grace_period_suppresses_ownership |
### REQ-038 (SL/TP 발동, change-req)
| AC | Evidence |
|----|----------|
| take_profit 발동 INFO + 카운터 | test_safety_sell_visibility.py::test_take_profit_fire_logs_info |
| stop_loss WARN(매트릭스) | _log_fire WARN reasons + forced_close WARN 테스트로 레벨 검증 |
### REQ-039 (market_close 발동, change-req)
| AC | Evidence |
|----|----------|
| market_close INFO + 카운터 | _log_fire INFO 경로(test_take_profit INFO 동형) + 기존 market_close 회귀 |
### REQ-032 (CLI 표시, change-req)
| AC | Evidence |
|----|----------|
| slots WATCHDOG-OWNED 표시 | tests/runtime/test_cli_operator.py::test_slots_marks_watchdog_owned |
### REQ-042 (baseline 문서, change-req)
| AC | Evidence |
|----|----------|
| runtime-flows/interface-contract 정합 | 문서 diff(BAT-021) |
## Evidence
- Test:
- 단위 전체 회귀: `335 passed / 34 skipped`(신규 visibility 6 + snapshot 1 + CLI 1 포함; `::test_sandbox_token_and_submit` deselect).
- 인프라 게이트(PG trex_core_test + RabbitMQ): `369 passed`.
- `ruff check trex_core tests` → All checks passed.
- Logs: `335 passed, 34 skipped, 1 deselected` / `369 passed, 1 deselected ~18.1s`.
## 성공 기준(dcy-017) 대응
- S-1(슬롯이 워치독 소유로 넘어감을 운영자가 인지): snapshot watchdog 필드 노출 + CLI WATCHDOG-OWNED 표시 + 소유 진입 WARN.
- S-2(소유 접수·발동·이탈 구분 관측, 무성 0): test_safety_sell_visibility(소유 진입/이탈, forced_close WARN, take_profit INFO) + 카운터.
- S-3(관측이 last_seen 미갱신): test_operator_query_suppresses_watchdog_touch(dcy-016) — snapshot watchdog 테스트도 operator 요청으로 비침습.
- S-4(가시화 실패가 청산 차단 안 함): `_attach_watchdog`/`_update_ownership` try/except 격리(설계), 판정·청산 source of truth 불변 — 기존 safety-sell 트리거 회귀 green.
## Carried-Over / 비차단
- WatchdogStore in-memory → 소유 상태 재시작 시 소실(폴링 재개 시 재구성, R-3). 발동 로그는 이력 유지. 영속 승격은 Out of Scope(별도 주기).
- Redis projection·push 알림 미채택(OQ-1 (a)) — push 하드 요구 시 후속 분리.
- 메트릭은 in-process 카운터(`SafetySellService.counters`) — 외부 메트릭 시스템 연동은 운영 배포 절차/후속.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. 워치독 상태 노출·safety-sell 가시화 모두 단위 green(369 passed 회귀, 판정·청산 무회귀). LIVE sandbox 토큰 deselect 는 `.env` 환경 의존(본 batch 무관).
+1
View File
@@ -28,3 +28,4 @@
| BAT-018 | standard | release-candidate | REQ-005 (CHG-20260608-01), REQ-048 (CHG-20260608-01), REQ-009 (CHG-20260608-01) | dcy-014 | [bat-018](./bat-018_20260608_trading-date-timezone-daily-preallocation/) |
| BAT-019 | standard | release-candidate | REQ-017 (CHG-20260608-01), REQ-019 (CHG-20260608-01), REQ-018 (CHG-20260608-03), REQ-020 (CHG-20260608-01) | dcy-015 | [bat-019](./bat-019_20260608_ledger-outbox-permanent-error-backoff/) |
| BAT-020 | standard | release-candidate | REQ-032 (CHG-20260608-01), REQ-022 (CHG-20260608-01), REQ-021 (CHG-20260608-02), REQ-034 (CHG-20260608-01), REQ-026 (CHG-20260608-01) | dcy-016 | [bat-020](./bat-020_20260608_operator-cli-rpc-commands/) |
| BAT-021 | standard | release-candidate | REQ-034 (CHG-20260608-02), REQ-021 (CHG-20260608-03), REQ-037 (CHG-20260608-01), REQ-038 (CHG-20260608-01), REQ-039 (CHG-20260608-01), REQ-032 (CHG-20260608-02), REQ-042 (CHG-20260608-01) | dcy-017 | [bat-021](./bat-021_20260608_watchdog-ownership-visibility/) |
@@ -0,0 +1,229 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-017_20260608_watchdog-ownership-visibility
- 이슈명: 전략 이탈로 슬롯이 내부 청산 루틴(워치독) 소유로 넘어간 상태를 가시화 (운영 모니터링)
- 작성 시각(KST): 2026-06-08 16:33 KST
- 마지막 갱신 시각(KST): 2026-06-08 20:00 KST
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-034/021/037/038/039/032/042 (전부 change-req, 신규 REQ 없음 — OQ-3). REQ-034=CHG-20260608-02, REQ-021=CHG-20260608-03, REQ-032=CHG-20260608-02, REQ-037/038/039/042=CHG-20260608-01. Redis 미채택. delivery: BAT-021
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #12 (fourblocks2020/trex-core), 관련 issue #11(dcy-011, ledger silent skip), dcy-016(운영자 CLI)
- 해석:
- 슬롯이 내부 청산 루틴(safety-sell 워치독) 소유로 넘어가는 것 = 그 슬롯을 책임지던 전략 모듈이 heartbeat(스냅샷 RPC)를 멈췄다는 신호다. 그런데 이 상태가 완전히 무성(silent)이라 운영자가 "전략이 죽어 슬롯이 안전망으로 넘어갔다"는 가장 중요한 경고를 인지할 방법이 없다. 이를 가시화한다.
- 이번 Discovery에서는 **가시화 메커니즘**을 판정한다 — issue #12 는 Redis projection(TTL+pub/sub)을 제안하나, dcy-016 으로 운영자 조회가 워치독을 리셋하던 함정이 이미 해소되었으므로, **CLI `slots`(스냅샷 RPC 에 워치독 상태 노출) + safety-sell 이벤트 로깅** 으로 Redis 없이 관측을 충족한다(OQ-1 (a)). 거버넌스는 OQ-3 결정에 따라 기존 REQ(REQ-034/021/037/038/039/032/042) `change-req` 로 처리하고 신규 REQ 는 생성하지 않는다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 Discovery(dcy-001~dcy-016) 중 워치독 소유/전략 이탈 상태의 운영 가시화를 다룬 문서는 없다. dcy-007(safety-sell)은 트리거 동작을, dcy-016(운영자 CLI)은 조회/매매 표면을 도입했으나 워치독 소유 상태 노출·알림을 정의하지 않았다.
- 변경이 워치독 상태 노출(REQ-034)·safety-sell 가시화(REQ-037) 의미를 확장하고(필요 시 Redis projection 신규 도입), 운영 모니터링 표면을 추가하므로 독립 주기로 추적하는 것이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- `trex_core/application/watchdog.py` 의 `WatchdogStore` 는 **순수 in-memory dict**(`last_seen` + `is_expired(slot_id, timeout_sec)`)다. DB 테이블도 조회용 RPC 도 없고, 데몬 재시작 시 소실된다. 만기 기준은 `watchdog_timeout_sec`(기본 120s, config.py).
- `trex_core/application/safety_sell.py` 의 `check_forced_close`/`check_prices`/`check_market_close`(143~)는 발동 슬롯 리스트(`fired`)를 반환하지만 `run_once` 가 이를 영구화·로깅하지 않고 버린다. safety_sell 로그는 WARN 3개(slot sell 실패·KIS 쿼터·market-close 실패)뿐이고 평가/발동/스킵에는 로그가 없다 → 무성.
- 스냅샷 RPC 응답은 슬롯 status/symbol/qty/price 만 운반하고 **워치독 상태(expired/last_seen)를 노출하지 않는다**(`make_snapshot_rpc_handler`, messaging_handlers.py:163~). 즉 현재는 CLI 로도 "어느 슬롯이 워치독 소유인지" 알 수 없다.
- **(중요) dcy-016 으로 "관측이 워치독을 리셋"하던 함정은 이미 해소됐다** — `make_snapshot_rpc_handler` 가 `touch_watchdog=false`/`actor="operator"` 요청이면 `last_seen` 을 갱신하지 않는다(messaging_handlers.py:201~207). 즉 issue #12 가 든 "유일한 조회 수단이 워치독을 리셋"한다는 전제는 더 이상 유효하지 않다.
- Redis 는 이미 의존성이다 — `redis>=5`, `RedisTokenCache`(best-effort, 오류 시 no-op), `TREX_KIS_REDIS_URL`. 새 인프라 도입이 아니라 기존 패턴 재사용 가능.
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`)는 존재하며 최신이다. 워치독·safety-sell·CLI 는 runtime flow 에 속한다.
- 기대 상태
- 운영자가 "전략 이탈 → 슬롯이 워치독 소유로 넘어감(=안전망 발동 대상)" 을 실시간으로 인지한다.
- 관측 행위가 워치독을 리셋하지 않는다(dcy-016 으로 이미 충족).
- safety-sell 의 워치독 소유 접수·발동(forced_close/stop_loss/take_profit/market_close)·전략 복귀 이탈이 로그/메트릭(또는 알림)으로 구분 가시화된다.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물 작성
- 리뷰어/승인자 — 가시화 메커니즘(CLI/RPC vs Redis)·거버넌스 승인
- 운영 담당자 — 전략 이탈/안전망 발동을 실시간 모니터링·대응
- 대시보드/알림 연동 — (Redis 채택 시) projection/pub-sub 소비
- 제약사항
- **Best-effort 원칙**: 가시화 부수효과(Redis/로그/메트릭)는 매매·청산을 절대 멈추면 안 된다. 워치독 판정의 source of truth 는 기존 in-memory 경로를 유지한다.
- 다중 인스턴스(dcy-009) 네임스페이스 — 키/표시에 instance 포함.
- WatchdogStore 가 in-memory 라 재시작 시 상태가 소실된다(메커니즘 선택에 영향).
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 전략 모듈이 죽어 스냅샷 폴링이 끊긴다 → 해당 holding 슬롯의 워치독이 만기되어 safety-sell 소유로 넘어간다 → 운영자가 `trex-core slots`(또는 알림)로 "이 슬롯이 워치독 소유(전략 이탈)" 임을 즉시 인지한다.
- safety-sell 이 forced_close/stop_loss 로 슬롯을 실제 청산한다 → 발동 사실·사유·슬롯이 로그/메트릭(또는 알림)으로 남아 사후 추적된다.
- 전략이 복귀해 폴링을 재개한다 → 워치독 소유에서 이탈한 것이 가시화되어 경고가 stuck 되지 않는다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- (OQ-1 (a)) 스냅샷 RPC 응답에 holding 슬롯의 워치독 상태(expired/last_seen) 필드를 추가하고 `trex-core slots` 가 "워치독 소유(전략 이탈)" 를 표시한다(워치독 억제 읽기 = dcy-016 으로 이미 비침습).
- safety-sell 의 워치독 소유 접수·발동(forced_close/stop_loss/take_profit/market_close)·이탈을 로그/메트릭으로 가시화한다(현재 무성 → 가시화, dcy-011/#11 패턴 정합).
- baseline 문서에 워치독 가시화 흐름·스냅샷 응답 워치독 필드를 반영한다.
- (미채택) Redis projection·pub/sub 알림은 본 주기에 도입하지 않는다 — push 알림이 후일 하드 요구가 되면 후속 분리.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/application/safety_sell.py` — `run_once`/`check_*` 가 발동·소유·스킵을 로그/메트릭으로 가시화(현재 버려지는 `fired` 리스트 활용)
- `trex_core/application/messaging_handlers.py` — 스냅샷 응답에 워치독 expired/last_seen 필드 추가(`WatchdogStore` + `watchdog_timeout_sec` 로 슬롯별 계산)
- `trex_core/runtime/cli.py` — `slots` 가 워치독 상태(소유/전략 이탈) 컬럼 표시(dcy-016 의 `slots` 확장)
- 문서 영향:
- `docs/runtime-flows.md` — 워치독 소유·safety-sell 발동 가시화 흐름 반영
- `docs/interface-contract.md` — 스냅샷 응답 워치독 상태 필드 계약 반영
- 운영 영향:
- 전략 이탈/안전망 발동을 CLI 조회·로그로 인지 → 운영 대응성 향상
- (push 알림·대시보드 연동은 본 주기 밖 — Redis projection 후속)
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: safety-sell 의 워치독 소유·발동을 phase별 차등 레벨로 가시화한다(OQ-2 A안) — WARN: 워치독 소유 진입(전략 이탈)·forced_close·stop_loss / INFO: take_profit·market_close·워치독 이탈(복귀) / 전 phase 카운터(phase·reason·instance_key 라벨) / per-tick 스킵·no-fire 는 카운터만(로그 X). 모든 로그·메트릭은 `slot_id`·`portfolio_id`·`reason`·`phase`·`instance_key` 구조화 필드를 포함한다. 소유 진입·이탈을 모두 남겨 경고가 stuck 되지 않는다.
- FR-2: 운영자가 "어느 슬롯이 워치독 소유(전략 이탈)인지" 를 관측할 수 있다. 관측 행위는 워치독을 리셋하지 않는다(dcy-016 의 `touch_watchdog=false` 억제 읽기 재사용).
- FR-3: (OQ-1 (a)) 스냅샷 RPC 응답에 holding 슬롯의 워치독 상태(expired 여부·last_seen)를 read-only 필드로 추가하고, `trex-core slots` 가 "워치독 소유(전략 이탈)" 를 표시한다. 데몬은 `WatchdogStore` + `watchdog_timeout_sec` 로 슬롯별 expired 를 계산해 응답에 싣는다. Redis projection·pub/sub 알림은 도입하지 않는다(미채택).
- FR-4: 가시화 부수효과는 best-effort 다 — Redis/로그/메트릭 오류가 매매·청산을 멈추지 않으며, 워치독 판정 source of truth 는 기존 in-memory 경로를 유지한다.
- FR-5: 다중 인스턴스 환경에서 워치독 상태 표시·로그에 instance_key 를 포함해 인스턴스 간 혼동이 없게 한다(dcy-016 의 대상 인스턴스 표시와 정합).
- FR-6: `docs/runtime-flows.md` 와 `docs/interface-contract.md`(스냅샷 응답 워치독 상태 필드)에 워치독 가시화 흐름·계약을 반영한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 안전 격리 — 가시화는 best-effort projection 이며, 어떤 실패도 워치독 판정·safety-sell 청산을 차단하지 않아야 한다.
- NFR-2: 비침습 관측 — 운영자의 관측이 워치독 `last_seen` 을 갱신해 강제청산을 지연시키지 않아야 한다(dcy-016 억제로 충족).
- NFR-3: 가시성 — 전략 이탈/안전망 발동이라는 결과 기반 신호가 무성으로 사라지지 않고 로그/메트릭/(알림)으로 관측 가능해야 한다(issue #11/dcy-011 패턴 정합).
- NFR-4: 멀티 인스턴스 정합 — 인스턴스별 상태가 키/표시에서 혼동 없이 구분되어야 한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- safety-sell 워치독 소유·발동·이탈의 로그/메트릭 가시화(FR-1, cheap·필수)
- 워치독 소유 상태의 운영자 관측(FR-2) — 비침습(dcy-016)
- (OQ-1 (a)) 스냅샷 RPC 응답 워치독 상태 필드 + `trex-core slots` 표시
- runtime-flows·interface-contract 문서 갱신
- Out of Scope
- 워치독 판정 로직·timeout 기준 변경(가시화만, 판정은 기존 유지)
- WatchdogStore 를 in-memory 에서 영속 store 로 승격(정본 변경) — 본 주기는 관측 노출까지
- Redis projection·pub/sub push 알림(OQ-1 (a) 로 미채택) — push 알림이 후일 하드 요구가 되면 후속 분리
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): 가시화 부수효과(특히 Redis)가 실패·지연하면 워치독 판정·safety-sell 청산을 막아 안전 기능을 해칠 수 있다.
- 완화 방안: FR-4 best-effort 격리 — `RedisTokenCache` 처럼 오류를 no-op 무시하고, source of truth 는 in-memory 유지. 가시화는 비동기·예외 격리.
- R-2 (Low, OQ-1 결정으로 해소): 메커니즘 과잉설계 위험.
- 완화 방안: OQ-1 (a)로 Redis projection 을 미채택하고 CLI/RPC 노출 + safety-sell 로깅으로 한정했다. dcy-016 으로 관측 함정이 이미 해소되어 on-demand 관측으로 issue #12 의 "무성 → 가시화" 를 충족한다. push 알림은 실제 요구 발생 시 후속.
- R-3 (Low): WatchdogStore 가 in-memory 라 재시작 시 소유 상태가 소실되어, 재시작 직후 CLI 조회가 비어 보일 수 있다(폴링 재개 시 다시 채워짐).
- 완화 방안: 가시화는 live 상태 반영임을 문서화한다. WatchdogStore 영속 승격은 Out of Scope(별도 주기). safety-sell 발동 로그(FR-1)는 재시작과 무관하게 이력으로 남는다.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: dcy-016 구현 완료로 스냅샷 RPC 에 워치독 억제 읽기(`touch_watchdog=false`/`actor="operator"`)와 `trex-core slots` 가 존재하므로, 워치독 상태 노출은 기존 표면 확장으로 충분하다(관측이 워치독을 리셋하지 않음).
- A-2: `safety_sell.check_*` 가 이미 발동 슬롯 리스트를 산출하므로, 로그/메트릭 가시화(FR-1)는 버려지던 결과를 활용하는 cheap 한 변경이다.
- A-3: Redis 가 이미 의존성이고 `RedisTokenCache` best-effort 패턴이 있어, Redis projection 채택 시 새 인프라 없이 동일 패턴을 재사용한다.
- A-4: 워치독 판정의 source of truth 는 in-memory 경로이며, 본 변경은 그 위의 관측 projection 만 추가한다(판정 동작 무변경).
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 가시화 메커니즘 — (a) 스냅샷 RPC 워치독 상태 노출 + `trex-core slots` 표시(Redis 불요, pull+로그) / (b) best-effort Redis projection(TTL 연속 키 + warn/fire 이산 키·pub/sub, push 알림·대시보드) / (c) 단계적((a) 먼저, push 알림 필요 시 (b)) 중 무엇인가?
- 상태: Answered (2026-06-08, 사용자 결정 — (a) CLI/RPC 노출)
- 결정: 스냅샷 RPC 응답에 holding 슬롯의 워치독 상태(expired/last_seen) 필드를 추가하고 `trex-core slots` 가 표시한다. 관측은 dcy-016 의 `touch_watchdog=false` 억제 읽기로 비침습이다. **Redis projection·pub/sub 알림은 미채택**(이번 주기 도입 안 함). safety-sell 발동·소유·이탈 로그/메트릭(FR-1)은 메커니즘과 독립적으로 유지한다. push 알림이 후일 하드 요구가 되면 Redis projection 을 후속 분리한다.
- 반영 위치: FR-3, 범위 경계(Out of Scope: Redis projection), R-2, DATA
- OQ-2: safety-sell 가시화(FR-1)의 범위 — 어떤 phase(소유 진입/forced_close/stop_loss/take_profit/market_close/스킵/이탈)를 어떤 레벨(WARN/INFO/메트릭)로 남길지, 메트릭 키 규약은?
- 상태: Answered (2026-06-08, 사용자 결정 — A안 차등 매트릭스)
- 결정: **WARN** = 워치독 소유 진입(전략 이탈)·forced_close 발동·stop_loss 발동(주의 사건). **INFO** = take_profit 발동·market_close 발동·워치독 이탈(복귀). **메트릭(카운터)** = 전 phase(발동·소유 진입/이탈·스킵), phase/reason 라벨 + instance_key 포함. **per-tick 스킵/no-fire 평가는 로그하지 않고 카운터만**(30초 주기 로그 스팸 방지; 쿨다운 영속 거부 경고는 dcy-010/REQ-052 가 별도 처리). 모든 로그/메트릭에 `slot_id`·`portfolio_id`·`reason`·`phase`·`instance_key` 구조화 필드. dcy-011(#11 `ledger_skipped`)·dcy-013 의 WARN+카운터 패턴과 정합.
- 반영 위치: FR-1, NFR-3
- OQ-3: 거버넌스 경로 — 워치독(REQ-034)·safety-sell(REQ-037)·설정(REQ-041)·문서(REQ-042)의 change-req 로 처리할지, (Redis projection 채택 시) 신규 REQ 로 추적할지?
- 상태: Answered (2026-06-08, 사용자 결정 — change-req 우선)
- 결정: 기존 Implemented REQ 본문 수정(change-req)으로 처리한다. change-req 대상 — REQ-034(per-slot 워치독: 워치독 상태 expired/last_seen 노출), REQ-021(메시지 계약: 스냅샷 응답에 워치독 상태 필드), REQ-037(forced_close 자동 매도 루프: forced_close·워치독 소유 진입/이탈 가시화), REQ-038(stop-loss·take-profit 트리거: 발동 가시화), REQ-039(장 마감 청산: market_close 가시화), REQ-032(CLI: `slots` 가 워치독 상태 표시 — dcy-016 확장), REQ-042(safety-sell baseline 문서 정합: runtime-flows/interface-contract 갱신). OQ-1 (a) 로 Redis projection 을 미채택했으므로 신규 REQ 는 생성하지 않는다(메트릭은 in-process 카운터로 기존 REQ 범위 내).
- 반영 위치: 1. 거버넌스 해석, R-1 완화, 8. 확인 체크
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 전략 폴링이 끊겨 슬롯이 워치독 소유로 넘어가면, 운영자가 (CLI 조회 또는 알림으로) 그 사실·해당 슬롯을 인지할 수 있다.
- S-2: safety-sell 의 워치독 소유 접수·발동(forced_close/stop_loss/take_profit/market_close)·이탈이 로그/메트릭으로 구분 관측된다(무성 0건).
- S-3: 운영자의 워치독 상태 관측이 `last_seen` 을 갱신하지 않아 강제청산을 지연시키지 않는다(dcy-016 억제 검증).
- S-4: 가시화 부수효과(Redis/로그/메트릭) 실패를 강제 주입해도 워치독 판정·safety-sell 청산이 정상 동작한다(best-effort 격리).
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: 폴링 중단 → 워치독 만기 시나리오에서 CLI 조회/알림에 소유 상태 표시 확인 / 데이터 출처: `slots` 출력 또는 projection, 워치독 상태
- S-2 측정 방식: safety-sell 발동 시나리오에서 phase별 로그/메트릭 발생 확인 / 데이터 출처: 데몬 로그, 메트릭, `check_*` 결과
- S-3 측정 방식: 운영자 모드(`touch_watchdog=false`) 조회 후 `last_seen` 미갱신 확인 / 데이터 출처: WatchdogStore 상태, 스냅샷 핸들러 테스트
- S-4 측정 방식: Redis/로깅 실패 주입 후 워치독 판정·청산 정상 동작 확인 / 데이터 출처: 장애 주입 테스트, safety-sell 동작 로그
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 가시화 메커니즘(OQ-1 (a))·safety-sell 가시화 범위(OQ-2 차등 매트릭스)·거버넌스(OQ-3) 모두 확정됨
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-4 의 테스트/검증 출처가 실제 실행 가능한 형태로 지정됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(가시화가 안전 기능 차단)의 best-effort 격리 방식과 change-req 대상(REQ-034/021/037/038/039/032/042)이 OQ-3 으로 확정됨. 해소 실무 책임자 배정은 REQ/batch planning 단계로 넘김(비차단).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~OQ-3 모두 `Answered`, `Open` 0개
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Confirmed By = Justin Ko (사람 승인자 지정 완료)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-017_20260608_watchdog-ownership-visibility.md
- 참조 문서:
- GitHub issue #12 (fourblocks2020/trex-core) — 워치독 소유 상태 Redis 가시화 제안
- docs/discovery/dcy-007_20260606_core-autonomous-safety-sell.md — safety-sell·워치독 도입 근거
- docs/discovery/dcy-016_20260608_operator-cli-rpc-commands.md — `slots` CLI·워치독 억제 읽기(A-1, CLI 대안의 기반)
- docs/discovery/dcy-011_..._buy-intent-portfolio-id-required-symbol-resolve.md — 무성 가시화 패턴(#11 계열)
- trex_core/application/watchdog.py, safety_sell.py, messaging_handlers.py — 워치독 in-memory·발동 미로깅·억제 읽기 지점
- trex_core/adapters/broker/token_cache.py — Redis best-effort 패턴(A-3)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- (없음 — OQ-1/OQ-2 확정으로 해소)
> 결정 완료(2026-06-08): OQ-1 가시화 메커니즘 = (a) 스냅샷 RPC 워치독 상태 노출 + `trex-core slots`(Redis 미채택, push 알림 후속) / OQ-2 safety-sell 가시화 = 차등 매트릭스(WARN: 소유진입·forced_close·stop_loss / INFO: take_profit·market_close·이탈 / 전 phase 카운터 / per-tick 스킵=카운터만). 결정 항목에서 제외됨.
## CONFIRM
- 없음 — 거버넌스 경로는 OQ-3 으로 확정됨(REQ-034/021/037/038/039/032/042 change-req, 신규 REQ 없음).
## DATA
- (참고) push 알림(전략 이탈 즉시 통지)은 본 주기 미채택(OQ-1 (a)). 후일 push 알림이 하드 요구로 확인되면 Redis projection 후속 분리.
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: confirm-discovery 완료. 오픈 질문 OQ-1~OQ-3 모두 Answered (가시화=(a) 스냅샷 RPC 워치독 노출 + `trex-core slots`, Redis 미채택 / safety-sell 가시화=차등 매트릭스 / 거버넌스=REQ-034/021/037/038/039/032/042 change-req, 신규 REQ 없음). 핵심: dcy-016 으로 관측 함정이 이미 해소되어 Redis 없이 충족. C 유형 플레이스홀더 0개, 저장소 사실관계 일치. REQ 초안 작성 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-08 17:10 KST
+2 -1
View File
@@ -21,4 +21,5 @@
| dcy-013_20260608_kis-external-cancel-pending-sell-stuck | 20260608 | KIS 외부 주문 취소 시 슬롯이 pending_sell 에 영구히 갇힘 — cncl_yn 미파싱 (issue #9) | [Discovery](./dcy-013_20260608_kis-external-cancel-pending-sell-stuck.md) | confirmed |
| dcy-014_20260608_trading-date-configurable-timezone-kst | 20260608 | 당일 슬롯 생성 기준 시간을 KST(설정 가능)로 분리 — trading_date 타임존 분리 + 일자 경계 preallocation (issue #14) | [Discovery](./dcy-014_20260608_trading-date-configurable-timezone-kst.md) | confirmed |
| dcy-015_20260608_ledger-outbox-permanent-error-backoff-cap | 20260608 | portfolio-hub 404(미등록 포트폴리오)가 retryable 로 분류되어 백오프·상한 없이 영구 재시도됨 (issue #13) | [Discovery](./dcy-015_20260608_ledger-outbox-permanent-error-backoff-cap.md) | confirmed |
| dcy-016_20260608_operator-cli-rpc-commands | 20260608 | 운영자 CLI 명령 확장 — universe/슬롯 확인 + enter/exit + pause/resume (데몬 RPC 통일) | [Discovery](./dcy-016_20260608_operator-cli-rpc-commands.md) | confirmed |
| dcy-016_20260608_operator-cli-rpc-commands | 20260608 | 운영자 CLI 명령 확장 — universe/슬롯 확인 + enter/exit + pause/resume (데몬 RPC 통일) | [Discovery](./dcy-016_20260608_operator-cli-rpc-commands.md) | confirmed |
| dcy-017_20260608_watchdog-ownership-visibility | 20260608 | 전략 이탈로 슬롯이 내부 청산 루틴(워치독) 소유로 넘어간 상태를 가시화 — CLI/RPC vs Redis projection (issue #12) | [Discovery](./dcy-017_20260608_watchdog-ownership-visibility.md) | confirmed |
+11 -1
View File
@@ -4,7 +4,7 @@
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
- Status: draft
- Owner: project maintainers
- Last Updated (KST): 2026-06-08 (BAT-020)
- Last Updated (KST): 2026-06-08 (BAT-021)
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); HTTP API = dcy-002 / BAT-002
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
@@ -163,6 +163,16 @@ sell payload 는 `{"intent_type":"sell", "intent_id":..., "slot_id":..., "reason
```
**No `price` field on items** (REQ-028 AC-3 / REQ-027 AC-1).
- 워치독 상태 (dcy-017/REQ-034, 선택 필드): 응답에 holding 슬롯의 워치독 상태를 read-only 로 동봉한다.
```
"watchdog": [ # best-effort; 실패 시 생략(NFR-1)
{ "slot_id": str, "portfolio_id": str | null, "symbol": str | null,
"expired": bool, # true => 전략 이탈(안전망 소유)
"last_seen": "ISO-8601" | null } # 마지막 스냅샷 폴링 시각
]
```
- `trex-core slots` 가 `expired=true` 를 **WATCHDOG-OWNED(전략 이탈)** 로 표시한다. 운영자 조회(`actor="operator"`/`touch_watchdog=false`)는 이 노출이 워치독을 리셋하지 않는다(비침습, dcy-016).
- 워치독 신호 (REQ-034): `trex.snapshot.req` 처리는 read-only 응답 산출에 더해, 응답에 포함된 모든 `holding` 슬롯의 워치독 `last_seen_at` 을 갱신한다. 즉 전략 모듈이 스냅샷을 폴링하는 것 자체가 해당 슬롯의 생존 신호이며, 폴링이 끊긴(워치독 만료) 슬롯만 safety-sell 트리거 대상이 된다. (dcy-016: `touch_watchdog=false`/`actor="operator"` 요청은 이 갱신을 건너뛴다 — 운영자 조회 제외.)
### 운영자 CLI ↔ RPC 매핑 (dcy-016 / REQ-032)
+11 -1
View File
@@ -4,7 +4,7 @@
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
- Status: draft
- Owner: project maintainers
- Last Updated (KST): 2026-06-08 (BAT-020)
- Last Updated (KST): 2026-06-08 (BAT-021)
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); 데몬/HTTP 흐름 = dcy-002 / BAT-002
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
@@ -138,6 +138,7 @@
2. `update_thresholds` intent(REQ-040)도 수신 시 해당 슬롯 `last_seen_at` 을 리셋한다(임계값 갱신 = 생존 신호).
3. `WatchdogStore.is_expired(slot_id, TREX_WATCHDOG_TIMEOUT)` 가 True 이고 grace period(`TREX_WATCHDOG_GRACE_PERIOD`, 데몬 부팅 직후) 밖이면 그 슬롯은 워치독 만료 → forced_close/가격 트리거 대상이 된다.
4. in-memory store 라 데몬 재시작 시 초기화되며 grace period 가 자동 재시작된다(부팅 직후 오탐 방지).
5. 가시화(dcy-017/REQ-034): 스냅샷 RPC 응답에 holding 슬롯의 워치독 상태 `watchdog: [{slot_id, portfolio_id, symbol, expired, last_seen}]` 를 read-only 로 동봉한다(best-effort — 실패 시 응답 불변, NFR-1). `trex-core slots` 가 `expired=true` 슬롯을 **WATCHDOG-OWNED(전략 이탈)** 로 표시한다. 운영자 조회는 `actor="operator"`/`touch_watchdog=false`(dcy-016)라 이 노출이 워치독을 리셋하지 않는다(비침습). Redis projection·push 알림은 미채택(OQ-1).
## Flow: `forced_close_at 만기 자동 매도` (BAT-011 / REQ-037)
@@ -174,6 +175,15 @@
4. `attempt_count >= TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT`(기본 3) 도달 시 재시도 직전 운영자 `WARNING` 로그(slot_id/count/reason/last_attempt_at).
5. 트리거 조건 해소(가격 회복 등)·체결 완료(슬롯 holding 이탈) 시 재시도/카운트 증가 없음. 데몬 재시작 시 쿨다운 초기화(재시작 직후 1회 재시도 허용).
## Flow: `safety-sell 가시화` (BAT-021 / dcy-017, REQ-037)
전략 이탈로 슬롯이 안전망(워치독) 소유로 넘어가거나 실제 청산이 발동하는 결과 기반 신호를 무성에서 가시화한다(issue #12). best-effort — 어떤 로깅/메트릭 실패도 워치독 판정·청산을 막지 않는다(NFR-1).
1. 워치독 소유 진입/이탈: `run_once` 가 매 tick `_update_ownership` 로 holding+워치독 만료(grace 밖) 슬롯 집합을 직전 tick 과 비교 → **진입(전략 이탈)=WARN**, **이탈(복귀)=INFO** + 카운터(`owned:enter`/`owned:exit`). grace 중엔 판정 안 함.
2. 발동 가시화(OQ-2 차등): **WARN** = `forced_close_at`·`stop_loss` 발동 / **INFO** = `take_profit`·`market_close_liquidation` 발동 + 카운터(`fire:{reason}`). 모든 로그는 `slot_id`·`portfolio_id`·`reason`·`phase`·`instance_key` 구조화 필드 포함.
3. per-tick 스킵/no-fire 평가는 로그하지 않고 카운터만(30초 주기 로그 스팸 방지; 영속 거부 반복 경고는 REQ-052 가 별도 담당).
4. WatchdogStore 가 in-memory 라 소유 *상태* 는 재시작 시 소실(폴링 재개 시 재구성); 발동 *로그* 는 이력으로 남는다(R-3). 메트릭은 in-process 카운터(`SafetySellService.counters`).
## Flow: `kill-switch 비상 정지` (BAT-012 / REQ-043/045/046)
운영자가 `trex.control.req` 로 비상 정지를 발동해 범위별 holding 을 즉시 청산하고 당일 전략을 차단한다.
@@ -149,3 +149,41 @@ dcy-007에서 정의된 자율 안전 매도 기능(REQ-034~041)의 구현 완
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current (Implemented)
- Follow-up Action: draft-release 로 BAT-011 배포 편성.
### CHG-20260608-01
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- (change-req, dcy-017) baseline 문서를 워치독 가시화에 맞춰 정합: `docs/runtime-flows.md`(워치독 상태 노출 + safety-sell 가시화 flow), `docs/interface-contract.md`(snapshot 응답 `watchdog` 필드). BAT-021 정합 완료 → Implemented 유지.
#### Intent
- 코드(watchdog/messaging_handlers/safety_sell/cli) 변경과 baseline 문서를 일치시켜 운영자 참조 정합 유지.
#### Acceptance Criteria Delta
- Updated:
- runtime-flows/interface-contract 의 워치독 가시화·safety-sell 발동 가시화 기술 정합.
#### Impacted Area Delta
- Docs: docs/runtime-flows.md, docs/interface-contract.md
#### Delivery Trace Delta
- Batches: BAT-021
- Releases: none
#### Revalidation Impact
- Reverification Needed: no (문서 정합)
- Existing Implementation Invalidated: no
- Status Recommendation: Implemented (change-req → BAT-021 정합)
- Follow-up Action: confirm-req-implemented(BAT-021).
#### Validation Plan
- BAT-021: 코드-문서 diff 대조.
@@ -222,3 +222,43 @@
#### Validation Plan
- BAT-020: 기본 요청 touch / operator 요청 suppress 단위(caplog/recorder).
### CHG-20260608-03
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- (change-req, dcy-017/REQ-034 연계) snapshot RPC 응답에 `watchdog` 필드(holding 슬롯 `{slot_id, portfolio_id, symbol, expired, last_seen}` 배열) 추가(best-effort, 실패 시 생략). 메시지 계약 확장. BAT-021 재구현·재검증 완료 → Implemented 유지.
#### Intent
- 워치독 소유 상태를 운영자 CLI 가 관측할 수 있도록 스냅샷 응답 계약에 read-only 필드 정식화.
#### Acceptance Criteria Delta
- Added:
- snapshot 응답 optional `watchdog[]` 필드(read-only).
#### Impacted Area Delta
- Modules: `trex_core/application/messaging_handlers.py`(_attach_watchdog)
- Docs: `docs/interface-contract.md`
- Tests: tests/application/test_messaging_snapshot_consumer.py
#### Delivery Trace Delta
- Batches: BAT-021
- Releases: none
#### Revalidation Impact
- Reverification Needed: yes (BAT-021 에서 완료)
- Existing Implementation Invalidated: no (additive 필드)
- Status Recommendation: Implemented (change-req → BAT-021 재검증)
- Follow-up Action: confirm-req-implemented(BAT-021).
#### Validation Plan
- BAT-021: snapshot 응답 watchdog 필드 shape 단위.
@@ -148,3 +148,42 @@
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current (Implemented)
- Follow-up Action: draft-release 로 BAT-011 배포 편성.
### CHG-20260608-01
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- (change-req, dcy-017/issue #12) safety-sell 가시화: 워치독 소유 진입(전략 이탈)=WARN/이탈(복귀)=INFO(`_update_ownership`, run_once 매 tick), forced_close 발동=WARN(`_log_fire`) + in-process 카운터(`SafetySellService.counters`). 구조화 필드(slot_id/portfolio_id/reason/phase/instance_key). best-effort(가시화 실패가 청산을 막지 않음). 트리거 판정 동작 불변. BAT-021 재구현·재검증 완료 → Implemented 유지.
#### Intent
- 전략 이탈→안전망 소유, forced_close 발동을 무성에서 가시화(OQ-2 차등 매트릭스).
#### Acceptance Criteria Delta
- Added:
- 워치독 소유 진입/이탈 + forced_close 발동 로그/카운터. per-tick 스킵은 카운터만.
#### Impacted Area Delta
- Modules: `trex_core/application/safety_sell.py`(_update_ownership/_log_fire/counters/instance_key), `trex_core/runtime/daemon.py`(instance_key 배선)
- Tests: tests/application/test_safety_sell_visibility.py
#### Delivery Trace Delta
- Batches: BAT-021
- Releases: none
#### Revalidation Impact
- Reverification Needed: yes (BAT-021 에서 완료)
- Existing Implementation Invalidated: no (관측만 추가, 트리거 불변)
- Status Recommendation: Implemented (change-req → BAT-021 재검증)
- Follow-up Action: confirm-req-implemented(BAT-021).
#### Validation Plan
- BAT-021: 소유 진입 WARN/이탈 INFO/forced_close WARN + 카운터 단위(caplog).
@@ -235,3 +235,42 @@
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current (Implemented)
- Follow-up Action: REL-003 재확인 후 배포.
### CHG-20260608-01
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- (change-req, dcy-017/issue #12) SL/TP 발동 가시화: `stop_loss` 발동=WARN, `take_profit` 발동=INFO(`_log_fire`) + 카운터(`fire:stop_loss`/`fire:take_profit`). 구조화 필드(slot_id/portfolio_id/reason/phase/instance_key). 판정·KIS polling 동작 불변(관측만). BAT-021 재구현·재검증 완료 → Implemented 유지.
#### Intent
- 손절/익절 발동을 무성에서 가시화(OQ-2: stop_loss=WARN 주의사건, take_profit=INFO).
#### Acceptance Criteria Delta
- Added:
- stop_loss(WARN)/take_profit(INFO) 발동 로그 + 카운터.
#### Impacted Area Delta
- Modules: `trex_core/application/safety_sell.py`(check_prices 발동 가시화)
- Tests: tests/application/test_safety_sell_visibility.py::test_take_profit_fire_logs_info
#### Delivery Trace Delta
- Batches: BAT-021
- Releases: none
#### Revalidation Impact
- Reverification Needed: yes (BAT-021 에서 완료)
- Existing Implementation Invalidated: no (관측만 추가)
- Status Recommendation: Implemented (change-req → BAT-021 재검증)
- Follow-up Action: confirm-req-implemented(BAT-021).
#### Validation Plan
- BAT-021: take_profit 발동 INFO + 카운터 단위.
@@ -145,3 +145,42 @@
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current (Implemented)
- Follow-up Action: draft-release 로 BAT-011 배포 편성.
### CHG-20260608-01
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- (change-req, dcy-017/issue #12) market_close 청산 발동 가시화: `market_close_liquidation` 발동=INFO(`_log_fire`) + 카운터. 구조화 필드 포함. 청산 동작(워치독/grace 무관, 1일 1회) 불변. BAT-021 재구현·재검증 완료 → Implemented 유지.
#### Intent
- 장 마감 청산 발동을 가시화(OQ-2: market_close=INFO).
#### Acceptance Criteria Delta
- Added:
- market_close 발동 INFO 로그 + 카운터.
#### Impacted Area Delta
- Modules: `trex_core/application/safety_sell.py`(check_market_close 발동 가시화)
- Tests: tests/application/test_safety_sell_visibility.py(가시화 전반) + 기존 market_close 회귀
#### Delivery Trace Delta
- Batches: BAT-021
- Releases: none
#### Revalidation Impact
- Reverification Needed: yes (BAT-021 에서 완료)
- Existing Implementation Invalidated: no (관측만 추가)
- Status Recommendation: Implemented (change-req → BAT-021 재검증)
- Follow-up Action: confirm-req-implemented(BAT-021).
#### Validation Plan
- BAT-021: market_close 발동 시 INFO + 카운터(기존 청산 회귀 동반).
@@ -174,3 +174,42 @@
#### Validation Plan
- BAT-020: CLI universe/slots/enter/exit RPC payload·안전장치·라우팅·--json 단위.
### CHG-20260608-02
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- (change-req, dcy-017) `trex-core slots` 가 snapshot 응답의 `watchdog` 필드를 읽어 `expired=true` 슬롯을 **WATCHDOG-OWNED(전략 이탈)** 로 표시(+`watchdog_owned` 카운트). dcy-016 `slots` 확장. BAT-021 재구현·재검증 완료 → Implemented 유지.
#### Intent
- 운영자가 CLI 로 워치독 소유(전략 이탈) 슬롯을 즉시 인지.
#### Acceptance Criteria Delta
- Updated:
- `slots` 출력에 워치독 소유 표시(snapshot watchdog 필드 기반).
#### Impacted Area Delta
- Modules: `trex_core/runtime/cli.py`(_cmd_slots watchdog 표시)
- Tests: tests/runtime/test_cli_operator.py::test_slots_marks_watchdog_owned
#### Delivery Trace Delta
- Batches: BAT-021
- Releases: none
#### Revalidation Impact
- Reverification Needed: yes (BAT-021 에서 완료)
- Existing Implementation Invalidated: no (표시 추가)
- Status Recommendation: Implemented (change-req → BAT-021 재검증)
- Follow-up Action: confirm-req-implemented(BAT-021).
#### Validation Plan
- BAT-021: slots watchdog-owned 표시 단위.
@@ -152,3 +152,42 @@
#### Validation Plan
- BAT-020: operator 요청 시 watchdog.update 미호출, 기본 요청 시 호출 단위.
### CHG-20260608-02
- Date: 2026-06-08
- Author: Justin Ko
#### Change Summary
- (change-req, dcy-017/issue #12) 워치독 상태 가시화: `WatchdogStore.last_seen(slot_id)` read-only getter 추가, snapshot RPC 응답에 holding 슬롯 워치독 상태(`expired`/`last_seen`) read-only 동봉(best-effort). 판정 로직·timeout 불변(관측만 추가). BAT-021 재구현·재검증 완료 → Implemented 유지.
#### Intent
- 전략 이탈로 슬롯이 안전망(워치독) 소유로 넘어간 상태를 운영자가 인지 가능하게(무성 → 가시화).
#### Acceptance Criteria Delta
- Added:
- snapshot 응답 워치독 상태 노출 + `last_seen` getter. 판정 동작 불변.
#### Impacted Area Delta
- Modules: `trex_core/application/watchdog.py`(last_seen), `trex_core/application/messaging_handlers.py`(_attach_watchdog)
- Tests: tests/application/test_messaging_snapshot_consumer.py::test_snapshot_exposes_watchdog_state_for_holding_slots
#### Delivery Trace Delta
- Batches: BAT-021
- Releases: none
#### Revalidation Impact
- Reverification Needed: yes (BAT-021 에서 완료)
- Existing Implementation Invalidated: no (관측만 추가, 판정 불변)
- Status Recommendation: Implemented (change-req → BAT-021 재검증)
- Follow-up Action: confirm-req-implemented(BAT-021).
#### Validation Plan
- BAT-021: 워치독 만료 holding 슬롯의 expired/last_seen 노출 단위.
+1 -1
View File
@@ -139,8 +139,8 @@
## Recent Change Log Summary
- 2026-06-08 :: REQ-034/021/037/038/039/032/042 change-req (dcy-017, BAT-021): 전략 이탈로 슬롯이 워치독(안전망) 소유로 넘어간 상태를 가시화(issue #12). snapshot RPC 응답에 holding 슬롯 워치독 상태(expired/last_seen) read-only 동봉(best-effort) + `WatchdogStore.last_seen`(REQ-034/021), `trex-core slots` WATCHDOG-OWNED 표시(REQ-032), safety-sell 워치독 소유 진입(WARN)/이탈(INFO)·발동(forced_close·stop_loss=WARN, take_profit·market_close=INFO) 로그+in-process 카운터(REQ-037/038/039), baseline 2문서 정합(REQ-042). best-effort(청산 무차단), Redis 미채택. 전부 change-req(신규 REQ 없음, OQ-3). 단위 335 passed, 인프라 게이트 369 passed, ruff clean. (change-req/confirm-req-implemented)
- 2026-06-08 :: REQ-032/022/021/034/026 change-req (dcy-016, BAT-020): 운영자 CLI 확장 — `universe`/`slots`(snapshot RPC)·`enter`(buy intent RPC)·`exit`(sell intent RPC) 추가(전부 데몬 RPC 통일, 단일 writer 보존, DB 직접 쓰기 없음)(REQ-032), CLI request-reply RPC 클라이언트 재사용(REQ-022), snapshot 요청 워치독 억제 플래그(`touch_watchdog`/`actor="operator"`)로 운영자 조회가 전략 생존 신호로 오인되지 않게(REQ-021/034, R-4), enter/exit 안전장치(확인 프롬프트/`--yes`/live 이중 ack/`--dry-run`)+`--json`+인스턴스 라우팅, baseline 2문서 정합(REQ-026). pause/resume 는 direct-DB 유지. 전부 change-req(신규 REQ 없음, OQ-3). 단위 327 passed, 인프라 게이트 361 passed, ruff clean. (change-req/confirm-req-implemented)
- 2026-06-08 :: REQ-017/019/018/020 change-req (dcy-015, BAT-019): portfolio-hub 404(미등록 포트폴리오)가 retryable 로 분류되어 백오프·상한 없이 영구 재시도되던 버그(issue #13) 정정. 404/400/422→`LedgerPermanentError`(REQ-017), outbox 영속→터미널 `delivery_failed` 즉시 + retryable 지수 백오프+jitter+`next_retry_at`+max_attempts→dead-letter + `list_pending` 백오프 스킵 + 마이그레이션 007(status enum/컬럼) + 백오프 env(REQ-019), 터미널 WARN+카운터 가시화(REQ-018), baseline 3문서 정합(REQ-020). 전부 change-req(신규 REQ 없음, OQ-3). 단위 311 passed, 인프라 게이트 345 passed, ruff clean. (change-req/confirm-req-implemented)
- 2026-06-08 :: REQ-005/048/009 change-req (dcy-014, BAT-018): 당일 슬롯 생성 기준을 시스템 UTC 날짜에서 설정 타임존(기본 KST) 기준 `trading_date` 로 분리(timestamp 는 UTC 유지) + 데몬 1회 preallocation 을 `DailyPreallocationScheduler`(TZ 일자당 1회 멱등 fire, 재시작 없이 다음날 자동 생성)로 대체(REQ-005), TOML `trading_timezone`/`daily_preallocation_time` 키(REQ-048), runtime-flows/data-model 정합(REQ-009). 전부 change-req(신규 REQ 없음, OQ-3; 스케줄러는 REQ-030 패턴 준용). 단위 302 passed, 인프라 게이트 334 passed, ruff clean. (change-req/confirm-req-implemented)
- 2026-06-08 :: REQ-012/013/018/031/014/015 change-req (dcy-013, BAT-017): KIS 외부 주문 취소 시 슬롯이 pending_sell 에 영구 갇히던 버그(issue #9) 정정. KIS `poll_fill` 이 `cncl_yn` 파싱해 외부 취소 감지 — 체결 0 취소→`cancelled`(롤백 자동 복구), 부분 체결 후 취소→`is_partial_cancel`(체결분 원장 1회+qty 차감+holding 복귀, OQ-1 A안)(REQ-012/013/018), 잔여 포지션 정합 연계(REQ-031), pending stale 워치독 Alert-only(`TREX_PENDING_STALE_THRESHOLD`, WARN+카운터, 상태 미변경)(REQ-014), baseline 2문서 정합(REQ-015). 전부 change-req(신규 REQ 없음, OQ-3). 단위 294 passed, 인프라 게이트 326 passed, ruff clean. (change-req/confirm-req-implemented)
- 2026-06-08 :: REQ-013/007/036/002/006/003/015 change-req (dcy-012, BAT-016): 매도 주문이 매수가(entry_price) 지정가로 나가던 버그(issue #10) 정정. 슬롯 `entry_price`→`limit_price`(side 무관 pending 지정가) rename + pending 이탈 clear(REQ-002), 컬럼 rename 마이그레이션 006(REQ-006), `_dispatch` 가 slot.limit_price 단일 필드로 가격/주문유형 결정(REQ-013), SellIntent/sell payload optional limit_price·_admit_sell 운반(REQ-007), SlotService.sell optional 가격→자율 매도 시장가(REQ-036), 컬럼 계약 가드(REQ-003), baseline 3문서 정합(REQ-015). 전부 change-req(신규 REQ 없음, OQ-3). 단위 286 passed, 인프라 게이트 318 passed, ruff clean. (change-req/confirm-req-implemented)