Files
TradingBot-NewBot/run_paper.py
T

235 lines
8.9 KiB
Python

#!/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())