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:
Ryan O'Hara-Reid
2022-10-14 16:43:37 +11:00
committed by GitHub
parent 74d0cc323d
commit 9acbdbf203
21 changed files with 9150 additions and 3037 deletions

View File

@@ -85,7 +85,10 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *
return f, err
}
// calculate an estimated slippage rate
price, amount = slippage.CalculateSlippageByOrderbook(ob, o.GetDirection(), allocatedFunds, f.ExchangeFee)
price, amount, err = slippage.CalculateSlippageByOrderbook(ob, o.GetDirection(), allocatedFunds, f.ExchangeFee)
if err != nil {
return f, err
}
f.Slippage = price.Sub(f.ClosePrice).Div(f.ClosePrice).Mul(decimal.NewFromInt(100))
} else {
slippageRate := slippage.EstimateSlippagePercentage(cs.MinimumSlippageRate, cs.MaximumSlippageRate)

View File

@@ -31,8 +31,12 @@ func EstimateSlippagePercentage(maximumSlippageRate, minimumSlippageRate decimal
// CalculateSlippageByOrderbook will analyse a provided orderbook and return the result of attempting to
// place the order on there
func CalculateSlippageByOrderbook(ob *orderbook.Base, side gctorder.Side, allocatedFunds, feeRate decimal.Decimal) (price, amount decimal.Decimal) {
result := ob.SimulateOrder(allocatedFunds.InexactFloat64(), side == gctorder.Buy)
func CalculateSlippageByOrderbook(ob *orderbook.Base, side gctorder.Side, allocatedFunds, feeRate decimal.Decimal) (price, amount decimal.Decimal, err error) {
var result *orderbook.WhaleBombResult
result, err = ob.SimulateOrder(allocatedFunds.InexactFloat64(), side == gctorder.Buy)
if err != nil {
return
}
rate := (result.MinimumPrice - result.MaximumPrice) / result.MaximumPrice
price = decimal.NewFromFloat(result.MinimumPrice * (rate + 1))
amount = decimal.NewFromFloat(result.Amount * (1 - feeRate.InexactFloat64()))

View File

@@ -30,7 +30,10 @@ func TestCalculateSlippageByOrderbook(t *testing.T) {
}
amountOfFunds := decimal.NewFromInt(1000)
feeRate := decimal.NewFromFloat(0.03)
price, amount := CalculateSlippageByOrderbook(ob, gctorder.Buy, amountOfFunds, feeRate)
price, amount, err := CalculateSlippageByOrderbook(ob, gctorder.Buy, amountOfFunds, feeRate)
if err != nil {
t.Fatal(err)
}
if price.Mul(amount).Add(price.Mul(amount).Mul(feeRate)).GreaterThan(amountOfFunds) {
t.Error("order size must be less than funds")
}

View File

@@ -585,118 +585,6 @@ func getTickers(c *cli.Context) error {
return nil
}
var getOrderbookCommand = &cli.Command{
Name: "getorderbook",
Usage: "gets the orderbook for a specific currency pair and exchange",
ArgsUsage: "<exchange> <pair> <asset>",
Action: getOrderbook,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "exchange",
Usage: "the exchange to get the orderbook for",
},
&cli.StringFlag{
Name: "pair",
Usage: "the currency pair to get the orderbook for",
},
&cli.StringFlag{
Name: "asset",
Usage: "the asset type of the currency pair to get the orderbook for",
},
},
}
func getOrderbook(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowCommandHelp(c, "getorderbook")
}
var exchangeName string
var currencyPair string
var assetType string
if c.IsSet("exchange") {
exchangeName = c.String("exchange")
} else {
exchangeName = c.Args().First()
}
if c.IsSet("pair") {
currencyPair = c.String("pair")
} else {
currencyPair = c.Args().Get(1)
}
if !validPair(currencyPair) {
return errInvalidPair
}
if c.IsSet("asset") {
assetType = c.String("asset")
} else {
assetType = c.Args().Get(2)
}
assetType = strings.ToLower(assetType)
if !validAsset(assetType) {
return errInvalidAsset
}
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.GetOrderbook(c.Context,
&gctrpc.GetOrderbookRequest{
Exchange: exchangeName,
Pair: &gctrpc.CurrencyPair{
Delimiter: p.Delimiter,
Base: p.Base.String(),
Quote: p.Quote.String(),
},
AssetType: assetType,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}
var getOrderbooksCommand = &cli.Command{
Name: "getorderbooks",
Usage: "gets all orderbooks for all enabled exchanges and currency pairs",
Action: getOrderbooks,
}
func getOrderbooks(c *cli.Context) error {
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.GetOrderbooks(c.Context, &gctrpc.GetOrderbooksRequest{})
if err != nil {
return err
}
jsonOutput(result)
return nil
}
var getAccountInfoCommand = &cli.Command{
Name: "getaccountinfo",
Usage: "gets the exchange account balance info",
@@ -1766,107 +1654,6 @@ func simulateOrder(c *cli.Context) error {
return nil
}
var whaleBombCommand = &cli.Command{
Name: "whalebomb",
Usage: "whale bomb finds the amount required to reach a price target",
ArgsUsage: "<exchange> <pair> <side> <price>",
Action: whaleBomb,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "exchange",
Usage: "the exchange to whale bomb",
},
&cli.StringFlag{
Name: "pair",
Usage: "the currency pair",
},
&cli.StringFlag{
Name: "side",
Usage: "the order side to use (BUY OR SELL)",
},
&cli.Float64Flag{
Name: "price",
Usage: "the price target",
},
},
}
func whaleBomb(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowCommandHelp(c, "whalebomb")
}
var exchangeName string
var currencyPair string
var orderSide string
var price float64
if c.IsSet("exchange") {
exchangeName = c.String("exchange")
} else {
exchangeName = c.Args().First()
}
if c.IsSet("pair") {
currencyPair = c.String("pair")
} else {
currencyPair = c.Args().Get(1)
}
if !validPair(currencyPair) {
return errInvalidPair
}
if c.IsSet("side") {
orderSide = c.String("side")
} else {
orderSide = c.Args().Get(2)
}
if orderSide == "" {
return errors.New("order side must be set")
}
if c.IsSet("price") {
price = c.Float64("price")
} else if c.Args().Get(3) != "" {
var err error
price, err = strconv.ParseFloat(c.Args().Get(3), 64)
if err != nil {
return err
}
}
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.WhaleBomb(c.Context, &gctrpc.WhaleBombRequest{
Exchange: exchangeName,
Pair: &gctrpc.CurrencyPair{
Delimiter: p.Delimiter,
Base: p.Base.String(),
Quote: p.Quote.String(),
},
Side: orderSide,
PriceTarget: price,
})
if err != nil {
return err
}
jsonOutput(result)
return nil
}
var cancelOrderCommand = &cli.Command{
Name: "cancelorder",
Usage: "cancel order cancels an exchange order",
@@ -3354,212 +3141,6 @@ func setLoggerDetails(c *cli.Context) error {
return nil
}
var getOrderbookStreamCommand = &cli.Command{
Name: "getorderbookstream",
Usage: "gets the orderbook stream for a specific currency pair and exchange",
ArgsUsage: "<exchange> <pair> <asset>",
Action: getOrderbookStream,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "exchange",
Usage: "the exchange to get the orderbook from",
},
&cli.StringFlag{
Name: "pair",
Usage: "currency pair",
},
&cli.StringFlag{
Name: "asset",
Usage: "the asset type of the currency pair",
},
},
}
func getOrderbookStream(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowCommandHelp(c, "getorderbookstream")
}
var exchangeName string
var pair string
var assetType string
if c.IsSet("exchange") {
exchangeName = c.String("exchange")
} else {
exchangeName = c.Args().First()
}
if c.IsSet("pair") {
pair = c.String("pair")
} else {
pair = c.Args().Get(1)
}
if !validPair(pair) {
return errInvalidPair
}
if c.IsSet("asset") {
assetType = c.String("asset")
} else {
assetType = c.Args().Get(2)
}
assetType = strings.ToLower(assetType)
if !validAsset(assetType) {
return errInvalidAsset
}
p, err := currency.NewPairDelimiter(pair, pairDelimiter)
if err != nil {
return err
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.GetOrderbookStream(c.Context,
&gctrpc.GetOrderbookStreamRequest{
Exchange: exchangeName,
Pair: &gctrpc.CurrencyPair{
Base: p.Base.String(),
Quote: p.Quote.String(),
Delimiter: p.Delimiter,
},
AssetType: assetType,
},
)
if err != nil {
return err
}
for {
resp, err := result.Recv()
if err != nil {
return err
}
err = clearScreen()
if err != nil {
return err
}
fmt.Printf("Orderbook stream for %s %s:\n\n", exchangeName, resp.Pair)
if resp.Error != "" {
fmt.Printf("%s\n", resp.Error)
continue
}
fmt.Println("\t\tBids\t\t\t\tAsks")
fmt.Println()
bidLen := len(resp.Bids) - 1
askLen := len(resp.Asks) - 1
var maxLen int
if bidLen >= askLen {
maxLen = bidLen
} else {
maxLen = askLen
}
for i := 0; i < maxLen; i++ {
var bidAmount, bidPrice float64
if i <= bidLen {
bidAmount = resp.Bids[i].Amount
bidPrice = resp.Bids[i].Price
}
var askAmount, askPrice float64
if i <= askLen {
askAmount = resp.Asks[i].Amount
askPrice = resp.Asks[i].Price
}
fmt.Printf("%.8f %s @ %.8f %s\t\t%.8f %s @ %.8f %s\n",
bidAmount,
resp.Pair.Base,
bidPrice,
resp.Pair.Quote,
askAmount,
resp.Pair.Base,
askPrice,
resp.Pair.Quote)
if i >= 49 {
// limits orderbook display output
break
}
}
}
}
var getExchangeOrderbookStreamCommand = &cli.Command{
Name: "getexchangeorderbookstream",
Usage: "gets a stream for all orderbooks associated with an exchange",
ArgsUsage: "<exchange>",
Action: getExchangeOrderbookStream,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "exchange",
Usage: "the exchange to get the orderbook from",
},
},
}
func getExchangeOrderbookStream(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowCommandHelp(c, "getexchangeorderbookstream")
}
var exchangeName string
if c.IsSet("exchange") {
exchangeName = c.String("exchange")
} else {
exchangeName = c.Args().First()
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.GetExchangeOrderbookStream(c.Context,
&gctrpc.GetExchangeOrderbookStreamRequest{
Exchange: exchangeName,
})
if err != nil {
return err
}
for {
resp, err := result.Recv()
if err != nil {
return err
}
err = clearScreen()
if err != nil {
return err
}
fmt.Printf("Orderbook streamed for %s %s", exchangeName, resp.Pair)
if resp.Error != "" {
fmt.Printf("%s\n", resp.Error)
}
}
}
var getTickerStreamCommand = &cli.Command{
Name: "gettickerstream",
Usage: "gets the ticker stream for a specific currency pair and exchange",

View File

@@ -162,8 +162,6 @@ func main() {
getExchangeInfoCommand,
getTickerCommand,
getTickersCommand,
getOrderbookCommand,
getOrderbooksCommand,
getAccountInfoCommand,
getAccountInfoStreamCommand,
updateAccountInfoCommand,
@@ -179,7 +177,6 @@ func main() {
getOrderCommand,
submitOrderCommand,
simulateOrderCommand,
whaleBombCommand,
cancelOrderCommand,
cancelBatchOrdersCommand,
cancelAllOrdersCommand,
@@ -196,8 +193,6 @@ func main() {
getLoggerDetailsCommand,
setLoggerDetailsCommand,
exchangePairManagerCommand,
getOrderbookStreamCommand,
getExchangeOrderbookStreamCommand,
getTickerStreamCommand,
getExchangeTickerStreamCommand,
getAuditEventCommand,
@@ -213,6 +208,7 @@ func main() {
shutdownCommand,
technicalAnalysisCommand,
getMarginRatesHistoryCommand,
orderbookCommand,
}
ctx, cancel := context.WithCancel(context.Background())

768
cmd/gctcli/orderbook.go Normal file
View File

@@ -0,0 +1,768 @@
package main
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/gctrpc"
"github.com/urfave/cli/v2"
)
var orderbookCommonFlags = []cli.Flag{
&cli.StringFlag{
Name: "exchange",
Usage: "the exchange to get the orderbook for",
},
&cli.StringFlag{
Name: "pair",
Usage: "the currency pair to get the orderbook for",
},
&cli.StringFlag{
Name: "asset",
Usage: "the asset type of the currency pair to get the orderbook for",
},
}
var orderbookCommand = &cli.Command{
Name: "orderbook",
Usage: "orderbook system simulations and analytics command",
ArgsUsage: "<command> <args>",
Subcommands: []*cli.Command{
{
Name: "sell",
Usage: "simulates sell to derive orderbook liquidity impact information",
ArgsUsage: "<command> <args>",
Subcommands: []*cli.Command{nominal, impact, base, quoteRequired},
Flags: []cli.Flag{&cli.BoolFlag{Name: "sell", Hidden: true, Value: true}},
},
{
Name: "buy",
Usage: "simulates buy to derive orderbook liquidity impact information",
ArgsUsage: "<command> <args>",
Subcommands: []*cli.Command{nominal, impact, quote, baseRequired},
},
getOrderbookCommand,
getOrderbooksCommand,
getOrderbookStreamCommand,
getExchangeOrderbookStreamCommand,
whaleBombCommand,
},
}
var nominal = &cli.Command{
Name: "nominal",
Usage: "simulates a buy or sell based off the percentage between the reference price and the average order cost",
ArgsUsage: "<exchange> <pair> <asset> <percent>",
Action: getNominal,
Flags: append(orderbookCommonFlags, &cli.Float64Flag{
Name: "percent",
Usage: "the max percentage slip you wish to occur e.g. 1 = 1% and 100 = 100%. Note: If selling base/hitting the bids you can only have a max value of 100%",
}),
}
func getNominal(c *cli.Context) error {
isSelling := c.Bool("sell")
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowCommandHelp(c, "nominal")
}
var exchangeName string
if c.IsSet("exchange") {
exchangeName = c.String("exchange")
} else {
exchangeName = c.Args().First()
}
var currencyPair string
if c.IsSet("pair") {
currencyPair = c.String("pair")
} else {
currencyPair = c.Args().Get(1)
}
if !validPair(currencyPair) {
return errInvalidPair
}
var assetType string
if c.IsSet("asset") {
assetType = c.String("asset")
} else {
assetType = c.Args().Get(2)
}
assetType = strings.ToLower(assetType)
if !validAsset(assetType) {
return errInvalidAsset
}
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
var percentage float64
if c.IsSet("asset") {
percentage = c.Float64("percent")
} else {
percentage, _ = strconv.ParseFloat(c.Args().Get(3), 64)
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.GetOrderbookAmountByNominal(c.Context,
&gctrpc.GetOrderbookAmountByNominalRequest{
Exchange: exchangeName,
Pair: &gctrpc.CurrencyPair{
Base: p.Base.String(),
Quote: p.Quote.String(),
},
Asset: assetType,
Sell: isSelling,
NominalPercentage: percentage,
})
if err != nil {
return err
}
jsonOutput(result)
return nil
}
var impact = &cli.Command{
Name: "impact",
Usage: "simulates a buy or sell based off the reference price and the orderbook impact slippage",
ArgsUsage: "<exchange> <pair> <asset> <percent>",
Action: getImpact,
Flags: append(orderbookCommonFlags, &cli.Float64Flag{
Name: "percent",
Usage: "the max percentage slip you wish to occur e.g. 1 = 1% and 100 = 100%. Note: If selling base/hitting the bids you can only have a max value of 100%",
}),
}
func getImpact(c *cli.Context) error {
isSelling := c.Bool("sell")
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowCommandHelp(c, "impact")
}
var exchangeName string
if c.IsSet("exchange") {
exchangeName = c.String("exchange")
} else {
exchangeName = c.Args().First()
}
var currencyPair string
if c.IsSet("pair") {
currencyPair = c.String("pair")
} else {
currencyPair = c.Args().Get(1)
}
if !validPair(currencyPair) {
return errInvalidPair
}
var assetType string
if c.IsSet("asset") {
assetType = c.String("asset")
} else {
assetType = c.Args().Get(2)
}
assetType = strings.ToLower(assetType)
if !validAsset(assetType) {
return errInvalidAsset
}
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
var percentage float64
if c.IsSet("asset") {
percentage = c.Float64("percent")
} else {
percentage, _ = strconv.ParseFloat(c.Args().Get(3), 64)
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.GetOrderbookAmountByImpact(c.Context,
&gctrpc.GetOrderbookAmountByImpactRequest{
Exchange: exchangeName,
Pair: &gctrpc.CurrencyPair{
Base: p.Base.String(),
Quote: p.Quote.String(),
},
Asset: assetType,
Sell: isSelling,
ImpactPercentage: percentage,
})
if err != nil {
return err
}
jsonOutput(result)
return nil
}
var purchase = &cli.BoolFlag{
Name: "purchase",
Hidden: true,
Value: true,
}
var quote = &cli.Command{
Name: "quote",
Usage: "simulates a buy using quotation amount",
ArgsUsage: "<exchange> <pair> <asset> <amount>",
Action: getMovement,
Flags: append(orderbookCommonFlags, &cli.Float64Flag{
Name: "amount",
Usage: "the amount of quotation currency lifting the asks",
}),
}
var baseRequired = &cli.Command{
Name: "baserequired",
Usage: "simulates a buy with a required base amount to be purchased",
ArgsUsage: "<exchange> <pair> <asset> <amount>",
Action: getMovement,
Flags: append(orderbookCommonFlags, &cli.Float64Flag{
Name: "amount",
Usage: "the amount of base currency required to be purchased when lifting the asks",
}, purchase),
}
var base = &cli.Command{
Name: "base",
Usage: "simulates a sell using base amount",
ArgsUsage: "<exchange> <pair> <asset> <amount>",
Action: getMovement,
Flags: append(orderbookCommonFlags, &cli.Float64Flag{
Name: "amount",
Usage: "the amount of base currency hitting the bids",
}),
}
var quoteRequired = &cli.Command{
Name: "quoterequired",
Usage: "simulates a sell with a required quote amount to be purchased",
ArgsUsage: "<exchange> <pair> <asset> <amount>",
Action: getMovement,
Flags: append(orderbookCommonFlags, &cli.Float64Flag{
Name: "amount",
Usage: "the amount of quotation currency required to be purchased when hitting the bids",
}, purchase),
}
func getMovement(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowCommandHelp(c, c.Command.Name)
}
var exchangeName string
if c.IsSet("exchange") {
exchangeName = c.String("exchange")
} else {
exchangeName = c.Args().First()
}
var currencyPair string
if c.IsSet("pair") {
currencyPair = c.String("pair")
} else {
currencyPair = c.Args().Get(1)
}
if !validPair(currencyPair) {
return errInvalidPair
}
var assetType string
if c.IsSet("asset") {
assetType = c.String("asset")
} else {
assetType = c.Args().Get(2)
}
assetType = strings.ToLower(assetType)
if !validAsset(assetType) {
return errInvalidAsset
}
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
var amount float64
if c.IsSet("amount") {
amount = c.Float64("amount")
} else {
amount, _ = strconv.ParseFloat(c.Args().Get(3), 64)
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.GetOrderbookMovement(c.Context, &gctrpc.GetOrderbookMovementRequest{
Exchange: exchangeName,
Pair: &gctrpc.CurrencyPair{
Base: p.Base.String(),
Quote: p.Quote.String(),
},
Asset: assetType,
Sell: c.Bool("sell"),
Amount: amount,
Purchase: c.Bool("purchase"),
})
if err != nil {
return err
}
jsonOutput(result)
return nil
}
var getOrderbookCommand = &cli.Command{
Name: "getorderbook",
Usage: "gets the orderbook for a specific currency pair and exchange",
ArgsUsage: "<exchange> <pair> <asset>",
Action: getOrderbook,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "exchange",
Usage: "the exchange to get the orderbook for",
},
&cli.StringFlag{
Name: "pair",
Usage: "the currency pair to get the orderbook for",
},
&cli.StringFlag{
Name: "asset",
Usage: "the asset type of the currency pair to get the orderbook for",
},
},
}
func getOrderbook(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowCommandHelp(c, "getorderbook")
}
var exchangeName string
var currencyPair string
var assetType string
if c.IsSet("exchange") {
exchangeName = c.String("exchange")
} else {
exchangeName = c.Args().First()
}
if c.IsSet("pair") {
currencyPair = c.String("pair")
} else {
currencyPair = c.Args().Get(1)
}
if !validPair(currencyPair) {
return errInvalidPair
}
if c.IsSet("asset") {
assetType = c.String("asset")
} else {
assetType = c.Args().Get(2)
}
assetType = strings.ToLower(assetType)
if !validAsset(assetType) {
return errInvalidAsset
}
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.GetOrderbook(c.Context,
&gctrpc.GetOrderbookRequest{
Exchange: exchangeName,
Pair: &gctrpc.CurrencyPair{
Delimiter: p.Delimiter,
Base: p.Base.String(),
Quote: p.Quote.String(),
},
AssetType: assetType,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}
var getOrderbooksCommand = &cli.Command{
Name: "getorderbooks",
Usage: "gets all orderbooks for all enabled exchanges and currency pairs",
Action: getOrderbooks,
}
func getOrderbooks(c *cli.Context) error {
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.GetOrderbooks(c.Context, &gctrpc.GetOrderbooksRequest{})
if err != nil {
return err
}
jsonOutput(result)
return nil
}
var getOrderbookStreamCommand = &cli.Command{
Name: "getorderbookstream",
Usage: "gets the orderbook stream for a specific currency pair and exchange",
ArgsUsage: "<exchange> <pair> <asset>",
Action: getOrderbookStream,
Flags: orderbookCommonFlags,
}
func getOrderbookStream(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowCommandHelp(c, "getorderbookstream")
}
var exchangeName string
var pair string
var assetType string
if c.IsSet("exchange") {
exchangeName = c.String("exchange")
} else {
exchangeName = c.Args().First()
}
if c.IsSet("pair") {
pair = c.String("pair")
} else {
pair = c.Args().Get(1)
}
if !validPair(pair) {
return errInvalidPair
}
if c.IsSet("asset") {
assetType = c.String("asset")
} else {
assetType = c.Args().Get(2)
}
assetType = strings.ToLower(assetType)
if !validAsset(assetType) {
return errInvalidAsset
}
p, err := currency.NewPairDelimiter(pair, pairDelimiter)
if err != nil {
return err
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.GetOrderbookStream(c.Context,
&gctrpc.GetOrderbookStreamRequest{
Exchange: exchangeName,
Pair: &gctrpc.CurrencyPair{
Base: p.Base.String(),
Quote: p.Quote.String(),
Delimiter: p.Delimiter,
},
AssetType: assetType,
},
)
if err != nil {
return err
}
for {
resp, err := result.Recv()
if err != nil {
return err
}
err = clearScreen()
if err != nil {
return err
}
fmt.Printf("Orderbook stream for %s %s:\n\n", exchangeName, resp.Pair)
if resp.Error != "" {
fmt.Printf("%s\n", resp.Error)
continue
}
fmt.Println("\t\tBids\t\t\t\tAsks")
fmt.Println()
bidLen := len(resp.Bids) - 1
askLen := len(resp.Asks) - 1
var maxLen int
if bidLen >= askLen {
maxLen = bidLen
} else {
maxLen = askLen
}
for i := 0; i < maxLen; i++ {
var bidAmount, bidPrice float64
if i <= bidLen {
bidAmount = resp.Bids[i].Amount
bidPrice = resp.Bids[i].Price
}
var askAmount, askPrice float64
if i <= askLen {
askAmount = resp.Asks[i].Amount
askPrice = resp.Asks[i].Price
}
fmt.Printf("%.8f %s @ %.8f %s\t\t%.8f %s @ %.8f %s\n",
bidAmount,
resp.Pair.Base,
bidPrice,
resp.Pair.Quote,
askAmount,
resp.Pair.Base,
askPrice,
resp.Pair.Quote)
if i >= 49 {
// limits orderbook display output
break
}
}
}
}
var getExchangeOrderbookStreamCommand = &cli.Command{
Name: "getexchangeorderbookstream",
Usage: "gets a stream for all orderbooks associated with an exchange",
ArgsUsage: "<exchange>",
Action: getExchangeOrderbookStream,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "exchange",
Usage: "the exchange to get the orderbook from",
},
},
}
func getExchangeOrderbookStream(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowCommandHelp(c, "getexchangeorderbookstream")
}
var exchangeName string
if c.IsSet("exchange") {
exchangeName = c.String("exchange")
} else {
exchangeName = c.Args().First()
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.GetExchangeOrderbookStream(c.Context,
&gctrpc.GetExchangeOrderbookStreamRequest{
Exchange: exchangeName,
})
if err != nil {
return err
}
for {
resp, err := result.Recv()
if err != nil {
return err
}
err = clearScreen()
if err != nil {
return err
}
fmt.Printf("Orderbook streamed for %s %s", exchangeName, resp.Pair)
if resp.Error != "" {
fmt.Printf("%s\n", resp.Error)
}
}
}
var whaleBombCommand = &cli.Command{
Name: "whalebomb",
Usage: "whale bomb finds the amount required to reach a price target",
ArgsUsage: "<exchange> <pair> <side> <asset> <price>",
Action: whaleBomb,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "exchange",
Usage: "the exchange to whale bomb",
},
&cli.StringFlag{
Name: "pair",
Usage: "the currency pair",
},
&cli.StringFlag{
Name: "side",
Usage: "the order side to use (BUY OR SELL)",
},
&cli.StringFlag{
Name: "asset",
Usage: "the asset type of the currency pair to get the orderbook for",
},
&cli.Float64Flag{
Name: "price",
Usage: "the price target",
},
},
}
func whaleBomb(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowCommandHelp(c, "whalebomb")
}
var exchangeName string
var currencyPair string
var orderSide string
var price float64
if c.IsSet("exchange") {
exchangeName = c.String("exchange")
} else {
exchangeName = c.Args().First()
}
if c.IsSet("pair") {
currencyPair = c.String("pair")
} else {
currencyPair = c.Args().Get(1)
}
if !validPair(currencyPair) {
return errInvalidPair
}
if c.IsSet("side") {
orderSide = c.String("side")
} else {
orderSide = c.Args().Get(2)
}
if orderSide == "" {
return errors.New("order side must be set")
}
var assetType string
if c.IsSet("asset") {
assetType = c.String("asset")
} else {
assetType = c.Args().Get(3)
}
if c.IsSet("price") {
price = c.Float64("price")
} else if c.Args().Get(4) != "" {
var err error
price, err = strconv.ParseFloat(c.Args().Get(4), 64)
if err != nil {
return err
}
}
p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter)
if err != nil {
return err
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := gctrpc.NewGoCryptoTraderServiceClient(conn)
result, err := client.WhaleBomb(c.Context, &gctrpc.WhaleBombRequest{
Exchange: exchangeName,
Pair: &gctrpc.CurrencyPair{
Delimiter: p.Delimiter,
Base: p.Base.String(),
Quote: p.Quote.String(),
},
Side: orderSide,
PriceTarget: price,
AssetType: assetType,
})
if err != nil {
return err
}
jsonOutput(result)
return nil
}

