Jimin Huang commited on
Commit
1d5d022
Β·
1 Parent(s): bfdb6e9

Change settings

Browse files
Files changed (3) hide show
  1. README.md +25 -38
  2. app.py +289 -0
  3. requirements.txt +6 -0
README.md CHANGED
@@ -1,50 +1,37 @@
1
  ---
2
- title: Agent Market Arena
3
- emoji: 🐒
4
- colorFrom: purple
5
- colorTo: indigo
6
- sdk: static
7
  pinned: false
8
- app_build_command: npm run build
9
- app_file: dist/index.html
10
  ---
11
 
12
- # vue
13
 
14
- This template should help get you started developing with Vue 3 in Vite.
15
 
16
- ## Recommended IDE Setup
17
 
18
- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
 
19
 
20
- ## Type Support for `.vue` Imports in TS
21
 
22
- TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
23
 
24
- ## Customize configuration
 
 
 
 
 
 
 
 
25
 
26
- See [Vite Configuration Reference](https://vite.dev/config/).
27
 
28
- ## Project Setup
29
-
30
- ```sh
31
- npm install
32
- ```
33
-
34
- ### Compile and Hot-Reload for Development
35
-
36
- ```sh
37
- npm run dev
38
- ```
39
-
40
- ### Type-Check, Compile and Minify for Production
41
-
42
- ```sh
43
- npm run build
44
- ```
45
-
46
- ### Lint with [ESLint](https://eslint.org/)
47
-
48
- ```sh
49
- npm run lint
50
- ```
 
1
  ---
2
+ title: Paper Trading Agents (Gradio)
3
+ emoji: πŸ“ˆ
4
+ colorFrom: indigo
5
+ colorTo: teal
6
+ sdk: gradio
7
  pinned: false
8
+ license: apache-2.0
 
9
  ---
10
 
11
+ A Gradio app that visualizes paper-trading agent decisions from Supabase, computes equity curves & metrics, and compares against a buy-and-hold baseline.
12
 
13
+ ## Configure
14
 
15
+ Set the following **Secrets** in your Space (Settings β†’ Variables and secrets):
16
 
17
+ - `SUPABASE_URL`
18
+ - `SUPABASE_ANON_KEY`
19
 
20
+ Optionally, set `DEFAULT_MAX_ROWS` (default 10000).
21
 
22
+ ## Schema (expected)
23
 
24
+ Table: `trading_decisions`
25
+ - `id` (uuid/text)
26
+ - `agent_name` (text)
27
+ - `asset` (text)
28
+ - `model` (text)
29
+ - `date` (timestamp or text ISO)
30
+ - `price` (numeric)
31
+ - `recommended_action` (text: BUY | SELL | HOLD)
32
+ - `updated_at` (timestamp)
33
 
34
+ ## Notes
35
 
36
+ - No service-role keys are used; ensure RLS policies permit read access for your Space domain.
37
+ - Holiday-aware calendars can be added; currently the app treats all days as trading days and sorts by date.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import math
3
+ from datetime import datetime
4
+ from dateutil import parser as dateparser
5
+ import pandas as pd
6
+ import numpy as np
7
+ import gradio as gr
8
+ import matplotlib.pyplot as plt
9
+
10
+ from supabase import create_client, Client
11
+
12
+ # --- Config ---
13
+ SUPABASE_URL = os.environ.get("SUPABASE_URL", "")
14
+ SUPABASE_ANON_KEY = os.environ.get("SUPABASE_ANON_KEY", "")
15
+ DEFAULT_MAX_ROWS = int(os.environ.get("DEFAULT_MAX_ROWS", "10000"))
16
+
17
+ if not SUPABASE_URL or not SUPABASE_ANON_KEY:
18
+ print("WARNING: SUPABASE_URL / SUPABASE_ANON_KEY not set. App will show a banner.")
19
+
20
+ def sb_client() -> Client:
21
+ return create_client(SUPABASE_URL, SUPABASE_ANON_KEY)
22
+
23
+ # --- Data fetch ---
24
+ def fetch_decisions(limit=DEFAULT_MAX_ROWS, filters=None):
25
+ """
26
+ Fetch trading decisions from Supabase.
27
+ Filters is a dict with optional keys: agent_name, asset, model, start_date, end_date.
28
+ """
29
+ filters = filters or {}
30
+ supabase = sb_client()
31
+ q = supabase.table("trading_decisions").select("*")
32
+
33
+ if filters.get("agent_name"):
34
+ q = q.eq("agent_name", filters["agent_name"])
35
+ if filters.get("asset"):
36
+ q = q.eq("asset", filters["asset"])
37
+ if filters.get("model"):
38
+ q = q.eq("model", filters["model"])
39
+ if filters.get("start_date"):
40
+ q = q.gte("date", filters["start_date"])
41
+ if filters.get("end_date"):
42
+ q = q.lte("date", filters["end_date"])
43
+
44
+ # Order by date ascending for time-series correctness
45
+ q = q.order("date", desc=False)
46
+ # Pull up to limit
47
+ data = q.limit(limit).execute().data
48
+ df = pd.DataFrame(data or [])
49
+ if not df.empty:
50
+ # Normalize types
51
+ df["date"] = pd.to_datetime(df["date"], errors="coerce", utc=True).dt.tz_convert(None)
52
+ df = df.sort_values("date")
53
+ return df
54
+
55
+ # --- Strategy logic ---
56
+ class StrategyConfig:
57
+ def __init__(self, long_only=True, aggressive=False, fee=0.0005):
58
+ self.long_only = long_only # if True, no SHORT positions
59
+ self.aggressive = aggressive # if True, HOLD = flatten; BUY/SELL switch directly
60
+ self.fee = float(fee)
61
+
62
+ def simulate_equity(rows: pd.DataFrame, cfg: StrategyConfig):
63
+ """
64
+ Simulate equity curve given rows with columns: date, price, recommended_action
65
+ Returns: equity DataFrame with columns [date, equity], plus stats dict.
66
+ """
67
+ if rows.empty:
68
+ 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}
69
+
70
+ dates = rows["date"].tolist()
71
+ prices = rows["price"].astype(float).tolist()
72
+ actions = rows["recommended_action"].fillna("HOLD").str.upper().tolist()
73
+
74
+ equity = []
75
+ capital = 1.0
76
+ fee = cfg.fee
77
+ position = "FLAT"
78
+ entry_price = None
79
+
80
+ trades = []
81
+ last_equity = capital
82
+
83
+ # Buy & Hold baseline
84
+ p0 = prices[0]
85
+ bh_equity = [1.0 * (p / p0) for p in prices]
86
+ # Returns series for sharpe (daily-ish)
87
+ eq_series = []
88
+
89
+ for i, (dt, price, act) in enumerate(zip(dates, prices, actions)):
90
+ # Normalize action
91
+ if act not in ("BUY","SELL","HOLD"):
92
+ act = "HOLD"
93
+
94
+ # Aggressive logic: HOLD => flatten
95
+ if cfg.aggressive and act == "HOLD" and position != "FLAT":
96
+ # close pos
97
+ if position == "LONG":
98
+ capital *= (price / entry_price) * (1 - fee)
99
+ elif position == "SHORT":
100
+ capital *= (entry_price / price) * (1 - fee)
101
+ trades.append({"entry": entry_price, "exit": price, "dir": position})
102
+ position, entry_price = "FLAT", None
103
+
104
+ if act == "BUY":
105
+ if position == "FLAT":
106
+ position = "LONG"
107
+ entry_price = price
108
+ capital *= (1 - fee)
109
+ elif position == "SHORT":
110
+ # close SHORT, open LONG (if aggressive) or ignore (if baseline)
111
+ # In both modes, we interpret a BUY while short as closing short then long
112
+ capital *= (entry_price / price) * (1 - fee)
113
+ trades.append({"entry": entry_price, "exit": price, "dir": "SHORT"})
114
+ if cfg.long_only:
115
+ position, entry_price = "FLAT", None
116
+ else:
117
+ position, entry_price = "LONG", price
118
+ capital *= (1 - fee)
119
+ else:
120
+ # already LONG: no change
121
+ pass
122
+
123
+ elif act == "SELL":
124
+ if cfg.long_only:
125
+ # In long-only, SELL means close long if any
126
+ if position == "LONG":
127
+ capital *= (price / entry_price) * (1 - fee)
128
+ trades.append({"entry": entry_price, "exit": price, "dir": "LONG"})
129
+ position, entry_price = "FLAT", None
130
+ else:
131
+ if position == "FLAT":
132
+ position = "SHORT"
133
+ entry_price = price
134
+ capital *= (1 - fee)
135
+ elif position == "LONG":
136
+ # close LONG, open SHORT
137
+ capital *= (price / entry_price) * (1 - fee)
138
+ trades.append({"entry": entry_price, "exit": price, "dir": "LONG"})
139
+ position, entry_price = "SHORT", price
140
+ capital *= (1 - fee)
141
+ else:
142
+ # already SHORT
143
+ pass
144
+
145
+ # HOLD in non-aggressive does nothing
146
+
147
+ equity.append(capital)
148
+ eq_series.append(capital)
149
+
150
+ # At end, mark-to-market (position still open)
151
+ if position != "FLAT" and entry_price is not None:
152
+ last_price = prices[-1]
153
+ if position == "LONG":
154
+ mtm = capital * (last_price / entry_price)
155
+ else:
156
+ mtm = capital * (entry_price / last_price)
157
+ equity[-1] = mtm
158
+
159
+ eq_df = pd.DataFrame({"date": dates, "equity": equity})
160
+ ret_total = (eq_df["equity"].iloc[-1] / eq_df["equity"].iloc[0]) - 1.0 if len(eq_df) > 1 else 0.0
161
+ ret_bh = (bh_equity[-1] / bh_equity[0]) - 1.0 if len(bh_equity) > 1 else 0.0
162
+ ret_vs_bh = ret_total - ret_bh
163
+
164
+ # Sharpe (daily-ish): simple approximation using equity pct change
165
+ eq_series = np.array(eq_series)
166
+ if len(eq_series) > 1:
167
+ rets = np.diff(eq_series) / eq_series[:-1]
168
+ sharpe_daily = (np.mean(rets) / (np.std(rets) + 1e-9)) * np.sqrt(252)
169
+ else:
170
+ sharpe_daily = 0.0
171
+
172
+ # Win rate
173
+ wins = 0
174
+ for t in trades:
175
+ if t["dir"] == "LONG":
176
+ wins += 1 if t["exit"] > t["entry"] else 0
177
+ else:
178
+ wins += 1 if t["exit"] < t["entry"] else 0
179
+ win_rate = (wins / len(trades)) if trades else 0.0
180
+
181
+ stats = {
182
+ "trades": len(trades),
183
+ "win_rate": win_rate,
184
+ "ret_total": ret_total,
185
+ "ret_bh": ret_bh,
186
+ "ret_vs_bh": ret_vs_bh,
187
+ "sharpe_daily": float(sharpe_daily),
188
+ }
189
+ return eq_df, stats
190
+
191
+ # --- Plotting ---
192
+ def plot_equities(main_eq: pd.DataFrame, bh_eq: pd.DataFrame, title: str = "Equity Curve"):
193
+ fig = plt.figure(figsize=(8,4.5))
194
+ ax = fig.gca()
195
+ ax.plot(main_eq["date"], main_eq["equity"], label="Strategy")
196
+ ax.plot(bh_eq["date"], bh_eq["equity"], label="Buy & Hold", linestyle="--")
197
+ ax.set_title(title)
198
+ ax.set_xlabel("Date")
199
+ ax.set_ylabel("Equity (normalized)")
200
+ ax.legend()
201
+ ax.grid(True, alpha=0.3)
202
+ return fig
203
+
204
+ def build_bh(df: pd.DataFrame):
205
+ if df.empty:
206
+ return pd.DataFrame(columns=["date","equity"])
207
+ prices = df["price"].astype(float).values
208
+ base = prices[0]
209
+ eq = prices / base
210
+ return pd.DataFrame({"date": df["date"].values, "equity": eq})
211
+
212
+ # --- UI Handlers ---
213
+ def list_unique(column):
214
+ try:
215
+ df = fetch_decisions(limit=1000)
216
+ if df.empty or column not in df:
217
+ return []
218
+ vals = df[column].dropna().unique().tolist()
219
+ return sorted([v for v in vals if isinstance(v, str)])
220
+ except Exception:
221
+ return []
222
+
223
+ def run_query(agent, asset, model, start, end, long_only, aggressive, fee, limit_rows):
224
+ if not SUPABASE_URL or not SUPABASE_ANON_KEY:
225
+ banner = "⚠️ Missing SUPABASE_URL or SUPABASE_ANON_KEY. Set them in Space Secrets."
226
+ else:
227
+ banner = ""
228
+
229
+ filters = {}
230
+ if agent: filters["agent_name"] = agent
231
+ if asset: filters["asset"] = asset
232
+ if model: filters["model"] = model
233
+ if start: filters["start_date"] = start
234
+ if end: filters["end_date"] = end
235
+
236
+ df = fetch_decisions(limit=limit_rows, filters=filters)
237
+ if df.empty:
238
+ return banner or "No data found for the selected filters.", None, pd.DataFrame(), pd.DataFrame()
239
+
240
+ # Simulate
241
+ cfg = StrategyConfig(long_only=long_only, aggressive=aggressive, fee=fee)
242
+ eq_df, stats = simulate_equity(df[["date","price","recommended_action"]], cfg)
243
+ bh_df = build_bh(df)
244
+
245
+ # Plot
246
+ fig = plot_equities(eq_df, bh_df, title="Equity Curve")
247
+
248
+ # Metrics table
249
+ metrics = pd.DataFrame([{
250
+ "Trades": stats["trades"],
251
+ "Win Rate": f"{stats['win_rate']*100:.1f}%",
252
+ "Total Return": f"{stats['ret_total']*100:.1f}%",
253
+ "Buy&Hold Return": f"{stats['ret_bh']*100:.1f}%",
254
+ "Excess vs B&H": f"{stats['ret_vs_bh']*100:.1f}%",
255
+ "Sharpe (daily)": f"{stats['sharpe_daily']:.2f}",
256
+ "Rows Used": len(df)
257
+ }])
258
+
259
+ return banner, fig, df[["date","agent_name","asset","model","price","recommended_action"]].tail(25), metrics
260
+
261
+ with gr.Blocks(title="Paper Trading Agents") as demo:
262
+ gr.Markdown("# πŸ“ˆ Paper Trading Agents\nVisualize agent decisions from Supabase and compute strategy equity vs buy&hold.\n")
263
+ with gr.Row():
264
+ agent = gr.Dropdown(choices=[], label="Agent Name (optional)", interactive=True)
265
+ asset = gr.Dropdown(choices=[], label="Asset (optional)", interactive=True)
266
+ model = gr.Dropdown(choices=[], label="Model (optional)", interactive=True)
267
+ with gr.Row():
268
+ start = gr.Textbox(label="Start Date (YYYY-MM-DD, optional)")
269
+ end = gr.Textbox(label="End Date (YYYY-MM-DD, optional)")
270
+ limit_rows = gr.Slider(1000, 50000, value=DEFAULT_MAX_ROWS, step=500, label="Max rows")
271
+ with gr.Row():
272
+ long_only = gr.Checkbox(value=True, label="Long Only")
273
+ aggressive = gr.Checkbox(value=False, label="Aggressive Mode (HOLD = flatten; BUY/SELL switch)")
274
+ fee = gr.Number(value=0.0005, label="Fee (per open/close)")
275
+
276
+ go = gr.Button("Run")
277
+ banner = gr.Markdown()
278
+ plot = gr.Plot()
279
+ tail = gr.Dataframe(headers=["date","agent_name","asset","model","price","recommended_action"], label="Sample of latest rows", wrap=True)
280
+ metrics = gr.Dataframe(label="Metrics", wrap=True)
281
+
282
+ def _init_choices():
283
+ return gr.update(choices=list_unique("agent_name")), gr.update(choices=list_unique("asset")), gr.update(choices=list_unique("model"))
284
+
285
+ demo.load(_init_choices, None, [agent, asset, model])
286
+ go.click(run_query, inputs=[agent, asset, model, start, end, long_only, aggressive, fee, limit_rows], outputs=[banner, plot, tail, metrics])
287
+
288
+ if __name__ == "__main__":
289
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio>=4.44.0
2
+ pandas>=2.2.2
3
+ numpy>=1.26.4
4
+ matplotlib>=3.8.4
5
+ supabase>=2.7.4
6
+ python-dateutil>=2.9.0.post0