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

     1  package scrapers
     2  
     3  import (
     4  	"errors"
     5  	"math"
     6  	"strconv"
     7  	"sync"
     8  	"time"
     9  
    10  	krakenapi "github.com/beldur/kraken-go-api-client"
    11  	"github.com/diadata-org/diadata/pkg/dia"
    12  	models "github.com/diadata-org/diadata/pkg/model"
    13  	"github.com/zekroTJA/timedmap"
    14  )
    15  
    16  const (
    17  	krakenRefreshDelay = time.Second * 30 * 1
    18  )
    19  
    20  type KrakenScraper struct {
    21  	// signaling channels
    22  	shutdown     chan nothing
    23  	shutdownDone chan nothing
    24  	// error handling; to read error or closed, first acquire read lock
    25  	// only cleanup method should hold write lock
    26  	errorLock    sync.RWMutex
    27  	error        error
    28  	closed       bool
    29  	pairScrapers map[string]*KrakenPairScraper // pc.ExchangePair -> pairScraperSet
    30  	api          *krakenapi.KrakenApi
    31  	ticker       *time.Ticker
    32  	exchangeName string
    33  	chanTrades   chan *dia.Trade
    34  	db           *models.RelDB
    35  }
    36  
    37  // NewKrakenScraper returns a new KrakenScraper initialized with default values.
    38  // The instance is asynchronously scraping as soon as it is created.
    39  func NewKrakenScraper(key string, secret string, exchange dia.Exchange, scrape bool, relDB *models.RelDB) *KrakenScraper {
    40  	s := &KrakenScraper{
    41  		shutdown:     make(chan nothing),
    42  		shutdownDone: make(chan nothing),
    43  		pairScrapers: make(map[string]*KrakenPairScraper),
    44  		api:          krakenapi.New(key, secret),
    45  		ticker:       time.NewTicker(krakenRefreshDelay),
    46  		exchangeName: exchange.Name,
    47  		error:        nil,
    48  		chanTrades:   make(chan *dia.Trade),
    49  		db:           relDB,
    50  	}
    51  	if scrape {
    52  		go s.mainLoop()
    53  	}
    54  	return s
    55  }
    56  
    57  func Round(x, unit float64) float64 {
    58  	return math.Round(x/unit) * unit
    59  }
    60  
    61  // func neededBalanceAdjustement(current float64, minChange float64, desired float64) (float64, string) {
    62  // 	obj := desired - current
    63  // 	roundedObj := Round(obj, minChange)
    64  // 	message := fmt.Sprintf("current position: %v, min change: %v, desired position: %v, delta current/desired: %v, rounded delta: %v", current, minChange, desired, obj, roundedObj)
    65  // 	return roundedObj, message
    66  // }
    67  
    68  func FloatToString(input_num float64) string {
    69  	// to convert a float number to a string
    70  	return strconv.FormatFloat(input_num, 'f', -1, 64)
    71  }
    72  
    73  // mainLoop runs in a goroutine until channel s is closed.
    74  func (s *KrakenScraper) mainLoop() {
    75  	for {
    76  		select {
    77  		case <-s.ticker.C:
    78  			s.Update()
    79  		case <-s.shutdown: // user requested shutdown
    80  			log.Printf("KrakenScraper shutting down")
    81  			s.cleanup(nil)
    82  			return
    83  		}
    84  	}
    85  }
    86  
    87  // closes all connected PairScrapers
    88  // must only be called from mainLoop
    89  func (s *KrakenScraper) cleanup(err error) {
    90  
    91  	s.errorLock.Lock()
    92  	defer s.errorLock.Unlock()
    93  
    94  	if err != nil {
    95  		s.error = err
    96  	}
    97  	s.closed = true
    98  
    99  	close(s.shutdownDone) // signal that shutdown is complete
   100  }
   101  
   102  // Close closes any existing API connections, as well as channels of
   103  // PairScrapers from calls to ScrapePair
   104  func (s *KrakenScraper) Close() error {
   105  	if s.closed {
   106  		return errors.New("KrakenScraper: Already closed")
   107  	}
   108  	close(s.shutdown)
   109  	<-s.shutdownDone
   110  	s.errorLock.RLock()
   111  	defer s.errorLock.RUnlock()
   112  	return s.error
   113  }
   114  
   115  // KrakenPairScraper implements PairScraper for Kraken
   116  type KrakenPairScraper struct {
   117  	parent     *KrakenScraper
   118  	pair       dia.ExchangePair
   119  	closed     bool
   120  	lastRecord int64
   121  }
   122  
   123  // ScrapePair returns a PairScraper that can be used to get trades for a single pair from
   124  // this APIScraper
   125  func (s *KrakenScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   126  
   127  	s.errorLock.RLock()
   128  	defer s.errorLock.RUnlock()
   129  	if s.error != nil {
   130  		return nil, s.error
   131  	}
   132  	if s.closed {
   133  		return nil, errors.New("KrakenScraper: Call ScrapePair on closed scraper")
   134  	}
   135  	ps := &KrakenPairScraper{
   136  		parent:     s,
   137  		pair:       pair,
   138  		lastRecord: 0, //TODO FIX to figure out the last we got...
   139  	}
   140  
   141  	s.pairScrapers[pair.Symbol] = ps
   142  
   143  	return ps, nil
   144  }
   145  
   146  func (s *KrakenScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   147  	return dia.Asset{Symbol: symbol}, nil
   148  }
   149  
   150  // FetchAvailablePairs returns a list with all available trade pairs
   151  func (s *KrakenScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   152  	return []dia.ExchangePair{}, errors.New("FetchAvailablePairs() not implemented")
   153  }
   154  
   155  // NormalizePair accounts for the par
   156  func (ps *KrakenScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   157  	if len(pair.ForeignName) == 7 {
   158  		if pair.ForeignName[4:5] == "Z" || pair.ForeignName[4:5] == "X" {
   159  			pair.ForeignName = pair.ForeignName[:4] + pair.ForeignName[5:]
   160  			return pair, nil
   161  		}
   162  		if pair.ForeignName[:1] == "Z" || pair.ForeignName[:1] == "X" {
   163  			pair.ForeignName = pair.ForeignName[1:]
   164  		}
   165  	}
   166  	if len(pair.ForeignName) == 8 {
   167  		if pair.ForeignName[4:5] == "Z" || pair.ForeignName[4:5] == "X" {
   168  			pair.ForeignName = pair.ForeignName[:4] + pair.ForeignName[5:]
   169  		}
   170  		if pair.ForeignName[:1] == "Z" || pair.ForeignName[:1] == "X" {
   171  			pair.ForeignName = pair.ForeignName[1:]
   172  		}
   173  	}
   174  	if pair.ForeignName[len(pair.ForeignName)-3:] == "XBT" {
   175  		pair.ForeignName = pair.ForeignName[:len(pair.ForeignName)-3] + "BTC"
   176  	}
   177  	if pair.ForeignName[:3] == "XBT" {
   178  		pair.ForeignName = "BTC" + pair.ForeignName[len(pair.ForeignName)-3:]
   179  	}
   180  	return pair, nil
   181  }
   182  
   183  // Channel returns a channel that can be used to receive trades/pricing information
   184  func (ps *KrakenScraper) Channel() chan *dia.Trade {
   185  	return ps.chanTrades
   186  }
   187  
   188  func (ps *KrakenPairScraper) Close() error {
   189  	ps.closed = true
   190  	return nil
   191  }
   192  
   193  // Error returns an error when the channel Channel() is closed
   194  // and nil otherwise
   195  func (ps *KrakenPairScraper) Error() error {
   196  	s := ps.parent
   197  	s.errorLock.RLock()
   198  	defer s.errorLock.RUnlock()
   199  	return s.error
   200  }
   201  
   202  // Pair returns the pair this scraper is subscribed to
   203  func (ps *KrakenPairScraper) Pair() dia.ExchangePair {
   204  	return ps.pair
   205  }
   206  
   207  func NewTrade(pair dia.ExchangePair, info krakenapi.TradeInfo, foreignTradeID string, relDB *models.RelDB) *dia.Trade {
   208  	volume := info.VolumeFloat
   209  	if info.Sell {
   210  		volume = -volume
   211  	}
   212  	exchangepair, err := relDB.GetExchangePairCache(dia.KrakenExchange, pair.ForeignName)
   213  	if err != nil {
   214  		log.Error("get exchangepair from cache: ", err)
   215  	}
   216  	t := &dia.Trade{
   217  		Pair:           pair.ForeignName,
   218  		Price:          info.PriceFloat,
   219  		Symbol:         pair.Symbol,
   220  		Volume:         volume,
   221  		Time:           time.Unix(info.Time, 0),
   222  		ForeignTradeID: foreignTradeID,
   223  		Source:         dia.KrakenExchange,
   224  		VerifiedPair:   exchangepair.Verified,
   225  		BaseToken:      exchangepair.UnderlyingPair.BaseToken,
   226  		QuoteToken:     exchangepair.UnderlyingPair.QuoteToken,
   227  	}
   228  	if exchangepair.Verified {
   229  		log.Infoln("Got verified trade", t)
   230  	}
   231  
   232  	return t
   233  }
   234  
   235  func (s *KrakenScraper) Update() {
   236  	tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   237  
   238  	for _, ps := range s.pairScrapers {
   239  
   240  		r, err := s.api.Trades(ps.pair.ForeignName, ps.lastRecord)
   241  
   242  		if err != nil {
   243  			log.Printf("err on collect trades %v %v", err, ps.pair.ForeignName)
   244  			time.Sleep(1 * time.Minute)
   245  		} else {
   246  			if r != nil {
   247  				ps.lastRecord = r.Last
   248  				for _, ti := range r.Trades {
   249  					// p, _ := s.NormalizePair(ps.pair)
   250  					t := NewTrade(ps.pair, ti, strconv.FormatInt(r.Last, 16), s.db)
   251  					// Handle duplicate trades.
   252  					t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   253  					ps.parent.chanTrades <- t
   254  				}
   255  			} else {
   256  				log.Printf("r nil")
   257  			}
   258  		}
   259  	}
   260  }