fix(broker): enforce operator broker truth boundary
This commit is contained in:
@@ -13,6 +13,21 @@ from decimal import Decimal
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
|
||||
class BrokerBalanceError(RuntimeError):
|
||||
"""Sanitized failure distinct from a validated empty broker balance."""
|
||||
|
||||
_CODES = frozenset({
|
||||
"token_issuance_failed", "authentication_failed", "authorization_failed",
|
||||
"transport_failed", "malformed_response", "broker_rejected",
|
||||
})
|
||||
|
||||
def __init__(self, code: str) -> None:
|
||||
if code not in self._CODES:
|
||||
raise ValueError(f"unsupported broker balance error code: {code}")
|
||||
self.code = code
|
||||
super().__init__(f"broker balance unavailable ({code})")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OrderRequest:
|
||||
slot_id: str # originating slot (audit/correlation)
|
||||
|
||||
@@ -58,7 +58,8 @@ def reconcile_core_holdings(
|
||||
→ set by reconcile_positions
|
||||
|
||||
warn_store: Optional — when None, WARN-log-only (backward compat, REQ-103 AC-5).
|
||||
Returns {key: 'error'|'qty_warning'|'ok'|'unmanaged_warning'} for telemetry.
|
||||
Returns {key: 'error'|'qty_warning'|'ok'|'unmanaged_position'|
|
||||
'managed_non_holding_divergence'} for telemetry.
|
||||
"""
|
||||
broker_qty: dict[str, int] = {
|
||||
item.symbol: int(item.hldg_qty) for item in broker.get_balance()
|
||||
@@ -114,13 +115,23 @@ def reconcile_core_holdings(
|
||||
)
|
||||
else:
|
||||
decisions[str(slot.slot_id)] = "ok"
|
||||
for sym, bq in broker_qty.items(): # unmanaged broker position
|
||||
for sym, bq in broker_qty.items(): # unmatched broker position
|
||||
if bq > 0 and sym not in matched:
|
||||
decisions[f"broker:{sym}"] = "unmanaged_warning"
|
||||
_log.warning(
|
||||
"broker holds %s qty=%d with no core holding slot (unmanaged position)",
|
||||
sym, bq,
|
||||
)
|
||||
managed = slot_repo.find_managed_slot_by_symbol(instance_key, sym)
|
||||
if managed is not None:
|
||||
decisions[f"broker:{sym}"] = "managed_non_holding_divergence"
|
||||
_log.warning(
|
||||
"managed non-holding divergence: slot_id=%s portfolio_id=%s "
|
||||
"symbol=%s status=%s instance_key=%s",
|
||||
managed.slot_id, managed.portfolio_id, sym,
|
||||
managed.status.value, instance_key,
|
||||
)
|
||||
else:
|
||||
decisions[f"broker:{sym}"] = "unmanaged_position"
|
||||
_log.warning(
|
||||
"unmanaged position: symbol=%s qty=%d instance_key=%s",
|
||||
sym, bq, instance_key,
|
||||
)
|
||||
return decisions
|
||||
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class SnapshotScheduler:
|
||||
warn_store: Optional[ReconcileWarningStore] = None,
|
||||
credential_fetch_enabled: bool = False,
|
||||
kis_reinit_callback: Optional[Callable[[BrokerCredentialPayload], None]] = None,
|
||||
credential_refresh: Optional[Callable[[], object]] = None,
|
||||
) -> None:
|
||||
self._read = read_port
|
||||
self._store = store
|
||||
@@ -77,6 +78,8 @@ class SnapshotScheduler:
|
||||
# KISBroker with the fetched day-start credential. None = no reinit (paper
|
||||
# broker or credential_fetch_enabled=False). Set by daemon.py assembly.
|
||||
self._kis_reinit_callback: Optional[Callable[[BrokerCredentialPayload], None]] = kis_reinit_callback
|
||||
# REQ-119: provider-owned refresh supersedes the legacy raw fetch hook.
|
||||
self._credential_refresh = credential_refresh
|
||||
# REQ-113: detect break-glass at init time (env var set by operator).
|
||||
self._break_glass: bool = (
|
||||
os.environ.get("TREX_KIS_BROKER_BREAK_GLASS", "").lower()
|
||||
@@ -250,7 +253,17 @@ class SnapshotScheduler:
|
||||
# Must run before portfolio snapshot so entry_allowed is set before
|
||||
# the first BuyIntent can arrive from the strategy daemon.
|
||||
if self._credential_fetch_enabled:
|
||||
self._maybe_fetch_credential()
|
||||
if self._credential_refresh is None:
|
||||
self._maybe_fetch_credential()
|
||||
else:
|
||||
try:
|
||||
self._credential_refresh()
|
||||
self._entry_allowed = True
|
||||
except Exception as exc: # provider exposes only sanitized failure
|
||||
_log.error("DAY_START_LOAD_FAILED: broker credential refresh failed: %s (instance=%s)",
|
||||
exc.__class__.__name__, self._instance_key)
|
||||
self._entry_allowed = False
|
||||
self._emit_day_start_alarm(reason="broker_credential_unavailable")
|
||||
|
||||
if self._daemon_id is None:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user