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  }