Jimin Huang commited on
Commit
2a89927
·
1 Parent(s): 2e529ca

Change settings

Browse files
Files changed (1) hide show
  1. src/views/LiveView.vue +121 -92
src/views/LiveView.vue CHANGED
@@ -26,7 +26,7 @@
26
  </div>
27
  </section>
28
 
29
- <!-- Cards: Buy & Hold + top 4 agents -->
30
  <section class="panel panel--cards" v-if="cards.length">
31
  <div class="cards5">
32
  <div
@@ -35,7 +35,6 @@
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">
40
  <div class="card__logo">
41
  <img v-if="c.logo" :src="c.logo" alt="" />
@@ -44,11 +43,9 @@
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>
50
 
51
- <!-- meta row -->
52
  <div class="card__meta">
53
  <div class="card__sub ellipsize" :title="c.subtitle">{{ c.subtitle }}</div>
54
 
@@ -64,7 +61,6 @@
64
  </template>
65
  </div>
66
 
67
- <!-- date row -->
68
  <div class="card__footer">
69
  <div class="card__sub">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
70
  </div>
@@ -75,16 +71,23 @@
75
  </template>
76
 
77
  <script setup>
78
- import { ref, computed, onMounted } from 'vue'
79
  import AssetTabs from '../components/AssetTabs.vue'
80
  import CompareChartE from '../components/CompareChartE.vue'
81
  import { dataService } from '../lib/dataService'
82
 
 
 
 
 
 
 
 
83
  /* ---------- config ---------- */
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,
@@ -99,11 +102,16 @@ const ASSET_ICONS = {
99
  TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
100
  }
101
 
 
 
 
102
  /* ---------- state ---------- */
103
  const mode = ref('usd') // 'usd' | 'pct'
104
  const asset = ref('BTC')
105
  const rowsRef = ref([])
106
 
 
 
107
  /* ---------- bootstrap ---------- */
