mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
feat: add metric formula tooltips with KaTeX rendering
This commit is contained in:
2522
docs/research/AI-Trader-Analysis-Report.md
Normal file
2522
docs/research/AI-Trader-Analysis-Report.md
Normal file
File diff suppressed because it is too large
Load Diff
26
web/package-lock.json
generated
26
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)}
|
||||
|
||||
364
web/src/components/MetricTooltip.tsx
Normal file
364
web/src/components/MetricTooltip.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user