orderbook: export NewDepth(), add methods to orderbook.Unsafe type (#907)

* orderbook: export NewDepth function, return unsafe pointer

* orderbook: Add unsafe methods and liquidity checks

* Update exchanges/orderbook/unsafe.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* orderbook: addr nits

* orderbook: update comments

* Update exchanges/orderbook/unsafe.go

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

* Update exchanges/orderbook/unsafe.go

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

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
This commit is contained in:
Ryan O'Hara-Reid
2022-03-25 11:38:56 +11:00
committed by GitHub
parent c84bc86a85
commit 489e2ebade
5 changed files with 330 additions and 17 deletions

View File

@@ -28,7 +28,7 @@ type Depth struct {
}
// NewDepth returns a new depth item
func newDepth(id uuid.UUID) *Depth {
func NewDepth(id uuid.UUID) *Depth {
return &Depth{
stack: newStack(),
id: id,

View File

@@ -14,7 +14,7 @@ import (
var id, _ = uuid.NewV4()
func TestGetLength(t *testing.T) {
d := newDepth(id)
d := NewDepth(id)
if d.GetAskLength() != 0 {
t.Errorf("expected len %v, but received %v", 0, d.GetAskLength())
}
@@ -25,7 +25,7 @@ func TestGetLength(t *testing.T) {
t.Errorf("expected len %v, but received %v", 1, d.GetAskLength())
}
d = newDepth(id)
d = NewDepth(id)
if d.GetBidLength() != 0 {
t.Errorf("expected len %v, but received %v", 0, d.GetBidLength())
}
@@ -38,7 +38,7 @@ func TestGetLength(t *testing.T) {
}
func TestRetrieve(t *testing.T) {
d := newDepth(id)
d := NewDepth(id)
d.asks.load([]Item{{Price: 1337}}, d.stack)
d.bids.load([]Item{{Price: 1337}}, d.stack)
d.options = options{
@@ -75,7 +75,7 @@ func TestRetrieve(t *testing.T) {
}
func TestTotalAmounts(t *testing.T) {
d := newDepth(id)
d := NewDepth(id)
liquidity, value := d.TotalBidAmounts()
if liquidity != 0 || value != 0 {
@@ -118,7 +118,7 @@ func TestTotalAmounts(t *testing.T) {
}
func TestLoadSnapshot(t *testing.T) {
d := newDepth(id)
d := NewDepth(id)
d.LoadSnapshot(Items{{Price: 1337, Amount: 1}}, Items{{Price: 1337, Amount: 10}}, 0, time.Time{}, false)
if d.Retrieve().Asks[0].Price != 1337 || d.Retrieve().Bids[0].Price != 1337 {
t.Fatal("not set")
@@ -126,7 +126,7 @@ func TestLoadSnapshot(t *testing.T) {
}
func TestFlush(t *testing.T) {
d := newDepth(id)
d := NewDepth(id)
d.LoadSnapshot(Items{{Price: 1337, Amount: 1}}, Items{{Price: 1337, Amount: 10}}, 0, time.Time{}, false)
d.Flush()
if len(d.Retrieve().Asks) != 0 || len(d.Retrieve().Bids) != 0 {
@@ -140,7 +140,7 @@ func TestFlush(t *testing.T) {
}
func TestUpdateBidAskByPrice(t *testing.T) {
d := newDepth(id)
d := NewDepth(id)
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
// empty
@@ -157,7 +157,7 @@ func TestUpdateBidAskByPrice(t *testing.T) {
}
func TestDeleteBidAskByID(t *testing.T) {
d := newDepth(id)
d := NewDepth(id)
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
err := d.DeleteBidAskByID(Items{{Price: 1337, Amount: 2, ID: 1}}, Items{{Price: 1337, Amount: 2, ID: 2}}, false, 0, time.Time{})
if err != nil {
@@ -184,7 +184,7 @@ func TestDeleteBidAskByID(t *testing.T) {
}
func TestUpdateBidAskByID(t *testing.T) {
d := newDepth(id)
d := NewDepth(id)
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
err := d.UpdateBidAskByID(Items{{Price: 1337, Amount: 2, ID: 1}}, Items{{Price: 1337, Amount: 2, ID: 2}}, 0, time.Time{})
if err != nil {
@@ -207,7 +207,7 @@ func TestUpdateBidAskByID(t *testing.T) {
}
func TestInsertBidAskByID(t *testing.T) {
d := newDepth(id)
d := NewDepth(id)
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
err := d.InsertBidAskByID(Items{{Price: 1338, Amount: 2, ID: 3}}, Items{{Price: 1336, Amount: 2, ID: 4}}, 0, time.Time{})
if err != nil {
@@ -219,7 +219,7 @@ func TestInsertBidAskByID(t *testing.T) {
}
func TestUpdateInsertByID(t *testing.T) {
d := newDepth(id)
d := NewDepth(id)
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
err := d.UpdateInsertByID(Items{{Price: 1338, Amount: 0, ID: 3}}, Items{{Price: 1336, Amount: 2, ID: 4}}, 0, time.Time{})

View File

@@ -75,7 +75,7 @@ func (s *Service) Update(b *Base) error {
book, ok := m3[b.Pair.Quote.Item]
if !ok {
book = newDepth(m1.ID)
book = NewDepth(m1.ID)
book.AssignOptions(b)
m3[b.Pair.Quote.Item] = book
}
@@ -122,7 +122,7 @@ func (s *Service) DeployDepth(exchange string, p currency.Pair, a asset.Item) (*
}
book, ok := m3[p.Quote.Item]
if !ok {
book = newDepth(m1.ID)
book = NewDepth(m1.ID)
book.exchange = exchange
book.pair = p
book.asset = a

View File

@@ -1,12 +1,16 @@
package orderbook
import (
"errors"
"fmt"
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/exchanges/alert"
)
var errNoLiquidity = errors.New("no liquidity")
// Unsafe is an exported linked list reference to the current bid/ask heads and
// a reference to the underlying depth mutex. This allows for the exposure of
// the internal list to an external strategy or subsystem. The bid and ask
@@ -52,8 +56,8 @@ func (src *Unsafe) UnlockWith(dst sync.Locker) {
}
// GetUnsafe returns an unsafe orderbook with pointers to the linked list heads.
func (d *Depth) GetUnsafe() Unsafe {
return Unsafe{
func (d *Depth) GetUnsafe() *Unsafe {
return &Unsafe{
BidHead: &d.bids.linkedList.head,
AskHead: &d.asks.linkedList.head,
m: &d.m,
@@ -62,3 +66,103 @@ func (d *Depth) GetUnsafe() Unsafe {
LastUpdated: &d.options.lastUpdated,
}
}
// CheckBidLiquidity determines if the liquidity is sufficient for usage
func (src *Unsafe) CheckBidLiquidity() error {
_, err := src.GetBidLiquidity()
return err
}
// CheckAskLiquidity determines if the liquidity is sufficient for usage
func (src *Unsafe) CheckAskLiquidity() error {
_, err := src.GetAskLiquidity()
return err
}
// GetBestBid returns the top bid price
func (src *Unsafe) GetBestBid() (float64, error) {
bid, err := src.GetBidLiquidity()
if err != nil {
return 0, fmt.Errorf("get orderbook best bid price %w", err)
}
return bid.Value.Price, nil
}
// GetBestAsk returns the top ask price
func (src *Unsafe) GetBestAsk() (float64, error) {
ask, err := src.GetAskLiquidity()
if err != nil {
return 0, fmt.Errorf("get orderbook best ask price %w", err)
}
return ask.Value.Price, nil
}
// GetBidLiquidity gets the head node for the bid liquidity
func (src *Unsafe) GetBidLiquidity() (*Node, error) {
n := *src.BidHead
if n == nil {
return nil, fmt.Errorf("bid %w", errNoLiquidity)
}
return n, nil
}
// GetAskLiquidity gets the head node for the ask liquidity
func (src *Unsafe) GetAskLiquidity() (*Node, error) {
n := *src.AskHead
if n == nil {
return nil, fmt.Errorf("ask %w", errNoLiquidity)
}
return n, nil
}
// GetLiquidity checks and returns nodes to the top bids and asks
func (src *Unsafe) GetLiquidity() (ask, bid *Node, err error) {
bid, err = src.GetBidLiquidity()
if err != nil {
return nil, nil, err
}
ask, err = src.GetAskLiquidity()
if err != nil {
return nil, nil, err
}
return ask, bid, nil
}
// GetMidPrice returns the average between the top bid and top ask.
func (src *Unsafe) GetMidPrice() (float64, error) {
ask, bid, err := src.GetLiquidity()
if err != nil {
return 0, fmt.Errorf("get orderbook mid price %w", err)
}
return (bid.Value.Price + ask.Value.Price) / 2, nil
}
// GetSpread returns the spread between the top bid and top asks.
func (src *Unsafe) GetSpread() (float64, error) {
ask, bid, err := src.GetLiquidity()
if err != nil {
return 0, fmt.Errorf("get orderbook price spread %w", err)
}
return ask.Value.Price - bid.Value.Price, nil
}
// GetImbalance returns difference between the top bid and top ask amounts
// divided by its sum.
func (src *Unsafe) GetImbalance() (float64, error) {
ask, bid, err := src.GetLiquidity()
if err != nil {
return 0, fmt.Errorf("get orderbook imbalance %w", err)
}
top := bid.Value.Amount - ask.Value.Amount
bottom := bid.Value.Amount + ask.Value.Amount
if bottom == 0 {
return 0, errNoLiquidity
}
return top / bottom, nil
}
// IsStreaming returns if the orderbook is updated by a streaming protocol and
// is most likely more up to date than that of a REST protocol update.
func (src *Unsafe) IsStreaming() bool {
return !*src.UpdatedViaREST
}

View File

@@ -1,7 +1,9 @@
package orderbook
import (
"errors"
"testing"
"time"
"github.com/gofrs/uuid"
)
@@ -14,7 +16,8 @@ func (e *externalBook) Lock() {}
func (e *externalBook) Unlock() {}
func TestUnsafe(t *testing.T) {
d := newDepth(unsafeID)
t.Parallel()
d := NewDepth(unsafeID)
ob := d.GetUnsafe()
if ob.AskHead == nil || ob.BidHead == nil || ob.m == nil {
t.Fatal("these items should not be nil")
@@ -26,3 +29,209 @@ func TestUnsafe(t *testing.T) {
ob.LockWith(ob2)
ob.UnlockWith(ob2)
}
func TestGetLiquidity(t *testing.T) {
t.Parallel()
d := NewDepth(unsafeID)
unsafe := d.GetUnsafe()
_, _, err := unsafe.GetLiquidity()
if !errors.Is(err, errNoLiquidity) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
}
d.LoadSnapshot([]Item{{Price: 2}}, nil, 0, time.Time{}, false)
_, _, err = unsafe.GetLiquidity()
if !errors.Is(err, errNoLiquidity) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
}
d.LoadSnapshot([]Item{{Price: 2}}, []Item{{Price: 2}}, 0, time.Time{}, false)
aN, bN, err := unsafe.GetLiquidity()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if aN == nil {
t.Fatal("unexpected value")
}
if bN == nil {
t.Fatal("unexpected value")
}
}
func TestCheckBidLiquidity(t *testing.T) {
t.Parallel()
d := NewDepth(unsafeID)
unsafe := d.GetUnsafe()
err := unsafe.CheckBidLiquidity()
if !errors.Is(err, errNoLiquidity) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
}
d.LoadSnapshot([]Item{{Price: 2}}, nil, 0, time.Time{}, false)
err = unsafe.CheckBidLiquidity()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestCheckAskLiquidity(t *testing.T) {
t.Parallel()
d := NewDepth(unsafeID)
unsafe := d.GetUnsafe()
err := unsafe.CheckAskLiquidity()
if !errors.Is(err, errNoLiquidity) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
}
d.LoadSnapshot(nil, []Item{{Price: 2}}, 0, time.Time{}, false)
err = unsafe.CheckAskLiquidity()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestGetBestBid(t *testing.T) {
t.Parallel()
d := NewDepth(unsafeID)
unsafe := d.GetUnsafe()
if _, err := unsafe.GetBestBid(); !errors.Is(err, errNoLiquidity) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
}
d.LoadSnapshot([]Item{{Price: 2}}, nil, 0, time.Time{}, false)
bestBid, err := unsafe.GetBestBid()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if bestBid != 2 {
t.Fatal("unexpected value")
}
}
func TestGetBestAsk(t *testing.T) {
t.Parallel()
d := NewDepth(unsafeID)
unsafe := d.GetUnsafe()
if _, err := unsafe.GetBestAsk(); !errors.Is(err, errNoLiquidity) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
}
d.LoadSnapshot(nil, []Item{{Price: 2}}, 0, time.Time{}, false)
bestAsk, err := unsafe.GetBestAsk()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if bestAsk != 2 {
t.Fatal("unexpected value")
}
}
func TestGetMidPrice(t *testing.T) {
t.Parallel()
d := NewDepth(unsafeID)
unsafe := d.GetUnsafe()
if _, err := unsafe.GetMidPrice(); !errors.Is(err, errNoLiquidity) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
}
d.LoadSnapshot([]Item{{Price: 1}}, []Item{{Price: 2}}, 0, time.Time{}, false)
mid, err := unsafe.GetMidPrice()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if mid != 1.5 {
t.Fatal("unexpected value")
}
}
func TestGetSpread(t *testing.T) {
t.Parallel()
d := NewDepth(unsafeID)
unsafe := d.GetUnsafe()
if _, err := unsafe.GetSpread(); !errors.Is(err, errNoLiquidity) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
}
d.LoadSnapshot([]Item{{Price: 1}}, []Item{{Price: 2}}, 0, time.Time{}, false)
spread, err := unsafe.GetSpread()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if spread != 1 {
t.Fatal("unexpected value")
}
}
func TestGetImbalance(t *testing.T) {
t.Parallel()
d := NewDepth(unsafeID)
unsafe := d.GetUnsafe()
_, err := unsafe.GetImbalance()
if !errors.Is(err, errNoLiquidity) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
}
// unlikely event zero amounts
d.LoadSnapshot([]Item{{Price: 1, Amount: 0}}, []Item{{Price: 2, Amount: 0}}, 0, time.Time{}, false)
_, err = unsafe.GetImbalance()
if !errors.Is(err, errNoLiquidity) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
}
// balance skewed to asks
d.LoadSnapshot([]Item{{Price: 1, Amount: 1}}, []Item{{Price: 2, Amount: 1000}}, 0, time.Time{}, false)
imbalance, err := unsafe.GetImbalance()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if imbalance != -0.998001998001998 {
t.Fatal("unexpected value")
}
// balance skewed to bids
d.LoadSnapshot([]Item{{Price: 1, Amount: 1000}}, []Item{{Price: 2, Amount: 1}}, 0, time.Time{}, false)
imbalance, err = unsafe.GetImbalance()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if imbalance != 0.998001998001998 {
t.Fatal("unexpected value")
}
// in balance
d.LoadSnapshot([]Item{{Price: 1, Amount: 1}}, []Item{{Price: 2, Amount: 1}}, 0, time.Time{}, false)
imbalance, err = unsafe.GetImbalance()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if imbalance != 0 {
t.Fatal("unexpected value")
}
}
func TestIsStreaming(t *testing.T) {
d := NewDepth(unsafeID)
unsafe := d.GetUnsafe()
if !unsafe.IsStreaming() {
t.Fatalf("received: '%v' but expected: '%v'", unsafe.IsStreaming(), true)
}
d.LoadSnapshot([]Item{{Price: 1, Amount: 1}}, []Item{{Price: 2, Amount: 1}}, 0, time.Time{}, true)
if unsafe.IsStreaming() {
t.Fatalf("received: '%v' but expected: '%v'", unsafe.IsStreaming(), false)
}
d.LoadSnapshot([]Item{{Price: 1, Amount: 1}}, []Item{{Price: 2, Amount: 1}}, 0, time.Time{}, false)
if !unsafe.IsStreaming() {
t.Fatalf("received: '%v' but expected: '%v'", unsafe.IsStreaming(), true)
}
}