mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +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:
@@ -27,4 +27,3 @@ config.json
|
||||
# 决策日志
|
||||
decision_logs/
|
||||
coin_pool_cache/
|
||||
nofx-auto
|
||||
|
||||
+123
-38
@@ -4,6 +4,8 @@ import { api } from './lib/api';
|
||||
import { EquityChart } from './components/EquityChart';
|
||||
import { CompetitionPage } from './components/CompetitionPage';
|
||||
import AILearning from './components/AILearning';
|
||||
import { LanguageProvider, useLanguage } from './contexts/LanguageContext';
|
||||
import { t, type Language } from './i18n/translations';
|
||||
import type {
|
||||
SystemStatus,
|
||||
AccountInfo,
|
||||
@@ -16,6 +18,7 @@ import type {
|
||||
type Page = 'competition' | 'trader';
|
||||
|
||||
function App() {
|
||||
const { language, setLanguage } = useLanguage();
|
||||
const [currentPage, setCurrentPage] = useState<Page>('competition');
|
||||
const [selectedTraderId, setSelectedTraderId] = useState<string | undefined>();
|
||||
const [lastUpdate, setLastUpdate] = useState<string>('--:--:--');
|
||||
@@ -106,14 +109,62 @@ function App() {
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
AI Trading Competition
|
||||
{t('appTitle', language)}
|
||||
</h1>
|
||||
<p className="text-xs mono" style={{ color: '#848E9C' }}>
|
||||
Qwen vs DeepSeek · Real-time
|
||||
{t('subtitle', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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 */}
|
||||
<div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
||||
<button
|
||||
@@ -126,7 +177,7 @@ function App() {
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
Competition
|
||||
{t('competition', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('trader')}
|
||||
@@ -136,7 +187,7 @@ function App() {
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
Details
|
||||
{t('details', language)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -170,7 +221,7 @@ function App() {
|
||||
style={{ background: status.is_running ? '#0ECB81' : '#F6465D' }}
|
||||
/>
|
||||
<span className="font-semibold mono text-xs">
|
||||
{status.is_running ? 'RUNNING' : 'STOPPED'}
|
||||
{t(status.is_running ? 'running' : 'stopped', language)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -192,6 +243,7 @@ function App() {
|
||||
decisions={decisions}
|
||||
stats={stats}
|
||||
lastUpdate={lastUpdate}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
@@ -199,8 +251,32 @@ function App() {
|
||||
{/* Footer */}
|
||||
<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' }}>
|
||||
<p>NOFX - AI Trading Competition System</p>
|
||||
<p className="mt-1">⚠️ Trading involves risk. Use at your own discretion.</p>
|
||||
<p>{t('footerTitle', language)}</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>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -216,6 +292,7 @@ function TraderDetailsPage({
|
||||
decisions,
|
||||
stats,
|
||||
lastUpdate,
|
||||
language,
|
||||
}: {
|
||||
selectedTrader?: TraderInfo;
|
||||
status?: SystemStatus;
|
||||
@@ -224,6 +301,7 @@ function TraderDetailsPage({
|
||||
decisions?: DecisionRecord[];
|
||||
stats?: Statistics;
|
||||
lastUpdate: string;
|
||||
language: Language;
|
||||
}) {
|
||||
if (!selectedTrader) {
|
||||
return (
|
||||
@@ -290,26 +368,26 @@ function TraderDetailsPage({
|
||||
{/* Account Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<StatCard
|
||||
title="Total Equity"
|
||||
title={t('totalEquity', language)}
|
||||
value={`${account?.total_equity.toFixed(2) || '0.00'} USDT`}
|
||||
change={account?.total_pnl_pct || 0}
|
||||
positive={account ? account.total_pnl > 0 : false}
|
||||
/>
|
||||
<StatCard
|
||||
title="Available Balance"
|
||||
title={t('availableBalance', language)}
|
||||
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
|
||||
title="Total P&L"
|
||||
title={t('totalPnL', language)}
|
||||
value={`${account?.total_pnl >= 0 ? '+' : ''}${account?.total_pnl.toFixed(2) || '0.00'} USDT`}
|
||||
change={account?.total_pnl_pct || 0}
|
||||
positive={account ? account.total_pnl >= 0 : false}
|
||||
/>
|
||||
<StatCard
|
||||
title="Positions"
|
||||
title={t('positions', language)}
|
||||
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>
|
||||
|
||||
@@ -326,11 +404,11 @@ function TraderDetailsPage({
|
||||
<div className="binance-card p-6 animate-slide-in" style={{ animationDelay: '0.15s' }}>
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<h2 className="text-xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
|
||||
📈 Current Positions
|
||||
📈 {t('currentPositions', language)}
|
||||
</h2>
|
||||
{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)' }}>
|
||||
{positions.length} Active
|
||||
{positions.length} {t('active', language)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -339,15 +417,15 @@ function TraderDetailsPage({
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-left border-b border-gray-800">
|
||||
<tr>
|
||||
<th className="pb-3 font-semibold text-gray-400">Symbol</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">Side</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">Entry Price</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">Mark Price</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">Quantity</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">Position Value</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">Leverage</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">Unrealized P&L</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">Liq. Price</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">{t('symbol', language)}</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">{t('side', language)}</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">{t('entryPrice', language)}</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">{t('markPrice', language)}</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">{t('quantity', language)}</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">{t('positionValue', language)}</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">{t('leverage', language)}</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">{t('unrealizedPnL', language)}</th>
|
||||
<th className="pb-3 font-semibold text-gray-400">{t('liqPrice', language)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -362,7 +440,7 @@ function TraderDetailsPage({
|
||||
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
||||
}
|
||||
>
|
||||
{pos.side.toUpperCase()}
|
||||
{t(pos.side === 'long' ? 'long' : 'short', language)}
|
||||
</span>
|
||||
</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-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('noPositions', language)}</div>
|
||||
<div className="text-sm">{t('noActivePositions', language)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -410,10 +488,10 @@ function TraderDetailsPage({
|
||||
🧠
|
||||
</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 && (
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
Last {decisions.length} trading cycles
|
||||
{t('lastCycles', language, { count: decisions.length })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -423,13 +501,13 @@ function TraderDetailsPage({
|
||||
<div className="space-y-4 overflow-y-auto pr-2" style={{ maxHeight: 'calc(100vh - 280px)' }}>
|
||||
{decisions && decisions.length > 0 ? (
|
||||
decisions.map((decision, i) => (
|
||||
<DecisionCard key={i} decision={decision} />
|
||||
<DecisionCard key={i} decision={decision} language={language} />
|
||||
))
|
||||
) : (
|
||||
<div className="py-16 text-center">
|
||||
<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-sm" style={{ color: '#848E9C' }}>AI trading decisions will appear here</div>
|
||||
<div className="text-lg font-semibold mb-2" style={{ color: '#EAECEF' }}>{t('noDecisionsYet', language)}</div>
|
||||
<div className="text-sm" style={{ color: '#848E9C' }}>{t('aiDecisionsWillAppear', language)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -480,7 +558,7 @@ function StatCard({
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
return (
|
||||
@@ -488,7 +566,7 @@ function DecisionCard({ decision }: { decision: DecisionRecord }) {
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<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' }}>
|
||||
{new Date(decision.timestamp).toLocaleString()}
|
||||
</div>
|
||||
@@ -500,7 +578,7 @@ function DecisionCard({ decision }: { decision: DecisionRecord }) {
|
||||
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
||||
}
|
||||
>
|
||||
{decision.success ? 'Success' : 'Failed'}
|
||||
{t(decision.success ? 'success' : 'failed', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -512,8 +590,8 @@ function DecisionCard({ decision }: { decision: DecisionRecord }) {
|
||||
className="flex items-center gap-2 text-sm transition-colors"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
<span className="font-semibold">💭 AI思维链分析</span>
|
||||
<span className="text-xs">{showCoT ? '▼ 收起' : '▶ 展开'}</span>
|
||||
<span className="font-semibold">{t('aiThinking', language)}</span>
|
||||
<span className="text-xs">{showCoT ? t('collapse', language) : t('expand', language)}</span>
|
||||
</button>
|
||||
{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' }}>
|
||||
@@ -586,4 +664,11 @@ function DecisionCard({ decision }: { decision: DecisionRecord }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
// Wrap App with LanguageProvider
|
||||
export default function AppWithLanguage() {
|
||||
return (
|
||||
<LanguageProvider>
|
||||
<App />
|
||||
</LanguageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user