Spaces:
Running
Running
| <template> | |
| <div class="chart-wrap"> | |
| <v-chart :option="option" autoresize class="h-96 w-full" /> | |
| </div> | |
| </template> | |
| <script> | |
| import { defineComponent } from 'vue' | |
| import VChart from 'vue-echarts' | |
| import * as echarts from 'echarts/core' | |
| import { LineChart } from 'echarts/charts' | |
| import { GridComponent, LegendComponent, TooltipComponent, DataZoomComponent } from 'echarts/components' | |
| import { CanvasRenderer } from 'echarts/renderers' | |
| 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 { getStrategyColor } from '../lib/chartColors' | |
| echarts.use([LineChart, GridComponent, LegendComponent, TooltipComponent, DataZoomComponent, CanvasRenderer]) | |
| export default defineComponent({ | |
| name: 'CompareChartE', | |
| components: { VChart }, | |
| props: { | |
| selected: { type: Array, default: () => [] }, | |
| visible: { type: Boolean, default: true } | |
| }, | |
| data(){ return { option: {} } }, | |
| watch: { | |
| selected: { deep: true, handler(){ this.rebuild() } }, | |
| visible(v){ if (v) this.$nextTick(() => this.rebuild()) } | |
| }, | |
| mounted(){ this.$nextTick(() => this.rebuild()) }, | |
| methods: { | |
| async getAll(){ | |
| let all = getAllDecisions() || [] | |
| if (!all.length) { | |
| try { const cached = await readAllRawDecisions(); if (cached?.length) all = cached } catch {} | |
| } | |
| return all | |
| }, | |
| async rebuild(){ | |
| if (!this.visible) return | |
| const selected = Array.isArray(this.selected) ? this.selected : [] | |
| const all = await this.getAll() | |
| const groupKeyToSeq = new Map() | |
| // 1) Build sequences exactly like CompareChart.vue | |
| for (const sel of selected) { | |
| const { agent_name: agent, asset, model } = sel | |
| const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : [] | |
| let seq = ids.length ? all.filter(r => ids.includes(r.id)) | |
| : 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}`, { sel, seq: filtered }) | |
| } | |
| // 2) Build series using (time,value) pairs => no misalignment | |
| const series = [] | |
| const legend = [] | |
| const assets = new Set() | |
| const agentColorIndex = new Map() | |
| for (const [_, { sel, seq }] of groupKeyToSeq.entries()) { | |
| if (!seq.length) continue | |
| const agent = sel.agent_name | |
| const asset = sel.asset | |
| assets.add(asset) | |
| const idx = agentColorIndex.get(agent) ?? agentColorIndex.size | |
| agentColorIndex.set(agent, idx) | |
| const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005, label: 'Selected' } | |
| const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || [] | |
| const points = seq.map((row, i) => [row.date, stratY[i]]) // << key change | |
| const name = `${agent} 路 ${sel.model} 路 ${cfg.label}` | |
| legend.push(name) | |
| series.push({ | |
| name, | |
| type: 'line', | |
| showSymbol: false, | |
| smooth: false, | |
| emphasis: { focus: 'series' }, | |
| lineStyle: { width: 2 }, | |
| areaStyle: { opacity: 0.06 }, | |
| data: points | |
| }) | |
| } | |
| // 3) Buy & Hold baseline per asset (also time/value points) | |
| for (const asset of assets) { | |
| const entry = [...groupKeyToSeq.values()].find(v => v.sel.asset === asset) | |
| if (!entry) continue | |
| const bhY = computeBuyHoldEquity(entry.seq, 100000) || [] | |
| const bhPoints = entry.seq.map((row, i) => [row.date, bhY[i]]) | |
| series.push({ | |
| name: `${asset} 路 Buy&Hold`, | |
| type: 'line', | |
| showSymbol: false, | |
| lineStyle: { width: 1.5 }, | |
| color: getStrategyColor('', true, 0), | |
| data: bhPoints | |
| }) | |
| legend.push(`${asset} 路 Buy&Hold`) | |
| } | |
| this.option = { | |
| animation: true, | |
| grid: { left: 56, right: 24, top: 16, bottom: 60 }, | |
| tooltip: { | |
| trigger: 'axis', | |
| axisPointer: { type: 'line' }, | |
| valueFormatter: v => typeof v === 'number' | |
| ? v.toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 }) | |
| : v | |
| }, | |
| legend: { type: 'scroll', bottom: 8, icon: 'roundRect', itemGap: 10, data: legend }, | |
| xAxis: { type: 'time' }, // << time axis = auto alignment | |
| yAxis: { | |
| type: 'value', scale: true, | |
| axisLabel: { formatter: v => v.toLocaleString(undefined, {style:'currency', currency:'USD', maximumFractionDigits:0 }) } | |
| }, | |
| dataZoom: [{ type: 'inside', throttle: 50 }, { type: 'slider', height: 14, bottom: 36 }], | |
| series | |
| } | |
| } | |
| } | |
| }) | |
| </script> | |
| <style scoped> | |
| .chart-wrap { width: 100%; } | |
| .h-96 { height: 24rem; } | |
| </style> | |