feat(cli): dcy-016 운영자 CLI 확장 (universe/slots/enter/exit) + 워치독 억제 (BAT-020)

운영자가 조회·매수·매도를 CLI 로 수행하되 데몬 RabbitMQ RPC 로 통일해 단일
writer admission·불변식을 보존(DB 직접 쓰기 없음)하고, 운영자 조회가 전략
생존 신호로 오인되지 않게 워치독을 억제한다.

거버넌스: 전부 change-req (신규 REQ 없음, dcy-016 OQ-3).
- REQ-032: CLI universe/slots(snapshot RPC) + enter(buy)/exit(sell) intent RPC
  + 인스턴스 라우팅(--config/--instance/default) + enter/exit 안전장치
  (확인 프롬프트 기본 ON / --yes(paper) / --yes --live(이중 ack) / --dry-run)
  + 전 명령 --json. pause/resume 는 direct-DB 유지
- REQ-022: CLI request-reply RPC 클라이언트(call_rpc) 재사용
- REQ-021/034: snapshot 요청 워치독 억제 플래그(touch_watchdog/actor=operator)
  → 핸들러가 last_seen_at 갱신 스킵. 기본값 현행 유지(하위호환). 운영자
  조회가 죽은 전략의 safety-sell 을 무력화하던 결함(R-4) 제거
- REQ-026: interface-contract/runtime-flows 정합

CLI 는 rpc/confirm 주입 가능(테스트 broker/DB-free). snapshot envelope 은
asset-centric 이라 exit 은 운영자 제공 slot_id 사용(slot_id 노출은 본 주기 밖).

