PINK: HOLD_DC_CONTRADICTED enum + trace log (104/104 green)
- contracts.py: DecisionAction.HOLD_DC_CONTRADICTED = "HOLD_DC_CONTRADICTED" Interim policy-gate veto enum. Comment marks CRITICAL TODO: KernelPolicyGate hook system (downstream-registered hooks; see memory). - pink_direct.py: dc_contradicts() now sets HOLD_DC_CONTRADICTED (was plain HOLD) + logger.info trace with vel_div / scan_number / symbol — observable in logs, CH persistence, and Hz engine_snapshot. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
245
prod/clean_arch/dita/contracts.py
Normal file
245
prod/clean_arch/dita/contracts.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user