Jimin Huang commited on
Commit
d9ae91a
Β·
1 Parent(s): e1b75e6

Change settings

Browse files
Files changed (1) hide show
  1. src/views/LiveView.vue +114 -104
src/views/LiveView.vue CHANGED
@@ -26,41 +26,51 @@
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
- <!-- Modern body: single, strong Net value row -->
53
- <div class="kpi kpi--net">
54
- <span class="kpi__label">Net value</span>
55
- <span class="kpi__value">{{ fmtUSD(c.balance) }}</span>
56
- </div>
 
 
 
57
  </div>
58
 
59
- <!-- Bottom bar: Profit / Delta chips on the left, EOD on the right -->
60
- <div class="card3__bottom">
61
  <div class="chips">
62
- <span class="pill" :class="{ pos: profitOf(c) >= 0, neg: profitOf(c) < 0 }">{{ signedMoney(profitOf(c)) }}</span>
63
- <span class="pill" :class="{ pos: (c.gapUsd ?? 0) >= 0, neg: (c.gapUsd ?? 0) < 0 }">
 
 
64
  <template v-if="c.kind==='agent'">
65
  <template v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</template>
66
  <template v-else>{{ signedPct(c.gapPct) }}</template>
@@ -68,7 +78,7 @@
68
  <template v-else>β€”</template>
69
  </span>
70
  </div>
71
- <footer class="card3__foot">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</footer>
72
  </div>
73
  </article>
74
  </div>