108
  onMounted(async () => {
109
  try {
@@ -115,6 +123,15 @@ onMounted(async () => {
115
  }
116
  rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
117
  if (!orderedAssets.includes(asset.value)) asset.value = orderedAssets[0]
 
 
 
 
 
 
 
 
 
118
  })
119
 
120
  /* ---------- helpers ---------- */
@@ -134,45 +151,7 @@ const filteredRows = computed(() =>
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
- console.log(rowsRef.value)
141
- for (const r of (rowsRef.value || [])) {
142
- if (!r?.asset) continue
143
- const s = (r.strategy || '').toLowerCase()
144
- const an = (r.agent_name || '').toLowerCase()
145
- const m = (r.model || '').toLowerCase()
146
- const isBH = s === 'buy_hold' || s === 'Buy&Hold' || (an.includes('buy') && an.includes('hold')) || (m.includes('buy') && m.includes('hold'))
147
- if (!isBH) continue
148
- const ts = new Date(r.end_date || r.last_nav_ts || 0).getTime()
149
- if (!Number.isFinite(ts)) continue
150
- const list = idx.get(r.asset) || []
151
- list.push({ ts, balance: r.balance, row: r })
152
- idx.set(r.asset, list)
153
- }
154
- for (const [a, list] of idx) list.sort((x, y) => x.ts - y.ts)
155
- return idx
156
- })
157
-
158
- /* Latest B&H for the selected asset (for the B&H card) */
159
- const bhLatest = computed(() => (bhIndex.value.get(asset.value) || []).at(-1) || null)
160
-
161
- /* For an agent date, get the matching (latest ≤ date) B&H balance */
162
- function getBHBalanceAt(assetCode, iso) {
163
- const list = bhIndex.value.get(assetCode)
164
- if (!list || !list.length) return null
165
- const t = new Date(iso || 0).getTime()
166
- if (!Number.isFinite(t)) return list[list.length - 1].balance
167
- let lo = 0, hi = list.length - 1, ans = -1
168
- while (lo <= hi) {
169
- const mid = (lo + hi) >> 1
170
- if (list[mid].ts <= t) { ans = mid; lo = mid + 1 } else { hi = mid - 1 }
171
- }
172
- return ans >= 0 ? list[ans].balance : list[0].balance
173
- }
174
-
175
- /* Best model per agent (by balance) */
176
  const winners = computed(() => {
177
  const byAgent = new Map()
178
  for (const r of filteredRows.value) {
@@ -183,7 +162,7 @@ const winners = computed(() => {
183
  return [...byAgent.values()]
184
  })
185
 
186
- /* Chart selections */
187
  const winnersForChart = computed(() =>
188
  winners.value.map(w => ({
189
  agent_name: w.agent_name,
@@ -194,44 +173,108 @@ const winnersForChart = computed(() =>
194
  }))
195
  )
196
 
197
- /* Cards: B&H + top 4 agents, gaps aligned to same (or latest ≤) EOD */
198
- const cards = computed(() => {
199
- const bhBal = bhLatest.value?.balance ?? 100000
200
- const bhDate = bhLatest.value?.row?.end_date || bhLatest.value?.row?.last_nav_ts || ''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
 
 
 
 
 
 
 
 
202
  const bhCard = {
203
- key: `bh|${asset.value}`,
204
  kind: 'bh',
205
  title: 'Buy & Hold',
206
- subtitle: asset.value,
207
- balance: bhBal,
208
- date: bhDate,
209
- logo: ASSET_ICONS[asset.value] || null,
210
  isWinner: false
211
  }
212
 
213
- const top = [...winners.value].sort((a,b) => score(b) - score(a)).slice(0, 4)
214
- const maxBal = top.length ? Math.max(...top.map(r => r.balance ?? -Infinity)) : -Infinity
215
-
216
- const agentCards = top.map(r => {
217
- const date = r.end_date || r.last_nav_ts || ''
218
- const base = getBHBalanceAt(r.asset, date) // aligned baseline
219
- const gapUsd = (typeof r.balance === 'number' && Number.isFinite(base)) ? (r.balance - base) : null
220
- const gapPct = (gapUsd != null && Number.isFinite(base) && base > 0) ? (r.balance / base - 1) : null
221
  return {
222
- key: `agent|${r.agent_name}|${r.model}`,
223
  kind: 'agent',
224
- title: r.agent_name,
225
- subtitle: r.model,
226
- balance: r.balance,
227
- date,
228
- logo: AGENT_LOGOS[r.agent_name] || null,
229
  gapUsd, gapPct,
230
- isWinner: r.balance === maxBal
231
  }
232
  })
233
 
234
- return [bhCard, ...agentCards]
 
 
 
 
235
  })
236
  </script>
237
 
@@ -239,22 +282,13 @@ const cards = computed(() => {
239
  .live { max-width: 1280px; margin: 0 auto; padding: 12px 20px 28px; }
240
 
241
  /* toolbar */
242
- .toolbar {
243
- position: sticky; top: 0; z-index: 10;
244
- display: flex; align-items: center; justify-content: space-between;
245
- gap: 16px; padding: 8px 0 10px; background: #fff;
246
- }
247
  .toolbar__left { min-width: 0; }
248
  .toolbar__right { display: flex; align-items: center; gap: 10px; }
249
 
250
  /* $/% toggle */
251
  .mode { display: inline-flex; gap: 8px; }
252
- .mode__btn {
253
- height: 32px; min-width: 40px; padding: 0 12px;
254
- border-radius: 10px; border: 1px solid #D6DAE1;
255
- background: #fff; font-weight: 700; color: #0F172A;
256
- transition: all .12s ease;
257
- }
258
  .mode__btn:hover { transform: translateY(-1px); }
259
  .mode__btn.is-active { background: #0F172A; color: #fff; border-color: #0F172A; }
260
 
@@ -269,14 +303,10 @@ const cards = computed(() => {
269
  /* 5 cards in a row */
270
  .cards5 { display: grid; gap: 12px; grid-template-columns: repeat(5, minmax(0, 1fr)); }
271
  @media (max-width: 1200px) { .cards5 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
272
- @media (max-width: 720px) { .cards5 { grid-template-columns: 1fr; } }
273
 
274
  /* card */
275
- .card {
276
- display: grid; grid-template-rows: auto auto auto; gap: 8px;
277
- padding: 12px 14px; border: 1px solid #EEF1F6; border-radius: 14px;
278
- background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.04); position: relative;
279
- }
280
  .card--bh { outline: 2px dashed rgba(15,23,42,.08); }
281
  .card--winner { border-color: #16a34a; box-shadow: 0 0 0 3px rgba(22,163,74,.12); }
282
 
@@ -292,8 +322,7 @@ const cards = computed(() => {
292
  /* title: clamp to 2 lines so balance never overlaps */
293
  .card__title {
294
  min-width: 0; font-weight: 800; color: #0F172A;
295
- white-space: normal;
296
- display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
297
  overflow: hidden; line-height: 1.15;
298
  }
299
 
 
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="cards5">
32
  <div
 
35
  class="card"
36
  :class="{ 'card--bh': c.kind==='bh', 'card--winner': c.isWinner }"
37
  >
 
38
  <div class="card__header">
39
  <div class="card__logo">
40
  <img v-if="c.logo" :src="c.logo" alt="" />
 
43
  </div>
44
 
45
  <div class="card__title" :title="c.title">{{ c.title }}</div>
 
46
  <div class="card__balance">{{ fmtUSD(c.balance) }}</div>
47
  </div>
48
 
 
49
  <div class="card__meta">
50
  <div class="card__sub ellipsize" :title="c.subtitle">{{ c.subtitle }}</div>
51
 
 
61
  </template>
62
  </div>
63
 
 
64
  <div class="card__footer">
65
  <div class="card__sub">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
66
  </div>
 
71
  </template>
72
 
73
  <script setup>
74
+ import { ref, computed, onMounted, watchEffect } from 'vue'
75
  import AssetTabs from '../components/AssetTabs.vue'
76
  import CompareChartE from '../components/CompareChartE.vue'
77
  import { dataService } from '../lib/dataService'
78
 
79
+ /* —— use the same helpers as the chart —— */
80
+ import { getAllDecisions } from '../lib/dataCache'
81
+ import { readAllRawDecisions } from '../lib/idb'
82
+ import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
83
+ import { STRATEGIES } from '../lib/strategies'
84
+ import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
85
+
86
  /* ---------- config ---------- */
87
+ const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA'] // (MRNA removed)
88
  const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla']) // case-insensitive
89
 
90
+ // optional logos
91
  const AGENT_LOGOS = {
92
  // 'DeepFundAgent': new URL('../assets/images/agents/deepfund.png', import.meta.url).href,
93
  // 'InvestorAgent': new URL('../assets/images/agents/investor.png', import.meta.url).href,
 
102
  TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
103
  }
104
 
105
+ /* match the chart’s cutoff so numbers align */
106
+ const ASSET_CUTOFF = { BTC: '2025-08-01' }
107
+
108
  /* ---------- state ---------- */
109
  const mode = ref('usd') // 'usd' | 'pct'
110
  const asset = ref('BTC')
111
  const rowsRef = ref([])
112
 
113
+ let allDecisions = [] // in-memory decisions for perf calc
114
+
115
  /* ---------- bootstrap ---------- */
116
  onMounted(async () => {
117
  try {
 
123
  }
124
  rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
125
  if (!orderedAssets.includes(asset.value)) asset.value = orderedAssets[0]
126
+
127
+ // pull the same decisions cache the chart uses
128
+ allDecisions = getAllDecisions() || []
129
+ if (!allDecisions.length) {
130
+ try {
131
+ const cached = await readAllRawDecisions()
132
+ if (cached?.length) allDecisions = cached
133
+ } catch {}
134
+ }
135
  })
136
 
137
  /* ---------- helpers ---------- */
 
151
  })
152
  )
153
 
154
+ /* Best model per agent (by balance) still from leaderboard rows for picking winners */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  const winners = computed(() => {
156
  const byAgent = new Map()
157
  for (const r of filteredRows.value) {
 
162
  return [...byAgent.values()]
163
  })
164
 
165
+ /* Chart selections built from winners */
166
  const winnersForChart = computed(() =>
167
  winners.value.map(w => ({
168
  agent_name: w.agent_name,
 
173
  }))
174
  )
175
 
176
+ /* ---------- PERF: compute B&H + strategy the same way as the chart ---------- */
177
+
178
+ /** build the ordered decision seq for a selection (same logic as chart) */
179
+ function buildSeq(sel) {
180
+ const { agent_name: agent, asset, model } = sel
181
+ const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
182
+ let seq = ids.length
183
+ ? allDecisions.filter(r => ids.includes(r.id))
184
+ : allDecisions.filter(r => r.agent_name === agent && r.asset === asset && r.model === model)
185
+
186
+ seq.sort((a, b) => (a.date > b.date ? 1 : -1))
187
+
188
+ const isCrypto = asset === 'BTC' || asset === 'ETH'
189
+ let filtered = isCrypto ? seq : filterRowsToNyseTradingDays(seq)
190
+
191
+ const cutoff = ASSET_CUTOFF[asset]
192
+ if (cutoff) {
193
+ const t0 = new Date(cutoff + 'T00:00:00Z')
194
+ filtered = filtered.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
195
+ }
196
+ return filtered
197
+ }
198
+
199
+ /** compute final equity & aligned B&H for a selection */
200
+ function computeEquities(sel) {
201
+ const seq = buildSeq(sel)
202
+ if (!seq.length) return null
203
+
204
+ // strategy params (mirror CompareChartE)
205
+ const cfg =
206
+ (STRATEGIES || []).find(s => s.id === sel.strategy) ||
207
+ { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
208
+
209
+ const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
210
+ const bhY = computeBuyHoldEquity(seq, 100000) || []
211
+
212
+ const lastIdx = Math.min(stratY.length, bhY.length) - 1
213
+ if (lastIdx < 0) return null
214
+
215
+ return {
216
+ date: seq[lastIdx].date,
217
+ stratLast: stratY[lastIdx],
218
+ bhLast: bhY[lastIdx],
219
+ seq, stratY, bhY
220
+ }
221
+ }
222
+
223
+ /* compute cards from perf (no leaderboard math) */
224
+ const cards = ref([])
225
+
226
+ watchEffect(() => {
227
+ if (!winnersForChart.value.length) {
228
+ cards.value = []
229
+ return
230
+ }
231
+
232
+ // compute perf for each winner
233
+ const perfs = winnersForChart.value
234
+ .map(sel => ({ sel, perf: computeEquities(sel) }))
235
+ .filter(x => x.perf)
236
 
237
+ if (!perfs.length) {
238
+ cards.value = []
239
+ return
240
+ }
241
+
242
+ // Buy & Hold card: use the first winner’s BH last for the asset
243
+ const first = perfs[0]
244
+ const assetCode = first.sel.asset
245
  const bhCard = {
246
+ key: `bh|${assetCode}`,
247
  kind: 'bh',
248
  title: 'Buy & Hold',
249
+ subtitle: assetCode,
250
+ balance: first.perf.bhLast,
251
+ date: first.perf.date,
252
+ logo: ASSET_ICONS[assetCode] || null,
253
  isWinner: false
254
  }
255
 
256
+ // agent cards and winner flag
257
+ const agentCards = perfs.map(({ sel, perf }) => {
258
+ const gapUsd = perf.stratLast - perf.bhLast
259
+ const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
 
 
 
 
260
  return {
261
+ key: `agent|${sel.agent_name}|${sel.model}`,
262
  kind: 'agent',
263
+ title: sel.agent_name,
264
+ subtitle: sel.model,
265
+ balance: perf.stratLast,
266
+ date: perf.date,
267
+ logo: AGENT_LOGOS[sel.agent_name] || null,
268
  gapUsd, gapPct,
269
+ isWinner: false // set below
270
  }
271
  })
272
 
273
+ const maxBal = Math.max(...agentCards.map(c => c.balance ?? -Infinity))
274
+ agentCards.forEach(c => { c.isWinner = c.balance === maxBal })
275
+
276
+ // top 4 agents + BH card
277
+ cards.value = [bhCard, ...agentCards.sort((a,b) => b.balance - a.balance).slice(0,4)]
278
  })
279
  </script>
280
 
 
282
  .live { max-width: 1280px; margin: 0 auto; padding: 12px 20px 28px; }
283
 
284
  /* toolbar */
285
+ .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; }
 
 
 
 
286
  .toolbar__left { min-width: 0; }
287
  .toolbar__right { display: flex; align-items: center; gap: 10px; }
288
 
289
  /* $/% toggle */
290
  .mode { display: inline-flex; gap: 8px; }
291
+ .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; }
 
 
 
 
 
292
  .mode__btn:hover { transform: translateY(-1px); }
293
  .mode__btn.is-active { background: #0F172A; color: #fff; border-color: #0F172A; }
294
 
 
303
  /* 5 cards in a row */
304
  .cards5 { display: grid; gap: 12px; grid-template-columns: repeat(5, minmax(0, 1fr)); }
305
  @media (max-width: 1200px) { .cards5 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
306
+ @media (max-width: 720px) { .cards5 { grid-template-columns: 1fr; } }
307
 
308
  /* card */
309
+ .card { display: grid; grid-template-rows: auto auto auto; gap: 8px; padding: 12px 14px; border: 1px solid #EEF1F6; border-radius: 14px; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.04); position: relative; }
 
 
 
 
310
  .card--bh { outline: 2px dashed rgba(15,23,42,.08); }
311
  .card--winner { border-color: #16a34a; box-shadow: 0 0 0 3px rgba(22,163,74,.12); }
312
 
 
322
  /* title: clamp to 2 lines so balance never overlaps */
323
  .card__title {
324
  min-width: 0; font-weight: 800; color: #0F172A;
325
+ white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
 
326
  overflow: hidden; line-height: 1.15;
327
  }
328