竞赛fix、交易员新增参数

This commit is contained in:
icy
2025-11-01 02:17:11 +08:00
parent 67f8e0f924
commit a7cc5e5ed4
20 changed files with 2065 additions and 585 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -14,12 +14,15 @@ import useSWR from 'swr';
import { api } from '../lib/api';
import type { CompetitionTraderData } from '../types';
import { getTraderColor } from '../utils/traderColors';
import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations';
interface ComparisonChartProps {
traders: CompetitionTraderData[];
}
export function ComparisonChart({ traders }: ComparisonChartProps) {
const { language } = useLanguage();
// 获取所有trader的历史数据 - 使用单个useSWR并发请求所有trader数据
// 生成唯一的key当traders变化时会触发重新请求
const tradersKey = traders.map(t => t.trader_id).sort().join(',');
@@ -134,8 +137,8 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
return (
<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>
);
}
@@ -311,25 +314,25 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{/* Stats */}
<div className="mt-6 grid grid-cols-4 gap-4 pt-5" style={{ borderTop: '1px solid #2B3139' }}>
<div className="p-3 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('comparisonMode', language)}</div>
<div className="text-base font-bold" style={{ color: '#EAECEF' }}>PnL %</div>
</div>
<div className="p-3 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-base font-bold mono" style={{ color: '#EAECEF' }}>{combinedData.length} </div>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('dataPoints', language)}</div>
<div className="text-base font-bold mono" style={{ color: '#EAECEF' }}>{t('count', language, {count: combinedData.length})}</div>
</div>
<div className="p-3 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('currentGap', language)}</div>
<div className="text-base font-bold mono" style={{ color: currentGap > 1 ? '#F0B90B' : '#EAECEF' }}>
{currentGap.toFixed(2)}%
</div>
</div>
<div className="p-3 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-base font-bold mono" style={{ color: '#EAECEF' }}>
{combinedData.length > MAX_DISPLAY_POINTS
? `最近 ${MAX_DISPLAY_POINTS}`
: '全部数据'}
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
: t('allData', language)}
</div>
</div>
</div>

View File

