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  }