Skip to content

Commit

Permalink
OKex changed their api
Browse files Browse the repository at this point in the history
  • Loading branch information
polyrabbit committed Apr 12, 2020
1 parent 097b221 commit 1977bcb
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 187 deletions.
73 changes: 27 additions & 46 deletions exchange/kraken.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
"time"

"github.com/polyrabbit/my-token/exchange/model"

"github.com/buger/jsonparser"
"github.com/polyrabbit/my-token/http"

"github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)

// https://www.kraken.com/help/api
Expand All @@ -27,20 +27,16 @@ func (client *krakenClient) GetName() string {
return "Kraken"
}

/**
Read response and check any potential errors
*/
func (client *krakenClient) readResponse(respBytes []byte) ([]byte, error) {
var errorMsg []string
jsonparser.ArrayEach(respBytes, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
if dataType == jsonparser.String {
errorMsg = append(errorMsg, string(value))
// Check to see if we have error in the response
func (client *krakenClient) extractError(respByte []byte) error {
errorArray := gjson.GetBytes(respByte, "error").Array()
if len(errorArray) > 0 {
errMsg := errorArray[0].Get("0").String()
if len(errMsg) != 0 {
return errors.New(errMsg)
}
}, "error")
if len(errorMsg) != 0 {
return nil, errors.New(strings.Join(errorMsg, ", "))
}
return respBytes, nil
return nil
}

func (client *krakenClient) GetKlinePrice(symbol string, since time.Time, interval int) (float64, error) {
Expand All @@ -50,55 +46,40 @@ func (client *krakenClient) GetKlinePrice(symbol string, since time.Time, interv
"since": strconv.FormatInt(since.Unix(), 10),
"interval": strconv.Itoa(interval),
})
if err != nil {
return 0, err
}

content, err := client.readResponse(respByte)
if err != nil {
return 0, err
if err := client.extractError(respByte); err != nil {
return 0, fmt.Errorf("kraken get kline: %w", 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
// gjson saved my life, no need to struggle with different/weird response types
candleV := gjson.GetBytes(respByte, fmt.Sprintf("result.%s.0", strings.ToUpper(symbol))).Array()
if len(candleV) != 8 {
return 0, fmt.Errorf("kraken malformed kline response, expecting 8 elements, got %d", len(candleV))
}

timestamp := candleV[0].Int()
openPrice := candleV[1].Float()
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)
return openPrice, nil
}

func (client *krakenClient) GetSymbolPrice(symbol string) (*model.SymbolPrice, error) {
respByte, err := http.Get(krakenBaseApi+"Ticker", map[string]string{"pair": strings.ToUpper(symbol)})
if err != nil {
return nil, err
if err := client.extractError(respByte); err != nil {
return nil, fmt.Errorf("kraken get ticker: %w", err)
}

content, err := client.readResponse(respByte)
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
lastPriceV := gjson.GetBytes(respByte, fmt.Sprintf("result.%s.c.0", strings.ToUpper(symbol)))
if !lastPriceV.Exists() {
return nil, fmt.Errorf("kraken malformed ticker response, missing key %s", fmt.Sprintf("result.%s.c.0", strings.ToUpper(symbol)))
}
lastPrice := lastPriceV.Float()

time.Sleep(time.Second) // API call rate limit
var (
Expand All @@ -121,7 +102,7 @@ func (client *krakenClient) GetSymbolPrice(symbol string) (*model.SymbolPrice, e

return &model.SymbolPrice{
Symbol: symbol,
Price: lastPriceString,
Price: lastPriceV.String(),
UpdateAt: time.Now(),
Source: client.GetName(),
PercentChange1h: percentChange1h,
Expand Down
150 changes: 61 additions & 89 deletions exchange/okex.go
Original file line number Diff line number Diff line change
@@ -1,148 +1,120 @@
package exchange

import (
"encoding/json"
"errors"
"fmt"
"math"
"strconv"
"strings"
"time"

"github.com/polyrabbit/my-token/exchange/model"

"github.com/polyrabbit/my-token/http"
"github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)

// https://github.com/okcoin-okex/API-docs-OKEx.com
const okexBaseApi = "https://www.okex.com/api/v1"
// https://www.okex.com/docs/zh/#spot-some
const okexBaseApi = "https://www.okex.com/api/spot/v3/instruments/"

type okexClient struct {
AccessKey string
SecretKey string
}

type okexErrorResponse struct {
ErrorCode int `json:"error_code"`
}

type okexTickerResponse struct {
okexErrorResponse
Date int64 `json:",string"`
Ticker struct {
Last float64 `json:",string"`
}
}

type okexKlineResponse struct {
okexErrorResponse
Data [][]interface{}
}

func (resp *okexTickerResponse) getCommonResponse() okexErrorResponse {
return resp.okexErrorResponse
}

func (resp *okexTickerResponse) getInternalData() interface{} {
return resp
}

func (resp *okexKlineResponse) getCommonResponse() okexErrorResponse {
return resp.okexErrorResponse
}

func (resp *okexKlineResponse) getInternalData() interface{} {
return &resp.Data
}

// Any way to hold the common response, instead of adding an interface here?
type okexCommonResponseProvider interface {
getCommonResponse() okexErrorResponse
getInternalData() interface{}
}

func (client *okexClient) GetName() string {
return "OKEx"
}

func (client *okexClient) decodeResponse(respBytes []byte, respJSON okexCommonResponseProvider) error {
// What a messy
respBody := strings.TrimSpace(string(respBytes))
if respBody[0] == '[' {
return json.Unmarshal(respBytes, respJSON.getInternalData())
func (client *okexClient) GetKlinePrice(symbol, granularity string, start, end time.Time) (float64, error) {
respByte, err := http.Get(okexBaseApi+symbol+"/candles", map[string]string{
"granularity": granularity,
"start": start.UTC().Format(time.RFC3339),
"end": end.UTC().Format(time.RFC3339),
})
if err := client.extractError(respByte); err != nil {
return 0, fmt.Errorf("okex get candles: %w", err)
}

if err := json.Unmarshal(respBytes, &respJSON); err != nil {
return err
if err != nil {
return 0, fmt.Errorf("okex get candles: %w", err)
}

// All I need is to get the common part, I don't like this
commonResponse := respJSON.getCommonResponse()
if commonResponse.ErrorCode != 0 {
return fmt.Errorf("error_code: %v", commonResponse.ErrorCode)
klines := gjson.ParseBytes(respByte).Array()
if len(klines) == 0 {
return 0, fmt.Errorf("okex got empty candles response")
}
return nil
}

func (client *okexClient) GetKlinePrice(symbol, period string, size int) (float64, error) {
symbol = strings.ToLower(symbol)
respByte, err := http.Get(okexBaseApi+"/kline.do", map[string]string{
"symbol": symbol,
"type": period,
"size": strconv.Itoa(size),
})
if err != nil {
return 0, err
lastKline := klines[len(klines)-1]
if len(lastKline.Array()) != 6 {
return 0, fmt.Errorf(`okex malformed kline response, got size %d`, len(lastKline.Array()))
}

var respJSON okexKlineResponse
err = client.decodeResponse(respByte, &respJSON)
if err != nil {
return 0, err
updated := time.Now()
if parsed, err := time.Parse(time.RFC3339, lastKline.Get("0").String()); err == nil {
updated = parsed
}
logrus.Debugf("%s - Kline for %s*%v uses price at %s", client.GetName(), period, size,
time.Unix(int64(respJSON.Data[0][0].(float64))/1000, 0))
return strconv.ParseFloat(respJSON.Data[0][1].(string), 64)
logrus.Debugf("%s - Kline for %s seconds uses price at %s",
client.GetName(), granularity, updated.Local())
return lastKline.Get("1").Float(), nil
}

func (client *okexClient) GetSymbolPrice(symbol string) (*model.SymbolPrice, error) {
respByte, err := http.Get(okexBaseApi+"/ticker.do", map[string]string{"symbol": strings.ToLower(symbol)})
respByte, err := http.Get(okexBaseApi+symbol+"/ticker", nil)
if err := client.extractError(respByte); err != nil {
// Extract more readable first if have
return nil, fmt.Errorf("okex get symbol price: %w", err)
}
if err != nil {
return nil, err
return nil, fmt.Errorf("okex get symbol price: %w", err)
}

var respJSON okexTickerResponse
err = client.decodeResponse(respByte, &respJSON)
lastV := gjson.GetBytes(respByte, "last")
if !lastV.Exists() {
return nil, fmt.Errorf(`okex malformed get symbol price response, missing "last" key`)
}
lastPrice := lastV.Float()
updateAtV := gjson.GetBytes(respByte, "timestamp")
if !updateAtV.Exists() {
return nil, fmt.Errorf(`okex malformed get symbol price response, missing "timestamp" key`)
}
updateAt, err := time.Parse(time.RFC3339, updateAtV.String())
if err != nil {
return nil, err
return nil, fmt.Errorf("okex parse timestamp: %w", err)
}

var percentChange1h, percentChange24h = math.MaxFloat64, math.MaxFloat64
price1hAgo, err := client.GetKlinePrice(symbol, "1min", 60)
price1hAgo, err := client.GetKlinePrice(symbol, "60", updateAt.Add(-time.Hour), updateAt)
if err != nil {
logrus.Warnf("%s - Failed to get price 1 hour ago, error: %v\n", client.GetName(), err)
} else if price1hAgo != 0 {
percentChange1h = (respJSON.Ticker.Last - price1hAgo) / price1hAgo * 100
percentChange1h = (lastPrice - price1hAgo) / price1hAgo * 100
}

time.Sleep(time.Second) // Limit 1 req/sec for Kline
price24hAgo, err := client.GetKlinePrice(symbol, "3min", 492) // Why not 480?
price24hAgo, err := client.GetKlinePrice(symbol, "900", updateAt.Add(-24*time.Hour), updateAt)
if err != nil {
logrus.Warnf("%s - Failed to get price 24 hours ago, error: %v\n", client.GetName(), err)
} else if price24hAgo != 0 {
percentChange24h = (respJSON.Ticker.Last - price24hAgo) / price24hAgo * 100
percentChange24h = (lastPrice - price24hAgo) / price24hAgo * 100
}

return &model.SymbolPrice{
Symbol: symbol,
Price: strconv.FormatFloat(respJSON.Ticker.Last, 'f', -1, 64),
UpdateAt: time.Unix(respJSON.Date, 0),
Price: strconv.FormatFloat(lastPrice, 'f', -1, 64),
UpdateAt: updateAt,
Source: client.GetName(),
PercentChange1h: percentChange1h,
PercentChange24h: percentChange24h,
}, nil
}

// Check to see if we have error in the response
func (client *okexClient) extractError(respByte []byte) error {
errorMsg := gjson.GetBytes(respByte, "error_message")
if !errorMsg.Exists() {
errorMsg = gjson.GetBytes(respByte, "message")
}
if len(errorMsg.String()) != 0 {
return errors.New(errorMsg.String())
}
return nil
}

func init() {
model.Register(new(okexClient))
}
5 changes: 3 additions & 2 deletions exchange/okex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ package exchange

import (
"testing"
"time"
)

func TestOKExClient(t *testing.T) {

var client = new(okexClient)

t.Run("GetKlinePrice", func(t *testing.T) {
_, err := client.GetKlinePrice("bTC_usdt", "1min", 60)
_, err := client.GetKlinePrice("bTC_usdt", "60", time.Now().Add(-time.Hour), time.Now())

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

t.Run("GetKlinePrice of unknown symbol", func(t *testing.T) {
_, err := client.GetKlinePrice("abcedfg", "1min", 60)
_, err := client.GetKlinePrice("abcedfg", "60", time.Now().Add(-time.Hour), time.Now())

if err == nil {
t.Fatalf("Expecting error when fetching unknown price, but get nil")
Expand Down
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ module github.com/polyrabbit/my-token
go 1.12

require (
github.com/buger/jsonparser v0.0.0-20180318095312-2cac668e8456
github.com/fatih/color v0.0.0-20180213133403-507f6050b856
github.com/gosuri/uilive v0.0.4
github.com/mattn/go-colorable v0.0.0-20180310133214-efa589957cd0
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/mattn/go-runewidth v0.0.8 // indirect
github.com/mitchellh/gox v1.0.1 // indirect
github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84
github.com/preichenberger/go-coinbasepro/v2 v2.0.5
github.com/sirupsen/logrus v1.4.2
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.6.2
github.com/tidwall/gjson v1.6.0
)
Loading

0 comments on commit 1977bcb

Please sign in to comment.