github.com/diadata-org/diadata@v1.4.593/internal/pkg/stock-scrapers/finage.go (about)

     1  package stockscrapers
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"os"
     9  	"sync"
    10  
    11  	"time"
    12  
    13  	models "github.com/diadata-org/diadata/pkg/model"
    14  	"github.com/gorilla/websocket"
    15  )
    16  
    17  const (
    18  	msciWorldIndexTop10 = "AAPL,MSFT,AMZN,FB,GOOGL,GOOG,TSLA,NVDA,JPM,JNJ"
    19  	subscribeMessage    = "{\"action\": \"subscribe\", \"symbols\":\"" + msciWorldIndexTop10 + "\"}"
    20  	apiKey              = "api_finage"
    21  )
    22  
    23  type FinageScraper struct {
    24  	stockScraper               StockScraper
    25  	apiWsURL                   string
    26  	timeResolutionMilliseconds int
    27  }
    28  
    29  func NewFinageScraper(db *models.DB) *FinageScraper {
    30  
    31  	stockScraper := StockScraper{
    32  		shutdown:     make(chan nothing),
    33  		shutdownDone: make(chan nothing),
    34  		errorLock:    new(sync.RWMutex),
    35  		error:        nil,
    36  		datastore:    db,
    37  		chanStock:    make(chan models.StockQuotation),
    38  		source:       "Finage",
    39  	}
    40  	s := &FinageScraper{
    41  		stockScraper:               stockScraper,
    42  		apiWsURL:                   getAPIKeyFromSecrets(),
    43  		timeResolutionMilliseconds: 1000,
    44  	}
    45  	fmt.Println("scraper built. Start main loop.")
    46  	go s.mainLoop()
    47  	return s
    48  }
    49  
    50  // mainLoop runs in a goroutine until channel s is closed.
    51  func (scraper *FinageScraper) mainLoop() {
    52  
    53  	// Either call FetchQuotes or implement quote fetching here and
    54  	// leave FetchQuotes blank.
    55  
    56  	defer close(scraper.GetStockQuotationChannel())
    57  
    58  	c, _, err := websocket.DefaultDialer.Dial(scraper.apiWsURL, nil)
    59  	if err != nil {
    60  		log.Error("connecting to web socket: ", err)
    61  		scraper.cleanup(err)
    62  		return
    63  	}
    64  	defer func(c *websocket.Conn) {
    65  		errClose := c.Close()
    66  		if errClose != nil {
    67  			log.Error("Error closing websocket connection ", errClose)
    68  		}
    69  	}(c)
    70  
    71  	if subscribeErr := c.WriteMessage(websocket.TextMessage, []byte(subscribeMessage)); subscribeErr != nil {
    72  		log.Error("creating subscription for the stock quotations: ", subscribeErr)
    73  		scraper.cleanup(subscribeErr)
    74  		return
    75  	}
    76  
    77  	for {
    78  		select {
    79  		case <-scraper.stockScraper.shutdown:
    80  			scraper.cleanup(nil)
    81  			return
    82  		default:
    83  			_, message, readErr := c.ReadMessage()
    84  			if readErr != nil {
    85  				log.Error("reading the response: ", readErr)
    86  				scraper.cleanup(err)
    87  				return
    88  			}
    89  			if quotation, unmarshalErr := scraper.unmarshalQuotationJSON(message); unmarshalErr != nil {
    90  				log.Error("parsing quotation from response: ", unmarshalErr)
    91  			} else {
    92  				scraper.GetStockQuotationChannel() <- quotation
    93  			}
    94  		}
    95  	}
    96  }
    97  
    98  // FetchQuotes fetches quotes from an API and feeds them into the channel.
    99  func (scraper *FinageScraper) FetchQuotes() error {
   100  	// ...
   101  	// scraper.GetStockChannel() <- quote
   102  	return nil
   103  }
   104  
   105  // GetDataChannel returns the scrapers data channel.
   106  func (scraper *FinageScraper) GetStockQuotationChannel() chan models.StockQuotation {
   107  	return scraper.stockScraper.chanStock
   108  }
   109  
   110  // closes all connected Scrapers. Must only be called from mainLoop
   111  func (scraper *FinageScraper) cleanup(err error) {
   112  	scraper.stockScraper.errorLock.Lock()
   113  	defer scraper.stockScraper.errorLock.Unlock()
   114  	if err != nil {
   115  		scraper.stockScraper.error = err
   116  	}
   117  	scraper.stockScraper.closed = true
   118  	close(scraper.stockScraper.shutdownDone)
   119  }
   120  
   121  // Close closes any existing API connections
   122  func (scraper *FinageScraper) Close() error {
   123  	if scraper.stockScraper.closed {
   124  		return errors.New("scraper already closed")
   125  	}
   126  	close(scraper.stockScraper.shutdown)
   127  	<-scraper.stockScraper.shutdownDone
   128  	scraper.stockScraper.errorLock.RLock()
   129  	defer scraper.stockScraper.errorLock.RUnlock()
   130  	return scraper.stockScraper.error
   131  }
   132  
   133  // unmarshalQuotationJSON unmarshalls the JSON response into an auxiliary struct
   134  // and converts it into models.StockQuotation
   135  func (scraper *FinageScraper) unmarshalQuotationJSON(data []byte) (models.StockQuotation, error) {
   136  	receivedQuotation := struct {
   137  		Symbol   string  `json:"s"`
   138  		PriceAsk float64 `json:"ap"`
   139  		PriceBid float64 `json:"bp"`
   140  		SizeAsk  float64 `json:"as"`
   141  		SizeBid  float64 `json:"bs"`
   142  		Time     int64   `json:"t"`
   143  	}{}
   144  	err := json.Unmarshal(data, &receivedQuotation)
   145  	if err != nil {
   146  		return models.StockQuotation{}, err
   147  	}
   148  	if len(receivedQuotation.Symbol) == 0 {
   149  		return models.StockQuotation{}, errors.New("not a valid stock quotation")
   150  	}
   151  	name, isin := getCompanyDetails(receivedQuotation.Symbol)
   152  	return models.StockQuotation{
   153  		Symbol:     receivedQuotation.Symbol,
   154  		Name:       name,
   155  		PriceAsk:   receivedQuotation.PriceAsk,
   156  		PriceBid:   receivedQuotation.PriceBid,
   157  		SizeAskLot: receivedQuotation.SizeAsk,
   158  		SizeBidLot: receivedQuotation.SizeBid,
   159  		Source:     scraper.stockScraper.source,
   160  		Time:       time.Unix(0, receivedQuotation.Time*int64(time.Millisecond)),
   161  		ISIN:       isin,
   162  	}, nil
   163  }
   164  
   165  func getCompanyDetails(symbol string) (name string, isin string) {
   166  	switch symbol {
   167  	case "AAPL":
   168  		return "APPLE", "US0378331005"
   169  	case "MSFT":
   170  		return "MICROSOFT CORP", "US5949181045"
   171  	case "AMZN":
   172  		return "AMAZON.COM", "US0231351067"
   173  	case "FB":
   174  		return "FACEBOOK A", "US30303M1027"
   175  	case "GOOGL":
   176  		return "ALPHABET A", "US02079K3059"
   177  	case "GOOG":
   178  		return "ALPHABET C", "US02079K1079"
   179  	case "TSLA":
   180  		return "TESLA", "US88160R1014"
   181  	case "NVDA":
   182  		return "NVIDIA", "US67066G1040"
   183  	case "JPM":
   184  		return "JPMORGAN CHASE & CO ", "US46625H1005"
   185  	case "JNJ":
   186  		return "JOHNSON & JOHNSON ", "US4781601046"
   187  	}
   188  	return "", ""
   189  }
   190  
   191  // getAPIKeyFromSecrets returns a github api key
   192  func getAPIKeyFromSecrets() string {
   193  	var lines []string
   194  	executionMode := os.Getenv("EXEC_MODE")
   195  	var file *os.File
   196  	var err error
   197  	if executionMode == "production" {
   198  		file, err = os.Open("/run/secrets/" + apiKey)
   199  		if err != nil {
   200  			log.Fatal(err)
   201  		}
   202  	} else {
   203  		file, err = os.Open("../../secrets/" + apiKey)
   204  		if err != nil {
   205  			log.Fatal(err)
   206  		}
   207  	}
   208  	defer func(file *os.File) {
   209  		err := file.Close()
   210  		if err != nil {
   211  			log.Error("error closing file ", err)
   212  		}
   213  	}(file)
   214  	scanner := bufio.NewScanner(file)
   215  	for scanner.Scan() {
   216  		lines = append(lines, scanner.Text())
   217  	}
   218  	if err := scanner.Err(); err != nil {
   219  		log.Fatal(err)
   220  	}
   221  	if len(lines) != 1 {
   222  		log.Fatal("Secrets file should have exactly one line")
   223  	}
   224  	return lines[0]
   225  }