"""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 {}, )