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

@@ -24,6 +24,7 @@ type Exchange struct {
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey"`
SecretKey string `json:"secretKey"`
Passphrase string `json:"passphrase"` // OKX专用
Testnet bool `json:"testnet"`
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"`
AsterUser string `json:"asterUser"`
@@ -46,6 +47,7 @@ func (s *ExchangeStore) initTables() error {
enabled BOOLEAN DEFAULT 0,
api_key TEXT DEFAULT '',
secret_key TEXT DEFAULT '',
passphrase TEXT DEFAULT '',
testnet BOOLEAN DEFAULT 0,
hyperliquid_wallet_addr TEXT DEFAULT '',
aster_user TEXT DEFAULT '',
@@ -63,6 +65,9 @@ func (s *ExchangeStore) initTables() error {
return err
}
// 迁移:添加 passphrase 列(如果不存在)
s.db.Exec(`ALTER TABLE exchanges ADD COLUMN passphrase TEXT DEFAULT ''`)
// 触发器
_, err = s.db.Exec(`
CREATE TRIGGER IF NOT EXISTS update_exchanges_updated_at
@@ -80,6 +85,7 @@ func (s *ExchangeStore) initDefaultData() error {
}{
{"binance", "Binance Futures", "binance"},
{"bybit", "Bybit Futures", "bybit"},
{"okx", "OKX Futures", "okx"},
{"hyperliquid", "Hyperliquid", "hyperliquid"},
{"aster", "Aster DEX", "aster"},
{"lighter", "LIGHTER DEX", "lighter"},
@@ -111,10 +117,41 @@ func (s *ExchangeStore) decrypt(encrypted string) string {
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 获取用户的交易所列表
func (s *ExchangeStore) List(userID string) ([]*Exchange, error) {
// 确保用户有所有支持的交易所记录
if err := s.EnsureUserExchanges(userID); err != nil {
logger.Debugf("⚠️ 确保用户交易所记录失败: %v", err)
}
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(aster_user, '') as aster_user,
COALESCE(aster_signer, '') as aster_signer,
@@ -136,7 +173,7 @@ func (s *ExchangeStore) List(userID string) ([]*Exchange, error) {
var createdAt, updatedAt string
err := rows.Scan(
&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.LighterWalletAddr, &e.LighterPrivateKey, &e.LighterAPIKeyPrivateKey,
&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.APIKey = s.decrypt(e.APIKey)
e.SecretKey = s.decrypt(e.SecretKey)
e.Passphrase = s.decrypt(e.Passphrase)
e.AsterPrivateKey = s.decrypt(e.AsterPrivateKey)
e.LighterPrivateKey = s.decrypt(e.LighterPrivateKey)
e.LighterAPIKeyPrivateKey = s.decrypt(e.LighterAPIKeyPrivateKey)
@@ -157,8 +195,8 @@ func (s *ExchangeStore) List(userID string) ([]*Exchange, error) {
}
// Update 更新交易所配置
func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKey string, testnet bool,
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey string) error {
func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKey, passphrase string, testnet bool,
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey, lighterApiKeyPrivateKey string) error {
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 = ?")
args = append(args, s.encrypt(secretKey))
}
if passphrase != "" {
setClauses = append(setClauses, "passphrase = ?")
args = append(args, s.encrypt(passphrase))
}
if asterPrivateKey != "" {
setClauses = append(setClauses, "aster_private_key = ?")
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 = ?")
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)
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()
if rowsAffected == 0 {
// 创建新记录
// 创建新记录type 使用交易所 ID 以便后续正确识别
var name, typ string
switch id {
case "binance":
name, typ = "Binance Futures", "cex"
name, typ = "Binance Futures", "binance"
case "bybit":
name, typ = "Bybit Futures", "cex"
name, typ = "Bybit Futures", "bybit"
case "okx":
name, typ = "OKX Futures", "okx"
case "hyperliquid":
name, typ = "Hyperliquid", "dex"
name, typ = "Hyperliquid", "hyperliquid"
case "aster":
name, typ = "Aster DEX", "dex"
name, typ = "Aster DEX", "aster"
case "lighter":
name, typ = "LIGHTER DEX", "dex"
name, typ = "LIGHTER DEX", "lighter"
default:
name, typ = id+" Exchange", "cex"
name, typ = id+" Exchange", id
}
_, 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,
lighter_wallet_addr, lighter_private_key, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`, id, userID, name, typ, enabled, s.encrypt(apiKey), s.encrypt(secretKey), testnet,
lighter_wallet_addr, lighter_private_key, lighter_api_key_private_key, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`, id, userID, name, typ, enabled, s.encrypt(apiKey), s.encrypt(secretKey), s.encrypt(passphrase), testnet,
hyperliquidWalletAddr, asterUser, asterSigner, s.encrypt(asterPrivateKey),
lighterWalletAddr, s.encrypt(lighterPrivateKey))
lighterWalletAddr, s.encrypt(lighterPrivateKey), s.encrypt(lighterApiKeyPrivateKey))
return err
}
return nil