github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/HuobiScraper.go (about) 1 package scrapers 2 3 import ( 4 "compress/gzip" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "strconv" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/diadata-org/diadata/pkg/dia" 14 "github.com/diadata-org/diadata/pkg/dia/helpers" 15 models "github.com/diadata-org/diadata/pkg/model" 16 utils "github.com/diadata-org/diadata/pkg/utils" 17 ws "github.com/gorilla/websocket" 18 "github.com/zekroTJA/timedmap" 19 ) 20 21 var _HuobiSocketurl string = "wss://api.huobi.pro/ws" 22 23 type EventType struct { 24 Sub string `json:"sub,omitempty"` 25 Id string `json:"id,omitempty"` 26 Pong int `json:"pong,omitempty"` 27 } 28 29 type ResponseType struct { 30 Id string `json:"id,omitempty"` 31 Status string `json:"status,omitempty"` 32 Subbed string `json:"subbed,omitempty"` 33 Ts int64 `json:"ts,omitempty"` 34 Ping int `json:"ping,omitempty"` 35 Ch string `json:"ch,omitempty"` 36 Tick interface{} `json:"tick,omitempty"` 37 } 38 39 type HuobiCurrency struct { 40 Code int `json:"code"` 41 Data []struct { 42 Currency string `json:"currency"` 43 AssetType int `json:"assetType"` 44 Chains []struct { 45 Chain string `json:"chain"` 46 DisplayName string `json:"displayName"` 47 BaseChain string `json:"baseChain"` 48 BaseChainProtocol string `json:"baseChainProtocol"` 49 IsDynamic bool `json:"isDynamic"` 50 NumOfConfirmations int `json:"numOfConfirmations"` 51 NumOfFastConfirmations int `json:"numOfFastConfirmations"` 52 DepositStatus string `json:"depositStatus"` 53 MinDepositAmt string `json:"minDepositAmt"` 54 WithdrawStatus string `json:"withdrawStatus"` 55 MinWithdrawAmt string `json:"minWithdrawAmt"` 56 WithdrawPrecision int `json:"withdrawPrecision"` 57 MaxWithdrawAmt string `json:"maxWithdrawAmt"` 58 WithdrawQuotaPerDay string `json:"withdrawQuotaPerDay"` 59 WithdrawQuotaPerYear interface{} `json:"withdrawQuotaPerYear"` 60 WithdrawQuotaTotal interface{} `json:"withdrawQuotaTotal"` 61 WithdrawFeeType string `json:"withdrawFeeType"` 62 TransactFeeWithdraw string `json:"transactFeeWithdraw"` 63 AddrWithTag bool `json:"addrWithTag"` 64 AddrDepositTag bool `json:"addrDepositTag"` 65 } `json:"chains"` 66 InstStatus string `json:"instStatus"` 67 } `json:"data"` 68 } 69 70 type HuobiScraper struct { 71 wsClient *ws.Conn 72 // signaling channels for session initialization and finishing 73 //TODO: Channel not used. Consider removing or refactoring 74 shutdown chan nothing 75 shutdownDone chan nothing 76 // error handling; to read error or closed, first acquire read lock 77 // only cleanup method should hold write lock 78 errorLock sync.RWMutex 79 error error 80 closed bool 81 // used to keep track of trading pairs that we subscribed to 82 pairScrapers map[string]*HuobiPairScraper 83 exchangeName string 84 chanTrades chan *dia.Trade 85 db *models.RelDB 86 } 87 88 // NewHuobiScraper returns a new HuobiScraper for the given pair 89 func NewHuobiScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *HuobiScraper { 90 91 s := &HuobiScraper{ 92 shutdown: make(chan nothing), 93 shutdownDone: make(chan nothing), 94 pairScrapers: make(map[string]*HuobiPairScraper), 95 exchangeName: exchange.Name, 96 error: nil, 97 chanTrades: make(chan *dia.Trade), 98 db: relDB, 99 } 100 101 var wsDialer ws.Dialer 102 SwConn, _, err := wsDialer.Dial(_HuobiSocketurl, nil) 103 if err != nil { 104 println(err.Error()) 105 } 106 s.wsClient = SwConn 107 108 if scrape { 109 go s.mainLoop() 110 } 111 return s 112 } 113 114 // runs in a goroutine until s is closed 115 func (s *HuobiScraper) mainLoop() { 116 tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 117 tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 118 119 for { 120 message := &ResponseType{} 121 _, testRead, err := s.wsClient.NextReader() 122 123 if err != nil { 124 // Conn errors are non-recoverable. 125 // Terminate the routine if theres any error 126 fmt.Println(err.Error()) 127 break 128 } else { 129 130 //It has to gzip response data 131 reader, _ := gzip.NewReader(testRead) 132 jsonBase := json.NewDecoder(reader) 133 err := jsonBase.Decode(message) 134 if err != nil { 135 log.Error(err) 136 } 137 138 // If msg is ping type, it needs to resend a pong msg to ws. 139 // for avoid to disconnect it 140 if message.Ping > 0 { 141 142 a := &EventType{ 143 Pong: message.Ping, 144 } 145 146 if err := s.wsClient.WriteJSON(a); err != nil { 147 // Conn errors are non-recoverable. 148 // Terminate the routine if theres any error 149 fmt.Println(err.Error()) 150 break 151 } 152 } else { 153 154 if message.Status == "" { 155 156 var splitString = strings.Split(message.Ch, ".") 157 var forName = strings.ToUpper(splitString[1]) 158 ps, ok := s.pairScrapers[forName] 159 160 if ok { 161 162 md := message.Tick.(map[string]interface{}) 163 md_data := md["data"].([]interface{}) 164 165 for _, value := range md_data { 166 167 md_element := value.(map[string]interface{}) 168 f64Price := md_element["price"].(float64) 169 f64Volume := md_element["amount"].(float64) 170 timeStamp := time.Now().UTC() 171 172 if md_element["direction"] == "sell" { 173 f64Volume = -f64Volume 174 } 175 176 exchangepair, err := s.db.GetExchangePairCache(s.exchangeName, forName) 177 if err != nil { 178 log.Error(err) 179 } 180 // element id is more than int64/uint64 in size 181 // leave the id in float64 format 182 t := &dia.Trade{ 183 Symbol: ps.pair.Symbol, 184 Pair: forName, 185 Price: f64Price, 186 Volume: f64Volume, 187 Time: timeStamp, 188 ForeignTradeID: strconv.FormatFloat(md_element["id"].(float64), 'E', -1, 64), 189 Source: s.exchangeName, 190 VerifiedPair: exchangepair.Verified, 191 BaseToken: exchangepair.UnderlyingPair.BaseToken, 192 QuoteToken: exchangepair.UnderlyingPair.QuoteToken, 193 } 194 195 if exchangepair.Verified { 196 log.Infoln("Got verified trade", t) 197 } 198 // Handle duplicate trades. 199 discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory) 200 if !discardTrade { 201 t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory) 202 ps.parent.chanTrades <- t 203 } 204 } 205 } else { 206 log.Printf("Unknown Pair %v", forName) 207 } 208 } 209 } 210 } 211 } 212 s.cleanup(nil) 213 } 214 215 // FillSymbolData collects all available information on an asset traded on huobi 216 func (s *HuobiScraper) FillSymbolData(symbol string) (dia.Asset, error) { 217 // var response HuobiCurrency 218 // data, _, err := utils.GetRequest("https://api.huobi.pro/v2/reference/currencies?currency=" + symbol) 219 // if err != nil { 220 // return 221 // } 222 // err = json.Unmarshal(data, &response) 223 // if err != nil { 224 // return 225 // } 226 227 // // Loop through chain if ETH is available put ETH chain details 228 // // TO DO: This has to be extended. So far, we only have symbol, which we already had before. 229 // asset.Symbol = response.Data[0].Currency 230 // asset.Name = response.Data[0].Currency 231 return dia.Asset{Symbol: symbol}, nil 232 } 233 234 func (s *HuobiScraper) cleanup(err error) { 235 s.errorLock.Lock() 236 defer s.errorLock.Unlock() 237 238 if err != nil { 239 s.error = err 240 } 241 s.closed = true 242 243 close(s.shutdownDone) 244 } 245 246 // Close closes any existing API connections, as well as channels of 247 // PairScrapers from calls to ScrapePair 248 func (s *HuobiScraper) Close() error { 249 250 if s.closed { 251 return errors.New("HuobiScraper: Already closed") 252 } 253 err := s.wsClient.Close() 254 if err != nil { 255 return err 256 } 257 close(s.shutdown) 258 <-s.shutdownDone 259 s.errorLock.RLock() 260 defer s.errorLock.RUnlock() 261 return s.error 262 } 263 264 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from 265 // this APIScraper 266 func (s *HuobiScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 267 s.errorLock.RLock() 268 defer s.errorLock.RUnlock() 269 if s.error != nil { 270 return nil, s.error 271 } 272 if s.closed { 273 return nil, errors.New("HuobiScraper: Call ScrapePair on closed scraper") 274 } 275 276 ps := &HuobiPairScraper{ 277 parent: s, 278 pair: pair, 279 } 280 s.pairScrapers[pair.ForeignName] = ps 281 a := &EventType{ 282 Sub: "market." + strings.ToLower(pair.ForeignName) + ".trade.detail", 283 Id: "id1", 284 } 285 if err := s.wsClient.WriteJSON(a); err != nil { 286 fmt.Println(err.Error()) 287 } 288 return ps, nil 289 } 290 291 func (s *HuobiScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 292 symbol := strings.ToUpper(pair.Symbol) 293 pair.Symbol = symbol 294 295 if helpers.NameForSymbol(symbol) == symbol { 296 if !helpers.SymbolIsName(symbol) { 297 if pair.Symbol == "IOTA" { 298 pair.Symbol = "MIOTA" 299 } 300 if pair.Symbol == "PROPY" { 301 pair.Symbol = "PRO" 302 } 303 return pair, errors.New("Foreign name can not be normalized:" + pair.ForeignName + " symbol:" + symbol) 304 } 305 } 306 if helpers.SymbolIsBlackListed(symbol) { 307 return pair, errors.New("Symbol is black listed:" + symbol) 308 } 309 return pair, nil 310 311 } 312 313 // FetchAvailablePairs returns a list with all available trade pairs 314 func (s *HuobiScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 315 type DataT struct { 316 Id string `json:"symbol"` 317 BaseCurrency string `json:"base-currency"` 318 } 319 type APIResponse struct { 320 Data []DataT `json:"data"` 321 } 322 323 data, _, err := utils.GetRequest("http://api.huobi.pro/v1/common/symbols") 324 325 if err != nil { 326 return 327 } 328 329 var ar APIResponse 330 err = json.Unmarshal(data, &ar) 331 if err == nil { 332 for _, p := range ar.Data { 333 pairToNormalize := dia.ExchangePair{ 334 Symbol: p.BaseCurrency, 335 ForeignName: p.Id, 336 Exchange: s.exchangeName, 337 } 338 pair, serr := s.NormalizePair(pairToNormalize) 339 if serr == nil { 340 pairs = append(pairs, pair) 341 } else { 342 log.Error(serr) 343 } 344 } 345 } 346 return 347 } 348 349 // HuobiPairScraper implements PairScraper for Huobi exchange 350 type HuobiPairScraper struct { 351 parent *HuobiScraper 352 pair dia.ExchangePair 353 closed bool 354 } 355 356 // Close stops listening for trades of the pair associated with s 357 func (ps *HuobiPairScraper) Close() error { 358 ps.closed = true 359 return nil 360 } 361 362 // Channel returns a channel that can be used to receive trades 363 func (ps *HuobiScraper) Channel() chan *dia.Trade { 364 return ps.chanTrades 365 } 366 367 // Error returns an error when the channel Channel() is closed 368 // and nil otherwise 369 func (ps *HuobiPairScraper) Error() error { 370 s := ps.parent 371 s.errorLock.RLock() 372 defer s.errorLock.RUnlock() 373 return s.error 374 } 375 376 // Pair returns the pair this scraper is subscribed to 377 func (ps *HuobiPairScraper) Pair() dia.ExchangePair { 378 return ps.pair 379 }