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:
2026-06-10 15:16:15 +09:00
co-authored by Claude Opus 4.8
parent cec34be5f4
commit 7d727824dc
67 changed files with 1584 additions and 1862 deletions
+24 -23
View File
@@ -1,8 +1,8 @@
"""Slot domain data classes (REQ-001, REQ-002)."""
"""Slot domain data classes (REQ-001, REQ-002; dcy-020/REQ-056 asset-centric)."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import date, datetime
from datetime import datetime
from decimal import Decimal
from typing import Optional
from uuid import UUID
@@ -14,43 +14,37 @@ from trex_core.domain.slot.state import SlotState
class Slot:
"""Single daily_slot row.
Matches the slot column contract recorded in dcy-001 부록 A and
docs/data-model.md (trex-core stores this in-memory for now; a future
persistence layer must satisfy the same contract).
dcy-020/REQ-056 (BAT-030): the slot is a **permanent asset slot** — one row
per (instance_key, portfolio_id) universe asset, living across trading days.
Pool-model columns `trading_date` and `entry_count` were dropped at the
cutover (migration 011); time attributes live on events/ledger rows, not on
the slot. Matches docs/data-model.md.
"""
slot_id: UUID
trading_date: date
status: SlotState
symbol: Optional[str] = None
qty: Optional[float] = None
avg_price: Optional[float] = None
# dcy-012 / REQ-002: side-agnostic "current pending order limit price"
# (renamed from entry_price). Set on pending entry (buy/sell limit), cleared
# on pending exit (→HOLDING, →CLOSED) so a buy limit never leaks into a sell
# order (issue #10). None => market order at dispatch.
# on pending exit (→HOLDING, →INACTIVE) so a buy limit never leaks into a
# sell order (issue #10). None => market order at dispatch.
limit_price: Optional[float] = None
allocated_capital: Optional[float] = None
order_id: Optional[str] = None
strategy_id: Optional[str] = None
broker_order_id: Optional[str] = None # BAT-003 / REQ-011 (trex-orbit lineage)
portfolio_id: Optional[str] = None # BAT-006 / REQ-025 (trex-orbit lineage)
portfolio_id: Optional[str] = None # BAT-006 / REQ-025; asset binding (REQ-056)
sell_strategy_id: Optional[str] = None # BAT-008 / REQ-047 (trex-orbit lineage)
forced_close_at: Optional[datetime] = None # BAT-014 / REQ-066 (trex-orbit lineage)
stop_loss_price: Optional[Decimal] = None # REQ-035 / BAT-010
take_profit_price: Optional[Decimal] = None # REQ-035 / BAT-010
# dcy-019 / REQ-002 (BAT-025): slot-scoped cumulative entry count. +1 on each
# allocation transition (idle/closed → pending_buy); drives admission slot
# selection (lowest entry_count first). Distinct from the symbol-scoped
# daily_symbol_entries counter (REQ-002 AC-3) — different key/purpose.
entry_count: int = 0
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
# dcy-020 / REQ-056 (BAT-027): asset-centric permanent slot foundation (안 B).
# instance_key scopes slots per daemon instance in a shared DB
# (UNIQUE(instance_key, portfolio_id)); advisory lock is also instance-scoped.
# retired_at marks a soft-retired / universe-detached slot (OQ-2). Both added
# additively here; entry_count/trading_date removal is deferred to cutover (BAT-δ).
# dcy-020 / REQ-056: instance_key scopes slots per daemon instance in a
# shared DB (UNIQUE(instance_key, portfolio_id)); advisory lock is also
# instance-scoped. retired_at marks a soft-retired / universe-detached slot.
instance_key: str = "default"
retired_at: Optional[datetime] = None
@@ -61,7 +55,6 @@ class Slot:
# / docs/data-model.md), not here — names/presence only.
EXPECTED_SLOT_FIELDS: frozenset[str] = frozenset({
"slot_id",
"trading_date",
"status",
"symbol",
"qty",
@@ -76,7 +69,6 @@ EXPECTED_SLOT_FIELDS: frozenset[str] = frozenset({
"forced_close_at",
"stop_loss_price", # REQ-035
"take_profit_price", # REQ-035
"entry_count", # dcy-019 / REQ-002 (BAT-025)
"created_at",
"updated_at",
"instance_key", # dcy-020 / REQ-056 (BAT-027)
@@ -86,11 +78,20 @@ EXPECTED_SLOT_FIELDS: frozenset[str] = frozenset({
@dataclass(frozen=True)
class SlotCapacity:
"""Snapshot of slot capacity returned by compute_slot_capacity (REQ-002 AC-6)."""
"""Capacity snapshot returned by compute_slot_capacity (REQ-002 AC-6;
dcy-020/REQ-059 asset-centric redefinition, BAT-030).
idle_count: int
active_count = holding + pending (position-occupying). available_count is
the remaining admission headroom: max(0, M − active) under a max_active_slots
cap M, else the inactive (admittable) slot count. total_slots = M when set,
else the instance's slot count. scarcity_ratio = 1 − available/total.
"""
inactive_count: int
holding_count: int
pending_count: int
active_count: int
available_count: int
total_slots: int
scarcity_ratio: float