132 lines
5.1 KiB
Python
132 lines
5.1 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
|
||
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)
|