github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/FinageForexScraper.go (about) 1 package scrapers 2 3 import ( 4 "encoding/json" 5 "errors" 6 "strconv" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/diadata-org/diadata/pkg/dia" 12 models "github.com/diadata-org/diadata/pkg/model" 13 "github.com/diadata-org/diadata/pkg/utils" 14 "github.com/gorilla/websocket" 15 ) 16 17 type FinageWSMessage struct { 18 Action string `json:"action"` 19 Symbols string `json:"symbols"` 20 } 21 22 type FinageTrade struct { 23 Symbol string `json:"s"` 24 PriceAsk float64 `json:"a"` 25 PriceBid float64 `json:"b"` 26 VolumeAsk string `json:"dc"` 27 VolumeBid string `json:"dd"` 28 Timestamp int64 `json:"t"` 29 } 30 31 // var pairs = "INR/USD,USD/INR,AED/INR, INR/AED,INR/CAD,CAD/INR" 32 33 type FinageForexScraper struct { 34 // signaling channels 35 shutdown chan nothing 36 shutdownDone chan nothing 37 // error handling; to read error or closed, first acquire read lock 38 // only cleanup method should hold write lock 39 errorLock sync.RWMutex 40 error error 41 closed bool 42 pairScrapers map[string]*FinageForexPairScraper // dia.ExchangePair -> pairScraperSet 43 ticker *time.Ticker 44 datastore *models.RelDB 45 chanTrades chan *dia.Trade 46 wsConn *websocket.Conn 47 exchangeName string 48 apiKey string 49 } 50 51 // SpawnECBScraper returns a new ECBScraper initialized with default values. 52 // The instance is asynchronously scraping as soon as it is created. 53 func NewFinageForexScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB, finageAPIkey string, finageWebsocketKey string) *FinageForexScraper { 54 var finage = "wss://w29hxx2ndd.finage.ws:8001/?token=" + finageWebsocketKey 55 56 c, _, err := websocket.DefaultDialer.Dial(finage, nil) 57 if err != nil { 58 log.Fatal("dial:", err) 59 } 60 61 scraper := &FinageForexScraper{ 62 wsConn: c, 63 shutdown: make(chan nothing), 64 exchangeName: exchange.Name, 65 shutdownDone: make(chan nothing), 66 pairScrapers: make(map[string]*FinageForexPairScraper), 67 error: nil, 68 ticker: time.NewTicker(refreshDelay), 69 chanTrades: make(chan *dia.Trade), 70 datastore: relDB, 71 apiKey: finageAPIkey, 72 } 73 74 log.Info("Scraper is built and initiated") 75 if scrape { 76 go scraper.mainLoop() 77 } 78 return scraper 79 } 80 81 func (scraper *FinageForexScraper) subscribe() error { 82 83 pairTosubscribe := "" 84 85 pairs, err := scraper.datastore.GetExchangePairSymbols(scraper.exchangeName) 86 if err != nil { 87 log.Errorln("Error getting pairs", err) 88 return err 89 } 90 91 log.Println("Pairs", pairs) 92 93 for _, ps := range pairs { 94 pairTosubscribe = pairTosubscribe + "," + ps.ForeignName 95 } 96 log.Infoln("pairTosubscribe", pairTosubscribe) 97 return scraper.wsConn.WriteJSON(FinageWSMessage{Action: "subscribe", Symbols: pairTosubscribe}) 98 99 } 100 101 // mainLoop runs in a goroutine until channel s is closed. 102 func (scraper *FinageForexScraper) mainLoop() { 103 subscribeErr := scraper.subscribe() 104 if subscribeErr != nil { 105 log.Error("got error subscribing to scraper ", subscribeErr) 106 } 107 log.Infoln("Sunbscribed to all asset pairs") 108 err := scraper.Update() 109 if err != nil { 110 log.Error(err) 111 } 112 for { 113 select { 114 case <-scraper.shutdown: // user requested shutdown 115 log.Println("FinageScraper shutting down") 116 scraper.cleanup(nil) 117 return 118 } 119 } 120 } 121 122 // Update performs a HTTP Get request for the rss feed and decodes the results. 123 func (scraper *FinageForexScraper) Update() error { 124 125 go func() { 126 for { 127 _, message, err := scraper.wsConn.ReadMessage() 128 if err != nil { 129 //s.subscribe() 130 log.Println("err", err) 131 } 132 133 var ftrade FinageTrade 134 err = json.Unmarshal(message, &ftrade) 135 log.Info("Symbol: ", ftrade.Symbol) 136 log.Info("PriceAsk: ", ftrade.PriceAsk) 137 log.Info("PriceBid: ", ftrade.PriceBid) 138 log.Info("VolumeAsk: ", ftrade.VolumeAsk) 139 log.Info("VolumeBid: ", ftrade.VolumeBid) 140 log.Info("Timestamp: ", ftrade.Timestamp) 141 142 if err != nil { 143 log.Errorln("Not a Trade", err) 144 break 145 } else { 146 tradePair, _ := scraper.datastore.GetExchangePairCache(scraper.exchangeName, strings.Replace(ftrade.Symbol, "/", "-", 1)) 147 if ftrade.Symbol != "" { 148 t := &dia.Trade{ 149 Symbol: strings.Split(ftrade.Symbol, "/")[0], 150 Pair: strings.Replace(ftrade.Symbol, "/", "-", 1), 151 Price: ftrade.PriceAsk, 152 Volume: 1, 153 BaseToken: tradePair.UnderlyingPair.BaseToken, 154 QuoteToken: tradePair.UnderlyingPair.QuoteToken, 155 VerifiedPair: tradePair.Verified, 156 Time: time.Unix(ftrade.Timestamp/1e3, 0), 157 Source: scraper.exchangeName, 158 } 159 if t.VerifiedPair { 160 log.Info("got verified trade: ", t) 161 } 162 scraper.chanTrades <- t 163 } 164 165 } 166 } 167 }() 168 169 return nil 170 } 171 172 // closes all connected PairScrapers 173 // must only be called from mainLoop 174 func (scraper *FinageForexScraper) cleanup(err error) { 175 176 scraper.errorLock.Lock() 177 defer scraper.errorLock.Unlock() 178 179 scraper.ticker.Stop() 180 181 if err != nil { 182 scraper.error = err 183 } 184 scraper.closed = true 185 186 close(scraper.shutdownDone) // signal that shutdown is complete 187 } 188 189 // Close closes any existing API connections, as well as channels of 190 // PairScrapers from calls to ScrapePair 191 func (scraper *FinageForexScraper) Close() error { 192 if scraper.closed { 193 return errors.New("FinageForexScraper: Already closed") 194 } 195 close(scraper.shutdown) 196 <-scraper.shutdownDone 197 scraper.errorLock.RLock() 198 defer scraper.errorLock.RUnlock() 199 return scraper.error 200 } 201 202 // ECBPairScraper implements PairScraper for ECB 203 type FinageForexPairScraper struct { 204 parent *FinageForexScraper 205 pair dia.ExchangePair 206 closed bool 207 } 208 209 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from 210 // this APIScraper 211 func (scraper *FinageForexScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 212 213 scraper.errorLock.RLock() 214 defer scraper.errorLock.RUnlock() 215 if scraper.error != nil { 216 return nil, scraper.error 217 } 218 if scraper.closed { 219 return nil, errors.New("ECBScraper: Call ScrapePair on closed scraper") 220 } 221 ps := &FinageForexPairScraper{ 222 parent: scraper, 223 pair: pair, 224 } 225 226 scraper.pairScrapers[pair.Symbol] = ps 227 228 return ps, nil 229 } 230 231 // Channel returns a channel that can be used to receive trades/pricing information 232 func (scraper *FinageForexScraper) Channel() chan *dia.Trade { 233 return scraper.chanTrades 234 } 235 236 func (pairScraper *FinageForexPairScraper) Close() error { 237 pairScraper.closed = true 238 return nil 239 } 240 241 // Error returns an error when the channel Channel() is closed 242 // and nil otherwise 243 func (pairScraper *FinageForexPairScraper) Error() error { 244 scraper := pairScraper.parent 245 scraper.errorLock.RLock() 246 defer scraper.errorLock.RUnlock() 247 return scraper.error 248 } 249 250 // Pair returns the pair this scraper is subscribed to 251 func (pairScraper *FinageForexPairScraper) Pair() dia.ExchangePair { 252 return pairScraper.pair 253 } 254 255 type FinageSymbolResponse struct { 256 Page int `json:"page"` 257 TotalPage int `json:"totalPage"` 258 Symbols []struct { 259 Symbol string `json:"symbol"` 260 Name string `json:"name"` 261 } `json:"symbols"` 262 } 263 264 func (scraper *FinageForexScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 265 log.Infoln("Fetching Available Pairs") 266 var finageurl = "https://api.finage.co.uk/symbol-list/forex?apikey=" + scraper.apiKey + "&page=" 267 var finageSymbolResponse FinageSymbolResponse 268 data, _, err := utils.GetRequest(finageurl + strconv.Itoa(1)) 269 if err != nil { 270 return 271 } 272 err = json.Unmarshal(data, &finageSymbolResponse) 273 if err != nil { 274 return 275 } 276 277 for i := 1; i <= finageSymbolResponse.TotalPage; i++ { 278 279 data, _, err := utils.GetRequest(finageurl + strconv.Itoa(i)) 280 if err != nil { 281 continue 282 } 283 284 err = json.Unmarshal(data, &finageSymbolResponse) 285 if err != nil { 286 log.Error("unmarshal pair: ", err) 287 continue 288 } 289 290 for _, symbol := range finageSymbolResponse.Symbols { 291 292 pairToNormalize := dia.ExchangePair{ 293 Symbol: "", 294 ForeignName: symbol.Symbol, 295 Exchange: scraper.exchangeName, 296 } 297 pair, serr := scraper.NormalizePair(pairToNormalize) 298 if serr == nil { 299 pairs = append(pairs, pair) 300 } else { 301 log.Error(serr) 302 } 303 304 } 305 306 } 307 308 return 309 } 310 311 func (scraper *FinageForexScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) { 312 asset.Symbol = symbol 313 asset.Blockchain = dia.FIAT 314 return asset, nil 315 } 316 317 func (scraper *FinageForexScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 318 runes := []rune(pair.ForeignName) 319 asset0 := runes[0:3] 320 asset1 := runes[3:6] 321 pair.ForeignName = string(asset0) + "/" + string(asset1) 322 pair.Symbol = string(asset0) 323 pair.Verified = true 324 return pair, nil 325 }