(Exchange) Add GetHistoricCandles() & GetHistoricCandlesEx() support to exchanges (#479)

* implemented binance and bitfinex GetHistoricCandles wrapper methods)

* coinbene supported added

* after and before clean up

* gateio wrapper completed

* merged upstream/master

* Added bsaic KlineIntervalSupported() method

* Converted binance fixed test

* WIP

* new KlineConvertToExchangeStandardString method added

* end of day WIP

* WIP

* end of day WIP started migration of trade history

* added kline support to hitbtc huobi lbank

* added exchangehistory to all supported exchanges started work on coinbase 300 candles/request method

* end of day WIP

* removed unused ta and misc changes to flag ready for review

* yobit cleanup

* revert coinbase changES

* general code clean up and added zb support

* poloniex support added

* renamed method to FormatExchangeKlineInterval other misc fixes

* linter fixes

* linter fixes

* removed verbose

* fixed poloniex test coverage

* revert poloniex mock data

* regenerated poloniex mock data

* a very verbose clean up

* binance mock clean up

* removed unneeded t.Log()

* setting verbose to true to debug CI issue

* first pass changes addressed

* common.ErrNotYetImplemented implemented :D

* comments added

* WIP-addressed exchange requests and reverted previous GetExchangeHistory changes

* WIP-addressed exchange requests and reverted previous GetExchangeHistory changes

* increased test coverage added kraken support

* OKGroup support completed started work on address GetExchangeHistory feedback and migrating to own PR under https://github.com/xtda/gocryptotrader/tree/exchange_history

* convert zb ratelimits

* gofmt run on okcoin

* increased delay on rate limit

* gofmt package

* fixed panic with coinbene and bithumb if conversion fails

* very broken end of day WIP

* added support for GetHistoricCandlesEx to coinbase and binance

* gofmt package

* coinbase, btcmarkets, zb ex wrapper function added

* added all exchange support for ex regenerated mock data

* update bithumb to return wrapper method

* gofmt package

* end of day started work on changes

* reworked test coverage added okgroup support general fixes/change requests addressed

* Added OneMonth

* limit checks on supportedexchanges

* reverted getexchangehistory

* reworked binance tesT

* added workaround for kraken panic

* renamed command to extended removed interval check on non-implemented commands

* added wrapperconfig back

* increased test coverage for FormatExchangeKlineInterval

* WIP

* increased test coverage for FormatExchangeKlineInterval bitfinex/gateio/huobi

* linter fixes

* zb kraken lbank coinbene btcmarkets support added

* removed verbose

* OK group support for other asset types added

* swapped margin to use spot endpoint

* index support added test coverage added for asset types

* added asset type to okcoin test

* gofmt

* add asset to extended method

* removed verbose

* add support for coinbene swap increase test coverage

* removed verbose

* small clean up of okgroup wrapper functions

* verbose to troubleshoot CI issues

* removed verbose

* added error check reverted coinbasechanges

* readme updated

* removed unused start/finish started work on decoupling api requests from kline package

* restructured coinbene, bithumb methods, added bitstamp support

* kraken time fix

* BTCMarkets restructure

* typo fix

* removed test for futures due to contact changing

* added start/end date to extended method over range

* converted to assettranslator

* removed verbose

* removed invalid char

* reverted incorrectly removed return

* added import

* further template updates

* macos hates my keyboard :D

* misc canges

* x -> i

* removed verbose

* updated fixCasing to allocate var before checks

* removed time conversion

* sort all outgoing kline candles

* fixCasing fix

* after/before checks added

* added parallel to test

* logic check on BTCmarkets

* removed unused param, used correct iterator

* converted HitBTC to use time.Time

* add iszero false check to candle times

* updated resultlimit to 5000

* new line added

* added comment to exported const

* use configured ratelimit

* fixed pair for test

* panic fixed WIP on fixCasing

* fixCasing rework, started work on readme docs

* enable rate limiter for wrapper issues tool

* docs updated

* removed err from return and formatted currency

* updated Yobit supported status

* Updated HitBTC to use onehour candles due to test exeuction times

* added further details to gctcli output

* added link to docs

* added link to tempalte

* disable FTX websocket in config_example

