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:
|
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
@@ -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
@@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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 "后端未响应"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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,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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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: '全仓模式',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user