Jimin Huang commited on
Commit
3b104a4
·
1 Parent(s): d7db594

Change settings

Browse files
Files changed (1) hide show
  1. src/views/LiveView.vue +221 -103
src/views/LiveView.vue CHANGED
@@ -3,7 +3,6 @@
3
  <!-- Toolbar: assets + mode -->
4
  <header class="toolbar">
5
  <div class="toolbar__left">
6
- <!-- If AssetTabs supports restricting options, this will remove MRNA from the UI -->
7
  <AssetTabs v-model="asset" :ordered-assets="orderedAssets" />
8
  </div>
9
  <div class="toolbar__right">
@@ -24,7 +23,7 @@
24
  </div>
25
  </header>
26
 
27
- <!-- Chart panel -->
28
  <section class="panel panel--chart">
29
  <CompareChartE
30
  v-if="winnersForChart.length"
@@ -37,25 +36,52 @@
37
  </div>
38
  </section>
39
 
40
- <!-- Winner cards -->
41
- <section v-if="winnersSorted.length" class="panel panel--cards">
42
- <div class="cards">
43
  <div
44
- v-for="row in winnersSorted"
45
- :key="row.agent_name+'|'+row.asset+'|'+row.model"
46
- class="card">
47
- <div class="card__left">
48
- <div class="card__title" :title="row.agent_name">{{ row.agent_name }}</div>
49
- <div class="card__sub" :title="row.model">{{ row.model }}</div>
 
 
50
  </div>
