Jimin Huang commited on
Commit
f13dc89
·
2 Parent(s): b1722bb 6ddefe6

Merge branch 'main' of https://huggingface.co/spaces/TheFinAI/Agent-Market-Arena into main

Browse files
.gitignore CHANGED
@@ -28,3 +28,4 @@ coverage
28
  *.sw?
29
 
30
  *.tsbuildinfo
 
 
28
  *.sw?
29
 
30
  *.tsbuildinfo
31
+ .env
agent_submissions_setup.sql ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- 1. 添加 agent_submissions_data JSON 列
2
+ ALTER TABLE trading_decisions
3
+ ADD COLUMN IF NOT EXISTS agent_submissions_data JSONB;
4
+
5
+ -- 2. 初始化数据(在 InvestorAgent, 2025-08-01, BTC, gpt_4.1 这一行)
6
+ UPDATE trading_decisions
7
+ SET agent_submissions_data = '[]'::jsonb
8
+ WHERE agent_name = 'InvestorAgent'
9
+ AND date = '2025-08-01'
10
+ AND asset = 'BTC'
11
+ AND model = 'gpt_4.1';
12
+
13
+ -- 3. 验证(可选)
14
+ SELECT agent_name, date, asset, model, agent_submissions_data
15
+ FROM trading_decisions
16
+ WHERE agent_name = 'InvestorAgent'
17
+ AND date = '2025-08-01'
18
+ AND asset = 'BTC'
19
+ AND model = 'gpt_4.1';
20
+
src/components/CompareChartE.vue CHANGED
@@ -368,7 +368,15 @@ export default defineComponent({
368
  }
369
  },
370
  legend: { show: false },
371
- xAxis: { type: 'time' },
 
 
 
 
 
 
 
 
372
  yAxis: this.mode === 'pct'
373
  ? {
374
  type: 'value', scale: true,
 
368
  }
369
  },
370
  legend: { show: false },
371
+ xAxis: {
372
+ type: 'time',
373
+ axisLabel: {
374
+ formatter: (value) => {
375
+ const date = new Date(value);
376
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
377
+ }
378
+ }
379
+ },
380
  yAxis: this.mode === 'pct'
