Files
gocryptotrader/exchanges/orderbook/levels_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

1857 lines
45 KiB
Go

package orderbook
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var ask = Levels{
{Price: 1337, Amount: 1},
{Price: 1338, Amount: 1},
{Price: 1339, Amount: 1},
{Price: 1340, Amount: 1},
{Price: 1341, Amount: 1},
{Price: 1342, Amount: 1},
{Price: 1343, Amount: 1},
{Price: 1344, Amount: 1},
{Price: 1345, Amount: 1},
{Price: 1346, Amount: 1},
{Price: 1347, Amount: 1},
{Price: 1348, Amount: 1},
{Price: 1349, Amount: 1},
{Price: 1350, Amount: 1},
{Price: 1351, Amount: 1},
{Price: 1352, Amount: 1},
{Price: 1353, Amount: 1},
{Price: 1354, Amount: 1},
{Price: 1355, Amount: 1},
{Price: 1356, Amount: 1},
}
var bid = Levels{
{Price: 1336, Amount: 1},
{Price: 1335, Amount: 1},
{Price: 1334, Amount: 1},
{Price: 1333, Amount: 1},
{Price: 1332, Amount: 1},
{Price: 1331, Amount: 1},
{Price: 1330, Amount: 1},
{Price: 1329, Amount: 1},
{Price: 1328, Amount: 1},
{Price: 1327, Amount: 1},
{Price: 1326, Amount: 1},
{Price: 1325, Amount: 1},
{Price: 1324, Amount: 1},
{Price: 1323, Amount: 1},
{Price: 1322, Amount: 1},
{Price: 1321, Amount: 1},
{Price: 1320, Amount: 1},
{Price: 1319, Amount: 1},
{Price: 1318, Amount: 1},
{Price: 1317, Amount: 1},
}
// Display displays depth content for tests
func (l Levels) display() {
for x := range l {
fmt.Printf("Level: %+v %p \n", l[x], &l[x])
}
fmt.Println()
}
func TestLoad(t *testing.T) {
list := askLevels{}
Check(t, list, 0, 0, 0)
list.load(Levels{
{Price: 1, Amount: 1},
{Price: 3, Amount: 1},
{Price: 5, Amount: 1},
{Price: 7, Amount: 1},
{Price: 9, Amount: 1},
{Price: 11, Amount: 1},
})
Check(t, list, 6, 36, 6)
list.load(Levels{
{Price: 1, Amount: 1},
{Price: 3, Amount: 1},
{Price: 5, Amount: 1},
})
Check(t, list, 3, 9, 3)
list.load(Levels{
{Price: 1, Amount: 1},
{Price: 3, Amount: 1},
{Price: 5, Amount: 1},
{Price: 7, Amount: 1},
})
Check(t, list, 4, 16, 4)
// purge entire list
list.load(nil)
Check(t, list, 0, 0, 0)
}
// 27906781 42.4 ns/op 0 B/op 0 allocs/op (old)
// 84119028 13.87 ns/op 0 B/op 0 allocs/op (new)
func BenchmarkLoad(b *testing.B) {
ts := Levels{}
for b.Loop() {
ts.load(ask)
}
}
func TestUpdateInsertByPrice(t *testing.T) {
a := askLevels{}
asksSnapshot := Levels{
{Price: 1, Amount: 1},
{Price: 3, Amount: 1},
{Price: 5, Amount: 1},
{Price: 7, Amount: 1},
{Price: 9, Amount: 1},
{Price: 11, Amount: 1},
}
a.load(asksSnapshot)
// Update one instance with matching price
a.updateInsertByPrice(Levels{{Price: 1, Amount: 2}}, 0)
Check(t, a, 7, 37, 6)
// Insert at head
a.updateInsertByPrice(Levels{
{Price: 0.5, Amount: 2},
}, 0)
Check(t, a, 9, 38, 7)
// Insert at tail
a.updateInsertByPrice(Levels{
{Price: 12, Amount: 2},
}, 0)
Check(t, a, 11, 62, 8)
// Insert between price and up to and beyond max allowable depth level
a.updateInsertByPrice(Levels{
{Price: 11.5, Amount: 2},
{Price: 10.5, Amount: 2},
{Price: 13, Amount: 2},
}, 10)
Check(t, a, 15, 106, 10)
// delete at tail
a.updateInsertByPrice(Levels{{Price: 12, Amount: 0}}, 0)
Check(t, a, 13, 82, 9)
// delete at mid
a.updateInsertByPrice(Levels{{Price: 7, Amount: 0}}, 0)
Check(t, a, 12, 75, 8)
// delete at head
a.updateInsertByPrice(Levels{{Price: 0.5, Amount: 0}}, 0)
Check(t, a, 10, 74, 7)
// purge if liquidity plunges to zero
a.load(nil)
// rebuild everything again
a.updateInsertByPrice(Levels{
{Price: 1, Amount: 1},
{Price: 3, Amount: 1},
{Price: 5, Amount: 1},
{Price: 7, Amount: 1},
{Price: 9, Amount: 1},
{Price: 11, Amount: 1},
}, 0)
Check(t, a, 6, 36, 6)
b := bidLevels{}
bidsSnapshot := Levels{
{Price: 11, Amount: 1},
{Price: 9, Amount: 1},
{Price: 7, Amount: 1},
{Price: 5, Amount: 1},
{Price: 3, Amount: 1},
{Price: 1, Amount: 1},
}
b.load(bidsSnapshot)
// Update one instance with matching price
b.updateInsertByPrice(Levels{{Price: 11, Amount: 2}}, 0)
Check(t, b, 7, 47, 6)
// Insert at head
b.updateInsertByPrice(Levels{{Price: 12, Amount: 2}}, 0)
Check(t, b, 9, 71, 7)
// Insert at tail
b.updateInsertByPrice(Levels{{Price: 0.5, Amount: 2}}, 0)
Check(t, b, 11, 72, 8)
// Insert between price and up to and beyond max allowable depth level
b.updateInsertByPrice(Levels{
{Price: 11.5, Amount: 2},
{Price: 10.5, Amount: 2},
{Price: 13, Amount: 2},
}, 10)
Check(t, b, 15, 141, 10)
// Insert between price and up to and beyond max allowable depth level
b.updateInsertByPrice(Levels{{Price: 1, Amount: 0}}, 0)
Check(t, b, 14, 140, 9)
// delete at mid
b.updateInsertByPrice(Levels{{Price: 10.5, Amount: 0}}, 0)
Check(t, b, 12, 119, 8)
// delete at head
b.updateInsertByPrice(Levels{{Price: 13, Amount: 0}}, 0)
Check(t, b, 10, 93, 7)
// purge if liquidity plunges to zero
b.load(nil)
// rebuild everything again
b.updateInsertByPrice(Levels{
{Price: 1, Amount: 1},
{Price: 3, Amount: 1},
{Price: 5, Amount: 1},
{Price: 7, Amount: 1},
{Price: 9, Amount: 1},
{Price: 11, Amount: 1},
}, 0)
Check(t, b, 6, 36, 6)
}
// 134830672 9.83 ns/op 0 B/op 0 allocs/op (old)
// 206689897 5.761 ns/op 0 B/op 0 allocs/op (new)
func BenchmarkUpdateInsertByPrice_Amend(b *testing.B) {
a := askLevels{}
a.load(ask)
updates := Levels{
{
Price: 1337, // Amend
Amount: 2,
},
{
Price: 1337, // Amend
Amount: 1,
},
}
for b.Loop() {
a.updateInsertByPrice(updates, 0)
}
}
// 49763002 24.9 ns/op 0 B/op 0 allocs/op (old)
// 25662849 45.32 ns/op 0 B/op 0 allocs/op (new)
func BenchmarkUpdateInsertByPrice_Insert_Delete(b *testing.B) {
a := askLevels{}
a.load(ask)
updates := Levels{
{
Price: 1337.5, // Insert
Amount: 2,
},
{
Price: 1337.5, // Delete
Amount: 0,
},
}
for b.Loop() {
a.updateInsertByPrice(updates, 0)
}
}
func TestUpdateByID(t *testing.T) {
a := askLevels{}
asksSnapshot := Levels{
{Price: 1, Amount: 1, ID: 1},
{Price: 3, Amount: 1, ID: 3},
{Price: 5, Amount: 1, ID: 5},
{Price: 7, Amount: 1, ID: 7},
{Price: 9, Amount: 1, ID: 9},
{Price: 11, Amount: 1, ID: 11},
}
a.load(asksSnapshot)
err := a.updateByID(Levels{
{Price: 1, Amount: 1, ID: 1},
{Price: 3, Amount: 1, ID: 3},
{Price: 5, Amount: 1, ID: 5},
{Price: 7, Amount: 1, ID: 7},
{Price: 9, Amount: 1, ID: 9},
{Price: 11, Amount: 1, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, a, 6, 36, 6)
err = a.updateByID(Levels{
{Price: 11, Amount: 1, ID: 1337},
})
require.ErrorIs(t, err, errIDCannotBeMatched)
err = a.updateByID(Levels{ // Simulate Bitmex updating
{Price: 0, Amount: 1337, ID: 3},
})
require.NoError(t, err)
if got := a.retrieve(2); len(got) != 2 || got[1].Price == 0 {
t.Fatal("price should not be replaced with zero")
}
if got := a.retrieve(3); len(got) != 3 || got[1].Amount != 1337 {
t.Fatal("unexpected value for update")
}
if got := a.retrieve(1000); len(got) != 6 {
t.Fatal("unexpected value for update")
}
}
// 46043871 25.9 ns/op 0 B/op 0 allocs/op (old)
// 63445401 18.51 ns/op 0 B/op 0 allocs/op (new)
func BenchmarkUpdateByID(b *testing.B) {
asks := Levels{}
asksSnapshot := Levels{
{Price: 1, Amount: 1, ID: 1},
{Price: 3, Amount: 1, ID: 3},
{Price: 5, Amount: 1, ID: 5},
{Price: 7, Amount: 1, ID: 7},
{Price: 9, Amount: 1, ID: 9},
{Price: 11, Amount: 1, ID: 11},
}
asks.load(asksSnapshot)
for b.Loop() {
err := asks.updateByID(asksSnapshot)
if err != nil {
b.Fatal(err)
}
}
}
func TestDeleteByID(t *testing.T) {
a := askLevels{}
asksSnapshot := Levels{
{Price: 1, Amount: 1, ID: 1},
{Price: 3, Amount: 1, ID: 3},
{Price: 5, Amount: 1, ID: 5},
{Price: 7, Amount: 1, ID: 7},
{Price: 9, Amount: 1, ID: 9},
{Price: 11, Amount: 1, ID: 11},
}
a.load(asksSnapshot)
// Delete at head
err := a.deleteByID(Levels{{Price: 1, Amount: 1, ID: 1}}, false)
if err != nil {
t.Fatal(err)
}
Check(t, a, 5, 35, 5)
// Delete at tail
err = a.deleteByID(Levels{{Price: 1, Amount: 1, ID: 11}}, false)
if err != nil {
t.Fatal(err)
}
Check(t, a, 4, 24, 4)
// Delete in middle
err = a.deleteByID(Levels{{Price: 1, Amount: 1, ID: 5}}, false)
if err != nil {
t.Fatal(err)
}
Check(t, a, 3, 19, 3)
// Intentional error
err = a.deleteByID(Levels{{Price: 11, Amount: 1, ID: 1337}}, false)
require.ErrorIs(t, err, errIDCannotBeMatched)
// Error bypass
err = a.deleteByID(Levels{{Price: 11, Amount: 1, ID: 1337}}, true)
if err != nil {
t.Fatal(err)
}
}
// 26724331 44.69 ns/op 0 B/op 0 allocs/op
func BenchmarkDeleteByID(b *testing.B) {
asks := Levels{}
asksSnapshot := Levels{
{Price: 1, Amount: 1, ID: 1},
{Price: 3, Amount: 1, ID: 3},
{Price: 5, Amount: 1, ID: 5},
{Price: 7, Amount: 1, ID: 7},
{Price: 9, Amount: 1, ID: 9},
{Price: 11, Amount: 1, ID: 11},
}
asks.load(asksSnapshot)
for b.Loop() {
err := asks.deleteByID(asksSnapshot, false)
if err != nil {
b.Fatal(err)
}
asks.load(asksSnapshot) // reset
}
}
func TestUpdateInsertByIDAsk(t *testing.T) {
a := askLevels{}
asksSnapshot := Levels{
{Price: 1, Amount: 1, ID: 1},
{Price: 3, Amount: 1, ID: 3},
{Price: 5, Amount: 1, ID: 5},
{Price: 7, Amount: 1, ID: 7},
{Price: 9, Amount: 1, ID: 9},
{Price: 11, Amount: 1, ID: 11},
}
a.load(asksSnapshot)
// Update one instance with matching ID
err := a.updateInsertByID(Levels{{Price: 1, Amount: 2, ID: 1}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 7, 37, 6)
// Reset
a.load(asksSnapshot)
// Update all instances with matching ID in order
err = a.updateInsertByID(Levels{
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 5, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, a, 12, 72, 6)
// Update all instances with matching ID in backwards
err = a.updateInsertByID(Levels{
{Price: 11, Amount: 2, ID: 11},
{Price: 9, Amount: 2, ID: 9},
{Price: 7, Amount: 2, ID: 7},
{Price: 5, Amount: 2, ID: 5},
{Price: 3, Amount: 2, ID: 3},
{Price: 1, Amount: 2, ID: 1},
})
if err != nil {
t.Fatal(err)
}
Check(t, a, 12, 72, 6)
// Update all instances with matching ID all over the ship
err = a.updateInsertByID(Levels{
{Price: 11, Amount: 2, ID: 11},
{Price: 3, Amount: 2, ID: 3},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 1, Amount: 2, ID: 1},
{Price: 5, Amount: 2, ID: 5},
})
if err != nil {
t.Fatal(err)
}
Check(t, a, 12, 72, 6)
// Update all instances move one before ID in middle
err = a.updateInsertByID(Levels{
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 2, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, a, 12, 66, 6)
// Update all instances move one before ID at head
err = a.updateInsertByID(Levels{
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: .5, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, a, 12, 63, 6)
// Reset
a.load(asksSnapshot)
// Update all instances move one after ID
err = a.updateInsertByID(Levels{
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 8, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, a, 12, 78, 6)
// Reset
a.load(asksSnapshot)
// Update all instances move one after ID to tail
err = a.updateInsertByID(Levels{
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 12, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, a, 12, 86, 6)
// Update all instances then pop new instance
err = a.updateInsertByID(Levels{
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 12, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
{Price: 10, Amount: 2, ID: 10},
})
if err != nil {
t.Fatal(err)
}
Check(t, a, 14, 106, 7)
// Reset
a.load(asksSnapshot)
// Update all instances pop at head
err = a.updateInsertByID(Levels{
{Price: 0.5, Amount: 2, ID: 0},
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 12, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, a, 14, 87, 7)
// bookmark head and move to mid
err = a.updateInsertByID(Levels{{Price: 7.5, Amount: 2, ID: 0}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 14, 101, 7)
// bookmark head and move to tail
err = a.updateInsertByID(Levels{{Price: 12.5, Amount: 2, ID: 1}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 14, 124, 7)
// move tail location to head
err = a.updateInsertByID(Levels{{Price: 2.5, Amount: 2, ID: 1}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 14, 104, 7)
// move tail location to mid
err = a.updateInsertByID(Levels{{Price: 8, Amount: 2, ID: 5}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 14, 96, 7)
// insert at tail dont match
err = a.updateInsertByID(Levels{{Price: 30, Amount: 2, ID: 1234}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 16, 156, 8)
// insert between last and 2nd last
err = a.updateInsertByID(Levels{{Price: 12, Amount: 2, ID: 12345}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 18, 180, 9)
// readjust at end
err = a.updateInsertByID(Levels{{Price: 29, Amount: 3, ID: 1234}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 19, 207, 9)
// readjust further and decrease price past tail
err = a.updateInsertByID(Levels{{Price: 31, Amount: 3, ID: 1234}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 19, 213, 9)
// purge
a.load(nil)
// insert with no liquidity and jumbled
err = a.updateInsertByID(Levels{
{Price: 11, Amount: 2, ID: 11},
{Price: 9, Amount: 2, ID: 9},
{Price: 7, Amount: 2, ID: 7},
{Price: 0.5, Amount: 2, ID: 0},
{Price: 12, Amount: 2, ID: 5},
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
})
if err != nil {
t.Fatal(err)
}
Check(t, a, 14, 87, 7)
}
// 21614455 81.74 ns/op 0 B/op 0 allocs/op
func BenchmarkUpdateInsertByID_asks(b *testing.B) {
asks := Levels{}
asksSnapshot := Levels{
{Price: 1, Amount: 1, ID: 1},
{Price: 3, Amount: 1, ID: 3},
{Price: 5, Amount: 1, ID: 5},
{Price: 7, Amount: 1, ID: 7},
{Price: 9, Amount: 1, ID: 9},
{Price: 11, Amount: 1, ID: 11},
}
asks.load(asksSnapshot)
for b.Loop() {
err := asks.updateInsertByID(asksSnapshot, askCompare)
if err != nil {
b.Fatal(err)
}
}
}
func TestUpdateInsertByIDBids(t *testing.T) {
b := bidLevels{}
bidsSnapshot := Levels{
{Price: 11, Amount: 1, ID: 11},
{Price: 9, Amount: 1, ID: 9},
{Price: 7, Amount: 1, ID: 7},
{Price: 5, Amount: 1, ID: 5},
{Price: 3, Amount: 1, ID: 3},
{Price: 1, Amount: 1, ID: 1},
}
b.load(bidsSnapshot)
// Update one instance with matching ID
err := b.updateInsertByID(Levels{{Price: 1, Amount: 2, ID: 1}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 7, 37, 6)
// Reset
b.load(bidsSnapshot)
// Update all instances with matching ID in order
err = b.updateInsertByID(Levels{
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 5, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, b, 12, 72, 6)
// Update all instances with matching ID in backwards
err = b.updateInsertByID(Levels{
{Price: 11, Amount: 2, ID: 11},
{Price: 9, Amount: 2, ID: 9},
{Price: 7, Amount: 2, ID: 7},
{Price: 5, Amount: 2, ID: 5},
{Price: 3, Amount: 2, ID: 3},
{Price: 1, Amount: 2, ID: 1},
})
if err != nil {
t.Fatal(err)
}
Check(t, b, 12, 72, 6)
// Update all instances with matching ID all over the ship
err = b.updateInsertByID(Levels{
{Price: 11, Amount: 2, ID: 11},
{Price: 3, Amount: 2, ID: 3},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 1, Amount: 2, ID: 1},
{Price: 5, Amount: 2, ID: 5},
})
if err != nil {
t.Fatal(err)
}
Check(t, b, 12, 72, 6)
// Update all instances move one before ID in middle
err = b.updateInsertByID(Levels{
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 2, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, b, 12, 66, 6)
// Update all instances move one before ID at head
err = b.updateInsertByID(Levels{
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: .5, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, b, 12, 63, 6)
// Reset
b.load(bidsSnapshot)
// Update all instances move one after ID
err = b.updateInsertByID(Levels{
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 8, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, b, 12, 78, 6)
// Reset
b.load(bidsSnapshot)
// Update all instances move one after ID to tail
err = b.updateInsertByID(Levels{
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 12, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, b, 12, 86, 6)
// Update all instances then pop new instance
err = b.updateInsertByID(Levels{
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 12, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
{Price: 10, Amount: 2, ID: 10},
})
if err != nil {
t.Fatal(err)
}
Check(t, b, 14, 106, 7)
// Reset
b.load(bidsSnapshot)
// Update all instances pop at tail
err = b.updateInsertByID(Levels{
{Price: 0.5, Amount: 2, ID: 0},
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 12, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, b, 14, 87, 7)
// bookmark head and move to mid
err = b.updateInsertByID(Levels{{Price: 9.5, Amount: 2, ID: 5}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 14, 82, 7)
// bookmark head and move to tail
err = b.updateInsertByID(Levels{{Price: 0.25, Amount: 2, ID: 11}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 14, 60.5, 7)
// move tail location to head
err = b.updateInsertByID(Levels{{Price: 10, Amount: 2, ID: 11}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 14, 80, 7)
// move tail location to mid
err = b.updateInsertByID(Levels{{Price: 7.5, Amount: 2, ID: 0}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 14, 94, 7)
// insert at head dont match
err = b.updateInsertByID(Levels{{Price: 30, Amount: 2, ID: 1234}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 16, 154, 8)
// insert between last and 2nd last
err = b.updateInsertByID(Levels{{Price: 1.5, Amount: 2, ID: 12345}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 18, 157, 9)
// readjust at end
err = b.updateInsertByID(Levels{{Price: 1, Amount: 3, ID: 1}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 19, 158, 9)
// readjust further and decrease price past tail
err = b.updateInsertByID(Levels{{Price: .9, Amount: 3, ID: 1}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 19, 157.7, 9)
// purge
b.load(nil)
// insert with no liquidity and jumbled
err = b.updateInsertByID(Levels{
{Price: 0.5, Amount: 2, ID: 0},
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 12, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
})
if err != nil {
t.Fatal(err)
}
Check(t, b, 14, 87, 7)
}
// 20328886 59.94 ns/op 0 B/op 0 allocs/op
func BenchmarkUpdateInsertByID_bids(b *testing.B) {
bids := Levels{}
bidsSnapshot := Levels{
{Price: 0.5, Amount: 2, ID: 0},
{Price: 1, Amount: 2, ID: 1},
{Price: 3, Amount: 2, ID: 3},
{Price: 12, Amount: 2, ID: 5},
{Price: 7, Amount: 2, ID: 7},
{Price: 9, Amount: 2, ID: 9},
{Price: 11, Amount: 2, ID: 11},
}
bids.load(bidsSnapshot)
for b.Loop() {
err := bids.updateInsertByID(bidsSnapshot, bidCompare)
if err != nil {
b.Fatal(err)
}
}
}
func TestInsertUpdatesBid(t *testing.T) {
b := bidLevels{}
bidsSnapshot := Levels{
{Price: 11, Amount: 1, ID: 11},
{Price: 9, Amount: 1, ID: 9},
{Price: 7, Amount: 1, ID: 7},
{Price: 5, Amount: 1, ID: 5},
{Price: 3, Amount: 1, ID: 3},
{Price: 1, Amount: 1, ID: 1},
}
b.load(bidsSnapshot)
err := b.insertUpdates(Levels{
{Price: 11, Amount: 1, ID: 11},
{Price: 9, Amount: 1, ID: 9},
{Price: 7, Amount: 1, ID: 7},
{Price: 5, Amount: 1, ID: 5},
{Price: 3, Amount: 1, ID: 3},
{Price: 1, Amount: 1, ID: 1},
})
require.ErrorIs(t, err, errCollisionDetected)
Check(t, b, 6, 36, 6)
// Insert at head
err = b.insertUpdates(Levels{{Price: 12, Amount: 1, ID: 11}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 7, 48, 7)
// Insert at tail
err = b.insertUpdates(Levels{{Price: 0.5, Amount: 1, ID: 12}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 8, 48.5, 8)
// Insert at mid
err = b.insertUpdates(Levels{{Price: 5.5, Amount: 1, ID: 13}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 9, 54, 9)
// purge
b.load(nil)
// Add one at head
err = b.insertUpdates(Levels{{Price: 5.5, Amount: 1, ID: 13}})
if err != nil {
t.Fatal(err)
}
Check(t, b, 1, 5.5, 1)
}
func TestInsertUpdatesAsk(t *testing.T) {
a := askLevels{}
askSnapshot := Levels{
{Price: 1, Amount: 1, ID: 1},
{Price: 3, Amount: 1, ID: 3},
{Price: 5, Amount: 1, ID: 5},
{Price: 7, Amount: 1, ID: 7},
{Price: 9, Amount: 1, ID: 9},
{Price: 11, Amount: 1, ID: 11},
}
a.load(askSnapshot)
err := a.insertUpdates(Levels{
{Price: 11, Amount: 1, ID: 11},
{Price: 9, Amount: 1, ID: 9},
{Price: 7, Amount: 1, ID: 7},
{Price: 5, Amount: 1, ID: 5},
{Price: 3, Amount: 1, ID: 3},
{Price: 1, Amount: 1, ID: 1},
})
require.ErrorIs(t, err, errCollisionDetected)
Check(t, a, 6, 36, 6)
// Insert at tail
err = a.insertUpdates(Levels{{Price: 12, Amount: 1, ID: 11}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 7, 48, 7)
// Insert at head
err = a.insertUpdates(Levels{{Price: 0.5, Amount: 1, ID: 12}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 8, 48.5, 8)
// Insert at mid
err = a.insertUpdates(Levels{{Price: 5.5, Amount: 1, ID: 13}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 9, 54, 9)
// purge
a.load(nil)
// Add one at head
err = a.insertUpdates(Levels{{Price: 5.5, Amount: 1, ID: 13}})
if err != nil {
t.Fatal(err)
}
Check(t, a, 1, 5.5, 1)
}
// check checks depth values after an update has taken place
func Check(t *testing.T, depth any, liquidity, value float64, expectedLen int) {
t.Helper()
b, isBid := depth.(bidLevels)
a, isAsk := depth.(askLevels)
var l Levels
switch {
case isBid:
l = b.Levels
case isAsk:
l = a.Levels
default:
t.Fatal("value passed in is not of type bids or asks")
}
liquidityTotal, valueTotal := l.amount()
if liquidityTotal != liquidity {
l.display()
t.Fatalf("mismatched liquidity expecting %v but received %v", liquidity, liquidityTotal)
}
if valueTotal != value {
l.display()
t.Fatalf("mismatched total value expecting %v but received %v", value, valueTotal)
}
if len(l) != expectedLen {
l.display()
t.Fatalf("mismatched expected length count expecting %v but received %v", expectedLen, len(l))
}
if len(l) == 0 {
return
}
var price float64
for x := range l {
switch {
case price == 0:
price = l[x].Price
case isBid && price < l[x].Price:
l.display()
t.Fatal("Bid pricing out of order should be descending")
case isAsk && price > l[x].Price:
l.display()
t.Fatal("Ask pricing out of order should be ascending")
default:
price = l[x].Price
}
}
}
func TestAmount(t *testing.T) {
a := askLevels{}
askSnapshot := Levels{
{Price: 1, Amount: 1, ID: 1},
{Price: 3, Amount: 1, ID: 3},
{Price: 5, Amount: 1, ID: 5},
{Price: 7, Amount: 1, ID: 7},
{Price: 9, Amount: 1, ID: 9},
{Price: 11, Amount: 1, ID: 11},
}
a.load(askSnapshot)
liquidity, value := a.amount()
if liquidity != 6 {
t.Fatalf("incorrect liquidity calculation expected 6 but received %f", liquidity)
}
if value != 36 {
t.Fatalf("incorrect value calculation expected 36 but received %f", value)
}
}
func TestGetMovementByBaseAmount(t *testing.T) {
t.Parallel()
cases := []struct {
Name string
BaseAmount float64
ReferencePrice float64
BidLiquidity Levels
ExpectedNominal float64
ExpectedImpact float64
ExpectedCost float64
ExpectedError error
}{
{
Name: "no amount",
ExpectedError: errBaseAmountInvalid,
},
{
Name: "no reference price",
BaseAmount: 1,
ExpectedError: errInvalidReferencePrice,
},
{
Name: "not enough liquidity to service quote amount",
BaseAmount: 1,
ReferencePrice: 1000,
ExpectedError: errNoLiquidity,
},
{
Name: "thrasher test",
BidLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
BaseAmount: 10,
ReferencePrice: 10000,
ExpectedNominal: 0.8999999999999999,
ExpectedImpact: 2,
ExpectedCost: 900,
},
{
Name: "consume first level",
BidLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
BaseAmount: 2,
ReferencePrice: 10000,
ExpectedNominal: 0,
ExpectedImpact: 1,
ExpectedCost: 0,
},
{
Name: "consume most of first level",
BidLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
BaseAmount: 1.5,
ReferencePrice: 10000,
ExpectedNominal: 0,
ExpectedImpact: 0,
ExpectedCost: 0,
},
{
Name: "consume full liquidity",
BidLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
BaseAmount: 12,
ReferencePrice: 10000,
ExpectedNominal: 1.0833333333333395,
ExpectedImpact: FullLiquidityExhaustedPercentage,
ExpectedCost: 1300,
},
}
for _, tt := range cases {
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
depth := NewDepth(id)
err := depth.LoadSnapshot(&Book{Bids: tt.BidLiquidity, LastUpdated: time.Now(), LastPushed: time.Now(), RestSnapshot: true})
if err != nil {
t.Fatal(err)
}
movement, err := depth.bidLevels.getMovementByBase(tt.BaseAmount, tt.ReferencePrice, false)
require.ErrorIs(t, err, tt.ExpectedError)
if movement == nil {
return
}
if movement.NominalPercentage != tt.ExpectedNominal {
t.Fatalf("nominal received: '%v' but expected: '%v'",
movement.NominalPercentage, tt.ExpectedNominal)
}
if movement.ImpactPercentage != tt.ExpectedImpact {
t.Fatalf("impact received: '%v' but expected: '%v'",
movement.ImpactPercentage, tt.ExpectedImpact)
}
if movement.SlippageCost != tt.ExpectedCost {
t.Fatalf("cost received: '%v' but expected: '%v'",
movement.SlippageCost, tt.ExpectedCost)
}
})
}
}
func assertMovement(tb testing.TB, expected, movement *Movement) {
tb.Helper()
assert.InDelta(tb, expected.Sold, movement.Sold, accuracy10dp, "Sold should be correct")
assert.InDelta(tb, expected.Purchased, movement.Purchased, accuracy10dp, "Purchased should be correct")
assert.InDelta(tb, expected.AverageOrderCost, movement.AverageOrderCost, accuracy10dp, "AverageOrderCost should be correct")
assert.InDelta(tb, expected.StartPrice, movement.StartPrice, accuracy10dp, "StartPrice should be correct")
assert.InDelta(tb, expected.EndPrice, movement.EndPrice, accuracy10dp, "EndPrice should be correct")
assert.InDelta(tb, expected.NominalPercentage, movement.NominalPercentage, accuracy10dp, "NominalPercentage should be correct")
assert.Equal(tb, expected.FullBookSideConsumed, movement.FullBookSideConsumed, "FullBookSideConsumed should be correct")
}
func TestGetBaseAmountFromNominalSlippage(t *testing.T) {
t.Parallel()
cases := []struct {
Name string
NominalSlippage float64
ReferencePrice float64
BidLiquidity Levels
ExpectedShift *Movement
ExpectedError error
}{
{
Name: "invalid slippage",
NominalSlippage: -1,
ExpectedError: errInvalidNominalSlippage,
},
{
Name: "invalid slippage - larger than 100%",
NominalSlippage: 101,
ExpectedError: errInvalidSlippageCannotExceed100,
},
{
Name: "no reference price",
NominalSlippage: 1,
ExpectedError: errInvalidReferencePrice,
},
{
Name: "no liquidity to service quote amount",
NominalSlippage: 1,
ReferencePrice: 1000,
ExpectedError: errNoLiquidity,
},
{
Name: "thrasher test",
BidLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
NominalSlippage: 1,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 11,
Purchased: 108900,
AverageOrderCost: 9900,
NominalPercentage: 1,
StartPrice: 10000,
EndPrice: 9800,
},
},
{
Name: "consume first level - take one amount out of second",
BidLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
NominalSlippage: 0.33333333333334,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 3.0000000000000275,
Purchased: 29900.00000000027,
AverageOrderCost: 9966.666666666664,
NominalPercentage: 0.33333333333334,
StartPrice: 10000,
EndPrice: 9900,
},
},
{
Name: "consume full liquidity",
BidLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
NominalSlippage: 10,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 12,
Purchased: 118700,
AverageOrderCost: 9891.666666666666,
NominalPercentage: 1.0833333333333395,
StartPrice: 10000,
EndPrice: 9800,
FullBookSideConsumed: true,
},
},
{
Name: "scotts lovely slippery slippage requirements",
BidLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
NominalSlippage: 0.00000000000000000000000000000000000000000000000000000000000000000000000000000000001,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 2,
Purchased: 20000,
AverageOrderCost: 10000,
StartPrice: 10000,
EndPrice: 10000,
},
},
}
for _, tt := range cases {
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
depth := NewDepth(id)
err := depth.LoadSnapshot(&Book{Bids: tt.BidLiquidity, LastUpdated: time.Now(), LastPushed: time.Now(), RestSnapshot: true})
assert.NoError(t, err, "LoadSnapshot should not error")
base, err := depth.bidLevels.hitBidsByNominalSlippage(tt.NominalSlippage, tt.ReferencePrice)
if tt.ExpectedError != nil {
assert.ErrorIs(t, err, tt.ExpectedError, "Should error correctly")
} else {
assertMovement(t, tt.ExpectedShift, base)
}
})
}
}
// IsEqual is a tester function for comparison.
func (m *Movement) IsEqual(that *Movement) bool {
if m == nil || that == nil {
return m == nil && that == nil
}
return m.FullBookSideConsumed == that.FullBookSideConsumed &&
m.Sold == that.Sold &&
m.Purchased == that.Purchased &&
m.NominalPercentage == that.NominalPercentage &&
m.ImpactPercentage == that.ImpactPercentage &&
m.EndPrice == that.EndPrice &&
m.StartPrice == that.StartPrice &&
m.AverageOrderCost == that.AverageOrderCost
}
func TestGetBaseAmountFromImpact(t *testing.T) {
t.Parallel()
cases := []struct {
Name string
ImpactSlippage float64
ReferencePrice float64
BidLiquidity Levels
ExpectedShift *Movement
ExpectedError error
}{
{
Name: "invalid slippage",
ExpectedError: errInvalidImpactSlippage,
},
{
Name: "invalid slippage - exceed 100%",
ImpactSlippage: 101,
ExpectedError: errInvalidSlippageCannotExceed100,
},
{
Name: "no reference price",
ImpactSlippage: 1,
ExpectedError: errInvalidReferencePrice,
},
{
Name: "no liquidity",
ImpactSlippage: 1,
ReferencePrice: 10000,
ExpectedError: errNoLiquidity,
},
{
Name: "thrasher test",
BidLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
ImpactSlippage: 1,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 2,
Purchased: 20000,
ImpactPercentage: 1,
AverageOrderCost: 10000,
StartPrice: 10000,
EndPrice: 9900,
},
},
{
Name: "consume first level and second level",
BidLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
ImpactSlippage: 2,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 9,
Purchased: 89300,
AverageOrderCost: 9922.222222222223,
ImpactPercentage: 2,
StartPrice: 10000,
EndPrice: 9800,
},
},
{
Name: "consume full liquidity",
BidLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
ImpactSlippage: 10,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 12,
Purchased: 118700,
ImpactPercentage: FullLiquidityExhaustedPercentage,
AverageOrderCost: 9891.666666666666,
StartPrice: 10000,
EndPrice: 9800,
FullBookSideConsumed: true,
},
},
}
for _, tt := range cases {
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
depth := NewDepth(id)
err := depth.LoadSnapshot(&Book{Bids: tt.BidLiquidity, LastUpdated: time.Now(), LastPushed: time.Now(), RestSnapshot: true})
if err != nil {
t.Fatal(err)
}
base, err := depth.bidLevels.hitBidsByImpactSlippage(tt.ImpactSlippage, tt.ReferencePrice)
require.ErrorIs(t, err, tt.ExpectedError)
if !base.IsEqual(tt.ExpectedShift) {
t.Fatalf("%s quote received: '%+v' but expected: '%+v'",
tt.Name, base, tt.ExpectedShift)
}
})
}
}
func TestGetMovementByQuoteAmount(t *testing.T) {
t.Parallel()
cases := []struct {
Name string
QuoteAmount float64
ReferencePrice float64
AskLiquidity Levels
ExpectedNominal float64
ExpectedImpact float64
ExpectedCost float64
ExpectedError error
}{
{
Name: "no amount",
ExpectedError: errQuoteAmountInvalid,
},
{
Name: "no reference price",
QuoteAmount: 1,
ExpectedError: errInvalidReferencePrice,
},
{
Name: "not enough liquidity to service quote amount",
QuoteAmount: 1,
ReferencePrice: 1000,
ExpectedError: errNoLiquidity,
},
{
Name: "thrasher test",
AskLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
QuoteAmount: 100900,
ReferencePrice: 10000,
ExpectedNominal: 0.8999999999999999,
ExpectedImpact: 2,
ExpectedCost: 900,
},
{
Name: "consume first level",
AskLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
QuoteAmount: 20000,
ReferencePrice: 10000,
ExpectedNominal: 0,
ExpectedImpact: 1,
ExpectedCost: 0,
},
{
Name: "consume most of first level",
AskLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
QuoteAmount: 15000,
ReferencePrice: 10000,
ExpectedNominal: 0,
ExpectedImpact: 0,
ExpectedCost: 0,
},
{
Name: "consume full liquidity",
AskLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
QuoteAmount: 121300,
ReferencePrice: 10000,
ExpectedNominal: 1.0833333333333395,
ExpectedImpact: FullLiquidityExhaustedPercentage,
ExpectedCost: 1300,
},
}
for _, tt := range cases {
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
depth := NewDepth(id)
err := depth.LoadSnapshot(&Book{Asks: tt.AskLiquidity, LastUpdated: time.Now(), LastPushed: time.Now(), RestSnapshot: true})
if err != nil {
t.Fatal(err)
}
movement, err := depth.askLevels.getMovementByQuotation(tt.QuoteAmount, tt.ReferencePrice, false)
require.ErrorIs(t, err, tt.ExpectedError)
if movement == nil {
return
}
if movement.NominalPercentage != tt.ExpectedNominal {
t.Fatalf("nominal received: '%v' but expected: '%v'",
movement.NominalPercentage, tt.ExpectedNominal)
}
if movement.ImpactPercentage != tt.ExpectedImpact {
t.Fatalf("impact received: '%v' but expected: '%v'",
movement.ImpactPercentage, tt.ExpectedImpact)
}
if movement.SlippageCost != tt.ExpectedCost {
t.Fatalf("cost received: '%v' but expected: '%v'",
movement.SlippageCost, tt.ExpectedCost)
}
})
}
}
func TestGetQuoteAmountFromNominalSlippage(t *testing.T) {
t.Parallel()
cases := []struct {
Name string
NominalSlippage float64
ReferencePrice float64
AskLiquidity Levels
ExpectedShift *Movement
ExpectedError error
}{
{
Name: "invalid slippage",
NominalSlippage: -1,
ExpectedError: errInvalidNominalSlippage,
},
{
Name: "no reference price",
NominalSlippage: 1,
ExpectedError: errInvalidReferencePrice,
},
{
Name: "no liquidity",
NominalSlippage: 1,
ReferencePrice: 10000,
ExpectedError: errNoLiquidity,
},
{
Name: "consume first level - one amount on second level",
AskLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
NominalSlippage: 0.33333333333334,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 30100.000000000276, // <- expected rounding issue
Purchased: 3.0000000000000275,
AverageOrderCost: 10033.333333333333333333333333333,
NominalPercentage: 0.33333333333334,
StartPrice: 10000,
EndPrice: 10100,
},
},
{
Name: "last level total agg meeting 1 percent nominally",
AskLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
NominalSlippage: 1,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 111100,
Purchased: 11,
AverageOrderCost: 10100,
NominalPercentage: 1,
StartPrice: 10000,
EndPrice: 10200,
},
},
{
Name: "take full second level",
AskLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
NominalSlippage: 0.7777777777777738,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 90700,
Purchased: 9,
AverageOrderCost: 10077.777777777777,
NominalPercentage: 0.7777777777777738,
StartPrice: 10000,
EndPrice: 10100,
},
},
{
Name: "consume full liquidity",
AskLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
NominalSlippage: 10,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 121300,
Purchased: 12,
AverageOrderCost: 10108.333333333334,
NominalPercentage: 1.0833333333333395,
StartPrice: 10000,
EndPrice: 10200,
FullBookSideConsumed: true,
},
},
{
Name: "scotts lovely slippery slippage requirements",
AskLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
NominalSlippage: 0.00000000000000000000000000000000000000000000000000000000000000000000000000000000001,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 20000,
Purchased: 2,
AverageOrderCost: 10000,
StartPrice: 10000,
EndPrice: 10000,
},
},
}
for _, tt := range cases {
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
depth := NewDepth(id)
err := depth.LoadSnapshot(&Book{Asks: tt.AskLiquidity, LastUpdated: time.Now(), LastPushed: time.Now(), RestSnapshot: true})
assert.NoError(t, err, "LoadSnapshot should not error")
quote, err := depth.askLevels.liftAsksByNominalSlippage(tt.NominalSlippage, tt.ReferencePrice)
if tt.ExpectedError != nil {
assert.ErrorIs(t, err, tt.ExpectedError, "Should error correctly")
} else {
assertMovement(t, tt.ExpectedShift, quote)
}
})
}
}
func TestGetQuoteAmountFromImpact(t *testing.T) {
t.Parallel()
cases := []struct {
Name string
ImpactSlippage float64
ReferencePrice float64
AskLiquidity Levels
ExpectedShift *Movement
ExpectedError error
}{
{
Name: "invalid slippage",
ImpactSlippage: -1,
ExpectedError: errInvalidImpactSlippage,
},
{
Name: "no reference price",
ImpactSlippage: 1,
ExpectedError: errInvalidReferencePrice,
},
{
Name: "no liquidity",
ImpactSlippage: 1,
ReferencePrice: 1000,
ExpectedError: errNoLiquidity,
},
{
Name: "thrasher test",
AskLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
ImpactSlippage: 1,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 20000,
Purchased: 2,
AverageOrderCost: 10000,
ImpactPercentage: 1,
StartPrice: 10000,
EndPrice: 10100,
},
},
{
Name: "consume first level and second level",
AskLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
ImpactSlippage: 2,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 90700,
Purchased: 9,
AverageOrderCost: 10077.777777777777777777777777778,
ImpactPercentage: 2,
StartPrice: 10000,
EndPrice: 10200,
},
},
{
Name: "consume full liquidity",
AskLiquidity: Levels{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
ImpactSlippage: 10,
ReferencePrice: 10000,
ExpectedShift: &Movement{
Sold: 121300,
Purchased: 12,
AverageOrderCost: 10108.333333333333333333333333333,
ImpactPercentage: FullLiquidityExhaustedPercentage,
StartPrice: 10000,
EndPrice: 10200,
FullBookSideConsumed: true,
},
},
}
for _, tt := range cases {
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
depth := NewDepth(id)
err := depth.LoadSnapshot(&Book{Asks: tt.AskLiquidity, LastUpdated: time.Now(), LastPushed: time.Now(), RestSnapshot: true})
assert.NoError(t, err, "LoadSnapshot should not error")
quote, err := depth.askLevels.liftAsksByImpactSlippage(tt.ImpactSlippage, tt.ReferencePrice)
if tt.ExpectedError != nil {
assert.ErrorIs(t, err, tt.ExpectedError, "Should error correctly")
} else {
assertMovement(t, tt.ExpectedShift, quote)
}
})
}
}
func TestGetHeadPrice(t *testing.T) {
t.Parallel()
depth := NewDepth(id)
_, err := depth.bidLevels.getHeadPriceNoLock()
require.ErrorIs(t, err, errNoLiquidity)
_, err = depth.askLevels.getHeadPriceNoLock()
require.ErrorIs(t, err, errNoLiquidity)
err = depth.LoadSnapshot(&Book{Bids: bid, Asks: ask, LastUpdated: time.Now(), LastPushed: time.Now(), RestSnapshot: true})
require.NoError(t, err, "LoadSnapshot must not error")
val, err := depth.bidLevels.getHeadPriceNoLock()
require.NoError(t, err)
if val != 1336 {
t.Fatal("unexpected value")
}
val, err = depth.askLevels.getHeadPriceNoLock()
require.NoError(t, err)
if val != 1337 {
t.Fatal("unexpected value", val)
}
}
func TestFinalizeFields(t *testing.T) {
m := &Movement{}
_, err := m.finalizeFields(0, 0, 0, 0, false)
assert.ErrorIs(t, err, errInvalidCost, "finalizeFields should error when cost is invalid")
_, err = m.finalizeFields(1, 0, 0, 0, false)
assert.ErrorIs(t, err, errInvalidAmount, "finalizeFields should error when amount is invalid")
_, err = m.finalizeFields(1, 1, 0, 0, false)
assert.ErrorIs(t, err, errInvalidHeadPrice, "finalizeFields should error correctly with bad head price")
// Test slippage as per https://en.wikipedia.org/wiki/Slippage_(finance)
mov, err := m.finalizeFields(20000*151.11585, 20000, 151.08, 0, false)
assert.NoError(t, err, "finalizeFields should not error")
assert.InDelta(t, 717.0, mov.SlippageCost, 0.000000001, "SlippageCost should be correct")
}
// 8384302 150.9 ns/op 480 B/op 1 allocs/op
func BenchmarkRetrieve(b *testing.B) {
asks := Levels{}
asksSnapshot := Levels{
{Price: 1, Amount: 1, ID: 1},
{Price: 3, Amount: 1, ID: 3},
{Price: 5, Amount: 1, ID: 5},
{Price: 7, Amount: 1, ID: 7},
{Price: 9, Amount: 1, ID: 9},
{Price: 11, Amount: 1, ID: 11},
}
asks.load(asksSnapshot)
for b.Loop() {
_ = asks.retrieve(6)
}
}