Skip to content

Commit e5d4ba0

Browse files
committed
Add HitBTC
1 parent cda23a8 commit e5d4ba0

File tree

6 files changed

+221
-10
lines changed

6 files changed

+221
-10
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88

99
> NEVER LEAVE YOUR TERMINAL
1010
11-
![token-ticker](https://user-images.githubusercontent.com/2657334/38620004-0a04640e-3dd0-11e8-9708-00484845cdb9.png)
11+
![token-ticker](https://user-images.githubusercontent.com/2657334/40175207-ff9e6504-5a09-11e8-9a3d-a887ebc4895a.png)
1212

1313
Track token prices in your favorite exchanges from the terminal. Best CLI tool for those who are both **Crypto investors** and **Engineers**.
1414

1515
### Features
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 8+ exchanges
19+
* Real-time prices from 9+ exchanges
2020

2121
### Supported Exchanges
2222

@@ -28,6 +28,7 @@ Track token prices in your favorite exchanges from the terminal. Best CLI tool f
2828
* [OKEx](https://www.okex.com/)
2929
* [Gate.io](https://gate.io/)
3030
* [Bittrex](https://bittrex.com/)
31+
* [HitBTC](https://hitbtc.com/)
3132
* _still adding..._
3233

3334
### Installation

exchange/coinmarketcap.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func (client *coinMarketCapClient) GetSymbolPrice(symbol string) (*SymbolPrice,
7676
token := tokens[0]
7777

7878
return &SymbolPrice{
79-
Symbol: token.Symbol,
79+
Symbol: symbol,
8080
Price: token.PriceUSD,
8181
Source: client.GetName(),
8282
UpdateAt: time.Unix(token.LastUpdated, 0),

exchange/hitbtc.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package exchange
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"github.com/sirupsen/logrus"
7+
"math"
8+
"net/http"
9+
"strconv"
10+
"strings"
11+
"time"
12+
)
13+
14+
// https://api.hitbtc.com/#market-data
15+
const hitBtcBaseApi = "https://api.hitbtc.com/api/2/"
16+
17+
// ZB api is very similar to OKEx, who copied whom?
18+
19+
type hitBtcClient struct {
20+
exchangeBaseClient
21+
AccessKey string
22+
SecretKey string
23+
}
24+
25+
type hitBtcCommonResponse struct {
26+
Error *struct {
27+
Code int
28+
Message string
29+
Description string
30+
}
31+
}
32+
33+
type hitBtcTickerResponse struct {
34+
hitBtcCommonResponse
35+
Last float64 `json:",string"`
36+
Open float64 `json:",string"`
37+
Timestamp string
38+
}
39+
40+
type hitBtcKlineResponse struct {
41+
hitBtcCommonResponse
42+
Timestamp string
43+
Open float64 `json:",string"`
44+
}
45+
46+
func (resp *hitBtcTickerResponse) getCommonResponse() hitBtcCommonResponse {
47+
return resp.hitBtcCommonResponse
48+
}
49+
50+
func (resp *hitBtcKlineResponse) getCommonResponse() hitBtcCommonResponse {
51+
return resp.hitBtcCommonResponse
52+
}
53+
54+
// Any way to hold the common response, instead of adding an interface here?
55+
type hitBtcCommonResponseProvider interface {
56+
getCommonResponse() hitBtcCommonResponse
57+
}
58+
59+
func NewHitBtcClient(httpClient *http.Client) *hitBtcClient {
60+
return &hitBtcClient{exchangeBaseClient: *newExchangeBase(hitBtcBaseApi, httpClient)}
61+
}
62+
63+
func (client *hitBtcClient) GetName() string {
64+
return "HitBTC"
65+
}
66+
67+
func (client *hitBtcClient) decodeResponse(resp *http.Response, respJSON hitBtcCommonResponseProvider) error {
68+
defer resp.Body.Close()
69+
70+
decoder := json.NewDecoder(resp.Body)
71+
if err := decoder.Decode(&respJSON); err != nil {
72+
if resp.StatusCode != 200 {
73+
return errors.New(resp.Status)
74+
}
75+
return err
76+
}
77+
78+
// All I need is to get the common part, I don't like this
79+
commonResponse := respJSON.getCommonResponse()
80+
if commonResponse.Error != nil {
81+
return errors.New(commonResponse.Error.Message + " - " + commonResponse.Error.Description)
82+
}
83+
return nil
84+
}
85+
86+
func (client *hitBtcClient) GetKlinePrice(symbol, period string, limit int) (float64, error) {
87+
symbol = strings.ToLower(symbol)
88+
resp, err := client.httpGet("public/candles/"+strings.ToUpper(symbol), map[string]string{
89+
"period": period,
90+
"limit": strconv.Itoa(limit),
91+
})
92+
if err != nil {
93+
return 0, err
94+
}
95+
96+
if resp.StatusCode != 200 {
97+
var respJSON hitBtcKlineResponse
98+
return 0, client.decodeResponse(resp, &respJSON)
99+
}
100+
var respJSON []hitBtcKlineResponse
101+
defer resp.Body.Close()
102+
103+
decoder := json.NewDecoder(resp.Body)
104+
if err := decoder.Decode(&respJSON); err != nil {
105+
if resp.StatusCode != 200 {
106+
return 0, errors.New(resp.Status)
107+
}
108+
return 0, err
109+
}
110+
logrus.Debugf("%s - Kline for %s*%v uses price at %s", client.GetName(), period, limit, respJSON[0].Timestamp)
111+
return respJSON[0].Open, nil
112+
}
113+
114+
func (client *hitBtcClient) GetSymbolPrice(symbol string) (*SymbolPrice, error) {
115+
resp, err := client.httpGet("/public/ticker/"+strings.ToUpper(symbol), nil)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
var respJSON hitBtcTickerResponse
121+
err = client.decodeResponse(resp, &respJSON)
122+
if err != nil {
123+
return nil, err
124+
}
125+
updated, err := time.Parse(time.RFC3339, respJSON.Timestamp)
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
var percentChange1h, percentChange24h = math.MaxFloat64, math.MaxFloat64
131+
price1hAgo, err := client.GetKlinePrice(symbol, "M1", 60)
132+
if err != nil {
133+
logrus.Warnf("%s - Failed to get price 1 hour ago, error: %v\n", client.GetName(), err)
134+
} else if price1hAgo != 0 {
135+
percentChange1h = (respJSON.Last - price1hAgo) / price1hAgo * 100
136+
}
137+
138+
//price24hAgo_, err := client.GetKlinePrice(symbol, "M3", 480)
139+
//logrus.Warnf("%s - %s", price24hAgo_, respJSON.Open)
140+
141+
price24hAgo := respJSON.Open
142+
percentChange24h = (respJSON.Last - price24hAgo) / price24hAgo * 100
143+
144+
return &SymbolPrice{
145+
Symbol: symbol,
146+
Price: strconv.FormatFloat(respJSON.Last, 'f', -1, 64),
147+
UpdateAt: updated,
148+
Source: client.GetName(),
149+
PercentChange1h: percentChange1h,
150+
PercentChange24h: percentChange24h,
151+
}, nil
152+
}
153+
154+
func init() {
155+
register((&hitBtcClient{}).GetName(), func(client *http.Client) ExchangeClient {
156+
// Limited by type system in Go, I hate wrapper/adapter
157+
return NewHitBtcClient(client)
158+
})
159+
}

exchange/hitbtc_test.go

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

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func renderTable(symbolPriceList []*SymbolPrice, writer *uilive.Writer) {
123123
// Fill in data
124124
for _, sp := range symbolPriceList {
125125
table.Append([]string{sp.Symbol, sp.Price, highlightChange(sp.PercentChange1h),
126-
highlightChange(sp.PercentChange24h), sp.Source, sp.UpdateAt.Format("15:04:05")})
126+
highlightChange(sp.PercentChange24h), sp.Source, sp.UpdateAt.Local().Format("15:04:05")})
127127
}
128128

129129
table.Render()

token_ticker.example.yaml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,31 @@
1515

1616
exchanges:
1717
## Exchanges are identified by name, following are supported exchanges
18-
- name: coinmarketcap
18+
- name: CoinMarketCap
1919
tokens:
2020
## Tokens supported by this exchange, note that different exchanges have different
2121
## formats to denote their tokens/markets, refer to their URLs to find the format
2222
- Bitcoin
2323
# - Ethereum
2424
# - Litecoin
2525

26-
- name: bitfinex
26+
- name: Bitfinex
2727
tokens:
2828
- btcusd
2929

30-
- name: binance
30+
- name: Binance
3131
tokens:
3232
## Tokens in Binance are actually token-currency pairs
3333
- BNBUSDT
3434
# - BTCUSDT
3535
# - ETHUSDT
3636
# - EOSETH
3737

38-
- name: huobi
38+
- name: Huobi
3939
tokens:
4040
- HTUSDT
4141

42-
- name: zb
42+
- name: ZB
4343
tokens:
4444
- ZB_QC
4545

@@ -53,4 +53,8 @@ exchanges:
5353

5454
- name: Bittrex
5555
tokens:
56-
- USDT-BTC
56+
- USDT-BTC
57+
58+
- name: HitBTC
59+
tokens:
60+
- BTCUSD

0 commit comments

Comments
 (0)