Revert "feat: 添加 OKX 交易所支持 (#1150)"

This reverts commit 174f59b907.
This commit is contained in:
tinkle-community
2025-12-03 11:31:50 +08:00
parent 62bce32d1f
commit 1dab5ef2ee
5 changed files with 9 additions and 1314 deletions

View File

@@ -443,7 +443,6 @@ type UpdateExchangeConfigRequest struct {
AsterPrivateKey string `json:"aster_private_key"` AsterPrivateKey string `json:"aster_private_key"`
LighterWalletAddr string `json:"lighter_wallet_addr"` LighterWalletAddr string `json:"lighter_wallet_addr"`
LighterPrivateKey string `json:"lighter_private_key"` LighterPrivateKey string `json:"lighter_private_key"`
OKXPassphrase string `json:"okx_passphrase"`
} `json:"exchanges"` } `json:"exchanges"`
} }
@@ -551,13 +550,6 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
switch req.ExchangeID { switch req.ExchangeID {
case "binance": case "binance":
tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey, userID) tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey, userID)
case "okx":
tempTrader = trader.NewOKXTrader(
exchangeCfg.APIKey,
exchangeCfg.SecretKey,
exchangeCfg.OKXPassphrase,
exchangeCfg.Testnet,
)
case "hyperliquid": case "hyperliquid":
tempTrader, createErr = trader.NewHyperliquidTrader( tempTrader, createErr = trader.NewHyperliquidTrader(
exchangeCfg.APIKey, // private key exchangeCfg.APIKey, // private key
@@ -1216,7 +1208,7 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
// 更新每个交易所的配置 // 更新每个交易所的配置
for exchangeID, exchangeData := range req.Exchanges { 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 { 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

View File

@@ -46,7 +46,6 @@ func SanitizeExchangeConfigForLog(exchanges map[string]struct {
AsterPrivateKey string `json:"aster_private_key"` AsterPrivateKey string `json:"aster_private_key"`
LighterWalletAddr string `json:"lighter_wallet_addr"` LighterWalletAddr string `json:"lighter_wallet_addr"`
LighterPrivateKey string `json:"lighter_private_key"` LighterPrivateKey string `json:"lighter_private_key"`
OKXPassphrase string `json:"okx_passphrase"`
}) map[string]interface{} { }) map[string]interface{} {
safe := make(map[string]interface{}) safe := make(map[string]interface{})
for exchangeID, cfg := range exchanges { for exchangeID, cfg := range exchanges {
@@ -68,9 +67,6 @@ func SanitizeExchangeConfigForLog(exchanges map[string]struct {
if cfg.LighterPrivateKey != "" { if cfg.LighterPrivateKey != "" {
safeExchange["lighter_private_key"] = MaskSensitiveString(cfg.LighterPrivateKey) safeExchange["lighter_private_key"] = MaskSensitiveString(cfg.LighterPrivateKey)
} }
if cfg.OKXPassphrase != "" {
safeExchange["okx_passphrase"] = MaskSensitiveString(cfg.OKXPassphrase)
}
// 非敏感字段直接添加 // 非敏感字段直接添加
if cfg.HyperliquidWalletAddr != "" { if cfg.HyperliquidWalletAddr != "" {

View File

@@ -152,8 +152,6 @@ func (d *Database) createTables() error {
lighter_wallet_addr TEXT DEFAULT '', lighter_wallet_addr TEXT DEFAULT '',
lighter_private_key TEXT DEFAULT '', lighter_private_key TEXT DEFAULT '',
lighter_api_key_private_key TEXT DEFAULT '', lighter_api_key_private_key TEXT DEFAULT '',
-- OKX 特定字段
okx_passphrase TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE 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_wallet_addr TEXT DEFAULT ''`,
`ALTER TABLE exchanges ADD COLUMN lighter_private_key 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 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 custom_prompt TEXT DEFAULT ''`,
`ALTER TABLE traders ADD COLUMN override_base_prompt BOOLEAN DEFAULT 0`, `ALTER TABLE traders ADD COLUMN override_base_prompt BOOLEAN DEFAULT 0`,
`ALTER TABLE traders ADD COLUMN is_cross_margin BOOLEAN DEFAULT 1`, // 默认为全仓模式 `ALTER TABLE traders ADD COLUMN is_cross_margin BOOLEAN DEFAULT 1`, // 默认为全仓模式
@@ -492,7 +489,6 @@ func (d *Database) 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"},
@@ -752,10 +748,8 @@ type ExchangeConfig struct {
LighterWalletAddr string `json:"lighterWalletAddr"` // Ethereum 钱包地址 (L1) LighterWalletAddr string `json:"lighterWalletAddr"` // Ethereum 钱包地址 (L1)
LighterPrivateKey string `json:"lighterPrivateKey"` // L1私钥用于识别账户 LighterPrivateKey string `json:"lighterPrivateKey"` // L1私钥用于识别账户
LighterAPIKeyPrivateKey string `json:"lighterAPIKeyPrivateKey"` // API Key私钥40字节用于签名交易 LighterAPIKeyPrivateKey string `json:"lighterAPIKeyPrivateKey"` // API Key私钥40字节用于签名交易
// OKX 特定字段 CreatedAt time.Time `json:"created_at"`
OKXPassphrase string `json:"okxPassphrase"` // OKX API Passphrase UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
// TraderRecord 交易员配置(数据库实体) // TraderRecord 交易员配置(数据库实体)
@@ -1146,16 +1140,7 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
COALESCE(aster_private_key, '') as aster_private_key, COALESCE(aster_private_key, '') as aster_private_key,
COALESCE(lighter_wallet_addr, '') as lighter_wallet_addr, COALESCE(lighter_wallet_addr, '') as lighter_wallet_addr,
COALESCE(lighter_private_key, '') as lighter_private_key, 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 created_at, updated_at
FROM exchanges WHERE user_id = ? ORDER BY id FROM exchanges WHERE user_id = ? ORDER BY id
`, userID) `, userID)
@@ -1176,7 +1161,6 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
&exchange.AsterSigner, &exchange.AsterPrivateKey, &exchange.AsterSigner, &exchange.AsterPrivateKey,
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey, &exchange.LighterWalletAddr, &exchange.LighterPrivateKey,
&exchange.LighterAPIKeyPrivateKey, &exchange.LighterAPIKeyPrivateKey,
&exchange.OKXPassphrase,
&createdAt, &updatedAt, &createdAt, &updatedAt,
) )
if err != nil { if err != nil {
@@ -1193,7 +1177,6 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey) exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey)
exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey) exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey)
exchange.LighterAPIKeyPrivateKey = d.decryptSensitiveData(exchange.LighterAPIKeyPrivateKey) exchange.LighterAPIKeyPrivateKey = d.decryptSensitiveData(exchange.LighterAPIKeyPrivateKey)
exchange.OKXPassphrase = d.decryptSensitiveData(exchange.OKXPassphrase)
exchanges = append(exchanges, &exchange) exchanges = append(exchanges, &exchange)
} }
@@ -1202,8 +1185,8 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
} }
// UpdateExchange 更新交易所配置,如果不存在则创建用户特定配置 // UpdateExchange 更新交易所配置,如果不存在则创建用户特定配置
// 🔒 安全特性空值不会覆盖现有的敏感字段api_key, secret_key, aster_private_key, lighter_private_key, okx_passphrase // 🔒 安全特性空值不会覆盖现有的敏感字段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, okxPassphrase string) error { 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) log.Printf("🔧 UpdateExchange: userID=%s, id=%s, enabled=%v", userID, id, enabled)
// 构建动态 UPDATE SET 子句 // 构建动态 UPDATE SET 子句
@@ -1244,12 +1227,6 @@ func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secre
args = append(args, encryptedLighterPrivateKey) args = append(args, encryptedLighterPrivateKey)
} }
if okxPassphrase != "" {
encryptedOKXPassphrase := d.encryptSensitiveData(okxPassphrase)
setClauses = append(setClauses, "okx_passphrase = ?")
args = append(args, encryptedOKXPassphrase)
}
// WHERE 条件 // WHERE 条件
args = append(args, id, userID) args = append(args, id, userID)
@@ -1290,9 +1267,6 @@ func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secre
} else if id == "hyperliquid" { } else if id == "hyperliquid" {
name = "Hyperliquid" name = "Hyperliquid"
typ = "dex" typ = "dex"
} else if id == "okx" {
name = "OKX Futures"
typ = "cex"
} else if id == "aster" { } else if id == "aster" {
name = "Aster DEX" name = "Aster DEX"
typ = "dex" typ = "dex"
@@ -1311,15 +1285,14 @@ func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secre
encryptedSecretKey := d.encryptSensitiveData(secretKey) encryptedSecretKey := d.encryptSensitiveData(secretKey)
encryptedAsterPrivateKey := d.encryptSensitiveData(asterPrivateKey) encryptedAsterPrivateKey := d.encryptSensitiveData(asterPrivateKey)
encryptedLighterPrivateKey := d.encryptSensitiveData(lighterPrivateKey) encryptedLighterPrivateKey := d.encryptSensitiveData(lighterPrivateKey)
encryptedOKXPassphrase := d.encryptSensitiveData(okxPassphrase)
// 创建用户特定的配置使用原始的交易所ID // 创建用户特定的配置使用原始的交易所ID
_, err = d.db.Exec(` _, err = d.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, 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, okx_passphrase, created_at, updated_at) lighter_wallet_addr, lighter_private_key, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
`, id, userID, name, typ, enabled, encryptedAPIKey, encryptedSecretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, encryptedAsterPrivateKey, lighterWalletAddr, encryptedLighterPrivateKey, encryptedOKXPassphrase) `, id, userID, name, typ, enabled, encryptedAPIKey, encryptedSecretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, encryptedAsterPrivateKey, lighterWalletAddr, encryptedLighterPrivateKey)
if err != nil { if err != nil {
log.Printf("❌ UpdateExchange: 创建记录失败: %v", err) log.Printf("❌ UpdateExchange: 创建记录失败: %v", err)

View File

@@ -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()
// 調用 APIGET /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()
// 調用 APIGET /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 // 空倉顯示負數
}
// 標準化 symbolBTC-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)
// 調用 APIPOST /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)
// 調用 APIGET /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()
}

View File

@@ -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(), "解析")
}