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