51
- <div class="card__right">
52
- <div class="card__balance">{{ fmtUSD(row.balance) }}</div>
53
- <div class="card__sub">
54
- EOD {{
55
- row.end_date
56
- ? new Date(row.end_date).toLocaleDateString()
57
- : (row.last_nav_ts ? new Date(row.last_nav_ts).toLocaleDateString() : '-')
58
- }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  </div>
60
  </div>
61
  </div>
@@ -65,21 +91,42 @@
65
  </template>
66
 
67
  <script setup>
68
- import { ref, computed, onMounted } from 'vue'
69
  import AssetTabs from '../components/AssetTabs.vue'
70
  import CompareChartE from '../components/CompareChartE.vue'
71
  import { dataService } from '../lib/dataService'
 
 
 
 
72
 
73
- // ---- config: hide MRNA in tabs; drop Vanilla/Vinilla agents globally ----
74
- const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA'] // MRNA removed
75
- const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla']) // case-insensitive match
76
 
77
- // ---- state ----
78
- const mode = ref('usd') // 'usd' | 'pct'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  const asset = ref('BTC')
80
- const agents = ref([])
 
 
81
 
82
- // ---- load once, copy rows from dataService ----
83
  onMounted(async () => {
84
  try {
85
  if (!dataService.loaded && !dataService.loading) {
@@ -88,35 +135,59 @@ onMounted(async () => {
88
  } catch (e) {
89
  console.error('LiveView: dataService.load failed', e)
90
  }
91
- // Copy rows; we’ll filter “vanilla/vinilla” in computed getters below
92
- agents.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
 
 
 
93
 
94
- // Safety: if current asset is MRNA (e.g., from persisted state), push to first allowed
95
- if (!orderedAssets.includes(asset.value)) {
96
- asset.value = orderedAssets[0]
 
 
 
97
  }
98
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
- // ---- helpers ----
101
  function score(row) {
102
  return typeof row.balance === 'number' ? row.balance : -Infinity
103
  }
104
- const fmtUSD = (n) =>
105
- (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
106
-
107
- const filteredRows = computed(() => {
108
- // exclude agents named vanilla/vinilla, case-insensitive
109
- return (agents.value || []).filter(r => {
110
  const name = (r?.agent_name || '').toLowerCase()
111
  return !EXCLUDED_AGENT_NAMES.has(name)
112
  })
113
- })
114
 
115
- // Best model per agent for the selected asset (by balance)
116
  const winners = computed(() => {
117
- const rows = filteredRows.value.filter(r => r.asset === asset.value)
118
  const byAgent = new Map()
119
- for (const r of rows) {
120
  const k = r.agent_name
121
  const cur = byAgent.get(k)
122
  if (!cur || score(r) > score(cur)) byAgent.set(k, r)
@@ -124,7 +195,7 @@ const winners = computed(() => {
124
  return [...byAgent.values()]
125
  })
126
 
127
- // Series selections for the chart
128
  const winnersForChart = computed(() =>
129
  winners.value.map(w => ({
130
  agent_name: w.agent_name,
@@ -135,19 +206,65 @@ const winnersForChart = computed(() =>
135
  }))
136
  )
137
 
138
- // Bottom cards sorted by balance
139
- const winnersSorted = computed(() => [...winners.value].sort((a,b) => score(b) - score(a)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  </script>
141
 
142
  <style scoped>
143
- /* page container */
144
  .live {
145
  max-width: 1280px;
146
  margin: 0 auto;
147
  padding: 12px 20px 28px;
148
  }
149
 
150
- /* sticky toolbar */
151
  .toolbar {
152
  position: sticky;
153
  top: 0;
@@ -162,84 +279,85 @@ const winnersSorted = computed(() => [...winners.value].sort((a,b) => score(b) -
162
  .toolbar__left { min-width: 0; }
163
  .toolbar__right { display: flex; align-items: center; gap: 10px; }
164
 
165
- /* $ / % toggle */
166
  .mode { display: inline-flex; gap: 8px; }
167
  .mode__btn {
168
- height: 32px;
169
- min-width: 40px;
170
- padding: 0 12px;
171
- border-radius: 10px;
172
- border: 1px solid #D6DAE1;
173
- background: #fff;
174
- font-weight: 700;
175
- color: #0F172A;
176
  transition: all .12s ease;
177
  }
178
  .mode__btn:hover { transform: translateY(-1px); }
179
- .mode__btn.is-active {
180
- background: #0F172A;
181
- color: #fff;
182
- border-color: #0F172A;
183
- }
184
 
185
  /* panels */
186
- .panel {
187
- background: #fff;
188
- border: 1px solid #EDF0F4;
189
- border-radius: 14px;
190
- }
191
  .panel--chart { padding: 10px 10px 2px; }
192
  .panel--cards { padding: 12px; }
193
 
194
- /* empty state */
195
  .empty {
196
- padding: 14px;
197
- border: 1px dashed #D6DAE1;
198
- border-radius: 12px;
199
- color: #6B7280;
200
- font-size: .92rem;
201
  }
202
 
203
- /* cards grid */
204
- .cards {
205
  display: grid;
206
  gap: 12px;
207
- grid-template-columns: repeat(1, minmax(0, 1fr));
 
 
 
 
 
 
208
  }
209
- @media (min-width: 640px) { .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
210
- @media (min-width: 1024px) { .cards { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
211
- @media (min-width: 1280px) { .cards { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
212
 
213
  /* card */
214
  .card {
215
  display: grid;
216
- grid-template-columns: 1fr auto;
217
- align-items: center;
218
  gap: 10px 12px;
219
- padding: 14px 16px;
220
- border: 1px solid #EEF1F6;
221
- border-radius: 14px;
222
- background: #fff;
223
- box-shadow: 0 1px 2px rgba(0,0,0,.04);
224
  }
225
- .card__title {
226
- font-weight: 700;
227
- color: #0F172A;
228
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
 
 
 
 
 
 
 
 
 
 
 
229
  }
230
- .card__sub {
231
- font-size: 12px;
232
- color: #5B6476;
233
- opacity: .8;
234
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
235
  }
236
- .card__balance {
237
- font-weight: 800;
238
- font-size: 18px;
239
- line-height: 1.1;
240
- color: #0F172A;
241
- text-align: right;
 
 
 
242
  }
243
- .card__left { min-width: 0; }
244
- .card__right { display: grid; gap: 4px; justify-items: end; }
 
 
 
 
245
  </style>
 
3
  <!-- Toolbar: assets + mode -->
4
  <header class="toolbar">
5
  <div class="toolbar__left">
 
6
  <AssetTabs v-model="asset" :ordered-assets="orderedAssets" />
7
  </div>
8
  <div class="toolbar__right">
 
23
  </div>
24
  </header>
25
 
26
+ <!-- Chart -->
27
  <section class="panel panel--chart">
28
  <CompareChartE
29
  v-if="winnersForChart.length"
 
36
  </div>
37
  </section>
38
 
39
+ <!-- Cards: Buy & Hold + top 4 agents = 5 in a row -->
40
+ <section class="panel panel--cards" v-if="cards.length">
41
+ <div class="cards5">
42
  <div
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="gap pill" :class="{ neg: c.gapUsd < 0 }">
70
+ <span>{{ signedMoney(c.gapUsd) }}</span>
71
+ <span class="muted"> ({{ signedPct(c.gapPct) }})</span>
72
+ </div>
73
+ </template>
74
+
75
+ <template v-else>
76
+ <div class="bh 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>
 
91
  </template>
92
 
93
  <script setup>
94
+ import { ref, computed, onMounted, watch } from 'vue'
95
  import AssetTabs from '../components/AssetTabs.vue'
96
  import CompareChartE from '../components/CompareChartE.vue'
97
  import { dataService } from '../lib/dataService'
98
+ import { getAllDecisions } from '../lib/dataCache'
99
+ import { readAllRawDecisions } from '../lib/idb'
100
+ import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
101
+ import { computeBuyHoldEquity } from '../lib/perf'
102
 
103
+ // ------- config -------
104
+ const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA'] // MRNA removed
105
+ const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla']) // case-insensitive
106
 
107
+ // Logos (hook up your real paths)
108
+ const AGENT_LOGOS = {
109
+ // 'DeepFundAgent': new URL('../assets/images/agents/deepfund.png', import.meta.url).href,
110
+ // 'InvestorAgent': new URL('../assets/images/agents/investor.png', import.meta.url).href,
111
+ // 'TradeAgent': new URL('../assets/images/agents/trade.png', import.meta.url).href,
112
+ // 'HedgeFundAgent': new URL('../assets/images/agents/hedge.png', import.meta.url).href,
113
+ }
114
+ const ASSET_ICONS = {
115
+ BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
116
+ ETH: new URL('../assets/images/assets_images/ETH.png', import.meta.url).href,
117
+ MSFT: new URL('../assets/images/assets_images/MSFT.png', import.meta.url).href,
118
+ BMRN: new URL('../assets/images/assets_images/BMRN.png', import.meta.url).href,
119
+ TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
120
+ }
121
+
122
+ // ------- state -------
123
+ const mode = ref('usd')
124
  const asset = ref('BTC')
125
+ const rowsRef = ref([])
126
+ const bhBalance = ref(null) // baseline Buy & Hold final balance
127
+ const bhDate = ref('')
128
 
129
+ // load table rows once
130
  onMounted(async () => {
131
  try {
132
  if (!dataService.loaded && !dataService.loading) {
 
135
  } catch (e) {
136
  console.error('LiveView: dataService.load failed', e)
137
  }
138
+ rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
139
+ })
140
+
141
+ // recompute Buy&Hold when asset changes
142
+ watch(asset, async () => { await recomputeBH() }, { immediate: true })
143
 
144
+ async function recomputeBH(){
145
+ // Build a clean sequence of decisions for the selected asset, then compute Buy&Hold equity.
146
+ // We'll reuse the longest sequence among available decisions for that asset.
147
+ let all = getAllDecisions() || []
148
+ if (!all.length) {
149
+ try { const cached = await readAllRawDecisions(); if (cached?.length) all = cached } catch {}
150
  }
151
+ let seq = all.filter(r => r.asset === asset.value)
152
+ seq.sort((a,b) => (a.date > b.date ? 1 : -1))
153
+
154
+ // Filter to trading days (NYSE for equities; pass-through for BTC/ETH)
155
+ const isCrypto = asset.value === 'BTC' || asset.value === 'ETH'
156
+ if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
157
+
158
+ const bh = computeBuyHoldEquity(seq, 100000) || []
159
+ if (bh.length) {
160
+ bhBalance.value = bh[bh.length - 1]
161
+ bhDate.value = seq[seq.length - 1]?.date || ''
162
+ } else {
163
+ bhBalance.value = null
164
+ bhDate.value = ''
165
+ }
166
+ }
167
+
168
+ // helpers
169
+ const fmtUSD = (n) => (n ?? 0).toLocaleString(
170
+ undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 }
171
+ )
172
+ const signedMoney = (n) => `${n >= 0 ? '+' : ''}${fmtUSD(Math.abs(n))}`
173
+ const signedPct = (p) =>
174
+ `${(p >= 0 ? '+' : '')}${Number(p * 100).toFixed(2)}%`
175
 
 
176
  function score(row) {
177
  return typeof row.balance === 'number' ? row.balance : -Infinity
178
  }
179
+ const filteredRows = computed(() =>
180
+ (rowsRef.value || []).filter(r => {
181
+ if (r.asset !== asset.value) return false
 
 
 
182
  const name = (r?.agent_name || '').toLowerCase()
183
  return !EXCLUDED_AGENT_NAMES.has(name)
184
  })
185
+ )
186
 
187
+ // winners: best per agent (by balance)
188
  const winners = computed(() => {
 
189
  const byAgent = new Map()
190
+ for (const r of filteredRows.value) {
191
  const k = r.agent_name
192
  const cur = byAgent.get(k)
193
  if (!cur || score(r) > score(cur)) byAgent.set(k, r)
 
195
  return [...byAgent.values()]
196
  })
197
 
198
+ // chart selections
199
  const winnersForChart = computed(() =>
200
  winners.value.map(w => ({
201
  agent_name: w.agent_name,
 
206
  }))
207
  )
208
 
209
+ // build 5 cards: Buy & Hold + top 4 agents
210
+ const cards = computed(() => {
211
+ const base = Number(bhBalance.value ?? 100000)
212
+ const baseDate = bhDate.value
213
+
214
+ // Buy & Hold card
215
+ const bhCard = {
216
+ key: `bh|${asset.value}`,
217
+ kind: 'bh',
218
+ title: 'Buy & Hold',
219
+ subtitle: asset.value,
220
+ balance: base,
221
+ date: baseDate,
222
+ logo: ASSET_ICONS[asset.value] || null,
223
+ }
224
+
225
+ // Top 4 agents
226
+ const top = [...winners.value].sort((a,b) => score(b) - score(a)).slice(0, 4)
227
+ const agentCards = top.map(r => {
228
+ const gapUsd = (typeof r.balance === 'number' && Number.isFinite(base))
229
+ ? (r.balance - base)
230
+ : 0
231
+ const gapPct = (Number.isFinite(base) && base > 0)
232
+ ? (r.balance / base - 1)
233
+ : 0
234
+
235
+ return {
236
+ key: `agent|${r.agent_name}|${r.model}`,
237
+ kind: 'agent',
238
+ title: r.agent_name,
239
+ subtitle: r.model,
240
+ balance: r.balance,
241
+ date: r.end_date || r.last_nav_ts || '',
242
+ logo: AGENT_LOGOS[r.agent_name] || null,
243
+ gapUsd, gapPct,
244
+ }
245
+ })
246
+
247
+ return [bhCard, ...agentCards]
248
+ })
249
+
250
+ // dynamic font size for long names
251
+ function nameFontSize(name='') {
252
+ const len = name.length
253
+ if (len <= 14) return '20px'
254
+ if (len <= 20) return '18px'
255
+ if (len <= 26) return '16px'
256
+ return '15px'
257
+ }
258
  </script>
259
 
260
  <style scoped>
 
261
  .live {
262
  max-width: 1280px;
263
  margin: 0 auto;
264
  padding: 12px 20px 28px;
265
  }
266
 
267
+ /* toolbar */
268
  .toolbar {
269
  position: sticky;
270
  top: 0;
 
279
  .toolbar__left { min-width: 0; }
280
  .toolbar__right { display: flex; align-items: center; gap: 10px; }
281
 
282
+ /* $/% toggle */
283
  .mode { display: inline-flex; gap: 8px; }
284
  .mode__btn {
285
+ height: 32px; min-width: 40px; padding: 0 12px;
286
+ border-radius: 10px; border: 1px solid #D6DAE1;
287
+ background: #fff; font-weight: 700; color: #0F172A;
 
 
 
 
 
288
  transition: all .12s ease;
289
  }
290
  .mode__btn:hover { transform: translateY(-1px); }
291
+ .mode__btn.is-active { background: #0F172A; color: #fff; border-color: #0F172A; }
 
 
 
 
292
 
293
  /* panels */
294
+ .panel { background: #fff; border: 1px solid #EDF0F4; border-radius: 14px; }
 
 
 
 
295
  .panel--chart { padding: 10px 10px 2px; }
296
  .panel--cards { padding: 12px; }
297
 
298
+ /* empty */
299
  .empty {
300
+ padding: 14px; border: 1px dashed #D6DAE1; border-radius: 12px;
301
+ color: #6B7280; font-size: .92rem;
 
 
 
302
  }
303
 
304
+ /* 5 cards in a row */
305
+ .cards5 {
306
  display: grid;
307
  gap: 12px;
308
+ grid-template-columns: repeat(5, minmax(0, 1fr));
309
+ }
310
+ @media (max-width: 1200px) {
311
+ .cards5 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
312
+ }
313
+ @media (max-width: 720px) {
314
+ .cards5 { grid-template-columns: 1fr; }
315
  }
 
 
 
316
 
317
  /* card */
318
  .card {
319
  display: grid;
320
+ grid-template-columns: 52px 1fr;
 
321
  gap: 10px 12px;
322
+ align-items: center;
323
+ padding: 12px 14px;
324
+ border: 1px solid #EEF1F6; border-radius: 14px;
325
+ background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.04);
 
326
  }
327
+ .card--bh { outline: 2px dashed rgba(15,23,42,.08); }
328
+
329
+ /* logo */
330
+ .card__logo {
331
+ width: 44px; height: 44px; border-radius: 999px;
332
+ background: #F3F4F6; display: grid; place-items: center;
333
+ overflow: hidden;
334
+ }
335
+ .card__logo img { width: 100%; height: 100%; object-fit: contain; }
336
+ .card__logo-fallback { width: 60%; height: 60%; border-radius: 999px; background: #E5E7EB; }
337
+
338
+ /* content */
339
+ .card__content { min-width: 0; }
340
+ .card__row {
341
+ display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
342
  }
343
+ .card__title {
344
+ font-weight: 800; color: #0F172A;
 
 
345
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
346
  }
347
+ .card__balance { font-weight: 900; color: #0F172A; font-size: 20px; }
348
+ .card__sub { font-size: 12px; color: #5B6476; opacity: .85; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
349
+ .card__meta { margin-top: 4px; align-items: center; }
350
+ .card__date { margin-top: 2px; }
351
+
352
+ .pill {
353
+ padding: 3px 8px; border-radius: 999px; font-size: 12px;
354
+ font-weight: 700; line-height: 1; white-space: nowrap;
355
+ background: #EEF2F7; color: #0F172A;
356
  }
357
+ .pill.neg { background: #FEE2E2; color: #B91C1C; }
358
+ .pill .muted { opacity: .7; }
359
+
360
+ /* utility */
361
+ .spacer { flex: 1 1 auto; }
362
+ strong { font-weight: 700; }
363
  </style>