mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 15:26:55 +08:00
feat: 添加 OKX 交易所支持 (#1150)
* feat: 添加 OKX 交易所支持(USDT Perpetual Swap) ## 新增功能 - 實現完整的 OKX API v5 REST 客戶端(純 Go 標準庫,無外部依賴) - 支持 USDT 永續合約交易(BTC-USDT-SWAP 等) - 實現 Trader 接口的 13 個核心方法 ## 技術細節 ### trader/okx_trader.go (NEW) - HMAC-SHA256 簽名機制(完全符合 OKX API v5 規範) - 餘額和持倉緩存(15秒,參考 Binance 實現) - 支持 Demo Trading(testnet 模式) - Symbol 格式轉換(BTCUSDT ↔ BTC-USDT-SWAP) - 全倉模式(Cross Margin)支持 - 自動槓桿設置 ### 實現的接口方法: - ✅ GetBalance() - 獲取賬戶餘額 - ✅ GetPositions() - 獲取所有持倉 - ✅ OpenLong() / OpenShort() - 開倉 - ✅ CloseLong() / CloseShort() - 平倉 - ✅ SetLeverage() - 設置槓桿 - ✅ SetMarginMode() - 設置保證金模式 - ✅ GetMarketPrice() - 獲取市場價格 - ✅ FormatQuantity() - 格式化數量 - ⚠️ 止盈止損功能標記為 TODO(非核心交易功能) ### config/database.go (MODIFIED) - 添加 "okx" 到預設交易所列表 - 新增 okx_passphrase 字段(OKX 需要 3 個認證參數) - 更新 ExchangeConfig 結構 - 添加數據庫遷移語句(ALTER TABLE) ### api/server.go (MODIFIED) - 在 handleCreateTrader() 添加 OKX 初始化邏輯 - switch case "okx" 分支 ## 代碼品質 - 代碼行數:~450 行 - 外部依賴:0 個 - 編譯狀態:✅ 通過 - 測試覆蓋:待實現(下一步) ## 待完成事項 - [ ] 撰寫單元測試(目標 >80% 覆蓋率) - [ ] 完善數據庫查詢邏輯(GetExchanges 添加 OKX passphrase 掃描) - [ ] 實現止盈止損功能(可選) * refactor: 完善 OKX passphrase 數據庫和 API 支持 - config/database.go: • GetExchanges() 添加 okx_passphrase 查詢和解密 • UpdateExchange() 函數簽名添加 okxPassphrase 參數 • UpdateExchange() UPDATE 邏輯添加 okx_passphrase SET 子句 • UpdateExchange() INSERT 添加 okx_passphrase 加密和列 - api/server.go: • UpdateExchangeConfigRequest 添加 OKXPassphrase 字段 • UpdateExchange 調用添加 OKXPassphrase 參數 - api/utils.go: • SanitizeExchangeConfigForLog 添加 OKXPassphrase 脫敏 ✅ 編譯測試通過,OKX 完整功能支持完成 * test: 添加 OKX Trader 完整單元測試套件 📊 測試覆蓋率:92.6% (遠超 80% 目標) ✅ 完成的測試: - 接口兼容性測試 - NewOKXTrader 構造函數測試(5個場景) - 符號格式轉換測試(5個場景) - HMAC-SHA256 簽名一致性測試 - GetBalance 測試(含字段驗證) - GetPositions 測試(含標準化數據驗證) - GetMarketPrice 測試(3個場景) - FormatQuantity 測試(5個場景) - SetLeverage/SetMarginMode 測試 - OpenLong/OpenShort 測試 - CloseLong/CloseShort 測試 - 緩存機制測試 - 錯誤處理測試(API錯誤、網絡錯誤、JSON錯誤) 🔧 測試套件架構: - OKXTraderTestSuite 繼承 TraderTestSuite - Mock HTTP 服務器模擬 OKX API v5 響應 - 完整覆蓋所有公開方法 - 包含邊界條件和錯誤場景測試 📈 方法覆蓋率明細: - request: 90.0% - GetBalance: 97.0% - GetPositions: 83.3% - formatSymbol, OpenLong, OpenShort, CloseLong, CloseShort: 100% - placeOrder, SetMarginMode, FormatQuantity, clearCache: 100% - Cancel* 方法系列: 100% - SetLeverage: 81.8% - GetMarketPrice: 85.7% --------- Co-authored-by: the-dev-z <the-dev-z@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
52e4ff6921
commit
62bce32d1f
@@ -152,6 +152,8 @@ func (d *Database) createTables() error {
|
||||
lighter_wallet_addr TEXT DEFAULT '',
|
||||
lighter_private_key TEXT DEFAULT '',
|
||||
lighter_api_key_private_key TEXT DEFAULT '',
|
||||
-- OKX 特定字段
|
||||
okx_passphrase TEXT DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
@@ -362,6 +364,7 @@ func (d *Database) createTables() error {
|
||||
`ALTER TABLE exchanges ADD COLUMN lighter_wallet_addr TEXT DEFAULT ''`,
|
||||
`ALTER TABLE exchanges ADD COLUMN lighter_private_key TEXT DEFAULT ''`,
|
||||
`ALTER TABLE exchanges ADD COLUMN lighter_api_key_private_key TEXT DEFAULT ''`,
|
||||
`ALTER TABLE exchanges ADD COLUMN okx_passphrase TEXT DEFAULT ''`,
|
||||
`ALTER TABLE traders ADD COLUMN custom_prompt TEXT DEFAULT ''`,
|
||||
`ALTER TABLE traders ADD COLUMN override_base_prompt BOOLEAN DEFAULT 0`,
|
||||
`ALTER TABLE traders ADD COLUMN is_cross_margin BOOLEAN DEFAULT 1`, // 默认为全仓模式
|
||||
@@ -489,6 +492,7 @@ func (d *Database) 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"},
|
||||
@@ -748,8 +752,10 @@ type ExchangeConfig struct {
|
||||
LighterWalletAddr string `json:"lighterWalletAddr"` // Ethereum 钱包地址 (L1)
|
||||
LighterPrivateKey string `json:"lighterPrivateKey"` // L1私钥(用于识别账户)
|
||||
LighterAPIKeyPrivateKey string `json:"lighterAPIKeyPrivateKey"` // API Key私钥(40字节,用于签名交易)
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// OKX 特定字段
|
||||
OKXPassphrase string `json:"okxPassphrase"` // OKX API Passphrase
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TraderRecord 交易员配置(数据库实体)
|
||||
@@ -1140,7 +1146,16 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
|
||||
COALESCE(aster_private_key, '') as aster_private_key,
|
||||
COALESCE(lighter_wallet_addr, '') as lighter_wallet_addr,
|
||||
COALESCE(lighter_private_key, '') as lighter_private_key,
|
||||
COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key,
|
||||
COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key,
|
||||
COALESCE(okx_passphrase, '') as okx_passphrase,
|
||||
COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key,
|
||||
COALESCE(okx_passphrase, '') as okx_passphrase,
|
||||
COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key,
|
||||
COALESCE(okx_passphrase, '') as okx_passphrase,
|
||||
COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key,
|
||||
COALESCE(okx_passphrase, '') as okx_passphrase,
|
||||
COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key,
|
||||
COALESCE(okx_passphrase, '') as okx_passphrase,
|
||||
created_at, updated_at
|
||||
FROM exchanges WHERE user_id = ? ORDER BY id
|
||||
`, userID)
|
||||
@@ -1161,6 +1176,7 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
|
||||
&exchange.AsterSigner, &exchange.AsterPrivateKey,
|
||||
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey,
|
||||
&exchange.LighterAPIKeyPrivateKey,
|
||||
&exchange.OKXPassphrase,
|
||||
&createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -1177,6 +1193,7 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
|
||||
exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey)
|
||||
exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey)
|
||||
exchange.LighterAPIKeyPrivateKey = d.decryptSensitiveData(exchange.LighterAPIKeyPrivateKey)
|
||||
exchange.OKXPassphrase = d.decryptSensitiveData(exchange.OKXPassphrase)
|
||||
|
||||
exchanges = append(exchanges, &exchange)
|
||||
}
|
||||
@@ -1185,8 +1202,8 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
|
||||
}
|
||||
|
||||
// UpdateExchange 更新交易所配置,如果不存在则创建用户特定配置
|
||||
// 🔒 安全特性:空值不会覆盖现有的敏感字段(api_key, secret_key, aster_private_key, lighter_private_key)
|
||||
func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey string) error {
|
||||
// 🔒 安全特性:空值不会覆盖现有的敏感字段(api_key, secret_key, aster_private_key, lighter_private_key, okx_passphrase)
|
||||
func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey, okxPassphrase string) error {
|
||||
log.Printf("🔧 UpdateExchange: userID=%s, id=%s, enabled=%v", userID, id, enabled)
|
||||
|
||||
// 构建动态 UPDATE SET 子句
|
||||
@@ -1227,6 +1244,12 @@ func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secre
|
||||
args = append(args, encryptedLighterPrivateKey)
|
||||
}
|
||||
|
||||
if okxPassphrase != "" {
|
||||
encryptedOKXPassphrase := d.encryptSensitiveData(okxPassphrase)
|
||||
setClauses = append(setClauses, "okx_passphrase = ?")
|
||||
args = append(args, encryptedOKXPassphrase)
|
||||
}
|
||||
|
||||
// WHERE 条件
|
||||
args = append(args, id, userID)
|
||||
|
||||
@@ -1267,6 +1290,9 @@ func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secre
|
||||
} else if id == "hyperliquid" {
|
||||
name = "Hyperliquid"
|
||||
typ = "dex"
|
||||
} else if id == "okx" {
|
||||
name = "OKX Futures"
|
||||
typ = "cex"
|
||||
} else if id == "aster" {
|
||||
name = "Aster DEX"
|
||||
typ = "dex"
|
||||
@@ -1285,14 +1311,15 @@ func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secre
|
||||
encryptedSecretKey := d.encryptSensitiveData(secretKey)
|
||||
encryptedAsterPrivateKey := d.encryptSensitiveData(asterPrivateKey)
|
||||
encryptedLighterPrivateKey := d.encryptSensitiveData(lighterPrivateKey)
|
||||
encryptedOKXPassphrase := d.encryptSensitiveData(okxPassphrase)
|
||||
|
||||
// 创建用户特定的配置,使用原始的交易所ID
|
||||
_, err = d.db.Exec(`
|
||||
INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, 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, encryptedAPIKey, encryptedSecretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, encryptedAsterPrivateKey, lighterWalletAddr, encryptedLighterPrivateKey)
|
||||
lighter_wallet_addr, lighter_private_key, okx_passphrase, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||||
`, id, userID, name, typ, enabled, encryptedAPIKey, encryptedSecretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, encryptedAsterPrivateKey, lighterWalletAddr, encryptedLighterPrivateKey, encryptedOKXPassphrase)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("❌ UpdateExchange: 创建记录失败: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user