"""Canonical DITA contracts. Decision -> Intent -> Trade -> Account. These contracts are intentionally separate from execution, exchange, and observability. They are also designed to remain BLUE-compatible where the exchange model allows it. """ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime from enum import Enum from typing import Any, Dict, Optional, Tuple class DecisionAction(str, Enum): """Decision-level action.""" ENTER = "ENTER" HOLD = "HOLD" EXIT = "EXIT" FLAT = "FLAT" # Policy-gate vetoes — ENTER was signalled but blocked by an external gate. # Each value preserves the gate identity for CH persistence + TUI observability. # CRITICAL TODO: replace with KernelPolicyGate hook system (see memory: # project_kernelpolicygate_arch.md) — these values are the interim trace mechanism. HOLD_DC_CONTRADICTED = "HOLD_DC_CONTRADICTED" # DC gate: rising price in lookback class TradeSide(str, Enum): """Trade side.""" LONG = "LONG" SHORT = "SHORT" FLAT = "FLAT" class TradeStage(str, Enum): """Canonical lifecycle milestones.""" DECISION_CREATED = "DECISION_CREATED" INTENT_CREATED = "INTENT_CREATED" ORDER_REQUESTED = "ORDER_REQUESTED" ORDER_SENT = "ORDER_SENT" ORDER_ACKED = "ORDER_ACKED" POSITION_OPENED = "POSITION_OPENED" POSITION_UPDATED = "POSITION_UPDATED" EXIT_REQUESTED = "EXIT_REQUESTED" EXIT_SENT = "EXIT_SENT" EXIT_ACKED = "EXIT_ACKED" POSITION_PARTIALLY_CLOSED = "POSITION_PARTIALLY_CLOSED" POSITION_CLOSED = "POSITION_CLOSED" TRADE_TERMINAL_WRITTEN = "TRADE_TERMINAL_WRITTEN" @dataclass(frozen=True) class DecisionConfig: """Pure decision configuration.""" vel_div_threshold: float = -0.02 vel_div_extreme: float = -0.05 fixed_tp_pct: float = 0.0095 max_hold_bars: int = 120 catastrophic_loss_pct: float = 0.05 capital_fraction: float = 0.20 max_leverage: float = 5.0 min_irp_alignment: float = 0.0 allow_long: bool = False allow_short: bool = True exit_leg_ratios: Tuple[float, ...] = (1.0,) policy_version: str = "clean_arch_dita_v1" @dataclass(frozen=True) class DecisionContext: """Minimal state required to make a decision.""" capital: float open_positions: int = 0 trade_seq: int = 0 @dataclass(frozen=True) class IntentContext: """Minimal state required to size and route an intent.""" capital: float open_positions: int = 0 trade_seq: int = 0 @dataclass(frozen=True) class Decision: """Pure decision emitted by the decision layer.""" timestamp: datetime decision_id: str asset: str action: DecisionAction side: TradeSide reason: str confidence: float velocity_divergence: float irp_alignment: float reference_price: float target_size: float = 0.0 leverage: float = 1.0 bars_held: int = 0 stage: TradeStage = TradeStage.DECISION_CREATED metadata: Dict[str, Any] = field(default_factory=dict) @dataclass(frozen=True) class Intent: """Executable intent chosen from a decision.""" timestamp: datetime trade_id: str decision_id: str asset: str action: DecisionAction side: TradeSide reason: str target_size: float leverage: float reference_price: float confidence: float bars_held: int = 0 stage: TradeStage = TradeStage.INTENT_CREATED exit_leg_ratios: Tuple[float, ...] = (1.0,) metadata: Dict[str, Any] = field(default_factory=dict) @dataclass class TradePosition: """Mutable trade-side position state.""" trade_id: str asset: str side: TradeSide entry_price: float entry_time: datetime size: float leverage: float entry_velocity_divergence: float entry_irp_alignment: float bars_held: int = 0 current_price: float = 0.0 realized_pnl: float = 0.0 unrealized_pnl: float = 0.0 exit_price: float = 0.0 closed: bool = False close_reason: str = "" initial_size: float = 0.0 exit_leg_ratios: Tuple[float, ...] = (1.0,) exit_leg_index: int = 0 def __post_init__(self) -> None: if self.initial_size <= 0: self.initial_size = self.size if self.current_price <= 0: self.current_price = self.entry_price def mark_price(self, price: float) -> None: """Refresh mark price and unrealized PnL.""" if price is None or price != price or price in (float("inf"), float("-inf")) or price <= 0: return self.current_price = price if self.entry_price <= 0: self.unrealized_pnl = 0.0 return delta = (price - self.entry_price) / self.entry_price if self.side == TradeSide.SHORT: delta = -delta self.unrealized_pnl = delta * self.size * self.entry_price * self.leverage def age_one_bar(self) -> None: self.bars_held += 1 @property def remaining_size(self) -> float: return max(0.0, self.size) def next_exit_ratio(self) -> float: if self.exit_leg_index < len(self.exit_leg_ratios): ratio = self.exit_leg_ratios[self.exit_leg_index] self.exit_leg_index += 1 return max(0.0, min(1.0, float(ratio))) return 1.0 @dataclass(frozen=True) class TradeEvent: """Append-only event row for observability.""" timestamp: datetime trade_id: str decision_id: str asset: str stage: TradeStage action: DecisionAction side: TradeSide reason: str price: float size: float leverage: float capital_before: float capital_after: float open_positions_before: int open_positions_after: int pnl: float = 0.0 pnl_pct: float = 0.0 bars_held: int = 0 metadata: Dict[str, Any] = field(default_factory=dict) @dataclass(frozen=True) class AccountEvent: """Canonical account projection row.""" timestamp: datetime runtime_namespace: str strategy_namespace: str event_namespace: str actor_name: str exec_venue: str data_venue: str ledger_authority: str capital: float equity: float open_positions: int current_open_notional: float current_account_leverage: float decision_id: str trade_id: str asset: str side: TradeSide reason: str stage: TradeStage pnl: float = 0.0 pnl_pct: float = 0.0 bars_held: int = 0 metadata: Dict[str, Any] = field(default_factory=dict)