Spaces:
Running
Running
Jimin Huang
commited on
Commit
Β·
70deb70
1
Parent(s):
727b381
Change settings
Browse files- 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
|
| 7 |
<div class="asset-tabs">
|
| 8 |
<button
|
| 9 |
v-for="a in orderedAssets"
|
|
@@ -55,7 +55,7 @@
|
|
| 55 |
</div>
|
| 56 |
</section>
|
| 57 |
|
| 58 |
-
<!--
|
| 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 +
|
| 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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
/* ---------- config ---------- */
|
| 148 |
-
const orderedAssets = ['BTC','ETH','
|
| 149 |
-
const EXCLUDED_AGENT_NAMES = new Set(['vanilla', 'vinilla'])
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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(
|
| 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:
|
| 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:
|
| 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 |
|