Jimin Huang commited on
Commit
70deb70
Β·
1 Parent(s): 727b381

Change settings

Browse files
Files changed (1) hide show
  1. src/views/LiveView.vue +107 -18
src/views/LiveView.vue CHANGED
@@ -3,7 +3,7 @@
3
  <!-- Toolbar: assets + mode -->
4
  <header class="toolbar">
5
  <div class="toolbar__left">
6
- <!-- Asset tabs: styled to AMA spec (gradient active, minimal, elegant) -->
7
  <div class="asset-tabs">
8
  <button
9
  v-for="a in orderedAssets"
@@ -55,7 +55,7 @@
55
  </div>
56
  </section>
57
 
58
- <!-- Podium + Cards -->
59
  <section class="panel panel--cards" v-if="cards.length">
60
  <div class="cards-grid-f1">
61
  <article
@@ -87,7 +87,7 @@
87
  </div>
88
  </header>
89
 
90
- <!-- KPI row: Net value + (primary) Profit -->
91
  <div class="kpis">
92
  <div class="kpi">
93
  <div class="kpi__label">Net Value</div>
@@ -101,6 +101,25 @@
101
  </div>
102
  </div>
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  <!-- Performance bar (vs B&H) -->
105
  <div
106
  class="bar"
@@ -138,15 +157,22 @@
138
  import { ref, computed, onMounted, onBeforeUnmount, watch, shallowRef } from 'vue'
139
  import CompareChartE from '../components/CompareChartE.vue'
140
  import { dataService } from '../lib/dataService'
 
 
141
  import { getAllDecisions } from '../lib/dataCache'
142
  import { readAllRawDecisions } from '../lib/idb'
143
  import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
144
  import { STRATEGIES } from '../lib/strategies'
145
- import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
 
 
 
 
 
146
 
147
  /* ---------- config ---------- */
148
- const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA'] // (MRNA removed)
149
- const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla']) // case-insensitive
150
 
