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() | |
| 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 }) | |
| } | |
| const keys = Array.from(groupKeyToSeq.keys()) | |
| const labels = keys.length ? (groupKeyToSeq.get(keys[0]).seq || []).map(s => s.date) : [] | |
| 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 stratSeries = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) | |
| if (stratSeries?.length) { | |
| const color = getStrategyColor(sel.strategy || 'aggressive_lo', false, idx) | |
| const vals = labels.length ? stratSeries.slice(0, labels.length) : stratSeries | |
| 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 }, | |
| data: vals | |
| }) | |
| } | |
| } | |
| for (const asset of assets) { | |
| const entry = [...groupKeyToSeq.values()].find(v => v.sel.asset === asset) | |
| if (!entry) continue | |
| const bh = computeBuyHoldEquity(entry.seq, 100000) || [] | |
| const baseline = labels.length ? bh.slice(0, labels.length) : bh | |
| series.push({ | |
| name: `${asset} 路 Buy&Hold`, | |
| type: 'line', | |
| showSymbol: false, | |
| lineStyle: { width: 1.5 }, | |
| color: getStrategyColor('', true, 0), | |
| data: baseline | |
| }) | |
| legend.push(`${asset} 路 Buy&Hold`) | |
| } | |
| this.option = { | |
| animation: true, | |
| grid: { left: 40, right: 20, top: 30, bottom: 40 }, | |
| tooltip: { | |
| trigger: 'axis', | |
| valueFormatter: v => typeof v === 'number' | |
| ? v.toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 }) | |
| : v | |
| }, | |
| legend: { type: 'scroll', bottom: 0, data: legend }, | |
| xAxis: { type: 'category', data: labels, axisLabel: { formatter: v => v?.slice?.(2) } }, | |
| 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: 16, bottom: 22 }], | |
| series | |
| } | |
| } | |
| } | |
| }) | |
| </script> | |
| <style scoped> | |
| .chart-wrap { width: 100%; } | |
| .h-96 { height: 24rem; } | |
| </style> | |