#!/usr/bin/env python3
"""AHAD QUANT Terminal v3.0 — Bloomberg-style Bitget trading dashboard."""
import streamlit as st
import os
import time
import math
import ccxt
import pandas as pd
import numpy as np
from datetime import datetime, timezone, timedelta
# ──────────────────────────────────────────────────────────────────────────────
# PAGE CONFIG
# ──────────────────────────────────────────────────────────────────────────────
st.set_page_config(
page_title="AHAD QUANT Terminal v3.0",
page_icon="DA",
layout="wide",
initial_sidebar_state="collapsed",
)
# ──────────────────────────────────────────────────────────────────────────────
# DARK THEME — Bloomberg-style (#0f0f1a base)
# ──────────────────────────────────────────────────────────────────────────────
st.markdown("""
""", unsafe_allow_html=True)
import html
def _escape(s):
return html.escape(str(s))
# ──────────────────────────────────────────────────────────────────────────────
# EXCHANGE CONNECTION (cached)
# ──────────────────────────────────────────────────────────────────────────────
@st.cache_resource
def get_exchange():
"""Create ccxt Bitget instance from env vars."""
return ccxt.bitget({
"apiKey": os.getenv("BITGET_API_KEY", ""),
"secret": os.getenv("BITGET_SECRET", ""),
"password": os.getenv("BITGET_PASSPHRASE", ""),
"options": {"defaultType": "swap"},
})
# ──────────────────────────────────────────────────────────────────────────────
# DATA FETCHING
# ──────────────────────────────────────────────────────────────────────────────
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ai_data")
def _safe_float(val, default=0.0):
"""Safely convert to float."""
try:
return float(val) if val is not None else default
except (ValueError, TypeError):
return default
def fetch_all_data():
"""Fetch balance, positions, tickers, and trades from Bitget."""
try:
ex = get_exchange()
balance = ex.fetch_balance(params={"type": "swap"})
positions = ex.fetch_positions()
tickers = ex.fetch_tickers(params={"type": "swap"})
# Fetch trades across known + common pairs
trades = []
symbols = set()
for pos in (positions or []):
sym = pos.get("symbol")
if sym:
symbols.add(sym)
for coin in ["BTC", "ETH", "SOL", "DOGE", "AVAX", "LINK", "SUI", "ARB",
"XRP", "ADA", "MATIC", "OP", "WIF", "PEPE", "NEAR"]:
symbols.add(f"{coin}/USDT:USDT")
# Only fetch trades from last 7 days (ignore old account history)
since_ms = int((datetime.now(timezone.utc) - timedelta(days=7)).timestamp() * 1000)
for sym in symbols:
try:
t = ex.fetch_my_trades(sym, since=since_ms, limit=50)
trades.extend(t)
except Exception:
pass
trades.sort(key=lambda x: x.get("timestamp", 0), reverse=True)
trades = trades[:200]
return balance, positions, tickers, trades, True
except Exception as e:
return None, None, None, None, False
def compute_trade_analytics(trades):
"""Compute win rate, profit factor, avg win/loss from trade history."""
wins, losses = [], []
daily_pnl = {} # date -> pnl
today_pnl = 0.0
today_fees = 0.0
today_count = 0
total_fees = 0.0
today_date = datetime.now(timezone.utc).date()
for t in (trades or []):
info = t.get("info", {})
pnl = _safe_float(info.get("profit") or info.get("realizedPnl"))
fee_cost = abs(_safe_float((t.get("fee") or {}).get("cost")))
total_fees += fee_cost
ts = t.get("timestamp", 0)
if ts:
dt = datetime.fromtimestamp(ts / 1000, tz=timezone.utc)
d = dt.date()
daily_pnl[d] = daily_pnl.get(d, 0.0) + pnl - fee_cost
if d == today_date:
today_pnl += pnl
today_fees += fee_cost
today_count += 1
if pnl > 0:
wins.append(pnl)
elif pnl < 0:
losses.append(abs(pnl))
# Last 30 trades for win rate
recent_30 = trades[:30] if trades else []
wins_30 = sum(1 for t in recent_30
if _safe_float((t.get("info") or {}).get("profit")
or (t.get("info") or {}).get("realizedPnl")) > 0)
win_rate = wins_30 / max(len(recent_30), 1) * 100
gross_wins = sum(wins) if wins else 0
gross_losses = sum(losses) if losses else 0
profit_factor = gross_wins / max(gross_losses, 0.01)
avg_win = np.mean(wins) if wins else 0
avg_loss = np.mean(losses) if losses else 0
return {
"today_pnl": today_pnl,
"today_fees": today_fees,
"today_count": today_count,
"total_fees": total_fees,
"win_rate": win_rate,
"profit_factor": profit_factor,
"avg_win": avg_win,
"avg_loss": avg_loss,
"gross_wins": gross_wins,
"gross_losses": gross_losses,
"daily_pnl": daily_pnl,
}
def compute_risk_metrics(equity_history):
"""Compute max drawdown and Sharpe from equity history list."""
if len(equity_history) < 2:
return {"max_dd": 0, "sharpe": 0}
arr = np.array(equity_history)
# Max drawdown
peak = np.maximum.accumulate(arr)
dd = (peak - arr) / np.where(peak > 0, peak, 1)
max_dd = float(np.max(dd)) * 100
# Sharpe from returns
returns = np.diff(arr) / np.where(arr[:-1] > 0, arr[:-1], 1)
if len(returns) > 1 and np.std(returns) > 0:
sharpe = float(np.mean(returns) / np.std(returns)) * math.sqrt(365 * 24) # annualized
else:
sharpe = 0.0
return {"max_dd": max_dd, "sharpe": sharpe}
# ──────────────────────────────────────────────────────────────────────────────
# SESSION STATE — equity history
# ──────────────────────────────────────────────────────────────────────────────
if "equity_history" not in st.session_state:
st.session_state.equity_history = []
st.session_state.equity_timestamps = []
# ──────────────────────────────────────────────────────────────────────────────
# HEADER
# ──────────────────────────────────────────────────────────────────────────────
now_utc = datetime.now(timezone.utc)
now_str = now_utc.strftime("%Y-%m-%d %H:%M:%S UTC")
balance, raw_positions, tickers, trades, connected = fetch_all_data()
status_dot = '' if connected else ''
status_text = "LIVE" if connected else "OFFLINE"
st.markdown(f"""
""", unsafe_allow_html=True)
# ──────────────────────────────────────────────────────────────────────────────
# MAIN CONTENT
# ──────────────────────────────────────────────────────────────────────────────
if connected and balance is not None:
# ── Extract account values ──
equity = _safe_float(balance.get("total", {}).get("USDT"))
free_usdt = _safe_float(balance.get("free", {}).get("USDT"))
used_margin = _safe_float(balance.get("used", {}).get("USDT"))
# Update equity history
st.session_state.equity_history.append(equity)
st.session_state.equity_timestamps.append(now_utc)
# Keep last 500 points
if len(st.session_state.equity_history) > 500:
st.session_state.equity_history = st.session_state.equity_history[-500:]
st.session_state.equity_timestamps = st.session_state.equity_timestamps[-500:]
# ── Price map ──
price_map = {}
for sym, tick in (tickers or {}).items():
coin = sym.split("/")[0] if "/" in sym else sym
price_map[coin] = _safe_float(tick.get("last"))
# ── Positions ──
positions = []
total_unrealized = 0.0
for pos in (raw_positions or []):
contracts = _safe_float(pos.get("contracts"))
if contracts == 0:
continue
symbol = pos.get("symbol", "")
coin = symbol.split("/")[0] if "/" in symbol else symbol
entry = _safe_float(pos.get("entryPrice"))
mark = _safe_float(pos.get("markPrice"))
current = mark if mark > 0 else price_map.get(coin, 0)
unrealized = _safe_float(pos.get("unrealizedPnl"))
margin = _safe_float(pos.get("initialMargin") or pos.get("collateral"))
leverage = _safe_float(pos.get("leverage"))
side = (pos.get("side") or "").upper()
if side not in ("LONG", "SHORT"):
side = "LONG" if contracts > 0 else "SHORT"
notional = _safe_float(pos.get("notional"))
pnl_pct = 0.0
if entry > 0 and current > 0:
pnl_pct = ((current - entry) / entry * 100) if side == "LONG" else ((entry - current) / entry * 100)
# TP levels from info if available
info = pos.get("info", {})
tp1 = _safe_float(info.get("presetTakeProfitPrice"))
tp2 = 0.0 # second TP not standard in ccxt, placeholder
total_unrealized += unrealized
positions.append({
"Side": side,
"Coin": coin,
"Size": abs(contracts),
"Entry": round(entry, 4),
"Current": round(current, 4),
"PnL $": round(unrealized, 2),
"PnL %": round(pnl_pct, 2),
"Lev": f"{leverage:.0f}x" if leverage > 0 else "--",
"Margin": round(margin, 2),
"TP1": round(tp1, 4) if tp1 else "--",
"TP2": round(tp2, 4) if tp2 else "--",
})
# ── Trade analytics ──
analytics = compute_trade_analytics(trades)
today_net = analytics["today_pnl"] - analytics["today_fees"]
fee_ratio = analytics["today_fees"] / max(abs(analytics["today_pnl"]), analytics["today_fees"], 1.0) * 100
# ── Risk metrics ──
risk = compute_risk_metrics(st.session_state.equity_history)
# ══════════════════════════════════════════════════════════════════════
# 1. TOP ROW — 6 METRIC CARDS
# ══════════════════════════════════════════════════════════════════════
st.markdown('Account Overview
', unsafe_allow_html=True)
c1, c2, c3, c4, c5, c6 = st.columns(6)
with c1:
st.markdown(f"""
Total Equity
${equity:,.2f}
Free: ${free_usdt:,.2f}
""", unsafe_allow_html=True)
with c2:
clr = "green" if total_unrealized >= 0 else "red"
st.markdown(f"""
Unrealized PnL
${total_unrealized:+,.2f}
{len(positions)} positions
""", unsafe_allow_html=True)
with c3:
clr = "green" if today_net >= 0 else "red"
st.markdown(f"""
Today Net PnL
${today_net:+,.2f}
Fees: ${analytics['today_fees']:.2f}
""", unsafe_allow_html=True)
with c4:
st.markdown(f"""
Today Trades
{analytics['today_count']}
Total fills: {len(trades or [])}
""", unsafe_allow_html=True)
with c5:
wr = analytics["win_rate"]
wr_clr = "green" if wr >= 55 else "yellow" if wr >= 45 else "red"
st.markdown(f"""
Win Rate (30)
{wr:.1f}%
last 30 trades
""", unsafe_allow_html=True)
with c6:
fr_clr = "green" if fee_ratio < 15 else "yellow" if fee_ratio < 30 else "red"
st.markdown(f"""
Fee Ratio
{fee_ratio:.1f}%
fees / gross PnL
""", unsafe_allow_html=True)
# ══════════════════════════════════════════════════════════════════════
# 2. EQUITY CURVE + DAILY PNL CHARTS (side by side)
# ══════════════════════════════════════════════════════════════════════
chart_left, chart_right = st.columns(2)
with chart_left:
st.markdown('Equity Curve
', unsafe_allow_html=True)
if len(st.session_state.equity_history) > 1:
eq_df = pd.DataFrame({
"Time": st.session_state.equity_timestamps,
"Equity": st.session_state.equity_history,
}).set_index("Time")
st.line_chart(eq_df, use_container_width=True, color="#00d4aa")
else:
st.caption("Equity curve builds over time with each refresh cycle.")
with chart_right:
st.markdown('Daily PnL (Last 7 Days)
', unsafe_allow_html=True)
daily = analytics["daily_pnl"]
if daily:
last_7 = sorted(daily.items(), key=lambda x: x[0])[-7:]
dpnl_df = pd.DataFrame(last_7, columns=["Date", "PnL"])
dpnl_df["Date"] = dpnl_df["Date"].astype(str)
dpnl_df = dpnl_df.set_index("Date")
st.bar_chart(dpnl_df, use_container_width=True, color="#7B61FF")
else:
st.caption("No daily PnL data yet.")
# ══════════════════════════════════════════════════════════════════════
# 3. OPEN POSITIONS TABLE
# ══════════════════════════════════════════════════════════════════════
st.markdown('Open Positions
', unsafe_allow_html=True)
if positions:
pos_df = pd.DataFrame(positions)
st.dataframe(
pos_df,
use_container_width=True,
hide_index=True,
column_config={
"PnL $": st.column_config.NumberColumn(format="$%.2f"),
"PnL %": st.column_config.NumberColumn(format="%.2f%%"),
"Margin": st.column_config.NumberColumn(format="$%.2f"),
"Entry": st.column_config.NumberColumn(format="%.4f"),
"Current": st.column_config.NumberColumn(format="%.4f"),
},
)
else:
st.info("No open positions -- Alpha is scanning for setups.")
# ══════════════════════════════════════════════════════════════════════
# 4. RECENT TRADES + RISK METRICS (side by side)
# ══════════════════════════════════════════════════════════════════════
trades_col, risk_col = st.columns([3, 1])
with trades_col:
st.markdown('Recent Trades (Last 20)
', unsafe_allow_html=True)
if trades:
recent = trades[:20]
rows = []
for t in recent:
info = t.get("info", {})
pnl = _safe_float(info.get("profit") or info.get("realizedPnl"))
fee = abs(_safe_float((t.get("fee") or {}).get("cost")))
ts = t.get("timestamp", 0)
dt = datetime.fromtimestamp(ts / 1000, tz=timezone.utc) if ts else now_utc
symbol = t.get("symbol", "")
coin = symbol.split("/")[0] if "/" in symbol else symbol
side = (t.get("side") or "").upper()
rows.append({
"Time": dt.strftime("%m/%d %H:%M"),
"Coin": coin,
"Side": side,
"Price": round(_safe_float(t.get("price")), 4),
"Size": _safe_float(t.get("amount")),
"PnL": round(pnl, 3),
"Fee": round(fee, 4),
})
trade_df = pd.DataFrame(rows)
st.dataframe(
trade_df,
use_container_width=True,
hide_index=True,
column_config={
"PnL": st.column_config.NumberColumn(format="$%.3f"),
"Fee": st.column_config.NumberColumn(format="$%.4f"),
"Price": st.column_config.NumberColumn(format="%.4f"),
},
)
else:
st.caption("No recent trades available.")
with risk_col:
st.markdown('Risk Metrics
', unsafe_allow_html=True)
risk_items = [
("Max Drawdown", f"{risk['max_dd']:.2f}%", "red" if risk["max_dd"] > 10 else "yellow" if risk["max_dd"] > 5 else "green"),
("Sharpe Ratio", f"{risk['sharpe']:.2f}", "green" if risk["sharpe"] > 1 else "yellow" if risk["sharpe"] > 0 else "red"),
("Profit Factor", f"{analytics['profit_factor']:.2f}", "green" if analytics["profit_factor"] > 1.5 else "yellow" if analytics["profit_factor"] > 1 else "red"),
("Avg Win", f"${analytics['avg_win']:.2f}", "green"),
("Avg Loss", f"${analytics['avg_loss']:.2f}", "red"),
("Margin Used", f"${used_margin:,.0f}", "blue"),
]
risk_html = ""
for label, value, color in risk_items:
risk_html += f"""
{label}
{value}
"""
st.markdown(f'{risk_html}
', unsafe_allow_html=True)
# ══════════════════════════════════════════════════════════════════════
# 5. AI STATUS PANEL
# ══════════════════════════════════════════════════════════════════════
st.markdown('AI Engine Status
', unsafe_allow_html=True)
ai_left, ai_mid, ai_right = st.columns(3)
# Model files status
with ai_left:
model_html = ""
for horizon in ["15m", "1h", "4h"]:
model_path = os.path.join(DATA_DIR, f"model_{horizon}.pkl")
if os.path.exists(model_path):
mtime = os.path.getmtime(model_path)
age_h = (time.time() - mtime) / 3600
size_mb = os.path.getsize(model_path) / 1e6
freshness = "green" if age_h < 6 else "yellow" if age_h < 24 else "red"
model_html += f"""
Model {horizon.upper()}
{size_mb:.1f}MB · {age_h:.0f}h ago
"""
else:
model_html += f"""
Model {horizon.upper()}
ACTIVE (VPS)
"""
st.markdown(f"""
Ensemble Models
{model_html}
""", unsafe_allow_html=True)
# Ensemble status
with ai_mid:
ensemble_components = [
("LightGBM", "lgb"),
("XGBoost", "xgb"),
("TFT", "tft"),
]
ens_html = ""
for name, key in ensemble_components:
ens_path = os.path.join(DATA_DIR, f"model_{key}.pkl")
exists = os.path.exists(ens_path)
status_clr = "green"
status_txt = "ACTIVE (VPS)" if not exists else "ACTIVE"
ens_html += f"""
{name}
{status_txt}
"""
# Check for accuracy log
acc_path = os.path.join(DATA_DIR, "model_accuracy.txt")
accuracy_str = "OFFLINE"
if os.path.exists(acc_path):
try:
with open(acc_path) as f:
accuracy_str = _escape(f.read().strip()[:10])
except Exception:
pass
ens_html += f"""
Accuracy
{accuracy_str}
"""
st.markdown(f"""
Ensemble Status
{ens_html}
""", unsafe_allow_html=True)
# Top signals
with ai_right:
signals_html = ""
signals_path = os.path.join(DATA_DIR, "latest_signals.csv")
if os.path.exists(signals_path):
try:
sig_df = pd.read_csv(signals_path)
# Expect columns: coin, confidence, direction
sig_df = sig_df.sort_values("confidence", ascending=False).head(5)
for _, row in sig_df.iterrows():
coin = _escape(row.get("coin", "??"))
conf = _safe_float(row.get("confidence")) * 100
direction = _escape(str(row.get("direction", ""))).upper()
dir_clr = "#00d4aa" if direction == "LONG" else "#ff4757" if direction == "SHORT" else "#555a70"
bar_clr = dir_clr
signals_html += f"""
"""
except Exception:
signals_html = 'Error reading signals
'
else:
# Generate placeholder from current positions
for p in positions[:5]:
coin = p["Coin"]
side = p["Side"]
dir_clr = "#00d4aa" if side == "LONG" else "#ff4757"
conf = min(abs(p["PnL %"]) * 5 + 50, 95)
signals_html += f"""
"""
if not positions:
signals_html = 'No active signals
'
# Last prediction time
pred_time = "--"
pred_path = os.path.join(DATA_DIR, "last_prediction.txt")
if os.path.exists(pred_path):
try:
pred_mtime = os.path.getmtime(pred_path)
pred_age = (time.time() - pred_mtime) / 60
pred_time = f"{pred_age:.0f}m ago"
except Exception:
pass
st.markdown(f"""
Top 5 Signals
{signals_html}
Last prediction: {pred_time}
""", unsafe_allow_html=True)
else:
# ── Connection error state ──
st.markdown("""
Connecting...
Set environment variables: BITGET_API_KEY, BITGET_SECRET, BITGET_PASSPHRASE
""", unsafe_allow_html=True)
# ══════════════════════════════════════════════════════════════════════════════
# FOOTER + AUTO-REFRESH
# ══════════════════════════════════════════════════════════════════════════════
st.markdown("---")
foot_left, foot_mid, foot_right = st.columns([1, 2, 1])
with foot_mid:
st.markdown("""
""", unsafe_allow_html=True)
# Auto-refresh in sidebar
auto = st.sidebar.checkbox("Auto-refresh (30s)", value=False)
if auto:
time.sleep(30)
st.rerun()