vincentjim1025 commited on
Commit
5481890
·
1 Parent(s): 6ddefe6
get_return.py ADDED
@@ -0,0 +1,662 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Trading Strategy Return Analysis Tool
3
+
4
+ Usage:
5
+ 1. Modify the configuration parameter lists (assets, models, agents) in the main() function
6
+ 2. Run directly: python get_return.py
7
+
8
+ Supports batch analysis:
9
+ - assets: Asset list, e.g., ["BTC", "TSLA", "AAPL"]
10
+ - models: Model list, e.g., ["gpt_4o", "gpt_4.1"]
11
+ - agents: Agent list, e.g., ["HedgeFundAgent", "FinAgent", "TradeAgent"]
12
+
13
+ Will automatically calculate all combinations and output results in order.
14
+
15
+ File naming format: action/{agent}_{asset}_{model}_trading_decisions.json
16
+ Example: action/HedgeFundAgent_BTC_gpt_4o_trading_decisions.json
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import pickle
22
+ import numpy as np
23
+ import pandas as pd
24
+ from scipy.stats import ttest_rel
25
+ from datetime import datetime, timedelta
26
+
27
+ # Import price fetching functions
28
+ from get_daily_news import get_asset_price, is_crypto, is_stock
29
+
30
+ # Global price cache to avoid repeated API calls
31
+ _price_cache = {}
32
+ CACHE_FILE = "cache/price_cache.pkl"
33
+
34
+ def load_price_cache():
35
+ """Load price cache from local pkl file"""
36
+ global _price_cache
37
+
38
+ try:
39
+ if os.path.exists(CACHE_FILE):
40
+ with open(CACHE_FILE, 'rb') as f:
41
+ _price_cache = pickle.load(f)
42
+
43
+ # Count loaded cache information
44
+ total_entries = sum(len(dates) for dates in _price_cache.values())
45
+ symbols = list(_price_cache.keys())
46
+ else:
47
+ _price_cache = {}
48
+ except Exception as e:
49
+ _price_cache = {}
50
+
51
+ def save_price_cache():
52
+ """Save price cache to local pkl file"""
53
+ global _price_cache
54
+
55
+ try:
56
+ # Ensure cache directory exists
57
+ os.makedirs(os.path.dirname(CACHE_FILE), exist_ok=True)
58
+
59
+ with open(CACHE_FILE, 'wb') as f:
60
+ pickle.dump(_price_cache, f)
61
+
62
+ # Count saved cache information
63
+ total_entries = sum(len(dates) for dates in _price_cache.values())
64
+ symbols = list(_price_cache.keys())
65
+
66
+ except Exception as e:
67
+ pass
68
+
69
+ def preload_prices(symbol, start_date, end_date):
70
+ """Preload all price data within specified time range to cache"""
71
+ global _price_cache
72
+
73
+ # On first call, load cache from local file
74
+ if not _price_cache:
75
+ load_price_cache()
76
+
77
+ # Preload price data
78
+
79
+ # Generate date range
80
+ dates = pd.date_range(start=start_date, end=end_date, freq='D')
81
+ cache_key = symbol
82
+
83
+ if cache_key not in _price_cache:
84
+ _price_cache[cache_key] = {}
85
+
86
+ # Count API calls
87
+ api_calls = 0
88
+ cached_hits = 0
89
+
90
+ # Batch fetch price data
91
+ for current_date in dates:
92
+ date_str = current_date.strftime('%Y-%m-%d')
93
+ if date_str not in _price_cache[cache_key]:
94
+ price = get_asset_price(symbol, date_str) # Directly call API to fill cache
95
+ _price_cache[cache_key][date_str] = price
96
+ api_calls += 1
97
+ else:
98
+ cached_hits += 1
99
+
100
+ # Complete price data preloading
101
+
102
+ # Save cache if there were new API calls
103
+ if api_calls > 0:
104
+ save_price_cache()
105
+
106
+ def get_cached_price(symbol, date_str):
107
+ """Get price from cache, call API directly if not in cache"""
108
+ global _price_cache
109
+
110
+ # On first call, load cache from local file
111
+ if not _price_cache:
112
+ load_price_cache()
113
+
114
+ cache_key = symbol
115
+ if cache_key in _price_cache and date_str in _price_cache[cache_key]:
116
+ # Get from cache
117
+ return _price_cache[cache_key][date_str]
118
+ else:
119
+ # If not in cache, call API directly (fallback solution)
120
+ price = get_asset_price(symbol, date_str)
121
+ # Cache the API result as well
122
+ if cache_key not in _price_cache:
123
+ _price_cache[cache_key] = {}
124
+ _price_cache[cache_key][date_str] = price
125
+ # Immediately save newly fetched price
126
+ save_price_cache()
127
+ return price
128
+
129
+ def clear_price_cache():
130
+ """Save price cache but don't clear memory (for compatibility with existing code)"""
131
+ global _price_cache
132
+
133
+ # Count cache information
134
+ total_entries = sum(len(dates) for dates in _price_cache.values())
135
+ symbols = list(_price_cache.keys())
136
+
137
+ # Save to file instead of clearing
138
+ save_price_cache()
139
+
140
+ def force_clear_cache():
141
+ """Force clear memory cache (actual clearing function)"""
142
+ global _price_cache
143
+
144
+ # Count cache information
145
+ total_entries = sum(len(dates) for dates in _price_cache.values())
146
+ symbols = list(_price_cache.keys())
147
+
148
+ # Save first then clear
149
+ save_price_cache()
150
+ _price_cache.clear()
151
+
152
+ def run_compounding_simulation(recommendations, initial_capital=100000, trade_fee=0.0005, strategy='long_short', trading_mode='normal', asset_type='stock', symbol=None):
153
+ """
154
+ Runs a realistic trading simulation with compounding capital and returns a daily capital series.
155
+
156
+ trading_mode:
157
+ - 'normal': Original strategy
158
+ - HOLD: keep current position
159
+ - BUY: open long if flat, ignore if in position
160
+ - SELL: open short if flat, close if long
161
+
162
+ - 'aggressive': New strategy
163
+ - HOLD: force close to flat
164
+ - BUY: close short (if short) then open long
165
+ - SELL: close long (if long) then open short
166
+ """
167
+ capital = float(initial_capital)
168
+ position = 'FLAT'
169
+ entry_price = 0
170
+ capital_series = []
171
+
172
+ rec_map = {rec['date']: rec for rec in recommendations}
173
+ start_date = datetime.fromisoformat(recommendations[0]['date'])
174
+ end_date = datetime.fromisoformat(recommendations[-1]['date'])
175
+
176
+ # symbol must be provided, no default values
177
+ if symbol is None:
178
+ raise ValueError("Symbol must be provided for run_compounding_simulation, cannot use default values")
179
+
180
+ # Use all calendar days (let price fetching function decide if valid)
181
+ dates = pd.date_range(start=start_date, end=end_date, freq='D')
182
+
183
+ # Record previous trading day's capital for filling non-trading days
184
+ last_capital = capital
185
+
186
+ for current_date in dates:
187
+ date_str = current_date.strftime('%Y-%m-%d')
188
+
189
+ # Actually get current day's price (based on asset type)
190
+ current_price = get_cached_price(symbol, date_str)
191
+ if current_price is None: # If price is null (market closed), skip this day
192
+ capital_series.append(last_capital)
193
+ continue
194
+
195
+ daily_capital = capital
196
+ if position == 'LONG':
197
+ daily_capital = capital * (current_price / entry_price) if entry_price != 0 else capital
198
+ elif position == 'SHORT':
199
+ daily_capital = capital * (1 + (entry_price - current_price) / entry_price) if entry_price != 0 else capital
200
+
201
+ # Execute trades for the current day BEFORE recording capital
202
+ # Check if the date exists in recommendations, default to HOLD if not
203
+ if date_str in rec_map:
204
+ action = rec_map[date_str].get('recommended_action', 'HOLD')
205
+ else:
206
+ action = 'HOLD' # Default action for missing dates
207
+
208
+ if trading_mode == 'normal': # Original strategy: HOLD keeps position
209
+ if action == 'HOLD':
210
+ # Keep current position, do nothing
211
+ pass
212
+ elif action == 'BUY':
213
+ if position == 'FLAT':
214
+ position, entry_price = 'LONG', current_price
215
+ capital *= (1 - trade_fee)
216
+ daily_capital = capital # Update daily capital after trade
217
+ elif position == 'SHORT':
218
+ # Close short position first
219
+ return_pct = (entry_price - current_price) / entry_price if entry_price != 0 else 0
220
+ capital *= (1 + return_pct) * (1 - trade_fee)
221
+ # Then open long position
222
+ position, entry_price = 'LONG', current_price
223
+ capital *= (1 - trade_fee)
224
+ daily_capital = capital
225
+ elif action == 'SELL':
226
+ if position == 'LONG':
227
+ return_pct = (current_price - entry_price) / entry_price if entry_price != 0 else 0
228
+ capital *= (1 + return_pct) * (1 - trade_fee)
229
+ position, entry_price = 'FLAT', 0
230
+ daily_capital = capital # Update daily capital after trade
231
+ elif position == 'FLAT' and strategy == 'long_short':
232
+ position, entry_price = 'SHORT', current_price
233
+ capital *= (1 - trade_fee)
234
+ daily_capital = capital # Update daily capital after trade
235
+
236
+ else: # New strategy: HOLD closes position, BUY/SELL switches position directly
237
+ if action == 'HOLD': # Force close position
238
+ if position == 'LONG':
239
+ return_pct = (current_price - entry_price) / entry_price if entry_price != 0 else 0
240
+ capital *= (1 + return_pct) * (1 - trade_fee)
241
+ position, entry_price = 'FLAT', 0
242
+ daily_capital = capital
243
+ elif position == 'SHORT':
244
+ return_pct = (entry_price - current_price) / entry_price if entry_price != 0 else 0
245
+ capital *= (1 + return_pct) * (1 - trade_fee)
246
+ position, entry_price = 'FLAT', 0
247
+ daily_capital = capital
248
+ elif action == 'BUY':
249
+ if position == 'SHORT': # First close short position
250
+ return_pct = (entry_price - current_price) / entry_price if entry_price != 0 else 0
251
+ capital *= (1 + return_pct) * (1 - trade_fee)
252
+ position, entry_price = 'FLAT', 0
253
+ daily_capital = capital # Update daily_capital
254
+ if position == 'FLAT': # Then open long position
255
+ position, entry_price = 'LONG', current_price
256
+ capital *= (1 - trade_fee)
257
+ daily_capital = capital
258
+ elif action == 'SELL':
259
+ if position == 'LONG': # First close long position
260
+ return_pct = (current_price - entry_price) / entry_price if entry_price != 0 else 0
261
+ capital *= (1 + return_pct) * (1 - trade_fee)
262
+ position, entry_price = 'FLAT', 0
263
+ daily_capital = capital # Update daily_capital
264
+ if position == 'FLAT' and strategy == 'long_short': # Then open short position
265
+ position, entry_price = 'SHORT', current_price
266
+ capital *= (1 - trade_fee)
267
+ daily_capital = capital
268
+
269
+ # Record capital after all trades are executed
270
+ capital_series.append(daily_capital)
271
+ last_capital = daily_capital
272
+
273
+ # Force close position on the last day
274
+ if current_date == dates[-1] and position != 'FLAT':
275
+ if position == 'LONG':
276
+ return_pct = (current_price - entry_price) / entry_price if entry_price != 0 else 0
277
+ capital *= (1 + return_pct) * (1 - trade_fee)
278
+ elif position == 'SHORT':
279
+ return_pct = (entry_price - current_price) / entry_price if entry_price != 0 else 0
280
+ capital *= (1 + return_pct) * (1 - trade_fee)
281
+ position, entry_price = 'FLAT', 0
282
+ capital_series[-1] = capital # Update the last capital value
283
+
284
+ return capital_series
285
+
286
+ def calculate_buy_and_hold_series(recommendations, initial_capital=100000, trade_fee=0.0005, asset_type='stock', symbol=None):
287
+ """Calculate buy and hold strategy performance"""
288
+ capital_series = []
289
+ rec_map = {rec['date']: rec for rec in recommendations}
290
+ start_date = datetime.fromisoformat(recommendations[0]['date'])
291
+ end_date = datetime.fromisoformat(recommendations[-1]['date'])
292
+
293
+ # symbol must be provided, no default values
294
+ if symbol is None:
295
+ raise ValueError("Symbol must be provided for calculate_buy_and_hold_series, cannot use default values")
296
+
297
+ # Get first valid price as buy price
298
+ buy_price = None
299
+ first_date_str = start_date.strftime('%Y-%m-%d')
300
+ buy_price = get_cached_price(symbol, first_date_str) # Use cache
301
+
302
+ if buy_price is None:
303
+ # If no price on first day, find first valid price
304
+ current_date = start_date
305
+ while current_date <= end_date and buy_price is None:
306
+ date_str = current_date.strftime('%Y-%m-%d')
307
+ buy_price = get_cached_price(symbol, date_str)
308
+ current_date += timedelta(days=1)
309
+
310
+ if buy_price is None or buy_price <= 0:
311
+ # If no valid price throughout the period, return empty sequence
312
+ print(f"Warning: No valid buy price found for {symbol} in period {start_date} to {end_date}")
313
+ return []
314
+
315
+ # Buy on first day, charge opening fee
316
+ capital = initial_capital * (1 - trade_fee)
317
+
318
+ # Use all calendar days (let price fetching function decide if valid)
319
+ dates = pd.date_range(start=start_date, end=end_date, freq='D')
320
+
321
+ last_price = buy_price
322
+ for i, current_date in enumerate(dates):
323
+ date_str = current_date.strftime('%Y-%m-%d')
324
+
325
+ # Actually get current day's price (based on asset type)
326
+ current_price = get_cached_price(symbol, date_str)
327
+
328
+ # If price is null, skip this day and use last valid price
329
+ if current_price is None:
330
+ daily_capital = capital * (last_price / buy_price) if buy_price != 0 else capital
331
+ capital_series.append(daily_capital)
332
+ continue
333
+
334
+ # Calculate current market value
335
+ daily_capital = capital * (current_price / buy_price) if buy_price != 0 else capital
336
+
337
+ # Sell on last day, charge closing fee
338
+ if i == len(dates) - 1: # Use index to determine last day
339
+ daily_capital *= (1 - trade_fee)
340
+
341
+ capital_series.append(daily_capital)
342
+ last_price = current_price
343
+
344
+ return capital_series
345
+
346
+ def get_daily_returns(capital_series):
347
+ """Calculate daily returns from capital series"""
348
+ series = pd.Series(capital_series)
349
+ return series.pct_change().fillna(0)
350
+
351
+ def calculate_metrics(capital_series, recommendations, asset_type='stock'):
352
+ """
353
+ Calculate performance metrics for different asset types
354
+
355
+ Parameters:
356
+ - capital_series: list of daily capital values
357
+ - recommendations: list of trading recommendations
358
+ - asset_type: 'stock' or 'crypto'
359
+ """
360
+ if len(capital_series) == 0:
361
+ return {
362
+ 'total_return': 0,
363
+ 'ann_return': 0,
364
+ 'ann_vol': 0,
365
+ 'sharpe_ratio': 0,
366
+ 'max_drawdown': 0
367
+ }
368
+
369
+ daily_returns = get_daily_returns(capital_series)
370
+
371
+ # Total Return
372
+ total_return = (capital_series[-1] - capital_series[0]) / capital_series[0] * 100
373
+
374
+ # Choose annualization parameters based on asset type
375
+ if asset_type == 'stock':
376
+ annual_days = 252 # Stock trading days per year
377
+ # For stocks, the capital series includes calendar days; weekends/holidays
378
+ # create zero returns that artificially depress volatility.
379
+ # Filter out zero-return days to approximate trading days only.
380
+ trading_returns = daily_returns[daily_returns != 0]
381
+ effective_returns = trading_returns if len(trading_returns) > 0 else daily_returns
382
+ n_days_effective = len(effective_returns) if len(effective_returns) > 0 else len(daily_returns)
383
+ ann_vol = (effective_returns.std() * np.sqrt(annual_days) * 100) if len(effective_returns) > 1 else 0
384
+ # Annualized return uses effective trading day count
385
+ if n_days_effective > 1:
386
+ ann_return = (((capital_series[-1] / capital_series[0]) ** (annual_days / n_days_effective)) - 1) * 100
387
+ else:
388
+ ann_return = total_return
389
+ else: # crypto
390
+ annual_days = 365 # Cryptocurrency trades year-round
391
+ n_days_effective = len(daily_returns)
392
+ ann_vol = daily_returns.std() * np.sqrt(annual_days) * 100 if len(daily_returns) > 1 else 0
393
+ if n_days_effective > 1:
394
+ ann_return = (((capital_series[-1] / capital_series[0]) ** (annual_days / n_days_effective)) - 1) * 100
395
+ else:
396
+ ann_return = total_return
397
+
398
+ # Sharpe Ratio (assuming risk-free rate = 0)
399
+ # Use standard daily mean/std approach with consistent day count per asset type
400
+ if asset_type == 'stock':
401
+ sharpe_base_returns = effective_returns
402
+ else:
403
+ sharpe_base_returns = daily_returns
404
+
405
+ mean_daily = sharpe_base_returns.mean() if len(sharpe_base_returns) > 0 else 0
406
+ std_daily = sharpe_base_returns.std() if len(sharpe_base_returns) > 1 else 0
407
+
408
+ if std_daily and std_daily > 0:
409
+ sharpe_ratio = (mean_daily / std_daily) * np.sqrt(annual_days)
410
+ else:
411
+ sharpe_ratio = 0
412
+
413
+ # Maximum Drawdown
414
+ capital_series_pd = pd.Series(capital_series)
415
+ rolling_max = capital_series_pd.expanding().max()
416
+ drawdowns = (capital_series_pd - rolling_max) / rolling_max
417
+ max_drawdown = drawdowns.min() * 100 if len(drawdowns) > 0 else 0
418
+
419
+ return {
420
+ 'total_return': total_return,
421
+ 'ann_return': ann_return,
422
+ 'ann_vol': ann_vol,
423
+ 'sharpe_ratio': sharpe_ratio,
424
+ 'max_drawdown': max_drawdown
425
+ }
426
+
427
+ def print_metrics_table(strategies_data, headers):
428
+ """Print formatted metrics table"""
429
+ metrics = ['total_return', 'ann_return', 'ann_vol', 'sharpe_ratio', 'max_drawdown']
430
+ metric_headers = {
431
+ 'total_return': 'Total Return % (↑)',
432
+ 'ann_return': 'Ann. Return % (↑)',
433
+ 'ann_vol': 'Ann. Vol % (↓)',
434
+ 'sharpe_ratio': 'Sharpe Ratio (↑)',
435
+ 'max_drawdown': 'Max DD % (↓)'
436
+ }
437
+
438
+ # Calculate column widths
439
+ col_widths = {m: max(12, len(metric_headers[m]) + 1) for m in metrics}
440
+
441
+ # Print header
442
+ header_line = f"{'Strategy':<20} | " + " | ".join(f"{metric_headers[m]:>{col_widths[m]}}" for m in metrics)
443
+ print(header_line)
444
+ print("-" * len(header_line))
445
+
446
+ # Print strategy data
447
+ for name, data in strategies_data:
448
+ line = f"{name:<20} | " + " | ".join(f"{data[metric]:>{col_widths[metric]}.2f}" for metric in metrics)
449
+ print(line)
450
+
451
+ def discover_available_files():
452
+ """
453
+ Automatically discover all trading decision files in action directory and return available combinations
454
+ """
455
+ action_dir = 'action'
456
+ if not os.path.exists(action_dir):
457
+ print(f"Error: {action_dir} directory not found")
458
+ return [], [], []
459
+
460
+ available_agents = set()
461
+ available_assets = set()
462
+ available_models = set()
463
+ found_files = []
464
+
465
+ # Scan all json files
466
+ for filename in os.listdir(action_dir):
467
+ if filename.endswith('_trading_decisions.json'):
468
+ # Parse filename format: {agent}_{asset}_{model}_trading_decisions.json
469
+ parts = filename.replace('_trading_decisions.json', '').split('_')
470
+ if len(parts) >= 3:
471
+ # Parse based on known model name patterns
472
+ base_name = '_'.join(parts)
473
+
474
+ if 'claude_sonnet_4_20250514' in base_name:
475
+ # claude_sonnet_4_20250514 format
476
+ model = 'claude_sonnet_4_20250514'
477
+ remaining = base_name.replace('_claude_sonnet_4_20250514', '')
478
+ elif 'claude_3_5_haiku_20241022' in base_name:
479
+ # claude_3_5_haiku_20241022 format
480
+ model = 'claude_3_5_haiku_20241022'
481
+ remaining = base_name.replace('_claude_3_5_haiku_20241022', '')
482
+ elif 'gemini_2.0_flash' in base_name:
483
+ # gemini_2.0_flash format
484
+ model = 'gemini_2.0_flash'
485
+ remaining = base_name.replace('_gemini_2.0_flash', '')
486
+ elif 'gpt_4o' in base_name:
487
+ # gpt_4o format
488
+ model = 'gpt_4o'
489
+ remaining = base_name.replace('_gpt_4o', '')
490
+ elif 'gpt_4.1' in base_name:
491
+ # gpt_4.1 format
492
+ model = 'gpt_4.1'
493
+ remaining = base_name.replace('_gpt_4.1', '')
494
+ elif 'vote' in base_name:
495
+ # vote format
496
+ model = 'vote'
497
+ remaining = base_name.replace('_vote', '')
498
+ else:
499
+ # Default handling: last two parts are model
500
+ model = '_'.join(parts[-2:])
501
+ remaining = '_'.join(parts[:-2])
502
+
503
+ # Extract asset and agent from remaining parts
504
+ remaining_parts = remaining.split('_')
505
+ if len(remaining_parts) >= 2:
506
+ asset = remaining_parts[-1] # Last part is asset
507
+ agent = '_'.join(remaining_parts[:-1]) # Previous parts are agent
508
+
509
+ available_agents.add(agent)
510
+ available_assets.add(asset)
511
+ available_models.add(model)
512
+ found_files.append((agent, asset, model, filename))
513
+
514
+ # Silently discover files, no detailed output
515
+
516
+ return sorted(available_agents), sorted(available_assets), sorted(available_models)
517
+
518
+ def analyze_and_print(title, recommendations, asset_type='stock', symbol=None):
519
+ """Analyze and print strategy performance comparison"""
520
+ print(f"\n{'='*60}")
521
+ print(f"{title:^60}")
522
+ print(f"{'='*60}")
523
+
524
+ if not recommendations:
525
+ print("No recommendations to analyze.")
526
+ return
527
+
528
+ # Preload price data (get all needed prices at once)
529
+ start_date = recommendations[0]['date']
530
+ end_date = recommendations[-1]['date']
531
+ preload_prices(symbol, start_date, end_date)
532
+
533
+ # Calculate Buy & Hold strategy (calculate only once)
534
+ bh_series = calculate_buy_and_hold_series(recommendations, asset_type=asset_type, symbol=symbol)
535
+ bh_metrics = calculate_metrics(bh_series, recommendations, asset_type=asset_type)
536
+
537
+ # Strategy 1: HOLD KEEP current (keep position)
538
+ ls_keep_current = run_compounding_simulation(recommendations, strategy='long_short', trading_mode='normal', asset_type=asset_type, symbol=symbol)
539
+ lo_keep_current = run_compounding_simulation(recommendations, strategy='long_only', trading_mode='normal', asset_type=asset_type, symbol=symbol)
540
+
541
+ # Calculate metrics for Strategy 1
542
+ ls_metrics = calculate_metrics(ls_keep_current, recommendations, asset_type=asset_type)
543
+ lo_metrics = calculate_metrics(lo_keep_current, recommendations, asset_type=asset_type)
544
+
545
+ # Print Strategy 1 metrics
546
+ print("\nStrategy 1 (HOLD keeps position):")
547
+ strategies_data = [
548
+ ('Long/Short', ls_metrics),
549
+ ('Long-Only', lo_metrics),
550
+ ('Buy & Hold', bh_metrics)
551
+ ]
552
+ print_metrics_table(strategies_data, None)
553
+
554
+ # Strategy 2: HOLD KEEP FLAT (force close position)
555
+ ls_keep_flat = run_compounding_simulation(recommendations, strategy='long_short', trading_mode='aggressive', asset_type=asset_type, symbol=symbol)
556
+ lo_keep_flat = run_compounding_simulation(recommendations, strategy='long_only', trading_mode='aggressive', asset_type=asset_type, symbol=symbol)
557
+
558
+ # Calculate metrics for Strategy 2
559
+ ls_flat_metrics = calculate_metrics(ls_keep_flat, recommendations, asset_type=asset_type)
560
+ lo_flat_metrics = calculate_metrics(lo_keep_flat, recommendations, asset_type=asset_type)
561
+
562
+ # Print Strategy 2 metrics
563
+ print("\nStrategy 2 (HOLD forces flat):")
564
+ strategies_data = [
565
+ ('Long/Short', ls_flat_metrics),
566
+ ('Long-Only', lo_flat_metrics),
567
+ ('Buy & Hold', bh_metrics)
568
+ ]
569
+ print_metrics_table(strategies_data, None)
570
+
571
+ print(f"{asset_type.upper()} {symbol} | {recommendations[0]['date']} to {recommendations[-1]['date']} | {len(ls_keep_current)} days")
572
+
573
+ def main():
574
+ """Main function to run the analysis"""
575
+
576
+ # ===========================================
577
+ # Configuration Parameters - Modify here
578
+ # ===========================================
579
+
580
+ # Whether to auto-discover available files (True: auto-discover, False: use manual configuration below)
581
+ auto_discover = False
582
+
583
+ # Manual configuration parameters (only used when auto_discover = False)
584
+ # Asset symbol list (e.g.: BTC, TSLA, AAPL, etc.)
585
+ assets = ['TSLA']#["BTC", 'TSLA'] # Only analyze BTC
586
+
587
+ # Model name list (e.g.: gpt_4o, gpt_4.1)
588
+ models = ["gpt_4o", "gpt_4.1", "gemini_2.0_flash","claude_3_5_haiku_20241022", "claude_sonnet_4_20250514", "vote"]
589
+ # models = ['vote']
590
+
591
+ # Agent name list (e.g.: HedgeFundAgent, FinAgent, TradeAgent)
592
+ agents = ['InvestorAgent', "TradeAgent"]# "InvestorAgent", "HedgeFundAgent", "DeepFundAgent"] # Multiple agents to analyze
593
+
594
+ # ===========================================
595
+ # Analysis Logic - No need to modify
596
+ # ===========================================
597
+
598
+ # If auto-discovery is enabled, scan existing files
599
+ if auto_discover:
600
+ print("🔍 Auto-discovering available files...")
601
+ discovered_agents, discovered_assets, discovered_models = discover_available_files()
602
+ print(f"Discovered files: Agents={discovered_agents}, Assets={discovered_assets}, Models={discovered_models}")
603
+ if discovered_agents and discovered_assets and discovered_models:
604
+ agents, assets, models = discovered_agents, discovered_assets, discovered_models
605
+ print(f"✅ Using auto-discovered parameters: Agents={agents}, Assets={assets}, Models={models}")
606
+ else:
607
+ print("⚠️ Auto-discovery failed, using manual configuration parameters")
608
+
609
+ # Iterate through all combinations
610
+ for agent in agents:
611
+ for asset in assets:
612
+ for model in models:
613
+ # Construct file path: action/{agent}_{asset}_{model}_trading_decisions.json
614
+ file_path = f'action/{agent}_{asset}_{model}_trading_decisions.json'
615
+
616
+ # Determine asset type
617
+ symbol = asset
618
+ if asset in ['BTC', 'ETH', 'ADA', 'SOL', 'DOT', 'LINK', 'UNI', 'MATIC', 'AVAX', 'ATOM']:
619
+ asset_type = 'crypto'
620
+ elif asset in ['TSLA', 'AAPL', 'MSFT', 'GOOGL', 'AMZN', 'NVDA', 'META', 'NFLX', 'AMD', 'INTC']:
621
+ asset_type = 'stock'
622
+ else:
623
+ asset_type = 'stock'
624
+
625
+ try:
626
+ if not os.path.exists(file_path):
627
+ print(f"File not found: {file_path}")
628
+ continue
629
+
630
+ with open(file_path, 'r', encoding='utf-8') as f:
631
+ data = json.load(f)
632
+
633
+ recs = data.get('recommendations', [])
634
+ if not recs:
635
+ print(f"No recommendations found in {file_path}")
636
+ continue
637
+
638
+ # Validate recommendation format
639
+ valid_format = True
640
+ for rec in recs:
641
+ if 'date' not in rec or 'price' not in rec:
642
+ print(f"Invalid recommendation format in {file_path}")
643
+ valid_format = False
644
+ break
645
+
646
+ if not valid_format:
647
+ continue
648
+
649
+ recs.sort(key=lambda x: datetime.fromisoformat(x['date']))
650
+
651
+ title = f"{agent}_{asset}_{model} ({data.get('start_date', 'Unknown')} to {data.get('end_date', 'Unknown')})"
652
+ analyze_and_print(title, recs, asset_type=asset_type, symbol=symbol)
653
+
654
+ except Exception as e:
655
+ print(f"Error processing {file_path}: {e}")
656
+ continue
657
+
658
+ # Clear price cache to free memory
659
+ clear_price_cache()
660
+
661
+ if __name__ == "__main__":
662
+ main()
src/assets/images/companies_images/columbia.png ADDED

