Spaces:
Running
Running
Download src/analytics/tracker.py from Simzy/big-brain-ape-bot: direct link, hf CLI and curl.
- Browser
- Download file 14.7 kB
-
https://huggingface.co/spaces/Simzy/big-brain-ape-bot/resolve/main/src/analytics/tracker.py
- Command line
-
hf download hf://spaces/Simzy/big-brain-ape-bot/src/analytics/tracker.py
-
curl -L -o tracker.py https://huggingface.co/spaces/Simzy/big-brain-ape-bot/resolve/main/src/analytics/tracker.py
14.7 kB
| """Async SQLite analytics tracker for trade events and statistics. | |
| Records every trade to a local SQLite database and maintains rolling | |
| daily aggregate stats so the bot can report win-rate, P&L, and volume | |
| without expensive queries. | |
| Tables: | |
| trades — one row per completed buy/sell | |
| daily_stats — per-user, per-day aggregate (used for rate limits & charts) | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import time | |
| from dataclasses import dataclass | |
| from datetime import datetime, timezone | |
| from typing import Any, Optional | |
| import aiosqlite | |
| DB_PATH = os.getenv("DATABASE_PATH", os.path.join(os.getcwd(), "sniper.db")) | |
| # --------------------------------------------------------------------------- # | |
| # Data containers | |
| # --------------------------------------------------------------------------- # | |
| class TradeRecord: | |
| """Normalised row that mirrors the *trades* table.""" | |
| id: Optional[int] | |
| user_id: int | |
| token_address: str | |
| token_symbol: str | |
| chain: str | |
| side: str # "buy" | "sell" | |
| amount_sol: float | |
| price_usd: float | |
| quantity: float | |
| tx_signature: str | |
| status: str # "pending" | "confirmed" | "failed" | "closed" | |
| pnl_usd: float | |
| pnl_pct: float | |
| position_id: Optional[str] | |
| created_at: float | |
| # --------------------------------------------------------------------------- # | |
| # Schema | |
| # --------------------------------------------------------------------------- # | |
| SCHEMA_TRADES = """ | |
| CREATE TABLE IF NOT EXISTS trades ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| user_id INTEGER NOT NULL, | |
| token_address TEXT NOT NULL, | |
| token_symbol TEXT NOT NULL, | |
| chain TEXT NOT NULL DEFAULT 'solana', | |
| side TEXT NOT NULL, | |
| amount_sol REAL NOT NULL, | |
| price_usd REAL NOT NULL, | |
| quantity REAL NOT NULL, | |
| tx_signature TEXT NOT NULL DEFAULT '', | |
| status TEXT NOT NULL DEFAULT 'pending', | |
| pnl_usd REAL NOT NULL DEFAULT 0.0, | |
| pnl_pct REAL NOT NULL DEFAULT 0.0, | |
| position_id TEXT, | |
| created_at REAL NOT NULL, | |
| FOREIGN KEY(user_id) REFERENCES users(user_id) | |
| ); | |
| """ | |
| SCHEMA_DAILY_STATS = """ | |
| CREATE TABLE IF NOT EXISTS daily_stats ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| user_id INTEGER NOT NULL, | |
| stat_date TEXT NOT NULL, -- YYYY-MM-DD (UTC) | |
| total_trades INTEGER NOT NULL DEFAULT 0, | |
| wins INTEGER NOT NULL DEFAULT 0, | |
| losses INTEGER NOT NULL DEFAULT 0, | |
| total_pnl_usd REAL NOT NULL DEFAULT 0.0, | |
| total_volume_sol REAL NOT NULL DEFAULT 0.0, | |
| UNIQUE(user_id, stat_date) | |
| ); | |
| """ | |
| INDEXES = [ | |
| "CREATE INDEX IF NOT EXISTS idx_trades_user ON trades(user_id);", | |
| "CREATE INDEX IF NOT EXISTS idx_trades_token ON trades(token_address);", | |
| "CREATE INDEX IF NOT EXISTS idx_trades_status ON trades(status);", | |
| "CREATE INDEX IF NOT EXISTS idx_trades_created ON trades(created_at);", | |
| "CREATE INDEX IF NOT EXISTS idx_daily_user_date ON daily_stats(user_id, stat_date);", | |
| ] | |
| # --------------------------------------------------------------------------- # | |
| # Tracker | |
| # --------------------------------------------------------------------------- # | |
| class AnalyticsTracker: | |
| """Async wrapper around the SQLite analytics database. | |
| Usage:: | |
| tracker = AnalyticsTracker() | |
| await tracker.init() | |
| await tracker.record_trade(...) | |
| await tracker.close() | |
| """ | |
| def __init__(self, db_path: str = DB_PATH) -> None: | |
| self.db_path = db_path | |
| self._db: Optional[aiosqlite.Connection] = None | |
| # --------------------------- lifecycle -------------------------------- # | |
| async def init(self) -> None: | |
| """Open the connection and ensure the schema exists.""" | |
| self._db = await aiosqlite.connect(self.db_path) | |
| self._db.row_factory = aiosqlite.Row | |
| await self._db.execute("PRAGMA journal_mode=WAL;") | |
| await self._db.execute("PRAGMA synchronous=NORMAL;") | |
| await self._db.execute(SCHEMA_TRADES) | |
| await self._db.execute(SCHEMA_DAILY_STATS) | |
| for idx in INDEXES: | |
| await self._db.execute(idx) | |
| await self._db.commit() | |
| from loguru import logger # local import to keep module importable | |
| logger.info(f"Analytics DB initialised at {self.db_path}") | |
| async def close(self) -> None: | |
| if self._db: | |
| await self._db.close() | |
| self._db = None | |
| def db(self) -> aiosqlite.Connection: | |
| if self._db is None: | |
| raise RuntimeError("AnalyticsTracker not initialised — call init() first") | |
| return self._db | |
| # --------------------------- writes ----------------------------------- # | |
| async def record_trade( | |
| self, | |
| user_id: int, | |
| token_address: str, | |
| token_symbol: str, | |
| side: str, | |
| amount_sol: float, | |
| price_usd: float, | |
| quantity: float, | |
| tx_signature: str = "", | |
| status: str = "confirmed", | |
| pnl_usd: float = 0.0, | |
| pnl_pct: float = 0.0, | |
| position_id: Optional[str] = None, | |
| chain: str = "solana", | |
| ) -> int: | |
| """Insert a trade row and update daily aggregates. | |
| Returns the new row id. | |
| """ | |
| now = time.time() | |
| cursor = await self.db.execute( | |
| """ | |
| INSERT INTO trades | |
| (user_id, token_address, token_symbol, chain, side, | |
| amount_sol, price_usd, quantity, tx_signature, status, | |
| pnl_usd, pnl_pct, position_id, created_at) | |
| VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) | |
| """, | |
| ( | |
| user_id, token_address, token_symbol, chain, side, | |
| amount_sol, price_usd, quantity, tx_signature, status, | |
| pnl_usd, pnl_pct, position_id, now, | |
| ), | |
| ) | |
| await self.db.commit() | |
| trade_id = cursor.lastrowid or 0 | |
| # Update daily stats (only confirmed / closed trades count) | |
| if status in ("confirmed", "closed"): | |
| await self._bump_daily_stats( | |
| user_id=user_id, | |
| pnl_usd=pnl_usd, | |
| is_win=pnl_usd > 0, | |
| is_loss=pnl_usd < 0, | |
| volume_sol=amount_sol, | |
| ) | |
| return trade_id | |
| async def update_trade_status( | |
| self, | |
| trade_id: int, | |
| status: str, | |
| pnl_usd: float = 0.0, | |
| pnl_pct: float = 0.0, | |
| ) -> None: | |
| """Update an existing trade row (e.g. when a close fills).""" | |
| await self.db.execute( | |
| """ | |
| UPDATE trades | |
| SET status = ?, pnl_usd = ?, pnl_pct = ? | |
| WHERE id = ? | |
| """, | |
| (status, pnl_usd, pnl_pct, trade_id), | |
| ) | |
| await self.db.commit() | |
| async def _bump_daily_stats( | |
| self, | |
| user_id: int, | |
| pnl_usd: float, | |
| is_win: bool, | |
| is_loss: bool, | |
| volume_sol: float, | |
| ) -> None: | |
| stat_date = datetime.now(timezone.utc).strftime("%Y-%m-%d") | |
| await self.db.execute( | |
| """ | |
| INSERT INTO daily_stats | |
| (user_id, stat_date, total_trades, wins, losses, | |
| total_pnl_usd, total_volume_sol) | |
| VALUES (?,?,1,?,?,?,?) | |
| ON CONFLICT(user_id, stat_date) DO UPDATE SET | |
| total_trades = total_trades + 1, | |
| wins = wins + excluded.wins, | |
| losses = losses + excluded.losses, | |
| total_pnl_usd = total_pnl_usd + excluded.total_pnl_usd, | |
| total_volume_sol= total_volume_sol+ excluded.total_volume_sol | |
| """, | |
| ( | |
| user_id, stat_date, | |
| 1 if is_win else 0, | |
| 1 if is_loss else 0, | |
| pnl_usd, | |
| volume_sol, | |
| ), | |
| ) | |
| await self.db.commit() | |
| # --------------------------- reads ------------------------------------ # | |
| async def get_user_stats(self, user_id: int, limit: int = 50) -> dict[str, Any]: | |
| """Return aggregate stats + recent trades for a single user.""" | |
| # Aggregates | |
| row = await ( | |
| await self.db.execute( | |
| """ | |
| SELECT | |
| COUNT(*) AS total_trades, | |
| SUM(CASE WHEN pnl_usd > 0 THEN 1 ELSE 0 END) AS wins, | |
| SUM(CASE WHEN pnl_usd < 0 THEN 1 ELSE 0 END) AS losses, | |
| COALESCE(SUM(pnl_usd), 0) AS total_pnl, | |
| COALESCE(SUM(amount_sol),0)AS total_volume | |
| FROM trades | |
| WHERE user_id = ? AND status IN ('confirmed','closed') | |
| """, | |
| (user_id,), | |
| ) | |
| ).fetchone() | |
| total_trades = row["total_trades"] or 0 | |
| wins = row["wins"] or 0 | |
| losses = row["losses"] or 0 | |
| win_rate = (wins / total_trades * 100) if total_trades else 0.0 | |
| recent = await ( | |
| await self.db.execute( | |
| """ | |
| SELECT * FROM trades | |
| WHERE user_id = ? | |
| ORDER BY created_at DESC | |
| LIMIT ? | |
| """, | |
| (user_id, limit), | |
| ) | |
| ).fetchall() | |
| return { | |
| "total_trades": total_trades, | |
| "wins": wins, | |
| "losses": losses, | |
| "win_rate": round(win_rate, 2), | |
| "total_pnl_usd": round(row["total_pnl"] or 0.0, 2), | |
| "total_volume_sol": round(row["total_volume"] or 0.0, 4), | |
| "recent_trades": [dict(r) for r in recent], | |
| } | |
| async def get_bot_stats(self) -> dict[str, Any]: | |
| """Return bot-wide aggregate stats (all users).""" | |
| row = await ( | |
| await self.db.execute( | |
| """ | |
| SELECT | |
| COUNT(*) AS total_trades, | |
| COUNT(DISTINCT user_id) AS unique_users, | |
| SUM(CASE WHEN pnl_usd > 0 THEN 1 ELSE 0 END) AS wins, | |
| SUM(CASE WHEN pnl_usd < 0 THEN 1 ELSE 0 END) AS losses, | |
| COALESCE(SUM(pnl_usd), 0) AS total_pnl, | |
| COALESCE(SUM(amount_sol),0)AS total_volume | |
| FROM trades | |
| WHERE status IN ('confirmed','closed') | |
| """ | |
| ) | |
| ).fetchone() | |
| total_trades = row["total_trades"] or 0 | |
| wins = row["wins"] or 0 | |
| losses = row["losses"] or 0 | |
| win_rate = (wins / total_trades * 100) if total_trades else 0.0 | |
| today = datetime.now(timezone.utc).strftime("%Y-%m-%d") | |
| today_row = await ( | |
| await self.db.execute( | |
| """ | |
| SELECT COALESCE(SUM(total_trades),0) AS trades_today, | |
| COALESCE(SUM(total_pnl_usd),0) AS pnl_today | |
| FROM daily_stats WHERE stat_date = ? | |
| """, | |
| (today,), | |
| ) | |
| ).fetchone() | |
| return { | |
| "total_trades": total_trades, | |
| "unique_users": row["unique_users"] or 0, | |
| "wins": wins, | |
| "losses": losses, | |
| "win_rate": round(win_rate, 2), | |
| "total_pnl_usd": round(row["total_pnl"] or 0.0, 2), | |
| "total_volume_sol": round(row["total_volume"] or 0.0, 4), | |
| "trades_today": today_row["trades_today"] or 0, | |
| "pnl_today": round(today_row["pnl_today"] or 0.0, 2), | |
| } | |
| async def calculate_win_rate(self, user_id: Optional[int] = None) -> float: | |
| """Return win rate as a percentage (0–100). | |
| If *user_id* is ``None`` the bot-wide win rate is returned. | |
| """ | |
| if user_id: | |
| row = await ( | |
| await self.db.execute( | |
| """ | |
| SELECT | |
| COUNT(*) AS total, | |
| SUM(CASE WHEN pnl_usd > 0 THEN 1 ELSE 0 END) AS wins | |
| FROM trades | |
| WHERE user_id = ? AND status IN ('confirmed','closed') | |
| AND pnl_usd != 0 | |
| """, | |
| (user_id,), | |
| ) | |
| ).fetchone() | |
| else: | |
| row = await ( | |
| await self.db.execute( | |
| """ | |
| SELECT | |
| COUNT(*) AS total, | |
| SUM(CASE WHEN pnl_usd > 0 THEN 1 ELSE 0 END) AS wins | |
| FROM trades | |
| WHERE status IN ('confirmed','closed') | |
| AND pnl_usd != 0 | |
| """ | |
| ) | |
| ).fetchone() | |
| total = row["total"] or 0 | |
| wins = row["wins"] or 0 | |
| return round((wins / total * 100) if total else 0.0, 2) | |
| async def get_total_pnl(self, user_id: Optional[int] = None) -> float: | |
| """Return cumulative realised P&L in USD.""" | |
| if user_id: | |
| row = await ( | |
| await self.db.execute( | |
| """ | |
| SELECT COALESCE(SUM(pnl_usd),0) AS pnl | |
| FROM trades | |
| WHERE user_id = ? AND status IN ('confirmed','closed') | |
| """, | |
| (user_id,), | |
| ) | |
| ).fetchone() | |
| else: | |
| row = await ( | |
| await self.db.execute( | |
| """ | |
| SELECT COALESCE(SUM(pnl_usd),0) AS pnl | |
| FROM trades | |
| WHERE status IN ('confirmed','closed') | |
| """ | |
| ) | |
| ).fetchone() | |
| return round(row["pnl"] or 0.0, 2) | |
| async def get_daily_trade_count(self, user_id: int) -> int: | |
| """Count of trades today for *user_id* (used by rate-limiter).""" | |
| today = datetime.now(timezone.utc).strftime("%Y-%m-%d") | |
| row = await ( | |
| await self.db.execute( | |
| "SELECT COALESCE(total_trades,0) AS n FROM daily_stats WHERE user_id=? AND stat_date=?", | |
| (user_id, today), | |
| ) | |
| ).fetchone() | |
| return row["n"] or 0 | |
| async def get_trade_history( | |
| self, user_id: int, limit: int = 20 | |
| ) -> list[dict[str, Any]]: | |
| """Return recent trade dicts (newest first).""" | |
| rows = await ( | |
| await self.db.execute( | |
| "SELECT * FROM trades WHERE user_id=? ORDER BY created_at DESC LIMIT ?", | |
| (user_id, limit), | |
| ) | |
| ).fetchall() | |
| return [dict(r) for r in rows] | |