Jimin Huang commited on
Commit
e9ff4a6
·
1 Parent(s): aed5d68

Change settings

Browse files
Files changed (1) hide show
  1. src/views/LiveView.vue +105 -111
src/views/LiveView.vue CHANGED
@@ -35,7 +35,7 @@
35
  class="card"
36
  :class="{ 'card--bh': c.kind==='bh', 'card--winner': c.isWinner }"
37
  >
38
- <!-- Header: logo | title (trunc) | balance (no-wrap) -->
39
  <div class="card__header">
40
  <div class="card__logo">
41
  <img v-if="c.logo" :src="c.logo" alt="" />
@@ -44,49 +44,49 @@
44
  </div>
45
 
46
  <div class="card__title-wrap">
47
- <div class="card__title ellipsize" :title="c.title">{{ c.title }}</div>
48
- <div class="card__subtitle ellipsize" :title="c.subtitle">{{ c.subtitle }}</div>
49
  </div>
50
 
51
  <div class="card__balance">{{ fmtUSD(c.balance) }}</div>
52
  </div>
53
 
54
- <!-- Meta row: strategy/badge on the left, performance pill on the right -->
55
  <div class="card__meta">
56
  <div class="meta__left">
57
  <div v-if="c.kind==='bh'" class="pill pill--neutral">Buy&nbsp;&amp;&nbsp;Hold</div>
58
  <div v-else class="pill pill--outline">Strategy</div>
59
  </div>
60
-
61
  <div class="meta__right" v-if="c.kind==='agent' && c.gapUsd != null">
62
- <div class="pill"
63
- :class="{ neg: c.gapUsd < 0 && !c.isWinner, pos: c.gapUsd >= 0 || c.isWinner }">
64
  <span v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</span>
65
  <span v-else>{{ signedPct(c.gapPct) }}</span>
66
  </div>
67
  </div>
68
- <div class="meta__right" v-else>
69
- <!-- spacer to keep height consistent -->
70
- </div>
71
  </div>
72
 
73
- <!-- Footer: EOD date -->
74
  <div class="card__footer">
75
  <div class="card__foot">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
76
  </div>
77
  </div>
78
  </div>
79
  </section>
 
 
 
 
80
  </div>
81
  </template>
82
 
83
  <script setup>
84
- import { ref, computed, onMounted, watchEffect } from 'vue'
85
  import AssetTabs from '../components/AssetTabs.vue'
86
  import CompareChartE from '../components/CompareChartE.vue'
87
  import { dataService } from '../lib/dataService'
88
 
89
- /* —— use the same helpers as the chart —— */
90
  import { getAllDecisions } from '../lib/dataCache'
91
  import { readAllRawDecisions } from '../lib/idb'
92
  import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
@@ -97,13 +97,6 @@ import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
97
  const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA'] // (MRNA removed)
98
  const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla']) // case-insensitive
99
 
