mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-14 15:09:51 +00:00
* orderbook/buffer: data integrity and resubscription pass * btcmarkets: REMOVE THAT LIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIINE!!!!!!!!!!!!!!!!! * buffer: reinstate publish, refaactor, invalidate more and comments * buffer/orderbook: improve update and snapshot performance. Move Update type to orderbook package to util. pointer through entire function calls. (cleanup). Change action string to uint8 for easier comparison. Add parsing helper. Update current test benchmark comments. * dispatch: change publish func to variadic id param * dispatch: remove sender receiver wait time as this adds overhead and complexity. update tests. * dispatch: don't create pointers for every job container * rpcserver: fix assertion issues with data publishing change * linter: fixes * glorious: nits addr * depth: change validation handling to incorporate and store err * linter: fix more issues * dispatch: fix race * travis: update before fetching * depth: wrap and return wrapped error in invalidate call and fix tests * btcmarkets: fix commenting * workflow: check * workflow: check * orderbook: check error * buffer/depth: return invalidation error and fix tests * gctcli: display errors on orderbook streams * buffer: remove unused types * orderbook/bitmex: shift function to bitmex * orderbook: Add specific comments to unexported functions that don't have locking require locking. * orderbook: restrict published data functionality to orderbook.Outbound interface * common: add assertion failure helper for error * dispatch: remove atomics, add mutex protection, remove add/remove worker, redo main tests * dispatch: export function * engine: revert and change sub logger to manager * engine: remove old test * dispatch: add common variable ;) * btcmarket: don't overflow int in tests on 32bit systems * ci: force 1.17.7 usage for go * Revert "ci: force 1.17.7 usage for go" This reverts commit af2f95563bf218cf2b9f36a9fcf3258e2c6a2d91. * golangci: bump version add and remove linter items * Revert "golangci: bump version add and remove linter items" This reverts commit 3c98bffc9d030e39faca0387ea40c151df2ab06b. * dispatch: remove unsused mutex from mux * order: slight optimizations * nits: glorious * dispatch: fix regression on uuid generation and input inline with master * linter: fix * linter: fix * glorious: nit - rm slice segration * account: fix test after merge * coinbasepro: revert change * account: close channel instead of needing a receiver, push alert in routine to prepare for waiter. Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
1180 lines
28 KiB
Go
1180 lines
28 KiB
Go
package buffer
|
|
|
|
import (
|
|
"errors"
|
|
"math/rand"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/thrasher-corp/gocryptotrader/config"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
|
)
|
|
|
|
var (
|
|
itemArray = [][]orderbook.Item{
|
|
{{Price: 1000, Amount: 1, ID: 1000}},
|
|
{{Price: 2000, Amount: 1, ID: 2000}},
|
|
{{Price: 3000, Amount: 1, ID: 3000}},
|
|
{{Price: 3000, Amount: 2, ID: 4000}},
|
|
{{Price: 4000, Amount: 0, ID: 6000}},
|
|
{{Price: 5000, Amount: 1, ID: 5000}},
|
|
}
|
|
|
|
cp, _ = currency.NewPairFromString("BTCUSD")
|
|
)
|
|
|
|
const (
|
|
exchangeName = "exchangeTest"
|
|
)
|
|
|
|
func createSnapshot() (holder *Orderbook, asks, bids orderbook.Items, err error) {
|
|
asks = orderbook.Items{{Price: 4000, Amount: 1, ID: 6}}
|
|
bids = orderbook.Items{{Price: 4000, Amount: 1, ID: 6}}
|
|
|
|
book := &orderbook.Base{
|
|
Exchange: exchangeName,
|
|
Asks: asks,
|
|
Bids: bids,
|
|
Asset: asset.Spot,
|
|
Pair: cp,
|
|
PriceDuplication: true,
|
|
}
|
|
|
|
newBook := make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder)
|
|
|
|
ch := make(chan interface{})
|
|
go func(<-chan interface{}) { // reader
|
|
for range ch {
|
|
}
|
|
}(ch)
|
|
holder = &Orderbook{
|
|
exchangeName: exchangeName,
|
|
dataHandler: ch,
|
|
ob: newBook,
|
|
}
|
|
err = holder.LoadSnapshot(book)
|
|
return
|
|
}
|
|
|
|
func bidAskGenerator() []orderbook.Item {
|
|
var response []orderbook.Item
|
|
randIterator := 100
|
|
for i := 0; i < randIterator; i++ {
|
|
price := float64(rand.Intn(1000)) // nolint:gosec // no need to import crypo/rand for testing
|
|
if price == 0 {
|
|
price = 1
|
|
}
|
|
response = append(response, orderbook.Item{
|
|
Amount: float64(rand.Intn(10)), // nolint:gosec // no need to import crypo/rand for testing
|
|
Price: price,
|
|
ID: int64(i),
|
|
})
|
|
}
|
|
return response
|
|
}
|
|
|
|
func BenchmarkUpdateBidsByPrice(b *testing.B) {
|
|
ob, _, _, err := createSnapshot()
|
|
if err != nil {
|
|
b.Error(err)
|
|
}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
bidAsks := bidAskGenerator()
|
|
update := &orderbook.Update{
|
|
Bids: bidAsks,
|
|
Asks: bidAsks,
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
}
|
|
holder := ob.ob[cp.Base][cp.Quote][asset.Spot]
|
|
holder.updateByPrice(update)
|
|
}
|
|
}
|
|
|
|
func BenchmarkUpdateAsksByPrice(b *testing.B) {
|
|
ob, _, _, err := createSnapshot()
|
|
if err != nil {
|
|
b.Error(err)
|
|
}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
bidAsks := bidAskGenerator()
|
|
update := &orderbook.Update{
|
|
Bids: bidAsks,
|
|
Asks: bidAsks,
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
}
|
|
holder := ob.ob[cp.Base][cp.Quote][asset.Spot]
|
|
holder.updateByPrice(update)
|
|
}
|
|
}
|
|
|
|
// BenchmarkBufferPerformance demonstrates buffer more performant than multi
|
|
// process calls
|
|
// 890016 1688 ns/op 416 B/op 3 allocs/op
|
|
func BenchmarkBufferPerformance(b *testing.B) {
|
|
holder, asks, bids, err := createSnapshot()
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
holder.bufferEnabled = true
|
|
update := &orderbook.Update{
|
|
Bids: bids,
|
|
Asks: asks,
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
randomIndex := rand.Intn(4) // nolint:gosec // no need to import crypo/rand for testing
|
|
update.Asks = itemArray[randomIndex]
|
|
update.Bids = itemArray[randomIndex]
|
|
err = holder.Update(update)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkBufferSortingPerformance benchmark
|
|
// 613964 2093 ns/op 440 B/op 4 allocs/op
|
|
func BenchmarkBufferSortingPerformance(b *testing.B) {
|
|
holder, asks, bids, err := createSnapshot()
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
holder.bufferEnabled = true
|
|
holder.sortBuffer = true
|
|
update := &orderbook.Update{
|
|
Bids: bids,
|
|
Asks: asks,
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
randomIndex := rand.Intn(4) // nolint:gosec // no need to import crypo/rand for testing
|
|
update.Asks = itemArray[randomIndex]
|
|
update.Bids = itemArray[randomIndex]
|
|
err = holder.Update(update)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkBufferSortingPerformance benchmark
|
|
// 914500 1599 ns/op 440 B/op 4 allocs/op
|
|
func BenchmarkBufferSortingByIDPerformance(b *testing.B) {
|
|
holder, asks, bids, err := createSnapshot()
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
holder.bufferEnabled = true
|
|
holder.sortBuffer = true
|
|
holder.sortBufferByUpdateIDs = true
|
|
update := &orderbook.Update{
|
|
Bids: bids,
|
|
Asks: asks,
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
randomIndex := rand.Intn(4) // nolint:gosec // no need to import crypo/rand for testing
|
|
update.Asks = itemArray[randomIndex]
|
|
update.Bids = itemArray[randomIndex]
|
|
err = holder.Update(update)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkNoBufferPerformance demonstrates orderbook process more performant
|
|
// than buffer
|
|
// 122659 12792 ns/op 972 B/op 7 allocs/op PRIOR
|
|
// 1225924 1028 ns/op 240 B/op 2 allocs/op CURRENT
|
|
|
|
func BenchmarkNoBufferPerformance(b *testing.B) {
|
|
obl, asks, bids, err := createSnapshot()
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
update := &orderbook.Update{
|
|
Bids: bids,
|
|
Asks: asks,
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
}
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
randomIndex := rand.Intn(4) // nolint:gosec // no need to import crypo/rand for testing
|
|
update.Asks = itemArray[randomIndex]
|
|
update.Bids = itemArray[randomIndex]
|
|
err = obl.Update(update)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdates(t *testing.T) {
|
|
holder, _, _, err := createSnapshot()
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
|
book.updateByPrice(&orderbook.Update{
|
|
Bids: itemArray[5],
|
|
Asks: itemArray[5],
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
book.updateByPrice(&orderbook.Update{
|
|
Bids: itemArray[0],
|
|
Asks: itemArray[0],
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
askLen, err := book.ob.GetAskLength()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
if askLen != 3 {
|
|
t.Error("Did not update")
|
|
}
|
|
}
|
|
|
|
// TestHittingTheBuffer logic test
|
|
func TestHittingTheBuffer(t *testing.T) {
|
|
holder, _, _, err := createSnapshot()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
holder.bufferEnabled = true
|
|
holder.obBufferLimit = 5
|
|
for i := range itemArray {
|
|
asks := itemArray[i]
|
|
bids := itemArray[i]
|
|
err = holder.Update(&orderbook.Update{
|
|
Bids: bids,
|
|
Asks: asks,
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
|
askLen, err := book.ob.GetAskLength()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
if askLen != 3 {
|
|
t.Errorf("expected 3 entries, received: %v", askLen)
|
|
}
|
|
|
|
bidLen, err := book.ob.GetBidLength()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
if bidLen != 3 {
|
|
t.Errorf("expected 3 entries, received: %v", bidLen)
|
|
}
|
|
}
|
|
|
|
// TestInsertWithIDs logic test
|
|
func TestInsertWithIDs(t *testing.T) {
|
|
holder, _, _, err := createSnapshot()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
holder.bufferEnabled = true
|
|
holder.updateEntriesByID = true
|
|
holder.obBufferLimit = 5
|
|
for i := range itemArray {
|
|
asks := itemArray[i]
|
|
if asks[0].Amount <= 0 {
|
|
continue
|
|
}
|
|
bids := itemArray[i]
|
|
err = holder.Update(&orderbook.Update{
|
|
Bids: bids,
|
|
Asks: asks,
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
Action: orderbook.UpdateInsert,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
|
askLen, err := book.ob.GetAskLength()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
if askLen != 6 {
|
|
t.Errorf("expected 6 entries, received: %v", askLen)
|
|
}
|
|
|
|
bidLen, err := book.ob.GetBidLength()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
if bidLen != 6 {
|
|
t.Errorf("expected 6 entries, received: %v", bidLen)
|
|
}
|
|
}
|
|
|
|
// TestSortIDs logic test
|
|
func TestSortIDs(t *testing.T) {
|
|
holder, _, _, err := createSnapshot()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
holder.bufferEnabled = true
|
|
holder.sortBufferByUpdateIDs = true
|
|
holder.sortBuffer = true
|
|
holder.obBufferLimit = 5
|
|
for i := range itemArray {
|
|
asks := itemArray[i]
|
|
bids := itemArray[i]
|
|
err = holder.Update(&orderbook.Update{
|
|
Bids: bids,
|
|
Asks: asks,
|
|
Pair: cp,
|
|
UpdateID: int64(i),
|
|
Asset: asset.Spot,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
|
askLen, err := book.ob.GetAskLength()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
if askLen != 3 {
|
|
t.Errorf("expected 3 entries, received: %v", askLen)
|
|
}
|
|
|
|
bidLen, err := book.ob.GetBidLength()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
if bidLen != 3 {
|
|
t.Errorf("expected 3 entries, received: %v", bidLen)
|
|
}
|
|
}
|
|
|
|
// TestOutOfOrderIDs logic test
|
|
func TestOutOfOrderIDs(t *testing.T) {
|
|
holder, _, _, err := createSnapshot()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
outOFOrderIDs := []int64{2, 1, 5, 3, 4, 6, 7}
|
|
if itemArray[0][0].Price != 1000 {
|
|
t.Errorf("expected sorted price to be 3000, received: %v",
|
|
itemArray[1][0].Price)
|
|
}
|
|
holder.bufferEnabled = true
|
|
holder.sortBuffer = true
|
|
holder.obBufferLimit = 5
|
|
for i := range itemArray {
|
|
asks := itemArray[i]
|
|
err = holder.Update(&orderbook.Update{
|
|
Asks: asks,
|
|
Pair: cp,
|
|
UpdateID: outOFOrderIDs[i],
|
|
Asset: asset.Spot,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
|
cpy, err := book.ob.Retrieve()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
// Index 1 since index 0 is price 7000
|
|
if cpy.Asks[1].Price != 2000 {
|
|
t.Errorf("expected sorted price to be 2000, received: %v", cpy.Asks[1].Price)
|
|
}
|
|
}
|
|
|
|
func TestOrderbookLastUpdateID(t *testing.T) {
|
|
holder, _, _, err := createSnapshot()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if exp := float64(1000); itemArray[0][0].Price != exp {
|
|
t.Errorf("expected sorted price to be %f, received: %v",
|
|
exp, itemArray[1][0].Price)
|
|
}
|
|
|
|
holder.checksum = func(state *orderbook.Base, checksum uint32) error { return errors.New("testerino") }
|
|
|
|
// this update invalidates the book
|
|
err = holder.Update(&orderbook.Update{
|
|
Asks: []orderbook.Item{{Price: 999999}},
|
|
Pair: cp,
|
|
UpdateID: -1,
|
|
Asset: asset.Spot,
|
|
})
|
|
if !errors.Is(err, orderbook.ErrOrderbookInvalid) {
|
|
t.Fatalf("received: %v but expected: %v", err, orderbook.ErrOrderbookInvalid)
|
|
}
|
|
|
|
holder, _, _, err = createSnapshot()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
holder.checksum = func(state *orderbook.Base, checksum uint32) error { return nil }
|
|
holder.updateIDProgression = true
|
|
|
|
for i := range itemArray {
|
|
asks := itemArray[i]
|
|
err = holder.Update(&orderbook.Update{
|
|
Asks: asks,
|
|
Pair: cp,
|
|
UpdateID: int64(i) + 1,
|
|
Asset: asset.Spot,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// out of order
|
|
holder.verbose = true
|
|
err = holder.Update(&orderbook.Update{
|
|
Asks: []orderbook.Item{{Price: 999999}},
|
|
Pair: cp,
|
|
UpdateID: 1,
|
|
Asset: asset.Spot,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ob, err := holder.GetOrderbook(cp, asset.Spot)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if exp := len(itemArray); ob.LastUpdateID != int64(exp) {
|
|
t.Errorf("expected last update id to be %d, received: %v", exp, ob.LastUpdateID)
|
|
}
|
|
}
|
|
|
|
// TestRunUpdateWithoutSnapshot logic test
|
|
func TestRunUpdateWithoutSnapshot(t *testing.T) {
|
|
t.Parallel()
|
|
var holder Orderbook
|
|
var snapShot1 orderbook.Base
|
|
asks := []orderbook.Item{
|
|
{Price: 4000, Amount: 1, ID: 8},
|
|
}
|
|
bids := []orderbook.Item{
|
|
{Price: 5999, Amount: 1, ID: 8},
|
|
{Price: 4000, Amount: 1, ID: 9},
|
|
}
|
|
snapShot1.Asks = asks
|
|
snapShot1.Bids = bids
|
|
snapShot1.Asset = asset.Spot
|
|
snapShot1.Pair = cp
|
|
holder.exchangeName = exchangeName
|
|
err := holder.Update(&orderbook.Update{
|
|
Bids: bids,
|
|
Asks: asks,
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
})
|
|
if !errors.Is(err, errDepthNotFound) {
|
|
t.Fatalf("expected %v but received %v", errDepthNotFound, err)
|
|
}
|
|
}
|
|
|
|
// TestRunUpdateWithoutAnyUpdates logic test
|
|
func TestRunUpdateWithoutAnyUpdates(t *testing.T) {
|
|
t.Parallel()
|
|
var obl Orderbook
|
|
var snapShot1 orderbook.Base
|
|
snapShot1.Asks = []orderbook.Item{}
|
|
snapShot1.Bids = []orderbook.Item{}
|
|
snapShot1.Asset = asset.Spot
|
|
snapShot1.Pair = cp
|
|
obl.exchangeName = exchangeName
|
|
err := obl.Update(&orderbook.Update{
|
|
Bids: snapShot1.Asks,
|
|
Asks: snapShot1.Bids,
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
})
|
|
if !errors.Is(err, errUpdateNoTargets) {
|
|
t.Fatalf("expected %v but received %v", errUpdateNoTargets, err)
|
|
}
|
|
}
|
|
|
|
// TestRunSnapshotWithNoData logic test
|
|
func TestRunSnapshotWithNoData(t *testing.T) {
|
|
t.Parallel()
|
|
var obl Orderbook
|
|
obl.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder)
|
|
obl.dataHandler = make(chan interface{}, 1)
|
|
var snapShot1 orderbook.Base
|
|
snapShot1.Asset = asset.Spot
|
|
snapShot1.Pair = cp
|
|
snapShot1.Exchange = "test"
|
|
obl.exchangeName = "test"
|
|
err := obl.LoadSnapshot(&snapShot1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// TestLoadSnapshot logic test
|
|
func TestLoadSnapshot(t *testing.T) {
|
|
t.Parallel()
|
|
var obl Orderbook
|
|
obl.dataHandler = make(chan interface{}, 100)
|
|
obl.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder)
|
|
var snapShot1 orderbook.Base
|
|
snapShot1.Exchange = "SnapshotWithOverride"
|
|
asks := []orderbook.Item{
|
|
{Price: 4000, Amount: 1, ID: 8},
|
|
}
|
|
bids := []orderbook.Item{
|
|
{Price: 4000, Amount: 1, ID: 9},
|
|
}
|
|
snapShot1.Asks = asks
|
|
snapShot1.Bids = bids
|
|
snapShot1.Asset = asset.Spot
|
|
snapShot1.Pair = cp
|
|
err := obl.LoadSnapshot(&snapShot1)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
// TestFlushBuffer logic test
|
|
func TestFlushBuffer(t *testing.T) {
|
|
obl, _, _, err := createSnapshot()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if obl.ob[cp.Base][cp.Quote][asset.Spot] == nil {
|
|
t.Error("expected ob to have ask entries")
|
|
}
|
|
obl.FlushBuffer()
|
|
if obl.ob[cp.Base][cp.Quote][asset.Spot] != nil {
|
|
t.Error("expected ob be flushed")
|
|
}
|
|
}
|
|
|
|
// TestInsertingSnapShots logic test
|
|
func TestInsertingSnapShots(t *testing.T) {
|
|
t.Parallel()
|
|
var holder Orderbook
|
|
holder.dataHandler = make(chan interface{}, 100)
|
|
holder.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder)
|
|
var snapShot1 orderbook.Base
|
|
snapShot1.Exchange = "WSORDERBOOKTEST1"
|
|
asks := []orderbook.Item{
|
|
{Price: 6000, Amount: 1, ID: 1},
|
|
{Price: 6001, Amount: 0.5, ID: 2},
|
|
{Price: 6002, Amount: 2, ID: 3},
|
|
{Price: 6003, Amount: 3, ID: 4},
|
|
{Price: 6004, Amount: 5, ID: 5},
|
|
{Price: 6005, Amount: 2, ID: 6},
|
|
{Price: 6006, Amount: 1.5, ID: 7},
|
|
{Price: 6007, Amount: 0.5, ID: 8},
|
|
{Price: 6008, Amount: 23, ID: 9},
|
|
{Price: 6009, Amount: 9, ID: 10},
|
|
{Price: 6010, Amount: 7, ID: 11},
|
|
}
|
|
|
|
bids := []orderbook.Item{
|
|
{Price: 5999, Amount: 1, ID: 12},
|
|
{Price: 5998, Amount: 0.5, ID: 13},
|
|
{Price: 5997, Amount: 2, ID: 14},
|
|
{Price: 5996, Amount: 3, ID: 15},
|
|
{Price: 5995, Amount: 5, ID: 16},
|
|
{Price: 5994, Amount: 2, ID: 17},
|
|
{Price: 5993, Amount: 1.5, ID: 18},
|
|
{Price: 5992, Amount: 0.5, ID: 19},
|
|
{Price: 5991, Amount: 23, ID: 20},
|
|
{Price: 5990, Amount: 9, ID: 21},
|
|
{Price: 5989, Amount: 7, ID: 22},
|
|
}
|
|
|
|
snapShot1.Asks = asks
|
|
snapShot1.Bids = bids
|
|
snapShot1.Asset = asset.Spot
|
|
snapShot1.Pair = cp
|
|
err := holder.LoadSnapshot(&snapShot1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var snapShot2 orderbook.Base
|
|
snapShot2.Exchange = "WSORDERBOOKTEST2"
|
|
asks = []orderbook.Item{
|
|
{Price: 51, Amount: 1, ID: 1},
|
|
{Price: 52, Amount: 0.5, ID: 2},
|
|
{Price: 53, Amount: 2, ID: 3},
|
|
{Price: 54, Amount: 3, ID: 4},
|
|
{Price: 55, Amount: 5, ID: 5},
|
|
{Price: 56, Amount: 2, ID: 6},
|
|
{Price: 57, Amount: 1.5, ID: 7},
|
|
{Price: 58, Amount: 0.5, ID: 8},
|
|
{Price: 59, Amount: 23, ID: 9},
|
|
{Price: 50, Amount: 9, ID: 10},
|
|
{Price: 60, Amount: 7, ID: 11},
|
|
}
|
|
|
|
bids = []orderbook.Item{
|
|
{Price: 49, Amount: 1, ID: 12},
|
|
{Price: 48, Amount: 0.5, ID: 13},
|
|
{Price: 47, Amount: 2, ID: 14},
|
|
{Price: 46, Amount: 3, ID: 15},
|
|
{Price: 45, Amount: 5, ID: 16},
|
|
{Price: 44, Amount: 2, ID: 17},
|
|
{Price: 43, Amount: 1.5, ID: 18},
|
|
{Price: 42, Amount: 0.5, ID: 19},
|
|
{Price: 41, Amount: 23, ID: 20},
|
|
{Price: 40, Amount: 9, ID: 21},
|
|
{Price: 39, Amount: 7, ID: 22},
|
|
}
|
|
|
|
snapShot2.Asks = asks
|
|
snapShot2.Asks.SortAsks()
|
|
snapShot2.Bids = bids
|
|
snapShot2.Bids.SortBids()
|
|
snapShot2.Asset = asset.Spot
|
|
snapShot2.Pair, err = currency.NewPairFromString("LTCUSD")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = holder.LoadSnapshot(&snapShot2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var snapShot3 orderbook.Base
|
|
snapShot3.Exchange = "WSORDERBOOKTEST3"
|
|
asks = []orderbook.Item{
|
|
{Price: 511, Amount: 1, ID: 1},
|
|
{Price: 52, Amount: 0.5, ID: 2},
|
|
{Price: 53, Amount: 2, ID: 3},
|
|
{Price: 54, Amount: 3, ID: 4},
|
|
{Price: 55, Amount: 5, ID: 5},
|
|
{Price: 56, Amount: 2, ID: 6},
|
|
{Price: 57, Amount: 1.5, ID: 7},
|
|
{Price: 58, Amount: 0.5, ID: 8},
|
|
{Price: 59, Amount: 23, ID: 9},
|
|
{Price: 50, Amount: 9, ID: 10},
|
|
{Price: 60, Amount: 7, ID: 11},
|
|
}
|
|
|
|
bids = []orderbook.Item{
|
|
{Price: 49, Amount: 1, ID: 12},
|
|
{Price: 48, Amount: 0.5, ID: 13},
|
|
{Price: 47, Amount: 2, ID: 14},
|
|
{Price: 46, Amount: 3, ID: 15},
|
|
{Price: 45, Amount: 5, ID: 16},
|
|
{Price: 44, Amount: 2, ID: 17},
|
|
{Price: 43, Amount: 1.5, ID: 18},
|
|
{Price: 42, Amount: 0.5, ID: 19},
|
|
{Price: 41, Amount: 23, ID: 20},
|
|
{Price: 40, Amount: 9, ID: 21},
|
|
{Price: 39, Amount: 7, ID: 22},
|
|
}
|
|
|
|
snapShot3.Asks = asks
|
|
snapShot3.Asks.SortAsks()
|
|
snapShot3.Bids = bids
|
|
snapShot3.Bids.SortBids()
|
|
snapShot3.Asset = asset.Futures
|
|
snapShot3.Pair, err = currency.NewPairFromString("LTCUSD")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = holder.LoadSnapshot(&snapShot3)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ob, err := holder.GetOrderbook(snapShot1.Pair, snapShot1.Asset)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ob.Asks[0] != snapShot1.Asks[0] {
|
|
t.Errorf("loaded data mismatch. Expected %v, received %v",
|
|
snapShot1.Asks[0],
|
|
ob.Asks[0])
|
|
}
|
|
ob, err = holder.GetOrderbook(snapShot2.Pair, snapShot2.Asset)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ob.Asks[0] != snapShot2.Asks[0] {
|
|
t.Errorf("loaded data mismatch. Expected %v, received %v",
|
|
snapShot2.Asks[0],
|
|
ob.Asks[0])
|
|
}
|
|
ob, err = holder.GetOrderbook(snapShot3.Pair, snapShot3.Asset)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ob.Asks[0] != snapShot3.Asks[0] {
|
|
t.Errorf("loaded data mismatch. Expected %v, received %v",
|
|
snapShot3.Asks[0],
|
|
ob.Asks[0])
|
|
}
|
|
}
|
|
|
|
func TestGetOrderbook(t *testing.T) {
|
|
holder, _, _, err := createSnapshot()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ob, err := holder.GetOrderbook(cp, asset.Spot)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bufferOb := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
|
b, err := bufferOb.ob.Retrieve()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
askLen, err := bufferOb.ob.GetAskLength()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
bidLen, err := bufferOb.ob.GetBidLength()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
if askLen != len(ob.Asks) ||
|
|
bidLen != len(ob.Bids) ||
|
|
b.Asset != ob.Asset ||
|
|
b.Exchange != ob.Exchange ||
|
|
b.LastUpdateID != ob.LastUpdateID ||
|
|
b.PriceDuplication != ob.PriceDuplication ||
|
|
b.Pair != ob.Pair {
|
|
t.Fatal("data on both books should be the same")
|
|
}
|
|
}
|
|
|
|
func TestSetup(t *testing.T) {
|
|
t.Parallel()
|
|
w := Orderbook{}
|
|
err := w.Setup(nil, nil, nil)
|
|
if !errors.Is(err, errExchangeConfigNil) {
|
|
t.Fatalf("expected error %v but received %v", errExchangeConfigNil, err)
|
|
}
|
|
|
|
exchangeConfig := &config.Exchange{}
|
|
err = w.Setup(exchangeConfig, nil, nil)
|
|
if !errors.Is(err, errBufferConfigNil) {
|
|
t.Fatalf("expected error %v but received %v", errBufferConfigNil, err)
|
|
}
|
|
|
|
bufferConf := &Config{}
|
|
err = w.Setup(exchangeConfig, bufferConf, nil)
|
|
if !errors.Is(err, errUnsetDataHandler) {
|
|
t.Fatalf("expected error %v but received %v", errUnsetDataHandler, err)
|
|
}
|
|
|
|
exchangeConfig.Orderbook.WebsocketBufferEnabled = true
|
|
err = w.Setup(exchangeConfig, bufferConf, make(chan interface{}))
|
|
if !errors.Is(err, errIssueBufferEnabledButNoLimit) {
|
|
t.Fatalf("expected error %v but received %v", errIssueBufferEnabledButNoLimit, err)
|
|
}
|
|
|
|
exchangeConfig.Orderbook.WebsocketBufferLimit = 1337
|
|
exchangeConfig.Orderbook.WebsocketBufferEnabled = true
|
|
exchangeConfig.Name = "test"
|
|
bufferConf.SortBuffer = true
|
|
bufferConf.SortBufferByUpdateIDs = true
|
|
bufferConf.UpdateEntriesByID = true
|
|
err = w.Setup(exchangeConfig, bufferConf, make(chan interface{}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if w.obBufferLimit != 1337 ||
|
|
!w.bufferEnabled ||
|
|
!w.sortBuffer ||
|
|
!w.sortBufferByUpdateIDs ||
|
|
!w.updateEntriesByID ||
|
|
w.exchangeName != "test" {
|
|
t.Errorf("Setup incorrectly loaded %s", w.exchangeName)
|
|
}
|
|
}
|
|
|
|
func TestValidate(t *testing.T) {
|
|
t.Parallel()
|
|
w := Orderbook{}
|
|
err := w.validate(nil)
|
|
if !errors.Is(err, errUpdateIsNil) {
|
|
t.Fatalf("expected error %v but received %v", errUpdateIsNil, err)
|
|
}
|
|
|
|
err = w.validate(&orderbook.Update{})
|
|
if !errors.Is(err, errUpdateNoTargets) {
|
|
t.Fatalf("expected error %v but received %v", errUpdateNoTargets, err)
|
|
}
|
|
}
|
|
|
|
func TestEnsureMultipleUpdatesViaPrice(t *testing.T) {
|
|
t.Parallel()
|
|
holder, _, _, err := createSnapshot()
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
asks := bidAskGenerator()
|
|
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
|
book.updateByPrice(&orderbook.Update{
|
|
Bids: asks,
|
|
Asks: asks,
|
|
Pair: cp,
|
|
UpdateTime: time.Now(),
|
|
Asset: asset.Spot,
|
|
})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
askLen, err := book.ob.GetAskLength()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
if askLen <= 3 {
|
|
t.Errorf("Insufficient updates")
|
|
}
|
|
}
|
|
|
|
func deploySliceOrdered(size int) orderbook.Items {
|
|
rand.Seed(time.Now().UnixNano())
|
|
var items []orderbook.Item
|
|
for i := 0; i < size; i++ {
|
|
items = append(items, orderbook.Item{Amount: 1, Price: rand.Float64() + float64(i), ID: rand.Int63()}) // nolint:gosec // Not needed for tests
|
|
}
|
|
return items
|
|
}
|
|
|
|
func TestUpdateByIDAndAction(t *testing.T) {
|
|
t.Parallel()
|
|
holder := orderbookHolder{}
|
|
|
|
asks := deploySliceOrdered(100)
|
|
// nolint: gocritic
|
|
bids := append(asks[:0:0], asks...)
|
|
bids.Reverse()
|
|
|
|
book, err := orderbook.DeployDepth("test", cp, asset.Spot)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
book.LoadSnapshot(append(bids[:0:0], bids...), append(asks[:0:0], asks...), 0, time.Time{}, true)
|
|
|
|
ob, err := book.Retrieve()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
err = ob.Verify()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
holder.ob = book
|
|
|
|
err = holder.updateByIDAndAction(&orderbook.Update{})
|
|
if !errors.Is(err, errInvalidAction) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidAction)
|
|
}
|
|
|
|
err = holder.updateByIDAndAction(&orderbook.Update{
|
|
Action: orderbook.Amend,
|
|
Bids: []orderbook.Item{
|
|
{
|
|
Price: 100,
|
|
ID: 6969,
|
|
},
|
|
},
|
|
})
|
|
if !strings.Contains(err.Error(), errAmendFailure.Error()) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, errAmendFailure)
|
|
}
|
|
|
|
book.LoadSnapshot(append(bids[:0:0], bids...), append(asks[:0:0], asks...), 0, time.Time{}, true)
|
|
// append to slice
|
|
err = holder.updateByIDAndAction(&orderbook.Update{
|
|
Action: orderbook.UpdateInsert,
|
|
Bids: []orderbook.Item{
|
|
{
|
|
Price: 0,
|
|
ID: 1337,
|
|
Amount: 1,
|
|
},
|
|
},
|
|
Asks: []orderbook.Item{
|
|
{
|
|
Price: 100,
|
|
ID: 1337,
|
|
Amount: 1,
|
|
},
|
|
},
|
|
})
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
cpy, err := book.Retrieve()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
if cpy.Bids[len(cpy.Bids)-1].Price != 0 {
|
|
t.Fatal("did not append bid item")
|
|
}
|
|
if cpy.Asks[len(cpy.Asks)-1].Price != 100 {
|
|
t.Fatal("did not append ask item")
|
|
}
|
|
|
|
// Change amount
|
|
err = holder.updateByIDAndAction(&orderbook.Update{
|
|
Action: orderbook.UpdateInsert,
|
|
Bids: []orderbook.Item{
|
|
{
|
|
Price: 0,
|
|
ID: 1337,
|
|
Amount: 100,
|
|
},
|
|
},
|
|
Asks: []orderbook.Item{
|
|
{
|
|
Price: 100,
|
|
ID: 1337,
|
|
Amount: 100,
|
|
},
|
|
},
|
|
})
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
cpy, err = book.Retrieve()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
if cpy.Bids[len(cpy.Bids)-1].Amount != 100 {
|
|
t.Fatal("did not update bid amount", cpy.Bids[len(cpy.Bids)-1].Amount)
|
|
}
|
|
|
|
if cpy.Asks[len(cpy.Asks)-1].Amount != 100 {
|
|
t.Fatal("did not update ask amount")
|
|
}
|
|
|
|
// Change price level
|
|
err = holder.updateByIDAndAction(&orderbook.Update{
|
|
Action: orderbook.UpdateInsert,
|
|
Bids: []orderbook.Item{
|
|
{
|
|
Price: 100,
|
|
ID: 1337,
|
|
Amount: 99,
|
|
},
|
|
},
|
|
Asks: []orderbook.Item{
|
|
{
|
|
Price: 0,
|
|
ID: 1337,
|
|
Amount: 99,
|
|
},
|
|
},
|
|
})
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
cpy, err = book.Retrieve()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
if cpy.Bids[0].Amount != 99 && cpy.Bids[0].Price != 100 {
|
|
t.Fatal("did not adjust bid item placement and details")
|
|
}
|
|
|
|
if cpy.Asks[0].Amount != 99 && cpy.Asks[0].Amount != 0 {
|
|
t.Fatal("did not adjust ask item placement and details")
|
|
}
|
|
|
|
book.LoadSnapshot(append(bids[:0:0], bids...), append(bids[:0:0], bids...), 0, time.Time{}, true) // nolint:gocritic
|
|
// Delete - not found
|
|
err = holder.updateByIDAndAction(&orderbook.Update{
|
|
Action: orderbook.Delete,
|
|
Asks: []orderbook.Item{
|
|
{
|
|
Price: 0,
|
|
ID: 1337,
|
|
Amount: 99,
|
|
},
|
|
},
|
|
})
|
|
if !strings.Contains(err.Error(), errDeleteFailure.Error()) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, errDeleteFailure)
|
|
}
|
|
|
|
book.LoadSnapshot(append(bids[:0:0], bids...), append(bids[:0:0], bids...), 0, time.Time{}, true) // nolint:gocritic
|
|
// Delete - found
|
|
err = holder.updateByIDAndAction(&orderbook.Update{
|
|
Action: orderbook.Delete,
|
|
Asks: []orderbook.Item{
|
|
asks[0],
|
|
},
|
|
})
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
askLen, err := book.GetAskLength()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
if askLen != 99 {
|
|
t.Fatal("element not deleted")
|
|
}
|
|
|
|
// Apply update
|
|
err = holder.updateByIDAndAction(&orderbook.Update{
|
|
Action: orderbook.Amend,
|
|
Asks: []orderbook.Item{
|
|
{ID: 123456},
|
|
},
|
|
})
|
|
if !strings.Contains(err.Error(), errAmendFailure.Error()) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, errAmendFailure)
|
|
}
|
|
|
|
book.LoadSnapshot(bids, bids, 0, time.Time{}, true)
|
|
|
|
ob, err = book.Retrieve()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
update := ob.Asks[0]
|
|
update.Amount = 1337
|
|
|
|
err = holder.updateByIDAndAction(&orderbook.Update{
|
|
Action: orderbook.Amend,
|
|
Asks: []orderbook.Item{
|
|
update,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ob, err = book.Retrieve()
|
|
if !errors.Is(err, nil) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
|
}
|
|
|
|
if ob.Asks[0].Amount != 1337 {
|
|
t.Fatal("element not updated")
|
|
}
|
|
}
|
|
|
|
func TestFlushOrderbook(t *testing.T) {
|
|
t.Parallel()
|
|
w := &Orderbook{}
|
|
err := w.Setup(&config.Exchange{Name: "test"}, &Config{}, make(chan interface{}, 2))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var snapShot1 orderbook.Base
|
|
snapShot1.Exchange = "Snapshooooot"
|
|
asks := []orderbook.Item{
|
|
{Price: 4000, Amount: 1, ID: 8},
|
|
}
|
|
bids := []orderbook.Item{
|
|
{Price: 4000, Amount: 1, ID: 9},
|
|
}
|
|
snapShot1.Asks = asks
|
|
snapShot1.Bids = bids
|
|
snapShot1.Asset = asset.Spot
|
|
snapShot1.Pair = cp
|
|
|
|
err = w.FlushOrderbook(cp, asset.Spot)
|
|
if err == nil {
|
|
t.Fatal("book not loaded error cannot be nil")
|
|
}
|
|
|
|
_, err = w.GetOrderbook(cp, asset.Spot)
|
|
if !errors.Is(err, errDepthNotFound) {
|
|
t.Fatalf("expected: %v but received: %v", errDepthNotFound, err)
|
|
}
|
|
|
|
err = w.LoadSnapshot(&snapShot1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = w.FlushOrderbook(cp, asset.Spot)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = w.GetOrderbook(cp, asset.Spot)
|
|
if !errors.Is(err, orderbook.ErrOrderbookInvalid) {
|
|
t.Fatalf("received: '%v' but expected: '%v'", err, orderbook.ErrOrderbookInvalid)
|
|
}
|
|
}
|