67 production .py modules that the running PINK service imports but which were never committed: prod/bingx/ (HTTP client, market/user streams, journal, config), prod/clean_arch/ adapters/persistence/runtime/dita/dita_v2 production modules and their co-located tests. Rule going forward: every module imported by launch_dolphin_pink.py / pink_direct.py must appear in git ls-files. Excludes _backup dirs, __pycache__, and non-code files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
133 lines
5.4 KiB
Python
133 lines
5.4 KiB
Python
"""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}"
|