Jimin Huang commited on
Commit
3d84ff1
1 Parent(s): e6fb472

Change settings

Browse files
Files changed (1) hide show
  1. src/components/CompareChartE.vue +179 -136
src/components/CompareChartE.vue CHANGED
@@ -1,152 +1,195 @@
1
  <template>
2
- <div class="chart-wrap">
3
- <v-chart :option="option" autoresize class="h-96 w-full" />
4
- </div>
5
  </template>
6
 
7
- <script>
8
- import { defineComponent } from 'vue'
9
- import VChart from 'vue-echarts'
10
- import * as echarts from 'echarts/core'
11
- import { LineChart } from 'echarts/charts'
12
- import { GridComponent, LegendComponent, TooltipComponent, DataZoomComponent } from 'echarts/components'
13
- import { CanvasRenderer } from 'echarts/renderers'
14
 
15
- import { getAllDecisions } from '../lib/dataCache'
16
- import { readAllRawDecisions } from '../lib/idb'
17
- import { filterRowsToNyseTradingDays } from '../lib/marketCalendar'
18
- import { STRATEGIES } from '../lib/strategies'
19
- import { computeBuyHoldEquity, computeStrategyEquity } from '../lib/perf'
20
- import { getStrategyColor } from '../lib/chartColors'
 
 
21
 
22
- echarts.use([LineChart, GridComponent, LegendComponent, TooltipComponent, DataZoomComponent, CanvasRenderer])
 
 
 
 
 
 
 
 
 
23
 
 
24
  const ASSET_CUTOFF = {
25
- BTC: '2025-08-01',
26
- // ETH: '2025-08-15', // example if you add others later
27
- };
28
-
29
- export default defineComponent({
30
- name: 'CompareChartE',
31
- components: { VChart },
32
- props: {
33
- selected: { type: Array, default: () => [] },
34
- visible: { type: Boolean, default: true }
35
- },
36
- data(){ return { option: {} } },
37
- watch: {
38
- selected: { deep: true, handler(){ this.rebuild() } },
39
- visible(v){ if (v) this.$nextTick(() => this.rebuild()) }
40
- },
41
- mounted(){ this.$nextTick(() => this.rebuild()) },
42
- methods: {
43
- async getAll(){
44
- let all = getAllDecisions() || []
45
- if (!all.length) {
46
- try { const cached = await readAllRawDecisions(); if (cached?.length) all = cached } catch {}
47
- }
48
- return all
49
- },
50
- async rebuild(){
51
- if (!this.visible) return
52
- const selected = Array.isArray(this.selected) ? this.selected : []
53
- const all = await this.getAll()
54
- const groupKeyToSeq = new Map()
55
-
56
- // 1) Build sequences exactly like CompareChart.vue
57
- for (const sel of selected) {
58
- const { agent_name: agent, asset, model } = sel
59
- const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
60
- let seq = ids.length ? all.filter(r => ids.includes(r.id))
61
- : all.filter(r => r.agent_name === agent && r.asset === asset && r.model === model)
62
- seq.sort((a,b) => (a.date > b.date ? 1 : -1))
63
- const isCrypto = asset === 'BTC' || asset === 'ETH'
64
- let filtered = isCrypto ? seq : await filterRowsToNyseTradingDays(seq)
65
-
66
- // --- asset-specific cutoff ---
67
- const cutoff = ASSET_CUTOFF[asset]
68
- if (cutoff) {
69
- const t0 = new Date(cutoff + 'T00:00:00Z')
70
- filtered = filtered.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  }
72
- groupKeyToSeq.set(`${agent}|${asset}|${model}`, { sel, seq: filtered })
73
- }
74
-
75
- // 2) Build series using (time,value) pairs => no misalignment
76
- const series = []
77
- const legend = []
78
- const assets = new Set()
79
- const agentColorIndex = new Map()
80
-
81
- for (const [_, { sel, seq }] of groupKeyToSeq.entries()) {
82
- if (!seq.length) continue
83
- const agent = sel.agent_name
84
- const asset = sel.asset
85
- assets.add(asset)
86
-
87
- const idx = agentColorIndex.get(agent) ?? agentColorIndex.size
88
- agentColorIndex.set(agent, idx)
89
-
90
- const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005, label: 'Selected' }
91
- const stratY = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
92
- const points = seq.map((row, i) => [row.date, stratY[i]]) // << key change
93
-
94
- const name = `${agent} 路 ${sel.model} 路 ${cfg.label}`
95
- legend.push(name)
96
- series.push({
97
- name,
98
- type: 'line',
99
- showSymbol: false,
100
- smooth: false,
101
- emphasis: { focus: 'series' },
102
- lineStyle: { width: 2 },
103
- areaStyle: { opacity: 0.06 },
104
- data: points
105
- })
106
- }
107
-
108
- // 3) Buy & Hold baseline per asset (also time/value points)
109
- for (const asset of assets) {
110
- const entry = [...groupKeyToSeq.values()].find(v => v.sel.asset === asset)
111
- if (!entry) continue
112
- const bhY = computeBuyHoldEquity(entry.seq, 100000) || []
113
- const bhPoints = entry.seq.map((row, i) => [row.date, bhY[i]])
114
- series.push({
115
- name: `${asset} 路 Buy&Hold`,
116
- type: 'line',
117
- showSymbol: false,
118
- lineStyle: { width: 1.5 },
119
- color: getStrategyColor('', true, 0),
120
- data: bhPoints
121
- })
122
- legend.push(`${asset} 路 Buy&Hold`)
123
- }
124
-
125
- this.option = {
126
- animation: true,
127
- grid: { left: 56, right: 24, top: 16, bottom: 60 },
128
- tooltip: {
129
- trigger: 'axis',
130
- axisPointer: { type: 'line' },
131
- valueFormatter: v => typeof v === 'number'
132
- ? v.toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
133
- : v
134
- },
135
- legend: { type: 'scroll', bottom: 8, icon: 'roundRect', itemGap: 10, data: legend },
136
- xAxis: { type: 'time' }, // << time axis = auto alignment
137
- yAxis: {
138
  type: 'value', scale: true,
139
- axisLabel: { formatter: v => v.toLocaleString(undefined, {style:'currency', currency:'USD', maximumFractionDigits:0 }) }
 
 
 
 
140
  },
141
- dataZoom: [{ type: 'inside', throttle: 50 }, { type: 'slider', height: 14, bottom: 36 }],
142
- series
143
- }
144
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  }
146
  })
 
 
 
 
 
