Files
gocryptotrader/exchanges/orderbook/depth.go
Scott 85403fe801 exchange/order/limits: Migrate to new package and integrate with exchanges (#1860)
* move limits, transition to key gen

* rollout NewExchangePairAssetKey everywhere

* test improvements

* self-review fixes

* ok, lets go

* fix merge issue

* slower value func,assertify,drop IsValidPairString

* remove binance reference for backtesting test

* Redundant nil checks removed due to redundancy

* Update order_test.go

* Move limits back into /exchanges/

* puts limits in a different box again

* SHAZBERT SPECIAL SUGGESTIONS

* Update gateio_wrapper.go

* fixes all build issues

* Many niteroos!

* something has gone awry

* bugfix

* gk's everywhere nits

* lint

* extra lint

* re-remove IsValidPairString

* lint fix

* standardise test

* revert some bads

* dupe rm

* another revert 360 mcgee

* un-in-revertify

* Update exchange/order/limits/levels_test.go

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

* fix

* Update exchanges/binance/binance_test.go

HERE'S HOPING GITHUB FORMATS THIS CORRECTLY!

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

* update text

* rn func, same line err gk4202000

---------

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
2025-08-26 12:30:21 +10:00

674 lines
20 KiB
Go

package orderbook
import (
"errors"
"fmt"
"sync"
"time"
"github.com/gofrs/uuid"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/dispatch"
"github.com/thrasher-corp/gocryptotrader/exchanges/alert"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/log"
)
// Public errors
var (
ErrOrderbookInvalid = errors.New("orderbook data integrity compromised")
ErrInvalidAction = errors.New("invalid action")
ErrLastUpdatedNotSet = errors.New("last updated not set")
)
var errInvalidBookDepth = errors.New("invalid book depth")
// Outbound restricts outbound usage of depth. NOTE: Type assert to
// *orderbook.Depth.
type Outbound interface {
Retrieve() (*Book, error)
}
// Depth defines a store of orderbook Levels
type Depth struct {
askLevels
bidLevels
alert.Notice
mux *dispatch.Mux
id uuid.UUID
options
// validationError defines current book state and why it was invalidated.
validationError error
m sync.RWMutex
}
// NewDepth returns a new orderbook depth
func NewDepth(id uuid.UUID) *Depth {
return &Depth{id: id, mux: s.signalMux}
}
// Publish alerts any subscribed routines using a dispatch mux
func (d *Depth) Publish() {
if err := d.mux.Publish(Outbound(d), d.id); err != nil {
log.Errorf(log.ExchangeSys, "Cannot publish orderbook update to mux %v", err)
}
}
// Retrieve returns a snapshot of the orderbook
// spread
func (d *Depth) Retrieve() (*Book, error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return nil, d.validationError
}
return &Book{
Bids: d.bidLevels.retrieve(0),
Asks: d.askLevels.retrieve(0),
Exchange: d.exchange,
Asset: d.asset,
Pair: d.pair,
LastUpdated: d.lastUpdated,
LastPushed: d.lastPushed,
InsertedAt: d.insertedAt,
LastUpdateID: d.lastUpdateID,
PriceDuplication: d.priceDuplication,
IsFundingRate: d.isFundingRate,
ValidateOrderbook: d.validateOrderbook,
MaxDepth: d.maxDepth,
ChecksumStringRequired: d.checksumStringRequired,
RestSnapshot: d.restSnapshot,
IDAlignment: d.idAligned,
}, nil
}
// LoadSnapshot flushes the bids and asks with a snapshot
func (d *Depth) LoadSnapshot(incoming *Book) error {
d.m.Lock()
defer d.m.Unlock()
if incoming.LastUpdated.IsZero() {
return fmt.Errorf("error loading orderbook snapshot: %s %s %s - %w", d.exchange, d.pair, d.asset, ErrLastUpdatedNotSet)
}
d.lastUpdateID = incoming.LastUpdateID
d.lastUpdated = incoming.LastUpdated
d.lastPushed = incoming.LastPushed
d.insertedAt = time.Now()
d.restSnapshot = incoming.RestSnapshot
d.bidLevels.load(incoming.Bids)
d.askLevels.load(incoming.Asks)
d.validationError = nil
d.Alert()
return nil
}
// Invalidate initialises the Depth, with a error to explain why it was invalid
func (d *Depth) Invalidate(withReason error) error {
d.m.Lock()
defer d.m.Unlock()
return d.invalidate(withReason)
}
// invalidate initialises the Depth, with a error to explain why it was invalid
// NOTE: This requires locking.
func (d *Depth) invalidate(withReason error) error {
d.lastUpdateID = 0
d.lastUpdated = time.Time{}
d.bidLevels.load(nil)
d.askLevels.load(nil)
d.validationError = fmt.Errorf("%s %s %s Reason: [%w]", d.exchange, d.pair, d.asset, common.AppendError(ErrOrderbookInvalid, withReason))
d.Alert()
return d.validationError
}
// IsValid returns if the underlying book is valid.
func (d *Depth) IsValid() bool {
d.m.RLock()
defer d.m.RUnlock()
return d.validationError == nil
}
// AssignOptions assigns the initial options for the depth instance
func (d *Depth) AssignOptions(b *Book) {
d.m.Lock()
d.options = options{
exchange: b.Exchange,
pair: b.Pair,
asset: b.Asset,
lastUpdated: b.LastUpdated,
lastUpdateID: b.LastUpdateID,
priceDuplication: b.PriceDuplication,
isFundingRate: b.IsFundingRate,
validateOrderbook: b.ValidateOrderbook,
restSnapshot: b.RestSnapshot,
idAligned: b.IDAlignment,
maxDepth: b.MaxDepth,
checksumStringRequired: b.ChecksumStringRequired,
}
d.m.Unlock()
}
// GetName returns name of exchange
func (d *Depth) GetName() string {
d.m.RLock()
defer d.m.RUnlock()
return d.exchange
}
// IsRESTSnapshot returns if the depth was updated via REST
func (d *Depth) IsRESTSnapshot() (bool, error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return false, d.validationError
}
return d.restSnapshot, nil
}
// LastUpdateID returns the last Update ID
func (d *Depth) LastUpdateID() (int64, error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return 0, d.validationError
}
return d.lastUpdateID, nil
}
// IsFundingRate returns if the depth is a funding rate
func (d *Depth) IsFundingRate() bool {
d.m.RLock()
defer d.m.RUnlock()
return d.isFundingRate
}
// ValidateOrderbook returns if the verify orderbook option is set
func (d *Depth) ValidateOrderbook() bool {
d.m.RLock()
defer d.m.RUnlock()
return d.validateOrderbook
}
// GetAskLength returns length of asks
func (d *Depth) GetAskLength() (int, error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return 0, d.validationError
}
return len(d.askLevels.Levels), nil
}
// GetBidLength returns length of bids
func (d *Depth) GetBidLength() (int, error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return 0, d.validationError
}
return len(d.bidLevels.Levels), nil
}
// TotalBidAmounts returns the total amount of bids and the total orderbook
// bids value
func (d *Depth) TotalBidAmounts() (liquidity, value float64, err error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return 0, 0, d.validationError
}
liquidity, value = d.bidLevels.amount()
return liquidity, value, nil
}
// TotalAskAmounts returns the total amount of asks and the total orderbook
// asks value
func (d *Depth) TotalAskAmounts() (liquidity, value float64, err error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return 0, 0, d.validationError
}
liquidity, value = d.askLevels.amount()
return liquidity, value, nil
}
// updateAndAlert updates the last updated ID and when it was updated to the
// recent update. Then alerts all pending routines. NOTE: This requires locking.
func (d *Depth) updateAndAlert(update *Update) {
d.lastUpdateID = update.UpdateID
d.lastUpdated = update.UpdateTime
d.lastPushed = update.LastPushed
d.insertedAt = time.Now()
d.Alert()
}
// HitTheBidsByNominalSlippage hits the bids by the required nominal slippage
// percentage, calculated from the reference price and returns orderbook
// movement details for the bid side.
func (d *Depth) HitTheBidsByNominalSlippage(maxSlippage, refPrice float64) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
return d.bidLevels.hitBidsByNominalSlippage(maxSlippage, refPrice)
}
// HitTheBidsByNominalSlippageFromMid hits the bids by the required nominal
// slippage percentage, calculated from the mid price and returns orderbook
// movement details for the bid side.
func (d *Depth) HitTheBidsByNominalSlippageFromMid(maxSlippage float64) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
mid, err := d.getMidPriceNoLock()
if err != nil {
return nil, err
}
return d.bidLevels.hitBidsByNominalSlippage(maxSlippage, mid)
}
// HitTheBidsByNominalSlippageFromBest hits the bids by the required nominal
// slippage percentage, calculated from the best bid price and returns orderbook
// movement details for the bid side.
func (d *Depth) HitTheBidsByNominalSlippageFromBest(maxSlippage float64) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
head, err := d.bidLevels.getHeadPriceNoLock()
if err != nil {
return nil, err
}
return d.bidLevels.hitBidsByNominalSlippage(maxSlippage, head)
}
// LiftTheAsksByNominalSlippage lifts the asks by the required nominal slippage
// percentage, calculated from the reference price and returns orderbook
// movement details for the ask side.
func (d *Depth) LiftTheAsksByNominalSlippage(maxSlippage, refPrice float64) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
return d.askLevels.liftAsksByNominalSlippage(maxSlippage, refPrice)
}
// LiftTheAsksByNominalSlippageFromMid lifts the asks by the required nominal
// slippage percentage, calculated from the mid price and returns orderbook
// movement details for the ask side.
func (d *Depth) LiftTheAsksByNominalSlippageFromMid(maxSlippage float64) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
mid, err := d.getMidPriceNoLock()
if err != nil {
return nil, err
}
return d.askLevels.liftAsksByNominalSlippage(maxSlippage, mid)
}
// LiftTheAsksByNominalSlippageFromBest lifts the asks by the required nominal
// slippage percentage, calculated from the best ask price and returns orderbook
// movement details for the ask side.
func (d *Depth) LiftTheAsksByNominalSlippageFromBest(maxSlippage float64) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
head, err := d.askLevels.getHeadPriceNoLock()
if err != nil {
return nil, err
}
return d.askLevels.liftAsksByNominalSlippage(maxSlippage, head)
}
// HitTheBidsByImpactSlippage hits the bids by the required impact slippage
// percentage, calculated from the reference price and returns orderbook
// movement details for the bid side.
func (d *Depth) HitTheBidsByImpactSlippage(maxSlippage, refPrice float64) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
return d.bidLevels.hitBidsByImpactSlippage(maxSlippage, refPrice)
}
// HitTheBidsByImpactSlippageFromMid hits the bids by the required impact
// slippage percentage, calculated from the mid price and returns orderbook
// movement details for the bid side.
func (d *Depth) HitTheBidsByImpactSlippageFromMid(maxSlippage float64) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
mid, err := d.getMidPriceNoLock()
if err != nil {
return nil, err
}
return d.bidLevels.hitBidsByImpactSlippage(maxSlippage, mid)
}
// HitTheBidsByImpactSlippageFromBest hits the bids by the required impact
// slippage percentage, calculated from the best bid price and returns orderbook
// movement details for the bid side.
func (d *Depth) HitTheBidsByImpactSlippageFromBest(maxSlippage float64) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
head, err := d.bidLevels.getHeadPriceNoLock()
if err != nil {
return nil, err
}
return d.bidLevels.hitBidsByImpactSlippage(maxSlippage, head)
}
// LiftTheAsksByImpactSlippage lifts the asks by the required impact slippage
// percentage, calculated from the reference price and returns orderbook
// movement details for the ask side.
func (d *Depth) LiftTheAsksByImpactSlippage(maxSlippage, refPrice float64) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
return d.askLevels.liftAsksByImpactSlippage(maxSlippage, refPrice)
}
// LiftTheAsksByImpactSlippageFromMid lifts the asks by the required impact
// slippage percentage, calculated from the mid price and returns orderbook
// movement details for the ask side.
func (d *Depth) LiftTheAsksByImpactSlippageFromMid(maxSlippage float64) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
mid, err := d.getMidPriceNoLock()
if err != nil {
return nil, err
}
return d.askLevels.liftAsksByImpactSlippage(maxSlippage, mid)
}
// LiftTheAsksByImpactSlippageFromBest lifts the asks by the required impact
// slippage percentage, calculated from the best ask price and returns orderbook
// movement details for the ask side.
func (d *Depth) LiftTheAsksByImpactSlippageFromBest(maxSlippage float64) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
head, err := d.askLevels.getHeadPriceNoLock()
if err != nil {
return nil, err
}
return d.askLevels.liftAsksByImpactSlippage(maxSlippage, head)
}
// HitTheBids derives full orderbook slippage information from reference price
// using an amount. Purchase refers to how much quote currency is desired else
// the amount would refer to base currency deployed to orderbook bid side.
func (d *Depth) HitTheBids(amount, refPrice float64, purchase bool) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
if purchase {
return d.bidLevels.getMovementByQuotation(amount, refPrice, false)
}
return d.bidLevels.getMovementByBase(amount, refPrice, false)
}
// HitTheBidsFromMid derives full orderbook slippage information from mid price
// using an amount. Purchase refers to how much quote currency is desired else
// the amount would refer to base currency deployed to orderbook bid side.
func (d *Depth) HitTheBidsFromMid(amount float64, purchase bool) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
mid, err := d.getMidPriceNoLock()
if err != nil {
return nil, err
}
if purchase {
return d.bidLevels.getMovementByQuotation(amount, mid, false)
}
return d.bidLevels.getMovementByBase(amount, mid, false)
}
// HitTheBidsFromBest derives full orderbook slippage information from best bid
// price using an amount. Purchase refers to how much quote currency is desired
// else the amount would refer to base currency deployed to orderbook bid side.
func (d *Depth) HitTheBidsFromBest(amount float64, purchase bool) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
head, err := d.bidLevels.getHeadPriceNoLock()
if err != nil {
return nil, err
}
if purchase {
return d.bidLevels.getMovementByQuotation(amount, head, false)
}
return d.bidLevels.getMovementByBase(amount, head, false)
}
// LiftTheAsks derives full orderbook slippage information from reference price
// using an amount. Purchase refers to how much base currency is desired else
// the amount would refer to quote currency deployed to orderbook ask side.
func (d *Depth) LiftTheAsks(amount, refPrice float64, purchase bool) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
if purchase {
return d.askLevels.getMovementByBase(amount, refPrice, true)
}
return d.askLevels.getMovementByQuotation(amount, refPrice, true)
}
// LiftTheAsksFromMid derives full orderbook slippage information from mid price
// using an amount. Purchase refers to how much base currency is desired else
// the amount would refer to quote currency deployed to orderbook ask side.
func (d *Depth) LiftTheAsksFromMid(amount float64, purchase bool) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
mid, err := d.getMidPriceNoLock()
if err != nil {
return nil, err
}
if purchase {
return d.askLevels.getMovementByBase(amount, mid, true)
}
return d.askLevels.getMovementByQuotation(amount, mid, true)
}
// LiftTheAsksFromBest derives full orderbook slippage information from best ask
// price using an amount. Purchase refers to how much base currency is desired
// else the amount would refer to quote currency deployed to orderbook ask side.
func (d *Depth) LiftTheAsksFromBest(amount float64, purchase bool) (*Movement, error) {
d.m.Lock()
defer d.m.Unlock()
if d.validationError != nil {
return nil, d.validationError
}
head, err := d.askLevels.getHeadPriceNoLock()
if err != nil {
return nil, err
}
if purchase {
return d.askLevels.getMovementByBase(amount, head, true)
}
return d.askLevels.getMovementByQuotation(amount, head, true)
}
// GetMidPrice returns the mid price between the ask and bid spread
func (d *Depth) GetMidPrice() (float64, error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return 0, d.validationError
}
return d.getMidPriceNoLock()
}
// getMidPriceNoLock is an unprotected helper that gets mid price
func (d *Depth) getMidPriceNoLock() (float64, error) {
bidHead, err := d.bidLevels.getHeadPriceNoLock()
if err != nil {
return 0, err
}
askHead, err := d.askLevels.getHeadPriceNoLock()
if err != nil {
return 0, err
}
return (bidHead + askHead) / 2, nil
}
// GetBestBid returns the best bid price
func (d *Depth) GetBestBid() (float64, error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return 0, d.validationError
}
return d.bidLevels.getHeadPriceNoLock()
}
// GetBestAsk returns the best ask price
func (d *Depth) GetBestAsk() (float64, error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return 0, d.validationError
}
return d.askLevels.getHeadPriceNoLock()
}
// GetSpreadAmount returns the spread as a quotation amount
func (d *Depth) GetSpreadAmount() (float64, error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return 0, d.validationError
}
askHead, err := d.askLevels.getHeadPriceNoLock()
if err != nil {
return 0, err
}
bidHead, err := d.bidLevels.getHeadPriceNoLock()
if err != nil {
return 0, err
}
return askHead - bidHead, nil
}
// GetSpreadPercentage returns the spread as a percentage
func (d *Depth) GetSpreadPercentage() (float64, error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return 0, d.validationError
}
askHead, err := d.askLevels.getHeadPriceNoLock()
if err != nil {
return 0, err
}
bidHead, err := d.bidLevels.getHeadPriceNoLock()
if err != nil {
return 0, err
}
return (askHead - bidHead) / askHead * 100, nil
}
// GetImbalance returns top orderbook imbalance
func (d *Depth) GetImbalance() (float64, error) {
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return 0, d.validationError
}
askVolume, err := d.askLevels.getHeadVolumeNoLock()
if err != nil {
return 0, err
}
bidVolume, err := d.bidLevels.getHeadVolumeNoLock()
if err != nil {
return 0, err
}
return (bidVolume - askVolume) / (bidVolume + askVolume), nil
}
// GetLevels returns the desired level for the required depth count. If
// 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) GetLevels(count int) (ask, bid []Level, err error) {
if count < 0 {
return nil, nil, errInvalidBookDepth
}
d.m.RLock()
defer d.m.RUnlock()
if d.validationError != nil {
return nil, nil, d.validationError
}
return d.askLevels.retrieve(count), d.bidLevels.retrieve(count), nil
}
// Pair returns the pair associated with the depth
func (d *Depth) Pair() currency.Pair {
d.m.RLock()
defer d.m.RUnlock()
return d.pair
}
// Asset returns the asset associated with the depth
func (d *Depth) Asset() asset.Item {
d.m.RLock()
defer d.m.RUnlock()
return d.asset
}
// Exchange returns the exchange associated with the depth
func (d *Depth) Exchange() string {
d.m.RLock()
defer d.m.RUnlock()
return d.exchange
}
// Key returns a combined key for the depth
func (d *Depth) Key() key.ExchangeAssetPair {
d.m.RLock()
defer d.m.RUnlock()
return key.NewExchangeAssetPair(d.exchange, d.asset, d.pair)
}