124 lines
4.7 KiB
Python
124 lines
4.7 KiB
Python
|
|
"""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 {}),
|
||
|
|
}
|