github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BinanceScraperUS.go (about) 1 package scrapers 2 3 import ( 4 "encoding/json" 5 "errors" 6 "strconv" 7 "sync" 8 "time" 9 10 "github.com/cryptwire/go-binance/v2" 11 "github.com/diadata-org/diadata/pkg/dia" 12 models "github.com/diadata-org/diadata/pkg/model" 13 utils "github.com/diadata-org/diadata/pkg/utils" 14 "github.com/zekroTJA/timedmap" 15 ) 16 17 const ( 18 BinanceUSWsURL = "wss://stream.binance.us:9443/ws" 19 ) 20 21 type BinanceUSPairScraperSet map[*BinanceUSPairScraper]nothing 22 23 // BinanceScraperUS is a Scraper for collecting trades from the Binance websocket API 24 type BinanceScraperUS struct { 25 client *binance.Client 26 // signaling channels for session initialization and finishing 27 initDone chan nothing 28 shutdown chan nothing 29 shutdownDone chan nothing 30 // error handling; to read error or closed, first acquire read lock 31 // only cleanup method should hold write lock 32 errorLock sync.RWMutex 33 error error 34 closed bool 35 // used to keep track of trading pairs that we subscribed to 36 // use sync.Maps to concurrently handle multiple pairs 37 pairScrapers sync.Map // dia.ExchangePair -> BinanceUSPairScraperSet 38 // pairSubscriptions sync.Map // dia.ExchangePair -> string (subscription ID) 39 // pairLocks sync.Map // dia.ExchangePair -> sync.Mutex 40 exchangeName string 41 chanTrades chan *dia.Trade 42 db *models.RelDB 43 } 44 45 // NewBinanceScraperUS returns a new BinanceScraperUS for the given pair 46 func NewBinanceScraperUS(apiKey string, secretKey string, exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BinanceScraperUS { 47 binance.BaseWsMainURL = BinanceUSWsURL 48 49 s := &BinanceScraperUS{ 50 client: binance.NewClient(apiKey, secretKey), 51 initDone: make(chan nothing), 52 shutdown: make(chan nothing), 53 shutdownDone: make(chan nothing), 54 exchangeName: exchange.Name, 55 error: nil, 56 chanTrades: make(chan *dia.Trade), 57 db: relDB, 58 } 59 60 // establish connection in the background 61 if scrape { 62 go s.mainLoop() 63 } 64 return s 65 } 66 67 func (up *BinanceScraperUS) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 68 if pair.Symbol == "MIOTA" { 69 pair.ForeignName = "M" + pair.ForeignName 70 } 71 if pair.Symbol == "YOYOW" { 72 pair.ForeignName = "YOYOW" + pair.ForeignName[4:] 73 } 74 if pair.Symbol == "ETHOS" { 75 pair.ForeignName = "ETHOS" + pair.ForeignName[3:] 76 } 77 if pair.Symbol == "WNXM" { 78 pair.Symbol = "wNXM" 79 pair.ForeignName = "wNXM" + pair.ForeignName[4:] 80 } 81 return pair, nil 82 } 83 84 // runs in a goroutine until s is closed 85 func (s *BinanceScraperUS) mainLoop() { 86 close(s.initDone) 87 for range s.shutdown { // user requested shutdown 88 log.Println("BinanceScraperUS shutting down") 89 s.cleanup() 90 return 91 } 92 select {} 93 } 94 95 func (s *BinanceScraperUS) FillSymbolData(symbol string) (dia.Asset, error) { 96 // TO DO 97 return dia.Asset{Symbol: symbol}, nil 98 } 99 100 // closes all connected PairScrapers 101 // must only be called from mainLoop 102 func (s *BinanceScraperUS) cleanup() { 103 s.errorLock.Lock() 104 defer s.errorLock.Unlock() 105 // close all channels of PairScraper children 106 s.pairScrapers.Range(func(k, v interface{}) bool { 107 for ps := range v.(BinanceUSPairScraperSet) { 108 ps.closed = true 109 } 110 s.pairScrapers.Delete(k) 111 return true 112 }) 113 114 s.closed = true 115 close(s.shutdownDone) // signal that shutdown is complete 116 } 117 118 // Close closes any existing API connections, as well as channels of 119 // PairScrapers from calls to ScrapePair 120 func (s *BinanceScraperUS) Close() error { 121 if s.closed { 122 return errors.New("BinanceScraperUS: Already closed") 123 } 124 close(s.shutdown) 125 <-s.shutdownDone 126 s.errorLock.RLock() 127 defer s.errorLock.RUnlock() 128 return s.error 129 } 130 131 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from 132 // this APIScraper 133 func (s *BinanceScraperUS) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 134 <-s.initDone // wait until client is connected 135 136 tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 137 tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 138 139 if s.closed { 140 return nil, errors.New("BinanceScraperUS: Call ScrapePair on closed scraper") 141 } 142 143 ps := &BinanceUSPairScraper{ 144 parent: s, 145 pair: pair, 146 } 147 148 wsAggTradeHandler := func(event *binance.WsAggTradeEvent) { 149 var exchangepair dia.ExchangePair 150 151 volume, err := strconv.ParseFloat(event.Quantity, 64) 152 price, err2 := strconv.ParseFloat(event.Price, 64) 153 154 if err == nil && err2 == nil && event.Event == "aggTrade" { 155 if !event.IsBuyerMaker { 156 volume = -volume 157 } 158 pairNormalized, _ := s.NormalizePair(pair) 159 exchangepair, err = s.db.GetExchangePairCache(s.exchangeName, pair.ForeignName) 160 if err != nil { 161 log.Error(err) 162 } 163 t := &dia.Trade{ 164 Symbol: pairNormalized.Symbol, 165 Pair: pairNormalized.ForeignName, 166 Price: price, 167 Volume: volume, 168 Time: time.Unix(event.TradeTime/1000, (event.TradeTime%1000)*int64(time.Millisecond)), 169 ForeignTradeID: strconv.FormatInt(event.AggTradeID, 16), 170 Source: s.exchangeName, 171 VerifiedPair: exchangepair.Verified, 172 BaseToken: exchangepair.UnderlyingPair.BaseToken, 173 QuoteToken: exchangepair.UnderlyingPair.QuoteToken, 174 } 175 if exchangepair.Verified { 176 log.Infoln("Got verified trade", t) 177 } 178 179 // Handle duplicate trades. 180 discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory) 181 if !discardTrade { 182 t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory) 183 ps.parent.chanTrades <- t 184 } 185 186 } else { 187 log.Println("ignoring event ", event, err, err2) 188 } 189 } 190 errHandler := func(err error) { 191 log.Error(err) 192 } 193 194 _, _, err := binance.WsAggTradeServe(pair.ForeignName, wsAggTradeHandler, errHandler) 195 if err != nil { 196 log.Errorf("serving pair %s", pair.ForeignName) 197 } 198 199 return ps, err 200 } 201 func (s *BinanceScraperUS) normalizeSymbol(p dia.ExchangePair, foreignName string, params ...string) (pair dia.ExchangePair, err error) { 202 pair = p 203 return pair, nil 204 } 205 206 // FetchAvailablePairs returns a list with all available trade pairs 207 func (s *BinanceScraperUS) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 208 209 data, _, err := utils.GetRequest("https://api.binance.us/api/v1/exchangeInfo") 210 211 if err != nil { 212 return 213 } 214 var ar binance.ExchangeInfo 215 err = json.Unmarshal(data, &ar) 216 if err == nil { 217 for _, p := range ar.Symbols { 218 pairToNormalise := dia.ExchangePair{ 219 Symbol: p.BaseAsset, 220 ForeignName: p.Symbol, 221 Exchange: s.exchangeName, 222 } 223 pair, serr := s.normalizeSymbol(pairToNormalise, p.BaseAsset, p.Status) 224 if serr == nil { 225 pairs = append(pairs, pair) 226 } else { 227 log.Error(serr) 228 } 229 } 230 } 231 return 232 } 233 234 // BinanceUSPairScraper implements PairScraper for Binance 235 type BinanceUSPairScraper struct { 236 parent *BinanceScraperUS 237 pair dia.ExchangePair 238 closed bool 239 } 240 241 // Close stops listening for trades of the pair associated with s 242 func (ps *BinanceUSPairScraper) Close() error { 243 var err error 244 s := ps.parent 245 // if parent already errored, return early 246 s.errorLock.RLock() 247 defer s.errorLock.RUnlock() 248 if s.error != nil { 249 return s.error 250 } 251 if ps.closed { 252 return errors.New("BinanceUSPairScraper: Already closed") 253 } 254 255 // TODO stop collection for the pair 256 257 ps.closed = true 258 return err 259 } 260 261 // Channel returns a channel that can be used to receive trades 262 func (ps *BinanceScraperUS) Channel() chan *dia.Trade { 263 return ps.chanTrades 264 } 265 266 // Error returns an error when the channel Channel() is closed 267 // and nil otherwise 268 func (ps *BinanceUSPairScraper) Error() error { 269 s := ps.parent 270 s.errorLock.RLock() 271 defer s.errorLock.RUnlock() 272 return s.error 273 } 274 275 // Pair returns the pair this scraper is subscribed to 276 func (ps *BinanceUSPairScraper) Pair() dia.ExchangePair { 277 return ps.pair 278 }