update 03-22 09:28

This commit is contained in:
2026-03-22 09:28:14 +09:00
commit 7f45211276
43 changed files with 9373 additions and 0 deletions

935
dashboard/app.py Normal file
View File

@@ -0,0 +1,935 @@
"""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("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
/* Global */
.stApp { background: #0a0e17; color: #e2e8f0; }
[data-testid="stHeader"] { background: transparent; }
[data-testid="stSidebar"] { background: #0f1420; border-right: 1px solid #1e293b; }
.block-container { padding-top: 1rem; max-width: 1400px; }
/* Hide default streamlit elements */
#MainMenu { visibility: hidden; }
footer { visibility: hidden; }
.stDeployButton { display: none; }
/* Typography */
h1, h2, h3, h4 { font-family: 'Inter', sans-serif !important; color: #f1f5f9 !important; }
/* Header bar */
.header-bar {
background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 50%, #0f172a 100%);
border: 1px solid #312e81;
border-radius: 16px;
padding: 1.5rem 2rem;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-title {
font-family: 'Inter', sans-serif;
font-size: 1.8rem;
font-weight: 800;
background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.02em;
}
.header-subtitle {
font-family: 'Inter', sans-serif;
color: #94a3b8;
font-size: 0.85rem;
margin-top: 4px;
}
.header-badge {
background: rgba(34, 197, 94, 0.15);
border: 1px solid rgba(34, 197, 94, 0.3);
color: #4ade80;
padding: 6px 14px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
font-family: 'Inter', sans-serif;
}
.header-badge-warn {
background: rgba(251, 191, 36, 0.15);
border: 1px solid rgba(251, 191, 36, 0.3);
color: #fbbf24;
}
/* Metric Cards */
.metric-card {
background: linear-gradient(145deg, #111827, #1e293b);
border: 1px solid #1e293b;
border-radius: 14px;
padding: 1.2rem 1.4rem;
position: relative;
overflow: hidden;
transition: border-color 0.2s;
}
.metric-card:hover { border-color: #334155; }
.metric-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
border-radius: 14px 14px 0 0;
}
.metric-card.accent-blue::before { background: linear-gradient(90deg, #3b82f6, #60a5fa); }
.metric-card.accent-green::before { background: linear-gradient(90deg, #22c55e, #4ade80); }
.metric-card.accent-red::before { background: linear-gradient(90deg, #ef4444, #f87171); }
.metric-card.accent-purple::before { background: linear-gradient(90deg, #8b5cf6, #a78bfa); }
.metric-card.accent-amber::before { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
.metric-card.accent-cyan::before { background: linear-gradient(90deg, #06b6d4, #22d3ee); }
.metric-label {
font-family: 'Inter', sans-serif;
font-size: 0.75rem;
font-weight: 500;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 8px;
}
.metric-value {
font-family: 'JetBrains Mono', monospace;
font-size: 1.7rem;
font-weight: 700;
line-height: 1.2;
}
.metric-sub {
font-family: 'Inter', sans-serif;
font-size: 0.78rem;
color: #64748b;
margin-top: 6px;
}
.green { color: #4ade80; }
.red { color: #f87171; }
.blue { color: #60a5fa; }
.purple { color: #a78bfa; }
.amber { color: #fbbf24; }
.cyan { color: #22d3ee; }
.white { color: #f1f5f9; }
/* Section headers */
.section-header {
font-family: 'Inter', sans-serif;
font-size: 1.1rem;
font-weight: 700;
color: #e2e8f0;
margin: 1.8rem 0 1rem 0;
padding-bottom: 0.5rem;
border-bottom: 1px solid #1e293b;
display: flex;
align-items: center;
gap: 10px;
}
.section-icon {
width: 28px;
height: 28px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
}
.section-icon.blue { background: rgba(59,130,246,0.15); }
.section-icon.green { background: rgba(34,197,94,0.15); }
.section-icon.purple { background: rgba(139,92,246,0.15); }
.section-icon.amber { background: rgba(245,158,11,0.15); }
/* Price cards */
.price-card {
background: linear-gradient(145deg, #111827, #1e293b);
border: 1px solid #1e293b;
border-radius: 14px;
padding: 1.2rem;
text-align: center;
}
.price-card:hover { border-color: #334155; }
.price-asset {
font-family: 'Inter', sans-serif;
font-size: 0.8rem;
font-weight: 600;
color: #94a3b8;
letter-spacing: 0.05em;
}
.price-value {
font-family: 'JetBrains Mono', monospace;
font-size: 1.5rem;
font-weight: 700;
color: #f1f5f9;
margin: 6px 0;
}
.price-change {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
font-weight: 600;
}
/* Window tracker */
.window-card {
background: #111827;
border: 1px solid #1e293b;
border-radius: 12px;
padding: 1rem 1.2rem;
margin-bottom: 0.6rem;
}
.window-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.window-asset-badge {
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
font-weight: 600;
padding: 3px 10px;
border-radius: 6px;
letter-spacing: 0.03em;
}
.badge-5m { background: rgba(59,130,246,0.15); color: #60a5fa; border: 1px solid rgba(59,130,246,0.3); }
.badge-15m { background: rgba(139,92,246,0.15); color: #a78bfa; border: 1px solid rgba(139,92,246,0.3); }
.window-prices {
display: flex;
justify-content: space-between;
font-family: 'JetBrains Mono', monospace;
font-size: 0.82rem;
color: #94a3b8;
}
.window-bar-bg {
background: #1e293b;
border-radius: 4px;
height: 5px;
margin-top: 8px;
overflow: hidden;
}
.window-bar-fill {
height: 5px;
border-radius: 4px;
transition: width 0.3s;
}
/* Connection dots */
.conn-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid rgba(30,41,59,0.5);
font-family: 'Inter', sans-serif;
font-size: 0.85rem;
}
.conn-item:last-child { border-bottom: none; }
.conn-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.conn-dot.on { background: #4ade80; box-shadow: 0 0 8px rgba(74,222,128,0.4); }
.conn-dot.off { background: #f87171; box-shadow: 0 0 8px rgba(248,113,113,0.4); }
.conn-dot.warn { background: #fbbf24; box-shadow: 0 0 8px rgba(251,191,36,0.4); }
.conn-name { color: #e2e8f0; font-weight: 500; }
.conn-status { color: #64748b; font-size: 0.78rem; margin-left: auto; }
/* Table styling */
[data-testid="stDataFrame"] {
border-radius: 12px;
overflow: hidden;
}
[data-testid="stDataFrame"] table {
font-family: 'JetBrains Mono', monospace !important;
font-size: 0.82rem !important;
}
/* Chart styling */
[data-testid="stVegaLiteChart"] {
background: #111827;
border: 1px solid #1e293b;
border-radius: 12px;
padding: 0.8rem;
}
/* Empty state */
.empty-state {
background: linear-gradient(145deg, #111827, #1e293b);
border: 1px dashed #334155;
border-radius: 14px;
padding: 3rem 2rem;
text-align: center;
}
.empty-icon { font-size: 2.5rem; margin-bottom: 1rem; }
.empty-title {
font-family: 'Inter', sans-serif;
font-size: 1.1rem;
font-weight: 600;
color: #e2e8f0;
margin-bottom: 0.5rem;
}
.empty-desc {
font-family: 'Inter', sans-serif;
font-size: 0.85rem;
color: #64748b;
line-height: 1.6;
}
/* Sidebar */
.sidebar-section {
background: #111827;
border: 1px solid #1e293b;
border-radius: 12px;
padding: 1rem;
margin-bottom: 1rem;
}
.sidebar-label {
font-family: 'Inter', sans-serif;
font-size: 0.7rem;
font-weight: 600;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 8px;
}
.sidebar-value {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
color: #e2e8f0;
font-weight: 500;
}
/* Hide streamlit metric delta arrows */
[data-testid="stMetricDelta"] { display: none; }
</style>
""", 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('<div class="sidebar-label">REFRESH</div>', 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"""
<div class="header-bar">
<div>
<div class="header-title">Polymarket Temporal Arb</div>
<div class="header-subtitle">Real-time arbitrage monitoring &bull; {mode_label} MODE</div>
</div>
<div style="display:flex; gap:10px; align-items:center">
<span class="{status_class}">{status_text}</span>
<span style="color:#64748b; font-size:0.78rem; font-family:'Inter',sans-serif">
{datetime.now(timezone.utc).strftime('%H:%M:%S UTC')}
</span>
</div>
</div>
""", 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"""
<div class="metric-card accent-blue">
<div class="metric-label">Balance</div>
<div class="metric-value white">${current_balance:,.2f}</div>
<div class="metric-sub">Start: ${starting_balance:,.2f}</div>
</div>
""", unsafe_allow_html=True)
with c2:
st.markdown(f"""
<div class="metric-card accent-{'green' if current_pnl >= 0 else 'red'}">
<div class="metric-label">Total PnL</div>
<div class="metric-value {pnl_color}">{pnl_sign}${current_pnl:,.2f}</div>
<div class="metric-sub">{pnl_sign}{pnl_pct:.2f}% return</div>
</div>
""", unsafe_allow_html=True)
with c3:
wr_color = "green" if win_rate >= 50 else "amber" if win_rate >= 40 else "red"
st.markdown(f"""
<div class="metric-card accent-purple">
<div class="metric-label">Win Rate</div>
<div class="metric-value {wr_color}">{win_rate:.1f}%</div>
<div class="metric-sub">{wins}W / {losses}L ({resolved_count} resolved)</div>
</div>
""", unsafe_allow_html=True)
with c4:
avg_bet = f"${total_bet/total_trades:,.0f}" if total_trades > 0 else "$0"
st.markdown(f"""
<div class="metric-card accent-cyan">
<div class="metric-label">Total Bets</div>
<div class="metric-value white">${total_bet:,.0f}</div>
<div class="metric-sub">{total_trades} trades &bull; avg {avg_bet}/trade</div>
</div>
""", unsafe_allow_html=True)
with c5:
st.markdown(f"""
<div class="metric-card accent-amber">
<div class="metric-label">Active Exposure</div>
<div class="metric-value amber">${active_exposure:,.0f}</div>
<div class="metric-sub">{len(open_df)} open positions</div>
</div>
""", 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"""
<div class="metric-card accent-blue">
<div class="metric-label">PnL / Trade</div>
<div class="metric-value {'green' if pnl_per_trade >= 0 else 'red'}">{'+' if pnl_per_trade >= 0 else ''}${pnl_per_trade:.2f}</div>
<div class="metric-sub">Avg edge: {avg_edge:.1f}%</div>
</div>
""", unsafe_allow_html=True)
else:
st.markdown("""
<div class="empty-state">
<div class="empty-icon">📡</div>
<div class="empty-title">Awaiting Balance Data</div>
<div class="empty-desc">Bot is initializing. Balance snapshots will appear once trading begins.</div>
</div>
""", unsafe_allow_html=True)
# ==================================================================
# Section 2: Live Prices + Connections
# ==================================================================
st.markdown("""
<div class="section-header">
<div class="section-icon blue">📡</div>
Market Data & Connections
</div>
""", 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"""
<div class="price-card" style="text-align:left; padding:1rem 1.2rem">
<div class="conn-item">
<div class="conn-dot {'on' if binance_ok else 'warn'}"></div>
<span class="conn-name">Binance WebSocket</span>
<span class="conn-status">{'Live' if binance_ok else 'Reconnecting'}</span>
</div>
<div class="conn-item">
<div class="conn-dot {'on' if poly_ok else 'warn'}"></div>
<span class="conn-name">Polymarket CLOB</span>
<span class="conn-status">{'Active' if poly_ok else 'Scanning'}</span>
</div>
<div class="conn-item">
<div class="conn-dot {'on' if not oracle_df.empty else 'warn'}"></div>
<span class="conn-name">Chainlink Oracle</span>
<span class="conn-status">{'Polling' if not oracle_df.empty else 'Waiting'}</span>
</div>
<div class="conn-item">
<div class="conn-dot on"></div>
<span class="conn-name">SQLite Database</span>
<span class="conn-status">{len(windows_df)} snapshots</span>
</div>
<div style="margin-top:8px; font-size:0.75rem; color:#475569; font-family:'Inter',sans-serif">
Last update: {last_update}
</div>
</div>
""", 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"""
<div class="price-card">
<div class="price-asset">{asset_icons[asset]} {asset}/USDT</div>
<div class="price-value">{fmt_price(price)}</div>
<div class="price-change {color}">{arrow} {change:+.4f}%</div>
</div>
""", unsafe_allow_html=True)
# ==================================================================
# Section 2b: Chainlink Oracle vs Binance
# ==================================================================
if not oracle_df.empty:
st.markdown("""
<div class="section-header">
<div class="section-icon purple">🔗</div>
Chainlink Oracle vs Binance (Arb Gap)
</div>
""", 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"""
<div class="metric-card accent-purple">
<div style="display:flex; justify-content:space-between; align-items:center">
<div class="metric-label">{asset} Oracle Gap</div>
<span style="color:{gap_color}; font-size:0.7rem; font-weight:700; font-family:'Inter',sans-serif">{gap_label}</span>
</div>
<div class="metric-value {dev_color}">{dev_sign}{dev:.4f}%</div>
<div style="display:flex; justify-content:space-between; margin-top:8px">
<div>
<div style="font-size:0.65rem; color:#475569; font-family:'Inter',sans-serif">CHAINLINK</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:0.85rem; color:#a78bfa">{fmt_price(o_price)}</div>
</div>
<div style="text-align:right">
<div style="font-size:0.65rem; color:#475569; font-family:'Inter',sans-serif">BINANCE</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:0.85rem; color:#60a5fa">{fmt_price(c_price)}</div>
</div>
</div>
<div class="metric-sub">Oracle lag: {lag:.0f}s</div>
</div>
""", 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("""
<div class="section-header">
<div class="section-icon green">⏱</div>
Active Windows
</div>
""", 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"""
<div class="window-card">
<div class="window-header">
<span style="font-family:'JetBrains Mono',monospace; font-weight:700; color:#e2e8f0; font-size:0.95rem">{asset}</span>
<div style="display:flex; gap:8px; align-items:center">
<span class="window-asset-badge {badge_class}">{tf}</span>
<span class="{color}" style="font-family:'JetBrains Mono',monospace; font-weight:600; font-size:0.85rem">{direction}</span>
</div>
</div>
<div class="window-prices">
<span>Open: {fmt_price(start_p)}</span>
<span>Now: {fmt_price(end_p)}</span>
<span class="{color}" style="font-weight:600">{change:+.4f}%</span>
</div>
<div class="window-bar-bg">
<div class="window-bar-fill" style="width:{bar_width}%; background:{bar_color}"></div>
</div>
<div style="text-align:right; margin-top:4px; font-family:'JetBrains Mono',monospace; font-size:0.72rem; color:#64748b">{remaining:.0f}s remaining</div>
</div>
""", 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("""
<div class="section-header">
<div class="section-icon purple">📈</div>
Performance Charts
</div>
""", 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("""
<div class="section-header">
<div class="section-icon green">🎯</div>
Active Positions
</div>
""", 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("""
<div class="section-header">
<div class="section-icon amber">⚡</div>
Trading Activity
</div>
""", 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("""
<div class="empty-state">
<div class="empty-icon">⚡</div>
<div class="empty-title">Scanning for Opportunities</div>
<div class="empty-desc">
The bot is monitoring CEX prices and scanning Polymarket markets.<br>
Trades will appear here once arbitrage opportunities are detected and executed.
</div>
</div>
""", unsafe_allow_html=True)
# ==================================================================
# Section 6: Window History
# ==================================================================
if not windows_df.empty:
st.markdown("""
<div class="section-header">
<div class="section-icon blue">🕐</div>
Window History
</div>
""", 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("""
<div class="section-header">
<div class="section-icon green">📅</div>
Daily Summary
</div>
""", unsafe_allow_html=True)
st.dataframe(daily_df, use_container_width=True, hide_index=True)
# ==================================================================
# Sidebar stats
# ==================================================================
st.sidebar.markdown("---")
st.sidebar.markdown('<div class="sidebar-label">ACCOUNT</div>', 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'<div class="sidebar-value">${sb_bal:,.2f}</div>', 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('<div class="sidebar-label">DATABASE</div>', 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()