View File

@@ -1297,7 +1297,11 @@ func (s *RPCServer) SimulateOrder(ctx context.Context, r *gctrpc.SimulateOrderRe
buy = false
}
result := o.SimulateOrder(r.Amount, buy)
result, err := o.SimulateOrder(r.Amount, buy)
if err != nil {
return nil, err
}
var resp gctrpc.SimulateOrderResponse
for x := range result.Orders {
resp.Orders = append(resp.Orders, &gctrpc.OrderbookItem{
@@ -1332,12 +1336,17 @@ func (s *RPCServer) WhaleBomb(ctx context.Context, r *gctrpc.WhaleBombRequest) (
return nil, err
}
err = checkParams(r.Exchange, exch, asset.Spot, p)
a, err := asset.New(r.AssetType)
if err != nil {
return nil, err
}
o, err := exch.FetchOrderbook(ctx, p, asset.Spot)
err = checkParams(r.Exchange, exch, a, p)
if err != nil {
return nil, err
}
o, err := exch.FetchOrderbook(ctx, p, a)
if err != nil {
return nil, err
}
@@ -5333,3 +5342,226 @@ func (s *RPCServer) GetMarginRatesHistory(ctx context.Context, r *gctrpc.GetMarg
return resp, nil
}
// GetOrderbookMovement using the requested amount simulates a buy or sell and
// returns the nominal/impact percentages and costings.
func (s *RPCServer) GetOrderbookMovement(ctx context.Context, r *gctrpc.GetOrderbookMovementRequest) (*gctrpc.GetOrderbookMovementResponse, error) {
exch, err := s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
}
as, err := asset.New(r.Asset)
if err != nil {
return nil, err
}
pair, err := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote)
if err != nil {
return nil, err
}
if pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
err = checkParams(r.Exchange, exch, as, pair)
if err != nil {
return nil, err
}
depth, err := orderbook.GetDepth(exch.GetName(), pair, as)
if err != nil {
return nil, err
}
isRest, err := depth.IsRESTSnapshot()
if err != nil {
return nil, err
}
updateProtocol := "WEBSOCKET"
if isRest {
updateProtocol = "REST"
}
var move *orderbook.Movement
var bought, sold, side string
if r.Sell {
move, err = depth.HitTheBidsFromBest(r.Amount, r.Purchase)
bought = pair.Quote.Upper().String()
sold = pair.Base.Upper().String()
side = order.Bid.String()
} else {
move, err = depth.LiftTheAsksFromBest(r.Amount, r.Purchase)
bought = pair.Base.Upper().String()
sold = pair.Quote.Upper().String()
side = order.Ask.String()
}
if err != nil {
return nil, err
}
return &gctrpc.GetOrderbookMovementResponse{
NominalPercentage: move.NominalPercentage,
ImpactPercentage: move.ImpactPercentage,
SlippageCost: move.SlippageCost,
CurrencyBought: bought,
Bought: move.Purchased,
CurrencySold: sold,
Sold: move.Sold,
SideAffected: side,
UpdateProtocol: updateProtocol,
FullOrderbookSideConsumed: move.FullBookSideConsumed,
NoSlippageOccurred: move.ImpactPercentage == 0,
StartPrice: move.StartPrice,
EndPrice: move.EndPrice,
AverageOrderCost: move.AverageOrderCost,
}, nil
}
// GetOrderbookAmountByNominal using the requested nominal percentage requirement
// returns the amount on orderbook that can fit without exceeding that value.
func (s *RPCServer) GetOrderbookAmountByNominal(ctx context.Context, r *gctrpc.GetOrderbookAmountByNominalRequest) (*gctrpc.GetOrderbookAmountByNominalResponse, error) {
exch, err := s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
}
as, err := asset.New(r.Asset)
if err != nil {
return nil, err
}
pair, err := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote)
if err != nil {
return nil, err
}
if pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
err = checkParams(r.Exchange, exch, as, pair)
if err != nil {
return nil, err
}
depth, err := orderbook.GetDepth(exch.GetName(), pair, as)
if err != nil {
return nil, err
}
isRest, err := depth.IsRESTSnapshot()
if err != nil {
return nil, err
}
updateProtocol := "WEBSOCKET"
if isRest {
updateProtocol = "REST"
}
var nominal *orderbook.Movement
var selling, buying, side string
if r.Sell {
nominal, err = depth.HitTheBidsByNominalSlippageFromBest(r.NominalPercentage)
selling = pair.Upper().Base.String()
buying = pair.Upper().Quote.String()
side = order.Bid.String()
} else {
nominal, err = depth.LiftTheAsksByNominalSlippageFromBest(r.NominalPercentage)
buying = pair.Upper().Base.String()
selling = pair.Upper().Quote.String()
side = order.Ask.String()
}
if err != nil {
return nil, err
}
return &gctrpc.GetOrderbookAmountByNominalResponse{
AmountRequired: nominal.Sold,
CurrencySelling: selling,
AmountReceived: nominal.Purchased,
CurrencyBuying: buying,
SideAffected: side,
ApproximateNominalSlippagePercentage: nominal.NominalPercentage,
UpdateProtocol: updateProtocol,
FullOrderbookSideConsumed: nominal.FullBookSideConsumed,
StartPrice: nominal.StartPrice,
EndPrice: nominal.EndPrice,
AverageOrderCost: nominal.AverageOrderCost,
}, nil
}
// GetOrderbookAmountByImpact using the requested impact percentage requirement
// determines the amount on orderbook that can fit that will slip the orderbook.
func (s *RPCServer) GetOrderbookAmountByImpact(ctx context.Context, r *gctrpc.GetOrderbookAmountByImpactRequest) (*gctrpc.GetOrderbookAmountByImpactResponse, error) {
exch, err := s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
}
as, err := asset.New(r.Asset)
if err != nil {
return nil, err
}
pair, err := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote)
if err != nil {
return nil, err
}
if pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
err = checkParams(r.Exchange, exch, as, pair)
if err != nil {
return nil, err
}
depth, err := orderbook.GetDepth(exch.GetName(), pair, as)
if err != nil {
return nil, err
}
isRest, err := depth.IsRESTSnapshot()
if err != nil {
return nil, err
}
updateProtocol := "WEBSOCKET"
if isRest {
updateProtocol = "REST"
}
var impact *orderbook.Movement
var selling, buying, side string
if r.Sell {
impact, err = depth.HitTheBidsByImpactSlippageFromBest(r.ImpactPercentage)
selling = pair.Upper().Base.String()
buying = pair.Upper().Quote.String()
side = order.Bid.String()
} else {
impact, err = depth.LiftTheAsksByImpactSlippageFromBest(r.ImpactPercentage)
buying = pair.Upper().Base.String()
selling = pair.Upper().Quote.String()
side = order.Ask.String()
}
if err != nil {
return nil, err
}
return &gctrpc.GetOrderbookAmountByImpactResponse{
AmountRequired: impact.Sold,
CurrencySelling: selling,
AmountReceived: impact.Purchased,
CurrencyBuying: buying,
SideAffected: side,
ApproximateImpactSlippagePercentage: impact.ImpactPercentage,
UpdateProtocol: updateProtocol,
FullOrderbookSideConsumed: impact.FullBookSideConsumed,
StartPrice: impact.StartPrice,
EndPrice: impact.EndPrice,
AverageOrderCost: impact.AverageOrderCost,
}, nil
}

