feat(core-A): reconcile-warning durable store + blocking gate (BAT-050, REQ-102/103/104/107/108)

B/C 불일치 시 슬롯을 durable 차단(ReconcileWarningStore)하고 전략 intent 경로에
pause∨reconcile_warned 게이트를 추가한다. 안전망(safety-sell/kill-switch/watchdog)
경로는 SlotService 직접 호출로 게이트를 구조적으로 우회(OQ-B 확정).
SlotState enum/전이표/wire 계약 필드는 불변, reconcile_warned 필드만 additive 추가.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
2026-06-26 00:43:04 +09:00
co-authored by Claude Sonnet 4.6
parent 35f3147250
commit c193388198
17 changed files with 1410 additions and 27 deletions
+16 -1
View File
@@ -4,6 +4,8 @@ Wakes at the configured KST time, fetches universe from PortfolioHubReadPort,
swaps the SnapshotStore, then runs reconciliation. Option D `*_after` writes
(REQ-030 AC-2/AC-3) are wired through `SnapshotStore.update_cash_position`
but disabled at the ledger layer until portfolio-hub#8 ships.
BAT-050 / REQ-103: reconcile now accepts warn_store for durable reconcile-warning.
"""
from __future__ import annotations
@@ -18,6 +20,7 @@ from trex_core.application.portfolio import (
DegradedReason,
PortfolioHubReadPort,
PortfolioHubUnavailableError,
ReconcileWarningStore,
SnapshotStore,
)
from trex_core.application.reconciliation import reconcile_positions
@@ -42,6 +45,7 @@ class SnapshotScheduler:
slot_service=None,
instance_key: str = "default",
alarm_emitter: Optional[AlarmEmitter] = None,
warn_store: Optional[ReconcileWarningStore] = None,
) -> None:
self._read = read_port
self._store = store
@@ -59,6 +63,8 @@ class SnapshotScheduler:
self._last_universe_size: Optional[int] = None
# dcy-033 / REQ-100: best-effort alarm emitter.
self._alarm_emitter = alarm_emitter
# BAT-050 / REQ-103: durable reconcile-warning store (Optional).
self._warn_store = warn_store
# ---- callable from a daemon thread loop --------------------------------
def run_forever(self, stop: threading.Event, *, tick_sec: float = 60.0) -> None:
@@ -124,7 +130,14 @@ class SnapshotScheduler:
self._store.swap(items)
try:
reconcile_positions(store=self._store, broker=self._broker)
# BAT-050 / REQ-103: pass warn_store + slot_repo for C-case durable warning.
reconcile_positions(
store=self._store,
broker=self._broker,
slot_repo=self._slot_repo,
instance_key=self._instance_key,
warn_store=self._warn_store,
)
except Exception: # noqa: BLE001
_log.exception("reconciliation pass failed")
if self._slot_repo is not None:
@@ -135,10 +148,12 @@ class SnapshotScheduler:
if self._slot_service is not None:
try:
from trex_core.application.reconciliation import reconcile_core_holdings
# BAT-050 / REQ-103: pass warn_store for B-case durable warning.
reconcile_core_holdings(
slot_repo=self._slot_repo, slot_service=self._slot_service,
broker=self._broker, instance_key=self._instance_key,
alarm_emitter=self._alarm_emitter, # dcy-033 / REQ-100
warn_store=self._warn_store, # BAT-050 / REQ-103
)
except Exception: # noqa: BLE001
_log.exception("core-holding divergence reconcile failed")