mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 01:14:40 +08:00
Feature/okx trading (#1177)
* feat: add OKX exchange trading support - Add OKX trader client with full trading API integration - Support API Key, Secret Key, and Passphrase authentication - Add OKX icon and frontend configuration modal - Update exchange store and types for OKX fields * fix: add passphrase column migration and fix exchange type mapping * fix: show OKX input fields in exchange config modal * fix: ensure all supported exchanges exist for user when listing * fix: simplify exchange type check condition for OKX * debug: add visible debug info for exchange id * fix: remove debug info from exchange config modal * fix: add OKX to exchange type condition in AITradersPage * feat: complete OKX trading support and fix exchange config issues - Add LIGHTER exchange UI support in AITradersPage - Add passphrase field to UpdateExchangeConfigRequest type - Fix OKX HTTP client to bypass proxy (disable system proxy) - Auto-fetch initial balance from exchange when not set - Support multiple balance field names for different exchanges - Add detailed error messages when trader fails to load - Add lighter_api_key_private_key field to exchange store
This commit is contained in:
@@ -454,16 +454,18 @@ type UpdateModelConfigRequest struct {
|
|||||||
|
|
||||||
type UpdateExchangeConfigRequest struct {
|
type UpdateExchangeConfigRequest struct {
|
||||||
Exchanges map[string]struct {
|
Exchanges map[string]struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
APIKey string `json:"api_key"`
|
APIKey string `json:"api_key"`
|
||||||
SecretKey string `json:"secret_key"`
|
SecretKey string `json:"secret_key"`
|
||||||
Testnet bool `json:"testnet"`
|
Passphrase string `json:"passphrase"` // OKX专用
|
||||||
HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"`
|
Testnet bool `json:"testnet"`
|
||||||
AsterUser string `json:"aster_user"`
|
HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"`
|
||||||
AsterSigner string `json:"aster_signer"`
|
AsterUser string `json:"aster_user"`
|
||||||
AsterPrivateKey string `json:"aster_private_key"`
|
AsterSigner string `json:"aster_signer"`
|
||||||
LighterWalletAddr string `json:"lighter_wallet_addr"`
|
AsterPrivateKey string `json:"aster_private_key"`
|
||||||
LighterPrivateKey string `json:"lighter_private_key"`
|
LighterWalletAddr string `json:"lighter_wallet_addr"`
|
||||||
|
LighterPrivateKey string `json:"lighter_private_key"`
|
||||||
|
LighterAPIKeyPrivateKey string `json:"lighter_api_key_private_key"`
|
||||||
} `json:"exchanges"`
|
} `json:"exchanges"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,8 +845,46 @@ func (s *Server) handleStartTrader(c *gin.Context) {
|
|||||||
|
|
||||||
trader, err := s.traderManager.GetTrader(traderID)
|
trader, err := s.traderManager.GetTrader(traderID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
|
// 交易员不在内存中,尝试从数据库加载
|
||||||
return
|
logger.Infof("🔄 交易员 %s 不在内存中,尝试加载...", traderID)
|
||||||
|
if loadErr := s.traderManager.LoadUserTradersFromStore(s.store, userID); loadErr != nil {
|
||||||
|
logger.Infof("❌ 加载用户交易员失败: %v", loadErr)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "加载交易员失败: " + loadErr.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 再次尝试获取
|
||||||
|
trader, err = s.traderManager.GetTrader(traderID)
|
||||||
|
if err != nil {
|
||||||
|
// 检查详细原因
|
||||||
|
fullCfg, _ := s.store.Trader().GetFullConfig(userID, traderID)
|
||||||
|
if fullCfg != nil && fullCfg.Trader != nil {
|
||||||
|
// 检查策略
|
||||||
|
if fullCfg.Strategy == nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员未配置策略,请先在策略工作室创建策略并关联到交易员"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 检查AI模型
|
||||||
|
if fullCfg.AIModel == nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员的AI模型不存在,请检查AI模型配置"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !fullCfg.AIModel.Enabled {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员的AI模型未启用,请先启用AI模型"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 检查交易所
|
||||||
|
if fullCfg.Exchange == nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员的交易所不存在,请检查交易所配置"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !fullCfg.Exchange.Enabled {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "交易员的交易所未启用,请先启用交易所"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "交易员加载失败,请检查AI模型、交易所和策略配置"})
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查交易员是否已经在运行
|
// 检查交易员是否已经在运行
|
||||||
@@ -1249,7 +1289,7 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
|
|||||||
|
|
||||||
// 更新每个交易所的配置
|
// 更新每个交易所的配置
|
||||||
for exchangeID, exchangeData := range req.Exchanges {
|
for exchangeID, exchangeData := range req.Exchanges {
|
||||||
err := s.store.Exchange().Update(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey, exchangeData.LighterWalletAddr, exchangeData.LighterPrivateKey)
|
err := s.store.Exchange().Update(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Passphrase, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey, exchangeData.LighterWalletAddr, exchangeData.LighterPrivateKey, exchangeData.LighterAPIKeyPrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易所 %s 失败: %v", exchangeID, err)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易所 %s 失败: %v", exchangeID, err)})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -443,9 +443,10 @@ func (tm *TraderManager) LoadUserTradersFromStore(st *store.Store, userID string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用现有的方法加载交易员
|
// 使用现有的方法加载交易员
|
||||||
|
logger.Infof("📦 正在加载交易员 %s (AI模型: %s, 交易所: %s, 策略ID: %s)", traderCfg.Name, aiModelCfg.Provider, exchangeCfg.ID, traderCfg.StrategyID)
|
||||||
err = tm.addTraderFromStore(traderCfg, aiModelCfg, exchangeCfg, maxDailyLoss, maxDrawdown, stopTradingMinutes, st)
|
err = tm.addTraderFromStore(traderCfg, aiModelCfg, exchangeCfg, maxDailyLoss, maxDrawdown, stopTradingMinutes, st)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Infof("⚠️ 加载交易员 %s 失败: %v", traderCfg.Name, err)
|
logger.Infof("❌ 加载交易员 %s 失败: %v", traderCfg.Name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,6 +631,10 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
|
|||||||
case "bybit":
|
case "bybit":
|
||||||
traderConfig.BybitAPIKey = exchangeCfg.APIKey
|
traderConfig.BybitAPIKey = exchangeCfg.APIKey
|
||||||
traderConfig.BybitSecretKey = exchangeCfg.SecretKey
|
traderConfig.BybitSecretKey = exchangeCfg.SecretKey
|
||||||
|
case "okx":
|
||||||
|
traderConfig.OKXAPIKey = exchangeCfg.APIKey
|
||||||
|
traderConfig.OKXSecretKey = exchangeCfg.SecretKey
|
||||||
|
traderConfig.OKXPassphrase = exchangeCfg.Passphrase
|
||||||
case "hyperliquid":
|
case "hyperliquid":
|
||||||
traderConfig.HyperliquidPrivateKey = exchangeCfg.APIKey
|
traderConfig.HyperliquidPrivateKey = exchangeCfg.APIKey
|
||||||
traderConfig.HyperliquidWalletAddr = exchangeCfg.HyperliquidWalletAddr
|
traderConfig.HyperliquidWalletAddr = exchangeCfg.HyperliquidWalletAddr
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Exchange struct {
|
|||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
SecretKey string `json:"secretKey"`
|
SecretKey string `json:"secretKey"`
|
||||||
|
Passphrase string `json:"passphrase"` // OKX专用
|
||||||
Testnet bool `json:"testnet"`
|
Testnet bool `json:"testnet"`
|
||||||
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"`
|
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"`
|
||||||
AsterUser string `json:"asterUser"`
|
AsterUser string `json:"asterUser"`
|
||||||
@@ -46,6 +47,7 @@ func (s *ExchangeStore) initTables() error {
|
|||||||
enabled BOOLEAN DEFAULT 0,
|
enabled BOOLEAN DEFAULT 0,
|
||||||
api_key TEXT DEFAULT '',
|
api_key TEXT DEFAULT '',
|
||||||
secret_key TEXT DEFAULT '',
|
secret_key TEXT DEFAULT '',
|
||||||
|
passphrase TEXT DEFAULT '',
|
||||||
testnet BOOLEAN DEFAULT 0,
|
testnet BOOLEAN DEFAULT 0,
|
||||||
hyperliquid_wallet_addr TEXT DEFAULT '',
|
hyperliquid_wallet_addr TEXT DEFAULT '',
|
||||||
aster_user TEXT DEFAULT '',
|
aster_user TEXT DEFAULT '',
|
||||||
@@ -63,6 +65,9 @@ func (s *ExchangeStore) initTables() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 迁移:添加 passphrase 列(如果不存在)
|
||||||
|
s.db.Exec(`ALTER TABLE exchanges ADD COLUMN passphrase TEXT DEFAULT ''`)
|
||||||
|
|
||||||
// 触发器
|
// 触发器
|
||||||
_, err = s.db.Exec(`
|
_, err = s.db.Exec(`
|
||||||
CREATE TRIGGER IF NOT EXISTS update_exchanges_updated_at
|
CREATE TRIGGER IF NOT EXISTS update_exchanges_updated_at
|
||||||
@@ -80,6 +85,7 @@ func (s *ExchangeStore) initDefaultData() error {
|
|||||||
}{
|
}{
|
||||||
{"binance", "Binance Futures", "binance"},
|
{"binance", "Binance Futures", "binance"},
|
||||||
{"bybit", "Bybit Futures", "bybit"},
|
{"bybit", "Bybit Futures", "bybit"},
|
||||||
|
{"okx", "OKX Futures", "okx"},
|
||||||
{"hyperliquid", "Hyperliquid", "hyperliquid"},
|
{"hyperliquid", "Hyperliquid", "hyperliquid"},
|
||||||
{"aster", "Aster DEX", "aster"},
|
{"aster", "Aster DEX", "aster"},
|
||||||
{"lighter", "LIGHTER DEX", "lighter"},
|
{"lighter", "LIGHTER DEX", "lighter"},
|
||||||
@@ -111,10 +117,41 @@ func (s *ExchangeStore) decrypt(encrypted string) string {
|
|||||||
return encrypted
|
return encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureUserExchanges 确保用户有所有支持的交易所记录
|
||||||
|
func (s *ExchangeStore) EnsureUserExchanges(userID string) error {
|
||||||
|
exchanges := []struct {
|
||||||
|
id, name, typ string
|
||||||
|
}{
|
||||||
|
{"binance", "Binance Futures", "binance"},
|
||||||
|
{"bybit", "Bybit Futures", "bybit"},
|
||||||
|
{"okx", "OKX Futures", "okx"},
|
||||||
|
{"hyperliquid", "Hyperliquid", "hyperliquid"},
|
||||||
|
{"aster", "Aster DEX", "aster"},
|
||||||
|
{"lighter", "LIGHTER DEX", "lighter"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, exchange := range exchanges {
|
||||||
|
_, err := s.db.Exec(`
|
||||||
|
INSERT OR IGNORE INTO exchanges (id, user_id, name, type, enabled)
|
||||||
|
VALUES (?, ?, ?, ?, 0)
|
||||||
|
`, exchange.id, userID, exchange.name, exchange.typ)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("确保用户交易所失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// List 获取用户的交易所列表
|
// List 获取用户的交易所列表
|
||||||
func (s *ExchangeStore) List(userID string) ([]*Exchange, error) {
|
func (s *ExchangeStore) List(userID string) ([]*Exchange, error) {
|
||||||
|
// 确保用户有所有支持的交易所记录
|
||||||
|
if err := s.EnsureUserExchanges(userID); err != nil {
|
||||||
|
logger.Debugf("⚠️ 确保用户交易所记录失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
rows, err := s.db.Query(`
|
rows, err := s.db.Query(`
|
||||||
SELECT id, user_id, name, type, enabled, api_key, secret_key, testnet,
|
SELECT id, user_id, name, type, enabled, api_key, secret_key,
|
||||||
|
COALESCE(passphrase, '') as passphrase, testnet,
|
||||||
COALESCE(hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr,
|
COALESCE(hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr,
|
||||||
COALESCE(aster_user, '') as aster_user,
|
COALESCE(aster_user, '') as aster_user,
|
||||||
COALESCE(aster_signer, '') as aster_signer,
|
COALESCE(aster_signer, '') as aster_signer,
|
||||||
@@ -136,7 +173,7 @@ func (s *ExchangeStore) List(userID string) ([]*Exchange, error) {
|
|||||||
var createdAt, updatedAt string
|
var createdAt, updatedAt string
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&e.ID, &e.UserID, &e.Name, &e.Type,
|
&e.ID, &e.UserID, &e.Name, &e.Type,
|
||||||
&e.Enabled, &e.APIKey, &e.SecretKey, &e.Testnet,
|
&e.Enabled, &e.APIKey, &e.SecretKey, &e.Passphrase, &e.Testnet,
|
||||||
&e.HyperliquidWalletAddr, &e.AsterUser, &e.AsterSigner, &e.AsterPrivateKey,
|
&e.HyperliquidWalletAddr, &e.AsterUser, &e.AsterSigner, &e.AsterPrivateKey,
|
||||||
&e.LighterWalletAddr, &e.LighterPrivateKey, &e.LighterAPIKeyPrivateKey,
|
&e.LighterWalletAddr, &e.LighterPrivateKey, &e.LighterAPIKeyPrivateKey,
|
||||||
&createdAt, &updatedAt,
|
&createdAt, &updatedAt,
|
||||||
@@ -148,6 +185,7 @@ func (s *ExchangeStore) List(userID string) ([]*Exchange, error) {
|
|||||||
e.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
e.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||||
e.APIKey = s.decrypt(e.APIKey)
|
e.APIKey = s.decrypt(e.APIKey)
|
||||||
e.SecretKey = s.decrypt(e.SecretKey)
|
e.SecretKey = s.decrypt(e.SecretKey)
|
||||||
|
e.Passphrase = s.decrypt(e.Passphrase)
|
||||||
e.AsterPrivateKey = s.decrypt(e.AsterPrivateKey)
|
e.AsterPrivateKey = s.decrypt(e.AsterPrivateKey)
|
||||||
e.LighterPrivateKey = s.decrypt(e.LighterPrivateKey)
|
e.LighterPrivateKey = s.decrypt(e.LighterPrivateKey)
|
||||||
e.LighterAPIKeyPrivateKey = s.decrypt(e.LighterAPIKeyPrivateKey)
|
e.LighterAPIKeyPrivateKey = s.decrypt(e.LighterAPIKeyPrivateKey)
|
||||||
@@ -157,8 +195,8 @@ func (s *ExchangeStore) List(userID string) ([]*Exchange, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update 更新交易所配置
|
// Update 更新交易所配置
|
||||||
func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKey string, testnet bool,
|
func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKey, passphrase string, testnet bool,
|
||||||
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey string) error {
|
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey, lighterApiKeyPrivateKey string) error {
|
||||||
|
|
||||||
logger.Debugf("🔧 ExchangeStore.Update: userID=%s, id=%s, enabled=%v", userID, id, enabled)
|
logger.Debugf("🔧 ExchangeStore.Update: userID=%s, id=%s, enabled=%v", userID, id, enabled)
|
||||||
|
|
||||||
@@ -181,6 +219,10 @@ func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKe
|
|||||||
setClauses = append(setClauses, "secret_key = ?")
|
setClauses = append(setClauses, "secret_key = ?")
|
||||||
args = append(args, s.encrypt(secretKey))
|
args = append(args, s.encrypt(secretKey))
|
||||||
}
|
}
|
||||||
|
if passphrase != "" {
|
||||||
|
setClauses = append(setClauses, "passphrase = ?")
|
||||||
|
args = append(args, s.encrypt(passphrase))
|
||||||
|
}
|
||||||
if asterPrivateKey != "" {
|
if asterPrivateKey != "" {
|
||||||
setClauses = append(setClauses, "aster_private_key = ?")
|
setClauses = append(setClauses, "aster_private_key = ?")
|
||||||
args = append(args, s.encrypt(asterPrivateKey))
|
args = append(args, s.encrypt(asterPrivateKey))
|
||||||
@@ -189,6 +231,10 @@ func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKe
|
|||||||
setClauses = append(setClauses, "lighter_private_key = ?")
|
setClauses = append(setClauses, "lighter_private_key = ?")
|
||||||
args = append(args, s.encrypt(lighterPrivateKey))
|
args = append(args, s.encrypt(lighterPrivateKey))
|
||||||
}
|
}
|
||||||
|
if lighterApiKeyPrivateKey != "" {
|
||||||
|
setClauses = append(setClauses, "lighter_api_key_private_key = ?")
|
||||||
|
args = append(args, s.encrypt(lighterApiKeyPrivateKey))
|
||||||
|
}
|
||||||
|
|
||||||
args = append(args, id, userID)
|
args = append(args, id, userID)
|
||||||
query := fmt.Sprintf(`UPDATE exchanges SET %s WHERE id = ? AND user_id = ?`, strings.Join(setClauses, ", "))
|
query := fmt.Sprintf(`UPDATE exchanges SET %s WHERE id = ? AND user_id = ?`, strings.Join(setClauses, ", "))
|
||||||
@@ -200,31 +246,33 @@ func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKe
|
|||||||
|
|
||||||
rowsAffected, _ := result.RowsAffected()
|
rowsAffected, _ := result.RowsAffected()
|
||||||
if rowsAffected == 0 {
|
if rowsAffected == 0 {
|
||||||
// 创建新记录
|
// 创建新记录,type 使用交易所 ID 以便后续正确识别
|
||||||
var name, typ string
|
var name, typ string
|
||||||
switch id {
|
switch id {
|
||||||
case "binance":
|
case "binance":
|
||||||
name, typ = "Binance Futures", "cex"
|
name, typ = "Binance Futures", "binance"
|
||||||
case "bybit":
|
case "bybit":
|
||||||
name, typ = "Bybit Futures", "cex"
|
name, typ = "Bybit Futures", "bybit"
|
||||||
|
case "okx":
|
||||||
|
name, typ = "OKX Futures", "okx"
|
||||||
case "hyperliquid":
|
case "hyperliquid":
|
||||||
name, typ = "Hyperliquid", "dex"
|
name, typ = "Hyperliquid", "hyperliquid"
|
||||||
case "aster":
|
case "aster":
|
||||||
name, typ = "Aster DEX", "dex"
|
name, typ = "Aster DEX", "aster"
|
||||||
case "lighter":
|
case "lighter":
|
||||||
name, typ = "LIGHTER DEX", "dex"
|
name, typ = "LIGHTER DEX", "lighter"
|
||||||
default:
|
default:
|
||||||
name, typ = id+" Exchange", "cex"
|
name, typ = id+" Exchange", id
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = s.db.Exec(`
|
_, err = s.db.Exec(`
|
||||||
INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet,
|
INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, passphrase, testnet,
|
||||||
hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key,
|
hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key,
|
||||||
lighter_wallet_addr, lighter_private_key, created_at, updated_at)
|
lighter_wallet_addr, lighter_private_key, lighter_api_key_private_key, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||||||
`, id, userID, name, typ, enabled, s.encrypt(apiKey), s.encrypt(secretKey), testnet,
|
`, id, userID, name, typ, enabled, s.encrypt(apiKey), s.encrypt(secretKey), s.encrypt(passphrase), testnet,
|
||||||
hyperliquidWalletAddr, asterUser, asterSigner, s.encrypt(asterPrivateKey),
|
hyperliquidWalletAddr, asterUser, asterSigner, s.encrypt(asterPrivateKey),
|
||||||
lighterWalletAddr, s.encrypt(lighterPrivateKey))
|
lighterWalletAddr, s.encrypt(lighterPrivateKey), s.encrypt(lighterApiKeyPrivateKey))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type AutoTraderConfig struct {
|
|||||||
AIModel string // AI模型: "qwen" 或 "deepseek"
|
AIModel string // AI模型: "qwen" 或 "deepseek"
|
||||||
|
|
||||||
// 交易平台选择
|
// 交易平台选择
|
||||||
Exchange string // "binance", "bybit", "hyperliquid", "aster" 或 "lighter"
|
Exchange string // "binance", "bybit", "okx", "hyperliquid", "aster" 或 "lighter"
|
||||||
|
|
||||||
// 币安API配置
|
// 币安API配置
|
||||||
BinanceAPIKey string
|
BinanceAPIKey string
|
||||||
@@ -32,6 +32,11 @@ type AutoTraderConfig struct {
|
|||||||
BybitAPIKey string
|
BybitAPIKey string
|
||||||
BybitSecretKey string
|
BybitSecretKey string
|
||||||
|
|
||||||
|
// OKX API配置
|
||||||
|
OKXAPIKey string
|
||||||
|
OKXSecretKey string
|
||||||
|
OKXPassphrase string
|
||||||
|
|
||||||
// Hyperliquid配置
|
// Hyperliquid配置
|
||||||
HyperliquidPrivateKey string
|
HyperliquidPrivateKey string
|
||||||
HyperliquidWalletAddr string
|
HyperliquidWalletAddr string
|
||||||
@@ -174,6 +179,9 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
|
|||||||
case "bybit":
|
case "bybit":
|
||||||
logger.Infof("🏦 [%s] 使用Bybit合约交易", config.Name)
|
logger.Infof("🏦 [%s] 使用Bybit合约交易", config.Name)
|
||||||
trader = NewBybitTrader(config.BybitAPIKey, config.BybitSecretKey)
|
trader = NewBybitTrader(config.BybitAPIKey, config.BybitSecretKey)
|
||||||
|
case "okx":
|
||||||
|
logger.Infof("🏦 [%s] 使用OKX合约交易", config.Name)
|
||||||
|
trader = NewOKXTrader(config.OKXAPIKey, config.OKXSecretKey, config.OKXPassphrase)
|
||||||
case "hyperliquid":
|
case "hyperliquid":
|
||||||
logger.Infof("🏦 [%s] 使用Hyperliquid交易", config.Name)
|
logger.Infof("🏦 [%s] 使用Hyperliquid交易", config.Name)
|
||||||
trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet)
|
trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet)
|
||||||
@@ -213,9 +221,28 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
|
|||||||
return nil, fmt.Errorf("不支持的交易平台: %s", config.Exchange)
|
return nil, fmt.Errorf("不支持的交易平台: %s", config.Exchange)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证初始金额配置
|
// 验证初始金额配置,如果为0则自动从交易所获取
|
||||||
if config.InitialBalance <= 0 {
|
if config.InitialBalance <= 0 {
|
||||||
return nil, fmt.Errorf("初始金额必须大于0,请在配置中设置InitialBalance")
|
logger.Infof("📊 [%s] 初始金额未设置,尝试从交易所获取当前余额...", config.Name)
|
||||||
|
account, err := trader.GetBalance()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("初始金额未设置且无法从交易所获取余额: %w", err)
|
||||||
|
}
|
||||||
|
// 尝试多种余额字段名(不同交易所返回格式不同)
|
||||||
|
balanceKeys := []string{"total_equity", "totalWalletBalance", "wallet_balance", "totalEq", "balance"}
|
||||||
|
var foundBalance float64
|
||||||
|
for _, key := range balanceKeys {
|
||||||
|
if balance, ok := account[key].(float64); ok && balance > 0 {
|
||||||
|
foundBalance = balance
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if foundBalance > 0 {
|
||||||
|
config.InitialBalance = foundBalance
|
||||||
|
logger.Infof("✓ [%s] 自动获取初始金额: %.2f USDT", config.Name, foundBalance)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("初始金额必须大于0,请在配置中设置InitialBalance或确保交易所账户有余额")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取最后的周期编号(用于恢复)
|
// 获取最后的周期编号(用于恢复)
|
||||||
|
|||||||
1123
trader/okx_trader.go
Normal file
1123
trader/okx_trader.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -601,11 +601,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
exchangeId: string,
|
exchangeId: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
secretKey?: string,
|
secretKey?: string,
|
||||||
|
passphrase?: string,
|
||||||
testnet?: boolean,
|
testnet?: boolean,
|
||||||
hyperliquidWalletAddr?: string,
|
hyperliquidWalletAddr?: string,
|
||||||
asterUser?: string,
|
asterUser?: string,
|
||||||
asterSigner?: string,
|
asterSigner?: string,
|
||||||
asterPrivateKey?: string
|
asterPrivateKey?: string,
|
||||||
|
lighterWalletAddr?: string,
|
||||||
|
lighterPrivateKey?: string,
|
||||||
|
lighterApiKeyPrivateKey?: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// 找到要配置的交易所(从supportedExchanges中)
|
// 找到要配置的交易所(从supportedExchanges中)
|
||||||
@@ -630,11 +634,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
...e,
|
...e,
|
||||||
apiKey,
|
apiKey,
|
||||||
secretKey,
|
secretKey,
|
||||||
|
passphrase,
|
||||||
testnet,
|
testnet,
|
||||||
hyperliquidWalletAddr,
|
hyperliquidWalletAddr,
|
||||||
asterUser,
|
asterUser,
|
||||||
asterSigner,
|
asterSigner,
|
||||||
asterPrivateKey,
|
asterPrivateKey,
|
||||||
|
lighterWalletAddr,
|
||||||
|
lighterPrivateKey,
|
||||||
|
lighterApiKeyPrivateKey,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
}
|
}
|
||||||
: e
|
: e
|
||||||
@@ -645,11 +653,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
...exchangeToUpdate,
|
...exchangeToUpdate,
|
||||||
apiKey,
|
apiKey,
|
||||||
secretKey,
|
secretKey,
|
||||||
|
passphrase,
|
||||||
testnet,
|
testnet,
|
||||||
hyperliquidWalletAddr,
|
hyperliquidWalletAddr,
|
||||||
asterUser,
|
asterUser,
|
||||||
asterSigner,
|
asterSigner,
|
||||||
asterPrivateKey,
|
asterPrivateKey,
|
||||||
|
lighterWalletAddr,
|
||||||
|
lighterPrivateKey,
|
||||||
|
lighterApiKeyPrivateKey,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
}
|
}
|
||||||
updatedExchanges = [...(allExchanges || []), newExchange]
|
updatedExchanges = [...(allExchanges || []), newExchange]
|
||||||
@@ -663,11 +675,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
enabled: exchange.enabled,
|
enabled: exchange.enabled,
|
||||||
api_key: exchange.apiKey || '',
|
api_key: exchange.apiKey || '',
|
||||||
secret_key: exchange.secretKey || '',
|
secret_key: exchange.secretKey || '',
|
||||||
|
passphrase: exchange.passphrase || '',
|
||||||
testnet: exchange.testnet || false,
|
testnet: exchange.testnet || false,
|
||||||
hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
|
hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
|
||||||
aster_user: exchange.asterUser || '',
|
aster_user: exchange.asterUser || '',
|
||||||
aster_signer: exchange.asterSigner || '',
|
aster_signer: exchange.asterSigner || '',
|
||||||
aster_private_key: exchange.asterPrivateKey || '',
|
aster_private_key: exchange.asterPrivateKey || '',
|
||||||
|
lighter_wallet_addr: exchange.lighterWalletAddr || '',
|
||||||
|
lighter_private_key: exchange.lighterPrivateKey || '',
|
||||||
|
lighter_api_key_private_key: exchange.lighterApiKeyPrivateKey || '',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
@@ -1758,11 +1774,15 @@ function ExchangeConfigModal({
|
|||||||
exchangeId: string,
|
exchangeId: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
secretKey?: string,
|
secretKey?: string,
|
||||||
|
passphrase?: string,
|
||||||
testnet?: boolean,
|
testnet?: boolean,
|
||||||
hyperliquidWalletAddr?: string,
|
hyperliquidWalletAddr?: string,
|
||||||
asterUser?: string,
|
asterUser?: string,
|
||||||
asterSigner?: string,
|
asterSigner?: string,
|
||||||
asterPrivateKey?: string
|
asterPrivateKey?: string,
|
||||||
|
lighterWalletAddr?: string,
|
||||||
|
lighterPrivateKey?: string,
|
||||||
|
lighterApiKeyPrivateKey?: string
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
onDelete: (exchangeId: string) => void
|
onDelete: (exchangeId: string) => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@@ -1796,9 +1816,14 @@ function ExchangeConfigModal({
|
|||||||
// Hyperliquid 特定字段
|
// Hyperliquid 特定字段
|
||||||
const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('')
|
const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('')
|
||||||
|
|
||||||
|
// LIGHTER 特定字段
|
||||||
|
const [lighterWalletAddr, setLighterWalletAddr] = useState('')
|
||||||
|
const [lighterPrivateKey, setLighterPrivateKey] = useState('')
|
||||||
|
const [lighterApiKeyPrivateKey, setLighterApiKeyPrivateKey] = useState('')
|
||||||
|
|
||||||
// 安全输入状态
|
// 安全输入状态
|
||||||
const [secureInputTarget, setSecureInputTarget] = useState<
|
const [secureInputTarget, setSecureInputTarget] = useState<
|
||||||
null | 'hyperliquid' | 'aster'
|
null | 'hyperliquid' | 'aster' | 'lighter'
|
||||||
>(null)
|
>(null)
|
||||||
|
|
||||||
// 获取当前编辑的交易所信息
|
// 获取当前编辑的交易所信息
|
||||||
@@ -1821,6 +1846,11 @@ function ExchangeConfigModal({
|
|||||||
|
|
||||||
// Hyperliquid 字段
|
// Hyperliquid 字段
|
||||||
setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '')
|
setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '')
|
||||||
|
|
||||||
|
// LIGHTER 字段
|
||||||
|
setLighterWalletAddr(selectedExchange.lighterWalletAddr || '')
|
||||||
|
setLighterPrivateKey('') // Don't load existing private key for security
|
||||||
|
setLighterApiKeyPrivateKey('') // Don't load existing API key for security
|
||||||
}
|
}
|
||||||
}, [editingExchangeId, selectedExchange])
|
}, [editingExchangeId, selectedExchange])
|
||||||
|
|
||||||
@@ -1926,15 +1956,19 @@ function ExchangeConfigModal({
|
|||||||
if (!selectedExchangeId) return
|
if (!selectedExchangeId) return
|
||||||
|
|
||||||
// 根据交易所类型验证不同字段
|
// 根据交易所类型验证不同字段
|
||||||
if (selectedExchange?.id === 'binance') {
|
if (selectedExchange?.id === 'binance' || selectedExchange?.id === 'bybit') {
|
||||||
if (!apiKey.trim() || !secretKey.trim()) return
|
if (!apiKey.trim() || !secretKey.trim()) return
|
||||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
|
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet)
|
||||||
|
} else if (selectedExchange?.id === 'okx') {
|
||||||
|
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
|
||||||
|
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet)
|
||||||
} else if (selectedExchange?.id === 'hyperliquid') {
|
} else if (selectedExchange?.id === 'hyperliquid') {
|
||||||
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址
|
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return
|
||||||
await onSave(
|
await onSave(
|
||||||
selectedExchangeId,
|
selectedExchangeId,
|
||||||
apiKey.trim(),
|
apiKey.trim(),
|
||||||
'',
|
'',
|
||||||
|
'',
|
||||||
testnet,
|
testnet,
|
||||||
hyperliquidWalletAddr.trim()
|
hyperliquidWalletAddr.trim()
|
||||||
)
|
)
|
||||||
@@ -1945,19 +1979,33 @@ function ExchangeConfigModal({
|
|||||||
selectedExchangeId,
|
selectedExchangeId,
|
||||||
'',
|
'',
|
||||||
'',
|
'',
|
||||||
|
'',
|
||||||
testnet,
|
testnet,
|
||||||
undefined,
|
undefined,
|
||||||
asterUser.trim(),
|
asterUser.trim(),
|
||||||
asterSigner.trim(),
|
asterSigner.trim(),
|
||||||
asterPrivateKey.trim()
|
asterPrivateKey.trim()
|
||||||
)
|
)
|
||||||
} else if (selectedExchange?.id === 'okx') {
|
} else if (selectedExchange?.id === 'lighter') {
|
||||||
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
|
if (!lighterWalletAddr.trim() || !lighterPrivateKey.trim()) return
|
||||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
|
await onSave(
|
||||||
|
selectedExchangeId,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
testnet,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
lighterWalletAddr.trim(),
|
||||||
|
lighterPrivateKey.trim(),
|
||||||
|
lighterApiKeyPrivateKey.trim()
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// 默认情况(其他CEX交易所)
|
// 默认情况(其他CEX交易所)
|
||||||
if (!apiKey.trim() || !secretKey.trim()) return
|
if (!apiKey.trim() || !secretKey.trim()) return
|
||||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
|
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2095,9 +2143,10 @@ function ExchangeConfigModal({
|
|||||||
|
|
||||||
{selectedExchange && (
|
{selectedExchange && (
|
||||||
<>
|
<>
|
||||||
{/* Binance/Bybit 和其他 CEX 交易所的字段 */}
|
{/* Binance/Bybit/OKX 和其他 CEX 交易所的字段 */}
|
||||||
{(selectedExchange.id === 'binance' ||
|
{(selectedExchange.id === 'binance' ||
|
||||||
selectedExchange.id === 'bybit' ||
|
selectedExchange.id === 'bybit' ||
|
||||||
|
selectedExchange.id === 'okx' ||
|
||||||
selectedExchange.type === 'cex') &&
|
selectedExchange.type === 'cex') &&
|
||||||
selectedExchange.id !== 'hyperliquid' &&
|
selectedExchange.id !== 'hyperliquid' &&
|
||||||
selectedExchange.id !== 'aster' && (
|
selectedExchange.id !== 'aster' && (
|
||||||
@@ -2553,6 +2602,104 @@ function ExchangeConfigModal({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* LIGHTER 交易所的字段 */}
|
||||||
|
{selectedExchange.id === 'lighter' && (
|
||||||
|
<>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label
|
||||||
|
className="block text-sm font-semibold mb-2"
|
||||||
|
style={{ color: '#EAECEF' }}
|
||||||
|
>
|
||||||
|
{t('lighterWalletAddress', language)}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={lighterWalletAddr}
|
||||||
|
onChange={(e) => setLighterWalletAddr(e.target.value)}
|
||||||
|
placeholder={t('enterLighterWalletAddress', 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('lighterWalletAddressDesc', language)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label
|
||||||
|
className="block text-sm font-semibold mb-2"
|
||||||
|
style={{ color: '#EAECEF' }}
|
||||||
|
>
|
||||||
|
{t('lighterPrivateKey', language)}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={lighterPrivateKey}
|
||||||
|
onChange={(e) => setLighterPrivateKey(e.target.value)}
|
||||||
|
placeholder={t('enterLighterPrivateKey', language)}
|
||||||
|
className="w-full px-3 py-2 rounded font-mono text-sm"
|
||||||
|
style={{
|
||||||
|
background: '#0B0E11',
|
||||||
|
border: '1px solid #2B3139',
|
||||||
|
color: '#EAECEF',
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||||
|
{t('lighterPrivateKeyDesc', language)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label
|
||||||
|
className="block text-sm font-semibold mb-2"
|
||||||
|
style={{ color: '#EAECEF' }}
|
||||||
|
>
|
||||||
|
{t('lighterApiKeyPrivateKey', language)} ⭐
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={lighterApiKeyPrivateKey}
|
||||||
|
onChange={(e) => setLighterApiKeyPrivateKey(e.target.value)}
|
||||||
|
placeholder={t('enterLighterApiKeyPrivateKey', language)}
|
||||||
|
className="w-full px-3 py-2 rounded font-mono text-sm"
|
||||||
|
style={{
|
||||||
|
background: '#0B0E11',
|
||||||
|
border: '1px solid #2B3139',
|
||||||
|
color: '#EAECEF',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||||
|
{t('lighterApiKeyPrivateKeyDesc', language)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4 p-3 rounded" style={{
|
||||||
|
background: lighterApiKeyPrivateKey ? '#0F3F2E' : '#3F2E0F',
|
||||||
|
border: '1px solid ' + (lighterApiKeyPrivateKey ? '#10B981' : '#F59E0B')
|
||||||
|
}}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-sm font-semibold" style={{
|
||||||
|
color: lighterApiKeyPrivateKey ? '#10B981' : '#F59E0B'
|
||||||
|
}}>
|
||||||
|
{lighterApiKeyPrivateKey ? '✅ LIGHTER V2' : '⚠️ LIGHTER V1'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||||
|
{lighterApiKeyPrivateKey
|
||||||
|
? t('lighterV2Description', language)
|
||||||
|
: t('lighterV1Description', language)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2575,25 +2722,20 @@ function ExchangeConfigModal({
|
|||||||
!selectedExchange ||
|
!selectedExchange ||
|
||||||
(selectedExchange.id === 'binance' &&
|
(selectedExchange.id === 'binance' &&
|
||||||
(!apiKey.trim() || !secretKey.trim())) ||
|
(!apiKey.trim() || !secretKey.trim())) ||
|
||||||
|
(selectedExchange.id === 'bybit' &&
|
||||||
|
(!apiKey.trim() || !secretKey.trim())) ||
|
||||||
(selectedExchange.id === 'okx' &&
|
(selectedExchange.id === 'okx' &&
|
||||||
(!apiKey.trim() ||
|
(!apiKey.trim() ||
|
||||||
!secretKey.trim() ||
|
!secretKey.trim() ||
|
||||||
!passphrase.trim())) ||
|
!passphrase.trim())) ||
|
||||||
(selectedExchange.id === 'hyperliquid' &&
|
(selectedExchange.id === 'hyperliquid' &&
|
||||||
(!apiKey.trim() || !hyperliquidWalletAddr.trim())) || // 验证私钥和钱包地址
|
(!apiKey.trim() || !hyperliquidWalletAddr.trim())) ||
|
||||||
(selectedExchange.id === 'aster' &&
|
(selectedExchange.id === 'aster' &&
|
||||||
(!asterUser.trim() ||
|
(!asterUser.trim() ||
|
||||||
!asterSigner.trim() ||
|
!asterSigner.trim() ||
|
||||||
!asterPrivateKey.trim())) ||
|
!asterPrivateKey.trim())) ||
|
||||||
(selectedExchange.id === 'bybit' &&
|
(selectedExchange.id === 'lighter' &&
|
||||||
(!apiKey.trim() || !secretKey.trim())) ||
|
(!lighterWalletAddr.trim() || !lighterPrivateKey.trim()))
|
||||||
(selectedExchange.type === 'cex' &&
|
|
||||||
selectedExchange.id !== 'hyperliquid' &&
|
|
||||||
selectedExchange.id !== 'aster' &&
|
|
||||||
selectedExchange.id !== 'binance' &&
|
|
||||||
selectedExchange.id !== 'bybit' &&
|
|
||||||
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' }}
|
||||||
|
|||||||
@@ -80,6 +80,28 @@ const BybitIcon: React.FC<IconProps> = ({
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// OKX SVG 图标组件
|
||||||
|
const OKXIcon: React.FC<IconProps> = ({
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
className,
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<rect width="200" height="200" rx="24" fill="#000"/>
|
||||||
|
<rect x="40" y="40" width="50" height="50" rx="8" fill="#fff"/>
|
||||||
|
<rect x="110" y="40" width="50" height="50" rx="8" fill="#fff"/>
|
||||||
|
<rect x="40" y="110" width="50" height="50" rx="8" fill="#fff"/>
|
||||||
|
<rect x="110" y="110" width="50" height="50" rx="8" fill="#fff"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
// Aster SVG 图标组件
|
// Aster SVG 图标组件
|
||||||
const AsterIcon: React.FC<IconProps> = ({
|
const AsterIcon: React.FC<IconProps> = ({
|
||||||
width = 24,
|
width = 24,
|
||||||
@@ -168,11 +190,13 @@ export const getExchangeIcon = (
|
|||||||
? 'binance'
|
? 'binance'
|
||||||
: exchangeType.toLowerCase().includes('bybit')
|
: exchangeType.toLowerCase().includes('bybit')
|
||||||
? 'bybit'
|
? 'bybit'
|
||||||
: exchangeType.toLowerCase().includes('hyperliquid')
|
: exchangeType.toLowerCase().includes('okx')
|
||||||
? 'hyperliquid'
|
? 'okx'
|
||||||
: exchangeType.toLowerCase().includes('aster')
|
: exchangeType.toLowerCase().includes('hyperliquid')
|
||||||
? 'aster'
|
? 'hyperliquid'
|
||||||
: exchangeType.toLowerCase()
|
: exchangeType.toLowerCase().includes('aster')
|
||||||
|
? 'aster'
|
||||||
|
: exchangeType.toLowerCase()
|
||||||
|
|
||||||
const iconProps = {
|
const iconProps = {
|
||||||
width: props.width || 24,
|
width: props.width || 24,
|
||||||
@@ -185,6 +209,8 @@ export const getExchangeIcon = (
|
|||||||
return <BinanceIcon {...iconProps} />
|
return <BinanceIcon {...iconProps} />
|
||||||
case 'bybit':
|
case 'bybit':
|
||||||
return <BybitIcon {...iconProps} />
|
return <BybitIcon {...iconProps} />
|
||||||
|
case 'okx':
|
||||||
|
return <OKXIcon {...iconProps} />
|
||||||
case 'hyperliquid':
|
case 'hyperliquid':
|
||||||
case 'dex':
|
case 'dex':
|
||||||
return <HyperliquidIcon {...iconProps} />
|
return <HyperliquidIcon {...iconProps} />
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface ExchangeConfigModalProps {
|
|||||||
exchangeId: string,
|
exchangeId: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
secretKey?: string,
|
secretKey?: string,
|
||||||
|
passphrase?: string, // OKX专用
|
||||||
testnet?: boolean,
|
testnet?: boolean,
|
||||||
hyperliquidWalletAddr?: string,
|
hyperliquidWalletAddr?: string,
|
||||||
asterUser?: string,
|
asterUser?: string,
|
||||||
@@ -222,13 +223,17 @@ export 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)
|
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet)
|
||||||
|
} else if (selectedExchange?.id === 'okx') {
|
||||||
|
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
|
||||||
|
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet)
|
||||||
} else if (selectedExchange?.id === 'hyperliquid') {
|
} else if (selectedExchange?.id === 'hyperliquid') {
|
||||||
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址
|
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址
|
||||||
await onSave(
|
await onSave(
|
||||||
selectedExchangeId,
|
selectedExchangeId,
|
||||||
apiKey.trim(),
|
apiKey.trim(),
|
||||||
'',
|
'',
|
||||||
|
'',
|
||||||
testnet,
|
testnet,
|
||||||
hyperliquidWalletAddr.trim()
|
hyperliquidWalletAddr.trim()
|
||||||
)
|
)
|
||||||
@@ -239,6 +244,7 @@ export function ExchangeConfigModal({
|
|||||||
selectedExchangeId,
|
selectedExchangeId,
|
||||||
'',
|
'',
|
||||||
'',
|
'',
|
||||||
|
'',
|
||||||
testnet,
|
testnet,
|
||||||
undefined,
|
undefined,
|
||||||
asterUser.trim(),
|
asterUser.trim(),
|
||||||
@@ -251,6 +257,7 @@ export function ExchangeConfigModal({
|
|||||||
selectedExchangeId,
|
selectedExchangeId,
|
||||||
lighterPrivateKey.trim(),
|
lighterPrivateKey.trim(),
|
||||||
'',
|
'',
|
||||||
|
'',
|
||||||
testnet,
|
testnet,
|
||||||
lighterWalletAddr.trim(),
|
lighterWalletAddr.trim(),
|
||||||
undefined,
|
undefined,
|
||||||
@@ -260,13 +267,10 @@ export function ExchangeConfigModal({
|
|||||||
lighterPrivateKey.trim(),
|
lighterPrivateKey.trim(),
|
||||||
lighterApiKeyPrivateKey.trim()
|
lighterApiKeyPrivateKey.trim()
|
||||||
)
|
)
|
||||||
} else if (selectedExchange?.id === 'okx') {
|
|
||||||
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
|
|
||||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
|
|
||||||
} else {
|
} else {
|
||||||
// 默认情况(其他CEX交易所)
|
// 默认情况(其他CEX交易所)
|
||||||
if (!apiKey.trim() || !secretKey.trim()) return
|
if (!apiKey.trim() || !secretKey.trim()) return
|
||||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
|
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,12 +408,10 @@ export function ExchangeConfigModal({
|
|||||||
|
|
||||||
{selectedExchange && (
|
{selectedExchange && (
|
||||||
<>
|
<>
|
||||||
{/* Binance/Bybit 和其他 CEX 交易所的字段 */}
|
{/* Binance/Bybit/OKX 的输入字段 */}
|
||||||
{(selectedExchange.id === 'binance' ||
|
{(selectedExchange.id === 'binance' ||
|
||||||
selectedExchange.id === 'bybit' ||
|
selectedExchange.id === 'bybit' ||
|
||||||
selectedExchange.type === 'cex') &&
|
selectedExchange.id === 'okx') && (
|
||||||
selectedExchange.id !== 'hyperliquid' &&
|
|
||||||
selectedExchange.id !== 'aster' && (
|
|
||||||
<>
|
<>
|
||||||
{/* 币安用户配置提示 (D1 方案) */}
|
{/* 币安用户配置提示 (D1 方案) */}
|
||||||
{selectedExchange.id === 'binance' && (
|
{selectedExchange.id === 'binance' && (
|
||||||
|
|||||||
@@ -445,6 +445,7 @@ export function useTraderActions({
|
|||||||
...e,
|
...e,
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
secretKey: '',
|
secretKey: '',
|
||||||
|
passphrase: '', // OKX专用
|
||||||
hyperliquidWalletAddr: '',
|
hyperliquidWalletAddr: '',
|
||||||
asterUser: '',
|
asterUser: '',
|
||||||
asterSigner: '',
|
asterSigner: '',
|
||||||
@@ -459,6 +460,7 @@ export function useTraderActions({
|
|||||||
enabled: exchange.enabled,
|
enabled: exchange.enabled,
|
||||||
api_key: exchange.apiKey || '',
|
api_key: exchange.apiKey || '',
|
||||||
secret_key: exchange.secretKey || '',
|
secret_key: exchange.secretKey || '',
|
||||||
|
passphrase: exchange.passphrase || '', // OKX专用
|
||||||
testnet: exchange.testnet || false,
|
testnet: exchange.testnet || false,
|
||||||
hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
|
hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
|
||||||
aster_user: exchange.asterUser || '',
|
aster_user: exchange.asterUser || '',
|
||||||
@@ -486,6 +488,7 @@ export function useTraderActions({
|
|||||||
exchangeId: string,
|
exchangeId: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
secretKey?: string,
|
secretKey?: string,
|
||||||
|
passphrase?: string, // OKX专用
|
||||||
testnet?: boolean,
|
testnet?: boolean,
|
||||||
hyperliquidWalletAddr?: string,
|
hyperliquidWalletAddr?: string,
|
||||||
asterUser?: string,
|
asterUser?: string,
|
||||||
@@ -518,6 +521,7 @@ export function useTraderActions({
|
|||||||
...e,
|
...e,
|
||||||
apiKey,
|
apiKey,
|
||||||
secretKey,
|
secretKey,
|
||||||
|
passphrase, // OKX专用
|
||||||
testnet,
|
testnet,
|
||||||
hyperliquidWalletAddr,
|
hyperliquidWalletAddr,
|
||||||
asterUser,
|
asterUser,
|
||||||
@@ -536,6 +540,7 @@ export function useTraderActions({
|
|||||||
...exchangeToUpdate,
|
...exchangeToUpdate,
|
||||||
apiKey,
|
apiKey,
|
||||||
secretKey,
|
secretKey,
|
||||||
|
passphrase, // OKX专用
|
||||||
testnet,
|
testnet,
|
||||||
hyperliquidWalletAddr,
|
hyperliquidWalletAddr,
|
||||||
asterUser,
|
asterUser,
|
||||||
@@ -557,6 +562,7 @@ export function useTraderActions({
|
|||||||
enabled: exchange.enabled,
|
enabled: exchange.enabled,
|
||||||
api_key: exchange.apiKey || '',
|
api_key: exchange.apiKey || '',
|
||||||
secret_key: exchange.secretKey || '',
|
secret_key: exchange.secretKey || '',
|
||||||
|
passphrase: exchange.passphrase || '', // OKX专用
|
||||||
testnet: exchange.testnet || false,
|
testnet: exchange.testnet || false,
|
||||||
hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
|
hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
|
||||||
aster_user: exchange.asterUser || '',
|
aster_user: exchange.asterUser || '',
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export interface Exchange {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
secretKey?: string
|
secretKey?: string
|
||||||
|
passphrase?: string // OKX 特定字段
|
||||||
testnet?: boolean
|
testnet?: boolean
|
||||||
// Hyperliquid 特定字段
|
// Hyperliquid 特定字段
|
||||||
hyperliquidWalletAddr?: string
|
hyperliquidWalletAddr?: string
|
||||||
@@ -163,6 +164,7 @@ export interface UpdateExchangeConfigRequest {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
api_key: string
|
api_key: string
|
||||||
secret_key: string
|
secret_key: string
|
||||||
|
passphrase?: string
|
||||||
testnet?: boolean
|
testnet?: boolean
|
||||||
// Hyperliquid 特定字段
|
// Hyperliquid 特定字段
|
||||||
hyperliquid_wallet_addr?: string
|
hyperliquid_wallet_addr?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user