Jimin Huang commited on
Commit
adc1d18
1 Parent(s): b1a0f4b

Change settings

Browse files
Files changed (1) hide show
  1. src/components/CompareChartE.vue +142 -191
src/components/CompareChartE.vue CHANGED
@@ -1,201 +1,152 @@
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
- import { dataService } from '../lib/dataService'
9
- import {
10
- computeStrategyEquity,
11
- computeBuyHoldEquity,
12
- STRATEGIES,
13
- getStrategyColor,
14
- } from '../lib/strategies'
15
-
16
- const props = defineProps({
17
- selected: { type: Array, default: () => [] }, // [{agent_name, asset, model?, strategy?, decision_ids?}, ...]
18
- visible: { type: Boolean, default: true },
19
- mode: { type: String, default: 'usd' }, // 'usd' | 'pct'
20
- })
21
-
22
- const root = ref(null)
23
- let chart = null
24
-
25
- // hard start dates by asset
26
- const ASSET_CUTOFF = { BTC: '2025-08-01' }
27
-
28
- // ------- helpers -------
29
- const val = (x, d) => (x === undefined || x === null ? d : x)
30
-
31
- // normalize date -> 'YYYY-MM-DD' for ECharts time axis
32
- function normalizeDateStr(r) {
33
- // string dates first
34
- const s = r.date ?? r.day ?? (typeof r.ts === 'string' ? r.ts : (typeof r.timestamp === 'string' ? r.timestamp : null))
35
- if (typeof s === 'string') {
36
- if (s.length >= 10) return s.slice(0, 10) // YYYY-MM-DD
37
- }
38
- // epoch millis/seconds
39
- const t = typeof r.ts === 'number' ? r.ts : (typeof r.timestamp === 'number' ? r.timestamp : null)
40
- if (typeof t === 'number') {
41
- const ms = t > 1e12 ? t : t * 1000
42
- return new Date(ms).toISOString().slice(0, 10)
43
- }
44
- return undefined
45
- }
46
-
47
- function toPct(points){
48
- if (!points?.length) return points
49
- const y0 = points[0][1]
50
- if (!Number.isFinite(y0) || y0 === 0) return points
51
- return points.map(([t, y]) => [t, ((y / y0) - 1) * 100])
52
- }
53
-
54
- function sameModel(r, model) {
55
- if (!model) return true
56
- return (
57
- r.model === model ||
58
- r.model_name === model ||
59
- r.base_model === model ||
60
- r.modelId === model
61
- )
62
- }
63
-
64
- function pickSeqByIdsOrFallback(all, sel) {
65
- const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
66
- const byIds = ids.length ? all.filter(r => ids.includes(r.id)) : []
67
- if (byIds.length) return byIds
68
-
69
- // fallback: agent + asset (+model if present)
70
- return all.filter(r =>
71
- r.agent_name === sel.agent_name &&
72
- r.asset === sel.asset &&
73
- sameModel(r, sel.model)
74
- )
75
- }
76
-
77
- async function rebuild() {
78
- if (!props.visible || !root.value) return
79
-
80
- const all = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
81
- const group = new Map()
82
-
83
- for (const sel of (props.selected || [])) {
84
- const assetCode = sel.asset
85
- let seq = pickSeqByIdsOrFallback(all, sel)
86
-
87
- // map/normalize dates + drop rows with no date
88
- seq = seq
89
- .map(r => ({ ...r, __d: normalizeDateStr(r) }))
90
- .filter(r => !!r.__d)
91
-
92
- // sort by date
93
- seq.sort((a, b) => (a.__d > b.__d ? 1 : -1))
94
-
95
- // NO weekend / holiday filter (per your request)
96
-
97
- // asset cutoff
98
- const cutoff = ASSET_CUTOFF[assetCode]
99
- if (cutoff) {
100
- const t0 = new Date(`${cutoff}T00:00:00Z`).getTime()
101
- seq = seq.filter(r => new Date(`${r.__d}T00:00:00Z`).getTime() >= t0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
103
-
104
- group.set(`${sel.agent_name}|${assetCode}|${sel.model ?? ''}`, { sel, seq })
105
- }
106
-
107
- // Build series
108
- const series = []
109
- const assetSet = new Set()
110
-
111
- for (const { sel, seq } of group.values()) {
112
- if (!seq.length) continue
113
- const assetCode = sel.asset
114
- assetSet.add(assetCode)
115
-
116
- const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy)
117
- || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005, label: 'Selected' }
118
-
119
- const equity = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
120
- let points = seq.map((row, i) => [row.__d, val(equity[i], null)])
121
- if (props.mode === 'pct') points = toPct(points)
122
-
123
- series.push({
124
- name: `${sel.agent_name} 路 ${sel.model ?? ''} 路 ${cfg.label}`,
125
- type: 'line',
126
- showSymbol: false,
127
- smooth: false,
128
- lineStyle: { width: 2, color: getStrategyColor(sel.strategy || 'aggressive_lo', false, 0) },
129
- data: points,
130
- })
131
- }
132
-
133
- // Buy & Hold (dashed) per asset
134
- for (const sym of assetSet) {
135
- const entry = [...group.values()].find(v => v.sel.asset === sym && v.seq.length)
136
- if (!entry) continue
137
- const bh = computeBuyHoldEquity(entry.seq, 100000) || []
138
- let points = entry.seq.map((row, i) => [row.__d, val(bh[i], null)])
139
- if (props.mode === 'pct') points = toPct(points)
140
-
141
- series.push({
142
- name: `${sym} 路 Buy&Hold`,
143
- type: 'line',
144
- showSymbol: false,
145
- lineStyle: { width: 2, type: 'dashed', color: '#7c7c7c' },
146
- data: points,
147
- })
148
  }
149
-
150
- // Bold best line
151
- if (series.length) {
152
- let bestIdx = -1, bestVal = -Infinity
153
- series.forEach((s, i) => {
154
- if (s.type !== 'line' || s.lineStyle?.type === 'dashed') return
155
- const y = s.data?.length ? s.data[s.data.length - 1][1] : -Infinity
156
- if (y > bestVal) { bestVal = y; bestIdx = i }
157
- })
158
- if (bestIdx >= 0) series[bestIdx].lineStyle.width = 3.5
159
- }
160
-
161
- const option = {
162
- animation: true,
163
- grid: { left: 56, right: 24, top: 12, bottom: 48 },
164
- xAxis: { type: 'time', axisLabel: { hideOverlap: true } },
165
- yAxis: props.mode === 'pct'
166
- ? { type: 'value', scale: true,
167
- axisLabel: { formatter: v => `${Number(v).toLocaleString(undefined,{maximumFractionDigits:0})}%` } }
168
- : { type: 'value', scale: true,
169
- axisLabel: { formatter: v => Number(v).toLocaleString(undefined,{style:'currency',currency:'USD',maximumFractionDigits:0}) } },
170
- legend: { show: false },
171
- tooltip: { show: false },
172
- dataZoom: [
173
- { type: 'inside', throttle: 50 },
174
- { type: 'slider', height: 14, bottom: 20 },
175
- ],
176
- series,
177
- }
178
-
179
- if (!chart) {
180
- chart = echarts.init(root.value, null, { renderer: 'canvas' })
181
- window.addEventListener('resize', onResize)
182
- }
183
- chart.setOption(option, true)
184
- }
185
-
186
- function onResize(){ chart && chart.resize() }
187
-
188
- onMounted(() => { rebuild() })
189
- onBeforeUnmount(() => {
190
- window.removeEventListener('resize', onResize)
191
- if (chart) { chart.dispose(); chart = null }
192
  })
193
-
194
- watch(() => props.mode, () => rebuild())
195
- watch(() => props.visible, v => { if (v) rebuild() })
196
- watch(() => props.selected, () => rebuild(), { deep: true })
197
  </script>
198
 
199
  <style scoped>
200
- /* container height set inline */
 
201
  </style>
 
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>