@@ -1,10 +1,18 @@
import { useState } from 'react';
import useSWR from 'swr';
import { api } from '../lib/api';
import type { CompetitionData } from '../types';
import { ComparisonChart } from './ComparisonChart';
import { TraderConfigModal } from './TraderConfigModal';
import { getTraderColor } from '../utils/traderColors';
import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations';
export function CompetitionPage() {
const { language } = useLanguage();
const [selectedTrader, setSelectedTrader] = useState<any>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const { data: competition } = useSWR<CompetitionData>(
'competition',
api.getCompetition,
@@ -15,6 +23,21 @@ export function CompetitionPage() {
}
);
const handleTraderClick = async (traderId: string) => {
try {
const traderConfig = await api.getTraderConfig(traderId);
setSelectedTrader(traderConfig);
setIsModalOpen(true);
} catch (error) {
console.error('Failed to fetch trader config:', error);
}
};
const closeModal = () => {
setIsModalOpen(false);
setSelectedTrader(null);
};
if (!competition || !competition.traders) {
return (
<div className="space-y-6">
@@ -59,18 +82,18 @@ export function CompetitionPage() {
</div>
<div>
<h1 className="text-2xl font-bold flex items-center gap-2" style={{ color: '#EAECEF' }}>
AI竞赛
{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}
{competition.count} {t('traders', language)}
</span>
</h1>
<p className="text-xs" style={{ color: '#848E9C' }}>
{t('liveBattle', language)}
</p>
</div>
</div>
<div className="text-right">
<div className="text-xs mb-1" style={{ color: '#848E9C' }}></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) >= 0 ? '#0ECB81' : '#F6465D' }}>
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
@@ -84,10 +107,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' }}>
{t('performanceComparison', language)}
</h2>
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('realTimePnL', language)}
</div>
</div>
<ComparisonChart traders={sortedTraders} />
@@ -97,10 +120,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' }}>
{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)' }}>
{t('live', language)}
</div>
</div>
<div className="space-y-2">
@@ -111,7 +134,8 @@ export function CompetitionPage() {
return (
<div
key={trader.trader_id}
className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px]"
onClick={() => handleTraderClick(trader.trader_id)}
className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px] cursor-pointer hover:shadow-lg"
style={{
background: isLeader ? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, #0B0E11 100%)' : '#0B0E11',
border: `1px solid ${isLeader ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
@@ -127,7 +151,7 @@ export function CompetitionPage() {
<div>
<div className="font-bold text-sm" style={{ color: '#EAECEF' }}>{trader.trader_name}</div>
<div className="text-xs mono font-semibold" style={{ color: traderColor }}>
{trader.ai_model.toUpperCase()}
{trader.ai_model.toUpperCase()} + {trader.exchange.toUpperCase()}
</div>
</div>
</div>
@@ -136,7 +160,7 @@ export function CompetitionPage() {
<div className="flex items-center gap-3">
{/* Total Equity */}
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}></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) || '0.00'}
</div>
@@ -144,7 +168,7 @@ export function CompetitionPage() {
{/* P&L */}
<div className="text-right min-w-[90px]">
<div className="text-xs" style={{ color: '#848E9C' }}></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) >= 0 ? '#0ECB81' : '#F6465D' }}
@@ -159,7 +183,7 @@ export function CompetitionPage() {
{/* Positions */}
<div className="text-right">
<div className="text-xs" style={{ color: '#848E9C' }}></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>
@@ -193,7 +217,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' }}>
{t('headToHead', language)}
</h2>
<div className="grid grid-cols-2 gap-4">
{sortedTraders.map((trader, index) => {
@@ -220,22 +244,25 @@ export function CompetitionPage() {
>
<div className="text-center">
<div
className="text-base font-bold mb-2"
className="text-base font-bold mb-1"
style={{ color: getTraderColor(sortedTraders, trader.trader_id) }}
>
{trader.trader_name}
</div>
<div className="text-xs mono mb-2" style={{ color: '#848E9C' }}>
{trader.ai_model.toUpperCase()} + {trader.exchange.toUpperCase()}
</div>
<div className="text-2xl font-bold mono mb-1" style={{ color: (trader.total_pnl ?? 0) >= 0 ? '#0ECB81' : '#F6465D' }}>
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
</div>
{isWinning && gap > 0 && (
<div className="text-xs font-semibold" style={{ color: '#0ECB81' }}>
{gap.toFixed(2)}%
{t('leadingBy', language, { gap: gap.toFixed(2) })}
</div>
)}
{!isWinning && gap < 0 && (
<div className="text-xs font-semibold" style={{ color: '#F6465D' }}>
{Math.abs(gap).toFixed(2)}%
{t('behindBy', language, { gap: Math.abs(gap).toFixed(2) })}
</div>
)}
</div>
@@ -245,6 +272,13 @@ export function CompetitionPage() {
</div>
</div>
)}
{/* Trader Config Modal */}
<TraderConfigModal
isOpen={isModalOpen}
onClose={closeModal}
traderData={selectedTrader}
/>
</div>
);
}

View File

@@ -14,9 +14,8 @@ export function Header({ simple = false }: HeaderProps) {
<div className="flex items-center justify-between">
{/* 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="flex items-center justify-center">
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="h-10 w-auto" />
</div>
<div>
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>

View File

@@ -57,9 +57,8 @@ export function LoginPage() {
<div className="w-full max-w-md">
{/* Logo */}
<div className="text-center mb-8">
<div className="w-16 h-16 rounded-full mx-auto mb-4 flex items-center justify-center text-3xl"
style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-16 h-16" />
</div>
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
{t('loginTitle', language)}

View File

@@ -75,9 +75,8 @@ export function RegisterPage() {
<div className="w-full max-w-md">
{/* Logo */}
<div className="text-center mb-8">
<div className="w-16 h-16 rounded-full mx-auto mb-4 flex items-center justify-center text-3xl"
style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-16 h-16" />
</div>
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
{t('appTitle', language)}

View File

@@ -0,0 +1,443 @@
import { useState, useEffect } from 'react';
import type { AIModel, Exchange, CreateTraderRequest } from '../types';
// 提取下划线后面的名称部分
function getShortName(fullName: string): string {
const parts = fullName.split('_');
return parts.length > 1 ? parts[parts.length - 1] : fullName;
}
interface TraderConfigData {
trader_id?: string;
trader_name: string;
ai_model: string;
exchange_id: string;
btc_eth_leverage: number;
altcoin_leverage: number;
trading_symbols: string;
custom_prompt: string;
override_base_prompt: boolean;
is_cross_margin: boolean;
use_coin_pool: boolean;
use_oi_top: boolean;
initial_balance: number;
}
interface TraderConfigModalProps {
isOpen: boolean;
onClose: () => void;
traderData?: TraderConfigData | null;
isEditMode?: boolean;
availableModels?: AIModel[];
availableExchanges?: Exchange[];
onSave?: (data: CreateTraderRequest) => Promise<void>;
}
export function TraderConfigModal({
isOpen,
onClose,
traderData,
isEditMode = false,
availableModels = [],
availableExchanges = [],
onSave
}: TraderConfigModalProps) {
const [formData, setFormData] = useState<TraderConfigData>({
trader_name: '',
ai_model: '',
exchange_id: '',
btc_eth_leverage: 5,
altcoin_leverage: 3,
trading_symbols: '',
custom_prompt: '',
override_base_prompt: false,
is_cross_margin: true,
use_coin_pool: false,
use_oi_top: false,
initial_balance: 1000,
});
const [isSaving, setIsSaving] = useState(false);
const [availableCoins, setAvailableCoins] = useState<string[]>([]);
const [selectedCoins, setSelectedCoins] = useState<string[]>([]);
const [showCoinSelector, setShowCoinSelector] = useState(false);
useEffect(() => {
if (traderData) {
setFormData(traderData);
// 设置已选择的币种
if (traderData.trading_symbols) {
const coins = traderData.trading_symbols.split(',').map(s => s.trim()).filter(s => s);
setSelectedCoins(coins);
}
} else if (!isEditMode) {
setFormData({
trader_name: '',
ai_model: availableModels[0]?.id || '',
exchange_id: availableExchanges[0]?.id || '',
btc_eth_leverage: 5,
altcoin_leverage: 3,
trading_symbols: '',
custom_prompt: '',
override_base_prompt: false,
is_cross_margin: true,
use_coin_pool: false,
use_oi_top: false,
initial_balance: 1000,
});
}
}, [traderData, isEditMode, availableModels, availableExchanges]);
// 获取系统配置中的币种列表
useEffect(() => {
const fetchConfig = async () => {
try {
const response = await fetch('/api/config');
const config = await response.json();
if (config.default_coins) {
setAvailableCoins(config.default_coins);
}
} catch (error) {
console.error('Failed to fetch config:', error);
// 使用默认币种列表
setAvailableCoins(['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT', 'DOGEUSDT', 'ADAUSDT']);
}
};
fetchConfig();
}, []);
// 当选择的币种改变时,更新输入框
useEffect(() => {
const symbolsString = selectedCoins.join(',');
setFormData(prev => ({ ...prev, trading_symbols: symbolsString }));
}, [selectedCoins]);
if (!isOpen) return null;
const handleInputChange = (field: keyof TraderConfigData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 如果是直接编辑trading_symbols同步更新selectedCoins
if (field === 'trading_symbols') {
const coins = value.split(',').map((s: string) => s.trim()).filter((s: string) => s);
setSelectedCoins(coins);
}
};
const handleCoinToggle = (coin: string) => {
setSelectedCoins(prev => {
if (prev.includes(coin)) {
return prev.filter(c => c !== coin);
} else {
return [...prev, coin];
}
});
};
const handleSave = async () => {
if (!onSave) return;
setIsSaving(true);
try {
const saveData: CreateTraderRequest = {
name: formData.trader_name,
ai_model_id: formData.ai_model,
exchange_id: formData.exchange_id,
btc_eth_leverage: formData.btc_eth_leverage,
altcoin_leverage: formData.altcoin_leverage,
trading_symbols: formData.trading_symbols,
custom_prompt: formData.custom_prompt,
override_base_prompt: formData.override_base_prompt,
is_cross_margin: formData.is_cross_margin,
use_coin_pool: formData.use_coin_pool,
use_oi_top: formData.use_oi_top,
initial_balance: formData.initial_balance,
};
await onSave(saveData);
onClose();
} catch (error) {
console.error('保存失败:', error);
} finally {
setIsSaving(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm">
<div
className="bg-[#1E2329] border border-[#2B3139] rounded-xl shadow-2xl max-w-3xl w-full mx-4 max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[#F0B90B] to-[#E1A706] flex items-center justify-center">
<span className="text-lg">{isEditMode ? '✏️' : ''}</span>
</div>
<div>
<h2 className="text-xl font-bold text-[#EAECEF]">
{isEditMode ? '修改交易员' : '创建交易员'}
</h2>
<p className="text-sm text-[#848E9C] mt-1">
{isEditMode ? '修改交易员配置参数' : '配置新的AI交易员'}
</p>
</div>
</div>
<button
onClick={onClose}
className="w-8 h-8 rounded-lg text-[#848E9C] hover:text-[#EAECEF] hover:bg-[#2B3139] transition-colors flex items-center justify-center"
>
</button>
</div>
{/* Content */}
<div className="p-6 space-y-8">
{/* Basic Info */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
🤖
</h3>
<div className="space-y-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2"></label>
<input
type="text"
value={formData.trader_name}
onChange={(e) => handleInputChange('trader_name', e.target.value)}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
placeholder="请输入交易员名称"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2">AI模型</label>
<select
value={formData.ai_model}
onChange={(e) => handleInputChange('ai_model', e.target.value)}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
>
{availableModels.map(model => (
<option key={model.id} value={model.id}>
{getShortName(model.name || model.id).toUpperCase()}
</option>
))}
</select>
</div>
<div>
<label className="text-sm text-[#EAECEF] block mb-2"></label>
<select
value={formData.exchange_id}
onChange={(e) => handleInputChange('exchange_id', e.target.value)}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
>
{availableExchanges.map(exchange => (
<option key={exchange.id} value={exchange.id}>
{getShortName(exchange.name || exchange.id).toUpperCase()}
</option>
))}
</select>
</div>
</div>
</div>
</div>
{/* Trading Configuration */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
</h3>
<div className="space-y-4">
{/* 第一行:保证金模式和初始余额 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2"></label>
<div className="flex gap-2">
<button
type="button"
onClick={() => handleInputChange('is_cross_margin', true)}
className={`flex-1 px-3 py-2 rounded text-sm ${
formData.is_cross_margin
? 'bg-[#F0B90B] text-black'
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
}`}
>
</button>
<button
type="button"
onClick={() => handleInputChange('is_cross_margin', false)}
className={`flex-1 px-3 py-2 rounded text-sm ${
!formData.is_cross_margin
? 'bg-[#F0B90B] text-black'
: 'bg-[#0B0E11] text-[#848E9C] border border-[#2B3139]'
}`}
>
</button>
</div>
</div>
<div>
<label className="text-sm text-[#EAECEF] block mb-2"> ($)</label>
<input
type="number"
value={formData.initial_balance}
onChange={(e) => handleInputChange('initial_balance', Number(e.target.value))}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
min="100"
step="100"
/>
</div>
</div>
{/* 第二行:杠杆设置 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-[#EAECEF] block mb-2">BTC/ETH </label>
<input
type="number"
value={formData.btc_eth_leverage}
onChange={(e) => handleInputChange('btc_eth_leverage', Number(e.target.value))}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
min="1"
max="125"
/>
</div>
<div>
<label className="text-sm text-[#EAECEF] block mb-2"></label>
<input
type="number"
value={formData.altcoin_leverage}
onChange={(e) => handleInputChange('altcoin_leverage', Number(e.target.value))}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
min="1"
max="75"
/>
</div>
</div>
{/* 第三行:交易币种 */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm text-[#EAECEF]"> (使)</label>
<button
type="button"
onClick={() => setShowCoinSelector(!showCoinSelector)}
className="px-3 py-1 text-xs bg-[#F0B90B] text-black rounded hover:bg-[#E1A706] transition-colors"
>
{showCoinSelector ? '收起选择' : '快速选择'}
</button>
</div>
<input
type="text"
value={formData.trading_symbols}
onChange={(e) => handleInputChange('trading_symbols', e.target.value)}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none"
placeholder="例如: BTCUSDT,ETHUSDT,ADAUSDT"
/>
{/* 币种选择器 */}
{showCoinSelector && (
<div className="mt-3 p-3 bg-[#0B0E11] border border-[#2B3139] rounded">
<div className="text-xs text-[#848E9C] mb-2"></div>
<div className="flex flex-wrap gap-2">
{availableCoins.map(coin => (
<button
key={coin}
type="button"
onClick={() => handleCoinToggle(coin)}
className={`px-2 py-1 text-xs rounded transition-colors ${
selectedCoins.includes(coin)
? 'bg-[#F0B90B] text-black'
: 'bg-[#1E2329] text-[#848E9C] border border-[#2B3139] hover:border-[#F0B90B]'
}`}
>
{coin.replace('USDT', '')}
</button>
))}
</div>
</div>
)}
</div>
</div>
</div>
{/* Signal Sources */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
📡
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center gap-3">
<input
type="checkbox"
checked={formData.use_coin_pool}
onChange={(e) => handleInputChange('use_coin_pool', e.target.checked)}
className="w-4 h-4"
/>
<label className="text-sm text-[#EAECEF]">使 Coin Pool </label>
</div>
<div className="flex items-center gap-3">
<input
type="checkbox"
checked={formData.use_oi_top}
onChange={(e) => handleInputChange('use_oi_top', e.target.checked)}
className="w-4 h-4"
/>
<label className="text-sm text-[#EAECEF]">使 OI Top </label>
</div>
</div>
</div>
{/* Trading Prompt */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-5 flex items-center gap-2">
💬
</h3>
<div className="space-y-4">
<div className="flex items-center gap-3">
<input
type="checkbox"
checked={formData.override_base_prompt}
onChange={(e) => handleInputChange('override_base_prompt', e.target.checked)}
className="w-4 h-4"
/>
<label className="text-sm text-[#EAECEF]"></label>
<span className="text-xs text-[#F0B90B]"> </span>
</div>
<div>
<label className="text-sm text-[#EAECEF] block mb-2">
{formData.override_base_prompt ? '自定义提示词' : '附加提示词'}
</label>
<textarea
value={formData.custom_prompt}
onChange={(e) => handleInputChange('custom_prompt', e.target.value)}
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF] focus:border-[#F0B90B] focus:outline-none h-24 resize-none"
placeholder={formData.override_base_prompt ? "输入完整的交易策略提示词..." : "输入额外的交易策略提示..."}
/>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="flex justify-end gap-3 p-6 border-t border-[#2B3139] bg-gradient-to-r from-[#1E2329] to-[#252B35]">
<button
onClick={onClose}
className="px-6 py-3 bg-[#2B3139] text-[#EAECEF] rounded-lg hover:bg-[#404750] transition-all duration-200 border border-[#404750]"
>
</button>
{onSave && (
<button
onClick={handleSave}
disabled={isSaving || !formData.trader_name || !formData.ai_model || !formData.exchange_id}
className="px-8 py-3 bg-gradient-to-r from-[#F0B90B] to-[#E1A706] text-black rounded-lg hover:from-[#E1A706] hover:to-[#D4951E] transition-all duration-200 disabled:bg-[#848E9C] disabled:cursor-not-allowed font-medium shadow-lg"
>
{isSaving ? '保存中...' : (isEditMode ? '保存修改' : '创建交易员')}
</button>
)}
</div>
</div>
</div>
);
}