"""Account projection for DITAv2.""" from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime from typing import Any, Dict, Iterable, Optional import math from .contracts import TradeSide, TradeSlot, TradeStage from .utils import safe_float @dataclass class AccountSnapshot: """Derived account state.""" 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 peak_capital: float = 0.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: """Aggregate account view over all active slots.""" runtime_namespace: str = "dita_v2" strategy_namespace: str = "dita_v2" event_namespace: str = "dita_v2" actor_name: str = "ExecutionKernel" 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_slots(self, slots: Iterable[TradeSlot]) -> None: open_positions = 0 open_notional = 0.0 unrealized_pnl = 0.0 for slot in slots: if slot.closed or slot.size <= 0: continue if slot.fsm_state in {TradeStage.POSITION_OPEN, TradeStage.POSITION_OPENED, TradeStage.ENTRY_WORKING, TradeStage.EXIT_WORKING}: open_positions += 1 mark = safe_float(slot.entry_price, 0.0) mark = safe_float(slot.metadata.get("mark_price"), mark) open_notional += abs(slot.size) * abs(mark) unrealized_pnl += float(slot.unrealized_pnl or 0.0) self.snapshot.open_positions = open_positions self.snapshot.open_notional = open_notional self.snapshot.unrealized_pnl = unrealized_pnl self.snapshot.equity = self.snapshot.capital + unrealized_pnl if not math.isfinite(self.snapshot.equity): self.snapshot.equity = self.snapshot.capital if open_notional > 0 and self.snapshot.capital > 0: self.snapshot.peak_capital = max(self.snapshot.peak_capital, self.snapshot.capital) def settle(self, realized_pnl: float, fees: float = 0.0) -> None: realized_pnl = safe_float(realized_pnl, 0.0) new_capital = safe_float(self.snapshot.capital + realized_pnl, 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 += safe_float(fees, 0.0) 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_account_event( self, *, timestamp: datetime, trade_id: str, asset: str, side: TradeSide, stage: TradeStage, reason: str, pnl: float = 0.0, pnl_pct: float = 0.0, bars_held: int = 0, metadata: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: self.snapshot.equity = self.snapshot.capital + self.snapshot.unrealized_pnl return { "timestamp": timestamp.isoformat() if hasattr(timestamp, "isoformat") else str(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": float(self.snapshot.capital), "equity": float(self.snapshot.equity), "open_positions": int(self.snapshot.open_positions), "current_open_notional": float(self.snapshot.open_notional), "current_account_leverage": float(self.snapshot.leverage), "trade_id": trade_id, "asset": asset, "side": side.value, "reason": reason, "stage": stage.value, "pnl": float(pnl), "pnl_pct": float(pnl_pct), "bars_held": int(bars_held), "metadata": dict(metadata or {}), }