Agent-Market-Arena / src /components /CompareChart.vue
Jimin Huang
add: Feature
ac784c2
raw
history blame
8.17 kB
<template>
<div class="chart-container">
<canvas ref="canvas"></canvas>
</div>
</template>
<script>
import { getAllDecisions } from '../lib/dataCache'
import { readAllRawDecisions } from '../lib/idb'
import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
import { STRATEGIES } from '../lib/strategies'
import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
import { Chart, LineElement, PointElement, LinearScale, CategoryScale, LineController, Legend, Tooltip } from 'chart.js'
import { getStrategyColor } from '../lib/chartColors'
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, CategoryScale, LineController, Legend, Tooltip, vLinePlugin)
export default {
name: 'CompareChart',
props: {
selected: { type: Array, default: () => [] },
visible: { type: Boolean, default: false }
},
data(){
return { chart: null, datasets: [], labels: [] }
},
watch: {
visible(v){ if (v) { this.$nextTick(() => this.buildAndDraw()) } },
selected: { deep: true, handler(){ this.buildAndDraw() } }
},
mounted(){ this.$nextTick(() => this.buildAndDraw()) },
beforeUnmount(){ try{ this.chart && this.chart.destroy() }catch(_){} },
methods: {
async buildAndDraw(){
if (!this.visible) return
await this.rebuildDatasetsFromSelection()
this.draw()
},
async getAll(){
let all = getAllDecisions() || []
if (!all.length) {
try { const cached = await readAllRawDecisions(); if (cached && cached.length) all = cached } catch(_) {}
}
return all
},
async rebuildDatasetsFromSelection(){
const selected = Array.isArray(this.selected) ? this.selected : []
const all = await this.getAll()
const groupKeyToSeq = new Map()
for (const sel of selected) {
const agent = sel.agent_name
const asset = sel.asset
const model = sel.model
const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
let seq = []
if (ids.length) seq = all.filter(r => ids.includes(r.id))
else seq = all.filter(r => r.agent_name === agent && r.asset === asset && r.model === model)
seq.sort((a,b) => (a.date > b.date ? 1 : -1))
const isCrypto = asset === 'BTC' || asset === 'ETH'
const filtered = isCrypto ? seq : await filterRowsToNyseTradingDays(seq)
groupKeyToSeq.set(`${agent}|${asset}|${model}`, filtered)
}
const ds = []
const keys = Array.from(groupKeyToSeq.keys())
// labels from first sequence
this.labels = []
if (keys.length) this.labels = (groupKeyToSeq.get(keys[0]) || []).map(s => s.date)
// 按 agent 分组以应用颜色规则
const agentGroups = new Map() // agent -> [selection items]
const selectedAssets = new Set()
for (const sel of selected) {
const gk = `${sel.agent_name}|${sel.asset}|${sel.model}`
const seq = groupKeyToSeq.get(gk) || []
if (!seq.length) continue
selectedAssets.add(sel.asset)
const agent = sel.agent_name
if (!agentGroups.has(agent)) {
agentGroups.set(agent, [])
}
agentGroups.get(agent).push({ sel, seq })
}
// 为每个 agent 的策略生成系列,同一 agent 使用相同色调
for (const [agent, items] of agentGroups.entries()) {
items.forEach((item, index) => {
const { sel, seq } = item
const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || {
strategy: 'long_short',
tradingMode: 'normal',
fee: 0.0005,
label: 'Selected'
}
const series = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode)
if (!series.length) return
// 同一 agent 的策略使用相同色调,通过亮度区分
const strategyColor = getStrategyColor(agent, false, index)
ds.push({
label: `${sel.agent_name}|${sel.asset}|${sel.model}|${cfg.label || sel.strategy || 'Strategy'}`,
data: this.labels.length ? series.slice(0, this.labels.length) : series,
borderColor: strategyColor,
pointRadius: 0,
tension: 0.15
})
})
}
// 为每个资产添加 baseline,统一使用黑色实线
const seenAsset = new Set()
for (const gk of keys) {
const asset = gk.split('|')[1]
if (!selectedAssets.has(asset)) continue
if (seenAsset.has(asset)) continue
seenAsset.add(asset)
const seq = groupKeyToSeq.get(gk) || []
const bh = computeBuyHoldEquity(seq, 100000)
if (!bh.length) continue
// baseline 统一使用黑色实线
const baselineColor = getStrategyColor('', true, 0)
ds.push({
label: `${asset} · Buy&Hold`,
data: this.labels.length ? bh.slice(0, this.labels.length) : bh,
borderColor: baselineColor,
borderWidth: 1.5,
pointRadius: 0,
tension: 0
})
}
this.datasets = ds
},
draw(){
if (!this.$refs.canvas) return
this.$nextTick(() => {
if (!this.$refs.canvas) return
try{ this.chart && this.chart.destroy() }catch(_){}
const parent = this.$refs.canvas.parentElement
if (parent) {
if (!parent.clientHeight || parent.clientHeight < 50) { parent.style.minHeight = '560px'; parent.style.height = '560px' }
const w = parent.clientWidth || 800
const h = parent.clientHeight || 560
this.$refs.canvas.style.width = '100%'
this.$refs.canvas.style.height = '100%'
this.$refs.canvas.width = w
this.$refs.canvas.height = h
}
const labels = this.labels.length ? this.labels : Array.from({ length: Math.max(0, ...this.datasets.map(d => (Array.isArray(d.data) ? d.data.length : 0))) }, (_, i) => `${i+1}`)
const allValues = this.datasets.flatMap(d => (Array.isArray(d.data) ? d.data : []))
const minV = allValues.length ? Math.min(...allValues) : 0
const maxV = allValues.length ? Math.max(...allValues) : 1
const pad = (maxV - minV) * 0.1 || 1000
const yMin = minV - pad
const yMax = maxV + pad
const ctx = this.$refs.canvas.getContext('2d')
this.chart = new Chart(ctx, {
type: 'line',
data: { labels, datasets: this.datasets },
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: true, position: 'left', labels: { usePointStyle: true, boxWidth: 8 } },
vLinePlugin: { color: 'rgba(0,0,0,0.35)', lineWidth: 1, dash: [4,4] }
},
scales: { x: { type: 'category', ticks: { autoSkip: true, maxTicksLimit: 10 } }, y: { min: yMin, max: yMax } }
}
})
})
}
}
}
</script>
<style scoped>
.chart-container { position: relative; height: 560px; display: flex; }
.chart-container > canvas { width: 100% !important; height: 100% !important; }
</style>