TradingAgents/tradingagents/paper_trading/dashboard.py

397 lines
15 KiB
Python

"""
Paper Trading Performance Dashboard
Real-time monitoring and analytics
"""
import json
import os
from datetime import datetime
from typing import Dict, List, Optional
import pandas as pd
from dataclasses import asdict
class PaperTradingDashboard:
"""
Performance dashboard for paper trading.
Features:
- Real-time metrics display
- Performance analytics
- Trade history analysis
- Risk metrics
"""
def __init__(self, engine):
"""
Initialize dashboard.
Args:
engine: PaperTradingEngine instance
"""
self.engine = engine
def print_live_status(self):
"""Print real-time trading status."""
portfolio_value = self.engine.get_portfolio_value()
total_return = (portfolio_value - self.engine.initial_capital) / self.engine.initial_capital
print("\n" + "="*80)
print(f"{'PAPER TRADING DASHBOARD':^80}")
print("="*80)
# Portfolio Overview
print("\n📊 PORTFOLIO OVERVIEW")
print(f" Portfolio Value: ${portfolio_value:,.2f}")
print(f" Initial Capital: ${self.engine.initial_capital:,.2f}")
print(f" Cash: ${self.engine.cash:,.2f}")
print(f" Total Return: {total_return:+.2%}")
print(f" Daily P&L: {self.engine.daily_pnl:+.2%}")
# Positions
print("\n📈 OPEN POSITIONS")
if self.engine.positions:
for symbol, pos in self.engine.positions.items():
price = self.engine.current_prices.get(symbol, pos.current_price)
pos.current_price = price
pos.unrealized_pnl = (price - pos.entry_price) * pos.amount
pos.unrealized_pnl_pct = (price / pos.entry_price - 1)
emoji = "🟢" if pos.unrealized_pnl >= 0 else "🔴"
print(f" {emoji} {symbol:12s} | Amount: {pos.amount:.6f} | Entry: ${pos.entry_price:,.2f} | Current: ${price:,.2f} | P&L: {pos.unrealized_pnl_pct:+.2%} (${pos.unrealized_pnl:+,.2f})")
else:
print(" No open positions")
# Recent Orders
print("\n📋 RECENT ORDERS (Last 5)")
recent_orders = self.engine.orders[-5:] if len(self.engine.orders) >= 5 else self.engine.orders
if recent_orders:
for order in reversed(recent_orders):
emoji = "🟢" if order.side.value == "buy" else "🔴"
time_str = order.timestamp.strftime("%H:%M:%S")
print(f" {emoji} [{time_str}] {order.side.value.upper():4s} {order.amount:.6f} {order.symbol:12s} @ ${order.price:,.2f}")
if order.reason:
print(f" └─ {order.reason}")
else:
print(" No orders yet")
# Statistics
print("\n📊 STATISTICS")
print(f" Total Orders: {len(self.engine.orders)}")
print(f" Buy Orders: {sum(1 for o in self.engine.orders if o.side.value == 'buy')}")
print(f" Sell Orders: {sum(1 for o in self.engine.orders if o.side.value == 'sell')}")
print(f" Updates: {len(self.engine.portfolio_value_history)}")
# Risk Metrics
print("\n⚠️ RISK METRICS")
print(f" Max Position Size: {self.engine.max_position_size:.1%}")
print(f" Max Daily Loss: {self.engine.max_daily_loss:.1%}")
print(f" Stop Loss: {self.engine.stop_loss_pct:.1%}")
print(f" Take Profit: {self.engine.take_profit_pct:.1%}")
print("\n" + "="*80 + "\n")
def get_performance_metrics(self) -> Dict:
"""Calculate comprehensive performance metrics."""
if not self.engine.portfolio_value_history:
return {}
# Get portfolio values
values = [h['portfolio_value'] for h in self.engine.portfolio_value_history]
# Calculate returns
returns = [(values[i] - values[i-1]) / values[i-1] for i in range(1, len(values))]
# Current metrics
current_value = values[-1]
total_return = (current_value - self.engine.initial_capital) / self.engine.initial_capital
# Calculate max drawdown
peak = self.engine.initial_capital
max_dd = 0
for value in values:
if value > peak:
peak = value
dd = (value - peak) / peak
if dd < max_dd:
max_dd = dd
# Win rate
winning_trades = 0
losing_trades = 0
total_profit = 0
total_loss = 0
for order in self.engine.orders:
if order.side.value == "sell" and "P&L:" in order.reason:
# Extract P&L from reason
pnl_str = order.reason.split("P&L: $")[1].split(" ")[0].replace(",", "")
pnl = float(pnl_str)
if pnl > 0:
winning_trades += 1
total_profit += pnl
else:
losing_trades += 1
total_loss += abs(pnl)
total_trades = winning_trades + losing_trades
win_rate = winning_trades / total_trades if total_trades > 0 else 0
# Average win/loss
avg_win = total_profit / winning_trades if winning_trades > 0 else 0
avg_loss = total_loss / losing_trades if losing_trades > 0 else 0
profit_factor = total_profit / total_loss if total_loss > 0 else float('inf')
# Sharpe ratio (annualized, simplified)
if returns:
import numpy as np
avg_return = np.mean(returns)
std_return = np.std(returns)
sharpe = (avg_return / std_return * (252 ** 0.5)) if std_return > 0 else 0
else:
sharpe = 0
return {
'current_value': current_value,
'initial_capital': self.engine.initial_capital,
'total_return': total_return,
'total_return_pct': total_return * 100,
'max_drawdown': max_dd,
'max_drawdown_pct': max_dd * 100,
'sharpe_ratio': sharpe,
'total_trades': total_trades,
'winning_trades': winning_trades,
'losing_trades': losing_trades,
'win_rate': win_rate,
'win_rate_pct': win_rate * 100,
'total_profit': total_profit,
'total_loss': total_loss,
'avg_win': avg_win,
'avg_loss': avg_loss,
'profit_factor': profit_factor,
'num_updates': len(self.engine.portfolio_value_history),
}
def print_performance_report(self):
"""Print comprehensive performance report."""
metrics = self.get_performance_metrics()
if not metrics:
print("No performance data yet.")
return
print("\n" + "="*80)
print(f"{'PERFORMANCE REPORT':^80}")
print("="*80)
# Returns
print("\n💰 RETURNS")
print(f" Current Value: ${metrics['current_value']:,.2f}")
print(f" Initial Capital: ${metrics['initial_capital']:,.2f}")
print(f" Total Return: {metrics['total_return']:+.2%} (${metrics['current_value'] - metrics['initial_capital']:+,.2f})")
print(f" Max Drawdown: {metrics['max_drawdown']:.2%}")
print(f" Sharpe Ratio: {metrics['sharpe_ratio']:.2f}")
# Trading Stats
print("\n📊 TRADING STATISTICS")
print(f" Total Trades: {metrics['total_trades']}")
print(f" Winning Trades: {metrics['winning_trades']} ({metrics['win_rate_pct']:.1f}%)")
print(f" Losing Trades: {metrics['losing_trades']} ({100 - metrics['win_rate_pct']:.1f}%)")
print(f" Win Rate: {metrics['win_rate_pct']:.1f}%")
# P&L
print("\n💵 PROFIT & LOSS")
print(f" Total Profit: ${metrics['total_profit']:,.2f}")
print(f" Total Loss: ${metrics['total_loss']:,.2f}")
print(f" Net P&L: ${metrics['total_profit'] - metrics['total_loss']:+,.2f}")
print(f" Avg Win: ${metrics['avg_win']:,.2f}")
print(f" Avg Loss: ${metrics['avg_loss']:,.2f}")
print(f" Profit Factor: {metrics['profit_factor']:.2f}")
# Data
print("\n📈 DATA COVERAGE")
print(f" Total Updates: {metrics['num_updates']}")
print(f" Update Interval: {self.engine.update_interval}s")
duration_seconds = metrics['num_updates'] * self.engine.update_interval
hours = duration_seconds // 3600
minutes = (duration_seconds % 3600) // 60
print(f" Trading Duration: {hours}h {minutes}m")
print("\n" + "="*80 + "\n")
def export_to_csv(self, filename: Optional[str] = None):
"""Export trading data to CSV."""
if not filename:
filename = f"paper_trading_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
# Prepare data
data = []
for order in self.engine.orders:
data.append({
'timestamp': order.timestamp,
'order_id': order.order_id,
'symbol': order.symbol,
'side': order.side.value,
'amount': order.amount,
'price': order.price,
'status': order.status.value,
'reason': order.reason,
})
if data:
df = pd.DataFrame(data)
filepath = os.path.join(self.engine.data_dir, filename)
df.to_csv(filepath, index=False)
print(f"✓ Exported {len(data)} orders to: {filepath}")
else:
print("No orders to export")
def export_portfolio_history(self, filename: Optional[str] = None):
"""Export portfolio value history to CSV."""
if not filename:
filename = f"portfolio_history_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
if self.engine.portfolio_value_history:
df = pd.DataFrame(self.engine.portfolio_value_history)
filepath = os.path.join(self.engine.data_dir, filename)
df.to_csv(filepath, index=False)
print(f"✓ Exported portfolio history to: {filepath}")
else:
print("No portfolio history to export")
def generate_html_report(self, filename: Optional[str] = None):
"""Generate HTML dashboard report."""
if not filename:
filename = f"dashboard_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
metrics = self.get_performance_metrics()
if not metrics:
print("No performance data yet.")
return
html = f"""
<!DOCTYPE html>
<html>
<head>
<title>Paper Trading Dashboard</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }}
.container {{ max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 10px; }}
h1 {{ color: #333; text-align: center; }}
.metric-grid {{ display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0; }}
.metric-card {{ background: #f8f9fa; padding: 20px; border-radius: 8px; text-align: center; }}
.metric-value {{ font-size: 32px; font-weight: bold; color: #007bff; }}
.metric-label {{ font-size: 14px; color: #666; margin-top: 10px; }}
.positive {{ color: #28a745; }}
.negative {{ color: #dc3545; }}
table {{ width: 100%; border-collapse: collapse; margin: 20px 0; }}
th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }}
th {{ background-color: #007bff; color: white; }}
.section {{ margin: 30px 0; }}
</style>
</head>
<body>
<div class="container">
<h1>📊 Paper Trading Dashboard</h1>
<p style="text-align: center; color: #666;">Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<div class="section">
<h2>Portfolio Overview</h2>
<div class="metric-grid">
<div class="metric-card">
<div class="metric-value">${metrics['current_value']:,.2f}</div>
<div class="metric-label">Current Value</div>
</div>
<div class="metric-card">
<div class="metric-value {'positive' if metrics['total_return'] >= 0 else 'negative'}">{metrics['total_return']:+.2%}</div>
<div class="metric-label">Total Return</div>
</div>
<div class="metric-card">
<div class="metric-value">{metrics['sharpe_ratio']:.2f}</div>
<div class="metric-label">Sharpe Ratio</div>
</div>
</div>
</div>
<div class="section">
<h2>Trading Statistics</h2>
<div class="metric-grid">
<div class="metric-card">
<div class="metric-value">{metrics['total_trades']}</div>
<div class="metric-label">Total Trades</div>
</div>
<div class="metric-card">
<div class="metric-value">{metrics['win_rate_pct']:.1f}%</div>
<div class="metric-label">Win Rate</div>
</div>
<div class="metric-card">
<div class="metric-value">{metrics['profit_factor']:.2f}</div>
<div class="metric-label">Profit Factor</div>
</div>
</div>
</div>
<div class="section">
<h2>Risk Metrics</h2>
<table>
<tr>
<th>Metric</th>
<th>Value</th>
</tr>
<tr>
<td>Max Drawdown</td>
<td class="negative">{metrics['max_drawdown']:.2%}</td>
</tr>
<tr>
<td>Average Win</td>
<td class="positive">${metrics['avg_win']:,.2f}</td>
</tr>
<tr>
<td>Average Loss</td>
<td class="negative">${metrics['avg_loss']:,.2f}</td>
</tr>
</table>
</div>
<div class="section">
<h2>Recent Orders</h2>
<table>
<tr>
<th>Time</th>
<th>Symbol</th>
<th>Side</th>
<th>Amount</th>
<th>Price</th>
<th>Reason</th>
</tr>
"""
for order in reversed(self.engine.orders[-10:]):
side_class = "positive" if order.side.value == "buy" else "negative"
html += f"""
<tr>
<td>{order.timestamp.strftime('%H:%M:%S')}</td>
<td>{order.symbol}</td>
<td class="{side_class}">{order.side.value.upper()}</td>
<td>{order.amount:.6f}</td>
<td>${order.price:,.2f}</td>
<td>{order.reason}</td>
</tr>
"""
html += """
</table>
</div>
</div>
</body>
</html>
"""
filepath = os.path.join(self.engine.data_dir, filename)
with open(filepath, 'w') as f:
f.write(html)
print(f"✓ Generated HTML dashboard: {filepath}")
return filepath