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