Jimin Huang commited on
Commit
c4dda5f
Β·
1 Parent(s): 049df43

Change settings

Browse files
Files changed (1) hide show
  1. src/views/LiveView.vue +87 -60
src/views/LiveView.vue CHANGED
@@ -43,46 +43,55 @@
43
  v-for="c in cards"
44
  :key="c.key"
45
  class="card"
46
- :class="{ 'card--bh': c.kind==='bh' }"
 
 
 
47
  >
48
- <div class="card__logo">
49
- <img v-if="c.logo" :src="c.logo" alt="" />
50
- <div v-else class="card__logo-fallback"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  </div>
52
 
53
- <div class="card__content">
54
- <div class="card__row">
 
 
 
55
  <div
56
- class="card__title"
57
- :style="{ fontSize: nameFontSize(c.title) }"
58
- :title="c.title"
 
 
59
  >
60
- {{ c.title }}
 
61
  </div>
62
- <div class="card__balance">{{ fmtUSD(c.balance) }}</div>
63
- </div>
64
-
65
- <div class="card__row card__meta">
66
- <div class="card__sub" :title="c.subtitle">{{ c.subtitle }}</div>
67
 
68
- <template v-if="c.kind==='agent'">
69
- <div class="pill" :class="{ neg: c.gapUsd < 0 }">
70
- <span v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</span>
71
- <span v-else>{{ signedPct(c.gapPct) }}</span>
72
- </div>
73
- </template>
74
-
75
- <template v-else>
76
- <div class="pill">Buy&nbsp;&amp;&nbsp;Hold</div>
77
- </template>
78
- </div>
79
 
80
- <div class="card__row card__date">
81
- <div class="card__sub">
82
- EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '-' }}
83
- </div>
84
- <div class="spacer"></div>
85
- </div>
86
  </div>
87
  </div>
88
  </div>
@@ -154,8 +163,6 @@ const filteredRows = computed(() =>
154
  /* Baseline (Buy & Hold) directly from tableRows */
155
  const baselineRow = computed(() => {
156
  const rows = (rowsRef.value || []).filter(r => r.asset === asset.value)
157
-
158
- // heuristics: prefer explicit strategy markers, else name/model contains buy+hold
159
  const candidates = rows.filter(r => {
160
  const s = (r.strategy || '').toLowerCase()
161
  const an = (r.agent_name || '').toLowerCase()
@@ -164,12 +171,11 @@ const baselineRow = computed(() => {
164
  (an.includes('buy') && an.includes('hold')) ||
165
  (m.includes('buy') && m.includes('hold'))
166
  })
167
-
168
  if (!candidates.length) return null
169
  const ts = (r) => new Date(r.end_date || r.last_nav_ts || 0).getTime()
170
  return candidates.sort((a, b) => ts(b) - ts(a))[0]
171
  })
172
- const bhBalance = computed(() => baselineRow.value?.balance ?? null)
173
  const bhDate = computed(() => baselineRow.value?.end_date || baselineRow.value?.last_nav_ts || '')
174
 
175
  /* Best model per agent (by balance) */
@@ -194,7 +200,7 @@ const winnersForChart = computed(() =>
194
  }))
195
  )
196
 
197
- /* Build 5 cards: Buy&Hold + top 4 agents */
198
  const cards = computed(() => {
199
  const base = Number(bhBalance.value ?? 100000)
200
  const baseDate = bhDate.value
@@ -207,9 +213,12 @@ const cards = computed(() => {
207
  balance: base,
208
  date: baseDate,
209
  logo: ASSET_ICONS[asset.value] || null,
 
210
  }
211
 
212
  const top = [...winners.value].sort((a,b) => score(b) - score(a)).slice(0, 4)
 
 
213
  const agentCards = top.map(r => {
214
  const gapUsd = (typeof r.balance === 'number' && Number.isFinite(base))
215
  ? (r.balance - base)
@@ -226,6 +235,7 @@ const cards = computed(() => {
226
  date: r.end_date || r.last_nav_ts || '',
227
  logo: AGENT_LOGOS[r.agent_name] || null,
228
  gapUsd, gapPct,
 
229
  }
230
  })
231
 
@@ -303,56 +313,73 @@ function nameFontSize(name='') {
303
  /* card */
304
  .card {
305
  display: grid;
306
- grid-template-columns: 52px 1fr;
307
- gap: 10px 12px;
308
- align-items: center;
309
  padding: 12px 14px;
310
  border: 1px solid #EEF1F6; border-radius: 14px;
311
  background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.04);
 
312
  }
313
  .card--bh { outline: 2px dashed rgba(15,23,42,.08); }
 
 
 
 
 
 
 
 
 
 
 
 
314
 
315
  /* logo */
316
  .card__logo {
317
  width: 44px; height: 44px; border-radius: 999px;
318
  background: #F3F4F6; display: grid; place-items: center;
319
- overflow: hidden;
320
  }
321
  .card__logo img { width: 100%; height: 100%; object-fit: contain; }
322
  .card__logo-fallback { width: 60%; height: 60%; border-radius: 999px; background: #E5E7EB; }
323
 
324
- /* content */
325
- .card__content { min-width: 0; }
326
- .card__row {
327
- display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
 
 
328
  }
329
 
330
- /* title shrinks and never pushes the number off the card */
331
  .card__title {
332
- flex: 1 1 auto; /* take available space */
333
- min-width: 0; /* allow shrink */
334
  font-weight: 800; color: #0F172A;
335
- white-space: nowrap;
336
  }
337
-
338
  .card__balance {
339
- flex: 0 0 auto; /* never shrink */
340
  white-space: nowrap;
341
  font-weight: 900; color: #0F172A; font-size: 20px;
342
  }
343
 
344
- .card__sub { font-size: 12px; color: #5B6476; opacity: .85; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
345
- .card__meta { margin-top: 4px; align-items: center; }
346
- .card__date { margin-top: 2px; }
 
 
 
 
 
 
347
 
 
348
  .pill {
349
- padding: 3px 8px; border-radius: 999px; font-size: 12px;
350
- font-weight: 700; line-height: 1; white-space: nowrap;
351
  background: #EEF2F7; color: #0F172A;
352
  }
353
  .pill.neg { background: #FEE2E2; color: #B91C1C; }
354
-
355
- /* utility */
356
- .spacer { flex: 1 1 auto; }
357
- strong { font-weight: 700; }
358
  </style>
 
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">
53
+ <div class="card__logo">
54
+ <img v-if="c.logo" :src="c.logo" alt="" />
55
+ <div v-else class="card__logo-fallback"></div>
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>
69
 
70
+ <!-- meta row -->
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>
85
+ </template>
 
 
 
 
86
 
87
+ <template v-else>
88
+ <div class="pill pill--neutral">Buy&nbsp;&amp;&nbsp;Hold</div>
89
+ </template>
90
+ </div>
 
 
 
 
 
 
 
91
 
92
+ <!-- date row -->
93
+ <div class="card__footer">
94
+ <div class="card__sub">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
 
 
 
95
  </div>
96
  </div>
97
  </div>
 
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()
 
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) */
 
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
 
213
  balance: base,
214
  date: baseDate,
215
  logo: ASSET_ICONS[asset.value] || null,
216
+ isWinner: false
217
  }
218
 
219
  const top = [...winners.value].sort((a,b) => score(b) - score(a)).slice(0, 4)
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)
 
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
239
  }
240
  })
241
 
 
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>