Files
siloqy/prod/clean_arch/dita_v2/account.py

124 lines
4.7 KiB
Python
Raw Normal View History

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