mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 13:00:59 +08:00
Merge remote tracking branch into local development
- Resolved conflicts in README.md: Combined web-based config updates with multi-exchange support - Resolved conflicts in main.go: Fixed database initialization and default coin settings - Resolved conflicts in manager/trader_manager.go: Updated trader management for new database structure - Resolved conflicts in web/src/App.tsx: Combined UI improvements with responsive design - Resolved conflicts in web/.dockerignore: Merged dependency exclusions - Removed deprecated files: Dockerfile, config/config.go, web/Dockerfile, ComparisonChart.tsx, CompetitionPage.tsx 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
199
web/src/App.tsx
199
web/src/App.tsx
@@ -42,9 +42,9 @@ function App() {
|
||||
: null,
|
||||
() => api.getStatus(selectedTraderId),
|
||||
{
|
||||
refreshInterval: 5000,
|
||||
revalidateOnFocus: true,
|
||||
dedupingInterval: 0,
|
||||
refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存)
|
||||
revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求
|
||||
dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求
|
||||
}
|
||||
);
|
||||
|
||||
@@ -54,9 +54,9 @@ function App() {
|
||||
: null,
|
||||
() => api.getAccount(selectedTraderId),
|
||||
{
|
||||
refreshInterval: 5000,
|
||||
revalidateOnFocus: true,
|
||||
dedupingInterval: 0,
|
||||
refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存)
|
||||
revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求
|
||||
dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求
|
||||
}
|
||||
);
|
||||
|
||||
@@ -66,9 +66,9 @@ function App() {
|
||||
: null,
|
||||
() => api.getPositions(selectedTraderId),
|
||||
{
|
||||
refreshInterval: 5000,
|
||||
revalidateOnFocus: true,
|
||||
dedupingInterval: 0,
|
||||
refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存)
|
||||
revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求
|
||||
dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求
|
||||
}
|
||||
);
|
||||
|
||||
@@ -77,7 +77,11 @@ function App() {
|
||||
? `decisions/latest-${selectedTraderId}`
|
||||
: null,
|
||||
() => api.getLatestDecisions(selectedTraderId),
|
||||
{ refreshInterval: 10000 }
|
||||
{
|
||||
refreshInterval: 30000, // 30秒刷新(决策更新频率较低)
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 20000,
|
||||
}
|
||||
);
|
||||
|
||||
const { data: stats } = useSWR<Statistics>(
|
||||
@@ -85,7 +89,11 @@ function App() {
|
||||
? `statistics-${selectedTraderId}`
|
||||
: null,
|
||||
() => api.getStatistics(selectedTraderId),
|
||||
{ refreshInterval: 10000 }
|
||||
{
|
||||
refreshInterval: 30000, // 30秒刷新(统计数据更新频率较低)
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 20000,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -101,55 +109,104 @@ function App() {
|
||||
<div className="min-h-screen" style={{ background: '#0B0E11', color: '#EAECEF' }}>
|
||||
{/* Header - Binance Style */}
|
||||
<header className="glass sticky top-0 z-50 backdrop-blur-xl">
|
||||
<div className="max-w-[1920px] mx-auto px-6 py-4">
|
||||
<div className="relative flex items-center">
|
||||
{/* Left - Logo and Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-xl" style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
|
||||
<div className="max-w-[1920px] mx-auto px-3 sm:px-6 py-3 sm:py-4">
|
||||
{/* Mobile: Two rows, Desktop: Single row */}
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
{/* Left: Logo and Title */}
|
||||
<div className="flex items-center gap-2 sm:gap-3 flex-shrink-0">
|
||||
<div className="w-7 h-7 sm:w-8 sm:h-8 rounded-full flex items-center justify-center text-lg sm:text-xl" style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
|
||||
⚡
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
<h1 className="text-base sm:text-xl font-bold leading-tight" style={{ color: '#EAECEF' }}>
|
||||
{t('appTitle', language)}
|
||||
</h1>
|
||||
<p className="text-xs mono" style={{ color: '#848E9C' }}>
|
||||
<p className="text-xs mono hidden sm:block" style={{ color: '#848E9C' }}>
|
||||
{t('subtitle', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center - Page Toggle (absolutely positioned) */}
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
||||
<button
|
||||
onClick={() => setCurrentPage('traders')}
|
||||
className={`px-4 py-2 rounded text-sm font-semibold transition-all`}
|
||||
style={currentPage === 'traders'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
|
||||
{/* Right: Controls - Wrap on mobile */}
|
||||
<div className="flex items-center gap-2 flex-wrap md:flex-nowrap">
|
||||
{/* GitHub Link - Hidden on mobile, icon only on tablet */}
|
||||
<a
|
||||
href="https://github.com/tinkle-community/nofx"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden sm:flex items-center gap-2 px-2 md:px-3 py-1.5 md: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';
|
||||
}}
|
||||
>
|
||||
{t('aiTraders', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('trader')}
|
||||
className={`px-4 py-2 rounded text-sm font-semibold transition-all`}
|
||||
style={currentPage === 'trader'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
{t('tradingPanel', language)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right - Actions */}
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
<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 className="hidden md:inline">GitHub</span>
|
||||
</a>
|
||||
|
||||
{/* Language Toggle */}
|
||||
<div className="flex gap-0.5 sm:gap-1 rounded p-0.5 sm:p-1" style={{ background: '#1E2329' }}>
|
||||
<button
|
||||
onClick={() => setLanguage('zh')}
|
||||
className="px-2 sm:px-3 py-1 sm: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-2 sm:px-3 py-1 sm: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-0.5 sm:gap-1 rounded p-0.5 sm:p-1" style={{ background: '#1E2329' }}>
|
||||
<button
|
||||
onClick={() => setCurrentPage('traders')}
|
||||
className="px-2 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-semibold transition-all"
|
||||
style={currentPage === 'traders'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
{t('aiTraders', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('trader')}
|
||||
className="px-2 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-semibold transition-all"
|
||||
style={currentPage === 'trader'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
{t('tradingPanel', language)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Trader Selector (only show on trader page) */}
|
||||
{currentPage === 'trader' && traders && traders.length > 0 && (
|
||||
<select
|
||||
value={selectedTraderId}
|
||||
onChange={(e) => setSelectedTraderId(e.target.value)}
|
||||
className="rounded px-3 py-2 text-sm font-medium cursor-pointer transition-colors"
|
||||
className="rounded px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm font-medium cursor-pointer transition-colors flex-1 sm:flex-initial"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
{traders.map((trader) => (
|
||||
@@ -160,34 +217,10 @@ function App() {
|
||||
</select>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Status Indicator (only show on trader page) */}
|
||||
{currentPage === 'trader' && status && (
|
||||
<div
|
||||
className="flex items-center gap-2 px-3 py-2 rounded"
|
||||
className="flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 rounded"
|
||||
style={status.is_running
|
||||
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81', border: '1px solid rgba(14, 203, 129, 0.2)' }
|
||||
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }
|
||||
@@ -323,23 +356,23 @@ function TraderDetailsPage({
|
||||
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) > 0 : false}
|
||||
positive={(account?.total_pnl ?? 0) > 0}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('availableBalance', language)}
|
||||
value={`${account?.available_balance?.toFixed(2) || '0.00'} USDT`}
|
||||
subtitle={`${((account?.available_balance && account?.total_equity ? (account.available_balance / account.total_equity) * 100 : 0)).toFixed(1)}% ${t('free', language)}`}
|
||||
subtitle={`${(account?.available_balance && account?.total_equity ? ((account.available_balance / account.total_equity) * 100).toFixed(1) : '0.0')}% ${t('free', language)}`}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('totalPnL', language)}
|
||||
value={`${(account?.total_pnl || 0) >= 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'} USDT`}
|
||||
value={`${account?.total_pnl !== undefined && account.total_pnl >= 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'} USDT`}
|
||||
change={account?.total_pnl_pct || 0}
|
||||
positive={account ? (account.total_pnl || 0) >= 0 : false}
|
||||
positive={(account?.total_pnl ?? 0) >= 0}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('positions', language)}
|
||||
value={`${account?.position_count || 0}`}
|
||||
subtitle={`${t('margin', language)}: ${account?.margin_used_pct.toFixed(1) || '0.0'}%`}
|
||||
subtitle={`${t('margin', language)}: ${account?.margin_used_pct?.toFixed(1) || '0.0'}%`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -511,7 +544,7 @@ function StatCard({
|
||||
|
||||
// Decision Card Component with CoT Trace - Binance Style
|
||||
function DecisionCard({ decision, language }: { decision: DecisionRecord; language: Language }) {
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [showInputPrompt, setShowInputPrompt] = useState(false);
|
||||
const [showCoT, setShowCoT] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -535,20 +568,20 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Input Prompt - Collapsible */}
|
||||
{(decision as any).input_prompt && (
|
||||
{/* Input Prompt - Collapsible */}
|
||||
{decision.input_prompt && (
|
||||
<div className="mb-3">
|
||||
<button
|
||||
onClick={() => setShowInput(!showInput)}
|
||||
onClick={() => setShowInputPrompt(!showInputPrompt)}
|
||||
className="flex items-center gap-2 text-sm transition-colors"
|
||||
style={{ color: '#8B5CF6' }}
|
||||
style={{ color: '#60a5fa' }}
|
||||
>
|
||||
<span className="font-semibold">📥 {t('inputPrompt', language)}</span>
|
||||
<span className="text-xs">{showInput ? t('collapse', language) : t('expand', language)}</span>
|
||||
<span className="text-xs">{showInputPrompt ? t('collapse', language) : t('expand', language)}</span>
|
||||
</button>
|
||||
{showInput && (
|
||||
{showInputPrompt && (
|
||||
<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' }}>
|
||||
{(decision as any).input_prompt}
|
||||
{decision.input_prompt}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import useSWR from 'swr';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
import { api } from '../lib/api';
|
||||
|
||||
interface TradeOutcome {
|
||||
symbol: string;
|
||||
side: string;
|
||||
quantity: number;
|
||||
leverage: number;
|
||||
open_price: number;
|
||||
close_price: number;
|
||||
position_value: number;
|
||||
margin_used: number;
|
||||
pn_l: number;
|
||||
pn_l_pct: number;
|
||||
duration: string;
|
||||
@@ -44,14 +49,16 @@ interface AILearningProps {
|
||||
traderId: string;
|
||||
}
|
||||
|
||||
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,
|
||||
{ refreshInterval: 10000 }
|
||||
traderId ? `performance-${traderId}` : 'performance',
|
||||
() => api.getPerformance(traderId),
|
||||
{
|
||||
refreshInterval: 30000, // 30秒刷新(AI学习分析数据更新频率较低)
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 20000,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -555,6 +562,34 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Position Details */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-3 text-xs">
|
||||
<div>
|
||||
<div style={{ color: '#94A3B8' }}>Quantity</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#CBD5E1' }}>
|
||||
{trade.quantity ? trade.quantity.toFixed(4) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div style={{ color: '#94A3B8' }}>Leverage</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#FCD34D' }}>
|
||||
{trade.leverage ? `${trade.leverage}x` : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#94A3B8' }}>Position Value</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#CBD5E1' }}>
|
||||
{trade.position_value ? `$${trade.position_value.toFixed(2)}` : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div style={{ color: '#94A3B8' }}>Margin Used</div>
|
||||
<div className="font-mono font-semibold" style={{ color: '#A78BFA' }}>
|
||||
{trade.margin_used ? `$${trade.margin_used.toFixed(2)}` : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg p-2 mb-2" style={{
|
||||
background: isProfitable ? 'rgba(16, 185, 129, 0.1)' : 'rgba(248, 113, 113, 0.1)'
|
||||
}}>
|
||||
|
||||
@@ -34,7 +34,9 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
traderId ? `equity-history-${traderId}` : 'equity-history',
|
||||
() => api.getEquityHistory(traderId),
|
||||
{
|
||||
refreshInterval: 10000, // 每10秒刷新
|
||||
refreshInterval: 30000, // 30秒刷新(历史数据更新频率较低)
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 20000,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -42,7 +44,9 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
traderId ? `account-${traderId}` : 'account',
|
||||
() => api.getAccount(traderId),
|
||||
{
|
||||
refreshInterval: 5000,
|
||||
refreshInterval: 15000, // 15秒刷新(配合后端缓存)
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 10000,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -60,7 +64,10 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!history || history.length === 0) {
|
||||
// 过滤掉无效数据:total_equity为0或小于1的数据点(API失败导致)
|
||||
const validHistory = history?.filter(point => point.total_equity > 1) || [];
|
||||
|
||||
if (!validHistory || validHistory.length === 0) {
|
||||
return (
|
||||
<div className="binance-card p-6">
|
||||
<h3 className="text-lg font-semibold mb-6" style={{ color: '#EAECEF' }}>{t('accountEquityCurve', language)}</h3>
|
||||
@@ -76,12 +83,14 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
// 限制显示最近的数据点(性能优化)
|
||||
// 如果数据超过2000个点,只显示最近2000个
|
||||
const MAX_DISPLAY_POINTS = 2000;
|
||||
const displayHistory = history.length > MAX_DISPLAY_POINTS
|
||||
? history.slice(-MAX_DISPLAY_POINTS)
|
||||
: history;
|
||||
const displayHistory = validHistory.length > MAX_DISPLAY_POINTS
|
||||
? validHistory.slice(-MAX_DISPLAY_POINTS)
|
||||
: validHistory;
|
||||
|
||||
// 计算初始余额(使用第一个数据点)
|
||||
const initialBalance = history[0]?.total_equity || 1000;
|
||||
// 计算初始余额(使用第一个有效数据点,如果无数据则从account获取,最后才用默认值)
|
||||
const initialBalance = validHistory[0]?.total_equity
|
||||
|| account?.total_equity
|
||||
|| 100; // 默认值改为100,与常见配置一致
|
||||
|
||||
// 转换数据格式
|
||||
const chartData = displayHistory.map((point) => {
|
||||
@@ -152,19 +161,19 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="binance-card p-5 animate-fade-in">
|
||||
<div className="binance-card p-3 sm:p-5 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<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' }}>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base sm:text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>{t('accountEquityCurve', language)}</h3>
|
||||
<div className="flex flex-col sm:flex-row sm:items-baseline gap-2 sm:gap-4">
|
||||
<span className="text-2xl sm:text-3xl font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{account?.total_equity.toFixed(2) || '0.00'}
|
||||
<span className="text-lg ml-1" style={{ color: '#848E9C' }}>USDT</span>
|
||||
<span className="text-base sm:text-lg ml-1" style={{ color: '#848E9C' }}>USDT</span>
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span
|
||||
className="text-lg font-bold mono px-3 py-1 rounded"
|
||||
className="text-sm sm:text-lg font-bold mono px-2 sm:px-3 py-1 rounded"
|
||||
style={{
|
||||
color: isProfit ? '#0ECB81' : '#F6465D',
|
||||
background: isProfit ? 'rgba(14, 203, 129, 0.1)' : 'rgba(246, 70, 93, 0.1)',
|
||||
@@ -174,7 +183,7 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
{isProfit ? '▲' : '▼'} {isProfit ? '+' : ''}
|
||||
{currentValue.raw_pnl_pct}%
|
||||
</span>
|
||||
<span className="text-sm mono" style={{ color: '#848E9C' }}>
|
||||
<span className="text-xs sm:text-sm mono" style={{ color: '#848E9C' }}>
|
||||
({isProfit ? '+' : ''}{currentValue.raw_pnl.toFixed(2)} USDT)
|
||||
</span>
|
||||
</div>
|
||||
@@ -182,10 +191,10 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
</div>
|
||||
|
||||
{/* Display Mode Toggle */}
|
||||
<div className="flex gap-1 rounded p-1" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="flex gap-0.5 sm:gap-1 rounded p-0.5 sm:p-1 self-start sm:self-auto" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<button
|
||||
onClick={() => setDisplayMode('dollar')}
|
||||
className="px-4 py-2 rounded text-sm font-bold transition-all"
|
||||
className="px-3 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-bold transition-all"
|
||||
style={displayMode === 'dollar'
|
||||
? { background: '#F0B90B', color: '#000', boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
@@ -195,7 +204,7 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDisplayMode('percent')}
|
||||
className="px-4 py-2 rounded text-sm font-bold transition-all"
|
||||
className="px-3 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-bold transition-all"
|
||||
style={displayMode === 'percent'
|
||||
? { background: '#F0B90B', color: '#000', boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
@@ -248,39 +257,40 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
type="natural"
|
||||
dataKey="value"
|
||||
stroke="url(#colorGradient)"
|
||||
strokeWidth={2.5}
|
||||
strokeWidth={3}
|
||||
dot={chartData.length > 50 ? false : { fill: '#F0B90B', r: 3 }}
|
||||
activeDot={{ r: 6, fill: '#FCD535', stroke: '#F0B90B', strokeWidth: 2 }}
|
||||
connectNulls={true}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* 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-2 sm:grid-cols-4 gap-2 sm: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' }}>{t('initialBalance', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
<div className="text-xs sm: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' }}>{t('currentEquity', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
<div className="text-xs sm: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' }}>{t('historicalCycles', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>{history.length} {t('cycles', language)}</div>
|
||||
<div className="text-xs sm:text-sm font-bold mono" style={{ color: '#EAECEF' }}>{validHistory.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' }}>{t('displayRange', language)}</div>
|
||||
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{history.length > MAX_DISPLAY_POINTS
|
||||
<div className="text-xs sm:text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{validHistory.length > MAX_DISPLAY_POINTS
|
||||
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
|
||||
: t('allData', language)
|
||||
}
|
||||
|
||||
@@ -161,4 +161,14 @@ export const api = {
|
||||
if (!res.ok) throw new Error('获取历史数据失败');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// 获取AI学习表现分析(支持trader_id)
|
||||
async getPerformance(traderId?: string): Promise<any> {
|
||||
const url = traderId
|
||||
? `${API_BASE}/performance?trader_id=${traderId}`
|
||||
: `${API_BASE}/performance`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error('获取AI学习数据失败');
|
||||
return res.json();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface AccountSnapshot {
|
||||
export interface DecisionRecord {
|
||||
timestamp: string;
|
||||
cycle_number: number;
|
||||
input_prompt: string;
|
||||
cot_trace: string;
|
||||
decision_json: string;
|
||||
account_state: AccountSnapshot;
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface DecisionAction {
|
||||
export interface DecisionRecord {
|
||||
timestamp: string;
|
||||
cycle_number: number;
|
||||
input_prompt: string;
|
||||
cot_trace: string;
|
||||
decision_json: string;
|
||||
account_state: {
|
||||
|
||||
Reference in New Issue
Block a user