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:
Codex
2026-06-03 17:05:49 +02:00
parent 29d44c338e
commit beef39eaf5
2 changed files with 248 additions and 1 deletions

View 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)

View File

@@ -849,7 +849,9 @@ class PinkDirectRuntime:
decision = self.decision_engine.decide(snapshot, context, legacy_position)
if dc_blocked and decision.action == DecisionAction.ENTER:
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)
intent_context = IntentContext(