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

     1  package scrapers
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     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  const ()
    21  
    22  type BitstampScraper struct {
    23  	wsClient     *ws.Conn
    24  	shutdown     chan nothing
    25  	shutdownDone chan nothing
    26  	// error handling; to read error or closed, first acquire read lock
    27  	// only cleanup method should hold write lock
    28  	errorLock sync.RWMutex
    29  	error     error
    30  	closed    bool
    31  
    32  	pairScrapers map[string]*BitstampPairScraper
    33  	exchangeName string
    34  	chanTrades   chan *dia.Trade
    35  	db           *models.RelDB
    36  }
    37  
    38  type BitstampPairScraper struct {
    39  	parent *BitstampScraper
    40  	pair   dia.ExchangePair
    41  	closed bool
    42  }
    43  
    44  type BitstampPairsInfo []struct {
    45  	Name                        string `json:"name"`
    46  	UrlSymbol                   string `json:"url_symbol"`
    47  	BaseDecimal                 uint8  `json:"base_decimal"`
    48  	CounterDecimals             uint8  `json:"counter_decimals"`
    49  	InstantOrderCounterDecimals uint8  `json:"instant_order_counter_decimals"`
    50  	MinimumOrder                string `json:"minimum_order"`
    51  	Trading                     string `json:"trading"`
    52  	InstantAndMarketOrders      string `json:"instant_and_market_orders"`
    53  	Description                 string `json:"description"`
    54  }
    55  
    56  type BitstampWsResponse struct {
    57  	Event   string      `json:"event"`
    58  	Channel string      `json:"channel"`
    59  	Data    interface{} `json:"data"`
    60  }
    61  
    62  type BitstampPingData struct {
    63  	Status string `json:"status"`
    64  }
    65  
    66  type BitstampTradeData struct {
    67  	Id             string  `json:"id"`
    68  	Amount         float64 `json:"amount"`
    69  	AmountStr      string  `json:"amount_str"`
    70  	Price          float64 `json:"price"`
    71  	PriceStr       string  `json:"price_str"`
    72  	Type           uint8   `json:"type"`
    73  	Timestamp      string  `json:"timestamp"`
    74  	Microtimestamp string  `json:"microtimestamp"`
    75  	BuyOrderId     uint64  `json:"buy_order_id"`
    76  	SellOrderId    uint64  `json:"sell_order_id"`
    77  }
    78  
    79  func NewBitstampScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BitstampScraper {
    80  	s := &BitstampScraper{
    81  		shutdown:     make(chan nothing),
    82  		shutdownDone: make(chan nothing),
    83  		error:        nil,
    84  		pairScrapers: make(map[string]*BitstampPairScraper),
    85  		exchangeName: exchange.Name,
    86  		chanTrades:   make(chan *dia.Trade),
    87  		db:           relDB,
    88  	}
    89  	var wsDialer ws.Dialer
    90  	wsConn, _, err := wsDialer.Dial("wss://ws.bitstamp.net", nil)
    91  	if err != nil {
    92  		log.Error("Websocket connect error", err)
    93  	}
    94  	s.wsClient = wsConn
    95  	if scrape {
    96  		go s.mainLoop()
    97  	}
    98  	return s
    99  }
   100  
   101  func extractUrlSymbolFromChannel(channel string) (after string) {
   102  	// Channel is live_trades_{pair}
   103  	// Remove the prefix live_trades_
   104  	if strings.HasPrefix(channel, "live_trades_") {
   105  		after = strings.Split(channel, "live_trades_")[1]
   106  	}
   107  	return after
   108  }
   109  
   110  func (s *BitstampScraper) foreignNameToUrlSymbol(foreignName string) string {
   111  	return strings.ToLower(strings.ReplaceAll(foreignName, "-", ""))
   112  }
   113  
   114  func (s *BitstampScraper) receive() {
   115  	tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   116  	tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   117  
   118  	var resp BitstampWsResponse
   119  	if err := s.wsClient.ReadJSON(&resp); err != nil {
   120  		log.Error("Receive message error:", err)
   121  		return
   122  	}
   123  	foreignName := extractUrlSymbolFromChannel(resp.Channel)
   124  
   125  	switch event := resp.Event; event {
   126  	case "bts:subscription_succeeded":
   127  		log.Info("Subsription succeeded:", foreignName)
   128  	// TODO: add subscription failed...
   129  	// Probably "bts:subscription_failed"
   130  	// Just a guess
   131  	case "bts:request_reconnect":
   132  		// TODO: handle reconnect
   133  		log.Warn("Server request for a reconnect")
   134  	case "bts:heartbeat":
   135  		var data BitstampPingData
   136  		if err := json.Unmarshal([]byte(resp.Data.(string)), &data); err != nil {
   137  			log.Warn("Unmarshal ping error:", err, resp.Data)
   138  		} else if data.Status == "success" {
   139  			log.Info("Heart is beating")
   140  		} else {
   141  			log.Warning("Check Heart", data)
   142  		}
   143  	default:
   144  
   145  		if strings.HasPrefix(event, "trade") {
   146  
   147  			data := resp.Data.(map[string]interface{})
   148  			ps, ok := s.pairScrapers[foreignName]
   149  
   150  			if ok {
   151  				timestamp, _ := strconv.ParseInt(data["microtimestamp"].(string), 10, 64)
   152  				volume := data["amount"].(float64)
   153  				side := data["type"].(float64)
   154  				if side == 1 {
   155  					volume *= -1
   156  				}
   157  
   158  				pair, err := s.db.GetExchangePairCache(s.exchangeName, foreignName)
   159  				if err != nil {
   160  					log.Error("get exchange pair from cache: ", err)
   161  				}
   162  
   163  				t := &dia.Trade{
   164  					Symbol:         ps.pair.Symbol,
   165  					Pair:           foreignName,
   166  					Price:          data["price"].(float64),
   167  					Volume:         volume,
   168  					Time:           time.Unix(0, 1000*timestamp),
   169  					Source:         s.exchangeName,
   170  					ForeignTradeID: fmt.Sprintf("%d", int(data["id"].(float64))),
   171  					VerifiedPair:   pair.Verified,
   172  					QuoteToken:     pair.UnderlyingPair.QuoteToken,
   173  					BaseToken:      pair.UnderlyingPair.BaseToken,
   174  				}
   175  
   176  				// Handle duplicate trades.
   177  				discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory)
   178  				if !discardTrade {
   179  					t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   180  					s.chanTrades <- t
   181  				}
   182  				log.Info("Found trade:", t)
   183  			}
   184  		} else {
   185  			log.Warnf("Unidentified response event %s: -- %v", event, resp)
   186  		}
   187  	}
   188  }
   189  
   190  func (s *BitstampScraper) mainLoop() {
   191  	for {
   192  		select {
   193  		case <-s.shutdown:
   194  			log.Warn("Shutting down BitstampScraper")
   195  			s.cleanup(nil)
   196  			return
   197  		default:
   198  		}
   199  		s.receive()
   200  	}
   201  }
   202  
   203  func (s *BitstampScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   204  	s.errorLock.RLock()
   205  	defer s.errorLock.RUnlock()
   206  	if s.error != nil {
   207  		return nil, s.error
   208  	}
   209  	if s.closed {
   210  		return nil, errors.New("BitstampScraper: Call ScrapePair on closed scraper")
   211  	}
   212  	ps := &BitstampPairScraper{
   213  		parent: s,
   214  		pair:   pair,
   215  	}
   216  	s.pairScrapers[pair.ForeignName] = ps
   217  
   218  	// Subscribe to pair
   219  	urlSymbol := s.foreignNameToUrlSymbol(pair.ForeignName)
   220  	message := map[string]interface{}{
   221  		"event": "bts:subscribe",
   222  		"data": map[string]interface{}{
   223  			"channel": "live_trades_" + urlSymbol,
   224  		},
   225  	}
   226  	err := s.wsClient.WriteJSON(message)
   227  	if err != nil {
   228  		log.Error("Error sending subscription for", ps.pair.ForeignName, err)
   229  	}
   230  	log.Info("Sent subscription for:", ps.pair.ForeignName)
   231  
   232  	return ps, nil
   233  }
   234  
   235  func (s *BitstampScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   236  	var bitstampPairsInfo BitstampPairsInfo
   237  	resp, err := http.Get("https://www.bitstamp.net/api/v2/trading-pairs-info/")
   238  	if err != nil {
   239  		log.Error("Get Pairs:", err)
   240  	}
   241  
   242  	defer resp.Body.Close()
   243  	body, err := io.ReadAll(resp.Body)
   244  	if err != nil {
   245  		log.Error("Read pair body:", err)
   246  	}
   247  
   248  	err = json.Unmarshal(body, &bitstampPairsInfo)
   249  	if err != nil {
   250  		log.Error("Unmarshal pairs:", err)
   251  	}
   252  
   253  	for _, p := range bitstampPairsInfo {
   254  		pairToNormalized := dia.ExchangePair{
   255  			Symbol:      strings.Split(p.Name, "/")[0],
   256  			ForeignName: p.UrlSymbol,
   257  			Exchange:    s.exchangeName,
   258  			UnderlyingPair: dia.Pair{
   259  				QuoteToken: dia.Asset{
   260  					Symbol: strings.Split(p.Name, "/")[0],
   261  				},
   262  				BaseToken: dia.Asset{
   263  					Symbol: strings.Split(p.Name, "/")[1],
   264  				},
   265  			},
   266  		}
   267  		pairs = append(pairs, pairToNormalized)
   268  	}
   269  	return
   270  }
   271  
   272  func (s *BitstampScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   273  	return dia.Asset{Symbol: symbol}, nil
   274  }
   275  
   276  func (s *BitstampScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   277  	return dia.ExchangePair{}, nil
   278  }
   279  
   280  func (s *BitstampScraper) Channel() chan *dia.Trade {
   281  	return s.chanTrades
   282  }
   283  
   284  func (s *BitstampScraper) cleanup(err error) {
   285  	s.errorLock.Lock()
   286  	defer s.errorLock.Unlock()
   287  	if err != nil {
   288  		s.error = err
   289  	}
   290  	s.closed = true
   291  	close(s.shutdownDone)
   292  }
   293  
   294  func (s *BitstampScraper) Close() error {
   295  	if s.closed {
   296  		return errors.New("BitstampScraper: Already closed")
   297  	}
   298  	if err := s.wsClient.Close(); err != nil {
   299  		log.Error("Error closing Bitstamp.wsClient", err)
   300  	}
   301  	close(s.shutdown)
   302  	<-s.shutdownDone
   303  	defer s.errorLock.RUnlock()
   304  	return s.error
   305  }
   306  
   307  func (ps *BitstampPairScraper) Close() error {
   308  	ps.closed = true
   309  	return nil
   310  }
   311  
   312  func (ps *BitstampPairScraper) Error() error {
   313  	s := ps.parent
   314  	s.errorLock.RLock()
   315  	defer s.errorLock.RUnlock()
   316  	return s.error
   317  }
   318  
   319  func (ps *BitstampPairScraper) Pair() dia.ExchangePair {
   320  	return ps.pair
   321  }