100
- // optional logos
101
- const AGENT_LOGOS = {
102
- // 'DeepFundAgent': new URL('../assets/images/agents/deepfund.png', import.meta.url).href,
103
- // 'InvestorAgent': new URL('../assets/images/agents/investor.png', import.meta.url).href,
104
- // 'TradeAgent': new URL('../assets/images/agents/trade.png', import.meta.url).href,
105
- // 'HedgeFundAgent': new URL('../assets/images/agents/hedge.png', import.meta.url).href,
106
- }
107
  const ASSET_ICONS = {
108
  BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
109
  ETH: new URL('../assets/images/assets_images/ETH.png', import.meta.url).href,
@@ -111,16 +104,18 @@ const ASSET_ICONS = {
111
  BMRN: new URL('../assets/images/assets_images/BMRN.png', import.meta.url).href,
112
  TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
113
  }
 
 
 
114
 
115
- /* match the chart’s cutoff so numbers align */
116
- const ASSET_CUTOFF = { BTC: '2025-08-01' }
117
 
118
  /* ---------- state ---------- */
119
  const mode = ref('usd') // 'usd' | 'pct'
120
  const asset = ref('BTC')
121
- const rowsRef = ref([])
122
-
123
- let allDecisions = [] // in-memory decisions for perf calc
124
 
125
  /* ---------- bootstrap ---------- */
126
  onMounted(async () => {
@@ -134,7 +129,6 @@ onMounted(async () => {
134
  rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
135
  if (!orderedAssets.includes(asset.value)) asset.value = orderedAssets[0]
136
 
137
- // pull the same decisions cache the chart uses
138
  allDecisions = getAllDecisions() || []
139
  if (!allDecisions.length) {
140
  try {
@@ -145,14 +139,12 @@ onMounted(async () => {
145
  })
146
 
147
  /* ---------- helpers ---------- */
148
- function score(row) {
149
- return typeof row.balance === 'number' ? row.balance : -Infinity
150
- }
151
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
152
  const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
153
  const signedPct = (p) => `${(p >= 0 ? '+' : '−')}${Math.abs(p * 100).toFixed(2)}%`
 
154
 
155
- /* Selected-asset rows (exclude vanilla/vinilla) */
156
  const filteredRows = computed(() =>
157
  (rowsRef.value || []).filter(r => {
158
  if (r.asset !== asset.value) return false
@@ -161,7 +153,7 @@ const filteredRows = computed(() =>
161
  })
162
  )
163
 
164
- /* Best model per agent (by balance) — still from leaderboard rows for picking winners */
165
  const winners = computed(() => {
166
  const byAgent = new Map()
167
  for (const r of filteredRows.value) {
@@ -172,7 +164,7 @@ const winners = computed(() => {
172
  return [...byAgent.values()]
173
  })
174
 
175
- /* Chart selections built from winners */
176
  const winnersForChart = computed(() =>
177
  winners.value.map(w => ({
178
  agent_name: w.agent_name,
@@ -183,22 +175,29 @@ const winnersForChart = computed(() =>
183
  }))
184
  )
185
 
186
- /* ---------- PERF: compute B&H + strategy the same way as the chart ---------- */
 
 
 
 
187
 
188
- /** build the ordered decision seq for a selection (same logic as chart) */
189
- function buildSeq(sel) {
190
- const { agent_name: agent, asset, model } = sel
191
  const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
192
  let seq = ids.length
193
  ? allDecisions.filter(r => ids.includes(r.id))
194
- : allDecisions.filter(r => r.agent_name === agent && r.asset === asset && r.model === model)
195
 
196
- seq.sort((a, b) => (a.date > b.date ? 1 : -1))
197
 
198
- const isCrypto = asset === 'BTC' || asset === 'ETH'
199
- let filtered = isCrypto ? seq : filterRowsToNyseTradingDays(seq)
 
 
 
200
 
201
- const cutoff = ASSET_CUTOFF[asset]
202
  if (cutoff) {
203
  const t0 = new Date(cutoff + 'T00:00:00Z')
204
  filtered = filtered.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
@@ -206,86 +205,82 @@ function buildSeq(sel) {
206
  return filtered
207
  }
208
 
209
- /** compute final equity & aligned B&H for a selection */
210
- function computeEquities(sel) {
211
- const seq = buildSeq(sel)
212
  if (!seq.length) return null
213
 
214
- // strategy params (mirror CompareChartE)
215
  const cfg =
216
  (STRATEGIES || []).find(s => s.id === sel.strategy) ||
217
  { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
218
 
219
  const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
220
  const bhY = computeBuyHoldEquity(seq, 100000) || []
221
-
222
  const lastIdx = Math.min(stratY.length, bhY.length) - 1
223
  if (lastIdx < 0) return null
224
 
225
- return {
226
- date: seq[lastIdx].date,
227
- stratLast: stratY[lastIdx],
228
- bhLast: bhY[lastIdx],
229
- seq, stratY, bhY
230
- }
231
  }
232
 
233
- /* compute cards from perf (no leaderboard math) */
234
- const cards = ref([])
235
-
236
- watchEffect(() => {
237
- if (!winnersForChart.value.length) {
238
- cards.value = []
239
- return
240
- }
241
-
242
- // compute perf for each winner
243
- const perfs = winnersForChart.value
244
- .map(sel => ({ sel, perf: computeEquities(sel) }))
245
- .filter(x => x.perf)
246
-
247
- if (!perfs.length) {
248
- cards.value = []
249
- return
250
- }
251
-
252
- // Buy & Hold card: use the first winner’s BH last for the asset
253
- const first = perfs[0]
254
- const assetCode = first.sel.asset
255
- const bhCard = {
256
- key: `bh|${assetCode}`,
257
- kind: 'bh',
258
- title: 'Buy & Hold',
259
- subtitle: assetCode,
260
- balance: first.perf.bhLast,
261
- date: first.perf.date,
262
- logo: ASSET_ICONS[assetCode] || null,
263
- isWinner: false
264
- }
265
-
266
- // agent cards and winner flag
267
- const agentCards = perfs.map(({ sel, perf }) => {
268
- const gapUsd = perf.stratLast - perf.bhLast
269
- const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
270
- return {
271
- key: `agent|${sel.agent_name}|${sel.model}`,
272
- kind: 'agent',
273
- title: sel.agent_name,
274
- subtitle: sel.model,
275
- balance: perf.stratLast,
276
- date: perf.date,
277
- logo: AGENT_LOGOS[sel.agent_name] || null,
278
- gapUsd, gapPct,
279
- isWinner: false // set below
 
 
 
 
 
 
 
 
 
280
  }
281
- })
282
-
283
- const maxBal = Math.max(...agentCards.map(c => c.balance ?? -Infinity))
284
- agentCards.forEach(c => { c.isWinner = c.balance === maxBal })
285
-
286
- // top 4 agents + BH card
287
- cards.value = [bhCard, ...agentCards.sort((a,b) => b.balance - a.balance).slice(0,4)]
288
- })
289
  </script>
290
 
291
  <style scoped>
@@ -310,7 +305,6 @@ watchEffect(() => {
310
  /* empty */
311
  .empty { padding: 14px; border: 1px dashed #D6DAE1; border-radius: 12px; color: #6B7280; font-size: .92rem; }
312
 
313
- /* 5 cards in a row */
314
  /* grid of 5 cards */
315
  .cards5 { display: grid; gap: 12px; grid-template-columns: repeat(5, minmax(0, 1fr)); }
316
  @media (max-width: 1280px) { .cards5 { grid-template-columns: repeat(3, minmax(0,1fr)); } }
@@ -349,7 +343,7 @@ watchEffect(() => {
349
  .card__header {
350
  grid-template-areas:
351
  "logo title balance"
352
- "logo . balance"; /* balance gets its own row when tight */
353
  }
354
  }
355
 
@@ -364,8 +358,7 @@ watchEffect(() => {
364
  .card__title-wrap { grid-area: title; min-width: 0; display: grid; gap: 2px; }
365
  .card__title {
366
  font-weight: 800; color: #0F172A; line-height: 1.15;
367
- /* two-line clamp to avoid 'Bu…' / 'Invest…' */
368
- display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
369
  overflow: hidden;
370
  font-size: clamp(16px, 1.6vw, 19px);
371
  }
@@ -405,3 +398,4 @@ watchEffect(() => {
405
  .pill.neg { background: #FEE2E2; color: #B91C1C; }
406
  .pill--neutral { background: #EEF2F7; color: #0F172A; }
407
  .pill--outline { background: transparent; color: #475569; border: 1px solid #E5E7EB; padding: 3px 8px; }
 
 
35
  class="card"
36
  :class="{ 'card--bh': c.kind==='bh', 'card--winner': c.isWinner }"
37
  >
38
+ <!-- HEADER (responsive) -->
39
  <div class="card__header">
40
  <div class="card__logo">
41
  <img v-if="c.logo" :src="c.logo" alt="" />
 
44
  </div>
45
 
46
  <div class="card__title-wrap">
47
+ <div class="card__title" :title="c.title">{{ c.title }}</div>
48
+ <div class="card__subtitle" :title="c.subtitle">{{ c.subtitle }}</div>
49
  </div>
50
 
51
  <div class="card__balance">{{ fmtUSD(c.balance) }}</div>
52
  </div>
53
 
54
+ <!-- META -->
55
  <div class="card__meta">
56
  <div class="meta__left">
57
  <div v-if="c.kind==='bh'" class="pill pill--neutral">Buy&nbsp;&amp;&nbsp;Hold</div>
58
  <div v-else class="pill pill--outline">Strategy</div>
59
  </div>
 
60
  <div class="meta__right" v-if="c.kind==='agent' && c.gapUsd != null">
61
+ <div class="pill" :class="{ neg: c.gapUsd < 0 && !c.isWinner, pos: c.gapUsd >= 0 || c.isWinner }">
 
62
  <span v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</span>
63
  <span v-else>{{ signedPct(c.gapPct) }}</span>
64
  </div>
65
  </div>
66
+ <div class="meta__right" v-else></div>
 
 
67
  </div>
68
 
69
+ <!-- FOOTER -->
70
  <div class="card__footer">
71
  <div class="card__foot">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
72
  </div>
73
  </div>
74
  </div>
75
  </section>
76
+
77
+ <section v-else class="panel panel--cards">
78
+ <div class="empty">No card data yet for <strong>{{ asset }}</strong>.</div>
79
+ </section>
80
  </div>
81
  </template>
82
 
83
  <script setup>
84
+ import { ref, computed, onMounted, watch, shallowRef } from 'vue'
85
  import AssetTabs from '../components/AssetTabs.vue'
86
  import CompareChartE from '../components/CompareChartE.vue'
87
  import { dataService } from '../lib/dataService'
88
 
89
+ /* --- same helpers as chart --- */
90
  import { getAllDecisions } from '../lib/dataCache'
91
  import { readAllRawDecisions } from '../lib/idb'
92
  import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
 
97
  const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA'] // (MRNA removed)
98
  const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla']) // case-insensitive
99
 
 
 
 
 
 
 
 
100
  const ASSET_ICONS = {
101
  BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
102
  ETH: new URL('../assets/images/assets_images/ETH.png', import.meta.url).href,
 
104
  BMRN: new URL('../assets/images/assets_images/BMRN.png', import.meta.url).href,
105
  TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
106
  }
107
+ // optional agent logos (fill if you have them)
108
+ // const AGENT_LOGOS = { DeepFundAgent: new URL('..', import.meta.url).href, ... }
109
+ const AGENT_LOGOS = {}
110
 
111
+ const ASSET_CUTOFF = { BTC: '2025-08-01' } // match chart
 
112
 
113
  /* ---------- state ---------- */
114
  const mode = ref('usd') // 'usd' | 'pct'
115
  const asset = ref('BTC')
116
+ const rowsRef = ref([]) // leaderboard rows
117
+ let allDecisions = [] // raw decisions for perf (chart parity)
118
+ const cards = shallowRef([]) // rendered cards
119
 
120
  /* ---------- bootstrap ---------- */
121
  onMounted(async () => {
 
129
  rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
130
  if (!orderedAssets.includes(asset.value)) asset.value = orderedAssets[0]
131
 
 
132
  allDecisions = getAllDecisions() || []
133
  if (!allDecisions.length) {
134
  try {
 
139
  })
140
 
141
  /* ---------- helpers ---------- */
 
 
 
142
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
143
  const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
144
  const signedPct = (p) => `${(p >= 0 ? '+' : '−')}${Math.abs(p * 100).toFixed(2)}%`
145
+ const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
146
 
147
+ /* rows for selected asset (exclude vanilla/vinilla) */
148
  const filteredRows = computed(() =>
149
  (rowsRef.value || []).filter(r => {
150
  if (r.asset !== asset.value) return false
 
153
  })
154
  )
155
 
156
+ /* winners: best model per agent (by leaderboard balance) */
157
  const winners = computed(() => {
158
  const byAgent = new Map()
159
  for (const r of filteredRows.value) {
 
164
  return [...byAgent.values()]
165
  })
166
 
167
+ /* chart selections */
168
  const winnersForChart = computed(() =>
169
  winners.value.map(w => ({
170
  agent_name: w.agent_name,
 
175
  }))
176
  )
177
 
178
+ /* stable key to avoid identity churn */
179
+ const winnersKey = computed(() => {
180
+ const sels = winnersForChart.value || []
181
+ return sels.map(s => `${s.agent_name}|${s.asset}|${s.model}|${s.strategy}|${(s.decision_ids?.length||0)}`).join('||')
182
+ })
183
 
184
+ /* ---------- PERF (chart parity) ---------- */
185
+ async function buildSeq(sel) {
186
+ const { agent_name: agentName, asset: assetCode, model } = sel
187
  const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
188
  let seq = ids.length
189
  ? allDecisions.filter(r => ids.includes(r.id))
190
+ : allDecisions.filter(r => r.agent_name === agentName && r.asset === assetCode && r.model === model)
191
 
192
+ seq.sort((a,b) => (a.date > b.date ? 1 : -1))
193
 
194
+ const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
195
+ let filtered = seq
196
+ if (!isCrypto) {
197
+ filtered = await filterRowsToNyseTradingDays(seq)
198
+ }
199
 
200
+ const cutoff = ASSET_CUTOFF[assetCode]
201
  if (cutoff) {
202
  const t0 = new Date(cutoff + 'T00:00:00Z')
203
  filtered = filtered.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
 
205
  return filtered
206
  }
207
 
208
+ async function computeEquities(sel) {
209
+ const seq = await buildSeq(sel)
 
210
  if (!seq.length) return null
211
 
 
212
  const cfg =
213
  (STRATEGIES || []).find(s => s.id === sel.strategy) ||
214
  { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
215
 
216
  const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
217
  const bhY = computeBuyHoldEquity(seq, 100000) || []
 
218
  const lastIdx = Math.min(stratY.length, bhY.length) - 1
219
  if (lastIdx < 0) return null
220
 
221
+ return { date: seq[lastIdx].date, stratLast: stratY[lastIdx], bhLast: bhY[lastIdx] }
 
 
 
 
 
222
  }
223
 
224
+ /* build cards whenever winners/asset change */
225
+ let computing = false
226
+ watch(
227
+ () => [asset.value, winnersKey.value],
228
+ async () => {
229
+ if (computing) return
230
+ if (!winnersForChart.value.length) { cards.value = []; return }
231
+ computing = true
232
+ try {
233
+ const perfs = (await Promise.all(
234
+ winnersForChart.value.map(async sel => ({ sel, perf: await computeEquities(sel) }))
235
+ )).filter(x => x.perf)
236
+
237
+ if (!perfs.length) { cards.value = []; return }
238
+
239
+ // Buy & Hold card from the asset’s aligned BH series
240
+ const first = perfs[0]
241
+ const assetCode = first.sel.asset
242
+ const bhCard = {
243
+ key: `bh|${assetCode}`,
244
+ kind: 'bh',
245
+ title: 'Buy & Hold',
246
+ subtitle: assetCode,
247
+ balance: first.perf.bhLast,
248
+ date: first.perf.date,
249
+ logo: ASSET_ICONS[assetCode] || null,
250
+ isWinner: false
251
+ }
252
+
253
+ // Agent cards (gap vs BH)
254
+ const agentCards = perfs.map(({ sel, perf }) => {
255
+ const gapUsd = perf.stratLast - perf.bhLast
256
+ const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
257
+ return {
258
+ key: `agent|${sel.agent_name}|${sel.model}`,
259
+ kind: 'agent',
260
+ title: sel.agent_name,
261
+ subtitle: sel.model,
262
+ balance: perf.stratLast,
263
+ date: perf.date,
264
+ logo: AGENT_LOGOS[sel.agent_name] || null,
265
+ gapUsd, gapPct,
266
+ isWinner: false
267
+ }
268
+ })
269
+
270
+ const maxBal = Math.max(...agentCards.map(c => c.balance ?? -Infinity))
271
+ agentCards.forEach(c => { c.isWinner = c.balance === maxBal })
272
+
273
+ // top 4 agents + BH card
274
+ cards.value = [bhCard, ...agentCards.sort((a,b) => b.balance - a.balance).slice(0,4)]
275
+ } catch (e) {
276
+ console.error('LiveView: compute cards failed', e)
277
+ cards.value = []
278
+ } finally {
279
+ computing = false
280
  }
281
+ },
282
+ { immediate: true }
283
+ )
 
 
 
 
 
284
  </script>
285
 
286
  <style scoped>
 
305
  /* empty */
306
  .empty { padding: 14px; border: 1px dashed #D6DAE1; border-radius: 12px; color: #6B7280; font-size: .92rem; }
307
 
 
308
  /* grid of 5 cards */
309
  .cards5 { display: grid; gap: 12px; grid-template-columns: repeat(5, minmax(0, 1fr)); }
310
  @media (max-width: 1280px) { .cards5 { grid-template-columns: repeat(3, minmax(0,1fr)); } }
 
343
  .card__header {
344
  grid-template-areas:
345
  "logo title balance"
346
+ "logo . balance"; /* lets balance move down, title gets more space */
347
  }
348
  }
349
 
 
358
  .card__title-wrap { grid-area: title; min-width: 0; display: grid; gap: 2px; }
359
  .card__title {
360
  font-weight: 800; color: #0F172A; line-height: 1.15;
361
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; /* 2-line clamp */
 
362
  overflow: hidden;
363
  font-size: clamp(16px, 1.6vw, 19px);
364
  }
 
398
  .pill.neg { background: #FEE2E2; color: #B91C1C; }
399
  .pill--neutral { background: #EEF2F7; color: #0F172A; }
400
  .pill--outline { background: transparent; color: #475569; border: 1px solid #E5E7EB; padding: 3px 8px; }
401
+ </style>