Files
gocryptotrader/exchanges/kline/request.go
Ryan O'Hara-Reid 83cfefa45c kline/exchanges: automatic creation of unsupported candle intervals (#1091)
* kline: Add builder and testing

* Ideas

* kline: deploy builder functionality across GCT

* exchanges: implement across gct

* exchanges: Add tests and fix implementations before kline package testing and veri.

* kline: Add tests and start to fix ConvertToNewInterval

* kline: fix ConvertToNewInterval add tests

* kline: complete overarching tests now on to exchanges

* kline: finish exchange tests and implement limits

* exchanges: more fixes

* linter: fix

* engine: fix tests

* kraken: fix recent trades and other fixes

* zb: fix tests

* bithumb: fix empty insertion

* kline: refactor/optimize CreateKline function

* kline: remove the mooos!

* kline: prealloc CalculateCandleDateRanges

* linter: fix

* exchanges: prealloc extended

* fix whoopsie

* reverse fix because this is a whoopsie

* okx: fix risidual issues

* linter: fix

* kline: initial nits from @gloriouscode

* kline: rename builder -> request and cascade change

* linter: fix + test

* kline: update forced alignment on start and end times when CreateKlineRequest is called.

* nits: more more more

* NITS: Addressed

* tests: fix race issue

* Update exchanges/kline/request.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* kline: add method AddPadding() to automatically fill in holes in kline.Request functionality and reject if missing data when converting

* kline: Add params start and end to addPadding() to insert blanks in between block

* kline: remove test comment code as it's not needed anymore

* kline: fix lint and test

* kline: sort slice without extra bool check every iteration

* okx: fix issues with timeing and candles and such from niterinos & address typo

* Update exchanges/kline/kline.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: niterinos

* Update exchanges/poloniex/poloniex_wrapper.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits now onto conflicts YAYA!!!

* Update exchanges/exchange_test.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits again

* thrasher: nitters

* thrasher: niterinos - adds partial flag for incomplete recent candles and fetching.

* kline: rm fmtizzle packageizzle

* glorious: nitters

* glorious: more niterinos

* fix last niterinos

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
2023-01-17 16:22:33 +11:00

207 lines
6.5 KiB
Go

