Jimin Huang commited on
Commit
71d3e4f
1 Parent(s): 5216ba1

Change settings

Browse files
Files changed (1) hide show
  1. src/components/CompareChartE.vue +73 -77
src/components/CompareChartE.vue CHANGED
@@ -5,8 +5,6 @@
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 } from '../lib/dataService'
11
  import {
12
  computeStrategyEquity,
@@ -15,79 +13,95 @@ import {
15
  getStrategyColor,
16
  } from '../lib/strategies'
17
 
18
-
19
- function isNyseTradingDay(dateStr) {
20
- const d = new Date(`${dateStr}T00:00:00Z`)
21
- const day = d.getUTCDay() // 0=Sun, 6=Sat
22
- return day !== 0 && day !== 6
23
- }
24
- async function filterRowsToNyseTradingDays(seq) {
25
- return Array.isArray(seq) ? seq.filter(r => isNyseTradingDay(r.date)) : []
26
- }
27
-
28
- // ---------- props ----------
29
  const props = defineProps({
30
- selected: { type: Array, default: () => [] }, // [{agent_name, asset, model, strategy, decision_ids?}, ...]
31
  visible: { type: Boolean, default: true },
32
  mode: { type: String, default: 'usd' }, // 'usd' | 'pct'
33
  })
34
 
35
- // ---------- refs ----------
36
  const root = ref(null)
37
  let chart = null
38
 
39
- // ---------- constants ----------
40
- const ASSET_CUTOFF = {
41
- BTC: '2025-08-01', // ignore earlier data
42
- }
43
 
44
- // ---------- helpers ----------
45
  const val = (x, d) => (x === undefined || x === null ? d : x)
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
- async function getAllRows() {
55
- // rely on dataService already loaded by the view
56
- const rows = Array.isArray(dataService.tableRows) ? dataService.tableRows : []
57
- return rows.map(r => ({ ...r })) // shallow copy
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
 
60
- // ---------- core build ----------
61
  async function rebuild() {
62
  if (!props.visible || !root.value) return
63
 
64
- const all = await getAllRows()
65
- const group = new Map() // key -> { sel, seq }
66
 
67
- // Collect sequences per requested selection
68
  for (const sel of (props.selected || [])) {
69
- const { agent_name: agent, asset: assetCode, model } = sel
70
- const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
71
 
72
- let seq = ids.length
73
- ? all.filter(r => ids.includes(r.id))
74
- : all.filter(r => r.agent_name === agent && r.asset === assetCode && r.model === model)
 
75
 
76
  // sort by date
77
- seq.sort((a, b) => (a.date > b.date ? 1 : -1))
78
 
79
- // trading-day filter for equities; crypto untouched
80
- const isCrypto = assetCode === 'BTC' || assetCode === 'ETH' || assetCode === 'SOL' || assetCode === 'BNB' || assetCode === 'DOGE' || assetCode === 'XRP'
81
- let filtered = isCrypto ? seq : await filterRowsToNyseTradingDays(seq)
82
 
83
- // asset cutoff (e.g., BTC from 2025-08-01)
84
  const cutoff = ASSET_CUTOFF[assetCode]
85
  if (cutoff) {
86
- const t0 = new Date(`${cutoff}T00:00:00Z`)
87
- filtered = filtered.filter(r => new Date(`${r.date}T00:00:00Z`) >= t0)
88
  }
89
 
90
- group.set(`${agent}|${assetCode}|${model}`, { sel, seq: filtered })
91
  }
92
 
93
  // Build series
@@ -103,26 +117,25 @@ async function rebuild() {
103
  || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005, label: 'Selected' }
104
 
105
  const equity = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode) || []
106
- let points = seq.map((row, i) => [row.date, val(equity[i], null)])
107
  if (props.mode === 'pct') points = toPct(points)
108
 
109
  series.push({
110
- name: `${sel.agent_name} 路 ${sel.model} 路 ${cfg.label}`,
111
  type: 'line',
112
  showSymbol: false,
113
  smooth: false,
114
  lineStyle: { width: 2, color: getStrategyColor(sel.strategy || 'aggressive_lo', false, 0) },
115
- // no areaStyle
116
  data: points,
117
  })
118
  }
119
 
120
- // Add Buy&Hold (dashed) per asset
121
  for (const sym of assetSet) {
122
- const entry = [...group.values()].find(v => v.sel.asset === sym)
123
- if (!entry || !entry.seq.length) continue
124
  const bh = computeBuyHoldEquity(entry.seq, 100000) || []
125
- let points = entry.seq.map((row, i) => [row.date, val(bh[i], null)])
126
  if (props.mode === 'pct') points = toPct(points)
127
 
128
  series.push({
@@ -134,7 +147,7 @@ async function rebuild() {
134
  })
135
  }
136
 
137
- // Bold best line (highest last Y)
138
  if (series.length) {
139
  let bestIdx = -1, bestVal = -Infinity
140
  series.forEach((s, i) => {
@@ -145,26 +158,17 @@ async function rebuild() {
145
  if (bestIdx >= 0) series[bestIdx].lineStyle.width = 3.5
146
  }
147
 
148
- // Compose option
149
  const option = {
150
  animation: true,
151
  grid: { left: 56, right: 24, top: 12, bottom: 48 },
152
  xAxis: { type: 'time', axisLabel: { hideOverlap: true } },
153
  yAxis: props.mode === 'pct'
154
- ? {
155
- type: 'value', scale: true,
156
- axisLabel: { formatter: v => `${Number(v).toLocaleString(undefined, { maximumFractionDigits: 0 })}%` }
157
- }
158
- : {
159
- type: 'value', scale: true,
160
- axisLabel: {
161
- formatter: v => Number(v).toLocaleString(undefined, {
162
- style: 'currency', currency: 'USD', maximumFractionDigits: 0
163
- })
164
- }
165
- },
166
  legend: { show: false },
167
- tooltip: { show: false }, // disabled
168
  dataZoom: [
169
  { type: 'inside', throttle: 50 },
170
  { type: 'slider', height: 14, bottom: 20 },
@@ -172,34 +176,26 @@ async function rebuild() {
172
  series,
173
  }
174
 
175
- // init / set
176
  if (!chart) {
177
  chart = echarts.init(root.value, null, { renderer: 'canvas' })
178
- window.addEventListener('resize', handleResize)
179
  }
180
  chart.setOption(option, true)
181
  }
182
 
183
- function handleResize() {
184
- if (chart) chart.resize()
185
- }
186
 
187
- // ---------- lifecycle ----------
188
  onMounted(() => { rebuild() })
189
  onBeforeUnmount(() => {
190
- window.removeEventListener('resize', handleResize)
191
- if (chart) {
192
- chart.dispose()
193
- chart = null
194
- }
195
  })
196
 
197
- // Rebuild when inputs change
198
  watch(() => props.mode, () => rebuild())
199
  watch(() => props.visible, v => { if (v) rebuild() })
200
  watch(() => props.selected, () => rebuild(), { deep: true })
201
  </script>
202
 
203
  <style scoped>
204
- /* no extra styles needed; container height set inline */
205
  </style>
 
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,
 
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
 
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({
 
147
  })
148
  }
149
 
150
+ // Bold best line
151
  if (series.length) {
152
  let bestIdx = -1, bestVal = -Infinity
153
  series.forEach((s, i) => {
 
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 },
 
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>