feat(cli): trex-core slots 에 --symbol/--slot-id 필터 + 필터 시 상세 출력
운영자 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
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):
|
def test_slots_marks_watchdog_owned(capsys):
|
||||||
"""dcy-017: slots flags strategy-absent (watchdog-owned) positions."""
|
"""dcy-017: slots flags strategy-absent (watchdog-owned) positions."""
|
||||||
resp = {
|
resp = {
|
||||||
|
|||||||
@@ -140,13 +140,30 @@ def _cmd_slots(args, cfg, rpc) -> int:
|
|||||||
if s.get("status") == "inactive" and not s.get("universe_detached")]
|
if s.get("status") == "inactive" and not s.get("universe_detached")]
|
||||||
elif status == "pending":
|
elif status == "pending":
|
||||||
slots = [s for s in slots if s.get("status") in ("pending_buy", "pending_sell")]
|
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).
|
# dcy-017: holding-slot watchdog state (strategy-absent ownership).
|
||||||
owned = {w.get("portfolio_id") 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")}
|
||||||
|
wd_by_slot = {w.get("slot_id"): w for w in resp.get("watchdog", [])}
|
||||||
if args.json:
|
if args.json:
|
||||||
_emit({"instance": instance, "status": status, "slots": slots}, as_json=True)
|
_emit({"instance": instance, "status": status, "slots": slots}, as_json=True)
|
||||||
else:
|
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])}")
|
f"watchdog_owned={len([s for s in slots if s.get('portfolio_id') in owned])}")
|
||||||
|
# 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:
|
for s in slots:
|
||||||
pid = s.get("portfolio_id")
|
pid = s.get("portfolio_id")
|
||||||
tag = " [WATCHDOG-OWNED: strategy absent]" if pid in owned else ""
|
tag = " [WATCHDOG-OWNED: strategy absent]" if pid in owned else ""
|
||||||
@@ -156,6 +173,32 @@ def _cmd_slots(args, cfg, rpc) -> int:
|
|||||||
return 0
|
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
|
||||||
|
|
||||||
|
|
||||||
def _intent_guard(args, cfg, summary: dict, action: str, confirm) -> int | None:
|
def _intent_guard(args, cfg, summary: dict, action: str, confirm) -> int | None:
|
||||||
"""Shared enter/exit safeguard. Returns an exit code to short-circuit, or
|
"""Shared enter/exit safeguard. Returns an exit code to short-circuit, or
|
||||||
None to proceed with submission."""
|
None to proceed with submission."""
|
||||||
@@ -290,6 +333,10 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||||||
p_slots = sub.add_parser("slots", help="List positions (snapshot RPC)")
|
p_slots = sub.add_parser("slots", help="List positions (snapshot RPC)")
|
||||||
p_slots.add_argument("--status", choices=["all", "held", "holding", "buyable", "pending"],
|
p_slots.add_argument("--status", choices=["all", "held", "holding", "buyable", "pending"],
|
||||||
default="all")
|
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)
|
_add_common(p_slots)
|
||||||
|
|
||||||
p_enter = sub.add_parser("enter", help="Submit a buy intent (intent RPC)")
|
p_enter = sub.add_parser("enter", help="Submit a buy intent (intent RPC)")
|
||||||
|
|||||||
Reference in New Issue
Block a user