Files
crypto_news/dashboard/app.py
2026-03-20 07:49:42 +09:00

449 lines
17 KiB
Python

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