Git LFS Details

  • SHA256: 46242e19bea69f5e0d7a43a31c0820aabd5372f32d5bfe9602a086f5ca0b4c07
  • Pointer size: 130 Bytes
  • Size of remote file: 32.2 kB
src/assets/images/companies_images/florida.png ADDED

Git LFS Details

  • SHA256: c32ea5a016f50182c523b6c974621fae80120e6a8427e414e25642734bfcf6ba
  • Pointer size: 130 Bytes
  • Size of remote file: 16.4 kB
src/assets/images/companies_images/georgia.png ADDED

Git LFS Details

  • SHA256: 74899b4d1d37dbdf3900a1b929db3cb5334c18d926a580e358ab43d6cd847b0f
  • Pointer size: 130 Bytes
  • Size of remote file: 35.3 kB
src/assets/images/companies_images/harvard.png ADDED

Git LFS Details

  • SHA256: c3ae92913f9bdb61cedf5e8ce5c39226239cd8e4754c239789865650ce38ac92
  • Pointer size: 131 Bytes
  • Size of remote file: 144 kB
src/assets/images/companies_images/montreal.png ADDED

Git LFS Details

  • SHA256: 040e19627661753ac3fa22108f52dccb30333da53106534bb32a5a9586c49a11
  • Pointer size: 131 Bytes
  • Size of remote file: 103 kB
src/assets/images/companies_images/stevens.png ADDED

Git LFS Details

  • SHA256: 75993a55d9197318cc795e8380385e21101ca6dd9cd249745d84ad656da1b58e
  • Pointer size: 130 Bytes
  • Size of remote file: 92.1 kB
src/components/AgentFilters.vue CHANGED
@@ -63,7 +63,7 @@
63
  <div class="flex flex-column gap-1">
64
  <div v-for="opt in modelOptions" :key="`model_${opt.value}`" class="flex align-items-center gap-1">
65
  <Checkbox v-model="modelsModel" :inputId="`model_${opt.value}`" :value="opt.value" />
66
- <label :for="`model_${opt.value}`" class="opt-label">{{ opt.label }}</label>
67
  </div>
68
  </div>
69
  </div>
@@ -179,6 +179,10 @@ export default {
179
  }
180
  },
