From 42475bf2b80eb8b2ba4e4711425cdb2e85c47273 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Thu, 27 Apr 2023 10:10:19 +1000 Subject: [PATCH] exchanges: add setTimeWindow boolean to GetKlineRequest param (#1160) * exchanges: add setTimeWindow boolean to GetKlineRequest params to differentiate between a set time period return from endpoint. * glorious: nits * exchange: conjugation * Update exchanges/exchange.go Co-authored-by: Scott * glorious: nits and an assortment of differences * exchanges: remove some comments * glorious: nits * cleanup * tests: fix * Update exchanges/hitbtc/hitbtc_wrapper.go Co-authored-by: Scott * Update exchanges/kline/kline.go Co-authored-by: Scott * Update exchanges/kline/kline_test.go Co-authored-by: Scott * glorious: nits * kline: fix test * rm unused variables * almost: nits * glorious: nits * linter: fix * rm unused variable * Refactored comment in the okex tests to ensure that it accurately reflects the variable name and the issue related to the time window, as requested by GloriousCode. The previous comment did not align with the identifier assigned to the property, which could cause confusion and misunderstanding among other programmers or stakeholders. The updated comment will improve the clarity and readability of the codebase and make it easier to understand the intended purpose of the associated variables. The change was made with the aim of improving the overall quality and maintainability of the code. --------- Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Scott --- backtester/engine/setup.go | 9 +- engine/rpcserver_test.go | 2 +- exchanges/binance/binance_wrapper.go | 38 +++---- exchanges/binanceus/binanceus_wrapper.go | 38 +++---- exchanges/bitfinex/bitfinex_wrapper.go | 33 +++--- exchanges/bithumb/bithumb_test.go | 6 +- exchanges/bithumb/bithumb_wrapper.go | 27 +++-- exchanges/bitstamp/bitstamp_wrapper.go | 32 +++--- exchanges/bittrex/bittrex_test.go | 25 ++++- exchanges/bittrex/bittrex_wrapper.go | 78 +++++++------ exchanges/btcmarkets/btcmarkets_wrapper.go | 30 ++--- exchanges/btse/btse_wrapper.go | 20 ++-- exchanges/bybit/bybit_wrapper.go | 46 ++++---- exchanges/coinbasepro/coinbasepro_wrapper.go | 16 +-- exchanges/exchange.go | 67 ++++++++--- exchanges/exchange_test.go | 109 ++++++++++++------ exchanges/gateio/gateio_wrapper.go | 26 ++--- exchanges/hitbtc/hitbtc.go | 2 +- exchanges/hitbtc/hitbtc_test.go | 14 ++- exchanges/hitbtc/hitbtc_wrapper.go | 71 ++++++++---- exchanges/huobi/huobi_wrapper.go | 30 +++-- exchanges/kline/kline.go | 112 +++++++++++++------ exchanges/kline/kline_test.go | 101 ++++++++++++++++- exchanges/kline/kline_types.go | 31 +++-- exchanges/kline/request.go | 17 ++- exchanges/kline/request_test.go | 44 +++++--- exchanges/kraken/kraken_wrapper.go | 22 ++-- exchanges/lbank/lbank_wrapper.go | 36 +++--- exchanges/okcoin/okcoin.go | 14 +-- exchanges/okcoin/okcoin_test.go | 7 +- exchanges/okcoin/okcoin_wrapper.go | 35 +++--- exchanges/okx/okx_test.go | 2 +- exchanges/okx/okx_wrapper.go | 45 ++++---- exchanges/poloniex/poloniex_wrapper.go | 32 +++--- exchanges/zb/zb_test.go | 26 +++-- exchanges/zb/zb_wrapper.go | 37 +++--- 36 files changed, 809 insertions(+), 471 deletions(-) diff --git a/backtester/engine/setup.go b/backtester/engine/setup.go index 73420981..06fef9eb 100644 --- a/backtester/engine/setup.go +++ b/backtester/engine/setup.go @@ -815,12 +815,19 @@ func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange, if cfg.DataSettings.APIData.InclusiveEndDate { cfg.DataSettings.APIData.EndDate = cfg.DataSettings.APIData.EndDate.Add(cfg.DataSettings.Interval.Duration()) } + + var limit int64 + limit, err = b.Features.Enabled.Kline.GetIntervalResultLimit(cfg.DataSettings.Interval) + if err != nil { + return nil, err + } + resp, err = loadAPIData( cfg, exch, fPair, a, - b.Features.Enabled.Kline.ResultLimit, + uint32(limit), dataType) if err != nil { return resp, err diff --git a/engine/rpcserver_test.go b/engine/rpcserver_test.go index 70c78a88..89d4d7f5 100644 --- a/engine/rpcserver_test.go +++ b/engine/rpcserver_test.go @@ -2530,7 +2530,7 @@ func TestGetTechnicalAnalysis(t *testing.T) { Enabled: currency.Pairs{cp}, } - b.Features.Enabled.Kline.Intervals = kline.DeployExchangeIntervals(kline.OneDay) + b.Features.Enabled.Kline.Intervals = kline.DeployExchangeIntervals(kline.IntervalCapacity{Interval: kline.OneDay}) err = em.Add(fExchange{IBotExchange: exch}) if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index ec86db46..97479720 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -167,23 +167,23 @@ func (b *Binance) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.ThreeMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.TwoHour, - kline.FourHour, - kline.SixHour, - kline.EightHour, - kline.TwelveHour, - kline.OneDay, - kline.ThreeDay, - kline.OneWeek, - kline.OneMonth, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.ThreeMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.TwoHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + kline.IntervalCapacity{Interval: kline.SixHour}, + kline.IntervalCapacity{Interval: kline.EightHour}, + kline.IntervalCapacity{Interval: kline.TwelveHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, + kline.IntervalCapacity{Interval: kline.ThreeDay}, + kline.IntervalCapacity{Interval: kline.OneWeek}, + kline.IntervalCapacity{Interval: kline.OneMonth}, ), - ResultLimit: 1000, + GlobalResultLimit: 1000, }, }, } @@ -1686,7 +1686,7 @@ func (b *Binance) FormatExchangeKlineInterval(interval kline.Interval) string { // GetHistoricCandles returns candles between a time period for a set time interval func (b *Binance) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := b.GetKlineRequest(pair, a, interval, start, end) + req, err := b.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } @@ -1701,7 +1701,7 @@ func (b *Binance) GetHistoricCandles(ctx context.Context, pair currency.Pair, a Symbol: req.Pair, StartTime: req.Start, EndTime: req.End, - Limit: int(b.Features.Enabled.Kline.ResultLimit), + Limit: int(req.RequestLimit), }) if err != nil { return nil, err @@ -1741,7 +1741,7 @@ func (b *Binance) GetHistoricCandlesExtended(ctx context.Context, pair currency. Symbol: req.Pair, StartTime: req.RangeHolder.Ranges[x].Start.Time, EndTime: req.RangeHolder.Ranges[x].End.Time, - Limit: int(b.Features.Enabled.Kline.ResultLimit), + Limit: int(req.RequestLimit), }) if err != nil { return nil, err diff --git a/exchanges/binanceus/binanceus_wrapper.go b/exchanges/binanceus/binanceus_wrapper.go index 57c672cb..478fd170 100644 --- a/exchanges/binanceus/binanceus_wrapper.go +++ b/exchanges/binanceus/binanceus_wrapper.go @@ -124,23 +124,23 @@ func (bi *Binanceus) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.ThreeMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.TwoHour, - kline.FourHour, - kline.SixHour, - kline.EightHour, - kline.TwelveHour, - kline.OneDay, - kline.ThreeDay, - kline.OneWeek, - kline.OneMonth, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.ThreeMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.TwoHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + kline.IntervalCapacity{Interval: kline.SixHour}, + kline.IntervalCapacity{Interval: kline.EightHour}, + kline.IntervalCapacity{Interval: kline.TwelveHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, + kline.IntervalCapacity{Interval: kline.ThreeDay}, + kline.IntervalCapacity{Interval: kline.OneWeek}, + kline.IntervalCapacity{Interval: kline.OneMonth}, ), - ResultLimit: 1000, + GlobalResultLimit: 1000, }, }, } @@ -869,7 +869,7 @@ func (bi *Binanceus) ValidateCredentials(ctx context.Context, assetType asset.It // GetHistoricCandles returns candles between a time period for a set time interval func (bi *Binanceus) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := bi.GetKlineRequest(pair, a, interval, start, end) + req, err := bi.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } @@ -879,7 +879,7 @@ func (bi *Binanceus) GetHistoricCandles(ctx context.Context, pair currency.Pair, Symbol: req.Pair, StartTime: req.Start, EndTime: req.End, - Limit: int64(bi.Features.Enabled.Kline.ResultLimit), + Limit: req.RequestLimit, }) if err != nil { return nil, err @@ -914,7 +914,7 @@ func (bi *Binanceus) GetHistoricCandlesExtended(ctx context.Context, pair curren Symbol: req.Pair, StartTime: req.RangeHolder.Ranges[x].Start.Time, EndTime: req.RangeHolder.Ranges[x].End.Time, - Limit: int64(bi.Features.Enabled.Kline.ResultLimit), + Limit: req.RequestLimit, }) if err != nil { return nil, err diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 4c32ce33..16c88bb8 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -147,20 +147,20 @@ func (b *Bitfinex) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.ThreeHour, - kline.SixHour, - kline.TwelveHour, - kline.OneDay, - kline.OneWeek, - kline.TwoWeek, - kline.OneMonth, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.ThreeHour}, + kline.IntervalCapacity{Interval: kline.SixHour}, + kline.IntervalCapacity{Interval: kline.TwelveHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, + kline.IntervalCapacity{Interval: kline.OneWeek}, + kline.IntervalCapacity{Interval: kline.TwoWeek}, + kline.IntervalCapacity{Interval: kline.OneMonth}, ), - ResultLimit: 10000, + GlobalResultLimit: 10000, }, }, } @@ -1084,7 +1084,7 @@ func (b *Bitfinex) FormatExchangeKlineInterval(in kline.Interval) string { // GetHistoricCandles returns candles between a time period for a set time interval func (b *Bitfinex) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := b.GetKlineRequest(pair, a, interval, start, end) + req, err := b.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } @@ -1099,7 +1099,8 @@ func (b *Bitfinex) GetHistoricCandles(ctx context.Context, pair currency.Pair, a b.FormatExchangeKlineInterval(req.ExchangeInterval), req.Start.UnixMilli(), req.End.UnixMilli(), - b.Features.Enabled.Kline.ResultLimit, true) + uint32(req.RequestLimit), + true) if err != nil { return nil, err } @@ -1138,7 +1139,7 @@ func (b *Bitfinex) GetHistoricCandlesExtended(ctx context.Context, pair currency b.FormatExchangeKlineInterval(req.ExchangeInterval), req.RangeHolder.Ranges[x].Start.Ticks*1000, req.RangeHolder.Ranges[x].End.Ticks*1000, - b.Features.Enabled.Kline.ResultLimit, + uint32(req.RequestLimit), true) if err != nil { return nil, err diff --git a/exchanges/bithumb/bithumb_test.go b/exchanges/bithumb/bithumb_test.go index 478dfec6..71471dd6 100644 --- a/exchanges/bithumb/bithumb_test.go +++ b/exchanges/bithumb/bithumb_test.go @@ -53,7 +53,7 @@ func TestMain(m *testing.M) { err = b.UpdateTradablePairs(context.Background(), false) if err != nil { - log.Fatal("Bithumb Setup() init error") + log.Fatal("Bithumb Setup() init error", err) } os.Exit(m.Run()) @@ -617,8 +617,8 @@ func TestGetHistoricCandles(t *testing.T) { if err != nil { t.Fatal(err) } - startTime := time.Now().AddDate(0, 0, -1) - _, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneMin, startTime, time.Now()) + startTime := time.Now().AddDate(0, -2, 0) + _, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneDay, startTime, time.Now()) if err != nil { t.Fatal(err) } diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index be2a04b3..3f29be5d 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -115,17 +115,22 @@ func (b *Bithumb) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.ThreeMin, - kline.FiveMin, - kline.TenMin, - kline.ThirtyMin, - kline.OneHour, - kline.SixHour, - kline.TwelveHour, - kline.OneDay, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.ThreeMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.TenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + // NOTE: The supported time intervals below are returned + // offset to the Asia/Seoul time zone. This may lead to + // issues with candle quality and conversion as the + // intervals may be broken up. Therefore the below intervals + // are constructed from hourly candles. + // kline.IntervalCapacity{Interval: kline.SixHour}, + // kline.IntervalCapacity{Interval: kline.TwelveHour}, + // kline.IntervalCapacity{Interval: kline.OneDay}, ), - ResultLimit: 1500, + GlobalResultLimit: 1500, }, }, } @@ -784,7 +789,7 @@ func (b *Bithumb) FormatExchangeKlineInterval(in kline.Interval) string { // GetHistoricCandles returns candles between a time period for a set time interval func (b *Bithumb) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := b.GetKlineRequest(pair, a, interval, start, end) + req, err := b.GetKlineRequest(pair, a, interval, start, end, true) if err != nil { return nil, err } diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 93cf48e9..6c0840a7 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -111,20 +111,20 @@ func (b *Bitstamp) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.ThreeMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.TwoHour, - kline.FourHour, - kline.SixHour, - kline.TwelveHour, - kline.OneDay, - kline.ThreeDay, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.ThreeMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.TwoHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + kline.IntervalCapacity{Interval: kline.SixHour}, + kline.IntervalCapacity{Interval: kline.TwelveHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, + kline.IntervalCapacity{Interval: kline.ThreeDay}, ), - ResultLimit: 1000, + GlobalResultLimit: 1000, }, }, } @@ -855,7 +855,7 @@ func (b *Bitstamp) ValidateCredentials(ctx context.Context, assetType asset.Item // GetHistoricCandles returns candles between a time period for a set time interval func (b *Bitstamp) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := b.GetKlineRequest(pair, a, interval, start, end) + req, err := b.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } @@ -865,7 +865,7 @@ func (b *Bitstamp) GetHistoricCandles(ctx context.Context, pair currency.Pair, a req.Start, req.End, b.FormatExchangeKlineInterval(req.ExchangeInterval), - strconv.FormatInt(int64(b.Features.Enabled.Kline.ResultLimit), 10)) + strconv.FormatInt(req.RequestLimit, 10)) if err != nil { return nil, err } @@ -903,7 +903,7 @@ func (b *Bitstamp) GetHistoricCandlesExtended(ctx context.Context, pair currency req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time, b.FormatExchangeKlineInterval(req.ExchangeInterval), - strconv.FormatInt(int64(b.Features.Enabled.Kline.ResultLimit), 10), + strconv.FormatInt(req.RequestLimit, 10), ) if err != nil { return nil, err diff --git a/exchanges/bittrex/bittrex_test.go b/exchanges/bittrex/bittrex_test.go index 05fe6bed..9494dc05 100644 --- a/exchanges/bittrex/bittrex_test.go +++ b/exchanges/bittrex/bittrex_test.go @@ -697,20 +697,43 @@ func TestGetHistoricTrades(t *testing.T) { } func TestGetHistoricCandles(t *testing.T) { + t.Parallel() pair, err := currency.NewPairFromString("btc-usdt") if err != nil { t.Fatal(err) } start := time.Unix(1546300800, 0) - end := time.Unix(1577836799, 0) + end := start.AddDate(0, 12, 0) _, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneDay, start, end) if err != nil { t.Fatal(err) } + + end = time.Now() + start = end.AddDate(0, -12, 0) + _, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneDay, start, end) + if err != nil { + t.Fatal(err) + } + + end = time.Now() + start = end.AddDate(0, 0, -30) + _, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneHour, start, end) + if err != nil { + t.Fatal(err) + } + + end = time.Now() + start = end.AddDate(0, 0, -1).Add(time.Minute * 5) + _, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.FiveMin, start, end) + if err != nil { + t.Fatal(err) + } } func TestGetHistoricCandlesExtended(t *testing.T) { + t.Parallel() pair, err := currency.NewPairFromString("btc-usdt") if err != nil { t.Fatal(err) diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index bbec58e5..ae1c76b3 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -28,6 +28,12 @@ import ( "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) +var ( + oneDay = time.Hour * 24 + oneMonth = oneDay * 31 + oneYear = oneDay * 366 +) + // GetDefaultConfig returns a default exchange config func (b *Bittrex) GetDefaultConfig(ctx context.Context) (*config.Exchange, error) { b.SetDefaults() @@ -111,12 +117,11 @@ func (b *Bittrex) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.FiveMin, - kline.OneHour, - kline.OneDay, + kline.IntervalCapacity{Interval: kline.OneMin, Capacity: 1440}, // 1m interval: candles for 1 day (0:00 - 23:59) + kline.IntervalCapacity{Interval: kline.FiveMin, Capacity: 288}, // 5m interval: candles for 1 day (0:00 - 23:55) + kline.IntervalCapacity{Interval: kline.OneHour, Capacity: 744}, // 1 hour interval: candles for 31 days (0:00 - 23:00) + kline.IntervalCapacity{Interval: kline.OneDay, Capacity: 366}, // 1 day interval: candles for 366 days ), - ResultLimit: 1000, }, }, } @@ -954,39 +959,36 @@ func (b *Bittrex) FormatExchangeKlineInterval(in kline.Interval) string { // This implementation rounds returns candles up to the next interval or to the end // time (whichever comes first) func (b *Bittrex) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := b.GetKlineRequest(pair, a, interval, start, end) + req, err := b.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } candleInterval := b.FormatExchangeKlineInterval(req.ExchangeInterval) if candleInterval == "notfound" { - return nil, errors.New("invalid interval") + return nil, fmt.Errorf("%w %v", kline.ErrInvalidInterval, interval) } - year, month, day := req.Start.Date() + year, month, day := req.End.Date() curYear, curMonth, curDay := time.Now().Date() - - getHistoric := false // nolint:ifshort,nolintlint // false positive and triggers only on Windows - getRecent := false // nolint:ifshort,nolintlint // false positive and triggers only on Windows - + var getHistoric, getRecent bool switch req.ExchangeInterval { case kline.OneMin, kline.FiveMin: - if time.Since(req.Start) > 24*time.Hour { + if time.Since(req.Start) > oneDay { getHistoric = true } if year >= curYear && month >= curMonth && day >= curDay { getRecent = true } case kline.OneHour: - if time.Since(req.Start) > 31*24*time.Hour { + if time.Since(req.Start) > oneMonth { getHistoric = true } if year >= curYear && month >= curMonth { getRecent = true } case kline.OneDay: - if time.Since(req.Start) > 366*24*time.Hour { + if time.Since(req.Start) > oneYear { getHistoric = true } if year >= curYear { @@ -994,46 +996,48 @@ func (b *Bittrex) GetHistoricCandles(ctx context.Context, pair currency.Pair, a } } - var ohlcData []CandleData + if !getHistoric && !getRecent { + return nil, errors.New("start end time range cannot get historic or recent candles") + } + + var candleData []CandleData if getHistoric { - var historicData []CandleData - historicData, err = b.GetHistoricalCandles(ctx, + historicData, err := b.GetHistoricalCandles(ctx, req.RequestFormatted.String(), - b.FormatExchangeKlineInterval(req.ExchangeInterval), + candleInterval, "TRADE", - year, - int(month), - day) + start.Year(), + int(start.Month()), + start.Day()) if err != nil { return nil, err } - ohlcData = append(ohlcData, historicData...) + candleData = append(candleData, historicData...) } + if getRecent { - var recentData []CandleData - recentData, err = b.GetRecentCandles(ctx, + recentData, err := b.GetRecentCandles(ctx, req.RequestFormatted.String(), - b.FormatExchangeKlineInterval(req.ExchangeInterval), + candleInterval, "TRADE") if err != nil { return nil, err } - ohlcData = append(ohlcData, recentData...) + candleData = append(candleData, recentData...) } - timeSeries := make([]kline.Candle, 0, len(ohlcData)) - for x := range ohlcData { - if ohlcData[x].StartsAt.Before(req.Start) || - ohlcData[x].StartsAt.After(req.End) { + timeSeries := make([]kline.Candle, 0, len(candleData)) + for x := range candleData { + if candleData[x].StartsAt.Before(req.Start) || candleData[x].StartsAt.After(req.End) { continue } timeSeries = append(timeSeries, kline.Candle{ - Time: ohlcData[x].StartsAt, - Open: ohlcData[x].Open, - High: ohlcData[x].High, - Low: ohlcData[x].Low, - Close: ohlcData[x].Close, - Volume: ohlcData[x].Volume, + Time: candleData[x].StartsAt, + Open: candleData[x].Open, + High: candleData[x].High, + Low: candleData[x].Low, + Close: candleData[x].Close, + Volume: candleData[x].Volume, }) } return req.ProcessResponse(timeSeries) diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index 214b754b..1dbbf474 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -116,21 +116,21 @@ func (b *BTCMarkets) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.ThreeMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.TwoHour, - kline.ThreeHour, - kline.FourHour, - kline.SixHour, - kline.OneDay, - kline.OneWeek, - kline.OneMonth, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.ThreeMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.TwoHour}, + kline.IntervalCapacity{Interval: kline.ThreeHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + kline.IntervalCapacity{Interval: kline.SixHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, + kline.IntervalCapacity{Interval: kline.OneWeek}, + kline.IntervalCapacity{Interval: kline.OneMonth}, ), - ResultLimit: 1000, + GlobalResultLimit: 1000, }, }, } @@ -1016,7 +1016,7 @@ func (b *BTCMarkets) FormatExchangeKlineInterval(in kline.Interval) string { // GetHistoricCandles returns candles between a time period for a set time interval func (b *BTCMarkets) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := b.GetKlineRequest(pair, a, interval, start, end) + req, err := b.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index 2805e594..ebab925e 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -130,15 +130,15 @@ func (b *BTSE) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.SixHour, - kline.OneDay, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.SixHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, ), - ResultLimit: 300, + GlobalResultLimit: 300, }, }, } @@ -972,7 +972,7 @@ func (b *BTSE) FormatExchangeKlineInterval(in kline.Interval) string { // GetHistoricCandles returns candles between a time period for a set time interval func (b *BTSE) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := b.GetKlineRequest(pair, a, interval, start, end) + req, err := b.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } @@ -988,7 +988,7 @@ func (b *BTSE) GetHistoricCandles(ctx context.Context, pair currency.Pair, a ass req, err := b.OHLCV(ctx, req.RequestFormatted.String(), req.Start, - req.End, + req.End.Add(-req.ExchangeInterval.Duration()), // End time is inclusive so we need to subtract the interval. intervalInt) if err != nil { return nil, err diff --git a/exchanges/bybit/bybit_wrapper.go b/exchanges/bybit/bybit_wrapper.go index 444486d4..2fb642c9 100644 --- a/exchanges/bybit/bybit_wrapper.go +++ b/exchanges/bybit/bybit_wrapper.go @@ -136,21 +136,21 @@ func (by *Bybit) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.ThreeMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.TwoHour, - kline.FourHour, - kline.SixHour, - kline.TwelveHour, - kline.OneDay, - kline.OneWeek, - kline.OneMonth, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.ThreeMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.TwoHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + kline.IntervalCapacity{Interval: kline.SixHour}, + kline.IntervalCapacity{Interval: kline.TwelveHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, + kline.IntervalCapacity{Interval: kline.OneWeek}, + kline.IntervalCapacity{Interval: kline.OneMonth}, ), - ResultLimit: 200, + GlobalResultLimit: 200, }, }, } @@ -1834,7 +1834,7 @@ func (by *Bybit) FormatExchangeKlineIntervalFutures(ctx context.Context, interva // GetHistoricCandles returns candles between a time period for a set time interval func (by *Bybit) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := by.GetKlineRequest(pair, a, interval, start, end) + req, err := by.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } @@ -1846,7 +1846,7 @@ func (by *Bybit) GetHistoricCandles(ctx context.Context, pair currency.Pair, a a candles, err = by.GetKlines(ctx, req.RequestFormatted.String(), by.FormatExchangeKlineInterval(ctx, req.ExchangeInterval), - int64(by.Features.Enabled.Kline.ResultLimit), + req.RequestLimit, req.Start, req.End) if err != nil { @@ -1869,7 +1869,7 @@ func (by *Bybit) GetHistoricCandles(ctx context.Context, pair currency.Pair, a a candles, err = by.GetFuturesKlineData(ctx, req.RequestFormatted, by.FormatExchangeKlineIntervalFutures(ctx, req.ExchangeInterval), - int64(by.Features.Enabled.Kline.ResultLimit), + req.RequestLimit, req.Start) if err != nil { return nil, err @@ -1891,7 +1891,7 @@ func (by *Bybit) GetHistoricCandles(ctx context.Context, pair currency.Pair, a a candles, err = by.GetUSDTFuturesKlineData(ctx, req.RequestFormatted, by.FormatExchangeKlineIntervalFutures(ctx, req.ExchangeInterval), - int64(by.Features.Enabled.Kline.ResultLimit), + req.RequestLimit, req.Start) if err != nil { return nil, err @@ -1914,7 +1914,7 @@ func (by *Bybit) GetHistoricCandles(ctx context.Context, pair currency.Pair, a a req.RequestFormatted, by.FormatExchangeKlineIntervalFutures(ctx, req.ExchangeInterval), req.Start, - int64(by.Features.Enabled.Kline.ResultLimit)) + req.RequestLimit) if err != nil { return nil, err } @@ -1951,7 +1951,7 @@ func (by *Bybit) GetHistoricCandlesExtended(ctx context.Context, pair currency.P candles, err = by.GetKlines(ctx, req.RequestFormatted.String(), by.FormatExchangeKlineInterval(ctx, req.ExchangeInterval), - int64(by.Features.Enabled.Kline.ResultLimit), + req.RequestLimit, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time) if err != nil { @@ -1973,7 +1973,7 @@ func (by *Bybit) GetHistoricCandlesExtended(ctx context.Context, pair currency.P candles, err = by.GetFuturesKlineData(ctx, req.RequestFormatted, by.FormatExchangeKlineIntervalFutures(ctx, req.ExchangeInterval), - int64(by.Features.Enabled.Kline.ResultLimit), + req.RequestLimit, req.RangeHolder.Ranges[x].Start.Time) if err != nil { return nil, err @@ -1994,7 +1994,7 @@ func (by *Bybit) GetHistoricCandlesExtended(ctx context.Context, pair currency.P candles, err = by.GetUSDTFuturesKlineData(ctx, req.RequestFormatted, by.FormatExchangeKlineIntervalFutures(ctx, req.ExchangeInterval), - int64(by.Features.Enabled.Kline.ResultLimit), + req.RequestLimit, req.RangeHolder.Ranges[x].Start.Time) if err != nil { return nil, err @@ -2016,7 +2016,7 @@ func (by *Bybit) GetHistoricCandlesExtended(ctx context.Context, pair currency.P req.RequestFormatted, by.FormatExchangeKlineIntervalFutures(ctx, req.ExchangeInterval), req.RangeHolder.Ranges[x].Start.Time, - int64(by.Features.Enabled.Kline.ResultLimit)) + req.RequestLimit) if err != nil { return nil, err } diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index 24350fc7..f21918d0 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -118,14 +118,14 @@ func (c *CoinbasePro) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.FiveMin, - kline.FifteenMin, - kline.OneHour, - kline.SixHour, - kline.OneDay, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.SixHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, ), - ResultLimit: 300, + GlobalResultLimit: 300, }, }, } @@ -892,7 +892,7 @@ func (c *CoinbasePro) GetOrderHistory(ctx context.Context, req *order.GetOrdersR // GetHistoricCandles returns a set of candle between two time periods for a // designated time period func (c *CoinbasePro) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := c.GetKlineRequest(pair, a, interval, start, end) + req, err := c.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } diff --git a/exchanges/exchange.go b/exchanges/exchange.go index c6abb6b8..2f6038bc 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -1500,7 +1500,7 @@ func (b *Base) IsPerpetualFutureCurrency(asset.Item, currency.Pair) (bool, error // GetKlineRequest returns a helper for the fetching of candle/kline data for // a single request within a pre-determined time window. -func (b *Base) GetKlineRequest(pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Request, error) { +func (b *Base) GetKlineRequest(pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time, fixedAPICandleLength bool) (*kline.Request, error) { if pair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } @@ -1516,17 +1516,6 @@ func (b *Base) GetKlineRequest(pair currency.Pair, a asset.Item, interval kline. return nil, err } - // NOTE: This check is here to make sure a client is notified that using - // this functionality will result in error if the total candles cannot be - // theoretically retrieved. - if count := kline.TotalCandlesPerInterval(start, end, exchangeInterval); count > - int64(b.Features.Enabled.Kline.ResultLimit) { - return nil, fmt.Errorf("candles count: %d, max limit: %d %w", - count, - b.Features.Enabled.Kline.ResultLimit, - kline.ErrRequestExceedsExchangeLimits) - } - err = b.ValidateKline(pair, a, exchangeInterval) if err != nil { return nil, err @@ -1537,7 +1526,49 @@ func (b *Base) GetKlineRequest(pair currency.Pair, a asset.Item, interval kline. return nil, err } - return kline.CreateKlineRequest(b.Name, pair, formatted, a, interval, exchangeInterval, start, end) + limit, err := b.Features.Enabled.Kline.GetIntervalResultLimit(exchangeInterval) + if err != nil { + return nil, err + } + + req, err := kline.CreateKlineRequest(b.Name, pair, formatted, a, interval, exchangeInterval, start, end, limit) + if err != nil { + return nil, err + } + + // NOTE: The checks below makes sure a client is notified that using this + // functionality will result in error if the total candles cannot be + // theoretically retrieved. + if fixedAPICandleLength { + origCount := kline.TotalCandlesPerInterval(req.Start, req.End, interval) + modifiedCount := kline.TotalCandlesPerInterval(req.Start, time.Now(), exchangeInterval) + if modifiedCount > limit { + errMsg := fmt.Sprintf("for %v %v candles between %v-%v. ", + origCount, + interval, + start.Format(common.SimpleTimeFormatWithTimezone), + end.Format(common.SimpleTimeFormatWithTimezone)) + if interval != exchangeInterval { + errMsg += fmt.Sprintf("Request converts to %v %v candles. ", + modifiedCount, + exchangeInterval) + } + boundary := time.Now().Add(-exchangeInterval.Duration() * time.Duration(limit)) + return nil, fmt.Errorf("%w %v, exceeding the limit of %v %v candles up to %v. Please reduce timeframe or use GetHistoricCandlesExtended", + kline.ErrRequestExceedsExchangeLimits, + errMsg, + limit, + exchangeInterval, + boundary.Format(common.SimpleTimeFormatWithTimezone)) + } + } else if count := kline.TotalCandlesPerInterval(req.Start, req.End, exchangeInterval); count > limit { + return nil, fmt.Errorf("candle count exceeded: %d. The endpoint has a set candle limit return of %d candles. Candle data will be incomplete: %w", + count, + limit, + kline.ErrRequestExceedsExchangeLimits) + } + + return req, nil } // GetKlineExtendedRequest returns a helper for the fetching of candle/kline @@ -1566,12 +1597,18 @@ func (b *Base) GetKlineExtendedRequest(pair currency.Pair, a asset.Item, interva return nil, err } - r, err := kline.CreateKlineRequest(b.Name, pair, formatted, a, interval, exchangeInterval, start, end) + limit, err := b.Features.Enabled.Kline.GetIntervalResultLimit(exchangeInterval) + if err != nil { + return nil, err + } + + r, err := kline.CreateKlineRequest(b.Name, pair, formatted, a, interval, exchangeInterval, start, end, limit) if err != nil { return nil, err } r.IsExtended = true - dates, err := r.GetRanges(b.Features.Enabled.Kline.ResultLimit) + + dates, err := r.GetRanges(uint32(limit)) if err != nil { return nil, err } diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index ba7aed67..9e367baa 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -1948,7 +1948,7 @@ func TestBase_ValidateKline(t *testing.T) { Features: Features{ Enabled: FeaturesEnabled{ Kline: kline.ExchangeCapabilitiesEnabled{ - Intervals: kline.DeployExchangeIntervals(kline.OneMin), + Intervals: kline.DeployExchangeIntervals(kline.IntervalCapacity{Interval: kline.OneMin}), }, }, }, @@ -2685,42 +2685,17 @@ func TestHasAssetTypeAccountSegregation(t *testing.T) { func TestGetKlineRequest(t *testing.T) { t.Parallel() b := Base{Name: "klineTest"} - _, err := b.GetKlineRequest(currency.EMPTYPAIR, asset.Empty, 0, time.Time{}, time.Time{}) + _, err := b.GetKlineRequest(currency.EMPTYPAIR, asset.Empty, 0, time.Time{}, time.Time{}, false) if !errors.Is(err, currency.ErrCurrencyPairEmpty) { t.Fatalf("received: '%v' but expected: '%v'", err, currency.ErrCurrencyPairEmpty) } pair := currency.NewPair(currency.BTC, currency.USDT) - _, err = b.GetKlineRequest(pair, asset.Empty, 0, time.Time{}, time.Time{}) + _, err = b.GetKlineRequest(pair, asset.Empty, 0, time.Time{}, time.Time{}, false) if !errors.Is(err, asset.ErrNotSupported) { t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported) } - _, err = b.GetKlineRequest(pair, asset.Spot, 0, time.Time{}, time.Time{}) - if !errors.Is(err, kline.ErrInvalidInterval) { - t.Fatalf("received: '%v' but expected: '%v'", err, kline.ErrInvalidInterval) - } - - _, err = b.GetKlineRequest(pair, asset.Spot, kline.OneMin, time.Time{}, time.Time{}) - if !errors.Is(err, kline.ErrCannotConstructInterval) { - t.Fatalf("received: '%v' but expected: '%v'", err, kline.ErrCannotConstructInterval) - } - - b.Features.Enabled.Kline.Intervals = kline.DeployExchangeIntervals(kline.OneMin) - b.Features.Enabled.Kline.ResultLimit = 1439 - start := time.Date(2020, 12, 1, 0, 0, 0, 0, time.UTC) - end := start.AddDate(0, 0, 1) - _, err = b.GetKlineRequest(pair, asset.Spot, kline.OneMin, start, end) - if !errors.Is(err, kline.ErrRequestExceedsExchangeLimits) { - t.Fatalf("received: '%v' but expected: '%v'", err, kline.ErrRequestExceedsExchangeLimits) - } - - b.Features.Enabled.Kline.Intervals = kline.DeployExchangeIntervals(kline.OneHour) - _, err = b.GetKlineRequest(pair, asset.Spot, kline.OneHour, start, end) - if !errors.Is(err, kline.ErrValidatingParams) { - t.Fatalf("received: '%v' but expected: '%v'", err, kline.ErrValidatingParams) - } - err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ AssetEnabled: convert.BoolPtr(true), Enabled: []currency.Pair{pair}, @@ -2730,7 +2705,19 @@ func TestGetKlineRequest(t *testing.T) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } - _, err = b.GetKlineRequest(pair, asset.Spot, kline.OneHour, start, end) + _, err = b.GetKlineRequest(pair, asset.Spot, 0, time.Time{}, time.Time{}, false) + if !errors.Is(err, kline.ErrInvalidInterval) { + t.Fatalf("received: '%v' but expected: '%v'", err, kline.ErrInvalidInterval) + } + + _, err = b.GetKlineRequest(pair, asset.Spot, kline.OneMin, time.Time{}, time.Time{}, false) + if !errors.Is(err, kline.ErrCannotConstructInterval) { + t.Fatalf("received: '%v' but expected: '%v'", err, kline.ErrCannotConstructInterval) + } + + b.Features.Enabled.Kline.Intervals = kline.DeployExchangeIntervals(kline.IntervalCapacity{Interval: kline.OneMin}) + b.Features.Enabled.Kline.GlobalResultLimit = 1439 + _, err = b.GetKlineRequest(pair, asset.Spot, kline.OneHour, time.Time{}, time.Time{}, false) if !errors.Is(err, errAssetRequestFormatIsNil) { t.Fatalf("received: '%v' but expected: '%v'", err, errAssetRequestFormatIsNil) } @@ -2745,7 +2732,65 @@ func TestGetKlineRequest(t *testing.T) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } - r, err := b.GetKlineRequest(pair, asset.Spot, kline.OneHour, start, end) + start := time.Date(2020, 12, 1, 0, 0, 0, 0, time.UTC) + end := start.AddDate(0, 0, 1) + _, err = b.GetKlineRequest(pair, asset.Spot, kline.OneMin, start, end, true) + if !errors.Is(err, kline.ErrRequestExceedsExchangeLimits) { + t.Fatalf("received: '%v' but expected: '%v'", err, kline.ErrRequestExceedsExchangeLimits) + } + + _, err = b.GetKlineRequest(pair, asset.Spot, kline.OneMin, start, end, false) + if !errors.Is(err, kline.ErrRequestExceedsExchangeLimits) { + t.Fatalf("received: '%v' but expected: '%v'", err, kline.ErrRequestExceedsExchangeLimits) + } + + _, err = b.GetKlineRequest(pair, asset.Futures, kline.OneHour, start, end, false) + if !errors.Is(err, kline.ErrValidatingParams) { + t.Fatalf("received: '%v' but expected: '%v'", err, kline.ErrValidatingParams) + } + + b.Features.Enabled.Kline.Intervals = kline.DeployExchangeIntervals(kline.IntervalCapacity{Interval: kline.OneHour}) + r, err := b.GetKlineRequest(pair, asset.Spot, kline.OneHour, start, end, false) + if !errors.Is(err, nil) { + t.Fatalf("received: '%v' but expected: '%v'", err, nil) + } + + if r.Exchange != "klineTest" { + t.Fatalf("received: '%v' but expected: '%v'", r.Exchange, "klineTest") + } + + if !r.Pair.Equal(pair) { + t.Fatalf("received: '%v' but expected: '%v'", r.Pair, pair) + } + + if r.Asset != asset.Spot { + t.Fatalf("received: '%v' but expected: '%v'", r.Asset, asset.Spot) + } + + if r.ExchangeInterval != kline.OneHour { + t.Fatalf("received: '%v' but expected: '%v'", r.ExchangeInterval, kline.OneHour) + } + + if r.ClientRequired != kline.OneHour { + t.Fatalf("received: '%v' but expected: '%v'", r.ClientRequired, kline.OneHour) + } + + if r.Start != start { + t.Fatalf("received: '%v' but expected: '%v'", r.Start, start) + } + + if r.End != end { + t.Fatalf("received: '%v' but expected: '%v'", r.End, end) + } + + if r.RequestFormatted.String() != "BTCUSDT" { + t.Fatalf("received: '%v' but expected: '%v'", r.RequestFormatted.String(), "BTCUSDT") + } + + end = time.Now().Truncate(kline.OneHour.Duration()).UTC() + start = end.Add(-kline.OneHour.Duration() * 1439) + + r, err = b.GetKlineRequest(pair, asset.Spot, kline.OneHour, start, end, true) if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } @@ -2807,8 +2852,8 @@ func TestGetKlineExtendedRequest(t *testing.T) { t.Fatalf("received: '%v' but expected: '%v'", err, kline.ErrCannotConstructInterval) } - b.Features.Enabled.Kline.Intervals = kline.DeployExchangeIntervals(kline.OneMin) - b.Features.Enabled.Kline.ResultLimit = 100 + b.Features.Enabled.Kline.Intervals = kline.DeployExchangeIntervals(kline.IntervalCapacity{Interval: kline.OneMin}) + b.Features.Enabled.Kline.GlobalResultLimit = 100 start := time.Date(2020, 12, 1, 0, 0, 0, 0, time.UTC) end := start.AddDate(0, 0, 1) diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 7ae041f9..cb23245a 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -115,19 +115,19 @@ func (g *Gateio) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.ThreeMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.TwoHour, - kline.FourHour, - kline.SixHour, - kline.TwelveHour, - kline.OneDay, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.ThreeMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.TwoHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + kline.IntervalCapacity{Interval: kline.SixHour}, + kline.IntervalCapacity{Interval: kline.TwelveHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, ), - ResultLimit: 1001, + GlobalResultLimit: 1001, }, }, } @@ -902,7 +902,7 @@ func (g *Gateio) FormatExchangeKlineInterval(in kline.Interval) string { // GetHistoricCandles returns candles between a time period for a set time interval func (g *Gateio) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := g.GetKlineRequest(pair, a, interval, start, end) + req, err := g.GetKlineRequest(pair, a, interval, start, end, true) if err != nil { return nil, err } diff --git a/exchanges/hitbtc/hitbtc.go b/exchanges/hitbtc/hitbtc.go index 37af72da..2d06d830 100644 --- a/exchanges/hitbtc/hitbtc.go +++ b/exchanges/hitbtc/hitbtc.go @@ -206,7 +206,7 @@ func (h *HitBTC) GetCandles(ctx context.Context, currencyPair, limit, period str } var resp []ChartData - path := fmt.Sprintf("/%s/%s?%s", apiV2Candles, currencyPair, vals.Encode()) + path := "/" + apiV2Candles + "/" + currencyPair + "?" + vals.Encode() return resp, h.SendHTTPRequest(ctx, exchange.RestSpot, path, &resp) } diff --git a/exchanges/hitbtc/hitbtc_test.go b/exchanges/hitbtc/hitbtc_test.go index d51004bd..b735a1af 100644 --- a/exchanges/hitbtc/hitbtc_test.go +++ b/exchanges/hitbtc/hitbtc_test.go @@ -116,7 +116,6 @@ func TestGetHistoricCandlesExtended(t *testing.T) { } startTime := time.Unix(1546300800, 0) end := time.Unix(1577836799, 0) - _, err = h.GetHistoricCandlesExtended(context.Background(), pair, asset.Spot, kline.OneHour, startTime, end) if err != nil { t.Fatal(err) @@ -979,6 +978,7 @@ func TestWsTrades(t *testing.T) { } func Test_FormatExchangeKlineInterval(t *testing.T) { + t.Parallel() testCases := []struct { name string interval kline.Interval @@ -1000,18 +1000,20 @@ func Test_FormatExchangeKlineInterval(t *testing.T) { "D7", }, { - "AllOther", + "OneMonth", kline.OneMonth, - "", + "1M", }, } for x := range testCases { test := testCases[x] - t.Run(test.name, func(t *testing.T) { - ret := h.FormatExchangeKlineInterval(test.interval) - + t.Parallel() + ret, err := h.FormatExchangeKlineInterval(test.interval) + if err != nil { + t.Fatal(err) + } if ret != test.output { t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret) } diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index b6691268..bae3e726 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -116,18 +116,18 @@ func (h *HitBTC) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.ThreeMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.FourHour, - kline.OneDay, - kline.SevenDay, - kline.OneMonth, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.ThreeMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, + kline.IntervalCapacity{Interval: kline.SevenDay}, + kline.IntervalCapacity{Interval: kline.OneMonth}, ), - ResultLimit: 1000, + GlobalResultLimit: 1000, }, }, } @@ -850,30 +850,48 @@ func (h *HitBTC) ValidateCredentials(ctx context.Context, assetType asset.Item) } // FormatExchangeKlineInterval returns Interval to exchange formatted string -func (h *HitBTC) FormatExchangeKlineInterval(in kline.Interval) string { +func (h *HitBTC) FormatExchangeKlineInterval(in kline.Interval) (string, error) { switch in { - case kline.OneMin, kline.ThreeMin, - kline.FiveMin, kline.FifteenMin, kline.ThirtyMin: - return "M" + in.Short()[:len(in.Short())-1] + case kline.OneMin: + return "M1", nil + case kline.ThreeMin: + return "M3", nil + case kline.FiveMin: + return "M5", nil + case kline.FifteenMin: + return "M15", nil + case kline.ThirtyMin: + return "M30", nil + case kline.OneHour: + return "H1", nil + case kline.FourHour: + return "H4", nil case kline.OneDay: - return "D1" - case kline.SevenDay: - return "D7" + return "D1", nil + case kline.OneWeek: + return "D7", nil + case kline.OneMonth: + return "1M", nil } - return "" + return "", fmt.Errorf("%w %v", kline.ErrInvalidInterval, in) } // GetHistoricCandles returns candles between a time period for a set time interval func (h *HitBTC) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := h.GetKlineRequest(pair, a, interval, start, end) + req, err := h.GetKlineRequest(pair, a, interval, start, end, false) + if err != nil { + return nil, err + } + + formattedInterval, err := h.FormatExchangeKlineInterval(req.ExchangeInterval) if err != nil { return nil, err } data, err := h.GetCandles(ctx, req.RequestFormatted.String(), - strconv.FormatInt(int64(h.Features.Enabled.Kline.ResultLimit), 10), - h.FormatExchangeKlineInterval(req.ExchangeInterval), + strconv.FormatInt(req.RequestLimit, 10), + formattedInterval, req.Start, req.End) if err != nil { @@ -901,13 +919,18 @@ func (h *HitBTC) GetHistoricCandlesExtended(ctx context.Context, pair currency.P return nil, err } + formattedInterval, err := h.FormatExchangeKlineInterval(req.ExchangeInterval) + if err != nil { + return nil, err + } + timeSeries := make([]kline.Candle, 0, req.Size()) for y := range req.RangeHolder.Ranges { var data []ChartData data, err = h.GetCandles(ctx, req.RequestFormatted.String(), - strconv.FormatInt(int64(h.Features.Enabled.Kline.ResultLimit), 10), - h.FormatExchangeKlineInterval(req.ExchangeInterval), + strconv.FormatInt(req.RequestLimit, 10), + formattedInterval, req.RangeHolder.Ranges[y].Start.Time, req.RangeHolder.Ranges[y].End.Time) if err != nil { diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index f6f9b44f..6e608822 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -145,18 +145,23 @@ func (h *HUOBI) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.FourHour, - kline.OneDay, - kline.OneWeek, - kline.OneMonth, - kline.OneYear, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + kline.IntervalCapacity{Interval: kline.OneYear}, + // NOTE: The supported time intervals below are returned + // offset to the Asia/Shanghai time zone. This may lead to + // issues with candle quality and conversion as the + // intervals may be broken up. Therefore the below intervals + // are constructed from hourly candles. + // kline.IntervalCapacity{Interval: kline.OneDay}, + // kline.IntervalCapacity{Interval: kline.OneWeek}, + // kline.IntervalCapacity{Interval: kline.OneMonth}, ), - ResultLimit: 2000, + GlobalResultLimit: 2000, }, }, } @@ -1745,7 +1750,7 @@ func (h *HUOBI) FormatExchangeKlineInterval(in kline.Interval) string { // GetHistoricCandles returns candles between a time period for a set time interval func (h *HUOBI) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := h.GetKlineRequest(pair, a, interval, start, end) + req, err := h.GetKlineRequest(pair, a, interval, start, end, true) if err != nil { return nil, err } @@ -1758,6 +1763,7 @@ func (h *HUOBI) GetHistoricCandles(ctx context.Context, pair currency.Pair, a as candles, err := h.GetSpotKline(ctx, KlinesRequestParams{ Period: h.FormatExchangeKlineInterval(req.ExchangeInterval), Symbol: req.Pair, + Size: int(req.RequestLimit), }) if err != nil { return nil, err diff --git a/exchanges/kline/kline.go b/exchanges/kline/kline.go index 43147ff3..0e49f878 100644 --- a/exchanges/kline/kline.go +++ b/exchanges/kline/kline.go @@ -163,17 +163,21 @@ func (k *Item) addPadding(start, exclusiveEnd time.Time, purgeOnPartial bool) er return errCannotEstablishTimeWindow } - segments := int(window / k.Interval.Duration()) - if segments == len(k.Candles) { - return nil - } - - padded := make([]Candle, segments) + padded := make([]Candle, int(window/k.Interval.Duration())) var target int for x := range padded { - if target >= len(k.Candles) || !k.Candles[target].Time.Equal(start) { + switch { + case target >= len(k.Candles): padded[x].Time = start - } else { + case !k.Candles[target].Time.Equal(start): + if k.Candles[target].Time.Before(start) { + return fmt.Errorf("%w when it should be %s truncated at a %s interval", + errCandleOpenTimeIsNotUTCAligned, + start.Add(k.Interval.Duration()), + k.Interval) + } + padded[x].Time = start + default: padded[x] = k.Candles[target] target++ } @@ -183,8 +187,15 @@ func (k *Item) addPadding(start, exclusiveEnd time.Time, purgeOnPartial bool) er // NOTE: This checks if the end time exceeds time.Now() and we are capturing // a partially created candle. This will only delete an element if it is // empty. - if purgeOnPartial && padded[len(padded)-1].Volume == 0 { - padded = padded[:len(padded)-1] + if purgeOnPartial { + lastElement := padded[len(padded)-1] + if lastElement.Volume == 0 && + lastElement.Open == 0 && + lastElement.High == 0 && + lastElement.Low == 0 && + lastElement.Close == 0 { + padded = padded[:len(padded)-1] + } } k.Candles = padded return nil @@ -349,29 +360,38 @@ func (k *Item) ConvertToNewInterval(newInterval Interval) (*Item, error) { if len(candles) == 0 { return nil, fmt.Errorf("%w to %v no candle data", ErrInsufficientCandleData, newInterval) } + var target int for x := range k.Candles { - if candles[target].Time.IsZero() { - candles[target].Time = k.Candles[x].Time - } + // If this check does not pass, this candle has zero values or is padding. + // It has nothing to apply to the new interval candle as it will distort + // candle data. + if k.Candles[x].Open != 0 && + k.Candles[x].High != 0 && + k.Candles[x].Low != 0 && + k.Candles[x].Close != 0 && + k.Candles[x].Volume != 0 { + if candles[target].Time.IsZero() { + candles[target].Time = k.Candles[x].Time + } - if candles[target].Open == 0 { - candles[target].Open = k.Candles[x].Open - } + if candles[target].Open == 0 { + candles[target].Open = k.Candles[x].Open + } - if k.Candles[x].High > candles[target].High { - candles[target].High = k.Candles[x].High - } + if k.Candles[x].High > candles[target].High { + candles[target].High = k.Candles[x].High + } - if candles[target].Low == 0 || k.Candles[x].Low < candles[target].Low { - candles[target].Low = k.Candles[x].Low - } + if candles[target].Low == 0 || k.Candles[x].Low < candles[target].Low { + candles[target].Low = k.Candles[x].Low + } - candles[target].Volume += k.Candles[x].Volume + candles[target].Volume += k.Candles[x].Volume + candles[target].Close = k.Candles[x].Close + } if (x+1)%oldIntervalsPerNewCandle == 0 { - candles[target].Close = k.Candles[x].Close - target++ // Note: Below checks the length of the proceeding slice so we can // break instantly if we cannot make an entire candle. e.g. 60 min // candles in an hour candle and we have 59 minute candles left. @@ -379,6 +399,7 @@ func (k *Item) ConvertToNewInterval(newInterval Interval) (*Item, error) { if len(k.Candles[x:])-1 < oldIntervalsPerNewCandle { break } + target++ } } return &Item{ @@ -582,12 +603,12 @@ func (k *Item) EqualSource(i *Item) error { // DeployExchangeIntervals aligns and stores supported intervals for an exchange // for future matching. -func DeployExchangeIntervals(enabled ...Interval) ExchangeIntervals { - sort.Slice(enabled, func(i, j int) bool { return enabled[i] < enabled[j] }) +func DeployExchangeIntervals(enabled ...IntervalCapacity) ExchangeIntervals { + sort.Slice(enabled, func(i, j int) bool { return enabled[i].Interval < enabled[j].Interval }) - supported := make(map[Interval]bool) + supported := make(map[Interval]int64) for x := range enabled { - supported[enabled[x]] = true + supported[enabled[x].Interval] = enabled[x].Capacity } return ExchangeIntervals{supported: supported, aligned: enabled} } @@ -596,7 +617,8 @@ func DeployExchangeIntervals(enabled ...Interval) ExchangeIntervals { // future this might be able to be deprecated because we can construct custom // intervals from the supported list. func (e *ExchangeIntervals) ExchangeSupported(in Interval) bool { - return e.supported[in] + _, ok := e.supported[in] + return ok } // Construct fetches supported interval that can construct the required interval @@ -606,17 +628,41 @@ func (e *ExchangeIntervals) Construct(required Interval) (Interval, error) { return 0, ErrInvalidInterval } - if e.supported[required] { + if _, ok := e.supported[required]; ok { // Directly supported by exchange can return. return required, nil } for x := len(e.aligned) - 1; x > -1; x-- { - if e.aligned[x] < required && required%e.aligned[x] == 0 { + if e.aligned[x].Interval < required && required%e.aligned[x].Interval == 0 { // Indirectly supported by exchange. Can generate required candle // from this lower time frame supported candle. - return e.aligned[x], nil + return e.aligned[x].Interval, nil } } return 0, ErrCannotConstructInterval } + +// GetIntervalResultLimit returns the maximum amount of candles that can be +// returned for a specific interval. If the individual interval limit is not set, +// it will be ignored and the global result limit will be returned. +func (e *ExchangeCapabilitiesEnabled) GetIntervalResultLimit(interval Interval) (int64, error) { + if e == nil { + return 0, errExchangeCapabilitiesEnabledIsNil + } + + val, ok := e.Intervals.supported[interval] + if !ok { + return 0, fmt.Errorf("[%s] %w", interval, errIntervalNotSupported) + } + + if val > 0 { + return val, nil + } + + if e.GlobalResultLimit == 0 { + return 0, fmt.Errorf("%w there is no global result limit set", errCannotFetchIntervalLimit) + } + + return int64(e.GlobalResultLimit), nil +} diff --git a/exchanges/kline/kline_test.go b/exchanges/kline/kline_test.go index fa64ca71..0c55c354 100644 --- a/exchanges/kline/kline_test.go +++ b/exchanges/kline/kline_test.go @@ -1033,6 +1033,18 @@ func TestConvertToNewInterval(t *testing.T) { Close: 5555, Volume: 2520, }, + { + Time: tn.AddDate(0, 0, 6), + // Empty end padding + }, + { + Time: tn.AddDate(0, 0, 7), + // Empty end padding + }, + { + Time: tn.AddDate(0, 0, 8), + // Empty end padding + }, } _, err = old.ConvertToNewInterval(newInterval) @@ -1040,7 +1052,7 @@ func TestConvertToNewInterval(t *testing.T) { t.Errorf("received '%v' expected '%v'", err, errCandleDataNotPadded) } - err = old.addPadding(tn, tn.AddDate(0, 0, 6), false) + err = old.addPadding(tn, tn.AddDate(0, 0, 9), false) if !errors.Is(err, nil) { t.Errorf("received '%v' expected '%v'", err, nil) } @@ -1050,8 +1062,8 @@ func TestConvertToNewInterval(t *testing.T) { t.Errorf("received '%v' expected '%v'", err, nil) } - if len(newCandle.Candles) != 2 { - t.Errorf("received '%v' expected '%v'", len(newCandle.Candles), 2) + if len(newCandle.Candles) != 3 { + t.Errorf("received '%v' expected '%v'", len(newCandle.Candles), 3) } } @@ -1106,6 +1118,37 @@ func TestAddPadding(t *testing.T) { t.Fatalf("received '%v' expected '%v'", err, errCannotEstablishTimeWindow) } + k.Candles = []Candle{ + { + Time: tn.Add(time.Hour * 8), + Open: 1337, + High: 1339, + Low: 1336, + Close: 1338, + Volume: 1337, + }, + { + Time: tn.AddDate(0, 0, 1).Add(time.Hour * 8), + Open: 1338, + High: 2000, + Low: 1332, + Close: 1696, + Volume: 6420, + }, + { + Time: tn.AddDate(0, 0, 2).Add(time.Hour * 8), + Open: 1696, + High: 1998, + Low: 1337, + Close: 6969, + Volume: 2520, + }} + + err = k.addPadding(tn, tn.AddDate(0, 0, 3), false) + if !errors.Is(err, errCandleOpenTimeIsNotUTCAligned) { + t.Fatalf("received '%v' expected '%v'", err, errCandleOpenTimeIsNotUTCAligned) + } + k.Candles = []Candle{ { Time: tn, @@ -1207,7 +1250,7 @@ func TestDeployExchangeIntervals(t *testing.T) { t.Errorf("received '%v' expected '%v'", exchangeIntervals.ExchangeSupported(OneWeek), false) } - exchangeIntervals = DeployExchangeIntervals(OneWeek) + exchangeIntervals = DeployExchangeIntervals(IntervalCapacity{Interval: OneWeek}) if !exchangeIntervals.ExchangeSupported(OneWeek) { t.Errorf("received '%v' expected '%v'", exchangeIntervals.ExchangeSupported(OneWeek), true) } @@ -1231,7 +1274,7 @@ func TestDeployExchangeIntervals(t *testing.T) { t.Errorf("received '%v' expected '%v'", request, OneWeek) } - exchangeIntervals = DeployExchangeIntervals(OneWeek, OneDay) + exchangeIntervals = DeployExchangeIntervals(IntervalCapacity{Interval: OneWeek}, IntervalCapacity{Interval: OneDay}) request, err = exchangeIntervals.Construct(OneMonth) if !errors.Is(err, nil) { @@ -1286,3 +1329,51 @@ func TestSetHasDataFromCandles(t *testing.T) { t.Errorf("received '%v' expected '%v'", true, false) } } + +func TestGetIntervalResultLimit(t *testing.T) { + t.Parallel() + + var e *ExchangeCapabilitiesEnabled + _, err := e.GetIntervalResultLimit(OneMin) + if !errors.Is(err, errExchangeCapabilitiesEnabledIsNil) { + t.Errorf("received '%v' expected '%v'", err, errExchangeCapabilitiesEnabledIsNil) + } + + e = &ExchangeCapabilitiesEnabled{} + e.Intervals = ExchangeIntervals{} + _, err = e.GetIntervalResultLimit(OneDay) + if !errors.Is(err, errIntervalNotSupported) { + t.Errorf("received '%v' expected '%v'", err, errIntervalNotSupported) + } + + e.Intervals = ExchangeIntervals{ + supported: map[Interval]int64{ + OneDay: 100000, + OneMin: 0, + }, + } + + _, err = e.GetIntervalResultLimit(OneMin) + if !errors.Is(err, errCannotFetchIntervalLimit) { + t.Errorf("received '%v' expected '%v'", err, errCannotFetchIntervalLimit) + } + + limit, err := e.GetIntervalResultLimit(OneDay) + if !errors.Is(err, nil) { + t.Errorf("received '%v' expected '%v'", err, nil) + } + + if limit != 100000 { + t.Errorf("received '%v' expected '%v'", limit, 100000) + } + + e.GlobalResultLimit = 1337 + limit, err = e.GetIntervalResultLimit(OneMin) + if !errors.Is(err, nil) { + t.Errorf("received '%v' expected '%v'", err, nil) + } + + if limit != 1337 { + t.Errorf("received '%v' expected '%v'", limit, 1337) + } +} diff --git a/exchanges/kline/kline_types.go b/exchanges/kline/kline_types.go index 9b035fa1..143f01b3 100644 --- a/exchanges/kline/kline_types.go +++ b/exchanges/kline/kline_types.go @@ -71,10 +71,14 @@ var ( // back further than what is allowed. ErrRequestExceedsMaxLookback = errors.New("the requested time window exceeds the maximum lookback period available in the historical data, please reduce window between start and end date of your request") - errInsufficientTradeData = errors.New("insufficient trade data") - errCandleDataNotPadded = errors.New("candle data not padded") - errCannotEstablishTimeWindow = errors.New("cannot establish time window") - errNilKline = errors.New("kline item is nil") + errInsufficientTradeData = errors.New("insufficient trade data") + errCandleDataNotPadded = errors.New("candle data not padded") + errCannotEstablishTimeWindow = errors.New("cannot establish time window") + errNilKline = errors.New("kline item is nil") + errExchangeCapabilitiesEnabledIsNil = errors.New("exchange capabilities enabled is nil") + errCannotFetchIntervalLimit = errors.New("cannot fetch interval limit") + errIntervalNotSupported = errors.New("interval not supported") + errCandleOpenTimeIsNotUTCAligned = errors.New("candle open time is not UTC aligned") oneYearDurationInNano = float64(OneYear.Duration().Nanoseconds()) @@ -138,15 +142,20 @@ type ExchangeCapabilitiesSupported struct { // ExchangeCapabilitiesEnabled all kline related exchange enabled options type ExchangeCapabilitiesEnabled struct { - Intervals ExchangeIntervals - ResultLimit uint32 + // Intervals defines whether the exchange supports interval kline requests. + Intervals ExchangeIntervals + // GlobalResultLimit is the maximum amount of candles that can be returned + // across all intervals. This is used to determine if a request will exceed + // the exchange limits. Indivudal interval limits are stored in the + // ExchangeIntervals struct. If this is set to 0, it will be ignored. + GlobalResultLimit uint32 } // ExchangeIntervals stores the supported intervals in an optimized lookup table // with a supplementary aligned retrieval list type ExchangeIntervals struct { - supported map[Interval]bool - aligned []Interval + supported map[Interval]int64 + aligned []IntervalCapacity } // Interval type for kline Interval usage @@ -183,3 +192,9 @@ type IntervalTime struct { Time time.Time Ticks int64 } + +// IntervalCapacity is used to store the interval and capacity for a candle return +type IntervalCapacity struct { + Interval Interval + Capacity int64 +} diff --git a/exchanges/kline/request.go b/exchanges/kline/request.go index 2efa3a16..d4fcc872 100644 --- a/exchanges/kline/request.go +++ b/exchanges/kline/request.go @@ -13,9 +13,10 @@ import ( var ( // ErrUnsetName is an error for when the exchange name is not set - ErrUnsetName = errors.New("unset exchange name") - errNilRequest = errors.New("nil kline request") - errNoTimeSeriesDataToConvert = errors.New("no time series data to convert") + ErrUnsetName = errors.New("unset exchange name") + errNilRequest = errors.New("nil kline request") + errNoTimeSeriesDataToConvert = errors.New("no time series data to convert") + errInvalidSpecificEndpointLimit = errors.New("specific endpoint limit must be greater than 0") // PartialCandle is string flag for when the most recent candle is partially // formed. @@ -53,11 +54,14 @@ type Request struct { // ProcessedCandles stores the candles that have been processed, but not converted // to the ClientRequiredInterval ProcessedCandles []Candle + // RequestLimit is the potential maximum amount of candles that can be + // returned + RequestLimit int64 } // CreateKlineRequest generates a `Request` type for interval conversions // supported by an exchange. -func CreateKlineRequest(name string, pair, formatted currency.Pair, a asset.Item, clientRequired, exchangeInterval Interval, start, end time.Time) (*Request, error) { +func CreateKlineRequest(name string, pair, formatted currency.Pair, a asset.Item, clientRequired, exchangeInterval Interval, start, end time.Time, specificEndpointLimit int64) (*Request, error) { if name == "" { return nil, ErrUnsetName } @@ -81,6 +85,10 @@ func CreateKlineRequest(name string, pair, formatted currency.Pair, a asset.Item return nil, err } + if specificEndpointLimit <= 0 { + return nil, errInvalidSpecificEndpointLimit + } + // Force UTC alignment start = start.UTC() end = end.UTC() @@ -116,6 +124,7 @@ func CreateKlineRequest(name string, pair, formatted currency.Pair, a asset.Item Start: start, End: end, PartialCandle: end.After(time.Now()), + RequestLimit: specificEndpointLimit, }, nil } diff --git a/exchanges/kline/request_test.go b/exchanges/kline/request_test.go index bbab3929..56968aa8 100644 --- a/exchanges/kline/request_test.go +++ b/exchanges/kline/request_test.go @@ -13,51 +13,56 @@ import ( func TestCreateKlineRequest(t *testing.T) { t.Parallel() - _, err := CreateKlineRequest("", currency.EMPTYPAIR, currency.EMPTYPAIR, 0, 0, 0, time.Time{}, time.Time{}) + _, err := CreateKlineRequest("", currency.EMPTYPAIR, currency.EMPTYPAIR, 0, 0, 0, time.Time{}, time.Time{}, 0) if !errors.Is(err, ErrUnsetName) { t.Fatalf("received: '%v', but expected '%v'", err, ErrUnsetName) } - _, err = CreateKlineRequest("name", currency.EMPTYPAIR, currency.EMPTYPAIR, 0, 0, 0, time.Time{}, time.Time{}) + _, err = CreateKlineRequest("name", currency.EMPTYPAIR, currency.EMPTYPAIR, 0, 0, 0, time.Time{}, time.Time{}, 0) if !errors.Is(err, currency.ErrCurrencyPairEmpty) { t.Fatalf("received: '%v', but expected '%v'", err, currency.ErrCurrencyPairEmpty) } pair := currency.NewPair(currency.BTC, currency.USDT) - _, err = CreateKlineRequest("name", pair, currency.EMPTYPAIR, 0, 0, 0, time.Time{}, time.Time{}) + _, err = CreateKlineRequest("name", pair, currency.EMPTYPAIR, 0, 0, 0, time.Time{}, time.Time{}, 0) if !errors.Is(err, currency.ErrCurrencyPairEmpty) { t.Fatalf("received: '%v', but expected '%v'", err, currency.ErrCurrencyPairEmpty) } pair2 := pair.Upper() - _, err = CreateKlineRequest("name", pair, pair2, 0, 0, 0, time.Time{}, time.Time{}) + _, err = CreateKlineRequest("name", pair, pair2, 0, 0, 0, time.Time{}, time.Time{}, 0) if !errors.Is(err, asset.ErrNotSupported) { t.Fatalf("received: '%v', but expected '%v'", err, asset.ErrNotSupported) } - _, err = CreateKlineRequest("name", pair, pair2, asset.Spot, 0, 0, time.Time{}, time.Time{}) + _, err = CreateKlineRequest("name", pair, pair2, asset.Spot, 0, 0, time.Time{}, time.Time{}, 0) if !errors.Is(err, ErrInvalidInterval) { t.Fatalf("received: '%v', but expected '%v'", err, ErrInvalidInterval) } - _, err = CreateKlineRequest("name", pair, pair2, asset.Spot, OneHour, 0, time.Time{}, time.Time{}) + _, err = CreateKlineRequest("name", pair, pair2, asset.Spot, OneHour, 0, time.Time{}, time.Time{}, 0) if !errors.Is(err, ErrInvalidInterval) { t.Fatalf("received: '%v', but expected '%v'", err, ErrInvalidInterval) } - _, err = CreateKlineRequest("name", pair, pair2, asset.Spot, OneHour, OneMin, time.Time{}, time.Time{}) + _, err = CreateKlineRequest("name", pair, pair2, asset.Spot, OneHour, OneMin, time.Time{}, time.Time{}, 0) if !errors.Is(err, common.ErrDateUnset) { t.Fatalf("received: '%v', but expected '%v'", err, common.ErrDateUnset) } start := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) - _, err = CreateKlineRequest("name", pair, pair2, asset.Spot, OneHour, OneMin, start, time.Time{}) + _, err = CreateKlineRequest("name", pair, pair2, asset.Spot, OneHour, OneMin, start, time.Time{}, 0) if !errors.Is(err, common.ErrDateUnset) { t.Fatalf("received: '%v', but expected '%v'", err, common.ErrDateUnset) } end := start.AddDate(0, 0, 1) - r, err := CreateKlineRequest("name", pair, pair2, asset.Spot, OneHour, OneMin, start, end) + _, err = CreateKlineRequest("name", pair, pair2, asset.Spot, OneHour, OneMin, start, end, 0) + if !errors.Is(err, errInvalidSpecificEndpointLimit) { + t.Fatalf("received: '%v', but expected '%v'", err, errInvalidSpecificEndpointLimit) + } + + r, err := CreateKlineRequest("name", pair, pair2, asset.Spot, OneHour, OneMin, start, end, 1) if !errors.Is(err, nil) { t.Fatalf("received: '%v', but expected '%v'", err, nil) } @@ -98,7 +103,7 @@ func TestCreateKlineRequest(t *testing.T) { // aligned correctly. end = end.Round(0) end = end.Add(time.Second * 30) - r, err = CreateKlineRequest("name", pair, pair2, asset.Spot, OneHour, OneMin, start, end) + r, err = CreateKlineRequest("name", pair, pair2, asset.Spot, OneHour, OneMin, start, end, 1) if !errors.Is(err, nil) { t.Fatalf("received: '%v', but expected '%v'", err, nil) } @@ -121,7 +126,7 @@ func TestGetRanges(t *testing.T) { t.Fatalf("received: '%v', but expected '%v'", err, errNilRequest) } - r, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneHour, OneMin, start, end) + r, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneHour, OneMin, start, end, 1) if !errors.Is(err, nil) { t.Fatalf("received: '%v', but expected '%v'", err, nil) } @@ -207,8 +212,13 @@ func TestRequest_ProcessResponse(t *testing.T) { t.Fatalf("received: '%v', but expected '%v'", err, errNoTimeSeriesDataToConvert) } + _, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneHour, OneHour, start, end, 0) + if !errors.Is(err, errInvalidSpecificEndpointLimit) { + t.Fatalf("received: '%v', but expected '%v'", err, nil) + } + // no conversion - r, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneHour, OneHour, start, end) + r, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneHour, OneHour, start, end, 1) if !errors.Is(err, nil) { t.Fatalf("received: '%v', but expected '%v'", err, nil) } @@ -223,7 +233,7 @@ func TestRequest_ProcessResponse(t *testing.T) { } // with conversion - r, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneHour, OneMin, start, end) + r, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneHour, OneMin, start, end, 1) if !errors.Is(err, nil) { t.Fatalf("received: '%v', but expected '%v'", err, nil) } @@ -240,7 +250,7 @@ func TestRequest_ProcessResponse(t *testing.T) { // Potential partial candle end = time.Now().UTC() start = end.AddDate(0, 0, -5).Truncate(time.Duration(OneDay)) - r, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneDay, OneDay, start, end) + r, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneDay, OneDay, start, end, 1) if !errors.Is(err, nil) { t.Fatalf("received: '%v', but expected '%v'", err, nil) } @@ -285,7 +295,7 @@ func TestRequest_ProcessResponse(t *testing.T) { } // end date far into the dark depths of future reality - r, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneDay, OneDay, start, end.AddDate(1, 0, 0)) + r, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneDay, OneDay, start, end.AddDate(1, 0, 0), 1) if !errors.Is(err, nil) { t.Fatalf("received: '%v', but expected '%v'", err, nil) } @@ -338,7 +348,7 @@ func TestExtendedRequest_ProcessResponse(t *testing.T) { } // no conversion - r, err := CreateKlineRequest("name", pair, pair, asset.Spot, OneHour, OneHour, start, end) + r, err := CreateKlineRequest("name", pair, pair, asset.Spot, OneHour, OneHour, start, end, 1) if !errors.Is(err, nil) { t.Fatalf("received: '%v', but expected '%v'", err, nil) } @@ -361,7 +371,7 @@ func TestExtendedRequest_ProcessResponse(t *testing.T) { // with conversion ohc = getOneMinute() - r, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneHour, OneMin, start, end) + r, err = CreateKlineRequest("name", pair, pair, asset.Spot, OneHour, OneMin, start, end, 1) if !errors.Is(err, nil) { t.Fatalf("received: '%v', but expected '%v'", err, nil) } diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 497a79e6..deaa1407 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -158,17 +158,17 @@ func (k *Kraken) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.FourHour, - kline.OneDay, - kline.OneWeek, - kline.FifteenDay, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, + kline.IntervalCapacity{Interval: kline.OneWeek}, + kline.IntervalCapacity{Interval: kline.FifteenDay}, ), - ResultLimit: 720, + GlobalResultLimit: 720, }, }, } @@ -1472,7 +1472,7 @@ func (k *Kraken) FormatExchangeKlineInterval(in kline.Interval) string { // GetHistoricCandles returns candles between a time period for a set time interval func (k *Kraken) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := k.GetKlineRequest(pair, a, interval, start, end) + req, err := k.GetKlineRequest(pair, a, interval, start, end, true) if err != nil { return nil, err } diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go index 3a3c6e9c..278a1d92 100644 --- a/exchanges/lbank/lbank_wrapper.go +++ b/exchanges/lbank/lbank_wrapper.go @@ -93,19 +93,25 @@ func (l *Lbank) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.FourHour, - kline.EightHour, - kline.TwelveHour, - kline.OneDay, - kline.OneWeek, - kline.OneMonth, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + + // NOTE: The supported time intervals below are returned + // offset to the Asia/HongKong time zone. This may lead to + // issues with candle quality and conversion as the + // intervals may be broken up. Therefore the below intervals + // are constructed from hourly -> 4 hourly candles. + // kline.IntervalCapacity{Interval: kline.EightHour}, // The docs suggest this is supported but it isn't. + // kline.IntervalCapacity{Interval: kline.TwelveHour}, // The docs suggest this is supported but it isn't. + // kline.IntervalCapacity{Interval: kline.OneDay}, + // kline.IntervalCapacity{Interval: kline.OneWeek}, + // kline.IntervalCapacity{Interval: kline.OneMonth}, ), - ResultLimit: 2000, + GlobalResultLimit: 2000, }, }, } @@ -889,14 +895,14 @@ func (l *Lbank) FormatExchangeKlineInterval(in kline.Interval) string { // GetHistoricCandles returns candles between a time period for a set time interval func (l *Lbank) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := l.GetKlineRequest(pair, a, interval, start, end) + req, err := l.GetKlineRequest(pair, a, interval, start, end, true) if err != nil { return nil, err } data, err := l.GetKlines(ctx, req.RequestFormatted.String(), - strconv.FormatInt(int64(l.Features.Enabled.Kline.ResultLimit), 10), + strconv.FormatInt(req.RequestLimit, 10), l.FormatExchangeKlineInterval(req.ExchangeInterval), strconv.FormatInt(req.Start.Unix(), 10)) if err != nil { @@ -929,7 +935,7 @@ func (l *Lbank) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pa var data []KlineResponse data, err = l.GetKlines(ctx, req.RequestFormatted.String(), - strconv.FormatInt(int64(l.Features.Enabled.Kline.ResultLimit), 10), + strconv.FormatInt(req.RequestLimit, 10), l.FormatExchangeKlineInterval(req.ExchangeInterval), strconv.FormatInt(req.RangeHolder.Ranges[x].Start.Ticks, 10)) if err != nil { diff --git a/exchanges/okcoin/okcoin.go b/exchanges/okcoin/okcoin.go index 36cf0307..d6786643 100644 --- a/exchanges/okcoin/okcoin.go +++ b/exchanges/okcoin/okcoin.go @@ -419,26 +419,24 @@ func (o *OKCoin) GetMarketData(ctx context.Context, request *GetMarketDataReques if !ok { return nil, common.GetAssertError("string", t[0]) } - var tempCandle kline.Candle - if tempCandle.Time, err = time.Parse(time.RFC3339, v); err != nil { + if candles[x].Time, err = time.Parse(time.RFC3339, v); err != nil { return nil, err } - if tempCandle.Open, err = convert.FloatFromString(t[1]); err != nil { + if candles[x].Open, err = convert.FloatFromString(t[1]); err != nil { return nil, err } - if tempCandle.High, err = convert.FloatFromString(t[2]); err != nil { + if candles[x].High, err = convert.FloatFromString(t[2]); err != nil { return nil, err } - if tempCandle.Low, err = convert.FloatFromString(t[3]); err != nil { + if candles[x].Low, err = convert.FloatFromString(t[3]); err != nil { return nil, err } - if tempCandle.Close, err = convert.FloatFromString(t[4]); err != nil { + if candles[x].Close, err = convert.FloatFromString(t[4]); err != nil { return nil, err } - if tempCandle.Volume, err = convert.FloatFromString(t[5]); err != nil { + if candles[x].Volume, err = convert.FloatFromString(t[5]); err != nil { return nil, err } - candles[x] = tempCandle } return candles, nil } diff --git a/exchanges/okcoin/okcoin_test.go b/exchanges/okcoin/okcoin_test.go index b00c98f7..42b7dd12 100644 --- a/exchanges/okcoin/okcoin_test.go +++ b/exchanges/okcoin/okcoin_test.go @@ -1277,8 +1277,8 @@ func TestGetHistoricCandles(t *testing.T) { t.Fatal(err) } startTime := time.Unix(1588636800, 0) - _, err = o.GetHistoricCandles(context.Background(), - pair, asset.Spot, kline.OneDay, startTime, time.Now()) + endTime := startTime.Add(time.Hour * 24 * 7) + _, err = o.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneDay, startTime, endTime) if err != nil { t.Fatal(err) } @@ -1292,8 +1292,7 @@ func TestGetHistoricCandlesExtended(t *testing.T) { } startTime := time.Unix(1588636800, 0) - _, err = o.GetHistoricCandlesExtended(context.Background(), - pair, asset.Spot, kline.OneWeek, startTime, time.Now()) + _, err = o.GetHistoricCandlesExtended(context.Background(), pair, asset.Spot, kline.OneWeek, startTime, time.Now()) if err != nil { t.Fatal(err) } diff --git a/exchanges/okcoin/okcoin_wrapper.go b/exchanges/okcoin/okcoin_wrapper.go index 09635a4b..5f96483e 100644 --- a/exchanges/okcoin/okcoin_wrapper.go +++ b/exchanges/okcoin/okcoin_wrapper.go @@ -119,21 +119,26 @@ func (o *OKCoin) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.ThreeMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.TwoHour, - kline.FourHour, - kline.SixHour, - kline.TwelveHour, - kline.OneDay, - kline.ThreeDay, - kline.OneWeek, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.ThreeMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.TwoHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + // NOTE: The supported time intervals below are returned + // offset to the Asia/Shanghai time zone. This may lead to + // issues with candle quality and conversion as the + // intervals may be broken up. Therefore the below intervals + // are constructed from hourly candles. + // kline.IntervalCapacity{Interval: kline.SixHour}, + // kline.IntervalCapacity{Interval: kline.TwelveHour}, + // kline.IntervalCapacity{Interval: kline.OneDay}, + // kline.IntervalCapacity{Interval: kline.ThreeDay}, + // kline.IntervalCapacity{Interval: kline.OneWeek}, ), - ResultLimit: 1440, + GlobalResultLimit: 1440, }, }, } @@ -1021,7 +1026,7 @@ func (o *OKCoin) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.I // GetHistoricCandles returns candles between a time period for a set time interval func (o *OKCoin) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := o.GetKlineRequest(pair, a, interval, start, end) + req, err := o.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index f8f5562e..5fe7a028 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -2303,7 +2303,7 @@ func TestGetHistoricCandles(t *testing.T) { t.Parallel() pair := currency.NewPair(currency.BTC, currency.USDT) startTime := time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC) - endTime := time.Date(2021, 9, 15, 0, 0, 0, 0, time.UTC) + endTime := startTime.AddDate(0, 0, 100) _, err := ok.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneDay, startTime, endTime) if err != nil { t.Fatal(err) diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index 2c7a4d9d..918ac3ad 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -131,27 +131,27 @@ func (ok *Okx) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.ThreeMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.TwoHour, - kline.FourHour, - kline.SixHour, - kline.TwelveHour, - kline.OneDay, - kline.TwoDay, - kline.ThreeDay, - kline.FiveDay, - kline.OneWeek, - kline.OneMonth, - kline.ThreeMonth, - kline.SixMonth, - kline.OneYear, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.ThreeMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.TwoHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + kline.IntervalCapacity{Interval: kline.SixHour}, + kline.IntervalCapacity{Interval: kline.TwelveHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, + kline.IntervalCapacity{Interval: kline.TwoDay}, + kline.IntervalCapacity{Interval: kline.ThreeDay}, + kline.IntervalCapacity{Interval: kline.FiveDay}, + kline.IntervalCapacity{Interval: kline.OneWeek}, + kline.IntervalCapacity{Interval: kline.OneMonth}, + kline.IntervalCapacity{Interval: kline.ThreeMonth}, + kline.IntervalCapacity{Interval: kline.SixMonth}, + kline.IntervalCapacity{Interval: kline.OneYear}, ), - ResultLimit: 300, + GlobalResultLimit: 100, // Reference: https://www.okx.com/docs-v5/en/#rest-api-market-data-get-candlesticks-history }, }, } @@ -363,8 +363,7 @@ func (ok *Okx) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) if err != nil { return nil, err } - var baseVolume float64 - var quoteVolume float64 + var baseVolume, quoteVolume float64 switch a { case asset.Spot, asset.Margin: baseVolume = mdata.Vol24H.Float64() @@ -1365,7 +1364,7 @@ func (ok *Okx) ValidateCredentials(ctx context.Context, assetType asset.Item) er // GetHistoricCandles returns candles between a time period for a set time interval func (ok *Okx) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := ok.GetKlineRequest(pair, a, interval, start, end) + req, err := ok.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index ddafcc15..4aed5398 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -123,22 +123,22 @@ func (p *Poloniex) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.FiveMin, - kline.TenMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.TwoHour, - kline.FourHour, - kline.SixHour, - kline.TwelveHour, - kline.OneDay, - kline.ThreeDay, - kline.OneWeek, - kline.OneMonth, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.TenMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.TwoHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + kline.IntervalCapacity{Interval: kline.SixHour}, + kline.IntervalCapacity{Interval: kline.TwelveHour}, + kline.IntervalCapacity{Interval: kline.OneDay}, + kline.IntervalCapacity{Interval: kline.ThreeDay}, + kline.IntervalCapacity{Interval: kline.OneWeek}, + kline.IntervalCapacity{Interval: kline.OneMonth}, ), - ResultLimit: 500, + GlobalResultLimit: 500, }, }, } @@ -950,7 +950,7 @@ func (p *Poloniex) ValidateCredentials(ctx context.Context, assetType asset.Item // GetHistoricCandles returns candles between a time period for a set time interval func (p *Poloniex) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := p.GetKlineRequest(pair, a, interval, start, end) + req, err := p.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { return nil, err } diff --git a/exchanges/zb/zb_test.go b/exchanges/zb/zb_test.go index eedeb402..ccbe18ae 100644 --- a/exchanges/zb/zb_test.go +++ b/exchanges/zb/zb_test.go @@ -867,23 +867,32 @@ func TestWsCreateSubUserResponse(t *testing.T) { } func TestGetSpotKline(t *testing.T) { + t.Parallel() + limit, err := z.Features.Enabled.Kline.GetIntervalResultLimit(kline.OneMin) + if err != nil { + t.Fatal(err) + } arg := KlinesRequestParams{ Symbol: testCurrency, Type: kline.OneMin.Short() + "in", - Size: int64(z.Features.Enabled.Kline.ResultLimit), + Size: limit, } if mockTests { startTime := time.Date(2020, 9, 1, 0, 0, 0, 0, time.UTC) arg.Since = startTime.UnixMilli() arg.Type = "1day" } - _, err := z.GetSpotKline(context.Background(), arg) + _, err = z.GetSpotKline(context.Background(), arg) if err != nil { t.Errorf("ZB GetSpotKline: %s", err) } } func TestGetHistoricCandles(t *testing.T) { + t.Parallel() + if mockTests { + t.Skip("mock testing is not supported for this function") + } currencyPair, err := currency.NewPairFromString(testCurrency) if err != nil { t.Fatal(err) @@ -891,20 +900,14 @@ func TestGetHistoricCandles(t *testing.T) { startTime := time.Now().Add(-time.Hour * 24) endTime := time.Now() - if mockTests { - startTime = time.Date(2020, 9, 1, 0, 0, 0, 0, time.UTC) - endTime = time.Date(2020, 9, 2, 0, 0, 0, 0, time.UTC) - } - - // Current endpoint is dead. - _, err = z.GetHistoricCandles(context.Background(), - currencyPair, asset.Spot, kline.OneDay, startTime, endTime) + _, err = z.GetHistoricCandles(context.Background(), currencyPair, asset.Spot, kline.OneDay, startTime, endTime) if err != nil { t.Fatal(err) } } func TestGetHistoricCandlesExtended(t *testing.T) { + t.Parallel() currencyPair, err := currency.NewPairFromString(testCurrency) if err != nil { t.Fatal(err) @@ -920,8 +923,7 @@ func TestGetHistoricCandlesExtended(t *testing.T) { startTime = time.Now().Add(-time.Hour * 24 * 365) endTime = time.Now() if mockTests { - startTime = time.UnixMilli(1674489600000) - endTime = startTime.Add(kline.OneDay.Duration()) + t.Skip("mock testing is not supported for this function") } _, err = z.GetHistoricCandlesExtended(context.Background(), currencyPair, asset.Spot, kline.OneDay, startTime, endTime) diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index 5fddcccb..e1fb6740 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -111,21 +111,26 @@ func (z *ZB) SetDefaults() { AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( - kline.OneMin, - kline.ThreeMin, - kline.FiveMin, - kline.FifteenMin, - kline.ThirtyMin, - kline.OneHour, - kline.TwoHour, - kline.FourHour, - kline.SixHour, - kline.TwelveHour, - kline.OneDay, - kline.ThreeDay, - kline.OneWeek, + kline.IntervalCapacity{Interval: kline.OneMin}, + kline.IntervalCapacity{Interval: kline.ThreeMin}, + kline.IntervalCapacity{Interval: kline.FiveMin}, + kline.IntervalCapacity{Interval: kline.FifteenMin}, + kline.IntervalCapacity{Interval: kline.ThirtyMin}, + kline.IntervalCapacity{Interval: kline.OneHour}, + kline.IntervalCapacity{Interval: kline.TwoHour}, + kline.IntervalCapacity{Interval: kline.FourHour}, + // NOTE: The supported time intervals below are returned + // offset to the Asia/Shanghai time zone. This may lead to + // issues with candle quality and conversion as the + // intervals may be broken up. Therefore the below intervals + // are constructed from hourly candles. + // kline.IntervalCapacity{Interval: kline.SixHour}, + // kline.IntervalCapacity{Interval: kline.TwelveHour}, + // kline.IntervalCapacity{Interval: kline.OneDay}, + // kline.IntervalCapacity{Interval: kline.ThreeDay}, + // kline.IntervalCapacity{Interval: kline.OneWeek}, ), - ResultLimit: 1000, + GlobalResultLimit: 1000, }, }, } @@ -885,7 +890,7 @@ func (z *ZB) FormatExchangeKlineInterval(in kline.Interval) string { // GetHistoricCandles returns candles between a time period for a set time interval func (z *ZB) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { - req, err := z.GetKlineRequest(pair, a, interval, start, end) + req, err := z.GetKlineRequest(pair, a, interval, start, end, true) if err != nil { return nil, err } @@ -894,7 +899,7 @@ func (z *ZB) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset Type: z.FormatExchangeKlineInterval(req.ExchangeInterval), Symbol: req.RequestFormatted.String(), Since: start.UnixMilli(), - Size: int64(z.Features.Enabled.Kline.ResultLimit), + Size: req.RequestLimit, }) if err != nil { return nil, err