package orderbook import ( "errors" "fmt" "testing" "time" ) var ask = Items{ Item{Price: 1337, Amount: 1}, Item{Price: 1338, Amount: 1}, Item{Price: 1339, Amount: 1}, Item{Price: 1340, Amount: 1}, Item{Price: 1341, Amount: 1}, Item{Price: 1342, Amount: 1}, Item{Price: 1343, Amount: 1}, Item{Price: 1344, Amount: 1}, Item{Price: 1345, Amount: 1}, Item{Price: 1346, Amount: 1}, Item{Price: 1347, Amount: 1}, Item{Price: 1348, Amount: 1}, Item{Price: 1349, Amount: 1}, Item{Price: 1350, Amount: 1}, Item{Price: 1351, Amount: 1}, Item{Price: 1352, Amount: 1}, Item{Price: 1353, Amount: 1}, Item{Price: 1354, Amount: 1}, Item{Price: 1355, Amount: 1}, Item{Price: 1356, Amount: 1}, } var bid = Items{ Item{Price: 1336, Amount: 1}, Item{Price: 1335, Amount: 1}, Item{Price: 1334, Amount: 1}, Item{Price: 1333, Amount: 1}, Item{Price: 1332, Amount: 1}, Item{Price: 1331, Amount: 1}, Item{Price: 1330, Amount: 1}, Item{Price: 1329, Amount: 1}, Item{Price: 1328, Amount: 1}, Item{Price: 1327, Amount: 1}, Item{Price: 1326, Amount: 1}, Item{Price: 1325, Amount: 1}, Item{Price: 1324, Amount: 1}, Item{Price: 1323, Amount: 1}, Item{Price: 1322, Amount: 1}, Item{Price: 1321, Amount: 1}, Item{Price: 1320, Amount: 1}, Item{Price: 1319, Amount: 1}, Item{Price: 1318, Amount: 1}, Item{Price: 1317, Amount: 1}, } // Display displays depth content for tests func (ll *linkedList) display() { for tip := ll.head; tip != nil; tip = tip.Next { fmt.Printf("NODE: %+v %p \n", tip, tip) } fmt.Println() } func TestLoad(t *testing.T) { list := asks{} Check(t, list, 0, 0, 0) stack := newStack() list.load(Items{ {Price: 1, Amount: 1}, {Price: 3, Amount: 1}, {Price: 5, Amount: 1}, {Price: 7, Amount: 1}, {Price: 9, Amount: 1}, {Price: 11, Amount: 1}, }, stack) if stack.getCount() != 0 { t.Fatalf("incorrect stack count expected: %v received: %v", 0, stack.getCount()) } Check(t, list, 6, 36, 6) list.load(Items{ {Price: 1, Amount: 1}, {Price: 3, Amount: 1}, {Price: 5, Amount: 1}, }, stack) if stack.getCount() != 3 { t.Fatalf("incorrect stack count expected: %v received: %v", 3, stack.getCount()) } Check(t, list, 3, 9, 3) list.load(Items{ {Price: 1, Amount: 1}, {Price: 3, Amount: 1}, {Price: 5, Amount: 1}, {Price: 7, Amount: 1}, }, stack) if stack.getCount() != 2 { t.Fatalf("incorrect stack count expected: %v received: %v", 2, stack.getCount()) } Check(t, list, 4, 16, 4) // purge entire list list.load(nil, stack) if stack.getCount() != 6 { t.Fatalf("incorrect stack count expected: %v received: %v", 6, stack.getCount()) } Check(t, list, 0, 0, 0) } // 22222386 57.3 ns/op 0 B/op 0 allocs/op (old) // 27906781 42.4 ns/op 0 B/op 0 allocs/op (new) func BenchmarkLoad(b *testing.B) { ll := linkedList{} s := newStack() for i := 0; i < b.N; i++ { ll.load(ask, s) } } func TestUpdateInsertByPrice(t *testing.T) { a := asks{} stack := newStack() asksSnapshot := Items{ {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, stack) // Update one instance with matching price a.updateInsertByPrice(Items{ {Price: 1, Amount: 2}, }, stack, 0, getNow()) Check(t, a, 7, 37, 6) if stack.getCount() != 0 { t.Fatalf("incorrect stack count expected: %v received: %v", 0, stack.getCount()) } // Insert at head a.updateInsertByPrice(Items{ {Price: 0.5, Amount: 2}, }, stack, 0, getNow()) Check(t, a, 9, 38, 7) if stack.getCount() != 0 { t.Fatalf("incorrect stack count expected: %v received: %v", 0, stack.getCount()) } // Insert at tail a.updateInsertByPrice(Items{ {Price: 12, Amount: 2}, }, stack, 0, getNow()) Check(t, a, 11, 62, 8) if stack.getCount() != 0 { t.Fatalf("incorrect stack count expected: %v received: %v", 0, stack.getCount()) } // Insert between price and up to and beyond max allowable depth level a.updateInsertByPrice(Items{ {Price: 11.5, Amount: 2}, {Price: 10.5, Amount: 2}, {Price: 13, Amount: 2}, }, stack, 10, getNow()) Check(t, a, 15, 106, 10) if stack.getCount() != 1 { t.Fatalf("incorrect stack count expected: %v received: %v", 1, stack.getCount()) } // delete at tail a.updateInsertByPrice(Items{ {Price: 12, Amount: 0}, }, stack, 0, getNow()) Check(t, a, 13, 82, 9) if stack.getCount() != 2 { t.Fatalf("incorrect stack count expected: %v received: %v", 2, stack.getCount()) } // delete at mid a.updateInsertByPrice(Items{ {Price: 7, Amount: 0}, }, stack, 0, getNow()) Check(t, a, 12, 75, 8) if stack.getCount() != 3 { t.Fatalf("incorrect stack count expected: %v received: %v", 3, stack.getCount()) } // delete at head a.updateInsertByPrice(Items{ {Price: 0.5, Amount: 0}, }, stack, 0, getNow()) Check(t, a, 10, 74, 7) if stack.getCount() != 4 { t.Fatalf("incorrect stack count expected: %v received: %v", 4, stack.getCount()) } // purge if liquidity plunges to zero a.load(nil, stack) // rebuild everything again a.updateInsertByPrice(Items{ {Price: 1, Amount: 1}, {Price: 3, Amount: 1}, {Price: 5, Amount: 1}, {Price: 7, Amount: 1}, {Price: 9, Amount: 1}, {Price: 11, Amount: 1}, }, stack, 0, getNow()) Check(t, a, 6, 36, 6) if stack.getCount() != 5 { t.Fatalf("incorrect stack count expected: %v received: %v", 4, stack.getCount()) } b := bids{} bidsSnapshot := Items{ {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, stack) // Update one instance with matching price b.updateInsertByPrice(Items{ {Price: 11, Amount: 2}, }, stack, 0, getNow()) Check(t, b, 7, 47, 6) if stack.getCount() != 0 { t.Fatalf("incorrect stack count expected: %v received: %v", 0, stack.getCount()) } // Insert at head b.updateInsertByPrice(Items{ {Price: 12, Amount: 2}, }, stack, 0, getNow()) Check(t, b, 9, 71, 7) if stack.getCount() != 0 { t.Fatalf("incorrect stack count expected: %v received: %v", 0, stack.getCount()) } // Insert at tail b.updateInsertByPrice(Items{ {Price: 0.5, Amount: 2}, }, stack, 0, getNow()) Check(t, b, 11, 72, 8) if stack.getCount() != 0 { t.Fatalf("incorrect stack count expected: %v received: %v", 0, stack.getCount()) } // Insert between price and up to and beyond max allowable depth level b.updateInsertByPrice(Items{ {Price: 11.5, Amount: 2}, {Price: 10.5, Amount: 2}, {Price: 13, Amount: 2}, }, stack, 10, getNow()) Check(t, b, 15, 141, 10) if stack.getCount() != 1 { t.Fatalf("incorrect stack count expected: %v received: %v", 0, stack.getCount()) } // Insert between price and up to and beyond max allowable depth level b.updateInsertByPrice(Items{ {Price: 1, Amount: 0}, }, stack, 0, getNow()) Check(t, b, 14, 140, 9) if stack.getCount() != 2 { t.Fatalf("incorrect stack count expected: %v received: %v", 2, stack.getCount()) } // delete at mid b.updateInsertByPrice(Items{ {Price: 10.5, Amount: 0}, }, stack, 0, getNow()) Check(t, b, 12, 119, 8) if stack.getCount() != 3 { t.Fatalf("incorrect stack count expected: %v received: %v", 3, stack.getCount()) } // delete at head b.updateInsertByPrice(Items{ {Price: 13, Amount: 0}, }, stack, 0, getNow()) Check(t, b, 10, 93, 7) if stack.getCount() != 4 { t.Fatalf("incorrect stack count expected: %v received: %v", 4, stack.getCount()) } // purge if liquidity plunges to zero b.load(nil, stack) // rebuild everything again b.updateInsertByPrice(Items{ {Price: 1, Amount: 1}, {Price: 3, Amount: 1}, {Price: 5, Amount: 1}, {Price: 7, Amount: 1}, {Price: 9, Amount: 1}, {Price: 11, Amount: 1}, }, stack, 0, getNow()) Check(t, b, 6, 36, 6) if stack.getCount() != 5 { t.Fatalf("incorrect stack count expected: %v received: %v", 4, stack.getCount()) } } func TestCleanup(t *testing.T) { a := asks{} stack := newStack() asksSnapshot := Items{ {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, stack) a.cleanup(6, stack) Check(t, a, 6, 36, 6) a.cleanup(5, stack) Check(t, a, 5, 25, 5) a.cleanup(1, stack) Check(t, a, 1, 1, 1) a.cleanup(10, stack) Check(t, a, 1, 1, 1) a.cleanup(0, stack) // will purge, underlying checks are done elseware to prevent this Check(t, a, 0, 0, 0) } // 46154023 24.0 ns/op 0 B/op 0 allocs/op (old) // 134830672 9.83 ns/op 0 B/op 0 allocs/op (new) func BenchmarkUpdateInsertByPrice_Amend(b *testing.B) { a := asks{} stack := newStack() a.load(ask, stack) updates := Items{ { Price: 1337, // Amend Amount: 2, }, { Price: 1337, // Amend Amount: 1, }, } for i := 0; i < b.N; i++ { a.updateInsertByPrice(updates, stack, 0, getNow()) } } // 49763002 24.9 ns/op 0 B/op 0 allocs/op func BenchmarkUpdateInsertByPrice_Insert_Delete(b *testing.B) { a := asks{} stack := newStack() a.load(ask, stack) updates := Items{ { Price: 1337.5, // Insert Amount: 2, }, { Price: 1337.5, // Delete Amount: 0, }, } for i := 0; i < b.N; i++ { a.updateInsertByPrice(updates, stack, 0, getNow()) } } func TestUpdateByID(t *testing.T) { a := asks{} s := newStack() asksSnapshot := Items{ {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, s) err := a.updateByID(Items{ {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(Items{ {Price: 11, Amount: 1, ID: 1337}, }) if !errors.Is(err, errIDCannotBeMatched) { t.Fatalf("expecting %s but received %v", errIDCannotBeMatched, err) } err = a.updateByID(Items{ // Simulate Bitmex updating {Price: 0, Amount: 1337, ID: 3}, }) if !errors.Is(err, nil) { t.Fatalf("expecting %v but received %v", nil, err) } if a.retrieve()[1].Price == 0 { t.Fatal("price should not be replaced with zero") } if a.retrieve()[1].Amount != 1337 { t.Fatal("unexpected value for update") } } // 46043871 25.9 ns/op 0 B/op 0 allocs/op func BenchmarkUpdateByID(b *testing.B) { asks := linkedList{} s := newStack() asksSnapshot := Items{ {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, s) for i := 0; i < b.N; i++ { err := asks.updateByID(Items{ {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 { b.Fatal(err) } } } func TestDeleteByID(t *testing.T) { a := asks{} s := newStack() asksSnapshot := Items{ {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, s) // Delete at head err := a.deleteByID(Items{ {Price: 1, Amount: 1, ID: 1}, }, s, false) if err != nil { t.Fatal(err) } Check(t, a, 5, 35, 5) // Delete at tail err = a.deleteByID(Items{ {Price: 1, Amount: 1, ID: 11}, }, s, false) if err != nil { t.Fatal(err) } Check(t, a, 4, 24, 4) // Delete in middle err = a.deleteByID(Items{ {Price: 1, Amount: 1, ID: 5}, }, s, false) if err != nil { t.Fatal(err) } Check(t, a, 3, 19, 3) // Intentional error err = a.deleteByID(Items{ {Price: 11, Amount: 1, ID: 1337}, }, s, false) if !errors.Is(err, errIDCannotBeMatched) { t.Fatalf("expecting %s but received %v", errIDCannotBeMatched, err) } // Error bypass err = a.deleteByID(Items{ {Price: 11, Amount: 1, ID: 1337}, }, s, true) if err != nil { t.Fatal(err) } } func TestUpdateInsertByIDAsk(t *testing.T) { a := asks{} s := newStack() asksSnapshot := Items{ {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, s) // Update one instance with matching ID err := a.updateInsertByID(Items{ {Price: 1, Amount: 2, ID: 1}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 7, 37, 6) // Reset a.load(asksSnapshot, s) // Update all instances with matching ID in order err = a.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 12, 72, 6) // Update all instances with matching ID in backwards err = a.updateInsertByID(Items{ {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}, }, s) 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(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 12, 72, 6) // Update all instances move one before ID in middle err = a.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 12, 66, 6) // Update all instances move one before ID at head err = a.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 12, 63, 6) // Reset a.load(asksSnapshot, s) // Update all instances move one after ID err = a.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 12, 78, 6) // Reset a.load(asksSnapshot, s) // Update all instances move one after ID to tail err = a.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 12, 86, 6) // Update all instances then pop new instance err = a.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 14, 106, 7) // Reset a.load(asksSnapshot, s) // Update all instances pop at head err = a.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 14, 87, 7) // bookmark head and move to mid err = a.updateInsertByID(Items{ {Price: 7.5, Amount: 2, ID: 0}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 14, 101, 7) // bookmark head and move to tail err = a.updateInsertByID(Items{ {Price: 12.5, Amount: 2, ID: 1}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 14, 124, 7) // move tail location to head err = a.updateInsertByID(Items{ {Price: 2.5, Amount: 2, ID: 1}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 14, 104, 7) // move tail location to mid err = a.updateInsertByID(Items{ {Price: 8, Amount: 2, ID: 5}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 14, 96, 7) // insert at tail dont match err = a.updateInsertByID(Items{ {Price: 30, Amount: 2, ID: 1234}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 16, 156, 8) // insert between last and 2nd last err = a.updateInsertByID(Items{ {Price: 12, Amount: 2, ID: 12345}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 18, 180, 9) // readjust at end err = a.updateInsertByID(Items{ {Price: 29, Amount: 3, ID: 1234}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 19, 207, 9) // readjust further and decrease price past tail err = a.updateInsertByID(Items{ {Price: 31, Amount: 3, ID: 1234}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 19, 213, 9) // purge a.load(nil, s) // insert with no liquidity and jumbled err = a.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 14, 87, 7) } func TestUpdateInsertByIDBids(t *testing.T) { b := bids{} s := newStack() bidsSnapshot := Items{ {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, s) // Update one instance with matching ID err := b.updateInsertByID(Items{ {Price: 1, Amount: 2, ID: 1}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 7, 37, 6) // Reset b.load(bidsSnapshot, s) // Update all instances with matching ID in order err = b.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 12, 72, 6) // Update all instances with matching ID in backwards err = b.updateInsertByID(Items{ {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}, }, s) 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(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 12, 72, 6) // Update all instances move one before ID in middle err = b.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 12, 66, 6) // Update all instances move one before ID at head err = b.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 12, 63, 6) // Reset b.load(bidsSnapshot, s) // Update all instances move one after ID err = b.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 12, 78, 6) // Reset b.load(bidsSnapshot, s) // Update all instances move one after ID to tail err = b.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 12, 86, 6) // Update all instances then pop new instance err = b.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 14, 106, 7) // Reset b.load(bidsSnapshot, s) // Update all instances pop at tail err = b.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 14, 87, 7) // bookmark head and move to mid err = b.updateInsertByID(Items{ {Price: 9.5, Amount: 2, ID: 5}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 14, 82, 7) // bookmark head and move to tail err = b.updateInsertByID(Items{ {Price: 0.25, Amount: 2, ID: 11}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 14, 60.5, 7) // move tail location to head err = b.updateInsertByID(Items{ {Price: 10, Amount: 2, ID: 11}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 14, 80, 7) // move tail location to mid err = b.updateInsertByID(Items{ {Price: 7.5, Amount: 2, ID: 0}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 14, 94, 7) // insert at head dont match err = b.updateInsertByID(Items{ {Price: 30, Amount: 2, ID: 1234}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 16, 154, 8) // insert between last and 2nd last err = b.updateInsertByID(Items{ {Price: 1.5, Amount: 2, ID: 12345}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 18, 157, 9) // readjust at end err = b.updateInsertByID(Items{ {Price: 1, Amount: 3, ID: 1}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 19, 158, 9) // readjust further and decrease price past tail err = b.updateInsertByID(Items{ {Price: .9, Amount: 3, ID: 1}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 19, 157.7, 9) // purge b.load(nil, s) // insert with no liquidity and jumbled err = b.updateInsertByID(Items{ {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}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 14, 87, 7) } func TestInsertUpdatesBid(t *testing.T) { b := bids{} s := newStack() bidsSnapshot := Items{ {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, s) err := b.insertUpdates(Items{ {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}, }, s) if !errors.Is(err, errCollisionDetected) { t.Fatalf("expected error %s but received %v", errCollisionDetected, err) } Check(t, b, 6, 36, 6) // Insert at head err = b.insertUpdates(Items{ {Price: 12, Amount: 1, ID: 11}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 7, 48, 7) // Insert at tail err = b.insertUpdates(Items{ {Price: 0.5, Amount: 1, ID: 12}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 8, 48.5, 8) // Insert at mid err = b.insertUpdates(Items{ {Price: 5.5, Amount: 1, ID: 13}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 9, 54, 9) // purge b.load(nil, s) // Add one at head err = b.insertUpdates(Items{ {Price: 5.5, Amount: 1, ID: 13}, }, s) if err != nil { t.Fatal(err) } Check(t, b, 1, 5.5, 1) } func TestInsertUpdatesAsk(t *testing.T) { a := asks{} s := newStack() askSnapshot := Items{ {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, s) err := a.insertUpdates(Items{ {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}, }, s) if !errors.Is(err, errCollisionDetected) { t.Fatalf("expected error %s but received %v", errCollisionDetected, err) } Check(t, a, 6, 36, 6) // Insert at tail err = a.insertUpdates(Items{ {Price: 12, Amount: 1, ID: 11}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 7, 48, 7) // Insert at head err = a.insertUpdates(Items{ {Price: 0.5, Amount: 1, ID: 12}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 8, 48.5, 8) // Insert at mid err = a.insertUpdates(Items{ {Price: 5.5, Amount: 1, ID: 13}, }, s) if err != nil { t.Fatal(err) } Check(t, a, 9, 54, 9) // purge a.load(nil, s) // Add one at head err = a.insertUpdates(Items{ {Price: 5.5, Amount: 1, ID: 13}, }, s) 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 interface{}, liquidity, value float64, nodeCount int) { t.Helper() b, isBid := depth.(bids) a, isAsk := depth.(asks) var ll linkedList switch { case isBid: ll = b.linkedList case isAsk: ll = a.linkedList default: t.Fatal("value passed in is not of type bids or asks") } liquidityTotal, valueTotal := ll.amount() if liquidityTotal != liquidity { ll.display() t.Fatalf("mismatched liquidity expecting %v but received %v", liquidity, liquidityTotal) } if valueTotal != value { ll.display() t.Fatalf("mismatched total value expecting %v but received %v", value, valueTotal) } if ll.length != nodeCount { ll.display() t.Fatalf("mismatched node count expecting %v but received %v", nodeCount, ll.length) } if ll.head == nil { return } var tail *Node var price float64 for tip := ll.head; ; tip = tip.Next { switch { case price == 0: price = tip.Value.Price case isBid && price < tip.Value.Price: ll.display() t.Fatal("Bid pricing out of order should be descending") case isAsk && price > tip.Value.Price: ll.display() t.Fatal("Ask pricing out of order should be ascending") default: price = tip.Value.Price } if tip.Next == nil { tail = tip break } } var liqReversed, valReversed float64 var nodeReversed int for tip := tail; tip != nil; tip = tip.Prev { liqReversed += tip.Value.Amount valReversed += tip.Value.Amount * tip.Value.Price nodeReversed++ } if liquidity-liqReversed != 0 { ll.display() fmt.Println(liquidity, liqReversed) t.Fatalf("mismatched liquidity when reversing direction expecting %v but received %v", 0, liquidity-liqReversed) } if nodeCount-nodeReversed != 0 { ll.display() t.Fatalf("mismatched node count when reversing direction expecting %v but received %v", 0, nodeCount-nodeReversed) } if value-valReversed != 0 { ll.display() fmt.Println(valReversed, value) t.Fatalf("mismatched total book value when reversing direction expecting %v but received %v", 0, value-valReversed) } } func TestAmount(t *testing.T) { a := asks{} s := newStack() askSnapshot := Items{ {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, s) 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 TestShiftBookmark(t *testing.T) { bookmarkedNode := &Node{ Value: Item{ ID: 1337, Amount: 1, Price: 2, }, Next: nil, Prev: nil, shelved: time.Time{}, } originalBookmarkPrev := &Node{ Value: Item{ ID: 1336, }, Next: bookmarkedNode, Prev: nil, // At head shelved: time.Time{}, } originalBookmarkNext := &Node{ Value: Item{ ID: 1338, }, Next: nil, // This can be left nil in actuality this will be // populated Prev: bookmarkedNode, shelved: time.Time{}, } // associate previous and next nodes to bookmarked node bookmarkedNode.Prev = originalBookmarkPrev bookmarkedNode.Next = originalBookmarkNext tip := &Node{ Value: Item{ ID: 69420, }, Next: nil, // In this case tip will be at tail Prev: nil, shelved: time.Time{}, } tipprev := &Node{ Value: Item{ ID: 69419, }, Next: tip, Prev: nil, // This can be left nil in actuality this will be // populated shelved: time.Time{}, } // associate tips prev field with the correct prev node tip.Prev = tipprev if !shiftBookmark(tip, &bookmarkedNode, nil, Item{Amount: 1336, ID: 1337, Price: 9999}) { t.Fatal("There should be liquidity so we don't need to set tip to bookmark") } if bookmarkedNode.Value.Price != 9999 || bookmarkedNode.Value.Amount != 1336 || bookmarkedNode.Value.ID != 1337 { t.Fatal("bookmarked details are not set correctly with shift") } if bookmarkedNode.Prev != tip { t.Fatal("bookmarked prev memory address does not point to tip") } if bookmarkedNode.Next != nil { t.Fatal("bookmarked next is at tail and should be nil") } if bookmarkedNode.Next != nil { t.Fatal("bookmarked next is at tail and should be nil") } if originalBookmarkPrev.Next != originalBookmarkNext { t.Fatal("original bookmarked prev node should be associated with original bookmarked next node") } if originalBookmarkNext.Prev != originalBookmarkPrev { t.Fatal("original bookmarked next node should be associated with original bookmarked prev node") } var nilBookmark *Node if shiftBookmark(tip, &nilBookmark, nil, Item{Amount: 1336, ID: 1337, Price: 9999}) { t.Fatal("there should not be a bookmarked node") } if tip != nilBookmark { t.Fatal("nilBookmark not reassigned") } head := bookmarkedNode bookmarkedNode.Prev = nil bookmarkedNode.Next = originalBookmarkNext tip.Next = nil if !shiftBookmark(tip, &bookmarkedNode, &head, Item{Amount: 1336, ID: 1337, Price: 9999}) { t.Fatal("There should be liquidity so we don't need to set tip to bookmark") } if head != originalBookmarkNext { t.Fatal("unexpected pointer variable") } } func TestGetMovementByBaseAmount(t *testing.T) { t.Parallel() cases := []struct { Name string BaseAmount float64 ReferencePrice float64 BidLiquidity Items 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: Items{{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 tranche", BidLiquidity: Items{{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 tranche", BidLiquidity: Items{{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: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}}, BaseAmount: 12, ReferencePrice: 10000, ExpectedNominal: 1.0833333333333395, ExpectedImpact: FullLiquidityExhaustedPercentage, ExpectedCost: 1300, }, } for x := range cases { tt := cases[x] t.Run(tt.Name, func(t *testing.T) { t.Parallel() depth := NewDepth(id) depth.LoadSnapshot(tt.BidLiquidity, nil, 0, time.Time{}, true) movement, err := depth.bids.getMovementByBase(tt.BaseAmount, tt.ReferencePrice, false) if !errors.Is(err, tt.ExpectedError) { t.Fatalf("received: '%v' but expected: '%v'", 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 TestGetBaseAmountFromNominalSlippage(t *testing.T) { t.Parallel() cases := []struct { Name string NominalSlippage float64 ReferencePrice float64 BidLiquidity Items 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: Items{{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 tranche - take one amount out of second", BidLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}}, NominalSlippage: 0.33333333333334, ReferencePrice: 10000, ExpectedShift: &Movement{ Sold: 3.0000000000000275, // <- expected rounding issue Purchased: 29900.00000000027, AverageOrderCost: 9966.666666666664, NominalPercentage: 0.33333333333334, StartPrice: 10000, EndPrice: 9900, }, }, { Name: "consume full liquidity", BidLiquidity: Items{{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: Items{{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 x := range cases { tt := cases[x] t.Run(tt.Name, func(t *testing.T) { t.Parallel() depth := NewDepth(id) depth.LoadSnapshot(tt.BidLiquidity, nil, 0, time.Time{}, true) base, err := depth.bids.hitBidsByNominalSlippage(tt.NominalSlippage, tt.ReferencePrice) if !errors.Is(err, tt.ExpectedError) { t.Fatalf("%s received: '%v' but expected: '%v'", tt.Name, err, tt.ExpectedError) } if !base.IsEqual(tt.ExpectedShift) { t.Fatalf("%s quote received: '%+v' but expected: '%+v'", tt.Name, base, tt.ExpectedShift) } }) } } // 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 Items 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: Items{{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 tranche and second tranche", BidLiquidity: Items{{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: Items{{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 x := range cases { tt := cases[x] t.Run(tt.Name, func(t *testing.T) { t.Parallel() depth := NewDepth(id) depth.LoadSnapshot(tt.BidLiquidity, nil, 0, time.Time{}, true) base, err := depth.bids.hitBidsByImpactSlippage(tt.ImpactSlippage, tt.ReferencePrice) if !errors.Is(err, tt.ExpectedError) { t.Fatalf("%s received: '%v' but expected: '%v'", tt.Name, 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 Items 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: Items{{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 tranche", AskLiquidity: Items{{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 tranche", AskLiquidity: Items{{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: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}}, QuoteAmount: 121300, ReferencePrice: 10000, ExpectedNominal: 1.0833333333333395, ExpectedImpact: FullLiquidityExhaustedPercentage, ExpectedCost: 1300, }, } for x := range cases { tt := cases[x] t.Run(tt.Name, func(t *testing.T) { t.Parallel() depth := NewDepth(id) depth.LoadSnapshot(nil, tt.AskLiquidity, 0, time.Time{}, true) movement, err := depth.asks.getMovementByQuotation(tt.QuoteAmount, tt.ReferencePrice, false) if !errors.Is(err, tt.ExpectedError) { t.Fatalf("received: '%v' but expected: '%v'", 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 Items 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 tranche - one amount on second tranche", AskLiquidity: Items{{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 tranche total agg meeting 1 percent nominally", AskLiquidity: Items{{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 tranche", AskLiquidity: Items{{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: Items{{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: Items{{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 x := range cases { tt := cases[x] t.Run(tt.Name, func(t *testing.T) { t.Parallel() depth := NewDepth(id) depth.LoadSnapshot(nil, tt.AskLiquidity, 0, time.Time{}, true) quote, err := depth.asks.liftAsksByNominalSlippage(tt.NominalSlippage, tt.ReferencePrice) if !errors.Is(err, tt.ExpectedError) { t.Fatalf("%s received: '%v' but expected: '%v'", tt.Name, err, tt.ExpectedError) } if !quote.IsEqual(tt.ExpectedShift) { t.Fatalf("%s quote received: '%+v' but expected: '%+v'", tt.Name, quote, tt.ExpectedShift) } }) } } func TestGetQuoteAmountFromImpact(t *testing.T) { t.Parallel() cases := []struct { Name string ImpactSlippage float64 ReferencePrice float64 AskLiquidity Items 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: Items{{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 tranche and second tranche", AskLiquidity: Items{{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: Items{{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 x := range cases { tt := cases[x] t.Run(tt.Name, func(t *testing.T) { t.Parallel() depth := NewDepth(id) depth.LoadSnapshot(nil, tt.AskLiquidity, 0, time.Time{}, true) quote, err := depth.asks.liftAsksByImpactSlippage(tt.ImpactSlippage, tt.ReferencePrice) if !errors.Is(err, tt.ExpectedError) { t.Fatalf("received: '%v' but expected: '%v'", err, tt.ExpectedError) } if !quote.IsEqual(tt.ExpectedShift) { t.Fatalf("%s quote received: '%+v' but expected: '%+v'", tt.Name, quote, tt.ExpectedShift) } }) } } func TestGetHeadPrice(t *testing.T) { t.Parallel() depth := NewDepth(id) if _, err := depth.bids.getHeadPriceNoLock(); !errors.Is(err, errNoLiquidity) { t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity) } if _, err := depth.asks.getHeadPriceNoLock(); !errors.Is(err, errNoLiquidity) { t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity) } depth.LoadSnapshot(bid, ask, 0, time.Time{}, true) val, err := depth.bids.getHeadPriceNoLock() if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } if val != 1336 { t.Fatal("unexpected value") } val, err = depth.asks.getHeadPriceNoLock() if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } if val != 1337 { t.Fatal("unexpected value", val) } } func TestFinalizeFields(t *testing.T) { m := &Movement{} _, err := m.finalizeFields(0, 0, 0, 0, false) if !errors.Is(err, errInvalidCost) { t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidCost) } _, err = m.finalizeFields(1, 0, 0, 0, false) if !errors.Is(err, errInvalidAmount) { t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidAmount) } _, err = m.finalizeFields(1, 1, 0, 0, false) if !errors.Is(err, errInvalidHeadPrice) { t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidHeadPrice) } // Test slippage as per https://en.wikipedia.org/wiki/Slippage_(finance) mov, err := m.finalizeFields(20000*151.11585, 20000, 151.08, 0, false) if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } if mov.SlippageCost != 716.9999999995343 { t.Fatalf("received: '%v' but expected: '%v'", mov.SlippageCost, 716.9999999995343) } }