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)
|
||||||
@@ -849,7 +849,9 @@ class PinkDirectRuntime:
|
|||||||
decision = self.decision_engine.decide(snapshot, context, legacy_position)
|
decision = self.decision_engine.decide(snapshot, context, legacy_position)
|
||||||
if dc_blocked and decision.action == DecisionAction.ENTER:
|
if dc_blocked and decision.action == DecisionAction.ENTER:
|
||||||
import dataclasses
|
import dataclasses
|
||||||
decision = dataclasses.replace(decision, action=DecisionAction.HOLD, reason="DC_CONTRADICT")
|
decision = dataclasses.replace(decision, action=DecisionAction.HOLD_DC_CONTRADICTED, reason="DC_CONTRADICT")
|
||||||
|
self.logger.info("DC CONTRADICT: ENTER blocked (vel_div=%.4f scan=%d symbol=%s)",
|
||||||
|
self._last_vel_div, self._last_scan_number, getattr(snapshot, "symbol", "?"))
|
||||||
self._emit("decision", decision=decision)
|
self._emit("decision", decision=decision)
|
||||||
|
|
||||||
intent_context = IntentContext(
|
intent_context = IntentContext(
|
||||||
|
|||||||
Reference in New Issue
Block a user