feat: use broker-authoritative qty in slots RPC holdings

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-08-24 14:15:43 +00:00
committed by GitHub
co-authored by genichin
parent 832dcb5fbb
commit 30bcbe1d12
11 changed files with 303 additions and 15 deletions
+13
View File
@@ -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,
*,
+38 -1
View File
@@ -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.
+9 -3
View File
@@ -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")),
+1
View File
@@ -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)