Jimin Huang
Change settings
5361a44
import os
import math
from datetime import datetime
from dateutil import parser as dateparser
import pandas as pd
import numpy as np
import gradio as gr
import matplotlib.pyplot as plt
from supabase import create_client, Client
# --- Config ---
SUPABASE_URL = os.environ.get("SUPABASE_URL", "")
SUPABASE_ANON_KEY = os.environ.get("SUPABASE_ANON_KEY", "")
DEFAULT_MAX_ROWS = int(os.environ.get("DEFAULT_MAX_ROWS", "10000"))
if not SUPABASE_URL or not SUPABASE_ANON_KEY:
print("WARNING: SUPABASE_URL / SUPABASE_ANON_KEY not set. App will show a banner.")
def sb_client() -> Client:
return create_client(SUPABASE_URL, SUPABASE_ANON_KEY)
# --- Data fetch ---
def fetch_decisions(limit=DEFAULT_MAX_ROWS, filters=None):
"""
Fetch trading decisions from Supabase.
Filters is a dict with optional keys: agent_name, asset, model, start_date, end_date.
"""
filters = filters or {}
supabase = sb_client()
q = supabase.table("trading_decisions").select("*")
if filters.get("agent_name"):
q = q.eq("agent_name", filters["agent_name"])
if filters.get("asset"):
q = q.eq("asset", filters["asset"])
if filters.get("model"):
q = q.eq("model", filters["model"])
if filters.get("start_date"):
q = q.gte("date", filters["start_date"])
if filters.get("end_date"):
q = q.lte("date", filters["end_date"])
# Order by date ascending for time-series correctness
q = q.order("date", desc=False)
# Pull up to limit
data = q.limit(limit).execute().data
df = pd.DataFrame(data or [])
if not df.empty:
# Normalize types
df["date"] = pd.to_datetime(df["date"], errors="coerce", utc=True).dt.tz_convert(None)
df = df.sort_values("date")
return df
# --- Strategy logic ---
class StrategyConfig:
def __init__(self, long_only=True, aggressive=False, fee=0.0005):
self.long_only = long_only # if True, no SHORT positions
self.aggressive = aggressive # if True, HOLD = flatten; BUY/SELL switch directly
self.fee = float(fee)
def simulate_equity(rows: pd.DataFrame, cfg: StrategyConfig):
"""
Simulate equity curve given rows with columns: date, price, recommended_action
Returns: equity DataFrame with columns [date, equity], plus stats dict.
"""
if rows.empty:
return pd.DataFrame(columns=["date", "equity"]), {"trades": 0, "win_rate": 0.0, "ret_total": 0.0, "ret_bh": 0.0, "ret_vs_bh": 0.0, "sharpe_daily": 0.0}
dates = rows["date"].tolist()
prices = rows["price"].astype(float).tolist()
actions = rows["recommended_action"].fillna("HOLD").str.upper().tolist()
equity = []
capital = 1.0
fee = cfg.fee
position = "FLAT"
entry_price = None
trades = []
last_equity = capital
# Buy & Hold baseline
p0 = prices[0]
bh_equity = [1.0 * (p / p0) for p in prices]
# Returns series for sharpe (daily-ish)
eq_series = []
for i, (dt, price, act) in enumerate(zip(dates, prices, actions)):
# Normalize action
if act not in ("BUY","SELL","HOLD"):
act = "HOLD"
# Aggressive logic: HOLD => flatten
if cfg.aggressive and act == "HOLD" and position != "FLAT":
# close pos
if position == "LONG":
capital *= (price / entry_price) * (1 - fee)
elif position == "SHORT":
capital *= (entry_price / price) * (1 - fee)
trades.append({"entry": entry_price, "exit": price, "dir": position})
position, entry_price = "FLAT", None
if act == "BUY":
if position == "FLAT":
position = "LONG"
entry_price = price
capital *= (1 - fee)
elif position == "SHORT":
# close SHORT, open LONG (if aggressive) or ignore (if baseline)
# In both modes, we interpret a BUY while short as closing short then long
capital *= (entry_price / price) * (1 - fee)
trades.append({"entry": entry_price, "exit": price, "dir": "SHORT"})
if cfg.long_only:
position, entry_price = "FLAT", None
else:
position, entry_price = "LONG", price
capital *= (1 - fee)
else:
# already LONG: no change
pass
elif act == "SELL":
if cfg.long_only:
# In long-only, SELL means close long if any
if position == "LONG":
capital *= (price / entry_price) * (1 - fee)
trades.append({"entry": entry_price, "exit": price, "dir": "LONG"})
position, entry_price = "FLAT", None
else:
if position == "FLAT":
position = "SHORT"
entry_price = price
capital *= (1 - fee)
elif position == "LONG":
# close LONG, open SHORT
capital *= (price / entry_price) * (1 - fee)
trades.append({"entry": entry_price, "exit": price, "dir": "LONG"})
position, entry_price = "SHORT", price
capital *= (1 - fee)
else:
# already SHORT
pass
# HOLD in non-aggressive does nothing
equity.append(capital)
eq_series.append(capital)
# At end, mark-to-market (position still open)
if position != "FLAT" and entry_price is not None:
last_price = prices[-1]
if position == "LONG":
mtm = capital * (last_price / entry_price)
else:
mtm = capital * (entry_price / last_price)
equity[-1] = mtm
eq_df = pd.DataFrame({"date": dates, "equity": equity})
ret_total = (eq_df["equity"].iloc[-1] / eq_df["equity"].iloc[0]) - 1.0 if len(eq_df) > 1 else 0.0
ret_bh = (bh_equity[-1] / bh_equity[0]) - 1.0 if len(bh_equity) > 1 else 0.0
ret_vs_bh = ret_total - ret_bh
# Sharpe (daily-ish): simple approximation using equity pct change
eq_series = np.array(eq_series)
if len(eq_series) > 1:
rets = np.diff(eq_series) / eq_series[:-1]
sharpe_daily = (np.mean(rets) / (np.std(rets) + 1e-9)) * np.sqrt(252)
else:
sharpe_daily = 0.0
# Win rate
wins = 0
for t in trades:
if t["dir"] == "LONG":
wins += 1 if t["exit"] > t["entry"] else 0
else:
wins += 1 if t["exit"] < t["entry"] else 0
win_rate = (wins / len(trades)) if trades else 0.0
stats = {
"trades": len(trades),
"win_rate": win_rate,
"ret_total": ret_total,
"ret_bh": ret_bh,
"ret_vs_bh": ret_vs_bh,
"sharpe_daily": float(sharpe_daily),
}
return eq_df, stats
# --- Plotting ---
def plot_equities(main_eq: pd.DataFrame, bh_eq: pd.DataFrame, title: str = "Equity Curve"):
fig = plt.figure(figsize=(8,4.5))
ax = fig.gca()
ax.plot(main_eq["date"], main_eq["equity"], label="Strategy")
ax.plot(bh_eq["date"], bh_eq["equity"], label="Buy & Hold", linestyle="--")
ax.set_title(title)
ax.set_xlabel("Date")
ax.set_ylabel("Equity (normalized)")
ax.legend()
ax.grid(True, alpha=0.3)
return fig
def build_bh(df: pd.DataFrame):
if df.empty:
return pd.DataFrame(columns=["date","equity"])
prices = df["price"].astype(float).values
base = prices[0]
eq = prices / base
return pd.DataFrame({"date": df["date"].values, "equity": eq})
# --- UI Handlers ---
def list_unique(column):
try:
df = fetch_decisions(limit=1000)
if df.empty or column not in df:
return []
vals = df[column].dropna().unique().tolist()
return sorted([v for v in vals if isinstance(v, str)])
except Exception:
return []
def run_query(agent, asset, model, start, end, long_only, aggressive, fee, limit_rows):
if not SUPABASE_URL or not SUPABASE_ANON_KEY:
banner = "โš ๏ธ Missing SUPABASE_URL or SUPABASE_ANON_KEY. Set them in Space Secrets."
else:
banner = ""
filters = {}
if agent: filters["agent_name"] = agent
if asset: filters["asset"] = asset
if model: filters["model"] = model
if start: filters["start_date"] = start
if end: filters["end_date"] = end
df = fetch_decisions(limit=limit_rows, filters=filters)
if df.empty:
return banner or "No data found for the selected filters.", None, pd.DataFrame(), pd.DataFrame()
# Simulate
cfg = StrategyConfig(long_only=long_only, aggressive=aggressive, fee=fee)
eq_df, stats = simulate_equity(df[["date","price","recommended_action"]], cfg)
bh_df = build_bh(df)
# Plot
fig = plot_equities(eq_df, bh_df, title="Equity Curve")
# Metrics table
metrics = pd.DataFrame([{
"Trades": stats["trades"],
"Win Rate": f"{stats['win_rate']*100:.1f}%",
"Total Return": f"{stats['ret_total']*100:.1f}%",
"Buy&Hold Return": f"{stats['ret_bh']*100:.1f}%",
"Excess vs B&H": f"{stats['ret_vs_bh']*100:.1f}%",
"Sharpe (daily)": f"{stats['sharpe_daily']:.2f}",
"Rows Used": len(df)
}])
return banner, fig, df[["date","agent_name","asset","model","price","recommended_action"]].tail(25), metrics
with gr.Blocks(title="Paper Trading Agents") as demo:
gr.Markdown("# ๐Ÿ“ˆ Paper Trading Agents\nVisualize agent decisions from Supabase and compute strategy equity vs buy&hold.\n")
with gr.Row():
agent = gr.Dropdown(choices=[], label="Agent Name (optional)", interactive=True)
asset = gr.Dropdown(choices=[], label="Asset (optional)", interactive=True)
model = gr.Dropdown(choices=[], label="Model (optional)", interactive=True)
with gr.Row():
start = gr.Textbox(label="Start Date (YYYY-MM-DD, optional)")
end = gr.Textbox(label="End Date (YYYY-MM-DD, optional)")
limit_rows = gr.Slider(1000, 50000, value=DEFAULT_MAX_ROWS, step=500, label="Max rows")
with gr.Row():
long_only = gr.Checkbox(value=True, label="Long Only")
aggressive = gr.Checkbox(value=False, label="Aggressive Mode (HOLD = flatten; BUY/SELL switch)")
fee = gr.Number(value=0.0005, label="Fee (per open/close)")
go = gr.Button("Run")
banner = gr.Markdown()
plot = gr.Plot()
tail = gr.Dataframe(headers=["date","agent_name","asset","model","price","recommended_action"], label="Sample of latest rows", wrap=True)
metrics = gr.Dataframe(label="Metrics", wrap=True)
def _init_choices():
return gr.update(choices=list_unique("agent_name")), gr.update(choices=list_unique("asset")), gr.update(choices=list_unique("model"))
demo.load(_init_choices, None, [agent, asset, model])
go.click(run_query, inputs=[agent, asset, model, start, end, long_only, aggressive, fee, limit_rows], outputs=[banner, plot, tail, metrics])
if __name__ == "__main__":
demo.launch()