mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
feat: add time period selector to competition chart (1D/3D/7D/30D/All)
This commit is contained in:
+30
-4
@@ -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
|
||||
|
||||
@@ -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
@@ -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!
|
||||
|
||||
Reference in New Issue
Block a user