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 }