fix(cli): BAT-022 slots 명령이 universe 대신 실제 슬롯 상태 표시 (dcy-016 후속)

`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 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 19:54:42 +09:00
co-authored by Claude Opus 4.8
parent a0a149114d
commit 63da87c9b3
16 changed files with 574 additions and 46 deletions
+28 -15
View File
@@ -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)")