feat: add kill-switch control flow
This commit is contained in:
@@ -164,6 +164,7 @@ def make_snapshot_rpc_handler(
|
||||
pause_store: Optional[PauseStore] = None,
|
||||
default_total_slots: int = 10,
|
||||
watchdog_store: Optional[WatchdogStore] = None,
|
||||
killswitch_store=None,
|
||||
):
|
||||
"""Build the RPC handler for `trex.snapshot.req` (REQ-028).
|
||||
|
||||
@@ -211,6 +212,7 @@ def make_snapshot_rpc_handler(
|
||||
view=view,
|
||||
strategy_key=strategy_key,
|
||||
trading_date=day,
|
||||
killswitch_store=killswitch_store,
|
||||
)
|
||||
return RpcResponse(payload=envelope.to_dict())
|
||||
|
||||
@@ -282,6 +284,78 @@ def _err(reason: str) -> RpcResponse:
|
||||
return RpcResponse(payload={"status": "error", "reason": reason})
|
||||
|
||||
|
||||
# ---- control RPC handler (REQ-043, BAT-012) ---------------------------------
|
||||
|
||||
def make_control_rpc_handler(pause_store: PauseStore, *, kill_switch_service=None):
|
||||
"""RPC handler for `trex.control.req` — action discriminator dispatch (REQ-043).
|
||||
|
||||
Actions: `pause`/`resume`/`list` (REQ-032, delegated), `kill_switch` (REQ-045),
|
||||
`resume_kill_switch` (REQ-046). Unknown actions return an error response.
|
||||
"""
|
||||
pause_handler = make_pause_rpc_handler(pause_store)
|
||||
|
||||
def handler(req: RpcRequest) -> RpcResponse:
|
||||
body = req.payload or {}
|
||||
action = str(body.get("action") or "").lower()
|
||||
if action == "kill_switch":
|
||||
return _handle_kill_switch(body, kill_switch_service)
|
||||
if action == "resume_kill_switch":
|
||||
return _handle_resume_kill_switch(body, kill_switch_service)
|
||||
if action in ("pause", "resume", "list"):
|
||||
return pause_handler(req)
|
||||
return _err(f"unknown_action:{action}")
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
def _killswitch_scope(body: dict):
|
||||
"""Validate scope/portfolio_id. Returns (scope, pid) or (None, RpcResponse error)."""
|
||||
scope = str(body.get("scope") or "").lower()
|
||||
pid = _opt_str(body.get("portfolio_id"))
|
||||
if scope not in ("portfolio", "global"):
|
||||
return None, _err(f"bad_scope:{scope}")
|
||||
if scope == "portfolio" and not pid:
|
||||
return None, _err("portfolio_id_required")
|
||||
return (scope, pid), None
|
||||
|
||||
|
||||
def _handle_kill_switch(body: dict, svc) -> RpcResponse:
|
||||
"""REQ-045: liquidate holding slots in scope + day-exclude."""
|
||||
if svc is None:
|
||||
return _err("kill_switch_unsupported")
|
||||
parsed, err = _killswitch_scope(body)
|
||||
if err is not None:
|
||||
return err
|
||||
scope, pid = parsed
|
||||
actor = str(body.get("actor") or "operator")
|
||||
try:
|
||||
liquidated = svc.fire(scope=scope, portfolio_id=pid, actor=actor)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return _err(f"kill_switch_failed:{exc.__class__.__name__}")
|
||||
payload = {"status": "ok", "scope": scope, "liquidated": liquidated}
|
||||
if pid:
|
||||
payload["portfolio_id"] = pid
|
||||
return RpcResponse(payload=payload)
|
||||
|
||||
|
||||
def _handle_resume_kill_switch(body: dict, svc) -> RpcResponse:
|
||||
"""REQ-046: clear the day-scoped exclusion (sold slots stay sold)."""
|
||||
if svc is None:
|
||||
return _err("resume_kill_switch_unsupported")
|
||||
parsed, err = _killswitch_scope(body)
|
||||
if err is not None:
|
||||
return err
|
||||
scope, pid = parsed
|
||||
try:
|
||||
svc.resume(scope=scope, portfolio_id=pid)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return _err(f"resume_kill_switch_failed:{exc.__class__.__name__}")
|
||||
payload = {"status": "ok", "scope": scope}
|
||||
if pid:
|
||||
payload["portfolio_id"] = pid
|
||||
return RpcResponse(payload=payload)
|
||||
|
||||
|
||||
# ---- event payload builder --------------------------------------------------
|
||||
|
||||
def slot_transition_event_payload(slot, *, event_type: str) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user