from __future__ import annotations from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from typing import Any def utc_now() -> datetime: return datetime.now(timezone.utc) @dataclass(slots=True) class Candle: timestamp: int open: float high: float low: float close: float volume: float turnover: float = 0.0 ema_20: float | None = None ema_50: float | None = None ema_200: float | None = None rsi_14: float | None = None atr_14: float | None = None volume_ma_20: float | None = None def as_dict(self) -> dict[str, Any]: return asdict(self) @dataclass(slots=True) class Ticker: symbol: str last_price: float bid: float ask: float turnover_24h: float volume_24h: float change_24h: float updated_at: datetime = field(default_factory=utc_now) @property def spread_percent(self) -> float: if self.bid <= 0 or self.ask <= 0: return 0.0 mid = (self.ask + self.bid) / 2 return ((self.ask - self.bid) / mid) * 100 if mid else 0.0 def as_dict(self) -> dict[str, Any]: data = asdict(self) data["updated_at"] = self.updated_at.isoformat() data["spread_percent"] = self.spread_percent return data @dataclass(slots=True) class Signal: symbol: str action: str confidence: float reason: str diagnostics: dict[str, Any] = field(default_factory=dict) created_at: datetime = field(default_factory=utc_now) def as_dict(self) -> dict[str, Any]: data = asdict(self) data["created_at"] = self.created_at.isoformat() return data @dataclass(slots=True) class Position: id: int | None symbol: str qty: float entry_price: float notional_usdt: float entry_fee_usdt: float stop_loss: float take_profit: float highest_price: float opened_at: datetime = field(default_factory=utc_now) entry_reason: str = "" entry_confidence: float = 0.0 entry_pattern: str = "" def mark_price(self, price: float) -> float: return self.qty * price def unrealized_pnl(self, price: float) -> float: return (price - self.entry_price) * self.qty - self.entry_fee_usdt def trailing_stop(self, percent: float) -> float | None: stop = self.highest_price * (1 - percent) return stop if stop > self.entry_price else None def as_dict(self, mark_price: float | None = None) -> dict[str, Any]: data = asdict(self) data["opened_at"] = self.opened_at.isoformat() if mark_price is not None: data["mark_price"] = mark_price data["market_value"] = self.mark_price(mark_price) data["unrealized_pnl"] = self.unrealized_pnl(mark_price) data["unrealized_pnl_percent"] = ( self.unrealized_pnl(mark_price) / self.notional_usdt * 100 if self.notional_usdt else 0.0 ) return data @dataclass(slots=True) class Trade: id: int | None symbol: str side: str qty: float entry_price: float | None = None exit_price: float | None = None gross_pnl: float = 0.0 fee_usdt: float = 0.0 net_pnl: float = 0.0 reason: str = "" entry_pattern: str = "" entry_confidence: float = 0.0 opened_at: datetime | None = None closed_at: datetime | None = None def as_dict(self) -> dict[str, Any]: data = asdict(self) data["opened_at"] = self.opened_at.isoformat() if self.opened_at else None data["closed_at"] = self.closed_at.isoformat() if self.closed_at else None return data @dataclass(slots=True) class BotStatus: running: bool mode: str live_trading_ready: bool symbols: list[str] started_at: datetime | None last_loop_at: datetime | None message: str = "" def as_dict(self) -> dict[str, Any]: data = asdict(self) data["started_at"] = self.started_at.isoformat() if self.started_at else None data["last_loop_at"] = self.last_loop_at.isoformat() if self.last_loop_at else None return data