#!/usr/bin/env python3 """ Paper trading runner with live dashboard. - Immediately runs a full signal scan (no waiting for bar boundary) - Opens paper positions where signals are found - Refreshes the dashboard every 60 seconds showing live P&L - Press Ctrl+C to stop """ import asyncio import json import os import sys from datetime import datetime, timezone from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) # ── imports ─────────────────────────────────────────────────────────────────── from bot import config, broker, risk from bot.signals import check from bot.loop import _execute, _has_open from bot.paper_client import _load_state, STATE_PATH import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s", handlers=[logging.StreamHandler(sys.stdout)], ) for noisy in ("httpx", "telegram", "asyncio", "yfinance", "peewee"): logging.getLogger(noisy).setLevel(logging.WARNING) logger = logging.getLogger("paper") # ── helpers ─────────────────────────────────────────────────────────────────── def _pip_size(symbol: str) -> float: return 0.01 if symbol.endswith("JPY") or symbol.endswith("CHF") and "JPY" in symbol else 0.0001 async def _unrealised_pnl(pos: dict) -> float: """Current unrealised P&L in pips for an open paper position.""" try: from bot.paper_client import get_tick tick = await get_tick(pos["symbol"]) if tick is None: return 0.0 mid = (tick["bid"] + tick["ask"]) / 2 ps = _pip_size(pos["symbol"]) direction = pos["direction"] # 0=BUY, 1=SELL raw = mid - pos["entry"] if direction == 0 else pos["entry"] - mid return raw / ps except Exception: return 0.0 def _cls(): os.system("clear" if os.name != "nt" else "cls") def _bar(pct: float, width: int = 20) -> str: filled = int(abs(pct) / max(abs(pct), 1e-9) * width) filled = min(filled, width) return ("█" * filled).ljust(width) # ── dashboard ───────────────────────────────────────────────────────────────── async def _dashboard(): state = _load_state() positions = state.get("positions", []) history = state.get("history", []) balance = state.get("balance", config.PAPER_INITIAL_BALANCE) now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") initial = config.PAPER_INITIAL_BALANCE total_ret = (balance / initial - 1) * 100 _cls() print("╔══════════════════════════════════════════════════════════════╗") print(f"║ NewBot — Paper Trading Dashboard {now} ║") print("╚══════════════════════════════════════════════════════════════╝") print() print(f" Balance : ${balance:>10,.2f} ({total_ret:+.2f}% since start)") print(f" Strategy : RSI Trend Pullback | {', '.join(config.SYMBOLS)}") print() # open positions print("── Open Positions ─────────────────────────────────────────────") if not positions: print(" (none)") else: print(f" {'Symbol':<10} {'Dir':<6} {'Entry':>10} {'Current':>10} " f"{'SL':>10} {'TP':>10} {'Pips':>8}") for pos in positions: upnl = await _unrealised_pnl(pos) try: from bot.paper_client import get_tick tick = await get_tick(pos["symbol"]) current = (tick["bid"] + tick["ask"]) / 2 if tick else pos["entry"] except Exception: current = pos["entry"] d = "LONG" if pos["direction"] == 0 else "SHORT" pip_str = f"{upnl:+.1f}" sign = "🟢" if upnl >= 0 else "🔴" print(f" {pos['symbol']:<10} {d:<6} {pos['entry']:>10.5f} " f"{current:>10.5f} {pos['sl']:>10.5f} {pos['tp']:>10.5f} " f"{sign}{pip_str:>7}") print() # recent closed trades print("── Recent Closed Trades ───────────────────────────────────────") recent = sorted(history, key=lambda x: x.get("closed_at",""), reverse=True)[:10] if not recent: print(" (none yet)") else: print(f" {'Symbol':<10} {'Dir':<6} {'Result':<6} {'Pips':>8} When") for t in recent: d = "LONG" if t["direction"] == 0 else "SHORT" pips = t.get("pnl_pips", 0) sign = "✅" if pips > 0 else "❌" closed = t.get("closed_at","")[:16] print(f" {t['symbol']:<10} {d:<6} {t.get('reason','?'):<6} " f"{sign}{pips:>+7.1f} {closed}") print() # stats if history: pips_list = [t.get("pnl_pips", 0) for t in history] wins = [p for p in pips_list if p > 0] total = len(pips_list) wr = len(wins) / total * 100 total_pips = sum(pips_list) avg_w = sum(wins) / len(wins) if wins else 0 losses = [p for p in pips_list if p <= 0] avg_l = sum(losses) / len(losses) if losses else 0 pf = abs(sum(wins) / sum(losses)) if losses and sum(losses) != 0 else float("inf") print("── Stats ──────────────────────────────────────────────────────") print(f" Trades : {total} | Wins: {len(wins)} Losses: {len(losses)}") print(f" Win % : {wr:.1f}% | PF: {pf:.2f}") print(f" Avg W : {avg_w:+.1f} pips Avg L : {avg_l:+.1f} pips") print(f" Total : {total_pips:+.1f} pips") print() print(" [Ctrl+C to stop] Refreshing every 60s…") # ── main scan loop ──────────────────────────────────────────────────────────── async def scan_once(label: str = "manual") -> None: logger.info("── Scanning symbols for signals (%s) ──", label) account = await broker.get_account_info() if account is None: logger.error("Broker not connected") return state = _load_state() open_positions = await broker.get_open_positions() for symbol in config.SYMBOLS: if _has_open(symbol, open_positions): logger.info("%-10s already has open position — skip", symbol) continue logger.info("Fetching candles for %s…", symbol) df = await broker.get_candles(symbol, config.TIMEFRAME, config.LOOKBACK) if df is None or len(df) < 300: logger.warning("%-10s insufficient data (%s bars)", symbol, len(df) if df is not None else 0) continue signal = check(df, symbol) if signal is None: logger.info("%-10s no signal (RSI=%.1f)", symbol, _last_rsi(df)) continue logger.info("SIGNAL: %s", signal) if not risk.can_trade(account, open_positions, state): logger.info("Trade limit reached") break success = await _execute(signal, account) if success: open_positions = await broker.get_open_positions() state = _load_state() def _last_rsi(df) -> float: try: from engine.indicators import rsi return float(rsi(df["close"], 14).iloc[-2]) except Exception: return float("nan") async def main(): if not broker.connect(): logger.error("Broker connect failed") return # immediate scan on startup await scan_once("startup") await _dashboard() # then loop every 60s: refresh dashboard + rescan on each new bar last_scan_minute = -1 try: while True: await asyncio.sleep(60) now = datetime.now(timezone.utc) await _dashboard() # rescan at each new M15 bar (minute % 15 == 0..2 window) if now.minute % 15 < 2 and now.minute != last_scan_minute: last_scan_minute = now.minute await scan_once(f"bar@{now.strftime('%H:%M')}") await _dashboard() except KeyboardInterrupt: pass finally: broker.disconnect() print("\nBot stopped. Final state:") await _dashboard() if __name__ == "__main__": asyncio.run(main())