Spaces:
Running
Running
Commit
·
5481890
1
Parent(s):
6ddefe6
backup
Browse files- get_return.py +662 -0
- src/assets/images/companies_images/columbia.png +3 -0
- src/assets/images/companies_images/florida.png +3 -0
- src/assets/images/companies_images/georgia.png +3 -0
- src/assets/images/companies_images/harvard.png +3 -0
- src/assets/images/companies_images/montreal.png +3 -0
- src/assets/images/companies_images/stevens.png +3 -0
- src/components/AgentFilters.vue +5 -1
- src/components/AgentTable.vue +52 -9
- src/components/AssetsFilter.vue +73 -41
- src/components/CompareChartE.vue +48 -18
- src/components/Footer.vue +0 -98
- src/components/FooterOpen.vue +138 -0
- src/components/Header.vue +2 -2
- src/components/HeaderOpen.vue +138 -0
- src/components/MiniEchart.vue +131 -0
- src/lib/autoRefresh.js +129 -0
- src/main.js +4 -0
- src/pages/Main.vue +12 -3
- src/router/index.js +5 -4
- src/styles/globals.css +37 -0
- src/views/AddAssetView.vue +111 -2
- src/views/{LeadboardView.vue → LeaderboardView.vue} +172 -22
- src/views/LiveView.vue +421 -237
- src/views/RequestView.vue +641 -0
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
|
src/assets/images/companies_images/florida.png
ADDED
|
Git LFS Details
|
src/assets/images/companies_images/georgia.png
ADDED
|
Git LFS Details
|
src/assets/images/companies_images/harvard.png
ADDED
|
Git LFS Details
|
src/assets/images/companies_images/montreal.png
ADDED
|
Git LFS Details
|
src/assets/images/companies_images/stevens.png
ADDED
|
Git LFS Details
|
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:
|
| 4 |
-
<Column expander style="width:
|
| 5 |
-
<Column field="agent_name" header="Agent & Model
|
| 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
},
|
| 47 |
selectedAsset() {
|
| 48 |
const selected = this.modelValue || []
|
|
@@ -77,15 +70,54 @@ export default {
|
|
| 77 |
</script>
|
| 78 |
|
| 79 |
<style scoped>
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: '#
|
| 31 |
-
DeepFundAgent: '#
|
| 32 |
-
TradeAgent: '#
|
| 33 |
-
InvestorAgent: '#
|
| 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}
|
| 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:
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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')">
|
| 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 |
-
<
|
|
|
|
|
|
|
| 5 |
<Footer />
|
| 6 |
</div>
|
| 7 |
</template>
|
| 8 |
|
| 9 |
<script>
|
| 10 |
-
import Header from '../components/
|
| 11 |
-
import Footer from '../components/
|
| 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
|
| 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: '/
|
| 15 |
children: [
|
| 16 |
-
{ path: '/
|
| 17 |
{ path: '/live', name: 'live', component: LiveView },
|
| 18 |
-
{ path: '/add-asset', name: 'add-asset', component:
|
| 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">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 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 |
-
<
|
| 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
|
| 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: '
|
| 104 |
-
components: { AgentTable, AgentFilters, AssetsFilter,
|
| 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 |
-
|
|
|
|
| 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:
|
|
|
|
| 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:
|
| 346 |
}
|
| 347 |
.loading-overlay {
|
| 348 |
position: fixed;
|
| 349 |
inset: 0;
|
| 350 |
-
background: rgba(255, 255, 255, 0.
|
| 351 |
z-index: 1000;
|
| 352 |
display: flex;
|
| 353 |
align-items: center;
|
| 354 |
justify-content: center;
|
| 355 |
}
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|
| 367 |
-
|
| 368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
/* compact spacing for higher information density */
|
| 371 |
-
.compact-card :deep(.p-card-body) {
|
| 372 |
-
|
| 373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
</div>
|
|
|
|
| 8 |
<div class="toolbar__right">
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
</div>
|
| 21 |
</div>
|
| 22 |
</header>
|
|
@@ -34,18 +62,25 @@
|
|
| 34 |
</div>
|
| 35 |
</section>
|
| 36 |
|
| 37 |
-
<!--
|
| 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="{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
:style="{ '--bar': (c.barPct ?? 0) + '%'}"
|
| 46 |
>
|
| 47 |
-
<!--
|
| 48 |
-
<
|
| 49 |
|
| 50 |
<!-- Header: logo + names -->
|
| 51 |
<header class="head">
|
|
@@ -59,32 +94,61 @@
|
|
| 59 |
</div>
|
| 60 |
</header>
|
| 61 |
|
| 62 |
-
<!-- Net value
|
| 63 |
-
<div class="
|
| 64 |
-
<div class="
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
</div>
|
| 67 |
|
| 68 |
<!-- Performance bar (vs B&H) -->
|
| 69 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
<span :style="{ width: (c.barPct ?? 0) + '%' }"></span>
|
| 71 |
</div>
|
| 72 |
|
| 73 |
-
<!--
|
| 74 |
-
<div class="
|
| 75 |
-
<div class="
|
| 76 |
-
<
|
| 77 |
-
|
| 78 |
-
|
| 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 |
-
</
|
| 84 |
-
|
| 85 |
-
|
| 86 |
</div>
|
| 87 |
-
<div class="eod">EOD {{ c.date
|
| 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 |
-
/*
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
/* ---------- config ---------- */
|
| 113 |
-
const orderedAssets = ['BTC','ETH','
|
| 114 |
-
const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla'])
|
|
|
|
|
|
|
| 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 |
-
|
| 142 |
-
|
| 143 |
-
|
| 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 |
-
|
| 159 |
-
|
| 160 |
-
|
| 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
|
| 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
|
| 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
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
//
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
}
|
| 281 |
|
| 282 |
/* build cards whenever winners/asset change */
|
|
@@ -294,7 +393,28 @@ watch(
|
|
| 294 |
|
| 295 |
if (!perfs.length) { cards.value = []; return }
|
| 296 |
|
| 297 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 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 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
transition: all 0.2s ease;
|
|
|
|
| 377 |
}
|
| 378 |
-
.
|
| 379 |
-
|
| 380 |
-
border-color:
|
| 381 |
}
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
}
|
| 386 |
-
.
|
| 387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
}
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
|
|
|
|
|
|
| 392 |
}
|
| 393 |
-
.
|
| 394 |
-
.
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 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 |
-
|
| 427 |
-
|
| 428 |
-
.
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
font-
|
| 437 |
-
color:
|
| 438 |
-
letter-spacing: .04em;
|
| 439 |
}
|
| 440 |
-
.
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
color: #4b5563;
|
| 446 |
}
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
}
|
| 454 |
|
| 455 |
-
/*
|
| 456 |
-
.
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
}
|
| 462 |
-
.
|
| 463 |
-
.
|
| 464 |
-
.
|
| 465 |
-
.
|
| 466 |
-
.
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
.
|
| 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 |
-
|
| 489 |
-
grid-
|
| 490 |
-
|
| 491 |
-
|
| 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&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&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 & 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>
|