Jimin Huang commited on
Commit
77677b6
Β·
1 Parent(s): 5d66707

Change settings

Browse files
Files changed (1) hide show
  1. src/views/LiveView.vue +39 -143
src/views/LiveView.vue CHANGED
@@ -26,43 +26,38 @@
26
  </div>
27
  </section>
28
 
29
- <!-- Cards: Buy & Hold + top 4 agents (computed with perf helpers) -->
30
  <section class="panel panel--cards" v-if="cards.length">
31
  <div class="cards-grid">
32
- <div
33
  v-for="c in cards"
34
  :key="c.key"
35
- class="card2"
36
- :class="{ 'is-bh': c.kind==='bh', 'is-winner': c.isWinner }"
37
  >
38
- <!-- Crown -->
39
- <div v-if="c.isWinner" class="card2__ribbon" aria-label="Top performer">πŸ‘‘</div>
40
 
41
- <!-- Header: LOGO + title -->
42
- <div class="card2__header">
43
- <div class="avatar">
44
  <img v-if="c.logo" :src="c.logo" alt="" />
45
- <div v-else class="avatar__fallback" aria-hidden="true"></div>
46
  </div>
47
- <div class="titleblock">
48
- <div class="title" :title="c.kind==='bh' ? 'Buy & Hold' : c.title">{{ c.kind==='bh' ? 'Buy & Hold' : c.title }}</div>
49
- <div class="subtitle" :title="c.subtitle">{{ c.subtitle }}</div>
50
  </div>
51
- </div>
52
 
53
- <!-- KPIs: Net value, Profit, Delta Profit -->
54
- <div class="card2__kpis">
55
- <div class="kpi kpi--net">
56
  <div class="kpi__label">Net value</div>
57
  <div class="kpi__value">{{ fmtUSD(c.balance) }}</div>
58
  </div>
59
-
60
- <div class="kpi kpi--profit">
61
  <div class="kpi__label">Profit</div>
62
- <div class="pill" :class="{ pos: c.profitUsd >= 0, neg: c.profitUsd < 0 }">{{ signedMoney(c.profitUsd) }}</div>
63
  </div>
64
-
65
- <div class="kpi kpi--delta">
66
  <div class="kpi__label">Delta profit</div>
67
  <div class="pill" :class="{ pos: (c.gapUsd ?? 0) >= 0, neg: (c.gapUsd ?? 0) < 0 }">
68
  <template v-if="c.kind==='agent'">
@@ -74,17 +69,14 @@
74
  </div>
75
  </div>
76
 
77
- <!-- EOD bottom-right -->
78
- <div class="card2__eod">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
79
- </div>
80
  </div>
81
  </section>
82
 
83
  <section v-else class="panel panel--cards">
84
  <div class="empty">No card data yet for <strong>{{ asset }}</strong>.</div>
85
  </section>
86
- <div class="empty">No card data yet for <strong>{{ asset }}</strong>.</div>
87
- </section>
88
  </div>
89
  </template>
90
 
@@ -151,6 +143,7 @@ const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', cu
151
  const signedMoney = (n) => `${n >= 0 ? '+' : 'βˆ’'}${fmtUSD(Math.abs(n))}`
152
  const signedPct = (p) => `${(p >= 0 ? '+' : 'βˆ’')}${Math.abs(p * 100).toFixed(2)}%`
153
  const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
 
154
 
155
  /* rows for selected asset (exclude vanilla/vinilla) */
156
  const filteredRows = computed(() =>
@@ -278,20 +271,6 @@ watch(
278
  profitUsd,
279
  isWinner: false
280
  }
281
- }) => {
282
- const gapUsd = perf.stratLast - perf.bhLast
283
- const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
284
- return {
285
- key: `agent|${sel.agent_name}|${sel.model}`,
286
- kind: 'agent',
287
- title: sel.agent_name,
288
- subtitle: sel.model,
289
- balance: perf.stratLast,
290
- date: perf.date,
291
- logo: AGENT_LOGOS[sel.agent_name] || null,
292
- gapUsd, gapPct,
293
- isWinner: false
294
- }
295
  })
296
 
297
  const maxBal = Math.max(...agentCards.map(c => c.balance ?? -Infinity))
