BAT-032 (dcy-022): timeout_sec/slippage_pct 실행 정책, 미체결 자동 취소, 보호 지정가, `trex-core cancel` 백스톱. 마이그레이션 012. BAT-033: `trex-core enter --qty` 필수 검증 — 미지정 시 RPC 전 즉시 오류. BAT-034 (dcy-023): allocated_capital 죽은 필드 전 경로 제거 (domain·intent· persistence·snapshot·CLI). 마이그레이션 013(비가역 DROP). 외부 발신자 unknown key 무시 수신 의미론 적용. REL-006 confirmed: BAT-026~034 통합 릴리즈, pre-release smoke 증거 포함. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
115 lines
4.2 KiB
Python
115 lines
4.2 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
|
||
|
||
|
||
# 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)
|
||
})
|
||
|
||
|
||
@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)
|