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  }