From 6d7bca14609aa2fe728c9920b6e89d19a7ac612d Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Mon, 8 Jun 2026 17:10:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(safety-sell):=20dcy-017=20=EC=9B=8C?= =?UTF-8?q?=EC=B9=98=EB=8F=85=20=EC=86=8C=EC=9C=A0=EA=B6=8C=20=EA=B0=80?= =?UTF-8?q?=EC=8B=9C=ED=99=94=20=E2=80=94=20snapshot=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=20+=20=EB=B0=9C=EB=8F=99=20=EB=A1=9C=EA=B9=85=20(BAT-021)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 전략 이탈로 슬롯이 내부 청산 루틴(워치독) 소유로 넘어간 상태가 무성이라 운영자가 인지할 수 없던 문제(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 --- .../design.md | 41 ++++ .../implementation.md | 39 +++ .../index.md | 34 +++ .../planning.md | 50 ++++ .../verification.md | 71 ++++++ docs/batches/index.md | 1 + ..._20260608_watchdog-ownership-visibility.md | 229 ++++++++++++++++++ docs/discovery/index.md | 3 +- docs/interface-contract.md | 12 +- docs/runtime-flows.md | 12 +- .../req-042_safety-sell-baseline-alignment.md | 38 +++ ...messaging-topology-and-contract-mapping.md | 40 +++ ...eq-037_forced-close-at-safety-sell-loop.md | 39 +++ ...q-038_stop-loss-take-profit-kis-polling.md | 39 +++ .../req-039_market-close-liquidation.md | 39 +++ .../req-032_runtime-pause-resume-control.md | 39 +++ .../Interface/req-034_per-slot-watchdog.md | 39 +++ docs/srs/index.md | 2 +- .../test_messaging_snapshot_consumer.py | 18 ++ .../test_safety_sell_visibility.py | 116 +++++++++ tests/runtime/test_cli_operator.py | 14 ++ trex_core/application/messaging_handlers.py | 38 ++- trex_core/application/safety_sell.py | 59 +++++ trex_core/application/watchdog.py | 7 + trex_core/runtime/cli.py | 11 +- trex_core/runtime/daemon.py | 2 + 26 files changed, 1022 insertions(+), 10 deletions(-) create mode 100644 docs/batches/bat-021_20260608_watchdog-ownership-visibility/design.md create mode 100644 docs/batches/bat-021_20260608_watchdog-ownership-visibility/implementation.md create mode 100644 docs/batches/bat-021_20260608_watchdog-ownership-visibility/index.md create mode 100644 docs/batches/bat-021_20260608_watchdog-ownership-visibility/planning.md create mode 100644 docs/batches/bat-021_20260608_watchdog-ownership-visibility/verification.md create mode 100644 docs/discovery/dcy-017_20260608_watchdog-ownership-visibility.md create mode 100644 tests/application/test_safety_sell_visibility.py diff --git a/docs/batches/bat-021_20260608_watchdog-ownership-visibility/design.md b/docs/batches/bat-021_20260608_watchdog-ownership-visibility/design.md new file mode 100644 index 0000000..3265689 --- /dev/null +++ b/docs/batches/bat-021_20260608_watchdog-ownership-visibility/design.md @@ -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 로 표시. diff --git a/docs/batches/bat-021_20260608_watchdog-ownership-visibility/implementation.md b/docs/batches/bat-021_20260608_watchdog-ownership-visibility/implementation.md new file mode 100644 index 0000000..a3531c8 --- /dev/null +++ b/docs/batches/bat-021_20260608_watchdog-ownership-visibility/implementation.md @@ -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. diff --git a/docs/batches/bat-021_20260608_watchdog-ownership-visibility/index.md b/docs/batches/bat-021_20260608_watchdog-ownership-visibility/index.md new file mode 100644 index 0000000..a38b07b --- /dev/null +++ b/docs/batches/bat-021_20260608_watchdog-ownership-visibility/index.md @@ -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). diff --git a/docs/batches/bat-021_20260608_watchdog-ownership-visibility/planning.md b/docs/batches/bat-021_20260608_watchdog-ownership-visibility/planning.md new file mode 100644 index 0000000..283cb77 --- /dev/null +++ b/docs/batches/bat-021_20260608_watchdog-ownership-visibility/planning.md @@ -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 갱신 안 함). diff --git a/docs/batches/bat-021_20260608_watchdog-ownership-visibility/verification.md b/docs/batches/bat-021_20260608_watchdog-ownership-visibility/verification.md new file mode 100644 index 0000000..a9872b3 --- /dev/null +++ b/docs/batches/bat-021_20260608_watchdog-ownership-visibility/verification.md @@ -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 무관). diff --git a/docs/batches/index.md b/docs/batches/index.md index 92fc9bf..1a17944 100644 --- a/docs/batches/index.md +++ b/docs/batches/index.md @@ -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/) | diff --git a/docs/discovery/dcy-017_20260608_watchdog-ownership-visibility.md b/docs/discovery/dcy-017_20260608_watchdog-ownership-visibility.md new file mode 100644 index 0000000..85cfc6b --- /dev/null +++ b/docs/discovery/dcy-017_20260608_watchdog-ownership-visibility.md @@ -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 diff --git a/docs/discovery/index.md b/docs/discovery/index.md index 00e1e6e..41633a0 100644 --- a/docs/discovery/index.md +++ b/docs/discovery/index.md @@ -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 | \ No newline at end of file +| 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 | \ No newline at end of file diff --git a/docs/interface-contract.md b/docs/interface-contract.md index 6960417..fbf0e66 100644 --- a/docs/interface-contract.md +++ b/docs/interface-contract.md @@ -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) diff --git a/docs/runtime-flows.md b/docs/runtime-flows.md index 594ab50..4cdacc3 100644 --- a/docs/runtime-flows.md +++ b/docs/runtime-flows.md @@ -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 을 즉시 청산하고 당일 전략을 차단한다. diff --git a/docs/srs/Documentation/req-042_safety-sell-baseline-alignment.md b/docs/srs/Documentation/req-042_safety-sell-baseline-alignment.md index d93a98c..662d38e 100644 --- a/docs/srs/Documentation/req-042_safety-sell-baseline-alignment.md +++ b/docs/srs/Documentation/req-042_safety-sell-baseline-alignment.md @@ -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 대조. diff --git a/docs/srs/Integration/req-021_messaging-topology-and-contract-mapping.md b/docs/srs/Integration/req-021_messaging-topology-and-contract-mapping.md index 6b3f021..0b84351 100644 --- a/docs/srs/Integration/req-021_messaging-topology-and-contract-mapping.md +++ b/docs/srs/Integration/req-021_messaging-topology-and-contract-mapping.md @@ -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 단위. diff --git a/docs/srs/Integration/req-037_forced-close-at-safety-sell-loop.md b/docs/srs/Integration/req-037_forced-close-at-safety-sell-loop.md index c9a84fe..b0b6607 100644 --- a/docs/srs/Integration/req-037_forced-close-at-safety-sell-loop.md +++ b/docs/srs/Integration/req-037_forced-close-at-safety-sell-loop.md @@ -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). diff --git a/docs/srs/Integration/req-038_stop-loss-take-profit-kis-polling.md b/docs/srs/Integration/req-038_stop-loss-take-profit-kis-polling.md index 06becc3..8c28a46 100644 --- a/docs/srs/Integration/req-038_stop-loss-take-profit-kis-polling.md +++ b/docs/srs/Integration/req-038_stop-loss-take-profit-kis-polling.md @@ -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 + 카운터 단위. diff --git a/docs/srs/Integration/req-039_market-close-liquidation.md b/docs/srs/Integration/req-039_market-close-liquidation.md index 51294e9..fd23cb9 100644 --- a/docs/srs/Integration/req-039_market-close-liquidation.md +++ b/docs/srs/Integration/req-039_market-close-liquidation.md @@ -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 + 카운터(기존 청산 회귀 동반). diff --git a/docs/srs/Interface/req-032_runtime-pause-resume-control.md b/docs/srs/Interface/req-032_runtime-pause-resume-control.md index 4a7a27c..9ab749c 100644 --- a/docs/srs/Interface/req-032_runtime-pause-resume-control.md +++ b/docs/srs/Interface/req-032_runtime-pause-resume-control.md @@ -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 표시 단위. diff --git a/docs/srs/Interface/req-034_per-slot-watchdog.md b/docs/srs/Interface/req-034_per-slot-watchdog.md index f063a86..fea5610 100644 --- a/docs/srs/Interface/req-034_per-slot-watchdog.md +++ b/docs/srs/Interface/req-034_per-slot-watchdog.md @@ -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 노출 단위. diff --git a/docs/srs/index.md b/docs/srs/index.md index 1f1f40a..980ffeb 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -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) diff --git a/tests/application/test_messaging_snapshot_consumer.py b/tests/application/test_messaging_snapshot_consumer.py index b033813..113e94e 100644 --- a/tests/application/test_messaging_snapshot_consumer.py +++ b/tests/application/test_messaging_snapshot_consumer.py @@ -117,3 +117,21 @@ def test_operator_query_suppresses_watchdog_touch(): ) handler(req) assert wd.updated == [] # dcy-016 R-4: operator query is not a liveness signal + + +def test_snapshot_exposes_watchdog_state_for_holding_slots(): + """dcy-017 / REQ-034: snapshot response carries holding-slot watchdog state.""" + from trex_core.application.watchdog import WatchdogStore + repo, svc, intents = _wire() + sid = _holding_slot(repo, svc) + wd = WatchdogStore() # never updated => expired (strategy absent) + handler = make_snapshot_rpc_handler(repo, svc, watchdog_store=wd, watchdog_timeout_sec=120) + # operator query so the response does not touch the watchdog before we read it + req = RpcRequest( + payload={"view": "all", "trading_date": DAY.isoformat(), "actor": "operator"}, + correlation_id="op", + ) + states = handler(req).payload["watchdog"] + assert len(states) == 1 + assert states[0]["slot_id"] == sid and states[0]["expired"] is True + assert states[0]["last_seen"] is None diff --git a/tests/application/test_safety_sell_visibility.py b/tests/application/test_safety_sell_visibility.py new file mode 100644 index 0000000..7ab625a --- /dev/null +++ b/tests/application/test_safety_sell_visibility.py @@ -0,0 +1,116 @@ +"""Safety-sell watchdog-ownership + fire visibility tests (dcy-017 / REQ-037, issue #12).""" +from __future__ import annotations + +import logging +from datetime import date, datetime, timedelta, timezone +from decimal import Decimal + +from trex_core.application.safety_sell import SafetySellService +from trex_core.application.watchdog import WatchdogStore +from trex_core.domain.slot import SlotService, SlotState +from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository +from trex_core.runtime.config import SafetySellConfig + +_DAY = date(2026, 6, 7) +_NOW = datetime(2026, 6, 7, 6, 0, 0, tzinfo=timezone.utc) + + +class _FakeBroker: + def __init__(self): + self.prices = {} + + def name(self): return "fake" + def submit_order(self, req): ... + def poll_fill(self, *a, **k): ... + def cancel_order(self, *a, **k): ... + def get_balance(self): return [] + def get_current_price(self, symbol): return self.prices.get(symbol) + + +def _cfg(**over): + base = dict(watchdog_timeout_sec=120, watchdog_grace_period_sec=0, + forced_close_check_interval_sec=60, stop_loss_poll_interval_sec=30, + market_close_liquidation_time=None) + base.update(over) + return SafetySellConfig(**base) + + +def _wire(**cfg_over): + repo = InMemorySlotRepository() + svc = SlotService(repo) + svc.preallocate_daily_slots(_DAY, 5) + broker = _FakeBroker() + watchdog = WatchdogStore() + service = SafetySellService( + slot_repo=repo, slot_service=svc, broker=broker, watchdog=watchdog, + config=_cfg(**cfg_over), instance_key="inst-1", + ) + return repo, svc, broker, watchdog, service + + +def _holding(svc, repo, *, symbol="005930", **fields): + idle = repo.find_idle_slot(_DAY) + p = svc.transit_slot(idle.slot_id, SlotState.IDLE, SlotState.PENDING_BUY, + reason="buy", actor="t", symbol=symbol, portfolio_id="PF-1", **fields) + return svc.transit_slot(p.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING, + reason="filled", actor="t", avg_price=50000.0, qty=1.0) + + +def test_watchdog_ownership_entry_warns(caplog): + repo, svc, broker, watchdog, service = _wire() + slot = _holding(svc, repo) # never polled => watchdog-expired (strategy absent) + with caplog.at_level(logging.WARNING): + service._update_ownership(_NOW) + assert service.counters.get("owned:enter") == 1 + rec = [r for r in caplog.records if "watchdog ownership entered" in r.message] + assert rec and str(slot.slot_id) in rec[0].message and "inst-1" in rec[0].message + + +def test_watchdog_ownership_exit_on_return(caplog): + repo, svc, broker, watchdog, service = _wire() + slot = _holding(svc, repo) + service._update_ownership(_NOW) # enter (expired) + watchdog.update(str(slot.slot_id)) # strategy returns (polls) + with caplog.at_level(logging.INFO): + service._update_ownership(_NOW) # exit + assert service.counters.get("owned:exit") == 1 + assert any("watchdog ownership released" in r.message for r in caplog.records) + + +def test_no_ownership_churn_when_stable(): + repo, svc, broker, watchdog, service = _wire() + _holding(svc, repo) + service._update_ownership(_NOW) + service._update_ownership(_NOW) # same set => no new enter + assert service.counters.get("owned:enter") == 1 + + +def test_forced_close_fire_logs_warn(caplog): + repo, svc, broker, watchdog, service = _wire() + slot = _holding(svc, repo, forced_close_at=_NOW - timedelta(seconds=1)) + with caplog.at_level(logging.WARNING): + fired = service.check_forced_close(_NOW) + assert slot.slot_id in fired + assert service.counters.get("fire:forced_close_at") == 1 + rec = [r for r in caplog.records if "safety-sell fired" in r.message + and r.levelno == logging.WARNING] + assert rec and "forced_close_at" in rec[0].message and "inst-1" in rec[0].message + + +def test_take_profit_fire_logs_info(caplog): + repo, svc, broker, watchdog, service = _wire() + slot = _holding(svc, repo, take_profit_price=Decimal("60000")) + broker.prices["005930"] = Decimal("61000") + with caplog.at_level(logging.INFO): + fired = service.check_prices(_NOW) + assert slot.slot_id in fired + assert service.counters.get("fire:take_profit") == 1 + rec = [r for r in caplog.records if "safety-sell fired" in r.message] + assert rec and rec[0].levelno == logging.INFO # take_profit is INFO (OQ-2) + + +def test_grace_period_suppresses_ownership(): + repo, svc, broker, watchdog, service = _wire(watchdog_grace_period_sec=9999) + _holding(svc, repo) + service._update_ownership(_NOW) + assert service.counters.get("owned:enter") is None # grace => no judgement diff --git a/tests/runtime/test_cli_operator.py b/tests/runtime/test_cli_operator.py index 6667d71..236ddaf 100644 --- a/tests/runtime/test_cli_operator.py +++ b/tests/runtime/test_cli_operator.py @@ -53,6 +53,20 @@ def test_slots_status_holding_maps_to_held_view(): assert rpc.calls[0]["payload"]["view"] == "held" +def test_slots_marks_watchdog_owned(capsys): + """dcy-017: slots flags strategy-absent (watchdog-owned) positions.""" + resp = { + "assets": [{"asset_id": "PF-1", "symbol": "005930", "asset_quantity": 10, + "active_strategy_id": "orb"}], + "view": "held", "available_slots": 9, "degraded": False, "reason": None, + "watchdog": [{"slot_id": "S-1", "portfolio_id": "PF-1", "symbol": "005930", + "expired": True, "last_seen": None}], + } + cli.main(["slots", "--status", "held"], rpc=_RpcRecorder(resp)) + out = capsys.readouterr().out + assert "watchdog_owned=1" in out and "WATCHDOG-OWNED" in out + + def test_instance_shortcut_routes_to_instance_queue(): rpc = _RpcRecorder(_SNAPSHOT) cli.main(["universe", "--instance", "inst-2"], rpc=rpc) diff --git a/trex_core/application/messaging_handlers.py b/trex_core/application/messaging_handlers.py index 4a59fab..3e0107b 100644 --- a/trex_core/application/messaging_handlers.py +++ b/trex_core/application/messaging_handlers.py @@ -168,6 +168,7 @@ def make_snapshot_rpc_handler( pause_store: Optional[PauseStore] = None, default_total_slots: int = 10, watchdog_store: Optional[WatchdogStore] = None, + watchdog_timeout_sec: int = 120, killswitch_store=None, ): """Build the RPC handler for `trex.snapshot.req` (REQ-028). @@ -225,7 +226,9 @@ def make_snapshot_rpc_handler( trading_date=day, killswitch_store=killswitch_store, ) - return RpcResponse(payload=envelope.to_dict()) + payload = envelope.to_dict() + _attach_watchdog(payload, slot_repo, day, watchdog_store, watchdog_timeout_sec) + return RpcResponse(payload=payload) # --- BAT-007 legacy stub-shape path --- view_raw = str(body.get("view") or "all") @@ -236,7 +239,7 @@ def make_snapshot_rpc_handler( if strategy_filter is not None: slots = [s for s in slots if s.strategy_id == strategy_filter] cap = slot_service.compute_slot_capacity(day, max(1, len(slot_repo.list_slots(day)))) - return RpcResponse(payload={ + payload = { "view": view_raw, "strategy_id": strategy_filter, "trading_date": day.isoformat(), @@ -248,11 +251,40 @@ def make_snapshot_rpc_handler( "total_slots": cap.total_slots, "scarcity_ratio": cap.scarcity_ratio, }, - }) + } + _attach_watchdog(payload, slot_repo, day, watchdog_store, watchdog_timeout_sec) + return RpcResponse(payload=payload) return handler +def _attach_watchdog(payload, slot_repo, day, watchdog_store, timeout_sec) -> None: + """Attach holding-slot watchdog state to a snapshot response (dcy-017/REQ-034). + + `watchdog` = list of {slot_id, portfolio_id, symbol, expired, last_seen} for + holding slots — lets `trex-core slots` surface watchdog ownership (strategy + absence). Best-effort: any failure leaves the response unchanged (FR-4).""" + if watchdog_store is None: + return + try: + states = [] + for s in slot_repo.list_slots(day): + if s.status is not SlotState.HOLDING: + continue + sid = str(s.slot_id) + last = watchdog_store.last_seen(sid) + states.append({ + "slot_id": sid, + "portfolio_id": s.portfolio_id, + "symbol": s.symbol, + "expired": watchdog_store.is_expired(sid, timeout_sec), + "last_seen": last.isoformat() if last else None, + }) + payload["watchdog"] = states + except Exception: # noqa: BLE001 - visibility must never break the response + return + + # ---- pause/resume RPC handler (REQ-032 AC-4) -------------------------------- def make_pause_rpc_handler(pause_store: PauseStore): diff --git a/trex_core/application/safety_sell.py b/trex_core/application/safety_sell.py index 0393408..c3c7872 100644 --- a/trex_core/application/safety_sell.py +++ b/trex_core/application/safety_sell.py @@ -56,18 +56,72 @@ class SafetySellService: broker: BrokerPort, watchdog: WatchdogStore, config: SafetySellConfig, + instance_key: str = "default", ) -> None: self._repo = slot_repo self._svc = slot_service self._broker = broker self._watchdog = watchdog self._cfg = config + self._instance_key = instance_key # dcy-017 FR-5: multi-instance label self._last_liquidation_date: date_cls | None = None self._last_forced_close_at: datetime | None = None self._last_price_check_at: datetime | None = None # BAT-014 / REQ-052: per-slot retry cooldown for persistent rejection. self._last_attempt: dict = {} # slot_id -> last sell-attempt datetime self._attempt_count: dict = {} # slot_id -> consecutive attempt count + # dcy-017 / REQ-037 (issue #12): watchdog-ownership visibility. + self._owned: set = set() # holding slot_ids currently watchdog-owned + self.counters: dict[str, int] = {} # phase/event counters (in-process metrics) + + # ----------------------------------------------- dcy-017 visibility helpers + def _count(self, key: str) -> None: + self.counters[key] = self.counters.get(key, 0) + 1 + + # WARN-level fire reasons (attention events); others log at INFO. + _WARN_REASONS = frozenset({"forced_close_at", "stop_loss"}) + + def _log_fire(self, slot: Slot, reason: str) -> None: + """Visibility for an actual safety-sell fire (dcy-017 FR-1, OQ-2).""" + self._count(f"fire:{reason}") + level = logging.WARNING if reason in self._WARN_REASONS else logging.INFO + _log.log( + level, + "safety-sell fired: phase=%s slot=%s portfolio_id=%s reason=%s instance_key=%s", + reason, slot.slot_id, slot.portfolio_id, reason, self._instance_key, + ) + + def _update_ownership(self, now: datetime) -> None: + """Log watchdog-ownership entry (strategy absent) / exit (returned). + + dcy-017 FR-1: WARN on entry (a holding slot's strategy went silent so the + safety net now owns it), INFO on exit. Counters track both. Best-effort — + never raises into the tick (NFR-1).""" + try: + if self._in_grace(): + return # no expiry judgement during grace (REQ-034) + current = { + s.slot_id for s in self._holding(now) if self._watchdog_expired(s) + } + by_id = {s.slot_id: s for s in self._holding(now)} + for sid in current - self._owned: + self._count("owned:enter") + s = by_id.get(sid) + _log.warning( + "watchdog ownership entered (strategy absent): slot=%s " + "portfolio_id=%s phase=watchdog_owned instance_key=%s", + sid, (s.portfolio_id if s else None), self._instance_key, + ) + for sid in self._owned - current: + self._count("owned:exit") + _log.info( + "watchdog ownership released (strategy returned): slot=%s " + "phase=watchdog_released instance_key=%s", + sid, self._instance_key, + ) + self._owned = current + except Exception: # noqa: BLE001 - visibility must not break the tick + return @property def base_tick_sec(self) -> float: @@ -155,6 +209,7 @@ class SafetySellService: if self._in_cooldown(s.slot_id, now): continue # REQ-052: persistent-rejection backoff if self._attempt_sell(s, "forced_close_at", now): + self._log_fire(s, "forced_close_at") fired.append(s.slot_id) return fired @@ -184,9 +239,11 @@ class SafetySellService: continue # unavailable — skip, retry next tick (no false trigger) if s.stop_loss_price is not None and price <= s.stop_loss_price: if self._attempt_sell(s, "stop_loss", now): + self._log_fire(s, "stop_loss") fired.append(s.slot_id) elif s.take_profit_price is not None and price >= s.take_profit_price: if self._attempt_sell(s, "take_profit", now): + self._log_fire(s, "take_profit") fired.append(s.slot_id) return fired @@ -205,6 +262,7 @@ class SafetySellService: fired = [] for s in self._holding(now): # watchdog/grace ignored (AC-2/AC-6) if self._safe_sell(s, "market_close_liquidation"): + self._log_fire(s, "market_close_liquidation") fired.append(s.slot_id) self._last_liquidation_date = today return fired @@ -214,6 +272,7 @@ class SafetySellService: """One worker tick — market-close, then cadence-gated forced_close + prices.""" now = now or datetime.now(tz=timezone.utc) self._prune_cooldown(now) # REQ-052 FR-4: drop state for non-holding slots + self._update_ownership(now) # dcy-017 FR-1: watchdog ownership enter/exit visibility try: self.check_market_close(now) except Exception: # noqa: BLE001 diff --git a/trex_core/application/watchdog.py b/trex_core/application/watchdog.py index fcf9605..24c5562 100644 --- a/trex_core/application/watchdog.py +++ b/trex_core/application/watchdog.py @@ -13,6 +13,7 @@ is needed. from __future__ import annotations from datetime import datetime, timezone +from typing import Optional class WatchdogStore: @@ -26,6 +27,12 @@ class WatchdogStore: """Record that slot_id appeared in a snapshot RPC response.""" self._store[slot_id] = datetime.now(tz=timezone.utc) + def last_seen(self, slot_id: str) -> Optional[datetime]: + """Return the last time slot_id was seen, or None if never (dcy-017). + + Read-only accessor for watchdog-ownership visibility (REQ-034).""" + return self._store.get(slot_id) + def is_expired(self, slot_id: str, timeout_sec: int) -> bool: """Return True if slot_id has not been seen for longer than timeout_sec. diff --git a/trex_core/runtime/cli.py b/trex_core/runtime/cli.py index 5b538c3..aaa21ac 100644 --- a/trex_core/runtime/cli.py +++ b/trex_core/runtime/cli.py @@ -121,13 +121,18 @@ def _cmd_slots(args, cfg, rpc) -> int: resp = rpc(topology.snapshot_queue(instance), payload, correlation_id=str(uuid.uuid4()), timeout=args.timeout) assets = resp.get("assets", []) + # dcy-017: holding-slot watchdog state (strategy-absent ownership). + owned = {w.get("portfolio_id"): w for w in resp.get("watchdog", []) if w.get("expired")} if args.json: _emit(resp, as_json=True) else: - print(f"instance={instance} view={view} count={len(assets)}") + print(f"instance={instance} view={view} count={len(assets)} " + f"watchdog_owned={len(owned)}") for a in assets: - print(f" {a.get('asset_id')}\t{a.get('symbol')}\tqty={a.get('asset_quantity')}\t" - f"strategy={a.get('active_strategy_id')}") + pid = a.get("asset_id") + tag = " [WATCHDOG-OWNED: strategy absent]" if pid in owned else "" + print(f" {pid}\t{a.get('symbol')}\tqty={a.get('asset_quantity')}\t" + f"strategy={a.get('active_strategy_id')}{tag}") return 0 diff --git a/trex_core/runtime/daemon.py b/trex_core/runtime/daemon.py index 89081bb..aeb5e44 100644 --- a/trex_core/runtime/daemon.py +++ b/trex_core/runtime/daemon.py @@ -202,6 +202,7 @@ class Daemon: broker=self.broker, watchdog=self.watchdog, config=config.safety_sell, + instance_key=config.instance_key, ) # ------------------------------------------------------------ buy symbol resolve @@ -267,6 +268,7 @@ class Daemon: pause_store=self.pause_store, default_total_slots=self.config.total_slots, watchdog_store=self.watchdog, + watchdog_timeout_sec=self.config.safety_sell.watchdog_timeout_sec, killswitch_store=self.killswitch_store, ) transport.consume_rpc_forever(