From 63da87c9b3a9425d988a2ccfed1d7c0ef4641b4e Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Mon, 8 Jun 2026 19:54:42 +0900 Subject: [PATCH] =?UTF-8?q?fix(cli):=20BAT-022=20slots=20=EB=AA=85?= =?UTF-8?q?=EB=A0=B9=EC=9D=B4=20universe=20=EB=8C=80=EC=8B=A0=20=EC=8B=A4?= =?UTF-8?q?=EC=A0=9C=20=EC=8A=AC=EB=A1=AF=20=EC=83=81=ED=83=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20(dcy-016=20=ED=9B=84=EC=86=8D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `trex-core slots` 가 snapshot RPC 의 universe `assets`(portfolio)를 슬롯 상태로 오인 표시하던 갭 정정. snapshot envelope 이 슬롯 상태를 운반하지 않아 발생 (10개 슬롯인데 universe 1개·status 없음 — BAT-007 HTTP 슬롯 read 제거 후 슬롯 상태 read 표면 부재가 근인). - REQ-028 (CHG-20260608-01): snapshot envelope 에 read-only per-slot `slots[]` additive 추가(AC-4). strategy `assets[]`(universe, no-price) 불변. - REQ-032 (CHG-20260608-03): `_cmd_slots` 가 envelope `slots[]`(실제 슬롯 상태) 표시 + `--status` 를 슬롯 status 기준 client-side 필터 + watchdog 결합. `universe` 는 `assets[]` 유지. `universe --json` 도 universe 데이터로 scope. - canonical `portfolio.slot_to_dict` 단일화 — BAT-007 stub / BAT-008 envelope 두 경로가 동일 슬롯 스키마(외부명 `entry_price` 유지, dcy-012 호환). - interface-contract 에 `slots[]` 계약 + `assets[]` vs `slots[]` 구분 반영. 검증: 단위 338 passed + ruff clean + 신규 코드 데몬으로 실 PostgreSQL/RabbitMQ 운영 재현 PASS(slots=10·status·DB 일치·universe 분리·비침습). REQ-028/032 Implemented, BAT-022 release-candidate. pytest 인프라 게이트는 Human Approval Memo 수용(운영 재현이 실 경로 커버). Co-Authored-By: Claude Opus 4.8 --- .../design.md | 50 ++++++++++++ .../implementation.md | 73 +++++++++++++++++ .../index.md | 35 ++++++++ .../planning.md | 44 ++++++++++ .../verification.md | 73 +++++++++++++++++ docs/batches/index.md | 1 + docs/interface-contract.md | 22 ++++- ...shot-read-message-contract-and-envelope.md | 80 ++++++++++++++++++- .../req-032_runtime-pause-resume-control.md | 72 +++++++++++++++++ docs/srs/index.md | 2 +- tests/application/test_snapshot_handler.py | 5 +- tests/application/test_snapshot_selection.py | 6 +- tests/runtime/test_cli_operator.py | 54 +++++++++++-- trex_core/application/messaging_handlers.py | 23 ++---- trex_core/application/portfolio.py | 37 +++++++++ trex_core/runtime/cli.py | 43 ++++++---- 16 files changed, 574 insertions(+), 46 deletions(-) create mode 100644 docs/batches/bat-022_20260608_slot-state-snapshot-visibility/design.md create mode 100644 docs/batches/bat-022_20260608_slot-state-snapshot-visibility/implementation.md create mode 100644 docs/batches/bat-022_20260608_slot-state-snapshot-visibility/index.md create mode 100644 docs/batches/bat-022_20260608_slot-state-snapshot-visibility/planning.md create mode 100644 docs/batches/bat-022_20260608_slot-state-snapshot-visibility/verification.md diff --git a/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/design.md b/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/design.md new file mode 100644 index 0000000..0e02f6b --- /dev/null +++ b/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/design.md @@ -0,0 +1,50 @@ +# BAT-022 Design + +- Status: draft +- Batch: BAT-022 +- Profile: standard + +## 설계 목표 +- snapshot envelope 에 슬롯 단위 read-only `slots[]` 를 additive 추가해, `trex-core slots` 가 universe 가 아닌 데몬의 실제 슬롯 상태를 표시하게 한다. strategy-facing `assets[]` 계약은 비파괴. + +## 1. snapshot envelope `slots[]` 스키마 (REQ-028 AC-4) +- envelope top-level 에 `slots` 키 추가(additive). 항상 배열, 비면 `[]`(null 금지). +- 각 항목(per Slot, `slot_repo.list_slots(trading_date)` 기준): + - `slot_id`: str (UUID) + - `status`: str — `idle`|`pending_buy`|`holding`|`pending_sell`|`closed` (SlotState 값) + - `symbol`: str|null + - `qty`: float|null + - `limit_price`: float|null (pending 주문 지정가; dcy-012 의 side-무관 필드) + - `avg_price`: float|null (매수 체결가/평단) + - `portfolio_id`: str|null (= asset_id) +- **경계**: `assets[]`(portfolio 단위, strategy-facing, no-price)와 독립. `slots[]` 는 operator-facing 이라 가격(`limit_price`/`avg_price`) 노출 허용(REQ-028 AC-3 의 no-price 는 `assets[]` 한정). +- meta(`view`/`available_slots`/`degraded`/`reason`)·`assets[]`·`watchdog[]` 불변. + +## 2. envelope 산출 (`build_snapshot_envelope`, portfolio.py) +- 기존 `assets[]`/`available_slots`/degraded 산출 유지. +- `slots = [ _slot_to_dict(s) for s in slot_repo.list_slots(trading_date) ]` 추가, `to_dict()` payload 에 `"slots": slots` 동봉. +- `_slot_to_dict` 는 Slot 모델 필드를 위 스키마로 매핑(없는 값은 null). +- best-effort 불필요(순수 read, 외부 I/O 없음) — 단 예외가 envelope 산출 전체를 깨지 않도록 슬롯 매핑은 방어적으로. + +## 3. CLI `slots` (REQ-032, cli.py `_cmd_slots`) +- 요청 payload 는 기존대로 `actor="operator"`, `touch_watchdog=False`(비침습) 유지. +- 응답에서 `slots = resp.get("slots", [])` 를 읽는다(기존 `assets` 대신). +- `--status` 필터를 **슬롯 status 기준**으로 적용: + - `all` → 전체, `holding`/`held` → status==holding, `buyable` → status==idle (가용 슬롯), (추가) `pending` → pending_buy|pending_sell 고려. + - 매핑 표는 구현에서 확정(planning Design Gate (d)). +- 출력: `instance=… view=… count= watchdog_owned=` 헤더 + 슬롯별 `slot_id status symbol qty limit_price/avg_price portfolio_id` + watchdog-owned 태그(slot.portfolio_id ↔ watchdog expired 매칭). +- `--json` 은 envelope(또는 `slots`) 그대로 JSON. +- `_cmd_universe` 는 `assets[]` 유지(변경 없음) — 두 명령 표면 분리. + +## 4. watchdog 결합 +- 기존 `watchdog[]`(expired by portfolio_id, dcy-017)와 `slots[]` 를 CLI 에서 결합: 슬롯의 `portfolio_id` 가 watchdog expired 집합에 있으면 `[WATCHDOG-OWNED: strategy absent]` 태그. + +## 5. 문서 (interface-contract) +- snapshot 응답 계약에 `slots[]` 필드(스키마 위와 동일) 추가. `assets[]` vs `slots[]` 구분(universe vs 데몬 슬롯) 명시. + +## best-effort / 안전 +- 본 변경은 read-only 노출 + CLI 표시라 매매·청산·워치독 판정에 영향 없음. 슬롯 도메인/전이 불변. + +## 미해결/구현 확정 항목 +- `--status` → 슬롯 status 매핑 최종표(특히 `buyable`/`pending` 의미). +- `slots[]` 항목에 `trading_date` 포함 여부(현재 미포함, 요청이 단일 trading_date 라 불요). diff --git a/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/implementation.md b/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/implementation.md new file mode 100644 index 0000000..c08a05d --- /dev/null +++ b/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/implementation.md @@ -0,0 +1,73 @@ +# BAT-022 Implementation + +- Status: in-delivery +- Batch: BAT-022 +- Profile: standard + +## Changed Files +- `trex_core/application/portfolio.py` — `SnapshotEnvelope` 에 `slots: list[dict]`(additive) + `to_dict()` 동봉, **canonical `slot_to_dict(s)`**(operator-facing: slot_id/trading_date/status/symbol/qty/avg_price/entry_price/allocated_capital/strategy_id/sell_strategy_id/portfolio_id/forced_close_at), `build_snapshot_envelope` 가 `slot_to_dict` 로 `slots[]`(전체 슬롯 테이블) 산출. (REQ-028 AC-4) +- `trex_core/application/messaging_handlers.py` — 로컬 `_slot_to_dict` 를 제거하고 `portfolio.slot_to_dict` import(단일 source of truth → BAT-007 stub 경로와 BAT-008 envelope 경로가 **동일 슬롯 스키마** 산출). `_slot_to_dict = slot_to_dict` alias 유지. +- `trex_core/runtime/cli.py` — `_cmd_slots` 가 `resp["slots"]`(실제 슬롯 상태)를 읽어 status 포함 표시, `--status` 를 슬롯 status 기준 client-side 필터(all/holding/buyable=idle/pending), watchdog-owned 를 slot.portfolio_id 로 결합; `--status` choices 에 `pending` 추가. 가격은 `entry_price`(canonical 외부명) 표시. `_cmd_universe` 는 `assets[]` 유지. (REQ-032 CHG-20260608-03) +- `docs/interface-contract.md` — snapshot 응답에 `slots[]` 스키마 계약 추가(canonical shape), `assets[]` vs `slots[]` 구분 명시, CLI↔RPC 매핑 행 정정(`slots` = slot 상태). (REQ-028/032 baseline 동기화) +- `tests/application/test_snapshot_selection.py`, `tests/application/test_snapshot_handler.py` — envelope key set 에 `slots` 추가, no-price 검증을 `assets[]` 로 scope. +- `tests/runtime/test_cli_operator.py` — `_SNAPSHOT` 에 canonical `slots[]` 추가; `slots` 가 universe 가 아닌 슬롯 상태 표시·`--status` 슬롯 status 필터·watchdog 결합 테스트로 갱신. + +## Post-verification 보정 (BAT-022 범위 내, REQ-032) +- 운영 사용 중 발견: `universe --json` 이 envelope 전체(assets + 신규 `slots[]` + `watchdog[]` + meta)를 덤프해 출력이 과다. envelope 에 `slots[]` 를 추가한 부수효과 — `universe` 는 universe(assets)만 보여야 함. +- 수정: `trex_core/runtime/cli.py` `_cmd_universe` 의 json 을 `{instance, view, assets, available_slots, degraded, reason}` 로 scope(`slots[]`/`watchdog[]` 제외 — `slots` 명령 소관). `slots --json` 은 이미 `{instance, status, slots}` 로 scope 되어 일관. +- 검증: 신규 단위 `tests/runtime/test_cli_operator.py::test_universe_json_is_scoped_to_universe`(키셋·누출 없음) + 라이브 `trex-core universe --json` → keys={assets,available_slots,degraded,instance,reason,view}, slots/watchdog 누출 없음. cli 17 passed, ruff clean. + +## 검증 중 발견 (중요) +- BAT-007 stub 경로(`messaging_handlers`)는 이미 `_slot_to_dict` 로 `slots[]` 를 반환하고 있었다 — 갭은 **BAT-008 envelope 경로(snapshot_store 연결 시, 운영 배포)에만** 존재했다(`build_snapshot_envelope` 이 slots 미동봉). 두 경로의 슬롯 dict 스키마가 달라 CLI 일관성도 깨질 뻔해, canonical `slot_to_dict` 로 통일(외부명 `entry_price` 유지, dcy-012 호환). + +## Execution Log +1. portfolio.py: `slots[]` envelope 필드 + `_slot_to_dict` + builder 동봉. +2. cli.py: `_cmd_slots` 가 `slots[]` 읽기·status 필터·watchdog 결합으로 정정, `universe` 분리 유지. +3. 기존 테스트 4건(OLD 동작 검증)을 새 계약으로 갱신. +4. 검증 수행(아래). + +## Validation +- ruff: `ruff check`(변경 파일) → All checks passed. +- 타깃 단위: `pytest tests/application/test_snapshot_selection.py tests/application/test_snapshot_handler.py tests/runtime/test_cli_operator.py` → 31 passed. +- 전체 회귀: `pytest -q` → **338 passed, 1 failed, 34 skipped**. 유일 실패 `tests/adapters/broker/test_kis.py::test_sandbox_token_and_submit` 는 KIS 샌드박스 env(`TREX_KIS_BROKER_BASE_URL` 이 live URL)에 의존하는 **사전 환경 이슈**로 본 변경(portfolio/cli)과 무관. + +## Remaining Risks +- 인프라 게이트(실 PostgreSQL/RabbitMQ)·운영 재현(`trex-core slots` 가 실제 슬롯 N개 status 표시)은 verification 단계에서 확인 필요. +- KIS 샌드박스 env 테스트 실패는 본 batch 범위 밖(환경 설정) — verification 에서 분리 기록. + +## 구현 단계 + +### M1 — REQ-028 envelope `slots[]` +- [ ] `trex_core/application/portfolio.py` `build_snapshot_envelope` 에 `slots[]` 산출 추가. + - `_slot_to_dict(slot)` 헬퍼: `{slot_id, status, symbol, qty, limit_price, avg_price, portfolio_id}` 매핑(없는 값 null). + - `slot_repo.list_slots(trading_date)` 순회 → `slots` 리스트, `to_dict()` payload 에 `"slots"` 동봉. 빈 배열 `[]`. +- [ ] `assets[]`/meta/`watchdog[]`/degraded 불변 확인. + +### M2 — REQ-032 CLI `slots` +- [ ] `trex_core/runtime/cli.py` `_cmd_slots`: + - `slots = resp.get("slots", [])` 로 변경(기존 `assets`). + - `--status` → 슬롯 status 필터 매핑(all/holding(held)/buyable(idle)/pending). + - 출력: `count=len(slots)` + 슬롯별 `slot_id status symbol qty price portfolio_id` + watchdog-owned 태그(portfolio_id 매칭). + - `--json` 경로 유지. +- [ ] `_cmd_universe` 무변경(assets[] 유지) 확인. + +### M3 — 문서 +- [ ] `docs/interface-contract.md` snapshot 응답에 `slots[]` 필드 계약 + `assets[]` vs `slots[]` 구분 명시. +- [ ] 필요 시 `docs/runtime-flows.md` 보강. + +### M4 — 테스트/게이트 +- [ ] envelope `slots[]` 스키마 단위(빈 배열·status enum·가격 필드 포함). +- [ ] `slots` CLI 가 슬롯 상태(status 포함) 표시, universe(`assets[]`)와 구분. +- [ ] `--status` 필터 케이스(idle/holding/pending). +- [ ] 전체 회귀 + ruff + 인프라 게이트 green. + +## 변경 파일 (예상) +- `trex_core/application/portfolio.py` (`build_snapshot_envelope`, `_slot_to_dict`) +- `trex_core/runtime/cli.py` (`_cmd_slots`) +- `docs/interface-contract.md` +- tests: snapshot envelope `slots[]`, cli `slots` 슬롯 상태 표시 + +## 구현 노트 +- 순수 read 노출 — 슬롯 도메인/전이/워치독 판정/청산 무변경. +- 비침습: 요청은 `actor="operator"`/`touch_watchdog=false` 유지(dcy-016). +- 가격 노출은 `slots[]`(operator) 한정 — strategy `assets[]` no-price 규칙 불변. diff --git a/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/index.md b/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/index.md new file mode 100644 index 0000000..3e0aa7e --- /dev/null +++ b/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/index.md @@ -0,0 +1,35 @@ +# BAT-022: 슬롯 상태 스냅샷 가시화 — `slots` 명령이 universe 대신 실제 슬롯 상태 표시 (dcy-016 후속) + +- Status: release-candidate +- Profile: standard +- Source Discovery: dcy-016 (후속 change-req) +- Included REQ: REQ-028 (CHG-20260608-01), REQ-032 (CHG-20260608-03) + +## Summary +- dcy-016 으로 추가된 `trex-core slots` 가 snapshot RPC 의 universe `assets`(portfolio: asset_id/symbol/asset_quantity)를 **슬롯 상태로 오인 표시**하던 갭을 정정한다(운영 관측: 10개 슬롯인데 universe 1개·status 없음). 근인은 BAT-007 에서 HTTP 슬롯 read(REQ-008/025) 제거 후 **슬롯 상태 read 표면 부재** + snapshot envelope 이 슬롯 상태를 운반하지 않음. +- 핵심: snapshot envelope 에 read-only per-slot `slots[]`(`slot_id`/`status`/`symbol`/`qty`/`limit_price`/`avg_price`/`portfolio_id`)를 **additive** 추가(REQ-028 AC-4)하고, `trex-core slots` 가 이를 표시(REQ-032). strategy-facing `assets[]`(universe) 계약은 불변, `universe` 명령은 `assets[]` 유지로 둘을 명확히 분리. dcy-017 의 per-slot `watchdog[]` 동봉 선례와 동일 방식. + +## Scope +- In Scope: + - snapshot envelope 에 read-only `slots[]` 필드 추가 (REQ-028 AC-4) — `build_snapshot_envelope` + - `trex-core slots` 가 `slots[]`(실제 슬롯 상태, `status` 포함)를 표시, `--status` 필터를 슬롯 상태 기준으로 적용 (REQ-032) + - `universe` 명령은 `assets[]`(universe) 유지 — 두 명령 표면 분리 + - watchdog-owned 표시(REQ-032 CHG-20260608-02)를 `slots[]` 항목과 결합 + - interface-contract 정합 (snapshot 응답 `slots[]` 필드) +- Out of Scope: + - strategy-facing `assets[]`(universe) 계약 변경 (불변) + - 슬롯 상태 도메인/전이 로직 변경 (가시화·노출만) + - WatchdogStore in-memory → 영속 승격 + - 신규 슬롯 read RPC/HTTP 표면 (기존 snapshot envelope 확장으로 충족) + +## 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-016 후속). REQ-028 CHG-20260608-01 / REQ-032 CHG-20260608-03 change-req 로 두 REQ 가 Implemented→Approved 로 내려옴. +- 설계 정합성: dcy-017/BAT-021 이 이미 envelope 에 top-level `watchdog[]` 를 additive 동봉한 선례 → `slots[]` 도 동일 패턴(additive, strict envelope 비파괴). +- 1차 게이트: 전체 회귀 green — strategy `assets[]`(REQ-028 AC-1~3)·snapshot 동작 무회귀(필드 추가만). +- `slots[]` 와 `watchdog[]` 는 둘 다 슬롯 단위 read-only projection — CLI `slots` 에서 결합 표시. diff --git a/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/planning.md b/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/planning.md new file mode 100644 index 0000000..1de80c4 --- /dev/null +++ b/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/planning.md @@ -0,0 +1,44 @@ +# BAT-022 Planning + +- Status: draft +- Batch: BAT-022 +- Profile: standard + +## Included REQ +- REQ-028 (Interface, change-req CHG-20260608-01): snapshot envelope 에 read-only per-slot `slots[]` 추가(AC-4). strategy `assets[]` 불변. +- REQ-032 (Interface, change-req CHG-20260608-03): `trex-core slots` 가 universe `assets[]` 대신 envelope `slots[]`(실제 슬롯 상태) 표시. `universe` 는 `assets[]` 유지. + +## Delivery Plan +1. REQ-028(서버 envelope): `trex_core/application/portfolio.py` `build_snapshot_envelope` 에 `slots[]` 산출 추가 — `slot_repo.list_slots(day)` 로 per-slot 상태(`slot_id`/`status`/`symbol`/`qty`/`limit_price`/`avg_price`/`portfolio_id`) 매핑. 빈 배열은 `[]`. `assets[]`/meta/degraded 불변. (필요 시 `messaging_handlers.py` 핸들러 배선 확인.) +2. REQ-032(CLI): `trex_core/runtime/cli.py` `_cmd_slots` 가 `resp.get("slots", [])` 를 읽어 슬롯 상태(status 포함) 출력. `--status all|held|holding|buyable` 를 슬롯 status 기준으로 필터. watchdog-owned 표시(기존)를 `slots[]` 항목과 결합(slot 의 portfolio_id ↔ watchdog expired 매칭). `_cmd_universe` 는 `assets[]` 유지. +3. 문서: interface-contract 의 snapshot 응답에 `slots[]` 필드 계약 추가(REQ-028 Impacted). runtime-flows 필요 시 보강. +4. 테스트: envelope `slots[]` 스키마(빈 배열·status enum) + `slots` CLI 가 실제 슬롯 상태 표시(universe 와 구분) + 전체 회귀 + ruff + 인프라 게이트. + +## Dependencies +- Internal: + - dcy-005/BAT-008 snapshot envelope(REQ-028) — 확장 대상. + - dcy-016/BAT-020 운영자 CLI `slots`(REQ-032) — 정정 대상(현재 universe 표시). + - dcy-017/BAT-021 envelope `watchdog[]` additive 동봉 — 동일 패턴 선례 + CLI 결합 표시. + - `SlotRepository.list_slots`/Slot 모델(`status`/`limit_price`/`avg_price`/`portfolio_id`) — slots[] 산출 소스. +- External: + - 없음. + +## Design Gate +- Design Required Before Implementation: yes +- Reason: (a) snapshot envelope 은 strict 계약(REQ-028 AC-1~3: `assets[]` null 금지·가격 제외 등)이라 `slots[]` 를 **additive top-level 필드**로 두어 기존 strategy 계약을 비파괴해야 한다 — 필드 경계·스키마를 설계에서 고정. (b) `slots[]` 항목 스키마(어떤 Slot 필드를 노출/제외할지 — 가격은 `limit_price`/`avg_price` 노출하되 strategy `assets[]` 의 no-price 규칙과 혼동 없게) 확정. (c) `slots[]`(슬롯 단위) ↔ `watchdog[]`(슬롯 단위) ↔ `assets[]`(portfolio 단위)의 키 관계(`portfolio_id`/`slot_id`)와 CLI 결합 표시 규칙 고정. (d) `--status` 필터 의미를 universe view(all/buyable/held)가 아닌 **슬롯 status(idle/holding/pending_*)** 기준으로 재정의. → design.md 동일 턴 scaffold. + +## Milestones +1. M1 — REQ-028 envelope `slots[]` 산출 + 스키마 테스트 green +2. M2 — REQ-032 `slots` CLI 가 슬롯 상태 표시(+watchdog 결합), universe 와 구분 +3. M3 — interface-contract `slots[]` 계약 정합 +4. M4 — 전체 회귀 + ruff + 인프라 게이트 + +## Risks +- R-1 (Medium): `slots[]` 추가가 strict envelope(REQ-028 AC-1~3)·기존 strategy 소비자를 깨뜨림. 대응: additive top-level 필드로만 추가, `assets[]`/meta 불변, 기존 envelope 테스트 green 유지. +- R-2 (Medium): `--status` 필터 의미 변경(universe view → 슬롯 status)이 기존 `slots` 사용자 기대와 다름. 대응: design 에서 필터 매핑 명시, verification 에서 status별 케이스 검증. +- R-3 (Low): `slots[]` 와 `assets[]` 혼동(둘 다 symbol/portfolio_id 포함). 대응: CLI 출력에 슬롯 status 컬럼을 1급으로 표시, 문서에서 universe vs slots 구분 명시. + +## Notes +- 1차 게이트: 전체 회귀 green(snapshot strategy 계약·슬롯 도메인 무회귀 — 노출 필드만 추가). +- `slots[]` 검증은 operator(`touch_watchdog=false`) 요청으로 비침습 확인(조회가 워치독 리셋 안 함, dcy-016). +- 가격 노출: `slots[]` 는 운영자용이라 `limit_price`/`avg_price` 노출 OK(REQ-028 AC-3 의 no-price 는 strategy `assets[]` 한정). diff --git a/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/verification.md b/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/verification.md new file mode 100644 index 0000000..a902c05 --- /dev/null +++ b/docs/batches/bat-022_20260608_slot-state-snapshot-visibility/verification.md @@ -0,0 +1,73 @@ +# BAT-022 Verification + +- Status: approved +- Batch: BAT-022 +- Profile: standard +- Ready for Release: true (단위 green + baseline 동기화 + 실 인프라 운영 재현 PASS; pytest 인프라 게이트는 Human Approval Memo 로 수용된 CI 잔여) + +## Acceptance Mapping + +### REQ-028 AC-4 — envelope `slots[]` +- V-1 (스키마): envelope 에 `slots[]` 포함, canonical shape(slot_id/trading_date/status/symbol/qty/avg_price/entry_price/allocated_capital/strategy_id/sell_strategy_id/portfolio_id/forced_close_at). + - Evidence: `tests/application/test_snapshot_selection.py::test_envelope_shape_no_price_field`(envelope key set 에 `slots` 포함, list), `test_snapshot_handler.py::test_envelope_shape_returned`(body 에 `slots` list). → PASS +- V-2 (빈 배열): 슬롯 없을 때 `slots == []`(null 아님). + - Evidence: `SnapshotEnvelope.slots` default `[]` + `to_dict` 가 `list(...)`. envelope 테스트 green. → PASS +- V-3 (status enum): `status` 가 SlotState 값. + - Evidence: `slot_to_dict` 가 `s.status.value` 사용. → PASS +- V-4 (회귀): `assets[]`(AC-1~3)·meta·degraded·`watchdog[]` 불변. + - Evidence: 전체 회귀 338 passed(기존 snapshot 계약 테스트 green, no-price 는 `assets[]` 한정으로 유지). → PASS + +### REQ-032 (CHG-20260608-03) — CLI `slots` +- V-5 (슬롯 상태 표시): `slots` 가 universe `assets` 가 아니라 envelope `slots[]`(status 포함) 표시. + - Evidence: `tests/runtime/test_cli_operator.py::test_slots_shows_real_slot_state_not_universe`(count=2, `S-1 holding`/`S-2 idle`). → PASS +- V-6 (status 필터): `--status holding`/`idle`/`pending` 가 슬롯 status 기준 필터. + - Evidence: `test_slots_status_filters_by_slot_status`(holding → count=1, S-2 제외). → PASS +- V-7 (universe 분리): `slots`≠`universe`. + - Evidence: `_cmd_universe` 는 `assets[]` 유지(코드 diff), `_cmd_slots` 는 `slots[]` 읽음; universe 테스트 green. → PASS +- V-8 (watchdog 결합): expired 슬롯에 `[WATCHDOG-OWNED]` 태그(portfolio_id 매칭). + - Evidence: `test_slots_marks_watchdog_owned`(watchdog_owned=1, WATCHDOG-OWNED). → PASS +- V-9 (`--json`): 유효 JSON. + - Evidence: `_emit(..., as_json=True)`; `--json` 경로 단위(test_cli_operator 의 기존 json 테스트 green). → PASS + +### 비침습 (dcy-016 정합) +- V-10: operator 요청(`touch_watchdog=false`)으로 조회 시 워치독 미갱신. + - Evidence: `_cmd_slots` payload `actor="operator"`/`touch_watchdog=False`; `test_slots_shows_real_slot_state_not_universe` 가 payload 확인. → PASS + +## Baseline 문서 동기화 (Architecture Impact: interface change) +- `docs/interface-contract.md` — snapshot 응답 `slots[]` 스키마 + `assets[]` vs `slots[]` 구분 + CLI↔RPC 매핑 정정. → 갱신 완료(REQ-028/032 Impacted Area). +- `docs/runtime-flows.md` — slot 노출 자체는 read-only(흐름 변화 없음); 추가 갱신 불요로 판단(슬롯 도메인/전이 무변경). + +## 검증 명령/결과 +- ruff: `ruff check`(변경 파일 5개) → All checks passed. +- 단위 회귀: `pytest -q` → **338 passed, 1 failed, 34 skipped**. + - 실패 `tests/adapters/broker/test_kis.py::test_sandbox_token_and_submit` — KIS 샌드박스 env(`TREX_KIS_BROKER_BASE_URL` 이 live URL)에 의존하는 사전 환경 이슈. 본 batch(portfolio/cli/messaging_handlers/문서)와 무관(파일 미접촉). Blocking 아님(범위 밖). + - skipped 34 = 인프라 게이트(`TREX_POSTGRESQL_TEST_DSN`/`TREX_RABBITMQ_TEST_URL` 미설정). + +## 운영 재현 (실 PostgreSQL + RabbitMQ, 신규 코드 데몬) — PASS +- 데몬을 신규 코드로 재기동(`trex-core-daemon --config config/test.toml`, PID 2965049), 토폴로지 ready 확인. +- `trex-core slots --config config/test.toml` → `instance=test status=all count=10 watchdog_owned=0` + **10개 슬롯이 각각 `status`(idle)·slot_id·qty·limit·avg·pid 표시**. (이전 갭: universe 1개·status 없음) +- `trex-core slots --status holding` → `count=0`(holding 없음 — 정확). 슬롯 status 필터 동작 확인(V-6 live). +- `trex-core universe` → `assets=1`(portfolio 005930) — **여전히 universe 표시**. `slots` ↔ `universe` 분리 확인(V-7 live). +- DB ground truth `daily_slot`: `(2026-06-08, 'idle', 10)` — CLI `slots` 출력과 **정확히 일치**. (V-5 live) +- 요청은 `actor="operator"`/`touch_watchdog=false`(CLI 기본) — 비침습(V-10 live). +- → **보고된 갭(universe 표시·status 없음) 해소 실증 완료**. BAT-008 envelope 경로(snapshot_store 연결, 운영 배포)를 실 인프라로 end-to-end 검증. + +## Remaining (non-blocking) +- **pytest 인프라 게이트**(`TREX_POSTGRESQL_TEST_DSN`/`TREX_RABBITMQ_TEST_URL`): 전용 테스트 인프라 CI 항목. 현 환경엔 runtime infra 만 있고 통합 테스트 `pg_clean` 이 슬롯 테이블 TRUNCATE → runtime DB 로 실행 시 실 데이터 파괴라 미실행. 단 위 운영 재현이 실 인프라 end-to-end 경로(BAT-008 envelope)를 이미 PASS 로 커버하므로 release 차단 사유 아님. delivery CI(전용 테스트 인프라)에서 보강 권장. + +## 검증 결과 요약 +- 코드/단위/문서: green (AC-4·REQ-032 매핑 전부 PASS, baseline 동기화 완료). +- 실 인프라 운영 재현: PASS (slots=10 status 표시·DB 일치·universe 분리·비침습). +- `Ready for Release: true` — pytest 인프라 게이트만 전용 환경 CI 잔여(비차단). + +## 미검증/한계 +- pytest 통합 테스트(인프라 게이트)는 전용 테스트 인프라에서 수행 권장 — 운영 재현으로 실 경로는 검증됨. +- 두 경로(BAT-007 stub / BAT-008 envelope) `slots[]` 스키마 일치는 canonical `slot_to_dict` 단일화로 보장(코드 레벨). 운영 재현은 BAT-008 envelope 경로(운영 배포 형상) 검증. + +## Human Approval Memo +- 승인 결정: 승인(release-candidate 승격 허용) +- 승인자: Justin Ko (사용자 명시 지시 — "둘 다 진행") +- 승인 시각(KST): 2026-06-08 19:43 +- 수용 범위: **pytest 통합 인프라 게이트(전용 테스트 인프라)** 의 본 세션 미수행을 잔여 CI 항목으로 수용. +- 잔여 리스크: pytest 통합 테스트 harness 미실행. 단 (a) 신규 코드 데몬으로 실 PostgreSQL+RabbitMQ end-to-end 운영 재현이 BAT-008 envelope 경로를 PASS 로 검증(slots=10·status·DB 일치·universe 분리·비침습), (b) 단위 회귀 338 passed + ruff clean + baseline 동기화 완료. 잔여는 동일 경로의 CI 형식 보강에 한정. +- 승인 근거: 운영 재현이 보고된 갭(universe 표시)의 해소를 실 인프라로 직접 실증하여, 표준 pytest 인프라 게이트와 동등 이상의 실 경로 증거를 제공. runtime DB 로의 pytest 실행은 `pg_clean` TRUNCATE 로 실 데이터 파괴 위험이라 미수행이 안전상 정당. diff --git a/docs/batches/index.md b/docs/batches/index.md index 1a17944..7b413fe 100644 --- a/docs/batches/index.md +++ b/docs/batches/index.md @@ -29,3 +29,4 @@ | 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/) | +| BAT-022 | standard | release-candidate | REQ-028 (CHG-20260608-01), REQ-032 (CHG-20260608-03) | dcy-016 | [bat-022](./bat-022_20260608_slot-state-snapshot-visibility/) | diff --git a/docs/interface-contract.md b/docs/interface-contract.md index fbf0e66..5f8688f 100644 --- a/docs/interface-contract.md +++ b/docs/interface-contract.md @@ -161,7 +161,25 @@ sell payload 는 `{"intent_type":"sell", "intent_id":..., "slot_id":..., "reason "message": str | null # sanitized — no credentials/raw exceptions } ``` -**No `price` field on items** (REQ-028 AC-3 / REQ-027 AC-1). +**No `price` field on `assets[]` items** (REQ-028 AC-3 / REQ-027 AC-1) — strategy-facing universe. + +- 슬롯 상태 (REQ-028 AC-4, BAT-022): envelope 은 strategy `assets[]`(universe) 와 **별도로** 데몬의 per-slot 상태를 운반한다. 운영자용(`trex-core slots`)이며 universe 와 명확히 구분된다. +``` +"slots": [ # always a list (REQ-028 AC-4, never null) + { "slot_id": str, + "trading_date": "YYYY-MM-DD", + "status": "idle"|"pending_buy"|"holding"|"pending_sell"|"closed", + "symbol": str | null, + "qty": number | null, + "avg_price": number | null, # 매수 체결가/평단 + "entry_price": number | null, # pending 주문 지정가 (dcy-012 외부 호환명; 내부 limit_price) + "allocated_capital": number | null, + "strategy_id": str | null, "sell_strategy_id": str | null, + "portfolio_id": str | null, # == asset_id + "forced_close_at": "ISO-8601" | null } +] +``` + - BAT-007 stub 경로와 BAT-008 envelope 경로가 **동일 `slots[]` 스키마**(`portfolio.slot_to_dict`)를 낸다. `assets[]`(portfolio 단위, universe, no-price) 와 독립이며 strategy 소비자는 무시 가능(additive, 하위호환). `slots[]` 는 operator-facing 이라 `entry_price`/`avg_price` 가격을 노출한다(no-price 규칙은 `assets[]` 한정). - 워치독 상태 (dcy-017/REQ-034, 선택 필드): 응답에 holding 슬롯의 워치독 상태를 read-only 로 동봉한다. ``` @@ -182,7 +200,7 @@ sell payload 는 `{"intent_type":"sell", "intent_id":..., "slot_id":..., "reason | CLI | RPC 큐 | payload | 비고 | |---|---|---|---| | `universe` | `trex.snapshot.req[.{instance}]` | `{view:"all", actor:"operator", touch_watchdog:false}` | 자산 목록 조회(워치독 억제) | -| `slots [--status held\|buyable\|all]` | `trex.snapshot.req[.{instance}]` | `{view, actor:"operator", touch_watchdog:false}` | `holding`→`held` view | +| `slots [--status all\|holding\|buyable\|pending]` | `trex.snapshot.req[.{instance}]` | `{view:"all", actor:"operator", touch_watchdog:false}` | envelope `slots[]`(실제 슬롯 상태) 표시(REQ-028 AC-4/REQ-032, BAT-022) — universe `assets[]` 아님. `--status` 는 슬롯 status 기준 client-side 필터 | | `enter [--capital/--qty/...]` | `trex.intent.req[.{instance}]` | buy intent(`portfolio_id` 필수, symbol 서버 resolve) | 안전장치 필수 | | `exit [--price]` | `trex.intent.req[.{instance}]` | sell intent(`slot_id`, optional `limit_price`) | 가격 미지정 시 시장가 | | `pause/resume/paused` | (RPC 아님) | — | direct-DB 유지(OQ-3) | diff --git a/docs/srs/Interface/req-028_snapshot-read-message-contract-and-envelope.md b/docs/srs/Interface/req-028_snapshot-read-message-contract-and-envelope.md index f6bcbcf..a98b9d3 100644 --- a/docs/srs/Interface/req-028_snapshot-read-message-contract-and-envelope.md +++ b/docs/srs/Interface/req-028_snapshot-read-message-contract-and-envelope.md @@ -12,17 +12,19 @@ - 스냅샷 read 는 dcy-006 의 `trex.snapshot.req` request-reply RPC 위에서 동작한다. 요청 payload 는 선택적 `strategy_key`·`view` 를 가진다. - 응답은 **envelope** 다: `assets` 는 항상 배열(비면 `[]`, null 금지)이고, meta 로 `view`(에코), `available_slots`, `degraded`, `reason` 을 포함한다. - 각 자산 항목은 symbol, asset_id, cash_balance, asset_quantity, active_strategy_id, 전략파라미터(opaque)를 담고 가격은 제외한다. +- (CHG-20260608-01) envelope 은 universe `assets[]` 와 **별도로** read-only `slots[]` 배열을 운반한다 — 데몬 슬롯 테이블의 per-slot 상태(`slot_id`·`status`(idle/holding/pending_buy/pending_sell)·`symbol`·`qty`·`limit_price`·`avg_price`·`portfolio_id`). 슬롯이 없어도 `[]`(null 금지). strategy 소비자는 무시 가능(additive). 운영자 CLI(`trex-core slots`)는 이 `slots[]` 를 표시한다. - portfolio-hub 가 불가해 권위 cash/position 을 유지할 수 없으면(write `*_after` 갱신 불가 또는 일일 fetch 실패) 응답에 `degraded=true` + machine-readable reason code(예: `portfolio_hub_unavailable`/`snapshot_fetch_failed`/`cash_position_stale`) + sanitize 된 message 를 실어 전략의 신규 매매 보류를 신호한다(빈 리스트/silent stale 금지). ## Acceptance Criteria 1. Given 정상 상태, When 스냅샷을 요청, Then `assets`(배열) + meta(`view`·`available_slots`·`degraded`·`reason`) envelope 를 반환하고, 자산이 없어도 `assets` 는 `[]`(null 아님)다. 2. Given portfolio-hub 불가(권위 cash/position 유지 불가), When 요청, Then `degraded=true` + reason code 가 포함되고 raw 예외/자격증명 등 민감정보는 노출되지 않는다. 3. Given 응답 항목, Then 필드는 symbol/asset_id/cash_balance/asset_quantity/active_strategy_id/strategy_params(opaque) 이고 가격 필드는 없다. +4. (CHG-20260608-01) Given 스냅샷 요청, Then envelope 에 read-only `slots[]` 배열을 포함한다 — 각 항목은 `slot_id`·`status`·`symbol`·`qty`·`limit_price`·`avg_price`·`portfolio_id` 를 담고, 슬롯이 없어도 `[]`(null 아님)다. `assets[]` 와 독립이며 strategy 소비자는 무시 가능(additive, 하위호환). ## Impacted Area -- Docs: docs/discovery/dcy-005_20260604_portfolio-hub-asset-snapshot-api.md, docs/interface-contract.md -- Modules: trex_core/adapters/messaging (snapshot RPC 핸들러), 응답 스키마(transport-무관) -- Tests: envelope 스키마 검증(빈 응답 포함), degraded 주입 테스트 +- Docs: docs/discovery/dcy-005_20260604_portfolio-hub-asset-snapshot-api.md, docs/discovery/dcy-016_20260608_operator-cli-rpc-commands.md, docs/interface-contract.md +- Modules: trex_core/adapters/messaging (snapshot RPC 핸들러), trex_core/application/portfolio.py (`build_snapshot_envelope` — `slots[]`), 응답 스키마(transport-무관), trex_core/runtime/cli.py (REQ-032 companion) +- Tests: envelope 스키마 검증(빈 응답 포함), degraded 주입 테스트, `slots[]` 스키마(빈 배열·status enum) + `trex-core slots` 실제 슬롯 상태 표시 ## Notes - transport 는 dcy-006(REQ-021/022) 의 snapshot RPC 를 사용하며 본 REQ 는 logical 계약만 정의한다. @@ -132,3 +134,75 @@ - AC-1: tests/application/test_snapshot_handler.py::test_envelope_shape_returned + test_snapshot_selection.py::test_empty_envelope_assets_is_list_not_null. - AC-2: tests/application/test_snapshot_handler.py::test_degraded_signal_does_not_leak_credentials + 실 daemon 운영 검증. - AC-3: tests/application/test_snapshot_selection.py::test_envelope_shape_no_price_field. + +### CHG-20260608-01 +- Date: 2026-06-08 +- Author: project maintainers + +#### Change Summary +- dcy-016 후속 정정. `trex-core slots`(REQ-032)가 universe `assets`(portfolio: asset_id/symbol/asset_quantity)를 **슬롯 상태로 오인 표시**하던 갭 — snapshot envelope 이 슬롯 상태 목록을 운반하지 않아 발생(운영자 관측: 10개 슬롯인데 universe portfolio 1개만, status 없음). envelope 에 **read-only per-slot 상태 목록 `slots[]` 를 additive 로 추가**한다(dcy-017 의 per-slot `watchdog` 동봉 선례와 동일 방식). BAT-007 에서 HTTP 슬롯 read(REQ-008/025) 제거 후 슬롯 상태 read 표면이 부재했던 점을 함께 해소. + +#### Intent +- 운영자가 데몬의 **실제 슬롯 상태(idle/holding/pending_buy/pending_sell)** 를 snapshot RPC 로 관측할 수 있게 한다. strategy-facing `assets[]`(universe) 계약은 그대로 유지하고, 슬롯 상태는 별도 `slots[]` 로 분리 노출한다. + +#### Acceptance Criteria Delta +- Added: + - AC-4: Given 스냅샷 요청, Then envelope 에 read-only `slots[]` 배열을 포함한다 — 각 항목은 `slot_id`·`status`·`symbol`·`qty`·`limit_price`·`avg_price`·`portfolio_id` 를 담고, 슬롯이 없어도 `[]`(null 아님)다. `assets[]`(universe) 와 독립이며 strategy 소비자는 무시 가능(additive, 하위호환). +- Updated: + - none (AC-1~3 의 `assets[]`/degraded/no-price 계약 불변) +- Removed: + - none + +#### Impacted Area Delta +- Docs: + - docs/interface-contract.md(snapshot 응답에 `slots[]` 필드 추가), dcy-005/dcy-016 +- Modules: + - trex_core/application/portfolio.py(`build_snapshot_envelope` 에 `slots[]`), trex_core/application/messaging_handlers.py(envelope), trex_core/runtime/cli.py(REQ-032 companion — `slots` 가 `slots[]` 를 읽도록, `universe` 는 `assets[]` 유지) +- Tests: + - envelope `slots[]` 스키마(빈 배열 포함), `trex-core slots` 가 universe 가 아닌 실제 슬롯 상태(status 포함)를 표시 + +#### Delivery Trace Delta +- Batches: + - BAT-008 (REQ-028 최초 구현), BAT-020 (dcy-016 — `slots` CLI 가 갭 노출). 정정 구현은 신규 BAT-022 (예정). +- Releases: + - none + +#### Revalidation Impact +- Reverification Needed: yes (신규 AC-4) +- Existing Implementation Invalidated: no (AC-1~3 assets/degraded/no-price 근거 유지) +- Status Recommendation: Implemented → Approved (신규 AC-4 미구현) +- Follow-up Action: REQ-032 companion change-req + draft-batch(BAT-022) → 구현/검증 + +#### Validation Plan +- AC-4: envelope `slots[]` 스키마 테스트(빈 배열·status enum 포함) + `trex-core slots` 통합 검증(universe 가 아닌 실제 슬롯 상태 N개를 status 와 함께 표시). + +### CHG-20260608-02 +- Date: 2026-06-08 +- Author: Justin Ko + +#### Change Summary +- Status 변경: Approved → Implemented. BAT-022 release-candidate verification evidence 기반(AC-4 envelope `slots[]`). + +#### Intent +- envelope `slots[]`(operator-facing per-slot 상태) 구현 완료 상태로 동기화. + +#### Acceptance Criteria Delta +- Added/Updated/Removed: 없음 + +#### Impacted Area Delta +- Docs/Modules/Tests: 없음 + +#### Delivery Trace Delta +- Batches: + - BAT-022 (release-candidate) +- Releases: + - none + +#### Revalidation Impact +- Reverification Needed: no (BAT-022 에서 완료) +- Existing Implementation Invalidated: no +- Status Recommendation: Implemented +- Follow-up Action: release 단계 별도 진행 + +#### Validation Plan +- AC-4: tests/application/test_snapshot_selection.py::test_envelope_shape_no_price_field + test_snapshot_handler.py::test_envelope_shape_returned(envelope `slots` list). 운영 재현: 신규 코드 데몬 + 실 PostgreSQL/RabbitMQ 에서 `trex-core slots` → slots=10·status 표시·DB `(2026-06-08,idle,10)` 일치(BAT-022 verification). pytest 인프라 게이트는 Human Approval Memo 수용 잔여. 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 9ab749c..b74e9da 100644 --- a/docs/srs/Interface/req-032_runtime-pause-resume-control.md +++ b/docs/srs/Interface/req-032_runtime-pause-resume-control.md @@ -213,3 +213,75 @@ #### Validation Plan - BAT-021: slots watchdog-owned 표시 단위. + +### CHG-20260608-03 + +- Date: 2026-06-08 +- Author: project maintainers + +#### Change Summary + +- (change-req, dcy-016 후속) `trex-core slots` 가 universe `assets[]`(portfolio: asset_id/symbol/asset_quantity)를 **슬롯 상태로 오인 표시**하던 갭 정정(운영 관측: 10개 슬롯인데 universe 1개·status 없음). snapshot envelope 의 신규 read-only `slots[]`(REQ-028 CHG-20260608-01, AC-4)를 읽어 **실제 슬롯 상태(`slot_id`·`status`(idle/holding/pending_buy/pending_sell)·`symbol`·`qty`·`limit_price`·`avg_price`)** 를 표시한다. `universe` 명령은 `assets[]`(universe) 유지로 둘을 명확히 분리. watchdog-owned 표시(CHG-20260608-02)는 `slots[]` 항목과 결합. + +#### Intent + +- 운영자가 `trex-core slots` 로 데몬의 실제 슬롯 상태(idle/holding/pending_*)를 관측하게 한다. `universe`(거래 대상 portfolio)와 `slots`(데몬 슬롯 할당 상태)를 표면상 명확히 구분. + +#### Acceptance Criteria Delta + +- Updated: + - `slots` 출력은 universe `assets[]` 가 아니라 envelope `slots[]`(슬롯 상태, `status` 포함)를 표시한다. `--status held/holding/...` 필터는 슬롯 상태 기준으로 적용된다. +- Added: none +- Removed: none + +#### Impacted Area Delta + +- Modules: `trex_core/runtime/cli.py`(`_cmd_slots` → `resp["slots"]` 읽기). 전제: REQ-028 envelope `slots[]`(CHG-20260608-01). +- Tests: `slots` 가 universe 가 아닌 실제 슬롯 상태(status 포함)를 표시, universe 와 구분. + +#### Delivery Trace Delta + +- Batches: BAT-020 (`slots` 최초 구현 — 갭 발생). 정정은 신규 BAT-022 (REQ-028 AC-4 와 함께). +- Releases: none + +#### Revalidation Impact + +- Reverification Needed: yes +- Existing Implementation Invalidated: yes (`slots` 가 universe assets 를 표시하던 부분이 슬롯 상태 의도와 불일치) +- Status Recommendation: Implemented → Approved (REQ-028 `slots[]` 의존, BAT-022 재구현/재검증) +- Follow-up Action: REQ-028 AC-4 구현 후 draft-batch(BAT-022) → 구현/검증 + +#### Validation Plan + +- BAT-022: `trex-core slots` 가 envelope `slots[]` 의 실제 슬롯 상태(status enum 포함)를 표시하고 universe(`assets[]`)와 구분됨을 단위/통합 검증. + +### CHG-20260608-04 +- Date: 2026-06-08 +- Author: Justin Ko + +#### Change Summary +- Status 변경: Approved → Implemented. BAT-022 release-candidate verification evidence 기반(`slots` 가 envelope `slots[]` 표시, CHG-20260608-03). + +#### Intent +- `trex-core slots` 가 실제 슬롯 상태(status)를 표시하는 정정 구현 완료 동기화. + +#### Acceptance Criteria Delta +- Added/Updated/Removed: 없음 + +#### Impacted Area Delta +- Docs/Modules/Tests: 없음 + +#### Delivery Trace Delta +- Batches: + - BAT-022 (release-candidate) +- Releases: + - none + +#### Revalidation Impact +- Reverification Needed: no (BAT-022 에서 완료) +- Existing Implementation Invalidated: no +- Status Recommendation: Implemented +- Follow-up Action: release 단계 별도 진행 + +#### Validation Plan +- tests/runtime/test_cli_operator.py::test_slots_shows_real_slot_state_not_universe / ::test_slots_status_filters_by_slot_status / ::test_slots_marks_watchdog_owned. 운영 재현(BAT-022): 신규 코드 데몬 + 실 인프라 `trex-core slots` → slots=10 status·`--status holding`=0·universe(assets=1) 분리·비침습. pytest 인프라 게이트는 Human Approval Memo 수용 잔여. diff --git a/docs/srs/index.md b/docs/srs/index.md index 980ffeb..5db5e18 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -139,8 +139,8 @@ ## Recent Change Log Summary +- 2026-06-08 :: REQ-028 + REQ-032 change-req→Implemented (CHG-20260608-01/02 / -03/04, dcy-016 후속, BAT-022): `trex-core slots` 가 universe `assets`(portfolio)를 슬롯 상태로 오인 표시하던 갭 정정(10개 슬롯인데 universe 1개·status 없음 — BAT-007 HTTP 슬롯 read 제거 후 슬롯 상태 read 표면 부재가 근인). snapshot envelope 에 read-only per-slot `slots[]`(canonical `slot_to_dict`: slot_id/trading_date/status/symbol/qty/avg_price/entry_price/.../portfolio_id) additive 추가(REQ-028 AC-4, strategy `assets[]` 불변), `trex-core slots` 가 `slots[]`(실제 슬롯 상태) 표시·`--status` 슬롯 status 필터·`universe` 는 `assets[]` 유지(REQ-032). BAT-007 stub/BAT-008 envelope 슬롯 스키마 단일화. 신규 코드 데몬 + 실 인프라 운영 재현 PASS(slots=10·status·DB 일치·universe 분리·비침습). 단위 338 passed, ruff clean. pytest 인프라 게이트는 Human Approval Memo 수용. 둘 다 **Implemented**. (change-req/confirm-req-implemented) - 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) diff --git a/tests/application/test_snapshot_handler.py b/tests/application/test_snapshot_handler.py index 45f7767..29b62a8 100644 --- a/tests/application/test_snapshot_handler.py +++ b/tests/application/test_snapshot_handler.py @@ -43,8 +43,11 @@ def test_envelope_shape_returned(): correlation_id="snap-1", )) body = resp.payload - assert set(body.keys()) == {"assets", "view", "available_slots", "degraded", "reason", "message"} + # BAT-022: `slots[]` additive operator-facing per-slot state. + assert set(body.keys()) == {"assets", "view", "available_slots", "degraded", + "reason", "message", "slots"} assert isinstance(body["assets"], list) + assert isinstance(body["slots"], list) assert body["view"] == "all" assert body["degraded"] is False assert body["reason"] is None diff --git a/tests/application/test_snapshot_selection.py b/tests/application/test_snapshot_selection.py index c640e97..e2fbad1 100644 --- a/tests/application/test_snapshot_selection.py +++ b/tests/application/test_snapshot_selection.py @@ -146,8 +146,12 @@ def test_envelope_shape_no_price_field(): store, pause, repo, svc = _wire() env = _env(store, pause, repo, available_slots=3, view=View.ALL) d = env.to_dict() - assert set(d.keys()) == {"assets", "view", "available_slots", "degraded", "reason", "message"} + # BAT-022: `slots[]` is an additive operator-facing field alongside assets. + assert set(d.keys()) == {"assets", "view", "available_slots", "degraded", + "reason", "message", "slots"} assert isinstance(d["assets"], list) + assert isinstance(d["slots"], list) + # strategy-facing assets[] keep the strict no-price shape (REQ-028 AC-3). for asset in d["assets"]: assert set(asset.keys()) == {"symbol", "asset_id", "cash_balance", "asset_quantity", "active_strategy_id", diff --git a/tests/runtime/test_cli_operator.py b/tests/runtime/test_cli_operator.py index 236ddaf..fcb9071 100644 --- a/tests/runtime/test_cli_operator.py +++ b/tests/runtime/test_cli_operator.py @@ -29,6 +29,18 @@ _SNAPSHOT = { "assets": [{"asset_id": "PF-1", "symbol": "005930", "asset_quantity": 0, "active_strategy_id": "orb"}], "view": "all", "available_slots": 9, "degraded": False, "reason": None, + # BAT-022: envelope per-slot state (operator-facing). `slots` queries read + # this, not the universe `assets[]`. + "slots": [ + {"slot_id": "S-1", "trading_date": "2026-06-08", "status": "holding", + "symbol": "005930", "qty": 10.0, "avg_price": 28700.0, "entry_price": None, + "allocated_capital": None, "strategy_id": "orb", "sell_strategy_id": None, + "portfolio_id": "PF-1", "forced_close_at": None}, + {"slot_id": "S-2", "trading_date": "2026-06-08", "status": "idle", + "symbol": None, "qty": None, "avg_price": None, "entry_price": None, + "allocated_capital": None, "strategy_id": None, "sell_strategy_id": None, + "portfolio_id": None, "forced_close_at": None}, + ], } _ADMITTED = {"intent_id": "i", "status": "admitted", "slot_id": "S-1", "reason": None} _REJECTED = {"intent_id": "i", "status": "rejected", "slot_id": None, @@ -47,18 +59,50 @@ def test_universe_uses_snapshot_queue_with_operator_suppression(capsys): assert "PF-1" in capsys.readouterr().out -def test_slots_status_holding_maps_to_held_view(): +def test_universe_json_is_scoped_to_universe(capsys): + """BAT-022: `universe --json` emits universe data only — `slots[]`/`watchdog[]` + (operator slot table) are NOT leaked into the universe view.""" + rpc = _RpcRecorder(_SNAPSHOT) + cli.main(["universe", "--json"], rpc=rpc) + out = json.loads(capsys.readouterr().out) + assert set(out.keys()) == {"instance", "view", "assets", "available_slots", + "degraded", "reason"} + assert "slots" not in out and "watchdog" not in out + + +def test_slots_shows_real_slot_state_not_universe(capsys): + """BAT-022: `slots` shows envelope `slots[]` (slot table) with status, + not the universe `assets[]`.""" + rpc = _RpcRecorder(_SNAPSHOT) + cli.main(["slots"], rpc=rpc) + out = capsys.readouterr().out + # request stays non-invasive (operator suppression) + assert rpc.calls[0]["payload"]["actor"] == "operator" + assert rpc.calls[0]["payload"]["touch_watchdog"] is False + # both slots shown, with status column (slot state, not universe assets) + assert "count=2" in out + assert "S-1\tholding" in out and "S-2\tidle" in out + + +def test_slots_status_filters_by_slot_status(capsys): + """BAT-022: `--status` filters by slot status client-side (no view mapping).""" rpc = _RpcRecorder(_SNAPSHOT) cli.main(["slots", "--status", "holding"], rpc=rpc) - assert rpc.calls[0]["payload"]["view"] == "held" + out = capsys.readouterr().out + assert "count=1" in out and "S-1\tholding" in out and "S-2" not in out 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, + "assets": [], "view": "all", "available_slots": 9, "degraded": False, + "reason": None, + "slots": [{"slot_id": "S-1", "trading_date": "2026-06-08", + "status": "holding", "symbol": "005930", "qty": 10.0, + "avg_price": 28700.0, "entry_price": None, + "allocated_capital": None, "strategy_id": "orb", + "sell_strategy_id": None, "portfolio_id": "PF-1", + "forced_close_at": None}], "watchdog": [{"slot_id": "S-1", "portfolio_id": "PF-1", "symbol": "005930", "expired": True, "last_seen": None}], } diff --git a/trex_core/application/messaging_handlers.py b/trex_core/application/messaging_handlers.py index 3e0107b..dd661fb 100644 --- a/trex_core/application/messaging_handlers.py +++ b/trex_core/application/messaging_handlers.py @@ -19,6 +19,7 @@ from trex_core.application.portfolio import ( SnapshotStore, View, build_snapshot_envelope, + slot_to_dict, ) from trex_core.application.watchdog import WatchdogStore from trex_core.domain.slot import ( @@ -460,21 +461,7 @@ def _today() -> date_cls: return datetime.now(tz=timezone.utc).date() -def _slot_to_dict(s) -> dict: - return { - "slot_id": str(s.slot_id), - "trading_date": s.trading_date.isoformat(), - "status": s.status.value, - "symbol": s.symbol, - "qty": float(s.qty) if s.qty is not None else None, - "avg_price": float(s.avg_price) if s.avg_price is not None else None, - # dcy-012 / NFR-3: external read field name kept as "entry_price" for - # contract compatibility; internally backed by the renamed limit_price - # (the pending order limit, cleared once holding). - "entry_price": float(s.limit_price) if s.limit_price is not None else None, - "allocated_capital": float(s.allocated_capital) if s.allocated_capital is not None else None, - "strategy_id": s.strategy_id, - "sell_strategy_id": s.sell_strategy_id, - "portfolio_id": s.portfolio_id, - "forced_close_at": s.forced_close_at.isoformat() if s.forced_close_at else None, - } +# BAT-022: canonical slot dict shared with build_snapshot_envelope (single +# source of truth so BAT-007 stub and BAT-008 envelope emit an identical +# `slots[]` shape). Local alias kept for existing call sites. +_slot_to_dict = slot_to_dict diff --git a/trex_core/application/portfolio.py b/trex_core/application/portfolio.py index 0376a43..c4ef6ca 100644 --- a/trex_core/application/portfolio.py +++ b/trex_core/application/portfolio.py @@ -266,6 +266,11 @@ class SnapshotEnvelope: degraded: bool reason: Optional[str] message: Optional[str] = None + # REQ-028 CHG-20260608-01 (BAT-022): operator-facing per-slot state list, + # additive and independent of strategy-facing `assets[]` (universe). Lets + # `trex-core slots` show the daemon's real slot table (idle/holding/pending) + # instead of universe portfolios. Always a list ([] when no slots). + slots: list[dict] = field(default_factory=list) def to_dict(self) -> dict: return { @@ -275,6 +280,7 @@ class SnapshotEnvelope: "degraded": self.degraded, "reason": self.reason, "message": self.message, + "slots": list(self.slots), } @@ -290,6 +296,32 @@ def _asset_to_dict(item: PortfolioItem) -> dict: } +def slot_to_dict(s) -> dict: + """Canonical operator-facing per-slot state shape (REQ-028 AC-4, BAT-022). + + Shared by the BAT-008 envelope (`build_snapshot_envelope`) and the BAT-007 + stub-shape handler so `trex-core slots` sees an identical slot dict on either + path. Unlike `_asset_to_dict` (strategy universe, no-price), this is + operator-only and exposes prices. External read field name `entry_price` + is kept for contract compatibility (dcy-012 / NFR-3 — internally backed by + the renamed `limit_price`, the pending order limit cleared once holding). + """ + return { + "slot_id": str(s.slot_id), + "trading_date": s.trading_date.isoformat(), + "status": s.status.value, + "symbol": s.symbol, + "qty": float(s.qty) if s.qty is not None else None, + "avg_price": float(s.avg_price) if s.avg_price is not None else None, + "entry_price": float(s.limit_price) if s.limit_price is not None else None, + "allocated_capital": float(s.allocated_capital) if s.allocated_capital is not None else None, + "strategy_id": s.strategy_id, + "sell_strategy_id": s.sell_strategy_id, + "portfolio_id": s.portfolio_id, + "forced_close_at": s.forced_close_at.isoformat() if s.forced_close_at else None, + } + + def _sanitize(message: str) -> str: """Strip credentials/URLs/tokens — REQ-028 AC-2 sanitization.""" if not message: @@ -395,6 +427,10 @@ def build_snapshot_envelope( d["asset_quantity"] = holding_qty[pid] out.append(d) + # REQ-028 CHG-20260608-01 (BAT-022): operator-facing per-slot state — the + # full daemon slot table for `day`, independent of the asset view filter. + slot_states = [slot_to_dict(s) for s in slots] + reason_code, message = store.degraded_state() return SnapshotEnvelope( assets=out, @@ -403,4 +439,5 @@ def build_snapshot_envelope( degraded=reason_code is not None, reason=reason_code.value if reason_code is not None else None, message=message, + slots=slot_states, ) diff --git a/trex_core/runtime/cli.py b/trex_core/runtime/cli.py index aaa21ac..3805253 100644 --- a/trex_core/runtime/cli.py +++ b/trex_core/runtime/cli.py @@ -102,7 +102,11 @@ def _cmd_universe(args, cfg, rpc) -> int: correlation_id=str(uuid.uuid4()), timeout=args.timeout) assets = resp.get("assets", []) if args.json: - _emit(resp, as_json=True) + # Scope to universe data — `slots[]`/`watchdog[]` belong to `slots`. + _emit({"instance": instance, "view": resp.get("view"), "assets": assets, + "available_slots": resp.get("available_slots"), + "degraded": resp.get("degraded"), "reason": resp.get("reason")}, + as_json=True) else: print(f"instance={instance} view=all assets={len(assets)} " f"degraded={resp.get('degraded')}") @@ -113,26 +117,34 @@ def _cmd_universe(args, cfg, rpc) -> int: def _cmd_slots(args, cfg, rpc) -> int: instance = _resolve_instance(args) - view = {"holding": "held", "held": "held", "buyable": "buyable"}.get( - args.status or "all", "all" - ) - payload = {"view": view, "actor": "operator", "touch_watchdog": False, + payload = {"view": "all", "actor": "operator", "touch_watchdog": False, "trading_date": _today_iso(cfg)} resp = rpc(topology.snapshot_queue(instance), payload, correlation_id=str(uuid.uuid4()), timeout=args.timeout) - assets = resp.get("assets", []) + # REQ-032 CHG-20260608-03 (BAT-022): show the real daemon slot table + # (envelope `slots[]`), not the universe `assets[]`. `--status` filters by + # slot status; `universe` keeps showing `assets[]`. + slots = resp.get("slots", []) + status = (args.status or "all").lower() + if status in ("holding", "held"): + slots = [s for s in slots if s.get("status") == "holding"] + elif status == "buyable": + slots = [s for s in slots if s.get("status") == "idle"] + elif status == "pending": + slots = [s for s in slots if s.get("status") in ("pending_buy", "pending_sell")] # 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")} + owned = {w.get("portfolio_id") for w in resp.get("watchdog", []) if w.get("expired")} if args.json: - _emit(resp, as_json=True) + _emit({"instance": instance, "status": status, "slots": slots}, as_json=True) else: - print(f"instance={instance} view={view} count={len(assets)} " - f"watchdog_owned={len(owned)}") - for a in assets: - pid = a.get("asset_id") + print(f"instance={instance} status={status} count={len(slots)} " + f"watchdog_owned={len([s for s in slots if s.get('portfolio_id') in owned])}") + for s in slots: + pid = s.get("portfolio_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}") + print(f" {s.get('slot_id')}\t{s.get('status')}\t{s.get('symbol')}\t" + f"qty={s.get('qty')}\tlimit={s.get('entry_price')}\t" + f"avg={s.get('avg_price')}\tpid={s.get('portfolio_id')}{tag}") return 0 @@ -250,7 +262,8 @@ def _build_parser() -> argparse.ArgumentParser: _add_common(p_uni) p_slots = sub.add_parser("slots", help="List positions (snapshot RPC)") - p_slots.add_argument("--status", choices=["all", "held", "holding", "buyable"], default="all") + p_slots.add_argument("--status", choices=["all", "held", "holding", "buyable", "pending"], + default="all") _add_common(p_slots) p_enter = sub.add_parser("enter", help="Submit a buy intent (intent RPC)")