Added basic paper-trader. Fix signals_health_client.py to support the multiple personalities.
This commit is contained in:
701
trader/paper_trader.py
Executable file
701
trader/paper_trader.py
Executable file
@@ -0,0 +1,701 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Paper Trading Bot
|
||||
Executes paper trades based on signals from the signal generator
|
||||
Real-time TUI interface with trade tracking
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List
|
||||
from dataclasses import dataclass, asdict
|
||||
from enum import Enum
|
||||
import threading
|
||||
from collections import deque
|
||||
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
from rich.table import Table
|
||||
from rich.layout import Layout
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich import box
|
||||
|
||||
|
||||
class TradeStatus(Enum):
|
||||
OPEN = "OPEN"
|
||||
CLOSED = "CLOSED"
|
||||
STOPPED = "STOPPED"
|
||||
|
||||
|
||||
class TradeType(Enum):
|
||||
LONG = "LONG"
|
||||
SHORT = "SHORT"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Trade:
|
||||
id: Optional[int]
|
||||
trade_type: str
|
||||
entry_price: float
|
||||
entry_time: str
|
||||
position_size: float
|
||||
stop_loss: float
|
||||
take_profit: Optional[float]
|
||||
timeframe: str
|
||||
personality: str
|
||||
signal_confidence: float
|
||||
status: str
|
||||
exit_price: Optional[float] = None
|
||||
exit_time: Optional[str] = None
|
||||
pnl: Optional[float] = None
|
||||
pnl_percent: Optional[float] = None
|
||||
exit_reason: Optional[str] = None
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, config_path: str = "config.json"):
|
||||
self.config_path = config_path
|
||||
self.reload()
|
||||
|
||||
def reload(self):
|
||||
with open(self.config_path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Database paths
|
||||
self.trades_db = data.get("trades_db", "trades.db")
|
||||
self.candles_db = data.get("candles_db", "../onramp/market_data.db")
|
||||
|
||||
# Signal socket
|
||||
self.signal_socket = data.get("signal_socket", "/tmp/signals.sock")
|
||||
|
||||
# Trading parameters
|
||||
self.initial_balance = data.get("initial_balance", 10000.0)
|
||||
self.position_size_percent = data.get("position_size_percent", 2.0) # % of balance per trade
|
||||
self.max_positions = data.get("max_positions", 3)
|
||||
self.stop_loss_percent = data.get("stop_loss_percent", 2.0)
|
||||
self.take_profit_percent = data.get("take_profit_percent", 4.0)
|
||||
|
||||
# Strategy filters
|
||||
self.min_confidence = data.get("min_confidence", 0.5)
|
||||
self.enabled_personalities = data.get("enabled_personalities", ["scalping", "swing"])
|
||||
self.enabled_timeframes = data.get("enabled_timeframes", ["1m", "5m"])
|
||||
|
||||
# Risk management
|
||||
self.max_daily_loss_percent = data.get("max_daily_loss_percent", 5.0)
|
||||
self.trailing_stop = data.get("trailing_stop", False)
|
||||
self.trailing_stop_percent = data.get("trailing_stop_percent", 1.5)
|
||||
|
||||
|
||||
class TradeDatabase:
|
||||
def __init__(self, db_path: str):
|
||||
self.db_path = db_path
|
||||
self.init_database()
|
||||
|
||||
def init_database(self):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS trades (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trade_type TEXT NOT NULL,
|
||||
entry_price REAL NOT NULL,
|
||||
entry_time TEXT NOT NULL,
|
||||
position_size REAL NOT NULL,
|
||||
stop_loss REAL NOT NULL,
|
||||
take_profit REAL,
|
||||
timeframe TEXT NOT NULL,
|
||||
personality TEXT NOT NULL,
|
||||
signal_confidence REAL NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
exit_price REAL,
|
||||
exit_time TEXT,
|
||||
pnl REAL,
|
||||
pnl_percent REAL,
|
||||
exit_reason TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS balance_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp TEXT NOT NULL,
|
||||
balance REAL NOT NULL,
|
||||
equity REAL NOT NULL,
|
||||
open_positions INTEGER NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def save_trade(self, trade: Trade) -> int:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
if trade.id is None:
|
||||
cursor.execute("""
|
||||
INSERT INTO trades (
|
||||
trade_type, entry_price, entry_time, position_size,
|
||||
stop_loss, take_profit, timeframe, personality,
|
||||
signal_confidence, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
trade.trade_type, trade.entry_price, trade.entry_time,
|
||||
trade.position_size, trade.stop_loss, trade.take_profit,
|
||||
trade.timeframe, trade.personality, trade.signal_confidence,
|
||||
trade.status
|
||||
))
|
||||
trade_id = cursor.lastrowid
|
||||
else:
|
||||
cursor.execute("""
|
||||
UPDATE trades SET
|
||||
exit_price = ?, exit_time = ?, pnl = ?,
|
||||
pnl_percent = ?, status = ?, exit_reason = ?
|
||||
WHERE id = ?
|
||||
""", (
|
||||
trade.exit_price, trade.exit_time, trade.pnl,
|
||||
trade.pnl_percent, trade.status, trade.exit_reason,
|
||||
trade.id
|
||||
))
|
||||
trade_id = trade.id
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return trade_id
|
||||
|
||||
def get_open_trades(self) -> List[Trade]:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT * FROM trades WHERE status = 'OPEN' ORDER BY entry_time DESC")
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
trades = []
|
||||
for row in rows:
|
||||
trades.append(Trade(**dict(row)))
|
||||
return trades
|
||||
|
||||
def get_recent_trades(self, limit: int = 10) -> List[Trade]:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT * FROM trades
|
||||
WHERE status != 'OPEN'
|
||||
ORDER BY exit_time DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
trades = []
|
||||
for row in rows:
|
||||
trades.append(Trade(**dict(row)))
|
||||
return trades
|
||||
|
||||
def save_balance_snapshot(self, balance: float, equity: float, open_positions: int):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO balance_history (timestamp, balance, equity, open_positions)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (datetime.now(timezone.utc).isoformat(), balance, equity, open_positions))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_statistics(self) -> Dict:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Total trades
|
||||
cursor.execute("SELECT COUNT(*) FROM trades WHERE status != 'OPEN'")
|
||||
total_trades = cursor.fetchone()[0]
|
||||
|
||||
# Win/Loss
|
||||
cursor.execute("SELECT COUNT(*) FROM trades WHERE status != 'OPEN' AND pnl > 0")
|
||||
wins = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM trades WHERE status != 'OPEN' AND pnl < 0")
|
||||
losses = cursor.fetchone()[0]
|
||||
|
||||
# Total PnL
|
||||
cursor.execute("SELECT SUM(pnl) FROM trades WHERE status != 'OPEN'")
|
||||
total_pnl = cursor.fetchone()[0] or 0.0
|
||||
|
||||
# Average win/loss
|
||||
cursor.execute("SELECT AVG(pnl) FROM trades WHERE status != 'OPEN' AND pnl > 0")
|
||||
avg_win = cursor.fetchone()[0] or 0.0
|
||||
|
||||
cursor.execute("SELECT AVG(pnl) FROM trades WHERE status != 'OPEN' AND pnl < 0")
|
||||
avg_loss = cursor.fetchone()[0] or 0.0
|
||||
|
||||
conn.close()
|
||||
|
||||
win_rate = (wins / total_trades * 100) if total_trades > 0 else 0.0
|
||||
|
||||
return {
|
||||
"total_trades": total_trades,
|
||||
"wins": wins,
|
||||
"losses": losses,
|
||||
"win_rate": win_rate,
|
||||
"total_pnl": total_pnl,
|
||||
"avg_win": avg_win,
|
||||
"avg_loss": avg_loss,
|
||||
}
|
||||
|
||||
|
||||
class PriceMonitor:
|
||||
"""Monitor current price from candles database"""
|
||||
|
||||
def __init__(self, candles_db: str):
|
||||
self.candles_db = candles_db
|
||||
self.current_price = None
|
||||
self.last_update = None
|
||||
|
||||
def get_current_price(self) -> Optional[float]:
|
||||
"""Get most recent close price from 1m timeframe"""
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{self.candles_db}?mode=ro", uri=True, timeout=5)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT close, timestamp
|
||||
FROM candles
|
||||
WHERE timeframe = '1m'
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if row:
|
||||
self.current_price = float(row[0])
|
||||
self.last_update = row[1]
|
||||
return self.current_price
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
class PaperTrader:
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.db = TradeDatabase(config.trades_db)
|
||||
self.price_monitor = PriceMonitor(config.candles_db)
|
||||
|
||||
self.balance = config.initial_balance
|
||||
self.open_trades: List[Trade] = []
|
||||
self.recent_signals = deque(maxlen=20)
|
||||
|
||||
self.running = False
|
||||
self.lock = threading.Lock()
|
||||
|
||||
# Stats
|
||||
self.stats = {
|
||||
"signals_received": 0,
|
||||
"signals_filtered": 0,
|
||||
"trades_opened": 0,
|
||||
"trades_closed": 0,
|
||||
}
|
||||
|
||||
# Load open trades from DB
|
||||
self.open_trades = self.db.get_open_trades()
|
||||
|
||||
def connect_to_signals(self) -> socket.socket:
|
||||
"""Connect to signal generator socket"""
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(self.config.signal_socket)
|
||||
return sock
|
||||
|
||||
def calculate_position_size(self, price: float) -> float:
|
||||
"""Calculate position size based on balance and config"""
|
||||
position_value = self.balance * (self.config.position_size_percent / 100)
|
||||
return position_value / price
|
||||
|
||||
def should_take_signal(self, signal: Dict) -> bool:
|
||||
"""Filter signals based on config"""
|
||||
# Check confidence
|
||||
if signal.get("confidence", 0) < self.config.min_confidence:
|
||||
return False
|
||||
|
||||
# Check personality
|
||||
if signal.get("personality") not in self.config.enabled_personalities:
|
||||
return False
|
||||
|
||||
# Check timeframe
|
||||
if signal.get("timeframe") not in self.config.enabled_timeframes:
|
||||
return False
|
||||
|
||||
# Check max positions
|
||||
if len(self.open_trades) >= self.config.max_positions:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def open_trade(self, signal: Dict):
|
||||
"""Open a new paper trade"""
|
||||
with self.lock:
|
||||
trade_type = TradeType.LONG.value if signal["signal"] == "BUY" else TradeType.SHORT.value
|
||||
entry_price = signal["price"]
|
||||
position_size = self.calculate_position_size(entry_price)
|
||||
|
||||
# Calculate stop loss and take profit
|
||||
if trade_type == TradeType.LONG.value:
|
||||
stop_loss = entry_price * (1 - self.config.stop_loss_percent / 100)
|
||||
take_profit = entry_price * (1 + self.config.take_profit_percent / 100)
|
||||
else:
|
||||
stop_loss = entry_price * (1 + self.config.stop_loss_percent / 100)
|
||||
take_profit = entry_price * (1 - self.config.take_profit_percent / 100)
|
||||
|
||||
trade = Trade(
|
||||
id=None,
|
||||
trade_type=trade_type,
|
||||
entry_price=entry_price,
|
||||
entry_time=signal.get("generated_at", datetime.now(timezone.utc).isoformat()),
|
||||
position_size=position_size,
|
||||
stop_loss=stop_loss,
|
||||
take_profit=take_profit,
|
||||
timeframe=signal["timeframe"],
|
||||
personality=signal["personality"],
|
||||
signal_confidence=signal["confidence"],
|
||||
status=TradeStatus.OPEN.value,
|
||||
)
|
||||
|
||||
trade_id = self.db.save_trade(trade)
|
||||
trade.id = trade_id
|
||||
self.open_trades.append(trade)
|
||||
|
||||
self.stats["trades_opened"] += 1
|
||||
|
||||
def check_exits(self, current_price: float):
|
||||
"""Check if any open trades should be closed"""
|
||||
with self.lock:
|
||||
for trade in self.open_trades[:]:
|
||||
should_exit = False
|
||||
exit_reason = None
|
||||
|
||||
if trade.trade_type == TradeType.LONG.value:
|
||||
# Long position checks
|
||||
if current_price <= trade.stop_loss:
|
||||
should_exit = True
|
||||
exit_reason = "Stop Loss"
|
||||
elif trade.take_profit and current_price >= trade.take_profit:
|
||||
should_exit = True
|
||||
exit_reason = "Take Profit"
|
||||
else:
|
||||
# Short position checks
|
||||
if current_price >= trade.stop_loss:
|
||||
should_exit = True
|
||||
exit_reason = "Stop Loss"
|
||||
elif trade.take_profit and current_price <= trade.take_profit:
|
||||
should_exit = True
|
||||
exit_reason = "Take Profit"
|
||||
|
||||
if should_exit:
|
||||
self.close_trade(trade, current_price, exit_reason)
|
||||
|
||||
def close_trade(self, trade: Trade, exit_price: float, reason: str):
|
||||
"""Close a trade and calculate PnL"""
|
||||
trade.exit_price = exit_price
|
||||
trade.exit_time = datetime.now(timezone.utc).isoformat()
|
||||
trade.exit_reason = reason
|
||||
|
||||
# Calculate PnL
|
||||
if trade.trade_type == TradeType.LONG.value:
|
||||
pnl = (exit_price - trade.entry_price) * trade.position_size
|
||||
else:
|
||||
pnl = (trade.entry_price - exit_price) * trade.position_size
|
||||
|
||||
trade.pnl = pnl
|
||||
trade.pnl_percent = (pnl / (trade.entry_price * trade.position_size)) * 100
|
||||
trade.status = TradeStatus.CLOSED.value
|
||||
|
||||
self.balance += pnl
|
||||
self.db.save_trade(trade)
|
||||
self.open_trades.remove(trade)
|
||||
|
||||
self.stats["trades_closed"] += 1
|
||||
|
||||
def get_equity(self, current_price: float) -> float:
|
||||
"""Calculate current equity (balance + unrealized PnL)"""
|
||||
unrealized_pnl = 0.0
|
||||
|
||||
for trade in self.open_trades:
|
||||
if trade.trade_type == TradeType.LONG.value:
|
||||
pnl = (current_price - trade.entry_price) * trade.position_size
|
||||
else:
|
||||
pnl = (trade.entry_price - current_price) * trade.position_size
|
||||
unrealized_pnl += pnl
|
||||
|
||||
return self.balance + unrealized_pnl
|
||||
|
||||
def process_signal(self, signal: Dict):
|
||||
"""Process incoming signal"""
|
||||
self.stats["signals_received"] += 1
|
||||
self.recent_signals.append(signal)
|
||||
|
||||
if self.should_take_signal(signal):
|
||||
self.open_trade(signal)
|
||||
else:
|
||||
self.stats["signals_filtered"] += 1
|
||||
|
||||
def signal_listener_thread(self):
|
||||
"""Background thread to listen for signals"""
|
||||
while self.running:
|
||||
try:
|
||||
sock = self.connect_to_signals()
|
||||
sock.settimeout(1.0)
|
||||
|
||||
buffer = ""
|
||||
while self.running:
|
||||
try:
|
||||
data = sock.recv(4096).decode("utf-8")
|
||||
if not data:
|
||||
break
|
||||
|
||||
buffer += data
|
||||
while "\n" in buffer:
|
||||
line, buffer = buffer.split("\n", 1)
|
||||
if line.strip():
|
||||
signal = json.loads(line)
|
||||
self.process_signal(signal)
|
||||
|
||||
except socket.timeout:
|
||||
continue
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
sock.close()
|
||||
|
||||
except Exception as e:
|
||||
if self.running:
|
||||
time.sleep(5) # Wait before reconnecting
|
||||
|
||||
def price_monitor_thread(self):
|
||||
"""Background thread to monitor prices and check exits"""
|
||||
while self.running:
|
||||
current_price = self.price_monitor.get_current_price()
|
||||
|
||||
if current_price and self.open_trades:
|
||||
self.check_exits(current_price)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
def build_tui(self) -> Layout:
|
||||
"""Build the TUI layout"""
|
||||
layout = Layout()
|
||||
layout.split_column(
|
||||
Layout(name="header", size=3),
|
||||
Layout(name="body"),
|
||||
Layout(name="footer", size=3),
|
||||
)
|
||||
|
||||
layout["body"].split_row(
|
||||
Layout(name="left"),
|
||||
Layout(name="right"),
|
||||
)
|
||||
|
||||
layout["left"].split_column(
|
||||
Layout(name="stats", size=10),
|
||||
Layout(name="open_trades"),
|
||||
)
|
||||
|
||||
layout["right"].split_column(
|
||||
Layout(name="recent_closed", size=15),
|
||||
Layout(name="signals"),
|
||||
)
|
||||
|
||||
return layout
|
||||
|
||||
def render_header(self) -> Panel:
|
||||
"""Render header panel"""
|
||||
current_price = self.price_monitor.current_price or 0.0
|
||||
equity = self.get_equity(current_price)
|
||||
pnl = equity - self.config.initial_balance
|
||||
pnl_percent = (pnl / self.config.initial_balance) * 100
|
||||
|
||||
pnl_color = "green" if pnl >= 0 else "red"
|
||||
|
||||
text = Text()
|
||||
text.append("PAPER TRADING BOT", style="bold cyan")
|
||||
text.append(f" | BTC: ${current_price:,.2f}", style="yellow")
|
||||
text.append(f" | Balance: ${self.balance:,.2f}", style="white")
|
||||
text.append(f" | Equity: ${equity:,.2f}", style="white")
|
||||
text.append(f" | PnL: ${pnl:+,.2f} ({pnl_percent:+.2f}%)", style=pnl_color)
|
||||
|
||||
return Panel(text, style="bold")
|
||||
|
||||
def render_stats(self) -> Panel:
|
||||
"""Render statistics panel"""
|
||||
db_stats = self.db.get_statistics()
|
||||
|
||||
table = Table(show_header=False, box=box.SIMPLE)
|
||||
table.add_column("Metric", style="cyan")
|
||||
table.add_column("Value", justify="right")
|
||||
|
||||
table.add_row("Total Trades", str(db_stats["total_trades"]))
|
||||
table.add_row("Wins", f"[green]{db_stats['wins']}[/green]")
|
||||
table.add_row("Losses", f"[red]{db_stats['losses']}[/red]")
|
||||
table.add_row("Win Rate", f"{db_stats['win_rate']:.1f}%")
|
||||
table.add_row("Total PnL", f"${db_stats['total_pnl']:,.2f}")
|
||||
table.add_row("", "")
|
||||
table.add_row("Signals RX", str(self.stats["signals_received"]))
|
||||
table.add_row("Filtered", str(self.stats["signals_filtered"]))
|
||||
|
||||
return Panel(table, title="Statistics", border_style="blue")
|
||||
|
||||
def render_open_trades(self) -> Panel:
|
||||
"""Render open trades table"""
|
||||
table = Table(box=box.SIMPLE)
|
||||
table.add_column("ID", style="cyan")
|
||||
table.add_column("Type")
|
||||
table.add_column("Entry", justify="right")
|
||||
table.add_column("Current", justify="right")
|
||||
table.add_column("PnL", justify="right")
|
||||
table.add_column("TF")
|
||||
table.add_column("Pers")
|
||||
|
||||
current_price = self.price_monitor.current_price or 0.0
|
||||
|
||||
for trade in self.open_trades:
|
||||
if trade.trade_type == TradeType.LONG.value:
|
||||
pnl = (current_price - trade.entry_price) * trade.position_size
|
||||
type_color = "green"
|
||||
else:
|
||||
pnl = (trade.entry_price - current_price) * trade.position_size
|
||||
type_color = "red"
|
||||
|
||||
pnl_color = "green" if pnl >= 0 else "red"
|
||||
|
||||
table.add_row(
|
||||
str(trade.id),
|
||||
f"[{type_color}]{trade.trade_type}[/{type_color}]",
|
||||
f"${trade.entry_price:,.2f}",
|
||||
f"${current_price:,.2f}",
|
||||
f"[{pnl_color}]${pnl:+,.2f}[/{pnl_color}]",
|
||||
trade.timeframe,
|
||||
trade.personality[:4].upper(),
|
||||
)
|
||||
|
||||
return Panel(table, title=f"Open Positions ({len(self.open_trades)})", border_style="green")
|
||||
|
||||
def render_recent_closed(self) -> Panel:
|
||||
"""Render recent closed trades"""
|
||||
table = Table(box=box.SIMPLE)
|
||||
table.add_column("ID", style="cyan")
|
||||
table.add_column("Type")
|
||||
table.add_column("PnL", justify="right")
|
||||
table.add_column("Exit")
|
||||
|
||||
recent = self.db.get_recent_trades(10)
|
||||
|
||||
for trade in recent:
|
||||
type_color = "green" if trade.trade_type == TradeType.LONG.value else "red"
|
||||
pnl_color = "green" if trade.pnl and trade.pnl >= 0 else "red"
|
||||
|
||||
table.add_row(
|
||||
str(trade.id),
|
||||
f"[{type_color}]{trade.trade_type}[/{type_color}]",
|
||||
f"[{pnl_color}]${trade.pnl:+,.2f}[/{pnl_color}]" if trade.pnl else "N/A",
|
||||
trade.exit_reason or "N/A",
|
||||
)
|
||||
|
||||
return Panel(table, title="Recent Closed Trades", border_style="yellow")
|
||||
|
||||
def render_signals(self) -> Panel:
|
||||
"""Render recent signals"""
|
||||
table = Table(box=box.SIMPLE)
|
||||
table.add_column("Signal")
|
||||
table.add_column("TF")
|
||||
table.add_column("Conf", justify="right")
|
||||
table.add_column("Pers")
|
||||
|
||||
for signal in list(self.recent_signals)[-10:]:
|
||||
signal_color = "green" if signal["signal"] == "BUY" else "red"
|
||||
|
||||
table.add_row(
|
||||
f"[{signal_color}]{signal['signal']}[/{signal_color}]",
|
||||
signal["timeframe"],
|
||||
f"{signal['confidence']:.2f}",
|
||||
signal["personality"][:4].upper(),
|
||||
)
|
||||
|
||||
return Panel(table, title="Recent Signals", border_style="magenta")
|
||||
|
||||
def render_footer(self) -> Panel:
|
||||
"""Render footer panel"""
|
||||
text = Text()
|
||||
text.append("Press ", style="dim")
|
||||
text.append("Ctrl+C", style="bold red")
|
||||
text.append(" to exit", style="dim")
|
||||
return Panel(text, style="dim")
|
||||
|
||||
def update_display(self, layout: Layout):
|
||||
"""Update all TUI panels"""
|
||||
layout["header"].update(self.render_header())
|
||||
layout["stats"].update(self.render_stats())
|
||||
layout["open_trades"].update(self.render_open_trades())
|
||||
layout["recent_closed"].update(self.render_recent_closed())
|
||||
layout["signals"].update(self.render_signals())
|
||||
layout["footer"].update(self.render_footer())
|
||||
|
||||
def run(self):
|
||||
"""Main run loop with TUI"""
|
||||
self.running = True
|
||||
|
||||
# Start background threads
|
||||
signal_thread = threading.Thread(target=self.signal_listener_thread, daemon=True)
|
||||
price_thread = threading.Thread(target=self.price_monitor_thread, daemon=True)
|
||||
|
||||
signal_thread.start()
|
||||
price_thread.start()
|
||||
|
||||
# TUI
|
||||
console = Console()
|
||||
layout = self.build_tui()
|
||||
|
||||
try:
|
||||
with Live(layout, console=console, screen=True, refresh_per_second=2):
|
||||
while self.running:
|
||||
self.update_display(layout)
|
||||
time.sleep(0.5)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
self.running = False
|
||||
signal_thread.join(timeout=2)
|
||||
price_thread.join(timeout=2)
|
||||
|
||||
|
||||
def main():
|
||||
config = Config()
|
||||
trader = PaperTrader(config)
|
||||
|
||||
def shutdown(sig, frame):
|
||||
trader.running = False
|
||||
|
||||
signal.signal(signal.SIGINT, shutdown)
|
||||
signal.signal(signal.SIGTERM, shutdown)
|
||||
|
||||
trader.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user