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:
tinkle-community
2025-12-06 18:04:59 +08:00
committed by GitHub
parent 5d1d0b6842
commit 1e5ece947c
11 changed files with 1489 additions and 68 deletions

View File

@@ -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

BIN
img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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' }}

View File

@@ -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} />

View File

@@ -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' && (

View File

@@ -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 || '',

View File

@@ -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