feat(slot): BAT-030 — 자산 중심 cutover 완성 (dcy-020 BAT-δ, REQ-056·057·062·064·065)
M1 literal inactive 전면화: SlotState 5-state(idle/closed 별칭 제거), 마이그레이션 010(CHECK 교체+행 rewrite), ops_checks 를 transition_log 기반 재작성. M2 집행 매도 수량 브로커 정본(D3): dispatch 매도 qty=broker hldg_qty, 미러 fallback 없음, 브로커 0 → skip+WARN, 리스트 기반 dispatch(head-block 방지). M3 컬럼 drop + legacy pool 제거: 마이그레이션 011(trading_date·entry_count DROP), repo 계약 instance 스코프 개편(집행 큐 포함), preallocation 폐기(데몬 7스레드), admission 자산 중심 단일 경로(slot_id 정본), capacity 재정의(available=M−active), daily_symbol_entries 코드 경로 제거. M4 cutover 도구(D1·D4): trex-core-cutover(전체 rebuild, preflight/브로커 carry-over/ --allow-unmapped/--promote-unique/dry-run) + PG 리허설 테스트 8건. M5 baseline 마감: data-model/runtime-flows/project-structure/interface-contract 자산 중심 최종 갱신 + run-trex-core-daemon 스킬 정합. 검증: 단위 367 passed + ruff clean; PG-gated 29 passed(010/011 round-trip·드리프트 가드·cutover 리허설); RabbitMQ 21 passed; run-the-app e2e(test DB+실 MQ, M=1): reconcile 슬롯 생성→slot_id 매수→dedup→capacity_full→fill→already_active→ 매도→inactive(자산 바인딩 보존)→영구 슬롯 재활성 전 구간 green. 실 cutover 는 운영자 실행(REL 단계, D4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,6 @@ from trex_core.domain.slot import (
|
||||
SlotNotFoundError,
|
||||
SlotRepository,
|
||||
SlotService,
|
||||
SlotState,
|
||||
)
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
@@ -72,12 +71,12 @@ def make_intent_rpc_handler(
|
||||
intent_type = str(body.get("intent_type") or "").lower()
|
||||
try:
|
||||
if intent_type == "buy":
|
||||
# dcy-011 / REQ-021: buy payload carries portfolio_id (legacy) or,
|
||||
# in asset-centric mode (dcy-020/REQ-061), slot_id (canonical key).
|
||||
# symbol is NOT a client field — resolved from the universe.
|
||||
# dcy-020 / REQ-061: buy is keyed by slot_id (canonical);
|
||||
# portfolio_id is the legacy dual-accept mapping. symbol is NOT
|
||||
# a client field — resolved from the universe. trading_date was
|
||||
# dropped from the contract at the cutover (BAT-030).
|
||||
buy = BuyIntent(
|
||||
intent_id=intent_id,
|
||||
trading_date=_parse_date(body.get("trading_date")),
|
||||
portfolio_id=_opt_str(body.get("portfolio_id")) or None,
|
||||
slot_id=(UUID(str(body["slot_id"])) if body.get("slot_id") else None),
|
||||
qty=_opt_float(body.get("qty")),
|
||||
@@ -166,9 +165,10 @@ def make_snapshot_rpc_handler(
|
||||
slot_repo: SlotRepository,
|
||||
slot_service: SlotService,
|
||||
*,
|
||||
instance_key: str = "default",
|
||||
snapshot_store: Optional[SnapshotStore] = None,
|
||||
pause_store: Optional[PauseStore] = None,
|
||||
default_total_slots: int = 10,
|
||||
max_active_slots: Optional[int] = None,
|
||||
watchdog_store: Optional[WatchdogStore] = None,
|
||||
watchdog_timeout_sec: int = 120,
|
||||
killswitch_store=None,
|
||||
@@ -206,9 +206,8 @@ def make_snapshot_rpc_handler(
|
||||
if str(body.get("actor") or "").lower() == "operator":
|
||||
touch_watchdog = False
|
||||
if watchdog_store is not None and touch_watchdog:
|
||||
for s in slot_repo.list_slots(day):
|
||||
if s.status is SlotState.HOLDING:
|
||||
watchdog_store.update(str(s.slot_id))
|
||||
for s in slot_repo.list_holding_slots(instance_key):
|
||||
watchdog_store.update(str(s.slot_id))
|
||||
|
||||
# --- BAT-008 envelope path (snapshot store wired) ---
|
||||
if snapshot_store is not None and pause_store is not None:
|
||||
@@ -218,45 +217,49 @@ def make_snapshot_rpc_handler(
|
||||
except ValueError:
|
||||
view = View.ALL
|
||||
strategy_key = _opt_str(body.get("strategy_key"))
|
||||
cap = slot_service.compute_slot_capacity(day, default_total_slots)
|
||||
# dcy-020 / REQ-059: available = remaining admission headroom under
|
||||
# the M cap (max(0, M − active)); inactive count when no cap.
|
||||
cap = slot_service.compute_slot_capacity(max_active_slots)
|
||||
envelope = build_snapshot_envelope(
|
||||
store=snapshot_store,
|
||||
pause_store=pause_store,
|
||||
slot_repo=slot_repo,
|
||||
available_slots=cap.idle_count,
|
||||
instance_key=instance_key,
|
||||
available_slots=cap.available_count,
|
||||
view=view,
|
||||
strategy_key=strategy_key,
|
||||
trading_date=day,
|
||||
killswitch_store=killswitch_store,
|
||||
)
|
||||
payload = envelope.to_dict()
|
||||
_attach_watchdog(payload, slot_repo, day, watchdog_store, watchdog_timeout_sec)
|
||||
_attach_watchdog(payload, slot_repo, instance_key, watchdog_store, watchdog_timeout_sec)
|
||||
_apply_cash_estimate(payload, ledger_outbox)
|
||||
return RpcResponse(payload=payload)
|
||||
|
||||
# --- BAT-007 legacy stub-shape path ---
|
||||
view_raw = str(body.get("view") or "all")
|
||||
strategy_filter = _opt_str(body.get("strategy_id"))
|
||||
slots = slot_repo.list_slots(day)
|
||||
slots = slot_repo.list_slots(instance_key)
|
||||
if view_raw == "open":
|
||||
slots = [s for s in slots if s.status.value in ("pending_buy", "holding", "pending_sell")]
|
||||
if strategy_filter is not None:
|
||||
slots = [s for s in slots if s.strategy_id == strategy_filter]
|
||||
cap = slot_service.compute_slot_capacity(day, max(1, len(slot_repo.list_slots(day))))
|
||||
cap = slot_service.compute_slot_capacity(max_active_slots)
|
||||
payload = {
|
||||
"view": view_raw,
|
||||
"strategy_id": strategy_filter,
|
||||
"trading_date": day.isoformat(),
|
||||
"slots": [_slot_to_dict(s) for s in slots],
|
||||
"capacity": {
|
||||
"idle_count": cap.idle_count,
|
||||
"inactive_count": cap.inactive_count,
|
||||
"holding_count": cap.holding_count,
|
||||
"pending_count": cap.pending_count,
|
||||
"active_count": cap.active_count,
|
||||
"available_count": cap.available_count,
|
||||
"total_slots": cap.total_slots,
|
||||
"scarcity_ratio": cap.scarcity_ratio,
|
||||
},
|
||||
}
|
||||
_attach_watchdog(payload, slot_repo, day, watchdog_store, watchdog_timeout_sec)
|
||||
_attach_watchdog(payload, slot_repo, instance_key, watchdog_store, watchdog_timeout_sec)
|
||||
return RpcResponse(payload=payload)
|
||||
|
||||
return handler
|
||||
@@ -283,7 +286,7 @@ def _apply_cash_estimate(payload, ledger_outbox) -> None:
|
||||
return
|
||||
|
||||
|
||||
def _attach_watchdog(payload, slot_repo, day, watchdog_store, timeout_sec) -> None:
|
||||
def _attach_watchdog(payload, slot_repo, instance_key, watchdog_store, timeout_sec) -> None:
|
||||
"""Attach holding-slot watchdog state to a snapshot response (dcy-017/REQ-034).
|
||||
|
||||
`watchdog` = list of {slot_id, portfolio_id, symbol, expired, last_seen} for
|
||||
@@ -293,9 +296,7 @@ def _attach_watchdog(payload, slot_repo, day, watchdog_store, timeout_sec) -> No
|
||||
return
|
||||
try:
|
||||
states = []
|
||||
for s in slot_repo.list_slots(day):
|
||||
if s.status is not SlotState.HOLDING:
|
||||
continue
|
||||
for s in slot_repo.list_holding_slots(instance_key):
|
||||
sid = str(s.slot_id)
|
||||
last = watchdog_store.last_seen(sid)
|
||||
states.append({
|
||||
@@ -441,7 +442,6 @@ def slot_transition_event_payload(slot, *, event_type: str) -> dict:
|
||||
"qty": float(slot.qty) if slot.qty is not None else None,
|
||||
"avg_price": float(slot.avg_price) if slot.avg_price is not None else None,
|
||||
"portfolio_id": slot.portfolio_id,
|
||||
"trading_date": slot.trading_date.isoformat(),
|
||||
"sequence_at": (slot.updated_at or datetime.now(tz=timezone.utc)).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user