File size: 5,290 Bytes
3ac9172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa77219
3ac9172
 
 
 
 
 
 
 
 
 
 
 
fa77219
3ac9172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa77219
3ac9172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa77219
3ac9172
 
 
fa77219
3ac9172
fa77219
3ac9172
 
 
 
 
 
 
 
 
 
 
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
<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>