diff --git a/api/server.go b/api/server.go index ff9e532d..974dab3c 100644 --- a/api/server.go +++ b/api/server.go @@ -2540,9 +2540,11 @@ func (s *Server) handleTopTraders(c *gin.Context) { } // handleEquityHistoryBatch Batch get return rate historical data for multiple traders (no authentication required, for performance comparison) +// Supports optional 'hours' parameter to filter data by time range (e.g., hours=24 for last 24 hours) func (s *Server) handleEquityHistoryBatch(c *gin.Context) { var requestBody struct { TraderIDs []string `json:"trader_ids"` + Hours int `json:"hours"` // Optional: filter by last N hours (0 = all data) } // Try to parse POST request JSON body @@ -2573,7 +2575,14 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) { } } - result := s.getEquityHistoryForTraders(traderIDs) + // Parse hours parameter from query + hoursParam := c.Query("hours") + hours := 0 + if hoursParam != "" { + fmt.Sscanf(hoursParam, "%d", &hours) + } + + result := s.getEquityHistoryForTraders(traderIDs, hours) c.JSON(http.StatusOK, result) return } @@ -2583,6 +2592,12 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) { for i := range requestBody.TraderIDs { requestBody.TraderIDs[i] = strings.TrimSpace(requestBody.TraderIDs[i]) } + + // Parse hours parameter from query + hoursParam := c.Query("hours") + if hoursParam != "" { + fmt.Sscanf(hoursParam, "%d", &requestBody.Hours) + } } // Limit to maximum 20 traders to prevent oversized requests @@ -2590,14 +2605,15 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) { requestBody.TraderIDs = requestBody.TraderIDs[:20] } - result := s.getEquityHistoryForTraders(requestBody.TraderIDs) + result := s.getEquityHistoryForTraders(requestBody.TraderIDs, requestBody.Hours) c.JSON(http.StatusOK, result) } // getEquityHistoryForTraders Get historical data for multiple traders // Query directly from database, not dependent on trader in memory (so historical data can be retrieved after restart) // Also appends current real-time data point to ensure chart matches leaderboard -func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]interface{} { +// hours: filter by last N hours (0 = use default limit of 500 records) +func (s *Server) getEquityHistoryForTraders(traderIDs []string, hours int) map[string]interface{} { result := make(map[string]interface{}) histories := make(map[string]interface{}) errors := make(map[string]string) @@ -2624,7 +2640,17 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]inter } // Get equity historical data from new equity table - snapshots, err := s.store.Equity().GetLatest(traderID, 500) + var snapshots []*store.EquitySnapshot + var err error + + if hours > 0 { + // Filter by time range + startTime := now.Add(-time.Duration(hours) * time.Hour) + snapshots, err = s.store.Equity().GetByTimeRange(traderID, startTime, now) + } else { + // Default: get latest 500 records + snapshots, err = s.store.Equity().GetLatest(traderID, 500) + } if err != nil { errors[traderID] = fmt.Sprintf("Failed to get historical data: %v", err) continue diff --git a/web/src/components/ComparisonChart.tsx b/web/src/components/ComparisonChart.tsx index 6beafa86..4c7a08e4 100644 --- a/web/src/components/ComparisonChart.tsx +++ b/web/src/components/ComparisonChart.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { Line, XAxis, @@ -19,24 +19,39 @@ import { useLanguage } from '../contexts/LanguageContext' import { t } from '../i18n/translations' import { BarChart3, TrendingUp, TrendingDown, Zap } from 'lucide-react' +// Time period options: 1D, 3D, 7D, 30D, All +const TIME_PERIODS = [ + { key: '1d', hours: 24, label: { en: '1D', zh: '1天' } }, + { key: '3d', hours: 72, label: { en: '3D', zh: '3天' } }, + { key: '7d', hours: 168, label: { en: '7D', zh: '7天' } }, + { key: '30d', hours: 720, label: { en: '30D', zh: '30天' } }, + { key: 'all', hours: 0, label: { en: 'All', zh: '全部' } }, +] + interface ComparisonChartProps { traders: CompetitionTraderData[] } export function ComparisonChart({ traders }: ComparisonChartProps) { const { language } = useLanguage() + const [selectedPeriod, setSelectedPeriod] = useState('7d') // Default to 7 days - // Generate unique key for SWR + // Get hours for selected period + const selectedHours = TIME_PERIODS.find(p => p.key === selectedPeriod)?.hours || 0 + + // Generate unique key for SWR (include period and hours) const tradersKey = traders .map((t) => t.trader_id) .sort() .join(',') const { data: allTraderHistories, isLoading } = useSWR( - traders.length > 0 ? `all-equity-histories-${tradersKey}` : null, + traders.length > 0 ? `equity-histories-${tradersKey}-${selectedHours}` : null, async () => { + console.log('Fetching equity history with hours:', selectedHours) const traderIds = traders.map((trader) => trader.trader_id) - const batchData = await api.getEquityHistoryBatch(traderIds) + const batchData = await api.getEquityHistoryBatch(traderIds, selectedHours) + console.log('Received data points:', Object.values(batchData.histories || {}).map((h: any) => h?.length)) return traders.map((trader) => { const history = batchData.histories?.[trader.trader_id] || [] @@ -56,7 +71,8 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { { refreshInterval: 30000, revalidateOnFocus: false, - dedupingInterval: 20000, + dedupingInterval: 0, // No deduping for immediate response + keepPreviousData: false, } ) @@ -96,10 +112,19 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { const normalizedTs = normalizeTimestamp(point.timestamp) if (!timestampMap.has(normalizedTs)) { - const time = new Date(normalizedTs).toLocaleTimeString('zh-CN', { - hour: '2-digit', - minute: '2-digit', - }) + const date = new Date(normalizedTs) + // Format time based on selected period + let time: string + if (selectedHours <= 24) { + // 1 day: show HH:mm + time = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) + } else if (selectedHours <= 72) { + // 3 days: show MM/DD HH:mm + time = `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}` + } else { + // 7+ days: show MM/DD + time = `${date.getMonth() + 1}/${date.getDate()}` + } timestampMap.set(normalizedTs, { timestamp: normalizedTs, time, @@ -156,7 +181,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { }) return combined - }, [allTraderHistories, traders]) + }, [allTraderHistories, traders, selectedHours]) // Get trader color const traderColor = (traderId: string) => getTraderColor(traders, traderId) @@ -310,27 +335,50 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { return (