Jimin Huang commited on
Commit
732f229
Β·
1 Parent(s): c4dda5f

Change settings

Browse files
Files changed (1) hide show
  1. src/views/LiveView.vue +78 -150
src/views/LiveView.vue CHANGED
@@ -7,18 +7,8 @@
7
  </div>
8
  <div class="toolbar__right">
9
  <div class="mode">
10
- <button
11
- class="mode__btn"
12
- :class="{ 'is-active': mode==='usd' }"
13
- @click="mode='usd'">
14
- $
15
- </button>
16
- <button
17
- class="mode__btn"
18
- :class="{ 'is-active': mode==='pct' }"
19
- @click="mode='pct'">
20
- %
21
- </button>
22
  </div>
23
  </div>
24
  </header>
@@ -43,10 +33,7 @@
43
  v-for="c in cards"
44
  :key="c.key"
45
  class="card"
46
- :class="{
47
- 'card--bh': c.kind==='bh',
48
- 'card--winner': c.isWinner
49
- }"
50
  >
51
  <!-- header: logo | title | balance -->
52
  <div class="card__header">
@@ -56,13 +43,7 @@
56
  <span v-if="c.isWinner" class="card__badge" aria-label="Top performer">πŸ‘‘</span>
57
  </div>
58
 
59
- <div
60
- class="card__title"
61
- :style="{ fontSize: nameFontSize(c.title) }"
62
- :title="c.title"
63
- >
64
- {{ c.title }}
65
- </div>
66
 
67
  <div class="card__balance">{{ fmtUSD(c.balance) }}</div>
68
  </div>
@@ -71,14 +52,8 @@
71
  <div class="card__meta">
72
  <div class="card__sub ellipsize" :title="c.subtitle">{{ c.subtitle }}</div>
73
 
74
- <template v-if="c.kind==='agent'">
75
- <div
76
- class="pill"
77
- :class="{
78
- neg: c.gapUsd < 0 && !c.isWinner,
79
- pos: c.gapUsd >= 0 || c.isWinner
80
- }"
81
- >
82
  <span v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</span>
83
  <span v-else>{{ signedPct(c.gapPct) }}</span>
84
  </div>
@@ -109,7 +84,7 @@ import { dataService } from '../lib/dataService'
109
  const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA'] // MRNA removed
110
  const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla']) // case-insensitive
111
 
