feat(slot): BAT-029 M2(a)/M3 — holding 불일치 reconcile + slots 단일 뷰 (dcy-020/REQ-062·063)

- REQ-062 divergence: list_holding_slots(protocol+in_memory+PG) +
  reconcile_core_holdings(A=코어 holding/broker0→error, C=수량 불일치→warning,
  B=broker 보유/코어 없음→warning; 자동 청산 없음 issue #15). snapshot_scheduler
  자산 중심 모드에서 best-effort 호출, daemon slot_service 배선.
- REQ-063: slot_to_dict 에 universe_detached(retired_at) 추가 — slots 단일 뷰 tombstone 표시.
- 신규 테스트: test_holding_divergence(4).

M2(b) 청산 수량 브로커 정본(execution dispatch)은 라이브 집행 변경 → BAT-δ carry-over.

검증: 단위 393 passed / 36 skipped, PG-gated 18 passed, ruff clean. legacy 무파손.
REQ-061/062/063 Approved 유지(γ 경로 도입 검증; default 전환·M2(b) 는 BAT-δ).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 12:18:56 +09:00
co-authored by Claude Opus 4.8
parent b80407098e
commit 78a743a2ef
10 changed files with 215 additions and 10 deletions
@@ -38,6 +38,7 @@ class SnapshotScheduler:
schedule_kst: time = time(hour=7, minute=30),
clock: Optional[Callable[[], datetime]] = None,
slot_repo=None,
slot_service=None,
instance_key: str = "default",
reconcile_enabled: bool = False,
trading_date_fn: Optional[Callable[[], object]] = None,
@@ -52,6 +53,7 @@ class SnapshotScheduler:
# dcy-020 / REQ-058 (BAT-028): asset-centric slot reconcile after each
# universe fetch (only when enabled = asset-centric mode).
self._slot_repo = slot_repo
self._slot_service = slot_service
self._instance_key = instance_key
self._reconcile_enabled = reconcile_enabled
self._trading_date_fn = trading_date_fn
@@ -115,6 +117,15 @@ class SnapshotScheduler:
self._reconcile_slots(items)
except Exception: # noqa: BLE001
_log.exception("slot reconcile pass failed")
if self._slot_service is not None:
try:
from trex_core.application.reconciliation import reconcile_core_holdings
reconcile_core_holdings(
slot_repo=self._slot_repo, slot_service=self._slot_service,
broker=self._broker, instance_key=self._instance_key,
)
except Exception: # noqa: BLE001
_log.exception("core-holding divergence reconcile failed")
self._last_fired_date = self._clock()
return True