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

     1  package scrapers
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"os"
     7  	"strconv"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	bitfinex "github.com/bitfinexcom/bitfinex-api-go/v2"
    13  	"github.com/bitfinexcom/bitfinex-api-go/v2/rest"
    14  	"github.com/bitfinexcom/bitfinex-api-go/v2/websocket"
    15  	"github.com/diadata-org/diadata/pkg/dia"
    16  	models "github.com/diadata-org/diadata/pkg/model"
    17  	utils "github.com/diadata-org/diadata/pkg/utils"
    18  	"github.com/op/go-logging"
    19  	"github.com/zekroTJA/timedmap"
    20  )
    21  
    22  type pairScraperSet map[*BitfinexPairScraper]nothing
    23  
    24  // BitfinexScraper is a Scraper for collecting trades from the Bitfinex websocket API
    25  type BitfinexScraper struct {
    26  	// the websocket connection to the Bitfinex API
    27  	wsClient   *websocket.Client
    28  	restClient *rest.Client
    29  	// signaling channels for session initialization and finishing
    30  	initDone     chan nothing
    31  	shutdown     chan nothing
    32  	shutdownDone chan nothing
    33  	// error handling; to read error or closed, first acquire read lock
    34  	// only cleanup method should hold write lock
    35  	errorLock sync.RWMutex
    36  	error     error
    37  	closed    bool
    38  	// used to keep track of trading pairs that we subscribed to
    39  	// use sync.Maps to concurrently handle multiple pairs
    40  	pairScrapers      sync.Map          // dia.ExchangePair -> pairScraperSet
    41  	pairSubscriptions sync.Map          // dia.ExchangePair -> string (subscription ID)
    42  	symbols           map[string]string // pair to symbol mapping
    43  	exchangeName      string
    44  	chanTrades        chan *dia.Trade
    45  	db                *models.RelDB
    46  }
    47  
    48  // NewBitfinexScraper returns a new BitfinexScraper for the given pair
    49  func NewBitfinexScraper(key string, secret string, exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BitfinexScraper {
    50  	// we want to ensure there are no gaps in our stream
    51  	// -> close the returned channel on disconnect, forcing the caller to handle
    52  	// possible gaps
    53  	params := websocket.NewDefaultParameters()
    54  	//TODO: Set to false again because now we can have holes in our data stream
    55  	params.AutoReconnect = true
    56  	// params.HeartbeatTimeout = 5 * time.Second // used for testing
    57  	// Only info messages should be sent to log backend
    58  	loggerBackend := logging.AddModuleLevel(logging.NewLogBackend(os.Stdout, "", 0))
    59  	loggerBackend.SetLevel(logging.INFO, "")
    60  	params.Logger = logging.MustGetLogger("scrapers")
    61  	params.Logger.SetBackend(loggerBackend)
    62  
    63  	s := &BitfinexScraper{
    64  		wsClient:     websocket.NewWithParams(params),
    65  		restClient:   rest.NewClient().Credentials(key, secret),
    66  		initDone:     make(chan nothing),
    67  		shutdown:     make(chan nothing),
    68  		shutdownDone: make(chan nothing),
    69  		symbols:      make(map[string]string),
    70  		exchangeName: exchange.Name,
    71  		error:        nil,
    72  		chanTrades:   make(chan *dia.Trade),
    73  		db:           relDB,
    74  	}
    75  
    76  	// establish connection in the background
    77  	if scrape {
    78  		go s.mainLoop()
    79  	}
    80  	return s
    81  }
    82  
    83  // runs in a goroutine until s is closed
    84  func (s *BitfinexScraper) mainLoop() {
    85  	err := s.wsClient.Connect()
    86  	listener := s.wsClient.Listen()
    87  	close(s.initDone)
    88  	if err != nil {
    89  		s.cleanup(err)
    90  		return
    91  	}
    92  
    93  	tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
    94  	tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
    95  
    96  	for {
    97  		select {
    98  		case msg, ok := <-listener:
    99  			if ok {
   100  				var exchangepair dia.ExchangePair
   101  				//	log.Printf("MSG RECV: %#v\n", msg)
   102  				// find out message type
   103  				switch m := msg.(type) {
   104  				case *bitfinex.Trade:
   105  					volume := m.Amount
   106  					if m.Side != bitfinex.Bid {
   107  						volume = -volume
   108  					}
   109  
   110  					exchangepair, err = s.db.GetExchangePairCache(s.exchangeName, m.Pair)
   111  					if err != nil {
   112  						log.Error(err)
   113  					}
   114  					// parse trade data structure
   115  					t := &dia.Trade{
   116  						Symbol:         s.symbols[m.Pair],
   117  						Pair:           m.Pair,
   118  						Price:          m.Price,
   119  						Volume:         volume,
   120  						Time:           time.Unix(m.MTS/1000, (m.MTS%1000)*int64(time.Millisecond)),
   121  						ForeignTradeID: strconv.FormatInt(m.ID, 16),
   122  						Source:         s.exchangeName,
   123  						VerifiedPair:   exchangepair.Verified,
   124  						BaseToken:      exchangepair.UnderlyingPair.BaseToken,
   125  						QuoteToken:     exchangepair.UnderlyingPair.QuoteToken,
   126  					}
   127  					if exchangepair.Verified {
   128  						log.Infoln("Got verified trade", t)
   129  					}
   130  
   131  					// Handle duplicate trades.
   132  					discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory)
   133  					if !discardTrade {
   134  						t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   135  						s.chanTrades <- t
   136  					}
   137  
   138  				case error:
   139  					s.cleanup(m)
   140  					return
   141  				}
   142  			} else {
   143  				s.cleanup(errors.New("BitfinexScraper: Listener channel was closed unexpectedly"))
   144  				return
   145  			}
   146  		case <-s.shutdown: // user requested shutdown
   147  			log.Println("BitfinexScraper shutting down")
   148  			s.cleanup(nil)
   149  			return
   150  		}
   151  	}
   152  }
   153  
   154  // closes all connected PairScrapers
   155  // must only be called from mainLoop
   156  func (s *BitfinexScraper) cleanup(err error) {
   157  	s.errorLock.Lock()
   158  	defer s.errorLock.Unlock()
   159  	// close all channels of PairScraper children
   160  	s.pairScrapers.Range(func(k, v interface{}) bool {
   161  		for ps := range v.(pairScraperSet) {
   162  			ps.closed = true
   163  		}
   164  		s.pairScrapers.Delete(k)
   165  		return true
   166  	})
   167  	if s.wsClient.IsConnected() {
   168  		s.wsClient.Close()
   169  	}
   170  	if err != nil {
   171  		s.error = err
   172  	}
   173  	s.closed = true
   174  	close(s.shutdownDone) // signal that shutdown is complete
   175  }
   176  
   177  // Close closes any existing API connections, as well as channels of
   178  // PairScrapers from calls to ScrapePair
   179  func (s *BitfinexScraper) Close() error {
   180  	if s.closed {
   181  		return errors.New("BitfinexScraper: Already closed")
   182  	}
   183  	close(s.shutdown)
   184  	<-s.shutdownDone
   185  	s.errorLock.RLock()
   186  	defer s.errorLock.RUnlock()
   187  	return s.error
   188  }
   189  
   190  // ScrapePair returns a PairScraper that can be used to get trades for a single pair from
   191  // this APIScraper
   192  func (s *BitfinexScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   193  	<-s.initDone // wait until wsClient is connected
   194  	s.errorLock.RLock()
   195  	defer s.errorLock.RUnlock()
   196  	if s.error != nil {
   197  		return nil, s.error
   198  	}
   199  	if s.closed {
   200  		return nil, errors.New("BitfinexScraper: Call ScrapePair on closed scraper")
   201  	}
   202  	ps := &BitfinexPairScraper{
   203  		parent: s,
   204  		pair:   pair,
   205  	}
   206  
   207  	s.symbols[pair.ForeignName] = pair.Symbol
   208  
   209  	// initialize pairScraperSet for pair if not already done
   210  	pairScrapers, _ := s.pairScrapers.LoadOrStore(pair.ForeignName, pairScraperSet{})
   211  	// register ps
   212  	pairScrapers.(pairScraperSet)[ps] = nothing{}
   213  	// subscribe to trading pair if we are the first scraper for this pair
   214  	if _, ok := s.pairSubscriptions.Load(pair.ForeignName); !ok {
   215  		ctx1, ctx1cancel := context.WithTimeout(context.Background(), 5*time.Second)
   216  		defer ctx1cancel()
   217  		id, err := s.wsClient.SubscribeTrades(ctx1, pair.ForeignName)
   218  		if err != nil {
   219  			// well that didn't work -> cleanup and return error
   220  			delete(pairScrapers.(pairScraperSet), ps)
   221  			return nil, err
   222  		}
   223  		s.pairSubscriptions.Store(pair.ForeignName, id)
   224  	}
   225  	return ps, nil
   226  }
   227  func (s *BitfinexScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   228  
   229  	switch pair.Symbol {
   230  	case "IOT":
   231  		pair.Symbol = "MIOTA"
   232  	case "IOS":
   233  		pair.Symbol = "IOST"
   234  	case "QTM":
   235  		pair.Symbol = "QTUM"
   236  	case "QSH":
   237  		pair.Symbol = "QASH"
   238  	case "DSH":
   239  		pair.Symbol = "DASH"
   240  	}
   241  	return pair, nil
   242  
   243  }
   244  
   245  // func (s *BitfinexScraper) normalizeSymbol(pair dia.Pair) (dia.Pair, error) {
   246  // 	pair.Symbol = strings.ToUpper(pair.ForeignName[0:3])
   247  // 	if helpers.NameForSymbol(pair.Symbol) == pair.Symbol {
   248  // 		if !helpers.SymbolIsName(pair.Symbol) {
   249  // 			pair, _ = s.NormalizePair(pair)
   250  // 			return pair, errors.New("Foreign name can not be normalized:" + pair.ForeignName + " symbol:" + pair.Symbol)
   251  // 		}
   252  // 	}
   253  // 	if helpers.SymbolIsBlackListed(pair.Symbol) {
   254  // 		return pair, errors.New("Symbol is black listed:" + pair.Symbol)
   255  // 	}
   256  // 	return pair, nil
   257  // }
   258  
   259  func (s *BitfinexScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) {
   260  	// TO DO
   261  	return dia.Asset{Symbol: symbol}, nil
   262  }
   263  
   264  // FetchAvailablePairs returns a list with all available trade pairs
   265  func (s *BitfinexScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   266  
   267  	data, _, err := utils.GetRequest("https://api.bitfinex.com/v1/symbols")
   268  	if err != nil {
   269  		return
   270  	}
   271  	ls := strings.Split(strings.Replace(string(data)[1:len(data)-1], "\"", "", -1), ",")
   272  	for _, p := range ls {
   273  		var pairToNormalize dia.ExchangePair
   274  		if len(p) == 6 {
   275  			pairToNormalize.Symbol = strings.ToUpper(p[0:3])
   276  		} else {
   277  			pairToNormalize.Symbol = strings.ToUpper(strings.Split(p, ":")[0])
   278  		}
   279  		pairToNormalize.ForeignName = strings.ToUpper(p)
   280  		pairToNormalize.Exchange = s.exchangeName
   281  		pair, serr := s.NormalizePair(pairToNormalize)
   282  		if serr == nil {
   283  			pairs = append(pairs, pair)
   284  		} else {
   285  			log.Error(serr)
   286  		}
   287  	}
   288  	return
   289  }
   290  
   291  // BitfinexPairScraper implements PairScraper for Bitfinex
   292  type BitfinexPairScraper struct {
   293  	parent *BitfinexScraper
   294  	pair   dia.ExchangePair
   295  	closed bool
   296  }
   297  
   298  // Close stops listening for trades of the pair associated with s
   299  func (ps *BitfinexPairScraper) Close() error {
   300  	var err error
   301  	s := ps.parent
   302  	// if parent already errored, return early
   303  	s.errorLock.RLock()
   304  	defer s.errorLock.RUnlock()
   305  	if s.error != nil {
   306  		return s.error
   307  	}
   308  	if ps.closed {
   309  		return errors.New("BitfinexPairScraper: Already closed")
   310  	}
   311  	pairScrapers, ok := s.pairScrapers.Load(ps.pair.Symbol)
   312  	if !ok { // should never happen
   313  		panic("BitfinexPairScraper: pairScraperSet not found")
   314  	}
   315  	// deregister and close channel
   316  	delete(pairScrapers.(pairScraperSet), ps)
   317  	// if we're the last one for this pair -> unsubscribe
   318  	if len(pairScrapers.(pairScraperSet)) == 0 {
   319  		id, ok := s.pairSubscriptions.Load(ps.pair.Symbol)
   320  		if !ok { // should never happen
   321  			panic("BitfinexPairScraper: Subscription ID not found")
   322  		}
   323  		ctx1, ctx1cancel := context.WithTimeout(context.Background(), 5*time.Second)
   324  		defer ctx1cancel()
   325  		err = s.wsClient.Unsubscribe(ctx1, id.(string))
   326  	}
   327  	ps.closed = true
   328  	return err
   329  }
   330  
   331  // Channel returns a channel that can be used to receive trades
   332  func (ps *BitfinexScraper) Channel() chan *dia.Trade {
   333  	return ps.chanTrades
   334  }
   335  
   336  // Error returns an error when the channel Channel() is closed
   337  // and nil otherwise
   338  func (ps *BitfinexPairScraper) Error() error {
   339  	s := ps.parent
   340  	s.errorLock.RLock()
   341  	defer s.errorLock.RUnlock()
   342  	return s.error
   343  }
   344  
   345  // Pair returns the pair this scraper is subscribed to
   346  func (ps *BitfinexPairScraper) Pair() dia.ExchangePair {
   347  	return ps.pair
   348  }