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 }