orderbook: Refactor package structure for simplicity and efficiency (#1465)

* initial purge and benchmarks proof before rn overhaul

* rn LinkedList -> Tranche(s) and purge references

* roll out acrost exchanges

* linterino

* rn silly billy label

* linter strikes AAAAAGAIN!

* fix some things

* rm comment

* Add actual comparison from master to branch benchmark for sorting algorithms

* lower case via git mv YAAY!

* drop code

* convert type name

* glorious: nits

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
This commit is contained in:
Ryan O'Hara-Reid
2024-05-14 15:51:34 +10:00
committed by GitHub
parent 2a92878afc
commit 4cd4fb06b4
76 changed files with 1575 additions and 2824 deletions

View File

@@ -21,7 +21,7 @@ type WhaleBombResult struct {
MinimumPrice float64
MaximumPrice float64
PercentageGainOrLoss float64
Orders Items
Orders Tranches
Status string
}
@@ -175,7 +175,7 @@ type DeploymentAction struct {
TranchePositionPrice float64
BaseAmount float64
QuoteAmount float64
Tranches Items
Tranches Tranches
FullLiquidityUsed bool
}
@@ -201,7 +201,7 @@ func (b *Base) buy(quote float64) (*DeploymentAction, error) {
}
}
subAmount := quote / b.Asks[x].Price
action.Tranches = append(action.Tranches, Item{
action.Tranches = append(action.Tranches, Tranche{
Price: b.Asks[x].Price,
Amount: subAmount,
})
@@ -238,7 +238,7 @@ func (b *Base) sell(base float64) (*DeploymentAction, error) {
action.FullLiquidityUsed = true
}
}
action.Tranches = append(action.Tranches, Item{
action.Tranches = append(action.Tranches, Tranche{
Price: b.Bids[x].Price,
Amount: base,
})
@@ -279,16 +279,16 @@ func (b *Base) GetAveragePrice(buy bool, amount float64) (float64, error) {
// FindNominalAmount finds the nominal amount spent in terms of the quote
// If the orderbook doesn't have enough liquidity it returns a non zero
// remaining amount value
func (elem Items) FindNominalAmount(amount float64) (aggNominalAmount, remainingAmount float64) {
func (ts Tranches) FindNominalAmount(amount float64) (aggNominalAmount, remainingAmount float64) {
remainingAmount = amount
for x := range elem {
if remainingAmount <= elem[x].Amount {
aggNominalAmount += elem[x].Price * remainingAmount
for x := range ts {
if remainingAmount <= ts[x].Amount {
aggNominalAmount += ts[x].Price * remainingAmount
remainingAmount = 0
break
}
aggNominalAmount += elem[x].Price * elem[x].Amount
remainingAmount -= elem[x].Amount
aggNominalAmount += ts[x].Price * ts[x].Amount
remainingAmount -= ts[x].Amount
}
return aggNominalAmount, remainingAmount
}

View File

@@ -13,11 +13,11 @@ func testSetup() Base {
return Base{
Exchange: "a",
Pair: currency.NewPair(currency.BTC, currency.USD),
Asks: []Item{
Asks: []Tranche{
{Price: 7000, Amount: 1},
{Price: 7001, Amount: 2},
},
Bids: []Item{
Bids: []Tranche{
{Price: 6999, Amount: 1},
{Price: 6998, Amount: 2},
},
@@ -507,14 +507,14 @@ func TestGetAveragePrice(t *testing.T) {
t.Error(err)
}
b.Pair = cp
b.Bids = []Item{}
b.Bids = []Tranche{}
_, err = b.GetAveragePrice(false, 5)
if errors.Is(errNotEnoughLiquidity, err) {
t.Error("expected: %w, received %w", errNotEnoughLiquidity, err)
}
b = Base{}
b.Pair = cp
b.Asks = []Item{
b.Asks = []Tranche{
{Amount: 5, Price: 1},
{Amount: 5, Price: 2},
{Amount: 5, Price: 3},
@@ -545,7 +545,7 @@ func TestGetAveragePrice(t *testing.T) {
}
func TestFindNominalAmount(t *testing.T) {
b := Items{
b := Tranches{
{Amount: 5, Price: 1},
{Amount: 5, Price: 2},
{Amount: 5, Price: 3},
@@ -555,7 +555,7 @@ func TestFindNominalAmount(t *testing.T) {
if nomAmt != 30 && remainingAmt != 0 {
t.Errorf("invalid return")
}
b = Items{}
b = Tranches{}
nomAmt, remainingAmt = b.FindNominalAmount(15)
if nomAmt != 0 && remainingAmt != 30 {
t.Errorf("invalid return")

View File

@@ -26,19 +26,15 @@ var (
)
// Outbound restricts outbound usage of depth. NOTE: Type assert to
// *orderbook.Depth or alternatively retrieve orderbook.Unsafe type to access
// underlying linked list.
// *orderbook.Depth.
type Outbound interface {
Retrieve() (*Base, error)
}
// Depth defines a linked list of orderbook items
// Depth defines a store of orderbook tranches
type Depth struct {
asks
bids
// unexported stack of nodes
stack *stack
askTranches
bidTranches
alert.Notice
@@ -53,13 +49,9 @@ type Depth struct {
m sync.Mutex
}
// NewDepth returns a new depth item
// NewDepth returns a new orderbook depth
func NewDepth(id uuid.UUID) *Depth {
return &Depth{
stack: newStack(),
_ID: id,
mux: service.Mux,
}
return &Depth{_ID: id, mux: service.Mux}
}
// Publish alerts any subscribed routines using a dispatch mux
@@ -78,8 +70,8 @@ func (d *Depth) Retrieve() (*Base, error) {
return nil, d.validationError
}
return &Base{
Bids: d.bids.retrieve(0),
Asks: d.asks.retrieve(0),
Bids: d.bidTranches.retrieve(0),
Asks: d.askTranches.retrieve(0),
Exchange: d.exchange,
Asset: d.asset,
Pair: d.pair,
@@ -94,7 +86,7 @@ func (d *Depth) Retrieve() (*Base, error) {
}
// LoadSnapshot flushes the bids and asks with a snapshot
func (d *Depth) LoadSnapshot(bids, asks []Item, lastUpdateID int64, lastUpdated time.Time, updateByREST bool) error {
func (d *Depth) LoadSnapshot(bids, asks []Tranche, lastUpdateID int64, lastUpdated time.Time, updateByREST bool) error {
d.m.Lock()
defer d.m.Unlock()
if lastUpdated.IsZero() {
@@ -107,8 +99,8 @@ func (d *Depth) LoadSnapshot(bids, asks []Item, lastUpdateID int64, lastUpdated
d.lastUpdateID = lastUpdateID
d.lastUpdated = lastUpdated
d.restSnapshot = updateByREST
d.bids.load(bids, d.stack, lastUpdated)
d.asks.load(asks, d.stack, lastUpdated)
d.bidTranches.load(bids)
d.askTranches.load(asks)
d.validationError = nil
d.Alert()
return nil
@@ -119,9 +111,8 @@ func (d *Depth) LoadSnapshot(bids, asks []Item, lastUpdateID int64, lastUpdated
func (d *Depth) invalidate(withReason error) error {
d.lastUpdateID = 0
d.lastUpdated = time.Time{}
tn := time.Now()
d.bids.load(nil, d.stack, tn)
d.asks.load(nil, d.stack, tn)
d.bidTranches.load(nil)
d.askTranches.load(nil)
d.validationError = fmt.Errorf("%s %s %s Reason: [%w]",
d.exchange,
d.pair,
@@ -160,10 +151,10 @@ func (d *Depth) UpdateBidAskByPrice(update *Update) error {
errLastUpdatedNotSet)
}
if len(update.Bids) != 0 {
d.bids.updateInsertByPrice(update.Bids, d.stack, d.options.maxDepth, update.UpdateTime)
d.bidTranches.updateInsertByPrice(update.Bids, d.options.maxDepth)
}
if len(update.Asks) != 0 {
d.asks.updateInsertByPrice(update.Asks, d.stack, d.options.maxDepth, update.UpdateTime)
d.askTranches.updateInsertByPrice(update.Asks, d.options.maxDepth)
}
d.updateAndAlert(update)
return nil
@@ -183,13 +174,13 @@ func (d *Depth) UpdateBidAskByID(update *Update) error {
}
if len(update.Bids) != 0 {
err := d.bids.updateByID(update.Bids)
err := d.bidTranches.updateByID(update.Bids)
if err != nil {
return d.invalidate(err)
}
}
if len(update.Asks) != 0 {
err := d.asks.updateByID(update.Asks)
err := d.askTranches.updateByID(update.Asks)
if err != nil {
return d.invalidate(err)
}
@@ -210,13 +201,13 @@ func (d *Depth) DeleteBidAskByID(update *Update, bypassErr bool) error {
errLastUpdatedNotSet)
}
if len(update.Bids) != 0 {
err := d.bids.deleteByID(update.Bids, d.stack, bypassErr, update.UpdateTime)
err := d.bidTranches.deleteByID(update.Bids, bypassErr)
if err != nil {
return d.invalidate(err)
}
}
if len(update.Asks) != 0 {
err := d.asks.deleteByID(update.Asks, d.stack, bypassErr, update.UpdateTime)
err := d.askTranches.deleteByID(update.Asks, bypassErr)
if err != nil {
return d.invalidate(err)
}
@@ -237,13 +228,13 @@ func (d *Depth) InsertBidAskByID(update *Update) error {
errLastUpdatedNotSet)
}
if len(update.Bids) != 0 {
err := d.bids.insertUpdates(update.Bids, d.stack)
err := d.bidTranches.insertUpdates(update.Bids)
if err != nil {
return d.invalidate(err)
}
}
if len(update.Asks) != 0 {
err := d.asks.insertUpdates(update.Asks, d.stack)
err := d.askTranches.insertUpdates(update.Asks)
if err != nil {
return d.invalidate(err)
}
@@ -264,13 +255,13 @@ func (d *Depth) UpdateInsertByID(update *Update) error {
errLastUpdatedNotSet)
}
if len(update.Bids) != 0 {
err := d.bids.updateInsertByID(update.Bids, d.stack)
err := d.bidTranches.updateInsertByID(update.Bids)
if err != nil {
return d.invalidate(err)
}
}
if len(update.Asks) != 0 {
err := d.asks.updateInsertByID(update.Asks, d.stack)
err := d.askTranches.updateInsertByID(update.Asks)
if err != nil {
return d.invalidate(err)
}
@@ -306,7 +297,7 @@ func (d *Depth) GetName() string {
return d.exchange
}
// IsRESTSnapshot returns if the depth item was updated via REST
// IsRESTSnapshot returns if the depth was updated via REST
func (d *Depth) IsRESTSnapshot() (bool, error) {
d.m.Lock()
defer d.m.Unlock()
@@ -340,7 +331,7 @@ func (d *Depth) GetAskLength() (int, error) {
if d.validationError != nil {
return 0, d.validationError
}
return d.asks.length, nil
return len(d.askTranches.Tranches), nil
}
// GetBidLength returns length of bids
@@ -350,7 +341,7 @@ func (d *Depth) GetBidLength() (int, error) {
if d.validationError != nil {
return 0, d.validationError
}
return d.bids.length, nil
return len(d.bidTranches.Tranches), nil
}
// TotalBidAmounts returns the total amount of bids and the total orderbook
@@ -361,7 +352,7 @@ func (d *Depth) TotalBidAmounts() (liquidity, value float64, err error) {
if d.validationError != nil {
return 0, 0, d.validationError
}
liquidity, value = d.bids.amount()
liquidity, value = d.bidTranches.amount()
return liquidity, value, nil
}
@@ -373,7 +364,7 @@ func (d *Depth) TotalAskAmounts() (liquidity, value float64, err error) {
if d.validationError != nil {
return 0, 0, d.validationError
}
liquidity, value = d.asks.amount()
liquidity, value = d.askTranches.amount()
return liquidity, value, nil
}
@@ -394,7 +385,7 @@ func (d *Depth) HitTheBidsByNominalSlippage(maxSlippage, refPrice float64) (*Mov
if d.validationError != nil {
return nil, d.validationError
}
return d.bids.hitBidsByNominalSlippage(maxSlippage, refPrice)
return d.bidTranches.hitBidsByNominalSlippage(maxSlippage, refPrice)
}
// HitTheBidsByNominalSlippageFromMid hits the bids by the required nominal
@@ -410,7 +401,7 @@ func (d *Depth) HitTheBidsByNominalSlippageFromMid(maxSlippage float64) (*Moveme
if err != nil {
return nil, err
}
return d.bids.hitBidsByNominalSlippage(maxSlippage, mid)
return d.bidTranches.hitBidsByNominalSlippage(maxSlippage, mid)
}
// HitTheBidsByNominalSlippageFromBest hits the bids by the required nominal
@@ -422,11 +413,11 @@ func (d *Depth) HitTheBidsByNominalSlippageFromBest(maxSlippage float64) (*Movem
if d.validationError != nil {
return nil, d.validationError
}
head, err := d.bids.getHeadPriceNoLock()
head, err := d.bidTranches.getHeadPriceNoLock()
if err != nil {
return nil, err
}
return d.bids.hitBidsByNominalSlippage(maxSlippage, head)
return d.bidTranches.hitBidsByNominalSlippage(maxSlippage, head)
}
// LiftTheAsksByNominalSlippage lifts the asks by the required nominal slippage
@@ -438,7 +429,7 @@ func (d *Depth) LiftTheAsksByNominalSlippage(maxSlippage, refPrice float64) (*Mo
if d.validationError != nil {
return nil, d.validationError
}
return d.asks.liftAsksByNominalSlippage(maxSlippage, refPrice)
return d.askTranches.liftAsksByNominalSlippage(maxSlippage, refPrice)
}
// LiftTheAsksByNominalSlippageFromMid lifts the asks by the required nominal
@@ -454,7 +445,7 @@ func (d *Depth) LiftTheAsksByNominalSlippageFromMid(maxSlippage float64) (*Movem
if err != nil {
return nil, err
}
return d.asks.liftAsksByNominalSlippage(maxSlippage, mid)
return d.askTranches.liftAsksByNominalSlippage(maxSlippage, mid)
}
// LiftTheAsksByNominalSlippageFromBest lifts the asks by the required nominal
@@ -466,11 +457,11 @@ func (d *Depth) LiftTheAsksByNominalSlippageFromBest(maxSlippage float64) (*Move
if d.validationError != nil {
return nil, d.validationError
}
head, err := d.asks.getHeadPriceNoLock()
head, err := d.askTranches.getHeadPriceNoLock()
if err != nil {
return nil, err
}
return d.asks.liftAsksByNominalSlippage(maxSlippage, head)
return d.askTranches.liftAsksByNominalSlippage(maxSlippage, head)
}
// HitTheBidsByImpactSlippage hits the bids by the required impact slippage
@@ -482,7 +473,7 @@ func (d *Depth) HitTheBidsByImpactSlippage(maxSlippage, refPrice float64) (*Move
if d.validationError != nil {
return nil, d.validationError
}
return d.bids.hitBidsByImpactSlippage(maxSlippage, refPrice)
return d.bidTranches.hitBidsByImpactSlippage(maxSlippage, refPrice)
}
// HitTheBidsByImpactSlippageFromMid hits the bids by the required impact
@@ -498,7 +489,7 @@ func (d *Depth) HitTheBidsByImpactSlippageFromMid(maxSlippage float64) (*Movemen
if err != nil {
return nil, err
}
return d.bids.hitBidsByImpactSlippage(maxSlippage, mid)
return d.bidTranches.hitBidsByImpactSlippage(maxSlippage, mid)
}
// HitTheBidsByImpactSlippageFromBest hits the bids by the required impact
@@ -510,11 +501,11 @@ func (d *Depth) HitTheBidsByImpactSlippageFromBest(maxSlippage float64) (*Moveme
if d.validationError != nil {
return nil, d.validationError
}
head, err := d.bids.getHeadPriceNoLock()
head, err := d.bidTranches.getHeadPriceNoLock()
if err != nil {
return nil, err
}
return d.bids.hitBidsByImpactSlippage(maxSlippage, head)
return d.bidTranches.hitBidsByImpactSlippage(maxSlippage, head)
}
// LiftTheAsksByImpactSlippage lifts the asks by the required impact slippage
@@ -526,7 +517,7 @@ func (d *Depth) LiftTheAsksByImpactSlippage(maxSlippage, refPrice float64) (*Mov
if d.validationError != nil {
return nil, d.validationError
}
return d.asks.liftAsksByImpactSlippage(maxSlippage, refPrice)
return d.askTranches.liftAsksByImpactSlippage(maxSlippage, refPrice)
}
// LiftTheAsksByImpactSlippageFromMid lifts the asks by the required impact
@@ -542,7 +533,7 @@ func (d *Depth) LiftTheAsksByImpactSlippageFromMid(maxSlippage float64) (*Moveme
if err != nil {
return nil, err
}
return d.asks.liftAsksByImpactSlippage(maxSlippage, mid)
return d.askTranches.liftAsksByImpactSlippage(maxSlippage, mid)
}
// LiftTheAsksByImpactSlippageFromBest lifts the asks by the required impact
@@ -554,11 +545,11 @@ func (d *Depth) LiftTheAsksByImpactSlippageFromBest(maxSlippage float64) (*Movem
if d.validationError != nil {
return nil, d.validationError
}
head, err := d.asks.getHeadPriceNoLock()
head, err := d.askTranches.getHeadPriceNoLock()
if err != nil {
return nil, err
}
return d.asks.liftAsksByImpactSlippage(maxSlippage, head)
return d.askTranches.liftAsksByImpactSlippage(maxSlippage, head)
}
// HitTheBids derives full orderbook slippage information from reference price
@@ -571,9 +562,9 @@ func (d *Depth) HitTheBids(amount, refPrice float64, purchase bool) (*Movement,
return nil, d.validationError
}
if purchase {
return d.bids.getMovementByQuotation(amount, refPrice, false)
return d.bidTranches.getMovementByQuotation(amount, refPrice, false)
}
return d.bids.getMovementByBase(amount, refPrice, false)
return d.bidTranches.getMovementByBase(amount, refPrice, false)
}
// HitTheBidsFromMid derives full orderbook slippage information from mid price
@@ -590,9 +581,9 @@ func (d *Depth) HitTheBidsFromMid(amount float64, purchase bool) (*Movement, err
return nil, err
}
if purchase {
return d.bids.getMovementByQuotation(amount, mid, false)
return d.bidTranches.getMovementByQuotation(amount, mid, false)
}
return d.bids.getMovementByBase(amount, mid, false)
return d.bidTranches.getMovementByBase(amount, mid, false)
}
// HitTheBidsFromBest derives full orderbook slippage information from best bid
@@ -604,14 +595,14 @@ func (d *Depth) HitTheBidsFromBest(amount float64, purchase bool) (*Movement, er
if d.validationError != nil {
return nil, d.validationError
}
head, err := d.bids.getHeadPriceNoLock()
head, err := d.bidTranches.getHeadPriceNoLock()
if err != nil {
return nil, err
}
if purchase {
return d.bids.getMovementByQuotation(amount, head, false)
return d.bidTranches.getMovementByQuotation(amount, head, false)
}
return d.bids.getMovementByBase(amount, head, false)
return d.bidTranches.getMovementByBase(amount, head, false)
}
// LiftTheAsks derives full orderbook slippage information from reference price
@@ -624,9 +615,9 @@ func (d *Depth) LiftTheAsks(amount, refPrice float64, purchase bool) (*Movement,
return nil, d.validationError
}
if purchase {
return d.asks.getMovementByBase(amount, refPrice, true)
return d.askTranches.getMovementByBase(amount, refPrice, true)
}
return d.asks.getMovementByQuotation(amount, refPrice, true)
return d.askTranches.getMovementByQuotation(amount, refPrice, true)
}
// LiftTheAsksFromMid derives full orderbook slippage information from mid price
@@ -643,9 +634,9 @@ func (d *Depth) LiftTheAsksFromMid(amount float64, purchase bool) (*Movement, er
return nil, err
}
if purchase {
return d.asks.getMovementByBase(amount, mid, true)
return d.askTranches.getMovementByBase(amount, mid, true)
}
return d.asks.getMovementByQuotation(amount, mid, true)
return d.askTranches.getMovementByQuotation(amount, mid, true)
}
// LiftTheAsksFromBest derives full orderbook slippage information from best ask
@@ -657,14 +648,14 @@ func (d *Depth) LiftTheAsksFromBest(amount float64, purchase bool) (*Movement, e
if d.validationError != nil {
return nil, d.validationError
}
head, err := d.asks.getHeadPriceNoLock()
head, err := d.askTranches.getHeadPriceNoLock()
if err != nil {
return nil, err
}
if purchase {
return d.asks.getMovementByBase(amount, head, true)
return d.askTranches.getMovementByBase(amount, head, true)
}
return d.asks.getMovementByQuotation(amount, head, true)
return d.askTranches.getMovementByQuotation(amount, head, true)
}
// GetMidPrice returns the mid price between the ask and bid spread
@@ -679,11 +670,11 @@ func (d *Depth) GetMidPrice() (float64, error) {
// getMidPriceNoLock is an unprotected helper that gets mid price
func (d *Depth) getMidPriceNoLock() (float64, error) {
bidHead, err := d.bids.getHeadPriceNoLock()
bidHead, err := d.bidTranches.getHeadPriceNoLock()
if err != nil {
return 0, err
}
askHead, err := d.asks.getHeadPriceNoLock()
askHead, err := d.askTranches.getHeadPriceNoLock()
if err != nil {
return 0, err
}
@@ -697,7 +688,7 @@ func (d *Depth) GetBestBid() (float64, error) {
if d.validationError != nil {
return 0, d.validationError
}
return d.bids.getHeadPriceNoLock()
return d.bidTranches.getHeadPriceNoLock()
}
// GetBestAsk returns the best ask price
@@ -707,7 +698,7 @@ func (d *Depth) GetBestAsk() (float64, error) {
if d.validationError != nil {
return 0, d.validationError
}
return d.asks.getHeadPriceNoLock()
return d.askTranches.getHeadPriceNoLock()
}
// GetSpreadAmount returns the spread as a quotation amount
@@ -717,11 +708,11 @@ func (d *Depth) GetSpreadAmount() (float64, error) {
if d.validationError != nil {
return 0, d.validationError
}
askHead, err := d.asks.getHeadPriceNoLock()
askHead, err := d.askTranches.getHeadPriceNoLock()
if err != nil {
return 0, err
}
bidHead, err := d.bids.getHeadPriceNoLock()
bidHead, err := d.bidTranches.getHeadPriceNoLock()
if err != nil {
return 0, err
}
@@ -735,11 +726,11 @@ func (d *Depth) GetSpreadPercentage() (float64, error) {
if d.validationError != nil {
return 0, d.validationError
}
askHead, err := d.asks.getHeadPriceNoLock()
askHead, err := d.askTranches.getHeadPriceNoLock()
if err != nil {
return 0, err
}
bidHead, err := d.bids.getHeadPriceNoLock()
bidHead, err := d.bidTranches.getHeadPriceNoLock()
if err != nil {
return 0, err
}
@@ -753,11 +744,11 @@ func (d *Depth) GetImbalance() (float64, error) {
if d.validationError != nil {
return 0, d.validationError
}
askVolume, err := d.asks.getHeadVolumeNoLock()
askVolume, err := d.askTranches.getHeadVolumeNoLock()
if err != nil {
return 0, err
}
bidVolume, err := d.bids.getHeadVolumeNoLock()
bidVolume, err := d.bidTranches.getHeadVolumeNoLock()
if err != nil {
return 0, err
}
@@ -768,7 +759,7 @@ func (d *Depth) GetImbalance() (float64, error) {
// count is 0, it will return the entire orderbook. Count == 1 will retrieve the
// best bid and ask. If the required count exceeds the orderbook depth, it will
// return the entire orderbook.
func (d *Depth) GetTranches(count int) (ask, bid []Item, err error) {
func (d *Depth) GetTranches(count int) (ask, bid []Tranche, err error) {
if count < 0 {
return nil, nil, errInvalidBookDepth
}
@@ -777,7 +768,7 @@ func (d *Depth) GetTranches(count int) (ask, bid []Item, err error) {
if d.validationError != nil {
return nil, nil, d.validationError
}
return d.asks.retrieve(count), d.bids.retrieve(count), nil
return d.askTranches.retrieve(count), d.bidTranches.retrieve(count), nil
}
// GetPair returns the pair associated with the depth

View File

@@ -28,14 +28,14 @@ func TestGetLength(t *testing.T) {
_, err = d.GetAskLength()
assert.ErrorIs(t, err, ErrOrderbookInvalid, "GetAskLength should error with invalid depth")
err = d.LoadSnapshot([]Item{{Price: 1337}}, nil, 0, time.Now(), true)
err = d.LoadSnapshot([]Tranche{{Price: 1337}}, nil, 0, time.Now(), true)
assert.NoError(t, err, "LoadSnapshot should not error")
askLen, err := d.GetAskLength()
assert.NoError(t, err, "GetAskLength should not error")
assert.Zero(t, askLen, "ask length should be zero")
d.asks.load([]Item{{Price: 1337}}, d.stack, time.Now())
d.askTranches.load([]Tranche{{Price: 1337}})
askLen, err = d.GetAskLength()
assert.NoError(t, err, "GetAskLength should not error")
@@ -48,14 +48,14 @@ func TestGetLength(t *testing.T) {
_, err = d.GetBidLength()
assert.ErrorIs(t, err, ErrOrderbookInvalid, "GetBidLength should error with invalid depth")
err = d.LoadSnapshot(nil, []Item{{Price: 1337}}, 0, time.Now(), true)
err = d.LoadSnapshot(nil, []Tranche{{Price: 1337}}, 0, time.Now(), true)
assert.NoError(t, err, "LoadSnapshot should not error")
bidLen, err := d.GetBidLength()
assert.NoError(t, err, "GetBidLength should not error")
assert.Zero(t, bidLen, "bid length should be zero")
d.bids.load([]Item{{Price: 1337}}, d.stack, time.Now())
d.bidTranches.load([]Tranche{{Price: 1337}})
bidLen, err = d.GetBidLength()
assert.NoError(t, err, "GetBidLength should not error")
@@ -65,8 +65,8 @@ func TestGetLength(t *testing.T) {
func TestRetrieve(t *testing.T) {
t.Parallel()
d := NewDepth(id)
d.asks.load([]Item{{Price: 1337}}, d.stack, time.Now())
d.bids.load([]Item{{Price: 1337}}, d.stack, time.Now())
d.askTranches.load([]Tranche{{Price: 1337}})
d.bidTranches.load([]Tranche{{Price: 1337}})
d.options = options{
exchange: "THE BIG ONE!!!!!!",
pair: currency.NewPair(currency.THETA, currency.USD),
@@ -125,8 +125,8 @@ func TestTotalAmounts(t *testing.T) {
assert.Zero(t, liquidity, "total ask liquidity should be zero")
assert.Zero(t, value, "total ask value should be zero")
d.asks.load([]Item{{Price: 1337, Amount: 1}}, d.stack, time.Now())
d.bids.load([]Item{{Price: 1337, Amount: 10}}, d.stack, time.Now())
d.askTranches.load([]Tranche{{Price: 1337, Amount: 1}})
d.bidTranches.load([]Tranche{{Price: 1337, Amount: 10}})
liquidity, value, err = d.TotalBidAmounts()
assert.NoError(t, err, "TotalBidAmounts should not error")
@@ -142,10 +142,10 @@ func TestTotalAmounts(t *testing.T) {
func TestLoadSnapshot(t *testing.T) {
t.Parallel()
d := NewDepth(id)
err := d.LoadSnapshot(Items{{Price: 1337, Amount: 1}}, Items{{Price: 1337, Amount: 10}}, 0, time.Time{}, false)
err := d.LoadSnapshot(Tranches{{Price: 1337, Amount: 1}}, Tranches{{Price: 1337, Amount: 10}}, 0, time.Time{}, false)
assert.ErrorIs(t, err, errLastUpdatedNotSet, "LoadSnapshot should error correctly")
err = d.LoadSnapshot(Items{{Price: 1337, Amount: 2}}, Items{{Price: 1338, Amount: 10}}, 0, time.Now(), false)
err = d.LoadSnapshot(Tranches{{Price: 1337, Amount: 2}}, Tranches{{Price: 1338, Amount: 10}}, 0, time.Now(), false)
assert.NoError(t, err, "LoadSnapshot should not error")
ob, err := d.Retrieve()
@@ -164,7 +164,7 @@ func TestInvalidate(t *testing.T) {
d.pair = currency.NewPair(currency.BTC, currency.WABI)
d.asset = asset.Spot
err := d.LoadSnapshot(Items{{Price: 1337, Amount: 1}}, Items{{Price: 1337, Amount: 10}}, 0, time.Now(), false)
err := d.LoadSnapshot(Tranches{{Price: 1337, Amount: 1}}, Tranches{{Price: 1337, Amount: 10}}, 0, time.Now(), false)
assert.NoError(t, err, "LoadSnapshot should not error")
ob, err := d.Retrieve()
@@ -192,7 +192,7 @@ func TestInvalidate(t *testing.T) {
func TestUpdateBidAskByPrice(t *testing.T) {
t.Parallel()
d := NewDepth(id)
err := d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1338, Amount: 10, ID: 2}}, 0, time.Now(), false)
err := d.LoadSnapshot(Tranches{{Price: 1337, Amount: 1, ID: 1}}, Tranches{{Price: 1338, Amount: 10, ID: 2}}, 0, time.Now(), false)
assert.NoError(t, err, "LoadSnapshot should not error")
err = d.UpdateBidAskByPrice(&Update{})
@@ -202,8 +202,8 @@ func TestUpdateBidAskByPrice(t *testing.T) {
assert.NoError(t, err, "UpdateBidAskByPrice should not error")
updates := &Update{
Bids: Items{{Price: 1337, Amount: 2, ID: 1}},
Asks: Items{{Price: 1338, Amount: 3, ID: 2}},
Bids: Tranches{{Price: 1337, Amount: 2, ID: 1}},
Asks: Tranches{{Price: 1338, Amount: 3, ID: 2}},
UpdateID: 1,
UpdateTime: time.Now(),
}
@@ -216,8 +216,8 @@ func TestUpdateBidAskByPrice(t *testing.T) {
assert.Equal(t, 2.0, ob.Bids[0].Amount, "Bids amount should be correct")
updates = &Update{
Bids: Items{{Price: 1337, Amount: 0, ID: 1}},
Asks: Items{{Price: 1338, Amount: 0, ID: 2}},
Bids: Tranches{{Price: 1337, Amount: 0, ID: 1}},
Asks: Tranches{{Price: 1338, Amount: 0, ID: 2}},
UpdateID: 2,
UpdateTime: time.Now(),
}
@@ -236,12 +236,12 @@ func TestUpdateBidAskByPrice(t *testing.T) {
func TestDeleteBidAskByID(t *testing.T) {
t.Parallel()
d := NewDepth(id)
err := d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
err := d.LoadSnapshot(Tranches{{Price: 1337, Amount: 1, ID: 1}}, Tranches{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
assert.NoError(t, err, "LoadSnapshot should not error")
updates := &Update{
Bids: Items{{Price: 1337, Amount: 2, ID: 1}},
Asks: Items{{Price: 1337, Amount: 2, ID: 2}},
Bids: Tranches{{Price: 1337, Amount: 2, ID: 1}},
Asks: Tranches{{Price: 1337, Amount: 2, ID: 2}},
}
err = d.DeleteBidAskByID(updates, false)
@@ -257,21 +257,21 @@ func TestDeleteBidAskByID(t *testing.T) {
assert.Empty(t, ob.Bids, "Bids should be empty")
updates = &Update{
Bids: Items{{Price: 1337, Amount: 2, ID: 1}},
Bids: Tranches{{Price: 1337, Amount: 2, ID: 1}},
UpdateTime: time.Now(),
}
err = d.DeleteBidAskByID(updates, false)
assert.ErrorIs(t, err, errIDCannotBeMatched, "DeleteBidAskByID should error correctly")
updates = &Update{
Asks: Items{{Price: 1337, Amount: 2, ID: 2}},
Asks: Tranches{{Price: 1337, Amount: 2, ID: 2}},
UpdateTime: time.Now(),
}
err = d.DeleteBidAskByID(updates, false)
assert.ErrorIs(t, err, errIDCannotBeMatched, "DeleteBidAskByID should error correctly")
updates = &Update{
Asks: Items{{Price: 1337, Amount: 2, ID: 2}},
Asks: Tranches{{Price: 1337, Amount: 2, ID: 2}},
UpdateTime: time.Now(),
}
err = d.DeleteBidAskByID(updates, true)
@@ -281,12 +281,12 @@ func TestDeleteBidAskByID(t *testing.T) {
func TestUpdateBidAskByID(t *testing.T) {
t.Parallel()
d := NewDepth(id)
err := d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
err := d.LoadSnapshot(Tranches{{Price: 1337, Amount: 1, ID: 1}}, Tranches{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
assert.NoError(t, err, "LoadSnapshot should not error")
updates := &Update{
Bids: Items{{Price: 1337, Amount: 2, ID: 1}},
Asks: Items{{Price: 1337, Amount: 2, ID: 2}},
Bids: Tranches{{Price: 1337, Amount: 2, ID: 1}},
Asks: Tranches{{Price: 1337, Amount: 2, ID: 2}},
}
err = d.UpdateBidAskByID(updates)
@@ -302,7 +302,7 @@ func TestUpdateBidAskByID(t *testing.T) {
assert.Equal(t, 2.0, ob.Bids[0].Amount, "First bid amount should be correct")
updates = &Update{
Bids: Items{{Price: 1337, Amount: 2, ID: 666}},
Bids: Tranches{{Price: 1337, Amount: 2, ID: 666}},
UpdateTime: time.Now(),
}
// random unmatching IDs
@@ -310,7 +310,7 @@ func TestUpdateBidAskByID(t *testing.T) {
assert.ErrorIs(t, err, errIDCannotBeMatched, "UpdateBidAskByID should error correctly")
updates = &Update{
Asks: Items{{Price: 1337, Amount: 2, ID: 69}},
Asks: Tranches{{Price: 1337, Amount: 2, ID: 69}},
UpdateTime: time.Now(),
}
err = d.UpdateBidAskByID(updates)
@@ -320,11 +320,11 @@ func TestUpdateBidAskByID(t *testing.T) {
func TestInsertBidAskByID(t *testing.T) {
t.Parallel()
d := NewDepth(id)
err := d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
err := d.LoadSnapshot(Tranches{{Price: 1337, Amount: 1, ID: 1}}, Tranches{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
assert.NoError(t, err, "LoadSnapshot should not error")
updates := &Update{
Asks: Items{{Price: 1337, Amount: 2, ID: 3}},
Asks: Tranches{{Price: 1337, Amount: 2, ID: 3}},
}
err = d.InsertBidAskByID(updates)
assert.ErrorIs(t, err, errLastUpdatedNotSet, "InsertBidAskByID should error correctly")
@@ -334,23 +334,23 @@ func TestInsertBidAskByID(t *testing.T) {
err = d.InsertBidAskByID(updates)
assert.ErrorIs(t, err, errCollisionDetected, "InsertBidAskByID should error correctly on collision")
err = d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
err = d.LoadSnapshot(Tranches{{Price: 1337, Amount: 1, ID: 1}}, Tranches{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
assert.NoError(t, err, "LoadSnapshot should not error")
updates = &Update{
Bids: Items{{Price: 1337, Amount: 2, ID: 3}},
Bids: Tranches{{Price: 1337, Amount: 2, ID: 3}},
UpdateTime: time.Now(),
}
err = d.InsertBidAskByID(updates)
assert.ErrorIs(t, err, errCollisionDetected, "InsertBidAskByID should error correctly on collision")
err = d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
err = d.LoadSnapshot(Tranches{{Price: 1337, Amount: 1, ID: 1}}, Tranches{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
assert.NoError(t, err, "LoadSnapshot should not error")
updates = &Update{
Bids: Items{{Price: 1338, Amount: 2, ID: 3}},
Asks: Items{{Price: 1336, Amount: 2, ID: 4}},
Bids: Tranches{{Price: 1338, Amount: 2, ID: 3}},
Asks: Tranches{{Price: 1336, Amount: 2, ID: 4}},
UpdateTime: time.Now(),
}
err = d.InsertBidAskByID(updates)
@@ -365,12 +365,12 @@ func TestInsertBidAskByID(t *testing.T) {
func TestUpdateInsertByID(t *testing.T) {
t.Parallel()
d := NewDepth(id)
err := d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
err := d.LoadSnapshot(Tranches{{Price: 1337, Amount: 1, ID: 1}}, Tranches{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
assert.NoError(t, err, "LoadSnapshot should not error")
updates := &Update{
Bids: Items{{Price: 1338, Amount: 0, ID: 3}},
Asks: Items{{Price: 1336, Amount: 2, ID: 4}},
Bids: Tranches{{Price: 1338, Amount: 0, ID: 3}},
Asks: Tranches{{Price: 1336, Amount: 2, ID: 4}},
}
err = d.UpdateInsertByID(updates)
assert.ErrorIs(t, err, errLastUpdatedNotSet, "UpdateInsertByID should error correctly")
@@ -383,12 +383,12 @@ func TestUpdateInsertByID(t *testing.T) {
_, err = d.Retrieve()
assert.ErrorIs(t, err, ErrOrderbookInvalid, "Retrieve should error correctly")
err = d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
err = d.LoadSnapshot(Tranches{{Price: 1337, Amount: 1, ID: 1}}, Tranches{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
assert.NoError(t, err, "LoadSnapshot should not error")
updates = &Update{
Bids: Items{{Price: 1338, Amount: 2, ID: 3}},
Asks: Items{{Price: 1336, Amount: 0, ID: 4}},
Bids: Tranches{{Price: 1338, Amount: 2, ID: 3}},
Asks: Tranches{{Price: 1336, Amount: 0, ID: 4}},
UpdateTime: time.Now(),
}
err = d.UpdateInsertByID(updates)
@@ -398,12 +398,12 @@ func TestUpdateInsertByID(t *testing.T) {
_, err = d.Retrieve()
assert.ErrorIs(t, err, ErrOrderbookInvalid, "Retrieve should error correctly")
err = d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
err = d.LoadSnapshot(Tranches{{Price: 1337, Amount: 1, ID: 1}}, Tranches{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Now(), false)
assert.NoError(t, err, "LoadSnapshot should not error")
updates = &Update{
Bids: Items{{Price: 1338, Amount: 2, ID: 3}},
Asks: Items{{Price: 1336, Amount: 2, ID: 4}},
Bids: Tranches{{Price: 1338, Amount: 2, ID: 3}},
Asks: Tranches{{Price: 1336, Amount: 2, ID: 4}},
UpdateTime: time.Now(),
}
err = d.UpdateInsertByID(updates)

View File

@@ -1,931 +0,0 @@
package orderbook
import (
"errors"
"fmt"
"time"
"github.com/thrasher-corp/gocryptotrader/common/math"
)
// FullLiquidityExhaustedPercentage defines when a book has been completely
// wiped out of potential liquidity.
const FullLiquidityExhaustedPercentage = -100
var (
errIDCannotBeMatched = errors.New("cannot match ID on linked list")
errCollisionDetected = errors.New("cannot insert update, collision detected")
errAmountCannotBeLessOrEqualToZero = errors.New("amount cannot be less than or equal to zero")
errInvalidNominalSlippage = errors.New("invalid slippage amount, its value must be greater than or equal to zero")
errInvalidImpactSlippage = errors.New("invalid slippage amount, its value must be greater than zero")
errInvalidSlippageCannotExceed100 = errors.New("invalid slippage amount, its value cannot exceed 100%")
errBaseAmountInvalid = errors.New("invalid base amount")
errInvalidReferencePrice = errors.New("invalid reference price")
errQuoteAmountInvalid = errors.New("quote amount invalid")
errInvalidCost = errors.New("invalid cost amount")
errInvalidAmount = errors.New("invalid amount")
errInvalidHeadPrice = errors.New("invalid head price")
)
// linkedList defines a linked list for a depth level, reutilisation of nodes
// to and from a stack.
type linkedList struct {
length int
head *Node
}
// comparison defines expected functionality to compare between two reference
// price levels
type comparison func(float64, float64) bool
// load iterates across new items and refreshes linked list. It creates a linked
// list exactly the same as the item slice that is supplied, if items is of nil
// value it will flush entire list.
func (ll *linkedList) load(items Items, stack *stack, tn time.Time) {
// Tip sets up a pointer to a struct field variable pointer. This is used
// so when a node is popped from the stack we can reference that current
// nodes' struct 'next' field and set on next iteration without utilising
// assignment for example `prev.Next = *node`.
var tip = &ll.head
// Prev denotes a place holder to node and all of its next references need
// to be pushed back onto stack.
var prev *Node
for i := range items {
if *tip == nil {
// Extend node chain
*tip = stack.Pop()
// Set current node prev to last node
(*tip).Prev = prev
ll.length++
}
// Set item value
(*tip).Value = items[i]
// Set previous to current node
prev = *tip
// Set tip to next node
tip = &(*tip).Next
}
// Push has references to dangling nodes that need to be removed and pushed
// back onto stack for reuse
var push *Node
// Cleave unused reference chain from main chain
if prev == nil {
// The entire chain will need to be pushed back on to stack
push = *tip
ll.head = nil
} else {
push = prev.Next
prev.Next = nil
}
// Push unused pointers back on stack
for push != nil {
pending := push.Next
stack.Push(push, tn)
ll.length--
push = pending
}
}
// updateByID amends price by corresponding ID and returns an error if not found
func (ll *linkedList) updateByID(updts []Item) error {
updates:
for x := range updts {
for tip := ll.head; tip != nil; tip = tip.Next {
if updts[x].ID != tip.Value.ID { // Filter IDs that don't match
continue
}
if updts[x].Price > 0 {
// Only apply changes when zero values are not present, Bitmex
// for example sends 0 price values.
tip.Value.Price = updts[x].Price
tip.Value.StrPrice = updts[x].StrPrice
}
tip.Value.Amount = updts[x].Amount
tip.Value.StrAmount = updts[x].StrAmount
continue updates
}
return fmt.Errorf("update error: %w ID: %d not found",
errIDCannotBeMatched,
updts[x].ID)
}
return nil
}
// deleteByID deletes reference by ID
func (ll *linkedList) deleteByID(updts Items, stack *stack, bypassErr bool, tn time.Time) error {
updates:
for x := range updts {
for tip := &ll.head; *tip != nil; tip = &(*tip).Next {
if updts[x].ID != (*tip).Value.ID {
continue
}
stack.Push(deleteAtTip(ll, tip), tn)
continue updates
}
if !bypassErr {
return fmt.Errorf("delete error: %w %d not found",
errIDCannotBeMatched,
updts[x].ID)
}
}
return nil
}
// cleanup reduces the max size of the depth length if exceeded. Is used after
// updates have been applied instead of adhoc, reason being its easier to prune
// at the end. (can't inline)
func (ll *linkedList) cleanup(maxChainLength int, stack *stack, tn time.Time) {
// Reduces the max length of total linked list chain, occurs after updates
// have been implemented as updates can push length out of bounds, if
// cleaved after that update, new update might not applied correctly.
n := ll.head
for i := 0; i < maxChainLength; i++ {
if n.Next == nil {
return
}
n = n.Next
}
// cleave reference to current node
if n.Prev != nil {
n.Prev.Next = nil
} else {
ll.head = nil
}
var pruned int
for n != nil {
pruned++
pending := n.Next
stack.Push(n, tn)
n = pending
}
ll.length -= pruned
}
// amount returns total depth liquidity and value
func (ll *linkedList) amount() (liquidity, value float64) {
for tip := ll.head; tip != nil; tip = tip.Next {
liquidity += tip.Value.Amount
value += tip.Value.Amount * tip.Value.Price
}
return
}
// retrieve returns a full slice of contents from the linked list
func (ll *linkedList) retrieve(count int) Items {
if count == 0 || ll.length < count {
count = ll.length
}
depth := make(Items, count)
for i, tip := 0, ll.head; i < count && tip != nil; i, tip = i+1, tip.Next {
depth[i] = tip.Value
}
return depth
}
// updateInsertByPrice amends, inserts, moves and cleaves length of depth by
// updates
func (ll *linkedList) updateInsertByPrice(updts Items, stack *stack, maxChainLength int, compare func(float64, float64) bool, tn time.Time) {
for x := range updts {
for tip := &ll.head; ; tip = &(*tip).Next {
if *tip == nil {
insertHeadSpecific(ll, &updts[x], stack)
break
}
if (*tip).Value.Price == updts[x].Price { // Match check
if updts[x].Amount <= 0 { // Capture delete update
stack.Push(deleteAtTip(ll, tip), tn)
} else { // Amend current amount value
(*tip).Value.Amount = updts[x].Amount
(*tip).Value.StrAmount = updts[x].StrAmount
}
break // Continue updates
}
if compare((*tip).Value.Price, updts[x].Price) { // Insert
// This check below filters zero values and provides an
// optimisation for when select exchanges send a delete update
// to a non-existent price level (OTC/Hidden order) so we can
// break instantly and reduce the traversal of the entire chain.
if updts[x].Amount > 0 {
insertAtTip(ll, tip, &updts[x], stack)
}
break // Continue updates
}
if (*tip).Next == nil { // Tip is at tail
// This check below is just a catch all in the event the above
// zero value check fails
if updts[x].Amount > 0 {
insertAtTail(ll, tip, &updts[x], stack)
}
break
}
}
}
// Reduces length of total linked list chain to a maxChainLength value
if maxChainLength != 0 && ll.length > maxChainLength {
ll.cleanup(maxChainLength, stack, tn)
}
}
// updateInsertByID updates or inserts if not found for a bid or ask depth
// 1) node ID found amount amended (best case)
// 2) node ID found amount and price amended and node moved to correct position
// (medium case)
// 3) Update price exceeds traversal node price before ID found, save node
// address for either; node ID matches then re-address node or end of depth pop
// a node from the stack (worst case)
func (ll *linkedList) updateInsertByID(updts Items, stack *stack, compare comparison) error {
updates:
for x := range updts {
if updts[x].Amount <= 0 {
return errAmountCannotBeLessOrEqualToZero
}
// bookmark allows for saving of a position of a node in the event that
// an update price exceeds the current node price. We can then match an
// ID and re-assign that ID's node to that positioning without popping
// from the stack and then pushing to the stack later for cleanup.
// If the ID is not found we can pop from stack then insert into that
// price level
var bookmark *Node
for tip := ll.head; tip != nil; tip = tip.Next {
if tip.Value.ID == updts[x].ID {
if tip.Value.Price != updts[x].Price { // Price level change
if tip.Next == nil {
// no movement needed just a re-adjustment
tip.Value.Price = updts[x].Price
tip.Value.StrPrice = updts[x].StrPrice
tip.Value.Amount = updts[x].Amount
tip.Value.StrAmount = updts[x].StrAmount
continue updates
}
// bookmark tip to move this node to correct price level
bookmark = tip
continue // continue through node depth
}
// no price change, amend amount and continue update
tip.Value.Amount = updts[x].Amount
tip.Value.StrAmount = updts[x].StrAmount
continue updates // continue to next update
}
if compare(tip.Value.Price, updts[x].Price) {
if bookmark != nil { // shift bookmarked node to current tip
bookmark.Value = updts[x]
move(&ll.head, bookmark, tip)
continue updates
}
// search for ID
for n := tip.Next; n != nil; n = n.Next {
if n.Value.ID == updts[x].ID {
n.Value = updts[x]
// inserting before the tip
move(&ll.head, n, tip)
continue updates
}
}
// ID not matched in depth so add correct level for insert
if tip.Next == nil {
n := stack.Pop()
n.Value = updts[x]
ll.length++
if tip.Prev == nil {
tip.Prev = n
n.Next = tip
ll.head = n
continue updates
}
tip.Prev.Next = n
n.Prev = tip.Prev
tip.Prev = n
n.Next = tip
continue updates
}
bookmark = tip
break
}
if tip.Next == nil {
if shiftBookmark(tip, &bookmark, &ll.head, &updts[x]) {
continue updates
}
}
}
n := stack.Pop()
n.Value = updts[x]
insertNodeAtBookmark(ll, bookmark, n) // Won't inline with stack
}
return nil
}
// insertUpdates inserts new updates for bids or asks based on price level
func (ll *linkedList) insertUpdates(updts Items, stack *stack, comp comparison) error {
for x := range updts {
var prev *Node
for tip := &ll.head; ; tip = &(*tip).Next {
if *tip == nil { // Head
n := stack.Pop()
n.Value = updts[x]
n.Prev = prev
ll.length++
*tip = n
break // Continue updates
}
if (*tip).Value.Price == updts[x].Price { // Price already found
return fmt.Errorf("%w for price %f",
errCollisionDetected,
updts[x].Price)
}
if comp((*tip).Value.Price, updts[x].Price) { // Alignment
n := stack.Pop()
n.Value = updts[x]
n.Prev = prev
ll.length++
// Reference current with new node
(*tip).Prev = n
// Push tip to the right
n.Next = *tip
// This is the same as prev.Next = n
*tip = n
break // Continue updates
}
if (*tip).Next == nil { // Tail
insertAtTail(ll, tip, &updts[x], stack)
break // Continue updates
}
prev = *tip
}
}
return nil
}
// getHeadPriceNoLock gets best/head price
func (ll *linkedList) getHeadPriceNoLock() (float64, error) {
if ll.head == nil {
return 0, errNoLiquidity
}
return ll.head.Value.Price, nil
}
// getHeadVolumeNoLock gets best/head volume
func (ll *linkedList) getHeadVolumeNoLock() (float64, error) {
if ll.head == nil {
return 0, errNoLiquidity
}
return ll.head.Value.Amount, nil
}
// getMovementByQuotation traverses through orderbook liquidity using quotation
// currency as a limiter and returns orderbook movement details. Swap boolean
// allows the swap of sold and purchased to reduce code so it doesn't need to be
// specific to bid or ask.
func (ll *linkedList) getMovementByQuotation(quote, refPrice float64, swap bool) (*Movement, error) {
if quote <= 0 {
return nil, errQuoteAmountInvalid
}
if refPrice <= 0 {
return nil, errInvalidReferencePrice
}
head, err := ll.getHeadPriceNoLock()
if err != nil {
return nil, err
}
m := Movement{StartPrice: refPrice}
for tip := ll.head; tip != nil; tip = tip.Next {
trancheValue := tip.Value.Amount * tip.Value.Price
leftover := quote - trancheValue
if leftover < 0 {
m.Purchased += quote
m.Sold += quote / trancheValue * tip.Value.Amount
// This tranche is not consumed so the book shifts to this price.
m.EndPrice = tip.Value.Price
quote = 0
break
}
// Full tranche consumed
m.Purchased += tip.Value.Price * tip.Value.Amount
m.Sold += tip.Value.Amount
quote = leftover
if leftover == 0 {
// Price no longer exists on the book so use next full price tranche
// to calculate book impact. If available.
if tip.Next != nil {
m.EndPrice = tip.Next.Value.Price
} else {
m.FullBookSideConsumed = true
}
}
}
return m.finalizeFields(m.Purchased, m.Sold, head, quote, swap)
}
// getMovementByBase traverses through orderbook liquidity using base currency
// as a limiter and returns orderbook movement details. Swap boolean allows the
// swap of sold and purchased to reduce code so it doesn't need to be specific
// to bid or ask.
func (ll *linkedList) getMovementByBase(base, refPrice float64, swap bool) (*Movement, error) {
if base <= 0 {
return nil, errBaseAmountInvalid
}
if refPrice <= 0 {
return nil, errInvalidReferencePrice
}
head, err := ll.getHeadPriceNoLock()
if err != nil {
return nil, err
}
m := Movement{StartPrice: refPrice}
for tip := ll.head; tip != nil; tip = tip.Next {
leftover := base - tip.Value.Amount
if leftover < 0 {
m.Purchased += tip.Value.Price * base
m.Sold += base
// This tranche is not consumed so the book shifts to this price.
m.EndPrice = tip.Value.Price
base = 0
break
}
// Full tranche consumed
m.Purchased += tip.Value.Price * tip.Value.Amount
m.Sold += tip.Value.Amount
base = leftover
if leftover == 0 {
// Price no longer exists on the book so use next full price tranche
// to calculate book impact.
if tip.Next != nil {
m.EndPrice = tip.Next.Value.Price
} else {
m.FullBookSideConsumed = true
}
}
}
return m.finalizeFields(m.Purchased, m.Sold, head, base, swap)
}
// bids embed a linked list to attach methods for bid depth specific
// functionality
type bids struct {
linkedList
}
// bidCompare ensures price is in correct descending alignment (can inline)
func bidCompare(left, right float64) bool {
return left < right
}
// updateInsertByPrice amends, inserts, moves and cleaves length of depth by
// updates
func (ll *bids) updateInsertByPrice(updts Items, stack *stack, maxChainLength int, tn time.Time) {
ll.linkedList.updateInsertByPrice(updts, stack, maxChainLength, bidCompare, tn)
}
// updateInsertByID updates or inserts if not found
func (ll *bids) updateInsertByID(updts Items, stack *stack) error {
return ll.linkedList.updateInsertByID(updts, stack, bidCompare)
}
// insertUpdates inserts new updates for bids based on price level
func (ll *bids) insertUpdates(updts Items, stack *stack) error {
return ll.linkedList.insertUpdates(updts, stack, bidCompare)
}
// hitBidsByNominalSlippage hits the bids by the required nominal slippage
// percentage, calculated from the reference price and returns orderbook
// movement details.
func (ll *bids) hitBidsByNominalSlippage(slippage, refPrice float64) (*Movement, error) {
if slippage < 0 {
return nil, errInvalidNominalSlippage
}
if slippage > 100 {
return nil, errInvalidSlippageCannotExceed100
}
if refPrice <= 0 {
return nil, errInvalidReferencePrice
}
if ll.head == nil {
return nil, errNoLiquidity
}
nominal := &Movement{StartPrice: refPrice, EndPrice: refPrice}
var cumulativeValue, cumulativeAmounts float64
for tip := ll.head; tip != nil; tip = tip.Next {
totalTrancheValue := tip.Value.Price * tip.Value.Amount
currentFullValue := totalTrancheValue + cumulativeValue
currentTotalAmounts := cumulativeAmounts + tip.Value.Amount
nominal.AverageOrderCost = currentFullValue / currentTotalAmounts
percent := math.CalculatePercentageGainOrLoss(nominal.AverageOrderCost, refPrice)
if percent != 0 {
percent *= -1
}
if slippage < percent {
targetCost := (1 - slippage/100) * refPrice
if targetCost == refPrice {
nominal.AverageOrderCost = cumulativeValue / cumulativeAmounts
// Rounding issue on requested nominal percentage
return nominal, nil
}
comparative := targetCost * cumulativeAmounts
comparativeDiff := comparative - cumulativeValue
trancheTargetPriceDiff := tip.Value.Price - targetCost
trancheAmountExpectation := comparativeDiff / trancheTargetPriceDiff
nominal.NominalPercentage = slippage
nominal.Sold = cumulativeAmounts + trancheAmountExpectation
nominal.Purchased += trancheAmountExpectation * tip.Value.Price
nominal.AverageOrderCost = nominal.Purchased / nominal.Sold
nominal.EndPrice = tip.Value.Price
return nominal, nil
}
nominal.EndPrice = tip.Value.Price
cumulativeValue = currentFullValue
nominal.NominalPercentage = percent
nominal.Sold += tip.Value.Amount
nominal.Purchased += totalTrancheValue
cumulativeAmounts = currentTotalAmounts
if slippage == percent {
nominal.FullBookSideConsumed = tip.Next == nil
return nominal, nil
}
}
nominal.FullBookSideConsumed = true
return nominal, nil
}
// hitBidsByImpactSlippage hits the bids by the required impact slippage
// percentage, calculated from the reference price and returns orderbook
// movement details.
func (ll *bids) hitBidsByImpactSlippage(slippage, refPrice float64) (*Movement, error) {
if slippage <= 0 {
return nil, errInvalidImpactSlippage
}
if slippage > 100 {
return nil, errInvalidSlippageCannotExceed100
}
if refPrice <= 0 {
return nil, errInvalidReferencePrice
}
if ll.head == nil {
return nil, errNoLiquidity
}
impact := &Movement{StartPrice: refPrice, EndPrice: refPrice}
for tip := ll.head; tip != nil; tip = tip.Next {
percent := math.CalculatePercentageGainOrLoss(tip.Value.Price, refPrice)
if percent != 0 {
percent *= -1
}
impact.EndPrice = tip.Value.Price
impact.ImpactPercentage = percent
if slippage <= percent {
// Don't include this tranche amount as this consumes the tranche
// book price, thus obtaining a higher percentage impact.
return impact, nil
}
impact.Sold += tip.Value.Amount
impact.Purchased += tip.Value.Amount * tip.Value.Price
impact.AverageOrderCost = impact.Purchased / impact.Sold
}
impact.FullBookSideConsumed = true
impact.ImpactPercentage = FullLiquidityExhaustedPercentage
return impact, nil
}
// asks embed a linked list to attach methods for ask depth specific
// functionality
type asks struct {
linkedList
}
// askCompare ensures price is in correct ascending alignment (can inline)
func askCompare(left, right float64) bool {
return left > right
}
// updateInsertByPrice amends, inserts, moves and cleaves length of depth by
// updates
func (ll *asks) updateInsertByPrice(updts Items, stack *stack, maxChainLength int, tn time.Time) {
ll.linkedList.updateInsertByPrice(updts, stack, maxChainLength, askCompare, tn)
}
// updateInsertByID updates or inserts if not found
func (ll *asks) updateInsertByID(updts Items, stack *stack) error {
return ll.linkedList.updateInsertByID(updts, stack, askCompare)
}
// insertUpdates inserts new updates for asks based on price level
func (ll *asks) insertUpdates(updts Items, stack *stack) error {
return ll.linkedList.insertUpdates(updts, stack, askCompare)
}
// liftAsksByNominalSlippage lifts the asks by the required nominal slippage
// percentage, calculated from the reference price and returns orderbook
// movement details.
func (ll *asks) liftAsksByNominalSlippage(slippage, refPrice float64) (*Movement, error) {
if slippage < 0 {
return nil, errInvalidNominalSlippage
}
if refPrice <= 0 {
return nil, errInvalidReferencePrice
}
if ll.head == nil {
return nil, errNoLiquidity
}
nominal := &Movement{StartPrice: refPrice, EndPrice: refPrice}
var cumulativeAmounts float64
for tip := ll.head; tip != nil; tip = tip.Next {
totalTrancheValue := tip.Value.Price * tip.Value.Amount
currentValue := totalTrancheValue + nominal.Sold
currentAmounts := cumulativeAmounts + tip.Value.Amount
nominal.AverageOrderCost = currentValue / currentAmounts
percent := math.CalculatePercentageGainOrLoss(nominal.AverageOrderCost, refPrice)
if slippage < percent {
targetCost := (1 + slippage/100) * refPrice
if targetCost == refPrice {
nominal.AverageOrderCost = nominal.Sold / nominal.Purchased
// Rounding issue on requested nominal percentage
return nominal, nil
}
comparative := targetCost * cumulativeAmounts
comparativeDiff := comparative - nominal.Sold
trancheTargetPriceDiff := tip.Value.Price - targetCost
trancheAmountExpectation := comparativeDiff / trancheTargetPriceDiff
nominal.NominalPercentage = slippage
nominal.Sold += trancheAmountExpectation * tip.Value.Price
nominal.Purchased += trancheAmountExpectation
nominal.AverageOrderCost = nominal.Sold / nominal.Purchased
nominal.EndPrice = tip.Value.Price
return nominal, nil
}
nominal.EndPrice = tip.Value.Price
nominal.Sold = currentValue
nominal.Purchased += tip.Value.Amount
nominal.NominalPercentage = percent
if slippage == percent {
return nominal, nil
}
cumulativeAmounts = currentAmounts
}
nominal.FullBookSideConsumed = true
return nominal, nil
}
// liftAsksByImpactSlippage lifts the asks by the required impact slippage
// percentage, calculated from the reference price and returns orderbook
// movement details.
func (ll *asks) liftAsksByImpactSlippage(slippage, refPrice float64) (*Movement, error) {
if slippage <= 0 {
return nil, errInvalidImpactSlippage
}
if refPrice <= 0 {
return nil, errInvalidReferencePrice
}
if ll.head == nil {
return nil, errNoLiquidity
}
impact := &Movement{StartPrice: refPrice, EndPrice: refPrice}
for tip := ll.head; tip != nil; tip = tip.Next {
percent := math.CalculatePercentageGainOrLoss(tip.Value.Price, refPrice)
impact.ImpactPercentage = percent
impact.EndPrice = tip.Value.Price
if slippage <= percent {
// Don't include this tranche amount as this consumes the tranche
// book price, thus obtaining a higher percentage impact.
return impact, nil
}
impact.Sold += tip.Value.Amount * tip.Value.Price
impact.Purchased += tip.Value.Amount
impact.AverageOrderCost = impact.Sold / impact.Purchased
}
impact.FullBookSideConsumed = true
impact.ImpactPercentage = FullLiquidityExhaustedPercentage
return impact, nil
}
// move moves a node from a point in a node chain to another node position,
// this left justified towards head as element zero is the top of the depth
// side. (can inline)
func move(head **Node, from, to *Node) {
if from.Next != nil { // From is at tail
from.Next.Prev = from.Prev
}
if from.Prev == nil { // From is at head
(*head).Next.Prev = nil
*head = (*head).Next
} else {
from.Prev.Next = from.Next
}
// insert from node next to 'to' node
if to.Prev == nil { // Destination is at head position
*head = from
} else {
to.Prev.Next = from
}
from.Prev = to.Prev
to.Prev = from
from.Next = to
}
// deleteAtTip removes a node from tip target returns old node (can inline)
func deleteAtTip(ll *linkedList, tip **Node) *Node {
// Old is a placeholder for current tips node value to push
// back on to the stack.
old := *tip
switch {
case old.Prev == nil: // At head position
// shift current tip head to the right
*tip = old.Next
// Remove reference to node from chain
if old.Next != nil { // This is when liquidity hits zero
old.Next.Prev = nil
}
case old.Next == nil: // At tail position
// Remove reference to node from chain
old.Prev.Next = nil
default:
// Reference prior node in chain to next node in chain
// bypassing current node
old.Prev.Next = old.Next
old.Next.Prev = old.Prev
}
ll.length--
return old
}
// insertAtTip inserts at a tip target (can inline)
func insertAtTip(ll *linkedList, tip **Node, updt *Item, stack *stack) {
n := stack.Pop()
n.Value = *updt
n.Next = *tip
n.Prev = (*tip).Prev
if (*tip).Prev == nil { // Tip is at head
// Replace head which will push everything to the right
// when this node will reference new node below
*tip = n
} else {
// Reference new node to previous node
(*tip).Prev.Next = n
}
// Reference next node to new node
n.Next.Prev = n
ll.length++
}
// insertAtTail inserts at tail end of node chain (can inline)
func insertAtTail(ll *linkedList, tip **Node, updt *Item, stack *stack) {
n := stack.Pop()
n.Value = *updt
// Reference tip to new node
(*tip).Next = n
// Reference new node with current tip
n.Prev = *tip
ll.length++
}
// insertHeadSpecific inserts at head specifically there might be an instance
// where the liquidity on an exchange does fall to zero through a streaming
// endpoint then it comes back online. (can inline)
func insertHeadSpecific(ll *linkedList, updt *Item, stack *stack) {
n := stack.Pop()
n.Value = *updt
ll.head = n
ll.length++
}
// insertNodeAtBookmark inserts a new node at a bookmarked node position
// returns if a node needs to replace head (can inline)
func insertNodeAtBookmark(ll *linkedList, bookmark, n *Node) {
switch {
case bookmark == nil: // Zero liquidity and we are rebuilding from scratch
ll.head = n
case bookmark.Prev == nil:
n.Prev = bookmark.Prev
bookmark.Prev = n
n.Next = bookmark
ll.head = n
case bookmark.Next == nil:
n.Prev = bookmark
bookmark.Next = n
default:
bookmark.Prev.Next = n
n.Prev = bookmark.Prev
bookmark.Prev = n
n.Next = bookmark
}
ll.length++
}
// shiftBookmark moves a bookmarked node to the tip's next position or if nil,
// sets tip as bookmark (can inline)
func shiftBookmark(tip *Node, bookmark, head **Node, updt *Item) bool {
if *bookmark == nil { // End of the chain and no bookmark set
*bookmark = tip // Set tip to bookmark so we can set a new node there
return false
}
(*bookmark).Value = *updt
(*bookmark).Next.Prev = (*bookmark).Prev
if (*bookmark).Prev == nil { // Bookmark is at head
*head = (*bookmark).Next
} else {
(*bookmark).Prev.Next = (*bookmark).Next
}
tip.Next = *bookmark
(*bookmark).Prev = tip
(*bookmark).Next = nil
return true
}
// finalizeFields sets average order costing, percentages, slippage cost and
// preserves existing fields.
func (m *Movement) finalizeFields(cost, amount, headPrice, leftover float64, swap bool) (*Movement, error) {
if cost <= 0 {
return nil, errInvalidCost
}
if amount <= 0 {
return nil, errInvalidAmount
}
if headPrice <= 0 {
return nil, errInvalidHeadPrice
}
if m.StartPrice != m.EndPrice {
// Average order cost defines the actual cost price as capital is
// deployed through the orderbook liquidity.
m.AverageOrderCost = cost / amount
} else {
// Edge case rounding issue for float64 with small numbers.
m.AverageOrderCost = m.StartPrice
}
// Nominal percentage is the difference from the reference price to average
// order cost.
m.NominalPercentage = math.CalculatePercentageGainOrLoss(m.AverageOrderCost, m.StartPrice)
if m.NominalPercentage < 0 {
m.NominalPercentage *= -1
}
if !m.FullBookSideConsumed && leftover == 0 {
// Impact percentage is how much the orderbook slips from the reference
// price to the remaining tranche price.
m.ImpactPercentage = math.CalculatePercentageGainOrLoss(m.EndPrice, m.StartPrice)
if m.ImpactPercentage < 0 {
m.ImpactPercentage *= -1
}
} else {
// Full liquidity exhausted by request amount
m.ImpactPercentage = FullLiquidityExhaustedPercentage
m.FullBookSideConsumed = true
}
// Slippage cost is the difference in quotation terms between the actual
// cost and the amounts at head price e.g.
// Let P(n)=Price A(n)=Amount and iterate through a descending bid order example;
// Cost: $270 (P1:100 x A1:1 + P2:90 x A2:1 + P3:80 x A3:1)
// No slippage cost: $300 (P1:100 x A1:1 + P1:100 x A2:1 + P1:100 x A3:1)
// $300 - $270 = $30 of slippage.
m.SlippageCost = cost - (headPrice * amount)
if m.SlippageCost < 0 {
m.SlippageCost *= -1
}
// Swap saves on code duplication for difference in ask or bid amounts.
if swap {
m.Sold, m.Purchased = m.Purchased, m.Sold
}
return m, nil
}

View File

@@ -1,126 +0,0 @@
package orderbook
import (
"sync/atomic"
"time"
)
const (
neutral uint32 = iota
active
)
var (
defaultInterval = time.Minute
defaultAllowance = time.Second * 30
)
// Node defines a linked list node for an orderbook item
type Node struct {
Value Item
Next *Node
Prev *Node
// Denotes time pushed to stack, this will influence cleanup routine when
// there is a pause or minimal actions during period
shelved time.Time
}
// stack defines a FILO list of reusable nodes
type stack struct {
nodes []*Node
sema uint32
count int32
}
// newStack returns a ptr to a new stack instance, also starts the cleaning
// service
func newStack() *stack {
s := &stack{}
go s.cleaner()
return s
}
// Push pushes a node pointer into the stack to be reused the time is passed in
// to allow for inlining which sets the time at which the node is theoretically
// pushed to a stack.
func (s *stack) Push(n *Node, tn time.Time) {
if !atomic.CompareAndSwapUint32(&s.sema, neutral, active) {
// Stack is in use, for now we can dereference pointer
return
}
// Adds a time when its placed back on to stack.
n.shelved = tn
n.Next = nil
n.Prev = nil
n.Value = Item{}
// Allows for resize when overflow TODO: rethink this
s.nodes = append(s.nodes[:s.count], n)
s.count++
atomic.StoreUint32(&s.sema, neutral)
}
// Pop returns the last pointer off the stack and reduces the count and if empty
// will produce a lovely fresh node
func (s *stack) Pop() *Node {
if !atomic.CompareAndSwapUint32(&s.sema, neutral, active) {
// Stack is in use, for now we can allocate a new node pointer
return &Node{}
}
if s.count == 0 {
// Create an empty node when no nodes are in slice or when cleaning
// service is running
atomic.StoreUint32(&s.sema, neutral)
return &Node{}
}
s.count--
n := s.nodes[s.count]
atomic.StoreUint32(&s.sema, neutral)
return n
}
// cleaner (POC) runs to the defaultTimer to clean excess nodes (nodes not being
// utilised) TODO: Couple time parameters to check for a reduction in activity.
// Add in counter per second function (?) so if there is a lot of activity don't
// inhibit stack performance.
func (s *stack) cleaner() {
tt := time.NewTimer(defaultInterval)
sleeperino:
for range tt.C {
if !atomic.CompareAndSwapUint32(&s.sema, neutral, active) {
// Stack is in use, reset timer to zero to recheck for neutral state.
tt.Reset(0)
continue
}
// As the old nodes are going to be left justified on this slice we
// should just be able to shift the nodes that are still within time
// allowance all the way to the left. Not going to resize capacity
// because if it can get this big, it might as well stay this big.
// TODO: Test and rethink if sizing is an issue
for x := int32(0); x < s.count; x++ {
if time.Since(s.nodes[x].shelved) > defaultAllowance {
// Old node found continue
continue
}
// First good node found, everything to the left of this on the
// slice can be reassigned
var counter int32
for y := int32(0); y+x < s.count; y++ { // Go through good nodes
// Reassign
s.nodes[y] = s.nodes[y+x]
// Add to the changed counter to remove from main
// counter
counter++
}
s.count -= counter
atomic.StoreUint32(&s.sema, neutral)
tt.Reset(defaultInterval)
continue sleeperino
}
// Nodes are old, flush entirety.
s.count = 0
atomic.StoreUint32(&s.sema, neutral)
tt.Reset(defaultInterval)
}
}

View File

@@ -1,101 +0,0 @@
package orderbook
import (
"fmt"
"sync/atomic"
"testing"
"time"
)
func TestPushPop(t *testing.T) {
s := newStack()
var nSlice [100]*Node
for i := 0; i < 100; i++ {
nSlice[i] = s.Pop()
}
if s.getCount() != 0 {
t.Fatalf("incorrect stack count expected %v but received %v", 0, s.getCount())
}
for i := 0; i < 100; i++ {
s.Push(nSlice[i], time.Now())
}
if s.getCount() != 100 {
t.Fatalf("incorrect stack count expected %v but received %v", 100, s.getCount())
}
}
func TestCleaner(t *testing.T) {
s := newStack()
var nSlice [100]*Node
for i := 0; i < 100; i++ {
nSlice[i] = s.Pop()
}
tn := time.Now()
for i := 0; i < 50; i++ {
s.Push(nSlice[i], tn)
}
// Makes all the 50 pushed nodes invalid
time.Sleep(time.Millisecond * 260)
tn = time.Now()
for i := 50; i < 100; i++ {
s.Push(nSlice[i], tn)
}
time.Sleep(time.Millisecond * 50)
if s.getCount() != 50 {
t.Fatalf("incorrect stack count expected %v but received %v", 50, s.getCount())
}
time.Sleep(time.Millisecond * 350)
if s.getCount() != 0 {
t.Fatalf("incorrect stack count expected %v but received %v", 0, s.getCount())
}
}
// Display nodes for testing purposes
func (s *stack) Display() {
for i := int32(0); i < s.getCount(); i++ {
fmt.Printf("NODE IN STACK: %+v %p \n", s.nodes[i], s.nodes[i])
}
fmt.Println("TOTAL COUNT:", s.getCount())
}
// 158 9,521,717 ns/op 9600104 B/op 100001 allocs/op
func BenchmarkWithoutStack(b *testing.B) {
var n *Node
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < 100000; j++ {
n = new(Node)
n.Value.Price = 1337
}
}
}
// 316 3,485,211 ns/op 1 B/op 0 allocs/op
func BenchmarkWithStack(b *testing.B) {
var n *Node
stack := newStack()
b.ReportAllocs()
b.ResetTimer()
tn := time.Now()
for i := 0; i < b.N; i++ {
for j := 0; j < 100000; j++ {
n = stack.Pop()
n.Value.Price = 1337
stack.Push(n, tn)
}
}
}
// getCount is a test helper function to derive the count that does not race.
func (s *stack) getCount() int32 {
if !atomic.CompareAndSwapUint32(&s.sema, neutral, active) {
return -1
}
defer atomic.StoreUint32(&s.sema, neutral)
return s.count
}

View File

@@ -146,8 +146,8 @@ func (s *Service) GetDepth(exchange string, p currency.Pair, a asset.Item) (*Dep
return book, nil
}
// Retrieve gets orderbook depth data from the associated linked list and
// returns the base equivalent copy
// Retrieve gets orderbook depth data from the stored tranches and returns the
// base equivalent copy
func (s *Service) Retrieve(exchange string, p currency.Pair, a asset.Item) (*Base, error) {
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
@@ -238,10 +238,10 @@ func (b *Base) Verify() error {
// checker defines specific functionality to determine ascending/descending
// validation
type checker func(current Item, previous Item) error
type checker func(current Tranche, previous Tranche) error
// asc specifically defines ascending price check
var asc = func(current Item, previous Item) error {
var asc = func(current Tranche, previous Tranche) error {
if current.Price < previous.Price {
return errPriceOutOfOrder
}
@@ -249,7 +249,7 @@ var asc = func(current Item, previous Item) error {
}
// dsc specifically defines descending price check
var dsc = func(current Item, previous Item) error {
var dsc = func(current Tranche, previous Tranche) error {
if current.Price > previous.Price {
return errPriceOutOfOrder
}
@@ -257,7 +257,7 @@ var dsc = func(current Item, previous Item) error {
}
// checkAlignment validates full orderbook
func checkAlignment(depth Items, fundingRate, priceDuplication, isIDAligned, requiresChecksumString bool, c checker, exch string) error {
func checkAlignment(depth Tranches, fundingRate, priceDuplication, isIDAligned, requiresChecksumString bool, c checker, exch string) error {
for i := range depth {
if depth[i].Price == 0 {
switch {
@@ -327,25 +327,25 @@ func (b *Base) Process() error {
// using a sort algorithm as the algorithm could be impeded by a worst case time
// complexity when elements are shifted as opposed to just swapping element
// values.
func (elem *Items) Reverse() {
eLen := len(*elem)
func (ts *Tranches) Reverse() {
eLen := len(*ts)
var target int
for i := eLen/2 - 1; i >= 0; i-- {
target = eLen - 1 - i
(*elem)[i], (*elem)[target] = (*elem)[target], (*elem)[i]
(*ts)[i], (*ts)[target] = (*ts)[target], (*ts)[i]
}
}
// SortAsks sorts ask items to the correct ascending order if pricing values are
// scattered. If order from exchange is descending consider using the Reverse
// function.
func (elem *Items) SortAsks() {
sort.Sort(byOBPrice(*elem))
func (ts Tranches) SortAsks() {
sort.Slice(ts, func(i, j int) bool { return ts[i].Price < ts[j].Price })
}
// SortBids sorts bid items to the correct descending order if pricing values
// are scattered. If order from exchange is ascending consider using the Reverse
// function.
func (elem *Items) SortBids() {
sort.Sort(sort.Reverse(byOBPrice(*elem)))
func (ts Tranches) SortBids() {
sort.Slice(ts, func(i, j int) bool { return ts[i].Price > ts[j].Price })
}

View File

@@ -16,9 +16,6 @@ import (
)
func TestMain(m *testing.M) {
// Sets up lower values for test environment
defaultInterval = time.Millisecond * 250
defaultAllowance = time.Millisecond * 100
err := dispatch.Start(dispatch.DefaultMaxWorkers, dispatch.DefaultJobsLimit*10)
if err != nil {
log.Fatal(err)
@@ -38,7 +35,7 @@ func TestSubscribeToExchangeOrderbooks(t *testing.T) {
Pair: p,
Asset: asset.Spot,
Exchange: "SubscribeToExchangeOrderbooks",
Bids: []Item{{Price: 100, Amount: 1}, {Price: 99, Amount: 1}},
Bids: []Tranche{{Price: 100, Amount: 1}, {Price: 99, Amount: 1}},
}
err = b.Process()
@@ -66,19 +63,19 @@ func TestVerify(t *testing.T) {
t.Fatalf("expecting %v error but received %v", nil, err)
}
b.Asks = []Item{{ID: 1337, Price: 99, Amount: 1}, {ID: 1337, Price: 100, Amount: 1}}
b.Asks = []Tranche{{ID: 1337, Price: 99, Amount: 1}, {ID: 1337, Price: 100, Amount: 1}}
err = b.Verify()
if !errors.Is(err, errIDDuplication) {
t.Fatalf("expecting %s error but received %v", errIDDuplication, err)
}
b.Asks = []Item{{Price: 100, Amount: 1}, {Price: 100, Amount: 1}}
b.Asks = []Tranche{{Price: 100, Amount: 1}, {Price: 100, Amount: 1}}
err = b.Verify()
if !errors.Is(err, errDuplication) {
t.Fatalf("expecting %s error but received %v", errDuplication, err)
}
b.Asks = []Item{{Price: 100, Amount: 1}, {Price: 99, Amount: 1}}
b.Asks = []Tranche{{Price: 100, Amount: 1}, {Price: 99, Amount: 1}}
b.IsFundingRate = true
err = b.Verify()
if !errors.Is(err, errPeriodUnset) {
@@ -91,31 +88,31 @@ func TestVerify(t *testing.T) {
t.Fatalf("expecting %s error but received %v", errPriceOutOfOrder, err)
}
b.Asks = []Item{{Price: 100, Amount: 1}, {Price: 100, Amount: 0}}
b.Asks = []Tranche{{Price: 100, Amount: 1}, {Price: 100, Amount: 0}}
err = b.Verify()
if !errors.Is(err, errAmountInvalid) {
t.Fatalf("expecting %s error but received %v", errAmountInvalid, err)
}
b.Asks = []Item{{Price: 100, Amount: 1}, {Price: 0, Amount: 100}}
b.Asks = []Tranche{{Price: 100, Amount: 1}, {Price: 0, Amount: 100}}
err = b.Verify()
if !errors.Is(err, errPriceNotSet) {
t.Fatalf("expecting %s error but received %v", errPriceNotSet, err)
}
b.Bids = []Item{{ID: 1337, Price: 100, Amount: 1}, {ID: 1337, Price: 99, Amount: 1}}
b.Bids = []Tranche{{ID: 1337, Price: 100, Amount: 1}, {ID: 1337, Price: 99, Amount: 1}}
err = b.Verify()
if !errors.Is(err, errIDDuplication) {
t.Fatalf("expecting %s error but received %v", errIDDuplication, err)
}
b.Bids = []Item{{Price: 100, Amount: 1}, {Price: 100, Amount: 1}}
b.Bids = []Tranche{{Price: 100, Amount: 1}, {Price: 100, Amount: 1}}
err = b.Verify()
if !errors.Is(err, errDuplication) {
t.Fatalf("expecting %s error but received %v", errDuplication, err)
}
b.Bids = []Item{{Price: 99, Amount: 1}, {Price: 100, Amount: 1}}
b.Bids = []Tranche{{Price: 99, Amount: 1}, {Price: 100, Amount: 1}}
b.IsFundingRate = true
err = b.Verify()
if !errors.Is(err, errPeriodUnset) {
@@ -128,13 +125,13 @@ func TestVerify(t *testing.T) {
t.Fatalf("expecting %s error but received %v", errPriceOutOfOrder, err)
}
b.Bids = []Item{{Price: 100, Amount: 1}, {Price: 100, Amount: 0}}
b.Bids = []Tranche{{Price: 100, Amount: 1}, {Price: 100, Amount: 0}}
err = b.Verify()
if !errors.Is(err, errAmountInvalid) {
t.Fatalf("expecting %s error but received %v", errAmountInvalid, err)
}
b.Bids = []Item{{Price: 100, Amount: 1}, {Price: 0, Amount: 100}}
b.Bids = []Tranche{{Price: 100, Amount: 1}, {Price: 0, Amount: 100}}
err = b.Verify()
if !errors.Is(err, errPriceNotSet) {
t.Fatalf("expecting %s error but received %v", errPriceNotSet, err)
@@ -149,7 +146,7 @@ func TestCalculateTotalBids(t *testing.T) {
}
base := Base{
Pair: curr,
Bids: []Item{{Price: 100, Amount: 10}},
Bids: []Tranche{{Price: 100, Amount: 10}},
LastUpdated: time.Now(),
}
@@ -167,7 +164,7 @@ func TestCalculateTotalAsks(t *testing.T) {
}
base := Base{
Pair: curr,
Asks: []Item{{Price: 100, Amount: 10}},
Asks: []Tranche{{Price: 100, Amount: 10}},
}
a, b := base.TotalAsksAmount()
@@ -183,8 +180,8 @@ func TestGetOrderbook(t *testing.T) {
}
base := &Base{
Pair: c,
Asks: []Item{{Price: 100, Amount: 10}},
Bids: []Item{{Price: 200, Amount: 10}},
Asks: []Tranche{{Price: 100, Amount: 10}},
Bids: []Tranche{{Price: 200, Amount: 10}},
Exchange: "Exchange",
Asset: asset.Spot,
}
@@ -242,8 +239,8 @@ func TestGetDepth(t *testing.T) {
}
base := &Base{
Pair: c,
Asks: []Item{{Price: 100, Amount: 10}},
Bids: []Item{{Price: 200, Amount: 10}},
Asks: []Tranche{{Price: 100, Amount: 10}},
Bids: []Tranche{{Price: 200, Amount: 10}},
Exchange: "Exchange",
Asset: asset.Spot,
}
@@ -301,8 +298,8 @@ func TestBaseGetDepth(t *testing.T) {
}
base := &Base{
Pair: c,
Asks: []Item{{Price: 100, Amount: 10}},
Bids: []Item{{Price: 200, Amount: 10}},
Asks: []Tranche{{Price: 100, Amount: 10}},
Bids: []Tranche{{Price: 200, Amount: 10}},
Exchange: "Exchange",
Asset: asset.Spot,
}
@@ -355,8 +352,8 @@ func TestCreateNewOrderbook(t *testing.T) {
}
base := &Base{
Pair: c,
Asks: []Item{{Price: 100, Amount: 10}},
Bids: []Item{{Price: 200, Amount: 10}},
Asks: []Tranche{{Price: 100, Amount: 10}},
Bids: []Tranche{{Price: 200, Amount: 10}},
Exchange: "testCreateNewOrderbook",
Asset: asset.Spot,
}
@@ -392,8 +389,8 @@ func TestProcessOrderbook(t *testing.T) {
t.Fatal(err)
}
base := Base{
Asks: []Item{{Price: 100, Amount: 10}},
Bids: []Item{{Price: 200, Amount: 10}},
Asks: []Tranche{{Price: 100, Amount: 10}},
Bids: []Tranche{{Price: 200, Amount: 10}},
Exchange: "ProcessOrderbook",
}
@@ -461,7 +458,7 @@ func TestProcessOrderbook(t *testing.T) {
t.Fatal("TestProcessOrderbook result pair is incorrect")
}
base.Asks = []Item{{Price: 200, Amount: 200}}
base.Asks = []Tranche{{Price: 200, Amount: 200}}
base.Asset = asset.Spot
err = base.Process()
if err != nil {
@@ -478,7 +475,7 @@ func TestProcessOrderbook(t *testing.T) {
t.Fatal("TestProcessOrderbook CalculateTotalsAsks incorrect values")
}
base.Bids = []Item{{Price: 420, Amount: 200}}
base.Bids = []Tranche{{Price: 420, Amount: 200}}
base.Exchange = "Blah"
base.Asset = asset.CoinMarginedFutures
err = base.Process()
@@ -498,8 +495,8 @@ func TestProcessOrderbook(t *testing.T) {
type quick struct {
Name string
P currency.Pair
Bids []Item
Asks []Item
Bids []Tranche
Asks []Tranche
}
var testArray []quick
@@ -524,8 +521,8 @@ func TestProcessOrderbook(t *testing.T) {
newPairs := currency.NewPair(currency.NewCode("BTC"+strconv.FormatInt(rand.Int63(), 10)),
currency.NewCode("USD"+strconv.FormatInt(rand.Int63(), 10))) //nolint:gosec // no need to import crypo/rand for testing
asks := []Item{{Price: rand.Float64(), Amount: rand.Float64()}} //nolint:gosec // no need to import crypo/rand for testing
bids := []Item{{Price: rand.Float64(), Amount: rand.Float64()}} //nolint:gosec // no need to import crypo/rand for testing
asks := []Tranche{{Price: rand.Float64(), Amount: rand.Float64()}} //nolint:gosec // no need to import crypo/rand for testing
bids := []Tranche{{Price: rand.Float64(), Amount: rand.Float64()}} //nolint:gosec // no need to import crypo/rand for testing
base := &Base{
Pair: newPairs,
Asks: asks,
@@ -582,12 +579,12 @@ func TestProcessOrderbook(t *testing.T) {
wg.Wait()
}
func deployUnorderedSlice() Items {
var items []Item
func deployUnorderedSlice() Tranches {
var ts []Tranche
for i := 0; i < 1000; i++ {
items = append(items, Item{Amount: 1, Price: rand.Float64(), ID: rand.Int63()}) //nolint:gosec // Not needed in tests
ts = append(ts, Tranche{Amount: 1, Price: rand.Float64(), ID: rand.Int63()}) //nolint:gosec // Not needed in tests
}
return items
return ts
}
func TestSorting(t *testing.T) {
@@ -619,12 +616,12 @@ func TestSorting(t *testing.T) {
}
}
func deploySliceOrdered() Items {
var items []Item
func deploySliceOrdered() Tranches {
var ts []Tranche
for i := 0; i < 1000; i++ {
items = append(items, Item{Amount: 1, Price: float64(i + 1), ID: rand.Int63()}) //nolint:gosec // Not needed in tests
ts = append(ts, Tranche{Amount: 1, Price: float64(i + 1), ID: rand.Int63()}) //nolint:gosec // Not needed in tests
}
return items
return ts
}
func TestReverse(t *testing.T) {
@@ -671,94 +668,77 @@ func BenchmarkReverse(b *testing.B) {
}
}
// 20209 56385 ns/op 49189 B/op 2 allocs/op
// 361266 3556 ns/op 24 B/op 1 allocs/op (old)
// 385783 3000 ns/op 152 B/op 3 allocs/op (new)
func BenchmarkSortAsksDecending(b *testing.B) {
s := deploySliceOrdered()
bucket := make(Tranches, len(s))
for i := 0; i < b.N; i++ {
//nolint: gocritic
ts := append(s[:0:0], s...)
ts.SortAsks()
copy(bucket, s)
bucket.SortAsks()
}
}
// 14924 79199 ns/op 49206 B/op 3 allocs/op
// 266998 4292 ns/op 40 B/op 2 allocs/op (old)
// 372396 3001 ns/op 152 B/op 3 allocs/op (new)
func BenchmarkSortBidsAscending(b *testing.B) {
s := deploySliceOrdered()
s.Reverse()
bucket := make(Tranches, len(s))
for i := 0; i < b.N; i++ {
//nolint: gocritic
ts := append(s[:0:0], s...)
ts.SortBids()
copy(bucket, s)
bucket.SortBids()
}
}
// 9842 133761 ns/op 49194 B/op 2 allocs/op
// 22119 46532 ns/op 35 B/op 1 allocs/op (old)
// 16233 76951 ns/op 167 B/op 3 allocs/op (new)
func BenchmarkSortAsksStandard(b *testing.B) {
s := deployUnorderedSlice()
bucket := make(Tranches, len(s))
for i := 0; i < b.N; i++ {
//nolint: gocritic
ts := append(s[:0:0], s...)
ts.SortAsks()
copy(bucket, s)
bucket.SortAsks()
}
}
// 7058 155057 ns/op 49214 B/op 3 allocs/op
// 19504 62518 ns/op 53 B/op 2 allocs/op (old)
// 15698 72859 ns/op 168 B/op 3 allocs/op (new)
func BenchmarkSortBidsStandard(b *testing.B) {
s := deployUnorderedSlice()
bucket := make(Tranches, len(s))
for i := 0; i < b.N; i++ {
//nolint: gocritic
ts := append(s[:0:0], s...)
ts.SortBids()
copy(bucket, s)
bucket.SortBids()
}
}
// 20565 57001 ns/op 49188 B/op 2 allocs/op
// 376708 3559 ns/op 24 B/op 1 allocs/op (old)
// 377113 3020 ns/op 152 B/op 3 allocs/op (new)
func BenchmarkSortAsksAscending(b *testing.B) {
s := deploySliceOrdered()
bucket := make(Tranches, len(s))
for i := 0; i < b.N; i++ {
//nolint: gocritic
ts := append(s[:0:0], s...)
ts.SortAsks()
copy(bucket, s)
bucket.SortAsks()
}
}
// 12565 97257 ns/op 49208 B/op 3 allocs/op
// 262874 4364 ns/op 40 B/op 2 allocs/op (old)
// 401788 3348 ns/op 152 B/op 3 allocs/op (new)
func BenchmarkSortBidsDescending(b *testing.B) {
s := deploySliceOrdered()
s.Reverse()
bucket := make(Tranches, len(s))
for i := 0; i < b.N; i++ {
//nolint: gocritic
ts := append(s[:0:0], s...)
ts.SortBids()
}
}
// 124867 8480 ns/op 49152 B/op 1 allocs/op
func BenchmarkDuplicatingSlice(b *testing.B) {
s := deploySliceOrdered()
for i := 0; i < b.N; i++ {
_ = append(s[:0:0], s...)
}
}
// 122998 8441 ns/op 49152 B/op 1 allocs/op
func BenchmarkCopySlice(b *testing.B) {
s := deploySliceOrdered()
for i := 0; i < b.N; i++ {
cpy := make([]Item, len(s))
copy(cpy, s)
copy(bucket, s)
bucket.SortBids()
}
}
func TestCheckAlignment(t *testing.T) {
t.Parallel()
itemWithFunding := Items{
{
Amount: 1337,
Price: 0,
Period: 1337,
},
}
itemWithFunding := Tranches{{Amount: 1337, Price: 0, Period: 1337}}
err := checkAlignment(itemWithFunding, true, true, false, false, dsc, "Bitfinex")
if err != nil {
t.Error(err)

View File

@@ -55,8 +55,8 @@ type Exchange struct {
ID uuid.UUID
}
// Item stores the amount and price values
type Item struct {
// Tranche defines a segmented portions of an order or options book
type Tranche struct {
Amount float64
// StrAmount is a string representation of the amount. e.g. 0.00000100 this
// parsed as a float will constrict comparison to 1e-6 not 1e-8 or
@@ -77,13 +77,10 @@ type Item struct {
OrderCount int64
}
// Items defines a slice of orderbook items
type Items []Item
// Base holds the fields for the orderbook base
type Base struct {
Bids Items
Asks Items
Bids Tranches
Asks Tranches
Exchange string
Pair currency.Pair
@@ -115,12 +112,6 @@ type Base struct {
ChecksumStringRequired bool
}
type byOBPrice []Item
func (a byOBPrice) Len() int { return len(a) }
func (a byOBPrice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byOBPrice) Less(i, j int) bool { return a[i].Price < a[j].Price }
type options struct {
exchange string
pair currency.Pair
@@ -158,8 +149,8 @@ type Update struct {
UpdateTime time.Time
Asset asset.Item
Action
Bids []Item
Asks []Item
Bids []Tranche
Asks []Tranche
Pair currency.Pair
// Checksum defines the expected value when the books have been verified
Checksum uint32

View File

@@ -0,0 +1,664 @@
package orderbook
import (
"errors"
"fmt"
"github.com/thrasher-corp/gocryptotrader/common/math"
)
// FullLiquidityExhaustedPercentage defines when a book has been completely
// wiped out of potential liquidity.
const FullLiquidityExhaustedPercentage = -100
var (
errIDCannotBeMatched = errors.New("cannot match ID")
errCollisionDetected = errors.New("cannot insert update, collision detected")
errAmountCannotBeLessOrEqualToZero = errors.New("amount cannot be less than or equal to zero")
errInvalidNominalSlippage = errors.New("invalid slippage amount, its value must be greater than or equal to zero")
errInvalidImpactSlippage = errors.New("invalid slippage amount, its value must be greater than zero")
errInvalidSlippageCannotExceed100 = errors.New("invalid slippage amount, its value cannot exceed 100%")
errBaseAmountInvalid = errors.New("invalid base amount")
errInvalidReferencePrice = errors.New("invalid reference price")
errQuoteAmountInvalid = errors.New("quote amount invalid")
errInvalidCost = errors.New("invalid cost amount")
errInvalidAmount = errors.New("invalid amount")
errInvalidHeadPrice = errors.New("invalid head price")
errNoLiquidity = errors.New("no liquidity")
)
// Tranches defines a slice of orderbook Tranche
type Tranches []Tranche
// comparison defines expected functionality to compare between two reference
// price levels
type comparison func(float64, float64) bool
// load iterates across new tranches and refreshes stored slice with this
// incoming snapshot.
func (ts *Tranches) load(incoming Tranches) {
if len(incoming) == 0 {
*ts = (*ts)[:0] // Flush
return
}
if len(incoming) <= len(*ts) {
copy(*ts, incoming) // Reuse
*ts = (*ts)[:len(incoming)] // Flush excess
return
}
*ts = make([]Tranche, len(incoming)) // Extend
copy(*ts, incoming) // Copy
}
// updateByID amends price by corresponding ID and returns an error if not found
func (ts Tranches) updateByID(updts []Tranche) error {
updates:
for x := range updts {
for y := range ts {
if updts[x].ID != ts[y].ID { // Filter IDs that don't match
continue
}
if updts[x].Price > 0 {
// Only apply changes when zero values are not present, Bitmex
// for example sends 0 price values.
ts[y].Price = updts[x].Price
ts[y].StrPrice = updts[x].StrPrice
}
ts[y].Amount = updts[x].Amount
ts[y].StrAmount = updts[x].StrAmount
continue updates
}
return fmt.Errorf("update error: %w ID: %d not found",
errIDCannotBeMatched,
updts[x].ID)
}
return nil
}
// deleteByID deletes reference by ID
func (ts *Tranches) deleteByID(updts Tranches, bypassErr bool) error {
updates:
for x := range updts {
for y := range *ts {
if updts[x].ID != (*ts)[y].ID {
continue
}
if y < len(*ts) {
copy((*ts)[y:], (*ts)[y+1:])
*ts = (*ts)[:len(*ts)-1]
} else {
*ts = append((*ts)[:y], (*ts)[y+1:]...)
}
continue updates
}
if !bypassErr {
return fmt.Errorf("delete error: %w %d not found",
errIDCannotBeMatched,
updts[x].ID)
}
}
return nil
}
// amount returns total depth liquidity and value
func (ts Tranches) amount() (liquidity, value float64) {
for x := range ts {
liquidity += ts[x].Amount
value += ts[x].Amount * ts[x].Price
}
return
}
// retrieve returns a slice of contents from the stored Tranches up to the
// count length. If count is zero or greater than the length of the stored
// Tranches, the entire slice is returned.
func (ts Tranches) retrieve(count int) Tranches {
if count == 0 || count >= len(ts) {
count = len(ts)
}
return append(Tranches{}, ts[:count]...)
}
// updateInsertByPrice amends, inserts, moves and cleaves length of depth by
// updates
func (ts *Tranches) updateInsertByPrice(updts Tranches, maxChainLength int, compare func(float64, float64) bool) {
updates:
for x := range updts {
for y := range *ts {
switch {
case (*ts)[y].Price == updts[x].Price:
if updts[x].Amount <= 0 {
// Delete
if y+1 == len(*ts) {
*ts = (*ts)[:y]
} else {
copy((*ts)[y:], (*ts)[y+1:])
*ts = (*ts)[:len(*ts)-1]
}
} else {
// Update
(*ts)[y].Amount = updts[x].Amount
(*ts)[y].StrAmount = updts[x].StrAmount
}
continue updates
case compare((*ts)[y].Price, updts[x].Price):
if updts[x].Amount > 0 {
*ts = append(*ts, Tranche{}) // Extend
copy((*ts)[y+1:], (*ts)[y:]) // Copy elements from index y onwards one position to the right
(*ts)[y] = updts[x] // Insert updts[x] at index y
}
continue updates
}
}
if updts[x].Amount > 0 {
*ts = append(*ts, updts[x])
}
}
// Reduces length of total stored slice length to a maxChainLength value
if maxChainLength != 0 && len(*ts) > maxChainLength {
*ts = (*ts)[:maxChainLength]
}
}
// updateInsertByID updates or inserts if not found for a bid or ask depth
func (ts *Tranches) updateInsertByID(updts Tranches, compare comparison) error {
updates:
for x := range updts {
if updts[x].Amount <= 0 {
return errAmountCannotBeLessOrEqualToZero
}
var popped bool
for y := 0; y < len(*ts); y++ {
if (*ts)[y].ID == updts[x].ID {
if (*ts)[y].Price != updts[x].Price { // Price level change
if y+1 == len(*ts) { // end of depth
// no movement needed just a re-adjustment
(*ts)[y] = updts[x]
continue updates
}
copy((*ts)[y:], (*ts)[y+1:]) // RM tranche and shift left
*ts = (*ts)[:len(*ts)-1] // Unlink residual element from end of slice
y-- // adjust index
popped = true
continue // continue through node depth
}
// no price change, amend amount and continue update
(*ts)[y].Amount = updts[x].Amount
(*ts)[y].StrAmount = updts[x].StrAmount
continue updates // continue to next update
}
if compare((*ts)[y].Price, updts[x].Price) {
*ts = append(*ts, Tranche{}) // Extend
copy((*ts)[y+1:], (*ts)[y:]) // Copy elements from index y onwards one position to the right
(*ts)[y] = updts[x] // Insert updts[x] at index y
if popped { // already found ID and popped
continue updates
}
// search for ID
for z := y + 1; z < len(*ts); z++ {
if (*ts)[z].ID == updts[x].ID {
copy((*ts)[z:], (*ts)[z+1:]) // RM tranche and shift left
*ts = (*ts)[:len(*ts)-1] // Unlink residual element from end of slice
break
}
}
continue updates
}
}
*ts = append(*ts, updts[x])
}
return nil
}
// insertUpdates inserts new updates for bids or asks based on price level
func (ts *Tranches) insertUpdates(updts Tranches, comp comparison) error {
updates:
for x := range updts {
if len(*ts) == 0 {
*ts = append(*ts, updts[x])
continue
}
for y := range *ts {
switch {
case (*ts)[y].Price == updts[x].Price: // Price already found
return fmt.Errorf("%w for price %f", errCollisionDetected, updts[x].Price)
case comp((*ts)[y].Price, updts[x].Price): // price at correct spot
*ts = append((*ts)[:y], append([]Tranche{updts[x]}, (*ts)[y:]...)...)
continue updates
}
}
*ts = append(*ts, updts[x])
}
return nil
}
// getHeadPriceNoLock gets best/head price
func (ts Tranches) getHeadPriceNoLock() (float64, error) {
if len(ts) == 0 {
return 0, errNoLiquidity
}
return ts[0].Price, nil
}
// getHeadVolumeNoLock gets best/head volume
func (ts Tranches) getHeadVolumeNoLock() (float64, error) {
if len(ts) == 0 {
return 0, errNoLiquidity
}
return ts[0].Amount, nil
}
// getMovementByQuotation traverses through orderbook liquidity using quotation
// currency as a limiter and returns orderbook movement details. Swap boolean
// allows the swap of sold and purchased to reduce code so it doesn't need to be
// specific to bid or ask.
func (ts Tranches) getMovementByQuotation(quote, refPrice float64, swap bool) (*Movement, error) {
if quote <= 0 {
return nil, errQuoteAmountInvalid
}
if refPrice <= 0 {
return nil, errInvalidReferencePrice
}
head, err := ts.getHeadPriceNoLock()
if err != nil {
return nil, err
}
m := Movement{StartPrice: refPrice}
for x := range ts {
trancheValue := ts[x].Amount * ts[x].Price
leftover := quote - trancheValue
if leftover < 0 {
m.Purchased += quote
m.Sold += quote / trancheValue * ts[x].Amount
// This tranche is not consumed so the book shifts to this price.
m.EndPrice = ts[x].Price
quote = 0
break
}
// Full tranche consumed
m.Purchased += ts[x].Price * ts[x].Amount
m.Sold += ts[x].Amount
quote = leftover
if leftover == 0 {
// Price no longer exists on the book so use next full price tranche
// to calculate book impact. If available.
if x+1 < len(ts) {
m.EndPrice = ts[x+1].Price
} else {
m.FullBookSideConsumed = true
}
break
}
}
return m.finalizeFields(m.Purchased, m.Sold, head, quote, swap)
}
// getMovementByBase traverses through orderbook liquidity using base currency
// as a limiter and returns orderbook movement details. Swap boolean allows the
// swap of sold and purchased to reduce code so it doesn't need to be specific
// to bid or ask.
func (ts Tranches) getMovementByBase(base, refPrice float64, swap bool) (*Movement, error) {
if base <= 0 {
return nil, errBaseAmountInvalid
}
if refPrice <= 0 {
return nil, errInvalidReferencePrice
}
head, err := ts.getHeadPriceNoLock()
if err != nil {
return nil, err
}
m := Movement{StartPrice: refPrice}
for x := range ts {
leftover := base - ts[x].Amount
if leftover < 0 {
m.Purchased += ts[x].Price * base
m.Sold += base
// This tranche is not consumed so the book shifts to this price.
m.EndPrice = ts[x].Price
base = 0
break
}
// Full tranche consumed
m.Purchased += ts[x].Price * ts[x].Amount
m.Sold += ts[x].Amount
base = leftover
if leftover == 0 {
// Price no longer exists on the book so use next full price tranche
// to calculate book impact.
if x+1 < len(ts) {
m.EndPrice = ts[x+1].Price
} else {
m.FullBookSideConsumed = true
}
break
}
}
return m.finalizeFields(m.Purchased, m.Sold, head, base, swap)
}
// bidTranches bid depth specific functionality
type bidTranches struct{ Tranches }
// bidCompare ensures price is in correct descending alignment (can inline)
func bidCompare(left, right float64) bool {
return left < right
}
// updateInsertByPrice amends, inserts, moves and cleaves length of depth by
// updates
func (bids *bidTranches) updateInsertByPrice(updts Tranches, maxChainLength int) {
bids.Tranches.updateInsertByPrice(updts, maxChainLength, bidCompare)
}
// updateInsertByID updates or inserts if not found
func (bids *bidTranches) updateInsertByID(updts Tranches) error {
return bids.Tranches.updateInsertByID(updts, bidCompare)
}
// insertUpdates inserts new updates for bids based on price level
func (bids *bidTranches) insertUpdates(updts Tranches) error {
return bids.Tranches.insertUpdates(updts, bidCompare)
}
// hitBidsByNominalSlippage hits the bids by the required nominal slippage
// percentage, calculated from the reference price and returns orderbook
// movement details.
func (bids *bidTranches) hitBidsByNominalSlippage(slippage, refPrice float64) (*Movement, error) {
if slippage < 0 {
return nil, errInvalidNominalSlippage
}
if slippage > 100 {
return nil, errInvalidSlippageCannotExceed100
}
if refPrice <= 0 {
return nil, errInvalidReferencePrice
}
if len(bids.Tranches) == 0 {
return nil, errNoLiquidity
}
nominal := &Movement{StartPrice: refPrice, EndPrice: refPrice}
var cumulativeValue, cumulativeAmounts float64
for x := range bids.Tranches {
totalTrancheValue := bids.Tranches[x].Price * bids.Tranches[x].Amount
currentFullValue := totalTrancheValue + cumulativeValue
currentTotalAmounts := cumulativeAmounts + bids.Tranches[x].Amount
nominal.AverageOrderCost = currentFullValue / currentTotalAmounts
percent := math.CalculatePercentageGainOrLoss(nominal.AverageOrderCost, refPrice)
if percent != 0 {
percent *= -1
}
if slippage < percent {
targetCost := (1 - slippage/100) * refPrice
if targetCost == refPrice {
nominal.AverageOrderCost = cumulativeValue / cumulativeAmounts
// Rounding issue on requested nominal percentage
return nominal, nil
}
comparative := targetCost * cumulativeAmounts
comparativeDiff := comparative - cumulativeValue
trancheTargetPriceDiff := bids.Tranches[x].Price - targetCost
trancheAmountExpectation := comparativeDiff / trancheTargetPriceDiff
nominal.NominalPercentage = slippage
nominal.Sold = cumulativeAmounts + trancheAmountExpectation
nominal.Purchased += trancheAmountExpectation * bids.Tranches[x].Price
nominal.AverageOrderCost = nominal.Purchased / nominal.Sold
nominal.EndPrice = bids.Tranches[x].Price
return nominal, nil
}
nominal.EndPrice = bids.Tranches[x].Price
cumulativeValue = currentFullValue
nominal.NominalPercentage = percent
nominal.Sold += bids.Tranches[x].Amount
nominal.Purchased += totalTrancheValue
cumulativeAmounts = currentTotalAmounts
if slippage == percent {
nominal.FullBookSideConsumed = x+1 >= len(bids.Tranches)
return nominal, nil
}
}
nominal.FullBookSideConsumed = true
return nominal, nil
}
// hitBidsByImpactSlippage hits the bids by the required impact slippage
// percentage, calculated from the reference price and returns orderbook
// movement details.
func (bids *bidTranches) hitBidsByImpactSlippage(slippage, refPrice float64) (*Movement, error) {
if slippage <= 0 {
return nil, errInvalidImpactSlippage
}
if slippage > 100 {
return nil, errInvalidSlippageCannotExceed100
}
if refPrice <= 0 {
return nil, errInvalidReferencePrice
}
if len(bids.Tranches) == 0 {
return nil, errNoLiquidity
}
impact := &Movement{StartPrice: refPrice, EndPrice: refPrice}
for x := range bids.Tranches {
percent := math.CalculatePercentageGainOrLoss(bids.Tranches[x].Price, refPrice)
if percent != 0 {
percent *= -1
}
impact.EndPrice = bids.Tranches[x].Price
impact.ImpactPercentage = percent
if slippage <= percent {
// Don't include this tranche amount as this consumes the tranche
// book price, thus obtaining a higher percentage impact.
return impact, nil
}
impact.Sold += bids.Tranches[x].Amount
impact.Purchased += bids.Tranches[x].Amount * bids.Tranches[x].Price
impact.AverageOrderCost = impact.Purchased / impact.Sold
}
impact.FullBookSideConsumed = true
impact.ImpactPercentage = FullLiquidityExhaustedPercentage
return impact, nil
}
// askTranches ask depth specific functionality
type askTranches struct{ Tranches }
// askCompare ensures price is in correct ascending alignment (can inline)
func askCompare(left, right float64) bool {
return left > right
}
// updateInsertByPrice amends, inserts, moves and cleaves length of depth by
// updates
func (ask *askTranches) updateInsertByPrice(updts Tranches, maxChainLength int) {
ask.Tranches.updateInsertByPrice(updts, maxChainLength, askCompare)
}
// updateInsertByID updates or inserts if not found
func (ask *askTranches) updateInsertByID(updts Tranches) error {
return ask.Tranches.updateInsertByID(updts, askCompare)
}
// insertUpdates inserts new updates for asks based on price level
func (ask *askTranches) insertUpdates(updts Tranches) error {
return ask.Tranches.insertUpdates(updts, askCompare)
}
// liftAsksByNominalSlippage lifts the asks by the required nominal slippage
// percentage, calculated from the reference price and returns orderbook
// movement details.
func (ask *askTranches) liftAsksByNominalSlippage(slippage, refPrice float64) (*Movement, error) {
if slippage < 0 {
return nil, errInvalidNominalSlippage
}
if refPrice <= 0 {
return nil, errInvalidReferencePrice
}
if len(ask.Tranches) == 0 {
return nil, errNoLiquidity
}
nominal := &Movement{StartPrice: refPrice, EndPrice: refPrice}
var cumulativeAmounts float64
for x := range ask.Tranches {
totalTrancheValue := ask.Tranches[x].Price * ask.Tranches[x].Amount
currentValue := totalTrancheValue + nominal.Sold
currentAmounts := cumulativeAmounts + ask.Tranches[x].Amount
nominal.AverageOrderCost = currentValue / currentAmounts
percent := math.CalculatePercentageGainOrLoss(nominal.AverageOrderCost, refPrice)
if slippage < percent {
targetCost := (1 + slippage/100) * refPrice
if targetCost == refPrice {
nominal.AverageOrderCost = nominal.Sold / nominal.Purchased
// Rounding issue on requested nominal percentage
return nominal, nil
}
comparative := targetCost * cumulativeAmounts
comparativeDiff := comparative - nominal.Sold
trancheTargetPriceDiff := ask.Tranches[x].Price - targetCost
trancheAmountExpectation := comparativeDiff / trancheTargetPriceDiff
nominal.NominalPercentage = slippage
nominal.Sold += trancheAmountExpectation * ask.Tranches[x].Price
nominal.Purchased += trancheAmountExpectation
nominal.AverageOrderCost = nominal.Sold / nominal.Purchased
nominal.EndPrice = ask.Tranches[x].Price
return nominal, nil
}
nominal.EndPrice = ask.Tranches[x].Price
nominal.Sold = currentValue
nominal.Purchased += ask.Tranches[x].Amount
nominal.NominalPercentage = percent
if slippage == percent {
return nominal, nil
}
cumulativeAmounts = currentAmounts
}
nominal.FullBookSideConsumed = true
return nominal, nil
}
// liftAsksByImpactSlippage lifts the asks by the required impact slippage
// percentage, calculated from the reference price and returns orderbook
// movement details.
func (ask *askTranches) liftAsksByImpactSlippage(slippage, refPrice float64) (*Movement, error) {
if slippage <= 0 {
return nil, errInvalidImpactSlippage
}
if refPrice <= 0 {
return nil, errInvalidReferencePrice
}
if len(ask.Tranches) == 0 {
return nil, errNoLiquidity
}
impact := &Movement{StartPrice: refPrice, EndPrice: refPrice}
for x := range ask.Tranches {
percent := math.CalculatePercentageGainOrLoss(ask.Tranches[x].Price, refPrice)
impact.ImpactPercentage = percent
impact.EndPrice = ask.Tranches[x].Price
if slippage <= percent {
// Don't include this tranche amount as this consumes the tranche
// book price, thus obtaining a higher percentage impact.
return impact, nil
}
impact.Sold += ask.Tranches[x].Amount * ask.Tranches[x].Price
impact.Purchased += ask.Tranches[x].Amount
impact.AverageOrderCost = impact.Sold / impact.Purchased
}
impact.FullBookSideConsumed = true
impact.ImpactPercentage = FullLiquidityExhaustedPercentage
return impact, nil
}
// finalizeFields sets average order costing, percentages, slippage cost and
// preserves existing fields.
func (m *Movement) finalizeFields(cost, amount, headPrice, leftover float64, swap bool) (*Movement, error) {
if cost <= 0 {
return nil, errInvalidCost
}
if amount <= 0 {
return nil, errInvalidAmount
}
if headPrice <= 0 {
return nil, errInvalidHeadPrice
}
if m.StartPrice != m.EndPrice {
// Average order cost defines the actual cost price as capital is
// deployed through the orderbook liquidity.
m.AverageOrderCost = cost / amount
} else {
// Edge case rounding issue for float64 with small numbers.
m.AverageOrderCost = m.StartPrice
}
// Nominal percentage is the difference from the reference price to average
// order cost.
m.NominalPercentage = math.CalculatePercentageGainOrLoss(m.AverageOrderCost, m.StartPrice)
if m.NominalPercentage < 0 {
m.NominalPercentage *= -1
}
if !m.FullBookSideConsumed && leftover == 0 {
// Impact percentage is how much the orderbook slips from the reference
// price to the remaining tranche price.
m.ImpactPercentage = math.CalculatePercentageGainOrLoss(m.EndPrice, m.StartPrice)
if m.ImpactPercentage < 0 {
m.ImpactPercentage *= -1
}
} else {
// Full liquidity exhausted by request amount
m.ImpactPercentage = FullLiquidityExhaustedPercentage
m.FullBookSideConsumed = true
}
// Slippage cost is the difference in quotation terms between the actual
// cost and the amounts at head price e.g.
// Let P(n)=Price A(n)=Amount and iterate through a descending bid order example;
// Cost: $270 (P1:100 x A1:1 + P2:90 x A2:1 + P3:80 x A3:1)
// No slippage cost: $300 (P1:100 x A1:1 + P1:100 x A2:1 + P1:100 x A3:1)
// $300 - $270 = $30 of slippage.
m.SlippageCost = cost - (headPrice * amount)
if m.SlippageCost < 0 {
m.SlippageCost *= -1
}
// Swap saves on code duplication for difference in ask or bid amounts.
if swap {
m.Sold, m.Purchased = m.Purchased, m.Sold
}
return m, nil
}

View File

@@ -1,168 +0,0 @@
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
// fields point to the actual head fields contained on both linked list structs,
// so that this struct can be reusable and not needed to be called on each
// inspection.
type Unsafe struct {
BidHead **Node
AskHead **Node
m *sync.Mutex
// UpdatedViaREST defines if sync manager is updating this book via the REST
// protocol then this book is not considered live and cannot be trusted.
UpdatedViaREST *bool
LastUpdated *time.Time
*alert.Notice
}
// Lock locks down the underlying linked list which inhibits all pending updates
// for strategy inspection.
func (src *Unsafe) Lock() {
src.m.Lock()
}
// Unlock unlocks the underlying linked list after inspection by a strategy to
// resume normal operations
func (src *Unsafe) Unlock() {
src.m.Unlock()
}
// LockWith locks both books for the context of cross orderbook inspection.
// WARNING: When inspecting diametrically opposed books a higher order mutex
// MUST be used or a dead lock will occur.
func (src *Unsafe) LockWith(dst sync.Locker) {
src.m.Lock()
dst.Lock()
}
// UnlockWith unlocks both books for the context of cross orderbook inspection
func (src *Unsafe) UnlockWith(dst sync.Locker) {
dst.Unlock() // Unlock in reverse order
src.m.Unlock()
}
// GetUnsafe returns an unsafe orderbook with pointers to the linked list heads.
func (d *Depth) GetUnsafe() *Unsafe {
return &Unsafe{
BidHead: &d.bids.linkedList.head,
AskHead: &d.asks.linkedList.head,
m: &d.m,
Notice: &d.Notice,
UpdatedViaREST: &d.options.restSnapshot,
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,279 +0,0 @@
package orderbook
import (
"errors"
"testing"
"time"
"github.com/gofrs/uuid"
)
var unsafeID, _ = uuid.NewV4()
type externalBook struct{}
func (e *externalBook) Lock() {}
func (e *externalBook) Unlock() {}
func TestUnsafe(t *testing.T) {
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")
}
ob2 := &externalBook{}
ob.Lock()
ob.Unlock() //nolint:staticcheck, gocritic // Not needed in test
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)
}
err = d.LoadSnapshot([]Item{{Price: 2}}, nil, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
_, _, err = unsafe.GetLiquidity()
if !errors.Is(err, errNoLiquidity) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
}
err = d.LoadSnapshot([]Item{{Price: 2}}, []Item{{Price: 2}}, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
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)
}
err = d.LoadSnapshot([]Item{{Price: 2}}, nil, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
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)
}
err = d.LoadSnapshot(nil, []Item{{Price: 2}}, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
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)
}
err := d.LoadSnapshot([]Item{{Price: 2}}, nil, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
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)
}
err := d.LoadSnapshot(nil, []Item{{Price: 2}}, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
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)
}
err := d.LoadSnapshot([]Item{{Price: 1}}, []Item{{Price: 2}}, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
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)
}
err := d.LoadSnapshot([]Item{{Price: 1}}, []Item{{Price: 2}}, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
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
err = d.LoadSnapshot([]Item{{Price: 1, Amount: 0}}, []Item{{Price: 2, Amount: 0}}, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
_, err = unsafe.GetImbalance()
if !errors.Is(err, errNoLiquidity) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
}
// balance skewed to asks
err = d.LoadSnapshot([]Item{{Price: 1, Amount: 1}}, []Item{{Price: 2, Amount: 1000}}, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
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
err = d.LoadSnapshot([]Item{{Price: 1, Amount: 1000}}, []Item{{Price: 2, Amount: 1}}, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
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
err = d.LoadSnapshot([]Item{{Price: 1, Amount: 1}}, []Item{{Price: 2, Amount: 1}}, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
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)
}
err := d.LoadSnapshot([]Item{{Price: 1, Amount: 1}}, []Item{{Price: 2, Amount: 1}}, 0, time.Now(), true)
if err != nil {
t.Fatal(err)
}
if unsafe.IsStreaming() {
t.Fatalf("received: '%v' but expected: '%v'", unsafe.IsStreaming(), false)
}
err = d.LoadSnapshot([]Item{{Price: 1, Amount: 1}}, []Item{{Price: 2, Amount: 1}}, 0, time.Now(), false)
if err != nil {
t.Fatal(err)
}
if !unsafe.IsStreaming() {
t.Fatalf("received: '%v' but expected: '%v'", unsafe.IsStreaming(), true)
}
}