Spaces:
Running
Running
Merge pull request #2 from Vincent-J-Z/aws-prod
Browse files- Dockerfile +16 -23
- src/components/AgentFilters.vue +18 -5
- src/views/LeaderboardView.vue +10 -3
- src/views/LiveView.vue +7 -8
Dockerfile
CHANGED
|
@@ -1,33 +1,26 @@
|
|
| 1 |
-
|
| 2 |
-
FROM node:20-alpine AS build
|
| 3 |
WORKDIR /app
|
| 4 |
-
|
| 5 |
-
# deps
|
| 6 |
COPY package.json package-lock.json* ./
|
| 7 |
RUN npm ci --no-audit --no-fund || npm i --no-audit --no-fund
|
| 8 |
|
| 9 |
-
|
|
|
|
|
|
|
| 10 |
COPY . .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
# (These IDs MUST match your Secrets names in the Settings tab)
|
| 14 |
-
RUN --mount=type=secret,id=VITE_SUPABASE_URL,mode=0444,required=true \
|
| 15 |
-
--mount=type=secret,id=VITE_SUPABASE_ANON_KEY,mode=0444,required=true \
|
| 16 |
-
sh -lc '\
|
| 17 |
-
URL="$(cat /run/secrets/VITE_SUPABASE_URL)"; \
|
| 18 |
-
ANON="$(cat /run/secrets/VITE_SUPABASE_ANON_KEY)"; \
|
| 19 |
-
printf "VITE_SUPABASE_URL=%s\nVITE_SUPABASE_ANON_KEY=%s\n" "$URL" "$ANON" > .env.production; \
|
| 20 |
-
echo "--- .env.production ---"; cat .env.production; echo "-----------------------"; \
|
| 21 |
-
npm run build \
|
| 22 |
-
'
|
| 23 |
-
|
| 24 |
-
# ---------- runtime ----------
|
| 25 |
-
FROM node:20-alpine AS runtime
|
| 26 |
WORKDIR /app
|
| 27 |
-
|
| 28 |
-
# serve built assets
|
| 29 |
COPY --from=build /app/dist ./dist
|
| 30 |
-
RUN npm i -g serve@14
|
| 31 |
-
|
| 32 |
EXPOSE 4173
|
| 33 |
CMD ["serve", "-s", "dist", "-l", "4173"]
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-alpine AS deps
|
|
|
|
| 2 |
WORKDIR /app
|
|
|
|
|
|
|
| 3 |
COPY package.json package-lock.json* ./
|
| 4 |
RUN npm ci --no-audit --no-fund || npm i --no-audit --no-fund
|
| 5 |
|
| 6 |
+
FROM node:20-alpine AS build
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
COPY --from=deps /app/node_modules ./node_modules
|
| 9 |
COPY . .
|
| 10 |
+
ARG VITE_SUPABASE_URL
|
| 11 |
+
ARG VITE_SUPABASE_ANON_KEY
|
| 12 |
+
ARG VITE_SUPABASE_SERVICE_ROLE_KEY
|
| 13 |
+
ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL \
|
| 14 |
+
VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY \
|
| 15 |
+
VITE_SUPABASE_SERVICE_ROLE_KEY=$VITE_SUPABASE_SERVICE_ROLE_KEY
|
| 16 |
+
RUN npm run build
|
| 17 |
|
| 18 |
+
FROM node:20-alpine AS runner
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
WORKDIR /app
|
| 20 |
+
ENV NODE_ENV=production
|
|
|
|
| 21 |
COPY --from=build /app/dist ./dist
|
| 22 |
+
RUN npm i -g serve@14.2.1
|
|
|
|
| 23 |
EXPOSE 4173
|
| 24 |
CMD ["serve", "-s", "dist", "-l", "4173"]
|
| 25 |
+
|
| 26 |
+
|
src/components/AgentFilters.vue
CHANGED
|
@@ -186,18 +186,29 @@ export default {
|
|
| 186 |
initializeSlider() {
|
| 187 |
if (this.hasDateBounds) {
|
| 188 |
// 查找 2025-08-01 在日期数组中的索引
|
| 189 |
-
const
|
| 190 |
let startIndex = 0
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
// 如果默认起始日期在范围内,找到对应的索引
|
| 193 |
-
if (
|
| 194 |
startIndex = this.allDates.findIndex(d => {
|
| 195 |
-
const dateStr = d.toISOString().split('T')[0]
|
| 196 |
-
return dateStr ===
|
| 197 |
})
|
| 198 |
// 如果没找到精确日期,找最接近的日期
|
| 199 |
if (startIndex === -1) {
|
| 200 |
-
startIndex = this.allDates.findIndex(d =>
|
|
|
|
|
|
|
|
|
|
| 201 |
if (startIndex === -1) startIndex = 0
|
| 202 |
}
|
| 203 |
}
|
|
@@ -281,6 +292,8 @@ export default {
|
|
| 281 |
// 重置到默认起始日期 2025-08-01
|
| 282 |
this.initializeSlider()
|
| 283 |
this.datesModel = []
|
|
|
|
|
|
|
| 284 |
}
|
| 285 |
},
|
| 286 |
beforeUnmount() {
|
|
|
|
| 186 |
initializeSlider() {
|
| 187 |
if (this.hasDateBounds) {
|
| 188 |
// 查找 2025-08-01 在日期数组中的索引
|
| 189 |
+
const targetDateStr = '2025-08-01'
|
| 190 |
let startIndex = 0
|
| 191 |
|
| 192 |
+
// Normalize date bounds to strings for comparison
|
| 193 |
+
const minDateStr = this.dateBounds.min instanceof Date
|
| 194 |
+
? this.dateBounds.min.toISOString().split('T')[0]
|
| 195 |
+
: (typeof this.dateBounds.min === 'string' ? this.dateBounds.min.split('T')[0] : '')
|
| 196 |
+
const maxDateStr = this.dateBounds.max instanceof Date
|
| 197 |
+
? this.dateBounds.max.toISOString().split('T')[0]
|
| 198 |
+
: (typeof this.dateBounds.max === 'string' ? this.dateBounds.max.split('T')[0] : '')
|
| 199 |
+
|
| 200 |
// 如果默认起始日期在范围内,找到对应的索引
|
| 201 |
+
if (targetDateStr >= minDateStr && targetDateStr <= maxDateStr) {
|
| 202 |
startIndex = this.allDates.findIndex(d => {
|
| 203 |
+
const dateStr = d instanceof Date ? d.toISOString().split('T')[0] : (typeof d === 'string' ? d.split('T')[0] : '')
|
| 204 |
+
return dateStr === targetDateStr
|
| 205 |
})
|
| 206 |
// 如果没找到精确日期,找最接近的日期
|
| 207 |
if (startIndex === -1) {
|
| 208 |
+
startIndex = this.allDates.findIndex(d => {
|
| 209 |
+
const dateStr = d instanceof Date ? d.toISOString().split('T')[0] : (typeof d === 'string' ? d.split('T')[0] : '')
|
| 210 |
+
return dateStr >= targetDateStr
|
| 211 |
+
})
|
| 212 |
if (startIndex === -1) startIndex = 0
|
| 213 |
}
|
| 214 |
}
|
|
|
|
| 292 |
// 重置到默认起始日期 2025-08-01
|
| 293 |
this.initializeSlider()
|
| 294 |
this.datesModel = []
|
| 295 |
+
// 触发 slider 结束事件
|
| 296 |
+
this.onSliderEnd()
|
| 297 |
}
|
| 298 |
},
|
| 299 |
beforeUnmount() {
|
src/views/LeaderboardView.vue
CHANGED
|
@@ -94,7 +94,6 @@ import { countNonTradingDaysBetweenForAsset, countTradingDaysBetweenForAsset } f
|
|
| 94 |
import { computeBuyHoldEquity, computeStrategyEquity, calculateMetricsFromSeries, computeWinRate } from '../lib/perf.js'
|
| 95 |
import { STRATEGIES } from '../lib/strategies.js'
|
| 96 |
import emailjs from 'emailjs-com'
|
| 97 |
-
|
| 98 |
export default {
|
| 99 |
name: 'LeaderboardView',
|
| 100 |
components: { AgentTable, AgentFilters, AssetsFilter, CompareChartE, Dialog, InputText },
|
|
@@ -208,9 +207,17 @@ export default {
|
|
| 208 |
try {
|
| 209 |
const isCrypto = row.asset === 'BTC' || row.asset === 'ETH'
|
| 210 |
const seriesAll = Array.isArray(row.series) ? row.series : []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
const inRange = seriesAll.filter(p => {
|
| 212 |
-
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
| 214 |
})
|
| 215 |
if (!inRange.length) return null
|
| 216 |
// Get correct strategy config - row.strategy is the ID, we need the strategy type
|
|
|
|
| 94 |
import { computeBuyHoldEquity, computeStrategyEquity, calculateMetricsFromSeries, computeWinRate } from '../lib/perf.js'
|
| 95 |
import { STRATEGIES } from '../lib/strategies.js'
|
| 96 |
import emailjs from 'emailjs-com'
|
|
|
|
| 97 |
export default {
|
| 98 |
name: 'LeaderboardView',
|
| 99 |
components: { AgentTable, AgentFilters, AssetsFilter, CompareChartE, Dialog, InputText },
|
|
|
|
| 207 |
try {
|
| 208 |
const isCrypto = row.asset === 'BTC' || row.asset === 'ETH'
|
| 209 |
const seriesAll = Array.isArray(row.series) ? row.series : []
|
| 210 |
+
|
| 211 |
+
// Normalize dates to YYYY-MM-DD format for comparison to avoid timezone issues
|
| 212 |
+
const startDateStr = start instanceof Date ? start.toISOString().split('T')[0] : (typeof start === 'string' ? start.split('T')[0] : start)
|
| 213 |
+
const endDateStr = end instanceof Date ? end.toISOString().split('T')[0] : (typeof end === 'string' ? end.split('T')[0] : end)
|
| 214 |
+
|
| 215 |
const inRange = seriesAll.filter(p => {
|
| 216 |
+
if (!p || !p.date) return false
|
| 217 |
+
// Extract date part (YYYY-MM-DD) from the date string
|
| 218 |
+
const dateStr = typeof p.date === 'string' ? p.date.split('T')[0] : new Date(p.date).toISOString().split('T')[0]
|
| 219 |
+
// Use string comparison for dates to avoid timezone issues
|
| 220 |
+
return dateStr >= startDateStr && dateStr <= endDateStr
|
| 221 |
})
|
| 222 |
if (!inRange.length) return null
|
| 223 |
// Get correct strategy config - row.strategy is the ID, we need the strategy type
|
src/views/LiveView.vue
CHANGED
|
@@ -204,6 +204,7 @@ const asset = ref('BTC')
|
|
| 204 |
const rowsRef = ref([])
|
| 205 |
let allDecisions = []
|
| 206 |
const cards = shallowRef([])
|
|
|
|
| 207 |
const refreshing = ref(false)
|
| 208 |
|
| 209 |
let unsubscribe = null
|
|
@@ -318,24 +319,22 @@ async function buildSeq(sel) {
|
|
| 318 |
: allDecisions.filter(r => r.agent_name === agentName && r.asset === assetCode && r.model === model)
|
| 319 |
|
| 320 |
seq.sort((a,b) => (a.date > b.date ? 1 : -1))
|
| 321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
// if using decision_ids, data is already prefiltered
|
| 323 |
if (!ids.length) {
|
| 324 |
const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
|
| 325 |
if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
|
| 326 |
-
|
| 327 |
-
const cutoff = ASSET_CUTOFF[assetCode]
|
| 328 |
-
if (cutoff) {
|
| 329 |
-
const t0 = new Date(cutoff + 'T00:00:00Z')
|
| 330 |
-
seq = seq.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
|
| 331 |
-
}
|
| 332 |
}
|
| 333 |
-
|
| 334 |
return seq
|
| 335 |
}
|
| 336 |
|
| 337 |
async function computeEquities(sel) {
|
| 338 |
const seq = await buildSeq(sel)
|
|
|
|
| 339 |
if (!seq.length) return null
|
| 340 |
|
| 341 |
const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
|
|
|
|
| 204 |
const rowsRef = ref([])
|
| 205 |
let allDecisions = []
|
| 206 |
const cards = shallowRef([])
|
| 207 |
+
window.cards = cards
|
| 208 |
const refreshing = ref(false)
|
| 209 |
|
| 210 |
let unsubscribe = null
|
|
|
|
| 319 |
: allDecisions.filter(r => r.agent_name === agentName && r.asset === assetCode && r.model === model)
|
| 320 |
|
| 321 |
seq.sort((a,b) => (a.date > b.date ? 1 : -1))
|
| 322 |
+
const cutoff = ASSET_CUTOFF[assetCode]
|
| 323 |
+
if (cutoff) {
|
| 324 |
+
const t0 = new Date(cutoff + 'T00:00:00Z')
|
| 325 |
+
seq = seq.filter(r => new Date(r.date + 'T00:00:00Z') >= t0)
|
| 326 |
+
}
|
| 327 |
// if using decision_ids, data is already prefiltered
|
| 328 |
if (!ids.length) {
|
| 329 |
const isCrypto = assetCode === 'BTC' || assetCode === 'ETH'
|
| 330 |
if (!isCrypto) seq = await filterRowsToNyseTradingDays(seq)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
}
|
|
|
|
| 332 |
return seq
|
| 333 |
}
|
| 334 |
|
| 335 |
async function computeEquities(sel) {
|
| 336 |
const seq = await buildSeq(sel)
|
| 337 |
+
|
| 338 |
if (!seq.length) return null
|
| 339 |
|
| 340 |
const cfg = (STRATEGIES || []).find(s => s.id === sel.strategy) || { strategy: 'long_only', tradingMode: 'aggressive', fee: 0.0005 }
|