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

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

View File

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

View File

@@ -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 != "" {

View File

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

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