mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
665 lines
15 KiB
Go
665 lines
15 KiB
Go
package trader
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/agiledragon/gomonkey/v2"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TraderTestSuite Generic Trader interface test suite (base suite)
|
|
// Used for black-box testing any trader that implements the Trader interface
|
|
//
|
|
// Usage:
|
|
// 1. Create a concrete test suite struct, embedding TraderTestSuite
|
|
// 2. Implement SetupMocks() method to configure gomonkey mocks
|
|
// 3. Call RunAllTests() to run all generic tests
|
|
type TraderTestSuite struct {
|
|
T *testing.T
|
|
Trader Trader
|
|
Patches *gomonkey.Patches
|
|
}
|
|
|
|
// NewTraderTestSuite Create new base test suite
|
|
func NewTraderTestSuite(t *testing.T, trader Trader) *TraderTestSuite {
|
|
return &TraderTestSuite{
|
|
T: t,
|
|
Trader: trader,
|
|
Patches: gomonkey.NewPatches(),
|
|
}
|
|
}
|
|
|
|
// Cleanup Clean up mock patches
|
|
func (s *TraderTestSuite) Cleanup() {
|
|
if s.Patches != nil {
|
|
s.Patches.Reset()
|
|
}
|
|
}
|
|
|
|
// RunAllTests Run all generic interface tests
|
|
// Note: Before calling this method, please set up required mocks via SetupMocks
|
|
func (s *TraderTestSuite) RunAllTests() {
|
|
// Basic query methods
|
|
s.T.Run("GetBalance", func(t *testing.T) { s.TestGetBalance() })
|
|
s.T.Run("GetPositions", func(t *testing.T) { s.TestGetPositions() })
|
|
s.T.Run("GetMarketPrice", func(t *testing.T) { s.TestGetMarketPrice() })
|
|
|
|
// Configuration methods
|
|
s.T.Run("SetLeverage", func(t *testing.T) { s.TestSetLeverage() })
|
|
s.T.Run("SetMarginMode", func(t *testing.T) { s.TestSetMarginMode() })
|
|
s.T.Run("FormatQuantity", func(t *testing.T) { s.TestFormatQuantity() })
|
|
|
|
// Core trading methods
|
|
s.T.Run("OpenLong", func(t *testing.T) { s.TestOpenLong() })
|
|
s.T.Run("OpenShort", func(t *testing.T) { s.TestOpenShort() })
|
|
s.T.Run("CloseLong", func(t *testing.T) { s.TestCloseLong() })
|
|
s.T.Run("CloseShort", func(t *testing.T) { s.TestCloseShort() })
|
|
|
|
// Stop-loss and take-profit
|
|
s.T.Run("SetStopLoss", func(t *testing.T) { s.TestSetStopLoss() })
|
|
s.T.Run("SetTakeProfit", func(t *testing.T) { s.TestSetTakeProfit() })
|
|
|
|
// Order management
|
|
s.T.Run("CancelAllOrders", func(t *testing.T) { s.TestCancelAllOrders() })
|
|
s.T.Run("CancelStopOrders", func(t *testing.T) { s.TestCancelStopOrders() })
|
|
s.T.Run("CancelStopLossOrders", func(t *testing.T) { s.TestCancelStopLossOrders() })
|
|
s.T.Run("CancelTakeProfitOrders", func(t *testing.T) { s.TestCancelTakeProfitOrders() })
|
|
}
|
|
|
|
// TestGetBalance Test getting account balance
|
|
func (s *TraderTestSuite) TestGetBalance() {
|
|
tests := []struct {
|
|
name string
|
|
wantError bool
|
|
validate func(*testing.T, map[string]interface{})
|
|
}{
|
|
{
|
|
name: "Successfully get balance",
|
|
wantError: false,
|
|
validate: func(t *testing.T, result map[string]interface{}) {
|
|
assert.NotNil(t, result)
|
|
assert.Contains(t, result, "totalWalletBalance")
|
|
assert.Contains(t, result, "availableBalance")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
result, err := s.Trader.GetBalance()
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
if tt.validate != nil {
|
|
tt.validate(t, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetPositions Test getting positions
|
|
func (s *TraderTestSuite) TestGetPositions() {
|
|
tests := []struct {
|
|
name string
|
|
wantError bool
|
|
validate func(*testing.T, []map[string]interface{})
|
|
}{
|
|
{
|
|
name: "Successfully get position list",
|
|
wantError: false,
|
|
validate: func(t *testing.T, positions []map[string]interface{}) {
|
|
assert.NotNil(t, positions)
|
|
// Positions can be empty array
|
|
for _, pos := range positions {
|
|
assert.Contains(t, pos, "symbol")
|
|
assert.Contains(t, pos, "side")
|
|
assert.Contains(t, pos, "positionAmt")
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
result, err := s.Trader.GetPositions()
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
if tt.validate != nil {
|
|
tt.validate(t, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetMarketPrice Test getting market price
|
|
func (s *TraderTestSuite) TestGetMarketPrice() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
wantError bool
|
|
validate func(*testing.T, float64)
|
|
}{
|
|
{
|
|
name: "Successfully get BTC price",
|
|
symbol: "BTCUSDT",
|
|
wantError: false,
|
|
validate: func(t *testing.T, price float64) {
|
|
assert.Greater(t, price, 0.0)
|
|
},
|
|
},
|
|
{
|
|
name: "Invalid trading pair returns error",
|
|
symbol: "INVALIDUSDT",
|
|
wantError: true,
|
|
validate: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
price, err := s.Trader.GetMarketPrice(tt.symbol)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
if tt.validate != nil {
|
|
tt.validate(t, price)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSetLeverage Test setting leverage
|
|
func (s *TraderTestSuite) TestSetLeverage() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
leverage int
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "Set 10x leverage",
|
|
symbol: "BTCUSDT",
|
|
leverage: 10,
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "Set 1x leverage",
|
|
symbol: "ETHUSDT",
|
|
leverage: 1,
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
err := s.Trader.SetLeverage(tt.symbol, tt.leverage)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSetMarginMode Test setting margin mode
|
|
func (s *TraderTestSuite) TestSetMarginMode() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
isCrossMargin bool
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "Set cross margin mode",
|
|
symbol: "BTCUSDT",
|
|
isCrossMargin: true,
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "Set isolated margin mode",
|
|
symbol: "ETHUSDT",
|
|
isCrossMargin: false,
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
err := s.Trader.SetMarginMode(tt.symbol, tt.isCrossMargin)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFormatQuantity Test formatting quantity
|
|
func (s *TraderTestSuite) TestFormatQuantity() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
quantity float64
|
|
wantError bool
|
|
validate func(*testing.T, string)
|
|
}{
|
|
{
|
|
name: "Format BTC quantity",
|
|
symbol: "BTCUSDT",
|
|
quantity: 1.23456789,
|
|
wantError: false,
|
|
validate: func(t *testing.T, result string) {
|
|
assert.NotEmpty(t, result)
|
|
},
|
|
},
|
|
{
|
|
name: "Format small quantity",
|
|
symbol: "ETHUSDT",
|
|
quantity: 0.001,
|
|
wantError: false,
|
|
validate: func(t *testing.T, result string) {
|
|
assert.NotEmpty(t, result)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
result, err := s.Trader.FormatQuantity(tt.symbol, tt.quantity)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
if tt.validate != nil {
|
|
tt.validate(t, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCancelAllOrders Test canceling all orders
|
|
func (s *TraderTestSuite) TestCancelAllOrders() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "Cancel all BTC orders",
|
|
symbol: "BTCUSDT",
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
err := s.Trader.CancelAllOrders(tt.symbol)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Core trading method tests
|
|
// ============================================================
|
|
|
|
// TestOpenLong Test opening long position
|
|
func (s *TraderTestSuite) TestOpenLong() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
quantity float64
|
|
leverage int
|
|
wantError bool
|
|
validate func(*testing.T, map[string]interface{})
|
|
}{
|
|
{
|
|
name: "Successfully open long",
|
|
symbol: "BTCUSDT",
|
|
quantity: 0.01,
|
|
leverage: 10,
|
|
wantError: false,
|
|
validate: func(t *testing.T, result map[string]interface{}) {
|
|
assert.NotNil(t, result)
|
|
assert.Contains(t, result, "symbol")
|
|
assert.Equal(t, "BTCUSDT", result["symbol"])
|
|
},
|
|
},
|
|
{
|
|
name: "Small quantity long",
|
|
symbol: "ETHUSDT",
|
|
quantity: 0.004, // Increased to 0.004 to meet Binance Futures minimum order value of 10 USDT (0.004 * 3000 = 12 USDT)
|
|
leverage: 5,
|
|
wantError: false,
|
|
validate: func(t *testing.T, result map[string]interface{}) {
|
|
assert.NotNil(t, result)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
result, err := s.Trader.OpenLong(tt.symbol, tt.quantity, tt.leverage)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
if tt.validate != nil {
|
|
tt.validate(t, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestOpenShort Test opening short position
|
|
func (s *TraderTestSuite) TestOpenShort() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
quantity float64
|
|
leverage int
|
|
wantError bool
|
|
validate func(*testing.T, map[string]interface{})
|
|
}{
|
|
{
|
|
name: "Successfully open short",
|
|
symbol: "BTCUSDT",
|
|
quantity: 0.01,
|
|
leverage: 10,
|
|
wantError: false,
|
|
validate: func(t *testing.T, result map[string]interface{}) {
|
|
assert.NotNil(t, result)
|
|
assert.Contains(t, result, "symbol")
|
|
assert.Equal(t, "BTCUSDT", result["symbol"])
|
|
},
|
|
},
|
|
{
|
|
name: "Small quantity short",
|
|
symbol: "ETHUSDT",
|
|
quantity: 0.004, // Increased to 0.004 to meet Binance Futures minimum order value of 10 USDT (0.004 * 3000 = 12 USDT)
|
|
leverage: 5,
|
|
wantError: false,
|
|
validate: func(t *testing.T, result map[string]interface{}) {
|
|
assert.NotNil(t, result)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
result, err := s.Trader.OpenShort(tt.symbol, tt.quantity, tt.leverage)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
if tt.validate != nil {
|
|
tt.validate(t, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCloseLong Test closing long position
|
|
func (s *TraderTestSuite) TestCloseLong() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
quantity float64
|
|
wantError bool
|
|
validate func(*testing.T, map[string]interface{})
|
|
}{
|
|
{
|
|
name: "Close specified quantity",
|
|
symbol: "BTCUSDT",
|
|
quantity: 0.01,
|
|
wantError: false,
|
|
validate: func(t *testing.T, result map[string]interface{}) {
|
|
assert.NotNil(t, result)
|
|
assert.Contains(t, result, "symbol")
|
|
},
|
|
},
|
|
{
|
|
name: "Close all with quantity=0 returns error when no position",
|
|
symbol: "ETHUSDT",
|
|
quantity: 0,
|
|
wantError: true, // When no position exists, quantity=0 should return error
|
|
validate: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
result, err := s.Trader.CloseLong(tt.symbol, tt.quantity)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
if tt.validate != nil {
|
|
tt.validate(t, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCloseShort Test closing short position
|
|
func (s *TraderTestSuite) TestCloseShort() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
quantity float64
|
|
wantError bool
|
|
validate func(*testing.T, map[string]interface{})
|
|
}{
|
|
{
|
|
name: "Close specified quantity",
|
|
symbol: "BTCUSDT",
|
|
quantity: 0.01,
|
|
wantError: false,
|
|
validate: func(t *testing.T, result map[string]interface{}) {
|
|
assert.NotNil(t, result)
|
|
assert.Contains(t, result, "symbol")
|
|
},
|
|
},
|
|
{
|
|
name: "Close all with quantity=0 returns error when no position",
|
|
symbol: "ETHUSDT",
|
|
quantity: 0,
|
|
wantError: true, // When no position exists, quantity=0 should return error
|
|
validate: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
result, err := s.Trader.CloseShort(tt.symbol, tt.quantity)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
if tt.validate != nil {
|
|
tt.validate(t, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Stop-loss and take-profit tests
|
|
// ============================================================
|
|
|
|
// TestSetStopLoss Test setting stop-loss
|
|
func (s *TraderTestSuite) TestSetStopLoss() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
positionSide string
|
|
quantity float64
|
|
stopPrice float64
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "Long stop-loss",
|
|
symbol: "BTCUSDT",
|
|
positionSide: "LONG",
|
|
quantity: 0.01,
|
|
stopPrice: 45000.0,
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "Short stop-loss",
|
|
symbol: "ETHUSDT",
|
|
positionSide: "SHORT",
|
|
quantity: 0.1,
|
|
stopPrice: 3200.0,
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
err := s.Trader.SetStopLoss(tt.symbol, tt.positionSide, tt.quantity, tt.stopPrice)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSetTakeProfit Test setting take-profit
|
|
func (s *TraderTestSuite) TestSetTakeProfit() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
positionSide string
|
|
quantity float64
|
|
takeProfitPrice float64
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "Long take-profit",
|
|
symbol: "BTCUSDT",
|
|
positionSide: "LONG",
|
|
quantity: 0.01,
|
|
takeProfitPrice: 55000.0,
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "Short take-profit",
|
|
symbol: "ETHUSDT",
|
|
positionSide: "SHORT",
|
|
quantity: 0.1,
|
|
takeProfitPrice: 2800.0,
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
err := s.Trader.SetTakeProfit(tt.symbol, tt.positionSide, tt.quantity, tt.takeProfitPrice)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCancelStopOrders Test canceling stop-loss/take-profit orders
|
|
func (s *TraderTestSuite) TestCancelStopOrders() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "Cancel BTC stop orders",
|
|
symbol: "BTCUSDT",
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
err := s.Trader.CancelStopOrders(tt.symbol)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCancelStopLossOrders Test canceling stop-loss orders
|
|
func (s *TraderTestSuite) TestCancelStopLossOrders() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "Cancel BTC stop-loss orders",
|
|
symbol: "BTCUSDT",
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
err := s.Trader.CancelStopLossOrders(tt.symbol)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCancelTakeProfitOrders Test canceling take-profit orders
|
|
func (s *TraderTestSuite) TestCancelTakeProfitOrders() {
|
|
tests := []struct {
|
|
name string
|
|
symbol string
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "Cancel BTC take-profit orders",
|
|
symbol: "BTCUSDT",
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.T.Run(tt.name, func(t *testing.T) {
|
|
err := s.Trader.CancelTakeProfitOrders(tt.symbol)
|
|
if tt.wantError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|