Files
gocryptotrader/exchanges/orderbook/calculator_test.go
Ryan O'Hara-Reid 2958e64afe orderbook: change Base struct name to Book (#1914)
* orderbook: change Base struct name to Snapshot

* linter: fix

* Update exchanges/exchange.go

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

* Update exchanges/orderbook/depth.go

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

* Update exchanges/orderbook/orderbook_types.go

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

* Snapshot -> Book

* Tranche(s) -> Level(s)

* Tranche(s) -> Level(s)

* rm tranche ref

* linter: fix

* linter: rides again

* update tests

* Update exchange/websocket/buffer/buffer.go

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

* Update backtester/eventhandlers/exchange/slippage/slippage.go

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

* Update exchange/websocket/buffer/buffer.go

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

* Update exchange/websocket/buffer/buffer.go

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

* Update exchanges/orderbook/orderbook_test.go

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

* Update exchanges/orderbook/orderbook_test.go

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

* Update exchanges/orderbook/orderbook_test.go

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

* Update exchanges/orderbook/orderbook_types.go

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

* Update exchanges/orderbook/orderbook_types.go

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

* fixup tests

* Update exchanges/orderbook/orderbook_test.go

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

* Update exchanges/orderbook/orderbook_test.go

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

* Update exchanges/orderbook/orderbook_test.go

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

* Update exchanges/orderbook/orderbook_test.go

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

* Update exchanges/orderbook/orderbook_test.go

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

* Update exchanges/orderbook/orderbook_test.go

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

* Update exchanges/orderbook/orderbook_test.go

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

* Update exchanges/orderbook/orderbook_test.go

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

* Update exchanges/orderbook/orderbook_types.go

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

* Update exchanges/orderbook/orderbook_types.go

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

* gk: nits and rm stuff that is not needed

* Update exchanges/orderbook/orderbook_test.go

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

* gk: nits

---------

Co-authored-by: shazbert <ryan.oharareid@thrasher.io>
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
2025-06-16 17:09:25 +10:00

493 lines
14 KiB
Go

