Files
siloqy/prod/clean_arch/dita/account.py
Codex 84e4a50e3f repo hygiene: track the PINK launcher import closure
67 production .py modules that the running PINK service imports but which
were never committed: prod/bingx/ (HTTP client, market/user streams,
journal, config), prod/clean_arch/ adapters/persistence/runtime/dita/dita_v2
production modules and their co-located tests. Rule going forward: every
module imported by launch_dolphin_pink.py / pink_direct.py must appear in
git ls-files. Excludes _backup dirs, __pycache__, and non-code files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 15:09:32 +02:00

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