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>
110 lines
3.9 KiB
Python
110 lines
3.9 KiB
Python
"""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 datetime
|
||
from decimal import Decimal
|
||
from typing import Optional
|
||
from uuid import UUID
|
||
|
||
from trex_core.domain.slot.state import SlotState
|
||
|
||
|
||
@dataclass
|
||
class Slot:
|
||
"""Single daily_slot row.
|
||
|
||
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
|
||
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, →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; 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
|
||
created_at: Optional[datetime] = None
|
||
updated_at: Optional[datetime] = None
|
||
# 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
|
||
|
||
|
||
# Canonical column contract for `Slot` (dcy-001 OQ-2 / FR-5).
|
||
# test_contract.py asserts this matches `dataclasses.fields(Slot)` so any
|
||
# field add/remove fails loudly. Type mapping lives in the docs (dcy-001 부록 A
|
||
# / docs/data-model.md), not here — names/presence only.
|
||
EXPECTED_SLOT_FIELDS: frozenset[str] = frozenset({
|
||
"slot_id",
|
||
"status",
|
||
"symbol",
|
||
"qty",
|
||
"avg_price",
|
||
"limit_price",
|
||
"allocated_capital",
|
||
"order_id",
|
||
"strategy_id",
|
||
"broker_order_id",
|
||
"portfolio_id",
|
||
"sell_strategy_id",
|
||
"forced_close_at",
|
||
"stop_loss_price", # REQ-035
|
||
"take_profit_price", # REQ-035
|
||
"created_at",
|
||
"updated_at",
|
||
"instance_key", # dcy-020 / REQ-056 (BAT-027)
|
||
"retired_at", # dcy-020 / REQ-056 (BAT-027)
|
||
})
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class SlotCapacity:
|
||
"""Capacity snapshot returned by compute_slot_capacity (REQ-002 AC-6;
|
||
dcy-020/REQ-059 asset-centric redefinition, BAT-030).
|
||
|
||
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
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class SlotTransitionRecord:
|
||
"""One append-only row in slot_transition_log (REQ-001, REQ-002)."""
|
||
|
||
slot_id: UUID
|
||
from_state: SlotState
|
||
to_state: SlotState
|
||
ts: datetime
|
||
reason: str
|
||
actor: str
|
||
extra: dict = field(default_factory=dict)
|