feat: use broker-authoritative qty in slots RPC holdings
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
832dcb5fbb
commit
30bcbe1d12
@@ -217,6 +217,19 @@ class AccountSnapshotService:
|
||||
)
|
||||
return list(snapshot.holdings)
|
||||
|
||||
def holdings_for_slots_rpc(self) -> tuple[list[BalanceItem], datetime]:
|
||||
"""Return freshness-validated snapshot holdings for strategy slots reads.
|
||||
|
||||
Unlike ``holdings_for_sell()``, this read path never performs a live broker
|
||||
call and therefore cannot mutate broker state or mask snapshot staleness.
|
||||
"""
|
||||
snapshot = self._load_current()
|
||||
self._age(snapshot.holdings_captured_at, self._sell_max_age)
|
||||
captured_at = snapshot.holdings_captured_at
|
||||
if captured_at is None:
|
||||
raise AccountSnapshotUnavailable("not_ready")
|
||||
return list(snapshot.holdings), captured_at
|
||||
|
||||
def buy_guard(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -9,10 +9,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
from datetime import date as date_cls
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from typing import Optional, Protocol
|
||||
from uuid import UUID
|
||||
|
||||
from trex_core.application.intents import BuyIntent, IntentService, SellIntent
|
||||
from trex_core.application.account_snapshot import AccountSnapshotUnavailable
|
||||
from trex_core.application.broker_port import BalanceItem
|
||||
from trex_core.application.messaging import RpcRequest, RpcResponse
|
||||
from trex_core.application.portfolio import (
|
||||
PauseStore,
|
||||
@@ -185,6 +187,10 @@ _FILTER_STATUS: dict[str, set[str]] = {
|
||||
}
|
||||
|
||||
|
||||
class SlotsBrokerQuantityReader(Protocol):
|
||||
def holdings_for_slots_rpc(self) -> tuple[list[BalanceItem], datetime]: ...
|
||||
|
||||
|
||||
def make_slots_rpc_handler(
|
||||
slot_repo: SlotRepository,
|
||||
*,
|
||||
@@ -197,6 +203,7 @@ def make_slots_rpc_handler(
|
||||
ledger_outbox=None,
|
||||
max_active_slots: Optional[int] = None,
|
||||
strategy_state_store=None,
|
||||
broker_quantity_reader: Optional[SlotsBrokerQuantityReader] = None,
|
||||
):
|
||||
"""Build the RPC handler for `trex.slots.req` (REQ-073/074/116).
|
||||
|
||||
@@ -252,6 +259,36 @@ def make_slots_rpc_handler(
|
||||
for s in slots
|
||||
]
|
||||
|
||||
broker_qty_by_symbol: dict[str, int] = {}
|
||||
broker_qty_available = False
|
||||
broker_qty_captured_at: Optional[datetime] = None
|
||||
if broker_quantity_reader is not None:
|
||||
try:
|
||||
holdings, broker_qty_captured_at = broker_quantity_reader.holdings_for_slots_rpc()
|
||||
broker_qty_by_symbol = {
|
||||
str(item.symbol): int(item.hldg_qty)
|
||||
for item in holdings
|
||||
}
|
||||
broker_qty_available = True
|
||||
except AccountSnapshotUnavailable:
|
||||
broker_qty_available = False
|
||||
|
||||
for row, slot in zip(slot_rows, slots):
|
||||
row["slot_quantity"] = row.get("qty")
|
||||
if slot.status.value != "holding":
|
||||
continue
|
||||
row["broker_quantity_available"] = broker_qty_available
|
||||
row["broker_quantity_captured_at"] = (
|
||||
broker_qty_captured_at.isoformat() if broker_qty_captured_at is not None else None
|
||||
)
|
||||
if broker_qty_available:
|
||||
broker_qty = int(broker_qty_by_symbol.get(str(slot.symbol), 0))
|
||||
row["qty"] = float(broker_qty)
|
||||
row["qty_source"] = "broker"
|
||||
else:
|
||||
row["qty"] = None
|
||||
row["qty_source"] = "unavailable"
|
||||
|
||||
# dcy-028 / REQ-073/074·079 (C-1): attach opaque strategy_state map per
|
||||
# row via one batched store read (read piggyback — no extra round trip).
|
||||
# Only entries matching the slot's current occupancy_seq are surfaced.
|
||||
|
||||
@@ -152,7 +152,8 @@ def _cmd_universe(args, cfg, rpc) -> int:
|
||||
f"degraded={resp.get('degraded')}")
|
||||
for s in slots:
|
||||
print(f" {s.get('portfolio_id')}\t{s.get('symbol')}\t"
|
||||
f"qty(hub)={s.get('asset_quantity')}\tqty(slot)={s.get('qty')}")
|
||||
f"qty(hub)={s.get('asset_quantity')}\t"
|
||||
f"qty(slot)={s.get('slot_quantity', s.get('qty'))}")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -192,7 +193,8 @@ def _cmd_slots(args, cfg, rpc) -> int:
|
||||
wd = s.get("watchdog") or {}
|
||||
tag = " [WATCHDOG: strategy absent]" if wd.get("expired") else ""
|
||||
print(f" {s.get('slot_id')}\t{s.get('status')}\t{s.get('symbol')}\t"
|
||||
f"qty={s.get('qty')}\taqty={s.get('asset_quantity')}\t"
|
||||
f"qty={s.get('qty')}\tsqty={s.get('slot_quantity')}\t"
|
||||
f"aqty={s.get('asset_quantity')}\t"
|
||||
f"limit={s.get('entry_price')}\tavg={s.get('avg_price')}\t"
|
||||
f"pid={pid}\tpaused={s.get('paused')}\twarned={s.get('warned')}{tag}")
|
||||
return 0
|
||||
@@ -205,7 +207,11 @@ def _print_slot_detail(s: dict) -> None:
|
||||
("status", s.get("status")),
|
||||
("symbol", s.get("symbol")),
|
||||
("portfolio_id", s.get("portfolio_id")),
|
||||
("qty (slot mirror)", s.get("qty")),
|
||||
("qty (authoritative)", s.get("qty")),
|
||||
("slot_quantity (core mirror)", s.get("slot_quantity")),
|
||||
("qty_source", s.get("qty_source")),
|
||||
("broker_quantity_available", s.get("broker_quantity_available")),
|
||||
("broker_quantity_captured_at", s.get("broker_quantity_captured_at")),
|
||||
("asset_quantity (hub)", s.get("asset_quantity")),
|
||||
("warned", s.get("warned")),
|
||||
("avg_price", s.get("avg_price")),
|
||||
|
||||
@@ -445,6 +445,7 @@ class Daemon:
|
||||
ledger_outbox=self.outbox,
|
||||
max_active_slots=self.config.max_active_slots,
|
||||
strategy_state_store=self.strategy_state_store, # dcy-028 C-1
|
||||
broker_quantity_reader=self.account_snapshot_service,
|
||||
)
|
||||
transport.consume_rpc_forever(
|
||||
topology.slots_queue(self.config.instance_key), handler, self.stop)
|
||||
|
||||
Reference in New Issue
Block a user