112
- // Plug in real logos if you have them
113
  const AGENT_LOGOS = {
114
  // 'DeepFundAgent': new URL('../assets/images/agents/deepfund.png', import.meta.url).href,
115
  // 'InvestorAgent': new URL('../assets/images/agents/investor.png', import.meta.url).href,
@@ -146,12 +121,11 @@ onMounted(async () => {
146
  function score(row) {
147
  return typeof row.balance === 'number' ? row.balance : -Infinity
148
  }
149
- const fmtUSD = (n) =>
150
- (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
151
  const signedMoney = (n) => `${n >= 0 ? '+' : 'βˆ’'}${fmtUSD(Math.abs(n))}`
152
  const signedPct = (p) => `${(p >= 0 ? '+' : 'βˆ’')}${Math.abs(p * 100).toFixed(2)}%`
153
 
154
- /* Filter rows: selected asset, exclude β€œvanilla/vinilla” */
155
  const filteredRows = computed(() =>
156
  (rowsRef.value || []).filter(r => {
157
  if (r.asset !== asset.value) return false
@@ -160,23 +134,42 @@ const filteredRows = computed(() =>
160
  })
161
  )
162
 
163
- /* Baseline (Buy & Hold) directly from tableRows */
164
- const baselineRow = computed(() => {
165
- const rows = (rowsRef.value || []).filter(r => r.asset === asset.value)
166
- const candidates = rows.filter(r => {
 
167
  const s = (r.strategy || '').toLowerCase()
168
  const an = (r.agent_name || '').toLowerCase()
169
  const m = (r.model || '').toLowerCase()
170
- return s === 'buy_hold' || s === 'buy&hold' ||
171
- (an.includes('buy') && an.includes('hold')) ||
172
- (m.includes('buy') && m.includes('hold'))
173
- })
174
- if (!candidates.length) return null
175
- const ts = (r) => new Date(r.end_date || r.last_nav_ts || 0).getTime()
176
- return candidates.sort((a, b) => ts(b) - ts(a))[0]
 
 
 
177
  })
178
- const bhBalance = computed(() => baselineRow.value?.balance ?? 100000)
179
- const bhDate = computed(() => baselineRow.value?.end_date || baselineRow.value?.last_nav_ts || '')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
  /* Best model per agent (by balance) */
182
  const winners = computed(() => {
@@ -200,18 +193,18 @@ const winnersForChart = computed(() =>
200
  }))
201
  )
202
 
203
- /* Build 5 cards: Buy&Hold + top 4 agents and mark the winner */
204
  const cards = computed(() => {
205
- const base = Number(bhBalance.value ?? 100000)
206
- const baseDate = bhDate.value
207
 
208
  const bhCard = {
209
  key: `bh|${asset.value}`,
210
  kind: 'bh',
211
  title: 'Buy & Hold',
212
  subtitle: asset.value,
213
- balance: base,
214
- date: baseDate,
215
  logo: ASSET_ICONS[asset.value] || null,
216
  isWinner: false
217
  }
@@ -220,19 +213,17 @@ const cards = computed(() => {
220
  const maxBal = top.length ? Math.max(...top.map(r => r.balance ?? -Infinity)) : -Infinity
221
 
222
  const agentCards = top.map(r => {
223
- const gapUsd = (typeof r.balance === 'number' && Number.isFinite(base))
224
- ? (r.balance - base)
225
- : 0
226
- const gapPct = (Number.isFinite(base) && base > 0)
227
- ? (r.balance / base - 1)
228
- : 0
229
  return {
230
  key: `agent|${r.agent_name}|${r.model}`,
231
  kind: 'agent',
232
  title: r.agent_name,
233
  subtitle: r.model,
234
  balance: r.balance,
235
- date: r.end_date || r.last_nav_ts || '',
236
  logo: AGENT_LOGOS[r.agent_name] || null,
237
  gapUsd, gapPct,
238
  isWinner: r.balance === maxBal
@@ -241,36 +232,16 @@ const cards = computed(() => {
241
 
242
  return [bhCard, ...agentCards]
243
  })
244
-
245
- /* Dynamic font size for long names (no clipping). */
246
- function nameFontSize(name='') {
247
- const len = name.length
248
- if (len <= 12) return '20px'
249
- if (len <= 16) return '18px'
250
- if (len <= 22) return '16px'
251
- if (len <= 28) return '15px'
252
- return '14px'
253
- }
254
  </script>
255
 
256
  <style scoped>
257
- .live {
258
- max-width: 1280px;
259
- margin: 0 auto;
260
- padding: 12px 20px 28px;
261
- }
262
 
263
  /* toolbar */
264
  .toolbar {
265
- position: sticky;
266
- top: 0;
267
- z-index: 10;
268
- display: flex;
269
- align-items: center;
270
- justify-content: space-between;
271
- gap: 16px;
272
- padding: 8px 0 10px;
273
- background: #fff;
274
  }
275
  .toolbar__left { min-width: 0; }
276
  .toolbar__right { display: flex; align-items: center; gap: 10px; }
@@ -292,94 +263,51 @@ function nameFontSize(name='') {
292
  .panel--cards { padding: 12px; }
293
 
294
  /* empty */
295
- .empty {
296
- padding: 14px; border: 1px dashed #D6DAE1; border-radius: 12px;
297
- color: #6B7280; font-size: .92rem;
298
- }
299
 
300
  /* 5 cards in a row */
301
- .cards5 {
302
- display: grid;
303
- gap: 12px;
304
- grid-template-columns: repeat(5, minmax(0, 1fr));
305
- }
306
- @media (max-width: 1200px) {
307
- .cards5 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
308
- }
309
- @media (max-width: 720px) {
310
- .cards5 { grid-template-columns: 1fr; }
311
- }
312
 
313
  /* card */
314
  .card {
315
- display: grid;
316
- grid-template-rows: auto auto auto;
317
- gap: 8px;
318
- padding: 12px 14px;
319
- border: 1px solid #EEF1F6; border-radius: 14px;
320
- background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.04);
321
- position: relative;
322
  }
323
  .card--bh { outline: 2px dashed rgba(15,23,42,.08); }
324
- .card--winner {
325
- border-color: #16a34a; /* green-600 */
326
- box-shadow: 0 0 0 3px rgba(22,163,74,.12);
327
- }
328
 
329
  /* header layout: logo | title | balance */
330
- .card__header {
331
- display: grid;
332
- grid-template-columns: 52px minmax(0,1fr) auto;
333
- align-items: center;
334
- gap: 12px;
335
- }
336
 
337
  /* logo */
338
- .card__logo {
339
- width: 44px; height: 44px; border-radius: 999px;
340
- background: #F3F4F6; display: grid; place-items: center;
341
- overflow: hidden; position: relative;
342
- }
343
  .card__logo img { width: 100%; height: 100%; object-fit: contain; }
344
  .card__logo-fallback { width: 60%; height: 60%; border-radius: 999px; background: #E5E7EB; }
 
345
 
346
- /* crown badge for winner */
347
- .card__badge {
348
- position: absolute;
349
- right: -6px; top: -6px;
350
- font-size: 16px;
351
- filter: drop-shadow(0 1px 1px rgba(0,0,0,.15));
352
- }
353
-
354
- /* title shrinks; never pushes number off */
355
  .card__title {
356
- min-width: 0;
357
- font-weight: 800; color: #0F172A;
358
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
359
- }
360
- /* right number never shrinks */
361
- .card__balance {
362
- white-space: nowrap;
363
- font-weight: 900; color: #0F172A; font-size: 20px;
364
  }
365
 
366
- /* meta row */
367
- .card__meta {
368
- display: flex; align-items: center; justify-content: space-between; gap: 10px;
369
- }
 
370
  .card__sub { font-size: 12px; color: #5B6476; opacity: .85; }
371
  .ellipsize { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
372
-
373
- /* footer row */
374
  .card__footer { margin-top: -2px; }
375
 
376
  /* pills */
377
- .pill {
378
- padding: 4px 9px; border-radius: 999px; font-size: 12px;
379
- font-weight: 800; line-height: 1; white-space: nowrap;
380
- background: #EEF2F7; color: #0F172A;
381
- }
382
  .pill.neg { background: #FEE2E2; color: #B91C1C; }
383
- .pill.pos { background: #DCFCE7; color: #166534; } /* green for winner/positive */
384
  .pill.pill--neutral { background: #EEF2F7; color: #0F172A; }
385
  </style>
 
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>
 
 
 
 
 
 
 
 
 
 
12
  </div>
13
  </div>
14
  </header>
 
33
  v-for="c in cards"
34
  :key="c.key"
35
  class="card"
36
+ :class="{ 'card--bh': c.kind==='bh', 'card--winner': c.isWinner }"
 
 
 
37
  >
38
  <!-- header: logo | title | balance -->
39
  <div class="card__header">
 
43
  <span v-if="c.isWinner" class="card__badge" aria-label="Top performer">πŸ‘‘</span>
44
  </div>
45
 
46
+ <div class="card__title" :title="c.title">{{ c.title }}</div>
 
 
 
 
 
 
47
 
48
  <div class="card__balance">{{ fmtUSD(c.balance) }}</div>
49
  </div>
 
52
  <div class="card__meta">
53
  <div class="card__sub ellipsize" :title="c.subtitle">{{ c.subtitle }}</div>
54
 
55
+ <template v-if="c.kind==='agent' && c.gapUsd != null">
56
+ <div class="pill" :class="{ neg: c.gapUsd < 0 && !c.isWinner, pos: c.gapUsd >= 0 || c.isWinner }">
 
 
 
 
 
 
57
  <span v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</span>
58
  <span v-else>{{ signedPct(c.gapPct) }}</span>
59
  </div>
 
84
  const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA'] // MRNA removed
85
  const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla']) // case-insensitive
86
 
87
+ // Optional: plug real logos if you have them
88
  const AGENT_LOGOS = {
89
  // 'DeepFundAgent': new URL('../assets/images/agents/deepfund.png', import.meta.url).href,
90
  // 'InvestorAgent': new URL('../assets/images/agents/investor.png', import.meta.url).href,
 
121
  function score(row) {
122
  return typeof row.balance === 'number' ? row.balance : -Infinity
123
  }
124
+ const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
 
125
  const signedMoney = (n) => `${n >= 0 ? '+' : 'βˆ’'}${fmtUSD(Math.abs(n))}`
126
  const signedPct = (p) => `${(p >= 0 ? '+' : 'βˆ’')}${Math.abs(p * 100).toFixed(2)}%`
127
 
128
+ /* Selected-asset rows (exclude vanilla/vinilla) */
129
  const filteredRows = computed(() =>
130
  (rowsRef.value || []).filter(r => {
131
  if (r.asset !== asset.value) return false
 
134
  })
135
  )
136
 
137
+ /* --- Build an index of Buy&Hold rows: asset -> [{ts, balance, row}] asc --- */
138
+ const bhIndex = computed(() => {
139
+ const idx = new Map()
140
+ for (const r of (rowsRef.value || [])) {
141
+ if (!r?.asset) continue
142
  const s = (r.strategy || '').toLowerCase()
143
  const an = (r.agent_name || '').toLowerCase()
144
  const m = (r.model || '').toLowerCase()
145
+ const isBH = s === 'buy_hold' || s === 'buy&hold' || (an.includes('buy') && an.includes('hold')) || (m.includes('buy') && m.includes('hold'))
146
+ if (!isBH) continue
147
+ const ts = new Date(r.end_date || r.last_nav_ts || 0).getTime()
148
+ if (!Number.isFinite(ts)) continue
149
+ const list = idx.get(r.asset) || []
150
+ list.push({ ts, balance: r.balance, row: r })
151
+ idx.set(r.asset, list)
152
+ }
153
+ for (const [a, list] of idx) list.sort((x, y) => x.ts - y.ts)
154
+ return idx
155
  })
156
+
157
+ /* Latest B&H for the selected asset (for the B&H card) */
158
+ const bhLatest = computed(() => (bhIndex.value.get(asset.value) || []).at(-1) || null)
159
+
160
+ /* For an agent date, get the matching (latest ≀ date) B&H balance */
161
+ function getBHBalanceAt(assetCode, iso) {
162
+ const list = bhIndex.value.get(assetCode)
163
+ if (!list || !list.length) return null
164
+ const t = new Date(iso || 0).getTime()
165
+ if (!Number.isFinite(t)) return list[list.length - 1].balance
166
+ let lo = 0, hi = list.length - 1, ans = -1
167
+ while (lo <= hi) {
168
+ const mid = (lo + hi) >> 1
169
+ if (list[mid].ts <= t) { ans = mid; lo = mid + 1 } else { hi = mid - 1 }
170
+ }
171
+ return ans >= 0 ? list[ans].balance : list[0].balance
172
+ }
173
 
174
  /* Best model per agent (by balance) */
175
  const winners = computed(() => {
 
193
  }))
194
  )
195
 
196
+ /* Cards: B&H + top 4 agents, gaps aligned to same (or latest ≀) EOD */
197
  const cards = computed(() => {
198
+ const bhBal = bhLatest.value?.balance ?? 100000
199
+ const bhDate = bhLatest.value?.row?.end_date || bhLatest.value?.row?.last_nav_ts || ''
200
 
201
  const bhCard = {
202
  key: `bh|${asset.value}`,
203
  kind: 'bh',
204
  title: 'Buy & Hold',
205
  subtitle: asset.value,
206
+ balance: bhBal,
207
+ date: bhDate,
208
  logo: ASSET_ICONS[asset.value] || null,
209
  isWinner: false
210
  }
 
213
  const maxBal = top.length ? Math.max(...top.map(r => r.balance ?? -Infinity)) : -Infinity
214
 
215
  const agentCards = top.map(r => {
216
+ const date = r.end_date || r.last_nav_ts || ''
217
+ const base = getBHBalanceAt(r.asset, date) // aligned baseline
218
+ const gapUsd = (typeof r.balance === 'number' && Number.isFinite(base)) ? (r.balance - base) : null
219
+ const gapPct = (gapUsd != null && Number.isFinite(base) && base > 0) ? (r.balance / base - 1) : null
 
 
220
  return {
221
  key: `agent|${r.agent_name}|${r.model}`,
222
  kind: 'agent',
223
  title: r.agent_name,
224
  subtitle: r.model,
225
  balance: r.balance,
226
+ date,
227
  logo: AGENT_LOGOS[r.agent_name] || null,
228
  gapUsd, gapPct,
229
  isWinner: r.balance === maxBal
 
232
 
233
  return [bhCard, ...agentCards]
234
  })
 
 
 
 
 
 
 
 
 
 
235
  </script>
236
 
237
  <style scoped>
238
+ .live { max-width: 1280px; margin: 0 auto; padding: 12px 20px 28px; }
 
 
 
 
239
 
240
  /* toolbar */
241
  .toolbar {
242
+ position: sticky; top: 0; z-index: 10;
243
+ display: flex; align-items: center; justify-content: space-between;
244
+ gap: 16px; padding: 8px 0 10px; background: #fff;
 
 
 
 
 
 
245
  }
246
  .toolbar__left { min-width: 0; }
247
  .toolbar__right { display: flex; align-items: center; gap: 10px; }
 
263
  .panel--cards { padding: 12px; }
264
 
265
  /* empty */
266
+ .empty { padding: 14px; border: 1px dashed #D6DAE1; border-radius: 12px; color: #6B7280; font-size: .92rem; }
 
 
 
267
 
268
  /* 5 cards in a row */
269
+ .cards5 { display: grid; gap: 12px; grid-template-columns: repeat(5, minmax(0, 1fr)); }
270
+ @media (max-width: 1200px) { .cards5 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
271
+ @media (max-width: 720px) { .cards5 { grid-template-columns: 1fr; } }
 
 
 
 
 
 
 
 
272
 
273
  /* card */
274
  .card {
275
+ display: grid; grid-template-rows: auto auto auto; gap: 8px;
276
+ padding: 12px 14px; border: 1px solid #EEF1F6; border-radius: 14px;
277
+ background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.04); position: relative;
 
 
 
 
278
  }
279
  .card--bh { outline: 2px dashed rgba(15,23,42,.08); }
280
+ .card--winner { border-color: #16a34a; box-shadow: 0 0 0 3px rgba(22,163,74,.12); }
 
 
 
281
 
282
  /* header layout: logo | title | balance */
283
+ .card__header { display: grid; grid-template-columns: 52px minmax(0,1fr) auto; align-items: start; gap: 12px; }
 
 
 
 
 
284
 
285
  /* logo */
286
+ .card__logo { width: 44px; height: 44px; border-radius: 999px; background: #F3F4F6; display: grid; place-items: center; overflow: hidden; position: relative; }
 
 
 
 
287
  .card__logo img { width: 100%; height: 100%; object-fit: contain; }
288
  .card__logo-fallback { width: 60%; height: 60%; border-radius: 999px; background: #E5E7EB; }
289
+ .card__badge { position: absolute; right: -6px; top: -6px; font-size: 16px; filter: drop-shadow(0 1px 1px rgba(0,0,0,.15)); }
290
 
291
+ /* title: clamp to 2 lines so balance never overlaps */
 
 
 
 
 
 
 
 
292
  .card__title {
293
+ min-width: 0; font-weight: 800; color: #0F172A;
294
+ white-space: normal;
295
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
296
+ overflow: hidden; line-height: 1.15;
 
 
 
 
297
  }
298
 
299
+ /* right-side balance never shrinks */
300
+ .card__balance { white-space: nowrap; font-weight: 900; color: #0F172A; font-size: 20px; }
301
+
302
+ /* meta row + footer */
303
+ .card__meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
304
  .card__sub { font-size: 12px; color: #5B6476; opacity: .85; }
305
  .ellipsize { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
 
 
306
  .card__footer { margin-top: -2px; }
307
 
308
  /* pills */
309
+ .pill { padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; }
 
 
 
 
310
  .pill.neg { background: #FEE2E2; color: #B91C1C; }
311
+ .pill.pos { background: #DCFCE7; color: #166534; }
312
  .pill.pill--neutral { background: #EEF2F7; color: #0F172A; }
313
  </style>