feat: add metric formula tooltips with KaTeX rendering

This commit is contained in:
tinkle-community
2025-12-29 00:28:20 +08:00
parent 7b30b687eb
commit 4776fc37ce
6 changed files with 2956 additions and 60 deletions

File diff suppressed because it is too large Load Diff

26
web/package-lock.json generated
View File

@@ -15,6 +15,7 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.23.24",
"katex": "^0.16.27",
"lightweight-charts": "^5.1.0",
"lucide-react": "^0.552.0",
"react": "^18.3.1",
@@ -5793,6 +5794,31 @@
"node": ">=4.0"
}
},
"node_modules/katex": {
"version": "0.16.27",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.27.tgz",
"integrity": "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
],
"license": "MIT",
"dependencies": {
"commander": "^8.3.0"
},
"bin": {
"katex": "cli.js"
}
},
"node_modules/katex/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",

View File

@@ -21,6 +21,7 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.23.24",
"katex": "^0.16.27",
"lightweight-charts": "^5.1.0",
"lucide-react": "^0.552.0",
"react": "^18.3.1",

View File

@@ -43,6 +43,7 @@ import { useLanguage } from '../contexts/LanguageContext'
import { t } from '../i18n/translations'
import { confirmToast } from '../lib/notify'
import { DecisionCard } from './DecisionCard'
import { MetricTooltip } from './MetricTooltip'
import type {
BacktestStatusPayload,
BacktestPositionStatus,
@@ -79,6 +80,8 @@ function StatCard({
suffix,
trend,
color = '#EAECEF',
metricKey,
language = 'en',
}: {
icon: typeof TrendingUp
label: string
@@ -86,6 +89,8 @@ function StatCard({
suffix?: string
trend?: 'up' | 'down' | 'neutral'
color?: string
metricKey?: string
language?: string
}) {
const trendColors = {
up: '#0ECB81',
@@ -103,6 +108,9 @@ function StatCard({
<span className="text-xs" style={{ color: '#848E9C' }}>
{label}
</span>
{metricKey && (
<MetricTooltip metricKey={metricKey} language={language} size={12} />
)}
</div>
<div className="flex items-baseline gap-1">
<span className="text-xl font-bold" style={{ color }}>
@@ -1779,6 +1787,7 @@ export function BacktestPage() {
label={language === 'zh' ? '当前净值' : 'Equity'}
value={(status?.equity ?? 0).toFixed(2)}
suffix="USDT"
language={language}
/>
<StatCard
icon={TrendingUp}
@@ -1786,17 +1795,23 @@ export function BacktestPage() {
value={`${(metrics?.total_return_pct ?? 0).toFixed(2)}%`}
trend={(metrics?.total_return_pct ?? 0) >= 0 ? 'up' : 'down'}
color={(metrics?.total_return_pct ?? 0) >= 0 ? '#0ECB81' : '#F6465D'}
metricKey="total_return"
language={language}
/>
<StatCard
icon={AlertTriangle}
label={language === 'zh' ? '最大回撤' : 'Max DD'}
value={`${(metrics?.max_drawdown_pct ?? 0).toFixed(2)}%`}
color="#F6465D"
metricKey="max_drawdown"
language={language}
/>
<StatCard
icon={BarChart3}
label={language === 'zh' ? '夏普比率' : 'Sharpe'}
value={(metrics?.sharpe_ratio ?? 0).toFixed(2)}
metricKey="sharpe_ratio"
language={language}
/>
</div>
@@ -1856,16 +1871,18 @@ export function BacktestPage() {
{metrics && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-4">
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="flex items-center gap-1 text-xs" style={{ color: '#848E9C' }}>
{language === 'zh' ? '胜率' : 'Win Rate'}
<MetricTooltip metricKey="win_rate" language={language} size={11} />
</div>
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
{(metrics.win_rate ?? 0).toFixed(1)}%
</div>
</div>
<div className="p-3 rounded-lg" style={{ background: '#1E2329' }}>
<div className="text-xs" style={{ color: '#848E9C' }}>
<div className="flex items-center gap-1 text-xs" style={{ color: '#848E9C' }}>
{language === 'zh' ? '盈亏因子' : 'Profit Factor'}
<MetricTooltip metricKey="profit_factor" language={language} size={11} />
</div>
<div className="text-lg font-bold" style={{ color: '#EAECEF' }}>
{(metrics.profit_factor ?? 0).toFixed(2)}

View File

@@ -0,0 +1,364 @@
import { useState, useRef, useEffect, useCallback } from 'react'
import { createPortal } from 'react-dom'
import { HelpCircle } from 'lucide-react'
import katex from 'katex'
import 'katex/dist/katex.min.css'
export interface MetricDefinition {
key: string
nameEn: string
nameZh: string
formula: string // LaTeX formula
descriptionEn: string
descriptionZh: string
}
// Metric definitions with formulas
export const METRIC_DEFINITIONS: Record<string, MetricDefinition> = {
total_return: {
key: 'total_return',
nameEn: 'Total Return',
nameZh: '总收益率',
formula: 'R_{total} = \\frac{V_{end} - V_{start}}{V_{start}} \\times 100\\%',
descriptionEn: 'Measures overall portfolio performance from start to end',
descriptionZh: '衡量投资组合从开始到结束的整体收益表现',
},
annualized_return: {
key: 'annualized_return',
nameEn: 'Annualized Return',
nameZh: '年化收益率',
formula: 'R_{ann} = \\left(1 + R_{total}\\right)^{\\frac{252}{n}} - 1',
descriptionEn: 'Standardized yearly return rate (252 trading days)',
descriptionZh: '标准化年度收益率按252个交易日计算',
},
max_drawdown: {
key: 'max_drawdown',
nameEn: 'Maximum Drawdown',
nameZh: '最大回撤',
formula: 'MDD = \\max_{t} \\left( \\frac{Peak_t - Trough_t}{Peak_t} \\right)',
descriptionEn: 'Largest peak-to-trough decline during the period',
descriptionZh: '期间内从峰值到谷底的最大跌幅',
},
sharpe_ratio: {
key: 'sharpe_ratio',
nameEn: 'Sharpe Ratio',
nameZh: '夏普比率',
formula: 'SR = \\frac{\\bar{r} - r_f}{\\sigma}',
descriptionEn: 'Risk-adjusted return per unit of volatility (r̄=avg return, rf=risk-free rate, σ=std dev)',
descriptionZh: '单位波动风险下的超额收益r̄=平均收益rf=无风险利率,σ=标准差)',
},
sortino_ratio: {
key: 'sortino_ratio',
nameEn: 'Sortino Ratio',
nameZh: '索提诺比率',
formula: 'Sortino = \\frac{\\bar{r} - r_f}{\\sigma_d}',
descriptionEn: 'Return per unit of downside risk (σd=downside deviation)',
descriptionZh: '单位下行风险的收益σd=下行标准差)',
},
calmar_ratio: {
key: 'calmar_ratio',
nameEn: 'Calmar Ratio',
nameZh: '卡玛比率',
formula: 'Calmar = \\frac{R_{ann}}{|MDD|}',
descriptionEn: 'Annualized return divided by maximum drawdown',
descriptionZh: '年化收益率与最大回撤的比值',
},
win_rate: {
key: 'win_rate',
nameEn: 'Win Rate',
nameZh: '胜率',
formula: 'WinRate = \\frac{N_{win}}{N_{total}} \\times 100\\%',
descriptionEn: 'Percentage of profitable trades',
descriptionZh: '盈利交易占总交易数的百分比',
},
profit_factor: {
key: 'profit_factor',
nameEn: 'Profit Factor',
nameZh: '盈亏比',
formula: 'PF = \\frac{\\sum Profits}{|\\sum Losses|}',
descriptionEn: 'Ratio of gross profit to gross loss',
descriptionZh: '总盈利与总亏损的比值',
},
volatility: {
key: 'volatility',
nameEn: 'Volatility',
nameZh: '波动率',
formula: '\\sigma = \\sqrt{\\frac{1}{n}\\sum_{i=1}^{n}(r_i - \\bar{r})^2}',
descriptionEn: 'Standard deviation of returns',
descriptionZh: '收益率的标准差',
},
var_95: {
key: 'var_95',
nameEn: 'VaR (95%)',
nameZh: '风险价值',
formula: 'P(R < VaR_{95\\%}) = 5\\%',
descriptionEn: '95% confidence level maximum expected loss',
descriptionZh: '95%置信水平下的最大预期损失',
},
alpha: {
key: 'alpha',
nameEn: 'Alpha',
nameZh: '超额收益',
formula: '\\alpha = R_{portfolio} - R_{benchmark}',
descriptionEn: 'Excess return over benchmark',
descriptionZh: '相对于基准的超额收益',
},
beta: {
key: 'beta',
nameEn: 'Beta',
nameZh: '贝塔系数',
formula: '\\beta = \\frac{Cov(R_p, R_m)}{Var(R_m)}',
descriptionEn: 'Portfolio sensitivity to market movements',
descriptionZh: '投资组合对市场波动的敏感度',
},
information_ratio: {
key: 'information_ratio',
nameEn: 'Information Ratio',
nameZh: '信息比率',
formula: 'IR = \\frac{\\alpha}{\\sigma_{tracking}}',
descriptionEn: 'Alpha per unit of tracking error',
descriptionZh: '单位跟踪误差的超额收益',
},
avg_trade_pnl: {
key: 'avg_trade_pnl',
nameEn: 'Avg Trade PnL',
nameZh: '平均盈亏',
formula: '\\bar{PnL} = \\frac{\\sum PnL_i}{N}',
descriptionEn: 'Average profit/loss per trade',
descriptionZh: '每笔交易的平均盈亏',
},
expectancy: {
key: 'expectancy',
nameEn: 'Expectancy',
nameZh: '期望收益',
formula: 'E = (WinRate \\times \\bar{W}) - (LossRate \\times \\bar{L})',
descriptionEn: 'Expected return per trade',
descriptionZh: '每笔交易的期望收益',
},
}
interface FormulaRendererProps {
formula: string
displayMode?: boolean
}
function FormulaRenderer({ formula, displayMode = true }: FormulaRendererProps) {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (containerRef.current) {
try {
katex.render(formula, containerRef.current, {
throwOnError: false,
displayMode,
output: 'html',
})
} catch (e) {
console.error('KaTeX render error:', e)
containerRef.current.textContent = formula
}
}
}, [formula, displayMode])
return <div ref={containerRef} className="formula-container" />
}
interface TooltipPosition {
top: number
left: number
placement: 'top' | 'bottom'
}
interface MetricTooltipProps {
metricKey: string
language?: string
size?: number
className?: string
}
export function MetricTooltip({
metricKey,
language = 'en',
size = 14,
className = '',
}: MetricTooltipProps) {
const [show, setShow] = useState(false)
const [position, setPosition] = useState<TooltipPosition>({ top: 100, left: 100, placement: 'bottom' })
const buttonRef = useRef<HTMLButtonElement>(null)
const tooltipWidth = 340
const tooltipHeight = 220
const metric = METRIC_DEFINITIONS[metricKey]
const calculatePosition = useCallback(() => {
if (!buttonRef.current) return
const rect = buttonRef.current.getBoundingClientRect()
const viewportHeight = window.innerHeight
const viewportWidth = window.innerWidth
// Calculate center position (fixed positioning uses viewport coordinates)
let left = rect.left + rect.width / 2 - tooltipWidth / 2
// Clamp to viewport bounds with padding
const padding = 16
left = Math.max(padding, Math.min(left, viewportWidth - tooltipWidth - padding))
// Decide placement: prefer bottom for reliability
const spaceBelow = viewportHeight - rect.bottom
let placement: 'top' | 'bottom' = 'bottom'
let top: number
if (spaceBelow >= tooltipHeight + 20) {
// Enough space below
placement = 'bottom'
top = rect.bottom + 8
} else {
// Show above
placement = 'top'
top = Math.max(8, rect.top - tooltipHeight - 8)
}
// Ensure top is never negative
top = Math.max(8, top)
setPosition({ top, left, placement })
}, [])
const handleMouseEnter = useCallback(() => {
calculatePosition()
setShow(true)
}, [calculatePosition])
const handleMouseLeave = useCallback(() => {
setShow(false)
}, [])
if (!metric) {
return null
}
const name = language === 'zh' ? metric.nameZh : metric.nameEn
const description = language === 'zh' ? metric.descriptionZh : metric.descriptionEn
const tooltipContent = (
<div
onMouseEnter={() => setShow(true)}
onMouseLeave={() => setShow(false)}
style={{
position: 'fixed',
top: `${position.top}px`,
left: `${position.left}px`,
width: `${tooltipWidth}px`,
zIndex: 99999,
pointerEvents: 'auto',
}}
>
<div
style={{
background: 'linear-gradient(145deg, #1E2329 0%, #2B3139 100%)',
border: '1px solid #3B4149',
borderRadius: '12px',
padding: '16px',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.8)',
}}
>
{/* Header */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '12px',
paddingBottom: '8px',
borderBottom: '1px solid #3B4149'
}}>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: '#F0B90B'
}} />
<span style={{ fontWeight: 'bold', fontSize: '14px', color: '#EAECEF' }}>
{name}
</span>
</div>
{/* Formula */}
<div style={{
background: 'rgba(0,0,0,0.3)',
borderRadius: '8px',
padding: '12px',
marginBottom: '12px'
}}>
<div style={{ fontSize: '12px', color: '#848E9C', marginBottom: '8px' }}>
{language === 'zh' ? '计算公式' : 'Formula'}
</div>
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '8px 4px',
color: '#EAECEF',
overflowX: 'auto',
overflowY: 'hidden',
maxWidth: '100%',
WebkitOverflowScrolling: 'touch',
}}>
<FormulaRenderer formula={metric.formula} displayMode={false} />
</div>
</div>
{/* Description */}
<p style={{ fontSize: '12px', lineHeight: '1.5', color: '#B7BDC6', margin: 0 }}>
{description}
</p>
</div>
</div>
)
return (
<>
<button
ref={buttonRef}
type="button"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={(e) => {
e.stopPropagation()
if (!show) {
calculatePosition()
}
setShow(!show)
}}
className={`p-0.5 rounded-full transition-colors hover:bg-white/10 ${className}`}
style={{ color: '#848E9C' }}
aria-label={`Info about ${name}`}
>
<HelpCircle size={size} />
</button>
{show && createPortal(tooltipContent, document.body)}
</>
)
}
// Convenience component for inline metric label with tooltip
interface MetricLabelProps {
metricKey: string
label?: string
language?: string
className?: string
}
export function MetricLabel({ metricKey, label, language = 'en', className = '' }: MetricLabelProps) {
const metric = METRIC_DEFINITIONS[metricKey]
const displayLabel = label || (language === 'zh' ? metric?.nameZh : metric?.nameEn) || metricKey
return (
<span className={`inline-flex items-center gap-1 ${className}`}>
{displayLabel}
<MetricTooltip metricKey={metricKey} language={language} size={12} />
</span>
)
}

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useMemo } from 'react'
import { api } from '../lib/api'
import { useLanguage } from '../contexts/LanguageContext'
import { t } from '../i18n/translations'
import { MetricTooltip } from './MetricTooltip'
import type {
HistoricalPosition,
TraderStats,
@@ -61,7 +62,8 @@ function StatCard({
color,
icon,
subtitle,
formula,
metricKey,
language = 'en',
}: {
title: string
value: string | number
@@ -69,10 +71,9 @@ function StatCard({
color?: string
icon: string
subtitle?: string
formula?: string
metricKey?: string
language?: string
}) {
const [showTooltip, setShowTooltip] = useState(false)
return (
<div
className="rounded-lg p-4 transition-all duration-200 hover:scale-[1.02]"
@@ -87,30 +88,8 @@ function StatCard({
<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>
{metricKey && (
<MetricTooltip metricKey={metricKey} language={language} size={12} />
)}
</div>
<div className="flex items-baseline gap-1">
@@ -519,9 +498,7 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
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'}
language={language}
/>
<StatCard
icon="🎯"
@@ -535,9 +512,8 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
? '#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%`}
metricKey="win_rate"
language={language}
/>
<StatCard
icon="💰"
@@ -545,9 +521,8 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
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'}
metricKey="total_return"
language={language}
/>
<StatCard
icon="📈"
@@ -555,9 +530,8 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
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'}
metricKey="profit_factor"
language={language}
/>
<StatCard
icon="⚖️"
@@ -565,9 +539,8 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
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)}`}
metricKey="expectancy"
language={language}
/>
</div>
)}
@@ -581,9 +554,8 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
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'}
metricKey="sharpe_ratio"
language={language}
/>
<StatCard
icon="🔻"
@@ -591,27 +563,23 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
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'}
metricKey="max_drawdown"
language={language}
/>
<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}`}
metricKey="avg_trade_pnl"
language={language}
/>
<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}`}
language={language}
/>
<StatCard
icon="💵"
@@ -619,9 +587,7 @@ export function PositionHistory({ traderId }: PositionHistoryProps) {
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)}`}
language={language}
/>
</div>
)}