View File

@@ -32,6 +32,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
"github.com/thrasher-corp/gocryptotrader/gctrpc"
@@ -3202,3 +3203,312 @@ func TestGetAllManagedPositions(t *testing.T) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
}
func TestGetOrderbookMovement(t *testing.T) {
t.Parallel()
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("ftx")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.Name = fakeExchangeName
b.Enabled = true
cp, err := currency.NewPairFromString("btc-metal")
if err != nil {
t.Fatal(err)
}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Delimiter: "/"},
RequestFormat: &currency.PairFormat{Delimiter: "/"},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
fakeExchange := fExchange{
IBotExchange: exch,
}
em.Add(fakeExchange)
s := RPCServer{Engine: &Engine{ExchangeManager: em}}
req := &gctrpc.GetOrderbookMovementRequest{}
_, err = s.GetOrderbookMovement(context.Background(), req)
if !errors.Is(err, ErrExchangeNameIsEmpty) {
t.Fatalf("received: '%+v' but expected: '%v'", err, ErrExchangeNameIsEmpty)
}
req.Exchange = "fake"
_, err = s.GetOrderbookMovement(context.Background(), req)
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%+v' but expected: '%v'", err, asset.ErrNotSupported)
}
req.Asset = asset.Spot.String()
req.Pair = &gctrpc.CurrencyPair{}
_, err = s.GetOrderbookMovement(context.Background(), req)
if !errors.Is(err, currency.ErrCurrencyPairEmpty) {
t.Fatalf("received: '%+v' but expected: '%v'", err, currency.ErrCurrencyPairEmpty)
}
req.Pair = &gctrpc.CurrencyPair{
Base: currency.BTC.String(),
Quote: currency.METAL.String(),
}
_, err = s.GetOrderbookMovement(context.Background(), req)
if !strings.Contains(err.Error(), "cannot find orderbook") {
t.Fatalf("received: '%+v' but expected: '%v'", err, "cannot find orderbook")
}
depth, err := orderbook.DeployDepth(req.Exchange, currency.NewPair(currency.BTC, currency.METAL), asset.Spot)
if err != nil {
t.Fatal(err)
}
bid := []orderbook.Item{
{Price: 10, Amount: 1},
{Price: 9, Amount: 1},
{Price: 8, Amount: 1},
{Price: 7, Amount: 1},
}
ask := []orderbook.Item{
{Price: 11, Amount: 1},
{Price: 12, Amount: 1},
{Price: 13, Amount: 1},
{Price: 14, Amount: 1},
}
depth.LoadSnapshot(bid, ask, 0, time.Time{}, true)
_, err = s.GetOrderbookMovement(context.Background(), req)
if err.Error() != "quote amount invalid" {
t.Fatalf("received: '%+v' but expected: '%v'", err, "quote amount invalid")
}
req.Amount = 11
move, err := s.GetOrderbookMovement(context.Background(), req)
if !errors.Is(err, nil) {
t.Fatalf("received: '%+v' but expected: '%v'", err, nil)
}
if move.Bought != 1 {
t.Fatalf("received: '%v' but expected: '%v'", move.Bought, 1)
}
req.Sell = true
req.Amount = 1
move, err = s.GetOrderbookMovement(context.Background(), req)
if !errors.Is(err, nil) {
t.Fatalf("received: '%+v' but expected: '%v'", err, nil)
}
if move.Bought != 10 {
t.Fatalf("received: '%v' but expected: '%v'", move.Bought, 10)
}
}
func TestGetOrderbookAmountByNominal(t *testing.T) {
t.Parallel()
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("ftx")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.Name = fakeExchangeName
b.Enabled = true
cp, err := currency.NewPairFromString("btc-meme")
if err != nil {
t.Fatal(err)
}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Delimiter: "/"},
RequestFormat: &currency.PairFormat{Delimiter: "/"},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
fakeExchange := fExchange{
IBotExchange: exch,
}
em.Add(fakeExchange)
s := RPCServer{Engine: &Engine{ExchangeManager: em}}
req := &gctrpc.GetOrderbookAmountByNominalRequest{}
_, err = s.GetOrderbookAmountByNominal(context.Background(), req)
if !errors.Is(err, ErrExchangeNameIsEmpty) {
t.Fatalf("received: '%+v' but expected: '%v'", err, ErrExchangeNameIsEmpty)
}
req.Exchange = "fake"
_, err = s.GetOrderbookAmountByNominal(context.Background(), req)
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%+v' but expected: '%v'", err, asset.ErrNotSupported)
}
req.Asset = asset.Spot.String()
req.Pair = &gctrpc.CurrencyPair{}
_, err = s.GetOrderbookAmountByNominal(context.Background(), req)
if !errors.Is(err, currency.ErrCurrencyPairEmpty) {
t.Fatalf("received: '%+v' but expected: '%v'", err, currency.ErrCurrencyPairEmpty)
}
req.Pair = &gctrpc.CurrencyPair{
Base: currency.BTC.String(),
Quote: currency.MEME.String(),
}
_, err = s.GetOrderbookAmountByNominal(context.Background(), req)
if !strings.Contains(err.Error(), "cannot find orderbook") {
t.Fatalf("received: '%+v' but expected: '%v'", err, "cannot find orderbook")
}
depth, err := orderbook.DeployDepth(req.Exchange, currency.NewPair(currency.BTC, currency.MEME), asset.Spot)
if err != nil {
t.Fatal(err)
}
bid := []orderbook.Item{
{Price: 10, Amount: 1},
{Price: 9, Amount: 1},
{Price: 8, Amount: 1},
{Price: 7, Amount: 1},
}
ask := []orderbook.Item{
{Price: 11, Amount: 1},
{Price: 12, Amount: 1},
{Price: 13, Amount: 1},
{Price: 14, Amount: 1},
}
depth.LoadSnapshot(bid, ask, 0, time.Time{}, true)
nominal, err := s.GetOrderbookAmountByNominal(context.Background(), req)
if !errors.Is(err, nil) {
t.Fatalf("received: '%+v' but expected: '%v'", err, nil)
}
if nominal.AmountRequired != 11 {
t.Fatalf("received: '%v' but expected: '%v'", nominal.AmountRequired, 11)
}
req.Sell = true
nominal, err = s.GetOrderbookAmountByNominal(context.Background(), req)
if !errors.Is(err, nil) {
t.Fatalf("received: '%+v' but expected: '%v'", err, nil)
}
if nominal.AmountRequired != 1 {
t.Fatalf("received: '%v' but expected: '%v'", nominal.AmountRequired, 1)
}
}
func TestGetOrderbookAmountByImpact(t *testing.T) {
t.Parallel()
em := SetupExchangeManager()
exch, err := em.NewExchangeByName("ftx")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.Name = fakeExchangeName
b.Enabled = true
cp, err := currency.NewPairFromString("btc-mad")
if err != nil {
t.Fatal(err)
}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Delimiter: "/"},
RequestFormat: &currency.PairFormat{Delimiter: "/"},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
fakeExchange := fExchange{
IBotExchange: exch,
}
em.Add(fakeExchange)
s := RPCServer{Engine: &Engine{ExchangeManager: em}}
req := &gctrpc.GetOrderbookAmountByImpactRequest{}
_, err = s.GetOrderbookAmountByImpact(context.Background(), req)
if !errors.Is(err, ErrExchangeNameIsEmpty) {
t.Fatalf("received: '%+v' but expected: '%v'", err, ErrExchangeNameIsEmpty)
}
req.Exchange = "fake"
_, err = s.GetOrderbookAmountByImpact(context.Background(), req)
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%+v' but expected: '%v'", err, asset.ErrNotSupported)
}
req.Asset = asset.Spot.String()
req.Pair = &gctrpc.CurrencyPair{}
_, err = s.GetOrderbookAmountByImpact(context.Background(), req)
if !errors.Is(err, currency.ErrCurrencyPairEmpty) {
t.Fatalf("received: '%+v' but expected: '%v'", err, currency.ErrCurrencyPairEmpty)
}
req.Pair = &gctrpc.CurrencyPair{
Base: currency.BTC.String(),
Quote: currency.MAD.String(),
}
_, err = s.GetOrderbookAmountByImpact(context.Background(), req)
if !strings.Contains(err.Error(), "cannot find orderbook") {
t.Fatalf("received: '%+v' but expected: '%v'", err, "cannot find orderbook")
}
depth, err := orderbook.DeployDepth(req.Exchange, currency.NewPair(currency.BTC, currency.MAD), asset.Spot)
if err != nil {
t.Fatal(err)
}
bid := []orderbook.Item{
{Price: 10, Amount: 1},
{Price: 9, Amount: 1},
{Price: 8, Amount: 1},
{Price: 7, Amount: 1},
}
ask := []orderbook.Item{
{Price: 11, Amount: 1},
{Price: 12, Amount: 1},
{Price: 13, Amount: 1},
{Price: 14, Amount: 1},
}
depth.LoadSnapshot(bid, ask, 0, time.Time{}, true)
req.ImpactPercentage = 9.090909090909092
impact, err := s.GetOrderbookAmountByImpact(context.Background(), req)
if !errors.Is(err, nil) {
t.Fatalf("received: '%+v' but expected: '%v'", err, nil)
}
if impact.AmountRequired != 11 {
t.Fatalf("received: '%v' but expected: '%v'", impact.AmountRequired, 11)
}
req.Sell = true
req.ImpactPercentage = 10
impact, err = s.GetOrderbookAmountByImpact(context.Background(), req)
if !errors.Is(err, nil) {
t.Fatalf("received: '%+v' but expected: '%v'", err, nil)
}
if impact.AmountRequired != 1 {
t.Fatalf("received: '%v' but expected: '%v'", impact.AmountRequired, 1)
}
}

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3293,6 +3293,114 @@ func local_request_GoCryptoTraderService_GetFundingRates_0(ctx context.Context,
}
var (
filter_GoCryptoTraderService_GetOrderbookMovement_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_GoCryptoTraderService_GetOrderbookMovement_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetOrderbookMovementRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_GetOrderbookMovement_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetOrderbookMovement(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_GoCryptoTraderService_GetOrderbookMovement_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetOrderbookMovementRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_GetOrderbookMovement_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetOrderbookMovement(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_GoCryptoTraderService_GetOrderbookAmountByNominal_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_GoCryptoTraderService_GetOrderbookAmountByNominal_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetOrderbookAmountByNominalRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_GetOrderbookAmountByNominal_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetOrderbookAmountByNominal(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_GoCryptoTraderService_GetOrderbookAmountByNominal_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetOrderbookAmountByNominalRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_GetOrderbookAmountByNominal_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetOrderbookAmountByNominal(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_GoCryptoTraderService_GetOrderbookAmountByImpact_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_GoCryptoTraderService_GetOrderbookAmountByImpact_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetOrderbookAmountByImpactRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_GetOrderbookAmountByImpact_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetOrderbookAmountByImpact(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_GoCryptoTraderService_GetOrderbookAmountByImpact_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetOrderbookAmountByImpactRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_GetOrderbookAmountByImpact_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetOrderbookAmountByImpact(ctx, &protoReq)
return msg, metadata, err
}
// RegisterGoCryptoTraderServiceHandlerServer registers the http handlers for service GoCryptoTraderService to "mux".
// UnaryRPC :call GoCryptoTraderServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
@@ -5645,6 +5753,78 @@ func RegisterGoCryptoTraderServiceHandlerServer(ctx context.Context, mux *runtim
})
mux.Handle("GET", pattern_GoCryptoTraderService_GetOrderbookMovement_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/GetOrderbookMovement", runtime.WithHTTPPathPattern("/v1/getorderbookmovement"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_GoCryptoTraderService_GetOrderbookMovement_0(ctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_GoCryptoTraderService_GetOrderbookMovement_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_GoCryptoTraderService_GetOrderbookAmountByNominal_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/GetOrderbookAmountByNominal", runtime.WithHTTPPathPattern("/v1/getorderbookamountbynominal"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_GoCryptoTraderService_GetOrderbookAmountByNominal_0(ctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_GoCryptoTraderService_GetOrderbookAmountByNominal_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_GoCryptoTraderService_GetOrderbookAmountByImpact_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/GetOrderbookAmountByImpact", runtime.WithHTTPPathPattern("/v1/getorderbookamountbyimpact"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_GoCryptoTraderService_GetOrderbookAmountByImpact_0(ctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_GoCryptoTraderService_GetOrderbookAmountByImpact_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@@ -7828,6 +8008,69 @@ func RegisterGoCryptoTraderServiceHandlerClient(ctx context.Context, mux *runtim
})
mux.Handle("GET", pattern_GoCryptoTraderService_GetOrderbookMovement_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/GetOrderbookMovement", runtime.WithHTTPPathPattern("/v1/getorderbookmovement"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_GoCryptoTraderService_GetOrderbookMovement_0(ctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_GoCryptoTraderService_GetOrderbookMovement_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_GoCryptoTraderService_GetOrderbookAmountByNominal_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/GetOrderbookAmountByNominal", runtime.WithHTTPPathPattern("/v1/getorderbookamountbynominal"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_GoCryptoTraderService_GetOrderbookAmountByNominal_0(ctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_GoCryptoTraderService_GetOrderbookAmountByNominal_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_GoCryptoTraderService_GetOrderbookAmountByImpact_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/GetOrderbookAmountByImpact", runtime.WithHTTPPathPattern("/v1/getorderbookamountbyimpact"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_GoCryptoTraderService_GetOrderbookAmountByImpact_0(ctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_GoCryptoTraderService_GetOrderbookAmountByImpact_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@@ -8035,6 +8278,12 @@ var (
pattern_GoCryptoTraderService_GetAllManagedPositions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getallmanagedpositions"}, ""))
pattern_GoCryptoTraderService_GetFundingRates_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getfundingrates"}, ""))
pattern_GoCryptoTraderService_GetOrderbookMovement_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getorderbookmovement"}, ""))
pattern_GoCryptoTraderService_GetOrderbookAmountByNominal_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getorderbookamountbynominal"}, ""))
pattern_GoCryptoTraderService_GetOrderbookAmountByImpact_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getorderbookamountbyimpact"}, ""))
)
var (
@@ -8241,4 +8490,10 @@ var (
forward_GoCryptoTraderService_GetAllManagedPositions_0 = runtime.ForwardResponseMessage
forward_GoCryptoTraderService_GetFundingRates_0 = runtime.ForwardResponseMessage
forward_GoCryptoTraderService_GetOrderbookMovement_0 = runtime.ForwardResponseMessage
forward_GoCryptoTraderService_GetOrderbookAmountByNominal_0 = runtime.ForwardResponseMessage
forward_GoCryptoTraderService_GetOrderbookAmountByImpact_0 = runtime.ForwardResponseMessage
)

View File

@@ -382,6 +382,7 @@ message WhaleBombRequest {
CurrencyPair pair = 2;
double price_target = 3;
string side = 4;
string asset_type = 5;
}
message CancelOrderRequest {
@@ -1279,6 +1280,79 @@ message GetMarginRatesHistoryResponse {
string taker_fee_rate = 9;
}
message GetOrderbookMovementRequest {
string exchange = 1;
string asset = 2;
CurrencyPair pair = 3;
double amount = 6;
bool sell = 7;
bool requires_rest_orderbook = 8;
bool purchase = 9;
}
message GetOrderbookMovementResponse {
double nominal_percentage = 1;
double impact_percentage = 2;
double slippage_cost = 3;
string currency_bought = 4;
double bought = 5;
string currency_sold = 6;
double sold = 7;
string side_affected = 8;
string update_protocol = 9;
bool full_orderbook_side_consumed = 10;
double start_price = 11;
double end_price = 12;
bool no_slippage_occurred = 13;
double average_order_cost = 14;
}
message GetOrderbookAmountByNominalRequest {
string exchange = 1;
string asset = 2;
CurrencyPair pair = 3;
double nominal_percentage = 6;
bool sell = 7;
bool requires_rest_orderbook = 8;
}
message GetOrderbookAmountByNominalResponse {
double amount_required = 1;
string currency_selling = 2;
double amount_received = 3;
string currency_buying = 4;
double start_price = 5;
double end_price = 6;
double average_order_cost = 7;
string side_affected = 8;
double approximate_nominal_slippage_percentage = 9;
string update_protocol = 10;
bool full_orderbook_side_consumed = 11;
}
message GetOrderbookAmountByImpactRequest {
string exchange = 1;
string asset = 2;
CurrencyPair pair = 3;
double impact_percentage = 6;
bool sell = 7;
bool requires_rest_orderbook = 8;
}
message GetOrderbookAmountByImpactResponse {
double amount_required = 1;
string currency_selling = 2;
double amount_received = 3;
string currency_buying = 4;
double start_price = 5;
double end_price = 6;
double average_order_cost = 7;
string side_affected = 8;
double approximate_impact_slippage_percentage = 9;
string update_protocol = 10;
bool full_orderbook_side_consumed = 11;
}
service GoCryptoTraderService {
rpc GetInfo(GetInfoRequest) returns (GetInfoResponse) {
option (google.api.http) = {
@@ -1907,4 +1981,19 @@ service GoCryptoTraderService {
get: "/v1/getfundingrates"
};
}
rpc GetOrderbookMovement(GetOrderbookMovementRequest) returns (GetOrderbookMovementResponse) {
option (google.api.http) = {
get: "/v1/getorderbookmovement"
};
}
rpc GetOrderbookAmountByNominal(GetOrderbookAmountByNominalRequest) returns (GetOrderbookAmountByNominalResponse) {
option (google.api.http) = {
get: "/v1/getorderbookamountbynominal"
};
}
rpc GetOrderbookAmountByImpact(GetOrderbookAmountByImpactRequest) returns (GetOrderbookAmountByImpactResponse) {
option (google.api.http) = {
get: "/v1/getorderbookamountbyimpact"
};
}
}

View File

@@ -2556,6 +2556,231 @@
]
}
},
"/v1/getorderbookamountbyimpact": {
"get": {
"operationId": "GoCryptoTraderService_GetOrderbookAmountByImpact",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/gctrpcGetOrderbookAmountByImpactResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "exchange",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "asset",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "pair.delimiter",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "pair.base",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "pair.quote",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "impactPercentage",
"in": "query",
"required": false,
"type": "number",
"format": "double"
},
{
"name": "sell",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "requiresRestOrderbook",
"in": "query",
"required": false,
"type": "boolean"
}
],
"tags": [
"GoCryptoTraderService"
]
}
},
"/v1/getorderbookamountbynominal": {
"get": {
"operationId": "GoCryptoTraderService_GetOrderbookAmountByNominal",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/gctrpcGetOrderbookAmountByNominalResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "exchange",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "asset",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "pair.delimiter",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "pair.base",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "pair.quote",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "nominalPercentage",
"in": "query",
"required": false,
"type": "number",
"format": "double"
},
{
"name": "sell",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "requiresRestOrderbook",
"in": "query",
"required": false,
"type": "boolean"
}
],
"tags": [
"GoCryptoTraderService"
]
}
},
"/v1/getorderbookmovement": {
"get": {
"operationId": "GoCryptoTraderService_GetOrderbookMovement",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/gctrpcGetOrderbookMovementResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "exchange",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "asset",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "pair.delimiter",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "pair.base",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "pair.quote",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "amount",
"in": "query",
"required": false,
"type": "number",
"format": "double"
},
{
"name": "sell",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "requiresRestOrderbook",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "purchase",
"in": "query",
"required": false,
"type": "boolean"
}
],
"tags": [
"GoCryptoTraderService"
]
}
},
"/v1/getorderbooks": {
"get": {
"operationId": "GoCryptoTraderService_GetOrderbooks",
@@ -5527,6 +5752,149 @@
}
}
},
"gctrpcGetOrderbookAmountByImpactResponse": {
"type": "object",
"properties": {
"amountRequired": {
"type": "number",
"format": "double"
},
"currencySelling": {
"type": "string"
},
"amountReceived": {
"type": "number",
"format": "double"
},
"currencyBuying": {
"type": "string"
},
"startPrice": {
"type": "number",
"format": "double"
},
"endPrice": {
"type": "number",
"format": "double"
},
"averageOrderCost": {
"type": "number",
"format": "double"
},
"sideAffected": {
"type": "string"
},
"approximateImpactSlippagePercentage": {
"type": "number",
"format": "double"
},
"updateProtocol": {
"type": "string"
},
"fullOrderbookSideConsumed": {
"type": "boolean"
}
}
},
"gctrpcGetOrderbookAmountByNominalResponse": {
"type": "object",
"properties": {
"amountRequired": {
"type": "number",
"format": "double"
},
"currencySelling": {
"type": "string"
},
"amountReceived": {
"type": "number",
"format": "double"
},
"currencyBuying": {
"type": "string"
},
"startPrice": {
"type": "number",
"format": "double"
},
"endPrice": {
"type": "number",
"format": "double"
},
"averageOrderCost": {
"type": "number",
"format": "double"
},
"sideAffected": {
"type": "string"
},
"approximateNominalSlippagePercentage": {
"type": "number",
"format": "double"
},
"updateProtocol": {
"type": "string"
},
"fullOrderbookSideConsumed": {
"type": "boolean"
}
}
},
"gctrpcGetOrderbookMovementResponse": {
"type": "object",
"properties": {
"nominalPercentage": {
"type": "number",
"format": "double"
},
"impactPercentage": {
"type": "number",
"format": "double"
},
"slippageCost": {
"type": "number",
"format": "double"
},
"currencyBought": {
"type": "string"
},
"bought": {
"type": "number",
"format": "double"
},
"currencySold": {
"type": "string"
},
"sold": {
"type": "number",
"format": "double"
},
"sideAffected": {
"type": "string"
},
"updateProtocol": {
"type": "string"
},
"fullOrderbookSideConsumed": {
"type": "boolean"
},
"startPrice": {
"type": "number",
"format": "double"
},
"endPrice": {
"type": "number",
"format": "double"
},
"noSlippageOccurred": {
"type": "boolean"
},
"averageOrderCost": {
"type": "number",
"format": "double"
}
}
},
"gctrpcGetOrderbookRequest": {
"type": "object",
"properties": {
@@ -6468,6 +6836,9 @@
},
"side": {
"type": "string"
},
"assetType": {
"type": "string"
}
}
},

