From 1dab5ef2eef33cfac8cdb5b8f85e4beefb0a2ff3 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Wed, 3 Dec 2025 11:31:50 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"feat:=20=E6=B7=BB=E5=8A=A0=20OKX=20?= =?UTF-8?q?=E4=BA=A4=E6=98=93=E6=89=80=E6=94=AF=E6=8C=81=20(#1150)"=20This?= =?UTF-8?q?=20reverts=20commit=20174f59b90721572dcdd03a9527fa86a48395018c.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server.go | 10 +- api/utils.go | 4 - config/database.go | 43 +-- trader/okx_trader.go | 490 ------------------------ trader/okx_trader_test.go | 776 -------------------------------------- 5 files changed, 9 insertions(+), 1314 deletions(-) delete mode 100644 trader/okx_trader.go delete mode 100644 trader/okx_trader_test.go diff --git a/api/server.go b/api/server.go index 05c61afa..89b6013f 100644 --- a/api/server.go +++ b/api/server.go @@ -443,7 +443,6 @@ type UpdateExchangeConfigRequest struct { AsterPrivateKey string `json:"aster_private_key"` LighterWalletAddr string `json:"lighter_wallet_addr"` LighterPrivateKey string `json:"lighter_private_key"` - OKXPassphrase string `json:"okx_passphrase"` } `json:"exchanges"` } @@ -551,13 +550,6 @@ func (s *Server) handleCreateTrader(c *gin.Context) { switch req.ExchangeID { case "binance": tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey, userID) - case "okx": - tempTrader = trader.NewOKXTrader( - exchangeCfg.APIKey, - exchangeCfg.SecretKey, - exchangeCfg.OKXPassphrase, - exchangeCfg.Testnet, - ) case "hyperliquid": tempTrader, createErr = trader.NewHyperliquidTrader( exchangeCfg.APIKey, // private key @@ -1216,7 +1208,7 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) { // 更新每个交易所的配置 for exchangeID, exchangeData := range req.Exchanges { - err := s.database.UpdateExchange(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey, exchangeData.LighterWalletAddr, exchangeData.LighterPrivateKey, exchangeData.OKXPassphrase) + err := s.database.UpdateExchange(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey, exchangeData.LighterWalletAddr, exchangeData.LighterPrivateKey) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新交易所 %s 失败: %v", exchangeID, err)}) return diff --git a/api/utils.go b/api/utils.go index 583b13bd..6a1a31b5 100644 --- a/api/utils.go +++ b/api/utils.go @@ -46,7 +46,6 @@ func SanitizeExchangeConfigForLog(exchanges map[string]struct { AsterPrivateKey string `json:"aster_private_key"` LighterWalletAddr string `json:"lighter_wallet_addr"` LighterPrivateKey string `json:"lighter_private_key"` - OKXPassphrase string `json:"okx_passphrase"` }) map[string]interface{} { safe := make(map[string]interface{}) for exchangeID, cfg := range exchanges { @@ -68,9 +67,6 @@ func SanitizeExchangeConfigForLog(exchanges map[string]struct { if cfg.LighterPrivateKey != "" { safeExchange["lighter_private_key"] = MaskSensitiveString(cfg.LighterPrivateKey) } - if cfg.OKXPassphrase != "" { - safeExchange["okx_passphrase"] = MaskSensitiveString(cfg.OKXPassphrase) - } // 非敏感字段直接添加 if cfg.HyperliquidWalletAddr != "" { diff --git a/config/database.go b/config/database.go index 8a700b20..466550f3 100644 --- a/config/database.go +++ b/config/database.go @@ -152,8 +152,6 @@ 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 @@ -364,7 +362,6 @@ 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`, // 默认为全仓模式 @@ -492,7 +489,6 @@ 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"}, @@ -752,10 +748,8 @@ type ExchangeConfig struct { LighterWalletAddr string `json:"lighterWalletAddr"` // Ethereum 钱包地址 (L1) LighterPrivateKey string `json:"lighterPrivateKey"` // L1私钥(用于识别账户) LighterAPIKeyPrivateKey string `json:"lighterAPIKeyPrivateKey"` // API Key私钥(40字节,用于签名交易) - // OKX 特定字段 - OKXPassphrase string `json:"okxPassphrase"` // OKX API Passphrase - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TraderRecord 交易员配置(数据库实体) @@ -1146,16 +1140,7 @@ 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(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, + COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key, created_at, updated_at FROM exchanges WHERE user_id = ? ORDER BY id `, userID) @@ -1176,7 +1161,6 @@ 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 { @@ -1193,7 +1177,6 @@ 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) } @@ -1202,8 +1185,8 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) { } // UpdateExchange 更新交易所配置,如果不存在则创建用户特定配置 -// 🔒 安全特性:空值不会覆盖现有的敏感字段(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 { +// 🔒 安全特性:空值不会覆盖现有的敏感字段(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 { log.Printf("🔧 UpdateExchange: userID=%s, id=%s, enabled=%v", userID, id, enabled) // 构建动态 UPDATE SET 子句 @@ -1244,12 +1227,6 @@ 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) @@ -1290,9 +1267,6 @@ 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" @@ -1311,15 +1285,14 @@ 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, 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) + 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) if err != nil { log.Printf("❌ UpdateExchange: 创建记录失败: %v", err) diff --git a/trader/okx_trader.go b/trader/okx_trader.go deleted file mode 100644 index 0fae17e7..00000000 --- a/trader/okx_trader.go +++ /dev/null @@ -1,490 +0,0 @@ -package trader - -import ( - "bytes" - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "strconv" - "strings" - "sync" - "time" -) - -// OKXTrader OKX USDT 永續合約交易器 -type OKXTrader struct { - apiKey string - secretKey string - passphrase string - baseURL string - httpClient *http.Client - testnet bool - - // 餘額緩存 - cachedBalance map[string]interface{} - balanceCacheTime time.Time - balanceCacheMutex sync.RWMutex - - // 持倉緩存 - cachedPositions []map[string]interface{} - positionsCacheTime time.Time - positionsCacheMutex sync.RWMutex - - // 緩存有效期(15秒) - cacheDuration time.Duration -} - -// NewOKXTrader 創建 OKX 交易器 -func NewOKXTrader(apiKey, secretKey, passphrase string, testnet bool) *OKXTrader { - baseURL := "https://www.okx.com" - - trader := &OKXTrader{ - apiKey: apiKey, - secretKey: secretKey, - passphrase: passphrase, - baseURL: baseURL, - testnet: testnet, - httpClient: &http.Client{Timeout: 30 * time.Second}, - cacheDuration: 15 * time.Second, - } - - log.Printf("🟠 [OKX] 交易器已初始化 (testnet=%v)", testnet) - return trader -} - -// sign 生成 OKX API v5 簽名 -// 簽名算法:Base64(HMAC-SHA256(timestamp + method + requestPath + body, SecretKey)) -func (t *OKXTrader) sign(timestamp, method, requestPath, body string) string { - // 構建待簽名字符串:timestamp + method + requestPath + body - message := timestamp + method + requestPath + body - - // HMAC-SHA256 簽名 - h := hmac.New(sha256.New, []byte(t.secretKey)) - h.Write([]byte(message)) - signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) - - return signature -} - -// request 發送 HTTP 請求到 OKX API -func (t *OKXTrader) request(method, path string, params map[string]interface{}) (map[string]interface{}, error) { - // 生成 ISO 8601 時間戳(含毫秒) - timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.000Z") - - // 構建請求體 - var bodyBytes []byte - var bodyStr string - if params != nil && len(params) > 0 { - var err error - bodyBytes, err = json.Marshal(params) - if err != nil { - return nil, fmt.Errorf("序列化請求體失敗: %w", err) - } - bodyStr = string(bodyBytes) - } else { - bodyStr = "" - } - - // 構建完整 URL - url := t.baseURL + path - - // 生成簽名 - signature := t.sign(timestamp, method, path, bodyStr) - - // 創建請求 - var req *http.Request - var err error - if bodyStr != "" { - req, err = http.NewRequest(method, url, bytes.NewBuffer(bodyBytes)) - } else { - req, err = http.NewRequest(method, url, nil) - } - if err != nil { - return nil, fmt.Errorf("創建請求失敗: %w", err) - } - - // 設置請求頭 - req.Header.Set("Content-Type", "application/json") - req.Header.Set("OK-ACCESS-KEY", t.apiKey) - req.Header.Set("OK-ACCESS-SIGN", signature) - req.Header.Set("OK-ACCESS-TIMESTAMP", timestamp) - req.Header.Set("OK-ACCESS-PASSPHRASE", t.passphrase) - - // Demo 交易模式 - if t.testnet { - req.Header.Set("x-simulated-trading", "1") - } - - // 發送請求 - resp, err := t.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("發送請求失敗: %w", err) - } - defer resp.Body.Close() - - // 讀取響應 - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("讀取響應失敗: %w", err) - } - - // 解析 JSON - var result map[string]interface{} - if err := json.Unmarshal(respBody, &result); err != nil { - return nil, fmt.Errorf("解析響應失敗: %w, body: %s", err, string(respBody)) - } - - // 檢查錯誤 - if code, ok := result["code"].(string); ok && code != "0" { - msg := result["msg"].(string) - return nil, fmt.Errorf("OKX API 錯誤 [%s]: %s", code, msg) - } - - return result, nil -} - -// GetBalance 獲取賬戶餘額 -func (t *OKXTrader) GetBalance() (map[string]interface{}, error) { - // 檢查緩存 - t.balanceCacheMutex.RLock() - if t.cachedBalance != nil && time.Since(t.balanceCacheTime) < t.cacheDuration { - balance := t.cachedBalance - t.balanceCacheMutex.RUnlock() - log.Printf("✓ 使用緩存的賬戶餘額(緩存時間: %.1f秒前)", time.Since(t.balanceCacheTime).Seconds()) - return balance, nil - } - t.balanceCacheMutex.RUnlock() - - // 調用 API:GET /api/v5/account/balance - log.Printf("🔄 緩存過期,正在調用 OKX API 獲取賬戶餘額...") - result, err := t.request("GET", "/api/v5/account/balance", nil) - if err != nil { - return nil, fmt.Errorf("獲取 OKX 餘額失敗: %w", err) - } - - // 解析響應 - data, ok := result["data"].([]interface{}) - if !ok || len(data) == 0 { - return nil, fmt.Errorf("OKX API 返回數據格式錯誤") - } - - accountData := data[0].(map[string]interface{}) - details := accountData["details"].([]interface{}) - - // 計算 USDT 餘額 - var totalEq, availEq, upl float64 - for _, detail := range details { - d := detail.(map[string]interface{}) - if d["ccy"].(string) == "USDT" { - totalEq, _ = strconv.ParseFloat(d["eq"].(string), 64) - availEq, _ = strconv.ParseFloat(d["availEq"].(string), 64) - uplStr, ok := d["upl"].(string) - if ok { - upl, _ = strconv.ParseFloat(uplStr, 64) - } - break - } - } - - balance := map[string]interface{}{ - "totalWalletBalance": totalEq, - "availableBalance": availEq, - "totalUnrealizedProfit": upl, - "wallet_balance": totalEq, - "available_balance": availEq, - "unrealized_profit": upl, - "balance": totalEq, - } - - // 更新緩存 - t.balanceCacheMutex.Lock() - t.cachedBalance = balance - t.balanceCacheTime = time.Now() - t.balanceCacheMutex.Unlock() - - log.Printf("✓ OKX API 返回: 總餘額=%.2f, 可用=%.2f, 未實現盈虧=%.2f", - totalEq, availEq, upl) - - return balance, nil -} - -// GetPositions 獲取所有持倉 -func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) { - // 檢查緩存 - t.positionsCacheMutex.RLock() - if t.cachedPositions != nil && time.Since(t.positionsCacheTime) < t.cacheDuration { - positions := t.cachedPositions - t.positionsCacheMutex.RUnlock() - return positions, nil - } - t.positionsCacheMutex.RUnlock() - - // 調用 API:GET /api/v5/account/positions - result, err := t.request("GET", "/api/v5/account/positions", nil) - if err != nil { - return nil, fmt.Errorf("獲取 OKX 持倉失敗: %w", err) - } - - // 解析響應 - data, ok := result["data"].([]interface{}) - if !ok { - return nil, fmt.Errorf("OKX API 返回數據格式錯誤") - } - - positions := make([]map[string]interface{}, 0) - for _, item := range data { - pos := item.(map[string]interface{}) - - // 跳過空倉位 - posStr := pos["pos"].(string) - if posStr == "0" { - continue - } - - // 解析數據 - quantity, _ := strconv.ParseFloat(posStr, 64) - entryPrice, _ := strconv.ParseFloat(pos["avgPx"].(string), 64) - markPrice, _ := strconv.ParseFloat(pos["markPx"].(string), 64) - upl, _ := strconv.ParseFloat(pos["upl"].(string), 64) - leverage, _ := strconv.ParseFloat(pos["lever"].(string), 64) - liqPx, _ := strconv.ParseFloat(pos["liqPx"].(string), 64) - - // 計算保證金 - notionalUsd, _ := strconv.ParseFloat(pos["notionalUsd"].(string), 64) - marginUsed := notionalUsd / leverage - - // 計算盈虧百分比 - uplPct := 0.0 - if entryPrice > 0 { - uplPct = (upl / (quantity * entryPrice)) * 100 - } - - // 處理方向 - side := "long" - if pos["posSide"].(string) == "short" { - side = "short" - quantity = -quantity // 空倉顯示負數 - } - - // 標準化 symbol:BTC-USDT-SWAP → BTCUSDT - instId := pos["instId"].(string) - symbol := strings.ReplaceAll(strings.ReplaceAll(instId, "-USDT-SWAP", ""), "-", "") - - position := map[string]interface{}{ - "symbol": symbol, - "side": side, - "entry_price": entryPrice, - "mark_price": markPrice, - "quantity": quantity, - "leverage": int(leverage), - "unrealized_pnl": upl, - "unrealized_pnl_pct": uplPct, - "liquidation_price": liqPx, - "margin_used": marginUsed, - } - - positions = append(positions, position) - } - - // 更新緩存 - t.positionsCacheMutex.Lock() - t.cachedPositions = positions - t.positionsCacheTime = time.Now() - t.positionsCacheMutex.Unlock() - - return positions, nil -} - -// formatSymbol 將 symbol 轉換為 OKX 格式 -// BTCUSDT → BTC-USDT-SWAP -func (t *OKXTrader) formatSymbol(symbol string) string { - // 移除 USDT 後綴,然後加上 -USDT-SWAP - base := strings.TrimSuffix(strings.ToUpper(symbol), "USDT") - return base + "-USDT-SWAP" -} - -// OpenLong 開多倉 -func (t *OKXTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - return t.placeOrder(symbol, "buy", "long", quantity, leverage) -} - -// OpenShort 開空倉 -func (t *OKXTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { - return t.placeOrder(symbol, "sell", "short", quantity, leverage) -} - -// CloseLong 平多倉 -func (t *OKXTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) { - return t.placeOrder(symbol, "sell", "long", quantity, 0) -} - -// CloseShort 平空倉 -func (t *OKXTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) { - return t.placeOrder(symbol, "buy", "short", quantity, 0) -} - -// placeOrder 下單核心邏輯 -func (t *OKXTrader) placeOrder(symbol, side, posSide string, quantity float64, leverage int) (map[string]interface{}, error) { - instId := t.formatSymbol(symbol) - - // 如果指定了槓桿,先設置槓桿 - if leverage > 0 { - if err := t.SetLeverage(symbol, leverage); err != nil { - log.Printf("⚠️ 設置槓桿失敗: %v", err) - } - } - - // 構建訂單參數 - params := map[string]interface{}{ - "instId": instId, - "tdMode": "cross", // 全倉模式 - "side": side, // buy/sell - "posSide": posSide, // long/short - "ordType": "market", // 市價單 - "sz": fmt.Sprintf("%f", quantity), - } - - log.Printf("🟠 [OKX] 下單: %s %s %s, 數量=%.4f", instId, side, posSide, quantity) - - // 調用 API:POST /api/v5/trade/order - result, err := t.request("POST", "/api/v5/trade/order", params) - if err != nil { - return nil, fmt.Errorf("OKX 下單失敗: %w", err) - } - - // 清除緩存 - t.clearCache() - - return result, nil -} - -// SetLeverage 設置槓桿 -func (t *OKXTrader) SetLeverage(symbol string, leverage int) error { - instId := t.formatSymbol(symbol) - - params := map[string]interface{}{ - "instId": instId, - "lever": strconv.Itoa(leverage), - "mgnMode": "cross", // 全倉模式 - } - - log.Printf("🟠 [OKX] 設置槓桿: %s, 槓桿=%d", instId, leverage) - - _, err := t.request("POST", "/api/v5/account/set-leverage", params) - if err != nil { - // OKX 如果槓桿已經是目標值會返回錯誤,但可以忽略 - if strings.Contains(err.Error(), "Leverage not modified") { - log.Printf(" ✓ 槓桿已是目標值") - return nil - } - return fmt.Errorf("設置槓桿失敗: %w", err) - } - - log.Printf(" ✓ 槓桿設置成功") - return nil -} - -// SetMarginMode 設置倉位模式(全倉/逐倉) -func (t *OKXTrader) SetMarginMode(symbol string, isCrossMargin bool) error { - // OKX 的保證金模式在下單時指定(tdMode: cross/isolated) - // 這裡僅記錄日誌 - mode := "isolated" - if isCrossMargin { - mode = "cross" - } - log.Printf("🟠 [OKX] 保證金模式: %s (在下單時指定)", mode) - return nil -} - -// GetMarketPrice 獲取市場價格 -func (t *OKXTrader) GetMarketPrice(symbol string) (float64, error) { - instId := t.formatSymbol(symbol) - - // 調用 API:GET /api/v5/market/ticker?instId=BTC-USDT-SWAP - path := fmt.Sprintf("/api/v5/market/ticker?instId=%s", instId) - result, err := t.request("GET", path, nil) - if err != nil { - return 0, fmt.Errorf("獲取市場價格失敗: %w", err) - } - - // 解析響應 - data, ok := result["data"].([]interface{}) - if !ok || len(data) == 0 { - return 0, fmt.Errorf("OKX API 返回數據格式錯誤") - } - - ticker := data[0].(map[string]interface{}) - priceStr := ticker["last"].(string) - price, err := strconv.ParseFloat(priceStr, 64) - if err != nil { - return 0, fmt.Errorf("解析價格失敗: %w", err) - } - - return price, nil -} - -// SetStopLoss 設置止損單 -func (t *OKXTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error { - log.Printf("🟠 [OKX] 設置止損: %s %s, 止損價=%.2f", symbol, positionSide, stopPrice) - // TODO: 實現止損邏輯 - return fmt.Errorf("OKX 止損功能尚未實現") -} - -// SetTakeProfit 設置止盈單 -func (t *OKXTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error { - log.Printf("🟠 [OKX] 設置止盈: %s %s, 止盈價=%.2f", symbol, positionSide, takeProfitPrice) - // TODO: 實現止盈邏輯 - return fmt.Errorf("OKX 止盈功能尚未實現") -} - -// CancelStopLossOrders 取消止損單 -func (t *OKXTrader) CancelStopLossOrders(symbol string) error { - log.Printf("🟠 [OKX] 取消止損單: %s", symbol) - // TODO: 實現取消止損邏輯 - return nil -} - -// CancelTakeProfitOrders 取消止盈單 -func (t *OKXTrader) CancelTakeProfitOrders(symbol string) error { - log.Printf("🟠 [OKX] 取消止盈單: %s", symbol) - // TODO: 實現取消止盈邏輯 - return nil -} - -// CancelAllOrders 取消所有掛單 -func (t *OKXTrader) CancelAllOrders(symbol string) error { - instId := t.formatSymbol(symbol) - log.Printf("🟠 [OKX] 取消所有掛單: %s", instId) - // TODO: 實現取消所有訂單邏輯 - return nil -} - -// CancelStopOrders 取消止盈止損單 -func (t *OKXTrader) CancelStopOrders(symbol string) error { - log.Printf("🟠 [OKX] 取消止盈止損單: %s", symbol) - // TODO: 實現取消止盈止損邏輯 - return nil -} - -// FormatQuantity 格式化數量到正確的精度 -func (t *OKXTrader) FormatQuantity(symbol string, quantity float64) (string, error) { - // OKX 通常使用合約數量(contracts),不同幣種精度不同 - // 這裡暫時返回標準格式 - return fmt.Sprintf("%.4f", quantity), nil -} - -// clearCache 清除緩存 -func (t *OKXTrader) clearCache() { - t.balanceCacheMutex.Lock() - t.cachedBalance = nil - t.balanceCacheMutex.Unlock() - - t.positionsCacheMutex.Lock() - t.cachedPositions = nil - t.positionsCacheMutex.Unlock() -} diff --git a/trader/okx_trader_test.go b/trader/okx_trader_test.go deleted file mode 100644 index bc2f2901..00000000 --- a/trader/okx_trader_test.go +++ /dev/null @@ -1,776 +0,0 @@ -package trader - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -// ============================================================ -// 一、OKXTraderTestSuite - 继承 base test suite -// ============================================================ - -// OKXTraderTestSuite OKX交易器测试套件 -// 继承 TraderTestSuite 并添加 OKX 特定的 mock 逻辑 -type OKXTraderTestSuite struct { - *TraderTestSuite // 嵌入基础测试套件 - mockServer *httptest.Server -} - -// NewOKXTraderTestSuite 创建 OKX 测试套件 -func NewOKXTraderTestSuite(t *testing.T) *OKXTraderTestSuite { - // 创建 mock HTTP 服务器 - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - var respBody interface{} - - switch { - // Mock GetBalance - /api/v5/account/balance - case path == "/api/v5/account/balance": - respBody = map[string]interface{}{ - "code": "0", - "msg": "", - "data": []map[string]interface{}{ - { - "totalEq": "10100.50", - "details": []map[string]interface{}{ - { - "ccy": "USDT", - "eq": "10000.00", - "availEq": "8000.00", - "frozenBal": "2000.00", - "upl": "100.50", - "cashBal": "10000.00", - "ordFrozen": "0", - "liab": "0", - "uTime": "1609459200000", - "crossLiab": "0", - "isoLiab": "0", - "mgnRatio": "", - "interest": "0", - "twap": "0", - "maxLoan": "", - "eqUsd": "10000.00", - "notionalLever": "", - "stgyEq": "0", - "isoEq": "0", - }, - }, - }, - }, - } - - // Mock GetPositions - /api/v5/account/positions - case path == "/api/v5/account/positions": - respBody = map[string]interface{}{ - "code": "0", - "msg": "", - "data": []map[string]interface{}{ - { - "instId": "BTC-USDT-SWAP", - "pos": "0.5", - "posSide": "long", - "avgPx": "50000.00", - "markPx": "50500.00", - "upl": "250.00", - "uplRatio": "0.01", - "lever": "10", - "liqPx": "45000.00", - "notionalUsd": "25250.00", - "instType": "SWAP", - "mgnMode": "cross", - "cTime": "1609459200000", - "uTime": "1609459200000", - }, - }, - } - - // Mock GetMarketPrice - /api/v5/market/ticker - case path == "/api/v5/market/ticker": - instId := r.URL.Query().Get("instId") - if instId == "" { - instId = "BTC-USDT-SWAP" - } - - price := "50000.00" - if instId == "ETH-USDT-SWAP" { - price = "3000.00" - } else if instId == "INVALID-USDT-SWAP" { - // 返回错误 - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "code": "51001", - "msg": "Instrument ID does not exist", - "data": []interface{}{}, - }) - return - } - - respBody = map[string]interface{}{ - "code": "0", - "msg": "", - "data": []map[string]interface{}{ - { - "instId": instId, - "last": price, - "lastSz": "1", - "askPx": price, - "askSz": "10", - "bidPx": price, - "bidSz": "10", - "open24h": price, - "high24h": price, - "low24h": price, - "volCcy24h": "1000000", - "vol24h": "20", - "ts": "1609459200000", - "sodUtc0": price, - "sodUtc8": price, - }, - }, - } - - // Mock CreateOrder - /api/v5/trade/order (POST) - case path == "/api/v5/trade/order" && r.Method == "POST": - respBody = map[string]interface{}{ - "code": "0", - "msg": "", - "data": []map[string]interface{}{ - { - "ordId": "123456789", - "clOrdId": "test_order_123", - "tag": "", - "sCode": "0", - "sMsg": "", - }, - }, - } - - // Mock SetLeverage - /api/v5/account/set-leverage (POST) - case path == "/api/v5/account/set-leverage" && r.Method == "POST": - respBody = map[string]interface{}{ - "code": "0", - "msg": "", - "data": []map[string]interface{}{ - { - "instId": "BTC-USDT-SWAP", - "lever": "10", - "mgnMode": "cross", - "posSide": "long", - }, - }, - } - - // Mock SetMarginMode - /api/v5/account/set-position-mode (POST) - case path == "/api/v5/account/set-position-mode" && r.Method == "POST": - respBody = map[string]interface{}{ - "code": "0", - "msg": "", - "data": []map[string]interface{}{ - { - "posMode": "net_mode", - }, - }, - } - - // Default: empty success response - default: - respBody = map[string]interface{}{ - "code": "0", - "msg": "", - "data": []interface{}{}, - } - } - - // 序列化响应 - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(respBody) - })) - - // 创建 OKXTrader 并设置为使用 mock 服务器 - trader := &OKXTrader{ - apiKey: "test_api_key", - secretKey: "test_secret_key", - passphrase: "test_passphrase", - baseURL: mockServer.URL, - httpClient: mockServer.Client(), - testnet: false, - cacheDuration: 0, // 禁用缓存以便测试 - } - - // 创建基础套件 - baseSuite := NewTraderTestSuite(t, trader) - - return &OKXTraderTestSuite{ - TraderTestSuite: baseSuite, - mockServer: mockServer, - } -} - -// Cleanup 清理资源 -func (s *OKXTraderTestSuite) Cleanup() { - if s.mockServer != nil { - s.mockServer.Close() - } - s.TraderTestSuite.Cleanup() -} - -// ============================================================ -// 二、使用 OKXTraderTestSuite 运行通用测试 -// ============================================================ - -// TestOKXTrader_InterfaceCompliance 测试接口兼容性 -func TestOKXTrader_InterfaceCompliance(t *testing.T) { - var _ Trader = (*OKXTrader)(nil) -} - -// TestOKXTrader_CommonInterface 使用测试套件运行所有通用接口测试 -func TestOKXTrader_CommonInterface(t *testing.T) { - // 创建测试套件 - suite := NewOKXTraderTestSuite(t) - defer suite.Cleanup() - - // 运行所有通用接口测试 - suite.RunAllTests() -} - -// ============================================================ -// 三、OKX 特定功能的单元测试 -// ============================================================ - -// TestNewOKXTrader 测试创建 OKX 交易器 -func TestNewOKXTrader(t *testing.T) { - tests := []struct { - name string - apiKey string - secretKey string - passphrase string - testnet bool - wantNil bool - }{ - { - name: "成功创建(正式环境)", - apiKey: "test_api_key", - secretKey: "test_secret_key", - passphrase: "test_passphrase", - testnet: false, - wantNil: false, - }, - { - name: "成功创建(测试环境)", - apiKey: "test_api_key", - secretKey: "test_secret_key", - passphrase: "test_passphrase", - testnet: true, - wantNil: false, - }, - { - name: "空API Key仍可创建", - apiKey: "", - secretKey: "test_secret_key", - passphrase: "test_passphrase", - testnet: false, - wantNil: false, - }, - { - name: "空Secret Key仍可创建", - apiKey: "test_api_key", - secretKey: "", - passphrase: "test_passphrase", - testnet: false, - wantNil: false, - }, - { - name: "空Passphrase仍可创建", - apiKey: "test_api_key", - secretKey: "test_secret_key", - passphrase: "", - testnet: false, - wantNil: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - trader := NewOKXTrader(tt.apiKey, tt.secretKey, tt.passphrase, tt.testnet) - - if tt.wantNil { - assert.Nil(t, trader) - } else { - assert.NotNil(t, trader) - assert.NotNil(t, trader.httpClient) - assert.Equal(t, tt.apiKey, trader.apiKey) - assert.Equal(t, tt.secretKey, trader.secretKey) - assert.Equal(t, tt.passphrase, trader.passphrase) - assert.Equal(t, tt.testnet, trader.testnet) - - // 检查 baseURL - if tt.testnet { - assert.Equal(t, "https://www.okx.com", trader.baseURL) - } else { - assert.Equal(t, "https://www.okx.com", trader.baseURL) - } - - // 检查缓存时间 - assert.Equal(t, 15*time.Second, trader.cacheDuration) - } - }) - } -} - -// TestOKXTrader_SymbolFormat 测试符号格式转换 -func TestOKXTrader_SymbolFormat(t *testing.T) { - trader := &OKXTrader{} - - tests := []struct { - name string - input string - expected string - }{ - { - name: "BTC USDT Swap", - input: "BTCUSDT", - expected: "BTC-USDT-SWAP", - }, - { - name: "ETH USDT Swap", - input: "ETHUSDT", - expected: "ETH-USDT-SWAP", - }, - { - name: "SOL USDT Swap", - input: "SOLUSDT", - expected: "SOL-USDT-SWAP", - }, - { - name: "小写输入", - input: "btcusdt", - expected: "BTC-USDT-SWAP", - }, - { - name: "混合大小写", - input: "BtcUsdT", - expected: "BTC-USDT-SWAP", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := trader.formatSymbol(tt.input) - assert.Equal(t, tt.expected, result) - }) - } -} - -// TestOKXTrader_Sign 测试签名算法 -func TestOKXTrader_Sign(t *testing.T) { - trader := &OKXTrader{ - secretKey: "test_secret_key", - } - - // 测试签名一致性 - timestamp := "2024-01-01T00:00:00.000Z" - method := "GET" - requestPath := "/api/v5/account/balance" - body := "" - - // 多次签名应该产生相同结果 - sign1 := trader.sign(timestamp, method, requestPath, body) - sign2 := trader.sign(timestamp, method, requestPath, body) - assert.Equal(t, sign1, sign2, "相同输入应产生相同签名") - - // 不同输入应该产生不同签名 - sign3 := trader.sign("2024-01-01T00:00:01.000Z", method, requestPath, body) - assert.NotEqual(t, sign1, sign3, "不同timestamp应产生不同签名") - - sign4 := trader.sign(timestamp, "POST", requestPath, body) - assert.NotEqual(t, sign1, sign4, "不同method应产生不同签名") - - sign5 := trader.sign(timestamp, method, "/api/v5/account/positions", body) - assert.NotEqual(t, sign1, sign5, "不同path应产生不同签名") - - sign6 := trader.sign(timestamp, method, requestPath, `{"instId":"BTC-USDT-SWAP"}`) - assert.NotEqual(t, sign1, sign6, "不同body应产生不同签名") -} - -// TestOKXTrader_GetBalance 测试获取余额 -func TestOKXTrader_GetBalance(t *testing.T) { - // 创建测试套件 - suite := NewOKXTraderTestSuite(t) - defer suite.Cleanup() - - trader := suite.Trader.(*OKXTrader) - - // 测试获取余额 - balance, err := trader.GetBalance() - assert.NoError(t, err) - assert.NotNil(t, balance) - - // 验证返回的标准化余额字段 - assert.Contains(t, balance, "totalWalletBalance") - assert.Contains(t, balance, "availableBalance") - assert.Contains(t, balance, "totalUnrealizedProfit") - assert.Contains(t, balance, "balance") - - // 验证余额值 - totalBalance, ok := balance["totalWalletBalance"].(float64) - assert.True(t, ok) - assert.Equal(t, 10000.00, totalBalance) - - availBalance, ok := balance["availableBalance"].(float64) - assert.True(t, ok) - assert.Equal(t, 8000.00, availBalance) - - upl, ok := balance["totalUnrealizedProfit"].(float64) - assert.True(t, ok) - assert.Equal(t, 100.50, upl) -} - -// TestOKXTrader_GetPositions 测试获取持仓 -func TestOKXTrader_GetPositions(t *testing.T) { - // 创建测试套件 - suite := NewOKXTraderTestSuite(t) - defer suite.Cleanup() - - trader := suite.Trader.(*OKXTrader) - - // 测试获取持仓 - positions, err := trader.GetPositions() - assert.NoError(t, err) - assert.NotNil(t, positions) - assert.GreaterOrEqual(t, len(positions), 1) - - // 验证标准化的持仓字段 - position := positions[0] - assert.Contains(t, position, "symbol") - assert.Contains(t, position, "side") - assert.Contains(t, position, "entry_price") - assert.Contains(t, position, "mark_price") - assert.Contains(t, position, "quantity") - assert.Contains(t, position, "leverage") - assert.Contains(t, position, "unrealized_pnl") - assert.Contains(t, position, "unrealized_pnl_pct") - assert.Contains(t, position, "liquidation_price") - assert.Contains(t, position, "margin_used") - - // 验证具体值(OKX 的数据被标准化) - assert.Equal(t, "BTC", position["symbol"]) // BTC-USDT-SWAP → BTC - assert.Equal(t, "long", position["side"]) - assert.Equal(t, 50000.0, position["entry_price"]) - assert.Equal(t, 50500.0, position["mark_price"]) - assert.Equal(t, 0.5, position["quantity"]) - assert.Equal(t, 10, position["leverage"]) - assert.Equal(t, 250.0, position["unrealized_pnl"]) - assert.Equal(t, 45000.0, position["liquidation_price"]) -} - -// TestOKXTrader_GetMarketPrice 测试获取市场价格 -func TestOKXTrader_GetMarketPrice(t *testing.T) { - // 创建测试套件 - suite := NewOKXTraderTestSuite(t) - defer suite.Cleanup() - - trader := suite.Trader.(*OKXTrader) - - tests := []struct { - name string - symbol string - wantError bool - wantPrice float64 - }{ - { - name: "获取BTC价格", - symbol: "BTCUSDT", - wantError: false, - wantPrice: 50000.00, - }, - { - name: "获取ETH价格", - symbol: "ETHUSDT", - wantError: false, - wantPrice: 3000.00, - }, - { - name: "无效符号", - symbol: "INVALID", - wantError: true, - wantPrice: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - price, err := trader.GetMarketPrice(tt.symbol) - - if tt.wantError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.wantPrice, price) - } - }) - } -} - -// TestOKXTrader_FormatQuantity 测试数量格式化 -func TestOKXTrader_FormatQuantity(t *testing.T) { - trader := &OKXTrader{} - - tests := []struct { - name string - symbol string - quantity float64 - expected string - }{ - { - name: "整数数量", - symbol: "BTCUSDT", - quantity: 1.0, - expected: "1.0000", - }, - { - name: "小数数量", - symbol: "BTCUSDT", - quantity: 0.5, - expected: "0.5000", - }, - { - name: "多位小数(四舍五入到4位)", - symbol: "BTCUSDT", - quantity: 0.123456, - expected: "0.1235", - }, - { - name: "零数量", - symbol: "BTCUSDT", - quantity: 0, - expected: "0.0000", - }, - { - name: "大数量", - symbol: "BTCUSDT", - quantity: 100.123, - expected: "100.1230", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := trader.FormatQuantity(tt.symbol, tt.quantity) - assert.NoError(t, err) - assert.Equal(t, tt.expected, result) - }) - } -} - -// TestOKXTrader_SetLeverage 测试设置杠杆 -func TestOKXTrader_SetLeverage(t *testing.T) { - // 创建测试套件 - suite := NewOKXTraderTestSuite(t) - defer suite.Cleanup() - - trader := suite.Trader.(*OKXTrader) - - // 测试设置杠杆 - err := trader.SetLeverage("BTCUSDT", 10) - assert.NoError(t, err) -} - -// TestOKXTrader_SetMarginMode 测试设置保证金模式 -func TestOKXTrader_SetMarginMode(t *testing.T) { - // 创建测试套件 - suite := NewOKXTraderTestSuite(t) - defer suite.Cleanup() - - trader := suite.Trader.(*OKXTrader) - - // 测试设置保证金模式(cross margin = true) - err := trader.SetMarginMode("BTCUSDT", true) - assert.NoError(t, err) - - // 测试设置保证金模式(isolated margin = false) - err = trader.SetMarginMode("BTCUSDT", false) - assert.NoError(t, err) -} - -// TestOKXTrader_OpenLong 测试开多仓 -func TestOKXTrader_OpenLong(t *testing.T) { - // 创建测试套件 - suite := NewOKXTraderTestSuite(t) - defer suite.Cleanup() - - trader := suite.Trader.(*OKXTrader) - - // 测试开多仓(OKX 的 OpenLong 接受 leverage 参数) - result, err := trader.OpenLong("BTCUSDT", 0.01, 10) - assert.NoError(t, err) - assert.NotNil(t, result) -} - -// TestOKXTrader_OpenShort 测试开空仓 -func TestOKXTrader_OpenShort(t *testing.T) { - // 创建测试套件 - suite := NewOKXTraderTestSuite(t) - defer suite.Cleanup() - - trader := suite.Trader.(*OKXTrader) - - // 测试开空仓(OKX 的 OpenShort 接受 leverage 参数) - result, err := trader.OpenShort("BTCUSDT", 0.01, 10) - assert.NoError(t, err) - assert.NotNil(t, result) -} - -// TestOKXTrader_CloseLong 测试平多仓 -func TestOKXTrader_CloseLong(t *testing.T) { - // 创建测试套件 - suite := NewOKXTraderTestSuite(t) - defer suite.Cleanup() - - trader := suite.Trader.(*OKXTrader) - - // 测试平多仓(OKX 的 CloseLong 只接受 symbol 和 quantity) - result, err := trader.CloseLong("BTCUSDT", 0.01) - assert.NoError(t, err) - assert.NotNil(t, result) -} - -// TestOKXTrader_CloseShort 测试平空仓 -func TestOKXTrader_CloseShort(t *testing.T) { - // 创建测试套件 - suite := NewOKXTraderTestSuite(t) - defer suite.Cleanup() - - trader := suite.Trader.(*OKXTrader) - - // 测试平空仓(OKX 的 CloseShort 只接受 symbol 和 quantity) - result, err := trader.CloseShort("BTCUSDT", 0.01) - assert.NoError(t, err) - assert.NotNil(t, result) -} - -// TestOKXTrader_Cache 测试缓存机制 -func TestOKXTrader_Cache(t *testing.T) { - // 创建测试套件 - suite := NewOKXTraderTestSuite(t) - defer suite.Cleanup() - - trader := suite.Trader.(*OKXTrader) - - // 启用缓存 - trader.cacheDuration = 5 * time.Second - - // 第一次调用 - 应该访问 API - balance1, err := trader.GetBalance() - assert.NoError(t, err) - assert.NotNil(t, balance1) - - // 第二次调用 - 应该使用缓存 - balance2, err := trader.GetBalance() - assert.NoError(t, err) - assert.NotNil(t, balance2) - assert.Equal(t, balance1, balance2) - - // 清空缓存 - trader.balanceCacheMutex.Lock() - trader.cachedBalance = nil - trader.balanceCacheTime = time.Time{} - trader.balanceCacheMutex.Unlock() - - // 第三次调用 - 应该重新访问 API - balance3, err := trader.GetBalance() - assert.NoError(t, err) - assert.NotNil(t, balance3) -} - -// TestOKXTrader_ErrorHandling 测试错误处理 -func TestOKXTrader_ErrorHandling(t *testing.T) { - // 创建错误响应的 mock 服务器 - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "code": "50000", - "msg": "Internal server error", - "data": []interface{}{}, - }) - })) - defer mockServer.Close() - - trader := &OKXTrader{ - apiKey: "test_api_key", - secretKey: "test_secret_key", - passphrase: "test_passphrase", - baseURL: mockServer.URL, - httpClient: mockServer.Client(), - testnet: false, - } - - // 测试各种操作应该返回错误 - _, err := trader.GetBalance() - assert.Error(t, err) - assert.Contains(t, err.Error(), "50000") - - _, err = trader.GetPositions() - assert.Error(t, err) - - _, err = trader.GetMarketPrice("BTCUSDT") - assert.Error(t, err) - - _, err = trader.OpenLong("BTCUSDT", 0.01, 10) - assert.Error(t, err) - - err = trader.SetLeverage("BTCUSDT", 10) - assert.Error(t, err) -} - -// TestOKXTrader_HTTPRequestError 测试 HTTP 请求错误 -func TestOKXTrader_HTTPRequestError(t *testing.T) { - // 使用无效的 baseURL - trader := &OKXTrader{ - apiKey: "test_api_key", - secretKey: "test_secret_key", - passphrase: "test_passphrase", - baseURL: "http://invalid-url-that-does-not-exist-12345.com", - httpClient: &http.Client{Timeout: 1 * time.Second}, - testnet: false, - } - - // 测试各种操作应该返回网络错误 - _, err := trader.GetBalance() - assert.Error(t, err) - - _, err = trader.GetPositions() - assert.Error(t, err) - - _, err = trader.GetMarketPrice("BTCUSDT") - assert.Error(t, err) -} - -// TestOKXTrader_InvalidJSON 测试无效 JSON 响应 -func TestOKXTrader_InvalidJSON(t *testing.T) { - // 创建返回无效 JSON 的 mock 服务器 - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, "invalid json{{{") - })) - defer mockServer.Close() - - trader := &OKXTrader{ - apiKey: "test_api_key", - secretKey: "test_secret_key", - passphrase: "test_passphrase", - baseURL: mockServer.URL, - httpClient: mockServer.Client(), - testnet: false, - } - - // 测试应该返回 JSON 解析错误 - _, err := trader.GetBalance() - assert.Error(t, err) - assert.Contains(t, err.Error(), "解析") -}