"""Streamlit real-time trading dashboard. Launch with: streamlit run dashboard/app.py """ from __future__ import annotations import sqlite3 import time from datetime import datetime, timezone from pathlib import Path import pandas as pd import streamlit as st # ------------------------------------------------------------------ # Config # ------------------------------------------------------------------ DB_PATH = Path(__file__).parent.parent / "trades.db" PAPER_DB_PATH = Path(__file__).parent.parent / "paper_trades.db" st.set_page_config( page_title="Polymarket Arb Bot", page_icon="⚡", layout="wide", initial_sidebar_state="collapsed", ) # ------------------------------------------------------------------ # Premium CSS # ------------------------------------------------------------------ st.markdown(""" """, unsafe_allow_html=True) # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ def get_db_path() -> Path: mode = st.sidebar.radio("Mode", ["Paper", "Live"], index=0, horizontal=True) return PAPER_DB_PATH if mode == "Paper" else DB_PATH def query_db(db_path: Path, sql: str) -> pd.DataFrame: if not db_path.exists(): return pd.DataFrame() try: conn = sqlite3.connect(str(db_path)) df = pd.read_sql_query(sql, conn) conn.close() return df except Exception: return pd.DataFrame() def fmt_price(price: float, asset: str = "") -> str: if price >= 1000: return f"${price:,.2f}" return f"${price:.2f}" def time_ago(ts: float) -> str: diff = time.time() - ts if diff < 60: return f"{diff:.0f}s ago" elif diff < 3600: return f"{diff/60:.0f}m ago" else: return f"{diff/3600:.1f}h ago" # ------------------------------------------------------------------ # Layout # ------------------------------------------------------------------ db_path = get_db_path() # Sidebar settings st.sidebar.markdown("---") st.sidebar.markdown('', unsafe_allow_html=True) refresh_rate = st.sidebar.selectbox("Interval (sec)", [5, 10, 30], index=1, label_visibility="collapsed") auto_refresh = st.sidebar.checkbox("Auto-refresh", value=True) # Load data trades_df = query_db(db_path, "SELECT * FROM trades ORDER BY created_at DESC") windows_df = query_db(db_path, "SELECT * FROM window_snapshots ORDER BY created_at DESC") daily_df = query_db(db_path, "SELECT * FROM daily_summary ORDER BY date DESC") balance_df = query_db(db_path, "SELECT * FROM balance_history ORDER BY timestamp ASC") oracle_df = query_db(db_path, "SELECT * FROM oracle_snapshots ORDER BY timestamp DESC") # ================================================================== # Header # ================================================================== # Use balance_history (every 30s) for liveness check instead of window_snapshots (every 5min) _latest_ts = balance_df.iloc[-1]["timestamp"] if not balance_df.empty else 0 has_recent = (time.time() - _latest_ts) < 120 mode_label = "PAPER" if "paper" in str(db_path) else "LIVE" status_class = "header-badge" if has_recent else "header-badge header-badge-warn" status_text = "RUNNING" if has_recent else "OFFLINE" st.markdown(f"""
Polymarket Temporal Arb
Real-time arbitrage monitoring • {mode_label} MODE
{status_text} {datetime.now(timezone.utc).strftime('%H:%M:%S UTC')}
""", unsafe_allow_html=True) # ================================================================== # Section 1: Key Metrics # ================================================================== if not balance_df.empty: latest_bal = balance_df.iloc[-1] current_balance = latest_bal["balance"] current_pnl = latest_bal["pnl"] starting_balance = balance_df.iloc[0]["balance"] pnl_pct = (current_pnl / starting_balance * 100) if starting_balance > 0 else 0 total_trades = len(trades_df) if not trades_df.empty else 0 # Only count resolved trades for win/loss (exclude pnl=0 which are still open) resolved_df = trades_df[trades_df["pnl"] != 0] if not trades_df.empty and "pnl" in trades_df.columns else pd.DataFrame() open_df = trades_df[trades_df["pnl"] == 0] if not trades_df.empty and "pnl" in trades_df.columns else pd.DataFrame() wins = len(resolved_df[resolved_df["pnl"] > 0]) if not resolved_df.empty else 0 losses = len(resolved_df[resolved_df["pnl"] < 0]) if not resolved_df.empty else 0 resolved_count = wins + losses win_rate = wins / resolved_count * 100 if resolved_count > 0 else 0 total_fees = trades_df["fee"].sum() if not trades_df.empty and "fee" in trades_df.columns else 0 avg_edge = trades_df["signal_edge"].mean() * 100 if not trades_df.empty and "signal_edge" in trades_df.columns else 0 # Total bet volume (cost basis = fill_price * size) total_bet = (trades_df["fill_price"] * trades_df["size"]).sum() if not trades_df.empty and "fill_price" in trades_df.columns else 0 # Active position exposure active_exposure = (open_df["fill_price"] * open_df["size"]).sum() if not open_df.empty else 0 pnl_color = "green" if current_pnl >= 0 else "red" pnl_sign = "+" if current_pnl >= 0 else "" c1, c2, c3, c4, c5, c6 = st.columns(6) with c1: st.markdown(f"""
Balance
${current_balance:,.2f}
Start: ${starting_balance:,.2f}
""", unsafe_allow_html=True) with c2: st.markdown(f"""
Total PnL
{pnl_sign}${current_pnl:,.2f}
{pnl_sign}{pnl_pct:.2f}% return
""", unsafe_allow_html=True) with c3: wr_color = "green" if win_rate >= 50 else "amber" if win_rate >= 40 else "red" st.markdown(f"""
Win Rate
{win_rate:.1f}%
{wins}W / {losses}L ({resolved_count} resolved)
""", unsafe_allow_html=True) with c4: avg_bet = f"${total_bet/total_trades:,.0f}" if total_trades > 0 else "$0" st.markdown(f"""
Total Bets
${total_bet:,.0f}
{total_trades} trades • avg {avg_bet}/trade
""", unsafe_allow_html=True) with c5: st.markdown(f"""
Active Exposure
${active_exposure:,.0f}
{len(open_df)} open positions
""", unsafe_allow_html=True) with c6: # PnL per resolved trade pnl_per_trade = current_pnl / resolved_count if resolved_count > 0 else 0 st.markdown(f"""
PnL / Trade
{'+' if pnl_per_trade >= 0 else ''}${pnl_per_trade:.2f}
Avg edge: {avg_edge:.1f}%
""", unsafe_allow_html=True) else: st.markdown("""
📡
Awaiting Balance Data
Bot is initializing. Balance snapshots will appear once trading begins.
""", unsafe_allow_html=True) # ================================================================== # Section 2: Live Prices + Connections # ================================================================== st.markdown("""
📡
Market Data & Connections
""", unsafe_allow_html=True) latest_prices = {} if not windows_df.empty: for asset in ["BTC", "ETH", "SOL"]: asset_rows = windows_df[windows_df["asset"] == asset] if not asset_rows.empty: row = asset_rows.iloc[0] latest_prices[asset] = { "price": row.get("end_price", row.get("start_price", 0)), "start": row.get("start_price", 0), "change_pct": row.get("price_change_pct", 0), "updated": row.get("created_at", 0), } col_conn, col_btc, col_eth, col_sol = st.columns([1.3, 1, 1, 1]) with col_conn: binance_ok = has_recent # based on balance_history (30s interval) poly_ok = not windows_df.empty and windows_df.iloc[0].get("market_condition_id") is not None last_update = time_ago(windows_df.iloc[0]["created_at"]) if not windows_df.empty else "N/A" st.markdown(f"""
Binance WebSocket {'Live' if binance_ok else 'Reconnecting'}
Polymarket CLOB {'Active' if poly_ok else 'Scanning'}
Chainlink Oracle {'Polling' if not oracle_df.empty else 'Waiting'}
SQLite Database {len(windows_df)} snapshots
Last update: {last_update}
""", unsafe_allow_html=True) asset_icons = {"BTC": "₿", "ETH": "Ξ", "SOL": "◎"} asset_colors = {"BTC": "#f7931a", "ETH": "#627eea", "SOL": "#9945ff"} for col, asset in [(col_btc, "BTC"), (col_eth, "ETH"), (col_sol, "SOL")]: with col: info = latest_prices.get(asset, {}) price = info.get("price", 0) change = info.get("change_pct", 0) color = "green" if change >= 0 else "red" arrow = "▲" if change > 0 else "▼" if change < 0 else "—" st.markdown(f"""
{asset_icons[asset]} {asset}/USDT
{fmt_price(price)}
{arrow} {change:+.4f}%
""", unsafe_allow_html=True) # ================================================================== # Section 2b: Chainlink Oracle vs Binance # ================================================================== if not oracle_df.empty: st.markdown("""
🔗
Chainlink Oracle vs Binance (Arb Gap)
""", unsafe_allow_html=True) # Latest oracle data per asset o_cols = st.columns(3) for i, asset in enumerate(["BTC", "ETH", "SOL"]): asset_oracle = oracle_df[oracle_df["asset"] == asset] if asset_oracle.empty: continue latest = asset_oracle.iloc[0] o_price = latest["oracle_price"] c_price = latest["cex_price"] dev = latest["deviation_pct"] lag = latest["oracle_lag_sec"] dev_color = "amber" if abs(dev) > 0.1 else "green" if abs(dev) < 0.05 else "cyan" dev_sign = "+" if dev >= 0 else "" gap_label = "GAP!" if abs(dev) > 0.1 else "OK" gap_color = "#fbbf24" if abs(dev) > 0.1 else "#4ade80" with o_cols[i]: st.markdown(f"""
{asset} Oracle Gap
{gap_label}
{dev_sign}{dev:.4f}%
CHAINLINK
{fmt_price(o_price)}
BINANCE
{fmt_price(c_price)}
Oracle lag: {lag:.0f}s
""", unsafe_allow_html=True) # Deviation over time chart if len(oracle_df) > 3: st.markdown("**Oracle Deviation Over Time (%)**") chart_oracle = oracle_df.copy() chart_oracle["time"] = pd.to_datetime(chart_oracle["timestamp"], unit="s") # Pivot by asset pivot = chart_oracle.pivot_table(index="time", columns="asset", values="deviation_pct", aggfunc="last") pivot = pivot.sort_index() if not pivot.empty: st.line_chart(pivot, color=["#f7931a", "#627eea", "#9945ff"]) # ================================================================== # Section 3: Window Tracker # ================================================================== st.markdown("""
Active Windows
""", unsafe_allow_html=True) if not windows_df.empty: seen = set() window_rows = [] for _, row in windows_df.iterrows(): key = f"{row['asset']}_{row['timeframe']}" if key not in seen: seen.add(key) window_rows.append(row) if len(seen) >= 6: break w_cols = st.columns(3) for i, row in enumerate(window_rows): asset = row["asset"] tf = row["timeframe"] start_p = row.get("start_price", 0) end_p = row.get("end_price", start_p) change = row.get("price_change_pct", 0) w_start = row.get("window_start", 0) w_end = row.get("window_end", 0) remaining = max(0, w_end - time.time()) color = "green" if change >= 0 else "red" direction = "▲ UP" if change > 0 else "▼ DOWN" if change < 0 else "— FLAT" badge_class = "badge-5m" if tf == "5M" else "badge-15m" total_window = w_end - w_start elapsed_frac = max(0, min(1, 1 - (remaining / total_window))) if total_window > 0 else 0 bar_width = int(elapsed_frac * 100) bar_color = "#4ade80" if change >= 0 else "#f87171" with w_cols[i % 3]: st.markdown(f"""
{asset}
{tf} {direction}
Open: {fmt_price(start_p)} Now: {fmt_price(end_p)} {change:+.4f}%
{remaining:.0f}s remaining
""", unsafe_allow_html=True) else: st.info("Waiting for first window data...") # ================================================================== # Section 4: Charts # ================================================================== if not balance_df.empty and len(balance_df) > 1: st.markdown("""
📈
Performance Charts
""", unsafe_allow_html=True) chart_col1, chart_col2 = st.columns(2) with chart_col1: st.markdown("**Balance Over Time**") chart_bal = balance_df.copy() chart_bal["time"] = pd.to_datetime(chart_bal["timestamp"], unit="s") chart_bal = chart_bal.set_index("time")[["balance"]] st.area_chart(chart_bal, color="#818cf8") with chart_col2: st.markdown("**PnL Curve**") chart_pnl = balance_df.copy() chart_pnl["time"] = pd.to_datetime(chart_pnl["timestamp"], unit="s") chart_pnl = chart_pnl.set_index("time")[["pnl"]] st.area_chart(chart_pnl, color="#4ade80") # ================================================================== # Section 5: Trades # ================================================================== # ------------------------------------------------------------------ # Active Positions (open trades with pnl=0) # ------------------------------------------------------------------ if not trades_df.empty and "pnl" in trades_df.columns: open_trades = trades_df[trades_df["pnl"] == 0].copy() if not open_trades.empty: st.markdown("""
🎯
Active Positions
""", unsafe_allow_html=True) open_trades["bet_amount"] = (open_trades["fill_price"] * open_trades["size"]).round(2) open_trades["max_payout"] = open_trades["size"].apply(lambda s: round(s * 1.0, 2)) open_trades["potential_profit"] = (open_trades["max_payout"] - open_trades["bet_amount"]).round(2) open_trades["edge_pct"] = (open_trades["signal_edge"] * 100).round(1) pos_cols = [c for c in ["asset", "direction", "timeframe", "fill_price", "size", "bet_amount", "potential_profit", "edge_pct"] if c in open_trades.columns] st.dataframe( open_trades[pos_cols].head(20).rename(columns={ "fill_price": "Entry Price", "size": "Shares", "bet_amount": "Bet ($)", "potential_profit": "Max Profit ($)", "edge_pct": "Edge %", "asset": "Asset", "direction": "Dir", "timeframe": "TF", }), use_container_width=True, hide_index=True, ) # ------------------------------------------------------------------ # Trading Activity (resolved trades) # ------------------------------------------------------------------ st.markdown("""
Trading Activity
""", unsafe_allow_html=True) if not trades_df.empty: # Cumulative PnL chart (only resolved trades) if "pnl" in trades_df.columns and "created_at" in trades_df.columns: resolved_chart = trades_df[trades_df["pnl"] != 0].sort_values("created_at").copy() if not resolved_chart.empty: st.markdown("**Cumulative PnL (Resolved Trades)**") resolved_chart["cumulative_pnl"] = resolved_chart["pnl"].cumsum() resolved_chart["trade_num"] = range(1, len(resolved_chart) + 1) st.line_chart(resolved_chart.set_index("trade_num")[["cumulative_pnl"]], color="#22d3ee") # Add bet amount column for display display_df = trades_df.copy() display_df["bet_amount"] = (display_df["fill_price"] * display_df["size"]).round(2) st.markdown("**Recent Trades**") display_cols = [c for c in ["asset", "direction", "timeframe", "fill_price", "size", "bet_amount", "pnl", "fee", "signal_edge", "status"] if c in display_df.columns] st.dataframe( display_df[display_cols].head(50).rename(columns={ "fill_price": "Price", "bet_amount": "Bet ($)", "signal_edge": "Edge", }), use_container_width=True, hide_index=True, ) else: st.markdown("""
Scanning for Opportunities
The bot is monitoring CEX prices and scanning Polymarket markets.
Trades will appear here once arbitrage opportunities are detected and executed.
""", unsafe_allow_html=True) # ================================================================== # Section 6: Window History # ================================================================== if not windows_df.empty: st.markdown("""
🕐
Window History
""", unsafe_allow_html=True) col_wh1, col_wh2 = st.columns([2, 1]) with col_wh1: hist_df = windows_df.copy() hist_df["time"] = pd.to_datetime(hist_df["created_at"], unit="s").dt.strftime("%H:%M:%S") hist_df["change"] = hist_df["price_change_pct"].apply(lambda x: f"{x:+.4f}%") hist_df["start"] = hist_df["start_price"].apply(lambda x: f"${x:,.2f}") hist_df["end"] = hist_df["end_price"].apply(lambda x: f"${x:,.2f}") hist_df["market"] = hist_df["market_condition_id"].apply(lambda x: x[:12] + "..." if x else "—") display = hist_df[["time", "asset", "timeframe", "start", "end", "change", "market"]].head(30) st.dataframe(display, use_container_width=True, hide_index=True) with col_wh2: st.markdown("**Avg Price Change by Asset**") chart_data = windows_df[["asset", "price_change_pct"]].copy() chart_data["abs_change"] = chart_data["price_change_pct"].abs() pivot = chart_data.groupby("asset")["abs_change"].mean() st.bar_chart(pivot, color="#a78bfa") st.markdown("**Windows by Asset**") counts = windows_df["asset"].value_counts() st.bar_chart(counts, color="#60a5fa") # ================================================================== # Section 7: Daily Summary # ================================================================== if not daily_df.empty: st.markdown("""
📅
Daily Summary
""", unsafe_allow_html=True) st.dataframe(daily_df, use_container_width=True, hide_index=True) # ================================================================== # Sidebar stats # ================================================================== st.sidebar.markdown("---") st.sidebar.markdown('', unsafe_allow_html=True) if not balance_df.empty: sb_bal = balance_df.iloc[-1]["balance"] sb_pnl = balance_df.iloc[-1]["pnl"] sb_icon = "🟢" if sb_pnl >= 0 else "🔴" st.sidebar.markdown(f'', unsafe_allow_html=True) st.sidebar.markdown(f"{sb_icon} PnL: **${sb_pnl:+,.2f}**") else: st.sidebar.markdown("Balance: **—**") st.sidebar.markdown("---") st.sidebar.markdown('', unsafe_allow_html=True) st.sidebar.code(str(db_path.name), language=None) st.sidebar.markdown(f"Windows: **{len(windows_df)}**") st.sidebar.markdown(f"Trades: **{len(trades_df)}**") st.sidebar.markdown(f"Snapshots: **{len(balance_df)}**") # ------------------------------------------------------------------ # Auto-refresh # ------------------------------------------------------------------ if auto_refresh: time.sleep(refresh_rate) st.rerun()