mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-02 07:26:53 +00:00
depth: Add methods to derive liquidity allowances on orderbooks by volume and slippage (#962)
* depth: methods to derive liquidity impact details * depth: fix comments on link list methods * depth: fix whoopsie * Update exchanges/orderbook/linked_list_test.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * glorious: nits * orderbook: standardise methods to GCT math package * averagePrice: implementation (WIP) * ll: hmmmmmm * continued * orderbook: reworked functions * WIP * orderbook: add tests link up with RPC * orderbook: refined calculations, add tests (WIP) * orderbook: add tests finalise/verify remove state until next PR if needed * rpcserver/orderbook: remove redundant type and change wording * linter: fix * Update exchanges/orderbook/linked_list.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * gctcli: noobed it up * orderbook: work work work (yesterday) * depth: WIP and testing * orderbook: improve calculations for bids traversal * orderbook: adjust tests * orderbook: linters/nits * orderbook: fix error returns and add asset to whalebomb * orderbook: drop error when full book is potentially consumed * Update cmd/gctcli/orderbook.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * orderbook: Add tests and nits * glorious: nits * backtester: handle new errors * grpc: linter * orderbook: remove pesky t.Log()s * glorious: nits (yesterday) * depth/gctcli: Add in purchase requirements into orderbook movement, will also standardize in next commit after tests are fixed. * orderbook: standardize and overhaul * orderbook: update comments, update tests * rpcserver: add average ordercost and amounts * depth: add spread and imbalance methods * linter: fix * glorious: nits * orderbook: don't purge price, rn test. * glorious: codes nits * linter: fix * Update exchanges/orderbook/linked_list.go Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io> * Update exchanges/orderbook/linked_list.go Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io> * Update exchanges/orderbook/linked_list.go Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io> * Update exchanges/orderbook/linked_list.go Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io> * Update exchanges/orderbook/linked_list.go Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io> * thrasher: nits Co-authored-by: Scott <gloriousCode@users.noreply.github.com> Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io> Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
This commit is contained in:
@@ -3,10 +3,16 @@ package orderbook
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
math "github.com/thrasher-corp/gocryptotrader/common/math"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
)
|
||||
|
||||
const fullLiquidityUsageWarning = "[WARNING]: Full liquidity exhausted."
|
||||
|
||||
var (
|
||||
errPriceTargetInvalid = errors.New("price target is invalid")
|
||||
errCannotShiftPrice = errors.New("cannot shift price")
|
||||
)
|
||||
|
||||
// WhaleBombResult returns the whale bomb result
|
||||
@@ -15,214 +21,240 @@ type WhaleBombResult struct {
|
||||
MinimumPrice float64
|
||||
MaximumPrice float64
|
||||
PercentageGainOrLoss float64
|
||||
Orders orderSummary
|
||||
Orders Items
|
||||
Status string
|
||||
}
|
||||
|
||||
// WhaleBomb finds the amount required to target a price
|
||||
func (b *Base) WhaleBomb(priceTarget float64, buy bool) (*WhaleBombResult, error) {
|
||||
if priceTarget < 0 {
|
||||
return nil, errors.New("price target is invalid")
|
||||
return nil, errPriceTargetInvalid
|
||||
}
|
||||
if buy {
|
||||
a, orders := b.findAmount(priceTarget, true)
|
||||
min, max := orders.MinimumPrice(false), orders.MaximumPrice(true)
|
||||
var err error
|
||||
if max < priceTarget {
|
||||
err = errors.New("unable to hit price target due to insufficient orderbook items")
|
||||
}
|
||||
status := fmt.Sprintf("Buying %.2f %v worth of %v will send the price from %v to %v [%.2f%%] and take %v orders.",
|
||||
a, b.Pair.Quote.String(), b.Pair.Base.String(), min, max,
|
||||
math.CalculatePercentageGainOrLoss(max, min), len(orders))
|
||||
return &WhaleBombResult{
|
||||
Amount: a,
|
||||
Orders: orders,
|
||||
MinimumPrice: min,
|
||||
MaximumPrice: max,
|
||||
Status: status,
|
||||
}, err
|
||||
action, err := b.findAmount(priceTarget, buy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a, orders := b.findAmount(priceTarget, false)
|
||||
min, max := orders.MinimumPrice(false), orders.MaximumPrice(true)
|
||||
var err error
|
||||
if min > priceTarget {
|
||||
err = errors.New("unable to hit price target due to insufficient orderbook items")
|
||||
var warning string
|
||||
if action.FullLiquidityUsed {
|
||||
warning = fullLiquidityUsageWarning
|
||||
}
|
||||
status := fmt.Sprintf("Selling %.2f %v worth of %v will send the price from %v to %v [%.2f%%] and take %v orders.",
|
||||
a, b.Pair.Base.String(), b.Pair.Quote.String(), max, min,
|
||||
math.CalculatePercentageGainOrLoss(min, max), len(orders))
|
||||
|
||||
var status string
|
||||
var percent, min, max, amount float64
|
||||
if buy {
|
||||
min = action.ReferencePrice
|
||||
max = action.TranchePositionPrice
|
||||
amount = action.QuoteAmount
|
||||
percent = math.CalculatePercentageGainOrLoss(action.TranchePositionPrice, action.ReferencePrice)
|
||||
status = fmt.Sprintf("Buying using %.2f %s worth of %s will send the price from %v to %v [%.2f%%] and impact %d price tranche(s). %s",
|
||||
amount, b.Pair.Quote, b.Pair.Base, min, max,
|
||||
percent, len(action.Tranches), warning)
|
||||
} else {
|
||||
min = action.TranchePositionPrice
|
||||
max = action.ReferencePrice
|
||||
amount = action.BaseAmount
|
||||
percent = math.CalculatePercentageGainOrLoss(action.TranchePositionPrice, action.ReferencePrice)
|
||||
status = fmt.Sprintf("Selling using %.2f %s worth of %s will send the price from %v to %v [%.2f%%] and impact %d price tranche(s). %s",
|
||||
amount, b.Pair.Base, b.Pair.Quote, max, min,
|
||||
percent, len(action.Tranches), warning)
|
||||
}
|
||||
|
||||
return &WhaleBombResult{
|
||||
Amount: a,
|
||||
Orders: orders,
|
||||
MinimumPrice: min,
|
||||
MaximumPrice: max,
|
||||
Status: status,
|
||||
Amount: amount,
|
||||
Orders: action.Tranches,
|
||||
MinimumPrice: min,
|
||||
MaximumPrice: max,
|
||||
Status: status,
|
||||
PercentageGainOrLoss: percent,
|
||||
}, err
|
||||
}
|
||||
|
||||
// OrderSimulationResult returns the order simulation result
|
||||
type OrderSimulationResult WhaleBombResult
|
||||
|
||||
// SimulateOrder simulates an order
|
||||
func (b *Base) SimulateOrder(amount float64, buy bool) *OrderSimulationResult {
|
||||
func (b *Base) SimulateOrder(amount float64, buy bool) (*WhaleBombResult, error) {
|
||||
var direction string
|
||||
var action *DeploymentAction
|
||||
var soldAmount, boughtAmount, minimumPrice, maximumPrice float64
|
||||
var sold, bought currency.Code
|
||||
var err error
|
||||
if buy {
|
||||
orders, amt := b.buy(amount)
|
||||
min, max := orders.MinimumPrice(false), orders.MaximumPrice(true)
|
||||
pct := math.CalculatePercentageGainOrLoss(max, min)
|
||||
status := fmt.Sprintf("Buying %.2f %v worth of %v will send the price from %v to %v [%.2f%%] and take %v orders.",
|
||||
amount, b.Pair.Quote.String(), b.Pair.Base.String(), min, max,
|
||||
pct, len(orders))
|
||||
return &OrderSimulationResult{
|
||||
Orders: orders,
|
||||
Amount: amt,
|
||||
MinimumPrice: min,
|
||||
MaximumPrice: max,
|
||||
PercentageGainOrLoss: pct,
|
||||
Status: status,
|
||||
direction = "Buying"
|
||||
action, err = b.buy(amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
soldAmount = action.QuoteAmount
|
||||
boughtAmount = action.BaseAmount
|
||||
maximumPrice = action.TranchePositionPrice
|
||||
minimumPrice = action.ReferencePrice
|
||||
sold = b.Pair.Quote
|
||||
bought = b.Pair.Base
|
||||
} else {
|
||||
direction = "Selling"
|
||||
action, err = b.sell(amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
soldAmount = action.BaseAmount
|
||||
boughtAmount = action.QuoteAmount
|
||||
minimumPrice = action.TranchePositionPrice
|
||||
maximumPrice = action.ReferencePrice
|
||||
sold = b.Pair.Base
|
||||
bought = b.Pair.Quote
|
||||
}
|
||||
orders, amt := b.sell(amount)
|
||||
min, max := orders.MinimumPrice(false), orders.MaximumPrice(true)
|
||||
pct := math.CalculatePercentageGainOrLoss(min, max)
|
||||
status := fmt.Sprintf("Selling %f %v worth of %v will send the price from %v to %v [%.2f%%] and take %v orders.",
|
||||
amount, b.Pair.Base.String(), b.Pair.Quote.String(), max, min,
|
||||
pct, len(orders))
|
||||
return &OrderSimulationResult{
|
||||
Orders: orders,
|
||||
Amount: amt,
|
||||
MinimumPrice: min,
|
||||
MaximumPrice: max,
|
||||
|
||||
var warning string
|
||||
if action.FullLiquidityUsed {
|
||||
warning = fullLiquidityUsageWarning
|
||||
}
|
||||
|
||||
pct := math.CalculatePercentageGainOrLoss(action.TranchePositionPrice, action.ReferencePrice)
|
||||
status := fmt.Sprintf("%s using %f %v worth of %v will send the price from %v to %v [%.2f%%] and impact %v price tranche(s). %s",
|
||||
direction, soldAmount, sold, bought, action.ReferencePrice,
|
||||
action.TranchePositionPrice, pct, len(action.Tranches), warning)
|
||||
return &WhaleBombResult{
|
||||
Orders: action.Tranches,
|
||||
Amount: boughtAmount,
|
||||
MinimumPrice: minimumPrice,
|
||||
MaximumPrice: maximumPrice,
|
||||
PercentageGainOrLoss: pct,
|
||||
Status: status,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
type orderSummary []Item
|
||||
|
||||
func (o orderSummary) Print() {
|
||||
for x := range o {
|
||||
log.Debugf(log.OrderBook, "Order: Price: %f Amount: %f", o[x].Price, o[x].Amount)
|
||||
}
|
||||
}
|
||||
|
||||
func (o orderSummary) MinimumPrice(reverse bool) float64 {
|
||||
if len(o) != 0 {
|
||||
sortOrdersByPrice(&o, reverse)
|
||||
return o[0].Price
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (o orderSummary) MaximumPrice(reverse bool) float64 {
|
||||
if len(o) != 0 {
|
||||
sortOrdersByPrice(&o, reverse)
|
||||
return o[0].Price
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ByPrice used for sorting orders by order date
|
||||
type ByPrice orderSummary
|
||||
|
||||
func (b ByPrice) Len() int { return len(b) }
|
||||
func (b ByPrice) Less(i, j int) bool { return b[i].Price < b[j].Price }
|
||||
func (b ByPrice) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||
|
||||
// sortOrdersByPrice the caller function to sort orders
|
||||
func sortOrdersByPrice(o *orderSummary, reverse bool) {
|
||||
if reverse {
|
||||
sort.Sort(sort.Reverse(ByPrice(*o)))
|
||||
} else {
|
||||
sort.Sort(ByPrice(*o))
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Base) findAmount(price float64, buy bool) (float64, orderSummary) {
|
||||
orders := make(orderSummary, 0)
|
||||
var amt float64
|
||||
|
||||
func (b *Base) findAmount(priceTarget float64, buy bool) (*DeploymentAction, error) {
|
||||
action := DeploymentAction{}
|
||||
if buy {
|
||||
if len(b.Asks) == 0 {
|
||||
return nil, errNoLiquidity
|
||||
}
|
||||
action.ReferencePrice = b.Asks[0].Price
|
||||
if action.ReferencePrice > priceTarget {
|
||||
return nil, fmt.Errorf("%w to %f as it's below ascending ask prices starting at %f",
|
||||
errCannotShiftPrice, priceTarget, action.ReferencePrice)
|
||||
}
|
||||
for x := range b.Asks {
|
||||
if b.Asks[x].Price >= price {
|
||||
amt += b.Asks[x].Price * b.Asks[x].Amount
|
||||
orders = append(orders, Item{
|
||||
Price: b.Asks[x].Price,
|
||||
Amount: b.Asks[x].Amount,
|
||||
})
|
||||
return amt, orders
|
||||
if b.Asks[x].Price >= priceTarget {
|
||||
action.TranchePositionPrice = b.Asks[x].Price
|
||||
return &action, nil
|
||||
}
|
||||
orders = append(orders, Item{
|
||||
Price: b.Asks[x].Price,
|
||||
Amount: b.Asks[x].Amount,
|
||||
})
|
||||
amt += b.Asks[x].Price * b.Asks[x].Amount
|
||||
action.Tranches = append(action.Tranches, b.Asks[x])
|
||||
action.QuoteAmount += b.Asks[x].Price * b.Asks[x].Amount
|
||||
action.BaseAmount += b.Asks[x].Amount
|
||||
}
|
||||
return amt, orders
|
||||
action.TranchePositionPrice = b.Asks[len(b.Asks)-1].Price
|
||||
action.FullLiquidityUsed = true
|
||||
return &action, nil
|
||||
}
|
||||
|
||||
for x := range b.Bids {
|
||||
if b.Bids[x].Price <= price {
|
||||
amt += b.Bids[x].Amount
|
||||
orders = append(orders, Item{
|
||||
Price: b.Bids[x].Price,
|
||||
Amount: b.Bids[x].Amount,
|
||||
})
|
||||
break
|
||||
}
|
||||
orders = append(orders, Item{
|
||||
Price: b.Bids[x].Price,
|
||||
Amount: b.Bids[x].Amount,
|
||||
})
|
||||
amt += b.Bids[x].Amount
|
||||
if len(b.Bids) == 0 {
|
||||
return nil, errNoLiquidity
|
||||
}
|
||||
return amt, orders
|
||||
action.ReferencePrice = b.Bids[0].Price
|
||||
if action.ReferencePrice < priceTarget {
|
||||
return nil, fmt.Errorf("%w to %f as it's above descending bid prices starting at %f",
|
||||
errCannotShiftPrice, priceTarget, action.ReferencePrice)
|
||||
}
|
||||
for x := range b.Bids {
|
||||
if b.Bids[x].Price <= priceTarget {
|
||||
action.TranchePositionPrice = b.Bids[x].Price
|
||||
return &action, nil
|
||||
}
|
||||
action.Tranches = append(action.Tranches, b.Bids[x])
|
||||
action.QuoteAmount += b.Bids[x].Price * b.Bids[x].Amount
|
||||
action.BaseAmount += b.Bids[x].Amount
|
||||
}
|
||||
action.TranchePositionPrice = b.Bids[len(b.Bids)-1].Price
|
||||
action.FullLiquidityUsed = true
|
||||
return &action, nil
|
||||
}
|
||||
|
||||
func (b *Base) buy(amount float64) (orders orderSummary, baseAmount float64) {
|
||||
var processedAmt float64
|
||||
// DeploymentAction defines deployment information on a liquidity side.
|
||||
type DeploymentAction struct {
|
||||
ReferencePrice float64
|
||||
TranchePositionPrice float64
|
||||
BaseAmount float64
|
||||
QuoteAmount float64
|
||||
Tranches Items
|
||||
FullLiquidityUsed bool
|
||||
}
|
||||
|
||||
func (b *Base) buy(quote float64) (*DeploymentAction, error) {
|
||||
if quote <= 0 {
|
||||
return nil, errQuoteAmountInvalid
|
||||
}
|
||||
if len(b.Asks) == 0 {
|
||||
return nil, errNoLiquidity
|
||||
}
|
||||
action := &DeploymentAction{ReferencePrice: b.Asks[0].Price}
|
||||
for x := range b.Asks {
|
||||
subtotal := b.Asks[x].Price * b.Asks[x].Amount
|
||||
if processedAmt+subtotal >= amount {
|
||||
diff := amount - processedAmt
|
||||
subAmt := diff / b.Asks[x].Price
|
||||
orders = append(orders, Item{
|
||||
action.TranchePositionPrice = b.Asks[x].Price
|
||||
trancheValue := b.Asks[x].Price * b.Asks[x].Amount
|
||||
action.QuoteAmount += trancheValue
|
||||
remaining := quote - trancheValue
|
||||
if remaining <= 0 {
|
||||
if remaining == 0 {
|
||||
if len(b.Asks)-1 > x {
|
||||
action.TranchePositionPrice = b.Asks[x+1].Price
|
||||
} else {
|
||||
action.FullLiquidityUsed = true
|
||||
}
|
||||
}
|
||||
subAmount := quote / b.Asks[x].Price
|
||||
action.Tranches = append(action.Tranches, Item{
|
||||
Price: b.Asks[x].Price,
|
||||
Amount: subAmt,
|
||||
Amount: subAmount,
|
||||
})
|
||||
baseAmount += subAmt
|
||||
break
|
||||
action.BaseAmount += subAmount
|
||||
return action, nil
|
||||
}
|
||||
processedAmt += subtotal
|
||||
baseAmount += b.Asks[x].Amount
|
||||
orders = append(orders, Item{
|
||||
Price: b.Asks[x].Price,
|
||||
Amount: b.Asks[x].Amount,
|
||||
})
|
||||
if len(b.Asks)-1 <= x {
|
||||
action.FullLiquidityUsed = true
|
||||
}
|
||||
quote = remaining
|
||||
action.BaseAmount += b.Asks[x].Amount
|
||||
action.Tranches = append(action.Tranches, b.Asks[x])
|
||||
}
|
||||
return
|
||||
|
||||
return action, nil
|
||||
}
|
||||
|
||||
func (b *Base) sell(amount float64) (orders orderSummary, quoteAmount float64) {
|
||||
var processedAmt float64
|
||||
for x := range b.Bids {
|
||||
if processedAmt+b.Bids[x].Amount >= amount {
|
||||
diff := amount - processedAmt
|
||||
orders = append(orders, Item{
|
||||
Price: b.Bids[x].Price,
|
||||
Amount: diff,
|
||||
})
|
||||
quoteAmount += diff * b.Bids[x].Price
|
||||
break
|
||||
}
|
||||
processedAmt += b.Bids[x].Amount
|
||||
quoteAmount += b.Bids[x].Amount * b.Bids[x].Price
|
||||
orders = append(orders, Item{
|
||||
Price: b.Bids[x].Price,
|
||||
Amount: b.Bids[x].Amount,
|
||||
})
|
||||
func (b *Base) sell(base float64) (*DeploymentAction, error) {
|
||||
if base <= 0 {
|
||||
return nil, errBaseAmountInvalid
|
||||
}
|
||||
return
|
||||
if len(b.Bids) == 0 {
|
||||
return nil, errNoLiquidity
|
||||
}
|
||||
action := &DeploymentAction{ReferencePrice: b.Bids[0].Price}
|
||||
for x := range b.Bids {
|
||||
action.TranchePositionPrice = b.Bids[x].Price
|
||||
remaining := base - b.Bids[x].Amount
|
||||
if remaining <= 0 {
|
||||
if remaining == 0 {
|
||||
if len(b.Bids)-1 > x {
|
||||
action.TranchePositionPrice = b.Bids[x+1].Price
|
||||
} else {
|
||||
action.FullLiquidityUsed = true
|
||||
}
|
||||
}
|
||||
action.Tranches = append(action.Tranches, Item{
|
||||
Price: b.Bids[x].Price,
|
||||
Amount: base,
|
||||
})
|
||||
action.BaseAmount += base
|
||||
action.QuoteAmount += base * b.Bids[x].Price
|
||||
return action, nil
|
||||
}
|
||||
if len(b.Bids)-1 <= x {
|
||||
action.FullLiquidityUsed = true
|
||||
}
|
||||
base = remaining
|
||||
action.BaseAmount += b.Bids[x].Amount
|
||||
action.QuoteAmount += b.Bids[x].Amount * b.Bids[x].Price
|
||||
action.Tranches = append(action.Tranches, b.Bids[x])
|
||||
}
|
||||
return action, nil
|
||||
}
|
||||
|
||||
// GetAveragePrice finds the average buy or sell price of a specified amount.
|
||||
@@ -254,10 +286,9 @@ func (elem Items) FindNominalAmount(amount float64) (aggNominalAmount, remaining
|
||||
aggNominalAmount += elem[x].Price * remainingAmount
|
||||
remainingAmount = 0
|
||||
break
|
||||
} else {
|
||||
aggNominalAmount += elem[x].Price * elem[x].Amount
|
||||
remainingAmount -= elem[x].Amount
|
||||
}
|
||||
aggNominalAmount += elem[x].Price * elem[x].Amount
|
||||
remainingAmount -= elem[x].Amount
|
||||
}
|
||||
return aggNominalAmount, remainingAmount
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package orderbook
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
@@ -27,69 +28,475 @@ func TestWhaleBomb(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := testSetup()
|
||||
|
||||
// invalid price amount
|
||||
_, err := b.WhaleBomb(-1, true)
|
||||
if err == nil {
|
||||
t.Error("unexpected result")
|
||||
if !errors.Is(err, errPriceTargetInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errPriceTargetInvalid)
|
||||
}
|
||||
|
||||
// valid
|
||||
_, err = b.WhaleBomb(7001, true)
|
||||
result, err := b.WhaleBomb(7001, true) // <- This price should not be wiped out on the book.
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 7000 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 7000)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 7001 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7001)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 7000 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
|
||||
}
|
||||
|
||||
if result.PercentageGainOrLoss != 0.014285714285714287 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.PercentageGainOrLoss, 0.014285714285714287)
|
||||
}
|
||||
|
||||
result, err = b.WhaleBomb(7000.5, true) // <- Slot between prices will lift to next ask tranche
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v', expected '%v'", err, nil)
|
||||
}
|
||||
// invalid
|
||||
_, err = b.WhaleBomb(7002, true)
|
||||
if err == nil {
|
||||
t.Error("unexpected result")
|
||||
|
||||
if result.Amount != 7000 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 7000)
|
||||
}
|
||||
|
||||
// valid
|
||||
_, err = b.WhaleBomb(6998, false)
|
||||
if result.MaximumPrice != 7001 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7001)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 7000 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
|
||||
}
|
||||
|
||||
if result.PercentageGainOrLoss != 0.014285714285714287 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.PercentageGainOrLoss, 0.014285714285714287)
|
||||
}
|
||||
|
||||
result, err = b.WhaleBomb(7002, true) // <- exceed available quotations
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if !strings.Contains(result.Status, fullLiquidityUsageWarning) {
|
||||
t.Fatal("expected status to contain liquidity warning")
|
||||
}
|
||||
|
||||
result, err = b.WhaleBomb(7000, true) // <- Book should not move
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 0 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 0)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 7000 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7000)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 7000 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
|
||||
}
|
||||
|
||||
if result.PercentageGainOrLoss != 0 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.PercentageGainOrLoss, 0)
|
||||
}
|
||||
|
||||
_, err = b.WhaleBomb(6000, true)
|
||||
if !errors.Is(err, errCannotShiftPrice) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errCannotShiftPrice)
|
||||
}
|
||||
|
||||
_, err = b.WhaleBomb(-1, false)
|
||||
if !errors.Is(err, errPriceTargetInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errPriceTargetInvalid)
|
||||
}
|
||||
|
||||
result, err = b.WhaleBomb(6998, false) // <- This price should not be wiped out on the book.
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 1 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 1)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 6999 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 6998 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6998)
|
||||
}
|
||||
|
||||
if result.PercentageGainOrLoss != -0.014287755393627661 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.PercentageGainOrLoss, -0.014287755393627661)
|
||||
}
|
||||
|
||||
result, err = b.WhaleBomb(6998.5, false) // <- Slot between prices will drop to next bid tranche
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v', expected '%v'", err, nil)
|
||||
}
|
||||
// invalid
|
||||
_, err = b.WhaleBomb(6997, false)
|
||||
if err == nil {
|
||||
t.Error("unexpected result")
|
||||
|
||||
if result.Amount != 1 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 1)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 6999 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 6998 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6998)
|
||||
}
|
||||
|
||||
if result.PercentageGainOrLoss != -0.014287755393627661 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.PercentageGainOrLoss, -0.014287755393627661)
|
||||
}
|
||||
|
||||
result, err = b.WhaleBomb(6997, false) // <- exceed available quotations
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if !strings.Contains(result.Status, fullLiquidityUsageWarning) {
|
||||
t.Fatal("expected status to contain liquidity warning")
|
||||
}
|
||||
|
||||
result, err = b.WhaleBomb(6999, false) // <- Book should not move
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 0 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 0)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 6999 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 6999 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6999)
|
||||
}
|
||||
|
||||
if result.PercentageGainOrLoss != 0 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.PercentageGainOrLoss, 0)
|
||||
}
|
||||
|
||||
_, err = b.WhaleBomb(7500, false)
|
||||
if !errors.Is(err, errCannotShiftPrice) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errCannotShiftPrice)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimulateOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := testSetup()
|
||||
b.SimulateOrder(8000, true)
|
||||
b.SimulateOrder(1.5, false)
|
||||
}
|
||||
|
||||
func TestOrderSummary(t *testing.T) {
|
||||
var o orderSummary
|
||||
if p := o.MaximumPrice(false); p != 0 {
|
||||
t.Error("unexpected result")
|
||||
}
|
||||
if p := o.MinimumPrice(false); p != 0 {
|
||||
t.Error("unexpected result")
|
||||
// Invalid
|
||||
_, err := b.SimulateOrder(-8000, true)
|
||||
if !errors.Is(err, errQuoteAmountInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errQuoteAmountInvalid)
|
||||
}
|
||||
|
||||
o = orderSummary{
|
||||
{Price: 1337, Amount: 1},
|
||||
{Price: 9001, Amount: 1},
|
||||
}
|
||||
if p := o.MaximumPrice(false); p != 1337 {
|
||||
t.Error("unexpected result")
|
||||
}
|
||||
if p := o.MaximumPrice(true); p != 9001 {
|
||||
t.Error("unexpected result")
|
||||
}
|
||||
if p := o.MinimumPrice(false); p != 1337 {
|
||||
t.Error("unexpected result")
|
||||
}
|
||||
if p := o.MinimumPrice(true); p != 9001 {
|
||||
t.Error("unexpected result")
|
||||
_, err = (&Base{}).SimulateOrder(1337, true)
|
||||
if !errors.Is(err, errNoLiquidity) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
|
||||
}
|
||||
|
||||
o.Print()
|
||||
// Full liquidity used
|
||||
result, err := b.SimulateOrder(21002, true)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 3 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 3)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 7000 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 7001 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7001)
|
||||
}
|
||||
|
||||
if !strings.Contains(result.Status, fullLiquidityUsageWarning) {
|
||||
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, fullLiquidityUsageWarning)
|
||||
}
|
||||
|
||||
if len(result.Orders) != 2 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 2)
|
||||
}
|
||||
|
||||
// Exceed full liquidity used
|
||||
result, err = b.SimulateOrder(21003, true)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 3 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 3)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 7000 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 7001 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7001)
|
||||
}
|
||||
|
||||
if !strings.Contains(result.Status, fullLiquidityUsageWarning) {
|
||||
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, fullLiquidityUsageWarning)
|
||||
}
|
||||
|
||||
if len(result.Orders) != 2 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 2)
|
||||
}
|
||||
|
||||
// First tranche
|
||||
result, err = b.SimulateOrder(7000, true)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 1 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 1)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 7000 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 7001 { // A full tranche is wiped out and this one should be preserved.
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7001)
|
||||
}
|
||||
|
||||
if strings.Contains(result.Status, fullLiquidityUsageWarning) {
|
||||
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, "NO WARNING")
|
||||
}
|
||||
|
||||
if len(result.Orders) != 1 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 1)
|
||||
}
|
||||
|
||||
// Half of first tranch
|
||||
result, err = b.SimulateOrder(3500, true)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != .5 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, .5)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 7000 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 7000 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7000)
|
||||
}
|
||||
|
||||
if strings.Contains(result.Status, fullLiquidityUsageWarning) {
|
||||
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, "NO WARNING")
|
||||
}
|
||||
|
||||
if len(result.Orders) != 1 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 1)
|
||||
}
|
||||
|
||||
if result.Orders[0].Amount != 0.5 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Orders[0].Amount, 0.5)
|
||||
}
|
||||
|
||||
// Half of second tranche
|
||||
result, err = b.SimulateOrder(14001, true)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 2 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 2)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 7000 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 7000)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 7001 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 7001)
|
||||
}
|
||||
|
||||
if strings.Contains(result.Status, fullLiquidityUsageWarning) {
|
||||
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, "NO WARNING")
|
||||
}
|
||||
|
||||
if len(result.Orders) != 2 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 1)
|
||||
}
|
||||
|
||||
if result.Orders[1].Amount != 1 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Orders[1].Amount, 1)
|
||||
}
|
||||
|
||||
// Hitting bids
|
||||
|
||||
// Invalid
|
||||
|
||||
_, err = (&Base{}).SimulateOrder(-1, false)
|
||||
if !errors.Is(err, errBaseAmountInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errBaseAmountInvalid)
|
||||
}
|
||||
|
||||
_, err = (&Base{}).SimulateOrder(2, false)
|
||||
if !errors.Is(err, errNoLiquidity) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
|
||||
}
|
||||
|
||||
// Full liquidity used
|
||||
result, err = b.SimulateOrder(3, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 20995 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 20995)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 6999 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 6998 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6998)
|
||||
}
|
||||
|
||||
if !strings.Contains(result.Status, fullLiquidityUsageWarning) {
|
||||
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, fullLiquidityUsageWarning)
|
||||
}
|
||||
|
||||
if len(result.Orders) != 2 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 2)
|
||||
}
|
||||
|
||||
// Exceed full liquidity used
|
||||
result, err = b.SimulateOrder(3.1, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 20995 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 20995)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 6999 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 6998 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6998)
|
||||
}
|
||||
|
||||
if !strings.Contains(result.Status, fullLiquidityUsageWarning) {
|
||||
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, fullLiquidityUsageWarning)
|
||||
}
|
||||
|
||||
if len(result.Orders) != 2 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 2)
|
||||
}
|
||||
|
||||
// First tranche
|
||||
result, err = b.SimulateOrder(1, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 6999 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 6999)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 6999 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 6998 { // A full tranche is wiped out and this one should be preserved.
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6998)
|
||||
}
|
||||
|
||||
if strings.Contains(result.Status, fullLiquidityUsageWarning) {
|
||||
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, "NO WARNING")
|
||||
}
|
||||
|
||||
if len(result.Orders) != 1 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 1)
|
||||
}
|
||||
|
||||
// Half of first tranch
|
||||
result, err = b.SimulateOrder(.5, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 3499.5 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 3499.5)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 6999 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6999)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 6999 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
|
||||
}
|
||||
|
||||
if strings.Contains(result.Status, fullLiquidityUsageWarning) {
|
||||
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, "NO WARNING")
|
||||
}
|
||||
|
||||
if len(result.Orders) != 1 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 1)
|
||||
}
|
||||
|
||||
if result.Orders[0].Amount != 0.5 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Orders[0].Amount, 0.5)
|
||||
}
|
||||
|
||||
// Half of second tranche
|
||||
result, err = b.SimulateOrder(2, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if result.Amount != 13997 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Amount, 13997)
|
||||
}
|
||||
|
||||
if result.MaximumPrice != 6999 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MaximumPrice, 6999)
|
||||
}
|
||||
|
||||
if result.MinimumPrice != 6998 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.MinimumPrice, 6998)
|
||||
}
|
||||
|
||||
if strings.Contains(result.Status, fullLiquidityUsageWarning) {
|
||||
t.Fatalf("received: '%v' but expected string to contain: '%v'", result.Status, "NO WARNING")
|
||||
}
|
||||
|
||||
if len(result.Orders) != 2 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", len(result.Orders), 2)
|
||||
}
|
||||
|
||||
if result.Orders[1].Amount != 1 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", result.Orders[1].Amount, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAveragePrice(t *testing.T) {
|
||||
|
||||
@@ -64,26 +64,6 @@ func (d *Depth) Publish() {
|
||||
}
|
||||
}
|
||||
|
||||
// GetAskLength returns length of asks
|
||||
func (d *Depth) GetAskLength() (int, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
return d.asks.length, nil
|
||||
}
|
||||
|
||||
// GetBidLength returns length of bids
|
||||
func (d *Depth) GetBidLength() (int, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
return d.bids.length, nil
|
||||
}
|
||||
|
||||
// Retrieve returns the orderbook base a copy of the underlying linked list
|
||||
// spread
|
||||
func (d *Depth) Retrieve() (*Base, error) {
|
||||
@@ -106,30 +86,6 @@ func (d *Depth) Retrieve() (*Base, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TotalBidAmounts returns the total amount of bids and the total orderbook
|
||||
// bids value
|
||||
func (d *Depth) TotalBidAmounts() (liquidity, value float64, err error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, 0, d.validationError
|
||||
}
|
||||
liquidity, value = d.bids.amount()
|
||||
return liquidity, value, nil
|
||||
}
|
||||
|
||||
// TotalAskAmounts returns the total amount of asks and the total orderbook
|
||||
// asks value
|
||||
func (d *Depth) TotalAskAmounts() (liquidity, value float64, err error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, 0, d.validationError
|
||||
}
|
||||
liquidity, value = d.asks.amount()
|
||||
return liquidity, value, nil
|
||||
}
|
||||
|
||||
// LoadSnapshot flushes the bids and asks with a snapshot
|
||||
func (d *Depth) LoadSnapshot(bids, asks []Item, lastUpdateID int64, lastUpdated time.Time, updateByREST bool) {
|
||||
d.m.Lock()
|
||||
@@ -323,6 +279,50 @@ func (d *Depth) IsFundingRate() bool {
|
||||
return d.isFundingRate
|
||||
}
|
||||
|
||||
// GetAskLength returns length of asks
|
||||
func (d *Depth) GetAskLength() (int, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
return d.asks.length, nil
|
||||
}
|
||||
|
||||
// GetBidLength returns length of bids
|
||||
func (d *Depth) GetBidLength() (int, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
return d.bids.length, nil
|
||||
}
|
||||
|
||||
// TotalBidAmounts returns the total amount of bids and the total orderbook
|
||||
// bids value
|
||||
func (d *Depth) TotalBidAmounts() (liquidity, value float64, err error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, 0, d.validationError
|
||||
}
|
||||
liquidity, value = d.bids.amount()
|
||||
return liquidity, value, nil
|
||||
}
|
||||
|
||||
// TotalAskAmounts returns the total amount of asks and the total orderbook
|
||||
// asks value
|
||||
func (d *Depth) TotalAskAmounts() (liquidity, value float64, err error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, 0, d.validationError
|
||||
}
|
||||
liquidity, value = d.asks.amount()
|
||||
return liquidity, value, nil
|
||||
}
|
||||
|
||||
// updateAndAlert updates the last updated ID and when it was updated to the
|
||||
// recent update. Then alerts all pending routines. NOTE: This requires locking.
|
||||
func (d *Depth) updateAndAlert(update *Update) {
|
||||
@@ -330,3 +330,382 @@ func (d *Depth) updateAndAlert(update *Update) {
|
||||
d.lastUpdated = update.UpdateTime
|
||||
d.Alert()
|
||||
}
|
||||
|
||||
// HitTheBidsByNominalSlippage hits the bids by the required nominal slippage
|
||||
// percentage, calculated from the reference price and returns orderbook
|
||||
// movement details for the bid side.
|
||||
func (d *Depth) HitTheBidsByNominalSlippage(maxSlippage, refPrice float64) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
return d.bids.hitBidsByNominalSlippage(maxSlippage, refPrice)
|
||||
}
|
||||
|
||||
// HitTheBidsByNominalSlippageFromMid hits the bids by the required nominal
|
||||
// slippage percentage, calculated from the mid price and returns orderbook
|
||||
// movement details for the bid side.
|
||||
func (d *Depth) HitTheBidsByNominalSlippageFromMid(maxSlippage float64) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
mid, err := d.getMidPriceNoLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.bids.hitBidsByNominalSlippage(maxSlippage, mid)
|
||||
}
|
||||
|
||||
// HitTheBidsByNominalSlippageFromBest hits the bids by the required nominal
|
||||
// slippage percentage, calculated from the best bid price and returns orderbook
|
||||
// movement details for the bid side.
|
||||
func (d *Depth) HitTheBidsByNominalSlippageFromBest(maxSlippage float64) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
head, err := d.bids.getHeadPriceNoLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.bids.hitBidsByNominalSlippage(maxSlippage, head)
|
||||
}
|
||||
|
||||
// LiftTheAsksByNominalSlippage lifts the asks by the required nominal slippage
|
||||
// percentage, calculated from the reference price and returns orderbook
|
||||
// movement details for the ask side.
|
||||
func (d *Depth) LiftTheAsksByNominalSlippage(maxSlippage, refPrice float64) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
return d.asks.liftAsksByNominalSlippage(maxSlippage, refPrice)
|
||||
}
|
||||
|
||||
// LiftTheAsksByNominalSlippageFromMid lifts the asks by the required nominal
|
||||
// slippage percentage, calculated from the mid price and returns orderbook
|
||||
// movement details for the ask side.
|
||||
func (d *Depth) LiftTheAsksByNominalSlippageFromMid(maxSlippage float64) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
mid, err := d.getMidPriceNoLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.asks.liftAsksByNominalSlippage(maxSlippage, mid)
|
||||
}
|
||||
|
||||
// LiftTheAsksByNominalSlippageFromBest lifts the asks by the required nominal
|
||||
// slippage percentage, calculated from the best ask price and returns orderbook
|
||||
// movement details for the ask side.
|
||||
func (d *Depth) LiftTheAsksByNominalSlippageFromBest(maxSlippage float64) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
head, err := d.asks.getHeadPriceNoLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.asks.liftAsksByNominalSlippage(maxSlippage, head)
|
||||
}
|
||||
|
||||
// HitTheBidsByImpactSlippage hits the bids by the required impact slippage
|
||||
// percentage, calculated from the reference price and returns orderbook
|
||||
// movement details for the bid side.
|
||||
func (d *Depth) HitTheBidsByImpactSlippage(maxSlippage, refPrice float64) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
return d.bids.hitBidsByImpactSlippage(maxSlippage, refPrice)
|
||||
}
|
||||
|
||||
// HitTheBidsByImpactSlippageFromMid hits the bids by the required impact
|
||||
// slippage percentage, calculated from the mid price and returns orderbook
|
||||
// movement details for the bid side.
|
||||
func (d *Depth) HitTheBidsByImpactSlippageFromMid(maxSlippage float64) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
mid, err := d.getMidPriceNoLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.bids.hitBidsByImpactSlippage(maxSlippage, mid)
|
||||
}
|
||||
|
||||
// HitTheBidsByImpactSlippageFromBest hits the bids by the required impact
|
||||
// slippage percentage, calculated from the best bid price and returns orderbook
|
||||
// movement details for the bid side.
|
||||
func (d *Depth) HitTheBidsByImpactSlippageFromBest(maxSlippage float64) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
head, err := d.bids.getHeadPriceNoLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.bids.hitBidsByImpactSlippage(maxSlippage, head)
|
||||
}
|
||||
|
||||
// LiftTheAsksByImpactSlippage lifts the asks by the required impact slippage
|
||||
// percentage, calculated from the reference price and returns orderbook
|
||||
// movement details for the ask side.
|
||||
func (d *Depth) LiftTheAsksByImpactSlippage(maxSlippage, refPrice float64) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
return d.asks.liftAsksByImpactSlippage(maxSlippage, refPrice)
|
||||
}
|
||||
|
||||
// LiftTheAsksByImpactSlippageFromMid lifts the asks by the required impact
|
||||
// slippage percentage, calculated from the mid price and returns orderbook
|
||||
// movement details for the ask side.
|
||||
func (d *Depth) LiftTheAsksByImpactSlippageFromMid(maxSlippage float64) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
mid, err := d.getMidPriceNoLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.asks.liftAsksByImpactSlippage(maxSlippage, mid)
|
||||
}
|
||||
|
||||
// LiftTheAsksByImpactSlippageFromBest lifts the asks by the required impact
|
||||
// slippage percentage, calculated from the best ask price and returns orderbook
|
||||
// movement details for the ask side.
|
||||
func (d *Depth) LiftTheAsksByImpactSlippageFromBest(maxSlippage float64) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
head, err := d.asks.getHeadPriceNoLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.asks.liftAsksByImpactSlippage(maxSlippage, head)
|
||||
}
|
||||
|
||||
// HitTheBids derives full orderbook slippage information from reference price
|
||||
// using an amount. Purchase refers to how much quote currency is desired else
|
||||
// the amount would refer to base currency deployed to orderbook bid side.
|
||||
func (d *Depth) HitTheBids(amount, refPrice float64, purchase bool) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
if purchase {
|
||||
return d.bids.getMovementByQuotation(amount, refPrice, false)
|
||||
}
|
||||
return d.bids.getMovementByBase(amount, refPrice, false)
|
||||
}
|
||||
|
||||
// HitTheBidsFromMid derives full orderbook slippage information from mid price
|
||||
// using an amount. Purchase refers to how much quote currency is desired else
|
||||
// the amount would refer to base currency deployed to orderbook bid side.
|
||||
func (d *Depth) HitTheBidsFromMid(amount float64, purchase bool) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
mid, err := d.getMidPriceNoLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if purchase {
|
||||
return d.bids.getMovementByQuotation(amount, mid, false)
|
||||
}
|
||||
return d.bids.getMovementByBase(amount, mid, false)
|
||||
}
|
||||
|
||||
// HitTheBidsFromBest derives full orderbook slippage information from best bid
|
||||
// price using an amount. Purchase refers to how much quote currency is desired
|
||||
// else the amount would refer to base currency deployed to orderbook bid side.
|
||||
func (d *Depth) HitTheBidsFromBest(amount float64, purchase bool) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
head, err := d.bids.getHeadPriceNoLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if purchase {
|
||||
return d.bids.getMovementByQuotation(amount, head, false)
|
||||
}
|
||||
return d.bids.getMovementByBase(amount, head, false)
|
||||
}
|
||||
|
||||
// LiftTheAsks derives full orderbook slippage information from reference price
|
||||
// using an amount. Purchase refers to how much base currency is desired else
|
||||
// the amount would refer to quote currency deployed to orderbook ask side.
|
||||
func (d *Depth) LiftTheAsks(amount, refPrice float64, purchase bool) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
if purchase {
|
||||
return d.asks.getMovementByBase(amount, refPrice, true)
|
||||
}
|
||||
return d.asks.getMovementByQuotation(amount, refPrice, true)
|
||||
}
|
||||
|
||||
// LiftTheAsksFromMid derives full orderbook slippage information from mid price
|
||||
// using an amount. Purchase refers to how much base currency is desired else
|
||||
// the amount would refer to quote currency deployed to orderbook ask side.
|
||||
func (d *Depth) LiftTheAsksFromMid(amount float64, purchase bool) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
mid, err := d.getMidPriceNoLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if purchase {
|
||||
return d.asks.getMovementByBase(amount, mid, true)
|
||||
}
|
||||
return d.asks.getMovementByQuotation(amount, mid, true)
|
||||
}
|
||||
|
||||
// LiftTheAsksFromBest derives full orderbook slippage information from best ask
|
||||
// price using an amount. Purchase refers to how much base currency is desired
|
||||
// else the amount would refer to quote currency deployed to orderbook ask side.
|
||||
func (d *Depth) LiftTheAsksFromBest(amount float64, purchase bool) (*Movement, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
head, err := d.asks.getHeadPriceNoLock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if purchase {
|
||||
return d.asks.getMovementByBase(amount, head, true)
|
||||
}
|
||||
return d.asks.getMovementByQuotation(amount, head, true)
|
||||
}
|
||||
|
||||
// GetMidPrice returns the mid price between the ask and bid spread
|
||||
func (d *Depth) GetMidPrice() (float64, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
return d.getMidPriceNoLock()
|
||||
}
|
||||
|
||||
// getMidPriceNoLock is an unprotected helper that gets mid price
|
||||
func (d *Depth) getMidPriceNoLock() (float64, error) {
|
||||
bidHead, err := d.bids.getHeadPriceNoLock()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
askHead, err := d.asks.getHeadPriceNoLock()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return (bidHead + askHead) / 2, nil
|
||||
}
|
||||
|
||||
// GetBestBid returns the best bid price
|
||||
func (d *Depth) GetBestBid() (float64, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
return d.bids.getHeadPriceNoLock()
|
||||
}
|
||||
|
||||
// GetBestAsk returns the best ask price
|
||||
func (d *Depth) GetBestAsk() (float64, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
return d.asks.getHeadPriceNoLock()
|
||||
}
|
||||
|
||||
// GetSpreadAmount returns the spread as a quotation amount
|
||||
func (d *Depth) GetSpreadAmount() (float64, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
askHead, err := d.asks.getHeadPriceNoLock()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
bidHead, err := d.bids.getHeadPriceNoLock()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return askHead - bidHead, nil
|
||||
}
|
||||
|
||||
// GetSpreadPercentage returns the spread as a percentage
|
||||
func (d *Depth) GetSpreadPercentage() (float64, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
askHead, err := d.asks.getHeadPriceNoLock()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
bidHead, err := d.bids.getHeadPriceNoLock()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return (askHead - bidHead) / askHead * 100, nil
|
||||
}
|
||||
|
||||
// GetImbalance returns top orderbook imbalance
|
||||
func (d *Depth) GetImbalance() (float64, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
askVolume, err := d.asks.getHeadVolumeNoLock()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
bidVolume, err := d.bids.getHeadVolumeNoLock()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return (bidVolume - askVolume) / (bidVolume + askVolume), nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,28 @@ package orderbook
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/common/math"
|
||||
)
|
||||
|
||||
var errIDCannotBeMatched = errors.New("cannot match ID on linked list")
|
||||
var errCollisionDetected = errors.New("cannot insert update collision detected")
|
||||
var errAmountCannotBeLessOrEqualToZero = errors.New("amount cannot be less or equal to zero")
|
||||
// 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.
|
||||
@@ -340,6 +357,115 @@ func (ll *linkedList) insertUpdates(updts Items, stack *stack, comp comparison)
|
||||
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 {
|
||||
@@ -367,6 +493,115 @@ 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 {
|
||||
@@ -394,6 +629,100 @@ 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)
|
||||
@@ -526,3 +855,67 @@ func shiftBookmark(tip *Node, bookmark, head **Node, updt Item) bool {
|
||||
(*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
|
||||
}
|
||||
|
||||
@@ -30,6 +30,29 @@ var ask = Items{
|
||||
Item{Price: 1356, Amount: 1},
|
||||
}
|
||||
|
||||
var bid = Items{
|
||||
Item{Price: 1336, Amount: 1},
|
||||
Item{Price: 1335, Amount: 1},
|
||||
Item{Price: 1334, Amount: 1},
|
||||
Item{Price: 1333, Amount: 1},
|
||||
Item{Price: 1332, Amount: 1},
|
||||
Item{Price: 1331, Amount: 1},
|
||||
Item{Price: 1330, Amount: 1},
|
||||
Item{Price: 1329, Amount: 1},
|
||||
Item{Price: 1328, Amount: 1},
|
||||
Item{Price: 1327, Amount: 1},
|
||||
Item{Price: 1326, Amount: 1},
|
||||
Item{Price: 1325, Amount: 1},
|
||||
Item{Price: 1324, Amount: 1},
|
||||
Item{Price: 1323, Amount: 1},
|
||||
Item{Price: 1322, Amount: 1},
|
||||
Item{Price: 1321, Amount: 1},
|
||||
Item{Price: 1320, Amount: 1},
|
||||
Item{Price: 1319, Amount: 1},
|
||||
Item{Price: 1318, Amount: 1},
|
||||
Item{Price: 1317, Amount: 1},
|
||||
}
|
||||
|
||||
// Display displays depth content for tests
|
||||
func (ll *linkedList) display() {
|
||||
for tip := ll.head; tip != nil; tip = tip.Next {
|
||||
@@ -1447,3 +1470,674 @@ func TestShiftBookmark(t *testing.T) {
|
||||
t.Fatal("unexpected pointer variable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMovementByBaseAmount(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
Name string
|
||||
BaseAmount float64
|
||||
ReferencePrice float64
|
||||
BidLiquidity Items
|
||||
ExpectedNominal float64
|
||||
ExpectedImpact float64
|
||||
ExpectedCost float64
|
||||
ExpectedError error
|
||||
}{
|
||||
{
|
||||
Name: "no amount",
|
||||
ExpectedError: errBaseAmountInvalid,
|
||||
},
|
||||
{
|
||||
Name: "no reference price",
|
||||
BaseAmount: 1,
|
||||
ExpectedError: errInvalidReferencePrice,
|
||||
},
|
||||
{
|
||||
Name: "not enough liquidity to service quote amount",
|
||||
BaseAmount: 1,
|
||||
ReferencePrice: 1000,
|
||||
ExpectedError: errNoLiquidity,
|
||||
},
|
||||
{
|
||||
Name: "thrasher test",
|
||||
BidLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
|
||||
BaseAmount: 10,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedNominal: 0.8999999999999999,
|
||||
ExpectedImpact: 2,
|
||||
ExpectedCost: 900,
|
||||
},
|
||||
{
|
||||
Name: "consume first tranche",
|
||||
BidLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
|
||||
BaseAmount: 2,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedNominal: 0,
|
||||
ExpectedImpact: 1,
|
||||
ExpectedCost: 0,
|
||||
},
|
||||
{
|
||||
Name: "consume most of first tranche",
|
||||
BidLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
|
||||
BaseAmount: 1.5,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedNominal: 0,
|
||||
ExpectedImpact: 0,
|
||||
ExpectedCost: 0,
|
||||
},
|
||||
{
|
||||
Name: "consume full liquidity",
|
||||
BidLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
|
||||
BaseAmount: 12,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedNominal: 1.0833333333333395,
|
||||
ExpectedImpact: FullLiquidityExhaustedPercentage,
|
||||
ExpectedCost: 1300,
|
||||
},
|
||||
}
|
||||
|
||||
for x := range cases {
|
||||
tt := cases[x]
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
depth := NewDepth(id)
|
||||
depth.LoadSnapshot(tt.BidLiquidity, nil, 0, time.Time{}, true)
|
||||
movement, err := depth.bids.getMovementByBase(tt.BaseAmount, tt.ReferencePrice, false)
|
||||
if !errors.Is(err, tt.ExpectedError) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, tt.ExpectedError)
|
||||
}
|
||||
|
||||
if movement == nil {
|
||||
return
|
||||
}
|
||||
if movement.NominalPercentage != tt.ExpectedNominal {
|
||||
t.Fatalf("nominal received: '%v' but expected: '%v'",
|
||||
movement.NominalPercentage, tt.ExpectedNominal)
|
||||
}
|
||||
|
||||
if movement.ImpactPercentage != tt.ExpectedImpact {
|
||||
t.Fatalf("impact received: '%v' but expected: '%v'",
|
||||
movement.ImpactPercentage, tt.ExpectedImpact)
|
||||
}
|
||||
|
||||
if movement.SlippageCost != tt.ExpectedCost {
|
||||
t.Fatalf("cost received: '%v' but expected: '%v'",
|
||||
movement.SlippageCost, tt.ExpectedCost)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBaseAmountFromNominalSlippage(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
Name string
|
||||
NominalSlippage float64
|
||||
ReferencePrice float64
|
||||
BidLiquidity Items
|
||||
ExpectedShift *Movement
|
||||
ExpectedError error
|
||||
}{
|
||||
{
|
||||
Name: "invalid slippage",
|
||||
NominalSlippage: -1,
|
||||
ExpectedError: errInvalidNominalSlippage,
|
||||
},
|
||||
{
|
||||
Name: "invalid slippage - larger than 100%",
|
||||
NominalSlippage: 101,
|
||||
ExpectedError: errInvalidSlippageCannotExceed100,
|
||||
},
|
||||
{
|
||||
Name: "no reference price",
|
||||
NominalSlippage: 1,
|
||||
ExpectedError: errInvalidReferencePrice,
|
||||
},
|
||||
{
|
||||
Name: "no liquidity to service quote amount",
|
||||
NominalSlippage: 1,
|
||||
ReferencePrice: 1000,
|
||||
ExpectedError: errNoLiquidity,
|
||||
},
|
||||
{
|
||||
Name: "thrasher test",
|
||||
BidLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
|
||||
NominalSlippage: 1,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 11,
|
||||
Purchased: 108900,
|
||||
AverageOrderCost: 9900,
|
||||
NominalPercentage: 1,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 9800,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "consume first tranche - take one amount out of second",
|
||||
BidLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
|
||||
NominalSlippage: 0.33333333333334,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 3.0000000000000275, // <- expected rounding issue
|
||||
Purchased: 29900.00000000027,
|
||||
AverageOrderCost: 9966.666666666664,
|
||||
NominalPercentage: 0.33333333333334,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 9900,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "consume full liquidity",
|
||||
BidLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
|
||||
NominalSlippage: 10,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 12,
|
||||
Purchased: 118700,
|
||||
AverageOrderCost: 9891.666666666666,
|
||||
NominalPercentage: 1.0833333333333395,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 9800,
|
||||
FullBookSideConsumed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "scotts lovely slippery slippage requirements",
|
||||
BidLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
|
||||
NominalSlippage: 0.00000000000000000000000000000000000000000000000000000000000000000000000000000000001,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 2,
|
||||
Purchased: 20000,
|
||||
AverageOrderCost: 10000,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 10000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for x := range cases {
|
||||
tt := cases[x]
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
depth := NewDepth(id)
|
||||
depth.LoadSnapshot(tt.BidLiquidity, nil, 0, time.Time{}, true)
|
||||
base, err := depth.bids.hitBidsByNominalSlippage(tt.NominalSlippage, tt.ReferencePrice)
|
||||
if !errors.Is(err, tt.ExpectedError) {
|
||||
t.Fatalf("%s received: '%v' but expected: '%v'",
|
||||
tt.Name, err, tt.ExpectedError)
|
||||
}
|
||||
if !base.IsEqual(tt.ExpectedShift) {
|
||||
t.Fatalf("%s quote received: '%+v' but expected: '%+v'",
|
||||
tt.Name, base, tt.ExpectedShift)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// IsEqual is a tester function for comparison.
|
||||
func (m *Movement) IsEqual(that *Movement) bool {
|
||||
if m == nil || that == nil {
|
||||
return m == nil && that == nil
|
||||
}
|
||||
return m.FullBookSideConsumed == that.FullBookSideConsumed &&
|
||||
m.Sold == that.Sold &&
|
||||
m.Purchased == that.Purchased &&
|
||||
m.NominalPercentage == that.NominalPercentage &&
|
||||
m.ImpactPercentage == that.ImpactPercentage &&
|
||||
m.EndPrice == that.EndPrice &&
|
||||
m.StartPrice == that.StartPrice &&
|
||||
m.AverageOrderCost == that.AverageOrderCost
|
||||
}
|
||||
|
||||
func TestGetBaseAmountFromImpact(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
Name string
|
||||
ImpactSlippage float64
|
||||
ReferencePrice float64
|
||||
BidLiquidity Items
|
||||
ExpectedShift *Movement
|
||||
ExpectedError error
|
||||
}{
|
||||
{
|
||||
Name: "invalid slippage",
|
||||
ExpectedError: errInvalidImpactSlippage,
|
||||
},
|
||||
{
|
||||
Name: "invalid slippage - exceed 100%",
|
||||
ImpactSlippage: 101,
|
||||
ExpectedError: errInvalidSlippageCannotExceed100,
|
||||
},
|
||||
{
|
||||
Name: "no reference price",
|
||||
ImpactSlippage: 1,
|
||||
ExpectedError: errInvalidReferencePrice,
|
||||
},
|
||||
{
|
||||
Name: "no liquidity",
|
||||
ImpactSlippage: 1,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedError: errNoLiquidity,
|
||||
},
|
||||
{
|
||||
Name: "thrasher test",
|
||||
BidLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
|
||||
ImpactSlippage: 1,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 2,
|
||||
Purchased: 20000,
|
||||
ImpactPercentage: 1,
|
||||
AverageOrderCost: 10000,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 9900,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "consume first tranche and second tranche",
|
||||
BidLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
|
||||
ImpactSlippage: 2,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 9,
|
||||
Purchased: 89300,
|
||||
AverageOrderCost: 9922.222222222223,
|
||||
ImpactPercentage: 2,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 9800,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "consume full liquidity",
|
||||
BidLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 9900, Amount: 7}, {Price: 9800, Amount: 3}},
|
||||
ImpactSlippage: 10,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 12,
|
||||
Purchased: 118700,
|
||||
ImpactPercentage: FullLiquidityExhaustedPercentage,
|
||||
AverageOrderCost: 9891.666666666666,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 9800,
|
||||
FullBookSideConsumed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for x := range cases {
|
||||
tt := cases[x]
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
depth := NewDepth(id)
|
||||
depth.LoadSnapshot(tt.BidLiquidity, nil, 0, time.Time{}, true)
|
||||
base, err := depth.bids.hitBidsByImpactSlippage(tt.ImpactSlippage, tt.ReferencePrice)
|
||||
if !errors.Is(err, tt.ExpectedError) {
|
||||
t.Fatalf("%s received: '%v' but expected: '%v'", tt.Name, err, tt.ExpectedError)
|
||||
}
|
||||
if !base.IsEqual(tt.ExpectedShift) {
|
||||
t.Fatalf("%s quote received: '%+v' but expected: '%+v'",
|
||||
tt.Name, base, tt.ExpectedShift)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMovementByQuoteAmount(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
Name string
|
||||
QuoteAmount float64
|
||||
ReferencePrice float64
|
||||
AskLiquidity Items
|
||||
ExpectedNominal float64
|
||||
ExpectedImpact float64
|
||||
ExpectedCost float64
|
||||
ExpectedError error
|
||||
}{
|
||||
{
|
||||
Name: "no amount",
|
||||
ExpectedError: errQuoteAmountInvalid,
|
||||
},
|
||||
{
|
||||
Name: "no reference price",
|
||||
QuoteAmount: 1,
|
||||
ExpectedError: errInvalidReferencePrice,
|
||||
},
|
||||
{
|
||||
Name: "not enough liquidity to service quote amount",
|
||||
QuoteAmount: 1,
|
||||
ReferencePrice: 1000,
|
||||
ExpectedError: errNoLiquidity,
|
||||
},
|
||||
{
|
||||
Name: "thrasher test",
|
||||
AskLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
|
||||
QuoteAmount: 100900,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedNominal: 0.8999999999999999,
|
||||
ExpectedImpact: 2,
|
||||
ExpectedCost: 900,
|
||||
},
|
||||
{
|
||||
Name: "consume first tranche",
|
||||
AskLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
|
||||
QuoteAmount: 20000,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedNominal: 0,
|
||||
ExpectedImpact: 1,
|
||||
ExpectedCost: 0,
|
||||
},
|
||||
{
|
||||
Name: "consume most of first tranche",
|
||||
AskLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
|
||||
QuoteAmount: 15000,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedNominal: 0,
|
||||
ExpectedImpact: 0,
|
||||
ExpectedCost: 0,
|
||||
},
|
||||
{
|
||||
Name: "consume full liquidity",
|
||||
AskLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
|
||||
QuoteAmount: 121300,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedNominal: 1.0833333333333395,
|
||||
ExpectedImpact: FullLiquidityExhaustedPercentage,
|
||||
ExpectedCost: 1300,
|
||||
},
|
||||
}
|
||||
|
||||
for x := range cases {
|
||||
tt := cases[x]
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
depth := NewDepth(id)
|
||||
depth.LoadSnapshot(nil, tt.AskLiquidity, 0, time.Time{}, true)
|
||||
movement, err := depth.asks.getMovementByQuotation(tt.QuoteAmount, tt.ReferencePrice, false)
|
||||
if !errors.Is(err, tt.ExpectedError) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, tt.ExpectedError)
|
||||
}
|
||||
|
||||
if movement == nil {
|
||||
return
|
||||
}
|
||||
if movement.NominalPercentage != tt.ExpectedNominal {
|
||||
t.Fatalf("nominal received: '%v' but expected: '%v'",
|
||||
movement.NominalPercentage, tt.ExpectedNominal)
|
||||
}
|
||||
|
||||
if movement.ImpactPercentage != tt.ExpectedImpact {
|
||||
t.Fatalf("impact received: '%v' but expected: '%v'",
|
||||
movement.ImpactPercentage, tt.ExpectedImpact)
|
||||
}
|
||||
|
||||
if movement.SlippageCost != tt.ExpectedCost {
|
||||
t.Fatalf("cost received: '%v' but expected: '%v'",
|
||||
movement.SlippageCost, tt.ExpectedCost)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetQuoteAmountFromNominalSlippage(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
Name string
|
||||
NominalSlippage float64
|
||||
ReferencePrice float64
|
||||
AskLiquidity Items
|
||||
ExpectedShift *Movement
|
||||
ExpectedError error
|
||||
}{
|
||||
{
|
||||
Name: "invalid slippage",
|
||||
NominalSlippage: -1,
|
||||
ExpectedError: errInvalidNominalSlippage,
|
||||
},
|
||||
{
|
||||
Name: "no reference price",
|
||||
NominalSlippage: 1,
|
||||
ExpectedError: errInvalidReferencePrice,
|
||||
},
|
||||
{
|
||||
Name: "no liquidity",
|
||||
NominalSlippage: 1,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedError: errNoLiquidity,
|
||||
},
|
||||
{
|
||||
Name: "consume first tranche - one amount on second tranche",
|
||||
AskLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
|
||||
NominalSlippage: 0.33333333333334,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 30100.000000000276, // <- expected rounding issue
|
||||
Purchased: 3.0000000000000275,
|
||||
AverageOrderCost: 10033.333333333333333333333333333,
|
||||
NominalPercentage: 0.33333333333334,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 10100,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "last tranche total agg meeting 1 percent nominally",
|
||||
AskLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
|
||||
NominalSlippage: 1,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 111100,
|
||||
Purchased: 11,
|
||||
AverageOrderCost: 10100,
|
||||
NominalPercentage: 1,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 10200,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "take full second tranche",
|
||||
AskLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
|
||||
NominalSlippage: 0.7777777777777738,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 90700,
|
||||
Purchased: 9,
|
||||
AverageOrderCost: 10077.777777777777,
|
||||
NominalPercentage: 0.7777777777777738,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 10100,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "consume full liquidity",
|
||||
AskLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
|
||||
NominalSlippage: 10,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 121300,
|
||||
Purchased: 12,
|
||||
AverageOrderCost: 10108.333333333334,
|
||||
NominalPercentage: 1.0833333333333395,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 10200,
|
||||
FullBookSideConsumed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "scotts lovely slippery slippage requirements",
|
||||
AskLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
|
||||
NominalSlippage: 0.00000000000000000000000000000000000000000000000000000000000000000000000000000000001,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 20000,
|
||||
Purchased: 2,
|
||||
AverageOrderCost: 10000,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 10000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for x := range cases {
|
||||
tt := cases[x]
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
depth := NewDepth(id)
|
||||
depth.LoadSnapshot(nil, tt.AskLiquidity, 0, time.Time{}, true)
|
||||
quote, err := depth.asks.liftAsksByNominalSlippage(tt.NominalSlippage, tt.ReferencePrice)
|
||||
if !errors.Is(err, tt.ExpectedError) {
|
||||
t.Fatalf("%s received: '%v' but expected: '%v'", tt.Name, err, tt.ExpectedError)
|
||||
}
|
||||
if !quote.IsEqual(tt.ExpectedShift) {
|
||||
t.Fatalf("%s quote received: '%+v' but expected: '%+v'",
|
||||
tt.Name, quote, tt.ExpectedShift)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetQuoteAmountFromImpact(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
Name string
|
||||
ImpactSlippage float64
|
||||
ReferencePrice float64
|
||||
AskLiquidity Items
|
||||
ExpectedShift *Movement
|
||||
ExpectedError error
|
||||
}{
|
||||
{
|
||||
Name: "invalid slippage",
|
||||
ImpactSlippage: -1,
|
||||
ExpectedError: errInvalidImpactSlippage,
|
||||
},
|
||||
{
|
||||
Name: "no reference price",
|
||||
ImpactSlippage: 1,
|
||||
ExpectedError: errInvalidReferencePrice,
|
||||
},
|
||||
{
|
||||
Name: "no liquidity",
|
||||
ImpactSlippage: 1,
|
||||
ReferencePrice: 1000,
|
||||
ExpectedError: errNoLiquidity,
|
||||
},
|
||||
{
|
||||
Name: "thrasher test",
|
||||
AskLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
|
||||
ImpactSlippage: 1,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 20000,
|
||||
Purchased: 2,
|
||||
AverageOrderCost: 10000,
|
||||
ImpactPercentage: 1,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 10100,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "consume first tranche and second tranche",
|
||||
AskLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
|
||||
ImpactSlippage: 2,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 90700,
|
||||
Purchased: 9,
|
||||
AverageOrderCost: 10077.777777777777777777777777778,
|
||||
ImpactPercentage: 2,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 10200,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "consume full liquidity",
|
||||
AskLiquidity: Items{{Price: 10000, Amount: 2}, {Price: 10100, Amount: 7}, {Price: 10200, Amount: 3}},
|
||||
ImpactSlippage: 10,
|
||||
ReferencePrice: 10000,
|
||||
ExpectedShift: &Movement{
|
||||
Sold: 121300,
|
||||
Purchased: 12,
|
||||
AverageOrderCost: 10108.333333333333333333333333333,
|
||||
ImpactPercentage: FullLiquidityExhaustedPercentage,
|
||||
StartPrice: 10000,
|
||||
EndPrice: 10200,
|
||||
FullBookSideConsumed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for x := range cases {
|
||||
tt := cases[x]
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
depth := NewDepth(id)
|
||||
depth.LoadSnapshot(nil, tt.AskLiquidity, 0, time.Time{}, true)
|
||||
quote, err := depth.asks.liftAsksByImpactSlippage(tt.ImpactSlippage, tt.ReferencePrice)
|
||||
if !errors.Is(err, tt.ExpectedError) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, tt.ExpectedError)
|
||||
}
|
||||
if !quote.IsEqual(tt.ExpectedShift) {
|
||||
t.Fatalf("%s quote received: '%+v' but expected: '%+v'",
|
||||
tt.Name, quote, tt.ExpectedShift)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHeadPrice(t *testing.T) {
|
||||
t.Parallel()
|
||||
depth := NewDepth(id)
|
||||
if _, err := depth.bids.getHeadPriceNoLock(); !errors.Is(err, errNoLiquidity) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
|
||||
}
|
||||
if _, err := depth.asks.getHeadPriceNoLock(); !errors.Is(err, errNoLiquidity) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errNoLiquidity)
|
||||
}
|
||||
depth.LoadSnapshot(bid, ask, 0, time.Time{}, true)
|
||||
|
||||
val, err := depth.bids.getHeadPriceNoLock()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if val != 1336 {
|
||||
t.Fatal("unexpected value")
|
||||
}
|
||||
|
||||
val, err = depth.asks.getHeadPriceNoLock()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if val != 1337 {
|
||||
t.Fatal("unexpected value", val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalizeFields(t *testing.T) {
|
||||
m := &Movement{}
|
||||
_, err := m.finalizeFields(0, 0, 0, 0, false)
|
||||
if !errors.Is(err, errInvalidCost) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidCost)
|
||||
}
|
||||
_, err = m.finalizeFields(1, 0, 0, 0, false)
|
||||
if !errors.Is(err, errInvalidAmount) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidAmount)
|
||||
}
|
||||
_, err = m.finalizeFields(1, 1, 0, 0, false)
|
||||
if !errors.Is(err, errInvalidHeadPrice) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidHeadPrice)
|
||||
}
|
||||
|
||||
// Test slippage as per https://en.wikipedia.org/wiki/Slippage_(finance)
|
||||
mov, err := m.finalizeFields(20000*151.11585, 20000, 151.08, 0, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if mov.SlippageCost != 716.9999999995343 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", mov.SlippageCost, 716.9999999995343)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,3 +148,40 @@ type Update struct {
|
||||
// only exchange utilising this field.
|
||||
MaxDepth int
|
||||
}
|
||||
|
||||
// Movement defines orderbook traversal details from either hitting the bids or
|
||||
// lifting the asks.
|
||||
type Movement struct {
|
||||
// NominalPercentage (real-world) defines how far in percentage terms is
|
||||
// your average order price away from the reference price.
|
||||
NominalPercentage float64
|
||||
// ImpactPercentage defines how far the price has moved on the order book
|
||||
// from the reference price.
|
||||
ImpactPercentage float64
|
||||
// SlippageCost is the cost of the slippage. This is priced in quotation.
|
||||
SlippageCost float64
|
||||
// StartPrice defines the reference price or the head of the orderbook side.
|
||||
StartPrice float64
|
||||
// EndPrice defines where the price has ended on the orderbook side.
|
||||
EndPrice float64
|
||||
// Sold defines the amount of currency sold.
|
||||
Sold float64
|
||||
// Purchases defines the amount of currency purchased.
|
||||
Purchased float64
|
||||
// AverageOrderCost defines the average order cost of position as it slips
|
||||
// through the orderbook tranches.
|
||||
AverageOrderCost float64
|
||||
// FullBookSideConsumed defines if the orderbook liquidty has been consumed
|
||||
// by the requested amount. This might not represent the actual book on the
|
||||
// exchange as they might restrict the amount of information being passed
|
||||
// back from either a REST request or websocket stream.
|
||||
FullBookSideConsumed bool
|
||||
}
|
||||
|
||||
// SideAmounts define the amounts total for the tranches, total value in
|
||||
// quotation and the cumulative base amounts.
|
||||
type SideAmounts struct {
|
||||
Tranches int64
|
||||
QuoteValue float64
|
||||
BaseAmount float64
|
||||
}
|
||||
|
||||
@@ -12,14 +12,18 @@ import (
|
||||
func TestSimulate(t *testing.T) {
|
||||
b := bitstamp.Bitstamp{}
|
||||
b.SetDefaults()
|
||||
b.Verbose = false
|
||||
o, err := b.FetchOrderbook(context.Background(),
|
||||
currency.NewPair(currency.BTC, currency.USD), asset.Spot)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
r := o.SimulateOrder(10000000, true)
|
||||
t.Log(r.Status)
|
||||
r = o.SimulateOrder(2171, false)
|
||||
t.Log(r.Status)
|
||||
_, err = o.SimulateOrder(10000000, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = o.SimulateOrder(2171, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user