Initial TradeBot implementation
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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
|
||||
Reference in New Issue
Block a user