feat: persist unmanaged position lifecycle state

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-12 13:24:51 +00:00
committed by GitHub
co-authored by genichin
parent ae2e598209
commit fe51d211d9
12 changed files with 1274 additions and 5 deletions
+97 -4
View File
@@ -33,7 +33,11 @@ from typing import cast
from trex_core.application.alarm_emitter import AlarmEmitter
from trex_core.application.broker_port import BrokerPort
from trex_core.application.portfolio import ReconcileWarningStore, SnapshotStore
from trex_core.application.portfolio import (
ReconcileWarningStore,
SnapshotStore,
UnmanagedPositionStore,
)
from trex_core.domain.slot.state import SlotState
_log = logging.getLogger(__name__)
@@ -341,6 +345,25 @@ def _retry_pending_resolved(
)
def _notify_unmanaged_position_transition(
alarm_emitter: AlarmEmitter,
*,
event_type: str,
instance_key: str,
symbol: str,
quantity: int,
) -> None:
alarm_emitter.publish_alarm(
f"unmanaged_position_{event_type}",
f"unmanaged position {event_type}: symbol={symbol}",
fields={
"instance_key": instance_key,
"symbol": symbol,
"broker_quantity": quantity,
},
)
def reconcile_core_holdings(
*,
slot_repo,
@@ -350,6 +373,7 @@ def reconcile_core_holdings(
alarm_emitter: AlarmEmitter | None = None,
warn_store: ReconcileWarningStore | None = None,
notification_store=None,
unmanaged_position_store: UnmanagedPositionStore | None = None,
) -> dict[str, str]:
"""dcy-020 / REQ-062 (BAT-029) + REQ-103 (BAT-050): surface core-slot holding
vs broker divergence and set durable reconcile-warning for B case.
@@ -377,6 +401,7 @@ def reconcile_core_holdings(
decisions: dict[str, str] = {}
matched: set[str] = set()
active_b_identity_keys: set[str] = set() # req-0007: track active B mismatches for retry
active_unmanaged_symbols: set[str] = set()
for slot in slot_repo.list_holding_slots(instance_key):
sym = slot.symbol
matched.add(sym)
@@ -458,10 +483,78 @@ def reconcile_core_holdings(
)
else:
decisions[f"broker:{sym}"] = "unmanaged_position"
_log.warning(
"unmanaged position: symbol=%s qty=%d instance_key=%s",
sym, bq, instance_key,
active_unmanaged_symbols.add(sym)
if unmanaged_position_store is None:
_log.warning(
"unmanaged position: symbol=%s qty=%d instance_key=%s",
sym, bq, instance_key,
)
else:
try:
observed = unmanaged_position_store.observe(
instance_key=instance_key,
symbol=sym,
quantity=bq,
)
except Exception: # noqa: BLE001
_log.exception(
"failed to persist unmanaged_position for symbol=%s instance=%s",
sym,
instance_key,
)
_log.warning(
"unmanaged position: symbol=%s qty=%d instance_key=%s",
sym, bq, instance_key,
)
else:
transition = observed["transition"]
if transition == "detected":
_log.warning(
"unmanaged position detected: symbol=%s qty=%d instance_key=%s",
sym, bq, instance_key,
)
if alarm_emitter is not None:
_notify_unmanaged_position_transition(
alarm_emitter,
event_type="detected",
instance_key=instance_key,
symbol=sym,
quantity=bq,
)
elif transition == "quantity_changed":
_log.warning(
"unmanaged position quantity changed: symbol=%s "
"previous_qty=%s qty=%d instance_key=%s",
sym, observed.get("previous_quantity"), bq, instance_key,
)
if alarm_emitter is not None:
_notify_unmanaged_position_transition(
alarm_emitter,
event_type="changed",
instance_key=instance_key,
symbol=sym,
quantity=bq,
)
if unmanaged_position_store is not None:
try:
for state in unmanaged_position_store.resolve_absent(
instance_key=instance_key,
observed_symbols=active_unmanaged_symbols,
):
_log.info(
"unmanaged position resolved: symbol=%s qty=%d instance_key=%s",
state["symbol"], state["quantity"], instance_key,
)
if alarm_emitter is not None:
_notify_unmanaged_position_transition(
alarm_emitter,
event_type="resolved",
instance_key=instance_key,
symbol=state["symbol"],
quantity=state["quantity"],
)
except Exception: # noqa: BLE001
_log.exception("reconcile_core_holdings: unmanaged_position resolve failed")
# req-0007 / ac-0024: retry pending_resolved for B-case identities no longer mismatched.
if alarm_emitter is not None and notification_store is not None:
try: