Files
gocryptotrader/exchanges/orderbook/incremental_updates_test.go
Ryan O'Hara-Reid c892f492a9 buffer/orderbook: shift orderbook update logic from buffer package to orderbook package (#1908)
* buffer/orderbook: shift orderbook update logic from buffer package to orderbook package

* Update exchanges/orderbook/depth.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* linter: fixes

* spelling: fix

* samboss: add in some todos

* sammy nit: add unlock on error

* sammy nits: rm ptr to slice field buffer in orderbookHolder

* sammy nits: Add more coverage bro

* sammy nits: even more coverage

* gk: nits on commentary

* gk: nits change sort.Slice to slices.SortFunc

* gk: fix commentary on buffer clearing

* gk: nits fin

* linter: fix

* Update exchange/websocket/buffer/buffer.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchange/websocket/buffer/buffer.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/orderbook/tranches.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/orderbook/orderbook.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchange/websocket/buffer/buffer_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchange/websocket/buffer/buffer_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/orderbook/incremental_updates.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: refresh action types and names

* gk nits: consolidate error vars and naming

* gk nits: more name changes

* gk nits; buffer tests update

* gk nits: error var names change

* linter: FIX

* it gets inlined but there is an alloc

* rn field in TODO

* Update exchanges/binance/binance_websocket.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update exchanges/binance/binance_websocket.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* orderbook: shift verify/validate funcs to validate.go and rn Verify() -> Validate()

* orderbook: validate even in presence of checksum and allow cowboy mode

* buffer; fix test

* kraken: fix futures orderbook by reversing incoming bids

* okx: change default spread pair

* Update exchanges/orderbook/validate.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/orderbook/validate.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/orderbook/validate.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/orderbook/validate.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/orderbook/validate.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: initial nits

* rn fields V(v)erifyorderbook to V(v)alidateOrderbook

* buffer/orderbook: nilguard in validate and change method receiver w -> o

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
2025-06-18 16:19:58 +10:00

375 lines
15 KiB
Go

package orderbook
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newSnapshot(length int) *Book {
return &Book{
Bids: newBids(length),
Asks: newAsks(length, length),
LastUpdated: time.Now(),
LastPushed: time.Now(),
LastUpdateID: 1,
}
}
func newBids(length int) Levels {
bids := make(Levels, length)
for i := range length {
bids[i] = Level{Price: 1337 - float64(i), Amount: 1, ID: int64(i + 1)}
}
return bids
}
func newAsks(idOffset, length int) Levels {
asks := make(Levels, length)
for i := range length {
asks[i] = Level{Price: 1338 + float64(i), Amount: 1, ID: int64(i + 1 + idOffset)}
}
return asks
}
func TestProcessUpdate(t *testing.T) {
t.Parallel()
d := NewDepth(id)
require.NoError(t, d.LoadSnapshot(newSnapshot(69)))
assert.ErrorIs(t, d.ProcessUpdate(&Update{}), ErrEmptyUpdate)
assert.ErrorIs(t, d.ProcessUpdate(&Update{AllowEmpty: true}), ErrEmptyUpdate, "exercise validation error return from last ProcessUpdate call which invalidates the orderbook")
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
assert.NoError(t, d.ProcessUpdate(&Update{UpdateTime: time.Now(), Asks: Levels{{Price: 1337.5, Amount: 69420, ID: 69420}}, SkipOutOfOrderLastUpdateID: true}))
ob, err := d.Retrieve()
require.NoError(t, err)
assert.NotEqual(t, int64(69420), ob.Asks[0].ID, "Update above should skip insertion")
d.options.restSnapshot = true // Simulate the snapshot has been loaded from REST
err = d.ProcessUpdate(&Update{UpdateTime: time.Now(), Asks: Levels{{Price: 1337.5, Amount: 69420, ID: 69420}}})
assert.ErrorIs(t, err, errRESTSnapshot)
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
err = d.ProcessUpdate(&Update{Asks: Levels{{Price: 1337.5, Amount: 69420, ID: 69420}}})
assert.ErrorIs(t, err, ErrLastUpdatedNotSet)
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
err = d.ProcessUpdate(&Update{Action: InsertAction, Asks: Levels{{Price: 1337.5, Amount: 69420, ID: 69420}}})
assert.ErrorIs(t, err, ErrLastUpdatedNotSet)
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
d.validateOrderbook = true
d.askLevels.Levels[0].Amount = 0
err = d.ProcessUpdate(&Update{UpdateTime: time.Now(), Asks: Levels{{Price: 1337.5, Amount: 69420, ID: 69420}}})
assert.ErrorIs(t, err, errAmountInvalid)
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
err = d.ProcessUpdate(&Update{UpdateTime: time.Now(), Asks: Levels{{Price: 1337.5, Amount: 69420, ID: 69420}}})
require.NoError(t, err)
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
err = d.ProcessUpdate(&Update{UpdateTime: time.Now(), Asks: Levels{{Price: 1337.5, Amount: 69420, ID: 69420}}, ExpectedChecksum: 1337})
require.ErrorIs(t, err, errChecksumGeneratorUnset)
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
err = d.ProcessUpdate(&Update{UpdateTime: time.Now(), Asks: Levels{{Price: 1337.5, Amount: 69420, ID: 69420}}, ExpectedChecksum: 1337, GenerateChecksum: func(*Book) uint32 { return 1336 }})
require.ErrorIs(t, err, errChecksumMismatch)
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
err = d.ProcessUpdate(&Update{UpdateTime: time.Now(), Asks: Levels{{Price: 1337.5, Amount: 69420, ID: 69420}}, ExpectedChecksum: 1337, GenerateChecksum: func(*Book) uint32 { return 1337 }})
require.NoError(t, err)
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
d.askLevels.Levels[0].Amount = 0
d.validateOrderbook = false // Disable verification
err = d.ProcessUpdate(&Update{UpdateTime: time.Now(), Asks: Levels{{Price: 1337.5, Amount: 69420, ID: 69420}}, ExpectedChecksum: 1337, GenerateChecksum: func(*Book) uint32 { return 1337 }})
require.NoError(t, err, "must not error when ValidateOrderbook is false")
}
func TestUpdate(t *testing.T) {
t.Parallel()
d := NewDepth(id)
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
err := d.update(&Update{})
assert.ErrorIs(t, err, errInvalidAction, "update should error correctly")
err = d.update(&Update{Action: UpdateAction, UpdateTime: time.Now(), Asks: Levels{{Price: 1338, Amount: 69420, ID: 69420}}})
assert.ErrorIs(t, err, errUpdateFailed, "update should error correctly")
assert.ErrorContains(t, err, "Update")
err = d.update(&Update{Action: UpdateAction, UpdateTime: time.Now(), Asks: Levels{{Price: 1338, Amount: 69420, ID: 21}}})
assert.NoError(t, err, "update should not error")
ob, err := d.Retrieve()
require.NoError(t, err)
assert.Equal(t, 69420.0, ob.Asks[0].Amount, "First ask amount should be correct")
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
err = d.update(&Update{Action: DeleteAction, UpdateTime: time.Now(), Asks: Levels{{Price: 1338, Amount: 1, ID: 69420}}})
assert.ErrorIs(t, err, errDeleteFailed, "update should error correctly")
assert.ErrorContains(t, err, "Delete")
err = d.update(&Update{Action: DeleteAction, UpdateTime: time.Now(), Asks: Levels{{ID: 21}}})
assert.NoError(t, err, "update should not error")
ob, err = d.Retrieve()
require.NoError(t, err)
assert.NotEqual(t, 21, ob.Asks[0].ID, "Ask element should be deleted")
assert.Len(t, ob.Asks, 19, "Asks length should be correct")
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
err = d.update(&Update{Action: InsertAction, UpdateTime: time.Now(), Asks: Levels{{Price: 1338, Amount: 1, ID: 21}}})
assert.ErrorIs(t, err, errUpdateFailed, "update should error correctly")
assert.ErrorContains(t, err, "Insert")
err = d.update(&Update{Action: InsertAction, UpdateTime: time.Now(), Asks: Levels{{Price: 1337.5, Amount: 1, ID: 69420}}})
assert.NoError(t, err, "update should not error")
ob, err = d.Retrieve()
require.NoError(t, err)
assert.Equal(t, int64(69420), ob.Asks[0].ID, "First ask ID should be correct")
require.NoError(t, d.LoadSnapshot(newSnapshot(20)))
err = d.update(&Update{Action: UpdateOrInsertAction, UpdateTime: time.Now(), Asks: Levels{{Price: 1338, Amount: 0, ID: 21}}})
assert.ErrorIs(t, err, errUpdateFailed, "update should error correctly")
assert.ErrorContains(t, err, "UpdateOrInsert")
err = d.update(&Update{Action: UpdateOrInsertAction, UpdateTime: time.Now(), Asks: Levels{{Price: 1337.5, Amount: 1, ID: 69420}}})
assert.NoError(t, err, "update should not error")
ob, err = d.Retrieve()
require.NoError(t, err)
assert.Equal(t, int64(69420), ob.Asks[0].ID, "First ask ID should be correct")
}
func TestUpdateBidAskByID(t *testing.T) {
t.Parallel()
d := NewDepth(id)
err := d.LoadSnapshot(&Book{Bids: Levels{{Price: 1337, Amount: 1, ID: 1}}, Asks: Levels{{Price: 1337, Amount: 10, ID: 2}}, LastUpdated: time.Now(), LastPushed: time.Now()})
assert.NoError(t, err, "LoadSnapshot should not error")
updates := &Update{
Bids: Levels{{Price: 1337, Amount: 2, ID: 1}},
Asks: Levels{{Price: 1337, Amount: 2, ID: 2}},
}
err = d.updateBidAskByID(updates)
assert.ErrorIs(t, err, ErrLastUpdatedNotSet, "UpdateBidAskByID should error correctly")
updates.UpdateTime = time.Now()
err = d.updateBidAskByID(updates)
assert.NoError(t, err, "UpdateBidAskByID should not error")
ob, err := d.Retrieve()
assert.NoError(t, err, "Retrieve should not error")
assert.Equal(t, 2.0, ob.Asks[0].Amount, "First ask amount should be correct")
assert.Equal(t, 2.0, ob.Bids[0].Amount, "First bid amount should be correct")
updates = &Update{
Bids: Levels{{Price: 1337, Amount: 2, ID: 666}},
UpdateTime: time.Now(),
}
// random unmatching IDs
err = d.updateBidAskByID(updates)
assert.ErrorIs(t, err, errIDCannotBeMatched, "UpdateBidAskByID should error correctly")
updates = &Update{
Asks: Levels{{Price: 1337, Amount: 2, ID: 69}},
UpdateTime: time.Now(),
}
err = d.updateBidAskByID(updates)
assert.ErrorIs(t, err, errIDCannotBeMatched, "UpdateBidAskByID should error correctly")
}
func TestDelete(t *testing.T) {
t.Parallel()
d := NewDepth(id)
err := d.LoadSnapshot(&Book{Bids: Levels{{Price: 1337, Amount: 1, ID: 1}}, Asks: Levels{{Price: 1337, Amount: 10, ID: 2}}, LastUpdated: time.Now(), LastPushed: time.Now()})
assert.NoError(t, err, "LoadSnapshot should not error")
updates := &Update{
Bids: Levels{{Price: 1337, Amount: 2, ID: 1}},
Asks: Levels{{Price: 1337, Amount: 2, ID: 2}},
}
err = d.delete(updates, false)
assert.ErrorIs(t, err, ErrLastUpdatedNotSet, "delete should error correctly")
updates.UpdateTime = time.Now()
err = d.delete(updates, false)
assert.NoError(t, err, "delete should not error")
ob, err := d.Retrieve()
assert.NoError(t, err, "Retrieve should not error")
assert.Empty(t, ob.Asks, "Asks should be empty")
assert.Empty(t, ob.Bids, "Bids should be empty")
updates = &Update{
Bids: Levels{{Price: 1337, Amount: 2, ID: 1}},
UpdateTime: time.Now(),
}
err = d.delete(updates, false)
assert.ErrorIs(t, err, errIDCannotBeMatched, "delete should error correctly")
updates = &Update{
Asks: Levels{{Price: 1337, Amount: 2, ID: 2}},
UpdateTime: time.Now(),
}
err = d.delete(updates, false)
assert.ErrorIs(t, err, errIDCannotBeMatched, "delete should error correctly")
updates = &Update{
Asks: Levels{{Price: 1337, Amount: 2, ID: 2}},
UpdateTime: time.Now(),
}
err = d.delete(updates, true)
assert.NoError(t, err, "delete should not error")
}
func TestInsert(t *testing.T) {
t.Parallel()
d := NewDepth(id)
err := d.LoadSnapshot(&Book{Bids: Levels{{Price: 1337, Amount: 1, ID: 1}}, Asks: Levels{{Price: 1337, Amount: 10, ID: 2}}, LastUpdated: time.Now(), LastPushed: time.Now()})
assert.NoError(t, err, "LoadSnapshot should not error")
updates := &Update{
Asks: Levels{{Price: 1337, Amount: 2, ID: 3}},
}
err = d.insert(updates)
assert.ErrorIs(t, err, ErrLastUpdatedNotSet, "insert should error correctly")
updates.UpdateTime = time.Now()
err = d.insert(updates)
assert.ErrorIs(t, err, errCollisionDetected, "insert should error correctly on collision")
err = d.LoadSnapshot(&Book{Bids: Levels{{Price: 1337, Amount: 1, ID: 1}}, Asks: Levels{{Price: 1337, Amount: 10, ID: 2}}, LastUpdated: time.Now(), LastPushed: time.Now()})
assert.NoError(t, err, "LoadSnapshot should not error")
updates = &Update{
Bids: Levels{{Price: 1337, Amount: 2, ID: 3}},
UpdateTime: time.Now(),
}
err = d.insert(updates)
assert.ErrorIs(t, err, errCollisionDetected, "insert should error correctly on collision")
err = d.LoadSnapshot(&Book{Bids: Levels{{Price: 1337, Amount: 1, ID: 1}}, Asks: Levels{{Price: 1337, Amount: 10, ID: 2}}, LastUpdated: time.Now(), LastPushed: time.Now()})
assert.NoError(t, err, "LoadSnapshot should not error")
updates = &Update{
Bids: Levels{{Price: 1338, Amount: 2, ID: 3}},
Asks: Levels{{Price: 1336, Amount: 2, ID: 4}},
UpdateTime: time.Now(),
}
err = d.insert(updates)
assert.NoError(t, err, "InsertBidAskByID should not error")
ob, err := d.Retrieve()
assert.NoError(t, err, "Retrieve should not error")
assert.Len(t, ob.Asks, 2, "Should have correct Asks")
assert.Len(t, ob.Bids, 2, "Should have correct Bids")
}
func TestUpdateOrInsert(t *testing.T) {
t.Parallel()
d := NewDepth(id)
err := d.LoadSnapshot(&Book{Bids: Levels{{Price: 1337, Amount: 1, ID: 1}}, Asks: Levels{{Price: 1337, Amount: 10, ID: 2}}, LastUpdated: time.Now(), LastPushed: time.Now()})
assert.NoError(t, err, "LoadSnapshot should not error")
updates := &Update{
Bids: Levels{{Price: 1338, Amount: 0, ID: 3}},
Asks: Levels{{Price: 1336, Amount: 2, ID: 4}},
}
err = d.updateOrInsert(updates)
assert.ErrorIs(t, err, ErrLastUpdatedNotSet, "updateOrInsert should error correctly")
updates.UpdateTime = time.Now()
err = d.updateOrInsert(updates)
assert.ErrorIs(t, err, errAmountCannotBeLessOrEqualToZero, "updateOrInsert should error correctly")
err = d.LoadSnapshot(&Book{Bids: Levels{{Price: 1337, Amount: 1, ID: 1}}, Asks: Levels{{Price: 1337, Amount: 10, ID: 2}}, LastUpdated: time.Now(), LastPushed: time.Now()})
assert.NoError(t, err, "LoadSnapshot should not error")
updates = &Update{
Bids: Levels{{Price: 1338, Amount: 2, ID: 3}},
Asks: Levels{{Price: 1336, Amount: 0, ID: 4}},
UpdateTime: time.Now(),
}
err = d.updateOrInsert(updates)
assert.ErrorIs(t, err, errAmountCannotBeLessOrEqualToZero, "updateOrInsert should error correctly")
err = d.LoadSnapshot(&Book{Bids: Levels{{Price: 1337, Amount: 1, ID: 1}}, Asks: Levels{{Price: 1337, Amount: 10, ID: 2}}, LastUpdated: time.Now(), LastPushed: time.Now()})
assert.NoError(t, err, "LoadSnapshot should not error")
updates = &Update{
Bids: Levels{{Price: 1338, Amount: 2, ID: 3}},
Asks: Levels{{Price: 1336, Amount: 2, ID: 4}},
UpdateTime: time.Now(),
}
err = d.updateOrInsert(updates)
assert.NoError(t, err, "updateOrInsert should not error")
ob, err := d.Retrieve()
assert.NoError(t, err, "Retrieve should not error")
assert.Len(t, ob.Asks, 2, "Should have correct Asks")
assert.Len(t, ob.Bids, 2, "Should have correct Bids")
}
func TestUpdateBidAskByPrice(t *testing.T) {
t.Parallel()
d := NewDepth(id)
err := d.LoadSnapshot(&Book{Bids: Levels{{Price: 1337, Amount: 1, ID: 1}}, Asks: Levels{{Price: 1338, Amount: 10, ID: 2}}, LastUpdated: time.Now(), LastPushed: time.Now()})
assert.NoError(t, err, "LoadSnapshot should not error")
err = d.updateBidAskByPrice(&Update{})
assert.ErrorIs(t, err, ErrLastUpdatedNotSet, "UpdateBidAskByPrice should error correctly")
err = d.updateBidAskByPrice(&Update{UpdateTime: time.Now()})
assert.NoError(t, err, "UpdateBidAskByPrice should not error")
updates := &Update{
Bids: Levels{{Price: 1337, Amount: 2, ID: 1}},
Asks: Levels{{Price: 1338, Amount: 3, ID: 2}},
UpdateID: 1,
UpdateTime: time.Now(),
}
err = d.updateBidAskByPrice(updates)
assert.NoError(t, err, "UpdateBidAskByPrice should not error")
ob, err := d.Retrieve()
assert.NoError(t, err, "Retrieve should not error")
assert.Equal(t, 3.0, ob.Asks[0].Amount, "Asks amount should be correct")
assert.Equal(t, 2.0, ob.Bids[0].Amount, "Bids amount should be correct")
updates = &Update{
Bids: Levels{{Price: 1337, Amount: 0, ID: 1}},
Asks: Levels{{Price: 1338, Amount: 0, ID: 2}},
UpdateID: 2,
UpdateTime: time.Now(),
}
err = d.updateBidAskByPrice(updates)
assert.NoError(t, err, "UpdateBidAskByPrice should not error")
askLen, err := d.GetAskLength()
assert.NoError(t, err, "GetAskLength should not error")
assert.Zero(t, askLen, "Ask Length should be correct")
bidLen, err := d.GetBidLength()
assert.NoError(t, err, "GetBidLength should not error")
assert.Zero(t, bidLen, "Bid Length should be correct")
}
func TestString(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
action ActionType
expected string
}{
{action: UpdateAction, expected: "Update"},
{action: InsertAction, expected: "Insert"},
{action: UpdateOrInsertAction, expected: "UpdateOrInsert"},
{action: DeleteAction, expected: "Delete"},
{action: UnknownAction, expected: "Unknown"},
{action: ActionType(69), expected: "Unknown(69)"},
} {
t.Run(tc.expected, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tc.expected, tc.action.String(), "String representation should match")
})
}
}