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 }