feat: add time period selector to competition chart (1D/3D/7D/30D/All)

This commit is contained in:
tinkle-community
2025-12-17 03:51:21 +08:00
parent 96d3ab6cc5
commit b169fcd3d2
3 changed files with 113 additions and 37 deletions

View File

@@ -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) // 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) { func (s *Server) handleEquityHistoryBatch(c *gin.Context) {
var requestBody struct { var requestBody struct {
TraderIDs []string `json:"trader_ids"` 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 // 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) c.JSON(http.StatusOK, result)
return return
} }
@@ -2583,6 +2592,12 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) {
for i := range requestBody.TraderIDs { for i := range requestBody.TraderIDs {
requestBody.TraderIDs[i] = strings.TrimSpace(requestBody.TraderIDs[i]) 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 // 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] requestBody.TraderIDs = requestBody.TraderIDs[:20]
} }
result := s.getEquityHistoryForTraders(requestBody.TraderIDs) result := s.getEquityHistoryForTraders(requestBody.TraderIDs, requestBody.Hours)
c.JSON(http.StatusOK, result) c.JSON(http.StatusOK, result)
} }
// getEquityHistoryForTraders Get historical data for multiple traders // 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) // 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 // 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{}) result := make(map[string]interface{})
histories := make(map[string]interface{}) histories := make(map[string]interface{})
errors := make(map[string]string) 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 // 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 { if err != nil {
errors[traderID] = fmt.Sprintf("Failed to get historical data: %v", err) errors[traderID] = fmt.Sprintf("Failed to get historical data: %v", err)
continue continue

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react' import { useMemo, useState } from 'react'
import { import {
Line, Line,
XAxis, XAxis,
@@ -19,24 +19,39 @@ import { useLanguage } from '../contexts/LanguageContext'
import { t } from '../i18n/translations' import { t } from '../i18n/translations'
import { BarChart3, TrendingUp, TrendingDown, Zap } from 'lucide-react' 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 { interface ComparisonChartProps {
traders: CompetitionTraderData[] traders: CompetitionTraderData[]
} }
export function ComparisonChart({ traders }: ComparisonChartProps) { export function ComparisonChart({ traders }: ComparisonChartProps) {
const { language } = useLanguage() 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 const tradersKey = traders
.map((t) => t.trader_id) .map((t) => t.trader_id)
.sort() .sort()
.join(',') .join(',')
const { data: allTraderHistories, isLoading } = useSWR( const { data: allTraderHistories, isLoading } = useSWR(
traders.length > 0 ? `all-equity-histories-${tradersKey}` : null, traders.length > 0 ? `equity-histories-${tradersKey}-${selectedHours}` : null,
async () => { async () => {
console.log('Fetching equity history with hours:', selectedHours)
const traderIds = traders.map((trader) => trader.trader_id) 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) => { return traders.map((trader) => {
const history = batchData.histories?.[trader.trader_id] || [] const history = batchData.histories?.[trader.trader_id] || []
@@ -56,7 +71,8 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{ {
refreshInterval: 30000, refreshInterval: 30000,
revalidateOnFocus: false, 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) const normalizedTs = normalizeTimestamp(point.timestamp)
if (!timestampMap.has(normalizedTs)) { if (!timestampMap.has(normalizedTs)) {
const time = new Date(normalizedTs).toLocaleTimeString('zh-CN', { const date = new Date(normalizedTs)
hour: '2-digit', // Format time based on selected period
minute: '2-digit', 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, { timestampMap.set(normalizedTs, {
timestamp: normalizedTs, timestamp: normalizedTs,
time, time,
@@ -156,7 +181,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
}) })
return combined return combined
}, [allTraderHistories, traders]) }, [allTraderHistories, traders, selectedHours])
// Get trader color // Get trader color
const traderColor = (traderId: string) => getTraderColor(traders, traderId) const traderColor = (traderId: string) => getTraderColor(traders, traderId)
@@ -310,27 +335,50 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* Mini Stats Bar */} {/* Time Period Selector + Mini Stats Bar */}
<div className="flex items-center gap-3 flex-wrap"> <div className="flex items-center justify-between flex-wrap gap-3">
{traderStats.map((trader, idx) => ( {/* Time Period Buttons */}
<div key={trader.trader_id} <div className="flex items-center gap-1">
className="flex items-center gap-2 px-3 py-1.5 rounded-full transition-all hover:scale-105" {TIME_PERIODS.map((period) => (
style={{ <button
background: idx === 0 ? 'rgba(240, 185, 11, 0.15)' : 'rgba(43, 49, 57, 0.5)', key={period.key}
border: `1px solid ${idx === 0 ? 'rgba(240, 185, 11, 0.3)' : '#2B3139'}` onClick={() => setSelectedPeriod(period.key)}
}}> className="px-3 py-1.5 text-xs font-medium rounded-lg transition-all"
<div className="w-2 h-2 rounded-full" style={{
style={{ background: traderColor(trader.trader_id) }} /> background: selectedPeriod === period.key
<span className="text-xs font-medium truncate max-w-[80px]" ? 'rgba(240, 185, 11, 0.2)'
style={{ color: '#EAECEF' }}> : 'rgba(43, 49, 57, 0.5)',
{trader.trader_name} color: selectedPeriod === period.key ? '#F0B90B' : '#848E9C',
</span> border: `1px solid ${selectedPeriod === period.key ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
<span className="text-xs font-bold mono" }}
style={{ color: trader.currentPnl >= 0 ? '#0ECB81' : '#F6465D' }}> >
{trader.currentPnl >= 0 ? '+' : ''}{trader.currentPnl.toFixed(2)}% {language === 'zh' ? period.label.zh : period.label.en}
</span> </button>
</div> ))}
))} </div>
{/* Mini Stats Bar */}
<div className="flex items-center gap-2 flex-wrap">
{traderStats.slice(0, 3).map((trader, idx) => (
<div key={trader.trader_id}
className="flex items-center gap-2 px-3 py-1.5 rounded-full transition-all hover:scale-105"
style={{
background: idx === 0 ? 'rgba(240, 185, 11, 0.15)' : 'rgba(43, 49, 57, 0.5)',
border: `1px solid ${idx === 0 ? 'rgba(240, 185, 11, 0.3)' : '#2B3139'}`
}}>
<div className="w-2 h-2 rounded-full"
style={{ background: traderColor(trader.trader_id) }} />
<span className="text-xs font-medium truncate max-w-[80px]"
style={{ color: '#EAECEF' }}>
{trader.trader_name}
</span>
<span className="text-xs font-bold mono"
style={{ color: trader.currentPnl >= 0 ? '#0ECB81' : '#F6465D' }}>
{trader.currentPnl >= 0 ? '+' : ''}{trader.currentPnl.toFixed(2)}%
</span>
</div>
))}
</div>
</div> </div>
{/* Chart */} {/* Chart */}

View File

@@ -409,10 +409,12 @@ export const api = {
}, },
// 批量获取多个交易员的历史数据(无需认证) // 批量获取多个交易员的历史数据(无需认证)
async getEquityHistoryBatch(traderIds: string[]): Promise<any> { // hours: 可选参数获取最近N小时的数据0表示全部数据
// 常用值: 24=1天, 72=3天, 168=7天, 720=30天, 0=全部
async getEquityHistoryBatch(traderIds: string[], hours?: number): Promise<any> {
const result = await httpClient.post<any>( const result = await httpClient.post<any>(
`${API_BASE}/equity-history-batch`, `${API_BASE}/equity-history-batch`,
{ trader_ids: traderIds } { trader_ids: traderIds, hours: hours || 0 }
) )
if (!result.success) throw new Error('获取批量历史数据失败') if (!result.success) throw new Error('获取批量历史数据失败')
return result.data! return result.data!