Skip to content

Commit

Permalink
Support Kraken and use "github.com/buger/jsonparser" to save my life
Browse files Browse the repository at this point in the history
  • Loading branch information
polyrabbit committed May 30, 2018
1 parent 3fcbb7f commit a5af5f0
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 4 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Token-ticker (or `tt` for short) is a CLI tool for those who are both **Crypto i

* Auto refresh on a specified interval, watch prices in live update mode
* Proxy aware HTTP request, for easy access to blocked exchanges
* Real-time prices from 11+ exchanges
* Real-time prices from 12+ exchanges

### Supported Exchanges

Expand All @@ -31,6 +31,7 @@ Token-ticker (or `tt` for short) is a CLI tool for those who are both **Crypto i
* [HitBTC](https://hitbtc.com/)
* [BigONE](https://big.one/)
* [Poloniex](https://poloniex.com/)
* [Kraken](https://www.kraken.com/)
* _still adding..._

### Installation
Expand Down
150 changes: 150 additions & 0 deletions exchange/kraken.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package exchange

import (
"errors"
"fmt"
"github.com/buger/jsonparser"
"github.com/sirupsen/logrus"
"io/ioutil"
"math"
"net/http"
"strconv"
"strings"
"time"
)

// https://www.kraken.com/help/api
const krakenBaseApi = "https://api.kraken.com/0/public/"

type krakenClient struct {
exchangeBaseClient
AccessKey string
SecretKey string
}

func NewkrakenClient(httpClient *http.Client) *krakenClient {
return &krakenClient{exchangeBaseClient: *newExchangeBase(krakenBaseApi, httpClient)}
}

func (client *krakenClient) GetName() string {
return "Kraken"
}

/**
Read response and check any potential errors
*/
func (client *krakenClient) readResponse(resp *http.Response) ([]byte, error) {
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

var errorMsg []string
jsonparser.ArrayEach(content, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
if dataType == jsonparser.String {
errorMsg = append(errorMsg, string(value))
}
}, "error")
if len(errorMsg) != 0 {
return nil, errors.New(strings.Join(errorMsg, ", "))
}

if resp.StatusCode != 200 {
return nil, errors.New(resp.Status)
}
return content, nil
}

func (client *krakenClient) GetKlinePrice(symbol string, since time.Time, interval int) (float64, error) {
symbolUpperCase := strings.ToUpper(symbol)
resp, err := client.httpGet("OHLC", map[string]string{
"pair": symbolUpperCase,
"since": strconv.FormatInt(since.Unix(), 10),
"interval": strconv.Itoa(interval),
})
if err != nil {
return 0, err
}

content, err := client.readResponse(resp)
if err != nil {
return 0, err
}
// jsonparser saved my life, no need to struggle with different/weird response types
klineBytes, dataType, _, err := jsonparser.Get(content, "result", symbolUpperCase, "[0]")
if err != nil {
return 0, err
}
if dataType != jsonparser.Array {
return 0, fmt.Errorf("kline should be an array, getting %s", dataType)
}

timestamp, err := jsonparser.GetInt(klineBytes, "[0]")
if err != nil {
return 0, err
}
openPrice, err := jsonparser.GetString(klineBytes, "[1]")
if err != nil {
return 0, err
}
logrus.Debugf("%s - Kline for %s uses open price at %s", client.GetName(), since.Local(),
time.Unix(timestamp, 0).Local())
return strconv.ParseFloat(openPrice, 64)
}

func (client *krakenClient) GetSymbolPrice(symbol string) (*SymbolPrice, error) {
resp, err := client.httpGet("Ticker", map[string]string{"pair": strings.ToUpper(symbol)})
if err != nil {
return nil, err
}

content, err := client.readResponse(resp)
if err != nil {
return nil, err
}

lastPriceString, err := jsonparser.GetString(content, "result", strings.ToUpper(symbol), "c", "[0]")
if err != nil {
return nil, err
}
lastPrice, err := strconv.ParseFloat(lastPriceString, 64)
if err != nil {
return nil, err
}

time.Sleep(time.Second) // API call rate limit
var (
now = time.Now()
percentChange1h = math.MaxFloat64
percentChange24h = math.MaxFloat64
)
price1hAgo, err := client.GetKlinePrice(symbol, now.Add(-61*time.Minute), 1)
if err != nil {
logrus.Warnf("%s - Failed to get price 1 hour ago, error: %v\n", client.GetName(), err)
} else if price1hAgo != 0 {
percentChange1h = (lastPrice - price1hAgo) / price1hAgo * 100
}
price24hAgo, err := client.GetKlinePrice(symbol, now.Add(-24*time.Hour), 5)
if err != nil {
logrus.Warnf("%s - Failed to get price 24 hours ago, error: %v\n", client.GetName(), err)
} else if price24hAgo != 0 {
percentChange24h = (lastPrice - price24hAgo) / price24hAgo * 100
}

return &SymbolPrice{
Symbol: symbol,
Price: lastPriceString,
UpdateAt: time.Now(),
Source: client.GetName(),
PercentChange1h: percentChange1h,
PercentChange24h: percentChange24h,
}, nil
}

func init() {
register((&krakenClient{}).GetName(), func(client *http.Client) ExchangeClient {
// Limited by type system in Go, I hate wrapper/adapter
return NewkrakenClient(client)
})
}
48 changes: 48 additions & 0 deletions exchange/kraken_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package exchange

import (
"net/http"
"testing"
"time"
)

func TestKrakenClient(t *testing.T) {

var client = NewkrakenClient(http.DefaultClient)

t.Run("GetKlinePrice", func(t *testing.T) {
_, err := client.GetKlinePrice("EOSETh", time.Now().Add(-61*time.Minute), 1)

if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
})

t.Run("GetKlinePrice of unknown symbol", func(t *testing.T) {
_, err := client.GetKlinePrice("fasfas", time.Now().Add(-61*time.Minute), 1)

if err == nil {
t.Fatalf("Expecting error when fetching unknown price, but get nil")
}
t.Logf("Returned error is `%v`, expected?", err)
})

t.Run("GetSymbolPrice", func(t *testing.T) {
sp, err := client.GetSymbolPrice("EOSETh")

if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if sp.Price == "" {
t.Fatalf("Get an empty price?")
}
})

t.Run("GetUnexistSymbolPrice", func(t *testing.T) {
_, err := client.GetSymbolPrice("ABC123")

if err == nil {
t.Fatalf("Should throws on invalid symbol")
}
})
}
6 changes: 4 additions & 2 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ import:
- package: github.com/spf13/pflag
- package: github.com/fatih/color
version: ~1.6.0
- package: github.com/buger/jsonparser
6 changes: 5 additions & 1 deletion token_ticker.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,8 @@ exchanges:

- name: Poloniex
tokens:
- BTC_ETH
- BTC_ETH

- name: Kraken
tokens:
- EOSETH

0 comments on commit a5af5f0

Please sign in to comment.