lfqian commited on
Commit
63b6304
·
2 Parent(s): ace48ae 51de453

Merge github/main into main - update from vjz repository

Browse files
.gitattributes CHANGED
@@ -1,4 +1,4 @@
1
- * text=auto eol=lf
2
  *.png filter=lfs diff=lfs merge=lfs -text
3
  *.jpg filter=lfs diff=lfs merge=lfs -text
4
  *.jpeg filter=lfs diff=lfs merge=lfs -text
 
1
+ Dockerfile merge=ours
2
  *.png filter=lfs diff=lfs merge=lfs -text
3
  *.jpg filter=lfs diff=lfs merge=lfs -text
4
  *.jpeg filter=lfs diff=lfs merge=lfs -text
Dockerfile CHANGED
@@ -1,33 +1,33 @@
1
  # ---------- build ----------
2
- FROM node:20-alpine AS build
3
- WORKDIR /app
4
-
5
- # deps
6
- COPY package.json package-lock.json* ./
7
- RUN npm ci --no-audit --no-fund || npm i --no-audit --no-fund
8
-
9
- # app source
10
- COPY . .
11
-
12
- # Mount HF secrets at build-time, write .env.production, then build
13
- # (These IDs MUST match your Secrets names in the Settings tab)
14
- RUN --mount=type=secret,id=VITE_SUPABASE_URL,mode=0444,required=true \
15
- --mount=type=secret,id=VITE_SUPABASE_ANON_KEY,mode=0444,required=true \
16
- sh -lc '\
17
- URL="$(cat /run/secrets/VITE_SUPABASE_URL)"; \
18
- ANON="$(cat /run/secrets/VITE_SUPABASE_ANON_KEY)"; \
19
- printf "VITE_SUPABASE_URL=%s\nVITE_SUPABASE_ANON_KEY=%s\n" "$URL" "$ANON" > .env.production; \
20
- echo "--- .env.production ---"; cat .env.production; echo "-----------------------"; \
21
- npm run build \
22
- '
23
-
24
- # ---------- runtime ----------
25
- FROM node:20-alpine AS runtime
26
- WORKDIR /app
27
-
28
- # serve built assets
29
- COPY --from=build /app/dist ./dist
30
- RUN npm i -g serve@14
31
-
32
- EXPOSE 4173
33
- CMD ["serve", "-s", "dist", "-l", "4173"]
 
1
  # ---------- build ----------
