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") // ErrNoTimeSeriesDataToConvert is returned when no data can be processed ErrNoTimeSeriesDataToConvert = errors.New("no candle data returned to process") errNilRequest = errors.New("nil kline request") errInvalidSpecificEndpointLimit = errors.New("specific endpoint limit must be greater than 0") // 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 // IsExtended denotes whether the candle request is for extended candles IsExtended bool // 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, specificEndpointLimit int64) (*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 } if specificEndpointLimit <= 0 { return nil, errInvalidSpecificEndpointLimit } // 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{ Exchange: name, Pair: pair, RequestFormatted: formatted, Asset: a, ExchangeInterval: exchangeInterval, ClientRequired: clientRequired, Start: start, End: end, PartialCandle: end.After(time.Now()), RequestLimit: specificEndpointLimit, }, 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.IsExtended { // NOTE: This allows for a processed candles to be analysed // in the context of ExtendedRequest's ProcessResponse function r.ProcessedCandles = make([]Candle, len(holder.Candles)) copy(r.ProcessedCandles, holder.Candles) } 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 } // Size returns the max length of return for pre-allocation. func (r *Request) Size() int { if r == nil { return 0 } return int(TotalCandlesPerInterval(r.Start, r.End, r.ExchangeInterval)) } // ExtendedRequest used in extended functionality for when candles requested // exceed exchange limits and require multiple requests. type ExtendedRequest struct { *Request RangeHolder *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 } err = r.RangeHolder.SetHasDataFromCandles(r.Request.ProcessedCandles) if err != nil { return nil, err } summary := r.RangeHolder.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.RangeHolder == nil { return 0 } if r.RangeHolder.Limit == 0 { log.Warnf(log.ExchangeSys, "%v candle request limit is zero while calling Size()", r.Exchange) } return r.RangeHolder.Limit * len(r.RangeHolder.Ranges) }