Jimin Huang commited on
Commit
3ac9172
·
1 Parent(s): 222b383

Change settings

Browse files
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
- <div>
3
- <h1>Live</h1>
 
 
 
 
 
 
4
  </div>
5
- </template>
6
 
7
- <script>
8
- export default {
9
- name: 'LiveView'
10
- }
11
- </script>
12
 
13
- <style scoped>
14
- .live-container {
15
- width: 100%;
16
- height: 100%;
17
- }
18
- </style>
 
 
 
 
 
 
 
 
 
 
 
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>