
C# jest super. Każda kolejna wersja bierze najlepsze rozwiązania z innych języków i utrzymuje go na wysokim poziomie. Aczkolwiek pisanie ciągle tylko w C# i w pracy, i w domu czasami staje się nudne. Lubię się uczyć nowych języków bo to dobre ćwiczenie dla mózgu. Postanowiłem napisać prosty program w Go. A z racji, że nie mam pomysłu co napisać, wezmę aplikację poprzednio pisaną dla .NET Core i przepiszę ją w Go. Aplikacja ściąga historię dziennych kursów kryptowalut na giełdzie Binance. Chcę ściągnąć wszystkie kursy od początku czasu aż do teraz. Przepisywanie rozpocząłem od najprostszej możliwej rzeczy:
package main
const hostUrl = "https://binance.com"
const apiResourceUri = "/api/v1/klines"
const apiGetSymbolUri = "/api/v1/exchangeInfo"
const downloadLocation = "data" //katalog z zapisanymi danymi
Na razie nieźle. Otwarcie urla http://binance.com/api/v1/exchangeInfo daje między innymi listę wszystkic symboli jakie ta giełda obsługuje. Wygląda mniej więcej tak:
{
[…]
"exchangeFilters": [],
"symbols": [
{
"symbol": "ETHBTC",
"status": "TRADING",
"baseAsset": "ETH",
"baseAssetPrecision": 8,
"quoteAsset": "BTC",
"quotePrecision": 8,
"orderTypes": [
"LIMIT",
"LIMIT_MAKER",
"MARKET",
"STOP_LOSS_LIMIT",
"TAKE_PROFIT_LIMIT"
],
"icebergAllowed": true,
"isSpotTradingAllowed": true,
"isMarginTradingAllowed": true,
"filters": […]
},
{
"symbol": "LTCBTC",
"status": "TRADING",
"baseAsset": "LTC",
"baseAssetPrecision": 8,
"quoteAsset": "BTC",
"quotePrecision": 8,
"orderTypes": [
"LIMIT",
"LIMIT_MAKER",
"MARKET",
"STOP_LOSS_LIMIT",
"TAKE_PROFIT_LIMIT"
],
"icebergAllowed": true,
"isSpotTradingAllowed": true,
"isMarginTradingAllowed": false,
"filters": […]
},
{
"symbol": "BNBBTC",
"status": "TRADING",
"baseAsset": "BNB"
i tak dalej. Potrzebuję tylko tablicy "symbols" a w niej tylko właściwości "symbol". Mój model do deserializacji responsa będzie wyglądać tak:
type Symbol struct {
Symbol string
}
type GetSymbolsResponse struct {
Symbols []Symbol
}
A metoda, która zwróci wszystkie symbole tak:
// pobiera wszystkie symbole z binance
func GetSymbols() []string {
resp, err := http.Get(hostUrl + apiGetSymbolUri)
if err != nil {
panic(err)
}
// defer jest jak finally z c#, wywoła się na wyjściu z funkcji
defer resp.Body.Close()
response := new(GetSymbolsResponse)
json.NewDecoder(resp.Body).Decode(response)
fmt.Println(len(response.Symbols))
result := make([]string, 0)
for _, val := range response.Symbols {
result = append(result, val.Symbol)
}
return result
}
Żeby to zadziałało, musże dodać importy (metoda powyżej używa http, fmt i json). Importy daję zaraz za package main.
import (
"encoding/json"
"fmt"
"net/http"
)
Teraz kiedy mam już wszystkie symbole, jakich potrzebuję, chcę ściągnąć wszystkie dane historyczne. Będę je ściągać po jednym miesiącu na raz i zapisywać w folderze z danymi. Szkic mojego programu wygląda mniej-więcej tak:
func loadHistoricalData() {
checkDownloadLocation() // jak nie ma folderu to go stwórz
fmt.Println("Getting symbols")
r := GetSymbols()
count := len(r)
syncChannel := make(chan bool, 3) // 3 równoległe zadania na raz
jobsCount := 0
fmt.Println("Found %v symbols", count)
for _, b := range r {
for year := time.Now().Year(); year > 2010; year-- { // pobieramy od 2010
for month := 12; month >= 1; month-- {
jobsCount++
go downloadMonthOfData(b, year, month, syncChannel)
}
}
}
for i := 0; i < jobsCount; i++ {
syncChannel <- true
Kanały (chan) wyglądają dziwnie ale chcę ograniczyć liczbę jednoczesnych pobierań. Właśnie do tego używam syncChannel. To sprawia, że wątek chcący wsadzić tam czwartą wartość zostanie zapauzowany aż zwolni się tam miejsce (pojemność kanału jest ustawiona na 3), czyli dodamy nową wartość do kanału dopiero, jak jakiś inny wątek wyjmie coś z niego. Wątki próbujące pobierać wartości z kanału też będą zawieszone dopóki się tam nie pojawi jakaś wartość. Wiem ile zadań w sumie będzie do przerobienia więc tyle właśnie wartości spróbuję wsadzić do kanału (pauzując za każdym razem jak nie ma miejsca na nowe wartości). Funkcja ściągająca miesiąc danych wygląda tak:
func downloadMonthOfData(symbol string, year, month int, c chan bool) {
signal := <-c // czekamy na sygnał na start
if !signal {
return
}
start := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
end := time.Date(year, time.Month(month+1), 1, 0, 0, 0, 0, time.UTC)
if start.After(time.Now()) {
return
}
fileName := fmt.Sprintf("%s_%d_%02d.json", symbol, year, month)
fmt.Println("Downloading: " + fileName)
pathToFile := path.Join(downloadLocation, fileName)
if _, err := os.Stat(pathToFile); os.IsNotExist(err) {
r, e := http.Get(createGetSymbolUrl(symbol, start.Unix()*1000, end.Unix()*1000))
if e != nil {
panic(e)
}
defer r.Body.Close()
out, _ := os.Create(pathToFile)
defer out.Close()
io.Copy(out, r.Body)
}
}
I tyle. Cały program jest dostępny tutaj: https://github.com/jaropawlak/getBinanceStats. Musiałem dodać trochę więcej importów żeby zadziałało więc nie wszystko, co jest w kodzie jest też w przykładach w tym artykule, ale wszystko jest na githubie :). To mój skromny początek z językiem go. Nie było ciężko a wszystkie niewiadome były rozwiązywane po chwili googlania. Muszę przyznać, że było to bardzo przyjemne doświadczenie. Na pewno nie jest to moje ostatnie spotkanie z Go. Jeśli masz wątpliwości czy spróbować swoich sił w go - bardzo polecam.
Komentarze
Prześlij komentarz