@@ -104,18 +114,15 @@ const ASSET_ICONS = {
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 () => {
@@ -123,9 +130,7 @@ onMounted(async () => {
123
  if (!dataService.loaded && !dataService.loading) {
124
  await dataService.load(false)
125
  }
126
- } catch (e) {
127
- console.error('LiveView: dataService.load failed', e)
128
- }
129
  rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
130
  if (!orderedAssets.includes(asset.value)) asset.value = orderedAssets[0]
131
 
@@ -194,9 +199,7 @@ async function buildSeq(sel) {
194
 
195
  const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
196
  let filtered = seq
197
- if (!isCrypto) {
198
- filtered = await filterRowsToNyseTradingDays(seq)
199
- }
200
 
201
  const cutoff = ASSET_CUTOFF[assetCode]
202
  if (cutoff) {
@@ -210,9 +213,7 @@ async function computeEquities(sel) {
210
  const seq = await buildSeq(sel)
211
  if (!seq.length) return null
212
 
213
- const cfg =
214
- (STRATEGIES || []).find(s => s.id === sel.strategy) ||
215
- { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
216
 
217
  const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
218
  const bhY = computeBuyHoldEquity(seq, 100000) || []
@@ -251,7 +252,9 @@ watch(
251
  profitUsd: (first.perf.bhLast ?? 0) - 100000,
252
  gapUsd: 0,
253
  gapPct: 0,
254
- isWinner: false
 
 
255
  }
256
 
257
  // Agent cards (gap vs BH)
@@ -269,21 +272,29 @@ watch(
269
  logo: AGENT_LOGOS[sel.agent_name] || null,
270
  gapUsd, gapPct,
271
  profitUsd,
272
- isWinner: false
 
 
273
  }
274
  })
275
 
276
- const maxBal = Math.max(...agentCards.map(c => c.balance ?? -Infinity))
277
- agentCards.forEach(c => { c.isWinner = c.balance === maxBal })
 
278
 
279
- // top 4 agents + BH card
280
- cards.value = [bhCard, ...agentCards.sort((a,b) => b.balance - a.balance).slice(0,4)]
 
 
 
 
 
 
 
281
  } catch (e) {
282
  console.error('LiveView: compute cards failed', e)
283
  cards.value = []
284
- } finally {
285
- computing = false
286
- }
287
  },
288
  { immediate: true }
289
  )
@@ -293,74 +304,73 @@ watch(
293
  .live { max-width: 1280px; margin: 0 auto; padding: 12px 20px 28px; }
294
 
295
  /* toolbar */
296
- .toolbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 0 10px; background: #fff; }
297
- .toolbar__left { min-width: 0; }
298
- .toolbar__right { display: flex; align-items: center; gap: 10px; }
299
-
300
- /* $/% toggle */
301
- .mode { display: inline-flex; gap: 8px; }
302
- .mode__btn { height: 32px; min-width: 40px; padding: 0 12px; border-radius: 10px; border: 1px solid #D6DAE1; background: #fff; font-weight: 700; color: #0F172A; transition: all .12s ease; }
303
- .mode__btn:hover { transform: translateY(-1px); }
304
- .mode__btn.is-active { background: #0F172A; color: #fff; border-color: #0F172A; }
305
 
306
  /* panels */
307
- .panel { background: #fff; border: 1px solid #EDF0F4; border-radius: 14px; }
308
  .panel--chart { padding: 10px 10px 2px; }
309
  .panel--cards { padding: 12px; }
310
 
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 {
323
  position: relative;
324
  display: grid;
325
- grid-template-rows: auto auto auto; /* head, kpis, bottom */
326
- gap: 14px;
327
- padding: 18px;
328
- min-height: 228px;
329
- border-radius: 18px;
330
- border: 1px solid #E8ECF3;
331
- background: radial-gradient(120% 140% at 0% 0%, #FFFFFF 0%, #FBFCFE 100%);
332
- box-shadow: 0 1px 2px rgba(16,24,40,.04), 0 8px 24px rgba(15,23,42,.04);
333
- transition: box-shadow .2s ease, transform .15s ease, border-color .2s ease;
 
334
  }
335
- .card3:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(15,23,42,.08); border-color: #D9E2F0; }
336
- .card3.is-winner { border-color: #16a34a; box-shadow: 0 0 0 3px rgba(22,163,74,.14); }
337
- .card3.is-bh { outline: 2px dashed rgba(15,23,42,.08); background: radial-gradient(120% 140% at 0% 0%, #FFFFFF 0%, #F9FBFF 100%); }
338
- .card3:hover { transform: translateY(-1px); box-shadow: 0 6px 16px rgba(0,0,0,.08); }
339
- .card3.is-winner { border-color: #16a34a; box-shadow: 0 0 0 3px rgba(22,163,74,.12); }
340
- .card3.is-bh { outline: 2px dashed rgba(15,23,42,.08); }
341
-
342
- .card3__crown { position: absolute; top: 12px; right: 12px; font-size: 18px; filter: drop-shadow(0 1px 1px rgba(0,0,0,.15)); }
343
-
344
- .card3__head { display: grid; grid-template-columns: 48px minmax(0,1fr); align-items: center; gap: 12px; }
345
- .logo { width: 48px; height: 48px; border-radius: 999px; background: #F3F4F6; display: grid; place-items: center; overflow: hidden; }
 
346
  .logo img { width: 100%; height: 100%; object-fit: contain; }
347
- .logo__fallback { width: 60%; height: 60%; border-radius: 999px; background: #E5E7EB; }
348
- .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; }
349
- .head__subtitle { font-size: 12px; color: #8A94A7; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
350
-
351
- .card3__kpis { display: grid; grid-template-columns: 1fr; align-items: center; }
352
- .kpi--net { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
353
- .kpi__label { font-size: 12px; color: #6B7280; margin: 0; }
354
- .kpi__value { font-weight: 900; color: #0B1220; font-size: clamp(24px, 2.2vw, 34px); letter-spacing: -.01em; white-space: nowrap; }
355
- .kpi__label { font-size: 12px; color: #6B7280; margin-bottom: 4px; }
356
- .kpi__value { font-weight: 900; color: #0F172A; font-size: clamp(22px, 2.2vw, 28px); white-space: nowrap; }
357
- .pill { padding: 6px 10px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #F1F5FB; color: #0F172A; display: inline-block; box-shadow: inset 0 -1px 0 rgba(15,23,42,.04); }
358
- .pill.pos { background: #E6F7ED; color: #0E7A3A; }
359
- .pill.neg { background: #FBE9E9; color: #B91C1C; }
360
- .pill.pos { background: #DCFCE7; color: #166534; }
361
- .pill.neg { background: #FEE2E2; color: #B91C1C; }
362
-
363
- .card3__foot { font-size: 12px; color: #5B6476; opacity: .9; }
364
- .card3__bottom { display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 10px; margin-top: 4px; }
365
- .card3__bottom .chips { display: inline-flex; gap: 8px; align-items: center; }
 
 
 
366
  </style>
 
26
  </div>
27
  </section>
28
 
29
+ <!-- F1 Scoreboard Cards -->
30
  <section class="panel panel--cards" v-if="cards.length">
31
+ <div class="cards-grid-f1">
32
  <article
33
  v-for="c in cards"
34
  :key="c.key"
35
+ class="card-f1"
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">P{{ 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 -->
45
+ <header class="head">
46
  <div class="logo">
47
  <img v-if="c.logo" :src="c.logo" alt="" />
48
  <div v-else class="logo__fallback" aria-hidden="true"></div>
49
  </div>
50
+ <div class="names">
51
+ <div class="agent">{{ c.kind==='bh' ? 'Buy & Hold' : c.title }}</div>
52
+ <div class="model">{{ c.subtitle }}</div>
53
  </div>
54
  </header>
55
 
56
+ <!-- Net value row -->
57
+ <div class="net">
58
+ <div class="net__label">Net value</div>
59
+ <div class="net__value">{{ fmtUSD(c.balance) }}</div>
60
+ </div>
61
+
62
+ <!-- Performance bar (vs B&H) -->
63
+ <div class="bar" :class="{ neg: (c.gapUsd ?? 0) < 0, pos: (c.gapUsd ?? 0) >= 0 }">
64
+ <span :style="{ width: (c.barPct ?? 0) + '%' }"></span>
65
  </div>
66
 
67
+ <!-- Bottom row: chips + EOD -->
68
+ <div class="bottom">
69
  <div class="chips">
70
+ <span class="chip" :class="{ pos: profitOf(c) >= 0, neg: profitOf(c) < 0 }">
71
+ {{ signedMoney(profitOf(c)) }}
72
+ </span>
73
+ <span class="chip" :class="{ pos: (c.gapUsd ?? 0) >= 0, neg: (c.gapUsd ?? 0) < 0 }">
74
  <template v-if="c.kind==='agent'">
75
  <template v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</template>
76
  <template v-else>{{ signedPct(c.gapPct) }}</template>
 
78
  <template v-else>β€”</template>
79
  </span>
80
  </div>
81
+ <div class="eod">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
82
  </div>
83
  </article>
84
  </div>
 
114
  BMRN: new URL('../assets/images/assets_images/BMRN.png', import.meta.url).href,
115
  TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
116
  }
 
 
117
  const AGENT_LOGOS = {}
118
+ const ASSET_CUTOFF = { BTC: '2025-08-01' }
 
119
 
120
  /* ---------- state ---------- */
121
+ const mode = ref('usd')
122
  const asset = ref('BTC')
123
+ const rowsRef = ref([])
124
+ let allDecisions = []
125
+ const cards = shallowRef([])
126
 
127
  /* ---------- bootstrap ---------- */
128
  onMounted(async () => {
 
130
  if (!dataService.loaded && !dataService.loading) {
131
  await dataService.load(false)
132
  }
133
+ } catch (e) { console.error('LiveView: dataService.load failed', e) }
 
 
134
  rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
135
  if (!orderedAssets.includes(asset.value)) asset.value = orderedAssets[0]
136
 
 
199
 
200
  const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
201
  let filtered = seq
202
+ if (!isCrypto) filtered = await filterRowsToNyseTradingDays(seq)
 
 
203
 
204
  const cutoff = ASSET_CUTOFF[assetCode]
205
  if (cutoff) {
 
213
  const seq = await buildSeq(sel)
214
  if (!seq.length) return null
215
 
216
+ const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
 
 
217
 
218
  const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
219
  const bhY = computeBuyHoldEquity(seq, 100000) || []
 
252
  profitUsd: (first.perf.bhLast ?? 0) - 100000,
253
  gapUsd: 0,
254
  gapPct: 0,
255
+ isWinner: false,
256
+ rank: null,
257
+ barPct: 0
258
  }
259
 
260
  // Agent cards (gap vs BH)
 
272
  logo: AGENT_LOGOS[sel.agent_name] || null,
273
  gapUsd, gapPct,
274
  profitUsd,
275
+ isWinner: false,
276
+ rank: null,
277
+ barPct: 0
278
  }
279
  })
280
 
281
+ // Rank by balance (agents only)
282
+ agentCards.sort((a,b) => (b.balance ?? -Infinity) - (a.balance ?? -Infinity))
283
+ agentCards.forEach((c, i) => { c.rank = i + 1 })
284
 
285
+ // Winner flag
286
+ if (agentCards.length) agentCards[0].isWinner = true
287
+
288
+ // Perf bar width scaled to max |gapUsd|
289
+ const maxAbsGap = Math.max(1, ...agentCards.map(c => Math.abs(c.gapUsd ?? 0)))
290
+ agentCards.forEach(c => { c.barPct = Math.max(3, Math.round((Math.abs(c.gapUsd ?? 0) / maxAbsGap) * 100)) })
291
+
292
+ // Build final list: BH first, then agents in rank order
293
+ cards.value = [bhCard, ...agentCards].slice(0,5)
294
  } catch (e) {
295
  console.error('LiveView: compute cards failed', e)
296
  cards.value = []
297
+ } finally { computing = false }
 
 
298
  },
299
  { immediate: true }
300
  )
 
304
  .live { max-width: 1280px; margin: 0 auto; padding: 12px 20px 28px; }
305
 
306
  /* toolbar */
307
+ .toolbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 0 10px; background: #0b0e14; color: #f0f3f9; border-bottom: 1px solid #1a1f2b; }
308
+ .mode__btn { height: 30px; min-width: 40px; padding: 0 10px; border-radius: 10px; border: 1px solid #2a3344; background: #101521; font-weight: 700; color: #d9e2f2; }
309
+ .mode__btn.is-active { background: #1b2333; color: #fff; border-color: #3a4865; }
 
 
 
 
 
 
310
 
311
  /* panels */
312
+ .panel { background: #0e141f; border: 1px solid #151c2a; border-radius: 14px; }
313
  .panel--chart { padding: 10px 10px 2px; }
314
  .panel--cards { padding: 12px; }
315
 
316
  /* empty */
317
+ .empty { padding: 14px; border: 1px dashed #2b3447; border-radius: 12px; color: #9aa4b2; font-size: .92rem; background: #0f1624; }
318
 
319
+ /* GRID */
320
+ .cards-grid-f1 { display: grid; gap: 12px; grid-template-columns: repeat(5, minmax(0,1fr)); }
321
+ @media (max-width: 1400px) { .cards-grid-f1 { grid-template-columns: repeat(4, minmax(0,1fr)); } }
322
+ @media (max-width: 1100px) { .cards-grid-f1 { grid-template-columns: repeat(3, minmax(0,1fr)); } }
323
+ @media (max-width: 900px) { .cards-grid-f1 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
324
+ @media (max-width: 640px) { .cards-grid-f1 { grid-template-columns: 1fr; } }
325
 
326
+ /* F1 Card */
327
+ .card-f1 {
328
  position: relative;
329
  display: grid;
330
+ grid-template-rows: auto auto auto; /* head, net, bottom */
331
+ gap: 10px;
332
+ padding: 16px 16px 18px;
333
+ min-height: 210px;
334
+ border-radius: 14px;
335
+ background: linear-gradient(145deg,#0f1421,#121826 55%,#0f1421 100%);
336
+ border: 1px solid #20283a;
337
+ box-shadow: inset 0 0 1px rgba(255,255,255,.06), 0 6px 18px rgba(0,0,0,.35);
338
+ color: #e9eef7;
339
+ transition: transform .18s ease, box-shadow .2s ease, border-color .2s ease;
340
  }
341
+ .card-f1:hover { transform: translateY(-2px); box-shadow: 0 10px 26px rgba(0,0,0,.5); border-color: #2b3650; }
342
+ .card-f1.winner { border-color: #ffd54a; box-shadow: 0 0 0 2px rgba(255,213,74,.25), 0 10px 26px rgba(0,0,0,.6); }
343
+ .card-f1.bh { border-style: dashed; opacity: .96; }
344
+
345
+ /* Rank / Crown */
346
+ .rank { position: absolute; top: 10px; left: 12px; font-weight: 900; font-size: 18px; color: rgba(255,255,255,.35); letter-spacing: .04em; }
347
+ .rank.bh-badge { font-size: 12px; background: rgba(255,255,255,.08); padding: 2px 6px; border-radius: 6px; color: #c9d2e3; }
348
+ .crown { position: absolute; top: 12px; right: 12px; font-size: 18px; filter: drop-shadow(0 1px 1px rgba(0,0,0,.3)); }
349
+
350
+ /* Head */
351
+ .head { display: grid; grid-template-columns: 40px minmax(0,1fr); align-items: center; gap: 10px; }
352
+ .logo { width: 40px; height: 40px; border-radius: 10px; background: #1b2333; display: grid; place-items: center; overflow: hidden; border: 1px solid #2a3344; }
353
  .logo img { width: 100%; height: 100%; object-fit: contain; }
354
+ .logo__fallback { width: 60%; height: 60%; border-radius: 6px; background: #2a3344; }
355
+ .names { min-width: 0; }
356
+ .agent { font-size: 16px; font-weight: 900; letter-spacing: .04em; text-transform: uppercase; color: #f4f7ff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
357
+ .model { font-size: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; color: #9aa4b2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
358
+
359
+ /* Net row */
360
+ .net { display: grid; grid-template-columns: 1fr auto; align-items: end; }
361
+ .net__label { font-size: 12px; color: #9aa4b2; }
362
+ .net__value { font-size: clamp(22px, 2.2vw, 30px); font-weight: 900; letter-spacing: -.01em; color: #fff; }
363
+
364
+ /* Bar vs B&H */
365
+ .bar { height: 6px; border-radius: 999px; background: #1b2333; overflow: hidden; border: 1px solid #2a3344; }
366
+ .bar span { display: block; height: 100%; background: linear-gradient(90deg,#00ff88,#00c864); width: var(--bar, 40%); transition: width .5s ease; }
367
+ .bar.neg span { background: linear-gradient(90deg,#ff7474,#d83a3a); }
368
+
369
+ /* Bottom */
370
+ .bottom { display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 8px; }
371
+ .chips { display: inline-flex; gap: 8px; }
372
+ .chip { font-size: 12px; font-weight: 800; padding: 4px 8px; border-radius: 999px; background: rgba(255,255,255,.08); backdrop-filter: blur(2px); }
373
+ .chip.pos { color: #00ff88; }
374
+ .chip.neg { color: #ff6b6b; }
375
+ .eod { font-size: 12px; color: #9aa4b2; }
376
  </style>