"""Intent planning layer.""" from __future__ import annotations from dataclasses import dataclass from datetime import datetime from typing import Optional from .contracts import Decision, DecisionAction, DecisionConfig, DecisionContext, Intent, IntentContext, TradePosition, TradeSide, TradeStage @dataclass(frozen=True) class IntentPlanResult: intent: Intent trade_id_created: bool class IntentEngine: """Converts a pure decision into an executable intent. This is where sizing and trade identity are attached. """ def __init__(self, config: Optional[DecisionConfig] = None): self.config = config or DecisionConfig() def plan( self, decision: Decision, context: IntentContext, position: Optional[TradePosition] = None, ) -> IntentPlanResult: if decision.action == DecisionAction.ENTER: return self._plan_entry(decision, context) if decision.action == DecisionAction.EXIT and position is not None: return self._plan_exit(decision, context, position) return IntentPlanResult( intent=Intent( timestamp=decision.timestamp, trade_id=decision.decision_id.replace("-D-", "-T-"), decision_id=decision.decision_id, asset=decision.asset, action=decision.action, side=decision.side, reason=decision.reason, target_size=0.0, leverage=1.0, reference_price=decision.reference_price, confidence=decision.confidence, bars_held=0, stage=TradeStage.INTENT_CREATED, exit_leg_ratios=self.config.exit_leg_ratios, metadata={"policy_version": self.config.policy_version, **decision.metadata}, ), trade_id_created=False, ) def _plan_entry(self, decision: Decision, context: IntentContext) -> IntentPlanResult: price = decision.reference_price confidence = max(0.05, min(1.0, decision.confidence)) # Honor the decision's sizing when present (BLUE-parity cubic sizer # attaches leverage + target_size in DecisionEngine._decide_entry). # For legacy decisions the recompute below yields the identical values, # so preferring the decision's numbers is behavior-preserving. if decision.leverage and decision.leverage > 0 and decision.target_size and decision.target_size > 0: leverage = float(decision.leverage) target_size = float(decision.target_size) target_exposure = target_size * price if price > 0 else 0.0 else: leverage = min(self.config.max_leverage, max(1.0, 1.0 + confidence * (self.config.max_leverage - 1.0))) target_exposure = context.capital * self.config.capital_fraction * leverage target_size = target_exposure / price if price > 0 else 0.0 trade_id = self._trade_id(decision.asset, context.trade_seq + 1) return IntentPlanResult( intent=Intent( timestamp=decision.timestamp, trade_id=trade_id, decision_id=decision.decision_id, asset=decision.asset, action=decision.action, side=decision.side, reason=decision.reason, target_size=target_size, leverage=leverage, reference_price=price, confidence=confidence, bars_held=0, stage=TradeStage.INTENT_CREATED, exit_leg_ratios=self.config.exit_leg_ratios, metadata={ "policy_version": self.config.policy_version, "target_exposure": target_exposure, "entry_velocity_divergence": decision.velocity_divergence, "entry_irp_alignment": decision.irp_alignment, **decision.metadata, }, ), trade_id_created=True, ) def _plan_exit(self, decision: Decision, context: IntentContext, position: TradePosition) -> IntentPlanResult: exit_ratio = position.next_exit_ratio() target_size = position.size * exit_ratio if exit_ratio > 0 else position.size return IntentPlanResult( intent=Intent( timestamp=decision.timestamp, trade_id=position.trade_id, decision_id=decision.decision_id, asset=position.asset, action=decision.action, side=position.side, reason=decision.reason, target_size=target_size, leverage=position.leverage, reference_price=decision.reference_price, confidence=decision.confidence, bars_held=position.bars_held, stage=TradeStage.INTENT_CREATED, exit_leg_ratios=position.exit_leg_ratios, metadata={ "policy_version": self.config.policy_version, "exit_ratio": exit_ratio, "remaining_size_before": position.size, **decision.metadata, }, ), trade_id_created=False, ) @staticmethod def _trade_id(symbol: str, seq: int) -> str: return f"{symbol}-T-{seq:012d}"