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

     1  package scrapers
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/diadata-org/diadata/pkg/dia"
    15  	models "github.com/diadata-org/diadata/pkg/model"
    16  	ws "github.com/gorilla/websocket"
    17  	"github.com/zekroTJA/timedmap"
    18  )
    19  
    20  type BKEXScraper struct {
    21  	wsClient map[int]*ws.Conn
    22  	// signaling channels for session initialization and finishing
    23  	shutdown     chan nothing
    24  	shutdownDone chan nothing
    25  	// error handling; to read error or closed, first acquire read lock
    26  	// only cleanup method should hold write lock
    27  	errorLock sync.RWMutex
    28  	error     error
    29  	closed    bool
    30  	// used to keep track of trading pairs that we subscribed to
    31  	pairScrapers map[string]*BKEXPairScraper
    32  	exchangeName string
    33  	scraperName  string
    34  	chanTrades   chan *dia.Trade
    35  	db           *models.RelDB
    36  }
    37  
    38  func NewBKEXScraper(exchange dia.Exchange, scraperName string, scrape bool, relDB *models.RelDB) *BKEXScraper {
    39  	s := &BKEXScraper{
    40  		wsClient:     make(map[int]*ws.Conn),
    41  		shutdown:     make(chan nothing),
    42  		shutdownDone: make(chan nothing),
    43  		pairScrapers: make(map[string]*BKEXPairScraper),
    44  		exchangeName: exchange.Name,
    45  		scraperName:  scraperName,
    46  		error:        nil,
    47  		chanTrades:   make(chan *dia.Trade),
    48  		db:           relDB,
    49  	}
    50  
    51  	if scrape {
    52  		go s.mainLoop()
    53  	}
    54  
    55  	return s
    56  }
    57  
    58  type BKEXTradeRecord struct {
    59  	Symbol    string  `json:"symbol"`
    60  	Price     string  `json:"price"`
    61  	Volume    float64 `json:"volume"`
    62  	Direction string  `json:"direction"`
    63  	Ts        int64   `json:"ts"`
    64  }
    65  
    66  type BKEXTradeResponse struct {
    67  	quotationAllDeal string
    68  	records          []BKEXTradeRecord
    69  }
    70  
    71  func chunkSlice(slice []string, chunkSize int) [][]string {
    72  	var chunks [][]string
    73  	for {
    74  		if len(slice) == 0 {
    75  			break
    76  		}
    77  
    78  		// necessary check to avoid slicing beyond
    79  		// slice capacity
    80  		if len(slice) < chunkSize {
    81  			chunkSize = len(slice)
    82  		}
    83  
    84  		chunks = append(chunks, slice[0:chunkSize])
    85  		slice = slice[chunkSize:]
    86  	}
    87  
    88  	return chunks
    89  }
    90  
    91  func (s *BKEXScraper) connect(i int) *ws.Conn {
    92  	var wsDialer ws.Dialer
    93  	SwConn, _, err := wsDialer.Dial("wss://api.bkex.com/socket.io/?EIO=3&transport=websocket", nil)
    94  
    95  	if err != nil {
    96  		println(err.Error())
    97  		return nil
    98  	}
    99  	s.wsClient[i] = SwConn
   100  
   101  	// Two time read message
   102  	messageType, p, err := SwConn.ReadMessage()
   103  
   104  	if err != nil {
   105  		println(err.Error())
   106  	}
   107  
   108  	log.Info("Connected ", messageType, "-", string(p))
   109  
   110  	messageType, p, err = SwConn.ReadMessage()
   111  
   112  	if err != nil {
   113  		println(err.Error())
   114  	}
   115  
   116  	log.Info("Connected ", messageType, "-", string(p))
   117  	// Connect Finished
   118  
   119  	// Send 40/quotation and receive it
   120  	writeErr := SwConn.WriteMessage(ws.TextMessage, []byte("40/quotation"))
   121  	if writeErr != nil {
   122  		log.Error("Error writing message ", writeErr)
   123  	}
   124  	messageType, p, err = SwConn.ReadMessage()
   125  	if err != nil {
   126  		log.Error("Error writing message ", err)
   127  	}
   128  	log.Info("Connected ", messageType, "-", string(p))
   129  
   130  	return SwConn
   131  }
   132  
   133  func (s *BKEXScraper) subLoop(wsClient *ws.Conn, pairs string) {
   134  	pingTimer := time.NewTicker(25 * time.Second)
   135  	go func() {
   136  		for range pingTimer.C {
   137  			go s.ping(wsClient)
   138  		}
   139  	}()
   140  
   141  	tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   142  	tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   143  
   144  	message := `42/quotation,["quotationDealConnect",{"symbol": "` + pairs + `","number": 50}]`
   145  
   146  	if err := wsClient.WriteMessage(ws.TextMessage, []byte(message)); err != nil {
   147  		log.Error("write pair sub: ", err.Error())
   148  	}
   149  	log.Info("Subscribed to get trades for ", pairs)
   150  
   151  	for {
   152  		messageType, p, err := wsClient.ReadMessage()
   153  		if err != nil {
   154  			log.Fatal("read message: ", err.Error())
   155  		}
   156  		if messageType != ws.TextMessage {
   157  			log.Fatal("unknow type ", messageType)
   158  		}
   159  
   160  		c := string(p)
   161  
   162  		if c == "3" {
   163  			continue
   164  		}
   165  
   166  		if len(strings.Split(c, "42/quotation,")) < 2 {
   167  			continue
   168  		}
   169  		d := strings.Split(c, "42/quotation,")[1]
   170  
   171  		var r BKEXTradeResponse
   172  		tmp := []interface{}{&r.quotationAllDeal, &r.records}
   173  
   174  		jsonErr := json.Unmarshal([]byte(d), &tmp)
   175  		if jsonErr != nil {
   176  			log.Error("can't unmarshal json ", jsonErr)
   177  		}
   178  
   179  		if e := len(tmp); e != 2 {
   180  			log.Fatal("unknow length ", e)
   181  		}
   182  
   183  		records := tmp[1].(*[]BKEXTradeRecord)
   184  
   185  		for _, trade := range *records {
   186  			var exchangePair dia.ExchangePair
   187  			priceFloat, _ := strconv.ParseFloat(trade.Price, 64)
   188  
   189  			exchangePair, err = s.db.GetExchangePairCache(s.scraperName, trade.Symbol)
   190  			if err != nil {
   191  				log.Error("Get Exchange Pair  ", trade.Symbol)
   192  			}
   193  			volume := trade.Volume
   194  			if trade.Direction == "S" {
   195  				volume *= -1
   196  			}
   197  
   198  			t := &dia.Trade{
   199  				Symbol:       strings.Split(trade.Symbol, "_")[0],
   200  				Pair:         trade.Symbol,
   201  				Price:        priceFloat,
   202  				Volume:       volume,
   203  				Time:         time.Unix(0, trade.Ts*int64(time.Millisecond)),
   204  				Source:       s.exchangeName,
   205  				VerifiedPair: exchangePair.Verified,
   206  				BaseToken:    exchangePair.UnderlyingPair.BaseToken,
   207  				QuoteToken:   exchangePair.UnderlyingPair.QuoteToken,
   208  			}
   209  
   210  			if exchangePair.Verified {
   211  				log.Infoln("Got verified trade", t)
   212  			}
   213  			// Handle duplicate trades.
   214  			discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory)
   215  			if !discardTrade {
   216  				t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   217  				s.chanTrades <- t
   218  			}
   219  
   220  		}
   221  	}
   222  }
   223  
   224  func (s *BKEXScraper) mainLoop() {
   225  
   226  	log.Info("Wait 5s untill subscribe all Pairs")
   227  	time.Sleep(5 * time.Second)
   228  
   229  	keys := make([]string, 0, len(s.pairScrapers))
   230  	for k := range s.pairScrapers {
   231  		keys = append(keys, k)
   232  	}
   233  
   234  	miniPairs := chunkSlice(keys, 10)
   235  
   236  	for i, v := range miniPairs {
   237  		log.Info("Connect Websocket ...", i, v)
   238  		time.Sleep(5 * time.Second)
   239  		conn := s.connect(i)
   240  		if conn != nil {
   241  			log.Info("Connect Done Websocket", i, v)
   242  			go s.subLoop(conn, strings.Join(v, ","))
   243  		} else {
   244  			log.Error("Connection Failed !!!", i)
   245  			return
   246  		}
   247  	}
   248  }
   249  
   250  func (s *BKEXScraper) ping(conn *ws.Conn) {
   251  	writeErr := conn.WriteMessage(ws.TextMessage, []byte("2"))
   252  	if writeErr != nil {
   253  		log.Error("Error writing message ", writeErr)
   254  	}
   255  }
   256  
   257  // FillSymbolData from MEXCScraper
   258  // @todo more update
   259  func (s *BKEXScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) {
   260  	asset.Symbol = symbol
   261  	return
   262  }
   263  
   264  func (s *BKEXScraper) Close() error {
   265  	if s.closed {
   266  		return errors.New("BKEXScraper: Already closed")
   267  	}
   268  	close(s.shutdown)
   269  	for _, c := range s.wsClient {
   270  		err := c.Close()
   271  		if err != nil {
   272  			return err
   273  		}
   274  	}
   275  
   276  	<-s.shutdownDone
   277  	s.errorLock.RLock()
   278  	defer s.errorLock.RUnlock()
   279  	return s.error
   280  }
   281  
   282  func (s *BKEXScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   283  	return dia.ExchangePair{}, nil
   284  }
   285  
   286  func (s *BKEXScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   287  	s.errorLock.RLock()
   288  	defer s.errorLock.RUnlock()
   289  
   290  	if s.error != nil {
   291  		return nil, s.error
   292  	}
   293  
   294  	if s.closed {
   295  		return nil, errors.New("BKEXScraper: Call ScrapePair on closed scraper")
   296  	}
   297  
   298  	ps := &BKEXPairScraper{
   299  		parent: s,
   300  		pair:   pair,
   301  	}
   302  
   303  	// message := `42/quotation,["quotationDealConnect",{"symbol": "` + pair.ForeignName + `","number": 50}]`
   304  	// // message := `42/quotation,["quotationDealConnect",{"symbol": "BTC_USDT","number": 50}]`
   305  	// log.Info(message)
   306  
   307  	// if err := s.wsClient.WriteMessage(ws.TextMessage, []byte(message)); err != nil {
   308  	// 	log.Error("write pair sub: ", err.Error())
   309  	// }
   310  	log.Info("Add to get trades for ", pair.ForeignName)
   311  	s.pairScrapers[pair.ForeignName] = ps
   312  	return ps, nil
   313  }
   314  
   315  type BKEXExchangeSymbol struct {
   316  	MinimumOrderSize   float64 `json:"minimumOrderSize"`
   317  	MinimumTradeVolume float64 `json:"minimumTradeVolume"`
   318  	PricePrecision     int     `json:"pricePrecision"`
   319  	SupportTrade       bool    `json:"supportTrade"`
   320  	Symbol             string  `json:"symbol"`
   321  	VolumePrecision    int     `json:"volumePrecision"`
   322  }
   323  
   324  type BKEXExchangeInfo struct {
   325  	Code string               `json:"code"`
   326  	Data []BKEXExchangeSymbol `json:"data"`
   327  }
   328  
   329  func (s *BKEXScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   330  	var bkexExchangeInfo BKEXExchangeInfo
   331  	response, err := http.Get("https://api.bkex.com/v2/common/symbols")
   332  	if err != nil {
   333  		log.Error("get symbols: ", err)
   334  	}
   335  
   336  	defer func(Body io.ReadCloser) {
   337  		errClose := Body.Close()
   338  		if errClose != nil {
   339  			log.Error("body close got error ", errClose)
   340  		}
   341  	}(response.Body)
   342  
   343  	body, err := ioutil.ReadAll(response.Body)
   344  
   345  	if err != nil {
   346  		log.Error("read symbols: ", err)
   347  	}
   348  
   349  	err = json.Unmarshal(body, &bkexExchangeInfo)
   350  
   351  	if err != nil {
   352  		log.Error("unmarshal symbols: ", err)
   353  	}
   354  
   355  	for _, p := range bkexExchangeInfo.Data {
   356  		pairToNormalized := dia.ExchangePair{
   357  			Symbol:      strings.Split(p.Symbol, "_")[0],
   358  			ForeignName: p.Symbol,
   359  			Exchange:    s.exchangeName,
   360  		}
   361  		pairs = append(pairs, pairToNormalized)
   362  	}
   363  	return
   364  }
   365  
   366  // Channel returns a channel that can be used to receive trades
   367  func (s *BKEXScraper) Channel() chan *dia.Trade {
   368  	return s.chanTrades
   369  }
   370  
   371  // BKEXPairScraper implements PairScraper for BKEX
   372  type BKEXPairScraper struct {
   373  	parent *BKEXScraper
   374  	pair   dia.ExchangePair
   375  	closed bool
   376  }
   377  
   378  // Close stops listening for trades of the pair associated with s
   379  func (ps *BKEXPairScraper) Close() error {
   380  	ps.closed = true
   381  	return nil
   382  }
   383  
   384  // Error returns an error when the channel Channel() is closed
   385  // and nil otherwise
   386  func (ps *BKEXPairScraper) Error() error {
   387  	s := ps.parent
   388  	s.errorLock.RLock()
   389  	defer s.errorLock.RUnlock()
   390  	return s.error
   391  }
   392  
   393  // Pair returns the pair this scraper is subscribed to
   394  func (ps *BKEXPairScraper) Pair() dia.ExchangePair {
   395  	return ps.pair
   396  }