검증: 단위 327 passed/34 skipped, 인프라 게이트(PG+RabbitMQ) 361 passed, ruff clean.
신규 test_cli_operator(14) + snapshot 워치독 억제(2). BAT-020 release-candidate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 16:48:26 +09:00
co-authored by Claude Opus 4.8
parent b851b927ee
commit d09b93195f
20 changed files with 1185 additions and 28 deletions
@@ -0,0 +1,45 @@
# BAT-020 Design
- Status: confirmed
- Batch: BAT-020
## 설계 목표
- 운영자가 CLI 로 조회·매수·매도·일시정지를 수행하되, 매매는 데몬 admission(단일 writer)을 거쳐 불변식을 보존한다. 운영자 조회가 전략 생존 신호로 오인되지 않게 한다(R-4).
## 1. 워치독 억제 (REQ-021/034, 서버)
- `make_snapshot_rpc_handler`: `touch_watchdog = bool(body.get("touch_watchdog", True))`; `actor=="operator"` 면 강제 false. `watchdog_store and touch_watchdog` 일 때만 holding 슬롯 `last_seen_at` 갱신.
- 기본값(플래그 미지정) = 현행(전략 폴링=갱신) → 전략 모듈 무변경(하위호환). 운영자 CLI 는 항상 억제.
## 2. CLI 구조 (REQ-032/022)
- `main(argv, *, rpc=None, confirm=None)`: `rpc(queue, payload, *, correlation_id, timeout)->dict`, `confirm(prompt)->bool` 주입 가능(테스트 broker-free). 미주입 시 config 기반 transport(`call_rpc`) + 대화형 confirm.
- 인스턴스 라우팅 `_resolve_instance`: `--instance` > `--config`(TOML instance_key) > `default`(OQ-1).
- 큐: `topology.snapshot_queue(instance)`/`intent_queue(instance)`.
- pause/resume/paused: 기존 `PostgreSQLPauseStore` direct-DB 유지(OQ-3).
## 3. 명령별 매핑
- `universe`: snapshot RPC `{view:"all", actor:"operator", touch_watchdog:false, trading_date}` → assets 출력.
- `slots [--status held|buyable|all]`: snapshot RPC(억제) → assets; `holding`→`held` view 매핑.
- `enter <portfolio_id> [--capital/--qty/--entry-price/--strategy-id/--stop-loss/--take-profit]`: buy intent RPC(portfolio_id 필수, symbol 서버 resolve; symbol 미전송).
- `exit <slot_id> [--price/--reason]`: sell intent RPC(slot_id + optional limit_price; 미지정 시 시장가).
- trading_date 는 설정 타임존(dcy-014) 기준 today.
## 4. enter/exit 안전장치 (REQ-032, OQ-2) — `_intent_guard`
- `--dry-run`: resolved intent + 대상 인스턴스만 출력, 미제출(exit 0).
- live(`broker=kis`+allow_live) + `--live` 없음 → 에러(exit 2), 미제출.
- `--yes` 없음: `--json`(비대화형)이면 에러(exit 2); 아니면 confirm 프롬프트(기본 No) — 거부 시 미제출(exit 1).
- 통과 시 intent RPC 제출. admission 응답(`status/slot_id/reason`) 표시, `admitted` 아니면 exit 1.
- `_broker_mode(cfg)` → (broker, is_live). is_live = kis + allow_live.
## 5. 출력 (REQ-032)
- 사람용: 요약/표. `--json`: RPC 응답을 그대로 JSON(`json.dumps`). 실패: stderr 에 error(json 모드는 `{"error":...}`), exit 3.
## 6. baseline 문서 (REQ-026)
- interface-contract: 운영자 CLI↔RPC 매핑 표 + snapshot 요청 `touch_watchdog`/`actor` 플래그.
- runtime-flows: 운영자 CLI flow(라우팅/억제/안전장치/실패).
## 범위 밖 정합
- snapshot envelope 은 asset-centric(REQ-028 strict no-price) — slot_id 미포함. `exit` 는 운영자 제공 slot_id 를 intent RPC 로 전달(snapshot 에 slot_id 노출은 별도 계약 변경, 본 주기 밖). `slots` 는 보유 자산 상황 인지용.
## 테스트 설계
- `tests/runtime/test_cli_operator.py`(주입 rpc recorder/confirm): universe 억제 플래그/큐, slots held view, 인스턴스 라우팅, `--json` 유효성, enter 확인 거부 미제출/`--yes` 제출/rejected 비0/`--dry-run`/`--json` 요구 `--yes`, exit 가격/시장가, live 이중 ack(gate), RPC 실패 표시.
- `tests/application/test_messaging_snapshot_consumer.py`: 기본 요청 watchdog touch / operator 요청 suppress.
@@ -0,0 +1,36 @@
# BAT-020 Implementation
- Status: completed
- Batch: BAT-020
## 구현 요약
- dcy-016 운영자 CLI 확장(universe/slots/enter/exit, 데몬 RPC 통일) + 운영자 조회 워치독 억제를 전부 change-req 로 구현(신규 REQ 없음).
## 변경 내역
### REQ-021/034 — 워치독 억제 (서버)
- `trex_core/application/messaging_handlers.py::make_snapshot_rpc_handler`: `touch_watchdog = bool(body.get("touch_watchdog", True))`; `actor=="operator"` → 강제 false. `watchdog_store and touch_watchdog` 일 때만 holding `last_seen_at` 갱신. 기본값=현행(전략 폴링 갱신, 하위호환).
### REQ-032/022 — 운영자 CLI
- `trex_core/runtime/cli.py`(재작성): `main(argv, *, rpc=None, confirm=None)` 주입 가능.
- 라우팅 `_resolve_instance`(`--instance` > `--config` instance_key > `default`).
- `universe`/`slots`: snapshot RPC(`actor="operator"`/`touch_watchdog=false`), `holding`→`held`.
- `enter <portfolio_id>`: buy intent RPC(portfolio_id 필수, symbol 미전송 — 서버 resolve).
- `exit <slot_id>`: sell intent RPC(optional `--price`→limit_price, 미지정 시 시장가).
- 안전장치 `_intent_guard`: `--dry-run`(미제출), live(`broker=kis`+allow_live)는 `--yes --live` 이중 ack, `--yes` 없으면 confirm(기본 No)/`--json` 비대화형은 `--yes` 필수.
- `--json` 출력, RPC 실패는 stderr error + exit 3. admission rejected → exit 1.
- pause/resume/paused: 기존 `PostgreSQLPauseStore` direct-DB 유지(OQ-3).
- transport RPC: config 기반 `RabbitMQTransport`/`InMemoryTransport` `call_rpc` 재사용(REQ-022).
### REQ-026 — baseline 문서
- `docs/interface-contract.md`: 운영자 CLI↔RPC 매핑 표 + snapshot 요청 `touch_watchdog`/`actor` 플래그. Last Updated → BAT-020.
- `docs/runtime-flows.md`: 운영자 CLI flow(라우팅/억제/안전장치/실패). Last Updated → BAT-020.
## 테스트
- 신규 `tests/runtime/test_cli_operator.py`(주입 rpc/confirm, broker/DB free): 14건 — universe 억제 플래그/큐, slots held view, 인스턴스 라우팅, `--json` 유효성, enter 확인거부 미제출/`--yes` 제출/rejected 비0/`--dry-run`/`--json` 요구 `--yes`, exit 가격/시장가, live 이중 ack(gate+proceed), RPC 실패 표시.
- `tests/application/test_messaging_snapshot_consumer.py` 확장: 기본 요청 watchdog touch / operator 요청 suppress 2건.
## 검증 실행
- 단위: `TREX_BROKER=paper .venv/bin/python -m pytest -q` → `327 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` → `361 passed`.
- `ruff check trex_core tests` → All checks passed.
@@ -0,0 +1,34 @@
# BAT-020: 운영자 CLI 확장 (universe/slots/enter/exit) + 워치독 억제 (dcy-016)
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-016
- Included REQ: 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)
## Summary
- dcy-016(운영자 CLI 명령 확장 — universe/슬롯 확인 + enter/exit + pause/resume, 데몬 RPC 통일)을 단일 standard batch 로 delivery 한다. 거버넌스는 dcy-016 OQ-3 결정대로 **전부 change-req, 신규 REQ 없음**.
- 핵심: `trex-core` CLI 에 `universe`/`slots`(snapshot RPC)·`enter`(buy intent RPC)·`exit`(sell intent RPC) 추가. 전부 데몬 RabbitMQ RPC 로 통일해 단일 writer admission·불변식 보존(DB 직접 쓰기 없음). 운영자 조회는 `actor="operator"`/`touch_watchdog=false` 로 워치독 생존 신호를 억제(R-4). enter/exit 안전장치(확인 프롬프트/`--yes`/`--yes --live`/`--dry-run`). pause/resume 은 기존 direct-DB 유지.
## Scope
- In Scope:
- `universe`/`slots`/`enter`/`exit` CLI(전부 데몬 RPC) + 응답 표시 (REQ-032)
- CLI request-reply RPC 클라이언트 재사용 (REQ-022)
- snapshot 요청 워치독 억제 플래그(`touch_watchdog`/`actor`) (REQ-021) + 핸들러 억제 동작 (REQ-034)
- enter/exit 안전장치(확인/`--yes`/live 이중 ack/`--dry-run`) + 인스턴스 라우팅(`--config`/`--instance`/default) + 전 명령 `--json`
- interface-contract/runtime-flows 정합 (REQ-026)
- Out of Scope:
- 신규 서버측 RPC 표면/도메인 로직(기존 표면 재사용 — 워치독 억제만 snapshot 핸들러 국소 변경)
- buy/sell 계약 자체 변경(dcy-011/dcy-012)
- kill_switch CLI 노출(별도 결정)
- pause/resume 의 control RPC 통일(미채택 — 기존 direct-DB 유지, OQ-3)
## 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). OQ-1(`--config`+`--instance`/default)·OQ-2(안전장치 A안)·OQ-3(전부 change-req)·OQ-4(워치독 플래그) Answered.
- snapshot envelope 은 asset-centric(REQ-028 strict no-price) — slot_id 미포함. 따라서 `exit <slot_id>` 는 운영자 제공 slot_id 를 intent RPC 로 전달(snapshot 에 slot_id 노출은 별도 계약 변경이라 본 주기 밖). `slots` 는 보유 자산(asset_id/symbol/qty) 상황 인지용.
- 테스트 결정론: CLI 는 `rpc`/`confirm` 주입 가능(no broker/DB), snapshot 핸들러는 fake watchdog recorder.
@@ -0,0 +1,49 @@
# BAT-020 Planning
- Status: confirmed
- Batch: BAT-020
- Profile: standard
## Included REQ
- REQ-032 (Interface, change-req): CLI universe/slots/enter/exit + 인스턴스 라우팅 + enter/exit 안전장치 + `--json`.
- REQ-022 (Integration, change-req): CLI request-reply RPC 클라이언트(`call_rpc`) 재사용.
- REQ-021 (Integration, change-req): snapshot 요청 워치독 억제 플래그.
- REQ-034 (Interface, change-req): 핸들러가 억제 플래그 시 `last_seen_at` 미갱신.
- REQ-026 (Documentation, change-req): interface-contract/runtime-flows 정합.
## Delivery Plan
1. REQ-021/034(서버): `messaging_handlers.py::make_snapshot_rpc_handler` 에 `touch_watchdog`/`actor` 억제 분기(기본 현행 유지).
2. REQ-032/022(CLI): `cli.py` 재작성 — pause/resume/paused(direct-DB 유지) + universe/slots/enter/exit(RPC). 인스턴스 라우팅(`_resolve_instance`), RPC 빌드/주입(`rpc`), 안전장치(`_intent_guard`: confirm/`--yes`/`--yes --live`/`--dry-run`), `--json`, 실패 표시.
3. REQ-026(문서): interface-contract(CLI↔RPC 매핑 + 억제 플래그), runtime-flows(운영자 CLI flow).
4. 테스트: CLI(주입 rpc/confirm) + snapshot 핸들러 억제 + 전체 회귀 + ruff + 인프라 게이트.
## Dependencies
- Internal:
- dcy-008/BAT-008 CLI(REQ-032 pause/resume) — 확장 대상.
- dcy-006/BAT-007 transport `call_rpc`(REQ-022) — 재사용.
- dcy-005/BAT-008 snapshot 핸들러/envelope(REQ-028/021) + 워치독(REQ-034) — 억제 대상.
- dcy-011 buy(portfolio_id 필수)·dcy-012 sell(slot_id+optional price) 계약 — CLI payload 근거.
- dcy-009 `_instance_queue`(REQ-049) — 라우팅.
- External:
- RabbitMQ(실 RPC). 단위는 주입 rpc 로 broker-free.
## Design Gate
- Design Required Before Implementation: yes
- Reason: (a) enter/exit 는 실주문 표면(R-1) — 안전장치(확인/`--yes`/live 이중 ack/`--dry-run`)와 `--json` 비대화형 상호작용 규칙을 설계에서 못박지 않으면 오발주 위험. (b) 워치독 억제는 snapshot 핸들러 서버 동작 변경(REQ-034) — 기본값 현행 유지(하위호환) 경계와 플래그 의미를 설계에서 고정해야 안전 기능(safety-sell) 무력화/회귀를 막는다. (c) 인스턴스 라우팅(`--config`/`--instance`/default) 우선순위와 테스트용 rpc/confirm 주입 구조를 설계에서 명시. → design.md 동일 턴 scaffold.
## Milestones
1. M1 — REQ-021/034 snapshot 억제 분기, 핸들러 테스트 green
2. M2 — REQ-032/022 CLI universe/slots/enter/exit + 안전장치, CLI 테스트 green
3. M3 — REQ-026 문서 정합
4. M4 — 전체 회귀 + ruff + 인프라 게이트
## Risks
- R-1 (High): enter/exit 오발주/실주문. 대응: 확인 프롬프트 기본 ON·`--yes`(paper)·`--yes --live` 이중 ack·`--dry-run`·rejected 표시·대상 인스턴스 항상 표시. admission portfolio_id 검증(dcy-011)이 1차 방어선.
- R-2 (Medium): 잘못된 인스턴스 라우팅. 대응: `--config` canonical + `--instance` override + default + 출력에 대상 인스턴스 표시.
- R-3 (Low): RPC 통일로 데몬 미가동 시 조회 불가. 대응: 의도된 트레이드오프 문서화 + 명확한 에러 표시.
- R-4 (High): 운영자 조회의 워치독 부수효과로 safety-sell 무력화. 대응: 억제 플래그(REQ-021/034), 기본값 현행 유지.
## Notes
- 1차 게이트: 전체 회귀 green(워치독 기본 동작 무회귀).
- CLI 단위는 broker/DB 없이 rpc/confirm 주입으로 결정론 확보.
- 실 RabbitMQ 왕복 e2e 는 기존 transport 인프라 게이트로 커버(call_rpc 자체).
@@ -0,0 +1,60 @@
# BAT-020 Verification
- Status: verified
- Batch: BAT-020
## Acceptance Mapping
### REQ-032 (운영자 CLI, change-req)
| AC | Evidence |
|----|----------|
| universe snapshot RPC 조회(직접 DB 없음) | tests/runtime/test_cli_operator.py::test_universe_uses_snapshot_queue_with_operator_suppression |
| slots view 매핑(holding→held) | ::test_slots_status_holding_maps_to_held_view |
| enter buy intent RPC 제출 | ::test_enter_with_yes_submits_buy_intent |
| exit sell intent RPC 제출(가격/시장가) | ::test_exit_with_yes_submits_sell_with_limit_price / ::test_exit_market_when_no_price |
| enter/exit 안전장치(확인/dry-run/json 요구 yes) | ::test_enter_aborts_without_confirmation_no_rpc / ::test_enter_dry_run_does_not_submit / ::test_enter_json_requires_yes |
| live 이중 ack | ::test_live_requires_live_flag / ::test_live_proceeds_with_yes_live |
| 인스턴스 라우팅 | ::test_instance_shortcut_routes_to_instance_queue |
| rejected 비0 종료 | ::test_enter_rejected_returns_nonzero |
### REQ-022 (CLI RPC 클라이언트, change-req)
| AC | Evidence |
|----|----------|
| call_rpc 재사용(주입 가능) | test_cli_operator.py 전반(주입 rpc recorder) |
| RPC 실패 명확 표시 | ::test_rpc_failure_surfaces_error |
### REQ-021/034 (워치독 억제, change-req)
| AC | Evidence |
|----|----------|
| 기본 요청 watchdog touch | tests/application/test_messaging_snapshot_consumer.py::test_strategy_poll_touches_watchdog_by_default |
| operator 요청 suppress | ::test_operator_query_suppresses_watchdog_touch |
| CLI 가 억제 플래그 전송 | test_cli_operator.py::test_universe_uses_snapshot_queue_with_operator_suppression |
### REQ-026 (baseline 문서, change-req)
| AC | Evidence |
|----|----------|
| interface-contract/runtime-flows 정합 | 문서 diff(BAT-020) |
## Evidence
- Test:
- 단위 전체 회귀: `327 passed / 34 skipped`(신규 CLI 14 + 워치독 2 포함; `::test_sandbox_token_and_submit` deselect).
- 인프라 게이트(PG trex_core_test + RabbitMQ): `361 passed`.
- `ruff check trex_core tests` → All checks passed.
- Logs: `327 passed, 34 skipped, 1 deselected` / `361 passed, 1 deselected ~17.8s`.
## 성공 기준(dcy-016) 대응
- S-1(universe/slots snapshot RPC 조회, 직접 DB 없음): test_universe_*/test_slots_*.
- S-2(enter/exit intent RPC 제출 + admitted/rejected 표시, DB 쓰기 0): test_enter_with_yes_*/test_exit_*/test_enter_rejected_*.
- S-3(확인 프롬프트/`--yes`/live 가드, 미확인 시 미제출): test_enter_aborts_*/test_live_requires_live_flag/test_live_proceeds_*.
- S-4(인스턴스 라우팅 + 실패 표시): test_instance_shortcut_*/test_rpc_failure_surfaces_error.
- S-5(`--json` 유효 + enter/exit `--json` 요구 `--yes`): test_json_output_is_valid_json/test_enter_json_requires_yes.
## Carried-Over / 비차단
- 실 RabbitMQ 왕복 CLI e2e: 기존 transport `call_rpc` 인프라 게이트로 커버(CLI 는 주입 rpc 단위). 실 다중 인스턴스 라우팅 e2e 는 운영 검증 비차단.
- snapshot envelope 에 slot_id 미노출 → `exit` 는 운영자 제공 slot_id 사용. snapshot 에 slot_id 노출은 REQ-028 strict shape 변경이라 본 주기 밖(별도 결정).
- kill_switch CLI 노출은 Out of Scope(후속).
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. CLI 명령·안전장치·워치독 억제 모두 단위 green(361 passed 회귀, 워치독 기본 동작 무회귀). LIVE sandbox 토큰 deselect 는 `.env` 환경 의존(본 batch 무관).
+1
View File
@@ -27,3 +27,4 @@
| BAT-017 | standard | release-candidate | REQ-012 (CHG-20260608-01), REQ-013 (CHG-20260608-02), REQ-018 (CHG-20260608-02), REQ-031 (CHG-20260608-01), REQ-014 (CHG-20260608-01), REQ-015 (CHG-20260608-02) | dcy-013 | [bat-017](./bat-017_20260608_external-cancel-detection-stale-watchdog/) |
| 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/) |