From bcabf44b8cb41b083fda57f44844a95e7b526d24 Mon Sep 17 00:00:00 2001 From: Scott Date: Thu, 14 Sep 2023 10:10:22 +1000 Subject: [PATCH] gctcli: Add colourful exchange-style rendering to orderbook fetching commands (optional) (#1348) * fancybook * fix bug * oopsie-doodle * now I remember why we don't use required --- cmd/gctcli/helpers.go | 9 ++ cmd/gctcli/orderbook.go | 216 +++++++++++++++++++++++++++++----------- engine/rpcserver.go | 6 +- 3 files changed, 171 insertions(+), 60 deletions(-) diff --git a/cmd/gctcli/helpers.go b/cmd/gctcli/helpers.go index 92bc2d01..1be1e4eb 100644 --- a/cmd/gctcli/helpers.go +++ b/cmd/gctcli/helpers.go @@ -10,6 +10,15 @@ import ( "google.golang.org/grpc" ) +var ( + // use these to change text colours in CMD output + redText = "\033[38;5;203m" + greenText = "\033[38;5;157m" + whiteText = "\033[38;5;255m" + grayText = "\033[38;5;243m" + defaultText = "\u001b[0m" +) + func clearScreen() error { switch runtime.GOOS { case "windows": diff --git a/cmd/gctcli/orderbook.go b/cmd/gctcli/orderbook.go index 27e2f824..f5811359 100644 --- a/cmd/gctcli/orderbook.go +++ b/cmd/gctcli/orderbook.go @@ -5,7 +5,9 @@ import ( "fmt" "strconv" "strings" + "time" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/gctrpc" "github.com/urfave/cli/v2" @@ -352,22 +354,17 @@ func getMovement(c *cli.Context) error { var getOrderbookCommand = &cli.Command{ Name: "getorderbook", Usage: "gets the orderbook for a specific currency pair and exchange", - ArgsUsage: " ", + ArgsUsage: " ", Action: getOrderbook, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "exchange", - Usage: "the exchange to get the orderbook for", + Flags: append(orderbookCommonFlags, + &cli.BoolFlag{ + Name: "exchangestyle", + Usage: "optional - renders the books like on an exchange website", }, - &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", - }, - }, + &cli.Int64Flag{ + Name: "depthlimit", + Usage: "optional - limit how deep the book rendering is, max 100 - only works if exchangestyle is true", + }), } func getOrderbook(c *cli.Context) error { @@ -375,9 +372,12 @@ func getOrderbook(c *cli.Context) error { return cli.ShowSubcommandHelp(c) } - var exchangeName string - var currencyPair string - var assetType string + var ( + exchangeName, pair, assetType string + depthLimit int64 + exchangeStyle bool + err error + ) if c.IsSet("exchange") { exchangeName = c.String("exchange") @@ -386,12 +386,12 @@ func getOrderbook(c *cli.Context) error { } if c.IsSet("pair") { - currencyPair = c.String("pair") + pair = c.String("pair") } else { - currencyPair = c.Args().Get(1) + pair = c.Args().Get(1) } - if !validPair(currencyPair) { + if !validPair(pair) { return errInvalidPair } @@ -401,12 +401,30 @@ func getOrderbook(c *cli.Context) error { assetType = c.Args().Get(2) } + if c.IsSet("exchangestyle") { + exchangeStyle = c.Bool("exchangestyle") + } else if c.Args().Get(3) != "" { + exchangeStyle, err = strconv.ParseBool(c.Args().Get(3)) + if err != nil { + return err + } + } + + if c.IsSet("depthlimit") { + depthLimit = c.Int64("depthlimit") + } else if c.Args().Get(4) != "" { + depthLimit, err = strconv.ParseInt(c.Args().Get(4), 10, 64) + if err != nil { + return err + } + } + assetType = strings.ToLower(assetType) if !validAsset(assetType) { return errInvalidAsset } - p, err := currency.NewPairDelimiter(currencyPair, pairDelimiter) + p, err := currency.NewPairDelimiter(pair, pairDelimiter) if err != nil { return err } @@ -434,7 +452,25 @@ func getOrderbook(c *cli.Context) error { return err } - jsonOutput(result) + if exchangeStyle { + var maxLen, bidLen, askLen int64 + bidLen = int64(len(result.Bids) - 1) + askLen = int64(len(result.Asks) - 1) + if bidLen >= askLen { + maxLen = bidLen + } else { + maxLen = askLen + } + if depthLimit > 0 && depthLimit < maxLen { + maxLen = depthLimit + } + if maxLen > 100 { + maxLen = 100 + } + renderOrderbookExchangeStyle(result, exchangeName, assetType, maxLen, askLen, bidLen) + } else { + jsonOutput(result) + } return nil } @@ -464,9 +500,17 @@ func getOrderbooks(c *cli.Context) error { var getOrderbookStreamCommand = &cli.Command{ Name: "getorderbookstream", Usage: "gets the orderbook stream for a specific currency pair and exchange", - ArgsUsage: " ", + ArgsUsage: " ", Action: getOrderbookStream, - Flags: orderbookCommonFlags, + Flags: append(orderbookCommonFlags, + &cli.BoolFlag{ + Name: "exchangestyle", + Usage: "optional - renders the books like on an exchange website", + }, + &cli.Int64Flag{ + Name: "depthlimit", + Usage: "optional - limit how deep the book rendering is, max 50", + }), } func getOrderbookStream(c *cli.Context) error { @@ -474,9 +518,12 @@ func getOrderbookStream(c *cli.Context) error { return cli.ShowSubcommandHelp(c) } - var exchangeName string - var pair string - var assetType string + var ( + exchangeName, pair, assetType string + depthLimit int64 + exchangeStyle bool + err error + ) if c.IsSet("exchange") { exchangeName = c.String("exchange") @@ -500,6 +547,24 @@ func getOrderbookStream(c *cli.Context) error { assetType = c.Args().Get(2) } + if c.IsSet("exchangestyle") { + exchangeStyle = c.Bool("exchangestyle") + } else if c.Args().Get(3) != "" { + exchangeStyle, err = strconv.ParseBool(c.Args().Get(3)) + if err != nil { + return err + } + } + + if c.IsSet("depthlimit") { + depthLimit = c.Int64("depthlimit") + } else if c.Args().Get(4) != "" { + depthLimit, err = strconv.ParseInt(c.Args().Get(4), 10, 64) + if err != nil { + return err + } + } + assetType = strings.ToLower(assetType) if !validAsset(assetType) { @@ -545,56 +610,91 @@ func getOrderbookStream(c *cli.Context) error { 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 := int64(len(resp.Bids) - 1) + askLen := int64(len(resp.Asks) - 1) - bidLen := len(resp.Bids) - 1 - askLen := len(resp.Asks) - 1 - - var maxLen int + var maxLen int64 if bidLen >= askLen { maxLen = bidLen } else { maxLen = askLen } + if depthLimit > 0 && depthLimit < maxLen { + maxLen = depthLimit + } + if maxLen > 50 { + maxLen = 50 + } - for i := 0; i < maxLen; i++ { - var bidAmount, bidPrice float64 - if i <= bidLen { - bidAmount = resp.Bids[i].Amount - bidPrice = resp.Bids[i].Price - } + if exchangeStyle { + renderOrderbookExchangeStyle(resp, exchangeName, assetType, maxLen, askLen, bidLen) + } else { + fmt.Printf("Orderbook stream for %s %s:\n\n", exchangeName, resp.Pair) + fmt.Println("\t\tBids\t\t\t\tAsks") + fmt.Println() - var askAmount, askPrice float64 - if i <= askLen { - askAmount = resp.Asks[i].Amount - askPrice = resp.Asks[i].Price - } + for i := int64(0); i < maxLen; i++ { + var bidAmount, bidPrice float64 + if i <= bidLen { + bidAmount = resp.Bids[i].Amount + bidPrice = resp.Bids[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) + var askAmount, askPrice float64 + if i <= askLen { + askAmount = resp.Asks[i].Amount + askPrice = resp.Asks[i].Price + } - if i >= 49 { - // limits orderbook display output - break + 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) } } } } +func renderOrderbookExchangeStyle(resp *gctrpc.OrderbookResponse, exchangeName, assetType string, maxLen, askLen, bidLen int64) { + maxLen-- // ensure we get the 0 index at the correct max length + upperBase := strings.ToUpper(resp.Pair.Base) + upperQuote := strings.ToUpper(resp.Pair.Quote) + printFmt := "%s%.8f\t\t%.8f\n" + fmt.Printf("%sOrderbook stream for %v %v %v - Last updated %v\n", + whiteText, strings.ToUpper(exchangeName), assetType, upperBase+"-"+upperQuote, time.UnixMicro(resp.LastUpdated).Format(common.SimpleTimeFormatWithTimezone)) + + fmt.Printf("%sPrice(%v)\t\tAmount(%s)\n", + grayText, upperQuote, upperBase) + for i := maxLen; i >= 0; i-- { + var askAmount, askPrice float64 + if i <= askLen { + askAmount = resp.Asks[i].Amount + askPrice = resp.Asks[i].Price + } + fmt.Printf(printFmt, redText, askPrice, askAmount) + } + fmt.Println() + for i := int64(0); i <= maxLen; i++ { + var bidAmount, bidPrice float64 + if i <= bidLen { + bidAmount = resp.Bids[i].Amount + bidPrice = resp.Bids[i].Price + } + fmt.Printf(printFmt, greenText, bidPrice, bidAmount) + } + fmt.Println(defaultText) +} + var getExchangeOrderbookStreamCommand = &cli.Command{ Name: "getexchangeorderbookstream", Usage: "gets a stream for all orderbooks associated with an exchange", @@ -647,7 +747,7 @@ func getExchangeOrderbookStream(c *cli.Context) error { return err } - fmt.Printf("Orderbook streamed for %s %s", exchangeName, resp.Pair) + fmt.Printf("Orderbook streamed for %s %s at %s", exchangeName, resp.Pair, time.UnixMicro(resp.LastUpdated).Format(common.SimpleTimeFormatWithTimezone)) if resp.Error != "" { fmt.Printf("%s\n", resp.Error) } diff --git a/engine/rpcserver.go b/engine/rpcserver.go index 2843f9ee..65651ed1 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -2141,8 +2141,9 @@ func (s *RPCServer) GetOrderbookStream(r *gctrpc.GetOrderbookStreamRequest, stre base, err := depth.Retrieve() if err != nil { resp.Error = err.Error() - resp.LastUpdated = time.Now().Unix() + resp.LastUpdated = time.Now().UnixMicro() } else { + resp.LastUpdated = base.LastUpdated.UnixMicro() resp.Bids = make([]*gctrpc.OrderbookItem, len(base.Bids)) for i := range base.Bids { resp.Bids[i] = &gctrpc.OrderbookItem{ @@ -2204,8 +2205,9 @@ func (s *RPCServer) GetExchangeOrderbookStream(r *gctrpc.GetExchangeOrderbookStr ob, err := d.Retrieve() if err != nil { resp.Error = err.Error() - resp.LastUpdated = time.Now().Unix() + resp.LastUpdated = time.Now().UnixMicro() } else { + resp.LastUpdated = ob.LastUpdated.UnixMicro() resp.Pair = &gctrpc.CurrencyPair{ Base: ob.Pair.Base.String(), Quote: ob.Pair.Quote.String(),