2
+ FROM node:20-alpine AS build
3
+ WORKDIR /app
4
+
5
+ # deps
6
+ COPY package.json package-lock.json* ./
7
+ RUN npm ci --no-audit --no-fund || npm i --no-audit --no-fund
8
+
9
+ # app source
10
+ COPY . .
11
+
12
+ # Mount HF secrets at build-time, write .env.production, then build
13
+ # (These IDs MUST match your Secrets names in the Settings tab)
14
+ RUN --mount=type=secret,id=VITE_SUPABASE_URL,mode=0444,required=true \
15
+ --mount=type=secret,id=VITE_SUPABASE_ANON_KEY,mode=0444,required=true \
16
+ sh -lc '\
17
+ URL="$(cat /run/secrets/VITE_SUPABASE_URL)"; \
18
+ ANON="$(cat /run/secrets/VITE_SUPABASE_ANON_KEY)"; \
19
+ printf "VITE_SUPABASE_URL=%s\nVITE_SUPABASE_ANON_KEY=%s\n" "$URL" "$ANON" > .env.production; \
20
+ echo "--- .env.production ---"; cat .env.production; echo "-----------------------"; \
21
+ npm run build \
22
+ '
23
+
24
+ # ---------- runtime ----------
25
+ FROM node:20-alpine AS runtime
26
+ WORKDIR /app
27
+
28
+ # serve built assets
29
+ COPY --from=build /app/dist ./dist
30
+ RUN npm i -g serve@14
31
+
32
+ EXPOSE 4173
33
+ CMD ["serve", "-s", "dist", "-l", "4173"]
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/components/AgentFilters.vue CHANGED
@@ -186,18 +186,29 @@ export default {
186
  initializeSlider() {
187
  if (this.hasDateBounds) {
188
  // 查找 2025-08-01 在日期数组中的索引
189
- const defaultStartDate = new Date('2025-08-01')
190
  let startIndex = 0
191
 
 
 
 
 
 
 
 
 
192
  // 如果默认起始日期在范围内,找到对应的索引
193
- if (defaultStartDate >= new Date(this.dateBounds.min) && defaultStartDate <= new Date(this.dateBounds.max)) {
194
  startIndex = this.allDates.findIndex(d => {
195
- const dateStr = d.toISOString().split('T')[0]
196
- return dateStr === '2025-08-01'
197
  })
198
  // 如果没找到精确日期,找最接近的日期
199
  if (startIndex === -1) {
200
- startIndex = this.allDates.findIndex(d => d >= defaultStartDate)
 
 
 
201
  if (startIndex === -1) startIndex = 0
202
  }
203
  }
@@ -281,6 +292,8 @@ export default {
281
  // 重置到默认起始日期 2025-08-01
282
  this.initializeSlider()
283
  this.datesModel = []
 
 
284
  }
285
  },
286
  beforeUnmount() {
 
186
  initializeSlider() {
187
  if (this.hasDateBounds) {
188
  // 查找 2025-08-01 在日期数组中的索引
189
+ const targetDateStr = '2025-08-01'
190
  let startIndex = 0
191
 
192
+ // Normalize date bounds to strings for comparison
193
+ const minDateStr = this.dateBounds.min instanceof Date
194
+ ? this.dateBounds.min.toISOString().split('T')[0]
195
+ : (typeof this.dateBounds.min === 'string' ? this.dateBounds.min.split('T')[0] : '')
196
+ const maxDateStr = this.dateBounds.max instanceof Date
197
+ ? this.dateBounds.max.toISOString().split('T')[0]
198
+ : (typeof this.dateBounds.max === 'string' ? this.dateBounds.max.split('T')[0] : '')
199
+
200
  // 如果默认起始日期在范围内,找到对应的索引
201
+ if (targetDateStr >= minDateStr && targetDateStr <= maxDateStr) {
202
  startIndex = this.allDates.findIndex(d => {
203
+ const dateStr = d instanceof Date ? d.toISOString().split('T')[0] : (typeof d === 'string' ? d.split('T')[0] : '')
204
+ return dateStr === targetDateStr
205
  })
206
  // 如果没找到精确日期,找最接近的日期
207
  if (startIndex === -1) {
208
+ startIndex = this.allDates.findIndex(d => {
209
+ const dateStr = d instanceof Date ? d.toISOString().split('T')[0] : (typeof d === 'string' ? d.split('T')[0] : '')
210
+ return dateStr >= targetDateStr
211
+ })
212
  if (startIndex === -1) startIndex = 0
213
  }
214
  }
 
292
  // 重置到默认起始日期 2025-08-01
293
  this.initializeSlider()
294
  this.datesModel = []
295
+ // 触发 slider 结束事件
296
+ this.onSliderEnd()
297
  }
298
  },
299
  beforeUnmount() {
src/views/LeaderboardView.vue CHANGED
@@ -94,7 +94,6 @@ import { countNonTradingDaysBetweenForAsset, countTradingDaysBetweenForAsset } f
94
  import { computeBuyHoldEquity, computeStrategyEquity, calculateMetricsFromSeries, computeWinRate } from '../lib/perf.js'
95
  import { STRATEGIES } from '../lib/strategies.js'
96
  import emailjs from 'emailjs-com'
97
-
98
  export default {
99
  name: 'LeaderboardView',
100
  components: { AgentTable, AgentFilters, AssetsFilter, CompareChartE, Dialog, InputText },
@@ -208,9 +207,17 @@ export default {
208
  try {
209
  const isCrypto = row.asset === 'BTC' || row.asset === 'ETH'
210
  const seriesAll = Array.isArray(row.series) ? row.series : []
 
 
 
 
 
211
  const inRange = seriesAll.filter(p => {
212
- const d = new Date(p.date)
213
- return d >= start && d <= end
 
 
 
214
  })
215
  if (!inRange.length) return null
216
  // Get correct strategy config - row.strategy is the ID, we need the strategy type
 
94
  import { computeBuyHoldEquity, computeStrategyEquity, calculateMetricsFromSeries, computeWinRate } from '../lib/perf.js'
95
  import { STRATEGIES } from '../lib/strategies.js'
96
  import emailjs from 'emailjs-com'
 
97
  export default {
98
  name: 'LeaderboardView',
99
  components: { AgentTable, AgentFilters, AssetsFilter, CompareChartE, Dialog, InputText },
 
207
  try {
208
  const isCrypto = row.asset === 'BTC' || row.asset === 'ETH'
209
  const seriesAll = Array.isArray(row.series) ? row.series : []
210
+
211
+ // Normalize dates to YYYY-MM-DD format for comparison to avoid timezone issues
212
+ const startDateStr = start instanceof Date ? start.toISOString().split('T')[0] : (typeof start === 'string' ? start.split('T')[0] : start)
213
+ const endDateStr = end instanceof Date ? end.toISOString().split('T')[0] : (typeof end === 'string' ? end.split('T')[0] : end)
214
+
215
  const inRange = seriesAll.filter(p => {
216
+ if (!p || !p.date) return false
217
+ // Extract date part (YYYY-MM-DD) from the date string
218
+ const dateStr = typeof p.date === 'string' ? p.date.split('T')[0] : new Date(p.date).toISOString().split('T')[0]
219
+ // Use string comparison for dates to avoid timezone issues
220
+ return dateStr >= startDateStr && dateStr <= endDateStr
221
  })
222
  if (!inRange.length) return null
223
  // Get correct strategy config - row.strategy is the ID, we need the strategy type
src/views/LiveView.vue CHANGED
@@ -204,6 +204,7 @@ 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
@@ -318,24 +319,22 @@ async function buildSeq(sel) {
318
  : allDecisions.filter(r => r.agent_name === agentName && r.asset === assetCode && r.model === model)
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
 
337
  async function computeEquities(sel) {
338
  const seq = await buildSeq(sel)
 
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 }
 
204
  const rowsRef = ref([])
205
  let allDecisions = []
206
  const cards = shallowRef([])
207
+ window.cards = cards
208
  const refreshing = ref(false)
209
 
210
  let unsubscribe = null
 
319
  : allDecisions.filter(r => r.agent_name === agentName && r.asset === assetCode && r.model === model)
320
 
321
  seq.sort((a,b) => (a.date > b.date ? 1 : -1))
322
+ const cutoff = ASSET_CUTOFF[assetCode]
323
+ if (cutoff) {
324
+ const t0 = new Date(cutoff + 'T00:00:00Z')
325
+ seq = seq.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
326
+ }
327
  // if using decision_ids, data is already prefiltered
328
  if (!ids.length) {
329
  const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
330
  if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
 
 
 
 
 
 
331
  }
 
332
  return seq
333
  }
334
 
335
  async function computeEquities(sel) {
336
  const seq = await buildSeq(sel)
337
+
338
  if (!seq.length) return null
339
 
340
  const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }