mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
664
exchanges/orderbook/tranches.go
Normal file
664
exchanges/orderbook/tranches.go
Normal 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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user