@@ -332,117 +311,34 @@ watch(
332
  /* empty */
333
  .empty { padding: 14px; border: 1px dashed #D6DAE1; border-radius: 12px; color: #6B7280; font-size: .92rem; }
334
 
335
- /* NEW GRID: five fixed columns on wide screens */
336
  .cards-grid { display: grid; gap: 16px; grid-template-columns: repeat(5, minmax(0, 1fr)); }
337
  @media (max-width: 1400px) { .cards-grid { grid-template-columns: repeat(4, minmax(0,1fr)); } }
338
  @media (max-width: 1100px) { .cards-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } }
339
  @media (max-width: 900px) { .cards-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
340
  @media (max-width: 640px) { .cards-grid { grid-template-columns: 1fr; } }
341
 
342
- /* CARD v2 */
343
- .card2 {
344
- position: relative;
345
- display: grid;
346
- grid-template-rows: auto auto; /* header, kpis */
347
- gap: 16px;
348
- padding: 18px;
349
- min-height: 220px;
350
- border: 1px solid #EEF1F6;
351
- border-radius: 16px;
352
- background: #fff;
353
- box-shadow: 0 1px 2px rgba(0,0,0,.04);
354
- transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease;
355
- }
356
- .card2:hover { transform: translateY(-1px); box-shadow: 0 6px 16px rgba(0,0,0,.08); }
357
- .card2.is-bh { outline: 2px dashed rgba(15,23,42,.08); }
358
- .card2.is-winner { border-color: #16a34a; box-shadow: 0 0 0 3px rgba(22,163,74,.12); }
359
- .card2:hover { transform: translateY(-1px); box-shadow: 0 6px 16px rgba(0,0,0,.08); }
360
- .card2.is-bh { outline: 2px dashed rgba(15,23,42,.08); }
361
- .card2.is-winner { border-color: #16a34a; box-shadow: 0 0 0 3px rgba(22,163,74,.12); }
362
-
363
- /* ribbon */
364
- .card2__ribbon { position: absolute; top: 10px; right: 12px; font-size: 18px; filter: drop-shadow(0 1px 1px rgba(0,0,0,.15)); }
365
-
366
- /* row layout */
367
- .card2__row { display: grid; align-items: center; }
368
- .card2__header { display: grid; grid-template-columns: 48px minmax(0,1fr); align-items: center; gap: 12px; }
369
- .avatar { width: 48px; height: 48px; border-radius: 999px; background: #F3F4F6; display: grid; place-items: center; overflow: hidden; }
370
- .avatar img { width: 100%; height: 100%; object-fit: contain; }
371
- .avatar__fallback { width: 60%; height: 60%; border-radius: 999px; background: #E5E7EB; }
372
- .titleblock { min-width: 0; }
373
- .title { font-weight: 900; color: #0F172A; line-height: 1.1; font-size: clamp(18px, 1.6vw, 22px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
374
- .subtitle { font-size: 13px; color: #64748B; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
375
- .card2__kpis { display: grid; grid-template-columns: 1.3fr .7fr .8fr; align-items: end; gap: 12px; }
376
- .kpi { min-width: 0; }
377
  .kpi__label { font-size: 12px; color: #6B7280; margin-bottom: 4px; }
378
  .kpi__value { font-weight: 900; color: #0F172A; font-size: clamp(22px, 2.2vw, 28px); white-space: nowrap; }
379
  .pill { padding: 6px 10px; border-radius: 999px; font-size: 13px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; display: inline-block; }
380
  .pill.pos { background: #DCFCE7; color: #166534; }
381
  .pill.neg { background: #FEE2E2; color: #B91C1C; }
382
- .card2__eod { position: absolute; bottom: 12px; right: 14px; font-size: 12px; color: #5B
383
-
384
- /* avatar */
385
- .avatar { width: 56px; height: 56px; border-radius: 999px; background: #F3F4F6; display: grid; place-items: center; overflow: hidden; }
386
- .avatar img { width: 100%; height: 100%; object-fit: contain; }
387
- .avatar__fallback { width: 60%; height: 60%; border-radius: 999px; background: #E5E7EB; }
388
-
389
- /* titles */
390
- .info { display: grid; grid-template-columns: 56px minmax(0,1fr); align-items: center; gap: 12px; min-width: 0; }
391
- .titles { min-width: 0; display: grid; gap: 4px; }
392
- .title { font-weight: 900; color: #0F172A; line-height: 1.1; font-size: clamp(18px, 1.6vw, 22px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
393
- .subtitle { font-size: 13px; color: #64748B; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
394
-
395
- /* primary metric */
396
- .primary { text-align: right; }
397
- .primary__label { font-size: 12px; letter-spacing: .02em; color: #6B7280; }
398
- .primary__value { font-weight: 900; color: #0F172A; font-size: clamp(22px, 2.2vw, 28px); white-space: nowrap; }
399
-
400
- /* metrics wrapper */
401
- .metrics { display: flex; align-items: center; gap: 12px; justify-content: flex-end; }
402
-
403
- /* chips & delta */
404
- .chips { display: flex; align-items: center; gap: 8px; min-width: 0; }
405
- .chip { padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; }
406
- .chip--neutral { background: #EEF2F7; color: #0F172A; }
407
- .chip--outline { background: transparent; color: #475569; border: 1px solid #E5E7EB; padding: 3px 8px; }
408
-
409
- .delta { display: flex; align-items: baseline; gap: 8px; }
410
- .delta__value { padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; }
411
- .delta__value.pos { background: #DCFCE7; color: #166534; }
412
- .delta__value.neg { background: #FEE2E2; color: #B91C1C; }
413
- .delta__label { font-size: 11px; color: #64748B; }
414
- .chips { display: flex; align-items: center; gap: 8px; min-width: 0; }
415
- .chip { padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; }
416
- .chip--neutral { background: #EEF2F7; color: #0F172A; }
417
- .chip--outline { background: transparent; color: #475569; border: 1px solid #E5E7EB; padding: 3px 8px; }
418
-
419
- .delta { display: flex; align-items: baseline; gap: 8px; }
420
- .delta__value { padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; }
421
- .delta__value.pos { background: #DCFCE7; color: #166534; }
422
- .delta__value.neg { background: #FEE2E2; color: #B91C1C; }
423
- .delta__label { font-size: 11px; color: #64748B; }
424
- .chips { display: flex; align-items: center; gap: 8px; min-width: 0; }
425
- .chip { padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; }
426
- .chip--neutral { background: #EEF2F7; color: #0F172A; }
427
- .chip--outline { background: transparent; color: #475569; border: 1px solid #E5E7EB; padding: 3px 8px; }
428
-
429
- .delta { display: flex; align-items: baseline; gap: 8px; }
430
- .delta__value { padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; }
431
- .delta__value.pos { background: #DCFCE7; color: #166534; }
432
- .delta__value.neg { background: #FEE2E2; color: #B91C1C; }
433
- .delta__label { font-size: 11px; color: #64748B; }
434
-
435
- /* foot */
436
- .foot__right { white-space: nowrap; }
437
-
438
- /* responsive nits */
439
- @media (max-width: 1200px) {
440
- .card2__row--top { grid-template-columns: 44px minmax(0,1fr) auto; }
441
- }
442
- @media (max-width: 960px) {
443
- .primary__value { font-size: 20px; }
444
- }
445
- @media (max-width: 640px) {
446
- .cards-grid { grid-template-columns: 1fr; }
447
- }
448
  </style>
 
26
  </div>
27
  </section>
28
 
29
+ <!-- Cards: LOGO + title (+ crown), KPIs, EOD bottom-right -->
30
  <section class="panel panel--cards" v-if="cards.length">
31
  <div class="cards-grid">
32
+ <article
33
  v-for="c in cards"
34
  :key="c.key"
35
+ class="card3"
36
+ :class="{ 'is-winner': c.isWinner, 'is-bh': c.kind==='bh' }"
37
  >
38
+ <span v-if="c.isWinner" class="card3__crown" aria-label="Top performer">πŸ‘‘</span>
 
39
 
40
+ <header class="card3__head">
41
+ <div class="logo">
 
42
  <img v-if="c.logo" :src="c.logo" alt="" />
43
+ <div v-else class="logo__fallback" aria-hidden="true"></div>
44
  </div>
45
+ <div class="head__text">
46
+ <h3 class="head__title">{{ c.kind==='bh' ? 'Buy & Hold' : c.title }}</h3>
47
+ <p class="head__subtitle">{{ c.subtitle }}</p>
48
  </div>
49
+ </header>
50
 
51
+ <div class="card3__kpis">
52
+ <div class="kpi">
 
53
  <div class="kpi__label">Net value</div>
54
  <div class="kpi__value">{{ fmtUSD(c.balance) }}</div>
55
  </div>
56
+ <div class="kpi">
 
57
  <div class="kpi__label">Profit</div>
58
+ <div class="pill" :class="{ pos: profitOf(c) >= 0, neg: profitOf(c) < 0 }">{{ signedMoney(profitOf(c)) }}</div>
59
  </div>
60
+ <div class="kpi">
 
61
  <div class="kpi__label">Delta profit</div>
62
  <div class="pill" :class="{ pos: (c.gapUsd ?? 0) >= 0, neg: (c.gapUsd ?? 0) < 0 }">
63
  <template v-if="c.kind==='agent'">
 
69
  </div>
70
  </div>
71
 
72
+ <footer class="card3__foot">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</footer>
73
+ </article>
 
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
 
 
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
+ const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
147
 
148
  /* rows for selected asset (exclude vanilla/vinilla) */
149
  const filteredRows = computed(() =>
 
271
  profitUsd,
272
  isWinner: false
273
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  })
275
 
276
  const maxBal = Math.max(...agentCards.map(c => c.balance ?? -Infinity))
 
311
  /* empty */
312
  .empty { padding: 14px; border: 1px dashed #D6DAE1; border-radius: 12px; color: #6B7280; font-size: .92rem; }
313
 
314
+ /* GRID: five fixed columns on wide screens */
315
  .cards-grid { display: grid; gap: 16px; grid-template-columns: repeat(5, minmax(0, 1fr)); }
316
  @media (max-width: 1400px) { .cards-grid { grid-template-columns: repeat(4, minmax(0,1fr)); } }
317
  @media (max-width: 1100px) { .cards-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } }
318
  @media (max-width: 900px) { .cards-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
319
  @media (max-width: 640px) { .cards-grid { grid-template-columns: 1fr; } }
320
 
321
+ /* CARD v3 */
322
+ .card3 { position: relative; display: grid; grid-template-rows: auto auto; gap: 16px; padding: 18px; min-height: 220px; border: 1px solid #EEF1F6; border-radius: 16px; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.04); transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease; }
323
+ .card3:hover { transform: translateY(-1px); box-shadow: 0 6px 16px rgba(0,0,0,.08); }
324
+ .card3.is-winner { border-color: #16a34a; box-shadow: 0 0 0 3px rgba(22,163,74,.12); }
325
+ .card3.is-bh { outline: 2px dashed rgba(15,23,42,.08); }
326
+
327
+ .card3__crown { position: absolute; top: 10px; right: 12px; font-size: 18px; filter: drop-shadow(0 1px 1px rgba(0,0,0,.15)); }
328
+
329
+ .card3__head { display: grid; grid-template-columns: 48px minmax(0,1fr); align-items: center; gap: 12px; }
330
+ .logo { width: 48px; height: 48px; border-radius: 999px; background: #F3F4F6; display: grid; place-items: center; overflow: hidden; }
331
+ .logo img { width: 100%; height: 100%; object-fit: contain; }
332
+ .logo__fallback { width: 60%; height: 60%; border-radius: 999px; background: #E5E7EB; }
333
+ .head__title { font-weight: 900; color: #0F172A; line-height: 1.1; font-size: clamp(18px, 1.6vw, 22px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
334
+ .head__subtitle { font-size: 13px; color: #64748B; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
335
+
336
+ .card3__kpis { display: grid; grid-template-columns: 1.3fr .7fr .8fr; align-items: end; gap: 12px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  .kpi__label { font-size: 12px; color: #6B7280; margin-bottom: 4px; }
338
  .kpi__value { font-weight: 900; color: #0F172A; font-size: clamp(22px, 2.2vw, 28px); white-space: nowrap; }
339
  .pill { padding: 6px 10px; border-radius: 999px; font-size: 13px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; display: inline-block; }
340
  .pill.pos { background: #DCFCE7; color: #166534; }
341
  .pill.neg { background: #FEE2E2; color: #B91C1C; }
342
+
343
+ .card3__foot { position: absolute; bottom: 12px; right: 14px; font-size: 12px; color: #5B6476; opacity: .9; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  </style>