From d334f34215da6ece76c5722f1a600ec3f291b875 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Wed, 10 Jun 2026 16:55:24 +0900 Subject: [PATCH] =?UTF-8?q?feat(cli):=20trex-core=20slots=20=EC=97=90=20--?= =?UTF-8?q?symbol/--slot-id=20=ED=95=84=ED=84=B0=20+=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EC=8B=9C=20=EC=83=81=EC=84=B8=20=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 운영자 CLI 의 slots 조회 편의: --symbol, --slot-id 클라이언트 측 필터(--status 와 조합 가능) 추가. 두 필터 중 하나라도 주면 한 줄 요약 대신 슬롯 상세 블록 (status/symbol/portfolio_id/qty/avg_price/limit/allocated_capital/strategy_id/ sell_strategy_id/forced_close_at/universe_detached, holding 이면 watchdog)으로 출력. 필터 없으면 기존 terse 테이블 유지(무회귀). 구조/계약/런타임 흐름 영향 없는 표시 기능. tests/runtime/test_cli_operator.py 필터 4건 추가(24 passed), ruff clean. Co-Authored-By: Claude Opus 4.8 --- tests/runtime/test_cli_operator.py | 34 +++++++++++++++++ trex_core/runtime/cli.py | 61 ++++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/tests/runtime/test_cli_operator.py b/tests/runtime/test_cli_operator.py index 453f775..0d45943 100644 --- a/tests/runtime/test_cli_operator.py +++ b/tests/runtime/test_cli_operator.py @@ -92,6 +92,40 @@ def test_slots_status_filters_by_slot_status(capsys): assert "count=1" in out and "S-1\tholding" in out and "S-2" not in out +def test_slots_symbol_filters_to_one_symbol(capsys): + """`--symbol` shows only the slot(s) bound to that symbol (client-side filter).""" + rpc = _RpcRecorder(_SNAPSHOT) + cli.main(["slots", "--symbol", "005930"], rpc=rpc) + out = capsys.readouterr().out + assert "count=1" in out and "S-1" in out and "S-2" not in out + + +def test_slots_symbol_combines_with_status(capsys): + """`--symbol` composes with `--status` (both must match).""" + rpc = _RpcRecorder(_SNAPSHOT) + cli.main(["slots", "--status", "holding", "--symbol", "999999"], rpc=rpc) + out = capsys.readouterr().out + assert "count=0" in out # S-1 is holding but symbol != 999999 + + +def test_slots_slot_id_filters_to_one_slot(capsys): + """`--slot-id` shows only the matching slot, in the detailed block.""" + rpc = _RpcRecorder(_SNAPSHOT) + cli.main(["slots", "--slot-id", "S-2"], rpc=rpc) + out = capsys.readouterr().out + assert "count=1" in out and "slot S-2" in out and "S-1" not in out + # detailed block fields present + assert "portfolio_id" in out and "universe_detached" in out + + +def test_slots_symbol_detailed_output(capsys): + """`--symbol` switches to the detailed per-slot block (not the terse table).""" + rpc = _RpcRecorder(_SNAPSHOT) + cli.main(["slots", "--symbol", "005930"], rpc=rpc) + out = capsys.readouterr().out + assert "slot S-1" in out and "status" in out and "avg_price" in out + + def test_slots_marks_watchdog_owned(capsys): """dcy-017: slots flags strategy-absent (watchdog-owned) positions.""" resp = { diff --git a/trex_core/runtime/cli.py b/trex_core/runtime/cli.py index 9d3d277..d40e92c 100644 --- a/trex_core/runtime/cli.py +++ b/trex_core/runtime/cli.py @@ -140,19 +140,62 @@ def _cmd_slots(args, cfg, rpc) -> int: if s.get("status") == "inactive" and not s.get("universe_detached")] elif status == "pending": slots = [s for s in slots if s.get("status") in ("pending_buy", "pending_sell")] + # --symbol / --slot-id: client-side filters (composable with --status). + if args.symbol: + slots = [s for s in slots if s.get("symbol") == args.symbol] + if args.slot_id: + slots = [s for s in slots if s.get("slot_id") == args.slot_id] # dcy-017: holding-slot watchdog state (strategy-absent ownership). owned = {w.get("portfolio_id") for w in resp.get("watchdog", []) if w.get("expired")} + wd_by_slot = {w.get("slot_id"): w for w in resp.get("watchdog", [])} if args.json: _emit({"instance": instance, "status": status, "slots": slots}, as_json=True) else: - print(f"instance={instance} status={status} count={len(slots)} " + header = f"instance={instance} status={status}" + if args.symbol: + header += f" symbol={args.symbol}" + if args.slot_id: + header += f" slot_id={args.slot_id}" + print(f"{header} 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" {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}") + # When filtered (--symbol / --slot-id), print a detailed per-slot block; + # otherwise the terse one-line table. + if args.symbol or args.slot_id: + for s in slots: + _print_slot_detail(s, wd_by_slot.get(s.get("slot_id"))) + else: + for s in slots: + pid = s.get("portfolio_id") + tag = " [WATCHDOG-OWNED: strategy absent]" if pid in owned else "" + 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 + + +def _print_slot_detail(s: dict, wd: dict | None) -> None: + """Verbose per-slot block used when `slots` is filtered (e.g. --symbol).""" + fields = [ + ("status", s.get("status")), + ("symbol", s.get("symbol")), + ("portfolio_id", s.get("portfolio_id")), + ("qty", s.get("qty")), + ("avg_price", s.get("avg_price")), + ("entry_price (limit)", s.get("entry_price")), + ("allocated_capital", s.get("allocated_capital")), + ("strategy_id", s.get("strategy_id")), + ("sell_strategy_id", s.get("sell_strategy_id")), + ("forced_close_at", s.get("forced_close_at")), + ("universe_detached", s.get("universe_detached")), + ] + if wd is not None: + fields.append(( + "watchdog", + f"expired={wd.get('expired')} last_seen={wd.get('last_seen')}", + )) + print(f"slot {s.get('slot_id')}") + for label, value in fields: + print(f" {label:<20}: {value}") return 0 @@ -290,6 +333,10 @@ def _build_parser() -> argparse.ArgumentParser: p_slots = sub.add_parser("slots", help="List positions (snapshot RPC)") p_slots.add_argument("--status", choices=["all", "held", "holding", "buyable", "pending"], default="all") + p_slots.add_argument("--symbol", default=None, + help="show only the slot(s) bound to this symbol") + p_slots.add_argument("--slot-id", dest="slot_id", default=None, + help="show only the slot with this slot_id") _add_common(p_slots) p_enter = sub.add_parser("enter", help="Submit a buy intent (intent RPC)")