Fixed health check; Fixed dex config; Add rank trader info view;

This commit is contained in:
icy
2025-11-01 18:58:32 +08:00
parent 29da893937
commit 8d93a8a095
17 changed files with 518 additions and 83 deletions
+2 -2
View File
@@ -97,7 +97,7 @@ docker compose up -d
Once deployed, open your browser and visit: Once deployed, open your browser and visit:
- **Web Interface**: http://localhost:3000 - **Web Interface**: http://localhost:3000
- **API Health Check**: http://localhost:8080/health - **API Health Check**: http://localhost:8080/api/health
## 📊 Service Management ## 📊 Service Management
@@ -280,7 +280,7 @@ docker inspect nofx-backend | jq '.[0].State.Health'
docker inspect nofx-frontend | jq '.[0].State.Health' docker inspect nofx-frontend | jq '.[0].State.Health'
# Manually test health endpoints # Manually test health endpoints
curl http://localhost:8080/health curl http://localhost:8080/api/health
curl http://localhost:3000/health curl http://localhost:3000/health
``` ```
+2 -2
View File
@@ -100,7 +100,7 @@ docker compose up -d
部署成功后,打开浏览器访问: 部署成功后,打开浏览器访问:
- **Web 界面**: http://localhost:3000 - **Web 界面**: http://localhost:3000
- **API 文档**: http://localhost:8080/health - **API 文档**: http://localhost:8080/api/health
## 📊 服务管理 ## 📊 服务管理
@@ -281,7 +281,7 @@ docker inspect nofx-backend | jq '.[0].State.Health'
docker inspect nofx-frontend | jq '.[0].State.Health' docker inspect nofx-frontend | jq '.[0].State.Health'
# 手动测试健康端点 # 手动测试健康端点
curl http://localhost:8080/health curl http://localhost:8080/api/health
curl http://localhost:3000/health curl http://localhost:3000/health
``` ```
+1 -1
View File
@@ -79,7 +79,7 @@ npm install -g pm2
- **前端 Web 界面**: http://localhost:3000 - **前端 Web 界面**: http://localhost:3000
- **后端 API**: http://localhost:8080 - **后端 API**: http://localhost:8080
- **健康检查**: http://localhost:8080/health - **健康检查**: http://localhost:8080/api/health
--- ---
+2 -2
View File
@@ -859,7 +859,7 @@ Open your web browser and visit:
```bash ```bash
# In a new terminal window # In a new terminal window
curl http://localhost:8080/health curl http://localhost:8080/api/health
``` ```
Should return: `{"status":"ok"}` Should return: `{"status":"ok"}`
@@ -1109,7 +1109,7 @@ GET /api/performance?trader_id=xxx # AI performance analysis
### System Endpoints ### System Endpoints
```bash ```bash
GET /health # Health check GET /api/health # Health check
``` ```
--- ---
+1 -1
View File
@@ -768,7 +768,7 @@ VITE v5.x.x ready in xxx ms
```bash ```bash
# В новом окне терминала # В новом окне терминала
curl http://localhost:8080/health curl http://localhost:8080/api/health
``` ```
Должно вернуть: `{"status":"ok"}` Должно вернуть: `{"status":"ok"}`
+1 -1
View File
@@ -768,7 +768,7 @@ VITE v5.x.x ready in xxx ms
```bash ```bash
# У новому вікні терміналу # У новому вікні терміналу
curl http://localhost:8080/health curl http://localhost:8080/api/health
``` ```
Повинно повернути: `{"status":"ok"}` Повинно повернути: `{"status":"ok"}`
+2 -2
View File
@@ -835,7 +835,7 @@ VITE v5.x.x ready in xxx ms
```bash ```bash
# 在新终端窗口中 # 在新终端窗口中
curl http://localhost:8080/health curl http://localhost:8080/api/health
``` ```
应返回:`{"status":"ok"}` 应返回:`{"status":"ok"}`
@@ -1042,7 +1042,7 @@ GET /api/statistics?trader_id=xxx # 统计信息
### 系统接口 ### 系统接口
```bash ```bash
GET /health # 健康检查 GET /api/health # 健康检查
GET /api/config # 系统配置 GET /api/config # 系统配置
``` ```
+4 -4
View File
@@ -65,12 +65,12 @@ func corsMiddleware() gin.HandlerFunc {
// setupRoutes 设置路由 // setupRoutes 设置路由
func (s *Server) setupRoutes() { func (s *Server) setupRoutes() {
// 健康检查
s.router.Any("/health", s.handleHealth)
// API路由组 // API路由组
api := s.router.Group("/api") api := s.router.Group("/api")
{ {
// 健康检查
api.Any("/health", s.handleHealth)
// 认证相关路由(无需认证) // 认证相关路由(无需认证)
api.POST("/register", s.handleRegister) api.POST("/register", s.handleRegister)
api.POST("/login", s.handleLogin) api.POST("/login", s.handleLogin)
@@ -1352,7 +1352,7 @@ func (s *Server) Start() error {
addr := fmt.Sprintf(":%d", s.port) addr := fmt.Sprintf(":%d", s.port)
log.Printf("🌐 API服务器启动在 http://localhost%s", addr) log.Printf("🌐 API服务器启动在 http://localhost%s", addr)
log.Printf("📊 API文档:") log.Printf("📊 API文档:")
log.Printf(" • GET /health - 健康检查") log.Printf(" • GET /api/health - 健康检查")
log.Printf(" • GET /api/traders - AI交易员列表") log.Printf(" • GET /api/traders - AI交易员列表")
log.Printf(" • POST /api/traders - 创建新的AI交易员") log.Printf(" • POST /api/traders - 创建新的AI交易员")
log.Printf(" • DELETE /api/traders/:id - 删除AI交易员") log.Printf(" • DELETE /api/traders/:id - 删除AI交易员")
+8 -1
View File
@@ -811,7 +811,12 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
err := d.db.QueryRow(` err := d.db.QueryRow(`
SELECT SELECT
t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running, t.created_at, t.updated_at, t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running,
COALESCE(t.btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(t.altcoin_leverage, 5) as altcoin_leverage,
COALESCE(t.trading_symbols, '') as trading_symbols, COALESCE(t.use_coin_pool, 0) as use_coin_pool,
COALESCE(t.use_oi_top, 0) as use_oi_top, COALESCE(t.custom_prompt, '') as custom_prompt,
COALESCE(t.override_base_prompt, 0) as override_base_prompt, COALESCE(t.is_cross_margin, 1) as is_cross_margin,
t.created_at, t.updated_at,
a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key, a.created_at, a.updated_at, a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key, a.created_at, a.updated_at,
e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet, e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet,
COALESCE(e.hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr, COALESCE(e.hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr,
@@ -826,6 +831,8 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
`, traderID, userID).Scan( `, traderID, userID).Scan(
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, &trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
&trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols, &trader.UseCoinPool,
&trader.UseOITop, &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.IsCrossMargin,
&trader.CreatedAt, &trader.UpdatedAt, &trader.CreatedAt, &trader.UpdatedAt,
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey, &aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
&aiModel.CreatedAt, &aiModel.UpdatedAt, &aiModel.CreatedAt, &aiModel.UpdatedAt,
+1 -1
View File
@@ -18,7 +18,7 @@ services:
networks: networks:
- nofx-network - nofx-network
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"] test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
+1 -1
View File
@@ -63,6 +63,6 @@ COPY --from=backend-builder /app/nofx .
EXPOSE 8080 EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 CMD wget --no-verbose --tries=1 --spider http://localhost:8080/api/health || exit 1
CMD ["./nofx"] CMD ["./nofx"]
+1 -1
View File
@@ -227,7 +227,7 @@ status() {
$COMPOSE_CMD ps $COMPOSE_CMD ps
echo "" echo ""
print_info "健康检查:" print_info "健康检查:"
curl -s "http://localhost:${NOFX_BACKEND_PORT}/health" | jq '.' || echo "后端未响应" curl -s "http://localhost:${NOFX_BACKEND_PORT}/api/health" | jq '.' || echo "后端未响应"
} }
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------
+216 -61
View File
@@ -3,7 +3,7 @@ import useSWR from 'swr';
import { api } from '../lib/api'; import { api } from '../lib/api';
import type { TraderInfo, CreateTraderRequest, AIModel, Exchange } from '../types'; import type { TraderInfo, CreateTraderRequest, AIModel, Exchange } from '../types';
import { useLanguage } from '../contexts/LanguageContext'; import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations'; import { t, Language } from '../i18n/translations';
import { getExchangeIcon } from './ExchangeIcons'; import { getExchangeIcon } from './ExchangeIcons';
import { getModelIcon } from './ModelIcons'; import { getModelIcon } from './ModelIcons';
import { TraderConfigModal } from './TraderConfigModal'; import { TraderConfigModal } from './TraderConfigModal';
@@ -107,12 +107,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
// Aster 交易所需要特殊字段 // Aster 交易所需要特殊字段
if (e.id === 'aster') { if (e.id === 'aster') {
return e.asterUser && e.asterSigner && e.asterPrivateKey; return e.asterUser && e.asterUser.trim() !== '' &&
e.asterSigner && e.asterSigner.trim() !== '' &&
e.asterPrivateKey && e.asterPrivateKey.trim() !== '';
} }
// Hyperliquid 只需要私钥(作为apiKey,不需要secretKey // Hyperliquid 只需要私钥(作为apiKey和钱包地址
if (e.id === 'hyperliquid') { if (e.id === 'hyperliquid') {
return e.apiKey && e.hyperliquidWalletAddr; return e.apiKey && e.apiKey.trim() !== '' &&
e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== '';
} }
// Binance 等其他交易所需要 apiKey 和 secretKey // Binance 等其他交易所需要 apiKey 和 secretKey
@@ -375,11 +378,31 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
if (existingExchange) { if (existingExchange) {
// 更新现有配置 // 更新现有配置
updatedExchanges = allExchanges?.map(e => updatedExchanges = allExchanges?.map(e =>
e.id === exchangeId ? { ...e, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, enabled: true } : e e.id === exchangeId ? {
...e,
apiKey,
secretKey,
testnet,
hyperliquidWalletAddr,
asterUser,
asterSigner,
asterPrivateKey,
enabled: true
} : e
) || []; ) || [];
} else { } else {
// 添加新配置 // 添加新配置
const newExchange = { ...exchangeToUpdate, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, enabled: true }; const newExchange = {
...exchangeToUpdate,
apiKey,
secretKey,
testnet,
hyperliquidWalletAddr,
asterUser,
asterSigner,
asterPrivateKey,
enabled: true
};
updatedExchanges = [...(allExchanges || []), newExchange]; updatedExchanges = [...(allExchanges || []), newExchange];
} }
@@ -780,6 +803,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setShowExchangeModal(false); setShowExchangeModal(false);
setEditingExchange(null); setEditingExchange(null);
}} }}
language={language}
/> />
)} )}
@@ -1083,19 +1107,29 @@ function ExchangeConfigModal({
editingExchangeId, editingExchangeId,
onSave, onSave,
onDelete, onDelete,
onClose onClose,
language
}: { }: {
allExchanges: Exchange[]; allExchanges: Exchange[];
editingExchangeId: string | null; editingExchangeId: string | null;
onSave: (exchangeId: string, apiKey: string, secretKey?: string, testnet?: boolean, hyperliquidWalletAddr?: string, asterUser?: string, asterSigner?: string, asterPrivateKey?: string) => Promise<void>; onSave: (exchangeId: string, apiKey: string, secretKey?: string, testnet?: boolean, hyperliquidWalletAddr?: string, asterUser?: string, asterSigner?: string, asterPrivateKey?: string) => Promise<void>;
onDelete: (exchangeId: string) => void; onDelete: (exchangeId: string) => void;
onClose: () => void; onClose: () => void;
language: Language;
}) { }) {
const [selectedExchangeId, setSelectedExchangeId] = useState(editingExchangeId || ''); const [selectedExchangeId, setSelectedExchangeId] = useState(editingExchangeId || '');
const [apiKey, setApiKey] = useState(''); const [apiKey, setApiKey] = useState('');
const [secretKey, setSecretKey] = useState(''); const [secretKey, setSecretKey] = useState('');
const [passphrase, setPassphrase] = useState(''); const [passphrase, setPassphrase] = useState('');
const [testnet, setTestnet] = useState(false); const [testnet, setTestnet] = useState(false);
// Hyperliquid 特定字段
const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('');
// Aster 特定字段
const [asterUser, setAsterUser] = useState('');
const [asterSigner, setAsterSigner] = useState('');
const [asterPrivateKey, setAsterPrivateKey] = useState('');
// 获取当前编辑的交易所信息 // 获取当前编辑的交易所信息
const selectedExchange = allExchanges?.find(e => e.id === selectedExchangeId); const selectedExchange = allExchanges?.find(e => e.id === selectedExchangeId);
@@ -1107,6 +1141,14 @@ function ExchangeConfigModal({
setSecretKey(selectedExchange.secretKey || ''); setSecretKey(selectedExchange.secretKey || '');
setPassphrase(''); // Don't load existing passphrase for security setPassphrase(''); // Don't load existing passphrase for security
setTestnet(selectedExchange.testnet || false); setTestnet(selectedExchange.testnet || false);
// Hyperliquid 字段
setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '');
// Aster 字段
setAsterUser(selectedExchange.asterUser || '');
setAsterSigner(selectedExchange.asterSigner || '');
setAsterPrivateKey(''); // Don't load existing private key for security
} }
}, [editingExchangeId, selectedExchange]); }, [editingExchangeId, selectedExchange]);
@@ -1117,11 +1159,21 @@ function ExchangeConfigModal({
// 根据交易所类型验证不同字段 // 根据交易所类型验证不同字段
if (selectedExchange?.id === 'binance') { if (selectedExchange?.id === 'binance') {
if (!apiKey.trim() || !secretKey.trim()) return; if (!apiKey.trim() || !secretKey.trim()) return;
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet);
} else if (selectedExchange?.id === 'hyperliquid') {
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return;
await onSave(selectedExchangeId, apiKey.trim(), '', testnet, hyperliquidWalletAddr.trim());
} else if (selectedExchange?.id === 'aster') {
if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim()) return;
await onSave(selectedExchangeId, '', '', testnet, undefined, asterUser.trim(), asterSigner.trim(), asterPrivateKey.trim());
} else if (selectedExchange?.id === 'okx') { } else if (selectedExchange?.id === 'okx') {
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return; if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return;
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet);
} else {
// 默认情况(其他CEX交易所)
if (!apiKey.trim() || !secretKey.trim()) return;
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet);
} }
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet, undefined, undefined, undefined, undefined);
}; };
// 可选择的交易所列表(所有支持的交易所) // 可选择的交易所列表(所有支持的交易所)
@@ -1192,51 +1244,147 @@ function ExchangeConfigModal({
{selectedExchange && ( {selectedExchange && (
<> <>
<div> {/* Binance 和其他 CEX 交易所的字段 */}
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> {(selectedExchange.id === 'binance' || selectedExchange.type === 'cex') && selectedExchange.id !== 'hyperliquid' && selectedExchange.id !== 'aster' && (
API Key <>
</label> <div>
<input <label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
type="password" {t('apiKey', language)}
value={apiKey} </label>
onChange={(e) => setApiKey(e.target.value)} <input
placeholder="输入API密钥" type="password"
className="w-full px-3 py-2 rounded" value={apiKey}
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} onChange={(e) => setApiKey(e.target.value)}
required placeholder={t('enterAPIKey', language)}
/> className="w-full px-3 py-2 rounded"
</div> style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
/>
</div>
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
Secret Key {t('secretKey', language)}
</label> </label>
<input <input
type="password" type="password"
value={secretKey} value={secretKey}
onChange={(e) => setSecretKey(e.target.value)} onChange={(e) => setSecretKey(e.target.value)}
placeholder="输入密钥" placeholder={t('enterSecretKey', language)}
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required required
/> />
</div> </div>
{selectedExchange.id === 'okx' && ( {selectedExchange.id === 'okx' && (
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
Passphrase {t('passphrase', language)}
</label> </label>
<input <input
type="password" type="password"
value={passphrase} value={passphrase}
onChange={(e) => setPassphrase(e.target.value)} onChange={(e) => setPassphrase(e.target.value)}
placeholder="输入Passphrase (OKX必填)" placeholder={t('enterPassphrase', language)}
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required required
/> />
</div> </div>
)}
</>
)}
{/* Hyperliquid 交易所的字段 */}
{selectedExchange.id === 'hyperliquid' && (
<>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('privateKey', language)}
</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder={t('enterPrivateKey', language)}
className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
{t('hyperliquidPrivateKeyDesc', language)}
</div>
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('walletAddress', language)}
</label>
<input
type="text"
value={hyperliquidWalletAddr}
onChange={(e) => setHyperliquidWalletAddr(e.target.value)}
placeholder={t('enterWalletAddress', language)}
className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
{t('hyperliquidWalletAddressDesc', language)}
</div>
</div>
</>
)}
{/* Aster 交易所的字段 */}
{selectedExchange.id === 'aster' && (
<>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('user', language)}
</label>
<input
type="text"
value={asterUser}
onChange={(e) => setAsterUser(e.target.value)}
placeholder={t('enterUser', language)}
className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
/>
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('signer', language)}
</label>
<input
type="text"
value={asterSigner}
onChange={(e) => setAsterSigner(e.target.value)}
placeholder={t('enterSigner', language)}
className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
/>
</div>
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('privateKey', language)}
</label>
<input
type="password"
value={asterPrivateKey}
onChange={(e) => setAsterPrivateKey(e.target.value)}
placeholder={t('enterPrivateKey', language)}
className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required
/>
</div>
</>
)} )}
<div> <div>
@@ -1248,21 +1396,21 @@ function ExchangeConfigModal({
className="form-checkbox rounded" className="form-checkbox rounded"
style={{ accentColor: '#F0B90B' }} style={{ accentColor: '#F0B90B' }}
/> />
<span style={{ color: '#EAECEF' }}>使</span> <span style={{ color: '#EAECEF' }}>{t('useTestnet', language)}</span>
</label> </label>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}> <div className="text-xs mt-1" style={{ color: '#848E9C' }}>
{t('testnetDescription', language)}
</div> </div>
</div> </div>
<div className="p-4 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}> <div className="p-4 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
<div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}> <div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}>
{t('securityWarning', language)}
</div> </div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}> <div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div> API密钥将被加密存储使</div> <div>{t('securityTip1', language)}</div>
<div> </div> <div>{t('securityTip2', language)}</div>
<div> </div> <div>{t('securityTip3', language)}</div>
</div> </div>
</div> </div>
</> </>
@@ -1275,15 +1423,22 @@ function ExchangeConfigModal({
className="flex-1 px-4 py-2 rounded text-sm font-semibold" className="flex-1 px-4 py-2 rounded text-sm font-semibold"
style={{ background: '#2B3139', color: '#848E9C' }} style={{ background: '#2B3139', color: '#848E9C' }}
> >
{t('cancel', language)}
</button> </button>
<button <button
type="submit" type="submit"
disabled={!selectedExchange || !apiKey.trim() || !secretKey.trim() || (selectedExchange?.id === 'okx' && !passphrase.trim())} disabled={
!selectedExchange ||
(selectedExchange.id === 'binance' && (!apiKey.trim() || !secretKey.trim())) ||
(selectedExchange.id === 'okx' && (!apiKey.trim() || !secretKey.trim() || !passphrase.trim())) ||
(selectedExchange.id === 'hyperliquid' && (!apiKey.trim() || !hyperliquidWalletAddr.trim())) ||
(selectedExchange.id === 'aster' && (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim())) ||
(selectedExchange.type === 'cex' && selectedExchange.id !== 'hyperliquid' && selectedExchange.id !== 'aster' && selectedExchange.id !== 'binance' && selectedExchange.id !== 'okx' && (!apiKey.trim() || !secretKey.trim()))
}
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50" className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }} style={{ background: '#F0B90B', color: '#000' }}
> >
{t('saveConfiguration', language)}
</button> </button>
</div> </div>
</form> </form>
+3 -3
View File
@@ -3,7 +3,7 @@ import useSWR from 'swr';
import { api } from '../lib/api'; import { api } from '../lib/api';
import type { CompetitionData } from '../types'; import type { CompetitionData } from '../types';
import { ComparisonChart } from './ComparisonChart'; import { ComparisonChart } from './ComparisonChart';
import { TraderConfigModal } from './TraderConfigModal'; import { TraderConfigViewModal } from './TraderConfigViewModal';
import { getTraderColor } from '../utils/traderColors'; import { getTraderColor } from '../utils/traderColors';
import { useLanguage } from '../contexts/LanguageContext'; import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations'; import { t } from '../i18n/translations';
@@ -273,8 +273,8 @@ export function CompetitionPage() {
</div> </div>
)} )}
{/* Trader Config Modal */} {/* Trader Config View Modal */}
<TraderConfigModal <TraderConfigViewModal
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={closeModal} onClose={closeModal}
traderData={selectedTrader} traderData={selectedTrader}
@@ -0,0 +1,211 @@
import { useState } from 'react';
import type { TraderConfigData } from '../types';
// 提取下划线后面的名称部分
function getShortName(fullName: string): string {
const parts = fullName.split('_');
return parts.length > 1 ? parts[parts.length - 1] : fullName;
}
interface TraderConfigViewModalProps {
isOpen: boolean;
onClose: () => void;
traderData?: TraderConfigData | null;
}
export function TraderConfigViewModal({
isOpen,
onClose,
traderData
}: TraderConfigViewModalProps) {
const [copiedField, setCopiedField] = useState<string | null>(null);
if (!isOpen || !traderData) return null;
const copyToClipboard = async (text: string, fieldName: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedField(fieldName);
setTimeout(() => setCopiedField(null), 2000);
} catch (error) {
console.error('Failed to copy:', error);
}
};
const CopyButton = ({ text, fieldName }: { text: string; fieldName: string }) => (
<button
onClick={() => copyToClipboard(text, fieldName)}
className="ml-2 px-2 py-1 text-xs rounded transition-all duration-200 hover:scale-105"
style={{
background: copiedField === fieldName ? 'rgba(14, 203, 129, 0.1)' : 'rgba(240, 185, 11, 0.1)',
color: copiedField === fieldName ? '#0ECB81' : '#F0B90B',
border: `1px solid ${copiedField === fieldName ? 'rgba(14, 203, 129, 0.3)' : 'rgba(240, 185, 11, 0.3)'}`
}}
>
{copiedField === fieldName ? '✓ 已复制' : '📋 复制'}
</button>
);
const InfoRow = ({ label, value, copyable = false, fieldName = '' }: {
label: string;
value: string | number | boolean;
copyable?: boolean;
fieldName?: string;
}) => (
<div className="flex justify-between items-start py-2 border-b border-[#2B3139] last:border-b-0">
<span className="text-sm text-[#848E9C] font-medium">{label}</span>
<div className="flex items-center text-right">
<span className="text-sm text-[#EAECEF] font-mono">
{typeof value === 'boolean' ? (value ? '是' : '否') : value}
</span>
{copyable && typeof value === 'string' && value && (
<CopyButton text={value} fieldName={fieldName} />
)}
</div>
</div>
);
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-2xl 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">👁</span>
</div>
<div>
<h2 className="text-xl font-bold text-[#EAECEF]">
</h2>
<p className="text-sm text-[#848E9C] mt-1">
{traderData.trader_name}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Running Status */}
<div
className="px-3 py-1 rounded-full text-xs font-bold flex items-center gap-1"
style={traderData.is_running
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
}
>
<span>{traderData.is_running ? '●' : '○'}</span>
{traderData.is_running ? '运行中' : '已停止'}
</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>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{/* Basic Info */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
🤖
</h3>
<div className="space-y-3">
<InfoRow label="交易员ID" value={traderData.trader_id || ''} copyable fieldName="trader_id" />
<InfoRow label="交易员名称" value={traderData.trader_name} copyable fieldName="trader_name" />
<InfoRow label="AI模型" value={getShortName(traderData.ai_model).toUpperCase()} />
<InfoRow label="交易所" value={getShortName(traderData.exchange_id).toUpperCase()} />
<InfoRow label="初始余额" value={`$${traderData.initial_balance.toLocaleString()}`} />
</div>
</div>
{/* Trading Configuration */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
</h3>
<div className="space-y-3">
<InfoRow label="保证金模式" value={traderData.is_cross_margin ? '全仓' : '逐仓'} />
<InfoRow label="BTC/ETH 杠杆" value={`${traderData.btc_eth_leverage}x`} />
<InfoRow label="山寨币杠杆" value={`${traderData.altcoin_leverage}x`} />
<InfoRow
label="交易币种"
value={traderData.trading_symbols || '使用默认币种'}
copyable
fieldName="trading_symbols"
/>
</div>
</div>
{/* Signal Sources */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<h3 className="text-lg font-semibold text-[#EAECEF] mb-4 flex items-center gap-2">
📡
</h3>
<div className="space-y-3">
<InfoRow label="Coin Pool 信号" value={traderData.use_coin_pool} />
<InfoRow label="OI Top 信号" value={traderData.use_oi_top} />
</div>
</div>
{/* Custom Prompt */}
<div className="bg-[#0B0E11] border border-[#2B3139] rounded-lg p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-[#EAECEF] flex items-center gap-2">
💬
</h3>
{traderData.custom_prompt && (
<CopyButton text={traderData.custom_prompt} fieldName="custom_prompt" />
)}
</div>
<div className="space-y-3">
<InfoRow label="覆盖默认提示词" value={traderData.override_base_prompt} />
{traderData.custom_prompt ? (
<div>
<div className="text-sm text-[#848E9C] mb-2">
{traderData.override_base_prompt ? '自定义提示词' : '附加提示词'}
</div>
<div
className="p-3 rounded border text-sm text-[#EAECEF] font-mono leading-relaxed max-h-48 overflow-y-auto"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
whiteSpace: 'pre-wrap'
}}
>
{traderData.custom_prompt}
</div>
</div>
) : (
<div className="text-sm text-[#848E9C] italic p-3 rounded border" style={{ border: '1px solid #2B3139' }}>
使
</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>
<button
onClick={() => copyToClipboard(JSON.stringify(traderData, null, 2), 'full_config')}
className="px-6 py-3 bg-gradient-to-r from-[#F0B90B] to-[#E1A706] text-black rounded-lg hover:from-[#E1A706] hover:to-[#D4951E] transition-all duration-200 font-medium shadow-lg"
>
{copiedField === 'full_config' ? '✓ 已复制配置' : '📋 复制完整配置'}
</button>
</div>
</div>
</div>
);
}
+44
View File
@@ -164,6 +164,28 @@ export const translations = {
useOfficialAPI: 'Use official API service', useOfficialAPI: 'Use official API service',
useCustomAPI: 'Use custom API endpoint', useCustomAPI: 'Use custom API endpoint',
// Exchange Configuration
secretKey: 'Secret Key',
privateKey: 'Private Key',
walletAddress: 'Wallet Address',
user: 'User',
signer: 'Signer',
passphrase: 'Passphrase',
enterSecretKey: 'Enter Secret Key',
enterPrivateKey: 'Enter Private Key',
enterWalletAddress: 'Enter Wallet Address',
enterUser: 'Enter User',
enterSigner: 'Enter Signer Address',
enterPassphrase: 'Enter Passphrase (Required for OKX)',
hyperliquidPrivateKeyDesc: 'Hyperliquid uses private key for trading authentication',
hyperliquidWalletAddressDesc: 'Wallet address corresponding to the private key',
securityWarning: '⚠️ Security Notice',
securityTip1: '• API keys will be encrypted and stored. Recommend using read-only or futures trading permissions',
securityTip2: '• Do not grant withdrawal permissions to ensure fund safety',
securityTip3: '• After deleting configuration, related traders will not be able to trade normally',
testnetDescription: 'Enable to connect to exchange testnet environment for simulation trading',
saveConfiguration: 'Save Configuration',
// Trader Configuration // Trader Configuration
positionMode: 'Position Mode', positionMode: 'Position Mode',
crossMarginMode: 'Cross Margin', crossMarginMode: 'Cross Margin',
@@ -406,6 +428,28 @@ export const translations = {
useOfficialAPI: '使用官方API服务', useOfficialAPI: '使用官方API服务',
useCustomAPI: '使用自定义API端点', useCustomAPI: '使用自定义API端点',
// Exchange Configuration
secretKey: '密钥',
privateKey: '私钥',
walletAddress: '钱包地址',
user: '用户名',
signer: '签名者',
passphrase: '口令',
enterSecretKey: '输入密钥',
enterPrivateKey: '输入私钥',
enterWalletAddress: '输入钱包地址',
enterUser: '输入用户名',
enterSigner: '输入签名者地址',
enterPassphrase: '输入Passphrase (OKX必填)',
hyperliquidPrivateKeyDesc: 'Hyperliquid 使用私钥进行交易认证',
hyperliquidWalletAddressDesc: '与私钥对应的钱包地址',
securityWarning: '⚠️ 安全提示',
securityTip1: '• API密钥将被加密存储,建议使用只读或期货交易权限',
securityTip2: '• 不要授予提现权限,确保资金安全',
securityTip3: '• 删除配置后,相关交易员将无法正常交易',
testnetDescription: '启用后将连接到交易所测试环境,用于模拟交易',
saveConfiguration: '保存配置',
// Trader Configuration // Trader Configuration
positionMode: '仓位模式', positionMode: '仓位模式',
crossMarginMode: '全仓模式', crossMarginMode: '全仓模式',
+18
View File
@@ -179,3 +179,21 @@ export interface CompetitionData {
traders: CompetitionTraderData[]; traders: CompetitionTraderData[];
count: number; count: number;
} }
// Trader Configuration Data for View Modal
export 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;
is_running: boolean;
}