|
| 1 | +package exchange |
| 2 | + |
| 3 | +import ( |
| 4 | + "errors" |
| 5 | + "fmt" |
| 6 | + "github.com/buger/jsonparser" |
| 7 | + "github.com/sirupsen/logrus" |
| 8 | + "io/ioutil" |
| 9 | + "math" |
| 10 | + "net/http" |
| 11 | + "strconv" |
| 12 | + "strings" |
| 13 | + "time" |
| 14 | +) |
| 15 | + |
| 16 | +// https://www.kraken.com/help/api |
| 17 | +const krakenBaseApi = "https://api.kraken.com/0/public/" |
| 18 | + |
| 19 | +type krakenClient struct { |
| 20 | + exchangeBaseClient |
| 21 | + AccessKey string |
| 22 | + SecretKey string |
| 23 | +} |
| 24 | + |
| 25 | +func NewkrakenClient(httpClient *http.Client) *krakenClient { |
| 26 | + return &krakenClient{exchangeBaseClient: *newExchangeBase(krakenBaseApi, httpClient)} |
| 27 | +} |
| 28 | + |
| 29 | +func (client *krakenClient) GetName() string { |
| 30 | + return "Kraken" |
| 31 | +} |
| 32 | + |
| 33 | +/** |
| 34 | +Read response and check any potential errors |
| 35 | +*/ |
| 36 | +func (client *krakenClient) readResponse(resp *http.Response) ([]byte, error) { |
| 37 | + defer resp.Body.Close() |
| 38 | + content, err := ioutil.ReadAll(resp.Body) |
| 39 | + if err != nil { |
| 40 | + return nil, err |
| 41 | + } |
| 42 | + |
| 43 | + var errorMsg []string |
| 44 | + jsonparser.ArrayEach(content, func(value []byte, dataType jsonparser.ValueType, offset int, err error) { |
| 45 | + if dataType == jsonparser.String { |
| 46 | + errorMsg = append(errorMsg, string(value)) |
| 47 | + } |
| 48 | + }, "error") |
| 49 | + if len(errorMsg) != 0 { |
| 50 | + return nil, errors.New(strings.Join(errorMsg, ", ")) |
| 51 | + } |
| 52 | + |
| 53 | + if resp.StatusCode != 200 { |
| 54 | + return nil, errors.New(resp.Status) |
| 55 | + } |
| 56 | + return content, nil |
| 57 | +} |
| 58 | + |
| 59 | +func (client *krakenClient) GetKlinePrice(symbol string, since time.Time, interval int) (float64, error) { |
| 60 | + symbolUpperCase := strings.ToUpper(symbol) |
| 61 | + resp, err := client.httpGet("OHLC", map[string]string{ |
| 62 | + "pair": symbolUpperCase, |
| 63 | + "since": strconv.FormatInt(since.Unix(), 10), |
| 64 | + "interval": strconv.Itoa(interval), |
| 65 | + }) |
| 66 | + if err != nil { |
| 67 | + return 0, err |
| 68 | + } |
| 69 | + |
| 70 | + content, err := client.readResponse(resp) |
| 71 | + if err != nil { |
| 72 | + return 0, err |
| 73 | + } |
| 74 | + // jsonparser saved my life, no need to struggle with different/weird response types |
| 75 | + klineBytes, dataType, _, err := jsonparser.Get(content, "result", symbolUpperCase, "[0]") |
| 76 | + if err != nil { |
| 77 | + return 0, err |
| 78 | + } |
| 79 | + if dataType != jsonparser.Array { |
| 80 | + return 0, fmt.Errorf("kline should be an array, getting %s", dataType) |
| 81 | + } |
| 82 | + |
| 83 | + timestamp, err := jsonparser.GetInt(klineBytes, "[0]") |
| 84 | + if err != nil { |
| 85 | + return 0, err |
| 86 | + } |
| 87 | + openPrice, err := jsonparser.GetString(klineBytes, "[1]") |
| 88 | + if err != nil { |
| 89 | + return 0, err |
| 90 | + } |
| 91 | + logrus.Debugf("%s - Kline for %s uses open price at %s", client.GetName(), since.Local(), |
| 92 | + time.Unix(timestamp, 0).Local()) |
| 93 | + return strconv.ParseFloat(openPrice, 64) |
| 94 | +} |
| 95 | + |
| 96 | +func (client *krakenClient) GetSymbolPrice(symbol string) (*SymbolPrice, error) { |
| 97 | + resp, err := client.httpGet("Ticker", map[string]string{"pair": strings.ToUpper(symbol)}) |
| 98 | + if err != nil { |
| 99 | + return nil, err |
| 100 | + } |
| 101 | + |
| 102 | + content, err := client.readResponse(resp) |
| 103 | + if err != nil { |
| 104 | + return nil, err |
| 105 | + } |
| 106 | + |
| 107 | + lastPriceString, err := jsonparser.GetString(content, "result", strings.ToUpper(symbol), "c", "[0]") |
| 108 | + if err != nil { |
| 109 | + return nil, err |
| 110 | + } |
| 111 | + lastPrice, err := strconv.ParseFloat(lastPriceString, 64) |
| 112 | + if err != nil { |
| 113 | + return nil, err |
| 114 | + } |
| 115 | + |
| 116 | + time.Sleep(time.Second) // API call rate limit |
| 117 | + var ( |
| 118 | + now = time.Now() |
| 119 | + percentChange1h = math.MaxFloat64 |
| 120 | + percentChange24h = math.MaxFloat64 |
| 121 | + ) |
| 122 | + price1hAgo, err := client.GetKlinePrice(symbol, now.Add(-61*time.Minute), 1) |
| 123 | + if err != nil { |
| 124 | + logrus.Warnf("%s - Failed to get price 1 hour ago, error: %v\n", client.GetName(), err) |
| 125 | + } else if price1hAgo != 0 { |
| 126 | + percentChange1h = (lastPrice - price1hAgo) / price1hAgo * 100 |
| 127 | + } |
| 128 | + price24hAgo, err := client.GetKlinePrice(symbol, now.Add(-24*time.Hour), 5) |
| 129 | + if err != nil { |
| 130 | + logrus.Warnf("%s - Failed to get price 24 hours ago, error: %v\n", client.GetName(), err) |
| 131 | + } else if price24hAgo != 0 { |
| 132 | + percentChange24h = (lastPrice - price24hAgo) / price24hAgo * 100 |
| 133 | + } |
| 134 | + |
| 135 | + return &SymbolPrice{ |
| 136 | + Symbol: symbol, |
| 137 | + Price: lastPriceString, |
| 138 | + UpdateAt: time.Now(), |
| 139 | + Source: client.GetName(), |
| 140 | + PercentChange1h: percentChange1h, |
| 141 | + PercentChange24h: percentChange24h, |
| 142 | + }, nil |
| 143 | +} |
| 144 | + |
| 145 | +func init() { |
| 146 | + register((&krakenClient{}).GetName(), func(client *http.Client) ExchangeClient { |
| 147 | + // Limited by type system in Go, I hate wrapper/adapter |
| 148 | + return NewkrakenClient(client) |
| 149 | + }) |
| 150 | +} |
0 commit comments