feat: add portfolio snapshot read layer
This commit is contained in:
@@ -14,6 +14,12 @@ from uuid import UUID
|
||||
|
||||
from trex_core.application.intents import BuyIntent, IntentService, SellIntent
|
||||
from trex_core.application.messaging import RpcRequest, RpcResponse
|
||||
from trex_core.application.portfolio import (
|
||||
PauseStore,
|
||||
SnapshotStore,
|
||||
View,
|
||||
build_snapshot_envelope,
|
||||
)
|
||||
from trex_core.domain.slot import SlotRepository, SlotService
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
@@ -89,32 +95,68 @@ def make_intent_rpc_handler(intent_service: IntentService):
|
||||
|
||||
# ---- snapshot RPC handler ---------------------------------------------------
|
||||
|
||||
def make_snapshot_rpc_handler(slot_repo: SlotRepository, slot_service: SlotService):
|
||||
"""Build the RPC handler for `trex.snapshot.req`.
|
||||
def make_snapshot_rpc_handler(
|
||||
slot_repo: SlotRepository,
|
||||
slot_service: SlotService,
|
||||
*,
|
||||
snapshot_store: Optional[SnapshotStore] = None,
|
||||
pause_store: Optional[PauseStore] = None,
|
||||
default_total_slots: int = 10,
|
||||
):
|
||||
"""Build the RPC handler for `trex.snapshot.req` (REQ-028).
|
||||
|
||||
Payload shape (stub for BAT-007 — dcy-005 logical contract is not yet
|
||||
REQ-normalized; this returns a transport-shape evidence equivalent of
|
||||
`GET /v1/slots` + `GET /v1/capacity`):
|
||||
When `snapshot_store` + `pause_store` are provided (production wiring),
|
||||
returns the BAT-008 envelope:
|
||||
{
|
||||
"strategy_id": "<filter or null>",
|
||||
"view": "open" | "all" (default "all"),
|
||||
"trading_date": "YYYY-MM-DD" (optional, default today UTC)
|
||||
"assets": list of {symbol, asset_id, cash_balance,
|
||||
asset_quantity, active_strategy_id,
|
||||
strategy_params},
|
||||
"view": "all" | "buyable" | "held",
|
||||
"available_slots": int,
|
||||
"degraded": bool,
|
||||
"reason": DegradedReason value or null,
|
||||
"message": sanitized text or null
|
||||
}
|
||||
|
||||
Without those, falls back to the BAT-007 slot+capacity stub shape so
|
||||
older callers and existing tests don't break.
|
||||
"""
|
||||
|
||||
def handler(req: RpcRequest) -> RpcResponse:
|
||||
body = req.payload or {}
|
||||
view = str(body.get("view") or "all")
|
||||
strategy_filter = _opt_str(body.get("strategy_id"))
|
||||
day = _parse_date(body.get("trading_date")) if body.get("trading_date") else _today()
|
||||
|
||||
# --- BAT-008 envelope path (snapshot store wired) ---
|
||||
if snapshot_store is not None and pause_store is not None:
|
||||
requested_view = str(body.get("view") or "all").lower()
|
||||
try:
|
||||
view = View(requested_view)
|
||||
except ValueError:
|
||||
view = View.ALL
|
||||
strategy_key = _opt_str(body.get("strategy_key"))
|
||||
cap = slot_service.compute_slot_capacity(day, default_total_slots)
|
||||
envelope = build_snapshot_envelope(
|
||||
store=snapshot_store,
|
||||
pause_store=pause_store,
|
||||
slot_repo=slot_repo,
|
||||
available_slots=cap.idle_count,
|
||||
view=view,
|
||||
strategy_key=strategy_key,
|
||||
trading_date=day,
|
||||
)
|
||||
return RpcResponse(payload=envelope.to_dict())
|
||||
|
||||
# --- BAT-007 legacy stub-shape path ---
|
||||
view_raw = str(body.get("view") or "all")
|
||||
strategy_filter = _opt_str(body.get("strategy_id"))
|
||||
slots = slot_repo.list_slots(day)
|
||||
if view == "open":
|
||||
if view_raw == "open":
|
||||
slots = [s for s in slots if s.status.value in ("pending_buy", "holding", "pending_sell")]
|
||||
if strategy_filter is not None:
|
||||
slots = [s for s in slots if s.strategy_id == strategy_filter]
|
||||
cap = slot_service.compute_slot_capacity(day, max(1, len(slot_repo.list_slots(day))))
|
||||
return RpcResponse(payload={
|
||||
"view": view,
|
||||
"view": view_raw,
|
||||
"strategy_id": strategy_filter,
|
||||
"trading_date": day.isoformat(),
|
||||
"slots": [_slot_to_dict(s) for s in slots],
|
||||
@@ -130,6 +172,48 @@ def make_snapshot_rpc_handler(slot_repo: SlotRepository, slot_service: SlotServi
|
||||
return handler
|
||||
|
||||
|
||||
# ---- pause/resume RPC handler (REQ-032 AC-4) --------------------------------
|
||||
|
||||
def make_pause_rpc_handler(pause_store: PauseStore):
|
||||
"""RPC handler for the `trex.pause.req` control queue.
|
||||
|
||||
Payload: {"action": "pause" | "resume" | "list", "portfolio_id": "..."}
|
||||
Response: {"status": "ok"|"error", "paused": [list-of-portfolio_id], "reason"?: str}
|
||||
"""
|
||||
|
||||
def handler(req: RpcRequest) -> RpcResponse:
|
||||
body = req.payload or {}
|
||||
action = str(body.get("action") or "").lower()
|
||||
pid = _opt_str(body.get("portfolio_id"))
|
||||
actor = str(body.get("actor") or "operator")
|
||||
try:
|
||||
if action == "pause":
|
||||
if not pid:
|
||||
return _err("portfolio_id required")
|
||||
pause_store.pause(pid, actor=actor)
|
||||
elif action == "resume":
|
||||
if not pid:
|
||||
return _err("portfolio_id required")
|
||||
pause_store.resume(pid)
|
||||
elif action == "list":
|
||||
pass
|
||||
else:
|
||||
return _err(f"unknown action: {action!r}")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return _err(f"pause store error: {exc.__class__.__name__}")
|
||||
return RpcResponse(payload={
|
||||
"status": "ok",
|
||||
"paused": sorted(pause_store.list_paused()),
|
||||
"action": action,
|
||||
})
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
def _err(reason: str) -> RpcResponse:
|
||||
return RpcResponse(payload={"status": "error", "reason": reason})
|
||||
|
||||
|
||||
# ---- event payload builder --------------------------------------------------
|
||||
|
||||
def slot_transition_event_payload(slot, *, event_type: str) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user