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
+30 -4
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)
// 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
+79 -31
View File
@@ -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 (
<div className="space-y-4">
{/* Mini Stats Bar */}
<div className="flex items-center gap-3 flex-wrap">
{traderStats.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>
))}
{/* Time Period Selector + Mini Stats Bar */}
<div className="flex items-center justify-between flex-wrap gap-3">
{/* Time Period Buttons */}
<div className="flex items-center gap-1">
{TIME_PERIODS.map((period) => (
<button
key={period.key}
onClick={() => setSelectedPeriod(period.key)}
className="px-3 py-1.5 text-xs font-medium rounded-lg transition-all"
style={{
background: selectedPeriod === period.key
? 'rgba(240, 185, 11, 0.2)'
: 'rgba(43, 49, 57, 0.5)',
color: selectedPeriod === period.key ? '#F0B90B' : '#848E9C',
border: `1px solid ${selectedPeriod === period.key ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
}}
>
{language === 'zh' ? period.label.zh : period.label.en}
</button>
))}
</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>
{/* Chart */}
+4 -2
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>(
`${API_BASE}/equity-history-batch`,
{ trader_ids: traderIds }
{ trader_ids: traderIds, hours: hours || 0 }
)
if (!result.success) throw new Error('获取批量历史数据失败')
return result.data!