181
  methods: {
 
 
 
 
182
  initializeSlider() {
183
  if (this.hasDateBounds) {
184
  // 查找 2025-08-01 在日期数组中的索引
 
63
  <div class="flex flex-column gap-1">
64
  <div v-for="opt in modelOptions" :key="`model_${opt.value}`" class="flex align-items-center gap-1">
65
  <Checkbox v-model="modelsModel" :inputId="`model_${opt.value}`" :value="opt.value" />
66
+ <label :for="`model_${opt.value}`" class="opt-label">{{ formatModelName(opt.label) }}</label>
67
  </div>
68
  </div>
69
  </div>
 
179
  }
180
  },
181
  methods: {
182
+ formatModelName(model) {
183
+ if (!model) return ''
184
+ return model.replace(/_?\d{8}$/, '')
185
+ },
186
  initializeSlider() {
187
  if (this.hasDateBounds) {
188
  // 查找 2025-08-01 在日期数组中的索引
src/components/AgentTable.vue CHANGED
@@ -1,21 +1,21 @@
1
  <template>
2
- <DataTable :value="rows" :rows="10" :rowsPerPageOptions="[10,25,50]" paginator scrollable scrollHeight="flex" :loading="loading" :sortMode="'multiple'" :multiSortMeta="multiSortMeta" v-model:expandedRows="expandedRows" :dataKey="'key'" @sort="onSort" @rowToggle="onRowToggle" @rowExpand="onRowExpand" :selection="selection" @update:selection="onSelectionUpdate">
3
- <Column v-if="selectable" selectionMode="multiple" style="width: 3rem" />
4
- <Column expander style="width: 3rem" />
5
- <Column field="agent_name" header="Agent & Model & Strategy">
6
  <template #body="{ data }">
7
  <div>
8
  <div style="display: flex; align-items: center; gap: 0.5rem;">
9
  <span>{{ data.agent_name }}</span>
10
  <span style="font-size: 1.25rem;">{{ getRankMedal(data) }}</span>
11
  </div>
12
- <div style="color:#6b7280; font-size: 0.875rem;">{{ data.model }}</div>
13
  <!-- <div style="color:#6b7280; font-size: 0.875rem;">{{ data.strategy_label }}</div> -->
14
  </div>
15
  </template>
16
  </Column>
17
  <!-- <Column field="asset" header="Asset"/> -->
18
- <Column field="ret_with_fees" header="Return" sortable>
19
  <template #body="{ data }">
20
  <div>
21
  <div :style="pctStyle(data.ret_with_fees)">{{ fmtSignedPct(data.ret_with_fees) }}</div>
@@ -24,19 +24,19 @@
24
  </template>
25
  </Column>
26
 
27
- <Column field="vs_bh_with_fees" header="Vs Buy & Hold" sortable>
28
  <template #body="{ data }">
29
  <span :style="pctStyle(data.vs_bh_with_fees)">{{ fmtSignedPct(data.vs_bh_with_fees) }}</span>
30
  </template>
31
  </Column>
32
 
33
- <Column field="sharpe" header="Sharpe Ratio" sortable>
34
  <template #body="{ data }">
35
  {{ fmtNum(data.sharpe) }}
36
  </template>
37
  </Column>
38
 
39
- <Column field="win_rate" header="Win Rate" sortable>
40
  <template #body="{ data }">
41
  {{ fmtPctNeutral(data.win_rate) }}
42
  </template>
@@ -82,6 +82,12 @@ export default {
82
  if (rank === 3) return '🥉'
83
  return ''
84
  },
 
 
 
 
 
 
85
  onSelectionUpdate(val){
86
  this.$emit('update:selection', Array.isArray(val) ? val : [])
87
  },
@@ -151,6 +157,43 @@ export default {
151
  </script>
152
 
153
  <style scoped>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  </style>
155
 
156
 
 
1
  <template>
2
+ <DataTable :value="rows" :rows="10" :rowsPerPageOptions="[10,25,50]" paginator scrollable scrollHeight="flex" :loading="loading" :sortMode="'multiple'" :multiSortMeta="multiSortMeta" v-model:expandedRows="expandedRows" :dataKey="'key'" @sort="onSort" @rowToggle="onRowToggle" @rowExpand="onRowExpand" :selection="selection" @update:selection="onSelectionUpdate" class="agent-table-scroll">
3
+ <Column v-if="selectable" selectionMode="multiple" :style="{ width: '50px', minWidth: '50px' }" frozen />
4
+ <Column expander :style="{ width: '50px', minWidth: '50px' }" frozen />
5
+ <Column field="agent_name" header="Agent & Model" :style="{ minWidth: '200px' }" frozen>
6
  <template #body="{ data }">
7
  <div>
8
  <div style="display: flex; align-items: center; gap: 0.5rem;">
9
  <span>{{ data.agent_name }}</span>
10
  <span style="font-size: 1.25rem;">{{ getRankMedal(data) }}</span>
11
  </div>
12
+ <div style="color:#6b7280; font-size: 0.875rem;">{{ formatModelName(data.model) }}</div>
13
  <!-- <div style="color:#6b7280; font-size: 0.875rem;">{{ data.strategy_label }}</div> -->
14
  </div>
15
  </template>
16
  </Column>
17
  <!-- <Column field="asset" header="Asset"/> -->
18
+ <Column field="ret_with_fees" header="Return" sortable :style="{ minWidth: '180px' }">
19
  <template #body="{ data }">
20
  <div>
21
  <div :style="pctStyle(data.ret_with_fees)">{{ fmtSignedPct(data.ret_with_fees) }}</div>
 
24
  </template>
25
  </Column>
26
 
27
+ <Column field="vs_bh_with_fees" header="Vs Buy & Hold" sortable :style="{ minWidth: '140px' }">
28
  <template #body="{ data }">
29
  <span :style="pctStyle(data.vs_bh_with_fees)">{{ fmtSignedPct(data.vs_bh_with_fees) }}</span>
30
  </template>
31
  </Column>
32
 
33
+ <Column field="sharpe" header="Sharpe Ratio" sortable :style="{ minWidth: '120px' }">
34
  <template #body="{ data }">
35
  {{ fmtNum(data.sharpe) }}
36
  </template>
37
  </Column>
38
 
39
+ <Column field="win_rate" header="Win Rate" sortable :style="{ minWidth: '110px' }">
40
  <template #body="{ data }">
41
  {{ fmtPctNeutral(data.win_rate) }}
42
  </template>
 
82
  if (rank === 3) return '🥉'
83
  return ''
84
  },
85
+ formatModelName(model) {
86
+ if (!model) return ''
87
+ // Remove date suffix pattern (8 digits at the end, like _20250514)
88
+ // Also handles patterns like _YYYYMMDD or just YYYYMMDD at the end
89
+ return model.replace(/_?\d{8}$/, '')
90
+ },
91
  onSelectionUpdate(val){
92
  this.$emit('update:selection', Array.isArray(val) ? val : [])
93
  },
 
157
  </script>
158
 
159
  <style scoped>
160
+ /* Enable horizontal scrolling */
161
+ :deep(.agent-table-scroll .p-datatable-wrapper) {
162
+ overflow-x: auto;
163
+ }
164
+
165
+ :deep(.agent-table-scroll .p-datatable-table) {
166
+ min-width: 800px;
167
+ }
168
+
169
+ /* Frozen column styles */
170
+ :deep(.agent-table-scroll .p-frozen-column) {
171
+ background: #ffffff;
172
+ z-index: 1;
173
+ }
174
+
175
+ :deep(.agent-table-scroll .p-datatable-thead > tr > th.p-frozen-column) {
176
+ background: #F6F8FB;
177
+ }
178
+
179
+ /* Better scrollbar */
180
+ :deep(.agent-table-scroll .p-datatable-wrapper)::-webkit-scrollbar {
181
+ height: 8px;
182
+ }
183
+
184
+ :deep(.agent-table-scroll .p-datatable-wrapper)::-webkit-scrollbar-track {
185
+ background: #f1f1f1;
186
+ border-radius: 4px;
187
+ }
188
+
189
+ :deep(.agent-table-scroll .p-datatable-wrapper)::-webkit-scrollbar-thumb {
190
+ background: #cbd5e1;
191
+ border-radius: 4px;
192
+ }
193
+
194
+ :deep(.agent-table-scroll .p-datatable-wrapper)::-webkit-scrollbar-thumb:hover {
195
+ background: #94a3b8;
196
+ }
197
  </style>
198
 
199
 
src/components/AssetsFilter.vue CHANGED
@@ -1,40 +1,22 @@
1
  <template>
2
- <Tabs :value="selectedAsset" @update:value="onTabChange" class="assets-filter-container">
3
- <TabList>
4
- <Tab
5
- v-for="asset in availableAssets"
6
- :key="asset.value"
7
- :value="asset.value"
8
- >
9
- <div class="flex align-items-center gap-2">
10
- <img
11
- :src="getAssetImage(asset.value)"
12
- :alt="asset.label"
13
- class="asset-image-small"
14
- />
15
- <span class="font-bold">{{ asset.label }}</span>
16
- </div>
17
- </Tab>
18
- </TabList>
19
- </Tabs>
20
  </template>
21
 
22
  <script>
23
- import Tabs from 'primevue/tabs'
24
- import TabList from 'primevue/tablist'
25
- import Tab from 'primevue/tab'
26
- import TabPanels from 'primevue/tabpanels'
27
- import TabPanel from 'primevue/tabpanel'
28
-
29
  export default {
30
  name: 'AssetsFilter',
31
- components: {
32
- Tabs,
33
- TabList,
34
- Tab,
35
- TabPanels,
36
- TabPanel
37
- },
38
  props: {
39
  modelValue: { type: Array, default: () => [] },
40
  assetOptions: { type: Array, default: () => [] }
@@ -42,7 +24,18 @@ export default {
42
  emits: ['update:modelValue'],
43
  computed: {
44
  availableAssets() {
45
- return this.assetOptions.sort((a, b) => a.label.localeCompare(b.label)) || []
 
 
 
 
 
 
 
 
 
 
 
46
  },
47
  selectedAsset() {
48
  const selected = this.modelValue || []
@@ -77,15 +70,54 @@ export default {
77
  </script>
78
 
79
  <style scoped>
80
- .assets-filter-container {
81
- width: 100%;
82
- overflow-x: auto;
83
- overflow-y: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  }
85
- .asset-image-small {
86
- width: 20px;
87
- height: 20px;
88
- object-fit: contain;
89
- flex-shrink: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  }
91
  </style>
 
1
  <template>
2
+ <div class="assets-filter-toolbar">
3
+ <div class="asset-tabs">
4
+ <button
5
+ v-for="asset in availableAssets"
6
+ :key="asset.value"
7
+ class="asset-tab"
8
+ :class="{ 'is-active': asset.value === selectedAsset }"
9
+ @click="onTabChange(asset.value)"
10
+ >
11
+ {{ asset.label }}
12
+ </button>
13
+ </div>
14
+ </div>
 
 
 
 
 
15
  </template>
16
 
17
  <script>
 
 
 
 
 
 
18
  export default {
19
  name: 'AssetsFilter',
 
 
 
 
 
 
 
20
  props: {
21
  modelValue: { type: Array, default: () => [] },
22
  assetOptions: { type: Array, default: () => [] }
 
24
  emits: ['update:modelValue'],
25
  computed: {
26
  availableAssets() {
27
+ // Keep the same order as LiveView: BTC, ETH, BMRN, TSLA
28
+ const order = ['BTC', 'ETH', 'BMRN', 'TSLA']
29
+ const sorted = [...(this.assetOptions || [])]
30
+ sorted.sort((a, b) => {
31
+ const indexA = order.indexOf(a.value)
32
+ const indexB = order.indexOf(b.value)
33
+ if (indexA === -1 && indexB === -1) return a.label.localeCompare(b.label)
34
+ if (indexA === -1) return 1
35
+ if (indexB === -1) return -1
36
+ return indexA - indexB
37
+ })
38
+ return sorted
39
  },
40
  selectedAsset() {
41
  const selected = this.modelValue || []
 
70
  </script>
71
 
72
  <style scoped>
73
+ :root {
74
+ --ama-start: 239, 68, 68;
75
+ --ama-end: 249, 115, 22;
76
+ --ink-900: #111827;
77
+ --surface-0: #ffffff;
78
+ --bd-subtle: #e5e7eb;
79
+ }
80
+
81
+ .assets-filter-toolbar {
82
+ padding: 1rem 1rem 0.5rem 1rem;
83
+ display: flex;
84
+ justify-content: flex-start;
85
+ align-items: center;
86
+ }
87
+
88
+ .asset-tabs {
89
+ display: inline-flex;
90
+ gap: 6px;
91
+ padding: 4px;
92
+ border-radius: 12px;
93
+ background: var(--surface-0);
94
+ border: 1px solid var(--bd-subtle);
95
+ box-shadow: 0 2px 8px rgba(0, 0, 0, .04);
96
  }
97
+
98
+ .asset-tab {
99
+ min-width: 54px;
100
+ height: 32px;
101
+ padding: 0 10px;
102
+ border-radius: 8px;
103
+ border: 1px solid transparent;
104
+ background: transparent;
105
+ color: var(--ink-900);
106
+ font-weight: 800;
107
+ letter-spacing: .02em;
108
+ cursor: pointer;
109
+ transition: all .2s ease;
110
+ }
111
+
112
+ .asset-tab:hover {
113
+ color: rgb(var(--ama-end));
114
+ border-color: rgba(var(--ama-end), .28);
115
+ }
116
+
117
+ .asset-tab.is-active {
118
+ color: #fff;
119
+ border: none;
120
+ background: linear-gradient(90deg, rgb(var(--ama-start)), rgb(var(--ama-end)));
121
+ box-shadow: 0 0 0 1px rgba(var(--ama-end), .22), 0 8px 18px rgba(var(--ama-end), .22);
122
  }
123
  </style>
src/components/CompareChartE.vue CHANGED
@@ -27,11 +27,11 @@ const ASSET_CUTOFF = {
27
  };
28
 
29
  const AGENT_COLOR_MAP = {
30
- HedgeFundAgent: '#6D4CFE',
31
- DeepFundAgent: '#FF6B6B',
32
- TradeAgent: '#10B981',
33
- InvestorAgent: '#F59E0B'
34
- }
35
 
36
  // pick color by agent name + index (from agentColorIndex map)
37
  function getAgentColor(agent, idx = 0) {
@@ -187,6 +187,10 @@ export default defineComponent({
187
  },
188
  mounted(){ this.$nextTick(() => this.rebuild()) },
189
  methods: {
 
 
 
 
190
  async getAll(){
191
  let all = getAllDecisions() || []
192
  if (!all.length) {
@@ -241,7 +245,7 @@ export default defineComponent({
241
  // NEW: convert to % mode if requested
242
  if (this.mode === 'pct') points = toPct(points)
243
 
244
- const name = `${agent} · ${sel.model} · ${cfg.label}`
245
  legend.push(name)
246
  series.push({
247
  name,
@@ -277,7 +281,7 @@ export default defineComponent({
277
  : Number(v ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
278
  return [
279
  `<div style="font-weight:600">${sel.agent_name}</div>`,
280
- sel.model ? `<div style="opacity:.8">${sel.model}</div>` : '',
281
  `<div style="opacity:.8">${sel.asset}</div>`,
282
  `<div style="margin-top:4px">${val}</div>`
283
  ].join('')
@@ -353,12 +357,17 @@ export default defineComponent({
353
  }
354
 
355
  this.option = {
 
 
356
  animation: true,
357
  locale: 'en',
358
- grid: { left: 64, right: 200, top: 8, bottom: 52 },
359
  tooltip: {
360
  trigger: 'axis',
361
  axisPointer: { type: 'line' },
 
 
 
 
362
  // NEW: format per mode
363
  valueFormatter: v => {
364
  if (typeof v !== 'number') return v
@@ -371,21 +380,27 @@ export default defineComponent({
371
  xAxis: {
372
  type: 'time',
373
  axisLabel: {
 
 
374
  formatter: (value) => {
375
  const date = new Date(value);
376
  return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
377
  }
378
- }
 
 
379
  },
380
- yAxis: this.mode === 'pct'
381
- ? {
382
- type: 'value', scale: true,
383
- axisLabel: { formatter: v => `${Number(v).toLocaleString(undefined, { maximumFractionDigits: 0 })}%` }
384
- }
385
- : {
386
- type: 'value', scale: true,
387
- axisLabel: { formatter: v => Number(v).toLocaleString(undefined, {style:'currency', currency:'USD', maximumFractionDigits:0 }) }
388
  },
 
 
389
  dataZoom: [{ type: 'inside', throttle: 50 }, { type: 'slider', height: 14, bottom: 36 }],
390
  series
391
  }
@@ -395,6 +410,21 @@ export default defineComponent({
395
  </script>
396
 
397
  <style scoped>
398
- .chart-wrap { width: 100%; }
 
 
 
 
 
 
 
 
399
  .h-96 { height: 24rem; }
 
 
 
 
 
 
 
400
  </style>
 
27
  };
28
 
29
  const AGENT_COLOR_MAP = {
30
+ HedgeFundAgent: '#3A0CA3', // violet-blue
31
+ DeepFundAgent: '#F72585', // magenta
32
+ TradeAgent: '#00BFA6', // teal
33
+ InvestorAgent: '#FFB703', // golden
34
+ };
35
 
36
  // pick color by agent name + index (from agentColorIndex map)
37
  function getAgentColor(agent, idx = 0) {
 
187
  },
188
  mounted(){ this.$nextTick(() => this.rebuild()) },
189
  methods: {
190
+ formatModelName(model) {
191
+ if (!model) return ''
192
+ return model.replace(/_?\d{8}$/, '')
193
+ },
194
  async getAll(){
195
  let all = getAllDecisions() || []
196
  if (!all.length) {
 
245
  // NEW: convert to % mode if requested
246
  if (this.mode === 'pct') points = toPct(points)
247
 
248
+ const name = `${agent} · ${this.formatModelName(sel.model)}`
249
  legend.push(name)
250
  series.push({
251
  name,
 
281
  : Number(v ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
282
  return [
283
  `<div style="font-weight:600">${sel.agent_name}</div>`,
284
+ sel.model ? `<div style="opacity:.8">${this.formatModelName(sel.model)}</div>` : '',
285
  `<div style="opacity:.8">${sel.asset}</div>`,
286
  `<div style="margin-top:4px">${val}</div>`
287
  ].join('')
 
357
  }
358
 
359
  this.option = {
360
+ backgroundColor: 'transparent',
361
+ grid: { left: 60, right: 160, top: 20, bottom: 60 },
362
  animation: true,
363
  locale: 'en',
 
364
  tooltip: {
365
  trigger: 'axis',
366
  axisPointer: { type: 'line' },
367
+ backgroundColor: 'rgba(255,255,255,0.9)',
368
+ borderColor: 'rgba(0,0,0,0.1)',
369
+ textStyle: { color: '#111', fontWeight: 600, fontSize: 13 },
370
+ extraCssText: 'box-shadow: 0 4px 10px rgba(0,0,0,0.08); backdrop-filter: blur(6px);',
371
  // NEW: format per mode
372
  valueFormatter: v => {
373
  if (typeof v !== 'number') return v
 
380
  xAxis: {
381
  type: 'time',
382
  axisLabel: {
383
+ fontWeight: 600,
384
+ color: '#333',
385
  formatter: (value) => {
386
  const date = new Date(value);
387
  return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
388
  }
389
+ },
390
+ axisLine: { lineStyle: { color: 'rgba(0,0,0,0.2)' } },
391
+ splitLine: { show: false }
392
  },
393
+ yAxis: {
394
+ type: 'value',
395
+ scale: true,
396
+ axisLabel: {
397
+ color: '#444',
398
+ formatter: (v) => this.mode === 'pct'
399
+ ? `${v.toFixed(0)}%`
400
+ : v.toLocaleString(undefined, { style:'currency', currency:'USD', maximumFractionDigits:0 })
401
  },
402
+ splitLine: { lineStyle: { color: 'rgba(0,0,0,0.05)' } }
403
+ },
404
  dataZoom: [{ type: 'inside', throttle: 50 }, { type: 'slider', height: 14, bottom: 36 }],
405
  series
406
  }
 
410
  </script>
411
 
412
  <style scoped>
413
+ .chart-wrap {
414
+ width: 100%;
415
+ background: linear-gradient(180deg, #ffffff 0%, #f7f9fb 100%);
416
+ border: 1px solid rgba(0,0,0,0.05);
417
+ border-radius: 16px;
418
+ box-shadow: 0 4px 16px rgba(0,0,0,0.04);
419
+ overflow: hidden;
420
+ padding: 10px;
421
+ }
422
  .h-96 { height: 24rem; }
423
+
424
+ :deep(.echarts-tooltip) {
425
+ font-family: 'Inter', sans-serif;
426
+ backdrop-filter: blur(8px);
427
+ border-radius: 8px;
428
+ border: 1px solid rgba(0,0,0,0.1);
429
+ }
430
  </style>
src/components/Footer.vue DELETED
@@ -1,98 +0,0 @@
1
- <template>
2
- <div class="page-header">
3
- <div class="logos-container">
4
- <span class="collaborators-text">Collaborators:</span>
5
- <div class="logo-item">
6
- <img src="../assets/images/companies_images/logofinai.png" alt="The Fin AI" class="logo-image" />
7
- </div>
8
- <div class="logo-item">
9
- <img src="../assets/images/companies_images/nactemlogo.png" alt="NaCTeM" class="logo-image" />
10
- </div>
11
- <div class="logo-item">
12
- <img src="../assets/images/companies_images/paalai_logo.png" alt="PAAL AI" class="logo-image" />
13
- </div>
14
- </div>
15
- </div>
16
- </template>
17
-
18
- <style scoped>
19
- .page-header {
20
- position: fixed;
21
- bottom: 0;
22
- left: 0;
23
- right: 0;
24
- display: flex;
25
- flex-direction: row;
26
- align-items: center;
27
- justify-content: center;
28
- gap: 0.5rem;
29
- padding: 0.75rem 1rem;
30
- background: linear-gradient(135deg, #f8f9fb 0%, #ffffff 100%);
31
- border-top: 1px solid #e5e7eb;
32
- z-index: 100;
33
- }
34
-
35
- .logos-container {
36
- display: flex;
37
- align-items: center;
38
- justify-content: center;
39
- gap: 1rem;
40
- flex-wrap: wrap;
41
- margin: 0;
42
- }
43
-
44
- .collaborators-text {
45
- color: #6b7280;
46
- font-size: 0.95rem;
47
- }
48
-
49
- .logo-item {
50
- flex-shrink: 0;
51
- width: 67px;
52
- height: 23px;
53
- display: flex;
54
- align-items: center;
55
- justify-content: center;
56
- padding: 0.15rem;
57
- transition: transform 0.2s ease;
58
- }
59
-
60
- .logo-item:hover {
61
- transform: translateY(-2px);
62
- }
63
-
64
- .logo-image {
65
- max-width: 100%;
66
- max-height: 100%;
67
- object-fit: contain;
68
- filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
69
- }
70
-
71
- /* 响应式设计 */
72
- @media (max-width: 768px) {
73
- .page-header {
74
- padding: 0.5rem 0.75rem;
75
- gap: 0.35rem;
76
- }
77
-
78
- .logos-container {
79
- gap: 0.75rem;
80
- }
81
-
82
- .logo-item {
83
- width: 54px;
84
- height: 20px;
85
- }
86
- }
87
-
88
- @media (max-width: 480px) {
89
- .logos-container {
90
- gap: 0.5rem;
91
- }
92
-
93
- .logo-item {
94
- width: 47px;
95
- height: 17px;
96
- }
97
- }
98
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/FooterOpen.vue ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <footer class="page-footer">
3
+ <div class="footer-section">
4
+ <span class="footer-label">Presented By</span>
5
+ <div class="logos-row">
6
+ <div v-for="(logo, i) in presentedBy" :key="'p-' + i" class="logo-item">
7
+ <img :src="logo.src" :alt="logo.name" class="logo-image" />
8
+ </div>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="footer-section">
13
+ <span class="footer-label">Academic Collaborators</span>
14
+ <div class="logos-row">
15
+ <div v-for="(logo, i) in collaborators" :key="'c-' + i" class="logo-item">
16
+ <img :src="logo.src" :alt="logo.name" class="logo-image" />
17
+ </div>
18
+ </div>
19
+ </div>
20
+ </footer>
21
+ </template>
22
+
23
+ <script>
24
+ export default {
25
+ name: "PageFooter",
26
+ data() {
27
+ return {
28
+ presentedBy: [
29
+ { name: "DeepKin", src: new URL("../assets/images/companies_images/deepkin_logo.png", import.meta.url).href },
30
+ { name: "The Fin AI", src: new URL("../assets/images/companies_images/logofinai.png", import.meta.url).href },
31
+ { name: "NaCTeM", src: new URL("../assets/images/companies_images/nactemlogo.png", import.meta.url).href },
32
+ { name: "PAAL AI", src: new URL("../assets/images/companies_images/paalai_logo.png", import.meta.url).href },
33
+ ],
34
+ collaborators: [
35
+ { name: "Stevens Institute of Technology", src: new URL("../assets/images/companies_images/stevens.png", import.meta.url).href },
36
+ { name: "University of Florida", src: new URL("../assets/images/companies_images/florida.png", import.meta.url).href },
37
+ { name: "Columbia University", src: new URL("../assets/images/companies_images/columbia.png", import.meta.url).href },
38
+ { name: "Harvard University", src: new URL("../assets/images/companies_images/harvard.png", import.meta.url).href },
39
+ { name: "Université de Montréal", src: new URL("../assets/images/companies_images/montreal.png", import.meta.url).href },
40
+ { name: "Georgia Institute of Technology", src: new URL("../assets/images/companies_images/georgia.png", import.meta.url).href },
41
+ ],
42
+ };
43
+ },
44
+ };
45
+ </script>
46
+
47
+ <style scoped>
48
+ .page-footer {
49
+ position: fixed;
50
+ bottom: 0;
51
+ left: 0;
52
+ right: 0;
53
+ display: flex;
54
+ flex-direction: column;
55
+ align-items: center;
56
+ gap: 0.75rem;
57
+ padding: 1rem 1.5rem;
58
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9fb 100%);
59
+ border-top: 2px solid rgba(0, 0, 0, 0.05);
60
+ z-index: 100;
61
+ backdrop-filter: blur(6px);
62
+ }
63
+
64
+ .footer-section {
65
+ text-align: center;
66
+ display: flex;
67
+ flex-direction: column;
68
+ align-items: center;
69
+ gap: 0.4rem;
70
+ }
71
+
72
+ .footer-label {
73
+ font-weight: 700;
74
+ font-size: 0.9rem;
75
+ text-transform: uppercase;
76
+ letter-spacing: 0.04em;
77
+ background: linear-gradient(90deg, rgb(0, 0, 185), rgb(240, 0, 15));
78
+ -webkit-background-clip: text;
79
+ color: transparent;
80
+ }
81
+
82
+ .logos-row {
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ gap: 1.25rem;
87
+ flex-wrap: wrap;
88
+ margin-top: 0.25rem;
89
+ }
90
+
91
+ .logo-item {
92
+ width: 72px;
93
+ height: 26px;
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ transition: transform 0.25s ease;
98
+ }
99
+
100
+ .logo-item:hover {
101
+ transform: translateY(-2px) scale(1.03);
102
+ }
103
+
104
+ .logo-image {
105
+ max-width: 100%;
106
+ max-height: 100%;
107
+ object-fit: contain;
108
+ filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.2));
109
+ }
110
+
111
+ /* Responsive */
112
+ @media (max-width: 768px) {
113
+ .page-footer {
114
+ padding: 0.75rem 1rem;
115
+ gap: 0.5rem;
116
+ }
117
+
118
+ .logo-item {
119
+ width: 60px;
120
+ height: 20px;
121
+ }
122
+ }
123
+
124
+ @media (max-width: 480px) {
125
+ .footer-label {
126
+ font-size: 0.8rem;
127
+ }
128
+
129
+ .logos-row {
130
+ gap: 0.75rem;
131
+ }
132
+
133
+ .logo-item {
134
+ width: 52px;
135
+ height: 18px;
136
+ }
137
+ }
138
+ </style>
src/components/Header.vue CHANGED
@@ -8,7 +8,7 @@
8
  <div class="menu-container">
9
  <span class="menu-item" @click="navigateTo('/live')">Live</span>
10
  <span class="menu-item" @click="navigateTo('/leadboard')">Leadboard</span>
11
- <span class="menu-item" @click="navigateTo('/add-asset')">Agent Arena</span>
12
  </div>
13
  </div>
14
  </template>
@@ -117,4 +117,4 @@ export default {
117
  height: 17px;
118
  }
119
  }
120
- </style>
 
8
  <div class="menu-container">
9
  <span class="menu-item" @click="navigateTo('/live')">Live</span>
10
  <span class="menu-item" @click="navigateTo('/leadboard')">Leadboard</span>
11
+ <span class="menu-item" @click="navigateTo('/add-asset')">Join Arena!</span>
12
  </div>
13
  </div>
14
  </template>
 
117
  height: 17px;
118
  }
119
  }
120
+ </style>
src/components/HeaderOpen.vue ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <header class="arena-header">
3
+ <div class="bar">
4
+ <!-- Title (left) -->
5
+ <button class="arena-title" aria-label="Agent Market Arena" @click="navigateTo('/')">
6
+ Agent Market Arena
7
+ </button>
8
+
9
+ <!-- Tabs (right) -->
10
+ <nav class="menu" aria-label="Primary">
11
+ <span
12
+ class="menu-item"
13
+ :class="{ active: isActive('/live') }"
14
+ @click="navigateTo('/live')"
15
+ >Live Arena</span>
16
+
17
+ <span
18
+ class="menu-item"
19
+ :class="{ active: isActive('/leaderboard') }"
20
+ @click="navigateTo('/leaderboard')"
21
+ >Leaderboard</span>
22
+
23
+ <span
24
+ class="menu-item"
25
+ :class="{ active: isActive('/add-asset') }"
26
+ @click="navigateTo('/add-asset')"
27
+ >Join Arena!</span>
28
+ </nav>
29
+ </div>
30
+
31
+ <!-- AMA gradient hairline -->
32
+ <div class="ama-gradient-rule" />
33
+ </header>
34
+ </template>
35
+
36
+ <script>
37
+ export default {
38
+ name: 'ArenaHeader',
39
+ methods: {
40
+ navigateTo(path) { this.$router.push(path) },
41
+ isActive(path) { return this.$route?.path?.startsWith(path) }
42
+ }
43
+ }
44
+ </script>
45
+
46
+ <!-- GLOBAL (UNSCOPED) — brand tokens must NOT be scoped -->
47
+ <style>
48
+ :root{
49
+ /* AMA brand gradient: rgb(0,0,185) → rgb(240,0,15) */
50
+ --ama-start: 0, 0, 185;
51
+ --ama-end: 240, 0, 15;
52
+
53
+ /* Podium palette (kept for future use) */
54
+ --gold: #D4AF37;
55
+ --silver: #C0C0C0;
56
+ --bronze: #CD7F32;
57
+ }
58
+ </style>
59
+
60
+ <!-- COMPONENT STYLES -->
61
+ <style scoped>
62
+ .arena-header{
63
+ position: sticky; top: 0; z-index: 50;
64
+ background: linear-gradient(135deg,#fbfcff 0%,#ffffff 100%);
65
+ border-bottom: 1px solid #e5e7eb;
66
+ backdrop-filter: blur(8px);
67
+ }
68
+
69
+ .bar{
70
+ max-width: 1200px;
71
+ margin: 0 auto;
72
+ padding: 16px 16px 12px;
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: space-between;
76
+ gap: 16px;
77
+ }
78
+
79
+ /* Title with AMA gradient text */
80
+ .arena-title{
81
+ all: unset;
82
+ cursor: pointer;
83
+ font-size: 26px;
84
+ font-weight: 900;
85
+ letter-spacing: -0.01em;
86
+ background-image: linear-gradient(90deg, rgb(var(--ama-start)), rgb(var(--ama-end)));
87
+ -webkit-background-clip: text;
88
+ background-clip: text;
89
+ color: transparent;
90
+ }
91
+
92
+ /* Tabs (right) */
93
+ .menu{
94
+ display: flex; gap: 28px; align-items: center; flex-wrap: wrap;
95
+ }
96
+
97
+ .menu-item{
98
+ cursor: pointer; position: relative;
99
+ font-size: 22px; font-weight: 700; color: #1f2937;
100
+ padding-bottom: 4px; transition: color .2s ease;
101
+ }
102
+
103
+ /* hover color + underline */
104
+ .menu-item:hover{ color: rgb(var(--ama-end)); }
105
+ .menu-item::after{
106
+ content:''; position:absolute; left:0; bottom:0; height:2px; width:0%;
107
+ background-image: linear-gradient(90deg, rgb(var(--ama-start)), rgb(var(--ama-end)));
108
+ transition: width .25s ease;
109
+ }
110
+ .menu-item:hover::after{ width:100%; }
111
+
112
+ /* active = gradient text + full underline */
113
+ .menu-item.active{
114
+ background-image: linear-gradient(90deg, rgb(var(--ama-start)), rgb(var(--ama-end)));
115
+ -webkit-background-clip:text; background-clip:text; color:transparent;
116
+ }
117
+ .menu-item.active::after{ width:100%; }
118
+
119
+ /* bottom hairline */
120
+ .ama-gradient-rule{
121
+ width:100%; height:3px; border-radius:2px;
122
+ background-image: linear-gradient(
123
+ 90deg,
124
+ rgba(var(--ama-start),0),
125
+ rgb(var(--ama-start)),
126
+ rgb(var(--ama-end)),
127
+ rgba(var(--ama-end),0)
128
+ );
129
+ }
130
+
131
+ /* responsive */
132
+ @media (max-width: 720px){
133
+ .bar{ flex-direction: column; align-items: stretch; }
134
+ .arena-title{ text-align: center; font-size: 22px; }
135
+ .menu{ justify-content: center; gap: 18px; }
136
+ .menu-item{ font-size: 18px; }
137
+ }
138
+ </style>
src/components/MiniEchart.vue ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div ref="chartContainer" class="echart-mini"></div>
3
+ </template>
4
+
5
+ <script>
6
+ import * as echarts from 'echarts/core'
7
+ import { LineChart } from 'echarts/charts'
8
+ import { GridComponent, TooltipComponent } from 'echarts/components'
9
+ import { CanvasRenderer } from 'echarts/renderers'
10
+ import { nextTick } from 'vue'
11
+
12
+ echarts.use([LineChart, GridComponent, TooltipComponent, CanvasRenderer])
13
+
14
+ export default {
15
+ name: 'MiniEchart',
16
+ props: {
17
+ data: { type: Array, default: () => [] },
18
+ color: { type: String, default: '#22c55e' }
19
+ },
20
+ data() {
21
+ return {
22
+ chart: null,
23
+ resizeObserver: null
24
+ }
25
+ },
26
+ mounted() {
27
+ console.log('[MiniEchart] Mounted with data:', this.data?.length || 0, 'items')
28
+ nextTick(() => this.initChart())
29
+ },
30
+ beforeUnmount() {
31
+ if (this.resizeObserver) {
32
+ this.resizeObserver.disconnect()
33
+ this.resizeObserver = null
34
+ }
35
+ if (this.chart) {
36
+ this.chart.dispose()
37
+ this.chart = null
38
+ }
39
+ },
40
+ watch: {
41
+ data(newVal) {
42
+ if (this.chart && Array.isArray(newVal)) {
43
+ this.updateChart(newVal)
44
+ }
45
+ }
46
+ },
47
+ methods: {
48
+ initChart() {
49
+ const el = this.$refs.chartContainer
50
+ console.log('[MiniEchart] initChart - el:', el)
51
+
52
+ if (!el) {
53
+ console.log('[MiniEchart] No element found')
54
+ return
55
+ }
56
+
57
+ const { clientWidth: w, clientHeight: h } = el
58
+ console.log('[MiniEchart] Element size:', w, 'x', h)
59
+
60
+ if (!w || !h) {
61
+ console.log('[MiniEchart] Size is 0, will retry on next tick')
62
+ setTimeout(() => this.initChart(), 100)
63
+ return
64
+ }
65
+
66
+ console.log('[MiniEchart] Initializing ECharts...')
67
+ this.chart = echarts.init(el, null, { renderer: 'canvas' })
68
+ console.log('[MiniEchart] Chart initialized, rendering data:', this.data?.length || 0)
69
+
70
+ this.renderChart(this.data)
71
+
72
+ // Listen for container resize
73
+ this.resizeObserver = new ResizeObserver(() => {
74
+ if (this.chart) {
75
+ this.chart.resize()
76
+ }
77
+ })
78
+ this.resizeObserver.observe(el)
79
+ },
80
+ renderChart(arr) {
81
+ if (!this.chart) return
82
+
83
+ const d = Array.isArray(arr) ? arr : []
84
+ console.log('[MiniEchart] Rendering chart with', d.length, 'data points')
85
+
86
+ this.chart.setOption({
87
+ animation: true,
88
+ grid: { left: 0, right: 0, top: 0, bottom: 0 },
89
+ xAxis: {
90
+ type: 'category',
91
+ show: false,
92
+ boundaryGap: false,
93
+ data: d.map((_, i) => i)
94
+ },
95
+ yAxis: {
96
+ type: 'value',
97
+ show: false,
98
+ scale: true
99
+ },
100
+ tooltip: { show: false },
101
+ color: [this.color || '#22c55e'],
102
+ series: [{
103
+ type: 'line',
104
+ data: d,
105
+ showSymbol: false,
106
+ smooth: 0.35,
107
+ lineStyle: { width: 2 },
108
+ areaStyle: { opacity: 0.15 }
109
+ }]
110
+ })
111
+ },
112
+ updateChart(newData) {
113
+ if (!this.chart) return
114
+
115
+ const d = Array.isArray(newData) ? newData : []
116
+ this.chart.setOption({
117
+ xAxis: { data: d.map((_, i) => i) },
118
+ series: [{ data: d }]
119
+ })
120
+ }
121
+ }
122
+ }
123
+ </script>
124
+
125
+ <style scoped>
126
+ .echart-mini {
127
+ width: 100%;
128
+ height: 100%;
129
+ }
130
+ </style>
131
+
src/lib/autoRefresh.js ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { dataService } from './dataService.js'
2
+
3
+ /**
4
+ * 自动刷新服务
5
+ * 每天美东时间早上5点自动从数据库刷新数据
6
+ */
7
+ class AutoRefreshService {
8
+ constructor() {
9
+ this.timerId = null
10
+ this.targetHour = 5 // 美东早上5点
11
+ this.isRunning = false
12
+ }
13
+
14
+ /**
15
+ * 计算下一次美东早上5点的时间
16
+ */
17
+ getNextRefreshTime() {
18
+ const now = new Date()
19
+
20
+ // 获取美东时区的当前时间
21
+ const etOptions = { timeZone: 'America/New_York', hour12: false }
22
+ const etTimeString = now.toLocaleString('en-US', etOptions)
23
+ const etDate = new Date(etTimeString)
24
+
25
+ // 设置为美东早上5点
26
+ const nextRefresh = new Date(etDate)
27
+ nextRefresh.setHours(this.targetHour, 0, 0, 0)
28
+
29
+ // 如果已经过了今天的5点,设置为明天
30
+ if (etDate.getTime() >= nextRefresh.getTime()) {
31
+ nextRefresh.setDate(nextRefresh.getDate() + 1)
32
+ }
33
+
34
+ // 转换回本地时区
35
+ const etTimestamp = nextRefresh.getTime()
36
+ const etOffset = this.getETOffset(nextRefresh)
37
+ const localOffset = now.getTimezoneOffset() * 60 * 1000
38
+ const localTimestamp = etTimestamp - etOffset + localOffset
39
+
40
+ return new Date(localTimestamp)
41
+ }
42
+
43
+ /**
44
+ * 获取美东时区的偏移量(毫秒)
45
+ */
46
+ getETOffset(date) {
47
+ const etString = date.toLocaleString('en-US', {
48
+ timeZone: 'America/New_York',
49
+ timeZoneName: 'short'
50
+ })
51
+ // EST is UTC-5, EDT is UTC-4
52
+ const isDST = etString.includes('EDT')
53
+ return isDST ? -4 * 60 * 60 * 1000 : -5 * 60 * 60 * 1000
54
+ }
55
+
56
+ /**
57
+ * 执行刷新
58
+ */
59
+ async refresh() {
60
+ console.log('[AutoRefresh] Refreshing data at', new Date().toLocaleString())
61
+ try {
62
+ await dataService.load(true)
63
+ console.log('[AutoRefresh] Data refreshed successfully')
64
+ } catch (error) {
65
+ console.error('[AutoRefresh] Failed to refresh data:', error)
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 调度下一次刷新
71
+ */
72
+ scheduleNext() {
73
+ // 清除现有的定时器
74
+ if (this.timerId) {
75
+ clearTimeout(this.timerId)
76
+ this.timerId = null
77
+ }
78
+
79
+ const nextRefresh = this.getNextRefreshTime()
80
+ const now = new Date()
81
+ const delay = nextRefresh.getTime() - now.getTime()
82
+
83
+ console.log('[AutoRefresh] Next refresh scheduled at:', nextRefresh.toLocaleString())
84
+ console.log('[AutoRefresh] Time until next refresh:', Math.round(delay / 1000 / 60), 'minutes')
85
+
86
+ this.timerId = setTimeout(async () => {
87
+ await this.refresh()
88
+ this.scheduleNext() // 刷新后调度下一次
89
+ }, delay)
90
+ }
91
+
92
+ /**
93
+ * 启动自动刷新
94
+ */
95
+ start() {
96
+ if (this.isRunning) {
97
+ console.log('[AutoRefresh] Already running')
98
+ return
99
+ }
100
+
101
+ console.log('[AutoRefresh] Starting auto-refresh service')
102
+ this.isRunning = true
103
+ this.scheduleNext()
104
+ }
105
+
106
+ /**
107
+ * 停止自动刷新
108
+ */
109
+ stop() {
110
+ console.log('[AutoRefresh] Stopping auto-refresh service')
111
+ if (this.timerId) {
112
+ clearTimeout(this.timerId)
113
+ this.timerId = null
114
+ }
115
+ this.isRunning = false
116
+ }
117
+
118
+ /**
119
+ * 手动触发刷新(不影响定时调度)
120
+ */
121
+ async triggerManualRefresh() {
122
+ console.log('[AutoRefresh] Manual refresh triggered')
123
+ await this.refresh()
124
+ }
125
+ }
126
+
127
+ // 创建单例
128
+ export const autoRefreshService = new AutoRefreshService()
129
+
src/main.js CHANGED
@@ -1,6 +1,7 @@
1
  import { createApp } from 'vue'
2
  import App from './App.vue'
3
  import router from './router/index.js'
 
4
 
5
  // PrimeVue setup
6
  import PrimeVue from 'primevue/config'
@@ -86,4 +87,7 @@ try {
86
  if (pubKey) { emailjs.init(pubKey) }
87
  } catch (_) { }
88
 
 
 
 
89
  app.mount('#app')
 
1
  import { createApp } from 'vue'
2
  import App from './App.vue'
3
  import router from './router/index.js'
4
+ import { autoRefreshService } from './lib/autoRefresh.js'
5
 
6
  // PrimeVue setup
7
  import PrimeVue from 'primevue/config'
 
87
  if (pubKey) { emailjs.init(pubKey) }
88
  } catch (_) { }
89
 
90
+ // Start auto-refresh service (daily at 5 AM ET)
91
+ autoRefreshService.start()
92
+
93
  app.mount('#app')
src/pages/Main.vue CHANGED
@@ -1,14 +1,16 @@
1
  <template>
2
  <div class="main-container">
3
  <Header />
4
- <router-view />
 
 
5
  <Footer />
6
  </div>
7
  </template>
8
 
9
  <script>
10
- import Header from '../components/Header.vue'
11
- import Footer from '../components/Footer.vue'
12
  export default {
13
  name: 'Main',
14
  components: {
@@ -23,5 +25,12 @@ export default {
23
  width: 100%;
24
  height: 100%;
25
  }
 
 
 
 
 
 
 
26
  </style>
27
 
 
1
  <template>
2
  <div class="main-container">
3
  <Header />
4
+ <main class="main-content">
5
+ <router-view />
6
+ </main>
7
  <Footer />
8
  </div>
9
  </template>
10
 
11
  <script>
12
+ import Header from '../components/HeaderOpen.vue'
13
+ import Footer from '../components/FooterOpen.vue'
14
  export default {
15
  name: 'Main',
16
  components: {
 
25
  width: 100%;
26
  height: 100%;
27
  }
28
+ .main-content {
29
+ flex: 1; /* take all remaining vertical space */
30
+ padding: 2rem 2.5rem 120px; /* bottom padding prevents overlap with footer */
31
+ box-sizing: border-box;
32
+ overflow-x: hidden;
33
+ background: transparent;
34
+ }
35
  </style>
36
 
src/router/index.js CHANGED
@@ -1,8 +1,9 @@
1
  import { createRouter, createWebHistory } from 'vue-router'
2
  import Main from '../pages/Main.vue'
3
- import LeadboardView from '../views/LeadboardView.vue'
4
  import LiveView from '../views/LiveView.vue'
5
  import AddAssetsView from '../views/AddAssetView.vue'
 
6
  import AssetRequestsView from '../views/AssetRequestsView.vue'
7
  import { dataService } from '../lib/dataService.js'
8
 
@@ -11,11 +12,11 @@ const routes = [
11
  path: '/',
12
  name: 'main',
13
  component: Main,
14
- redirect: '/leadboard',
15
  children: [
16
- { path: '/leadboard', name: 'leadboard', component: LeadboardView },
17
  { path: '/live', name: 'live', component: LiveView },
18
- { path: '/add-asset', name: 'add-asset', component: AddAssetsView },
19
  { path: '/asset-requests', name: 'asset-requests', component: AssetRequestsView },
20
  ]
21
  }
 
1
  import { createRouter, createWebHistory } from 'vue-router'
2
  import Main from '../pages/Main.vue'
3
+ import LeaderboardView from '../views/LeaderboardView.vue'
4
  import LiveView from '../views/LiveView.vue'
5
  import AddAssetsView from '../views/AddAssetView.vue'
6
+ import RequestView from '../views/RequestView.vue'
7
  import AssetRequestsView from '../views/AssetRequestsView.vue'
8
  import { dataService } from '../lib/dataService.js'
9
 
 
12
  path: '/',
13
  name: 'main',
14
  component: Main,
15
+ redirect: '/live',
16
  children: [
17
+ { path: '/leaderboard', name: 'leadboard', component: LeaderboardView },
18
  { path: '/live', name: 'live', component: LiveView },
19
+ { path: '/add-asset', name: 'add-asset', component: RequestView },
20
  { path: '/asset-requests', name: 'asset-requests', component: AssetRequestsView },
21
  ]
22
  }
src/styles/globals.css ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* globals.css */
2
+ :root{
3
+ --ama-start: 0 0 185; /* rgb */
4
+ --ama-end: 240 0 15;
5
+ --gold: #D4AF37;
6
+ --silver: #C0C0C0;
7
+ --bronze: #CD7F32;
8
+ }
9
+
10
+ @layer utilities {
11
+ /* Text gradient utility for AMA */
12
+ .text-ama {
13
+ background-image: linear-gradient(90deg, rgb(var(--ama-start)), rgb(var(--ama-end)));
14
+ -webkit-background-clip: text;
15
+ background-clip: text;
16
+ color: transparent;
17
+ }
18
+
19
+ /* Button background using AMA gradient */
20
+ .bg-ama {
21
+ background-image: linear-gradient(90deg, rgb(var(--ama-start)), rgb(var(--ama-end)));
22
+ }
23
+
24
+ /* Hairline divider using AMA gradient */
25
+ .rule-ama {
26
+ height: 2px;
27
+ background-image: linear-gradient(90deg, rgba(var(--ama-start), .0), rgb(var(--ama-start)), rgb(var(--ama-end)), rgba(var(--ama-end), .0));
28
+ }
29
+
30
+ /* Subtle gradient border (works on white cards) */
31
+ .border-ama {
32
+ border: 1px solid transparent;
33
+ background:
34
+ linear-gradient(#fff,#fff) padding-box,
35
+ linear-gradient(90deg, rgb(var(--ama-start)), rgb(var(--ama-end))) border-box;
36
+ }
37
+ }
src/views/AddAssetView.vue CHANGED
@@ -1,7 +1,32 @@
1
  <template>
2
  <div class="page-container">
3
  <div class="title-container">
4
- <span class="main-title">Agent Arena</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  </div>
6
 
7
  <div class="cards-container">
@@ -28,7 +53,7 @@
28
  <pre class="code-block">{
29
  "date": "2025-10-24",
30
  "price": {"BTC": 67890.50},
31
- "news": {"BTC": "Bitcoin news..."},
32
  "model": "gpt-4o",
33
  "history_price": {
34
  "BTC": [
@@ -392,6 +417,90 @@ export default {
392
  color: #1f1f33;
393
  }
394
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  .cards-container {
396
  display: grid;
397
  grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
 
1
  <template>
2
  <div class="page-container">
3
  <div class="title-container">
4
+ <span class="main-title">Join Arena!</span>
5
+ <div class="agents-info">
6
+ <div class="agent-info-item">
7
+ <span class="agent-name">InvestorAgent</span>
8
+ <div class="agent-links">
9
+ <a href="https://arxiv.org/abs/2412.18174" target="_blank" rel="noopener noreferrer" class="agent-link">
10
+ <i class="pi pi-file-pdf"></i>
11
+ <span>Paper</span>
12
+ </a>
13
+ </div>
14
+ </div>
15
+ <div class="agent-info-divider">|</div>
16
+ <div class="agent-info-item">
17
+ <span class="agent-name">TradeAgent</span>
18
+ <div class="agent-links">
19
+ <a href="https://arxiv.org/abs/2412.20138" target="_blank" rel="noopener noreferrer" class="agent-link">
20
+ <i class="pi pi-file-pdf"></i>
21
+ <span>Paper</span>
22
+ </a>
23
+ <a href="https://github.com/TauricResearch/TradingAgents" target="_blank" rel="noopener noreferrer" class="agent-link">
24
+ <i class="pi pi-github"></i>
25
+ <span>GitHub</span>
26
+ </a>
27
+ </div>
28
+ </div>
29
+ </div>
30
  </div>
31
 
32
  <div class="cards-container">
 
53
  <pre class="code-block">{
54
  "date": "2025-10-24",
55
  "price": {"BTC": 67890.50},
56
+ "news": {"BTC": ["Bitcoin news 1...", "Bitcoin news 2...", "Bitcoin news 3..."]},
57
  "model": "gpt-4o",
58
  "history_price": {
59
  "BTC": [
 
417
  color: #1f1f33;
418
  }
419
 
420
+ .agents-info {
421
+ margin-top: 1rem;
422
+ display: flex;
423
+ align-items: center;
424
+ justify-content: center;
425
+ gap: 1.5rem;
426
+ flex-wrap: wrap;
427
+ }
428
+
429
+ .agent-info-item {
430
+ display: flex;
431
+ flex-direction: column;
432
+ align-items: center;
433
+ gap: 0.5rem;
434
+ }
435
+
436
+ .agent-name {
437
+ font-size: 0.9rem;
438
+ font-weight: 600;
439
+ color: #1f2937;
440
+ letter-spacing: 0.02em;
441
+ }
442
+
443
+ .agent-links {
444
+ display: flex;
445
+ align-items: center;
446
+ gap: 0.75rem;
447
+ }
448
+
449
+ .agent-link {
450
+ display: flex;
451
+ align-items: center;
452
+ gap: 0.35rem;
453
+ padding: 0.35rem 0.75rem;
454
+ background: #f3f4f6;
455
+ border: 1px solid #e5e7eb;
456
+ border-radius: 6px;
457
+ color: #4b5563;
458
+ text-decoration: none;
459
+ font-size: 0.85rem;
460
+ font-weight: 500;
461
+ transition: all 0.2s;
462
+ cursor: pointer;
463
+ position: relative;
464
+ z-index: 10;
465
+ }
466
+
467
+ .agent-link:hover {
468
+ background: #e5e7eb;
469
+ border-color: #d1d5db;
470
+ color: #1f2937;
471
+ transform: translateY(-1px);
472
+ }
473
+
474
+ .agent-link i {
475
+ font-size: 0.9rem;
476
+ }
477
+
478
+ .agent-link:has(.pi-file-pdf) {
479
+ color: #dc2626;
480
+ }
481
+
482
+ .agent-link:has(.pi-file-pdf):hover {
483
+ background: #fee2e2;
484
+ border-color: #fecaca;
485
+ color: #b91c1c;
486
+ }
487
+
488
+ .agent-link:has(.pi-github) {
489
+ color: #0f172a;
490
+ }
491
+
492
+ .agent-link:has(.pi-github):hover {
493
+ background: #f1f5f9;
494
+ border-color: #e2e8f0;
495
+ color: #000;
496
+ }
497
+
498
+ .agent-info-divider {
499
+ color: #d1d5db;
500
+ font-size: 1.2rem;
501
+ font-weight: 300;
502
+ }
503
+
504
  .cards-container {
505
  display: grid;
506
  grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
src/views/{LeadboardView.vue → LeaderboardView.vue} RENAMED
@@ -1,8 +1,5 @@
1
  <template>
2
  <div>
3
- <div class="title-container">
4
- <span class="main-title">Agent Leadboard</span>
5
- </div>
6
  <AssetsFilter v-model="filters.assets" :assetOptions="assetOptions" />
7
  <div v-if="loading" class="loading-overlay">
8
  <div class="loading-box">
@@ -25,13 +22,12 @@
25
  <div class="flex gap-2 align-items-stretch">
26
  <!-- Desktop filter panel -->
27
  <div class="col-panel desktop-filter-panel" style="flex: 1; min-width: 280px;">
28
- <Card class="mb-2 card-full compact-card content-card">
29
  <template #title>
30
- <div class="mb-4 text-900" style="font-size: 20px; font-weight: 600">Filter Matrix</div>
31
- <Divider />
32
  </template>
33
  <template #content>
34
- <div>
35
  <AgentFilters v-model="filters" :nameOptions="nameOptions" :assetOptions="assetOptions" :modelOptions="modelOptions" :strategyOptions="strategyOptions" :dateBounds="dateBounds" />
36
  </div>
37
  </template>
@@ -75,7 +71,7 @@
75
  </div>
76
  <Dialog v-model:visible="compareVisible" modal header="Equity Curve Comparison" style="width: 90vw; max-width: 1200px">
77
  <div>
78
- <CompareChart :selected="selectedRows.map(r => ({ agent_name: r.agent_name, asset: r.asset, model: r.model, strategy: r.strategy, decision_ids: r.decision_ids || [] }))" :visible="compareVisible" />
79
  </div>
80
  </Dialog>
81
  <Dialog v-model:visible="requestAssetsVisible" modal header="Request Asset" style="width: 90vw; max-width: 400px">
@@ -91,7 +87,7 @@ import { dataService } from '../lib/dataService.js'
91
  import AgentTable from '../components/AgentTable.vue'
92
  import AgentFilters from '../components/AgentFilters.vue'
93
  import AssetsFilter from '../components/AssetsFilter.vue'
94
- import CompareChart from '../components/CompareChart.vue'
95
  import InputText from 'primevue/inputtext'
96
  import Dialog from 'primevue/dialog'
97
  import { countNonTradingDaysBetweenForAsset, countTradingDaysBetweenForAsset } from '../lib/marketCalendar.js'
@@ -100,8 +96,8 @@ import { STRATEGIES } from '../lib/strategies.js'
100
  import emailjs from 'emailjs-com'
101
 
102
  export default {
103
- name: 'LeadboardView',
104
- components: { AgentTable, AgentFilters, AssetsFilter, CompareChart, Dialog, InputText },
105
  data() {
106
  return {
107
  loading: true,
@@ -189,7 +185,8 @@ export default {
189
  this.filters.names = state.nameOptions.map(o => o.value)
190
  }
191
  if (!this.filters.assets.length && state.assetOptions.length) {
192
- this.filters.assets = state.assetOptions.map(o => o.value)
 
193
  }
194
  if (!this.filters.models.length && state.modelOptions.length) {
195
  this.filters.models = state.modelOptions.map(o => o.value)
@@ -328,32 +325,69 @@ export default {
328
  </script>
329
 
330
  <style scoped>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  .page-wrapper {
332
  max-width: 1600px;
333
  margin: 0 auto;
334
- padding: 0 1rem 1rem 1rem;
 
335
  }
336
 
337
  .title-container {
338
  text-align: center;
 
339
  }
340
 
341
  .main-title {
342
  font-size: 2rem;
343
  letter-spacing: -0.02em;
344
  font-weight: 800;
345
- color: #1f1f33;
346
  }
347
  .loading-overlay {
348
  position: fixed;
349
  inset: 0;
350
- background: rgba(255, 255, 255, 0.85);
351
  z-index: 1000;
352
  display: flex;
353
  align-items: center;
354
  justify-content: center;
355
  }
356
- .loading-box { text-align: center; }
 
 
 
 
 
 
 
357
 
358
  .card--with-divider :deep(.p-card-title) {
359
  border-bottom: 1px solid var(--surface-200);
@@ -363,14 +397,80 @@ export default {
363
 
364
  /* equal-height for side-by-side cards */
365
  .col-panel { display: flex; }
366
- .card-full { width: 100%; display: flex; flex-direction: column; }
367
- .card-full :deep(.p-card-body) { display: flex; flex-direction: column; height: 100%; }
368
- .card-full :deep(.p-card-content) { flex: 1; display: flex; flex-direction: column; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
  /* compact spacing for higher information density */
371
- .compact-card :deep(.p-card-body) { padding: 0.75rem; }
372
- .compact-card :deep(.p-card-content) { padding-top: 0; padding-bottom: 0; overflow-y: auto; }
373
- .compact-card :deep(.p-card-title) { margin-bottom: 0.5rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
  /* datatable compact paddings */
376
  :deep(.p-datatable .p-datatable-header) { padding: 0.5rem 0.75rem; }
@@ -468,5 +568,55 @@ export default {
468
  :deep(.filter-drawer .p-drawer-content) {
469
  padding: 1rem;
470
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  </style>
472
 
 
1
  <template>
2
  <div>
 
 
 
3
  <AssetsFilter v-model="filters.assets" :assetOptions="assetOptions" />
4
  <div v-if="loading" class="loading-overlay">
5
  <div class="loading-box">
 
22
  <div class="flex gap-2 align-items-stretch">
23
  <!-- Desktop filter panel -->
24
  <div class="col-panel desktop-filter-panel" style="flex: 1; min-width: 280px;">
25
+ <Card class="mb-2 card-full compact-card content-card filter-matrix-card">
26
  <template #title>
27
+ <div class="filter-matrix-title">Filter Matrix</div>
 
28
  </template>
29
  <template #content>
30
+ <div class="filter-matrix-content">
31
  <AgentFilters v-model="filters" :nameOptions="nameOptions" :assetOptions="assetOptions" :modelOptions="modelOptions" :strategyOptions="strategyOptions" :dateBounds="dateBounds" />
32
  </div>
33
  </template>
 
71
  </div>
72
  <Dialog v-model:visible="compareVisible" modal header="Equity Curve Comparison" style="width: 90vw; max-width: 1200px">
73
  <div>
74
+ <CompareChartE :selected="selectedRows.map(r => ({ agent_name: r.agent_name, asset: r.asset, model: r.model, strategy: r.strategy, decision_ids: r.decision_ids || [] }))" :visible="compareVisible" :mode="'usd'" />
75
  </div>
76
  </Dialog>
77
  <Dialog v-model:visible="requestAssetsVisible" modal header="Request Asset" style="width: 90vw; max-width: 400px">
 
87
  import AgentTable from '../components/AgentTable.vue'
88
  import AgentFilters from '../components/AgentFilters.vue'
89
  import AssetsFilter from '../components/AssetsFilter.vue'
90
+ import CompareChartE from '../components/CompareChartE.vue'
91
  import InputText from 'primevue/inputtext'
92
  import Dialog from 'primevue/dialog'
93
  import { countNonTradingDaysBetweenForAsset, countTradingDaysBetweenForAsset } from '../lib/marketCalendar.js'
 
96
  import emailjs from 'emailjs-com'
97
 
98
  export default {
99
+ name: 'LeaderboardView',
100
+ components: { AgentTable, AgentFilters, AssetsFilter, CompareChartE, Dialog, InputText },
101
  data() {
102
  return {
103
  loading: true,
 
185
  this.filters.names = state.nameOptions.map(o => o.value)
186
  }
187
  if (!this.filters.assets.length && state.assetOptions.length) {
188
+ // Default to BTC (matching LiveView default)
189
+ this.filters.assets = ['BTC']
190
  }
191
  if (!this.filters.models.length && state.modelOptions.length) {
192
  this.filters.models = state.modelOptions.map(o => o.value)
 
325
  </script>
326
 
327
  <style scoped>
328
+ /* ===== AMA Color Variables (from LiveView) ===== */
329
+ :root {
330
+ --ama-start: 0,0,185;
331
+ --ama-mid: 123,44,191;
332
+ --ama-end: 240,0,15;
333
+ --gold: #d4af37;
334
+ --silver: #c0c0c0;
335
+ --bronze: #cd7f32;
336
+ --ink-900: #0f172a;
337
+ --ink-700: #334155;
338
+ --ink-600: #64748b;
339
+ --bd-soft: #E7ECF3;
340
+ --pos-fg: #0e7a3a;
341
+ --pos-bg: #f0fdf4;
342
+ --pos-br: #bbf7d0;
343
+ --neg-fg: #B91C1C;
344
+ --neg-bg: #fef2f2;
345
+ --neg-br: #fecaca;
346
+ }
347
+
348
+ /* ===== AMA Gradient ===== */
349
+ .ama-gradient {
350
+ background: linear-gradient(90deg, rgb(0,0,185), #7b2cbf 40%, #d946ef 70%, rgb(240,0,15));
351
+ -webkit-background-clip: text;
352
+ -webkit-text-fill-color: transparent;
353
+ background-clip: text;
354
+ }
355
+
356
  .page-wrapper {
357
  max-width: 1600px;
358
  margin: 0 auto;
359
+ padding: 16px 20px 60px;
360
+ background: #fff;
361
  }
362
 
363
  .title-container {
364
  text-align: center;
365
+ margin-bottom: 24px;
366
  }
367
 
368
  .main-title {
369
  font-size: 2rem;
370
  letter-spacing: -0.02em;
371
  font-weight: 800;
372
+ color: var(--ink-900);
373
  }
374
  .loading-overlay {
375
  position: fixed;
376
  inset: 0;
377
+ background: rgba(255, 255, 255, 0.95);
378
  z-index: 1000;
379
  display: flex;
380
  align-items: center;
381
  justify-content: center;
382
  }
383
+
384
+ .loading-box {
385
+ text-align: center;
386
+ padding: 32px;
387
+ background: #fff;
388
+ border-radius: 16px;
389
+ box-shadow: 0 10px 40px rgba(0,0,0,0.1);
390
+ }
391
 
392
  .card--with-divider :deep(.p-card-title) {
393
  border-bottom: 1px solid var(--surface-200);
 
397
 
398
  /* equal-height for side-by-side cards */
399
  .col-panel { display: flex; }
400
+ .card-full {
401
+ width: 100%;
402
+ display: flex;
403
+ flex-direction: column;
404
+ background: #ffffff;
405
+ border: 1px solid #e5e7eb;
406
+ border-radius: 16px;
407
+ box-shadow: 0 1px 2px rgba(0,0,0,0.02), 0 2px 6px rgba(0,0,0,0.04);
408
+ transition: all 0.2s ease;
409
+ }
410
+
411
+ .card-full:hover {
412
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.06);
413
+ transform: translateY(-1px);
414
+ }
415
+
416
+ /* Filter Matrix Card Special Styling */
417
+ .filter-matrix-card {
418
+ background: linear-gradient(135deg, #fefefe 0%, #fafbfc 100%);
419
+ border: 1px solid #e5e7eb;
420
+ box-shadow: 0 2px 8px rgba(0,0,0,0.03), 0 4px 16px rgba(0,0,0,0.04);
421
+ }
422
+
423
+ .filter-matrix-card:hover {
424
+ transform: none;
425
+ }
426
+
427
+ .filter-matrix-title {
428
+ font-size: 22px;
429
+ font-weight: 800;
430
+ letter-spacing: -0.02em;
431
+ background: linear-gradient(90deg, rgb(239, 68, 68), rgb(249, 115, 22));
432
+ -webkit-background-clip: text;
433
+ -webkit-text-fill-color: transparent;
434
+ background-clip: text;
435
+ margin-bottom: 8px;
436
+ padding-bottom: 12px;
437
+ border-bottom: 2px solid #f3f4f6;
438
+ }
439
+
440
+ .filter-matrix-content {
441
+ padding-top: 8px;
442
+ }
443
+
444
+ .card-full :deep(.p-card-body) {
445
+ display: flex;
446
+ flex-direction: column;
447
+ height: 100%;
448
+ }
449
+
450
+ .card-full :deep(.p-card-content) {
451
+ flex: 1;
452
+ display: flex;
453
+ flex-direction: column;
454
+ }
455
 
456
  /* compact spacing for higher information density */
457
+ .compact-card :deep(.p-card-body) {
458
+ padding: 20px;
459
+ }
460
+
461
+ .compact-card :deep(.p-card-content) {
462
+ padding-top: 0;
463
+ padding-bottom: 0;
464
+ overflow-y: auto;
465
+ }
466
+
467
+ .compact-card :deep(.p-card-title) {
468
+ margin-bottom: 16px;
469
+ font-size: 20px;
470
+ font-weight: 800;
471
+ letter-spacing: -0.02em;
472
+ color: var(--ink-900);
473
+ }
474
 
475
  /* datatable compact paddings */
476
  :deep(.p-datatable .p-datatable-header) { padding: 0.5rem 0.75rem; }
 
568
  :deep(.filter-drawer .p-drawer-content) {
569
  padding: 1rem;
570
  }
571
+
572
+ /* ===== Button Improvements ===== */
573
+ :deep(.p-button) {
574
+ font-weight: 700;
575
+ letter-spacing: -0.01em;
576
+ transition: all 0.2s ease;
577
+ }
578
+
579
+ :deep(.p-button:not(.p-button-text):not(.p-button-outlined):hover) {
580
+ transform: translateY(-1px);
581
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
582
+ }
583
+
584
+ /* ===== Empty State ===== */
585
+ .empty-offset > span {
586
+ color: var(--ink-600);
587
+ font-size: 16px;
588
+ }
589
+
590
+ /* ===== Divider ===== */
591
+ :deep(.p-divider) {
592
+ margin: 0.5rem 0;
593
+ border-color: var(--bd-soft);
594
+ }
595
+
596
+ /* ===== DataTable Improvements ===== */
597
+ :deep(.p-datatable) {
598
+ border-radius: 12px;
599
+ overflow: hidden;
600
+ }
601
+
602
+ :deep(.p-datatable .p-datatable-thead > tr > th) {
603
+ background: #F6F8FB;
604
+ color: var(--ink-700);
605
+ font-weight: 700;
606
+ border-color: var(--bd-soft);
607
+ }
608
+
609
+ :deep(.p-datatable .p-datatable-tbody > tr) {
610
+ transition: background 0.15s ease;
611
+ }
612
+
613
+ :deep(.p-datatable .p-datatable-tbody > tr:hover) {
614
+ background: #F6F8FB;
615
+ }
616
+
617
+ :deep(.p-datatable .p-datatable-tbody > tr > td) {
618
+ border-color: var(--bd-soft);
619
+ color: var(--ink-700);
620
+ }
621
  </style>
622
 
src/views/LiveView.vue CHANGED
@@ -1,22 +1,50 @@
1
  <template>
2
  <div class="live">
 
 
 
 
 
 
 
3
  <!-- Toolbar: assets + mode -->
4
  <header class="toolbar">
5
  <div class="toolbar__left">
6
- <AssetTabs v-model="asset" :ordered-assets="orderedAssets" />
 
 
 
 
 
 
 
 
 
 
 
7
  </div>
 
8
  <div class="toolbar__right">
9
- <button
10
- class="refresh-btn"
11
- @click="refreshData"
12
- :disabled="refreshing"
13
- title="Refresh data from database"
14
- >
15
- <i class="pi pi-refresh" :class="{ 'spinning': refreshing }"></i>
16
- </button>
17
- <div class="mode">
18
- <button class="mode__btn" :class="{ 'is-active': mode==='usd' }" @click="mode='usd'">$</button>
19
- <button class="mode__btn" :class="{ 'is-active': mode==='pct' }" @click="mode='pct'">%</button>
 
 
 
 
 
 
 
 
 
20
  </div>
21
  </div>
22
  </header>
@@ -34,18 +62,25 @@
34
  </div>
35
  </section>
36
 
37
- <!-- F1 Scoreboard Cards -->
38
  <section class="panel panel--cards" v-if="cards.length">
39
  <div class="cards-grid-f1">
40
  <article
41
  v-for="c in cards"
42
  :key="c.key"
43
  class="card-f1"
44
- :class="{ winner: c.isWinner, bh: c.kind==='bh', neg: (c.gapUsd ?? 0) < 0, pos: (c.gapUsd ?? 0) >= 0 }"
 
 
 
 
 
 
 
45
  :style="{ '--bar': (c.barPct ?? 0) + '%'}"
46
  >
47
- <!-- Crown -->
48
- <span v-if="c.isWinner" class="crown" aria-label="Top performer">👑</span>
49
 
50
  <!-- Header: logo + names -->
51
  <header class="head">
@@ -59,32 +94,61 @@
59
  </div>
60
  </header>
61
 
62
- <!-- Net value row -->
63
- <div class="net">
64
- <div class="net__label">Net value</div>
65
- <div class="net__value">{{ fmtUSD(c.balance) }}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </div>
67
 
68
  <!-- Performance bar (vs B&H) -->
69
- <div class="bar" :class="{ neg: (c.gapUsd ?? 0) < 0, pos: (c.gapUsd ?? 0) >= 0 }">
 
 
 
 
70
  <span :style="{ width: (c.barPct ?? 0) + '%' }"></span>
71
  </div>
72
 
73
- <!-- Bottom row: chips + EOD -->
74
- <div class="bottom">
75
- <div class="chips">
76
- <span class="chip" :class="{ pos: profitOf(c) >= 0, neg: profitOf(c) < 0 }">
77
- {{ signedMoney(profitOf(c)) }}
78
- </span>
79
- <span class="chip" :class="{ pos: (c.gapUsd ?? 0) >= 0, neg: (c.gapUsd ?? 0) < 0 }">
80
- <template v-if="c.kind==='agent'">
81
  <template v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</template>
82
  <template v-else>{{ signedPct(c.gapPct) }}</template>
83
- </template>
84
- <template v-else>—</template>
85
- </span>
86
  </div>
87
- <div class="eod">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
88
  </div>
89
  </article>
90
  </div>
@@ -97,21 +161,27 @@
97
  </template>
98
 
99
  <script setup>
100
- import { ref, computed, onMounted, watch, shallowRef } from 'vue'
101
- import AssetTabs from '../components/AssetTabs.vue'
102
  import CompareChartE from '../components/CompareChartE.vue'
103
  import { dataService } from '../lib/dataService'
104
 
105
- /* --- same helpers as chart --- */
106
  import { getAllDecisions } from '../lib/dataCache'
107
  import { readAllRawDecisions } from '../lib/idb'
108
  import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
109
  import { STRATEGIES } from '../lib/strategies'
110
- import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
 
 
 
 
 
111
 
112
  /* ---------- config ---------- */
113
- const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA'] // (MRNA removed)
114
- const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla']) // case-insensitive
 
 
115
 
116
  const ASSET_ICONS = {
117
  BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
@@ -132,18 +202,25 @@ const ASSET_CUTOFF = { BTC: '2025-08-01' }
132
  const mode = ref('usd')
133
  const asset = ref('BTC')
134
  const rowsRef = ref([])
135
- const refreshing = ref(false)
136
  let allDecisions = []
137
  const cards = shallowRef([])
 
 
 
138
 
139
  /* ---------- bootstrap ---------- */
140
  onMounted(async () => {
141
- try {
142
- if (!dataService.loaded && !dataService.loading) {
143
- await dataService.load(false)
144
- }
145
- } catch (e) { console.error('LiveView: dataService.load failed', e) }
146
  rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
 
 
 
 
 
 
147
  if (!orderedAssets.includes(asset.value)) asset.value = orderedAssets[0]
148
 
149
  allDecisions = getAllDecisions() || []
@@ -155,35 +232,44 @@ onMounted(async () => {
155
  }
156
  })
157
 
158
- /* ---------- refresh data ---------- */
159
- async function refreshData() {
160
- if (refreshing.value) return
161
- refreshing.value = true
162
- try {
163
- console.log('[Live] Force refreshing data from Supabase...')
164
- await dataService.forceRefresh()
165
- rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
166
- allDecisions = getAllDecisions() || []
167
- console.log('[Live] Data refreshed successfully')
168
- } catch (e) {
169
- console.error('[Live] Error refreshing data:', e)
170
- } finally {
171
- refreshing.value = false
172
- }
173
- }
174
 
175
  /* ---------- helpers ---------- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
177
  const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
178
- const signedPct = (p) => `${(p >= 0 ? '+' : '−')}${Math.abs(p * 100).toFixed(2)}%`
179
  const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
180
  const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
181
 
182
- /* rows for selected asset (exclude vanilla/vinilla) - only show long_only strategy like leaderboard */
183
  const filteredRows = computed(() =>
184
  (rowsRef.value || []).filter(r => {
185
  if (r.asset !== asset.value) return false
186
- if (r.strategy !== 'long_only') return false // 只显示 Aggressive Long Only 策略
187
  const name = (r?.agent_name || '').toLowerCase()
188
  return !EXCLUDED_AGENT_NAMES.has(name)
189
  })
@@ -197,26 +283,25 @@ const winners = computed(() => {
197
  const cur = byAgent.get(k)
198
  if (!cur || score(r) > score(cur)) byAgent.set(k, r)
199
  }
200
- const result = [...byAgent.values()]
201
- console.log('[Live winners from leaderboard]', result.map(r => ({
202
- agent: r.agent_name,
203
- model: r.model,
204
- strategy: r.strategy,
205
- balance: r.balance
206
- })))
207
- return result
208
  })
209
 
210
  /* chart selections */
211
- const winnersForChart = computed(() =>
212
- winners.value.map(w => ({
 
 
 
 
 
213
  agent_name: w.agent_name,
214
  asset: w.asset,
215
  model: w.model,
216
  strategy: w.strategy,
217
- decision_ids: Array.isArray(w.decision_ids) ? w.decision_ids : undefined
 
218
  }))
219
- )
220
 
221
  /* stable key to avoid identity churn */
222
  const winnersKey = computed(() => {
@@ -224,7 +309,7 @@ const winnersKey = computed(() => {
224
  return sels.map(s => `${s.agent_name}|${s.asset}|${s.model}|${s.strategy}|${(s.decision_ids?.length||0)}`).join('||')
225
  })
226
 
227
- /* ---------- PERF (chart parity) ---------- */
228
  async function buildSeq(sel) {
229
  const { agent_name: agentName, asset: assetCode, model } = sel
230
  const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
@@ -234,19 +319,18 @@ async function buildSeq(sel) {
234
 
235
  seq.sort((a,b) => (a.date > b.date ? 1 : -1))
236
 
237
- // 如果使用了 decision_ids,数据已经预过滤,不需要再次处理
238
- // 只有在没有 decision_ids 时才需要过滤交易日
239
  if (!ids.length) {
240
  const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
241
  if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
242
-
243
  const cutoff = ASSET_CUTOFF[assetCode]
244
  if (cutoff) {
245
  const t0 = new Date(cutoff + 'T00:00:00Z')
246
  seq = seq.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
247
  }
248
  }
249
-
250
  return seq
251
  }
252
 
@@ -255,28 +339,43 @@ async function computeEquities(sel) {
255
  if (!seq.length) return null
256
 
257
  const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
258
-
259
- console.log('[Live computeEquities]', {
260
- agent: sel.agent_name,
261
- model: sel.model,
262
- strategy: sel.strategy,
263
- config: cfg,
264
- seqLength: seq.length,
265
- decision_ids: sel.decision_ids?.length || 'none'
266
- })
267
 
268
  const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
269
  const bhY = computeBuyHoldEquity(seq, 100000) || []
270
  const lastIdx = Math.min(stratY.length, bhY.length) - 1
271
  if (lastIdx < 0) return null
272
-
273
- console.log('[Live computeEquities result]', {
274
- agent: sel.agent_name,
275
- stratLast: stratY[lastIdx],
276
- bhLast: bhY[lastIdx]
277
- })
278
 
279
- return { date: seq[lastIdx].date, stratLast: stratY[lastIdx], bhLast: bhY[lastIdx] }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  }
281
 
282
  /* build cards whenever winners/asset change */
@@ -294,7 +393,28 @@ watch(
294
 
295
  if (!perfs.length) { cards.value = []; return }
296
 
297
- // Buy & Hold card from the asset’s aligned BH series
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  const first = perfs[0]
299
  const assetCode = first.sel.asset
300
  const bhCard = {
@@ -303,17 +423,22 @@ watch(
303
  title: 'Buy & Hold',
304
  subtitle: assetCode,
305
  balance: first.perf.bhLast,
306
- date: first.perf.date,
307
  logo: ASSET_ICONS[assetCode] || null,
308
  profitUsd: (first.perf.bhLast ?? 0) - 100000,
309
  gapUsd: 0,
310
  gapPct: 0,
311
- isWinner: false,
312
  rank: null,
313
- barPct: 0
 
 
 
 
 
 
314
  }
315
 
316
- // Agent cards (gap vs BH)
317
  const agentCards = perfs.map(({ sel, perf }) => {
318
  const gapUsd = perf.stratLast - perf.bhLast
319
  const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
@@ -322,13 +447,17 @@ watch(
322
  key: `agent|${sel.agent_name}|${sel.model}`,
323
  kind: 'agent',
324
  title: sel.agent_name,
325
- subtitle: sel.model,
326
  balance: perf.stratLast,
327
- date: perf.date,
328
  logo: AGENT_LOGOS[sel.agent_name] || null,
329
  gapUsd, gapPct,
330
  profitUsd,
331
- isWinner: false,
 
 
 
 
332
  rank: null,
333
  barPct: 0
334
  }
@@ -338,162 +467,217 @@ watch(
338
  agentCards.sort((a,b) => (b.balance ?? -Infinity) - (a.balance ?? -Infinity))
339
  agentCards.forEach((c, i) => { c.rank = i + 1 })
340
 
341
- // Winner flag
342
- if (agentCards.length) agentCards[0].isWinner = true
343
-
344
  // Perf bar width scaled to max |gapUsd|
345
  const maxAbsGap = Math.max(1, ...agentCards.map(c => Math.abs(c.gapUsd ?? 0)))
346
  agentCards.forEach(c => { c.barPct = Math.max(3, Math.round((Math.abs(c.gapUsd ?? 0) / maxAbsGap) * 100)) })
347
 
348
- // Build final list: BH first, then agents in rank order
349
  cards.value = [bhCard, ...agentCards].slice(0,5)
350
- } catch (e) {
351
- console.error('LiveView: compute cards failed', e)
352
- cards.value = []
353
  } finally { computing = false }
354
  },
355
  { immediate: true }
356
  )
357
  </script>
358
 
 
 
359
  <style scoped>
360
- .live { max-width: 1280px; margin: 0 auto; padding: 12px 20px 28px; background: #ffffff; padding-bottom: 56px; }
361
-
362
- /* toolbar */
363
- .toolbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 0 10px; background: #ffffff; color: #0f172a; border-bottom: 1px solid #E7ECF3; }
364
- .toolbar__right { display: flex; align-items: center; gap: 12px; }
365
- .refresh-btn {
366
- height: 30px;
367
- width: 30px;
368
- border-radius: 10px;
369
- border: 1px solid #D7DDE7;
370
- background: #ffffff;
371
- color: #0f172a;
372
- cursor: pointer;
373
- display: flex;
374
- align-items: center;
375
- justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
 
376
  transition: all 0.2s ease;
 
377
  }
378
- .refresh-btn:hover:not(:disabled) {
379
- background: #f8f9fa;
380
- border-color: #0f172a;
381
  }
382
- .refresh-btn:disabled {
383
- opacity: 0.5;
384
- cursor: not-allowed;
 
 
 
 
 
385
  }
386
- .refresh-btn i.spinning {
387
- animation: spin 1s linear infinite;
 
 
 
 
 
388
  }
389
- @keyframes spin {
390
- from { transform: rotate(0deg); }
391
- to { transform: rotate(360deg); }
 
 
392
  }
393
- .mode__btn { height: 30px; min-width: 40px; padding: 0 10px; border-radius: 10px; border: 1px solid #D7DDE7; background: #ffffff; font-weight: 700; color: #0f172a; }
394
- .mode__btn.is-active { background: #0f172a; color: #ffffff; border-color: #0f172a; }
395
-
396
- /* panels */
397
- .panel { background: #ffffff; border: 1px solid #E7ECF3; border-radius: 14px; }
398
- .panel--chart { padding: 10px 10px 2px; }
399
- .panel--cards { padding: 12px; }
400
-
401
- /* empty */
402
- .empty { padding: 14px; border: 1px dashed #D7DDE7; border-radius: 12px; color: #6B7280; font-size: .92rem; background: #ffffff; }
403
-
404
- /* GRID */
405
- .cards-grid-f1 { display: grid; gap: 12px; grid-template-columns: repeat(5, minmax(0,1fr)); }
406
- @media (max-width: 1400px) { .cards-grid-f1 { grid-template-columns: repeat(4, minmax(0,1fr)); } }
407
- @media (max-width: 1100px) { .cards-grid-f1 { grid-template-columns: repeat(3, minmax(0,1fr)); } }
408
- @media (max-width: 900px) { .cards-grid-f1 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
409
- @media (max-width: 640px) { .cards-grid-f1 { grid-template-columns: 1fr; } }
410
-
411
- /* F1 Card */
412
- .card-f1 {
413
- position: relative;
414
- display: grid;
415
- grid-template-rows: auto auto auto; /* head, net, bottom */
416
- gap: 10px;
417
- padding: 16px 16px 18px;
418
- min-height: 210px;
419
- border-radius: 14px;
420
- background: linear-gradient(145deg,#ffffff,#fafbfd 55%,#ffffff 100%);
421
- border: 1px solid #E7ECF3;
422
- box-shadow: 0 1px 2px rgba(16,24,40,.03), 0 4px 12px rgba(16,24,40,.04);
423
- color: #0f172a;
424
- transition: transform .18s ease, box-shadow .2s ease, border-color .2s ease;
425
  }
426
- .card-f1:hover { transform: translateY(-2px); box-shadow: 0 10px 26px rgba(16,24,40,.08); border-color: #D9E2EF; }
427
- .card-f1.winner { border-color: #16a34a; box-shadow: 0 0 0 2px rgba(22,163,74,.18), 0 10px 26px rgba(16,24,40,.10); }
428
- .card-f1.bh { border-style: dashed; opacity: 1; }
429
-
430
- /* Rank / Crown */
431
- .rank {
432
- position: absolute;
433
- top: 10px;
434
- left: 12px;
435
- font-weight: 900;
436
- font-size: 18px;
437
- color: rgba(15,23,42,.30);
438
- letter-spacing: .04em;
439
  }
440
- .rank.bh-badge {
441
- font-size: 12px;
442
- background: rgba(15,23,42,.06);
443
- padding: 2px 6px;
444
- border-radius: 6px;
445
- color: #4b5563;
446
  }
447
- .crown {
448
- position: absolute;
449
- top: 12px;
450
- right: 12px;
451
- font-size: 18px;
452
- filter: drop-shadow(0 1px 1px rgba(0,0,0,.12));
 
 
 
 
453
  }
454
 
455
- /* Head */
456
- .head {
457
- display: grid;
458
- grid-template-columns: 40px minmax(0,1fr);
459
- align-items: center;
460
- gap: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  }
462
- .logo { width: 40px; height: 40px; border-radius: 10px; background: #f3f4f6; display: grid; place-items: center; overflow: hidden; border: 1px solid #E7ECF3; }
463
- .logo img { width: 100%; height: 100%; object-fit: contain; }
464
- .logo__fallback { width: 60%; height: 60%; border-radius: 6px; background: #e5e7eb; }
465
- .names { min-width: 0; }
466
- .agent { font-size: 16px; font-weight: 900; letter-spacing: .02em; text-transform: uppercase; color: #0f172a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
467
- .model { font-size: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
468
-
469
- /* Net row */
470
- .net { display: grid; grid-template-columns: 1fr auto; align-items: end; }
471
- .net__label { font-size: 12px; color: #6b7280; }
472
- .net__value { font-size: clamp(18px, 2.2vw, 25px); font-weight: 700; letter-spacing: -.01em; color: #0f172a; }
473
-
474
- /* Bar vs B&H */
475
- .bar { height: 6px; border-radius: 999px; background: #F2F4F8; overflow: hidden; border: 1px solid #E7ECF3; }
476
- .bar span { display: block; height: 100%; background: linear-gradient(90deg,#16a34a,#22c55e); width: var(--bar, 40%); transition: width .5s ease; }
477
- .bar.neg span { background: linear-gradient(90deg,#ef4444,#dc2626); }
478
-
479
- /* Bottom */
480
- .bottom { display: grid; grid-template-columns: 1fr auto; grid-template-rows: auto auto; align-items: end; gap: 8px; }
481
- .chips{
482
- grid-column:1 / -1; /* span both columns on row 1 */
483
- grid-row:1;
484
- display:inline-flex;
485
- gap:8px;
486
- flex-wrap:wrap; /* chips can wrap within row 1 */
487
  }
488
- .eod{
489
- grid-column:2; /* row 2, right column */
490
- grid-row:2;
491
- justify-self:end; /* align to the right */
492
- /* allow wrapping if needed; remove nowrap */
493
- font-size:12px;
494
- color:#6b7280;
495
  }
496
- .chip { font-size: 12px; font-weight: 800; padding: 4px 8px; border-radius: 999px; background: #F6F8FB; color: #0f172a; border: 1px solid #E7ECF3; }
497
- .chip.pos { color: #0e7a3a; background: #E9F7EF; border-color: #d7f0e0; }
498
- .chip.neg { color: #B91C1C; background: #FBEAEA; border-color: #F3DADA; }
499
  </style>
 
1
  <template>
2
  <div class="live">
3
+ <!-- About Text -->
4
+ <div class="about-banner">
5
+ <p class="about-text">
6
+ A real-time, ever-evolving platform for continuous competition among AI trading agents.
7
+ </p>
8
+ </div>
9
+
10
  <!-- Toolbar: assets + mode -->
11
  <header class="toolbar">
12
  <div class="toolbar__left">
13
+ <!-- Asset tabs styled per AMA -->
14
+ <div class="asset-tabs">
15
+ <button
16
+ v-for="a in orderedAssets"
17
+ :key="a"
18
+ class="asset-tab"
19
+ :class="{ 'is-active': asset === a }"
20
+ @click="asset = a"
21
+ >
22
+ {{ a }}
23
+ </button>
24
+ </div>
25
  </div>
26
+
27
  <div class="toolbar__right">
28
+ <!-- Segmented mode switch (USD / %) -->
29
+ <div class="mode-switch" role="tablist" aria-label="Value mode">
30
+ <button
31
+ role="tab"
32
+ aria-selected="mode==='usd'"
33
+ class="mode-switch__btn"
34
+ :class="{ 'is-active': mode==='usd' }"
35
+ @click="mode='usd'"
36
+ >
37
+ $
38
+ </button>
39
+ <button
40
+ role="tab"
41
+ aria-selected="mode==='pct'"
42
+ class="mode-switch__btn"
43
+ :class="{ 'is-active': mode==='pct' }"
44
+ @click="mode='pct'"
45
+ >
46
+ %
47
+ </button>
48
  </div>
49
  </div>
50
  </header>
 
62
  </div>
63
  </section>
64
 
65
+ <!-- Tournament Cards -->
66
  <section class="panel panel--cards" v-if="cards.length">
67
  <div class="cards-grid-f1">
68
  <article
69
  v-for="c in cards"
70
  :key="c.key"
71
  class="card-f1"
72
+ :class="{
73
+ bh: c.kind==='bh',
74
+ neg: (c.gapUsd ?? 0) < 0,
75
+ pos: (c.gapUsd ?? 0) >= 0,
76
+ gold: c.rank === 1,
77
+ silver: c.rank === 2,
78
+ bronze: c.rank === 3
79
+ }"
80
  :style="{ '--bar': (c.barPct ?? 0) + '%'}"
81
  >
82
+ <!-- Podium ribbon (rank 1-3 only) -->
83
+ <div v-if="c.rank && c.rank <= 3" class="podium-ribbon" :data-rank="c.rank"></div>
84
 
85
  <!-- Header: logo + names -->
86
  <header class="head">
 
94
  </div>
95
  </header>
96
 
97
+ <!-- KPI row: Net value + P&L -->
98
+ <div class="kpis">
99
+ <div class="kpi">
100
+ <div class="kpi__label">Net Value</div>
101
+ <div class="kpi__value">{{ fmtUSD(c.balance) }}</div>
102
+ </div>
103
+ <div class="kpi align-right">
104
+ <div class="kpi__label">P&amp;L</div>
105
+ <div class="kpi__pill" :class="{ pos: profitOf(c) >= 0, neg: profitOf(c) < 0 }">
106
+ {{ signedMoney(profitOf(c)) }}
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Quality row: Sharpe / MaxDD -->
112
+ <div class="quality">
113
+ <div class="qitem">
114
+ <span class="qitem__label">Sharpe</span>
115
+ <span class="qitem__value" :class="{ strong: (c.sharpe ?? 0) >= 2 }">{{ fmtSharpe(c.sharpe) }}</span>
116
+ </div>
117
+ <div class="qitem">
118
+ <span class="qitem__label">MaxDD</span>
119
+ <span class="qitem__value dd">{{ signedPct(c.maxDrawdown) }}</span>
120
+ </div>
121
+ </div>
122
+
123
+ <!-- Match stats: Win Rate / Trades / Days -->
124
+ <div class="stats">
125
+ <div class="stat"><span class="stat__label">Win Rate</span><span class="stat__val" :class="{ pos: (c.winRate ?? 0) >= 0.5, neg: (c.winRate ?? 0) < 0.5 }">{{ fmtRate(c.winRate) }}</span></div>
126
+ <div class="stat"><span class="stat__label">Trades</span><span class="stat__val">{{ c.trades ?? '—' }}</span></div>
127
+ <div class="stat"><span class="stat__label">Days</span><span class="stat__val">{{ c.days ?? '—' }}</span></div>
128
  </div>
129
 
130
  <!-- Performance bar (vs B&H) -->
131
+ <div
132
+ class="bar"
133
+ :class="{ neg: (c.gapUsd ?? 0) < 0, pos: (c.gapUsd ?? 0) >= 0 }"
134
+ :aria-label="mode==='usd' ? ('Gap vs B&H: ' + signedMoney(c.gapUsd)) : ('Gap vs B&H: ' + signedPct(c.gapPct))"
135
+ >
136
  <span :style="{ width: (c.barPct ?? 0) + '%' }"></span>
137
  </div>
138
 
139
+ <!-- Meta row: Gap vs B&H + EOD -->
140
+ <div class="meta">
141
+ <div class="gap chip" :class="{ pos: (c.gapUsd ?? 0) >= 0, neg: (c.gapUsd ?? 0) < 0 }">
142
+ <template v-if="c.kind==='agent'">
143
+ <span class="chip__label">vs B&amp;H</span>
144
+ <span class="chip__value">
 
 
145
  <template v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</template>
146
  <template v-else>{{ signedPct(c.gapPct) }}</template>
147
+ </span>
148
+ </template>
149
+ <template v-else>—</template>
150
  </div>
151
+ <div class="eod">EOD {{ c.date || '–' }}</div>
152
  </div>
153
  </article>
154
  </div>
 
161
  </template>
162
 
163
  <script setup>
164
+ import { ref, computed, onMounted, onBeforeUnmount, watch, shallowRef } from 'vue'
 
165
  import CompareChartE from '../components/CompareChartE.vue'
166
  import { dataService } from '../lib/dataService'
167
 
168
+ /* helpers & metrics */
169
  import { getAllDecisions } from '../lib/dataCache'
170
  import { readAllRawDecisions } from '../lib/idb'
171
  import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
172
  import { STRATEGIES } from '../lib/strategies'
173
+ import {
174
+ computeBuyHoldEquity,
175
+ computeStrategyEquity,
176
+ calculateMetricsFromSeries,
177
+ computeWinRate
178
+ } from '../lib/perf'
179
 
180
  /* ---------- config ---------- */
181
+ const orderedAssets = ['BTC','ETH','BMRN','TSLA']
182
+ const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla'])
183
+
184
+ import { supabase } from '../lib/supabase'
185
 
186
  const ASSET_ICONS = {
187
  BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
 
202
  const mode = ref('usd')
203
  const asset = ref('BTC')
204
  const rowsRef = ref([])
 
205
  let allDecisions = []
206
  const cards = shallowRef([])
207
+ const refreshing = ref(false)
208
+
209
+ let unsubscribe = null
210
 
211
  /* ---------- bootstrap ---------- */
212
  onMounted(async () => {
213
+ unsubscribe = dataService.subscribe((state) => {
214
+ rowsRef.value = Array.isArray(state.tableRows) ? state.tableRows : []
215
+ })
216
+
 
217
  rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
218
+
219
+ // Force refresh data from Supabase to get latest end_date
220
+ if (!dataService.loading) {
221
+ dataService.load(true).catch(e => console.error('LiveView: force refresh failed', e))
222
+ }
223
+
224
  if (!orderedAssets.includes(asset.value)) asset.value = orderedAssets[0]
225
 
226
  allDecisions = getAllDecisions() || []
 
232
  }
233
  })
234
 
235
+ onBeforeUnmount(() => {
236
+ if (unsubscribe) { unsubscribe(); unsubscribe = null }
237
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
238
 
239
  /* ---------- helpers ---------- */
240
+ const formatModelName = (model) => {
241
+ if (!model) return ''
242
+ return model.replace(/_?\d{8}$/, '')
243
+ }
244
+
245
+ const asRatio = (x) => {
246
+ if (x == null || Number.isNaN(x)) return null;
247
+ // if |x| > 1.2 we assume it's already a % number (e.g., 58 or -79.23), convert to ratio
248
+ return Math.abs(x) > 0.5 ? (x / 100) : x;
249
+ };
250
+ const signedPct = (p) => {
251
+ const r = asRatio(p);
252
+ if (r == null) return '—';
253
+ const sign = r >= 0 ? '+' : '−';
254
+ return `${sign}${(Math.abs(r) * 100).toFixed(2)}%`;
255
+ };
256
+
257
+ const fmtRate = (r) => {
258
+ const rr = asRatio(r);
259
+ if (rr == null) return '—';
260
+ return `${(rr * 100).toFixed(0)}%`;
261
+ };
262
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
263
  const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
264
+ const fmtSharpe = (s) => (s == null ? '' : Number(s).toFixed(2))
265
  const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
266
  const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
267
 
268
+ /* rows for selected asset (exclude vanilla/vinilla) - only show long_only */
269
  const filteredRows = computed(() =>
270
  (rowsRef.value || []).filter(r => {
271
  if (r.asset !== asset.value) return false
272
+ if (r.strategy !== 'long_only') return false
273
  const name = (r?.agent_name || '').toLowerCase()
274
  return !EXCLUDED_AGENT_NAMES.has(name)
275
  })
 
283
  const cur = byAgent.get(k)
284
  if (!cur || score(r) > score(cur)) byAgent.set(k, r)
285
  }
286
+ return [...byAgent.values()]
 
 
 
 
 
 
 
287
  })
288
 
289
  /* chart selections */
290
+ const winnersForChart = computed(() => {
291
+ // Get max end_date for current asset from all rows
292
+ const rowsForAsset = (rowsRef.value || []).filter(r => r.asset === asset.value)
293
+ const endDates = rowsForAsset.map(r => r.end_date).filter(Boolean)
294
+ const maxEndDate = endDates.length ? endDates.sort().pop() : null
295
+
296
+ return winners.value.map(w => ({
297
  agent_name: w.agent_name,
298
  asset: w.asset,
299
  model: w.model,
300
  strategy: w.strategy,
301
+ decision_ids: Array.isArray(w.decision_ids) ? w.decision_ids : undefined,
302
+ end_date: maxEndDate || w.end_date // use max end_date from all rows for this asset
303
  }))
304
+ })
305
 
306
  /* stable key to avoid identity churn */
307
  const winnersKey = computed(() => {
 
309
  return sels.map(s => `${s.agent_name}|${s.asset}|${s.model}|${s.strategy}|${(s.decision_ids?.length||0)}`).join('||')
310
  })
311
 
312
+ /* ---------- PERF ---------- */
313
  async function buildSeq(sel) {
314
  const { agent_name: agentName, asset: assetCode, model } = sel
315
  const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
 
319
 
320
  seq.sort((a,b) => (a.date > b.date ? 1 : -1))
321
 
322
+ // if using decision_ids, data is already prefiltered
 
323
  if (!ids.length) {
324
  const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
325
  if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
326
+
327
  const cutoff = ASSET_CUTOFF[assetCode]
328
  if (cutoff) {
329
  const t0 = new Date(cutoff + 'T00:00:00Z')
330
  seq = seq.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
331
  }
332
  }
333
+
334
  return seq
335
  }
336
 
 
339
  if (!seq.length) return null
340
 
341
  const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
 
 
 
 
 
 
 
 
 
342
 
343
  const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
344
  const bhY = computeBuyHoldEquity(seq, 100000) || []
345
  const lastIdx = Math.min(stratY.length, bhY.length) - 1
346
  if (lastIdx < 0) return null
 
 
 
 
 
 
347
 
348
+ // quality metrics
349
+ const isCrypto = sel.asset === 'BTC' || sel.asset === 'ETH'
350
+ const metrics = calculateMetricsFromSeries(stratY, isCrypto ? 'crypto' : 'stock') || {}
351
+ const { sharpe_ratio: sharpe, max_drawdown: maxDrawdown, total_return: totalReturnPct } = metrics
352
+
353
+ // win rate / trades
354
+ let winRate = null
355
+ let trades = null
356
+ try {
357
+ const r = computeWinRate(seq, cfg.strategy, cfg.tradingMode) || {}
358
+ winRate = r.winRate
359
+ trades = r.trades
360
+ } catch {}
361
+
362
+ // approximate "days" from sequence
363
+ let days = 0
364
+ try {
365
+ const start = new Date(seq[0].date)
366
+ const end = new Date(seq[lastIdx].date)
367
+ days = Math.max(1, Math.round((end - start) / 86400000) + 1)
368
+ } catch {}
369
+
370
+ return {
371
+ date: seq[lastIdx]?.date || sel.end_date, // prefer actual sequence date over leaderboard end_date
372
+ stratLast: stratY[lastIdx],
373
+ bhLast: bhY[lastIdx],
374
+ sharpe,
375
+ maxDrawdown, // as fraction (e.g., -0.053)
376
+ winRate, trades, days,
377
+ totalReturnPct // for future use if needed
378
+ }
379
  }
380
 
381
  /* build cards whenever winners/asset change */
 
393
 
394
  if (!perfs.length) { cards.value = []; return }
395
 
396
+ // Get max date directly from Supabase for this asset
397
+ const currentAsset = asset.value
398
+ let maxEndDate = null
399
+ try {
400
+ const { data, error } = await supabase
401
+ .from('trading_decisions')
402
+ .select('date')
403
+ .eq('asset', currentAsset)
404
+ .order('date', { ascending: false })
405
+ .limit(1)
406
+
407
+ if (!error && data && data.length > 0) {
408
+ maxEndDate = data[0].date
409
+ console.log('[LiveView] Direct Supabase query - Asset:', currentAsset, 'Max date:', maxEndDate)
410
+ } else {
411
+ console.log('[LiveView] Supabase query failed or no data:', error)
412
+ }
413
+ } catch (e) {
414
+ console.error('[LiveView] Error querying Supabase:', e)
415
+ }
416
+
417
+ // Buy & Hold first
418
  const first = perfs[0]
419
  const assetCode = first.sel.asset
420
  const bhCard = {
 
423
  title: 'Buy & Hold',
424
  subtitle: assetCode,
425
  balance: first.perf.bhLast,
426
+ date: maxEndDate || first.perf.date, // Use max date from Supabase
427
  logo: ASSET_ICONS[assetCode] || null,
428
  profitUsd: (first.perf.bhLast ?? 0) - 100000,
429
  gapUsd: 0,
430
  gapPct: 0,
 
431
  rank: null,
432
+ barPct: 0,
433
+ // BH quality fields are neutral/na
434
+ sharpe: null,
435
+ maxDrawdown: null,
436
+ winRate: null,
437
+ trades: null,
438
+ days: null
439
  }
440
 
441
+ // Agents
442
  const agentCards = perfs.map(({ sel, perf }) => {
443
  const gapUsd = perf.stratLast - perf.bhLast
444
  const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
 
447
  key: `agent|${sel.agent_name}|${sel.model}`,
448
  kind: 'agent',
449
  title: sel.agent_name,
450
+ subtitle: formatModelName(sel.model),
451
  balance: perf.stratLast,
452
+ date: maxEndDate || perf.date, // Use max date from Supabase
453
  logo: AGENT_LOGOS[sel.agent_name] || null,
454
  gapUsd, gapPct,
455
  profitUsd,
456
+ sharpe: perf.sharpe,
457
+ maxDrawdown: perf.maxDrawdown,
458
+ winRate: perf.winRate,
459
+ trades: perf.trades,
460
+ days: perf.days,
461
  rank: null,
462
  barPct: 0
463
  }
 
467
  agentCards.sort((a,b) => (b.balance ?? -Infinity) - (a.balance ?? -Infinity))
468
  agentCards.forEach((c, i) => { c.rank = i + 1 })
469
 
 
 
 
470
  // Perf bar width scaled to max |gapUsd|
471
  const maxAbsGap = Math.max(1, ...agentCards.map(c => Math.abs(c.gapUsd ?? 0)))
472
  agentCards.forEach(c => { c.barPct = Math.max(3, Math.round((Math.abs(c.gapUsd ?? 0) / maxAbsGap) * 100)) })
473
 
474
+ // BH first, then top agents (limit 5)
475
  cards.value = [bhCard, ...agentCards].slice(0,5)
 
 
 
476
  } finally { computing = false }
477
  },
478
  { immediate: true }
479
  )
480
  </script>
481
 
482
+ <!-- Global brand tokens (unscoped to ensure cascade) -->
483
+ <!-- Paste this inside your Live view component as the full style -->
484
  <style scoped>
485
+ /* ======= Page ======= */
486
+ .live{
487
+ max-width:1280px;
488
+ margin:0 auto;
489
+ padding:0 24px 64px;
490
+ color:var(--ink-900);
491
+ background: radial-gradient(ellipse at 30% -10%, #fdfdfd 0%, var(--surface-2) 60%, var(--surface-0) 100%);
492
+ margin-top: -40px;
493
+ }
494
+
495
+ /* ======= About Banner ======= */
496
+ .about-banner{
497
+ padding: 8px 24px 16px 56px;
498
+ background: var(--surface-0);
499
+ margin: 0 -24px 0 -24px;
500
+ }
501
+ .about-text{
502
+ margin: 0;
503
+ font-size: 15px;
504
+ font-weight: 500;
505
+ color: #6b7280;
506
+ letter-spacing: 0.01em;
507
+ line-height: 1.5;
508
+ }
509
+ .about-text .paper-link{
510
+ color: rgb(var(--ama-start));
511
+ font-weight: 700;
512
+ text-decoration: none;
513
  transition: all 0.2s ease;
514
+ border-bottom: 1px solid transparent;
515
  }
516
+ .about-text .paper-link:hover{
517
+ color: rgb(var(--ama-end));
518
+ border-bottom-color: rgb(var(--ama-end));
519
  }
520
+
521
+ /* ======= Toolbar (assets + mode) ======= */
522
+ .toolbar{
523
+ position:sticky; top:0; z-index:10;
524
+ display:flex; align-items:center; justify-content:space-between; gap:16px;
525
+ padding:12px 0 14px; background:var(--surface-0);
526
+ border-bottom:2px solid rgba(var(--ama-end), .15);
527
+ backdrop-filter: blur(10px);
528
  }
529
+ .toolbar__right{ display:flex; align-items:center; gap:12px; }
530
+
531
+ /* ======= Asset tabs ======= */
532
+ .asset-tabs{
533
+ display:inline-flex; gap:6px; padding:4px;
534
+ border-radius:12px; background:var(--surface-0);
535
+ border:1px solid var(--bd-subtle); box-shadow:0 2px 8px rgba(0,0,0,.04);
536
  }
537
+ .asset-tab{
538
+ min-width:54px; height:32px; padding:0 10px;
539
+ border-radius:8px; border:1px solid transparent;
540
+ background:transparent; color:var(--ink-900);
541
+ font-weight:800; letter-spacing:.02em; cursor:pointer; transition:all .2s ease;
542
  }
543
+ .asset-tab:hover{ color:rgb(var(--ama-end)); border-color:rgba(var(--ama-end), .28); }
544
+ .asset-tab.is-active{
545
+ color:#fff; border:none;
546
+ background: linear-gradient(90deg, rgb(var(--ama-start)), rgb(var(--ama-end)));
547
+ box-shadow: 0 0 0 1px rgba(var(--ama-end), .22), 0 8px 18px rgba(var(--ama-end), .22);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  }
549
+
550
+ /* ======= Mode switch ($ / %) ======= */
551
+ .mode-switch{
552
+ display:inline-grid; grid-template-columns:1fr 1fr;
553
+ border-radius:12px; overflow:hidden;
554
+ border:1px solid var(--bd-subtle); background:var(--surface-0);
555
+ box-shadow:0 2px 8px rgba(0,0,0,.04);
556
+ }
557
+ .mode-switch__btn{
558
+ height:32px; min-width:44px; padding:0 12px;
559
+ font-weight:800; letter-spacing:.02em; border:none; background:transparent;
560
+ color:var(--ink-900); cursor:pointer; transition:all .2s ease;
 
561
  }
562
+ .mode-switch__btn:not(.is-active):hover{ color:rgb(var(--ama-end)); background:#f8fafc; }
563
+ .mode-switch__btn.is-active{
564
+ color:#fff;
565
+ background: linear-gradient(90deg, rgb(var(--ama-start)), rgb(var(--ama-end)));
566
+ box-shadow: inset 0 -1px 0 rgba(255,255,255,.2);
 
567
  }
568
+
569
+ /* ======= Panels ======= */
570
+ .panel{ background:var(--surface-0); border:1px solid var(--bd-soft); border-radius:16px; margin-top:16px; }
571
+ .panel--chart{ padding:10px; }
572
+ .panel--cards{ padding:16px; }
573
+
574
+ /* ======= Empty State ======= */
575
+ .empty{
576
+ padding:20px; border:1px dashed var(--bd-soft); border-radius:12px;
577
+ color:var(--ink-600); font-size:.95rem; background:var(--surface-0); text-align:center;
578
  }
579
 
580
+ /* ======= Grid ======= */
581
+ .cards-grid-f1{ display:grid; gap:14px; grid-template-columns:repeat(auto-fit, minmax(260px,1fr)); }
582
+
583
+ /* ======= Cards (Tournament) ======= */
584
+ .card-f1{
585
+ position:relative;
586
+ display:grid; grid-template-rows:auto auto auto auto auto; gap:10px;
587
+ padding:18px 18px 20px; min-height:230px; border-radius:16px;
588
+ background: linear-gradient(145deg, var(--surface-0), var(--surface-2) 75%, var(--surface-0) 100%);
589
+ border:1px solid rgba(0,0,0,.05);
590
+ box-shadow:0 2px 10px rgba(var(--ama-start), .05), 0 6px 20px rgba(var(--ama-end), .08);
591
+ color:var(--ink-900); transition: transform .25s ease, box-shadow .25s ease;
592
+ }
593
+ .card-f1:hover{ transform: translateY(-2px); box-shadow:0 10px 28px rgba(0,0,0,.08); }
594
+ .card-f1.bh{ border-style:dashed; }
595
+
596
+ /* Podium metals (attach .gold/.silver/.bronze) */
597
+ .card-f1.gold{
598
+ border-color:var(--gold);
599
+ box-shadow:0 0 0 1px rgba(212,175,55,.32), 0 10px 28px rgba(212,175,55,.18);
600
+ }
601
+ .card-f1.silver{
602
+ border-color:var(--silver);
603
+ box-shadow:0 0 0 1px rgba(192,192,192,.28), 0 10px 28px rgba(192,192,192,.14);
604
+ }
605
+ .card-f1.bronze{
606
+ border-color:var(--bronze);
607
+ box-shadow:0 0 0 1px rgba(205,127,50,.28), 0 10px 28px rgba(205,127,50,.14);
608
+ }
609
+
610
+ /* Optional thin ribbon bar on podium cards */
611
+ .podium-ribbon{ position:absolute; top:0; left:0; right:0; height:6px; border-top-left-radius:16px; border-top-right-radius:16px; }
612
+ .card-f1.gold .podium-ribbon{ background: linear-gradient(90deg, #f6e27a, var(--gold)); }
613
+ .card-f1.silver .podium-ribbon{ background: linear-gradient(90deg, #e9eef2, var(--silver)); }
614
+ .card-f1.bronze .podium-ribbon{ background: linear-gradient(90deg, #f0c6a1, var(--bronze)); }
615
+
616
+ /* ======= Card: Head ======= */
617
+ .head{ display:grid; grid-template-columns:44px minmax(0,1fr); align-items:center; gap:12px; }
618
+ .logo{ width:44px; height:44px; border-radius:12px; background:#f3f4f6; display:grid; place-items:center; overflow:hidden; border:1px solid #E5E7EB; }
619
+ .logo img{ width:100%; height:100%; object-fit:contain; }
620
+ .logo__fallback{ width:60%; height:60%; border-radius:6px; background:#e5e7eb; }
621
+ .names{ min-width:0; }
622
+ .agent{ font-size:16px; font-weight:900; letter-spacing:.02em; text-transform:uppercase; color:var(--ink-900); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
623
+ .model{ font-size:11px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; color:#64748b; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
624
+
625
+ /* ======= KPIs (Net Value + P&L) ======= */
626
+ .kpis{ display:grid; grid-template-columns:1fr auto; align-items:end; gap:12px; }
627
+ .kpi__label{ font-size:12px; color:var(--ink-600); }
628
+ .kpi__value{ font-size: clamp(18px, 2.2vw, 25px); font-weight:700; letter-spacing:-.01em; color:var(--ink-900); }
629
+ .kpi__pill{
630
+ display:inline-block; font-size:12px; font-weight:800; padding:4px 10px; border-radius:999px;
631
+ border:1px solid var(--bd-soft); background:#F6F8FB; color:var(--ink-900);
632
+ }
633
+ .kpi__pill.pos{ color:var(--pos-fg); background:var(--pos-bg); border-color:var(--pos-br); }
634
+ .kpi__pill.neg{ color:var(--neg-fg); background:var(--neg-bg); border-color:var(--neg-br); }
635
+
636
+ /* ======= Quality (Sharpe, MaxDD) ======= */
637
+ .quality{ display:grid; grid-template-columns:1fr 1fr; gap:12px; padding:2px 0 0; }
638
+ .qitem{ display:flex; align-items:baseline; gap:8px; }
639
+ .qitem__label{ font-size:12px; color:var(--ink-600); }
640
+ .qitem__value{ font-size:14px; font-weight:800; color:var(--ink-900); }
641
+ .qitem__value.strong{ color:var(--pos-fg); }
642
+ .qitem__value.dd-pos{ color:var(--pos-fg); } /* small drawdown */
643
+ .qitem__value.dd-mid{ color:var(--ink-900); } /* medium drawdown */
644
+ .qitem__value.dd-neg{ color:var(--neg-fg); } /* large drawdown */
645
+
646
+ /* ======= Stats (WinRate, Trades, Days) ======= */
647
+ .stats{ display:grid; grid-template-columns:repeat(3, 1fr); gap:10px; font-size:12px; color:var(--ink-700); }
648
+ .stat{ display:flex; align-items:center; justify-content:space-between; background:#F8FAFD; border:1px solid var(--bd-soft); border-radius:10px; padding:6px 8px; }
649
+ .stat__label{ color:var(--ink-600); }
650
+ .stat__val{ font-weight:800; color:var(--ink-900); }
651
+ .stat__val.pos{ color:var(--pos-fg); }
652
+ .stat__val.neg{ color:var(--neg-fg); }
653
+
654
+ /* ======= Bar vs B&H ======= */
655
+ .bar{ height:6px; border-radius:999px; background:#F2F4F8; overflow:hidden; border:1px solid var(--bd-soft); margin:2px 0 8px; }
656
+ .bar span{ display:block; height:100%; width:var(--bar, 40%); transition:width .5s ease;
657
+ background: linear-gradient(90deg,#16a34a,#22c55e);
658
+ }
659
+ .bar.neg span{ background: linear-gradient(90deg,#ef4444,#dc2626); }
660
+
661
+ /* ======= Meta (Gap vs B&H chip + EOD) ======= */
662
+ .meta{ display:grid; grid-template-columns:1fr auto; align-items:center; gap:8px; }
663
+ .chip{
664
+ display:inline-flex; align-items:center; gap:6px;
665
+ font-size:12px; font-weight:800; padding:4px 8px; border-radius:999px;
666
+ background:#F6F8FB; color:var(--ink-900); border:1px solid var(--bd-soft);
667
  }
668
+ .chip.pos{ color:var(--pos-fg); background:var(--pos-bg); border-color:var(--pos-br); }
669
+ .chip.neg{ color:var(--neg-fg); background:var(--neg-bg); border-color:var(--neg-br); }
670
+ .chip__label{ opacity:.8; }
671
+ .chip__value{ font-variant-numeric: tabular-nums; }
672
+ .eod{ font-size:12px; color:var(--ink-600); }
673
+
674
+ /* ======= Responsive ======= */
675
+ @media (max-width: 900px){
676
+ .stats{ grid-template-columns:1fr 1fr; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  }
678
+ @media (max-width: 640px){
679
+ .cards-grid-f1{ grid-template-columns:1fr; }
680
+ .kpi__value{ font-size:22px; }
681
+ .about-text{ font-size: 14px; }
 
 
 
682
  }
 
 
 
683
  </style>
src/views/RequestView.vue ADDED
@@ -0,0 +1,641 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="arena-page">
3
+ <!-- ============== ASSETS ============== -->
4
+ <section class="section">
5
+ <header class="section-head">
6
+ <h2 class="section-title">
7
+ <span class="ama-gradient">Assets in the Arena</span>
8
+ </h2>
9
+ </header>
10
+
11
+ <!-- Time Window Slider -->
12
+ <div class="time-window-control">
13
+ <div class="time-window-header">
14
+ <span class="time-window-label">Time Window:</span>
15
+ <span class="time-window-range">{{ formatDate(dateRange[0]) }} - {{ formatDate(dateRange[1]) }}</span>
16
+ </div>
17
+ <Slider v-model="dateRange" :min="0" :max="maxDateIndex" :range="true" class="time-slider" />
18
+ </div>
19
+
20
+ <div class="grid grid-assets-4">
21
+ <article v-for="a in assets" :key="a.code" class="card asset-card">
22
+ <div class="asset-head">
23
+ <div class="asset-logo-wrap">
24
+ <img :src="a.icon" :alt="a.code" class="asset-logo" />
25
+ </div>
26
+ <div class="asset-name">
27
+ <div class="asset-code truncate">{{ a.code }}</div>
28
+ <div class="asset-full truncate" :title="a.name">{{ a.name }}</div>
29
+ </div>
30
+ <span class="type-badge" :class="'type-' + a.type.toLowerCase()">{{ a.type }}</span>
31
+ </div>
32
+
33
+ <div class="asset-body">
34
+ <div class="spark-wrap">
35
+ <MiniEchart :data="a.month" :color="a.sparkColor" />
36
+ </div>
37
+
38
+ <div class="asset-stats">
39
+ <div class="stat">
40
+ <div class="stat-label">Return</div>
41
+ <div class="stat-value mono" :class="a.change1m >= 0 ? 'pos' : 'neg'">
42
+ {{ signedPct(a.change1m) }}
43
+ </div>
44
+ </div>
45
+ <div class="stat">
46
+ <div class="stat-label">Volatility</div>
47
+ <div class="stat-value mono">{{ formatPct(a.volatility) }}</div>
48
+ </div>
49
+ <div class="stat">
50
+ <div class="stat-label">Runs</div>
51
+ <div class="stat-value mono">{{ a.runs }}</div>
52
+ </div>
53
+ <div class="stat">
54
+ <div class="stat-label">Agents</div>
55
+ <div class="stat-value mono">{{ a.agents }}</div>
56
+ </div>
57
+ <div class="stat">
58
+ <div class="stat-label">Days</div>
59
+ <div class="stat-value mono">{{ a.days }}</div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="asset-foot">
65
+ <div class="asset-foot-row">
66
+ <span class="muted">EOD</span>
67
+ <span class="mono">{{ a.eod || '—' }}</span>
68
+ </div>
69
+ </div>
70
+ </article>
71
+ </div>
72
+
73
+ <div class="cta-row">
74
+ <a :href="assetFormUrl" target="_blank" rel="noopener" class="btn-cta link-btn">
75
+ <i class="pi pi-plus mr-2"></i> Vote to Add Asset
76
+ </a>
77
+ </div>
78
+ </section>
79
+
80
+
81
+ <!-- ============== AGENTS (no performance) ============== -->
82
+ <section class="section">
83
+ <header class="section-head">
84
+ <h2 class="section-title">
85
+ <span class="ama-gradient">Agents in the Arena</span>
86
+ </h2>
87
+ <p class="section-sub">Tournament cards • GitHub &amp; arXiv links (no performance)</p>
88
+ </header>
89
+
90
+ <div class="grid grid-agents">
91
+ <article v-for="g in agents" :key="g.name" class="card agent-card">
92
+ <div class="agent-top">
93
+ <div class="agent-badge" :style="{ borderColor: g.color }">
94
+ <div class="agent-ring" :style="{ background: g.color }"></div>
95
+ <img :src="g.logo" :alt="g.name" class="agent-logo" />
96
+ </div>
97
+ <div class="agent-title">
98
+ <div class="agent-name truncate" :title="g.name">{{ g.name }}</div>
99
+ <div class="agent-links">
100
+ <a :href="g.github" target="_blank" rel="noopener" class="link">GitHub</a>
101
+ <span class="dot">•</span>
102
+ <a :href="g.arxiv" target="_blank" rel="noopener" class="link">arXiv</a>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </article>
107
+ </div>
108
+
109
+ <!-- Info-only integration guide -->
110
+ <div class="card integration-guide">
111
+ <h3>Agent Integration Guide</h3>
112
+
113
+ <div class="guide-section">
114
+ <h4>Quick Summary</h4>
115
+ <p><strong>What you need:</strong> Create an API that receives market data and returns trading decisions.</p>
116
+ </div>
117
+
118
+ <div class="guide-section">
119
+ <h4>Input (What we send to your agent)</h4>
120
+ <pre class="code-block">{
121
+ "date": "2025-10-24",
122
+ "price": {"BTC": 67890.50},
123
+ "news": {"BTC": ["Bitcoin news 1...", "Bitcoin news 2...", "Bitcoin news 3..."]},
124
+ "model": "gpt-4o",
125
+ "history_price": {
126
+ "BTC": [
127
+ {"date": "2025-10-14", "price": 65000.00},
128
+ {"date": "2025-10-15", "price": 65500.00},
129
+ {"date": "2025-10-16", "price": 66000.00},
130
+ {"date": "2025-10-17", "price": 66200.00},
131
+ {"date": "2025-10-18", "price": 66500.00},
132
+ {"date": "2025-10-21", "price": 66800.00},
133
+ {"date": "2025-10-22", "price": 67000.00},
134
+ {"date": "2025-10-23", "price": 67500.00}
135
+ ]
136
+ }
137
+ }</pre>
138
+ <p class="note"><strong>Note:</strong> <code>history_price</code> contains the last 10 days of price data (if available).</p>
139
+ </div>
140
+
141
+ <div class="guide-section">
142
+ <h4>Output (What we expect from your agent)</h4>
143
+ <pre class="code-block">{
144
+ "recommended_action": "BUY"
145
+ }</pre>
146
+ <p class="note"><strong>Valid actions:</strong> <code>"BUY"</code>, <code>"SELL"</code>, or <code>"HOLD"</code> (uppercase)</p>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Submit Agent button (Google Form link) -->
151
+ <div class="cta-row">
152
+ <a :href="agentFormUrl" target="_blank" rel="noopener" class="btn-cta link-btn">
153
+ <i class="pi pi-send mr-2"></i> Submit Agent (Google Form)
154
+ </a>
155
+ </div>
156
+ </section>
157
+
158
+ <!-- ============== PAPER & CITATION ============== -->
159
+ <section class="section paper-section">
160
+ <header class="section-head">
161
+ <h2 class="section-title">
162
+ <span class="ama-gradient">Paper & Citation</span>
163
+ </h2>
164
+ </header>
165
+
166
+ <div class="paper-card">
167
+ <div class="paper-content">
168
+ <div class="paper-description">
169
+ For more information, please check our released paper in arXiv. And remember to cite us if you find it useful!
170
+ </div>
171
+ <div class="paper-links">
172
+ <a href="https://arxiv.org/pdf/2510.11695" target="_blank" rel="noopener" class="paper-link">
173
+ <i class="pi pi-file-pdf"></i> Read Paper on arXiv
174
+ </a>
175
+ </div>
176
+
177
+ <div class="citation-box">
178
+ <div class="citation-header">
179
+ <span class="citation-label">BibTeX Citation</span>
180
+ <button @click="copyCitation" class="copy-btn" :class="{ 'copied': citationCopied }">
181
+ <i :class="citationCopied ? 'pi pi-check' : 'pi pi-copy'"></i>
182
+ {{ citationCopied ? 'Copied!' : 'Copy' }}
183
+ </button>
184
+ </div>
185
+ <pre class="citation-text">@article{qian2025agents,
186
+ title={When Agents Trade: Live Multi-Market Trading Benchmark for LLM Agents},
187
+ author={Qian, Lingfei and Peng, Xueqing and Wang, Yan and Zhang, Vincent Jim and He, Huan and Smith, Hanley and Han, Yi and He, Yueru and Li, Haohang and Cao, Yupeng and others},
188
+ journal={arXiv preprint arXiv:2510.11695},
189
+ year={2025}
190
+ }</pre>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </section>
195
+ </div>
196
+ </template>
197
+
198
+ <script>
199
+ import { dataService } from '../lib/dataService'
200
+ import { getAllDecisions } from '../lib/dataCache'
201
+ import { readAllRawDecisions } from '../lib/idb'
202
+ import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
203
+ import { computeBuyHoldEquity } from '../lib/perf'
204
+ import MiniEchart from '../components/MiniEchart.vue'
205
+
206
+ export default {
207
+ name: 'RequestView',
208
+ components: { MiniEchart },
209
+ data() {
210
+ return {
211
+ assetFormUrl: 'https://docs.google.com/forms/d/e/1FAIpQLSdA4bZau8X2gEWjxRY3oFolcGa4-s4_D4F5Ky1uh48hnwMkVA/viewform?usp=dialog',
212
+ agentFormUrl: 'https://docs.google.com/forms/d/e/1FAIpQLSdwaheaiVYcBQVNOEyUD6ERTLXD6fKrOgGPKkjTrG9zi_feqw/viewform?usp=publish-editor',
213
+
214
+ // dynamic from dataService
215
+ assets: [],
216
+
217
+ // time window control
218
+ allDates: [],
219
+ dateRange: [0, 0],
220
+ maxDateIndex: 0,
221
+
222
+ // agents (no performance fields shown)
223
+ agents: [
224
+ {
225
+ name: 'InvestorAgent',
226
+ github: 'https://github.com/felis33/INVESTOR-BENCH',
227
+ arxiv: 'https://arxiv.org/abs/2412.18174',
228
+ strategy: 'Aggressive Long Only',
229
+ focus: 'BTC, MSFT',
230
+ color: 'linear-gradient(90deg,#ffd700,#eab308)',
231
+ logo: new URL('../assets/images/agents_images/investor.png', import.meta.url).href
232
+ },
233
+ {
234
+ name: 'TradeAgent',
235
+ github: 'https://github.com/TauricResearch/TradingAgents',
236
+ arxiv: 'https://arxiv.org/abs/2412.20138',
237
+ strategy: 'Aggressive Long Only',
238
+ focus: 'BTC, ETH',
239
+ color: 'linear-gradient(90deg,#4338ca,#6d28d9)',
240
+ logo: new URL('../assets/images/agents_images/tradeagent.png', import.meta.url).href
241
+ },
242
+ {
243
+ name: 'DeepFundAgent',
244
+ github: 'https://github.com/HKUSTDial/DeepFund',
245
+ arxiv: 'https://arxiv.org/abs/2505.11065',
246
+ strategy: 'Aggressive Long Only',
247
+ focus: '—',
248
+ color: 'linear-gradient(90deg,#0ea5e9,#22d3ee)',
249
+ logo: new URL('../assets/images/agents_images/deepfund.png', import.meta.url).href
250
+ },
251
+ {
252
+ name: 'HedgeFundAgent',
253
+ github: 'https://github.com/virattt/ai-hedge-fund',
254
+ arxiv: 'https://github.com/virattt/ai-hedge-fund',
255
+ strategy: 'Aggressive Long Only',
256
+ focus: 'BMRN, TSLA',
257
+ color: 'linear-gradient(90deg,#f43f5e,#ef4444)',
258
+ logo: new URL('../assets/images/agents_images/hedgefund.png', import.meta.url).href
259
+ }
260
+ ],
261
+
262
+ // live state
263
+ rowsRef: [],
264
+ allDecisions: [],
265
+ unsubscribe: null,
266
+
267
+ ASSET_ICONS: {
268
+ BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
269
+ ETH: new URL('../assets/images/assets_images/ETH.png', import.meta.url).href,
270
+ MSFT: new URL('../assets/images/assets_images/MSFT.png', import.meta.url).href,
271
+ BMRN: new URL('../assets/images/assets_images/BMRN.png', import.meta.url).href,
272
+ TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href
273
+ },
274
+
275
+ // Paper & Citation
276
+ citationCopied: false
277
+ }
278
+ },
279
+
280
+ mounted() {
281
+ this.unsubscribe = dataService.subscribe((state) => {
282
+ this.rowsRef = Array.isArray(state.tableRows) ? state.tableRows : []
283
+ this.rebuildAssets().catch(() => {})
284
+ })
285
+ this.rowsRef = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
286
+ if (!dataService.loaded && !dataService.loading) {
287
+ dataService.load(false).catch(e => console.error('RequestView: load failed', e))
288
+ }
289
+
290
+ // warm decision cache
291
+ this.allDecisions = getAllDecisions() || []
292
+ if (!this.allDecisions.length) {
293
+ readAllRawDecisions().then(cached => { if (cached?.length) this.allDecisions = cached })
294
+ }
295
+ this.rebuildAssets().catch(() => {})
296
+ },
297
+
298
+ beforeUnmount() {
299
+ if (this.unsubscribe) { this.unsubscribe(); this.unsubscribe = null }
300
+ },
301
+
302
+ methods: {
303
+ signedPct(p) {
304
+ const v = Number(p || 0) * 100
305
+ const s = v >= 0 ? '+' : '−'
306
+ return `${s}${Math.abs(v).toFixed(2)}%`
307
+ },
308
+ fmtUSD(n) {
309
+ return (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
310
+ },
311
+ formatPct(p) {
312
+ const v = Number(p || 0) * 100
313
+ return `${Math.abs(v).toFixed(2)}%`
314
+ },
315
+ formatDate(index) {
316
+ if (!this.allDates || index < 0 || index >= this.allDates.length) return '—'
317
+ return this.allDates[index]
318
+ },
319
+ calculateVolatility(values) {
320
+ if (!Array.isArray(values) || values.length < 2) return 0
321
+
322
+ // Calculate returns
323
+ const returns = []
324
+ for (let i = 1; i < values.length; i++) {
325
+ if (values[i-1] > 0) {
326
+ returns.push((values[i] - values[i-1]) / values[i-1])
327
+ }
328
+ }
329
+
330
+ if (returns.length === 0) return 0
331
+
332
+ // Calculate mean
333
+ const mean = returns.reduce((sum, r) => sum + r, 0) / returns.length
334
+
335
+ // Calculate variance
336
+ const variance = returns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / returns.length
337
+
338
+ // Return standard deviation (volatility)
339
+ return Math.sqrt(variance)
340
+ },
341
+
342
+ /* ===== Dynamic assets built from B&H equity ===== */
343
+ async buildAssetSeq(assetCode) {
344
+ let seq = (this.allDecisions || []).filter(r => r.asset === assetCode)
345
+ seq.sort((a,b) => (a.date > b.date ? 1 : -1))
346
+ const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
347
+ if (!isCrypto) {
348
+ try { seq = await filterRowsToNyseTradingDays(seq) } catch {}
349
+ }
350
+ return seq
351
+ },
352
+ sampleSeries(values, n = 12) {
353
+ if (!Array.isArray(values) || !values.length) return []
354
+ if (values.length <= n) return values.slice(-n)
355
+ const step = (values.length - 1) / (n - 1)
356
+ const out = []
357
+ for (let i = 0; i < n; i++) out.push(values[Math.round(i * step)])
358
+ return out
359
+ },
360
+ pctChange(a, b) {
361
+ if (a == null || b == null || a === 0) return 0
362
+ return (b / a) - 1
363
+ },
364
+
365
+ async rebuildAssets() {
366
+ console.log('[RequestView] rebuildAssets called, allDecisions:', this.allDecisions?.length || 0, 'rowsRef:', this.rowsRef?.length || 0)
367
+ const assetsInRows = Array.from(new Set((this.rowsRef || []).map(r => r.asset))).filter(Boolean)
368
+ console.log('[RequestView] Assets in rows:', assetsInRows)
369
+ if (!assetsInRows.length) { this.assets = []; return }
370
+
371
+ // Initialize all dates from first asset to set up slider
372
+ if (this.allDates.length === 0 && assetsInRows.length > 0) {
373
+ const firstSeq = await this.buildAssetSeq(assetsInRows[0])
374
+ if (firstSeq.length > 0) {
375
+ this.allDates = firstSeq.map(r => r.date).filter(d => d >= '2025-08-01')
376
+ this.maxDateIndex = this.allDates.length - 1
377
+ this.dateRange = [0, this.maxDateIndex]
378
+ }
379
+ }
380
+
381
+ const cards = []
382
+ for (const code of assetsInRows) {
383
+ const fullSeq = await this.buildAssetSeq(code)
384
+ console.log('[RequestView] Asset', code, 'full seq length:', fullSeq?.length || 0)
385
+ if (!fullSeq.length) continue
386
+
387
+ // Filter sequence based on date range
388
+ const startDate = this.allDates[this.dateRange[0]] || '2025-08-01'
389
+ const endDate = this.allDates[this.dateRange[1]] || this.allDates[this.allDates.length - 1]
390
+ const seq = fullSeq.filter(r => r.date >= startDate && r.date <= endDate)
391
+ console.log('[RequestView] Asset', code, 'filtered seq length:', seq?.length || 0, 'from', startDate, 'to', endDate)
392
+ if (!seq.length) continue
393
+
394
+ // B&H equity as normalized series for spark and change
395
+ let bh = []
396
+ try { bh = computeBuyHoldEquity(seq, 100000) || [] } catch { bh = [] }
397
+ const lastIdx = bh.length - 1
398
+ console.log('[RequestView] Asset', code, 'bh length:', bh?.length || 0)
399
+ if (lastIdx < 0) continue
400
+
401
+ // Get end_date from actual sequence data (most accurate)
402
+ const rowsForAsset = (this.rowsRef || []).filter(r => r.asset === code)
403
+ const lastDate = seq[lastIdx]?.date || null
404
+
405
+ const spark = this.sampleSeries(bh.map(v => v), 12)
406
+ console.log('[RequestView] Asset', code, 'spark data:', spark)
407
+ const change1m = this.pctChange(spark[0], spark[spark.length - 1])
408
+
409
+ // Calculate volatility
410
+ const volatility = this.calculateVolatility(bh)
411
+
412
+ // trading day span
413
+ let days = 0
414
+ try {
415
+ const start = new Date(seq[0].date)
416
+ const end = new Date(seq[lastIdx].date)
417
+ days = Math.max(1, Math.round((end - start) / 86400000) + 1)
418
+ } catch {}
419
+
420
+ // runs & agents from rows table
421
+ const runs = rowsForAsset.length
422
+ const agents = new Set(rowsForAsset.map(r => r.agent_name)).size
423
+
424
+ cards.push({
425
+ code,
426
+ name: code,
427
+ type: (code === 'BTC' || code === 'ETH') ? 'Crypto' : 'Stock',
428
+ icon: this.ASSET_ICONS[code] || null,
429
+ month: spark,
430
+ sparkColor: (code === 'BTC') ? '#f59e0b' : (code === 'ETH' ? '#6366f1' : '#22c55e'),
431
+ change1m,
432
+ volatility,
433
+ runs, agents, days,
434
+ eod: lastDate
435
+ })
436
+ }
437
+
438
+ // Stable display order
439
+ const order = ['BTC','ETH','MSFT','BMRN','TSLA']
440
+ cards.sort((a,b) => (order.indexOf(a.code) - order.indexOf(b.code)))
441
+ this.assets = cards
442
+ },
443
+
444
+ async copyCitation() {
445
+ const citation = `@article{qian2025agents,
446
+ title={When Agents Trade: Live Multi-Market Trading Benchmark for LLM Agents},
447
+ author={Qian, Lingfei and Peng, Xueqing and Wang, Yan and Zhang, Vincent Jim and He, Huan and Smith, Hanley and Han, Yi and He, Yueru and Li, Haohang and Cao, Yupeng and others},
448
+ journal={arXiv preprint arXiv:2510.11695},
449
+ year={2025}
450
+ }`
451
+ try {
452
+ await navigator.clipboard.writeText(citation)
453
+ this.citationCopied = true
454
+ setTimeout(() => {
455
+ this.citationCopied = false
456
+ }, 2000)
457
+ } catch (err) {
458
+ console.error('Failed to copy citation:', err)
459
+ }
460
+ }
461
+ },
462
+
463
+ watch: {
464
+ dateRange() {
465
+ // Rebuild assets when date range changes
466
+ this.rebuildAssets().catch(() => {})
467
+ }
468
+ }
469
+ }
470
+ </script>
471
+
472
+ <style scoped>
473
+ /* ===== AMA Core ===== */
474
+ .ama-gradient{
475
+ background: linear-gradient(90deg, rgb(0,0,185), #7b2cbf, #d946ef, rgb(240,0,15));
476
+ -webkit-background-clip: text; background-clip: text; color: transparent;
477
+ }
478
+
479
+ .arena-page{
480
+ max-width:1280px; margin:0 auto;
481
+ padding:16px 20px 120px; background:#fff;
482
+ }
483
+
484
+ /* ===== Helpers ===== */
485
+ .truncate{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
486
+ .clamp-2{ display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
487
+ .w-full{ width:100%; }
488
+
489
+ /* ===== Time Window Control ===== */
490
+ .time-window-control{ margin:12px 0 16px; padding:16px; background:#F6F8FB; border:1px solid #E7ECF3; border-radius:12px; }
491
+ .time-window-header{ display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
492
+ .time-window-label{ font-weight:700; font-size:14px; color:#0f172a; }
493
+ .time-window-range{ font-size:13px; color:#64748b; font-family: ui-monospace, monospace; }
494
+ .time-slider{ margin-top:8px; }
495
+
496
+ /* ===== Sections ===== */
497
+ .section{ margin-top:18px; }
498
+ .section-head{ margin-bottom:8px; }
499
+ .section-title{ font-size:clamp(1.2rem, 1.2rem + 0.4vw, 1.6rem); font-weight:800; letter-spacing:-0.02em; display:inline-block; }
500
+ .section-sub{ margin-top:4px; color:#6b7280; }
501
+
502
+ /* ===== Grids ===== */
503
+ .grid{ display:grid; gap:14px; min-width:0; }
504
+ .grid-assets-4{ grid-template-columns: repeat(4, minmax(0,1fr)); }
505
+ .grid-agents{ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
506
+ @media (max-width: 1200px){ .grid-assets-4{ grid-template-columns: repeat(3, minmax(0,1fr)); } }
507
+ @media (max-width: 900px){ .grid-assets-4{ grid-template-columns: repeat(2, minmax(0,1fr)); } }
508
+ @media (max-width: 620px){ .grid-assets-4{ grid-template-columns: 1fr; } }
509
+
510
+ /* ===== Card base ===== */
511
+ .card{
512
+ background:#ffffff; border:1px solid #E7ECF3; border-radius:14px;
513
+ box-shadow:0 1px 2px rgba(16,24,40,.03), 0 4px 12px rgba(16,24,40,.04);
514
+ transition: transform .18s ease, box-shadow .2s ease, border-color .2s ease;
515
+ min-width:0;
516
+ }
517
+ .card:hover{ transform:translateY(-2px); box-shadow:0 10px 26px rgba(16,24,40,.08); border-color:#D9E2EF; }
518
+
519
+ /* ===== Assets ===== */
520
+ .asset-card{ padding:12px; display:flex; flex-direction:column; gap:10px; }
521
+ .asset-head{ display:grid; grid-template-columns:44px 1fr auto; gap:10px; align-items:center; min-width:0; }
522
+ .asset-logo-wrap{ width:44px; height:44px; border-radius:12px; background:#f3f4f6; display:grid; place-items:center; border:1px solid #E7ECF3; overflow:hidden; }
523
+ .asset-logo{ width:70%; height:70%; object-fit:contain; }
524
+ .asset-name{ min-width:0; }
525
+ .asset-code{ font-weight:900; color:#0f172a; letter-spacing:.02em; }
526
+ .asset-full{ font-size:12px; color:#64748b; }
527
+ .type-badge{
528
+ border-radius:999px; padding:4px 8px; font-size:12px; font-weight:700;
529
+ border:1px solid #E7ECF3; background:#F6F8FB; color:#0f172a; white-space:nowrap;
530
+ }
531
+ .type-crypto{ background:#f8fbff; border-color:#e0e7ff; color:#1e3a8a; }
532
+ .type-stock { background:#f6fffb; border-color:#d9fbe6; color:#064e3b; }
533
+
534
+ .asset-body{ display:flex; flex-direction:column; gap:10px; }
535
+ .spark-wrap{ width:100%; height:90px; overflow:hidden; border-radius:8px; background:#F6F8FB; border:1px solid #E7ECF3; position:relative; }
536
+ .asset-stats{ display:grid; grid-template-columns:repeat(5, minmax(0, 1fr)); gap:6px; }
537
+ .stat{ background:#F6F8FB; border:1px solid #E7ECF3; border-radius:8px; padding:4px 6px; min-width:0; display:flex; flex-direction:column; align-items:center; text-align:center; }
538
+ .stat-label{ font-size:9px; color:#6b7280; margin-bottom:2px; white-space:nowrap; }
539
+ .stat-value{ font-weight:700; font-size:11.5px; color:#0f172a; line-height:1.2; }
540
+ .mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
541
+ .muted{ color:#6b7280; }
542
+
543
+ .asset-desc{ margin-top:2px; font-size:12.5px; line-height:1.45; }
544
+
545
+ .asset-foot{ margin-top:4px; }
546
+ .asset-bar{ height:6px; border-radius:999px; border:1px solid #E7ECF3; background:#F2F4F8; position:relative; overflow:hidden; }
547
+ .asset-bar::after{ content:''; position:absolute; left:0; top:0; height:100%; width:var(--pct,0%); background:linear-gradient(90deg,#16a34a,#22c55e); }
548
+ .asset-foot-row{ margin-top:6px; display:flex; align-items:center; justify-content:space-between; color:#6b7280; }
549
+
550
+ /* ===== Agents (perf-free) ===== */
551
+ .agent-card{ padding:12px; display:flex; flex-direction:column; gap:8px; }
552
+ .agent-top{ display:grid; grid-template-columns:52px 1fr; gap:10px; align-items:center; min-width:0; }
553
+ .agent-badge{ width:52px; height:52px; border-radius:16px; border:2px solid #e5e7eb; position:relative; display:grid; place-items:center; overflow:hidden; }
554
+ .agent-ring{ position:absolute; inset:0; opacity:.18; }
555
+ .agent-logo{ width:70%; height:70%; object-fit:contain; position:relative; z-index:1; }
556
+ .agent-title{ min-width:0; }
557
+ .agent-name{ font-weight:900; letter-spacing:.02em; color:#0f172a; text-transform:uppercase; }
558
+ .agent-links{ display:flex; align-items:center; gap:8px; color:#64748b; flex-wrap:wrap; min-width:0; }
559
+ .link{ color:#334155; font-weight:600; text-decoration:none; }
560
+ .link:hover{ color:#0f172a; text-decoration:underline; }
561
+ .dot{ color:#94a3b8; }
562
+ .agent-foot{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:2px; }
563
+ .chip{ display:inline-flex; align-items:center; gap:6px; padding:4px 8px; border-radius:999px; font-size:12px; font-weight:700; background:#F6F8FB; color:#0f172a; border:1px solid #E7ECF3; white-space:nowrap; }
564
+ .chip-outline{ background:#fff; }
565
+
566
+ /* ===== Integration Guide ===== */
567
+ .integration-guide{ margin-top:12px; padding:16px; }
568
+ .integration-guide h3{ font-weight:800; font-size:1.1rem; color:#0f172a; margin-bottom:8px; }
569
+ .guide-section{ margin:10px 0; }
570
+ .code-block{ background:#0b1020; color:#E6EDF3; border-radius:10px; padding:12px; overflow:auto; font-size:12.5px; line-height:1.5; }
571
+ .note{ color:#334155; margin-top:6px; }
572
+
573
+ /* ===== Buttons ===== */
574
+ .cta-row{ margin-top:12px; display:flex; justify-content:center; }
575
+ .link-btn{ display:inline-flex; align-items:center; justify-content:center; gap:.5rem; text-decoration:none; }
576
+ .btn-cta{ background:linear-gradient(90deg, rgb(0,0,185), #7b2cbf, #d946ef, rgb(240,0,15)); border:none; color:#fff; font-weight:800; letter-spacing:.02em; padding:10px 16px; border-radius:12px; cursor:pointer; }
577
+ .mr-2{ margin-right:.5rem; }
578
+ .pos{ color:#0e7a3a; } .neg{ color:#B91C1C; }
579
+
580
+ /* ===== Paper & Citation ===== */
581
+ .paper-section{ margin-top:40px; margin-bottom:40px; }
582
+ .paper-card{
583
+ background:#ffffff; border:1px solid #E7ECF3; border-radius:14px;
584
+ box-shadow:0 1px 2px rgba(16,24,40,.03), 0 4px 12px rgba(16,24,40,.04);
585
+ padding:24px;
586
+ }
587
+ .paper-content{ display:flex; flex-direction:column; gap:20px; }
588
+ .paper-description{
589
+ font-size:1.05rem; font-weight:500; color:#334155;
590
+ line-height:1.6; margin:0;
591
+ }
592
+ .paper-links{ display:flex; gap:12px; align-items:center; }
593
+ .paper-link{
594
+ display:inline-flex; align-items:center; gap:8px;
595
+ padding:10px 18px; background:#f3f4f6; border:1px solid #E7ECF3;
596
+ border-radius:10px; text-decoration:none; color:#0f172a;
597
+ font-weight:700; font-size:14px; transition:all .2s ease;
598
+ }
599
+ .paper-link:hover{
600
+ background:#e5e7eb; border-color:#cbd5e1; transform:translateY(-1px);
601
+ box-shadow:0 4px 12px rgba(16,24,40,.08);
602
+ }
603
+ .paper-link i{ font-size:16px; }
604
+
605
+ .citation-box{
606
+ background:#f8fafc; border:1px solid #e2e8f0;
607
+ border-radius:12px; padding:16px;
608
+ }
609
+ .citation-header{
610
+ display:flex; justify-content:space-between; align-items:center;
611
+ margin-bottom:12px;
612
+ }
613
+ .citation-label{
614
+ font-weight:700; font-size:14px; color:#0f172a;
615
+ }
616
+ .copy-btn{
617
+ display:inline-flex; align-items:center; gap:6px;
618
+ padding:6px 12px; background:#ffffff; border:1px solid #cbd5e1;
619
+ border-radius:8px; cursor:pointer; font-weight:600;
620
+ font-size:13px; color:#334155; transition:all .2s ease;
621
+ }
622
+ .copy-btn:hover{
623
+ background:#f1f5f9; border-color:#94a3b8;
624
+ }
625
+ .copy-btn.copied{
626
+ background:#dcfce7; border-color:#86efac; color:#166534;
627
+ }
628
+ .copy-btn i{ font-size:12px; }
629
+
630
+ .citation-text{
631
+ background:#0b1020; color:#E6EDF3; border-radius:8px;
632
+ padding:14px; overflow:auto; font-family: ui-monospace, monospace;
633
+ font-size:13px; line-height:1.6; margin:0; white-space:pre;
634
+ }
635
+
636
+ @media (max-width: 620px){
637
+ .paper-card{ padding:16px; }
638
+ .paper-description{ font-size:0.95rem; }
639
+ .citation-header{ flex-direction:column; align-items:flex-start; gap:8px; }
640
+ }
641
+ </style>