mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 15:26:55 +08:00
* feat: 添加 OKX 交易所支持(USDT Perpetual Swap) ## 新增功能 - 實現完整的 OKX API v5 REST 客戶端(純 Go 標準庫,無外部依賴) - 支持 USDT 永續合約交易(BTC-USDT-SWAP 等) - 實現 Trader 接口的 13 個核心方法 ## 技術細節 ### trader/okx_trader.go (NEW) - HMAC-SHA256 簽名機制(完全符合 OKX API v5 規範) - 餘額和持倉緩存(15秒,參考 Binance 實現) - 支持 Demo Trading(testnet 模式) - Symbol 格式轉換(BTCUSDT ↔ BTC-USDT-SWAP) - 全倉模式(Cross Margin)支持 - 自動槓桿設置 ### 實現的接口方法: - ✅ GetBalance() - 獲取賬戶餘額 - ✅ GetPositions() - 獲取所有持倉 - ✅ OpenLong() / OpenShort() - 開倉 - ✅ CloseLong() / CloseShort() - 平倉 - ✅ SetLeverage() - 設置槓桿 - ✅ SetMarginMode() - 設置保證金模式 - ✅ GetMarketPrice() - 獲取市場價格 - ✅ FormatQuantity() - 格式化數量 - ⚠️ 止盈止損功能標記為 TODO(非核心交易功能) ### config/database.go (MODIFIED) - 添加 "okx" 到預設交易所列表 - 新增 okx_passphrase 字段(OKX 需要 3 個認證參數) - 更新 ExchangeConfig 結構 - 添加數據庫遷移語句(ALTER TABLE) ### api/server.go (MODIFIED) - 在 handleCreateTrader() 添加 OKX 初始化邏輯 - switch case "okx" 分支 ## 代碼品質 - 代碼行數:~450 行 - 外部依賴:0 個 - 編譯狀態:✅ 通過 - 測試覆蓋:待實現(下一步) ## 待完成事項 - [ ] 撰寫單元測試(目標 >80% 覆蓋率) - [ ] 完善數據庫查詢邏輯(GetExchanges 添加 OKX passphrase 掃描) - [ ] 實現止盈止損功能(可選) * refactor: 完善 OKX passphrase 數據庫和 API 支持 - config/database.go: • GetExchanges() 添加 okx_passphrase 查詢和解密 • UpdateExchange() 函數簽名添加 okxPassphrase 參數 • UpdateExchange() UPDATE 邏輯添加 okx_passphrase SET 子句 • UpdateExchange() INSERT 添加 okx_passphrase 加密和列 - api/server.go: • UpdateExchangeConfigRequest 添加 OKXPassphrase 字段 • UpdateExchange 調用添加 OKXPassphrase 參數 - api/utils.go: • SanitizeExchangeConfigForLog 添加 OKXPassphrase 脫敏 ✅ 編譯測試通過,OKX 完整功能支持完成 * test: 添加 OKX Trader 完整單元測試套件 📊 測試覆蓋率:92.6% (遠超 80% 目標) ✅ 完成的測試: - 接口兼容性測試 - NewOKXTrader 構造函數測試(5個場景) - 符號格式轉換測試(5個場景) - HMAC-SHA256 簽名一致性測試 - GetBalance 測試(含字段驗證) - GetPositions 測試(含標準化數據驗證) - GetMarketPrice 測試(3個場景) - FormatQuantity 測試(5個場景) - SetLeverage/SetMarginMode 測試 - OpenLong/OpenShort 測試 - CloseLong/CloseShort 測試 - 緩存機制測試 - 錯誤處理測試(API錯誤、網絡錯誤、JSON錯誤) 🔧 測試套件架構: - OKXTraderTestSuite 繼承 TraderTestSuite - Mock HTTP 服務器模擬 OKX API v5 響應 - 完整覆蓋所有公開方法 - 包含邊界條件和錯誤場景測試 📈 方法覆蓋率明細: - request: 90.0% - GetBalance: 97.0% - GetPositions: 83.3% - formatSymbol, OpenLong, OpenShort, CloseLong, CloseShort: 100% - placeOrder, SetMarginMode, FormatQuantity, clearCache: 100% - Cancel* 方法系列: 100% - SetLeverage: 81.8% - GetMarketPrice: 85.7% --------- Co-authored-by: the-dev-z <the-dev-z@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
777 lines
19 KiB
Go
777 lines
19 KiB
Go
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(), "解析")
|
||
}
|