Jimin Huang commited on
Commit
9821537
Β·
1 Parent(s): 68e4a57

Change settings

Browse files
Files changed (1) hide show
  1. src/views/LiveView.vue +202 -62
src/views/LiveView.vue CHANGED
@@ -14,52 +14,44 @@
14
 
15
  <section class="panel panel--chart">
16
  <CompareChartE v-if="winnersForChart.length" :selected="winnersForChart" :visible="true" :mode="mode" />
17
- <div v-else class="empty">No data for <strong>{{ asset }}</strong> yet.</div>
18
  </section>
19
 
20
  <section class="panel panel--cards" v-if="cards.length">
21
- <div class="cards-grid-f1">
22
- <article v-for="c in cards" :key="c.key" class="card-f1" :class="{ winner: c.isWinner, bh: c.kind==='bh' }" :style="{ '--bar': (c.barPct ?? 0) + '%' }">
23
- <div v-if="c.rank" class="rank">P{{ c.rank }}</div>
24
- <div v-else-if="c.kind==='bh'" class="rank bh-badge">B&H</div>
25
- <span v-if="c.isWinner" class="crown">πŸ‘‘</span>
26
-
27
- <header class="head">
28
- <div class="logo">
29
  <img v-if="c.logo" :src="c.logo" alt="" />
30
- <div v-else class="logo__fallback"></div>
 
31
  </div>
32
- <div class="names">
33
- <div class="agent">{{ c.kind==='bh' ? 'Buy & Hold' : c.title }}</div>
34
- <div class="model">{{ c.subtitle }}</div>
35
- </div>
36
- </header>
37
 
38
- <div class="net">
39
- <div class="net__label">Net value</div>
40
- <div class="net__value">{{ fmtUSD(c.balance) }}</div>
41
- </div>
42
 
43
- <div class="bar" :class="{ neg: (c.gapUsd ?? 0) < 0, pos: (c.gapUsd ?? 0) >= 0 }">
44
- <span :style="{ width: (c.barPct ?? 0) + '%' }"></span>
45
  </div>
46
 
47
- <div class="bottom">
48
- <div class="chips">
49
- <span class="chip" :class="{ pos: profitOf(c) >= 0, neg: profitOf(c) < 0 }">{{ signedMoney(profitOf(c)) }}</span>
50
- <span class="chip" :class="{ pos: (c.gapUsd ?? 0) >= 0, neg: (c.gapUsd ?? 0) < 0 }">
51
- <template v-if="c.kind==='agent'">
52
- <template v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</template>
53
- <template v-else>{{ signedPct(c.gapPct) }}</template>
54
- </template>
55
- <template v-else>β€”</template>
56
- </span>
57
  </div>
58
- <div class="footer-row">
59
- <div class="eod">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
 
 
 
60
  </div>
61
  </div>
62
- </article>
 
 
 
 
63
  </div>
64
  </section>
65
 
@@ -81,6 +73,8 @@ import { STRATEGIES } from '../lib/strategies'
81
  import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
82
 
83
  const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA']
 
 
