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:
tinkle-community
2025-10-28 22:25:36 +08:00
parent 3eabfbb402
commit 8a26b8161b
7 changed files with 484 additions and 103 deletions
-1
View File
@@ -27,4 +27,3 @@ config.json
# 决策日志 # 决策日志
decision_logs/ decision_logs/
coin_pool_cache/ coin_pool_cache/
nofx-auto
+123 -38
View File
@@ -4,6 +4,8 @@ import { api } from './lib/api';
import { EquityChart } from './components/EquityChart'; import { EquityChart } from './components/EquityChart';
import { CompetitionPage } from './components/CompetitionPage'; import { CompetitionPage } from './components/CompetitionPage';
import AILearning from './components/AILearning'; import AILearning from './components/AILearning';
import { LanguageProvider, useLanguage } from './contexts/LanguageContext';
import { t, type Language } from './i18n/translations';
import type { import type {
SystemStatus, SystemStatus,
AccountInfo, AccountInfo,
@@ -16,6 +18,7 @@ import type {
type Page = 'competition' | 'trader'; type Page = 'competition' | 'trader';
function App() { function App() {
const { language, setLanguage } = useLanguage();
const [currentPage, setCurrentPage] = useState<Page>('competition'); const [currentPage, setCurrentPage] = useState<Page>('competition');
const [selectedTraderId, setSelectedTraderId] = useState<string | undefined>(); const [selectedTraderId, setSelectedTraderId] = useState<string | undefined>();
const [lastUpdate, setLastUpdate] = useState<string>('--:--:--'); const [lastUpdate, setLastUpdate] = useState<string>('--:--:--');
@@ -106,14 +109,62 @@ function App() {
</div> </div>
<div> <div>
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}> <h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
AI Trading Competition {t('appTitle', language)}
</h1> </h1>
<p className="text-xs mono" style={{ color: '#848E9C' }}> <p className="text-xs mono" style={{ color: '#848E9C' }}>
Qwen vs DeepSeek · Real-time {t('subtitle', language)}
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* GitHub Link */}
<a
href="https://github.com/tinkle-community/nofx"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{ background: '#1E2329', color: '#848E9C', border: '1px solid #2B3139' }}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139';
e.currentTarget.style.color = '#EAECEF';
e.currentTarget.style.borderColor = '#F0B90B';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329';
e.currentTarget.style.color = '#848E9C';
e.currentTarget.style.borderColor = '#2B3139';
}}
>
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
<span>GitHub</span>
</a>
{/* Language Toggle */}
<div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
<button
onClick={() => setLanguage('zh')}
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
style={language === 'zh'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
</button>
<button
onClick={() => setLanguage('en')}
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
style={language === 'en'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
EN
</button>
</div>
{/* Page Toggle */} {/* Page Toggle */}
<div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}> <div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
<button <button
@@ -126,7 +177,7 @@ function App() {
: { background: 'transparent', color: '#848E9C' } : { background: 'transparent', color: '#848E9C' }
} }
> >
Competition {t('competition', language)}
</button> </button>
<button <button
onClick={() => setCurrentPage('trader')} onClick={() => setCurrentPage('trader')}
@@ -136,7 +187,7 @@ function App() {
: { background: 'transparent', color: '#848E9C' } : { background: 'transparent', color: '#848E9C' }
} }
> >
Details {t('details', language)}
</button> </button>
</div> </div>
@@ -170,7 +221,7 @@ function App() {
style={{ background: status.is_running ? '#0ECB81' : '#F6465D' }} style={{ background: status.is_running ? '#0ECB81' : '#F6465D' }}
/> />
<span className="font-semibold mono text-xs"> <span className="font-semibold mono text-xs">
{status.is_running ? 'RUNNING' : 'STOPPED'} {t(status.is_running ? 'running' : 'stopped', language)}
</span> </span>
</div> </div>
)} )}
@@ -192,6 +243,7 @@ function App() {
decisions={decisions} decisions={decisions}
stats={stats} stats={stats}
lastUpdate={lastUpdate} lastUpdate={lastUpdate}
language={language}
/> />
)} )}
</main> </main>
@@ -199,8 +251,32 @@ function App() {
{/* Footer */} {/* Footer */}
<footer className="mt-16" style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}> <footer className="mt-16" style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}>
<div className="max-w-[1920px] mx-auto px-6 py-6 text-center text-sm" style={{ color: '#5E6673' }}> <div className="max-w-[1920px] mx-auto px-6 py-6 text-center text-sm" style={{ color: '#5E6673' }}>
<p>NOFX - AI Trading Competition System</p> <p>{t('footerTitle', language)}</p>
<p className="mt-1"> Trading involves risk. Use at your own discretion.</p> <p className="mt-1">{t('footerWarning', language)}</p>
<div className="mt-4 flex items-center justify-center gap-2">
<a
href="https://github.com/tinkle-community/nofx"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{ background: '#1E2329', color: '#848E9C', border: '1px solid #2B3139' }}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139';
e.currentTarget.style.color = '#EAECEF';
e.currentTarget.style.borderColor = '#F0B90B';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329';
e.currentTarget.style.color = '#848E9C';
e.currentTarget.style.borderColor = '#2B3139';
}}
>
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
<span>Star on GitHub</span>
</a>
</div>
</div> </div>
</footer> </footer>
</div> </div>
@@ -216,6 +292,7 @@ function TraderDetailsPage({
decisions, decisions,
stats, stats,
lastUpdate, lastUpdate,
language,
}: { }: {
selectedTrader?: TraderInfo; selectedTrader?: TraderInfo;
status?: SystemStatus; status?: SystemStatus;
@@ -224,6 +301,7 @@ function TraderDetailsPage({
decisions?: DecisionRecord[]; decisions?: DecisionRecord[];
stats?: Statistics; stats?: Statistics;
lastUpdate: string; lastUpdate: string;
language: Language;
}) { }) {
if (!selectedTrader) { if (!selectedTrader) {
return ( return (
@@ -290,26 +368,26 @@ function TraderDetailsPage({
{/* Account Overview */} {/* Account Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<StatCard <StatCard
title="Total Equity" title={t('totalEquity', language)}
value={`${account?.total_equity.toFixed(2) || '0.00'} USDT`} value={`${account?.total_equity.toFixed(2) || '0.00'} USDT`}
change={account?.total_pnl_pct || 0} change={account?.total_pnl_pct || 0}
positive={account ? account.total_pnl > 0 : false} positive={account ? account.total_pnl > 0 : false}
/> />
<StatCard <StatCard
title="Available Balance" title={t('availableBalance', language)}
value={`${account?.available_balance.toFixed(2) || '0.00'} USDT`} value={`${account?.available_balance.toFixed(2) || '0.00'} USDT`}
subtitle={`${((account?.available_balance / account?.total_equity) * 100 || 0).toFixed(1)}% Free`} subtitle={`${((account?.available_balance / account?.total_equity) * 100 || 0).toFixed(1)}% ${t('free', language)}`}
/> />
<StatCard <StatCard
title="Total P&L" title={t('totalPnL', language)}
value={`${account?.total_pnl >= 0 ? '+' : ''}${account?.total_pnl.toFixed(2) || '0.00'} USDT`} value={`${account?.total_pnl >= 0 ? '+' : ''}${account?.total_pnl.toFixed(2) || '0.00'} USDT`}
change={account?.total_pnl_pct || 0} change={account?.total_pnl_pct || 0}
positive={account ? account.total_pnl >= 0 : false} positive={account ? account.total_pnl >= 0 : false}
/> />
<StatCard <StatCard
title="Positions" title={t('positions', language)}
value={`${account?.position_count || 0}`} value={`${account?.position_count || 0}`}
subtitle={`Margin: ${account?.margin_used_pct.toFixed(1) || '0.0'}%`} subtitle={`${t('margin', language)}: ${account?.margin_used_pct.toFixed(1) || '0.0'}%`}
/> />
</div> </div>
@@ -326,11 +404,11 @@ function TraderDetailsPage({
<div className="binance-card p-6 animate-slide-in" style={{ animationDelay: '0.15s' }}> <div className="binance-card p-6 animate-slide-in" style={{ animationDelay: '0.15s' }}>
<div className="flex items-center justify-between mb-5"> <div className="flex items-center justify-between mb-5">
<h2 className="text-xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}> <h2 className="text-xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
📈 Current Positions 📈 {t('currentPositions', language)}
</h2> </h2>
{positions && positions.length > 0 && ( {positions && positions.length > 0 && (
<div className="text-xs px-3 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', color: '#F0B90B', border: '1px solid rgba(240, 185, 11, 0.2)' }}> <div className="text-xs px-3 py-1 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', color: '#F0B90B', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
{positions.length} Active {positions.length} {t('active', language)}
</div> </div>
)} )}
</div> </div>
@@ -339,15 +417,15 @@ function TraderDetailsPage({
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead className="text-left border-b border-gray-800"> <thead className="text-left border-b border-gray-800">
<tr> <tr>
<th className="pb-3 font-semibold text-gray-400">Symbol</th> <th className="pb-3 font-semibold text-gray-400">{t('symbol', language)}</th>
<th className="pb-3 font-semibold text-gray-400">Side</th> <th className="pb-3 font-semibold text-gray-400">{t('side', language)}</th>
<th className="pb-3 font-semibold text-gray-400">Entry Price</th> <th className="pb-3 font-semibold text-gray-400">{t('entryPrice', language)}</th>
<th className="pb-3 font-semibold text-gray-400">Mark Price</th> <th className="pb-3 font-semibold text-gray-400">{t('markPrice', language)}</th>
<th className="pb-3 font-semibold text-gray-400">Quantity</th> <th className="pb-3 font-semibold text-gray-400">{t('quantity', language)}</th>
<th className="pb-3 font-semibold text-gray-400">Position Value</th> <th className="pb-3 font-semibold text-gray-400">{t('positionValue', language)}</th>
<th className="pb-3 font-semibold text-gray-400">Leverage</th> <th className="pb-3 font-semibold text-gray-400">{t('leverage', language)}</th>
<th className="pb-3 font-semibold text-gray-400">Unrealized P&L</th> <th className="pb-3 font-semibold text-gray-400">{t('unrealizedPnL', language)}</th>
<th className="pb-3 font-semibold text-gray-400">Liq. Price</th> <th className="pb-3 font-semibold text-gray-400">{t('liqPrice', language)}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -362,7 +440,7 @@ function TraderDetailsPage({
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' } : { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
} }
> >
{pos.side.toUpperCase()} {t(pos.side === 'long' ? 'long' : 'short', language)}
</span> </span>
</td> </td>
<td className="py-3 font-mono" style={{ color: '#EAECEF' }}>{pos.entry_price.toFixed(4)}</td> <td className="py-3 font-mono" style={{ color: '#EAECEF' }}>{pos.entry_price.toFixed(4)}</td>
@@ -391,8 +469,8 @@ function TraderDetailsPage({
) : ( ) : (
<div className="text-center py-16" style={{ color: '#848E9C' }}> <div className="text-center py-16" style={{ color: '#848E9C' }}>
<div className="text-6xl mb-4 opacity-50">📊</div> <div className="text-6xl mb-4 opacity-50">📊</div>
<div className="text-lg font-semibold mb-2"></div> <div className="text-lg font-semibold mb-2">{t('noPositions', language)}</div>
<div className="text-sm"></div> <div className="text-sm">{t('noActivePositions', language)}</div>
</div> </div>
)} )}
</div> </div>
@@ -410,10 +488,10 @@ function TraderDetailsPage({
🧠 🧠
</div> </div>
<div> <div>
<h2 className="text-xl font-bold" style={{ color: '#EAECEF' }}>Recent Decisions</h2> <h2 className="text-xl font-bold" style={{ color: '#EAECEF' }}>{t('recentDecisions', language)}</h2>
{decisions && decisions.length > 0 && ( {decisions && decisions.length > 0 && (
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
Last {decisions.length} trading cycles {t('lastCycles', language, { count: decisions.length })}
</div> </div>
)} )}
</div> </div>
@@ -423,13 +501,13 @@ function TraderDetailsPage({
<div className="space-y-4 overflow-y-auto pr-2" style={{ maxHeight: 'calc(100vh - 280px)' }}> <div className="space-y-4 overflow-y-auto pr-2" style={{ maxHeight: 'calc(100vh - 280px)' }}>
{decisions && decisions.length > 0 ? ( {decisions && decisions.length > 0 ? (
decisions.map((decision, i) => ( decisions.map((decision, i) => (
<DecisionCard key={i} decision={decision} /> <DecisionCard key={i} decision={decision} language={language} />
)) ))
) : ( ) : (
<div className="py-16 text-center"> <div className="py-16 text-center">
<div className="text-6xl mb-4 opacity-30">🧠</div> <div className="text-6xl mb-4 opacity-30">🧠</div>
<div className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>No Decisions Yet</div> <div className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>{t('noDecisionsYet', language)}</div>
<div className="text-sm" style={{ color: '#848E9C' }}>AI trading decisions will appear here</div> <div className="text-sm" style={{ color: '#848E9C' }}>{t('aiDecisionsWillAppear', language)}</div>
</div> </div>
)} )}
</div> </div>
@@ -480,7 +558,7 @@ function StatCard({
} }
// Decision Card Component with CoT Trace - Binance Style // Decision Card Component with CoT Trace - Binance Style
function DecisionCard({ decision }: { decision: DecisionRecord }) { function DecisionCard({ decision, language }: { decision: DecisionRecord; language: Language }) {
const [showCoT, setShowCoT] = useState(false); const [showCoT, setShowCoT] = useState(false);
return ( return (
@@ -488,7 +566,7 @@ function DecisionCard({ decision }: { decision: DecisionRecord }) {
{/* Header */} {/* Header */}
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div> <div>
<div className="font-semibold" style={{ color: '#EAECEF' }}>Cycle #{decision.cycle_number}</div> <div className="font-semibold" style={{ color: '#EAECEF' }}>{t('cycle', language)} #{decision.cycle_number}</div>
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
{new Date(decision.timestamp).toLocaleString()} {new Date(decision.timestamp).toLocaleString()}
</div> </div>
@@ -500,7 +578,7 @@ function DecisionCard({ decision }: { decision: DecisionRecord }) {
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' } : { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
} }
> >
{decision.success ? 'Success' : 'Failed'} {t(decision.success ? 'success' : 'failed', language)}
</div> </div>
</div> </div>
@@ -512,8 +590,8 @@ function DecisionCard({ decision }: { decision: DecisionRecord }) {
className="flex items-center gap-2 text-sm transition-colors" className="flex items-center gap-2 text-sm transition-colors"
style={{ color: '#F0B90B' }} style={{ color: '#F0B90B' }}
> >
<span className="font-semibold">💭 AI思维链分析</span> <span className="font-semibold">{t('aiThinking', language)}</span>
<span className="text-xs">{showCoT ? '▼ 收起' : '▶ 展开'}</span> <span className="text-xs">{showCoT ? t('collapse', language) : t('expand', language)}</span>
</button> </button>
{showCoT && ( {showCoT && (
<div className="mt-2 rounded p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}> <div className="mt-2 rounded p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}>
@@ -586,4 +664,11 @@ function DecisionCard({ decision }: { decision: DecisionRecord }) {
); );
} }
export default App; // Wrap App with LanguageProvider
export default function AppWithLanguage() {
return (
<LanguageProvider>
<App />
</LanguageProvider>
);
}
+40 -37
View File
@@ -1,5 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations';
interface TradeOutcome { interface TradeOutcome {
symbol: string; symbol: string;
@@ -52,6 +54,7 @@ interface DecisionRecord {
const fetcher = (url: string) => fetch(url).then(res => res.json()); const fetcher = (url: string) => fetch(url).then(res => res.json());
export default function AILearning({ traderId }: AILearningProps) { export default function AILearning({ traderId }: AILearningProps) {
const { language } = useLanguage();
const { data: performance, error } = useSWR<PerformanceAnalysis>( const { data: performance, error } = useSWR<PerformanceAnalysis>(
`http://localhost:8080/api/performance?trader_id=${traderId}`, `http://localhost:8080/api/performance?trader_id=${traderId}`,
fetcher, fetcher,
@@ -68,7 +71,7 @@ export default function AILearning({ traderId }: AILearningProps) {
if (error) { if (error) {
return ( return (
<div className="rounded p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}> <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> </div>
); );
} }
@@ -76,7 +79,7 @@ export default function AILearning({ traderId }: AILearningProps) {
if (!performance) { if (!performance) {
return ( return (
<div className="rounded p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}> <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> </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="rounded p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="text-xl">🧠</span> <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>
<div style={{ color: '#848E9C' }}> <div style={{ color: '#848E9C' }}>
{t('noCompleteData', language)}
</div> </div>
</div> </div>
); );
@@ -115,9 +118,9 @@ export default function AILearning({ traderId }: AILearningProps) {
🧠 🧠
</div> </div>
<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' }}> <p className="text-sm" style={{ color: '#848E9C' }}>
{performance.total_trades} trades analyzed · Real-time evolution {t('tradesAnalyzed', language, { count: performance.total_trades })}
</p> </p>
</div> </div>
</div> </div>
@@ -150,10 +153,10 @@ export default function AILearning({ traderId }: AILearningProps) {
</div> </div>
<div> <div>
<h3 className="text-base font-bold" style={{ color: '#C4B5FD' }}> <h3 className="text-base font-bold" style={{ color: '#C4B5FD' }}>
Latest Reflection {t('latestReflection', language)}
</h3> </h3>
<p className="text-xs" style={{ color: '#94A3B8' }}> <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> </p>
</div> </div>
</div> </div>
@@ -171,7 +174,7 @@ export default function AILearning({ traderId }: AILearningProps) {
{latestDecisions[0].cot_trace && ( {latestDecisions[0].cot_trace && (
<details className="mt-4"> <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' }}> <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> </summary>
<div className="mt-3 rounded-xl p-4 text-xs leading-relaxed whitespace-pre-wrap max-h-80 overflow-y-auto" style={{ <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)', 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)', border: '1px solid rgba(99, 102, 241, 0.2)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)' 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' }}> <div className="text-3xl font-bold mono" style={{ color: '#E0E7FF' }}>
{performance.total_trades} {performance.total_trades}
</div> </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)'}`, 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)' 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={{ <div className="text-3xl font-bold mono" style={{
color: (performance.win_rate || 0) >= 50 ? '#10B981' : '#F87171' 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)', border: '1px solid rgba(14, 203, 129, 0.2)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)' 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' }}> <div className="text-3xl font-bold mono" style={{ color: '#10B981' }}>
+{(performance.avg_win || 0).toFixed(2)}% +{(performance.avg_win || 0).toFixed(2)}%
</div> </div>
@@ -238,7 +241,7 @@ export default function AILearning({ traderId }: AILearningProps) {
border: '1px solid rgba(246, 70, 93, 0.2)', border: '1px solid rgba(246, 70, 93, 0.2)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)' 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' }}> <div className="text-3xl font-bold mono" style={{ color: '#F87171' }}>
{(performance.avg_loss || 0).toFixed(2)}% {(performance.avg_loss || 0).toFixed(2)}%
</div> </div>
@@ -258,9 +261,9 @@ export default function AILearning({ traderId }: AILearningProps) {
<div className="relative flex items-center justify-between"> <div className="relative flex items-center justify-between">
<div> <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' }}> <div className="text-xs" style={{ color: '#94A3B8' }}>
Avg Win ÷ Avg Loss {t('avgWinDivLoss', language)}
</div> </div>
</div> </div>
<div className="text-5xl font-bold mono" style={{ <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' : color: (performance.profit_factor || 0) >= 2.0 ? '#10B981' :
(performance.profit_factor || 0) >= 1.5 ? '#F0B90B' : '#94A3B8' (performance.profit_factor || 0) >= 1.5 ? '#F0B90B' : '#94A3B8'
}}> }}>
{(performance.profit_factor || 0) >= 2.0 && '🔥 Excellent - Strong profitability'} {(performance.profit_factor || 0) >= 2.0 && t('excellent', language)}
{(performance.profit_factor || 0) >= 1.5 && (performance.profit_factor || 0) < 2.0 && '✓ Good - Stable profits'} {(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 && '⚠️ Fair - Needs optimization'} {(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 && '❌ Poor - Losses exceed gains'} {(performance.profit_factor || 0) > 0 && (performance.profit_factor || 0) < 1.0 && t('poor', language)}
</div> </div>
</div> </div>
</div> </div>
@@ -298,7 +301,7 @@ export default function AILearning({ traderId }: AILearningProps) {
}}> }}>
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<span className="text-xl">🏆</span> <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>
<div className="text-2xl font-bold mono mb-1" style={{ color: '#10B981' }}> <div className="text-2xl font-bold mono mb-1" style={{ color: '#10B981' }}>
{performance.best_symbol} {performance.best_symbol}
@@ -306,7 +309,7 @@ export default function AILearning({ traderId }: AILearningProps) {
{symbolStats[performance.best_symbol] && ( {symbolStats[performance.best_symbol] && (
<div className="text-sm font-semibold" style={{ color: '#6EE7B7' }}> <div className="text-sm font-semibold" style={{ color: '#6EE7B7' }}>
{symbolStats[performance.best_symbol].total_pn_l > 0 ? '+' : ''} {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>
)} )}
</div> </div>
@@ -320,7 +323,7 @@ export default function AILearning({ traderId }: AILearningProps) {
}}> }}>
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<span className="text-xl">📉</span> <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>
<div className="text-2xl font-bold mono mb-1" style={{ color: '#F87171' }}> <div className="text-2xl font-bold mono mb-1" style={{ color: '#F87171' }}>
{performance.worst_symbol} {performance.worst_symbol}
@@ -328,7 +331,7 @@ export default function AILearning({ traderId }: AILearningProps) {
{symbolStats[performance.worst_symbol] && ( {symbolStats[performance.worst_symbol] && (
<div className="text-sm font-semibold" style={{ color: '#FCA5A5' }}> <div className="text-sm font-semibold" style={{ color: '#FCA5A5' }}>
{symbolStats[performance.worst_symbol].total_pn_l > 0 ? '+' : ''} {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>
)} )}
</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)' }}> <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' }}> <h3 className="font-bold flex items-center gap-2" style={{ color: '#E0E7FF' }}>
📊 Symbol Performance {t('symbolPerformance', language)}
</h3> </h3>
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@@ -411,11 +414,11 @@ export default function AILearning({ traderId }: AILearningProps) {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xl">📜</span> <span className="text-xl">📜</span>
<div> <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' }}> <p className="text-xs" style={{ color: '#94A3B8' }}>
{performance?.recent_trades && performance.recent_trades.length > 0 {performance?.recent_trades && performance.recent_trades.length > 0
? `Recent ${performance.recent_trades.length} completed trades` ? t('completedTrades', language, { count: performance.recent_trades.length })
: 'Completed trades will appear here'} : t('completedTradesWillAppear', language)}
</p> </p>
</div> </div>
</div> </div>
@@ -459,7 +462,7 @@ export default function AILearning({ traderId }: AILearningProps) {
background: 'rgba(240, 185, 11, 0.2)', background: 'rgba(240, 185, 11, 0.2)',
color: '#FCD34D' color: '#FCD34D'
}}> }}>
Latest {t('latest', language)}
</span> </span>
)} )}
</div> </div>
@@ -473,13 +476,13 @@ export default function AILearning({ traderId }: AILearningProps) {
{/* 价格信息 */} {/* 价格信息 */}
<div className="grid grid-cols-2 gap-2 mb-3 text-xs"> <div className="grid grid-cols-2 gap-2 mb-3 text-xs">
<div> <div>
<div style={{ color: '#94A3B8' }}>Entry</div> <div style={{ color: '#94A3B8' }}>{t('entry', language)}</div>
<div className="font-mono font-semibold" style={{ color: '#CBD5E1' }}> <div className="font-mono font-semibold" style={{ color: '#CBD5E1' }}>
{trade.open_price.toFixed(4)} {trade.open_price.toFixed(4)}
</div> </div>
</div> </div>
<div className="text-right"> <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' }}> <div className="font-mono font-semibold" style={{ color: '#CBD5E1' }}>
{trade.close_price.toFixed(4)} {trade.close_price.toFixed(4)}
</div> </div>
@@ -508,7 +511,7 @@ export default function AILearning({ traderId }: AILearningProps) {
background: 'rgba(248, 113, 113, 0.2)', background: 'rgba(248, 113, 113, 0.2)',
color: '#FCA5A5' color: '#FCA5A5'
}}> }}>
Stop Loss {t('stopLoss', language)}
</span> </span>
)} )}
</div> </div>
@@ -531,7 +534,7 @@ export default function AILearning({ traderId }: AILearningProps) {
) : ( ) : (
<div className="p-6 text-center"> <div className="p-6 text-center">
<div className="text-4xl mb-2 opacity-50">📜</div> <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>
)} )}
</div> </div>
@@ -556,23 +559,23 @@ export default function AILearning({ traderId }: AILearningProps) {
💡 💡
</div> </div>
<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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<span style={{ color: '#F0B90B' }}></span> <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>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<span style={{ color: '#F0B90B' }}></span> <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>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<span style={{ color: '#F0B90B' }}></span> <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>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<span style={{ color: '#F0B90B' }}></span> <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> </div>
</div> </div>
+17 -14
View File
@@ -2,8 +2,11 @@ import useSWR from 'swr';
import { api } from '../lib/api'; import { api } from '../lib/api';
import type { CompetitionData } from '../types'; import type { CompetitionData } from '../types';
import { ComparisonChart } from './ComparisonChart'; import { ComparisonChart } from './ComparisonChart';
import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations';
export function CompetitionPage() { export function CompetitionPage() {
const { language } = useLanguage();
const { data: competition } = useSWR<CompetitionData>( const { data: competition } = useSWR<CompetitionData>(
'competition', 'competition',
api.getCompetition, api.getCompetition,
@@ -57,18 +60,18 @@ export function CompetitionPage() {
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}> <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' }}> <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> </span>
</h1> </h1>
<p className="text-xs" style={{ color: '#848E9C' }}> <p className="text-xs" style={{ color: '#848E9C' }}>
Qwen vs DeepSeek · Live Battle {t('liveBattle', language)}
</p> </p>
</div> </div>
</div> </div>
<div className="text-right"> <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-lg font-bold" style={{ color: '#F0B90B' }}>{leader?.trader_name}</div>
<div className="text-sm font-semibold" style={{ color: leader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}> <div className="text-sm font-semibold" style={{ color: leader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}>
{leader.total_pnl >= 0 ? '+' : ''}{leader.total_pnl_pct.toFixed(2)}% {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="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}> <h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
📈 Performance Comparison {t('performanceComparison', language)}
</h2> </h2>
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
Real-time PnL % {t('realTimePnL', language)}
</div> </div>
</div> </div>
<ComparisonChart traders={sortedTraders} /> <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="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.1s' }}>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}> <h2 className="text-lg font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
🥇 Leaderboard {t('leaderboard', language)}
</h2> </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)' }}> <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> </div>
<div className="space-y-2"> <div className="space-y-2">
@@ -134,7 +137,7 @@ export function CompetitionPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* Total Equity */} {/* Total Equity */}
<div className="text-right"> <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' }}> <div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
{trader.total_equity.toFixed(2)} {trader.total_equity.toFixed(2)}
</div> </div>
@@ -142,7 +145,7 @@ export function CompetitionPage() {
{/* P&L */} {/* P&L */}
<div className="text-right min-w-[90px]"> <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 <div
className="text-lg font-bold mono" className="text-lg font-bold mono"
style={{ color: trader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }} style={{ color: trader.total_pnl >= 0 ? '#0ECB81' : '#F6465D' }}
@@ -157,7 +160,7 @@ export function CompetitionPage() {
{/* Positions */} {/* Positions */}
<div className="text-right"> <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' }}> <div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
{trader.position_count} {trader.position_count}
</div> </div>
@@ -191,7 +194,7 @@ export function CompetitionPage() {
{competition.traders.length === 2 && ( {competition.traders.length === 2 && (
<div className="binance-card p-5 animate-slide-in" style={{ animationDelay: '0.3s' }}> <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' }}> <h2 className="text-lg font-bold mb-4 flex items-center gap-2" style={{ color: '#EAECEF' }}>
Head-to-Head Battle {t('headToHead', language)}
</h2> </h2>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{sortedTraders.map((trader, index) => { {sortedTraders.map((trader, index) => {
@@ -228,12 +231,12 @@ export function CompetitionPage() {
</div> </div>
{isWinning && gap > 0 && ( {isWinning && gap > 0 && (
<div className="text-xs font-semibold" style={{ color: '#0ECB81' }}> <div className="text-xs font-semibold" style={{ color: '#0ECB81' }}>
Leading by {gap.toFixed(2)}% {t('leadingBy', language, { gap: gap.toFixed(2) })}
</div> </div>
)} )}
{!isWinning && gap < 0 && ( {!isWinning && gap < 0 && (
<div className="text-xs font-semibold" style={{ color: '#F6465D' }}> <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>
)} )}
</div> </div>
+16 -13
View File
@@ -11,6 +11,8 @@ import {
} from 'recharts'; } from 'recharts';
import useSWR from 'swr'; import useSWR from 'swr';
import { api } from '../lib/api'; import { api } from '../lib/api';
import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations';
interface EquityPoint { interface EquityPoint {
timestamp: string; timestamp: string;
@@ -25,6 +27,7 @@ interface EquityChartProps {
} }
export function EquityChart({ traderId }: EquityChartProps) { export function EquityChart({ traderId }: EquityChartProps) {
const { language } = useLanguage();
const [displayMode, setDisplayMode] = useState<'dollar' | 'percent'>('dollar'); const [displayMode, setDisplayMode] = useState<'dollar' | 'percent'>('dollar');
const { data: history, error } = useSWR<EquityPoint[]>( 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="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 className="text-2xl"></div>
<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 className="text-sm" style={{ color: '#848E9C' }}>{error.message}</div>
</div> </div>
</div> </div>
@@ -60,11 +63,11 @@ export function EquityChart({ traderId }: EquityChartProps) {
if (!history || history.length === 0) { if (!history || history.length === 0) {
return ( return (
<div className="binance-card p-6"> <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-center py-16" style={{ color: '#848E9C' }}>
<div className="text-6xl mb-4 opacity-50">📊</div> <div className="text-6xl mb-4 opacity-50">📊</div>
<div className="text-lg font-semibold mb-2"></div> <div className="text-lg font-semibold mb-2">{t('noHistoricalData', language)}</div>
<div className="text-sm">线</div> <div className="text-sm">{t('dataWillAppear', language)}</div>
</div> </div>
</div> </div>
); );
@@ -153,7 +156,7 @@ export function EquityChart({ traderId }: EquityChartProps) {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div> <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"> <div className="flex items-baseline gap-4">
<span className="text-3xl font-bold mono" style={{ color: '#EAECEF' }}> <span className="text-3xl font-bold mono" style={{ color: '#EAECEF' }}>
{account?.total_equity.toFixed(2) || '0.00'} {account?.total_equity.toFixed(2) || '0.00'}
@@ -239,7 +242,7 @@ export function EquityChart({ traderId }: EquityChartProps) {
stroke="#474D57" stroke="#474D57"
strokeDasharray="3 3" strokeDasharray="3 3"
label={{ label={{
value: displayMode === 'dollar' ? '初始' : '0%', value: displayMode === 'dollar' ? t('initialBalance', language).split(' ')[0] : '0%',
fill: '#848E9C', fill: '#848E9C',
fontSize: 12, fontSize: 12,
}} }}
@@ -259,27 +262,27 @@ export function EquityChart({ traderId }: EquityChartProps) {
{/* Footer Stats */} {/* Footer Stats */}
<div className="mt-3 grid grid-cols-4 gap-3 pt-3" style={{ borderTop: '1px solid #2B3139' }}> <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="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' }}> <div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
{initialBalance.toFixed(2)} USDT {initialBalance.toFixed(2)} USDT
</div> </div>
</div> </div>
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}> <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' }}> <div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
{currentValue.raw_equity.toFixed(2)} USDT {currentValue.raw_equity.toFixed(2)} USDT
</div> </div>
</div> </div>
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}> <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('historicalCycles', language)}</div>
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>{history.length} </div> <div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>{history.length} {t('cycles', language)}</div>
</div> </div>
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}> <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' }}> <div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
{history.length > MAX_DISPLAY_POINTS {history.length > MAX_DISPLAY_POINTS
? `最近 ${MAX_DISPLAY_POINTS}` ? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
: '全部数据' : t('allData', language)
} }
</div> </div>
</div> </div>
+37
View File
@@ -0,0 +1,37 @@
import { createContext, useContext, useState, ReactNode } from 'react';
import type { Language } from '../i18n/translations';
interface LanguageContextType {
language: Language;
setLanguage: (lang: Language) => void;
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
export function LanguageProvider({ children }: { children: ReactNode }) {
// Initialize language from localStorage or default to English
const [language, setLanguage] = useState<Language>(() => {
const saved = localStorage.getItem('language');
return (saved === 'en' || saved === 'zh') ? saved : 'en';
});
// Save language to localStorage whenever it changes
const handleSetLanguage = (lang: Language) => {
setLanguage(lang);
localStorage.setItem('language', lang);
};
return (
<LanguageContext.Provider value={{ language, setLanguage: handleSetLanguage }}>
{children}
</LanguageContext.Provider>
);
}
export function useLanguage() {
const context = useContext(LanguageContext);
if (!context) {
throw new Error('useLanguage must be used within LanguageProvider');
}
return context;
}
+251
View File
@@ -0,0 +1,251 @@
export type Language = 'en' | 'zh';
export const translations = {
en: {
// Header
appTitle: 'AI Trading Competition',
subtitle: 'Qwen vs DeepSeek · Real-time',
competition: 'Competition',
details: 'Details',
running: 'RUNNING',
stopped: 'STOPPED',
// Footer
footerTitle: 'NOFX - AI Trading Competition System',
footerWarning: '⚠️ Trading involves risk. Use at your own discretion.',
// Stats Cards
totalEquity: 'Total Equity',
availableBalance: 'Available Balance',
totalPnL: 'Total P&L',
positions: 'Positions',
margin: 'Margin',
free: 'Free',
// Positions Table
currentPositions: 'Current Positions',
active: 'Active',
symbol: 'Symbol',
side: 'Side',
entryPrice: 'Entry Price',
markPrice: 'Mark Price',
quantity: 'Quantity',
positionValue: 'Position Value',
leverage: 'Leverage',
unrealizedPnL: 'Unrealized P&L',
liqPrice: 'Liq. Price',
long: 'LONG',
short: 'SHORT',
noPositions: 'No Positions',
noActivePositions: 'No active trading positions',
// Recent Decisions
recentDecisions: 'Recent Decisions',
lastCycles: 'Last {count} trading cycles',
noDecisionsYet: 'No Decisions Yet',
aiDecisionsWillAppear: 'AI trading decisions will appear here',
cycle: 'Cycle',
success: 'Success',
failed: 'Failed',
aiThinking: 'AI Chain of Thought',
collapse: 'Collapse',
expand: 'Expand',
// Equity Chart
accountEquityCurve: 'Account Equity Curve',
noHistoricalData: 'No Historical Data',
dataWillAppear: 'Equity curve will appear after running a few cycles',
initialBalance: 'Initial Balance',
currentEquity: 'Current Equity',
historicalCycles: 'Historical Cycles',
displayRange: 'Display Range',
recent: 'Recent',
allData: 'All Data',
cycles: 'Cycles',
// Competition Page
aiCompetition: 'AI Competition',
traders: 'traders',
liveBattle: 'Qwen vs DeepSeek · Live Battle',
leader: 'Leader',
leaderboard: 'Leaderboard',
live: 'LIVE',
performanceComparison: 'Performance Comparison',
realTimePnL: 'Real-time PnL %',
headToHead: 'Head-to-Head Battle',
leadingBy: 'Leading by {gap}%',
behindBy: 'Behind by {gap}%',
equity: 'Equity',
pnl: 'P&L',
pos: 'Pos',
// AI Learning
aiLearning: 'AI Learning & Reflection',
tradesAnalyzed: '{count} trades analyzed · Real-time evolution',
latestReflection: 'Latest Reflection',
fullCoT: 'Full Chain of Thought',
totalTrades: 'Total Trades',
winRate: 'Win Rate',
avgWin: 'Avg Win',
avgLoss: 'Avg Loss',
profitFactor: 'Profit Factor',
avgWinDivLoss: 'Avg Win ÷ Avg Loss',
excellent: '🔥 Excellent - Strong profitability',
good: '✓ Good - Stable profits',
fair: '⚠️ Fair - Needs optimization',
poor: '❌ Poor - Losses exceed gains',
bestPerformer: 'Best Performer',
worstPerformer: 'Worst Performer',
symbolPerformance: 'Symbol Performance',
tradeHistory: 'Trade History',
completedTrades: 'Recent {count} completed trades',
noCompletedTrades: 'No completed trades yet',
completedTradesWillAppear: 'Completed trades will appear here',
entry: 'Entry',
exit: 'Exit',
stopLoss: 'Stop Loss',
latest: 'Latest',
// AI Learning Description
howAILearns: 'How AI Learns & Evolves',
aiLearningPoint1: 'Analyzes last 20 trading cycles before each decision',
aiLearningPoint2: 'Identifies best & worst performing symbols',
aiLearningPoint3: 'Optimizes position sizing based on win rate',
aiLearningPoint4: 'Avoids repeating past mistakes',
// Loading & Error
loading: 'Loading...',
loadingError: '⚠️ Failed to load AI learning data',
noCompleteData: 'No complete trading data (needs to complete open → close cycle)',
},
zh: {
// Header
appTitle: 'AI交易竞赛',
subtitle: 'Qwen vs DeepSeek · 实时',
competition: '竞赛',
details: '详情',
running: '运行中',
stopped: '已停止',
// Footer
footerTitle: 'NOFX - AI交易竞赛系统',
footerWarning: '⚠️ 交易有风险,请谨慎使用。',
// Stats Cards
totalEquity: '总净值',
availableBalance: '可用余额',
totalPnL: '总盈亏',
positions: '持仓',
margin: '保证金',
free: '空闲',
// Positions Table
currentPositions: '当前持仓',
active: '活跃',
symbol: '币种',
side: '方向',
entryPrice: '入场价',
markPrice: '标记价',
quantity: '数量',
positionValue: '仓位价值',
leverage: '杠杆',
unrealizedPnL: '未实现盈亏',
liqPrice: '强平价',
long: '多头',
short: '空头',
noPositions: '无持仓',
noActivePositions: '当前没有活跃的交易持仓',
// Recent Decisions
recentDecisions: '最近决策',
lastCycles: '最近 {count} 个交易周期',
noDecisionsYet: '暂无决策',
aiDecisionsWillAppear: 'AI交易决策将显示在这里',
cycle: '周期',
success: '成功',
failed: '失败',
aiThinking: '💭 AI思维链分析',
collapse: '▼ 收起',
expand: '▶ 展开',
// Equity Chart
accountEquityCurve: '账户净值曲线',
noHistoricalData: '暂无历史数据',
dataWillAppear: '运行几个周期后将显示收益率曲线',
initialBalance: '初始余额',
currentEquity: '当前净值',
historicalCycles: '历史周期',
displayRange: '显示范围',
recent: '最近',
allData: '全部数据',
cycles: '个',
// Competition Page
aiCompetition: 'AI竞赛',
traders: '位交易者',
liveBattle: 'Qwen vs DeepSeek · 实时对战',
leader: '🥇 领先者',
leaderboard: '🥇 排行榜',
live: '直播',
performanceComparison: '📈 表现对比',
realTimePnL: '实时盈亏百分比',
headToHead: '⚔️ 正面对决',
leadingBy: '领先 {gap}%',
behindBy: '落后 {gap}%',
equity: '净值',
pnl: '盈亏',
pos: '仓位',
// AI Learning
aiLearning: 'AI学习与反思',
tradesAnalyzed: '已分析 {count} 笔交易 · 实时演化',
latestReflection: '最新反思',
fullCoT: '📋 完整思维链',
totalTrades: '总交易数',
winRate: '胜率',
avgWin: '平均盈利',
avgLoss: '平均亏损',
profitFactor: '盈亏比',
avgWinDivLoss: '平均盈利 ÷ 平均亏损',
excellent: '🔥 优秀 - 盈利能力强',
good: '✓ 良好 - 稳定盈利',
fair: '⚠️ 一般 - 需要优化',
poor: '❌ 较差 - 亏损超过盈利',
bestPerformer: '最佳表现',
worstPerformer: '最差表现',
symbolPerformance: '📊 币种表现',
tradeHistory: '历史成交',
completedTrades: '最近 {count} 笔已完成交易',
noCompletedTrades: '暂无完成的交易',
completedTradesWillAppear: '已完成的交易将显示在这里',
entry: '入场',
exit: '出场',
stopLoss: '止损',
latest: '最新',
// AI Learning Description
howAILearns: '💡 AI如何学习和进化',
aiLearningPoint1: '每次决策前分析最近20个交易周期',
aiLearningPoint2: '识别表现最好和最差的币种',
aiLearningPoint3: '根据胜率优化仓位大小',
aiLearningPoint4: '避免重复过去的错误',
// Loading & Error
loading: '加载中...',
loadingError: '⚠️ 加载AI学习数据失败',
noCompleteData: '暂无完整交易数据(需要完成开仓→平仓的完整周期)',
}
};
export function t(key: string, lang: Language, params?: Record<string, string | number>): string {
let text = translations[lang][key as keyof typeof translations['en']] || key;
// Replace parameters like {count}, {gap}, etc.
if (params) {
Object.entries(params).forEach(([param, value]) => {
text = text.replace(`{${param}}`, String(value));
});
}
return text;
}