"""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()