repo hygiene: track the PINK launcher import closure
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>
This commit is contained in:
49
prod/clean_arch/dita/__init__.py
Normal file
49
prod/clean_arch/dita/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""DITA boundary for clean-arch trading experiments.
|
||||
|
||||
Decision -> Intent -> Trade -> Account
|
||||
|
||||
This package is infrastructure-free. It provides the canonical contracts
|
||||
and pure engines used by the simulator and by any future adapters that need
|
||||
BLUE/PINK comparable semantics.
|
||||
"""
|
||||
|
||||
from .account import AccountProjection, AccountSnapshot
|
||||
from .contracts import (
|
||||
AccountEvent,
|
||||
Decision,
|
||||
DecisionAction,
|
||||
DecisionConfig,
|
||||
DecisionContext,
|
||||
Intent,
|
||||
IntentContext,
|
||||
TradeEvent,
|
||||
TradePosition,
|
||||
TradeSide,
|
||||
TradeStage,
|
||||
)
|
||||
from .decision import DecisionEngine
|
||||
from .intent import IntentEngine
|
||||
from .observability import DitaObservabilityNamespace, LEGACY_ANOMALY_SENSOR_KEY
|
||||
from .trade import TradeExecutionResult, TradeExecutor
|
||||
|
||||
__all__ = [
|
||||
"AccountEvent",
|
||||
"AccountProjection",
|
||||
"AccountSnapshot",
|
||||
"Decision",
|
||||
"DecisionAction",
|
||||
"DecisionConfig",
|
||||
"DecisionContext",
|
||||
"DecisionEngine",
|
||||
"DitaObservabilityNamespace",
|
||||
"Intent",
|
||||
"IntentContext",
|
||||
"IntentEngine",
|
||||
"LEGACY_ANOMALY_SENSOR_KEY",
|
||||
"TradeEvent",
|
||||
"TradeExecutionResult",
|
||||
"TradeExecutor",
|
||||
"TradePosition",
|
||||
"TradeSide",
|
||||
"TradeStage",
|
||||
]
|
||||
118
prod/clean_arch/dita/account.py
Normal file
118
prod/clean_arch/dita/account.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""Account projection and CH/HZ-shaped rows."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
import math
|
||||
|
||||
from .contracts import AccountEvent, Decision, Intent, TradePosition, TradeSide, TradeStage
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccountSnapshot:
|
||||
"""Derived account state used for projections and row emission."""
|
||||
|
||||
capital: float
|
||||
equity: float
|
||||
realized_pnl: float = 0.0
|
||||
unrealized_pnl: float = 0.0
|
||||
open_positions: int = 0
|
||||
open_notional: float = 0.0
|
||||
fees_paid: float = 0.0
|
||||
trade_seq: int = 0
|
||||
|
||||
@property
|
||||
def leverage(self) -> float:
|
||||
if self.capital <= 0 or self.open_notional <= 0:
|
||||
return 0.0
|
||||
return self.open_notional / self.capital
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccountProjection:
|
||||
"""Thin account projection.
|
||||
|
||||
This is not policy. It only projects confirmed execution facts into the
|
||||
live account view and the durable row shape used by CH/HZ/TUI consumers.
|
||||
"""
|
||||
|
||||
runtime_namespace: str = "pink"
|
||||
strategy_namespace: str = "pink"
|
||||
event_namespace: str = "pink"
|
||||
actor_name: str = "clean_arch"
|
||||
exec_venue: str = "bingx"
|
||||
data_venue: str = "binance"
|
||||
ledger_authority: str = "exchange"
|
||||
min_capital: float = 0.0
|
||||
max_capital: Optional[float] = None
|
||||
snapshot: AccountSnapshot = field(default_factory=lambda: AccountSnapshot(capital=25_000.0, equity=25_000.0))
|
||||
|
||||
def observe_position(self, position: Optional[TradePosition]) -> None:
|
||||
if position is None:
|
||||
self.snapshot.open_positions = 0
|
||||
self.snapshot.open_notional = 0.0
|
||||
self.snapshot.unrealized_pnl = 0.0
|
||||
self.snapshot.equity = self.snapshot.capital
|
||||
return
|
||||
self.snapshot.open_positions = 1
|
||||
mark = position.current_price
|
||||
if not math.isfinite(mark) or mark <= 0:
|
||||
mark = position.entry_price if math.isfinite(position.entry_price) and position.entry_price > 0 else 0.0
|
||||
self.snapshot.open_notional = mark * position.size
|
||||
self.snapshot.unrealized_pnl = position.unrealized_pnl
|
||||
self.snapshot.equity = self.snapshot.capital + position.unrealized_pnl
|
||||
|
||||
def settle(self, realized_pnl: float, fees: float = 0.0) -> None:
|
||||
if not math.isfinite(realized_pnl):
|
||||
realized_pnl = 0.0
|
||||
new_capital = self.snapshot.capital + realized_pnl
|
||||
if not math.isfinite(new_capital):
|
||||
new_capital = self.snapshot.capital
|
||||
if self.max_capital is not None:
|
||||
new_capital = min(new_capital, self.max_capital)
|
||||
new_capital = max(self.min_capital, new_capital)
|
||||
self.snapshot.capital = new_capital
|
||||
self.snapshot.realized_pnl += realized_pnl
|
||||
self.snapshot.fees_paid += fees
|
||||
self.snapshot.equity = self.snapshot.capital + self.snapshot.unrealized_pnl
|
||||
if not math.isfinite(self.snapshot.equity):
|
||||
self.snapshot.equity = self.snapshot.capital
|
||||
|
||||
def to_event(
|
||||
self,
|
||||
*,
|
||||
timestamp: datetime,
|
||||
decision: Decision,
|
||||
intent: Intent,
|
||||
position: Optional[TradePosition],
|
||||
stage: TradeStage,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
) -> AccountEvent:
|
||||
self.observe_position(position)
|
||||
return AccountEvent(
|
||||
timestamp=timestamp,
|
||||
runtime_namespace=self.runtime_namespace,
|
||||
strategy_namespace=self.strategy_namespace,
|
||||
event_namespace=self.event_namespace,
|
||||
actor_name=self.actor_name,
|
||||
exec_venue=self.exec_venue,
|
||||
data_venue=self.data_venue,
|
||||
ledger_authority=self.ledger_authority,
|
||||
capital=self.snapshot.capital,
|
||||
equity=self.snapshot.equity,
|
||||
open_positions=self.snapshot.open_positions,
|
||||
current_open_notional=self.snapshot.open_notional,
|
||||
current_account_leverage=self.snapshot.leverage,
|
||||
decision_id=decision.decision_id,
|
||||
trade_id=intent.trade_id,
|
||||
asset=decision.asset,
|
||||
side=intent.side,
|
||||
reason=intent.reason,
|
||||
stage=stage,
|
||||
pnl=self.snapshot.realized_pnl,
|
||||
pnl_pct=0.0 if self.snapshot.capital <= 0 else (self.snapshot.realized_pnl / self.snapshot.capital),
|
||||
bars_held=intent.bars_held,
|
||||
metadata=extra or {},
|
||||
)
|
||||
132
prod/clean_arch/dita/intent.py
Normal file
132
prod/clean_arch/dita/intent.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""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}"
|
||||
32
prod/clean_arch/dita/observability.py
Normal file
32
prod/clean_arch/dita/observability.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""DITA observability namespace helpers.
|
||||
|
||||
These helpers keep DITA diagnostics isolated by runtime namespace while still
|
||||
allowing optional legacy key mirroring when explicitly requested.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
LEGACY_ANOMALY_SENSOR_KEY = "dita_anomaly_sensors"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DitaObservabilityNamespace:
|
||||
"""Namespace contract for DITA observability payloads."""
|
||||
|
||||
runtime_namespace: str = "pink"
|
||||
feature_map: str = "DOLPHIN_FEATURES"
|
||||
meta_health_map: str = "DOLPHIN_META_HEALTH"
|
||||
state_map: str = "DOLPHIN_STATE_PINK"
|
||||
anomaly_sensor_key: str | None = None
|
||||
mirror_legacy_key: bool = False
|
||||
|
||||
def resolved_sensor_key(self) -> str:
|
||||
value = str(self.anomaly_sensor_key or "").strip()
|
||||
if value:
|
||||
return value
|
||||
ns = str(self.runtime_namespace or "pink").strip().lower()
|
||||
return f"{LEGACY_ANOMALY_SENSOR_KEY}_{ns}"
|
||||
|
||||
139
prod/clean_arch/dita/trade.py
Normal file
139
prod/clean_arch/dita/trade.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Trade execution and single-slot FSM."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Sequence
|
||||
|
||||
from .contracts import DecisionAction, Intent, TradeEvent, TradePosition, TradeSide, TradeStage
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TradeExecutionResult:
|
||||
"""Result of applying an intent to a trade slot."""
|
||||
|
||||
intent: Intent
|
||||
receipt: Optional[Any]
|
||||
stages: Sequence[TradeStage]
|
||||
position_before: Optional[TradePosition]
|
||||
position_after: Optional[TradePosition]
|
||||
partial_close: bool = False
|
||||
|
||||
|
||||
class TradeExecutor:
|
||||
"""Single-slot trade FSM.
|
||||
|
||||
Owns the live position and translates executable intents into exchange
|
||||
requests and canonical lifecycle stages.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.position: Optional[TradePosition] = None
|
||||
self.trade_history: List[TradeEvent] = []
|
||||
|
||||
def execute(self, intent: Intent, exchange: Any, capital_before: float) -> TradeExecutionResult:
|
||||
position_before = self._clone_position(self.position)
|
||||
if intent.action == DecisionAction.ENTER:
|
||||
return self._execute_enter(intent, exchange, capital_before, position_before)
|
||||
if intent.action == DecisionAction.EXIT:
|
||||
return self._execute_exit(intent, exchange, capital_before, position_before)
|
||||
return TradeExecutionResult(
|
||||
intent=intent,
|
||||
receipt=None,
|
||||
stages=(TradeStage.INTENT_CREATED,),
|
||||
position_before=position_before,
|
||||
position_after=self._clone_position(self.position),
|
||||
)
|
||||
|
||||
def apply_fill(self, receipt: Any, intent: Intent) -> None:
|
||||
if receipt is None:
|
||||
return
|
||||
if intent.action == DecisionAction.ENTER and receipt.status == "FILLED":
|
||||
self.position = TradePosition(
|
||||
trade_id=intent.trade_id,
|
||||
asset=intent.asset,
|
||||
side=intent.side,
|
||||
entry_price=receipt.fill_price,
|
||||
entry_time=intent.timestamp,
|
||||
size=receipt.fill_size,
|
||||
leverage=intent.leverage,
|
||||
entry_velocity_divergence=float(
|
||||
intent.metadata.get("entry_velocity_divergence", intent.metadata.get("velocity_divergence", 0.0))
|
||||
),
|
||||
entry_irp_alignment=float(intent.metadata.get("entry_irp_alignment", intent.confidence)),
|
||||
current_price=receipt.fill_price,
|
||||
initial_size=receipt.fill_size,
|
||||
exit_leg_ratios=tuple(intent.exit_leg_ratios),
|
||||
)
|
||||
return
|
||||
if intent.action == DecisionAction.EXIT and self.position is not None and receipt.status == "FILLED":
|
||||
self.position.size = max(0.0, float(receipt.remaining_size))
|
||||
self.position.exit_price = receipt.fill_price
|
||||
self.position.realized_pnl += receipt.realized_pnl
|
||||
self.position.mark_price(receipt.fill_price)
|
||||
self.position.closed = self.position.size <= 1e-12
|
||||
self.position.close_reason = intent.reason if self.position.closed else "PARTIAL_" + intent.reason
|
||||
if self.position.closed:
|
||||
self.position = None
|
||||
|
||||
def _execute_enter(self, intent: Intent, exchange: Any, capital_before: float, position_before: Optional[TradePosition]) -> TradeExecutionResult:
|
||||
if self.position is not None and not self.position.closed:
|
||||
return TradeExecutionResult(intent=intent, receipt=exchange.reject(intent, "POSITION_ALREADY_OPEN"), stages=(TradeStage.ORDER_REQUESTED,), position_before=position_before, position_after=self._clone_position(self.position))
|
||||
receipt = exchange.submit(intent)
|
||||
stages = (TradeStage.ORDER_REQUESTED, TradeStage.ORDER_SENT)
|
||||
if receipt and receipt.status == "FILLED":
|
||||
self.apply_fill(receipt, intent)
|
||||
stages = stages + (TradeStage.ORDER_ACKED, TradeStage.POSITION_OPENED)
|
||||
return TradeExecutionResult(intent=intent, receipt=receipt, stages=stages, position_before=position_before, position_after=self._clone_position(self.position))
|
||||
|
||||
def _execute_exit(self, intent: Intent, exchange: Any, capital_before: float, position_before: Optional[TradePosition]) -> TradeExecutionResult:
|
||||
if self.position is None or self.position.closed:
|
||||
return TradeExecutionResult(intent=intent, receipt=exchange.reject(intent, "NO_OPEN_POSITION"), stages=(TradeStage.EXIT_REQUESTED,), position_before=position_before, position_after=None)
|
||||
receipt = exchange.submit(intent)
|
||||
stages = [TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT]
|
||||
if receipt and receipt.status == "FILLED":
|
||||
self.apply_fill(receipt, intent)
|
||||
stages.append(TradeStage.EXIT_ACKED)
|
||||
if self.position is None:
|
||||
stages.extend([TradeStage.POSITION_CLOSED, TradeStage.TRADE_TERMINAL_WRITTEN])
|
||||
else:
|
||||
stages.extend([TradeStage.POSITION_PARTIALLY_CLOSED, TradeStage.POSITION_UPDATED])
|
||||
return TradeExecutionResult(
|
||||
intent=intent,
|
||||
receipt=receipt,
|
||||
stages=tuple(stages),
|
||||
position_before=position_before,
|
||||
position_after=self._clone_position(self.position),
|
||||
partial_close=self.position is not None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _clone_position(position: Optional[TradePosition]) -> Optional[TradePosition]:
|
||||
if position is None:
|
||||
return None
|
||||
return TradePosition(
|
||||
trade_id=position.trade_id,
|
||||
asset=position.asset,
|
||||
side=position.side,
|
||||
entry_price=position.entry_price,
|
||||
entry_time=position.entry_time,
|
||||
size=position.size,
|
||||
leverage=position.leverage,
|
||||
entry_velocity_divergence=position.entry_velocity_divergence,
|
||||
entry_irp_alignment=position.entry_irp_alignment,
|
||||
bars_held=position.bars_held,
|
||||
current_price=position.current_price,
|
||||
realized_pnl=position.realized_pnl,
|
||||
unrealized_pnl=position.unrealized_pnl,
|
||||
exit_price=position.exit_price,
|
||||
closed=position.closed,
|
||||
close_reason=position.close_reason,
|
||||
initial_size=position.initial_size,
|
||||
exit_leg_ratios=position.exit_leg_ratios,
|
||||
exit_leg_index=position.exit_leg_index,
|
||||
)
|
||||
|
||||
|
||||
def decision_confidence_from_intent(intent: Intent) -> float:
|
||||
return max(0.0, min(1.0, float(intent.confidence)))
|
||||
Reference in New Issue
Block a user