Spaces:
Running
Running
File size: 8,171 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 204 205 206 207 208 209 210 211 212 |
<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>
|