Files
trex-core/trex_core/domain/slot/models.py
T

132 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)