Spaces:
Running
Running
File size: 7,778 Bytes
ac784c2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
<template>
<div style="position: relative;">
<canvas ref="perfCanvas" height="280"></canvas>
<div v-if="isZoomed" style="position: absolute; top: 10px; right: 10px; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; z-index: 10;">
<button
@click="resetZoom"
style="padding: 6px 12px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.2);"
@mouseover="$event.target.style.background='#2563eb'"
@mouseout="$event.target.style.background='#3b82f6'"
>
Reset Zoom
</button>
<span style="font-size: 11px; color: #64748b; background: rgba(255,255,255,0.95); padding: 4px 8px; border-radius: 3px; white-space: nowrap; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
Drag to zoom | Shift+Drag to pan
</span>
</div>
</div>
</template>
<script>
import { getAllDecisions } from '../lib/dataCache'
import { STRATEGIES } from '../lib/strategies'
import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
import { Chart, LineElement, PointElement, LinearScale, TimeScale, CategoryScale, LineController, Legend, Tooltip } from 'chart.js'
import zoomPlugin from 'chartjs-plugin-zoom'
// vertical crosshair plugin
const vLinePlugin = {
id: 'vLinePlugin',
afterDatasetsDraw(chart, args, pluginOptions) {
const active = (typeof chart.getActiveElements === 'function') ? chart.getActiveElements() : (chart.tooltip && chart.tooltip._active) || []
if (!active || !active.length) return
const { datasetIndex, index } = active[0]
const meta = chart.getDatasetMeta(datasetIndex)
const pt = meta && meta.data && meta.data[index]
if (!pt) return
const x = pt.x
const { top, bottom } = chart.chartArea
const ctx = chart.ctx
ctx.save()
ctx.beginPath()
ctx.moveTo(x, top)
ctx.lineTo(x, bottom)
ctx.lineWidth = (pluginOptions && pluginOptions.lineWidth) || 1
ctx.strokeStyle = (pluginOptions && pluginOptions.color) || 'rgba(0,0,0,0.35)'
ctx.setLineDash((pluginOptions && pluginOptions.dash) || [4, 4])
ctx.stroke()
ctx.restore()
}
}
Chart.register(LineElement, PointElement, LinearScale, TimeScale, CategoryScale, LineController, Legend, Tooltip, vLinePlugin, zoomPlugin)
export default {
name: 'PerformanceChart',
props: {
data: { type: Object, required: true }
},
data(){
return {
chart: null,
isZoomed: false
}
},
watch: {
data: {
handler(){ this.draw() }, immediate: true, deep: true
}
},
mounted(){ this.$nextTick(() => this.draw()) },
beforeUnmount(){ try{ this.chart && this.chart.destroy() }catch(_){} },
methods: {
getSeqFromIds(row){
const all = getAllDecisions() || []
const ids = row && (row.decision_ids || row.ids || [])
let seq = []
if (ids && ids.length) {
seq = all.filter(r => ids.includes(r.id))
} else if (row) {
// Only fallback to full data if no decision_ids were provided at all
seq = all.filter(r => r.agent_name === row.agent_name && r.asset === row.asset && r.model === row.model)
}
seq.sort((a,b) => (a.date > b.date ? 1 : -1))
return seq
},
fmtMoney(v){ try{ return `$${Number(v).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}` }catch{return v} },
async draw(){
if (!this.$refs.perfCanvas || !this.data) return
const row = this.data
const rawSeq = this.getSeqFromIds(row)
const isCrypto = row.asset === 'BTC' || row.asset === 'ETH'
const seq = isCrypto ? rawSeq : (await filterRowsToNyseTradingDays(rawSeq) || [])
if (!seq.length) { try{ this.chart && this.chart.destroy() }catch(_){}; return }
const strategyCfg = (STRATEGIES || []).find(s => s.id === row.strategy) || { strategy: 'long_short', tradingMode: 'normal', fee: 0.0005 }
const st = computeStrategyEquity(seq, 100000, strategyCfg.fee, strategyCfg.strategy, strategyCfg.tradingMode)
const bh = computeBuyHoldEquity(seq, 100000)
const labels = seq.map(s => s.date)
// keep arrays aligned in length
const n = Math.min(st.length, bh.length, labels.length)
const stS = st.slice(0, n)
const bhS = bh.slice(0, n)
const lab = labels.slice(0, n)
const stRet = stS.length ? ((stS[stS.length-1] - stS[0]) / stS[0]) * 100 : 0
const bhRet = bhS.length ? ((bhS[bhS.length-1] - bhS[0]) / bhS[0]) * 100 : 0
const vsBh = stRet - bhRet
const agentColor = vsBh >= 0 ? '#16a34a' : '#dc2626' // green when outperform, red when underperform
const agentBgColor = vsBh >= 0 ? 'rgba(22,163,74,0.15)' : 'rgba(220,38,38,0.15)'
const baselineColor = '#3b82f6' // blue
try{ this.chart && this.chart.destroy() }catch(_){ }
this.chart = new Chart(this.$refs.perfCanvas.getContext('2d'), {
type: 'line',
data: {
labels: lab,
datasets: [
{
label: 'Agent Balance',
data: stS,
borderColor: agentColor,
backgroundColor: agentBgColor,
pointRadius: 3,
tension: 0.2
},
{
label: 'Baseline',
data: bhS,
borderColor: baselineColor,
borderDash: [6,4],
pointRadius: 0,
tension: 0.2
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: true },
tooltip: {
callbacks: {
title: (items) => items && items.length ? `Date: ${items[0].label}` : '',
afterTitle: () => '',
label: (ctx) => `${ctx.dataset.label}: ${this.fmtMoney(ctx.parsed.y)}`,
afterBody: (items) => {
const idx = items && items.length ? items[0].dataIndex : null
let sRet = stRet
let bRet = bhRet
if (idx != null && idx >= 0) {
if (stS.length > 0 && stS[idx] != null) sRet = ((stS[idx] - stS[0]) / stS[0]) * 100
if (bhS.length > 0 && bhS[idx] != null) bRet = ((bhS[idx] - bhS[0]) / bhS[0]) * 100
}
return [
`Strategy Return: ${sRet.toFixed(2)}%`,
`Baseline Return: ${bRet.toFixed(2)}%`
]
}
}
},
vLinePlugin: { color: 'rgba(0,0,0,0.35)', lineWidth: 1, dash: [4,4] },
zoom: {
zoom: {
drag: {
enabled: true,
backgroundColor: 'rgba(59, 130, 246, 0.2)',
borderColor: 'rgba(59, 130, 246, 0.8)',
borderWidth: 1
},
mode: 'x',
onZoomComplete: () => {
this.isZoomed = true
}
},
pan: {
enabled: true,
mode: 'x',
modifierKey: 'shift'
},
limits: {
x: { min: 'original', max: 'original' }
}
}
},
scales: {
x: { ticks: { autoSkip: true, maxTicksLimit: 8 } },
y: { beginAtZero: false }
}
}
})
},
resetZoom(){
if (this.chart) {
this.chart.resetZoom()
this.isZoomed = false
}
}
}
}
</script> |