"""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 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 # dcy-022 / REQ-068: execution policy — set on PENDING entry, cleared on exit. # timeout_sec: cancel_order is sent after this many seconds of no fill. # slippage_pct: effective limit = entry_price * (1 ± pct/100); None = no band. timeout_sec: Optional[int] = None slippage_pct: Optional[float] = None # dcy-028 / REQ-081 (BAT-042): monotonic occupancy counter. Incremented when # the slot begins a new occupancy (inactive→pending_buy), stable across one # holding episode. Used by the strategy-state CAS to reject writes computed # against a prior occupancy of a reused slot (point-ABA guard, dcy-028 R-1). occupancy_seq: int = 0 # Order venue carried from the admitted intent through dispatch/cancel. # It is reset to KRX when the permanent asset slot returns to INACTIVE. venue: str = "KRX" # Immediate cancel-request result. The slot remains pending until fill # polling confirms cancelled/filled. Persisted to prevent blind retries # across ticks and restarts. cancel_state: Optional[str] = None cancel_requested_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", "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) "timeout_sec", # dcy-022 / REQ-068 (BAT-032) "slippage_pct", # dcy-022 / REQ-068 (BAT-032) "occupancy_seq", # dcy-028 / REQ-081 (BAT-042) "venue", "cancel_state", "cancel_requested_at", }) @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)