Files
nofx/trader/aster_trader_test.go
WquGuru ae09647468 test(trader): add comprehensive unit tests and CI coverage reporting (#823)
* chore(config): add Python and uv support to project

- Add comprehensive Python .gitignore rules (pycache, venv, pytest, etc.)
- Add uv package manager specific ignores (.uv/, uv.lock)
- Initialize pyproject.toml for Python tooling

Co-authored-by: tinkle-community <tinklefund@gmail.com>

* chore(deps): add testing dependencies

- Add github.com/stretchr/testify v1.11.1 for test assertions
- Add github.com/agiledragon/gomonkey/v2 v2.13.0 for mocking
- Promote github.com/rs/zerolog to direct dependency

Co-authored-by: tinkle-community <tinklefund@gmail.com>

* ci(workflow): add PR test coverage reporting

Add GitHub Actions workflow to run unit tests and report coverage on PRs:
- Run Go tests with race detection and coverage profiling
- Calculate coverage statistics and generate detailed reports
- Post coverage results as PR comments with visual indicators
- Fix Go version to 1.23 (was incorrectly set to 1.25.0)

Coverage guidelines:
- Green (>=80%): excellent
- Yellow (>=60%): good
- Orange (>=40%): fair
- Red (<40%): needs improvement

This workflow is advisory only and does not block PR merging.

Co-authored-by: tinkle-community <tinklefund@gmail.com>

* test(trader): add comprehensive unit tests for trader modules

Add unit test suites for multiple trader implementations:
- aster_trader_test.go: AsterTrader functionality tests
- auto_trader_test.go: AutoTrader lifecycle and operations tests
- binance_futures_test.go: Binance futures trader tests
- hyperliquid_trader_test.go: Hyperliquid trader tests
- trader_test_suite.go: Common test suite utilities and helpers

Also fix minor formatting issue in auto_trader.go (trailing whitespace)

Co-authored-by: tinkle-community <tinklefund@gmail.com>

* test(trader): preserve existing calculatePnLPercentage unit tests

Merge existing calculatePnLPercentage tests with incoming comprehensive test suite:
- Preserve TestCalculatePnLPercentage with 9 test cases covering edge cases
- Preserve TestCalculatePnLPercentage_RealWorldScenarios with 3 trading scenarios
- Add math package import for floating-point precision comparison
- All tests validate PnL percentage calculation with different leverage scenarios

Co-authored-by: tinkle-community <tinklefund@gmail.com>

---------

Co-authored-by: tinkle-community <tinklefund@gmail.com>
2025-11-09 17:43:28 +08:00

300 lines
8.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package trader
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
)
// ============================================================
// 一、AsterTraderTestSuite - 继承 base test suite
// ============================================================
// AsterTraderTestSuite Aster交易器测试套件
// 继承 TraderTestSuite 并添加 Aster 特定的 mock 逻辑
type AsterTraderTestSuite struct {
*TraderTestSuite // 嵌入基础测试套件
mockServer *httptest.Server
}
// NewAsterTraderTestSuite 创建 Aster 测试套件
func NewAsterTraderTestSuite(t *testing.T) *AsterTraderTestSuite {
// 创建 mock HTTP 服务器
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 根据不同的 URL 路径返回不同的 mock 响应
path := r.URL.Path
var respBody interface{}
switch {
// Mock GetBalance - /fapi/v3/balance (返回数组)
case path == "/fapi/v3/balance":
respBody = []map[string]interface{}{
{
"asset": "USDT",
"walletBalance": "10000.00",
"unrealizedProfit": "100.50",
"marginBalance": "10100.50",
"maintMargin": "200.00",
"initialMargin": "2000.00",
"maxWithdrawAmount": "8000.00",
"crossWalletBalance": "10000.00",
"crossUnPnl": "100.50",
"availableBalance": "8000.00",
},
}
// Mock GetPositions - /fapi/v3/positionRisk
case path == "/fapi/v3/positionRisk":
respBody = []map[string]interface{}{
{
"symbol": "BTCUSDT",
"positionAmt": "0.5",
"entryPrice": "50000.00",
"markPrice": "50500.00",
"unRealizedProfit": "250.00",
"liquidationPrice": "45000.00",
"leverage": "10",
"positionSide": "LONG",
},
}
// Mock GetMarketPrice - /fapi/v3/ticker/price (返回单个对象)
case path == "/fapi/v3/ticker/price":
// 从查询参数获取symbol
symbol := r.URL.Query().Get("symbol")
if symbol == "" {
symbol = "BTCUSDT"
}
// 根据symbol返回不同价格
price := "50000.00"
if symbol == "ETHUSDT" {
price = "3000.00"
} else if symbol == "INVALIDUSDT" {
// 返回错误响应
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": -1121,
"msg": "Invalid symbol",
})
return
}
respBody = map[string]interface{}{
"symbol": symbol,
"price": price,
}
// Mock ExchangeInfo - /fapi/v3/exchangeInfo
case path == "/fapi/v3/exchangeInfo":
respBody = map[string]interface{}{
"symbols": []map[string]interface{}{
{
"symbol": "BTCUSDT",
"pricePrecision": 1,
"quantityPrecision": 3,
"baseAssetPrecision": 8,
"quotePrecision": 8,
"filters": []map[string]interface{}{
{
"filterType": "PRICE_FILTER",
"tickSize": "0.1",
},
{
"filterType": "LOT_SIZE",
"stepSize": "0.001",
},
},
},
{
"symbol": "ETHUSDT",
"pricePrecision": 2,
"quantityPrecision": 3,
"baseAssetPrecision": 8,
"quotePrecision": 8,
"filters": []map[string]interface{}{
{
"filterType": "PRICE_FILTER",
"tickSize": "0.01",
},
{
"filterType": "LOT_SIZE",
"stepSize": "0.001",
},
},
},
},
}
// Mock CreateOrder - /fapi/v1/order and /fapi/v3/order
case (path == "/fapi/v1/order" || path == "/fapi/v3/order") && r.Method == "POST":
// 从请求中解析参数以确定symbol
bodyBytes, _ := io.ReadAll(r.Body)
var orderParams map[string]interface{}
json.Unmarshal(bodyBytes, &orderParams)
symbol := "BTCUSDT"
if s, ok := orderParams["symbol"].(string); ok {
symbol = s
}
respBody = map[string]interface{}{
"orderId": 123456,
"symbol": symbol,
"status": "FILLED",
"side": orderParams["side"],
"type": orderParams["type"],
}
// Mock CancelOrder - /fapi/v1/order (DELETE)
case path == "/fapi/v1/order" && r.Method == "DELETE":
respBody = map[string]interface{}{
"orderId": 123456,
"symbol": "BTCUSDT",
"status": "CANCELED",
}
// Mock ListOpenOrders - /fapi/v1/openOrders and /fapi/v3/openOrders
case path == "/fapi/v1/openOrders" || path == "/fapi/v3/openOrders":
respBody = []map[string]interface{}{}
// Mock SetLeverage - /fapi/v1/leverage
case path == "/fapi/v1/leverage":
respBody = map[string]interface{}{
"leverage": 10,
"symbol": "BTCUSDT",
}
// Mock SetMarginMode - /fapi/v1/marginType
case path == "/fapi/v1/marginType":
respBody = map[string]interface{}{
"code": 200,
"msg": "success",
}
// Default: empty response
default:
respBody = map[string]interface{}{}
}
// 序列化响应
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(respBody)
}))
// 生成一个测试用的私钥
privateKey, _ := crypto.GenerateKey()
// 创建 mock trader使用 mock server 的 URL
trader := &AsterTrader{
ctx: context.Background(),
user: "0x1234567890123456789012345678901234567890",
signer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
privateKey: privateKey,
client: mockServer.Client(),
baseURL: mockServer.URL, // 使用 mock server 的 URL
symbolPrecision: make(map[string]SymbolPrecision),
}
// 创建基础套件
baseSuite := NewTraderTestSuite(t, trader)
return &AsterTraderTestSuite{
TraderTestSuite: baseSuite,
mockServer: mockServer,
}
}
// Cleanup 清理资源
func (s *AsterTraderTestSuite) Cleanup() {
if s.mockServer != nil {
s.mockServer.Close()
}
s.TraderTestSuite.Cleanup()
}
// ============================================================
// 二、使用 AsterTraderTestSuite 运行通用测试
// ============================================================
// TestAsterTrader_InterfaceCompliance 测试接口兼容性
func TestAsterTrader_InterfaceCompliance(t *testing.T) {
var _ Trader = (*AsterTrader)(nil)
}
// TestAsterTrader_CommonInterface 使用测试套件运行所有通用接口测试
func TestAsterTrader_CommonInterface(t *testing.T) {
// 创建测试套件
suite := NewAsterTraderTestSuite(t)
defer suite.Cleanup()
// 运行所有通用接口测试
suite.RunAllTests()
}
// ============================================================
// 三、Aster 特定功能的单元测试
// ============================================================
// TestNewAsterTrader 测试创建 Aster 交易器
func TestNewAsterTrader(t *testing.T) {
tests := []struct {
name string
user string
signer string
privateKeyHex string
wantError bool
errorContains string
}{
{
name: "成功创建",
user: "0x1234567890123456789012345678901234567890",
signer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
privateKeyHex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
wantError: false,
},
{
name: "无效私钥格式",
user: "0x1234567890123456789012345678901234567890",
signer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
privateKeyHex: "invalid_key",
wantError: true,
errorContains: "解析私钥失败",
},
{
name: "带0x前缀的私钥",
user: "0x1234567890123456789012345678901234567890",
signer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
privateKeyHex: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
wantError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
trader, err := NewAsterTrader(tt.user, tt.signer, tt.privateKeyHex)
if tt.wantError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
assert.Nil(t, trader)
} else {
assert.NoError(t, err)
assert.NotNil(t, trader)
if trader != nil {
assert.Equal(t, tt.user, trader.user)
assert.Equal(t, tt.signer, trader.signer)
assert.NotNil(t, trader.privateKey)
}
}
})
}
}