github.com/InjectiveLabs/sdk-go@v1.53.0/examples/exchange/spot/8_StreamOrderbookUpdate/example.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"sort"
     9  
    10  	"github.com/InjectiveLabs/sdk-go/client/common"
    11  	exchangeclient "github.com/InjectiveLabs/sdk-go/client/exchange"
    12  	spotExchangePB "github.com/InjectiveLabs/sdk-go/exchange/spot_exchange_rpc/pb"
    13  	"github.com/shopspring/decimal"
    14  )
    15  
    16  type MapOrderbook struct {
    17  	Sequence uint64
    18  	Levels   map[bool]map[string]*spotExchangePB.PriceLevel
    19  }
    20  
    21  func main() {
    22  	network := common.LoadNetwork("devnet-1", "")
    23  	exchangeClient, err := exchangeclient.NewExchangeClient(network)
    24  	if err != nil {
    25  		fmt.Println(err)
    26  		panic(err)
    27  	}
    28  
    29  	ctx := context.Background()
    30  	marketIds := []string{"0xa508cb32923323679f29a032c70342c147c17d0145625922b0ef22e955c844c0"}
    31  	stream, err := exchangeClient.StreamSpotOrderbookUpdate(ctx, marketIds)
    32  	if err != nil {
    33  		fmt.Println(err)
    34  		panic(err)
    35  	}
    36  
    37  	updatesCh := make(chan *spotExchangePB.OrderbookLevelUpdates, 100000)
    38  	receiving := make(chan struct{})
    39  	var receivingClosed bool
    40  
    41  	// stream orderbook price levels
    42  	go func() {
    43  		for {
    44  			select {
    45  			case <-ctx.Done():
    46  				return
    47  			default:
    48  				res, err := stream.Recv()
    49  				if err == io.EOF {
    50  					fmt.Println("change stream needs to be restarted")
    51  					os.Exit(0)
    52  				}
    53  				if err != nil {
    54  					panic(err) // rester on errors
    55  				}
    56  				u := res.OrderbookLevelUpdates
    57  				if !receivingClosed {
    58  					fmt.Println("receiving updates from stream")
    59  					close(receiving)
    60  					receivingClosed = true
    61  				}
    62  				updatesCh <- u
    63  			}
    64  		}
    65  	}()
    66  
    67  	// ensure we are receiving updates before getting the orderbook snapshot,
    68  	// we will skip past updates later.
    69  	fmt.Println("waiting for streaming updates")
    70  	<-receiving
    71  
    72  	// prepare orderbooks map
    73  	orderbooks := map[string]*MapOrderbook{}
    74  	res, err := exchangeClient.GetSpotOrderbooksV2(ctx, marketIds)
    75  	if err != nil {
    76  		panic(err)
    77  	}
    78  	for _, ob := range res.Orderbooks {
    79  		// init inner maps not ready
    80  		_, ok := orderbooks[ob.MarketId]
    81  		if !ok {
    82  			orderbook := &MapOrderbook{
    83  				Sequence: ob.Orderbook.Sequence,
    84  				Levels:   map[bool]map[string]*spotExchangePB.PriceLevel{},
    85  			}
    86  			orderbook.Levels[true] = map[string]*spotExchangePB.PriceLevel{}
    87  			orderbook.Levels[false] = map[string]*spotExchangePB.PriceLevel{}
    88  			orderbooks[ob.MarketId] = orderbook
    89  		}
    90  
    91  		for _, level := range ob.Orderbook.Buys {
    92  			orderbooks[ob.MarketId].Levels[true][level.Price] = level
    93  		}
    94  		for _, level := range ob.Orderbook.Sells {
    95  			orderbooks[ob.MarketId].Levels[false][level.Price] = level
    96  		}
    97  	}
    98  
    99  	// continuously consume level updates and maintain orderbook
   100  	skippedPastEvents := false
   101  	for {
   102  		updates := <-updatesCh
   103  
   104  		// validate orderbook
   105  		orderbook, ok := orderbooks[updates.MarketId]
   106  		if !ok {
   107  			fmt.Println("updates channel closed, must restart")
   108  			return // closed
   109  		}
   110  
   111  		// skip if update sequence <= orderbook sequence until it's ready to consume
   112  		if !skippedPastEvents {
   113  			if orderbook.Sequence >= updates.Sequence {
   114  				continue
   115  			}
   116  			skippedPastEvents = true
   117  		}
   118  
   119  		// panic if update sequence > orderbook sequence + 1
   120  		if updates.Sequence > orderbook.Sequence+1 {
   121  			fmt.Printf("skipping levels: update sequence %d vs orderbook sequence %d\n", updates.Sequence, orderbook.Sequence)
   122  			panic("missing orderbook update events from stream, must restart")
   123  		}
   124  
   125  		// update orderbook map
   126  		orderbook.Sequence = updates.Sequence
   127  		for isBuy, update := range map[bool][]*spotExchangePB.PriceLevelUpdate{
   128  			true:  updates.Buys,
   129  			false: updates.Sells,
   130  		} {
   131  			for _, level := range update {
   132  				if level.IsActive {
   133  					// upsert
   134  					orderbook.Levels[isBuy][level.Price] = &spotExchangePB.PriceLevel{
   135  						Price:     level.Price,
   136  						Quantity:  level.Quantity,
   137  						Timestamp: level.Timestamp,
   138  					}
   139  				} else {
   140  					// remove inactive level
   141  					delete(orderbook.Levels[isBuy], level.Price)
   142  				}
   143  			}
   144  		}
   145  
   146  		// construct orderbook arrays
   147  		sells, buys := maintainOrderbook(orderbook.Levels)
   148  		fmt.Println("after", orderbook.Sequence, len(sells), len(buys))
   149  
   150  		if len(sells) > 0 && len(buys) > 0 {
   151  			// assert orderbook
   152  			topBuyPrice := decimal.RequireFromString(buys[0].Price)
   153  			lowestSellPrice := decimal.RequireFromString(sells[0].Price)
   154  			if topBuyPrice.GreaterThanOrEqual(lowestSellPrice) {
   155  				panic(fmt.Errorf("crossed orderbook, must restart: buy %s >= %s sell", topBuyPrice, lowestSellPrice))
   156  			}
   157  		}
   158  
   159  		res, _ = exchangeClient.GetSpotOrderbooksV2(ctx, marketIds)
   160  		fmt.Println("query", res.Orderbooks[0].Orderbook.Sequence, len(res.Orderbooks[0].Orderbook.Sells), len(res.Orderbooks[0].Orderbook.Buys))
   161  
   162  		// print orderbook
   163  		fmt.Println(" [SELLS] ========")
   164  		for _, s := range sells {
   165  			fmt.Println(s)
   166  		}
   167  		fmt.Println(" [BUYS] ========")
   168  		for _, b := range buys {
   169  			fmt.Println(b)
   170  		}
   171  		fmt.Println("=======================================================")
   172  	}
   173  }
   174  
   175  func maintainOrderbook(orderbook map[bool]map[string]*spotExchangePB.PriceLevel) (buys, sells []*spotExchangePB.PriceLevel) {
   176  	for _, v := range orderbook[false] {
   177  		sells = append(sells, v)
   178  	}
   179  	for _, v := range orderbook[true] {
   180  		buys = append(buys, v)
   181  	}
   182  
   183  	sort.Slice(sells, func(i, j int) bool {
   184  		return decimal.RequireFromString(sells[i].Price).LessThan(decimal.RequireFromString(sells[j].Price))
   185  	})
   186  
   187  	sort.Slice(buys, func(i, j int) bool {
   188  		return decimal.RequireFromString(buys[i].Price).GreaterThan(decimal.RequireFromString(buys[j].Price))
   189  	})
   190  
   191  	return sells, buys
   192  }