mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-05 03:50:59 +08:00
Feature: Add multi-language support and UI improvements
- Add language context and translation system (Chinese/English) - Enhance UI components with i18n support - Update AILearning, EquityChart, and CompetitionPage - Add language toggle in header - Improve user experience with localized text
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
|
||||
interface TradeOutcome {
|
||||
symbol: string;
|
||||
@@ -52,6 +54,7 @@ interface DecisionRecord {
|
||||
const fetcher = (url: string) => fetch(url).then(res => res.json());
|
||||
|
||||
export default function AILearning({ traderId }: AILearningProps) {
|
||||
const { language } = useLanguage();
|
||||
const { data: performance, error } = useSWR<PerformanceAnalysis>(
|
||||
`http://localhost:8080/api/performance?trader_id=${traderId}`,
|
||||
fetcher,
|
||||
@@ -68,7 +71,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="rounded p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||
<div style={{ color: '#F6465D' }}>⚠️ 加载AI学习数据失败</div>
|
||||
<div style={{ color: '#F6465D' }}>{t('loadingError', language)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -76,7 +79,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
if (!performance) {
|
||||
return (
|
||||
<div className="rounded p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||
<div style={{ color: '#848E9C' }}>📊 加载中...</div>
|
||||
<div style={{ color: '#848E9C' }}>📊 {t('loading', language)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -86,10 +89,10 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
<div className="rounded p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xl">🧠</span>
|
||||
<h2 className="text-lg font-bold" style={{ color: '#EAECEF' }}>AI 学习分析</h2>
|
||||
<h2 className="text-lg font-bold" style={{ color: '#EAECEF' }}>{t('aiLearning', language)}</h2>
|
||||
</div>
|
||||
<div style={{ color: '#848E9C' }}>
|
||||
暂无完整交易数据(需要完成开仓→平仓的完整周期)
|
||||
{t('noCompleteData', language)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -115,9 +118,9 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
🧠
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>AI Learning & Reflection</h2>
|
||||
<h2 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>{t('aiLearning', language)}</h2>
|
||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{performance.total_trades} trades analyzed · Real-time evolution
|
||||
{t('tradesAnalyzed', language, { count: performance.total_trades })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,10 +153,10 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-bold" style={{ color: '#C4B5FD' }}>
|
||||
Latest Reflection
|
||||
{t('latestReflection', language)}
|
||||
</h3>
|
||||
<p className="text-xs" style={{ color: '#94A3B8' }}>
|
||||
Cycle #{latestDecisions[0].cycle_number} · {new Date(latestDecisions[0].timestamp).toLocaleTimeString()}
|
||||
{t('cycle', language)} #{latestDecisions[0].cycle_number} · {new Date(latestDecisions[0].timestamp).toLocaleTimeString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,7 +174,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
{latestDecisions[0].cot_trace && (
|
||||
<details className="mt-4">
|
||||
<summary className="cursor-pointer text-sm font-semibold flex items-center gap-2 hover:opacity-80 transition-opacity" style={{ color: '#A78BFA' }}>
|
||||
<span>📋 Full Chain of Thought</span>
|
||||
<span>{t('fullCoT', language)}</span>
|
||||
</summary>
|
||||
<div className="mt-3 rounded-xl p-4 text-xs leading-relaxed whitespace-pre-wrap max-h-80 overflow-y-auto" style={{
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
@@ -195,7 +198,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
border: '1px solid rgba(99, 102, 241, 0.2)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
|
||||
}}>
|
||||
<div className="text-xs font-semibold mb-2" style={{ color: '#94A3B8' }}>Total Trades</div>
|
||||
<div className="text-xs font-semibold mb-2" style={{ color: '#94A3B8' }}>{t('totalTrades', language)}</div>
|
||||
<div className="text-3xl font-bold mono" style={{ color: '#E0E7FF' }}>
|
||||
{performance.total_trades}
|
||||
</div>
|
||||
@@ -209,7 +212,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
border: `1px solid ${(performance.win_rate || 0) >= 50 ? 'rgba(14, 203, 129, 0.3)' : 'rgba(246, 70, 93, 0.3)'}`,
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
|
||||
}}>
|
||||
<div className="text-xs font-semibold mb-2" style={{ color: '#94A3B8' }}>Win Rate</div>
|
||||
<div className="text-xs font-semibold mb-2" style={{ color: '#94A3B8' }}>{t('winRate', language)}</div>
|
||||
<div className="text-3xl font-bold mono" style={{
|
||||
color: (performance.win_rate || 0) >= 50 ? '#10B981' : '#F87171'
|
||||
}}>
|
||||
@@ -226,7 +229,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
border: '1px solid rgba(14, 203, 129, 0.2)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
|
||||
}}>
|
||||
<div className="text-xs font-semibold mb-2" style={{ color: '#94A3B8' }}>Avg Win</div>
|
||||
<div className="text-xs font-semibold mb-2" style={{ color: '#94A3B8' }}>{t('avgWin', language)}</div>
|
||||
<div className="text-3xl font-bold mono" style={{ color: '#10B981' }}>
|
||||
+{(performance.avg_win || 0).toFixed(2)}%
|
||||
</div>
|
||||
@@ -238,7 +241,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
border: '1px solid rgba(246, 70, 93, 0.2)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
|
||||
}}>
|
||||
<div className="text-xs font-semibold mb-2" style={{ color: '#94A3B8' }}>Avg Loss</div>
|
||||
<div className="text-xs font-semibold mb-2" style={{ color: '#94A3B8' }}>{t('avgLoss', language)}</div>
|
||||
<div className="text-3xl font-bold mono" style={{ color: '#F87171' }}>
|
||||
{(performance.avg_loss || 0).toFixed(2)}%
|
||||
</div>
|
||||
@@ -258,9 +261,9 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
|
||||
<div className="relative flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-semibold mb-1" style={{ color: '#FCD34D' }}>Profit Factor</div>
|
||||
<div className="text-sm font-semibold mb-1" style={{ color: '#FCD34D' }}>{t('profitFactor', language)}</div>
|
||||
<div className="text-xs" style={{ color: '#94A3B8' }}>
|
||||
Avg Win ÷ Avg Loss
|
||||
{t('avgWinDivLoss', language)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-5xl font-bold mono" style={{
|
||||
@@ -276,10 +279,10 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
color: (performance.profit_factor || 0) >= 2.0 ? '#10B981' :
|
||||
(performance.profit_factor || 0) >= 1.5 ? '#F0B90B' : '#94A3B8'
|
||||
}}>
|
||||
{(performance.profit_factor || 0) >= 2.0 && '🔥 Excellent - Strong profitability'}
|
||||
{(performance.profit_factor || 0) >= 1.5 && (performance.profit_factor || 0) < 2.0 && '✓ Good - Stable profits'}
|
||||
{(performance.profit_factor || 0) >= 1.0 && (performance.profit_factor || 0) < 1.5 && '⚠️ Fair - Needs optimization'}
|
||||
{(performance.profit_factor || 0) > 0 && (performance.profit_factor || 0) < 1.0 && '❌ Poor - Losses exceed gains'}
|
||||
{(performance.profit_factor || 0) >= 2.0 && t('excellent', language)}
|
||||
{(performance.profit_factor || 0) >= 1.5 && (performance.profit_factor || 0) < 2.0 && t('good', language)}
|
||||
{(performance.profit_factor || 0) >= 1.0 && (performance.profit_factor || 0) < 1.5 && t('fair', language)}
|
||||
{(performance.profit_factor || 0) > 0 && (performance.profit_factor || 0) < 1.0 && t('poor', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -298,7 +301,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
}}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xl">🏆</span>
|
||||
<span className="text-xs font-semibold" style={{ color: '#6EE7B7' }}>Best Performer</span>
|
||||
<span className="text-xs font-semibold" style={{ color: '#6EE7B7' }}>{t('bestPerformer', language)}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold mono mb-1" style={{ color: '#10B981' }}>
|
||||
{performance.best_symbol}
|
||||
@@ -306,7 +309,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
{symbolStats[performance.best_symbol] && (
|
||||
<div className="text-sm font-semibold" style={{ color: '#6EE7B7' }}>
|
||||
{symbolStats[performance.best_symbol].total_pn_l > 0 ? '+' : ''}
|
||||
{symbolStats[performance.best_symbol].total_pn_l.toFixed(2)}% P&L
|
||||
{symbolStats[performance.best_symbol].total_pn_l.toFixed(2)}% {t('pnl', language)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -320,7 +323,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
}}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xl">📉</span>
|
||||
<span className="text-xs font-semibold" style={{ color: '#FCA5A5' }}>Worst Performer</span>
|
||||
<span className="text-xs font-semibold" style={{ color: '#FCA5A5' }}>{t('worstPerformer', language)}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold mono mb-1" style={{ color: '#F87171' }}>
|
||||
{performance.worst_symbol}
|
||||
@@ -328,7 +331,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
{symbolStats[performance.worst_symbol] && (
|
||||
<div className="text-sm font-semibold" style={{ color: '#FCA5A5' }}>
|
||||
{symbolStats[performance.worst_symbol].total_pn_l > 0 ? '+' : ''}
|
||||
{symbolStats[performance.worst_symbol].total_pn_l.toFixed(2)}% P&L
|
||||
{symbolStats[performance.worst_symbol].total_pn_l.toFixed(2)}% {t('pnl', language)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -345,7 +348,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
}}>
|
||||
<div className="p-5 border-b" style={{ borderColor: 'rgba(99, 102, 241, 0.2)', background: 'rgba(30, 35, 41, 0.6)' }}>
|
||||
<h3 className="font-bold flex items-center gap-2" style={{ color: '#E0E7FF' }}>
|
||||
📊 Symbol Performance
|
||||
{t('symbolPerformance', language)}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
@@ -411,11 +414,11 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">📜</span>
|
||||
<div>
|
||||
<h3 className="font-bold text-sm" style={{ color: '#FCD34D' }}>Trade History</h3>
|
||||
<h3 className="font-bold text-sm" style={{ color: '#FCD34D' }}>{t('tradeHistory', language)}</h3>
|
||||
<p className="text-xs" style={{ color: '#94A3B8' }}>
|
||||
{performance?.recent_trades && performance.recent_trades.length > 0
|
||||
? `Recent ${performance.recent_trades.length} completed trades`
|
||||
: 'Completed trades will appear here'}
|
||||
? t('completedTrades', language, { count: performance.recent_trades.length })
|
||||
: t('completedTradesWillAppear', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -459,7 +462,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
background: 'rgba(240, 185, 11, 0.2)',
|
||||
color: '#FCD34D'
|
||||
}}>
|
||||
Latest
|
||||
{t('latest', language)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -473,13 +476,13 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
{/* 价格信息 */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-3 text-xs">
|
||||
<div>
|
||||
<div style={{ color: '#94A3B8' }}>Entry</div>
|
||||
<div style={{ color: '#94A3B8' }}>{t('entry', language)}</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#CBD5E1' }}>
|
||||
{trade.open_price.toFixed(4)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div style={{ color: '#94A3B8' }}>Exit</div>
|
||||
<div style={{ color: '#94A3B8' }}>{t('exit', language)}</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#CBD5E1' }}>
|
||||
{trade.close_price.toFixed(4)}
|
||||
</div>
|
||||
@@ -508,7 +511,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
background: 'rgba(248, 113, 113, 0.2)',
|
||||
color: '#FCA5A5'
|
||||
}}>
|
||||
Stop Loss
|
||||
{t('stopLoss', language)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -531,7 +534,7 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
) : (
|
||||
<div className="p-6 text-center">
|
||||
<div className="text-4xl mb-2 opacity-50">📜</div>
|
||||
<div style={{ color: '#94A3B8' }}>No completed trades yet</div>
|
||||
<div style={{ color: '#94A3B8' }}>{t('noCompletedTrades', language)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -556,23 +559,23 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
💡
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold mb-3 text-base" style={{ color: '#FCD34D' }}>How AI Learns & Evolves</h3>
|
||||
<h3 className="font-bold mb-3 text-base" style={{ color: '#FCD34D' }}>{t('howAILearns', language)}</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<span style={{ color: '#F0B90B' }}>•</span>
|
||||
<span style={{ color: '#CBD5E1' }}>Analyzes last 20 trading cycles before each decision</span>
|
||||
<span style={{ color: '#CBD5E1' }}>{t('aiLearningPoint1', language)}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span style={{ color: '#F0B90B' }}>•</span>
|
||||
<span style={{ color: '#CBD5E1' }}>Identifies best & worst performing symbols</span>
|
||||
<span style={{ color: '#CBD5E1' }}>{t('aiLearningPoint2', language)}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span style={{ color: '#F0B90B' }}>•</span>
|
||||
<span style={{ color: '#CBD5E1' }}>Optimizes position sizing based on win rate</span>
|
||||
<span style={{ color: '#CBD5E1' }}>{t('aiLearningPoint3', language)}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span style={{ color: '#F0B90B' }}>•</span>
|
||||
<span style={{ color: '#CBD5E1' }}>Avoids repeating past mistakes</span>
|
||||
<span style={{ color: '#CBD5E1' }}>{t('aiLearningPoint4', language)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,11 @@ import useSWR from 'swr';
|
||||
import { api } from '../lib/api';
|
||||
import type { CompetitionData } from '../types';
|
||||
import { ComparisonChart } from './ComparisonChart';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
|
||||
export function CompetitionPage() {
|
||||
const { language } = useLanguage();
|
||||
const { data: competition } = useSWR<CompetitionData>(
|
||||
'competition',
|
||||
api.getCompetition,
|
||||
@@ -57,18 +60,18 @@ export function CompetitionPage() {
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
AI Competition
|
||||
{t('aiCompetition', language)}
|
||||
<span className="text-xs font-normal px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.15)', color: '#F0B90B' }}>
|
||||
{competition.count} traders
|
||||
{competition.count} {t('traders', language)}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
Qwen vs DeepSeek · Live Battle
|
||||
{t('liveBattle', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>🥇 Leader</div>
|
||||
<div className="text-xs mb-1" style={{ color: '#848E9C' }}>{t('leader', language)}</div>
|
||||
<div className="text-lg font-bold" style={{ color: '#F0B90B' }}>{leader?.trader_name}</div>
|
||||
<div className="text-sm font-semibold" style={{ color: leader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}>
|
||||
{leader.total_pnl >= 0 ? '+' : ''}{leader.total_pnl_pct.toFixed(2)}%
|
||||
@@ -82,10 +85,10 @@ export function CompetitionPage() {
|
||||
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
📈 Performance Comparison
|
||||
{t('performanceComparison', language)}
|
||||
</h2>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
Real-time PnL %
|
||||
{t('realTimePnL', language)}
|
||||
</div>
|
||||
</div>
|
||||
<ComparisonChart traders={sortedTraders} />
|
||||
@@ -95,10 +98,10 @@ export function CompetitionPage() {
|
||||
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
🥇 Leaderboard
|
||||
{t('leaderboard', language)}
|
||||
</h2>
|
||||
<div className="text-xs px-2 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', color: '#F0B90B', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
|
||||
LIVE
|
||||
{t('live', language)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -134,7 +137,7 @@ export function CompetitionPage() {
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Total Equity */}
|
||||
<div className="text-right">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>Equity</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>{t('equity', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{trader.total_equity.toFixed(2)}
|
||||
</div>
|
||||
@@ -142,7 +145,7 @@ export function CompetitionPage() {
|
||||
|
||||
{/* P&L */}
|
||||
<div className="text-right min-w-[90px]">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>P&L</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pnl', language)}</div>
|
||||
<div
|
||||
className="text-lg font-bold mono"
|
||||
style={{ color: trader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}
|
||||
@@ -157,7 +160,7 @@ export function CompetitionPage() {
|
||||
|
||||
{/* Positions */}
|
||||
<div className="text-right">
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>Pos</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>{t('pos', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{trader.position_count}
|
||||
</div>
|
||||
@@ -191,7 +194,7 @@ export function CompetitionPage() {
|
||||
{competition.traders.length === 2 && (
|
||||
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.3s' }}>
|
||||
<h2 className="text-lg font-bold mb-4 flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
⚔️ Head-to-Head Battle
|
||||
{t('headToHead', language)}
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{sortedTraders.map((trader, index) => {
|
||||
@@ -228,12 +231,12 @@ export function CompetitionPage() {
|
||||
</div>
|
||||
{isWinning && gap > 0 && (
|
||||
<div className="text-xs font-semibold" style={{ color: '#0ECB81' }}>
|
||||
Leading by {gap.toFixed(2)}%
|
||||
{t('leadingBy', language, { gap: gap.toFixed(2) })}
|
||||
</div>
|
||||
)}
|
||||
{!isWinning && gap < 0 && (
|
||||
<div className="text-xs font-semibold" style={{ color: '#F6465D' }}>
|
||||
Behind by {Math.abs(gap).toFixed(2)}%
|
||||
{t('behindBy', language, { gap: Math.abs(gap).toFixed(2) })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
} from 'recharts';
|
||||
import useSWR from 'swr';
|
||||
import { api } from '../lib/api';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
|
||||
interface EquityPoint {
|
||||
timestamp: string;
|
||||
@@ -25,6 +27,7 @@ interface EquityChartProps {
|
||||
}
|
||||
|
||||
export function EquityChart({ traderId }: EquityChartProps) {
|
||||
const { language } = useLanguage();
|
||||
const [displayMode, setDisplayMode] = useState<'dollar' | 'percent'>('dollar');
|
||||
|
||||
const { data: history, error } = useSWR<EquityPoint[]>(
|
||||
@@ -49,7 +52,7 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
<div className="flex items-center gap-3 p-4 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', border: '1px solid rgba(246, 70, 93, 0.2)' }}>
|
||||
<div className="text-2xl">⚠️</div>
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#F6465D' }}>加载失败</div>
|
||||
<div className="font-semibold" style={{ color: '#F6465D' }}>{t('loadingError', language)}</div>
|
||||
<div className="text-sm" style={{ color: '#848E9C' }}>{error.message}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,11 +63,11 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
if (!history || history.length === 0) {
|
||||
return (
|
||||
<div className="binance-card p-6">
|
||||
<h3 className="text-lg font-semibold mb-6" style={{ color: '#EAECEF' }}>账户净值曲线</h3>
|
||||
<h3 className="text-lg font-semibold mb-6" style={{ color: '#EAECEF' }}>{t('accountEquityCurve', language)}</h3>
|
||||
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
||||
<div className="text-6xl mb-4 opacity-50">📊</div>
|
||||
<div className="text-lg font-semibold mb-2">暂无历史数据</div>
|
||||
<div className="text-sm">运行几个周期后将显示收益率曲线</div>
|
||||
<div className="text-lg font-semibold mb-2">{t('noHistoricalData', language)}</div>
|
||||
<div className="text-sm">{t('dataWillAppear', language)}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -153,7 +156,7 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>账户净值曲线</h3>
|
||||
<h3 className="text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>{t('accountEquityCurve', language)}</h3>
|
||||
<div className="flex items-baseline gap-4">
|
||||
<span className="text-3xl font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{account?.total_equity.toFixed(2) || '0.00'}
|
||||
@@ -239,7 +242,7 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
stroke="#474D57"
|
||||
strokeDasharray="3 3"
|
||||
label={{
|
||||
value: displayMode === 'dollar' ? '初始' : '0%',
|
||||
value: displayMode === 'dollar' ? t('initialBalance', language).split(' ')[0] : '0%',
|
||||
fill: '#848E9C',
|
||||
fontSize: 12,
|
||||
}}
|
||||
@@ -259,27 +262,27 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
{/* Footer Stats */}
|
||||
<div className="mt-3 grid grid-cols-4 gap-3 pt-3" style={{ borderTop: '1px solid #2B3139' }}>
|
||||
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>初始余额</div>
|
||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('initialBalance', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{initialBalance.toFixed(2)} USDT
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>当前净值</div>
|
||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('currentEquity', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{currentValue.raw_equity.toFixed(2)} USDT
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>历史周期</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>{history.length} 个</div>
|
||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('historicalCycles', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>{history.length} {t('cycles', language)}</div>
|
||||
</div>
|
||||
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
|
||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>显示范围</div>
|
||||
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('displayRange', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{history.length > MAX_DISPLAY_POINTS
|
||||
? `最近 ${MAX_DISPLAY_POINTS}`
|
||||
: '全部数据'
|
||||
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
|
||||
: t('allData', language)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user