Skip to content

Commit a5af5f0

Browse files
committed
Support Kraken and use "github.com/buger/jsonparser" to save my life
1 parent 3fcbb7f commit a5af5f0

File tree

6 files changed

+210
-4
lines changed

6 files changed

+210
-4
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Token-ticker (or `tt` for short) is a CLI tool for those who are both **Crypto i
1616

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

2121
### Supported Exchanges
2222

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

3637
### Installation

exchange/kraken.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
}

exchange/kraken_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package exchange
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestKrakenClient(t *testing.T) {
10+
11+
var client = NewkrakenClient(http.DefaultClient)
12+
13+
t.Run("GetKlinePrice", func(t *testing.T) {
14+
_, err := client.GetKlinePrice("EOSETh", time.Now().Add(-61*time.Minute), 1)
15+
16+
if err != nil {
17+
t.Fatalf("Unexpected error: %v", err)
18+
}
19+
})
20+
21+
t.Run("GetKlinePrice of unknown symbol", func(t *testing.T) {
22+
_, err := client.GetKlinePrice("fasfas", time.Now().Add(-61*time.Minute), 1)
23+
24+
if err == nil {
25+
t.Fatalf("Expecting error when fetching unknown price, but get nil")
26+
}
27+
t.Logf("Returned error is `%v`, expected?", err)
28+
})
29+
30+
t.Run("GetSymbolPrice", func(t *testing.T) {
31+
sp, err := client.GetSymbolPrice("EOSETh")
32+
33+
if err != nil {
34+
t.Fatalf("Unexpected error: %v", err)
35+
}
36+
if sp.Price == "" {
37+
t.Fatalf("Get an empty price?")
38+
}
39+
})
40+
41+
t.Run("GetUnexistSymbolPrice", func(t *testing.T) {
42+
_, err := client.GetSymbolPrice("ABC123")
43+
44+
if err == nil {
45+
t.Fatalf("Should throws on invalid symbol")
46+
}
47+
})
48+
}

glide.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

glide.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ import:
77
- package: github.com/spf13/pflag
88
- package: github.com/fatih/color
99
version: ~1.6.0
10+
- package: github.com/buger/jsonparser

token_ticker.example.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,8 @@ exchanges:
6565

6666
- name: Poloniex
6767
tokens:
68-
- BTC_ETH
68+
- BTC_ETH
69+
70+
- name: Kraken
71+
tokens:
72+
- EOSETH

0 commit comments

Comments
 (0)