deploy: 2026-03-20 07:49
This commit is contained in:
448
dashboard/app.py
Normal file
448
dashboard/app.py
Normal file
@@ -0,0 +1,448 @@
|
||||
"""Streamlit dashboard for ICT Crypto Bot monitoring.
|
||||
|
||||
Run with: streamlit run dashboard/app.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import date, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("SMC_CREDIT", "0")
|
||||
|
||||
# Ensure project root is on the path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
from plotly.subplots import make_subplots
|
||||
|
||||
from database.repository import TradingRepository
|
||||
|
||||
|
||||
def get_repo() -> TradingRepository:
|
||||
repo = TradingRepository()
|
||||
repo.connect()
|
||||
return repo
|
||||
|
||||
|
||||
def load_bot_state(repo: TradingRepository) -> dict:
|
||||
"""Load bot runtime state from DB."""
|
||||
state = {}
|
||||
for key in ["bot_status", "balance", "trading_pairs", "last_analysis", "start_time"]:
|
||||
val = repo.get_state(key)
|
||||
if val:
|
||||
state[key] = val
|
||||
return state
|
||||
|
||||
|
||||
def main():
|
||||
st.set_page_config(
|
||||
page_title="ICT Crypto Bot Dashboard",
|
||||
page_icon="📊",
|
||||
layout="wide",
|
||||
)
|
||||
|
||||
st.title("📊 ICT Smart Money Concepts Trading Bot")
|
||||
|
||||
repo = get_repo()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Sidebar
|
||||
# ------------------------------------------------------------------
|
||||
st.sidebar.header("Navigation")
|
||||
page = st.sidebar.radio(
|
||||
"Page",
|
||||
["Overview", "Open Positions", "Trade History", "Performance", "Bot Status"],
|
||||
)
|
||||
st.sidebar.markdown("---")
|
||||
st.sidebar.caption("Auto-refresh: 10s")
|
||||
st_autorefresh = st.sidebar.checkbox("Auto Refresh", value=True)
|
||||
if st_autorefresh:
|
||||
try:
|
||||
from streamlit_autorefresh import st_autorefresh as auto_ref
|
||||
auto_ref(interval=10000, key="refresh")
|
||||
except ImportError:
|
||||
st.sidebar.info("Install streamlit-autorefresh for auto-refresh")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Overview
|
||||
# ------------------------------------------------------------------
|
||||
if page == "Overview":
|
||||
st.header("Dashboard Overview")
|
||||
|
||||
open_positions = repo.get_open_positions()
|
||||
perf = repo.get_daily_performance()
|
||||
closed = repo.get_closed_positions(limit=10000)
|
||||
total_realized = sum(p.realized_pnl for p in closed)
|
||||
total_trades = len(closed)
|
||||
wins = sum(1 for p in closed if p.realized_pnl > 0)
|
||||
win_rate = wins / total_trades * 100 if total_trades > 0 else 0
|
||||
|
||||
# Top metrics
|
||||
col1, col2, col3, col4, col5 = st.columns(5)
|
||||
col1.metric("Open Positions", len(open_positions))
|
||||
col2.metric("Total Trades", total_trades)
|
||||
col3.metric(
|
||||
"Today's PnL",
|
||||
f"${perf.total_pnl:,.2f}" if perf else "$0.00",
|
||||
)
|
||||
col4.metric("Total PnL", f"${total_realized:,.2f}")
|
||||
col5.metric("Win Rate", f"{win_rate:.1f}%")
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# Two columns: chart + recent trades
|
||||
chart_col, trades_col = st.columns([2, 1])
|
||||
|
||||
with chart_col:
|
||||
history = repo.get_performance_history(30)
|
||||
if history:
|
||||
df = pd.DataFrame([
|
||||
{"date": h.date, "pnl": h.total_pnl, "trades": h.total_trades}
|
||||
for h in reversed(history)
|
||||
])
|
||||
df["cumulative_pnl"] = df["pnl"].cumsum()
|
||||
|
||||
fig = make_subplots(
|
||||
rows=2, cols=1, shared_xaxes=True,
|
||||
row_heights=[0.7, 0.3],
|
||||
subplot_titles=("Cumulative PnL", "Daily Trades"),
|
||||
)
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df["date"], y=df["cumulative_pnl"],
|
||||
mode="lines+markers", name="Cumulative PnL",
|
||||
line=dict(color="cyan", width=2),
|
||||
fill="tozeroy", fillcolor="rgba(0,255,255,0.1)",
|
||||
),
|
||||
row=1, col=1,
|
||||
)
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
x=df["date"], y=df["trades"],
|
||||
name="Trades", marker_color="rgba(100,200,255,0.6)",
|
||||
),
|
||||
row=2, col=1,
|
||||
)
|
||||
fig.update_layout(
|
||||
template="plotly_dark", height=400,
|
||||
showlegend=False, margin=dict(t=30, b=10),
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
else:
|
||||
st.info("No performance history yet. The bot is monitoring markets and will trade when ICT conditions are met.")
|
||||
|
||||
with trades_col:
|
||||
st.subheader("Recent Trades")
|
||||
recent = repo.get_closed_positions(limit=10)
|
||||
if recent:
|
||||
for p in recent:
|
||||
emoji = "🟢" if p.realized_pnl >= 0 else "🔴"
|
||||
st.markdown(
|
||||
f"{emoji} **{p.symbol}** {p.direction} "
|
||||
f"PnL: ${p.realized_pnl:,.2f} ({p.close_reason or ''})"
|
||||
)
|
||||
else:
|
||||
st.info("No trades yet")
|
||||
|
||||
# Trading pairs being monitored
|
||||
st.markdown("---")
|
||||
st.subheader("Monitored Trading Pairs")
|
||||
try:
|
||||
from config import settings
|
||||
pairs = settings.TRADING_PAIRS
|
||||
cols = st.columns(min(len(pairs), 5))
|
||||
for i, pair in enumerate(pairs):
|
||||
with cols[i % len(cols)]:
|
||||
st.markdown(f"**{pair}**")
|
||||
except Exception:
|
||||
st.info("Could not load trading pairs config")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Open Positions
|
||||
# ------------------------------------------------------------------
|
||||
elif page == "Open Positions":
|
||||
st.header("Open Positions")
|
||||
positions = repo.get_open_positions()
|
||||
if not positions:
|
||||
st.info("No open positions. The bot is waiting for ICT confluence signals (minimum 3/6 conditions).")
|
||||
else:
|
||||
data = []
|
||||
for p in positions:
|
||||
data.append({
|
||||
"Symbol": p.symbol,
|
||||
"Direction": p.direction,
|
||||
"Entry Price": p.entry_price,
|
||||
"Stop Loss": p.stop_loss,
|
||||
"Take Profit": p.take_profit,
|
||||
"Amount": p.amount,
|
||||
"Confluence": f"{p.confluence_score}/6",
|
||||
"Opened": p.opened_at,
|
||||
})
|
||||
st.dataframe(pd.DataFrame(data), use_container_width=True, hide_index=True)
|
||||
|
||||
# Unrealized PnL summary
|
||||
total_unreal = sum(p.realized_pnl for p in positions) # placeholder
|
||||
st.metric("Total Unrealized PnL (estimate)", f"${total_unreal:,.2f}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Trade History
|
||||
# ------------------------------------------------------------------
|
||||
elif page == "Trade History":
|
||||
st.header("Closed Positions")
|
||||
limit = st.slider("Show last N trades", 10, 500, 50)
|
||||
closed = repo.get_closed_positions(limit=limit)
|
||||
if not closed:
|
||||
st.info("No closed positions yet. Trades will appear here once positions are opened and closed.")
|
||||
else:
|
||||
data = []
|
||||
for p in closed:
|
||||
data.append({
|
||||
"Symbol": p.symbol,
|
||||
"Direction": p.direction,
|
||||
"Entry": p.entry_price,
|
||||
"PnL": p.realized_pnl,
|
||||
"Reason": p.close_reason or "",
|
||||
"Confluence": p.confluence_score,
|
||||
"Closed": p.closed_at or "",
|
||||
})
|
||||
df = pd.DataFrame(data)
|
||||
st.dataframe(df, use_container_width=True, hide_index=True)
|
||||
|
||||
# PnL distribution
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
fig = go.Figure(data=[
|
||||
go.Histogram(x=df["PnL"], nbinsx=30, marker_color="cyan")
|
||||
])
|
||||
fig.update_layout(
|
||||
title="PnL Distribution",
|
||||
xaxis_title="PnL ($)", yaxis_title="Count",
|
||||
template="plotly_dark", height=300,
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
with col2:
|
||||
# Win/Loss by symbol
|
||||
symbol_stats = df.groupby("Symbol").agg(
|
||||
trades=("PnL", "count"),
|
||||
total_pnl=("PnL", "sum"),
|
||||
avg_pnl=("PnL", "mean"),
|
||||
).reset_index()
|
||||
fig = go.Figure(data=[
|
||||
go.Bar(
|
||||
x=symbol_stats["Symbol"],
|
||||
y=symbol_stats["total_pnl"],
|
||||
marker_color=["lime" if x >= 0 else "red" for x in symbol_stats["total_pnl"]],
|
||||
)
|
||||
])
|
||||
fig.update_layout(
|
||||
title="PnL by Symbol",
|
||||
yaxis_title="Total PnL ($)",
|
||||
template="plotly_dark", height=300,
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Performance
|
||||
# ------------------------------------------------------------------
|
||||
elif page == "Performance":
|
||||
st.header("Performance Metrics")
|
||||
|
||||
# Overall stats from all closed trades
|
||||
all_closed = repo.get_closed_positions(limit=10000)
|
||||
|
||||
if not all_closed:
|
||||
st.info("No trading data yet. Performance metrics will be calculated from completed trades.")
|
||||
|
||||
# Show what the bot is monitoring
|
||||
st.markdown("---")
|
||||
st.subheader("What the bot is doing right now")
|
||||
st.markdown("""
|
||||
The ICT trading bot is actively monitoring markets for Smart Money Concepts signals:
|
||||
|
||||
1. **Higher Timeframe (4H)** — Determining market bias (Bullish/Bearish/Neutral)
|
||||
2. **Middle Timeframe (1H)** — Scanning for Order Blocks and Fair Value Gaps
|
||||
3. **Lower Timeframe (15M)** — Looking for precise entry points
|
||||
|
||||
A trade signal requires **minimum 3 out of 6** confluence conditions:
|
||||
- Market Structure alignment
|
||||
- Liquidity Sweep detection
|
||||
- Order Block price entry
|
||||
- Fair Value Gap price entry
|
||||
- Break of Structure confirmation
|
||||
- Change of Character confirmation
|
||||
|
||||
The bot will automatically execute paper trades when conditions are met.
|
||||
""")
|
||||
else:
|
||||
total_trades = len(all_closed)
|
||||
wins = [p for p in all_closed if p.realized_pnl > 0]
|
||||
losses = [p for p in all_closed if p.realized_pnl <= 0]
|
||||
total_pnl = sum(p.realized_pnl for p in all_closed)
|
||||
avg_win = sum(p.realized_pnl for p in wins) / len(wins) if wins else 0
|
||||
avg_loss = sum(p.realized_pnl for p in losses) / len(losses) if losses else 0
|
||||
profit_factor = abs(sum(p.realized_pnl for p in wins) / sum(p.realized_pnl for p in losses)) if losses and sum(p.realized_pnl for p in losses) != 0 else 0
|
||||
|
||||
# Key metrics row
|
||||
m1, m2, m3, m4, m5, m6 = st.columns(6)
|
||||
m1.metric("Total Trades", total_trades)
|
||||
m2.metric("Win Rate", f"{len(wins)/total_trades*100:.1f}%")
|
||||
m3.metric("Total PnL", f"${total_pnl:,.2f}")
|
||||
m4.metric("Avg Win", f"${avg_win:,.2f}")
|
||||
m5.metric("Avg Loss", f"${avg_loss:,.2f}")
|
||||
m6.metric("Profit Factor", f"{profit_factor:.2f}")
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# Charts
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
# Equity curve
|
||||
pnls = [p.realized_pnl for p in reversed(all_closed)]
|
||||
equity = [300] # initial balance
|
||||
for pnl in pnls:
|
||||
equity.append(equity[-1] + pnl)
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(go.Scatter(
|
||||
y=equity, mode="lines",
|
||||
name="Equity",
|
||||
line=dict(color="cyan", width=2),
|
||||
fill="tozeroy", fillcolor="rgba(0,255,255,0.1)",
|
||||
))
|
||||
fig.add_hline(y=300, line_dash="dash", line_color="gray",
|
||||
annotation_text="Initial $300")
|
||||
fig.update_layout(
|
||||
title="Equity Curve",
|
||||
yaxis_title="Balance ($)",
|
||||
template="plotly_dark", height=350,
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
with col2:
|
||||
# Win/Loss pie
|
||||
fig = go.Figure(data=[go.Pie(
|
||||
labels=["Wins", "Losses"],
|
||||
values=[len(wins), len(losses)],
|
||||
marker_colors=["lime", "red"],
|
||||
hole=0.4,
|
||||
)])
|
||||
fig.update_layout(
|
||||
title="Win/Loss Ratio",
|
||||
template="plotly_dark", height=350,
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# Daily performance table
|
||||
st.markdown("---")
|
||||
st.subheader("Daily Breakdown")
|
||||
history = repo.get_performance_history(90)
|
||||
if history:
|
||||
df = pd.DataFrame([
|
||||
{
|
||||
"Date": h.date,
|
||||
"Trades": h.total_trades,
|
||||
"Wins": h.winning_trades,
|
||||
"Losses": h.losing_trades,
|
||||
"Win Rate": f"{h.win_rate:.1%}",
|
||||
"PnL": f"${h.total_pnl:,.2f}",
|
||||
"Max DD": f"{h.max_drawdown:.2%}",
|
||||
}
|
||||
for h in history # already DESC
|
||||
])
|
||||
st.dataframe(df, use_container_width=True, hide_index=True)
|
||||
|
||||
# Win rate chart
|
||||
rates = pd.DataFrame([
|
||||
{"date": h.date, "win_rate": h.win_rate, "pnl": h.total_pnl}
|
||||
for h in reversed(history)
|
||||
])
|
||||
fig = make_subplots(
|
||||
rows=2, cols=1, shared_xaxes=True,
|
||||
row_heights=[0.5, 0.5],
|
||||
subplot_titles=("Daily Win Rate", "Daily PnL"),
|
||||
)
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
x=rates["date"], y=rates["win_rate"],
|
||||
marker_color="lime", name="Win Rate",
|
||||
),
|
||||
row=1, col=1,
|
||||
)
|
||||
fig.add_hline(y=0.6, line_dash="dash", line_color="yellow",
|
||||
annotation_text="Target 60%", row=1, col=1)
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
x=rates["date"], y=rates["pnl"],
|
||||
marker_color=["lime" if x >= 0 else "red" for x in rates["pnl"]],
|
||||
name="Daily PnL",
|
||||
),
|
||||
row=2, col=1,
|
||||
)
|
||||
fig.update_layout(
|
||||
template="plotly_dark", height=500,
|
||||
showlegend=False,
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Bot Status
|
||||
# ------------------------------------------------------------------
|
||||
elif page == "Bot Status":
|
||||
st.header("Bot Status")
|
||||
|
||||
try:
|
||||
from config import settings
|
||||
pairs = settings.TRADING_PAIRS
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.subheader("Configuration")
|
||||
st.json({
|
||||
"Exchange": settings.EXCHANGE_ID,
|
||||
"Sandbox Mode": settings.SANDBOX_MODE,
|
||||
"Trading Pairs": len(pairs),
|
||||
"HTF Timeframe": settings.HTF_TIMEFRAME,
|
||||
"MTF Timeframe": settings.MTF_TIMEFRAME,
|
||||
"LTF Timeframe": settings.LTF_TIMEFRAME,
|
||||
"Min Confluence": settings.MIN_CONFLUENCE_SCORE,
|
||||
"Max Risk/Trade": f"{settings.MAX_RISK_PER_TRADE:.1%}",
|
||||
"Max Daily Loss": f"{settings.MAX_DAILY_LOSS:.1%}",
|
||||
"Max Drawdown": f"{settings.MAX_DRAWDOWN:.1%}",
|
||||
"Max Positions": settings.MAX_CONCURRENT_POSITIONS,
|
||||
})
|
||||
|
||||
with col2:
|
||||
st.subheader("Trading Pairs")
|
||||
for i in range(0, len(pairs), 4):
|
||||
cols = st.columns(4)
|
||||
for j, col in enumerate(cols):
|
||||
idx = i + j
|
||||
if idx < len(pairs):
|
||||
col.markdown(f"**{pairs[idx]}**")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Could not load config: {e}")
|
||||
|
||||
# DB stats
|
||||
st.markdown("---")
|
||||
st.subheader("Database Stats")
|
||||
open_count = len(repo.get_open_positions())
|
||||
closed_count = len(repo.get_closed_positions(limit=10000))
|
||||
st.json({
|
||||
"Open Positions": open_count,
|
||||
"Closed Positions": closed_count,
|
||||
"DB Path": str(Path("data/trading.db").resolve()),
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user