442 lines
13 KiB
Python
442 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
📊 Trading Bot Dashboard - Streamlit
|
||
Real-time monitoring für V1.8 Trading Bot
|
||
"""
|
||
|
||
import streamlit as st
|
||
import sqlite3
|
||
import pandas as pd
|
||
from datetime import datetime, timedelta
|
||
import plotly.express as px
|
||
import plotly.graph_objects as go
|
||
|
||
# ==========================================
|
||
# PAGE CONFIG
|
||
# ==========================================
|
||
|
||
st.set_page_config(
|
||
page_title="Trading Bot Dashboard",
|
||
page_icon="📊",
|
||
layout="wide"
|
||
)
|
||
|
||
# ==========================================
|
||
# DATABASE CONNECTION
|
||
# ==========================================
|
||
|
||
@st.cache_resource
|
||
def get_connection():
|
||
return sqlite3.connect("trading_bot.db", check_same_thread=False)
|
||
|
||
conn = get_connection()
|
||
|
||
# ==========================================
|
||
# HEADER
|
||
# ==========================================
|
||
|
||
st.title("📊 Trading Bot Dashboard V1.8")
|
||
st.markdown("---")
|
||
|
||
# Controls
|
||
col1, col2, col3 = st.columns([1, 1, 2])
|
||
with col1:
|
||
if st.button("🔄 Refresh Data"):
|
||
st.cache_data.clear()
|
||
st.experimental_rerun()
|
||
|
||
with col2:
|
||
auto_refresh = st.checkbox("Auto-refresh (30s)")
|
||
|
||
with col3:
|
||
trade_filter = st.selectbox(
|
||
"📊 Filter Trades:",
|
||
["All Trades", "Live Trades Only", "Historical Only"],
|
||
index=1 # Default to "Live Trades Only"
|
||
)
|
||
|
||
if auto_refresh:
|
||
st.markdown("*Auto-refreshing every 30 seconds...*")
|
||
import time
|
||
time.sleep(30)
|
||
st.experimental_rerun()
|
||
|
||
# ==========================================
|
||
# LOAD DATA
|
||
# ==========================================
|
||
|
||
@st.cache_data(ttl=30)
|
||
def load_all_trades():
|
||
query = """
|
||
SELECT
|
||
ticket,
|
||
position_id,
|
||
symbol,
|
||
strategy_name,
|
||
type,
|
||
volume,
|
||
entry_price,
|
||
sl_price,
|
||
tp_price,
|
||
entry_time,
|
||
exit_time,
|
||
session,
|
||
regime,
|
||
quality,
|
||
confidence,
|
||
timeframe_alignment,
|
||
risk_amount,
|
||
risk_pct,
|
||
net_profit,
|
||
profit_pct,
|
||
rr_ratio,
|
||
status,
|
||
exit_reason
|
||
FROM trades
|
||
ORDER BY entry_time DESC
|
||
"""
|
||
return pd.read_sql_query(query, conn)
|
||
|
||
@st.cache_data(ttl=30)
|
||
def load_bot_status():
|
||
query = "SELECT * FROM bot_status ORDER BY timestamp DESC LIMIT 1"
|
||
return pd.read_sql_query(query, conn)
|
||
|
||
# Load data
|
||
df_trades_raw = load_all_trades()
|
||
df_status = load_bot_status()
|
||
|
||
# ==========================================
|
||
# APPLY TRADE FILTER
|
||
# ==========================================
|
||
|
||
if trade_filter == "Live Trades Only":
|
||
df_trades = df_trades_raw[df_trades_raw['status'] != 'historical'].copy()
|
||
st.info(f"📊 Showing **Live Trades Only** (excluding {(df_trades_raw['status'] == 'historical').sum()} historical imports)")
|
||
elif trade_filter == "Historical Only":
|
||
df_trades = df_trades_raw[df_trades_raw['status'] == 'historical'].copy()
|
||
st.info(f"📚 Showing **Historical Trades Only** ({len(df_trades)} trades)")
|
||
else: # All Trades
|
||
df_trades = df_trades_raw.copy()
|
||
live_count = (df_trades['status'] != 'historical').sum()
|
||
hist_count = (df_trades['status'] == 'historical').sum()
|
||
st.info(f"📊 Showing **All Trades** ({live_count} live + {hist_count} historical)")
|
||
|
||
# ==========================================
|
||
# TOP METRICS
|
||
# ==========================================
|
||
|
||
st.subheader("📈 Key Metrics")
|
||
|
||
col1, col2, col3, col4, col5 = st.columns(5)
|
||
|
||
total_trades = len(df_trades)
|
||
closed_trades = len(df_trades[df_trades['status'] == 'closed'])
|
||
open_trades = len(df_trades[df_trades['status'] == 'open'])
|
||
|
||
if closed_trades > 0:
|
||
winning_trades = len(df_trades[(df_trades['status'] == 'closed') & (df_trades['net_profit'] > 0)])
|
||
win_rate = (winning_trades / closed_trades) * 100
|
||
total_profit = df_trades[df_trades['status'] == 'closed']['net_profit'].sum()
|
||
else:
|
||
win_rate = 0
|
||
total_profit = 0
|
||
|
||
with col1:
|
||
st.metric("Total Trades", total_trades)
|
||
|
||
with col2:
|
||
st.metric("Open Positions", open_trades)
|
||
|
||
with col3:
|
||
st.metric("Win Rate", f"{win_rate:.1f}%")
|
||
|
||
with col4:
|
||
profit_color = "normal" if total_profit >= 0 else "inverse"
|
||
st.metric("Total Profit", f"${total_profit:.2f}", delta=None)
|
||
|
||
with col5:
|
||
if closed_trades > 0:
|
||
avg_profit = total_profit / closed_trades
|
||
st.metric("Avg Profit/Trade", f"${avg_profit:.2f}")
|
||
else:
|
||
st.metric("Avg Profit/Trade", "N/A")
|
||
|
||
st.markdown("---")
|
||
|
||
# ==========================================
|
||
# SESSION FILTER CHECK
|
||
# ==========================================
|
||
|
||
st.subheader("🎯 Session Filter Status")
|
||
|
||
# Calculate session distribution
|
||
session_counts = df_trades['session'].value_counts()
|
||
|
||
col1, col2 = st.columns([1, 2])
|
||
|
||
with col1:
|
||
st.markdown("#### Session Distribution")
|
||
|
||
for session in ['ny', 'london', 'asian', 'overlap']:
|
||
count = session_counts.get(session, 0)
|
||
pct = (count / total_trades * 100) if total_trades > 0 else 0
|
||
|
||
if session == 'ny':
|
||
if count == total_trades:
|
||
st.success(f"✅ NY: {count} ({pct:.1f}%) - PERFECT!")
|
||
else:
|
||
st.warning(f"⚠️ NY: {count} ({pct:.1f}%)")
|
||
else:
|
||
if count > 0:
|
||
st.error(f"❌ {session.upper()}: {count} ({pct:.1f}%) - VIOLATION!")
|
||
else:
|
||
st.success(f"✅ {session.upper()}: {count} (0%) - Blocked")
|
||
|
||
with col2:
|
||
st.markdown("#### Verdict")
|
||
|
||
ny_count = session_counts.get('ny', 0)
|
||
violation_count = total_trades - ny_count
|
||
|
||
if total_trades == 0:
|
||
st.info("ℹ️ No trades yet - waiting for data...")
|
||
elif violation_count == 0:
|
||
st.success("✅ **SESSION FILTER WORKING PERFECTLY!**")
|
||
st.markdown("All trades are in NY session (13:00-21:00 UTC)")
|
||
else:
|
||
st.error(f"❌ **SESSION FILTER NOT WORKING!**")
|
||
st.markdown(f"**{violation_count} trades** ({violation_count/total_trades*100:.1f}%) outside NY session!")
|
||
st.markdown("**Action Required:** See FIX_DUPLICATE_SCHEDULER.md")
|
||
|
||
st.markdown("---")
|
||
|
||
# ==========================================
|
||
# HOURLY DISTRIBUTION
|
||
# ==========================================
|
||
|
||
st.subheader("⏰ Trades by Hour (UTC)")
|
||
|
||
if not df_trades.empty:
|
||
# Extract hour from entry_time (handle both ISO8601 and standard format)
|
||
df_trades['hour_utc'] = pd.to_datetime(df_trades['entry_time'], format='mixed').dt.hour
|
||
|
||
# Count trades by hour
|
||
hourly_dist = df_trades.groupby('hour_utc').size().reset_index(name='count')
|
||
|
||
# Create visualization
|
||
fig = go.Figure()
|
||
|
||
# Add bars
|
||
colors = ['red' if (h < 13 or h >= 21) else 'green' for h in hourly_dist['hour_utc']]
|
||
|
||
fig.add_trace(go.Bar(
|
||
x=hourly_dist['hour_utc'],
|
||
y=hourly_dist['count'],
|
||
marker_color=colors,
|
||
text=hourly_dist['count'],
|
||
textposition='outside'
|
||
))
|
||
|
||
# Add NY session marker
|
||
fig.add_vrect(x0=13, x1=21, fillcolor="green", opacity=0.1, layer="below", line_width=0)
|
||
fig.add_annotation(x=17, y=hourly_dist['count'].max(), text="NY Session (13-21 UTC)", showarrow=False)
|
||
|
||
fig.update_layout(
|
||
xaxis_title="Hour (UTC)",
|
||
yaxis_title="Number of Trades",
|
||
height=400,
|
||
showlegend=False,
|
||
xaxis=dict(dtick=1, range=[-0.5, 23.5])
|
||
)
|
||
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
|
||
# Check for violations
|
||
violations = df_trades[(df_trades['hour_utc'] < 13) | (df_trades['hour_utc'] >= 21)]
|
||
if not violations.empty:
|
||
st.error(f"⚠️ **{len(violations)} trades outside NY hours detected!**")
|
||
with st.expander("Show violation details"):
|
||
st.dataframe(violations[['ticket', 'session', 'entry_time', 'hour_utc', 'type', 'net_profit']])
|
||
else:
|
||
st.info("No trades to display")
|
||
|
||
st.markdown("---")
|
||
|
||
# ==========================================
|
||
# SESSION PERFORMANCE
|
||
# ==========================================
|
||
|
||
st.subheader("📊 Performance by Session")
|
||
|
||
if closed_trades > 0:
|
||
session_perf = df_trades[df_trades['status'] == 'closed'].groupby('session').agg({
|
||
'ticket': 'count',
|
||
'net_profit': ['sum', 'mean']
|
||
}).round(2)
|
||
|
||
session_perf.columns = ['Trades', 'Total Profit', 'Avg Profit']
|
||
|
||
# Calculate win rate per session
|
||
win_rates = []
|
||
for session in session_perf.index:
|
||
session_trades = df_trades[(df_trades['status'] == 'closed') & (df_trades['session'] == session)]
|
||
wins = len(session_trades[session_trades['net_profit'] > 0])
|
||
wr = (wins / len(session_trades) * 100) if len(session_trades) > 0 else 0
|
||
win_rates.append(wr)
|
||
|
||
session_perf['Win Rate %'] = win_rates
|
||
|
||
# Color code
|
||
def color_sessions(row):
|
||
if row.name == 'ny':
|
||
return ['background-color: #90EE90'] * len(row) # Light green
|
||
else:
|
||
return ['background-color: #FFB6C6'] * len(row) # Light red
|
||
|
||
st.dataframe(session_perf.style.apply(color_sessions, axis=1), use_container_width=True)
|
||
else:
|
||
st.info("No closed trades yet")
|
||
|
||
st.markdown("---")
|
||
|
||
# ==========================================
|
||
# RECENT TRADES
|
||
# ==========================================
|
||
|
||
st.subheader("📋 Recent Trades (Last 20)")
|
||
|
||
if not df_trades.empty:
|
||
recent = df_trades.head(20).copy()
|
||
|
||
# Color code session
|
||
def highlight_session(row):
|
||
if row['session'] == 'ny':
|
||
return ['background-color: #90EE90'] * len(row)
|
||
else:
|
||
return ['background-color: #FFB6C6'] * len(row)
|
||
|
||
# Select columns
|
||
display_cols = ['ticket', 'type', 'session', 'entry_time', 'confidence', 'quality', 'regime', 'status', 'net_profit']
|
||
recent_display = recent[display_cols]
|
||
|
||
st.dataframe(
|
||
recent_display.style.apply(highlight_session, axis=1),
|
||
use_container_width=True,
|
||
height=400
|
||
)
|
||
else:
|
||
st.info("No trades yet")
|
||
|
||
st.markdown("---")
|
||
|
||
# ==========================================
|
||
# PROFIT OVER TIME
|
||
# ==========================================
|
||
|
||
st.subheader("💰 Cumulative Profit Over Time")
|
||
|
||
if closed_trades > 0:
|
||
profit_timeline = df_trades[df_trades['status'] == 'closed'].copy()
|
||
profit_timeline['exit_time'] = pd.to_datetime(profit_timeline['exit_time'], format='mixed')
|
||
profit_timeline = profit_timeline.sort_values('exit_time')
|
||
profit_timeline['cumulative_profit'] = profit_timeline['net_profit'].cumsum()
|
||
|
||
fig = px.line(
|
||
profit_timeline,
|
||
x='exit_time',
|
||
y='cumulative_profit',
|
||
title='Cumulative Profit',
|
||
labels={'exit_time': 'Date', 'cumulative_profit': 'Profit ($)'}
|
||
)
|
||
|
||
fig.update_traces(line_color='green' if profit_timeline['cumulative_profit'].iloc[-1] > 0 else 'red')
|
||
fig.add_hline(y=0, line_dash="dash", line_color="gray")
|
||
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
else:
|
||
st.info("No closed trades yet")
|
||
|
||
st.markdown("---")
|
||
|
||
# ==========================================
|
||
# CONFIDENCE & QUALITY ANALYSIS
|
||
# ==========================================
|
||
|
||
st.subheader("🎯 Signal Quality Analysis")
|
||
|
||
col1, col2 = st.columns(2)
|
||
|
||
with col1:
|
||
st.markdown("#### Confidence Distribution")
|
||
if not df_trades.empty:
|
||
fig = px.histogram(
|
||
df_trades,
|
||
x='confidence',
|
||
nbins=20,
|
||
title='Trade Confidence Distribution'
|
||
)
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
else:
|
||
st.info("No data")
|
||
|
||
with col2:
|
||
st.markdown("#### Quality Breakdown")
|
||
if not df_trades.empty:
|
||
quality_counts = df_trades['quality'].value_counts()
|
||
fig = px.pie(
|
||
values=quality_counts.values,
|
||
names=quality_counts.index,
|
||
title='Signal Quality Distribution'
|
||
)
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
else:
|
||
st.info("No data")
|
||
|
||
st.markdown("---")
|
||
|
||
# ==========================================
|
||
# BOT STATUS
|
||
# ==========================================
|
||
|
||
st.subheader("🤖 Bot Status")
|
||
|
||
if not df_status.empty:
|
||
status = df_status.iloc[0]
|
||
|
||
col1, col2, col3 = st.columns(3)
|
||
|
||
with col1:
|
||
st.markdown("**Version:**")
|
||
st.code(status.get('version', 'N/A'))
|
||
|
||
with col2:
|
||
st.markdown("**Status:**")
|
||
bot_status = status.get('status', 'unknown')
|
||
if bot_status == 'running':
|
||
st.success("🟢 Running")
|
||
elif bot_status == 'stopped':
|
||
st.error("🔴 Stopped")
|
||
else:
|
||
st.warning("⚠️ Unknown")
|
||
|
||
with col3:
|
||
st.markdown("**Last Update:**")
|
||
st.code(status.get('timestamp', 'N/A'))
|
||
|
||
if 'config' in status and status['config']:
|
||
with st.expander("Show Configuration"):
|
||
st.json(status['config'])
|
||
else:
|
||
st.warning("No bot status available")
|
||
|
||
st.markdown("---")
|
||
|
||
# ==========================================
|
||
# FOOTER
|
||
# ==========================================
|
||
|
||
st.markdown("---")
|
||
st.caption(f"Dashboard last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||
st.caption("📊 Trading Bot V1.8 - Aggressive Mode (NY Only)")
|