mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
Fixed health check; Fixed dex config; Add rank trader info view;
This commit is contained in:
+2
-2
@@ -97,7 +97,7 @@ docker compose up -d
|
||||
Once deployed, open your browser and visit:
|
||||
|
||||
- **Web Interface**: http://localhost:3000
|
||||
- **API Health Check**: http://localhost:8080/health
|
||||
- **API Health Check**: http://localhost:8080/api/health
|
||||
|
||||
## 📊 Service Management
|
||||
|
||||
@@ -280,7 +280,7 @@ docker inspect nofx-backend | jq '.[0].State.Health'
|
||||
docker inspect nofx-frontend | jq '.[0].State.Health'
|
||||
|
||||
# Manually test health endpoints
|
||||
curl http://localhost:8080/health
|
||||
curl http://localhost:8080/api/health
|
||||
curl http://localhost:3000/health
|
||||
```
|
||||
|
||||
|
||||
+2
-2
@@ -100,7 +100,7 @@ docker compose up -d
|
||||
部署成功后,打开浏览器访问:
|
||||
|
||||
- **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'
|
||||
|
||||
# 手动测试健康端点
|
||||
curl http://localhost:8080/health
|
||||
curl http://localhost:8080/api/health
|
||||
curl http://localhost:3000/health
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ npm install -g pm2
|
||||
|
||||
- **前端 Web 界面**: http://localhost:3000
|
||||
- **后端 API**: http://localhost:8080
|
||||
- **健康检查**: http://localhost:8080/health
|
||||
- **健康检查**: http://localhost:8080/api/health
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -859,7 +859,7 @@ Open your web browser and visit:
|
||||
|
||||
```bash
|
||||
# In a new terminal window
|
||||
curl http://localhost:8080/health
|
||||
curl http://localhost:8080/api/health
|
||||
```
|
||||
|
||||
Should return: `{"status":"ok"}`
|
||||
@@ -1109,7 +1109,7 @@ GET /api/performance?trader_id=xxx # AI performance analysis
|
||||
### System Endpoints
|
||||
|
||||
```bash
|
||||
GET /health # Health check
|
||||
GET /api/health # Health check
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+1
-1
@@ -768,7 +768,7 @@ VITE v5.x.x ready in xxx ms
|
||||
|
||||
```bash
|
||||
# В новом окне терминала
|
||||
curl http://localhost:8080/health
|
||||
curl http://localhost:8080/api/health
|
||||
```
|
||||
|
||||
Должно вернуть: `{"status":"ok"}`
|
||||
|
||||
+1
-1
@@ -768,7 +768,7 @@ VITE v5.x.x ready in xxx ms
|
||||
|
||||
```bash
|
||||
# У новому вікні терміналу
|
||||
curl http://localhost:8080/health
|
||||
curl http://localhost:8080/api/health
|
||||
```
|
||||
|
||||
Повинно повернути: `{"status":"ok"}`
|
||||
|
||||
+2
-2
@@ -835,7 +835,7 @@ VITE v5.x.x ready in xxx ms
|
||||
|
||||
```bash
|
||||
# 在新终端窗口中
|
||||
curl http://localhost:8080/health
|
||||
curl http://localhost:8080/api/health
|
||||
```
|
||||
|
||||
应返回:`{"status":"ok"}`
|
||||
@@ -1042,7 +1042,7 @@ GET /api/statistics?trader_id=xxx # 统计信息
|
||||
### 系统接口
|
||||
|
||||
```bash
|
||||
GET /health # 健康检查
|
||||
GET /api/health # 健康检查
|
||||
GET /api/config # 系统配置
|
||||
```
|
||||
|
||||
|
||||
+4
-4
@@ -65,12 +65,12 @@ func corsMiddleware() gin.HandlerFunc {
|
||||
|
||||
// setupRoutes 设置路由
|
||||
func (s *Server) setupRoutes() {
|
||||
// 健康检查
|
||||
s.router.Any("/health", s.handleHealth)
|
||||
|
||||
// API路由组
|
||||
api := s.router.Group("/api")
|
||||
{
|
||||
// 健康检查
|
||||
api.Any("/health", s.handleHealth)
|
||||
|
||||
// 认证相关路由(无需认证)
|
||||
api.POST("/register", s.handleRegister)
|
||||
api.POST("/login", s.handleLogin)
|
||||
@@ -1352,7 +1352,7 @@ func (s *Server) Start() error {
|
||||
addr := fmt.Sprintf(":%d", s.port)
|
||||
log.Printf("🌐 API服务器启动在 http://localhost%s", addr)
|
||||
log.Printf("📊 API文档:")
|
||||
log.Printf(" • GET /health - 健康检查")
|
||||
log.Printf(" • GET /api/health - 健康检查")
|
||||
log.Printf(" • GET /api/traders - AI交易员列表")
|
||||
log.Printf(" • POST /api/traders - 创建新的AI交易员")
|
||||
log.Printf(" • DELETE /api/traders/:id - 删除AI交易员")
|
||||
|
||||
+8
-1
@@ -811,7 +811,12 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
|
||||
|
||||
err := d.db.QueryRow(`
|
||||
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,
|
||||
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,
|
||||
@@ -826,6 +831,8 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
|
||||
`, traderID, userID).Scan(
|
||||
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
|
||||
&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,
|
||||
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
|
||||
&aiModel.CreatedAt, &aiModel.UpdatedAt,
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ services:
|
||||
networks:
|
||||
- nofx-network
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -63,6 +63,6 @@ COPY --from=backend-builder /app/nofx .
|
||||
EXPOSE 8080
|
||||
|
||||
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"]
|
||||
|
||||
@@ -227,7 +227,7 @@ status() {
|
||||
$COMPOSE_CMD ps
|
||||
echo ""
|
||||
print_info "健康检查:"
|
||||
curl -s "http://localhost:${NOFX_BACKEND_PORT}/health" | jq '.' || echo "后端未响应"
|
||||
curl -s "http://localhost:${NOFX_BACKEND_PORT}/api/health" | jq '.' || echo "后端未响应"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
@@ -3,7 +3,7 @@ import useSWR from 'swr';
|
||||
import { api } from '../lib/api';
|
||||
import type { TraderInfo, CreateTraderRequest, AIModel, Exchange } from '../types';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
import { t, Language } from '../i18n/translations';
|
||||
import { getExchangeIcon } from './ExchangeIcons';
|
||||
import { getModelIcon } from './ModelIcons';
|
||||
import { TraderConfigModal } from './TraderConfigModal';
|
||||
@@ -107,12 +107,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
|
||||
// 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') {
|
||||
return e.apiKey && e.hyperliquidWalletAddr;
|
||||
return e.apiKey && e.apiKey.trim() !== '' &&
|
||||
e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== '';
|
||||
}
|
||||
|
||||
// Binance 等其他交易所需要 apiKey 和 secretKey
|
||||
@@ -375,11 +378,31 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
if (existingExchange) {
|
||||
// 更新现有配置
|
||||
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 {
|
||||
// 添加新配置
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -780,6 +803,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
setShowExchangeModal(false);
|
||||
setEditingExchange(null);
|
||||
}}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1083,13 +1107,15 @@ function ExchangeConfigModal({
|
||||
editingExchangeId,
|
||||
onSave,
|
||||
onDelete,
|
||||
onClose
|
||||
onClose,
|
||||
language
|
||||
}: {
|
||||
allExchanges: Exchange[];
|
||||
editingExchangeId: string | null;
|
||||
onSave: (exchangeId: string, apiKey: string, secretKey?: string, testnet?: boolean, hyperliquidWalletAddr?: string, asterUser?: string, asterSigner?: string, asterPrivateKey?: string) => Promise<void>;
|
||||
onDelete: (exchangeId: string) => void;
|
||||
onClose: () => void;
|
||||
language: Language;
|
||||
}) {
|
||||
const [selectedExchangeId, setSelectedExchangeId] = useState(editingExchangeId || '');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
@@ -1097,6 +1123,14 @@ function ExchangeConfigModal({
|
||||
const [passphrase, setPassphrase] = useState('');
|
||||
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);
|
||||
|
||||
@@ -1107,6 +1141,14 @@ function ExchangeConfigModal({
|
||||
setSecretKey(selectedExchange.secretKey || '');
|
||||
setPassphrase(''); // Don't load existing passphrase for security
|
||||
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]);
|
||||
|
||||
@@ -1117,11 +1159,21 @@ function ExchangeConfigModal({
|
||||
// 根据交易所类型验证不同字段
|
||||
if (selectedExchange?.id === 'binance') {
|
||||
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') {
|
||||
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 && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
API Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="输入API密钥"
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{/* Binance 和其他 CEX 交易所的字段 */}
|
||||
{(selectedExchange.id === 'binance' || selectedExchange.type === 'cex') && selectedExchange.id !== 'hyperliquid' && selectedExchange.id !== 'aster' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
{t('apiKey', language)}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={t('enterAPIKey', 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' }}>
|
||||
Secret Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
placeholder="输入密钥"
|
||||
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('secretKey', language)}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={secretKey}
|
||||
onChange={(e) => setSecretKey(e.target.value)}
|
||||
placeholder={t('enterSecretKey', language)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedExchange.id === 'okx' && (
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
Passphrase
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passphrase}
|
||||
onChange={(e) => setPassphrase(e.target.value)}
|
||||
placeholder="输入Passphrase (OKX必填)"
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{selectedExchange.id === 'okx' && (
|
||||
<div>
|
||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||
{t('passphrase', language)}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={passphrase}
|
||||
onChange={(e) => setPassphrase(e.target.value)}
|
||||
placeholder={t('enterPassphrase', language)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
@@ -1248,21 +1396,21 @@ function ExchangeConfigModal({
|
||||
className="form-checkbox rounded"
|
||||
style={{ accentColor: '#F0B90B' }}
|
||||
/>
|
||||
<span style={{ color: '#EAECEF' }}>使用测试网</span>
|
||||
<span style={{ color: '#EAECEF' }}>{t('useTestnet', language)}</span>
|
||||
</label>
|
||||
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||
启用后将连接到交易所测试环境,用于模拟交易
|
||||
{t('testnetDescription', language)}
|
||||
</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="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}>
|
||||
⚠️ 安全提示
|
||||
{t('securityWarning', language)}
|
||||
</div>
|
||||
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
|
||||
<div>• API密钥将被加密存储,建议使用只读或期货交易权限</div>
|
||||
<div>• 不要授予提现权限,确保资金安全</div>
|
||||
<div>• 删除配置后,相关交易员将无法正常交易</div>
|
||||
<div>{t('securityTip1', language)}</div>
|
||||
<div>{t('securityTip2', language)}</div>
|
||||
<div>{t('securityTip3', language)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -1275,15 +1423,22 @@ function ExchangeConfigModal({
|
||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
|
||||
style={{ background: '#2B3139', color: '#848E9C' }}
|
||||
>
|
||||
取消
|
||||
{t('cancel', language)}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
style={{ background: '#F0B90B', color: '#000' }}
|
||||
>
|
||||
保存配置
|
||||
{t('saveConfiguration', language)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -3,7 +3,7 @@ import useSWR from 'swr';
|
||||
import { api } from '../lib/api';
|
||||
import type { CompetitionData } from '../types';
|
||||
import { ComparisonChart } from './ComparisonChart';
|
||||
import { TraderConfigModal } from './TraderConfigModal';
|
||||
import { TraderConfigViewModal } from './TraderConfigViewModal';
|
||||
import { getTraderColor } from '../utils/traderColors';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
@@ -273,8 +273,8 @@ export function CompetitionPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Trader Config Modal */}
|
||||
<TraderConfigModal
|
||||
{/* Trader Config View Modal */}
|
||||
<TraderConfigViewModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -164,6 +164,28 @@ export const translations = {
|
||||
useOfficialAPI: 'Use official API service',
|
||||
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
|
||||
positionMode: 'Position Mode',
|
||||
crossMarginMode: 'Cross Margin',
|
||||
@@ -406,6 +428,28 @@ export const translations = {
|
||||
useOfficialAPI: '使用官方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
|
||||
positionMode: '仓位模式',
|
||||
crossMarginMode: '全仓模式',
|
||||
|
||||
@@ -179,3 +179,21 @@ export interface CompetitionData {
|
||||
traders: CompetitionTraderData[];
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user