View File

@@ -124,6 +124,9 @@ type GoCryptoTraderServiceClient interface {
GetManagedPosition(ctx context.Context, in *GetManagedPositionRequest, opts ...grpc.CallOption) (*GetManagedPositionsResponse, error)
GetAllManagedPositions(ctx context.Context, in *GetAllManagedPositionsRequest, opts ...grpc.CallOption) (*GetManagedPositionsResponse, error)
GetFundingRates(ctx context.Context, in *GetFundingRatesRequest, opts ...grpc.CallOption) (*GetFundingRatesResponse, error)
GetOrderbookMovement(ctx context.Context, in *GetOrderbookMovementRequest, opts ...grpc.CallOption) (*GetOrderbookMovementResponse, error)
GetOrderbookAmountByNominal(ctx context.Context, in *GetOrderbookAmountByNominalRequest, opts ...grpc.CallOption) (*GetOrderbookAmountByNominalResponse, error)
GetOrderbookAmountByImpact(ctx context.Context, in *GetOrderbookAmountByImpactRequest, opts ...grpc.CallOption) (*GetOrderbookAmountByImpactResponse, error)
}
type goCryptoTraderServiceClient struct {
@@ -1190,6 +1193,33 @@ func (c *goCryptoTraderServiceClient) GetFundingRates(ctx context.Context, in *G
return out, nil
}
func (c *goCryptoTraderServiceClient) GetOrderbookMovement(ctx context.Context, in *GetOrderbookMovementRequest, opts ...grpc.CallOption) (*GetOrderbookMovementResponse, error) {
out := new(GetOrderbookMovementResponse)
err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTraderService/GetOrderbookMovement", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *goCryptoTraderServiceClient) GetOrderbookAmountByNominal(ctx context.Context, in *GetOrderbookAmountByNominalRequest, opts ...grpc.CallOption) (*GetOrderbookAmountByNominalResponse, error) {
out := new(GetOrderbookAmountByNominalResponse)
err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTraderService/GetOrderbookAmountByNominal", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *goCryptoTraderServiceClient) GetOrderbookAmountByImpact(ctx context.Context, in *GetOrderbookAmountByImpactRequest, opts ...grpc.CallOption) (*GetOrderbookAmountByImpactResponse, error) {
out := new(GetOrderbookAmountByImpactResponse)
err := c.cc.Invoke(ctx, "/gctrpc.GoCryptoTraderService/GetOrderbookAmountByImpact", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// GoCryptoTraderServiceServer is the server API for GoCryptoTraderService service.
// All implementations must embed UnimplementedGoCryptoTraderServiceServer
// for forward compatibility
@@ -1296,6 +1326,9 @@ type GoCryptoTraderServiceServer interface {
GetManagedPosition(context.Context, *GetManagedPositionRequest) (*GetManagedPositionsResponse, error)
GetAllManagedPositions(context.Context, *GetAllManagedPositionsRequest) (*GetManagedPositionsResponse, error)
GetFundingRates(context.Context, *GetFundingRatesRequest) (*GetFundingRatesResponse, error)
GetOrderbookMovement(context.Context, *GetOrderbookMovementRequest) (*GetOrderbookMovementResponse, error)
GetOrderbookAmountByNominal(context.Context, *GetOrderbookAmountByNominalRequest) (*GetOrderbookAmountByNominalResponse, error)
GetOrderbookAmountByImpact(context.Context, *GetOrderbookAmountByImpactRequest) (*GetOrderbookAmountByImpactResponse, error)
mustEmbedUnimplementedGoCryptoTraderServiceServer()
}
@@ -1609,6 +1642,15 @@ func (UnimplementedGoCryptoTraderServiceServer) GetAllManagedPositions(context.C
func (UnimplementedGoCryptoTraderServiceServer) GetFundingRates(context.Context, *GetFundingRatesRequest) (*GetFundingRatesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetFundingRates not implemented")
}
func (UnimplementedGoCryptoTraderServiceServer) GetOrderbookMovement(context.Context, *GetOrderbookMovementRequest) (*GetOrderbookMovementResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetOrderbookMovement not implemented")
}
func (UnimplementedGoCryptoTraderServiceServer) GetOrderbookAmountByNominal(context.Context, *GetOrderbookAmountByNominalRequest) (*GetOrderbookAmountByNominalResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetOrderbookAmountByNominal not implemented")
}
func (UnimplementedGoCryptoTraderServiceServer) GetOrderbookAmountByImpact(context.Context, *GetOrderbookAmountByImpactRequest) (*GetOrderbookAmountByImpactResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetOrderbookAmountByImpact not implemented")
}
func (UnimplementedGoCryptoTraderServiceServer) mustEmbedUnimplementedGoCryptoTraderServiceServer() {}
// UnsafeGoCryptoTraderServiceServer may be embedded to opt out of forward compatibility for this service.
@@ -3476,6 +3518,60 @@ func _GoCryptoTraderService_GetFundingRates_Handler(srv interface{}, ctx context
return interceptor(ctx, in, info, handler)
}
func _GoCryptoTraderService_GetOrderbookMovement_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetOrderbookMovementRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GoCryptoTraderServiceServer).GetOrderbookMovement(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/gctrpc.GoCryptoTraderService/GetOrderbookMovement",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GoCryptoTraderServiceServer).GetOrderbookMovement(ctx, req.(*GetOrderbookMovementRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GoCryptoTraderService_GetOrderbookAmountByNominal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetOrderbookAmountByNominalRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GoCryptoTraderServiceServer).GetOrderbookAmountByNominal(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/gctrpc.GoCryptoTraderService/GetOrderbookAmountByNominal",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GoCryptoTraderServiceServer).GetOrderbookAmountByNominal(ctx, req.(*GetOrderbookAmountByNominalRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GoCryptoTraderService_GetOrderbookAmountByImpact_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetOrderbookAmountByImpactRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GoCryptoTraderServiceServer).GetOrderbookAmountByImpact(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/gctrpc.GoCryptoTraderService/GetOrderbookAmountByImpact",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GoCryptoTraderServiceServer).GetOrderbookAmountByImpact(ctx, req.(*GetOrderbookAmountByImpactRequest))
}
return interceptor(ctx, in, info, handler)
}
// GoCryptoTraderService_ServiceDesc is the grpc.ServiceDesc for GoCryptoTraderService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -3867,6 +3963,18 @@ var GoCryptoTraderService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetFundingRates",
Handler: _GoCryptoTraderService_GetFundingRates_Handler,
},
{
MethodName: "GetOrderbookMovement",
Handler: _GoCryptoTraderService_GetOrderbookMovement_Handler,
},
{
MethodName: "GetOrderbookAmountByNominal",
Handler: _GoCryptoTraderService_GetOrderbookAmountByNominal_Handler,
},
{
MethodName: "GetOrderbookAmountByImpact",
Handler: _GoCryptoTraderService_GetOrderbookAmountByImpact_Handler,
},
},
Streams: []grpc.StreamDesc{
{