github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/OKExScraper.go (about)

     1  package scrapers
     2  
     3  import (
     4  	"bytes"
     5  	"compress/flate"
     6  	"encoding/json"
     7  	"errors"
     8  	"io/ioutil"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/diadata-org/diadata/pkg/dia"
    15  	"github.com/diadata-org/diadata/pkg/dia/helpers"
    16  	models "github.com/diadata-org/diadata/pkg/model"
    17  	utils "github.com/diadata-org/diadata/pkg/utils"
    18  	ws "github.com/gorilla/websocket"
    19  	"github.com/zekroTJA/timedmap"
    20  )
    21  
    22  var _OKExSocketURL = utils.Getenv("OKEX_WS_URL", "wss://ws.okx.com:8443/ws/v5/public")
    23  
    24  //var _OKExSocketURL = url.URL{Scheme: "wss", Host: "real.okex.com:10441", Path: "/ws/v1", RawQuery: "compress=true"}
    25  
    26  type Response struct {
    27  	Channel string     `json:"channel"`
    28  	Data    [][]string `json:"data"`
    29  	Binary  int        `json:"binary"`
    30  }
    31  
    32  type Responses []Response
    33  
    34  type Subscribe struct {
    35  	OP   string     `json:"op"`
    36  	Args []OKEXArgs `json:"args"`
    37  }
    38  
    39  type OKEXArgs struct {
    40  	Channel string `json:"channel"`
    41  	InstID  string `json:"instId"`
    42  }
    43  
    44  type OKExScraper struct {
    45  	wsClient *ws.Conn
    46  	// signaling channels for session initialization and finishing
    47  	run          bool
    48  	shutdown     chan nothing
    49  	shutdownDone chan nothing
    50  	// error handling; to read error or closed, first acquire read lock
    51  	// only cleanup method should hold write lock
    52  	errorLock sync.RWMutex
    53  	error     error
    54  	closed    bool
    55  	// used to keep track of trading pairs that we subscribed to
    56  	pairScrapers map[string]*OKExPairScraper
    57  	exchangeName string
    58  	chanTrades   chan *dia.Trade
    59  	db           *models.RelDB
    60  }
    61  
    62  // NewOKExScraper returns a new OKExScraper for the given pair
    63  func NewOKExScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *OKExScraper {
    64  
    65  	s := &OKExScraper{
    66  		shutdown:     make(chan nothing),
    67  		shutdownDone: make(chan nothing),
    68  		pairScrapers: make(map[string]*OKExPairScraper),
    69  		exchangeName: exchange.Name,
    70  		error:        nil,
    71  		chanTrades:   make(chan *dia.Trade),
    72  		db:           relDB,
    73  	}
    74  
    75  	var wsDialer ws.Dialer
    76  	SwConn, _, err := wsDialer.Dial(_OKExSocketURL, nil)
    77  	if err != nil {
    78  		log.Error("dial:", err)
    79  	}
    80  	s.wsClient = SwConn
    81  	if scrape {
    82  		go s.mainLoop()
    83  	}
    84  	return s
    85  }
    86  
    87  // Useful to reconnect to ws when the connection is down
    88  func (s *OKExScraper) reconnectToWS() {
    89  
    90  	var wsDialer ws.Dialer
    91  	SwConn, _, err := wsDialer.Dial(_OKExSocketURL, nil)
    92  
    93  	if err != nil {
    94  		log.Error("dial:", err)
    95  	}
    96  
    97  	s.wsClient = SwConn
    98  }
    99  
   100  type OKEXMarket struct {
   101  	Alias     string `json:"alias"`
   102  	BaseCcy   string `json:"baseCcy"`
   103  	Category  string `json:"category"`
   104  	CtMult    string `json:"ctMult"`
   105  	CtType    string `json:"ctType"`
   106  	CtVal     string `json:"ctVal"`
   107  	CtValCcy  string `json:"ctValCcy"`
   108  	ExpTime   string `json:"expTime"`
   109  	InstID    string `json:"instId"`
   110  	InstType  string `json:"instType"`
   111  	Lever     string `json:"lever"`
   112  	ListTime  string `json:"listTime"`
   113  	LotSz     string `json:"lotSz"`
   114  	MinSz     string `json:"minSz"`
   115  	OptType   string `json:"optType"`
   116  	QuoteCcy  string `json:"quoteCcy"`
   117  	SettleCcy string `json:"settleCcy"`
   118  	State     string `json:"state"`
   119  	Stk       string `json:"stk"`
   120  	TickSz    string `json:"tickSz"`
   121  	Uly       string `json:"uly"`
   122  }
   123  
   124  type AllOKEXMarketResponse struct {
   125  	Code string       `json:"code"`
   126  	Data []OKEXMarket `json:"data"`
   127  	Msg  string       `json:"msg"`
   128  }
   129  
   130  // Subscribe again to all channels
   131  func (s *OKExScraper) subscribeToALL() {
   132  	var (
   133  		resp     AllOKEXMarketResponse
   134  		allPairs []OKEXArgs
   135  	)
   136  
   137  	b, _, err := utils.GetRequest("https://okx.com/api/v5/public/instruments?instType=SPOT")
   138  	if err != nil {
   139  		log.Errorln("Error getting OKex market", err)
   140  	}
   141  
   142  	err = json.Unmarshal(b, &resp)
   143  	if err != nil {
   144  		log.Errorln("Error Unmarshalling OKex market json", err)
   145  	}
   146  
   147  	for _, v := range resp.Data {
   148  		allPairs = append(allPairs, OKEXArgs{Channel: "trades", InstID: v.InstID})
   149  	}
   150  
   151  	a := &Subscribe{
   152  		OP:   "subscribe",
   153  		Args: allPairs,
   154  	}
   155  
   156  	if err := s.wsClient.WriteJSON(a); err != nil {
   157  		log.Errorln(err.Error())
   158  	}
   159  
   160  }
   161  
   162  type OKEXWSResponse struct {
   163  	Arg struct {
   164  		Channel string `json:"channel"`
   165  		InstID  string `json:"instId"`
   166  	} `json:"arg"`
   167  	Data []struct {
   168  		InstID  string `json:"instId"`
   169  		TradeID string `json:"tradeId"`
   170  		Px      string `json:"px"`
   171  		Sz      string `json:"sz"`
   172  		Side    string `json:"side"`
   173  		Ts      string `json:"ts"`
   174  	} `json:"data"`
   175  }
   176  
   177  // runs in a goroutine until s is closed
   178  func (s *OKExScraper) mainLoop() {
   179  
   180  	tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   181  	tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   182  
   183  	s.run = true
   184  	for s.run {
   185  		var message OKEXWSResponse
   186  		messageType, messageTemp, err := s.wsClient.ReadMessage()
   187  		if err != nil {
   188  			log.Warning("reconnect the scraping to ws, ", err, ":", message)
   189  			s.reconnectToWS()
   190  			s.subscribeToALL()
   191  		} else {
   192  			switch messageType {
   193  			case ws.TextMessage:
   194  				// no need uncompressed
   195  				err := json.Unmarshal(messageTemp, &message)
   196  				if err != nil {
   197  					log.Errorln("Error parsing response")
   198  				}
   199  				ps, ok := s.pairScrapers[message.Arg.InstID]
   200  
   201  				if ok && len(message.Data) > 0 {
   202  
   203  					f64PriceString := message.Data[0].Px
   204  					f64Price, err := strconv.ParseFloat(f64PriceString, 64)
   205  
   206  					if err == nil {
   207  
   208  						f64VolumeString := message.Data[0].Sz
   209  						f64Volume, err := strconv.ParseFloat(f64VolumeString, 64)
   210  
   211  						if err == nil {
   212  
   213  							ts, _ := strconv.ParseInt(message.Data[0].Ts, 10, 64)
   214  							timeStamp := time.Unix(int64(ts)/1e3, 0)
   215  							if message.Data[0].Side == "sell" {
   216  								f64Volume = -f64Volume
   217  							}
   218  
   219  							exchangepair, err := s.db.GetExchangePairCache(s.exchangeName, message.Arg.InstID)
   220  							if err != nil {
   221  								log.Error(err)
   222  							}
   223  
   224  							t := &dia.Trade{
   225  								Symbol:         ps.pair.Symbol,
   226  								Pair:           message.Arg.InstID,
   227  								Price:          f64Price,
   228  								Volume:         f64Volume,
   229  								Time:           timeStamp,
   230  								ForeignTradeID: message.Data[0].TradeID,
   231  								Source:         s.exchangeName,
   232  								VerifiedPair:   exchangepair.Verified,
   233  								BaseToken:      exchangepair.UnderlyingPair.BaseToken,
   234  								QuoteToken:     exchangepair.UnderlyingPair.QuoteToken,
   235  							}
   236  							if exchangepair.Verified {
   237  								log.Infoln("Got verified trade", t)
   238  							}
   239  							// Handle duplicate trades.
   240  							discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory)
   241  							if !discardTrade {
   242  								t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   243  
   244  								ps.parent.chanTrades <- t
   245  							}
   246  						} else {
   247  							log.Errorf("parsing volume %v", f64VolumeString)
   248  						}
   249  
   250  					} else {
   251  						log.Errorf("parsing price %v", f64PriceString)
   252  					}
   253  				}
   254  
   255  			}
   256  		}
   257  	}
   258  	s.cleanup(errors.New("main loop terminated by Close()"))
   259  }
   260  
   261  func GzipDecode(in []byte) (content []byte, err error) {
   262  	reader := flate.NewReader(bytes.NewReader(in))
   263  	defer func() {
   264  		cerr := reader.Close()
   265  		if err == nil {
   266  			err = cerr
   267  		}
   268  	}()
   269  	content, err = ioutil.ReadAll(reader)
   270  
   271  	return
   272  }
   273  
   274  func (s *OKExScraper) cleanup(err error) {
   275  	s.errorLock.Lock()
   276  	defer s.errorLock.Unlock()
   277  
   278  	if err != nil {
   279  		s.error = err
   280  	}
   281  	s.closed = true
   282  
   283  	close(s.shutdownDone)
   284  }
   285  
   286  // Close closes any existing API connections, as well as channels of
   287  // PairScrapers from calls to ScrapePair
   288  func (s *OKExScraper) Close() error {
   289  
   290  	if s.closed {
   291  		return errors.New("OKExScraper: Already closed")
   292  	}
   293  
   294  	close(s.shutdown)
   295  	// Set false first to prevent reconnect
   296  	s.run = false
   297  	err := s.wsClient.Close()
   298  	if err != nil {
   299  		return err
   300  	}
   301  	<-s.shutdownDone
   302  	s.errorLock.RLock()
   303  	defer s.errorLock.RUnlock()
   304  	return s.error
   305  }
   306  
   307  // ScrapePair returns a PairScraper that can be used to get trades for a single pair from
   308  // this APIScraper
   309  func (s *OKExScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   310  
   311  	s.errorLock.RLock()
   312  	defer s.errorLock.RUnlock()
   313  
   314  	if s.error != nil {
   315  		return nil, s.error
   316  	}
   317  
   318  	if s.closed {
   319  		return nil, errors.New("OKExScraper: Call ScrapePair on closed scraper")
   320  	}
   321  
   322  	ps := &OKExPairScraper{
   323  		parent: s,
   324  		pair:   pair,
   325  	}
   326  
   327  	s.pairScrapers[pair.ForeignName] = ps
   328  
   329  	//a := &Subscribe{
   330  	//	OP:      "addChannel",
   331  	//	Args: "ok_sub_spot_" + strings.ToLower(pair.ForeignName) + "_deals",
   332  	//}
   333  	//
   334  	//subByteString := `{"channel":` + `"` + a.Channel + `"` + `,"event":` + `"` + a.Event + `"}`
   335  	//if err := s.wsClient.WriteMessage(ws.TextMessage, []byte(subByteString)); err != nil {
   336  	//	fmt.Println(err.Error())
   337  	//}
   338  
   339  	return ps, nil
   340  }
   341  
   342  /*
   343  func (s *OKExScraper) normalizeSymbol(foreignName string, baseCurrency string) (symbol string, err error) {
   344  	symbol = strings.ToUpper(baseCurrency)
   345  	if helpers.NameForSymbol(symbol) == symbol {
   346  		if !helpers.SymbolIsName(symbol) {
   347  			if symbol == "IOTA" {
   348  				return "MIOTA", nil
   349  			}
   350  			if symbol == "YOYO" {
   351  				return "YOYOW", nil
   352  			}
   353  			return symbol, errors.New("Foreign name can not be normalized:" + foreignName + " symbol:" + symbol)
   354  		}
   355  	}
   356  	if helpers.SymbolIsBlackListed(symbol) {
   357  		return symbol, errors.New("Symbol is black listed:" + symbol)
   358  	}
   359  	return symbol, nil
   360  }
   361  */
   362  
   363  func (s *OKExScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   364  	symbol := strings.ToUpper(pair.Symbol)
   365  	pair.Symbol = symbol
   366  
   367  	if helpers.NameForSymbol(symbol) == symbol {
   368  		if !helpers.SymbolIsName(symbol) {
   369  			if pair.Symbol == "IOTA" {
   370  				pair.Symbol = "MIOTA"
   371  			}
   372  			if pair.Symbol == "YOYO" {
   373  				pair.Symbol = "YOYOW"
   374  			}
   375  			return pair, errors.New("Foreign name can not be normalized:" + pair.ForeignName + " symbol:" + symbol)
   376  		}
   377  	}
   378  	if helpers.SymbolIsBlackListed(symbol) {
   379  		return pair, errors.New("Symbol is black listed:" + symbol)
   380  	}
   381  	return pair, nil
   382  
   383  }
   384  
   385  func (s *OKExScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) {
   386  	//  TO DO
   387  	return dia.Asset{Symbol: symbol}, nil
   388  }
   389  
   390  // FetchAvailablePairs returns a list with all available trade pairs
   391  func (s *OKExScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   392  	type APIResponse struct {
   393  		Id           string `json:"instrument_id"`
   394  		BaseCurrency string `json:"base_currency"`
   395  	}
   396  
   397  	data, _, err := utils.GetRequest("https://www.okx.com/api/spot/v3/products")
   398  
   399  	if err != nil {
   400  		return
   401  	}
   402  
   403  	var ar []APIResponse
   404  	err = json.Unmarshal(data, &ar)
   405  	if err == nil {
   406  		for _, p := range ar {
   407  			pairToNormalize := dia.ExchangePair{
   408  				Symbol:      p.BaseCurrency,
   409  				ForeignName: p.Id,
   410  				Exchange:    s.exchangeName,
   411  			}
   412  			pair, serr := s.NormalizePair(pairToNormalize)
   413  			if serr == nil {
   414  				pairs = append(pairs, pair)
   415  			} else {
   416  				log.Error(serr)
   417  			}
   418  		}
   419  	}
   420  	return
   421  }
   422  
   423  // OKExPairScraper implements PairScraper for OKEx exchange
   424  type OKExPairScraper struct {
   425  	parent *OKExScraper
   426  	pair   dia.ExchangePair
   427  	closed bool
   428  }
   429  
   430  // Close stops listening for trades of the pair associated with s
   431  func (ps *OKExPairScraper) Close() error {
   432  	ps.closed = true
   433  	return nil
   434  }
   435  
   436  // Channel returns a channel that can be used to receive trades
   437  func (s *OKExScraper) Channel() chan *dia.Trade {
   438  	return s.chanTrades
   439  }
   440  
   441  // Error returns an error when the channel Channel() is closed
   442  // and nil otherwise
   443  func (ps *OKExPairScraper) Error() error {
   444  	s := ps.parent
   445  	s.errorLock.RLock()
   446  	defer s.errorLock.RUnlock()
   447  	return s.error
   448  }
   449  
   450  // Pair returns the pair this scraper is subscribed to
   451  func (ps *OKExPairScraper) Pair() dia.ExchangePair {
   452  	return ps.pair
   453  }