fix: remove leverage from position history and fix max drawdown calculation

- Remove leverage column from position history table
- Fix max drawdown calculation using virtual starting equity
- Add formula tooltips for all statistics metrics
- Remove leverage parameter from pnlPct calculation
This commit is contained in:
tinkle-community
2025-12-28 21:03:37 +08:00
parent 0f3ba1382a
commit 1c32c2ab08
3 changed files with 856 additions and 6 deletions
+11 -5
View File
@@ -668,19 +668,25 @@ func calculateSharpeRatioFromPnls(pnls []float64) float64 {
}
// calculateMaxDrawdownFromPnls calculates maximum drawdown
// Uses a virtual starting equity of 10000 to calculate percentage drawdown
func calculateMaxDrawdownFromPnls(pnls []float64) float64 {
if len(pnls) == 0 {
return 0
}
var cumulative, peak, maxDD float64
// Use virtual starting equity for percentage calculation
const startingEquity = 10000.0
equity := startingEquity
peak := startingEquity
var maxDD float64
for _, pnl := range pnls {
cumulative += pnl
if cumulative > peak {
peak = cumulative
equity += pnl
if equity > peak {
peak = equity
}
if peak > 0 {
dd := (peak - cumulative) / peak * 100
dd := (peak - equity) / peak * 100
if dd > maxDD {
maxDD = dd
}
+3 -1
View File
@@ -2,6 +2,7 @@ package trader
import (
"database/sql"
"math"
"nofx/store"
"testing"
"time"
@@ -382,7 +383,8 @@ func TestHyperliquidBugScenario(t *testing.T) {
}
expectedTotalPnL := 4.72 + 5.0 // Sum of both close trades
if totalPnL != expectedTotalPnL {
// Use tolerance for floating point comparison
if math.Abs(totalPnL-expectedTotalPnL) > 0.01 {
t.Errorf("Expected total PnL %.2f, got %.2f", expectedTotalPnL, totalPnL)
}
}
+842
View File
@@ -0,0 +1,842 @@
import { useState, useEffect, useMemo } from 'react'
import { api } from '../lib/api'
import { useLanguage } from '../contexts/LanguageContext'
import { t } from '../i18n/translations'
import type {
HistoricalPosition,
TraderStats,
SymbolStats,
DirectionStats,
} from '../types'
interface PositionHistoryProps {
traderId: string
}
// Format number with proper decimals
function formatNumber(value: number, decimals: number = 2): string {
if (Math.abs(value) >= 1000000) {
return (value / 1000000).toFixed(2) + 'M'
}
if (Math.abs(value) >= 1000) {
return (value / 1000).toFixed(2) + 'K'
}
return value.toFixed(decimals)
}
// Format price with proper decimals
function formatPrice(price: number): string {
if (!price || price === 0) return '-'
if (price >= 1000) return price.toFixed(2)
if (price >= 1) return price.toFixed(4)
return price.toFixed(6)
}
// Format duration from minutes
function formatDuration(minutes: number): string {
if (!minutes || minutes <= 0) return '-'
if (minutes < 60) return `${minutes.toFixed(0)}m`
if (minutes < 1440) return `${(minutes / 60).toFixed(1)}h`
return `${(minutes / 1440).toFixed(1)}d`
}
// Format date
function formatDate(dateStr: string): string {
if (!dateStr) return '-'
const date = new Date(dateStr)
if (isNaN(date.getTime())) return '-'
return date.toLocaleDateString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
}
// Stats Card Component with formula tooltip
function StatCard({
title,
value,
suffix,
color,
icon,
subtitle,
formula,
}: {
title: string
value: string | number
suffix?: string
color?: string
icon: string
subtitle?: string
formula?: string
}) {
const [showTooltip, setShowTooltip] = useState(false)
return (
<div
className="rounded-lg p-4 transition-all duration-200 hover:scale-[1.02]"
style={{
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
border: '1px solid #2B3139',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)',
}}
>
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">{icon}</span>
<span className="text-xs" style={{ color: '#848E9C' }}>
{title}
</span>
{formula && (
<div className="relative">
<span
className="cursor-help text-xs px-1 rounded"
style={{ color: '#848E9C', background: '#2B3139' }}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
?
</span>
{showTooltip && (
<div
className="absolute z-50 left-0 top-6 p-2 rounded text-xs whitespace-pre-wrap min-w-[200px] max-w-[300px]"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
color: '#EAECEF',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4)',
}}
>
{formula}
</div>
)}
</div>
)}
</div>
<div className="flex items-baseline gap-1">
<span
className="text-xl font-bold font-mono"
style={{ color: color || '#EAECEF' }}
>
{value}
</span>
{suffix && (
<span className="text-sm" style={{ color: '#848E9C' }}>
{suffix}
</span>
)}
</div>
{subtitle && (
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
{subtitle}
</div>
)}
</div>
)
}
// Symbol Stats Row
function SymbolStatsRow({ stat }: { stat: SymbolStats }) {
const totalPnl = stat.total_pnl || 0
const winRate = stat.win_rate || 0
const pnlColor = totalPnl >= 0 ? '#0ECB81' : '#F6465D'
const winRateColor =
winRate >= 60 ? '#0ECB81' : winRate >= 40 ? '#F0B90B' : '#F6465D'
return (
<div
className="flex items-center justify-between p-3 rounded-lg transition-all duration-200 hover:bg-white/5"
style={{ borderBottom: '1px solid #2B3139' }}
>
<div className="flex items-center gap-3">
<span className="font-mono font-semibold" style={{ color: '#EAECEF' }}>
{(stat.symbol || '').replace('USDT', '')}
</span>
<span className="text-xs" style={{ color: '#848E9C' }}>
{stat.total_trades || 0} trades
</span>
</div>
<div className="flex items-center gap-6">
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}>
Win Rate
</div>
<div className="font-mono font-semibold" style={{ color: winRateColor }}>
{winRate.toFixed(1)}%
</div>
</div>
<div className="text-right min-w-[80px]">
<div className="text-xs" style={{ color: '#848E9C' }}>
P&L
</div>
<div className="font-mono font-semibold" style={{ color: pnlColor }}>
{totalPnl >= 0 ? '+' : ''}
{formatNumber(totalPnl)}
</div>
</div>
</div>
</div>
)
}
// Direction Stats Card
function DirectionStatsCard({ stat, language }: { stat: DirectionStats; language: 'en' | 'zh' }) {
const isLong = (stat.side || '').toLowerCase() === 'long'
const iconColor = isLong ? '#0ECB81' : '#F6465D'
const totalPnl = stat.total_pnl || 0
const winRate = stat.win_rate || 0
const tradeCount = stat.trade_count || 0
const avgPnl = stat.avg_pnl || 0
const pnlColor = totalPnl >= 0 ? '#0ECB81' : '#F6465D'
return (
<div
className="rounded-lg p-4"
style={{
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
border: `1px solid ${iconColor}33`,
}}
>
<div className="flex items-center gap-2 mb-3">
<span className="text-xl">{isLong ? '📈' : '📉'}</span>
<span
className="font-bold uppercase"
style={{ color: iconColor }}
>
{stat.side || 'Unknown'}
</span>
</div>
<div className="grid grid-cols-4 gap-4">
<div>
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
{t('positionHistory.trades', language)}
</div>
<div className="font-mono font-semibold" style={{ color: '#EAECEF' }}>
{tradeCount}
</div>
</div>
<div>
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
{t('positionHistory.winRate', language)}
</div>
<div
className="font-mono font-semibold"
style={{
color:
winRate >= 60
? '#0ECB81'
: winRate >= 40
? '#F0B90B'
: '#F6465D',
}}
>
{winRate.toFixed(1)}%
</div>
</div>
<div>
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
{t('positionHistory.totalPnL', language)}
</div>
<div className="font-mono font-semibold" style={{ color: pnlColor }}>
{totalPnl >= 0 ? '+' : ''}
{formatNumber(totalPnl)}
</div>
</div>
<div>
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>
{t('positionHistory.avgPnL', language)}
</div>
<div className="font-mono font-semibold" style={{ color: avgPnl >= 0 ? '#0ECB81' : '#F6465D' }}>
{avgPnl >= 0 ? '+' : ''}
{formatNumber(avgPnl)}
</div>
</div>
</div>
</div>
)
}
// Position Row Component
function PositionRow({ position }: { position: HistoricalPosition }) {
const side = position.side || ''
const isLong = side.toUpperCase() === 'LONG'
const realizedPnl = position.realized_pnl || 0
const isProfitable = realizedPnl >= 0
const sideColor = isLong ? '#0ECB81' : '#F6465D'
const pnlColor = isProfitable ? '#0ECB81' : '#F6465D'
// Calculate holding time
const entryTime = position.entry_time ? new Date(position.entry_time).getTime() : 0
const exitTime = position.exit_time ? new Date(position.exit_time).getTime() : 0
const holdingMinutes = entryTime && exitTime && exitTime > entryTime ? (exitTime - entryTime) / 60000 : 0
// Calculate PnL percentage based on entry price
const entryPrice = position.entry_price || 0
const exitPrice = position.exit_price || 0
let pnlPct = 0
if (entryPrice > 0) {
if (isLong) {
pnlPct = ((exitPrice - entryPrice) / entryPrice) * 100
} else {
pnlPct = ((entryPrice - exitPrice) / entryPrice) * 100
}
}
// Use entry_quantity for display (original position size)
const displayQty = position.entry_quantity || position.quantity || 0
return (
<tr
className="transition-all duration-200 hover:bg-white/5"
style={{ borderBottom: '1px solid #2B3139' }}
>
{/* Symbol */}
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<span className="font-mono font-semibold" style={{ color: '#EAECEF' }}>
{(position.symbol || '').replace('USDT', '')}
</span>
<span
className="px-2 py-0.5 rounded text-xs font-semibold uppercase"
style={{
background: `${sideColor}22`,
color: sideColor,
border: `1px solid ${sideColor}44`,
}}
>
{side}
</span>
</div>
</td>
{/* Entry Price */}
<td className="py-3 px-4 text-right font-mono" style={{ color: '#EAECEF' }}>
{formatPrice(entryPrice)}
</td>
{/* Exit Price */}
<td className="py-3 px-4 text-right font-mono" style={{ color: '#EAECEF' }}>
{formatPrice(exitPrice)}
</td>
{/* Quantity */}
<td className="py-3 px-4 text-right font-mono" style={{ color: '#848E9C' }}>
{displayQty.toFixed(4)}
</td>
{/* P&L */}
<td className="py-3 px-4 text-right">
<div className="font-mono font-semibold" style={{ color: pnlColor }}>
{isProfitable ? '+' : ''}
{formatNumber(realizedPnl)}
</div>
<div className="text-xs" style={{ color: pnlColor }}>
{pnlPct >= 0 ? '+' : ''}
{pnlPct.toFixed(2)}%
</div>
</td>
{/* Fee */}
<td className="py-3 px-4 text-right font-mono text-xs" style={{ color: '#848E9C' }}>
-{(position.fee || 0).toFixed(2)}
</td>
{/* Duration */}
<td className="py-3 px-4 text-center text-sm" style={{ color: '#848E9C' }}>
{formatDuration(holdingMinutes)}
</td>
{/* Exit Time */}
<td className="py-3 px-4 text-right text-xs" style={{ color: '#848E9C' }}>
{formatDate(position.exit_time)}
</td>
</tr>
)
}
export function PositionHistory({ traderId }: PositionHistoryProps) {
const { language } = useLanguage()
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [positions, setPositions] = useState<HistoricalPosition[]>([])
const [stats, setStats] = useState<TraderStats | null>(null)
const [symbolStats, setSymbolStats] = useState<SymbolStats[]>([])
const [directionStats, setDirectionStats] = useState<DirectionStats[]>([])
// Filter state
const [filterSymbol, setFilterSymbol] = useState<string>('all')
const [filterSide, setFilterSide] = useState<string>('all')
const [sortBy, setSortBy] = useState<'time' | 'pnl' | 'pnl_pct'>('time')
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
setError(null)
const data = await api.getPositionHistory(traderId, 200)
setPositions(data.positions || [])
setStats(data.stats)
setSymbolStats(data.symbol_stats || [])
setDirectionStats(data.direction_stats || [])
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load history')
} finally {
setLoading(false)
}
}
if (traderId) {
fetchData()
}
}, [traderId])
// Get unique symbols for filter
const uniqueSymbols = useMemo(() => {
const symbols = new Set(positions.map((p) => p.symbol))
return Array.from(symbols).sort()
}, [positions])
// Filtered and sorted positions
const filteredPositions = useMemo(() => {
let result = [...positions]
// Apply filters
if (filterSymbol !== 'all') {
result = result.filter((p) => p.symbol === filterSymbol)
}
if (filterSide !== 'all') {
result = result.filter(
(p) => (p.side || '').toUpperCase() === filterSide.toUpperCase()
)
}
// Apply sorting
result.sort((a, b) => {
let comparison = 0
switch (sortBy) {
case 'time':
comparison =
new Date(a.exit_time || 0).getTime() - new Date(b.exit_time || 0).getTime()
break
case 'pnl':
comparison = (a.realized_pnl || 0) - (b.realized_pnl || 0)
break
case 'pnl_pct': {
const aPrice = a.entry_price || 1
const bPrice = b.entry_price || 1
const aPct = ((a.exit_price || 0) - aPrice) / aPrice * 100
const bPct = ((b.exit_price || 0) - bPrice) / bPrice * 100
comparison = aPct - bPct
break
}
}
return sortOrder === 'desc' ? -comparison : comparison
})
return result
}, [positions, filterSymbol, filterSide, sortBy, sortOrder])
// Calculate profit/loss ratio (avg win / avg loss)
const profitLossRatio = useMemo(() => {
if (!stats) return 0
const avgWin = stats.avg_win || 0
const avgLoss = stats.avg_loss || 0
if (avgLoss === 0) return avgWin > 0 ? Infinity : 0
return avgWin / avgLoss
}, [stats])
if (loading) {
return (
<div
className="flex items-center justify-center p-12"
style={{ color: '#848E9C' }}
>
<div className="animate-spin mr-3">
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
</div>
{t('positionHistory.loading', language)}
</div>
)
}
if (error) {
return (
<div
className="rounded-lg p-6 text-center"
style={{
background: 'rgba(246, 70, 93, 0.1)',
border: '1px solid rgba(246, 70, 93, 0.3)',
color: '#F6465D',
}}
>
{error}
</div>
)
}
if (positions.length === 0) {
return (
<div
className="rounded-lg p-12 text-center"
style={{
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
border: '1px solid #2B3139',
}}
>
<div className="text-4xl mb-4">📊</div>
<div className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('positionHistory.noHistory', language)}
</div>
<div style={{ color: '#848E9C' }}>
{t('positionHistory.noHistoryDesc', language)}
</div>
</div>
)
}
return (
<div className="space-y-6">
{/* Overall Stats - Row 1: Core Metrics */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
<StatCard
icon="📊"
title={t('positionHistory.totalTrades', language)}
value={stats.total_trades || 0}
subtitle={t('positionHistory.winLoss', language, { win: stats.win_trades || 0, loss: stats.loss_trades || 0 })}
formula={language === 'zh'
? '总交易次数 = 所有已平仓位数量'
: 'Total Trades = Count of all closed positions'}
/>
<StatCard
icon="🎯"
title={t('positionHistory.winRate', language)}
value={(stats.win_rate || 0).toFixed(1)}
suffix="%"
color={
(stats.win_rate || 0) >= 60
? '#0ECB81'
: (stats.win_rate || 0) >= 40
? '#F0B90B'
: '#F6465D'
}
formula={language === 'zh'
? `胜率 = 盈利交易数 / 总交易数 × 100%\n= ${stats.win_trades || 0} / ${stats.total_trades || 0} × 100%`
: `Win Rate = Winning Trades / Total Trades × 100%\n= ${stats.win_trades || 0} / ${stats.total_trades || 0} × 100%`}
/>
<StatCard
icon="💰"
title={t('positionHistory.totalPnL', language)}
value={((stats.total_pnl || 0) >= 0 ? '+' : '') + formatNumber(stats.total_pnl || 0)}
color={(stats.total_pnl || 0) >= 0 ? '#0ECB81' : '#F6465D'}
subtitle={`${t('positionHistory.fee', language)}: -${formatNumber(stats.total_fee || 0)}`}
formula={language === 'zh'
? '总盈亏 = Σ(每笔已平仓位的 realized_pnl)\n不含手续费'
: 'Total P&L = Σ(realized_pnl of each closed position)\nExcluding fees'}
/>
<StatCard
icon="📈"
title={t('positionHistory.profitFactor', language)}
value={(stats.profit_factor || 0).toFixed(2)}
color={(stats.profit_factor || 0) >= 1.5 ? '#0ECB81' : (stats.profit_factor || 0) >= 1 ? '#F0B90B' : '#F6465D'}
subtitle={t('positionHistory.profitFactorDesc', language)}
formula={language === 'zh'
? '盈利因子 = 总盈利 / 总亏损\n>1.5 优秀, >1 盈利, <1 亏损'
: 'Profit Factor = Total Profit / Total Loss\n>1.5 Excellent, >1 Profitable, <1 Loss'}
/>
<StatCard
icon="⚖️"
title={t('positionHistory.plRatio', language)}
value={profitLossRatio === Infinity ? '∞' : profitLossRatio.toFixed(2)}
color={profitLossRatio >= 1.5 ? '#0ECB81' : profitLossRatio >= 1 ? '#F0B90B' : '#F6465D'}
subtitle={t('positionHistory.plRatioDesc', language)}
formula={language === 'zh'
? `盈亏比 = 平均盈利 / 平均亏损\n= ${formatNumber(stats.avg_win || 0)} / ${formatNumber(stats.avg_loss || 0)}`
: `P/L Ratio = Avg Win / Avg Loss\n= ${formatNumber(stats.avg_win || 0)} / ${formatNumber(stats.avg_loss || 0)}`}
/>
</div>
)}
{/* Overall Stats - Row 2: Advanced Metrics */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
<StatCard
icon="📉"
title={t('positionHistory.sharpeRatio', language)}
value={(stats.sharpe_ratio || 0).toFixed(2)}
color={(stats.sharpe_ratio || 0) >= 1 ? '#0ECB81' : (stats.sharpe_ratio || 0) >= 0 ? '#F0B90B' : '#F6465D'}
subtitle={t('positionHistory.sharpeRatioDesc', language)}
formula={language === 'zh'
? '夏普比率 = 平均收益 / 收益标准差\n衡量风险调整后的收益\n>1 良好, >2 优秀'
: 'Sharpe Ratio = Mean Return / Std Dev\nMeasures risk-adjusted return\n>1 Good, >2 Excellent'}
/>
<StatCard
icon="🔻"
title={t('positionHistory.maxDrawdown', language)}
value={(stats.max_drawdown_pct || 0).toFixed(1)}
suffix="%"
color={(stats.max_drawdown_pct || 0) <= 10 ? '#0ECB81' : (stats.max_drawdown_pct || 0) <= 20 ? '#F0B90B' : '#F6465D'}
formula={language === 'zh'
? '最大回撤 = (峰值 - 谷值) / 峰值 × 100%\n基于虚拟起始资金10000计算\n衡量最大亏损幅度'
: 'Max Drawdown = (Peak - Trough) / Peak × 100%\nBased on virtual starting equity of 10000\nMeasures largest loss from peak'}
/>
<StatCard
icon="🏆"
title={t('positionHistory.avgWin', language)}
value={'+' + formatNumber(stats.avg_win || 0)}
color="#0ECB81"
formula={language === 'zh'
? `平均盈利 = 总盈利 / 盈利交易数\n盈利交易数: ${stats.win_trades || 0}`
: `Avg Win = Total Profit / Winning Trades\nWinning Trades: ${stats.win_trades || 0}`}
/>
<StatCard
icon="💸"
title={t('positionHistory.avgLoss', language)}
value={'-' + formatNumber(stats.avg_loss || 0)}
color="#F6465D"
formula={language === 'zh'
? `平均亏损 = 总亏损 / 亏损交易数\n亏损交易数: ${stats.loss_trades || 0}`
: `Avg Loss = Total Loss / Losing Trades\nLosing Trades: ${stats.loss_trades || 0}`}
/>
<StatCard
icon="💵"
title={t('positionHistory.netPnL', language)}
value={((stats.total_pnl || 0) - (stats.total_fee || 0) >= 0 ? '+' : '') + formatNumber((stats.total_pnl || 0) - (stats.total_fee || 0))}
color={(stats.total_pnl || 0) - (stats.total_fee || 0) >= 0 ? '#0ECB81' : '#F6465D'}
subtitle={t('positionHistory.netPnLDesc', language)}
formula={language === 'zh'
? `净盈亏 = 总盈亏 - 手续费\n= ${formatNumber(stats.total_pnl || 0)} - ${formatNumber(stats.total_fee || 0)}`
: `Net P&L = Total P&L - Fees\n= ${formatNumber(stats.total_pnl || 0)} - ${formatNumber(stats.total_fee || 0)}`}
/>
</div>
)}
{/* Direction Stats */}
{directionStats.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{directionStats.map((stat) => (
<DirectionStatsCard key={stat.side} stat={stat} language={language} />
))}
</div>
)}
{/* Symbol Performance */}
{symbolStats.length > 0 && (
<div
className="rounded-lg p-4"
style={{
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
border: '1px solid #2B3139',
}}
>
<div className="flex items-center gap-2 mb-4">
<span className="text-lg">🏅</span>
<span className="font-semibold" style={{ color: '#EAECEF' }}>
{t('positionHistory.symbolPerformance', language)}
</span>
</div>
<div className="space-y-1">
{symbolStats.slice(0, 10).map((stat) => (
<SymbolStatsRow key={stat.symbol} stat={stat} />
))}
</div>
</div>
)}
{/* Position List */}
<div
className="rounded-lg overflow-hidden"
style={{
background: 'linear-gradient(135deg, #1E2329 0%, #181C21 100%)',
border: '1px solid #2B3139',
}}
>
{/* Filters */}
<div
className="flex flex-wrap items-center gap-4 p-4"
style={{ borderBottom: '1px solid #2B3139' }}
>
<div className="flex items-center gap-2">
<span className="text-sm" style={{ color: '#848E9C' }}>
{t('positionHistory.symbol', language)}:
</span>
<select
value={filterSymbol}
onChange={(e) => setFilterSymbol(e.target.value)}
className="rounded px-3 py-1.5 text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
>
<option value="all">{t('positionHistory.allSymbols', language)}</option>
{uniqueSymbols.map((symbol) => (
<option key={symbol} value={symbol}>
{(symbol || '').replace('USDT', '')}
</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<span className="text-sm" style={{ color: '#848E9C' }}>
{t('positionHistory.side', language)}:
</span>
<div className="flex rounded overflow-hidden" style={{ border: '1px solid #2B3139' }}>
{['all', 'LONG', 'SHORT'].map((side) => (
<button
key={side}
onClick={() => setFilterSide(side)}
className="px-3 py-1.5 text-sm capitalize transition-colors"
style={{
background: filterSide === side ? '#2B3139' : 'transparent',
color: filterSide === side ? '#EAECEF' : '#848E9C',
}}
>
{side === 'all' ? t('positionHistory.all', language) : side}
</button>
))}
</div>
</div>
<div className="flex items-center gap-2 ml-auto">
<span className="text-sm" style={{ color: '#848E9C' }}>
{t('positionHistory.sort', language)}:
</span>
<select
value={`${sortBy}-${sortOrder}`}
onChange={(e) => {
const [by, order] = e.target.value.split('-') as [
'time' | 'pnl' | 'pnl_pct',
'asc' | 'desc',
]
setSortBy(by)
setSortOrder(order)
}}
className="rounded px-3 py-1.5 text-sm"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
>
<option value="time-desc">{t('positionHistory.latestFirst', language)}</option>
<option value="time-asc">{t('positionHistory.oldestFirst', language)}</option>
<option value="pnl-desc">{t('positionHistory.highestPnL', language)}</option>
<option value="pnl-asc">{t('positionHistory.lowestPnL', language)}</option>
</select>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr style={{ background: '#0B0E11' }}>
<th
className="py-3 px-4 text-left text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
>
{t('positionHistory.symbol', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
>
{t('positionHistory.entry', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
>
{t('positionHistory.exit', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
>
{t('positionHistory.qty', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
>
{t('positionHistory.pnl', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
>
{t('positionHistory.fee', language)}
</th>
<th
className="py-3 px-4 text-center text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
>
{t('positionHistory.duration', language)}
</th>
<th
className="py-3 px-4 text-right text-xs font-semibold uppercase tracking-wider"
style={{ color: '#848E9C' }}
>
{t('positionHistory.closedAt', language)}
</th>
</tr>
</thead>
<tbody>
{filteredPositions.map((position) => (
<PositionRow key={position.id} position={position} />
))}
</tbody>
</table>
</div>
{/* Footer */}
<div
className="flex items-center justify-between p-4 text-sm"
style={{ borderTop: '1px solid #2B3139', color: '#848E9C' }}
>
<span>
{t('positionHistory.showingPositions', language, { count: filteredPositions.length, total: positions.length })}
</span>
{filteredPositions.length > 0 && (
<span>
{t('positionHistory.totalPnL', language)}:{' '}
<span
style={{
color:
filteredPositions.reduce((sum, p) => sum + (p.realized_pnl || 0), 0) >= 0
? '#0ECB81'
: '#F6465D',
}}
>
{filteredPositions.reduce((sum, p) => sum + (p.realized_pnl || 0), 0) >= 0
? '+'
: ''}
{formatNumber(
filteredPositions.reduce((sum, p) => sum + (p.realized_pnl || 0), 0)
)}
</span>
</span>
)}
</div>
</div>
</div>
)
}