Spaces:
Running
Running
Jimin Huang
commited on
Commit
·
3ac9172
1
Parent(s):
222b383
Change settings
Browse files- package-lock.json +33 -5
- package.json +2 -0
- src/components/CompareChartE.vue +157 -0
- src/views/LiveView.vue +70 -14
package-lock.json
CHANGED
|
@@ -17,6 +17,7 @@
|
|
| 17 |
"@supabase/supabase-js": "^2.45.4",
|
| 18 |
"chart.js": "^4.5.1",
|
| 19 |
"chartjs-plugin-zoom": "^2.2.0",
|
|
|
|
| 20 |
"emailjs-com": "^3.2.0",
|
| 21 |
"hammerjs": "^2.0.8",
|
| 22 |
"install": "^0.13.0",
|
|
@@ -26,6 +27,7 @@
|
|
| 26 |
"primeicons": "^7.0.0",
|
| 27 |
"primevue": "^4.1.0",
|
| 28 |
"vue": "^3.4.38",
|
|
|
|
| 29 |
"vue-router": "^4.4.5"
|
| 30 |
},
|
| 31 |
"devDependencies": {
|
|
@@ -484,7 +486,6 @@
|
|
| 484 |
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
|
| 485 |
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
|
| 486 |
"license": "MIT",
|
| 487 |
-
"peer": true,
|
| 488 |
"dependencies": {
|
| 489 |
"@fortawesome/fontawesome-common-types": "7.1.0"
|
| 490 |
},
|
|
@@ -1175,7 +1176,6 @@
|
|
| 1175 |
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
| 1176 |
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
| 1177 |
"license": "MIT",
|
| 1178 |
-
"peer": true,
|
| 1179 |
"dependencies": {
|
| 1180 |
"@kurkle/color": "^0.3.0"
|
| 1181 |
},
|
|
@@ -1208,6 +1208,15 @@
|
|
| 1208 |
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
|
| 1209 |
"license": "MIT"
|
| 1210 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1211 |
"node_modules/emailjs-com": {
|
| 1212 |
"version": "3.2.0",
|
| 1213 |
"resolved": "https://registry.npmjs.org/emailjs-com/-/emailjs-com-3.2.0.tgz",
|
|
@@ -3530,7 +3539,6 @@
|
|
| 3530 |
"version": "4.0.3",
|
| 3531 |
"inBundle": true,
|
| 3532 |
"license": "MIT",
|
| 3533 |
-
"peer": true,
|
| 3534 |
"engines": {
|
| 3535 |
"node": ">=12"
|
| 3536 |
},
|
|
@@ -3872,6 +3880,11 @@
|
|
| 3872 |
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
| 3873 |
"license": "MIT"
|
| 3874 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3875 |
"node_modules/undici-types": {
|
| 3876 |
"version": "7.14.0",
|
| 3877 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
|
|
@@ -3884,7 +3897,6 @@
|
|
| 3884 |
"integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
|
| 3885 |
"dev": true,
|
| 3886 |
"license": "MIT",
|
| 3887 |
-
"peer": true,
|
| 3888 |
"dependencies": {
|
| 3889 |
"esbuild": "^0.21.3",
|
| 3890 |
"postcss": "^8.4.43",
|
|
@@ -3944,7 +3956,6 @@
|
|
| 3944 |
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
| 3945 |
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
| 3946 |
"license": "MIT",
|
| 3947 |
-
"peer": true,
|
| 3948 |
"dependencies": {
|
| 3949 |
"@vue/compiler-dom": "3.5.22",
|
| 3950 |
"@vue/compiler-sfc": "3.5.22",
|
|
@@ -3961,6 +3972,15 @@
|
|
| 3961 |
}
|
| 3962 |
}
|
| 3963 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3964 |
"node_modules/vue-router": {
|
| 3965 |
"version": "4.5.1",
|
| 3966 |
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
|
@@ -4012,6 +4032,14 @@
|
|
| 4012 |
"optional": true
|
| 4013 |
}
|
| 4014 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4015 |
}
|
| 4016 |
}
|
| 4017 |
}
|
|
|
|
| 17 |
"@supabase/supabase-js": "^2.45.4",
|
| 18 |
"chart.js": "^4.5.1",
|
| 19 |
"chartjs-plugin-zoom": "^2.2.0",
|
| 20 |
+
"echarts": "^6.0.0",
|
| 21 |
"emailjs-com": "^3.2.0",
|
| 22 |
"hammerjs": "^2.0.8",
|
| 23 |
"install": "^0.13.0",
|
|
|
|
| 27 |
"primeicons": "^7.0.0",
|
| 28 |
"primevue": "^4.1.0",
|
| 29 |
"vue": "^3.4.38",
|
| 30 |
+
"vue-echarts": "^8.0.1",
|
| 31 |
"vue-router": "^4.4.5"
|
| 32 |
},
|
| 33 |
"devDependencies": {
|
|
|
|
| 486 |
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
|
| 487 |
"integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
|
| 488 |
"license": "MIT",
|
|
|
|
| 489 |
"dependencies": {
|
| 490 |
"@fortawesome/fontawesome-common-types": "7.1.0"
|
| 491 |
},
|
|
|
|
| 1176 |
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
| 1177 |
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
| 1178 |
"license": "MIT",
|
|
|
|
| 1179 |
"dependencies": {
|
| 1180 |
"@kurkle/color": "^0.3.0"
|
| 1181 |
},
|
|
|
|
| 1208 |
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
|
| 1209 |
"license": "MIT"
|
| 1210 |
},
|
| 1211 |
+
"node_modules/echarts": {
|
| 1212 |
+
"version": "6.0.0",
|
| 1213 |
+
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
|
| 1214 |
+
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
|
| 1215 |
+
"dependencies": {
|
| 1216 |
+
"tslib": "2.3.0",
|
| 1217 |
+
"zrender": "6.0.0"
|
| 1218 |
+
}
|
| 1219 |
+
},
|
| 1220 |
"node_modules/emailjs-com": {
|
| 1221 |
"version": "3.2.0",
|
| 1222 |
"resolved": "https://registry.npmjs.org/emailjs-com/-/emailjs-com-3.2.0.tgz",
|
|
|
|
| 3539 |
"version": "4.0.3",
|
| 3540 |
"inBundle": true,
|
| 3541 |
"license": "MIT",
|
|
|
|
| 3542 |
"engines": {
|
| 3543 |
"node": ">=12"
|
| 3544 |
},
|
|
|
|
| 3880 |
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
| 3881 |
"license": "MIT"
|
| 3882 |
},
|
| 3883 |
+
"node_modules/tslib": {
|
| 3884 |
+
"version": "2.3.0",
|
| 3885 |
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
| 3886 |
+
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
| 3887 |
+
},
|
| 3888 |
"node_modules/undici-types": {
|
| 3889 |
"version": "7.14.0",
|
| 3890 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
|
|
|
|
| 3897 |
"integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
|
| 3898 |
"dev": true,
|
| 3899 |
"license": "MIT",
|
|
|
|
| 3900 |
"dependencies": {
|
| 3901 |
"esbuild": "^0.21.3",
|
| 3902 |
"postcss": "^8.4.43",
|
|
|
|
| 3956 |
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
| 3957 |
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
| 3958 |
"license": "MIT",
|
|
|
|
| 3959 |
"dependencies": {
|
| 3960 |
"@vue/compiler-dom": "3.5.22",
|
| 3961 |
"@vue/compiler-sfc": "3.5.22",
|
|
|
|
| 3972 |
}
|
| 3973 |
}
|
| 3974 |
},
|
| 3975 |
+
"node_modules/vue-echarts": {
|
| 3976 |
+
"version": "8.0.1",
|
| 3977 |
+
"resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-8.0.1.tgz",
|
| 3978 |
+
"integrity": "sha512-23rJTFLu1OUEGRWjJGmdGt8fP+8+ja1gVgzMYPIPaHWpXegcO1viIAaeu2H4QHESlVeHzUAHIxKXGrwjsyXAaA==",
|
| 3979 |
+
"peerDependencies": {
|
| 3980 |
+
"echarts": "^6.0.0",
|
| 3981 |
+
"vue": "^3.3.0"
|
| 3982 |
+
}
|
| 3983 |
+
},
|
| 3984 |
"node_modules/vue-router": {
|
| 3985 |
"version": "4.5.1",
|
| 3986 |
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
|
|
|
| 4032 |
"optional": true
|
| 4033 |
}
|
| 4034 |
}
|
| 4035 |
+
},
|
| 4036 |
+
"node_modules/zrender": {
|
| 4037 |
+
"version": "6.0.0",
|
| 4038 |
+
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
|
| 4039 |
+
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
|
| 4040 |
+
"dependencies": {
|
| 4041 |
+
"tslib": "2.3.0"
|
| 4042 |
+
}
|
| 4043 |
}
|
| 4044 |
}
|
| 4045 |
}
|
package.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
| 18 |
"@supabase/supabase-js": "^2.45.4",
|
| 19 |
"chart.js": "^4.5.1",
|
| 20 |
"chartjs-plugin-zoom": "^2.2.0",
|
|
|
|
| 21 |
"emailjs-com": "^3.2.0",
|
| 22 |
"hammerjs": "^2.0.8",
|
| 23 |
"install": "^0.13.0",
|
|
@@ -27,6 +28,7 @@
|
|
| 27 |
"primeicons": "^7.0.0",
|
| 28 |
"primevue": "^4.1.0",
|
| 29 |
"vue": "^3.4.38",
|
|
|
|
| 30 |
"vue-router": "^4.4.5"
|
| 31 |
},
|
| 32 |
"devDependencies": {
|
|
|
|
| 18 |
"@supabase/supabase-js": "^2.45.4",
|
| 19 |
"chart.js": "^4.5.1",
|
| 20 |
"chartjs-plugin-zoom": "^2.2.0",
|
| 21 |
+
"echarts": "^6.0.0",
|
| 22 |
"emailjs-com": "^3.2.0",
|
| 23 |
"hammerjs": "^2.0.8",
|
| 24 |
"install": "^0.13.0",
|
|
|
|
| 28 |
"primeicons": "^7.0.0",
|
| 29 |
"primevue": "^4.1.0",
|
| 30 |
"vue": "^3.4.38",
|
| 31 |
+
"vue-echarts": "^8.0.1",
|
| 32 |
"vue-router": "^4.4.5"
|
| 33 |
},
|
| 34 |
"devDependencies": {
|
src/components/CompareChartE.vue
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
export default defineComponent({
|
| 25 |
+
name: 'CompareChartE',
|
| 26 |
+
components: { VChart },
|
| 27 |
+
props: {
|
| 28 |
+
selected: { type: Array, default: () => [] }, // [{agent_name, asset, model, strategy, decision_ids?}]
|
| 29 |
+
visible: { type: Boolean, default: true }
|
| 30 |
+
},
|
| 31 |
+
data(){ return { option: {} } },
|
| 32 |
+
watch: {
|
| 33 |
+
selected: { deep: true, handler(){ this.rebuild() } },
|
| 34 |
+
visible(v){ if (v) this.$nextTick(() => this.rebuild()) }
|
| 35 |
+
},
|
| 36 |
+
mounted(){ this.$nextTick(() => this.rebuild()) },
|
| 37 |
+
methods: {
|
| 38 |
+
async getAll(){
|
| 39 |
+
let all = getAllDecisions() || []
|
| 40 |
+
if (!all.length) {
|
| 41 |
+
try { const cached = await readAllRawDecisions(); if (cached?.length) all = cached } catch(_) {}
|
| 42 |
+
}
|
| 43 |
+
return all
|
| 44 |
+
},
|
| 45 |
+
async rebuild(){
|
| 46 |
+
if (!this.visible) return
|
| 47 |
+
const selected = Array.isArray(this.selected) ? this.selected : []
|
| 48 |
+
const all = await this.getAll()
|
| 49 |
+
const groupKeyToSeq = new Map()
|
| 50 |
+
|
| 51 |
+
// Build grouped sequences from selected (borrowed from CompareChart.vue)
|
| 52 |
+
for (const sel of selected) {
|
| 53 |
+
const { agent_name: agent, asset, model } = sel
|
| 54 |
+
const ids = Array.isArray(sel.decision_ids) ? sel.decision_ids : []
|
| 55 |
+
let seq = ids.length ? all.filter(r => ids.includes(r.id))
|
| 56 |
+
: all.filter(r => r.agent_name === agent && r.asset === asset && r.model === model)
|
| 57 |
+
seq.sort((a,b) => (a.date > b.date ? 1 : -1))
|
| 58 |
+
const isCrypto = asset === 'BTC' || asset === 'ETH'
|
| 59 |
+
const filtered = isCrypto ? seq : await filterRowsToNyseTradingDays(seq)
|
| 60 |
+
groupKeyToSeq.set(`${agent}|${asset}|${model}`, { sel, seq: filtered })
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
const keys = Array.from(groupKeyToSeq.keys())
|
| 64 |
+
const labels = keys.length ? (groupKeyToSeq.get(keys[0]).seq || []).map(s => s.date) : []
|
| 65 |
+
|
| 66 |
+
// Build ECharts series: strategy lines per selection + asset Buy&Hold once
|
| 67 |
+
const series = []
|
| 68 |
+
const legend = []
|
| 69 |
+
const assets = new Set()
|
| 70 |
+
|
| 71 |
+
// Per-agent color hue: reuse your getStrategyColor hue system
|
| 72 |
+
const agentColorIndex = new Map()
|
| 73 |
+
|
| 74 |
+
for (const [gk, { sel, seq }] of groupKeyToSeq.entries()) {
|
| 75 |
+
if (!seq.length) continue
|
| 76 |
+
const agent = sel.agent_name
|
| 77 |
+
const asset = sel.asset
|
| 78 |
+
assets.add(asset)
|
| 79 |
+
|
| 80 |
+
const idx = agentColorIndex.get(agent) ?? agentColorIndex.size
|
| 81 |
+
agentColorIndex.set(agent, idx)
|
| 82 |
+
|
| 83 |
+
// strategy config
|
| 84 |
+
const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005, label: 'Selected' }
|
| 85 |
+
const stratSeries = computeStrategyEquity(seq, 100000, cfg.fee, cfg.strategy, cfg.tradingMode)
|
| 86 |
+
if (stratSeries?.length) {
|
| 87 |
+
const color = getStrategyColor(sel.strategy || 'aggressive_lo', false, idx)
|
| 88 |
+
const vals = labels.length ? stratSeries.slice(0, labels.length) : stratSeries
|
| 89 |
+
const name = `${agent} · ${sel.model} · ${cfg.label}`
|
| 90 |
+
legend.push(name)
|
| 91 |
+
series.push({
|
| 92 |
+
name,
|
| 93 |
+
type: 'line',
|
| 94 |
+
showSymbol: false,
|
| 95 |
+
smooth: false,
|
| 96 |
+
emphasis: { focus: 'series' },
|
| 97 |
+
lineStyle: { width: 2 },
|
| 98 |
+
data: vals
|
| 99 |
+
})
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Add Buy&Hold baseline once per asset (in black)
|
| 104 |
+
for (const asset of assets) {
|
| 105 |
+
// find any seq for this asset to build BH labels
|
| 106 |
+
const entry = [...groupKeyToSeq.values()].find(v => v.sel.asset === asset)
|
| 107 |
+
if (!entry) continue
|
| 108 |
+
const bh = computeBuyHoldEquity(entry.seq, 100000) || []
|
| 109 |
+
const baseline = labels.length ? bh.slice(0, labels.length) : bh
|
| 110 |
+
series.push({
|
| 111 |
+
name: `${asset} · Buy&Hold`,
|
| 112 |
+
type: 'line',
|
| 113 |
+
showSymbol: false,
|
| 114 |
+
lineStyle: { width: 1.5 },
|
| 115 |
+
color: getStrategyColor('', true, 0),
|
| 116 |
+
data: baseline
|
| 117 |
+
})
|
| 118 |
+
legend.push(`${asset} · Buy&Hold`)
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
this.option = {
|
| 122 |
+
animation: true,
|
| 123 |
+
grid: { left: 40, right: 20, top: 30, bottom: 40 },
|
| 124 |
+
tooltip: {
|
| 125 |
+
trigger: 'axis',
|
| 126 |
+
valueFormatter: v => typeof v === 'number'
|
| 127 |
+
? v.toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 2 })
|
| 128 |
+
: v
|
| 129 |
+
},
|
| 130 |
+
legend: { type: 'scroll', bottom: 0, data: legend },
|
| 131 |
+
xAxis: {
|
| 132 |
+
type: 'category',
|
| 133 |
+
data: labels,
|
| 134 |
+
axisLabel: { formatter: v => v?.slice?.(2) } // compact YY-MM-DD
|
| 135 |
+
},
|
| 136 |
+
yAxis: {
|
| 137 |
+
type: 'value',
|
| 138 |
+
scale: true,
|
| 139 |
+
axisLabel: {
|
| 140 |
+
formatter: v => v.toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })
|
| 141 |
+
}
|
| 142 |
+
},
|
| 143 |
+
dataZoom: [
|
| 144 |
+
{ type: 'inside', throttle: 50 },
|
| 145 |
+
{ type: 'slider', height: 16, bottom: 22 }
|
| 146 |
+
],
|
| 147 |
+
series
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
})
|
| 152 |
+
</script>
|
| 153 |
+
|
| 154 |
+
<style scoped>
|
| 155 |
+
.chart-wrap { width: 100%; }
|
| 156 |
+
.h-96 { height: 24rem; }
|
| 157 |
+
</style>
|
src/views/LiveView.vue
CHANGED
|
@@ -1,18 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
<template>
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
</div>
|
| 5 |
-
</template>
|
| 6 |
|
| 7 |
-
<
|
| 8 |
-
export default {
|
| 9 |
-
name: 'LiveView'
|
| 10 |
-
}
|
| 11 |
-
</script>
|
| 12 |
|
| 13 |
-
<
|
| 14 |
-
.
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
}
|
| 18 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- src/views/LiveView.vue -->
|
| 2 |
+
<script setup>
|
| 3 |
+
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
| 4 |
+
import CompareChartE from '@/components/CompareChartE.vue'
|
| 5 |
+
import { dataService } from '@/lib/dataService'
|
| 6 |
+
|
| 7 |
+
const ASSETS = ['BTC','ETH','SOL','BNB','DOGE','XRP']
|
| 8 |
+
const asset = ref(ASSETS[0])
|
| 9 |
+
const agents = ref([])
|
| 10 |
+
let unsub = null
|
| 11 |
+
|
| 12 |
+
function score(row){ return typeof row.balance === 'number' ? row.balance : -Infinity }
|
| 13 |
+
|
| 14 |
+
const winners = computed(() => {
|
| 15 |
+
const rows = (agents.value || []).filter(r => r.asset === asset.value)
|
| 16 |
+
const byAgent = new Map()
|
| 17 |
+
for (const r of rows) {
|
| 18 |
+
const k = r.agent_name
|
| 19 |
+
const cur = byAgent.get(k)
|
| 20 |
+
if (!cur || score(r) > score(cur)) byAgent.set(k, r)
|
| 21 |
+
}
|
| 22 |
+
return [...byAgent.values()]
|
| 23 |
+
})
|
| 24 |
+
|
| 25 |
+
const winnersForChart = computed(() =>
|
| 26 |
+
winners.value.map(w => ({
|
| 27 |
+
agent_name: w.agent_name,
|
| 28 |
+
asset: w.asset,
|
| 29 |
+
model: w.model,
|
| 30 |
+
strategy: w.strategy,
|
| 31 |
+
decision_ids: Array.isArray(w.decision_ids) ? w.decision_ids : undefined
|
| 32 |
+
}))
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
const winnersSorted = computed(() => [...winners.value].sort((a,b) => score(b) - score(a)))
|
| 36 |
+
|
| 37 |
+
const fmtUSD = n => (n ?? 0).toLocaleString(undefined,{ style:'currency', currency:'USD', maximumFractionDigits:2 })
|
| 38 |
+
|
| 39 |
+
onMounted(async () => {
|
| 40 |
+
unsub = dataService.subscribe((snap) => { agents.value = snap.agents || [] })
|
| 41 |
+
if (!dataService.loaded && !dataService.loading) { try { await dataService.load(false) } catch {} }
|
| 42 |
+
})
|
| 43 |
+
onBeforeUnmount(() => unsub && unsub())
|
| 44 |
+
</script>
|
| 45 |
+
|
| 46 |
<template>
|
| 47 |
+
<div class="p-4 space-y-4">
|
| 48 |
+
<div class="flex gap-2 flex-wrap">
|
| 49 |
+
<button
|
| 50 |
+
v-for="a in ASSETS" :key="a"
|
| 51 |
+
class="px-3 py-1 rounded-full border"
|
| 52 |
+
:class="a===asset ? 'bg-black text-white' : 'bg-white'"
|
| 53 |
+
@click="asset=a"
|
| 54 |
+
>{{ a }}</button>
|
| 55 |
</div>
|
|
|
|
| 56 |
|
| 57 |
+
<CompareChartE :selected="winnersForChart" :visible="true" />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
| 60 |
+
<div v-for="row in winnersSorted" :key="row.agent_name+'|'+row.asset+'|'+row.model" class="p-3 rounded-xl border flex justify-between items-center">
|
| 61 |
+
<div>
|
| 62 |
+
<div class="font-semibold">{{ row.agent_name }}</div>
|
| 63 |
+
<div class="text-xs opacity-70">{{ row.model }}</div>
|
| 64 |
+
</div>
|
| 65 |
+
<div class="text-right">
|
| 66 |
+
<div class="font-bold text-lg">{{ fmtUSD(row.balance) }}</div>
|
| 67 |
+
<div class="text-xs opacity-70">
|
| 68 |
+
EOD {{ (row.end_date || row.last_nav_ts) ? new Date(row.end_date || row.last_nav_ts).toLocaleDateString() : '-' }}
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</template>
|