github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BitmaxScraper.go (about) 1 package scrapers 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "strconv" 10 "strings" 11 "sync" 12 "time" 13 14 ws "github.com/gorilla/websocket" 15 "github.com/zekroTJA/timedmap" 16 17 "github.com/diadata-org/diadata/pkg/dia" 18 models "github.com/diadata-org/diadata/pkg/model" 19 "github.com/diadata-org/diadata/pkg/utils" 20 ) 21 22 const ( 23 bitmaxMaxNumSubscriptionsPerConn = 200 24 ) 25 26 type BitMaxPairResponse struct { 27 Code int `json:"code"` 28 Data []BitMaxPair `json:"data"` 29 } 30 31 type BitMaxAssets struct { 32 Code int `json:"code"` 33 Data []BitMaxAsset `json:"data"` 34 } 35 36 type BitMaxAsset struct { 37 AssetCode string `json:"assetCode"` 38 AssetName string `json:"assetName"` 39 PrecisionScale int `json:"precisionScale"` 40 NativeScale int `json:"nativeScale"` 41 WithdrawalFee string `json:"withdrawalFee"` 42 MinWithdrawalAmt string `json:"minWithdrawalAmt"` 43 Status string `json:"status"` 44 } 45 46 type BitMaxPair struct { 47 Symbol string `json:"symbol"` 48 DisplayName string `json:"displayName"` 49 BaseAsset string `json:"baseAsset"` 50 QuoteAsset string `json:"quoteAsset"` 51 Status string `json:"status"` 52 MinNotional string `json:"minNotional"` 53 MaxNotional string `json:"maxNotional"` 54 MarginTradable bool `json:"marginTradable"` 55 CommissionType string `json:"commissionType"` 56 CommissionReserveRate string `json:"commissionReserveRate"` 57 TickSize string `json:"tickSize"` 58 LotSize string `json:"lotSize"` 59 } 60 61 type BitMaxScraper struct { 62 // signaling channels for session initialization and finishing 63 initDone chan nothing 64 shutdown chan nothing 65 shutdownDone chan nothing 66 // error handling; to read error or closed, first acquire read lock 67 // only cleanup method should hold write lock 68 errorLock sync.RWMutex 69 error error 70 closed bool 71 // used to keep track of trading pairs that we subscribed to 72 // use sync.Maps to concurrently handle multiple pairs 73 pairScrapers map[string]*BitMaxPairScraper // dia.Pair -> BitMaxPairScraper 74 exchangeName string 75 chanTrades chan *dia.Trade 76 wsClient1 *ws.Conn 77 wsClient2 *ws.Conn 78 numPairsClient1 int 79 numPairsClient2 int 80 currencySymbolName map[string]string 81 isTickerMapInitialised bool 82 db *models.RelDB 83 } 84 85 func NewBitMaxScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BitMaxScraper { 86 var bitmaxSocketURL = "wss://ascendex.com/0/api/pro/v1/stream" 87 s := &BitMaxScraper{ 88 initDone: make(chan nothing), 89 shutdown: make(chan nothing), 90 shutdownDone: make(chan nothing), 91 exchangeName: exchange.Name, 92 pairScrapers: make(map[string]*BitMaxPairScraper), 93 error: nil, 94 chanTrades: make(chan *dia.Trade), 95 currencySymbolName: make(map[string]string), 96 isTickerMapInitialised: false, 97 db: relDB, 98 } 99 100 // establish connection in the background 101 var wsDialer ws.Dialer 102 SwConn1, _, err := wsDialer.Dial(bitmaxSocketURL, nil) 103 if err != nil { 104 log.Fatal("connect to websocket server: ", err) 105 } 106 s.wsClient1 = SwConn1 107 SwConn2, _, err := wsDialer.Dial(bitmaxSocketURL, nil) 108 if err != nil { 109 log.Fatal("connect to websocket server: ", err) 110 } 111 s.wsClient2 = SwConn2 112 113 if scrape { 114 go s.mainLoop(s.wsClient1) 115 go s.mainLoop(s.wsClient2) 116 } 117 return s 118 } 119 120 type BitMaxTradeResponse struct { 121 M string `json:"m"` 122 Symbol string `json:"symbol"` 123 Data []struct { 124 P string `json:"p"` 125 Q string `json:"q"` 126 Ts int64 `json:"ts"` 127 Bm bool `json:"bm"` 128 Seqnum int64 `json:"seqnum"` 129 } `json:"data"` 130 } 131 132 // runs in a goroutine until s is closed 133 func (s *BitMaxScraper) mainLoop(client *ws.Conn) { 134 var err error 135 tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 136 tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 137 138 for { 139 message := &BitMaxTradeResponse{} 140 if err = client.ReadJSON(&message); err != nil { 141 log.Error("read message: ", err.Error()) 142 // break 143 } 144 switch message.M { 145 case "trades": 146 { 147 for _, trade := range message.Data { 148 var exchangepair dia.ExchangePair 149 priceFloat, _ := strconv.ParseFloat(trade.P, 64) 150 volumeFloat, _ := strconv.ParseFloat(trade.Q, 64) 151 exchangepair, err = s.db.GetExchangePairCache(s.exchangeName, message.Symbol) 152 if err != nil { 153 log.Error("get exchange pair from cache: ", err) 154 } 155 t := &dia.Trade{ 156 Symbol: strings.Split(message.Symbol, "/")[0], 157 Pair: message.Symbol, 158 Price: priceFloat, 159 Volume: volumeFloat, 160 Time: time.Unix(0, trade.Ts*int64(time.Millisecond)), 161 ForeignTradeID: strconv.FormatInt(trade.Seqnum, 10), 162 Source: s.exchangeName, 163 VerifiedPair: exchangepair.Verified, 164 BaseToken: exchangepair.UnderlyingPair.BaseToken, 165 QuoteToken: exchangepair.UnderlyingPair.QuoteToken, 166 } 167 if exchangepair.Verified { 168 log.Infoln("Got verified trade", t) 169 } 170 171 // Handle duplicate trades. 172 discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory) 173 if !discardTrade { 174 t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory) 175 s.chanTrades <- t 176 } 177 178 } 179 180 } 181 case "ping": 182 { 183 a := &BitMaxRequest{ 184 Op: "pong", 185 } 186 err := client.WriteJSON(a) 187 if err != nil { 188 log.Warn("send pong to server: ", err) 189 } 190 log.Infoln("Send Pong to keep connection alive") 191 192 } 193 } 194 } 195 196 } 197 198 // FillSymbolData collects all available information on an asset traded on Bitmax 199 func (s *BitMaxScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) { 200 201 // Fetch Data 202 if !s.isTickerMapInitialised { 203 var ( 204 response BitMaxAssets 205 data []byte 206 ) 207 data, _, err = utils.GetRequest("https://ascendex.com/api/pro/v1/assets") 208 if err != nil { 209 return 210 } 211 err = json.Unmarshal(data, &response) 212 if err != nil { 213 return 214 } 215 216 for _, asset := range response.Data { 217 s.currencySymbolName[asset.AssetCode] = asset.AssetName 218 } 219 s.isTickerMapInitialised = true 220 221 } 222 223 asset.Symbol = symbol 224 asset.Name = s.currencySymbolName[symbol] 225 return asset, nil 226 } 227 228 func (s *BitMaxScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 229 return dia.ExchangePair{}, nil 230 } 231 232 // Close closes any existing API connections, as well as channels of 233 // PairScrapers from calls to ScrapePair 234 func (s *BitMaxScraper) Close() error { 235 if s.closed { 236 return errors.New("BitMaxScraper: Already closed") 237 } 238 close(s.shutdown) 239 <-s.shutdownDone 240 s.errorLock.RLock() 241 defer s.errorLock.RUnlock() 242 return s.error 243 } 244 245 type BitMaxRequest struct { 246 Op string `json:"op"` 247 ID string `json:"id"` 248 Ch string `json:"ch"` 249 } 250 251 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from 252 // this APIScraper 253 func (s *BitMaxScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 254 s.errorLock.RLock() 255 defer s.errorLock.RUnlock() 256 if s.error != nil { 257 return nil, s.error 258 } 259 if s.closed { 260 return nil, errors.New("LoopringScraper: Call ScrapePair on closed scraper") 261 } 262 ps := &BitMaxPairScraper{ 263 parent: s, 264 pair: pair, 265 } 266 a := &BitMaxRequest{ 267 Op: "sub", 268 Ch: "trades:" + pair.ForeignName, 269 ID: fmt.Sprint(time.Now().Unix()), 270 } 271 if s.numPairsClient1 < bitmaxMaxNumSubscriptionsPerConn { 272 if err := s.wsClient1.WriteJSON(a); err != nil { 273 log.Error("write pair sub: ", err.Error()) 274 } 275 s.numPairsClient1++ 276 } else { 277 if err := s.wsClient2.WriteJSON(a); err != nil { 278 log.Error("write pair sub: ", err.Error()) 279 } 280 s.numPairsClient2++ 281 } 282 log.Info("Subscribed to get trades for ", pair.ForeignName) 283 s.pairScrapers[pair.ForeignName] = ps 284 return ps, nil 285 } 286 287 func (s *BitMaxScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 288 var bitmaxResponse BitMaxPairResponse 289 response, err := http.Get("https://ascendex.com/api/pro/v1/products") 290 if err != nil { 291 log.Error("get symbols: ", err) 292 } 293 294 defer func() { 295 if cerr := response.Body.Close(); cerr != nil { 296 // Handle the error from closing the response body 297 log.Println("Error closing response body:", cerr) 298 } 299 }() 300 body, err := ioutil.ReadAll(response.Body) 301 if err != nil { 302 log.Error("read symbols: ", err) 303 } 304 305 err = json.Unmarshal(body, &bitmaxResponse) 306 if err != nil { 307 log.Error("unmarshal symbols: ", err) 308 } 309 310 for _, p := range bitmaxResponse.Data { 311 pairToNormalize := dia.ExchangePair{ 312 Symbol: strings.Split(p.Symbol, "/")[0], 313 ForeignName: p.Symbol, 314 Exchange: s.exchangeName, 315 } 316 pairs = append(pairs, pairToNormalize) 317 } 318 return 319 } 320 321 // BitMax implements PairScraper for BitMax 322 type BitMaxPairScraper struct { 323 parent *BitMaxScraper 324 pair dia.ExchangePair 325 closed bool 326 } 327 328 // Close stops listening for trades of the pair associated with s 329 func (ps *BitMaxPairScraper) Close() error { 330 var err error 331 s := ps.parent 332 // if parent already errored, return early 333 s.errorLock.RLock() 334 defer s.errorLock.RUnlock() 335 if s.error != nil { 336 return s.error 337 } 338 if ps.closed { 339 return errors.New("BitMaxPairScraper: Already closed") 340 } 341 342 // TODO stop collection for the pair 343 344 ps.closed = true 345 return err 346 } 347 348 // Channel returns a channel that can be used to receive trades 349 func (ps *BitMaxScraper) Channel() chan *dia.Trade { 350 return ps.chanTrades 351 } 352 353 // Error returns an error when the channel Channel() is closed 354 // and nil otherwise 355 func (ps *BitMaxPairScraper) Error() error { 356 s := ps.parent 357 s.errorLock.RLock() 358 defer s.errorLock.RUnlock() 359 return s.error 360 } 361 362 // Pair returns the pair this scraper is subscribed to 363 func (ps *BitMaxPairScraper) Pair() dia.ExchangePair { 364 return ps.pair 365 }