package kline
import (
"errors"
"fmt"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/log"
)
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")
// PartialCandle is string flag for when the most recent candle is partially
// formed.
PartialCandle = "Partial Candle"
)
// Request is a helper to request and convert time series to a required candle
// interval.
type Request struct {
// Exchange refers to the exchange name
Exchange string
// Pair refers to the currency pair
Pair currency.Pair
// RequestFormatted refers to the currency pair formatted by the exchange
// asset for outbound requests
RequestFormatted currency.Pair
// Asset refers to the asset type
Asset asset.Item
// ExchangeInterval refers to the interval that is used to construct the
// client required interval, this will be less than or equal to the client
// required interval.
ExchangeInterval Interval
// ClientRequired refers to the clients' actual required interval
// needed.
ClientRequired Interval
// Start is the start time aligned to UTC and to the Required interval candle
Start time.Time
// End is the end time aligned to UTC and to the Required interval candle
End time.Time
// PartialCandle defines when a request's end time interval goes beyond
// current time it potentially has a partially formed candle.
PartialCandle bool
}
// 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) {
if name == "" {
return nil, ErrUnsetName
}
if pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if formatted.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if !a.IsValid() {
return nil, asset.ErrNotSupported
}
if clientRequired == 0 {
return nil, fmt.Errorf("client required %w", ErrInvalidInterval)
}
if exchangeInterval == 0 {
return nil, fmt.Errorf("exchange interval %w", ErrInvalidInterval)
}
err := common.StartEndTimeCheck(start, end)
if err != nil {
return nil, err
}
// Force UTC alignment
start = start.UTC()
end = end.UTC()
// Force alignment to required interval which is the higher time value e.g.
// 1hr required as opposed to 1min request/outbound interval used to
// construct the higher time value candle. This is to make sure there are
// minimal missing candles which is used to create the bigger candle.
start = start.Truncate(clientRequired.Duration())
// Strip future time to current time so there is no extra padding.
if end.After(time.Now()) {
end = time.Now().UTC()
}
// Strip monotonic clock reading for comparison
end = end.Round(0)
endTrunc := end.Truncate(clientRequired.Duration())
// Check to see if truncation moves end time and if so we want to make sure
// the candle period is included on the end.
if !endTrunc.Equal(end) {
end = endTrunc.Add(clientRequired.Duration())
}
return &Request{name, pair, formatted, a, exchangeInterval, clientRequired, start, end, end.After(time.Now())}, nil
}
// GetRanges returns the date ranges for candle intervals broken up over
// requests
func (r *Request) GetRanges(limit uint32) (*IntervalRangeHolder, error) {
if r == nil {
return nil, errNilRequest
}
return CalculateCandleDateRanges(r.Start, r.End, r.ExchangeInterval, limit)
}
// ProcessResponse converts time series candles into a kline.Item type. This
// will auto convert from a lower to higher time series if applicable.
func (r *Request) ProcessResponse(timeSeries []Candle) (*Item, error) {
if r == nil {
return nil, errNilRequest
}
if len(timeSeries) == 0 {
return nil, errNoTimeSeriesDataToConvert
}
holder := &Item{
Exchange: r.Exchange,
Pair: r.Pair,
Asset: r.Asset,
Interval: r.ExchangeInterval,
Candles: timeSeries,
}
// NOTE: timeSeries param above must keep underlying slice reference in this
// function as it is used for method ConvertCandles on type ExtendedRequest
// for SetHasDataFromCandles candle matching.
// TODO: Shift burden of proof to the caller e.g. only find duplicates and error.
holder.RemoveDuplicates()
holder.RemoveOutsideRange(r.Start, r.End)
holder.SortCandlesByTimestamp(false)
err := holder.addPadding(r.Start, r.End, r.PartialCandle)
if err != nil {
return nil, err
}
if r.ClientRequired != r.ExchangeInterval {
holder, err = holder.ConvertToNewInterval(r.ClientRequired)
}
if r.PartialCandle {
// NOTE: Some endpoints do not return incomplete candles, verify for
// incomplete candle.
recentCandle := &holder.Candles[len(holder.Candles)-1]
if recentCandle.Time.Add(r.ClientRequired.Duration()).After(time.Now()) {
recentCandle.ValidationIssues = PartialCandle
}
}
return holder, err
}
// ExtendedRequest used in extended functionality for when candles requested
// exceed exchange limits and require multiple requests.
type ExtendedRequest struct {
*Request
*IntervalRangeHolder
}
// ProcessResponse converts time series candles into a kline.Item type. This
// will auto convert from a lower to higher time series if applicable.
func (r *ExtendedRequest) ProcessResponse(timeSeries []Candle) (*Item, error) {
if r == nil {
return nil, errNilRequest
}
if len(timeSeries) == 0 {
return nil, errNoTimeSeriesDataToConvert
}
holder, err := r.Request.ProcessResponse(timeSeries)
if err != nil {
return nil, err
}
// This checks from pre-converted time series data for date range matching.
// NOTE: If there are any optimizations which copy timeSeries param slice
// in the function call ConvertCandles above then false positives can
// occur. // TODO: Improve implementation.
r.SetHasDataFromCandles(timeSeries)
summary := r.DataSummary(false)
if len(summary) > 0 {
log.Warnf(log.ExchangeSys, "%v - %v", r.Exchange, summary)
}
return holder, nil
}
// Size returns the max length of return for pre-allocation.
func (r *ExtendedRequest) Size() int {
if r == nil || r.IntervalRangeHolder == nil {
return 0
}
if r.IntervalRangeHolder.Limit == 0 {
log.Warnf(log.ExchangeSys, "%v candle request limit is zero while calling Size()", r.Exchange)
}
return r.IntervalRangeHolder.Limit * len(r.IntervalRangeHolder.Ranges)
}