* fix poloneix

* regenerated poloniex mock data

* removed recording flag
This commit is contained in:
Andrew
2020-07-08 10:51:54 +10:00
committed by GitHub
parent c2c200cd1b
commit 4a736fb335
112 changed files with 52287 additions and 12550 deletions

View File

@@ -13,6 +13,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
@@ -184,8 +185,8 @@ func (g *Gateio) GetOrderbook(symbol string) (Orderbook, error) {
}
// GetSpotKline returns kline data for the most recent time period
func (g *Gateio) GetSpotKline(arg KlinesRequestParams) ([]*KLineResponse, error) {
urlPath := fmt.Sprintf("%s/%s/%s/%s?group_sec=%d&range_hour=%d",
func (g *Gateio) GetSpotKline(arg KlinesRequestParams) (kline.Item, error) {
urlPath := fmt.Sprintf("%s/%s/%s/%s?group_sec=%s&range_hour=%d",
g.API.Endpoints.URLSecondary,
gateioAPIVersion,
gateioKline,
@@ -196,58 +197,59 @@ func (g *Gateio) GetSpotKline(arg KlinesRequestParams) ([]*KLineResponse, error)
var rawKlines map[string]interface{}
err := g.SendHTTPRequest(urlPath, &rawKlines)
if err != nil {
return nil, err
return kline.Item{}, err
}
result := kline.Item{
Exchange: g.Name,
}
var result []*KLineResponse
if rawKlines == nil || rawKlines["data"] == nil {
return nil, fmt.Errorf("rawKlines is nil. Err: %s", err)
return kline.Item{}, errors.New("rawKlines is nil")
}
rawKlineDatasString, _ := json.Marshal(rawKlines["data"].([]interface{}))
var rawKlineDatas [][]interface{}
if err := json.Unmarshal(rawKlineDatasString, &rawKlineDatas); err != nil {
return nil, fmt.Errorf("rawKlines unmarshal failed. Err: %s", err)
return kline.Item{}, fmt.Errorf("rawKlines unmarshal failed. Err: %s", err)
}
for _, k := range rawKlineDatas {
otString, _ := strconv.ParseFloat(k[0].(string), 64)
otString, err := strconv.ParseFloat(k[0].(string), 64)
if err != nil {
return kline.Item{}, err
}
ot, err := convert.TimeFromUnixTimestampFloat(otString)
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.OpenTime. Err: %s", err)
return kline.Item{}, fmt.Errorf("cannot parse Kline.OpenTime. Err: %s", err)
}
_vol, err := convert.FloatFromString(k[1])
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.Volume. Err: %s", err)
}
_id, err := convert.FloatFromString(k[0])
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.Id. Err: %s", err)
return kline.Item{}, fmt.Errorf("cannot parse Kline.Volume. Err: %s", err)
}
_close, err := convert.FloatFromString(k[2])
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.Close. Err: %s", err)
return kline.Item{}, fmt.Errorf("cannot parse Kline.Close. Err: %s", err)
}
_high, err := convert.FloatFromString(k[3])
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.High. Err: %s", err)
return kline.Item{}, fmt.Errorf("cannot parse Kline.High. Err: %s", err)
}
_low, err := convert.FloatFromString(k[4])
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.Low. Err: %s", err)
return kline.Item{}, fmt.Errorf("cannot parse Kline.Low. Err: %s", err)
}
_open, err := convert.FloatFromString(k[5])
if err != nil {
return nil, fmt.Errorf("cannot parse Kline.Open. Err: %s", err)
return kline.Item{}, fmt.Errorf("cannot parse Kline.Open. Err: %s", err)
}
result = append(result, &KLineResponse{
ID: _id,
KlineTime: ot,
Volume: _vol,
Close: _close,
High: _high,
Low: _low,
Open: _open,
result.Candles = append(result.Candles, kline.Candle{
Time: ot,
Volume: _vol,
Close: _close,
High: _high,
Low: _low,
Open: _open,
})
}
return result, nil

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"os"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
@@ -13,6 +14,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/core"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
@@ -148,11 +151,10 @@ func TestGetOrderbook(t *testing.T) {
func TestGetSpotKline(t *testing.T) {
t.Parallel()
_, err := g.GetSpotKline(KlinesRequestParams{
Symbol: "btc_usdt",
GroupSec: TimeIntervalFiveMinutes, // 5 minutes or less
HourSize: 1, // 1 hour data
GroupSec: "5", // 5 minutes or less
HourSize: 1, // 1 hour data
})
if err != nil {
@@ -578,7 +580,6 @@ func setupWSTestAuth(t *testing.T) {
// TestWsUnsubscribe dials websocket, sends an unsubscribe request.
func TestWsUnsubscribe(t *testing.T) {
setupWSTestAuth(t)
g.Verbose = true
err := g.Unsubscribe(wshandler.WebsocketChannelSubscription{
Channel: "ticker.subscribe",
Currency: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_"),
@@ -762,3 +763,52 @@ func TestParseTime(t *testing.T) {
t.Error("unexpected result")
}
}
func TestGetHistoricCandles(t *testing.T) {
currencyPair := currency.NewPairFromString("BTC_USDT")
startTime := time.Now().Add(-time.Hour * 6)
_, err := g.GetHistoricCandles(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
}
func TestGetHistoricCandlesExtended(t *testing.T) {
currencyPair := currency.NewPairFromString("BTC_USDT")
startTime := time.Now().Add(-time.Hour * 6)
_, err := g.GetHistoricCandlesExtended(currencyPair, asset.Spot, startTime, time.Now(), kline.OneMin)
if err != nil {
t.Fatal(err)
}
}
func Test_FormatExchangeKlineInterval(t *testing.T) {
testCases := []struct {
name string
interval kline.Interval
output string
}{
{
"OneMin",
kline.OneMin,
"60",
},
{
"OneDay",
kline.OneDay,
"86400",
},
}
for x := range testCases {
test := testCases[x]
t.Run(test.name, func(t *testing.T) {
ret := g.FormatExchangeKlineInterval(test.interval)
if ret != test.output {
t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret)
}
})
}
}

View File

@@ -52,7 +52,7 @@ type BalancesResponse struct {
type KlinesRequestParams struct {
Symbol string // Required field; example LTCBTC,BTCUSDT
HourSize int // How many hours of data
GroupSec TimeInterval
GroupSec string
}
// KLineResponse holds the kline response data

View File

@@ -108,12 +108,29 @@ func (g *Gateio) SetDefaults() {
},
WithdrawPermissions: exchange.AutoWithdrawCrypto |
exchange.NoFiatWithdrawals,
Kline: kline.ExchangeCapabilitiesSupported{
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: map[string]bool{
kline.OneMin.Word(): true,
kline.ThreeMin.Word(): true,
kline.FiveMin.Word(): true,
kline.FifteenMin.Word(): true,
kline.ThirtyMin.Word(): true,
kline.OneHour.Word(): true,
kline.TwoHour.Word(): true,
kline.FourHour.Word(): true,
kline.SixHour.Word(): true,
kline.TwelveHour.Word(): true,
kline.OneDay.Word(): true,
},
},
},
}
g.Requester = request.New(g.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
@@ -710,7 +727,39 @@ func (g *Gateio) ValidateCredentials() error {
return g.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (g *Gateio) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
// FormatExchangeKlineInterval returns Interval to exchange formatted string
func (g *Gateio) FormatExchangeKlineInterval(in kline.Interval) string {
return strconv.FormatFloat(in.Duration().Seconds(), 'f', 0, 64)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (g *Gateio) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
if !g.KlineIntervalEnabled(interval) {
return kline.Item{}, kline.ErrorKline{
Interval: interval,
}
}
hours := end.Sub(start).Hours()
params := KlinesRequestParams{
Symbol: g.FormatExchangeCurrency(pair, a).String(),
GroupSec: g.FormatExchangeKlineInterval(interval),
HourSize: int(hours),
}
klineData, err := g.GetSpotKline(params)
if err != nil {
return kline.Item{}, err
}
klineData.Interval = interval
klineData.Pair = pair
klineData.Asset = a
klineData.SortCandlesByTimestamp(false)
return klineData, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (g *Gateio) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return g.GetHistoricCandles(pair, a, start, end, interval)
}