Spaces:
Running
Running
Jimin Huang
commited on
Commit
路
71d3e4f
1
Parent(s):
5216ba1
Change settings
Browse files- 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
|
| 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 |
-
//
|
| 40 |
-
const ASSET_CUTOFF = {
|
| 41 |
-
BTC: '2025-08-01', // ignore earlier data
|
| 42 |
-
}
|
| 43 |
|
| 44 |
-
//
|
| 45 |
const val = (x, d) => (x === undefined || x === null ? d : x)
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
-
// ---------- core build ----------
|
| 61 |
async function rebuild() {
|
| 62 |
if (!props.visible || !root.value) return
|
| 63 |
|
| 64 |
-
const all =
|
| 65 |
-
const group = new Map()
|
| 66 |
|
| 67 |
-
// Collect sequences per requested selection
|
| 68 |
for (const sel of (props.selected || [])) {
|
| 69 |
-
const
|
| 70 |
-
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
| 75 |
|
| 76 |
// sort by date
|
| 77 |
-
seq.sort((a, b) => (a.
|
| 78 |
|
| 79 |
-
//
|
| 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
|
| 84 |
const cutoff = ASSET_CUTOFF[assetCode]
|
| 85 |
if (cutoff) {
|
| 86 |
-
const t0 = new Date(`${cutoff}T00:00:00Z`)
|
| 87 |
-
|
| 88 |
}
|
| 89 |
|
| 90 |
-
group.set(`${
|
| 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.
|
| 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 |
-
//
|
| 121 |
for (const sym of assetSet) {
|
| 122 |
-
const entry = [...group.values()].find(v => v.sel.asset === sym)
|
| 123 |
-
if (!entry
|
| 124 |
const bh = computeBuyHoldEquity(entry.seq, 100000) || []
|
| 125 |
-
let points = entry.seq.map((row, i) => [row.
|
| 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
|
| 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 |
-
|
| 156 |
-
|
| 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 },
|
| 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',
|
| 179 |
}
|
| 180 |
chart.setOption(option, true)
|
| 181 |
}
|
| 182 |
|
| 183 |
-
function
|
| 184 |
-
if (chart) chart.resize()
|
| 185 |
-
}
|
| 186 |
|
| 187 |
-
// ---------- lifecycle ----------
|
| 188 |
onMounted(() => { rebuild() })
|
| 189 |
onBeforeUnmount(() => {
|
| 190 |
-
window.removeEventListener('resize',
|
| 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 |
-
/*
|
| 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>
|