151
  const ASSET_ICONS = {
152
  BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
@@ -178,10 +204,8 @@ onMounted(async () => {
178
  rowsRef.value = Array.isArray(state.tableRows) ? state.tableRows : []
179
  })
180
 
181
- // immediate sync with current state (in case data already loaded)
182
  rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
183
 
184
- // trigger a load only if nothing is in-flight
185
  if (!dataService.loaded && !dataService.loading) {
186
  dataService.load(false).catch(e => console.error('LiveView: load failed', e))
187
  }
@@ -203,11 +227,13 @@ onBeforeUnmount(() => {
203
  /* ---------- helpers ---------- */
204
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
205
  const signedMoney = (n) => `${n >= 0 ? '+' : 'βˆ’'}${fmtUSD(Math.abs(n))}`
206
- const signedPct = (p) => `${(p >= 0 ? '+' : 'βˆ’')}${Math.abs(p * 100).toFixed(2)}%`
 
 
207
  const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
208
  const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
209
 
210
- /* rows for selected asset (exclude vanilla/vinilla) - only show long_only strategy */
211
  const filteredRows = computed(() =>
212
  (rowsRef.value || []).filter(r => {
213
  if (r.asset !== asset.value) return false
@@ -255,7 +281,7 @@ async function buildSeq(sel) {
255
 
256
  seq.sort((a,b) => (a.date > b.date ? 1 : -1))
257
 
258
- // if using decision_ids, data already prefiltered
259
  if (!ids.length) {
260
  const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
261
  if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
@@ -281,7 +307,37 @@ async function computeEquities(sel) {
281
  const lastIdx = Math.min(stratY.length, bhY.length) - 1
282
  if (lastIdx < 0) return null
283
 
284
- return { date: seq[lastIdx].date, stratLast: stratY[lastIdx], bhLast: bhY[lastIdx] }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  }
286
 
287
  /* build cards whenever winners/asset change */
@@ -314,7 +370,13 @@ watch(
314
  gapUsd: 0,
315
  gapPct: 0,
316
  rank: null,
317
- barPct: 0
 
 
 
 
 
 
318
  }
319
 
320
  // Agents
@@ -332,6 +394,11 @@ watch(
332
  logo: AGENT_LOGOS[sel.agent_name] || null,
333
  gapUsd, gapPct,
334
  profitUsd,
 
 
 
 
 
335
  rank: null,
336
  barPct: 0
337
  }
@@ -393,7 +460,7 @@ watch(
393
  }
394
  .toolbar__right { display: flex; align-items: center; gap: 12px; }
395
 
396
- /* Asset tabs (internal replacement to ensure style control) */
397
  .asset-tabs {
398
  display: inline-flex; gap: 6px; padding: 4px; border-radius: 12px;
399
  background: #ffffff; border: 1px solid #E1E6F0;
@@ -440,16 +507,16 @@ watch(
440
  .empty { padding: 20px; border: 1px dashed #D7DDE7; border-radius: 12px; color: #6B7280; font-size: .95rem; background: #ffffff; text-align: center; }
441
 
442
  /* grid */
443
- .cards-grid-f1 { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
444
 
445
  /* card base with subtle AMA hue */
446
  .card-f1 {
447
  position: relative;
448
  display: grid;
449
- grid-template-rows: auto auto auto;
450
  gap: 10px;
451
  padding: 18px 18px 20px;
452
- min-height: 210px;
453
  border-radius: 16px;
454
  background: linear-gradient(145deg,var(--card-bg-1),var(--card-bg-2) 75%,var(--card-bg-1) 100%);
455
  border: 1px solid rgba(0,0,0,0.05);
@@ -490,8 +557,30 @@ watch(
490
  .kpi__pill.neg{ color: var(--neg-fg); background: var(--neg-bg); border-color: var(--neg-br); }
491
  .align-right{ text-align:right; }
492
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  /* Bar vs B&H */
494
- .bar { height: 6px; border-radius: 999px; background: #F2F4F8; overflow: hidden; border: 1px solid #E7ECF3; margin: 8px 0; }
495
  .bar span { display: block; height: 100%; background: linear-gradient(90deg,#16a34a,#22c55e); width: var(--bar, 40%); transition: width .5s ease; }
496
  .bar.neg span { background: linear-gradient(90deg,#ef4444,#dc2626); }
497
 
 
3
  <!-- Toolbar: assets + mode -->
4
  <header class="toolbar">
5
  <div class="toolbar__left">
6
+ <!-- Asset tabs styled per AMA -->
7
  <div class="asset-tabs">
8
  <button
9
  v-for="a in orderedAssets"
 
55
  </div>
56
  </section>
57
 
58
+ <!-- Tournament Cards -->
59
  <section class="panel panel--cards" v-if="cards.length">
60
  <div class="cards-grid-f1">
61
  <article
 
87
  </div>
88
  </header>
89
 
90
+ <!-- KPI row: Net value + P&L -->
91
  <div class="kpis">
92
  <div class="kpi">
93
  <div class="kpi__label">Net Value</div>
 
101
  </div>
102
  </div>
103
 
104
+ <!-- Quality row: Sharpe / MaxDD -->
105
+ <div class="quality">
106
+ <div class="qitem">
107
+ <span class="qitem__label">Sharpe</span>
108
+ <span class="qitem__value" :class="{ strong: (c.sharpe ?? 0) >= 2 }">{{ fmtSharpe(c.sharpe) }}</span>
109
+ </div>
110
+ <div class="qitem">
111
+ <span class="qitem__label">MaxDD</span>
112
+ <span class="qitem__value dd">{{ signedPct(c.maxDrawdown) }}</span>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Match stats: Win Rate / Trades / Days -->
117
+ <div class="stats">
118
+ <div class="stat"><span class="stat__label">Win Rate</span><span class="stat__val" :class="{ pos: (c.winRate ?? 0) >= 0.5, neg: (c.winRate ?? 0) < 0.5 }">{{ fmtRate(c.winRate) }}</span></div>
119
+ <div class="stat"><span class="stat__label">Trades</span><span class="stat__val">{{ c.trades ?? 'β€”' }}</span></div>
120
+ <div class="stat"><span class="stat__label">Days</span><span class="stat__val">{{ c.days ?? 'β€”' }}</span></div>
121
+ </div>
122
+
123
  <!-- Performance bar (vs B&H) -->
124
  <div
125
  class="bar"
 
157
  import { ref, computed, onMounted, onBeforeUnmount, watch, shallowRef } from 'vue'
158
  import CompareChartE from '../components/CompareChartE.vue'
159
  import { dataService } from '../lib/dataService'
160
+
161
+ /* helpers & metrics */
162
  import { getAllDecisions } from '../lib/dataCache'
163
  import { readAllRawDecisions } from '../lib/idb'
164
  import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
165
  import { STRATEGIES } from '../lib/strategies'
166
+ import {
167
+ computeBuyHoldEquity,
168
+ computeStrategyEquity,
169
+ calculateMetricsFromSeries,
170
+ computeWinRate
171
+ } from '../lib/perf'
172
 
173
  /* ---------- config ---------- */
174
+ const orderedAssets = ['BTC','ETH','BMRN','TSLA']
175
+ const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla'])
176
 
177
  const ASSET_ICONS = {
178
  BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
 
204
  rowsRef.value = Array.isArray(state.tableRows) ? state.tableRows : []
205
  })
206
 
 
207
  rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
208
 
 
209
  if (!dataService.loaded && !dataService.loading) {
210
  dataService.load(false).catch(e => console.error('LiveView: load failed', e))
211
  }
 
227
  /* ---------- helpers ---------- */
228
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
229
  const signedMoney = (n) => `${n >= 0 ? '+' : 'βˆ’'}${fmtUSD(Math.abs(n))}`
230
+ const signedPct = (p) => (p == null ? 'β€”' : `${(p >= 0 ? '+' : 'βˆ’')}${Math.abs(p * 100).toFixed(2)}%`)
231
+ const fmtSharpe = (s) => (s == null ? 'β€”' : Number(s).toFixed(2))
232
+ const fmtRate = (r) => (r == null ? 'β€”' : `${(r * 100).toFixed(0)}%`)
233
  const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
234
  const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
235
 
236
+ /* rows for selected asset (exclude vanilla/vinilla) - only show long_only */
237
  const filteredRows = computed(() =>
238
  (rowsRef.value || []).filter(r => {
239
  if (r.asset !== asset.value) return false
 
281
 
282
  seq.sort((a,b) => (a.date > b.date ? 1 : -1))
283
 
284
+ // if using decision_ids, data is already prefiltered
285
  if (!ids.length) {
286
  const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
287
  if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
 
307
  const lastIdx = Math.min(stratY.length, bhY.length) - 1
308
  if (lastIdx < 0) return null
309
 
310
+ // quality metrics
311
+ const isCrypto = sel.asset === 'BTC' || sel.asset === 'ETH'
312
+ const metrics = calculateMetricsFromSeries(stratY, isCrypto ? 'crypto' : 'stock') || {}
313
+ const { sharpe_ratio: sharpe, max_drawdown: maxDrawdown, total_return: totalReturnPct } = metrics
314
+
315
+ // win rate / trades
316
+ let winRate = null
317
+ let trades = null
318
+ try {
319
+ const r = computeWinRate(seq, cfg.strategy, cfg.tradingMode) || {}
320
+ winRate = r.winRate
321
+ trades = r.trades
322
+ } catch {}
323
+
324
+ // approximate "days" from sequence
325
+ let days = 0
326
+ try {
327
+ const start = new Date(seq[0].date)
328
+ const end = new Date(seq[lastIdx].date)
329
+ days = Math.max(1, Math.round((end - start) / 86400000) + 1)
330
+ } catch {}
331
+
332
+ return {
333
+ date: seq[lastIdx].date,
334
+ stratLast: stratY[lastIdx],
335
+ bhLast: bhY[lastIdx],
336
+ sharpe,
337
+ maxDrawdown, // as fraction (e.g., -0.053)
338
+ winRate, trades, days,
339
+ totalReturnPct // for future use if needed
340
+ }
341
  }
342
 
343
  /* build cards whenever winners/asset change */
 
370
  gapUsd: 0,
371
  gapPct: 0,
372
  rank: null,
373
+ barPct: 0,
374
+ // BH quality fields are neutral/na
375
+ sharpe: null,
376
+ maxDrawdown: null,
377
+ winRate: null,
378
+ trades: null,
379
+ days: null
380
  }
381
 
382
  // Agents
 
394
  logo: AGENT_LOGOS[sel.agent_name] || null,
395
  gapUsd, gapPct,
396
  profitUsd,
397
+ sharpe: perf.sharpe,
398
+ maxDrawdown: perf.maxDrawdown,
399
+ winRate: perf.winRate,
400
+ trades: perf.trades,
401
+ days: perf.days,
402
  rank: null,
403
  barPct: 0
404
  }
 
460
  }
461
  .toolbar__right { display: flex; align-items: center; gap: 12px; }
462
 
463
+ /* Asset tabs */
464
  .asset-tabs {
465
  display: inline-flex; gap: 6px; padding: 4px; border-radius: 12px;
466
  background: #ffffff; border: 1px solid #E1E6F0;
 
507
  .empty { padding: 20px; border: 1px dashed #D7DDE7; border-radius: 12px; color: #6B7280; font-size: .95rem; background: #ffffff; text-align: center; }
508
 
509
  /* grid */
510
+ .cards-grid-f1 { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
511
 
512
  /* card base with subtle AMA hue */
513
  .card-f1 {
514
  position: relative;
515
  display: grid;
516
+ grid-template-rows: auto auto auto auto auto;
517
  gap: 10px;
518
  padding: 18px 18px 20px;
519
+ min-height: 230px;
520
  border-radius: 16px;
521
  background: linear-gradient(145deg,var(--card-bg-1),var(--card-bg-2) 75%,var(--card-bg-1) 100%);
522
  border: 1px solid rgba(0,0,0,0.05);
 
557
  .kpi__pill.neg{ color: var(--neg-fg); background: var(--neg-bg); border-color: var(--neg-br); }
558
  .align-right{ text-align:right; }
559
 
560
+ /* Quality row */
561
+ .quality{
562
+ display:grid; grid-template-columns: 1fr 1fr; gap:12px;
563
+ padding: 2px 0 0;
564
+ }
565
+ .qitem{ display:flex; align-items:baseline; gap:8px; }
566
+ .qitem__label{ font-size:12px; color:#6b7280; }
567
+ .qitem__value{ font-size:14px; font-weight:800; color:#0f172a; }
568
+ .qitem__value.strong{ color:#0e7a3a; }
569
+ .qitem__value.dd{ color:#B91C1C; }
570
+
571
+ /* Stats row */
572
+ .stats{
573
+ display:grid; grid-template-columns: repeat(3, 1fr); gap:10px;
574
+ font-size:12px; color:#374151;
575
+ }
576
+ .stat{ display:flex; align-items:center; justify-content:space-between; background:#F8FAFD; border:1px solid #E7ECF3; border-radius:10px; padding:6px 8px; }
577
+ .stat__label{ color:#6b7280; }
578
+ .stat__val{ font-weight:800; }
579
+ .stat__val.pos{ color: var(--pos-fg); }
580
+ .stat__val.neg{ color: var(--neg-fg); }
581
+
582
  /* Bar vs B&H */
583
+ .bar { height: 6px; border-radius: 999px; background: #F2F4F8; overflow: hidden; border: 1px solid #E7ECF3; margin: 2px 0 8px; }
584
  .bar span { display: block; height: 100%; background: linear-gradient(90deg,#16a34a,#22c55e); width: var(--bar, 40%); transition: width .5s ease; }
585
  .bar.neg span { background: linear-gradient(90deg,#ef4444,#dc2626); }
586