package orderbook
import (
"math"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/currency"
)
func testSetup() Book {
return Book{
Exchange: "a",
Pair: currency.NewBTCUSD(),
Asks: []Level{{Price: 7000, Amount: 1}, {Price: 7001, Amount: 2}},
Bids: []Level{{Price: 6999, Amount: 1}, {Price: 6998, Amount: 2}},
}
}
func TestWhaleBomb(t *testing.T) {
t.Parallel()
b := testSetup()
_, err := b.WhaleBomb(-1, true)
require.ErrorIs(t, err, errPriceTargetInvalid)
result, err := b.WhaleBomb(7001, true) // <- This price should not be wiped out on the book.
require.NoError(t, err)
if result.Amount != 7000 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 7000)
}
if result.MaximumPrice != 7001 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7001)
}
if result.MinimumPrice != 7000 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
}
if result.PercentageGainOrLoss != 0.014285714285714287 {
t.Fatalf("received: '%v' but expected: '%v'", result.PercentageGainOrLoss, 0.014285714285714287)
}
result, err = b.WhaleBomb(7000.5, true) // <- Slot between prices will lift to next ask level
assert.NoError(t, err)
if result.Amount != 7000 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 7000)
}
if result.MaximumPrice != 7001 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7001)
}
if result.MinimumPrice != 7000 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
}
if result.PercentageGainOrLoss != 0.014285714285714287 {
t.Fatalf("received: '%v' but expected: '%v'", result.PercentageGainOrLoss, 0.014285714285714287)
}
result, err = b.WhaleBomb(7002, true) // <- exceed available quotations
require.NoError(t, err)
if !strings.Contains(result.Status, fullLiquidityUsageWarning) {
t.Fatal("expected status to contain liquidity warning")
}
result, err = b.WhaleBomb(7000, true) // <- Book should not move
require.NoError(t, err)
if result.Amount != 0 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 0)
}
if result.MaximumPrice != 7000 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7000)
}
if result.MinimumPrice != 7000 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
}
if result.PercentageGainOrLoss != 0 {
t.Fatalf("received: '%v' but expected: '%v'", result.PercentageGainOrLoss, 0)
}
_, err = b.WhaleBomb(6000, true)
require.ErrorIs(t, err, errCannotShiftPrice)
_, err = b.WhaleBomb(-1, false)
require.ErrorIs(t, err, errPriceTargetInvalid)
result, err = b.WhaleBomb(6998, false) // <- This price should not be wiped out on the book.
require.NoError(t, err)
if result.Amount != 1 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 1)
}
if result.MaximumPrice != 6999 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
}
if result.MinimumPrice != 6998 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6998)
}
if result.PercentageGainOrLoss != -0.014287755393627661 {
t.Fatalf("received: '%v' but expected: '%v'", result.PercentageGainOrLoss, -0.014287755393627661)
}
result, err = b.WhaleBomb(6998.5, false) // <- Slot between prices will drop to next bid level
assert.NoError(t, err)
if result.Amount != 1 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 1)
}
if result.MaximumPrice != 6999 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
}
if result.MinimumPrice != 6998 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6998)
}
if result.PercentageGainOrLoss != -0.014287755393627661 {
t.Fatalf("received: '%v' but expected: '%v'", result.PercentageGainOrLoss, -0.014287755393627661)
}
result, err = b.WhaleBomb(6997, false) // <- exceed available quotations
require.NoError(t, err)
if !strings.Contains(result.Status, fullLiquidityUsageWarning) {
t.Fatal("expected status to contain liquidity warning")
}
result, err = b.WhaleBomb(6999, false) // <- Book should not move
require.NoError(t, err)
if result.Amount != 0 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 0)
}
if result.MaximumPrice != 6999 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
}
if result.MinimumPrice != 6999 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6999)
}
if result.PercentageGainOrLoss != 0 {
t.Fatalf("received: '%v' but expected: '%v'", result.PercentageGainOrLoss, 0)
}
_, err = b.WhaleBomb(7500, false)
require.ErrorIs(t, err, errCannotShiftPrice)
}
func TestSimulateOrder(t *testing.T) {
t.Parallel()
b := testSetup()
// Invalid
_, err := b.SimulateOrder(-8000, true)
require.ErrorIs(t, err, errQuoteAmountInvalid)
_, err = (&Book{}).SimulateOrder(1337, true)
require.ErrorIs(t, err, errNoLiquidity)
// Full liquidity used
result, err := b.SimulateOrder(21002, true)
require.NoError(t, err)
if result.Amount != 3 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 3)
}
if result.MinimumPrice != 7000 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
}
if result.MaximumPrice != 7001 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7001)
}
if !strings.Contains(result.Status, fullLiquidityUsageWarning) {
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, fullLiquidityUsageWarning)
}
if len(result.Orders) != 2 {
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 2)
}
// Exceed full liquidity used
result, err = b.SimulateOrder(21003, true)
require.NoError(t, err)
if result.Amount != 3 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 3)
}
if result.MinimumPrice != 7000 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
}
if result.MaximumPrice != 7001 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7001)
}
if !strings.Contains(result.Status, fullLiquidityUsageWarning) {
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, fullLiquidityUsageWarning)
}
if len(result.Orders) != 2 {
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 2)
}
// First level
result, err = b.SimulateOrder(7000, true)
require.NoError(t, err)
if result.Amount != 1 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 1)
}
if result.MinimumPrice != 7000 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
}
if result.MaximumPrice != 7001 { // A full level is wiped out and this one should be preserved.
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7001)
}
if strings.Contains(result.Status, fullLiquidityUsageWarning) {
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, "NO WARNING")
}
if len(result.Orders) != 1 {
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 1)
}
// Half of first tranch
result, err = b.SimulateOrder(3500, true)
require.NoError(t, err)
if result.Amount != .5 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, .5)
}
if result.MinimumPrice != 7000 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
}
if result.MaximumPrice != 7000 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7000)
}
if strings.Contains(result.Status, fullLiquidityUsageWarning) {
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, "NO WARNING")
}
if len(result.Orders) != 1 {
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 1)
}
if result.Orders[0].Amount != 0.5 {
t.Fatalf("received: '%v' but expected: '%v'", result.Orders[0].Amount, 0.5)
}
// Half of second level
result, err = b.SimulateOrder(14001, true)
require.NoError(t, err)
if result.Amount != 2 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 2)
}
if result.MinimumPrice != 7000 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
}
if result.MaximumPrice != 7001 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7001)
}
if strings.Contains(result.Status, fullLiquidityUsageWarning) {
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, "NO WARNING")
}
if len(result.Orders) != 2 {
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 1)
}
if result.Orders[1].Amount != 1 {
t.Fatalf("received: '%v' but expected: '%v'", result.Orders[1].Amount, 1)
}
// Hitting bids
// Invalid
_, err = (&Book{}).SimulateOrder(-1, false)
require.ErrorIs(t, err, errBaseAmountInvalid)
_, err = (&Book{}).SimulateOrder(2, false)
require.ErrorIs(t, err, errNoLiquidity)
// Full liquidity used
result, err = b.SimulateOrder(3, false)
require.NoError(t, err)
if result.Amount != 20995 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 20995)
}
if result.MaximumPrice != 6999 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
}
if result.MinimumPrice != 6998 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6998)
}
if !strings.Contains(result.Status, fullLiquidityUsageWarning) {
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, fullLiquidityUsageWarning)
}
if len(result.Orders) != 2 {
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 2)
}
// Exceed full liquidity used
result, err = b.SimulateOrder(3.1, false)
require.NoError(t, err)
if result.Amount != 20995 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 20995)
}
if result.MaximumPrice != 6999 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
}
if result.MinimumPrice != 6998 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6998)
}
if !strings.Contains(result.Status, fullLiquidityUsageWarning) {
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, fullLiquidityUsageWarning)
}
if len(result.Orders) != 2 {
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 2)
}
// First level
result, err = b.SimulateOrder(1, false)
require.NoError(t, err)
if result.Amount != 6999 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 6999)
}
if result.MaximumPrice != 6999 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
}
if result.MinimumPrice != 6998 { // A full level is wiped out and this one should be preserved.
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6998)
}
if strings.Contains(result.Status, fullLiquidityUsageWarning) {
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, "NO WARNING")
}
if len(result.Orders) != 1 {
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 1)
}
// Half of first tranch
result, err = b.SimulateOrder(.5, false)
require.NoError(t, err)
if result.Amount != 3499.5 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 3499.5)
}
if result.MinimumPrice != 6999 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6999)
}
if result.MaximumPrice != 6999 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
}
if strings.Contains(result.Status, fullLiquidityUsageWarning) {
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, "NO WARNING")
}
if len(result.Orders) != 1 {
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 1)
}
if result.Orders[0].Amount != 0.5 {
t.Fatalf("received: '%v' but expected: '%v'", result.Orders[0].Amount, 0.5)
}
// Half of second level
result, err = b.SimulateOrder(2, false)
require.NoError(t, err)
if result.Amount != 13997 {
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 13997)
}
if result.MaximumPrice != 6999 {
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
}
if result.MinimumPrice != 6998 {
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6998)
}
if strings.Contains(result.Status, fullLiquidityUsageWarning) {
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, "NO WARNING")
}
if len(result.Orders) != 2 {
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 2)
}
if result.Orders[1].Amount != 1 {
t.Fatalf("received: '%v' but expected: '%v'", result.Orders[1].Amount, 1)
}
}
func TestGetAveragePrice(t *testing.T) {
b := Book{
Exchange: "Binance",
Pair: currency.NewBTCUSD(),
}
_, err := b.GetAveragePrice(false, 5)
assert.ErrorIs(t, err, errNotEnoughLiquidity)
b = Book{
Asks: []Level{
{Amount: 5, Price: 1},
{Amount: 5, Price: 2},
{Amount: 5, Price: 3},
{Amount: 5, Price: 4},
},
}
_, err = b.GetAveragePrice(true, -2)
assert.ErrorIs(t, err, errAmountInvalid)
avgPrice, err := b.GetAveragePrice(true, 15)
require.NoError(t, err)
assert.Equal(t, 2.0, avgPrice)
avgPrice, err = b.GetAveragePrice(true, 18)
require.NoError(t, err)
assert.Equal(t, 2.333, math.Round(avgPrice*1000)/1000)
_, err = b.GetAveragePrice(true, 25)
assert.ErrorIs(t, err, errNotEnoughLiquidity)
}
func TestFindNominalAmount(t *testing.T) {
b := Levels{
{Amount: 5, Price: 1},
{Amount: 5, Price: 2},
{Amount: 5, Price: 3},
{Amount: 5, Price: 4},
}
nomAmt, remainingAmt := b.FindNominalAmount(15)
if nomAmt != 30 && remainingAmt != 0 {
t.Errorf("invalid return")
}
b = Levels{}
nomAmt, remainingAmt = b.FindNominalAmount(15)
if nomAmt != 0 && remainingAmt != 30 {
t.Errorf("invalid return")
}
}