Files
gocryptotrader/exchanges/order/order_test.go
Gareth Kirwan 16d2d9f35a Config: AssetEnabled upgrade (#1735)
* Config: Move assetEnabled upgrade to Version management

* Assets: Do not error on asset not enabled, or disabled

This became more messy with Disabling something that's defaulted to
disabled.
Taking an idealogical stance against erroring that what you want to have
done is already done.

* CurrencyManager: Set AssetEnabled when StorePairs(enabled)

* RPCServer: Fix tests expecting StoreAssetPairFormat to enable the asset

Also assertifies

* Bitfinex: Fix tests for MarginFunding subs

* GCTWrapper: Improve TestMain clarity

* BTSE: Add futures to testconfig

* Exchanges: Rename StoreAssetPairStore

Previously we were calling it "Format", but accepting everything from
the PairStore.
We were also defaulting to turning the Asset on.

Now callers need to get their AssetEnabled set as they want it, so
there's no magic

This change also moves responsibility for error wrapping outside to the
caller.

* Config: AssetEnabled upgrade should respect assetTypes

Previously we ignored the field and just turned on everything.
I think that was because we couldn't get at the old value.
In either case, we have the option to do better, and respect the
assetEnabled value

* Config: Improve exchange config version upgrade error messages
2025-03-17 21:47:37 +11:00

2206 lines
58 KiB
Go

package order
import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"testing"
"time"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/validate"
)
var errValidationCheckFailed = errors.New("validation check failed")
func TestSubmitValidate(t *testing.T) {
t.Parallel()
testPair := currency.NewPair(currency.BTC, currency.LTC)
tester := []struct {
ExpectedErr error
Submit *Submit
ValidOpts validate.Checker
HasToPurchaseWithQuoteAmountSet bool
HasToSellWithBaseAmountSet bool
RequiresID bool
}{
{
ExpectedErr: ErrSubmissionIsNil,
Submit: nil,
}, // nil struct
{
ExpectedErr: errExchangeNameUnset,
Submit: &Submit{},
}, // empty exchange
{
ExpectedErr: ErrPairIsEmpty,
Submit: &Submit{Exchange: "test"},
}, // empty pair
{
ExpectedErr: ErrAssetNotSet,
Submit: &Submit{Exchange: "test", Pair: testPair},
}, // valid pair but invalid asset
{
ExpectedErr: asset.ErrNotSupported,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
AssetType: asset.All,
},
}, // valid pair but invalid asset
{
ExpectedErr: ErrSideIsInvalid,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
AssetType: asset.Spot,
},
}, // valid pair but invalid order side
{
ExpectedErr: errTimeInForceConflict,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
AssetType: asset.Spot,
Side: Ask,
Type: Market,
ImmediateOrCancel: true,
FillOrKill: true,
},
},
{
ExpectedErr: ErrTypeIsInvalid,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Buy,
AssetType: asset.Spot,
},
}, // valid pair and order side but invalid order type
{
ExpectedErr: ErrTypeIsInvalid,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Sell,
AssetType: asset.Spot,
},
}, // valid pair and order side but invalid order type
{
ExpectedErr: ErrTypeIsInvalid,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Bid,
AssetType: asset.Spot,
},
}, // valid pair and order side but invalid order type
{
ExpectedErr: ErrTypeIsInvalid,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Ask,
AssetType: asset.Spot,
},
}, // valid pair and order side but invalid order type
{
ExpectedErr: ErrAmountIsInvalid,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Ask,
Type: Market,
AssetType: asset.Spot,
},
}, // valid pair, order side, type but invalid amount
{
ExpectedErr: ErrAmountIsInvalid,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Ask,
Type: Market,
AssetType: asset.Spot,
Amount: -1,
},
}, // valid pair, order side, type but invalid amount
{
ExpectedErr: ErrAmountIsInvalid,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Ask,
Type: Market,
AssetType: asset.Spot,
QuoteAmount: -1,
},
}, // valid pair, order side, type but invalid amount
{
ExpectedErr: ErrPriceMustBeSetIfLimitOrder,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Ask,
Type: Limit,
Amount: 1,
AssetType: asset.Spot,
},
}, // valid pair, order side, type, amount but invalid price
{
ExpectedErr: errValidationCheckFailed,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Ask,
Type: Limit,
Amount: 1,
Price: 1000,
AssetType: asset.Spot,
},
ValidOpts: validate.Check(func() error { return errValidationCheckFailed }),
}, // custom validation error check
{
ExpectedErr: nil,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Ask,
Type: Limit,
Amount: 1,
Price: 1000,
AssetType: asset.Spot,
},
ValidOpts: validate.Check(func() error { return nil }),
}, // valid order!
{
ExpectedErr: ErrAmountMustBeSet,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Buy,
Type: Market,
Amount: 1,
AssetType: asset.Spot,
},
HasToPurchaseWithQuoteAmountSet: true,
ValidOpts: validate.Check(func() error { return nil }),
},
{
ExpectedErr: ErrAmountMustBeSet,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Sell,
Type: Market,
QuoteAmount: 1,
AssetType: asset.Spot,
},
HasToSellWithBaseAmountSet: true,
ValidOpts: validate.Check(func() error { return nil }),
},
{
ExpectedErr: ErrClientOrderIDMustBeSet,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Buy,
Type: Market,
Amount: 1,
AssetType: asset.Spot,
},
RequiresID: true,
ValidOpts: validate.Check(func() error { return nil }),
},
{
ExpectedErr: nil,
Submit: &Submit{
Exchange: "test",
Pair: testPair,
Side: Buy,
Type: Market,
Amount: 1,
AssetType: asset.Spot,
ClientOrderID: "69420",
},
RequiresID: true,
ValidOpts: validate.Check(func() error { return nil }),
},
}
for x, tc := range tester {
t.Run(strconv.Itoa(x), func(t *testing.T) {
t.Parallel()
requirements := protocol.TradingRequirements{
SpotMarketOrderAmountPurchaseQuotationOnly: tc.HasToPurchaseWithQuoteAmountSet,
SpotMarketOrderAmountSellBaseOnly: tc.HasToSellWithBaseAmountSet,
ClientOrderID: tc.RequiresID,
}
err := tc.Submit.Validate(requirements, tc.ValidOpts)
assert.ErrorIs(t, err, tc.ExpectedErr)
})
}
}
func TestSubmit_DeriveSubmitResponse(t *testing.T) {
t.Parallel()
var s *Submit
_, err := s.DeriveSubmitResponse("")
if !errors.Is(err, errOrderSubmitIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errOrderSubmitIsNil)
}
s = &Submit{}
_, err = s.DeriveSubmitResponse("")
if !errors.Is(err, ErrOrderIDNotSet) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderIDNotSet)
}
resp, err := s.DeriveSubmitResponse("1337")
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if resp.OrderID != "1337" {
t.Fatal("unexpected value")
}
if resp.Status != New {
t.Fatal("unexpected value")
}
if resp.Date.IsZero() {
t.Fatal("unexpected value")
}
if resp.LastUpdated.IsZero() {
t.Fatal("unexpected value")
}
}
func TestSubmitResponse_DeriveDetail(t *testing.T) {
t.Parallel()
var s *SubmitResponse
_, err := s.DeriveDetail(uuid.Nil)
if !errors.Is(err, errOrderSubmitResponseIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errOrderSubmitResponseIsNil)
}
id, err := uuid.NewV4()
if err != nil {
t.Fatal(err)
}
s = &SubmitResponse{}
deets, err := s.DeriveDetail(id)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if deets.InternalOrderID != id {
t.Fatal("unexpected value")
}
}
func TestOrderSides(t *testing.T) {
t.Parallel()
var os = Buy
if os.String() != "BUY" {
t.Errorf("unexpected string %s", os.String())
}
if os.Lower() != "buy" {
t.Errorf("unexpected string %s", os.Lower())
}
if os.Title() != "Buy" {
t.Errorf("unexpected string %s", os.Title())
}
}
func TestTitle(t *testing.T) {
t.Parallel()
orderType := Limit
if orderType.Title() != "Limit" {
t.Errorf("received '%v' expected 'Limit'", orderType.Title())
}
}
func TestOrderTypes(t *testing.T) {
t.Parallel()
var orderType Type
if orderType.String() != "UNKNOWN" {
t.Errorf("unexpected string %s", orderType.String())
}
if orderType.Lower() != "unknown" {
t.Errorf("unexpected string %s", orderType.Lower())
}
if orderType.Title() != "Unknown" {
t.Errorf("unexpected string %s", orderType.Title())
}
}
func TestInferCostsAndTimes(t *testing.T) {
t.Parallel()
var detail Detail
detail.InferCostsAndTimes()
if detail.Amount != detail.ExecutedAmount+detail.RemainingAmount {
t.Errorf(
"Order detail amounts not equals. Expected 0, received %f",
detail.Amount-(detail.ExecutedAmount+detail.RemainingAmount),
)
}
detail.CloseTime = time.Now()
detail.InferCostsAndTimes()
if detail.LastUpdated != detail.CloseTime {
t.Errorf(
"Order last updated not equals close time. Expected %s, received %s",
detail.CloseTime,
detail.LastUpdated,
)
}
detail.Amount = 1
detail.ExecutedAmount = 1
detail.InferCostsAndTimes()
if detail.AverageExecutedPrice != 0 {
t.Errorf(
"Unexpected AverageExecutedPrice. Expected 0, received %f",
detail.AverageExecutedPrice,
)
}
detail.Amount = 1
detail.ExecutedAmount = 1
detail.InferCostsAndTimes()
if detail.Cost != 0 {
t.Errorf(
"Unexpected Cost. Expected 0, received %f",
detail.Cost,
)
}
detail.ExecutedAmount = 0
detail.Amount = 1
detail.RemainingAmount = 1
detail.InferCostsAndTimes()
if detail.Amount != detail.ExecutedAmount+detail.RemainingAmount {
t.Errorf(
"Order detail amounts not equals. Expected 0, received %f",
detail.Amount-(detail.ExecutedAmount+detail.RemainingAmount),
)
}
detail.RemainingAmount = 0
detail.Amount = 1
detail.ExecutedAmount = 1
detail.Price = 2
detail.InferCostsAndTimes()
if detail.AverageExecutedPrice != 2 {
t.Errorf(
"Unexpected AverageExecutedPrice. Expected 2, received %f",
detail.AverageExecutedPrice,
)
}
detail = Detail{Amount: 1, ExecutedAmount: 2, Cost: 3, Price: 0}
detail.InferCostsAndTimes()
if detail.AverageExecutedPrice != 1.5 {
t.Errorf(
"Unexpected AverageExecutedPrice. Expected 1.5, received %f",
detail.AverageExecutedPrice,
)
}
detail = Detail{Amount: 1, ExecutedAmount: 2, AverageExecutedPrice: 3}
detail.InferCostsAndTimes()
if detail.Cost != 6 {
t.Errorf(
"Unexpected Cost. Expected 6, received %f",
detail.Cost,
)
}
}
func TestFilterOrdersByType(t *testing.T) {
t.Parallel()
var orders = []Detail{
{
Type: ImmediateOrCancel,
},
{
Type: Limit,
},
{}, // Unpopulated fields are preserved for API differences
}
FilterOrdersByType(&orders, AnyType)
if len(orders) != 3 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 2, len(orders))
}
FilterOrdersByType(&orders, Limit)
if len(orders) != 2 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders))
}
FilterOrdersByType(&orders, Stop)
if len(orders) != 1 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 0, len(orders))
}
}
var filterOrdersByTypeBenchmark = &[]Detail{
{Type: Limit},
{Type: Limit},
{Type: Limit},
{Type: Limit},
{Type: Limit},
{Type: Limit},
{Type: Limit},
{Type: Limit},
{Type: Limit},
{Type: Limit},
}
// BenchmarkFilterOrdersByType benchmark
//
// 392455 3226 ns/op 15840 B/op 5 allocs/op // PREV
// 9486490 109.5 ns/op 0 B/op 0 allocs/op // CURRENT
func BenchmarkFilterOrdersByType(b *testing.B) {
for b.Loop() {
FilterOrdersByType(filterOrdersByTypeBenchmark, Limit)
}
}
func TestFilterOrdersBySide(t *testing.T) {
t.Parallel()
var orders = []Detail{
{
Side: Buy,
},
{
Side: Sell,
},
{}, // Unpopulated fields are preserved for API differences
}
FilterOrdersBySide(&orders, AnySide)
if len(orders) != 3 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders))
}
FilterOrdersBySide(&orders, Buy)
if len(orders) != 2 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders))
}
FilterOrdersBySide(&orders, Sell)
if len(orders) != 1 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 0, len(orders))
}
}
var filterOrdersBySideBenchmark = &[]Detail{
{Side: Ask},
{Side: Ask},
{Side: Ask},
{Side: Ask},
{Side: Ask},
{Side: Ask},
{Side: Ask},
{Side: Ask},
{Side: Ask},
{Side: Ask},
}
// BenchmarkFilterOrdersBySide benchmark
//
// 372594 3049 ns/op 15840 B/op 5 allocs/op // PREV
// 7412187 148.8 ns/op 0 B/op 0 allocs/op // CURRENT
func BenchmarkFilterOrdersBySide(b *testing.B) {
for b.Loop() {
FilterOrdersBySide(filterOrdersBySideBenchmark, Ask)
}
}
func TestFilterOrdersByTimeRange(t *testing.T) {
t.Parallel()
var orders = []Detail{
{
Date: time.Unix(100, 0),
},
{
Date: time.Unix(110, 0),
},
{
Date: time.Unix(111, 0),
},
}
err := FilterOrdersByTimeRange(&orders, time.Unix(0, 0), time.Unix(0, 0))
if err != nil {
t.Fatal(err)
}
if len(orders) != 3 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders))
}
err = FilterOrdersByTimeRange(&orders, time.Unix(100, 0), time.Unix(111, 0))
if err != nil {
t.Fatal(err)
}
if len(orders) != 3 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders))
}
err = FilterOrdersByTimeRange(&orders, time.Unix(101, 0), time.Unix(111, 0))
if err != nil {
t.Fatal(err)
}
if len(orders) != 2 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 2, len(orders))
}
err = FilterOrdersByTimeRange(&orders, time.Unix(200, 0), time.Unix(300, 0))
if err != nil {
t.Fatal(err)
}
if len(orders) != 0 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 0, len(orders))
}
orders = append(orders, Detail{})
// test for event no timestamp is set on an order, best to include it
err = FilterOrdersByTimeRange(&orders, time.Unix(200, 0), time.Unix(300, 0))
if err != nil {
t.Fatal(err)
}
if len(orders) != 1 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders))
}
err = FilterOrdersByTimeRange(&orders, time.Unix(300, 0), time.Unix(50, 0))
if !errors.Is(err, common.ErrStartAfterEnd) {
t.Fatalf("received: '%v' but expected: '%v'", err, common.ErrStartAfterEnd)
}
}
var filterOrdersByTimeRangeBenchmark = &[]Detail{
{Date: time.Unix(100, 0)},
{Date: time.Unix(100, 0)},
{Date: time.Unix(100, 0)},
{Date: time.Unix(100, 0)},
{Date: time.Unix(100, 0)},
{Date: time.Unix(100, 0)},
{Date: time.Unix(100, 0)},
{Date: time.Unix(100, 0)},
{Date: time.Unix(100, 0)},
{Date: time.Unix(100, 0)},
}
// BenchmarkFilterOrdersByTimeRange benchmark
//
// 390822 3335 ns/op 15840 B/op 5 allocs/op // PREV
// 6201034 172.1 ns/op 0 B/op 0 allocs/op // CURRENT
func BenchmarkFilterOrdersByTimeRange(b *testing.B) {
for b.Loop() {
err := FilterOrdersByTimeRange(filterOrdersByTimeRangeBenchmark, time.Unix(50, 0), time.Unix(150, 0))
if err != nil {
b.Fatal(err)
}
}
}
func TestFilterOrdersByPairs(t *testing.T) {
t.Parallel()
var orders = []Detail{
{
Pair: currency.NewPair(currency.BTC, currency.USD),
},
{
Pair: currency.NewPair(currency.LTC, currency.EUR),
},
{
Pair: currency.NewPair(currency.DOGE, currency.RUB),
},
{}, // Unpopulated fields are preserved for API differences
}
currencies := []currency.Pair{currency.NewPair(currency.BTC, currency.USD),
currency.NewPair(currency.LTC, currency.EUR),
currency.NewPair(currency.DOGE, currency.RUB)}
FilterOrdersByPairs(&orders, currencies)
if len(orders) != 4 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders))
}
currencies = []currency.Pair{currency.NewPair(currency.BTC, currency.USD),
currency.NewPair(currency.LTC, currency.EUR)}
FilterOrdersByPairs(&orders, currencies)
if len(orders) != 3 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 2, len(orders))
}
currencies = []currency.Pair{currency.NewPair(currency.BTC, currency.USD)}
FilterOrdersByPairs(&orders, currencies)
if len(orders) != 2 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders))
}
currencies = []currency.Pair{currency.NewPair(currency.USD, currency.BTC)}
FilterOrdersByPairs(&orders, currencies)
if len(orders) != 2 {
t.Errorf("Reverse Orders failed to be filtered. Expected %v, received %v", 1, len(orders))
}
currencies = []currency.Pair{}
FilterOrdersByPairs(&orders, currencies)
if len(orders) != 2 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders))
}
currencies = append(currencies, currency.EMPTYPAIR)
FilterOrdersByPairs(&orders, currencies)
if len(orders) != 2 {
t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders))
}
}
var filterOrdersByPairsBenchmark = &[]Detail{
{Pair: currency.NewPair(currency.BTC, currency.USD)},
{Pair: currency.NewPair(currency.BTC, currency.USD)},
{Pair: currency.NewPair(currency.BTC, currency.USD)},
{Pair: currency.NewPair(currency.BTC, currency.USD)},
{Pair: currency.NewPair(currency.BTC, currency.USD)},
{Pair: currency.NewPair(currency.BTC, currency.USD)},
{Pair: currency.NewPair(currency.BTC, currency.USD)},
{Pair: currency.NewPair(currency.BTC, currency.USD)},
{Pair: currency.NewPair(currency.BTC, currency.USD)},
{Pair: currency.NewPair(currency.BTC, currency.USD)},
}
// BenchmarkFilterOrdersByPairs benchmark
//
// 400032 2977 ns/op 15840 B/op 5 allocs/op // PREV
// 6977242 172.8 ns/op 0 B/op 0 allocs/op // CURRENT
func BenchmarkFilterOrdersByPairs(b *testing.B) {
pairs := []currency.Pair{currency.NewPair(currency.BTC, currency.USD)}
for b.Loop() {
FilterOrdersByPairs(filterOrdersByPairsBenchmark, pairs)
}
}
func TestSortOrdersByPrice(t *testing.T) {
t.Parallel()
orders := []Detail{
{
Price: 100,
}, {
Price: 0,
}, {
Price: 50,
},
}
SortOrdersByPrice(&orders, false)
if orders[0].Price != 0 {
t.Errorf("Expected: '%v', received: '%v'", 0, orders[0].Price)
}
SortOrdersByPrice(&orders, true)
if orders[0].Price != 100 {
t.Errorf("Expected: '%v', received: '%v'", 100, orders[0].Price)
}
}
func TestSortOrdersByDate(t *testing.T) {
t.Parallel()
orders := []Detail{
{
Date: time.Unix(0, 0),
}, {
Date: time.Unix(1, 0),
}, {
Date: time.Unix(2, 0),
},
}
SortOrdersByDate(&orders, false)
if orders[0].Date.Unix() != time.Unix(0, 0).Unix() {
t.Errorf("Expected: '%v', received: '%v'",
time.Unix(0, 0).Unix(),
orders[0].Date.Unix())
}
SortOrdersByDate(&orders, true)
if orders[0].Date.Unix() != time.Unix(2, 0).Unix() {
t.Errorf("Expected: '%v', received: '%v'",
time.Unix(2, 0).Unix(),
orders[0].Date.Unix())
}
}
func TestSortOrdersByCurrency(t *testing.T) {
t.Parallel()
orders := []Detail{
{
Pair: currency.NewPairWithDelimiter(currency.BTC.String(),
currency.USD.String(),
"-"),
}, {
Pair: currency.NewPairWithDelimiter(currency.DOGE.String(),
currency.USD.String(),
"-"),
}, {
Pair: currency.NewPairWithDelimiter(currency.BTC.String(),
currency.RUB.String(),
"-"),
}, {
Pair: currency.NewPairWithDelimiter(currency.LTC.String(),
currency.EUR.String(),
"-"),
}, {
Pair: currency.NewPairWithDelimiter(currency.LTC.String(),
currency.AUD.String(),
"-"),
},
}
SortOrdersByCurrency(&orders, false)
if orders[0].Pair.String() != currency.BTC.String()+"-"+currency.RUB.String() {
t.Errorf("Expected: '%v', received: '%v'",
currency.BTC.String()+"-"+currency.RUB.String(),
orders[0].Pair.String())
}
SortOrdersByCurrency(&orders, true)
if orders[0].Pair.String() != currency.LTC.String()+"-"+currency.EUR.String() {
t.Errorf("Expected: '%v', received: '%v'",
currency.LTC.String()+"-"+currency.EUR.String(),
orders[0].Pair.String())
}
}
func TestSortOrdersByOrderSide(t *testing.T) {
t.Parallel()
orders := []Detail{
{
Side: Buy,
}, {
Side: Sell,
}, {
Side: Sell,
}, {
Side: Buy,
},
}
SortOrdersBySide(&orders, false)
if !strings.EqualFold(orders[0].Side.String(), Buy.String()) {
t.Errorf("Expected: '%v', received: '%v'",
Buy,
orders[0].Side)
}
SortOrdersBySide(&orders, true)
if !strings.EqualFold(orders[0].Side.String(), Sell.String()) {
t.Errorf("Expected: '%v', received: '%v'",
Sell,
orders[0].Side)
}
}
func TestSortOrdersByOrderType(t *testing.T) {
t.Parallel()
orders := []Detail{
{
Type: Market,
}, {
Type: Limit,
}, {
Type: ImmediateOrCancel,
}, {
Type: TrailingStop,
},
}
SortOrdersByType(&orders, false)
if !strings.EqualFold(orders[0].Type.String(), ImmediateOrCancel.String()) {
t.Errorf("Expected: '%v', received: '%v'",
ImmediateOrCancel,
orders[0].Type)
}
SortOrdersByType(&orders, true)
if !strings.EqualFold(orders[0].Type.String(), TrailingStop.String()) {
t.Errorf("Expected: '%v', received: '%v'",
TrailingStop,
orders[0].Type)
}
}
func TestStringToOrderSide(t *testing.T) {
cases := []struct {
in string
out Side
err error
}{
{"buy", Buy, nil},
{"BUY", Buy, nil},
{"bUy", Buy, nil},
{"sell", Sell, nil},
{"SELL", Sell, nil},
{"sElL", Sell, nil},
{"bid", Bid, nil},
{"BID", Bid, nil},
{"bId", Bid, nil},
{"ask", Ask, nil},
{"ASK", Ask, nil},
{"aSk", Ask, nil},
{"lOnG", Long, nil},
{"ShoRt", Short, nil},
{"any", AnySide, nil},
{"ANY", AnySide, nil},
{"aNy", AnySide, nil},
{"woahMan", UnknownSide, ErrSideIsInvalid},
}
for i := range cases {
testData := &cases[i]
t.Run(testData.in, func(t *testing.T) {
out, err := StringToOrderSide(testData.in)
if !errors.Is(err, testData.err) {
t.Fatalf("received: '%v' but expected: '%v'", err, testData.err)
}
if out != testData.out {
t.Errorf("Unexpected output %v. Expected %v", out, testData.out)
}
})
}
}
var sideBenchmark Side
// 9756914 126.7 ns/op 0 B/op 0 allocs/op // PREV
// 25200660 57.63 ns/op 3 B/op 1 allocs/op // CURRENT
func BenchmarkStringToOrderSide(b *testing.B) {
for b.Loop() {
sideBenchmark, _ = StringToOrderSide("any")
}
}
func TestStringToOrderType(t *testing.T) {
cases := []struct {
in string
out Type
err error
}{
{"limit", Limit, nil},
{"LIMIT", Limit, nil},
{"lImIt", Limit, nil},
{"market", Market, nil},
{"MARKET", Market, nil},
{"mArKeT", Market, nil},
{"immediate_or_cancel", ImmediateOrCancel, nil},
{"IMMEDIATE_OR_CANCEL", ImmediateOrCancel, nil},
{"iMmEdIaTe_Or_CaNcEl", ImmediateOrCancel, nil},
{"iMmEdIaTe Or CaNcEl", ImmediateOrCancel, nil},
{"stop", Stop, nil},
{"STOP", Stop, nil},
{"sToP", Stop, nil},
{"sToP LiMit", StopLimit, nil},
{"ExchangE sToP Limit", StopLimit, nil},
{"trailing_stop", TrailingStop, nil},
{"TRAILING_STOP", TrailingStop, nil},
{"tRaIlInG_sToP", TrailingStop, nil},
{"tRaIlInG sToP", TrailingStop, nil},
{"fOk", FillOrKill, nil},
{"exchange fOk", FillOrKill, nil},
{"ios", IOS, nil},
{"post_ONly", PostOnly, nil},
{"any", AnyType, nil},
{"ANY", AnyType, nil},
{"aNy", AnyType, nil},
{"trigger", Trigger, nil},
{"TRIGGER", Trigger, nil},
{"tRiGgEr", Trigger, nil},
{"conDitiOnal", ConditionalStop, nil},
{"oCo", OCO, nil},
{"mMp", MarketMakerProtection, nil},
{"Mmp_And_Post_oNly", MarketMakerProtectionAndPostOnly, nil},
{"tWaP", TWAP, nil},
{"TWAP", TWAP, nil},
{"woahMan", UnknownType, errUnrecognisedOrderType},
{"chase", Chase, nil},
{"MOVE_ORDER_STOP", TrailingStop, nil},
{"mOVe_OrdeR_StoP", TrailingStop, nil},
{"optimal_limit_IoC", OptimalLimitIOC, nil},
{"Stop_market", StopMarket, nil},
{"liquidation", Liquidation, nil},
{"LiQuidation", Liquidation, nil},
{"take_profit", TakeProfit, nil},
{"Take ProfIt", TakeProfit, nil},
{"TAKE PROFIT MARkEt", TakeProfitMarket, nil},
{"TAKE_PROFIT_MARkEt", TakeProfitMarket, nil},
}
for i := range cases {
testData := &cases[i]
t.Run(testData.in, func(t *testing.T) {
out, err := StringToOrderType(testData.in)
if !errors.Is(err, testData.err) {
t.Fatalf("received: '%v' but expected: '%v'", err, testData.err)
}
if out != testData.out {
t.Errorf("Unexpected output %v. Expected %v", out, testData.out)
}
})
}
}
var typeBenchmark Type
// 5703705 299.9 ns/op 0 B/op 0 allocs/op // PREV
// 16353608 81.23 ns/op 8 B/op 1 allocs/op // CURRENT
func BenchmarkStringToOrderType(b *testing.B) {
for b.Loop() {
typeBenchmark, _ = StringToOrderType("trigger")
}
}
var stringsToOrderStatus = []struct {
in string
out Status
err error
}{
{"any", AnyStatus, nil},
{"ANY", AnyStatus, nil},
{"aNy", AnyStatus, nil},
{"new", New, nil},
{"NEW", New, nil},
{"nEw", New, nil},
{"active", Active, nil},
{"ACTIVE", Active, nil},
{"aCtIvE", Active, nil},
{"partially_filled", PartiallyFilled, nil},
{"PARTIALLY_FILLED", PartiallyFilled, nil},
{"pArTiAlLy_FiLlEd", PartiallyFilled, nil},
{"filled", Filled, nil},
{"FILLED", Filled, nil},
{"fIlLeD", Filled, nil},
{"cancelled", Cancelled, nil},
{"CANCELlED", Cancelled, nil},
{"cAnCellEd", Cancelled, nil},
{"pending_cancel", PendingCancel, nil},
{"PENDING_CANCEL", PendingCancel, nil},
{"pENdInG_cAnCeL", PendingCancel, nil},
{"rejected", Rejected, nil},
{"REJECTED", Rejected, nil},
{"rEjEcTeD", Rejected, nil},
{"expired", Expired, nil},
{"EXPIRED", Expired, nil},
{"eXpIrEd", Expired, nil},
{"hidden", Hidden, nil},
{"HIDDEN", Hidden, nil},
{"hIdDeN", Hidden, nil},
{"market_unavailable", MarketUnavailable, nil},
{"MARKET_UNAVAILABLE", MarketUnavailable, nil},
{"mArKeT_uNaVaIlAbLe", MarketUnavailable, nil},
{"insufficient_balance", InsufficientBalance, nil},
{"INSUFFICIENT_BALANCE", InsufficientBalance, nil},
{"iNsUfFiCiEnT_bAlAnCe", InsufficientBalance, nil},
{"PARTIALLY_CANCELLEd", PartiallyCancelled, nil},
{"partially canceLLed", PartiallyCancelled, nil},
{"opeN", Open, nil},
{"cLosEd", Closed, nil},
{"cancellinG", Cancelling, nil},
{"woahMan", UnknownStatus, errUnrecognisedOrderStatus},
{"PLAcED", New, nil},
{"ACCePTED", New, nil},
{"FAILeD", Rejected, nil},
}
func TestStringToOrderStatus(t *testing.T) {
for i := range stringsToOrderStatus {
testData := &stringsToOrderStatus[i]
t.Run(testData.in, func(t *testing.T) {
out, err := StringToOrderStatus(testData.in)
if !errors.Is(err, testData.err) {
t.Fatalf("received: '%v' but expected: '%v'", err, testData.err)
}
if out != testData.out {
t.Errorf("Unexpected output %v. Expected %v", out, testData.out)
}
})
}
}
var statusBenchmark Status
// 3569052 351.8 ns/op 0 B/op 0 allocs/op // PREV
// 11126791 101.9 ns/op 24 B/op 1 allocs/op // CURRENT
func BenchmarkStringToOrderStatus(b *testing.B) {
for b.Loop() {
statusBenchmark, _ = StringToOrderStatus("market_unavailable")
}
}
func TestUpdateOrderFromModifyResponse(t *testing.T) {
od := Detail{OrderID: "1"}
updated := time.Now()
pair, err := currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
om := ModifyResponse{
ImmediateOrCancel: true,
PostOnly: true,
Price: 1,
Amount: 1,
TriggerPrice: 1,
RemainingAmount: 1,
Exchange: "1",
Type: 1,
Side: 1,
Status: 1,
AssetType: 1,
LastUpdated: updated,
Pair: pair,
}
od.UpdateOrderFromModifyResponse(&om)
if !od.ImmediateOrCancel {
t.Error("Failed to update")
}
if !od.PostOnly {
t.Error("Failed to update")
}
if od.Price != 1 {
t.Error("Failed to update")
}
if od.Amount != 1 {
t.Error("Failed to update")
}
if od.TriggerPrice != 1 {
t.Error("Failed to update")
}
if od.RemainingAmount != 1 {
t.Error("Failed to update")
}
if od.Exchange != "" {
t.Error("Should not be able to update exchange via modify")
}
if od.OrderID != "1" {
t.Error("Failed to update")
}
if od.Type != 1 {
t.Error("Failed to update")
}
if od.Side != 1 {
t.Error("Failed to update")
}
if od.Status != 1 {
t.Error("Failed to update")
}
if od.AssetType != 1 {
t.Error("Failed to update")
}
if od.LastUpdated != updated {
t.Error("Failed to update")
}
if od.Pair.String() != "BTCUSD" {
t.Error("Failed to update")
}
if od.Trades != nil {
t.Error("Failed to update")
}
}
func TestUpdateOrderFromDetail(t *testing.T) {
var leet = "1337"
updated := time.Now()
pair, err := currency.NewPairFromString("BTCUSD")
if err != nil {
t.Fatal(err)
}
id, err := uuid.NewV4()
if err != nil {
t.Fatal(err)
}
var od *Detail
err = od.UpdateOrderFromDetail(nil)
if !errors.Is(err, ErrOrderDetailIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderDetailIsNil)
}
om := &Detail{
ImmediateOrCancel: true,
HiddenOrder: true,
FillOrKill: true,
PostOnly: true,
Leverage: 1,
Price: 1,
Amount: 1,
LimitPriceUpper: 1,
LimitPriceLower: 1,
TriggerPrice: 1,
QuoteAmount: 1,
ExecutedAmount: 1,
RemainingAmount: 1,
Fee: 1,
Exchange: "1",
InternalOrderID: id,
OrderID: "1",
AccountID: "1",
ClientID: "1",
ClientOrderID: "DukeOfWombleton",
WalletAddress: "1",
Type: 1,
Side: 1,
Status: 1,
AssetType: 1,
LastUpdated: updated,
Pair: pair,
Trades: []TradeHistory{},
}
od = &Detail{Exchange: "test"}
err = od.UpdateOrderFromDetail(nil)
if !errors.Is(err, ErrOrderDetailIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderDetailIsNil)
}
err = od.UpdateOrderFromDetail(om)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if od.InternalOrderID != id {
t.Error("Failed to initialize the internal order ID")
}
if !od.ImmediateOrCancel {
t.Error("Failed to update")
}
if !od.HiddenOrder {
t.Error("Failed to update")
}
if !od.FillOrKill {
t.Error("Failed to update")
}
if !od.PostOnly {
t.Error("Failed to update")
}
if od.Leverage != 1 {
t.Error("Failed to update")
}
if od.Price != 1 {
t.Error("Failed to update")
}
if od.Amount != 1 {
t.Error("Failed to update")
}
if od.LimitPriceLower != 1 {
t.Error("Failed to update")
}
if od.LimitPriceUpper != 1 {
t.Error("Failed to update")
}
if od.TriggerPrice != 1 {
t.Error("Failed to update")
}
if od.QuoteAmount != 1 {
t.Error("Failed to update")
}
if od.ExecutedAmount != 1 {
t.Error("Failed to update")
}
if od.RemainingAmount != 1 {
t.Error("Failed to update")
}
if od.Fee != 1 {
t.Error("Failed to update")
}
if od.Exchange != "test" {
t.Error("Should not be able to update exchange via modify")
}
if od.OrderID != "1" {
t.Error("Failed to update")
}
if od.ClientID != "1" {
t.Error("Failed to update")
}
if od.ClientOrderID != "DukeOfWombleton" {
t.Error("Failed to update")
}
if od.WalletAddress != "1" {
t.Error("Failed to update")
}
if od.Type != 1 {
t.Error("Failed to update")
}
if od.Side != 1 {
t.Error("Failed to update")
}
if od.Status != 1 {
t.Error("Failed to update")
}
if od.AssetType != 1 {
t.Error("Failed to update")
}
if od.LastUpdated != updated {
t.Error("Failed to update")
}
if od.Pair.String() != "BTCUSD" {
t.Error("Failed to update")
}
if od.Trades != nil {
t.Error("Failed to update")
}
om.Trades = append(om.Trades, TradeHistory{TID: "1"}, TradeHistory{TID: "2"})
err = od.UpdateOrderFromDetail(om)
if err != nil {
t.Fatal(err)
}
if len(od.Trades) != 2 {
t.Error("Failed to add trades")
}
om.Trades[0].Exchange = leet
om.Trades[0].Price = 1337
om.Trades[0].Fee = 1337
om.Trades[0].IsMaker = true
om.Trades[0].Timestamp = updated
om.Trades[0].Description = leet
om.Trades[0].Side = UnknownSide
om.Trades[0].Type = UnknownType
om.Trades[0].Amount = 1337
err = od.UpdateOrderFromDetail(om)
if err != nil {
t.Fatal(err)
}
if od.Trades[0].Exchange == leet {
t.Error("Should not be able to update exchange from update")
}
if od.Trades[0].Price != 1337 {
t.Error("Failed to update trades")
}
if od.Trades[0].Fee != 1337 {
t.Error("Failed to update trades")
}
if !od.Trades[0].IsMaker {
t.Error("Failed to update trades")
}
if od.Trades[0].Timestamp != updated {
t.Error("Failed to update trades")
}
if od.Trades[0].Description != leet {
t.Error("Failed to update trades")
}
if od.Trades[0].Side != UnknownSide {
t.Error("Failed to update trades")
}
if od.Trades[0].Type != UnknownType {
t.Error("Failed to update trades")
}
if od.Trades[0].Amount != 1337 {
t.Error("Failed to update trades")
}
id, err = uuid.NewV4()
if err != nil {
t.Fatal(err)
}
om = &Detail{
InternalOrderID: id,
}
err = od.UpdateOrderFromDetail(om)
if err != nil {
t.Fatal(err)
}
if od.InternalOrderID == id {
t.Error("Should not be able to update the internal order ID after initialization")
}
}
func TestClassificationError_Error(t *testing.T) {
class := ClassificationError{OrderID: "1337", Exchange: "test", Err: errors.New("test error")}
if class.Error() != "Exchange test: OrderID: 1337 classification error: test error" {
t.Fatal("unexpected output")
}
class.OrderID = ""
if class.Error() != "Exchange test: classification error: test error" {
t.Fatal("unexpected output")
}
}
func TestValidationOnOrderTypes(t *testing.T) {
var cancelMe *Cancel
if cancelMe.Validate() != ErrCancelOrderIsNil {
t.Fatal("unexpected error")
}
cancelMe = new(Cancel)
err := cancelMe.Validate()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = cancelMe.Validate(cancelMe.PairAssetRequired())
if err == nil || err.Error() != ErrPairIsEmpty.Error() {
t.Errorf("received '%v' expected '%v'", err, ErrPairIsEmpty)
}
cancelMe.Pair = currency.NewPair(currency.BTC, currency.USDT)
err = cancelMe.Validate(cancelMe.PairAssetRequired())
if err == nil || err.Error() != ErrAssetNotSet.Error() {
t.Errorf("received '%v' expected '%v'", err, ErrAssetNotSet)
}
cancelMe.AssetType = asset.Spot
err = cancelMe.Validate(cancelMe.PairAssetRequired())
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if cancelMe.Validate(cancelMe.StandardCancel()) == nil {
t.Fatal("expected error")
}
if cancelMe.Validate(validate.Check(func() error {
return nil
})) != nil {
t.Fatal("should return nil")
}
cancelMe.OrderID = "1337"
if cancelMe.Validate(cancelMe.StandardCancel()) != nil {
t.Fatal("should return nil")
}
var getOrders *MultiOrderRequest
err = getOrders.Validate()
if !errors.Is(err, ErrGetOrdersRequestIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrGetOrdersRequestIsNil)
}
getOrders = new(MultiOrderRequest)
err = getOrders.Validate()
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}
getOrders.AssetType = asset.Spot
err = getOrders.Validate()
if !errors.Is(err, ErrSideIsInvalid) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrSideIsInvalid)
}
getOrders.Side = AnySide
err = getOrders.Validate()
if !errors.Is(err, errUnrecognisedOrderType) {
t.Fatalf("received: '%v' but expected: '%v'", err, errUnrecognisedOrderType)
}
var errTestError = errors.New("test error")
getOrders.Type = AnyType
err = getOrders.Validate(validate.Check(func() error {
return errTestError
}))
if !errors.Is(err, errTestError) {
t.Fatalf("received: '%v' but expected: '%v'", err, errTestError)
}
err = getOrders.Validate(validate.Check(func() error {
return nil
}))
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
var modifyOrder *Modify
if modifyOrder.Validate() != ErrModifyOrderIsNil {
t.Fatal("unexpected error")
}
modifyOrder = new(Modify)
if modifyOrder.Validate() != ErrPairIsEmpty {
t.Fatal("unexpected error")
}
p, err := currency.NewPairFromString("BTC-USD")
if err != nil {
t.Fatal(err)
}
modifyOrder.Pair = p
if modifyOrder.Validate() != ErrAssetNotSet {
t.Fatal("unexpected error")
}
modifyOrder.AssetType = asset.Spot
if modifyOrder.Validate() != ErrOrderIDNotSet {
t.Fatal("unexpected error")
}
modifyOrder.ClientOrderID = "1337"
if modifyOrder.Validate() != nil {
t.Fatal("should not error")
}
if modifyOrder.Validate(validate.Check(func() error {
return errors.New("this should error")
})) == nil {
t.Fatal("expected error")
}
if modifyOrder.Validate(validate.Check(func() error {
return nil
})) != nil {
t.Fatal("unexpected error")
}
}
func TestMatchFilter(t *testing.T) {
t.Parallel()
id, err := uuid.NewV4()
if err != nil {
t.Fatal(err)
}
filters := map[int]*Filter{
0: {},
1: {Exchange: "Binance"},
2: {InternalOrderID: id},
3: {OrderID: "2222"},
4: {ClientOrderID: "3333"},
5: {ClientID: "4444"},
6: {WalletAddress: "5555"},
7: {Type: AnyType},
8: {Type: Limit},
9: {Side: AnySide},
10: {Side: Sell},
11: {Status: AnyStatus},
12: {Status: New},
13: {AssetType: asset.Spot},
14: {Pair: currency.NewPair(currency.BTC, currency.USD)},
15: {Exchange: "Binance", Type: Limit, Status: New},
16: {Exchange: "Binance", Type: AnyType},
17: {AccountID: "8888"},
}
orders := map[int]Detail{
0: {},
1: {Exchange: "Binance"},
2: {InternalOrderID: id},
3: {OrderID: "2222"},
4: {ClientOrderID: "3333"},
5: {ClientID: "4444"},
6: {WalletAddress: "5555"},
7: {Type: AnyType},
8: {Type: Limit},
9: {Side: AnySide},
10: {Side: Sell},
11: {Status: AnyStatus},
12: {Status: New},
13: {AssetType: asset.Spot},
14: {Pair: currency.NewPair(currency.BTC, currency.USD)},
15: {Exchange: "Binance", Type: Limit, Status: New},
16: {AccountID: "8888"},
}
// empty filter tests
emptyFilter := filters[0]
for _, o := range orders {
if !o.MatchFilter(emptyFilter) {
t.Error("empty filter should match everything")
}
}
tests := map[int]struct {
f *Filter
o Detail
expectedResult bool
}{
0: {filters[1], orders[1], true},
1: {filters[1], orders[0], false},
2: {filters[2], orders[2], true},
3: {filters[2], orders[3], false},
4: {filters[3], orders[3], true},
5: {filters[3], orders[4], false},
6: {filters[4], orders[4], true},
7: {filters[4], orders[5], false},
8: {filters[5], orders[5], true},
9: {filters[5], orders[6], false},
10: {filters[6], orders[6], true},
11: {filters[6], orders[7], false},
12: {filters[7], orders[7], true},
13: {filters[7], orders[8], true},
14: {filters[7], orders[9], true},
15: {filters[8], orders[7], false},
16: {filters[8], orders[8], true},
17: {filters[8], orders[9], false},
18: {filters[9], orders[9], true},
19: {filters[9], orders[10], true},
20: {filters[9], orders[11], true},
21: {filters[10], orders[10], true},
22: {filters[10], orders[11], false},
23: {filters[10], orders[9], false},
24: {filters[11], orders[11], true},
25: {filters[11], orders[12], true},
26: {filters[11], orders[10], true},
27: {filters[12], orders[12], true},
28: {filters[12], orders[13], false},
29: {filters[12], orders[11], false},
30: {filters[13], orders[13], true},
31: {filters[13], orders[12], false},
32: {filters[14], orders[14], true},
33: {filters[14], orders[13], false},
34: {filters[15], orders[15], true},
35: {filters[16], orders[15], true},
36: {filters[17], orders[16], true},
37: {filters[17], orders[15], false},
}
// specific tests
for num, tt := range tests {
t.Run(strconv.Itoa(num), func(t *testing.T) {
t.Parallel()
if tt.o.MatchFilter(tt.f) != tt.expectedResult {
t.Errorf("tests[%v] failed", num)
}
})
}
}
func TestIsActive(t *testing.T) {
orders := map[int]Detail{
0: {Amount: 0.0, Status: Active},
1: {Amount: 1.0, ExecutedAmount: 0.9, Status: Active},
2: {Amount: 1.0, ExecutedAmount: 1.0, Status: Active},
3: {Amount: 1.0, ExecutedAmount: 1.1, Status: Active},
}
amountTests := map[int]struct {
o Detail
expectedResult bool
}{
0: {orders[0], false},
1: {orders[1], true},
2: {orders[2], false},
3: {orders[3], false},
}
// specific tests
for num, tt := range amountTests {
if tt.o.IsActive() != tt.expectedResult {
t.Errorf("amountTests[%v] failed", num)
}
}
statusTests := map[int]struct {
o Detail
expectedResult bool
}{
// For now force inactive on any status
0: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: AnyStatus}, false},
1: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: New}, true},
2: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Active}, true},
3: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PartiallyCancelled}, false},
4: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PartiallyFilled}, true},
5: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Filled}, false},
6: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Cancelled}, false},
7: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PendingCancel}, true},
8: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: InsufficientBalance}, false},
9: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: MarketUnavailable}, false},
10: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Rejected}, false},
11: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Expired}, false},
12: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Hidden}, true},
// For now force inactive on unknown status
13: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: UnknownStatus}, false},
14: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Open}, true},
15: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: AutoDeleverage}, true},
16: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Closed}, false},
17: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Pending}, true},
}
// specific tests
for num, tt := range statusTests {
if tt.o.IsActive() != tt.expectedResult {
t.Fatalf("statusTests[%v] failed", num)
}
}
}
var activeBenchmark = Detail{Status: Pending, Amount: 1}
// 610732089 2.414 ns/op 0 B/op 0 allocs/op // PREV
// 1000000000 1.188 ns/op 0 B/op 0 allocs/op // CURRENT
func BenchmarkIsActive(b *testing.B) {
for b.Loop() {
if !activeBenchmark.IsActive() {
b.Fatal("expected true")
}
}
}
func TestIsInactive(t *testing.T) {
orders := map[int]Detail{
0: {Amount: 0.0, Status: Active},
1: {Amount: 1.0, ExecutedAmount: 0.9, Status: Active},
2: {Amount: 1.0, ExecutedAmount: 1.0, Status: Active},
3: {Amount: 1.0, ExecutedAmount: 1.1, Status: Active},
}
amountTests := map[int]struct {
o Detail
expectedResult bool
}{
0: {orders[0], true},
1: {orders[1], false},
2: {orders[2], true},
3: {orders[3], true},
}
// specific tests
for num, tt := range amountTests {
if tt.o.IsInactive() != tt.expectedResult {
t.Errorf("amountTests[%v] failed", num)
}
}
statusTests := map[int]struct {
o Detail
expectedResult bool
}{
// For now force inactive on any status
0: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: AnyStatus}, true},
1: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: New}, false},
2: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Active}, false},
3: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PartiallyCancelled}, true},
4: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PartiallyFilled}, false},
5: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Filled}, true},
6: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Cancelled}, true},
7: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PendingCancel}, false},
8: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: InsufficientBalance}, true},
9: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: MarketUnavailable}, true},
10: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Rejected}, true},
11: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Expired}, true},
12: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Hidden}, false},
// For now force inactive on unknown status
13: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: UnknownStatus}, true},
14: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Open}, false},
15: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: AutoDeleverage}, false},
16: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Closed}, true},
17: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Pending}, false},
}
// specific tests
for num, tt := range statusTests {
if tt.o.IsInactive() != tt.expectedResult {
t.Errorf("statusTests[%v] failed", num)
}
}
}
var inactiveBenchmark = Detail{Status: Closed, Amount: 1}
// 1000000000 1.043 ns/op 0 B/op 0 allocs/op // CURRENT
func BenchmarkIsInactive(b *testing.B) {
for b.Loop() {
if !inactiveBenchmark.IsInactive() {
b.Fatal("expected true")
}
}
}
func TestIsOrderPlaced(t *testing.T) {
t.Parallel()
statusTests := map[int]struct {
o Detail
expectedResult bool
}{
0: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: AnyStatus}, false},
1: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: New}, true},
2: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Active}, true},
3: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PartiallyCancelled}, true},
4: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PartiallyFilled}, true},
5: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Filled}, true},
6: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Cancelled}, true},
7: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: PendingCancel}, true},
8: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: InsufficientBalance}, false},
9: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: MarketUnavailable}, false},
10: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Rejected}, false},
11: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Expired}, true},
12: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Hidden}, true},
13: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: UnknownStatus}, false},
14: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Open}, true},
15: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: AutoDeleverage}, true},
16: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Closed}, true},
17: {Detail{Amount: 1.0, ExecutedAmount: 0.0, Status: Pending}, true},
}
// specific tests
for num, tt := range statusTests {
t.Run(fmt.Sprintf("TEST CASE: %d", num), func(t *testing.T) {
t.Parallel()
if tt.o.WasOrderPlaced() != tt.expectedResult {
t.Errorf("statusTests[%v] failed", num)
}
})
}
}
func TestGenerateInternalOrderID(t *testing.T) {
id, err := uuid.NewV4()
if err != nil {
t.Errorf("unable to create uuid: %s", err)
}
od := Detail{
InternalOrderID: id,
}
od.GenerateInternalOrderID()
if od.InternalOrderID != id {
t.Error("Should not be able to generate a new internal order ID")
}
od = Detail{}
od.GenerateInternalOrderID()
if od.InternalOrderID.IsNil() {
t.Error("unable to generate internal order ID")
}
}
func TestDetail_Copy(t *testing.T) {
t.Parallel()
d := []Detail{
{
Exchange: "Binance",
},
{
Exchange: "Binance",
Trades: []TradeHistory{
{Price: 1},
},
},
}
for i := range d {
r := d[i].Copy()
if !reflect.DeepEqual(d[i], r) {
t.Errorf("[%d] Copy does not contain same elements, expected: %v\ngot:%v", i, d[i], r)
}
if len(d[i].Trades) > 0 {
if &d[i].Trades[0] == &r.Trades[0] {
t.Errorf("[%d]Trades point to the same data elements", i)
}
}
}
}
func TestDetail_CopyToPointer(t *testing.T) {
t.Parallel()
d := []Detail{
{
Exchange: "Binance",
},
{
Exchange: "Binance",
Trades: []TradeHistory{
{Price: 1},
},
},
}
for i := range d {
r := d[i].CopyToPointer()
if !reflect.DeepEqual(d[i], *r) {
t.Errorf("[%d] Copy does not contain same elements, expected: %v\ngot:%v", i, d[i], r)
}
if len(d[i].Trades) > 0 {
if &d[i].Trades[0] == &r.Trades[0] {
t.Errorf("[%d]Trades point to the same data elements", i)
}
}
}
}
func TestDetail_CopyPointerOrderSlice(t *testing.T) {
t.Parallel()
d := []*Detail{
{
Exchange: "Binance",
},
{
Exchange: "Binance",
Trades: []TradeHistory{
{Price: 1},
},
},
}
sliceCopy := CopyPointerOrderSlice(d)
for i := range sliceCopy {
if !reflect.DeepEqual(*sliceCopy[i], *d[i]) {
t.Errorf("[%d] Copy does not contain same elements, expected: %v\ngot:%v", i, sliceCopy[i], d[i])
}
if len(sliceCopy[i].Trades) > 0 {
if &sliceCopy[i].Trades[0] == &d[i].Trades[0] {
t.Errorf("[%d]Trades point to the same data elements", i)
}
}
}
}
func TestDeriveModify(t *testing.T) {
t.Parallel()
var o *Detail
_, err := o.DeriveModify()
require.ErrorIs(t, err, errOrderDetailIsNil)
pair := currency.NewPair(currency.BTC, currency.AUD)
o = &Detail{
Exchange: "wow",
OrderID: "wow2",
ClientOrderID: "wow3",
Type: Market,
Side: Long,
AssetType: asset.Futures,
Pair: pair,
}
mod, err := o.DeriveModify()
require.NoError(t, err)
require.NotNil(t, mod)
exp := &Modify{
Exchange: "wow",
OrderID: "wow2",
ClientOrderID: "wow3",
Type: Market,
Side: Long,
AssetType: asset.Futures,
Pair: pair,
}
assert.Equal(t, exp, mod)
}
func TestDeriveModifyResponse(t *testing.T) {
t.Parallel()
var mod *Modify
_, err := mod.DeriveModifyResponse()
require.ErrorIs(t, err, errOrderDetailIsNil)
pair := currency.NewPair(currency.BTC, currency.AUD)
mod = &Modify{
Exchange: "wow",
OrderID: "wow2",
ClientOrderID: "wow3",
Type: Market,
Side: Long,
AssetType: asset.Futures,
Pair: pair,
}
modresp, err := mod.DeriveModifyResponse()
require.NoError(t, err, "DeriveModifyResponse must not error")
require.NotNil(t, modresp)
exp := &ModifyResponse{
Exchange: "wow",
OrderID: "wow2",
ClientOrderID: "wow3",
Type: Market,
Side: Long,
AssetType: asset.Futures,
Pair: pair,
}
assert.Equal(t, exp, modresp)
}
func TestDeriveCancel(t *testing.T) {
t.Parallel()
var o *Detail
if _, err := o.DeriveCancel(); !errors.Is(err, errOrderDetailIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errOrderDetailIsNil)
}
pair := currency.NewPair(currency.BTC, currency.AUD)
o = &Detail{
Exchange: "wow",
OrderID: "wow1",
AccountID: "wow2",
ClientID: "wow3",
ClientOrderID: "wow4",
WalletAddress: "wow5",
Type: Market,
Side: Long,
Pair: pair,
AssetType: asset.Futures,
}
cancel, err := o.DeriveCancel()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if cancel.Exchange != "wow" ||
cancel.OrderID != "wow1" ||
cancel.AccountID != "wow2" ||
cancel.ClientID != "wow3" ||
cancel.ClientOrderID != "wow4" ||
cancel.WalletAddress != "wow5" ||
cancel.Type != Market ||
cancel.Side != Long ||
!cancel.Pair.Equal(pair) ||
cancel.AssetType != asset.Futures {
t.Fatalf("unexpected values %+v", cancel)
}
}
func TestGetOrdersRequest_Filter(t *testing.T) {
request := new(MultiOrderRequest)
request.AssetType = asset.Spot
request.Type = AnyType
request.Side = AnySide
var orders = []Detail{
{OrderID: "0", Pair: btcusd, AssetType: asset.Spot, Type: Limit, Side: Buy},
{OrderID: "1", Pair: btcusd, AssetType: asset.Spot, Type: Limit, Side: Sell},
{OrderID: "2", Pair: btcusd, AssetType: asset.Spot, Type: Market, Side: Buy},
{OrderID: "3", Pair: btcusd, AssetType: asset.Spot, Type: Market, Side: Sell},
{OrderID: "4", Pair: btcusd, AssetType: asset.Futures, Type: Limit, Side: Buy},
{OrderID: "5", Pair: btcusd, AssetType: asset.Futures, Type: Limit, Side: Sell},
{OrderID: "6", Pair: btcusd, AssetType: asset.Futures, Type: Market, Side: Buy},
{OrderID: "7", Pair: btcusd, AssetType: asset.Futures, Type: Market, Side: Sell},
{OrderID: "8", Pair: btcltc, AssetType: asset.Spot, Type: Limit, Side: Buy},
{OrderID: "9", Pair: btcltc, AssetType: asset.Spot, Type: Limit, Side: Sell},
{OrderID: "10", Pair: btcltc, AssetType: asset.Spot, Type: Market, Side: Buy},
{OrderID: "11", Pair: btcltc, AssetType: asset.Spot, Type: Market, Side: Sell},
{OrderID: "12", Pair: btcltc, AssetType: asset.Futures, Type: Limit, Side: Buy},
{OrderID: "13", Pair: btcltc, AssetType: asset.Futures, Type: Limit, Side: Sell},
{OrderID: "14", Pair: btcltc, AssetType: asset.Futures, Type: Market, Side: Buy},
{OrderID: "15", Pair: btcltc, AssetType: asset.Futures, Type: Market, Side: Sell},
}
shinyAndClean := request.Filter("test", orders)
if len(shinyAndClean) != 16 {
t.Fatalf("received: '%v' but expected: '%v'", len(shinyAndClean), 16)
}
for x := range shinyAndClean {
if strconv.FormatInt(int64(x), 10) != shinyAndClean[x].OrderID {
t.Fatalf("received: '%v' but expected: '%v'", shinyAndClean[x].OrderID, int64(x))
}
}
request.Pairs = []currency.Pair{btcltc}
// Kicks off time error
request.EndTime = time.Unix(1336, 0)
request.StartTime = time.Unix(1337, 0)
shinyAndClean = request.Filter("test", orders)
if len(shinyAndClean) != 8 {
t.Fatalf("received: '%v' but expected: '%v'", len(shinyAndClean), 8)
}
for x := range shinyAndClean {
if strconv.FormatInt(int64(x)+8, 10) != shinyAndClean[x].OrderID {
t.Fatalf("received: '%v' but expected: '%v'", shinyAndClean[x].OrderID, int64(x)+8)
}
}
}
func TestIsValidOrderSubmissionSide(t *testing.T) {
t.Parallel()
if IsValidOrderSubmissionSide(UnknownSide) {
t.Error("expected false")
}
if !IsValidOrderSubmissionSide(Buy) {
t.Error("expected true")
}
if IsValidOrderSubmissionSide(CouldNotBuy) {
t.Error("expected false")
}
}
func TestAdjustBaseAmount(t *testing.T) {
t.Parallel()
var s *SubmitResponse
err := s.AdjustBaseAmount(0)
if !errors.Is(err, errOrderSubmitResponseIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errOrderSubmitResponseIsNil)
}
s = &SubmitResponse{}
err = s.AdjustBaseAmount(0)
if !errors.Is(err, errAmountIsZero) {
t.Fatalf("received: '%v' but expected: '%v'", err, errAmountIsZero)
}
s.Amount = 1.7777777777
err = s.AdjustBaseAmount(1.7777777777)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if s.Amount != 1.7777777777 {
t.Fatalf("received: '%v' but expected: '%v'", s.Amount, 1.7777777777)
}
s.Amount = 1.7777777777
err = s.AdjustBaseAmount(1.777)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if s.Amount != 1.777 {
t.Fatalf("received: '%v' but expected: '%v'", s.Amount, 1.777)
}
}
func TestAdjustQuoteAmount(t *testing.T) {
t.Parallel()
var s *SubmitResponse
err := s.AdjustQuoteAmount(0)
if !errors.Is(err, errOrderSubmitResponseIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errOrderSubmitResponseIsNil)
}
s = &SubmitResponse{}
err = s.AdjustQuoteAmount(0)
if !errors.Is(err, errAmountIsZero) {
t.Fatalf("received: '%v' but expected: '%v'", err, errAmountIsZero)
}
s.QuoteAmount = 5.222222222222
err = s.AdjustQuoteAmount(5.222222222222)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if s.QuoteAmount != 5.222222222222 {
t.Fatalf("received: '%v' but expected: '%v'", s.Amount, 5.222222222222)
}
s.QuoteAmount = 5.222222222222
err = s.AdjustQuoteAmount(5.22222222)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if s.QuoteAmount != 5.22222222 {
t.Fatalf("received: '%v' but expected: '%v'", s.Amount, 5.22222222)
}
}
func TestSideUnmarshal(t *testing.T) {
t.Parallel()
var s Side
assert.NoError(t, s.UnmarshalJSON([]byte(`"SELL"`)), "Quoted valid side okay")
assert.Equal(t, Sell, s, "Correctly set order Side")
assert.ErrorIs(t, s.UnmarshalJSON([]byte(`"STEAL"`)), ErrSideIsInvalid, "Quoted invalid side errors")
var jErr *json.UnmarshalTypeError
assert.ErrorAs(t, s.UnmarshalJSON([]byte(`14`)), &jErr, "non-string valid json is rejected")
}
func TestSideMarshalJSON(t *testing.T) {
t.Parallel()
b, err := Buy.MarshalJSON()
assert.NoError(t, err)
assert.Equal(t, `"BUY"`, string(b))
b, err = UnknownSide.MarshalJSON()
assert.NoError(t, err)
assert.Equal(t, `"UNKNOWN"`, string(b))
}
func TestGetTradeAmount(t *testing.T) {
t.Parallel()
var s *Submit
require.Zero(t, s.GetTradeAmount(protocol.TradingRequirements{}))
baseAmount := 420.0
quoteAmount := 69.0
s = &Submit{Amount: baseAmount, QuoteAmount: quoteAmount}
// below will default to base amount with nothing set
require.Equal(t, baseAmount, s.GetTradeAmount(protocol.TradingRequirements{}))
require.Equal(t, baseAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketOrderAmountPurchaseQuotationOnly: true}))
s.AssetType = asset.Spot
s.Type = Market
s.Side = Buy
require.Equal(t, quoteAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketOrderAmountPurchaseQuotationOnly: true}))
require.Equal(t, baseAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketOrderAmountSellBaseOnly: true}))
s.Side = Sell
require.Equal(t, baseAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketOrderAmountSellBaseOnly: true}))
}
func TestStringToTrackingMode(t *testing.T) {
t.Parallel()
inputs := map[string]TrackingMode{
"diStance": Distance,
"distance": Distance,
"Percentage": Percentage,
"percentage": Percentage,
"": UnknownTrackingMode,
}
for k, v := range inputs {
assert.Equal(t, v, StringToTrackingMode(k))
}
}
func TestTrackingModeString(t *testing.T) {
t.Parallel()
inputs := map[TrackingMode]string{
Distance: "distance",
Percentage: "percentage",
UnknownTrackingMode: "",
}
for k, v := range inputs {
require.Equal(t, v, k.String())
}
}
func TestMarshalOrder(t *testing.T) {
t.Parallel()
btx := currency.NewBTCUSDT()
btx.Delimiter = "-"
orderSubmit := Submit{
Exchange: "test",
Pair: btx,
AssetType: asset.Spot,
MarginType: margin.Multi,
Side: Buy,
Type: Market,
Amount: 1,
Price: 1000,
}
j, err := json.Marshal(orderSubmit)
require.NoError(t, err, "json.Marshal must not error")
exp := []byte(`{"Exchange":"test","Type":4,"Side":"BUY","Pair":"BTC-USDT","AssetType":"spot","ImmediateOrCancel":false,"FillOrKill":false,"PostOnly":false,"ReduceOnly":false,"Leverage":0,"Price":1000,"Amount":1,"QuoteAmount":0,"TriggerPrice":0,"TriggerPriceType":0,"ClientID":"","ClientOrderID":"","AutoBorrow":false,"MarginType":"multi","RetrieveFees":false,"RetrieveFeeDelay":0,"RiskManagementModes":{"Mode":"","TakeProfit":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0},"StopLoss":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0},"StopEntry":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0}},"Hidden":false,"Iceberg":false,"TrackingMode":0,"TrackingValue":0}`)
assert.Equal(t, exp, j)
}