84
  const ASSET_ICONS = {
85
  BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
86
  ETH: new URL('../assets/images/assets_images/ETH.png', import.meta.url).href,
@@ -88,6 +82,9 @@ const ASSET_ICONS = {
88
  BMRN: new URL('../assets/images/assets_images/BMRN.png', import.meta.url).href,
89
  TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
90
  }
 
 
 
91
  const mode = ref('usd')
92
  const asset = ref('BTC')
93
  const rowsRef = ref([])
@@ -96,39 +93,182 @@ const cards = shallowRef([])
96
 
97
  onMounted(async () => {
98
  try {
99
- if (!dataService.loaded && !dataService.loading) await dataService.load(false)
100
- } catch (e) { console.error(e) }
101
- rowsRef.value = dataService.tableRows || []
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  })
103
 
104
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
105
  const signedMoney = (n) => `${n >= 0 ? '+' : 'βˆ’'}${fmtUSD(Math.abs(n))}`
106
- const signedPct = (p) => `${p >= 0 ? '+' : 'βˆ’'}${Math.abs(p * 100).toFixed(2)}%`
107
- const profitOf = (c) => (typeof c?.profitUsd === 'number' ? c.profitUsd : ((c?.balance ?? 0) - 100000))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  </script>
109
 
110
  <style scoped>
111
- .live { background: #ffffff; max-width: 1280px; margin: 0 auto; padding: 12px 20px 28px; }
112
- .toolbar { background: #ffffff; }
113
- .cards-grid-f1 { display: grid; gap: 12px; grid-template-columns: repeat(5, minmax(0,1fr)); }
114
- .card-f1 { background: #fff; border: 1px solid #E7ECF3; border-radius: 14px; box-shadow: 0 1px 2px rgba(16,24,40,.05); padding: 16px; display: flex; flex-direction: column; justify-content: space-between; }
115
- .net__value { font-size: clamp(18px, 2vw, 26px); font-weight: 900; color: #0f172a; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
116
- .bottom { display: flex; flex-direction: column; gap: 6px; }
117
- .footer-row { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #6b7280; margin-top: 4px; }
118
- /* Greyed B&H to de-emphasize */
119
- .card-f1.bh {
120
- opacity: .58;
121
- filter: grayscale(1) saturate(.2);
122
- border-style: dashed;
123
- border-color: #E5E7EB;
124
- box-shadow: none;
125
- }
126
- .card-f1.bh .agent,
127
- .card-f1.bh .model,
128
- .card-f1.bh .net__label,
129
- .card-f1.bh .net__value,
130
- .card-f1.bh .chip,
131
- .card-f1.bh .eod { color: #9AA4B2; }
132
- .card-f1.bh .chip { background: #F1F5F9; border-color: #E2E8F0; }
133
- .card-f1.bh .logo { background: #F5F7FA; border-color: #E5E7EB; }
 
 
 
 
 
134
  </style>
 
14
 
15
  <section class="panel panel--chart">
16
  <CompareChartE v-if="winnersForChart.length" :selected="winnersForChart" :visible="true" :mode="mode" />
17
+ <div v-else class="empty">No data for <strong>{{ asset }}</strong> yet. Check Supabase runs or try another asset.</div>
18
  </section>
19
 
20
  <section class="panel panel--cards" v-if="cards.length">
21
+ <div class="cards5">
22
+ <div v-for="c in cards" :key="c.key" class="card" :class="{ 'card--bh': c.kind==='bh', 'card--winner': c.isWinner }">
23
+ <div class="card__header">
24
+ <div class="card__logo">
 
 
 
 
25
  <img v-if="c.logo" :src="c.logo" alt="" />
26
+ <div v-else class="card__logo-fallback"></div>
27
+ <span v-if="c.isWinner" class="card__badge" aria-label="Top performer">πŸ‘‘</span>
28
  </div>
 
 
 
 
 
29
 
30
+ <div class="card__title-wrap">
31
+ <div class="card__title" :title="c.title">{{ c.title }}</div>
32
+ <div class="card__subtitle" :title="c.subtitle">{{ c.subtitle }}</div>
33
+ </div>
34
 
35
+ <div class="card__balance">{{ fmtUSD(c.balance) }}</div>
 
36
  </div>
37
 
38
+ <div class="card__meta">
39
+ <div class="meta__left">
40
+ <div v-if="c.kind==='bh'" class="pill pill--neutral">Buy&nbsp;&amp;&nbsp;Hold</div>
41
+ <div v-else class="pill pill--outline">Strategy</div>
 
 
 
 
 
 
42
  </div>
43
+ <div class="meta__right" v-if="c.kind==='agent' && c.gapUsd != null">
44
+ <div class="pill" :class="{ neg: c.gapUsd < 0 && !c.isWinner, pos: c.gapUsd >= 0 || c.isWinner }">
45
+ <span v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</span>
46
+ <span v-else>{{ signedPct(c.gapPct) }}</span>
47
+ </div>
48
  </div>
49
  </div>
50
+
51
+ <div class="card__footer">
52
+ <div class="card__foot">EOD {{ c.date ? new Date(c.date).toLocaleDateString() : '–' }}</div>
53
+ </div>
54
+ </div>
55
  </div>
56
  </section>
57
 
 
73
  import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
74
 
75
  const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA']
76
+ const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla'])
77
+
78
  const ASSET_ICONS = {
79
  BTC: new URL('../assets/images/assets_images/BTC.png', import.meta.url).href,
80
  ETH: new URL('../assets/images/assets_images/ETH.png', import.meta.url).href,
 
82
  BMRN: new URL('../assets/images/assets_images/BMRN.png', import.meta.url).href,
83
  TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
84
  }
85
+ const AGENT_LOGOS = {}
86
+ const ASSET_CUTOFF = { BTC: '2025-08-01' }
87
+
88
  const mode = ref('usd')
89
  const asset = ref('BTC')
90
  const rowsRef = ref([])
 
93
 
94
  onMounted(async () => {
95
  try {
96
+ if (!dataService.loaded && !dataService.loading) {
97
+ await dataService.load(false)
98
+ }
99
+ } catch (e) {
100
+ console.error('LiveView: dataService.load failed', e)
101
+ }
102
+ rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
103
+ if (!orderedAssets.includes(asset.value)) asset.value = orderedAssets[0]
104
+
105
+ allDecisions = getAllDecisions() || []
106
+ if (!allDecisions.length) {
107
+ try {
108
+ const cached = await readAllRawDecisions()
109
+ if (cached?.length) allDecisions = cached
110
+ } catch {}
111
+ }
112
  })
113
 
114
  const fmtUSD = (n) => (n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
115
  const signedMoney = (n) => `${n >= 0 ? '+' : 'βˆ’'}${fmtUSD(Math.abs(n))}`
116
+ const signedPct = (p) => `${(p >= 0 ? '+' : 'βˆ’')}${Math.abs(p * 100).toFixed(2)}%`
117
+ const score = (row) => (typeof row.balance === 'number' ? row.balance : -Infinity)
118
+
119
+ const filteredRows = computed(() =>
120
+ (rowsRef.value || []).filter(r => {
121
+ if (r.asset !== asset.value) return false
122
+ const name = (r?.agent_name || '').toLowerCase()
123
+ return !EXCLUDED_AGENT_NAMES.has(name)
124
+ })
125
+ )
126
+
127
+ const winners = computed(() => {
128
+ const byAgent = new Map()
129
+ for (const r of filteredRows.value) {
130
+ const k = r.agent_name
131
+ const cur = byAgent.get(k)
132
+ if (!cur || score(r) > score(cur)) byAgent.set(k, r)
133
+ }
134
+ return [...byAgent.values()]
135
+ })
136
+
137
+ const winnersForChart = computed(() =>
138
+ winners.value.map(w => ({
139
+ agent_name: w.agent_name,
140
+ asset: w.asset,
141
+ model: w.model,
142
+ strategy: w.strategy,
143
+ decision_ids: Array.isArray(w.decision_ids) ? w.decision_ids : undefined
144
+ }))
145
+ )
146
+
147
+ const winnersKey = computed(() => {
148
+ const sels = winnersForChart.value || []
149
+ return sels.map(s => `${s.agent_name}|${s.asset}|${s.model}|${s.strategy}|${(s.decision_ids?.length||0)}`).join('||')
150
+ })
151
+
152
+ async function buildSeq(sel) {
153
+ const { agent_name: agentName, asset: assetCode, model } = sel
154
+ const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
155
+ let seq = ids.length
156
+ ? allDecisions.filter(r => ids.includes(r.id))
157
+ : allDecisions.filter(r => r.agent_name === agentName && r.asset === assetCode && r.model === model)
158
+
159
+ seq.sort((a,b) => (a.date > b.date ? 1 : -1))
160
+
161
+ const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
162
+ let filtered = seq
163
+ if (!isCrypto) filtered = await filterRowsToNyseTradingDays(seq)
164
+
165
+ const cutoff = ASSET_CUTOFF[assetCode]
166
+ if (cutoff) {
167
+ const t0 = new Date(cutoff + 'T00:00:00Z')
168
+ filtered = filtered.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
169
+ }
170
+ return filtered
171
+ }
172
+
173
+ async function computeEquities(sel) {
174
+ const seq = await buildSeq(sel)
175
+ if (!seq.length) return null
176
+
177
+ const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
178
+
179
+ const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
180
+ const bhY = computeBuyHoldEquity(seq, 100000) || []
181
+ const lastIdx = Math.min(stratY.length, bhY.length) - 1
182
+ if (lastIdx < 0) return null
183
+
184
+ return { date: seq[lastIdx].date, stratLast: stratY[lastIdx], bhLast: bhY[lastIdx] }
185
+ }
186
+
187
+ let computing = false
188
+ watch(
189
+ () => [asset.value, winnersKey.value],
190
+ async () => {
191
+ if (computing) return
192
+ if (!winnersForChart.value.length) { cards.value = []; return }
193
+ computing = true
194
+ try {
195
+ const perfs = (await Promise.all(
196
+ winnersForChart.value.map(async sel => ({ sel, perf: await computeEquities(sel) }))
197
+ )).filter(x => x.perf)
198
+
199
+ if (!perfs.length) { cards.value = []; return }
200
+
201
+ const first = perfs[0]
202
+ const assetCode = first.sel.asset
203
+ const bhCard = {
204
+ key: `bh|${assetCode}`,
205
+ kind: 'bh',
206
+ title: 'Buy & Hold',
207
+ subtitle: assetCode,
208
+ balance: first.perf.bhLast,
209
+ date: first.perf.date,
210
+ logo: ASSET_ICONS[assetCode] || null,
211
+ isWinner: false
212
+ }
213
+
214
+ const agentCards = perfs.map(({ sel, perf }) => {
215
+ const gapUsd = perf.stratLast - perf.bhLast
216
+ const gapPct = perf.bhLast > 0 ? (perf.stratLast / perf.bhLast - 1) : 0
217
+ return {
218
+ key: `agent|${sel.agent_name}|${sel.model}`,
219
+ kind: 'agent',
220
+ title: sel.agent_name,
221
+ subtitle: sel.model,
222
+ balance: perf.stratLast,
223
+ date: perf.date,
224
+ logo: AGENT_LOGOS[sel.agent_name] || null,
225
+ gapUsd, gapPct,
226
+ isWinner: false
227
+ }
228
+ })
229
+
230
+ const maxBal = Math.max(...agentCards.map(c => c.balance ?? -Infinity))
231
+ agentCards.forEach(c => { c.isWinner = c.balance === maxBal })
232
+
233
+ cards.value = [bhCard, ...agentCards.sort((a,b) => b.balance - a.balance).slice(0,4)]
234
+ } catch (e) {
235
+ console.error('LiveView: compute cards failed', e)
236
+ cards.value = []
237
+ } finally {
238
+ computing = false
239
+ }
240
+ },
241
+ { immediate: true }
242
+ )
243
  </script>
244
 
245
  <style scoped>
246
+ .live { max-width: 1280px; margin: 0 auto; padding: 12px 20px 28px; }
247
+ .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; }
248
+ .mode { display: inline-flex; gap: 8px; }
249
+ .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; }
250
+ .mode__btn:hover { transform: translateY(-1px); }
251
+ .mode__btn.is-active { background: #0F172A; color: #fff; border-color: #0F172A; }
252
+ .panel { background: #fff; border: 1px solid #EDF0F4; border-radius: 14px; }
253
+ .panel--chart { padding: 10px 10px 2px; }
254
+ .panel--cards { padding: 12px; }
255
+ .empty { padding: 14px; border: 1px dashed #D6DAE1; border-radius: 12px; color: #6B7280; font-size: .92rem; }
256
+ .cards5 { display: grid; gap: 12px; grid-template-columns: repeat(5, minmax(0, 1fr)); }
257
+ .card { display: grid; grid-template-rows: auto auto auto; gap: 10px; padding: 12px 14px; border: 1px solid #EEF1F6; border-radius: 14px; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.04); }
258
+ .card--bh { outline: 2px dashed rgba(15,23,42,.08); }
259
+ .card--winner { border-color: #16a34a; box-shadow: 0 0 0 3px rgba(22,163,74,.12); }
260
+ .card__header { display: grid; grid-template-columns: 48px minmax(0,1fr) auto; align-items: center; column-gap: 12px; }
261
+ .card__logo { width: 44px; height: 44px; border-radius: 999px; background: #F3F4F6; display: grid; place-items: center; overflow: hidden; position: relative; }
262
+ .card__logo img { width: 100%; height: 100%; object-fit: contain; }
263
+ .card__badge { position: absolute; right: -6px; top: -6px; font-size: 16px; filter: drop-shadow(0 1px 1px rgba(0,0,0,.15)); }
264
+ .card__title { font-weight: 800; color: #0F172A; line-height: 1.15; font-size: clamp(16px, 1.6vw, 19px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
265
+ .card__subtitle { font-size: 12px; color: #5B6476; opacity: .85; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
266
+ .card__balance { white-space: nowrap; font-weight: 900; color: #0F172A; font-size: clamp(18px, 1.9vw, 22px); align-self: start; overflow: hidden; text-overflow: ellipsis; }
267
+ .card__meta { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 10px; }
268
+ .card__footer { font-size: 12px; color: #5B6476; opacity: .85; }
269
+ .pill { padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; background: #EEF2F7; color: #0F172A; }
270
+ .pill.pos { background: #DCFCE7; color: #166534; }
271
+ .pill.neg { background: #FEE2E2; color: #B91C1C; }
272
+ .pill--neutral { background: #EEF2F7; color: #0F172A; }
273
+ .pill--outline { background: transparent; color: #475569; border: 1px solid #E5E7EB; padding: 3px 8px; }
274
  </style>