147
  </script>
148
 
149
  <style scoped>
150
- .chart-wrap { width: 100%; }
151
- .h-96 { height: 24rem; }
152
  </style>
 
1
  <template>
2
+ <div ref="root" class="w-full" style="height:520px;"></div>
 
 
3
  </template>
4
 
5
+ <script setup>
6
+ import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
7
+ import * as echarts from 'echarts'
 
 
 
 
8
 
9
+ // 猬囷笍 adjust these imports to match your project structure if needed
10
+ import { dataService, filterRowsToNyseTradingDays } from '../lib/dataService'
11
+ import {
12
+ computeStrategyEquity,
13
+ computeBuyHoldEquity,
14
+ STRATEGIES,
15
+ getStrategyColor,
16
+ } from '../lib/strategies'
17
 
18
+ // ---------- props ----------
19
+ const props = defineProps({
20
+ selected: { type: Array, default: () => [] }, // [{agent_name, asset, model, strategy, decision_ids?}, ...]
21
+ visible: { type: Boolean, default: true },
22
+ mode: { type: String, default: 'usd' }, // 'usd' | 'pct'
23
+ })
24
+
25
+ // ---------- refs ----------
26
+ const root = ref(null)
27
+ let chart = null
28
 
29
+ // ---------- constants ----------
30
  const ASSET_CUTOFF = {
31
+ BTC: '2025-08-01', // ignore earlier data
32
+ }
33
+
34
+ // ---------- helpers ----------
35
+ const val = (x, d) => (x === undefined || x === null ? d : x)
36
+
37
+ function toPct(points) {
38
+ if (!points?.length) return points
39
+ const y0 = points[0][1]
40
+ if (!Number.isFinite(y0) || y0 === 0) return points
41
+ return points.map(([t, y]) => [t, ((y / y0) - 1) * 100])
42
+ }
43
+
44
+ async function getAllRows() {
45
+ // rely on dataService already loaded by the view
46
+ const rows = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
47
+ return rows.map(r => ({ ...r })) // shallow copy
48
+ }
49
+
50
+ // ---------- core build ----------
51
+ async function rebuild() {
52
+ if (!props.visible || !root.value) return
53
+
54
+ const all = await getAllRows()
55
+ const group = new Map() // key -> { sel, seq }
56
+
57
+ // Collect sequences per requested selection
58
+ for (const sel of (props.selected || [])) {
59
+ const { agent_name: agent, asset: assetCode, model } = sel
60
+ const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
61
+
62
+ let seq = ids.length
63
+ ? all.filter(r => ids.includes(r.id))
64
+ : all.filter(r => r.agent_name === agent && r.asset === assetCode && r.model === model)
65
+
66
+ // sort by date
67
+ seq.sort((a, b) => (a.date > b.date ? 1 : -1))
68
+
69
+ // trading-day filter for equities; crypto untouched
70
+ const isCrypto = assetCode === 'BTC' || assetCode === 'ETH' || assetCode === 'SOL' || assetCode === 'BNB' || assetCode === 'DOGE' || assetCode === 'XRP'
71
+ let filtered = isCrypto ? seq : await filterRowsToNyseTradingDays(seq)
72
+
73
+ // asset cutoff (e.g., BTC from 2025-08-01)
74
+ const cutoff = ASSET_CUTOFF[assetCode]
75
+ if (cutoff) {
76
+ const t0 = new Date(`${cutoff}T00:00:00Z`)
77
+ filtered = filtered.filter(r => new Date(`${r.date}T00:00:00Z`) >= t0)
78
+ }
79
+
80
+ group.set(`${agent}|${assetCode}|${model}`, { sel, seq: filtered })
81
+ }
82
+
83
+ // Build series
84
+ const series = []
85
+ const assetSet = new Set()
86
+
87
+ for (const { sel, seq } of group.values()) {
88
+ if (!seq.length) continue
89
+ const assetCode = sel.asset
90
+ assetSet.add(assetCode)
91
+
92
+ const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy)
93
+ || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005, label: 'Selected' }
94
+
95
+ const equity = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
96
+ let points = seq.map((row, i) => [row.date, val(equity[i], null)])
97
+ if (props.mode === 'pct') points = toPct(points)
98
+
99
+ series.push({
100
+ name: `${sel.agent_name} 路 ${sel.model} 路 ${cfg.label}`,
101
+ type: 'line',
102
+ showSymbol: false,
103
+ smooth: false,
104
+ lineStyle: { width: 2, color: getStrategyColor(sel.strategy || 'aggressive_lo', false, 0) },
105
+ // no areaStyle
106
+ data: points,
107
+ })
108
+ }
109
+
110
+ // Add Buy&Hold (dashed) per asset
111
+ for (const sym of assetSet) {
112
+ const entry = [...group.values()].find(v => v.sel.asset === sym)
113
+ if (!entry || !entry.seq.length) continue
114
+ const bh = computeBuyHoldEquity(entry.seq, 100000) || []
115
+ let points = entry.seq.map((row, i) => [row.date, val(bh[i], null)])
116
+ if (props.mode === 'pct') points = toPct(points)
117
+
118
+ series.push({
119
+ name: `${sym} 路 Buy&Hold`,
120
+ type: 'line',
121
+ showSymbol: false,
122
+ lineStyle: { width: 2, type: 'dashed', color: '#7c7c7c' },
123
+ data: points,
124
+ })
125
+ }
126
+
127
+ // Bold best line (highest last Y)
128
+ if (series.length) {
129
+ let bestIdx = -1, bestVal = -Infinity
130
+ series.forEach((s, i) => {
131
+ if (s.type !== 'line' || s.lineStyle?.type === 'dashed') return
132
+ const y = s.data?.length ? s.data[s.data.length - 1][1] : -Infinity
133
+ if (y > bestVal) { bestVal = y; bestIdx = i }
134
+ })
135
+ if (bestIdx >= 0) series[bestIdx].lineStyle.width = 3.5
136
+ }
137
+
138
+ // Compose option
139
+ const option = {
140
+ animation: true,
141
+ grid: { left: 56, right: 24, top: 12, bottom: 48 },
142
+ xAxis: { type: 'time', axisLabel: { hideOverlap: true } },
143
+ yAxis: props.mode === 'pct'
144
+ ? {
145
+ type: 'value', scale: true,
146
+ axisLabel: { formatter: v => `${Number(v).toLocaleString(undefined, { maximumFractionDigits: 0 })}%` }
147
  }
148
+ : {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  type: 'value', scale: true,
150
+ axisLabel: {
151
+ formatter: v => Number(v).toLocaleString(undefined, {
152
+ style: 'currency', currency: 'USD', maximumFractionDigits: 0
153
+ })
154
+ }
155
  },
156
+ legend: { show: false },
157
+ tooltip: { show: false }, // disabled
158
+ dataZoom: [
159
+ { type: 'inside', throttle: 50 },
160
+ { type: 'slider', height: 14, bottom: 20 },
161
+ ],
162
+ series,
163
+ }
164
+
165
+ // init / set
166
+ if (!chart) {
167
+ chart = echarts.init(root.value, null, { renderer: 'canvas' })
168
+ window.addEventListener('resize', handleResize)
169
+ }
170
+ chart.setOption(option, true)
171
+ }
172
+
173
+ function handleResize() {
174
+ if (chart) chart.resize()
175
+ }
176
+
177
+ // ---------- lifecycle ----------
178
+ onMounted(() => { rebuild() })
179
+ onBeforeUnmount(() => {
180
+ window.removeEventListener('resize', handleResize)
181
+ if (chart) {
182
+ chart.dispose()
183
+ chart = null
184
  }
185
  })
186
+
187
+ // Rebuild when inputs change
188
+ watch(() => props.mode, () => rebuild())
189
+ watch(() => props.visible, v => { if (v) rebuild() })
190
+ watch(() => props.selected, () => rebuild(), { deep: true })
191
  </script>
192
 
193
  <style scoped>
194
+ /* no extra styles needed; container height set inline */
 
195
  </style>