Spaces:
Running
Running
Jimin Huang
commited on
Commit
·
3c4f27f
1
Parent(s):
3b104a4
Change settings
Browse files- src/views/LiveView.vue +72 -64
src/views/LiveView.vue
CHANGED
|
@@ -36,7 +36,7 @@
|
|
| 36 |
</div>
|
| 37 |
</section>
|
| 38 |
|
| 39 |
-
<!-- Cards: Buy & Hold + top 4 agents
|
| 40 |
<section class="panel panel--cards" v-if="cards.length">
|
| 41 |
<div class="cards5">
|
| 42 |
<div
|
|
@@ -66,14 +66,14 @@
|
|
| 66 |
<div class="card__sub" :title="c.subtitle">{{ c.subtitle }}</div>
|
| 67 |
|
| 68 |
<template v-if="c.kind==='agent'">
|
| 69 |
-
<div class="
|
| 70 |
-
<span>{{ signedMoney(c.gapUsd) }}</span>
|
| 71 |
-
<span
|
| 72 |
</div>
|
| 73 |
</template>
|
| 74 |
|
| 75 |
<template v-else>
|
| 76 |
-
<div class="
|
| 77 |
</template>
|
| 78 |
</div>
|
| 79 |
|
|
@@ -100,11 +100,11 @@ import { readAllRawDecisions } from '../lib/idb'
|
|
| 100 |
import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
|
| 101 |
import { computeBuyHoldEquity } from '../lib/perf'
|
| 102 |
|
| 103 |
-
|
| 104 |
const orderedAssets = ['BTC','ETH','MSFT','BMRN','TSLA'] // MRNA removed
|
| 105 |
const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla']) // case-insensitive
|
| 106 |
|
| 107 |
-
//
|
| 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,
|
|
@@ -119,14 +119,14 @@ const ASSET_ICONS = {
|
|
| 119 |
TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
|
| 120 |
}
|
| 121 |
|
| 122 |
-
|
| 123 |
-
const mode = ref('usd')
|
| 124 |
const asset = ref('BTC')
|
| 125 |
const rowsRef = ref([])
|
| 126 |
-
const bhBalance = ref(null)
|
| 127 |
const bhDate = ref('')
|
| 128 |
|
| 129 |
-
|
| 130 |
onMounted(async () => {
|
| 131 |
try {
|
| 132 |
if (!dataService.loaded && !dataService.loading) {
|
|
@@ -136,46 +136,19 @@ onMounted(async () => {
|
|
| 136 |
console.error('LiveView: dataService.load failed', e)
|
| 137 |
}
|
| 138 |
rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
|
|
|
|
| 139 |
})
|
| 140 |
|
| 141 |
-
|
| 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
|
|
@@ -184,7 +157,7 @@ const filteredRows = computed(() =>
|
|
| 184 |
})
|
| 185 |
)
|
| 186 |
|
| 187 |
-
|
| 188 |
const winners = computed(() => {
|
| 189 |
const byAgent = new Map()
|
| 190 |
for (const r of filteredRows.value) {
|
|
@@ -195,23 +168,58 @@ const winners = computed(() => {
|
|
| 195 |
return [...byAgent.values()]
|
| 196 |
})
|
| 197 |
|
| 198 |
-
|
| 199 |
const winnersForChart = computed(() =>
|
| 200 |
winners.value.map(w => ({
|
| 201 |
-
agent_name:
|
| 202 |
-
asset:
|
| 203 |
-
model:
|
| 204 |
-
strategy:
|
| 205 |
decision_ids: Array.isArray(w.decision_ids) ? w.decision_ids : undefined
|
| 206 |
}))
|
| 207 |
)
|
| 208 |
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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',
|
|
@@ -222,7 +230,6 @@ const cards = computed(() => {
|
|
| 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))
|
|
@@ -231,7 +238,6 @@ const cards = computed(() => {
|
|
| 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',
|
|
@@ -247,13 +253,14 @@ const cards = computed(() => {
|
|
| 247 |
return [bhCard, ...agentCards]
|
| 248 |
})
|
| 249 |
|
| 250 |
-
|
| 251 |
function nameFontSize(name='') {
|
| 252 |
const len = name.length
|
| 253 |
-
if (len <=
|
| 254 |
-
if (len <=
|
| 255 |
-
if (len <=
|
| 256 |
-
return '15px'
|
|
|
|
| 257 |
}
|
| 258 |
</script>
|
| 259 |
|
|
@@ -340,10 +347,12 @@ function nameFontSize(name='') {
|
|
| 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; }
|
|
@@ -355,7 +364,6 @@ function nameFontSize(name='') {
|
|
| 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; }
|
|
|
|
| 36 |
</div>
|
| 37 |
</section>
|
| 38 |
|
| 39 |
+
<!-- Cards: Buy & Hold + top 4 agents -->
|
| 40 |
<section class="panel panel--cards" v-if="cards.length">
|
| 41 |
<div class="cards5">
|
| 42 |
<div
|
|
|
|
| 66 |
<div class="card__sub" :title="c.subtitle">{{ c.subtitle }}</div>
|
| 67 |
|
| 68 |
<template v-if="c.kind==='agent'">
|
| 69 |
+
<div class="pill" :class="{ neg: c.gapUsd < 0 }">
|
| 70 |
+
<span v-if="mode==='usd'">{{ signedMoney(c.gapUsd) }}</span>
|
| 71 |
+
<span v-else>{{ signedPct(c.gapPct) }}</span>
|
| 72 |
</div>
|
| 73 |
</template>
|
| 74 |
|
| 75 |
<template v-else>
|
| 76 |
+
<div class="pill">Buy & Hold</div>
|
| 77 |
</template>
|
| 78 |
</div>
|
| 79 |
|
|
|
|
| 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 |
+
// Plug in your real logos here
|
| 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,
|
|
|
|
| 119 |
TSLA: new URL('../assets/images/assets_images/TSLA.png', import.meta.url).href,
|
| 120 |
}
|
| 121 |
|
| 122 |
+
/* ---------- state ---------- */
|
| 123 |
+
const mode = ref('usd') // 'usd' | 'pct'
|
| 124 |
const asset = ref('BTC')
|
| 125 |
const rowsRef = ref([])
|
| 126 |
+
const bhBalance = ref(null) // Buy&Hold final balance (100k start)
|
| 127 |
const bhDate = ref('')
|
| 128 |
|
| 129 |
+
/* ---------- bootstrap ---------- */
|
| 130 |
onMounted(async () => {
|
| 131 |
try {
|
| 132 |
if (!dataService.loaded && !dataService.loading) {
|
|
|
|
| 136 |
console.error('LiveView: dataService.load failed', e)
|
| 137 |
}
|
| 138 |
rowsRef.value = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
|
| 139 |
+
if (!orderedAssets.includes(asset.value)) asset.value = orderedAssets[0]
|
| 140 |
})
|
| 141 |
|
| 142 |
+
/* ---------- helpers ---------- */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
function score(row) {
|
| 144 |
return typeof row.balance === 'number' ? row.balance : -Infinity
|
| 145 |
}
|
| 146 |
+
const fmtUSD = (n) =>
|
| 147 |
+
(n ?? 0).toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
|
| 148 |
+
const signedMoney = (n) => `${n >= 0 ? '+' : '−'}${fmtUSD(Math.abs(n))}`
|
| 149 |
+
const signedPct = (p) => `${(p >= 0 ? '+' : '−')}${Math.abs(p * 100).toFixed(2)}%`
|
| 150 |
+
|
| 151 |
+
/* Filter rows: selected asset, exclude “vanilla/vinilla” */
|
| 152 |
const filteredRows = computed(() =>
|
| 153 |
(rowsRef.value || []).filter(r => {
|
| 154 |
if (r.asset !== asset.value) return false
|
|
|
|
| 157 |
})
|
| 158 |
)
|
| 159 |
|
| 160 |
+
/* Best model per agent (by balance) */
|
| 161 |
const winners = computed(() => {
|
| 162 |
const byAgent = new Map()
|
| 163 |
for (const r of filteredRows.value) {
|
|
|
|
| 168 |
return [...byAgent.values()]
|
| 169 |
})
|
| 170 |
|
| 171 |
+
/* Chart selections */
|
| 172 |
const winnersForChart = computed(() =>
|
| 173 |
winners.value.map(w => ({
|
| 174 |
+
agent_name: w.agent_name,
|
| 175 |
+
asset: w.asset,
|
| 176 |
+
model: w.model,
|
| 177 |
+
strategy: w.strategy,
|
| 178 |
decision_ids: Array.isArray(w.decision_ids) ? w.decision_ids : undefined
|
| 179 |
}))
|
| 180 |
)
|
| 181 |
|
| 182 |
+
/* === Buy&Hold: recompute from the SAME sequence the chart uses === */
|
| 183 |
+
watch(
|
| 184 |
+
() => winnersForChart.value,
|
| 185 |
+
async (list) => { await recomputeBHFromSelection(list) },
|
| 186 |
+
{ immediate: true, deep: true }
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
async function recomputeBHFromSelection(list) {
|
| 190 |
+
const sel = Array.isArray(list) && list.length ? list[0] : null
|
| 191 |
+
if (!sel) { bhBalance.value = null; bhDate.value = ''; return }
|
| 192 |
+
|
| 193 |
+
// fetch decisions once
|
| 194 |
+
let all = getAllDecisions() || []
|
| 195 |
+
if (!all.length) {
|
| 196 |
+
try { const cached = await readAllRawDecisions(); if (cached?.length) all = cached } catch {}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
|
| 200 |
+
let seq = ids.length
|
| 201 |
+
? all.filter(r => ids.includes(r.id))
|
| 202 |
+
: all.filter(r => r.agent_name === sel.agent_name && r.asset === sel.asset && r.model === sel.model)
|
| 203 |
+
seq.sort((a,b) => (a.date > b.date ? 1 : -1))
|
| 204 |
+
|
| 205 |
+
const isCrypto = sel.asset === 'BTC' || sel.asset === 'ETH'
|
| 206 |
+
if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
|
| 207 |
+
|
| 208 |
+
const bh = computeBuyHoldEquity(seq, 100000) || []
|
| 209 |
+
if (bh.length) {
|
| 210 |
+
bhBalance.value = bh[bh.length - 1]
|
| 211 |
+
bhDate.value = seq[seq.length - 1]?.date || ''
|
| 212 |
+
} else {
|
| 213 |
+
bhBalance.value = null
|
| 214 |
+
bhDate.value = ''
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
/* Build 5 cards: Buy&Hold + top 4 agents */
|
| 219 |
const cards = computed(() => {
|
| 220 |
const base = Number(bhBalance.value ?? 100000)
|
| 221 |
const baseDate = bhDate.value
|
| 222 |
|
|
|
|
| 223 |
const bhCard = {
|
| 224 |
key: `bh|${asset.value}`,
|
| 225 |
kind: 'bh',
|
|
|
|
| 230 |
logo: ASSET_ICONS[asset.value] || null,
|
| 231 |
}
|
| 232 |
|
|
|
|
| 233 |
const top = [...winners.value].sort((a,b) => score(b) - score(a)).slice(0, 4)
|
| 234 |
const agentCards = top.map(r => {
|
| 235 |
const gapUsd = (typeof r.balance === 'number' && Number.isFinite(base))
|
|
|
|
| 238 |
const gapPct = (Number.isFinite(base) && base > 0)
|
| 239 |
? (r.balance / base - 1)
|
| 240 |
: 0
|
|
|
|
| 241 |
return {
|
| 242 |
key: `agent|${r.agent_name}|${r.model}`,
|
| 243 |
kind: 'agent',
|
|
|
|
| 253 |
return [bhCard, ...agentCards]
|
| 254 |
})
|
| 255 |
|
| 256 |
+
/* Dynamic font size for long names (no clipping) */
|
| 257 |
function nameFontSize(name='') {
|
| 258 |
const len = name.length
|
| 259 |
+
if (len <= 12) return '20px'
|
| 260 |
+
if (len <= 16) return '18px'
|
| 261 |
+
if (len <= 22) return '16px'
|
| 262 |
+
if (len <= 28) return '15px'
|
| 263 |
+
return '14px'
|
| 264 |
}
|
| 265 |
</script>
|
| 266 |
|
|
|
|
| 347 |
.card__row {
|
| 348 |
display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
|
| 349 |
}
|
| 350 |
+
|
| 351 |
+
/* title shrinks (no ellipsis) */
|
| 352 |
.card__title {
|
| 353 |
+
font-weight: 800; color: #0F172A; white-space: nowrap;
|
|
|
|
| 354 |
}
|
| 355 |
+
|
| 356 |
.card__balance { font-weight: 900; color: #0F172A; font-size: 20px; }
|
| 357 |
.card__sub { font-size: 12px; color: #5B6476; opacity: .85; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 358 |
.card__meta { margin-top: 4px; align-items: center; }
|
|
|
|
| 364 |
background: #EEF2F7; color: #0F172A;
|
| 365 |
}
|
| 366 |
.pill.neg { background: #FEE2E2; color: #B91C1C; }
|
|
|
|
| 367 |
|
| 368 |
/* utility */
|
| 369 |
.spacer { flex: 1 1 auto; }
|