381
  ? {
382
  type: 'value', scale: true,
src/views/AddAssetView.vue CHANGED
@@ -214,17 +214,53 @@ export default {
214
  this.agentForm.message = ''
215
 
216
  try {
217
- // 保存到 localStorage 以便管理员查看
218
- const agentSubmissions = JSON.parse(localStorage.getItem('agentSubmissions') || '[]')
219
- agentSubmissions.push({
 
 
 
 
 
 
 
 
 
 
 
 
220
  name: this.agentForm.name,
221
  endpoint: this.agentForm.endpoint,
222
  description: this.agentForm.description,
223
- timestamp: new Date().toISOString()
224
- })
225
- localStorage.setItem('agentSubmissions', JSON.stringify(agentSubmissions))
226
 
227
- this.agentForm.message = 'Agent submitted successfully! We will review it soon.'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  this.agentForm.messageType = 'success'
229
 
230
  // 滚动到消息位置
@@ -235,7 +271,7 @@ export default {
235
  }
236
  })
237
 
238
- // Clear form
239
  setTimeout(() => {
240
  this.agentForm.name = ''
241
  this.agentForm.endpoint = ''
@@ -245,7 +281,8 @@ export default {
245
 
246
  } catch (error) {
247
  console.error('Error submitting agent:', error)
248
- this.agentForm.message = 'Failed to submit agent. Please try again.'
 
249
  this.agentForm.messageType = 'error'
250
 
251
  // 滚动到消息位置
@@ -360,6 +397,7 @@ export default {
360
  grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
361
  gap: 2rem;
362
  padding: 0 1rem;
 
363
  }
364
 
365
  .feature-card {
@@ -367,6 +405,8 @@ export default {
367
  border-radius: 12px;
368
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
369
  transition: transform 0.2s, box-shadow 0.2s;
 
 
370
  }
371
 
372
  .feature-card:hover {
 
214
  this.agentForm.message = ''
215
 
216
  try {
217
+ // 读取现有的 agent submissions 数据
218
+ const { data: row, error: fetchError } = await supabase
219
+ .from('trading_decisions')
220
+ .select('agent_submissions_data')
221
+ .eq('agent_name', 'InvestorAgent')
222
+ .eq('date', '2025-08-01')
223
+ .eq('asset', 'BTC')
224
+ .eq('model', 'gpt_4.1')
225
+ .single()
226
+
227
+ if (fetchError) throw fetchError
228
+
229
+ let submissions = row?.agent_submissions_data || []
230
+
231
+ const agentSubmission = {
232
  name: this.agentForm.name,
233
  endpoint: this.agentForm.endpoint,
234
  description: this.agentForm.description,
235
+ timestamp: new Date().toISOString(),
236
+ votes: 1
237
+ }
238
 
239
+ // 检查是否已存在相同 name agent
240
+ const existingIndex = submissions.findIndex(s => s.name === agentSubmission.name)
241
+ if (existingIndex >= 0) {
242
+ // 重复提交时更新所有信息和增加投票
243
+ submissions[existingIndex].votes += 1
244
+ submissions[existingIndex].timestamp = agentSubmission.timestamp
245
+ submissions[existingIndex].endpoint = agentSubmission.endpoint
246
+ submissions[existingIndex].description = agentSubmission.description
247
+ } else {
248
+ submissions.push(agentSubmission)
249
+ }
250
+
251
+ // 更新到 Supabase
252
+ const { error: updateError } = await supabase
253
+ .from('trading_decisions')
254
+ .update({ agent_submissions_data: submissions })
255
+ .eq('agent_name', 'InvestorAgent')
256
+ .eq('date', '2025-08-01')
257
+ .eq('asset', 'BTC')
258
+ .eq('model', 'gpt_4.1')
259
+
260
+ if (updateError) throw updateError
261
+
262
+ // 显示成功消息并清空表单
263
+ this.agentForm.message = 'Agent submitted successfully! Thank you for joining the arena.'
264
  this.agentForm.messageType = 'success'
265
 
266
  // 滚动到消息位置
 
271
  }
272
  })
273
 
274
+ // 5秒后清空表单
275
  setTimeout(() => {
276
  this.agentForm.name = ''
277
  this.agentForm.endpoint = ''
 
281
 
282
  } catch (error) {
283
  console.error('Error submitting agent:', error)
284
+ console.error('Error details:', JSON.stringify(error, null, 2))
285
+ this.agentForm.message = `Failed to submit agent: ${error.message || 'Please try again'}`
286
  this.agentForm.messageType = 'error'
287
 
288
  // 滚动到消息位置
 
397
  grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
398
  gap: 2rem;
399
  padding: 0 1rem;
400
+ margin-bottom: 4rem;
401
  }
402
 
403
  .feature-card {
 
405
  border-radius: 12px;
406
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
407
  transition: transform 0.2s, box-shadow 0.2s;
408
+ max-height: 800px;
409
+ overflow-y: auto;
410
  }
411
 
412
  .feature-card:hover {
src/views/LiveView.vue CHANGED
@@ -6,6 +6,14 @@
6
  <AssetTabs v-model="asset" :ordered-assets="orderedAssets" />
7
  </div>
8
  <div class="toolbar__right">
 
 
 
 
 
 
 
 
9
  <div class="mode">
10
  <button class="mode__btn" :class="{ 'is-active': mode==='usd' }" @click="mode='usd'">$</button>
11
  <button class="mode__btn" :class="{ 'is-active': mode==='pct' }" @click="mode='pct'">%</button>
@@ -36,9 +44,7 @@
36
  :class="{ winner: c.isWinner, bh: c.kind==='bh', neg: (c.gapUsd ?? 0) < 0, pos: (c.gapUsd ?? 0) >= 0 }"
37
  :style="{ '--bar': (c.barPct ?? 0) + '%'}"
38
  >
39
- <!-- Rank / BH badge -->
40
- <div v-if="c.rank" class="rank">{{ c.rank }}</div>
41
- <div v-else-if="c.kind==='bh'" class="rank bh-badge">B&H</div>
42
  <span v-if="c.isWinner" class="crown" aria-label="Top performer">👑</span>
43
 
44
  <!-- Header: logo + names -->
@@ -126,6 +132,7 @@ const ASSET_CUTOFF = { BTC: '2025-08-01' }
126
  const mode = ref('usd')
127
  const asset = ref('BTC')
128
  const rowsRef = ref([])
 
129
  let allDecisions = []
130
  const cards = shallowRef([])
131
 
@@ -148,6 +155,23 @@ onMounted(async () => {
148
  }
149
  })
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  /* ---------- helpers ---------- */
152
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
153
  const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
@@ -155,10 +179,11 @@ const signedPct = (p) => `${(p >= 0 ? '+' : '−')}${Math.abs(p * 100).toFixed(2
155
  const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
156
  const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
157
 
158
- /* rows for selected asset (exclude vanilla/vinilla) */
159
  const filteredRows = computed(() =>
160
  (rowsRef.value || []).filter(r => {
161
  if (r.asset !== asset.value) return false
 
162
  const name = (r?.agent_name || '').toLowerCase()
163
  return !EXCLUDED_AGENT_NAMES.has(name)
164
  })
@@ -172,7 +197,14 @@ const winners = computed(() => {
172
  const cur = byAgent.get(k)
173
  if (!cur || score(r) > score(cur)) byAgent.set(k, r)
174
  }
175
- return [...byAgent.values()]
 
 
 
 
 
 
 
176
  })
177
 
178
  /* chart selections */
@@ -202,16 +234,20 @@ async function buildSeq(sel) {
202
 
203
  seq.sort((a,b) => (a.date > b.date ? 1 : -1))
204
 
205
- const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
206
- let filtered = seq
207
- if (!isCrypto) filtered = await filterRowsToNyseTradingDays(seq)
208
-
209
- const cutoff = ASSET_CUTOFF[assetCode]
210
- if (cutoff) {
211
- const t0 = new Date(cutoff + 'T00:00:00Z')
212
- filtered = filtered.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
 
 
 
213
  }
214
- return filtered
 
215
  }
216
 
217
  async function computeEquities(sel) {
@@ -219,11 +255,26 @@ async function computeEquities(sel) {
219
  if (!seq.length) return null
220
 
221
  const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
 
 
 
 
 
 
 
 
 
222
 
223
  const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
224
  const bhY = computeBuyHoldEquity(seq, 100000) || []
225
  const lastIdx = Math.min(stratY.length, bhY.length) - 1
226
  if (lastIdx < 0) return null
 
 
 
 
 
 
227
 
228
  return { date: seq[lastIdx].date, stratLast: stratY[lastIdx], bhLast: bhY[lastIdx] }
229
  }
@@ -310,6 +361,35 @@ watch(
310
 
311
  /* toolbar */
312
  .toolbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 0 10px; background: #ffffff; color: #0f172a; border-bottom: 1px solid #E7ECF3; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  .mode__btn { height: 30px; min-width: 40px; padding: 0 10px; border-radius: 10px; border: 1px solid #D7DDE7; background: #ffffff; font-weight: 700; color: #0f172a; }
314
  .mode__btn.is-active { background: #0f172a; color: #ffffff; border-color: #0f172a; }
315
 
@@ -348,12 +428,37 @@ watch(
348
  .card-f1.bh { border-style: dashed; opacity: 1; }
349
 
350
  /* Rank / Crown */
351
- .rank { position: absolute; top: 10px; left: 12px; font-weight: 900; font-size: 18px; color: rgba(15,23,42,.30); letter-spacing: .04em; }
352
- .rank.bh-badge { font-size: 12px; background: rgba(15,23,42,.06); padding: 2px 6px; border-radius: 6px; color: #4b5563; }
353
- .crown { position: absolute; top: 12px; right: 12px; font-size: 18px; filter: drop-shadow(0 1px 1px rgba(0,0,0,.12)); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
 
355
  /* Head */
356
- .head { display: grid; grid-template-columns: 40px minmax(0,1fr); align-items: center; gap: 10px; }
 
 
 
 
 
357
  .logo { width: 40px; height: 40px; border-radius: 10px; background: #f3f4f6; display: grid; place-items: center; overflow: hidden; border: 1px solid #E7ECF3; }
358
  .logo img { width: 100%; height: 100%; object-fit: contain; }
359
  .logo__fallback { width: 60%; height: 60%; border-radius: 6px; background: #e5e7eb; }
 
6
  <AssetTabs v-model="asset" :ordered-assets="orderedAssets" />
7
  </div>
8
  <div class="toolbar__right">
9
+ <button
10
+ class="refresh-btn"
11
+ @click="refreshData"
12
+ :disabled="refreshing"
13
+ title="Refresh data from database"
14
+ >
15
+ <i class="pi pi-refresh" :class="{ 'spinning': refreshing }"></i>
16
+ </button>
17
  <div class="mode">
18
  <button class="mode__btn" :class="{ 'is-active': mode==='usd' }" @click="mode='usd'">$</button>
19
  <button class="mode__btn" :class="{ 'is-active': mode==='pct' }" @click="mode='pct'">%</button>
 
44
  :class="{ winner: c.isWinner, bh: c.kind==='bh', neg: (c.gapUsd ?? 0) < 0, pos: (c.gapUsd ?? 0) >= 0 }"
45
  :style="{ '--bar': (c.barPct ?? 0) + '%'}"
46
  >
47
+ <!-- Crown -->
 
 
48
  <span v-if="c.isWinner" class="crown" aria-label="Top performer">👑</span>
49
 
50
  <!-- Header: logo + names -->
 
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
 
 
155
  }
156
  })
157
 
158
+ /* ---------- refresh data ---------- */
159
+ async function refreshData() {
160
+ if (refreshing.value) return
161
+ refreshing.value = true
162
+ try {
163
+ console.log('[Live] Force refreshing data from Supabase...')
164
+ await dataService.forceRefresh()
165
+ rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
166
+ allDecisions = getAllDecisions() || []
167
+ console.log('[Live] Data refreshed successfully')
168
+ } catch (e) {
169
+ console.error('[Live] Error refreshing data:', e)
170
+ } finally {
171
+ refreshing.value = false
172
+ }
173
+ }
174
+
175
  /* ---------- helpers ---------- */
176
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
177
  const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
 
179
  const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
180
  const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
181
 
182
+ /* rows for selected asset (exclude vanilla/vinilla) - only show long_only strategy like leaderboard */
183
  const filteredRows = computed(() =>
184
  (rowsRef.value || []).filter(r => {
185
  if (r.asset !== asset.value) return false
186
+ if (r.strategy !== 'long_only') return false // 只显示 Aggressive Long Only 策略
187
  const name = (r?.agent_name || '').toLowerCase()
188
  return !EXCLUDED_AGENT_NAMES.has(name)
189
  })
 
197
  const cur = byAgent.get(k)
198
  if (!cur || score(r) > score(cur)) byAgent.set(k, r)
199
  }
200
+ const result = [...byAgent.values()]
201
+ console.log('[Live winners from leaderboard]', result.map(r => ({
202
+ agent: r.agent_name,
203
+ model: r.model,
204
+ strategy: r.strategy,
205
+ balance: r.balance
206
+ })))
207
+ return result
208
  })
209
 
210
  /* chart selections */
 
234
 
235
  seq.sort((a,b) => (a.date > b.date ? 1 : -1))
236
 
237
+ // 如果使用了 decision_ids,数据已经预过滤,不需要再次处理
238
+ // 只有在没有 decision_ids 时才需要过滤交易日
239
+ if (!ids.length) {
240
+ const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
241
+ if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
242
+
243
+ const cutoff = ASSET_CUTOFF[assetCode]
244
+ if (cutoff) {
245
+ const t0 = new Date(cutoff + 'T00:00:00Z')
246
+ seq = seq.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
247
+ }
248
  }
249
+
250
+ return seq
251
  }
252
 
253
  async function computeEquities(sel) {
 
255
  if (!seq.length) return null
256
 
257
  const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
258
+
259
+ console.log('[Live computeEquities]', {
260
+ agent: sel.agent_name,
261
+ model: sel.model,
262
+ strategy: sel.strategy,
263
+ config: cfg,
264
+ seqLength: seq.length,
265
+ decision_ids: sel.decision_ids?.length || 'none'
266
+ })
267
 
268
  const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
269
  const bhY = computeBuyHoldEquity(seq, 100000) || []
270
  const lastIdx = Math.min(stratY.length, bhY.length) - 1
271
  if (lastIdx < 0) return null
272
+
273
+ console.log('[Live computeEquities result]', {
274
+ agent: sel.agent_name,
275
+ stratLast: stratY[lastIdx],
276
+ bhLast: bhY[lastIdx]
277
+ })
278
 
279
  return { date: seq[lastIdx].date, stratLast: stratY[lastIdx], bhLast: bhY[lastIdx] }
280
  }
 
361
 
362
  /* toolbar */
363
  .toolbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 0 10px; background: #ffffff; color: #0f172a; border-bottom: 1px solid #E7ECF3; }
364
+ .toolbar__right { display: flex; align-items: center; gap: 12px; }
365
+ .refresh-btn {
366
+ height: 30px;
367
+ width: 30px;
368
+ border-radius: 10px;
369
+ border: 1px solid #D7DDE7;
370
+ background: #ffffff;
371
+ color: #0f172a;
372
+ cursor: pointer;
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ transition: all 0.2s ease;
377
+ }
378
+ .refresh-btn:hover:not(:disabled) {
379
+ background: #f8f9fa;
380
+ border-color: #0f172a;
381
+ }
382
+ .refresh-btn:disabled {
383
+ opacity: 0.5;
384
+ cursor: not-allowed;
385
+ }
386
+ .refresh-btn i.spinning {
387
+ animation: spin 1s linear infinite;
388
+ }
389
+ @keyframes spin {
390
+ from { transform: rotate(0deg); }
391
+ to { transform: rotate(360deg); }
392
+ }
393
  .mode__btn { height: 30px; min-width: 40px; padding: 0 10px; border-radius: 10px; border: 1px solid #D7DDE7; background: #ffffff; font-weight: 700; color: #0f172a; }
394
  .mode__btn.is-active { background: #0f172a; color: #ffffff; border-color: #0f172a; }
395
 
 
428
  .card-f1.bh { border-style: dashed; opacity: 1; }
429
 
430
  /* Rank / Crown */
431
+ .rank {
432
+ position: absolute;
433
+ top: 10px;
434
+ left: 12px;
435
+ font-weight: 900;
436
+ font-size: 18px;
437
+ color: rgba(15,23,42,.30);
438
+ letter-spacing: .04em;
439
+ }
440
+ .rank.bh-badge {
441
+ font-size: 12px;
442
+ background: rgba(15,23,42,.06);
443
+ padding: 2px 6px;
444
+ border-radius: 6px;
445
+ color: #4b5563;
446
+ }
447
+ .crown {
448
+ position: absolute;
449
+ top: 12px;
450
+ right: 12px;
451
+ font-size: 18px;
452
+ filter: drop-shadow(0 1px 1px rgba(0,0,0,.12));
453
+ }
454
 
455
  /* Head */
456
+ .head {
457
+ display: grid;
458
+ grid-template-columns: 40px minmax(0,1fr);
459
+ align-items: center;
460
+ gap: 10px;
461
+ }
462
  .logo { width: 40px; height: 40px; border-radius: 10px; background: #f3f4f6; display: grid; place-items: center; overflow: hidden; border: 1px solid #E7ECF3; }
463
  .logo img { width: 100%; height: 100%; object-fit: contain; }
464
  .logo__fallback { width: 60%; height: 60%; border-radius: 6px; background: #e5e7eb; }