package kline import ( "errors" "fmt" "sort" "strings" "sync" "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/exchanges/order" ) // CreateKline creates candles out of trade history data for a set time interval func CreateKline(trades []order.TradeHistory, interval Interval, p currency.Pair, a asset.Item, exchange string) (Item, error) { if interval.Duration() < time.Minute { return Item{}, fmt.Errorf("invalid time interval: [%s]", interval) } err := validateData(trades) if err != nil { return Item{}, err } timeIntervalStart := trades[0].Timestamp.Truncate(interval.Duration()) timeIntervalEnd := trades[len(trades)-1].Timestamp // Adds time interval buffer zones var timeIntervalCache [][]order.TradeHistory var candleStart []time.Time for t := timeIntervalStart; t.Before(timeIntervalEnd); t = t.Add(interval.Duration()) { timeBufferEnd := t.Add(interval.Duration()) insertionCount := 0 var zonedTradeHistory []order.TradeHistory for i := 0; i < len(trades); i++ { if (trades[i].Timestamp.After(t) || trades[i].Timestamp.Equal(t)) && (trades[i].Timestamp.Before(timeBufferEnd) || trades[i].Timestamp.Equal(timeBufferEnd)) { zonedTradeHistory = append(zonedTradeHistory, trades[i]) insertionCount++ continue } trades = trades[i:] break } candleStart = append(candleStart, t) // Insert dummy in time period when there is no price action if insertionCount == 0 { timeIntervalCache = append(timeIntervalCache, []order.TradeHistory{}) continue } timeIntervalCache = append(timeIntervalCache, zonedTradeHistory) } if candleStart == nil { return Item{}, errors.New("candle start cannot be nil") } var candles = Item{ Exchange: exchange, Pair: p, Asset: a, Interval: interval, } var closePriceOfLast float64 for x := range timeIntervalCache { if len(timeIntervalCache[x]) == 0 { candles.Candles = append(candles.Candles, Candle{ Time: candleStart[x], High: closePriceOfLast, Low: closePriceOfLast, Close: closePriceOfLast, Open: closePriceOfLast}) continue } var newCandle = Candle{ Open: timeIntervalCache[x][0].Price, Time: candleStart[x], } for y := range timeIntervalCache[x] { if y == len(timeIntervalCache[x])-1 { newCandle.Close = timeIntervalCache[x][y].Price closePriceOfLast = timeIntervalCache[x][y].Price } if newCandle.High < timeIntervalCache[x][y].Price { newCandle.High = timeIntervalCache[x][y].Price } if newCandle.Low > timeIntervalCache[x][y].Price || newCandle.Low == 0 { newCandle.Low = timeIntervalCache[x][y].Price } newCandle.Volume += timeIntervalCache[x][y].Amount } candles.Candles = append(candles.Candles, newCandle) } return candles, nil } // validateData checks for zero values on data and sorts before turning // converting into OHLC func validateData(trades []order.TradeHistory) error { if len(trades) < 2 { return errors.New("insufficient data") } for i := range trades { if trades[i].Timestamp.IsZero() || trades[i].Timestamp.Unix() == 0 { return fmt.Errorf("timestamp not set for element %d", i) } if trades[i].Amount == 0 { return fmt.Errorf("amount not set for element %d", i) } if trades[i].Price == 0 { return fmt.Errorf("price not set for element %d", i) } } sort.Slice(trades, func(i, j int) bool { return trades[i].Timestamp.Before(trades[j].Timestamp) }) return nil } // String returns numeric string func (i Interval) String() string { return i.Duration().String() } // Word returns text version of Interval func (i Interval) Word() string { return durationToWord(i) } // Duration returns interval casted as time.Duration for compatibility func (i Interval) Duration() time.Duration { return time.Duration(i) } // Short returns short string version of interval func (i Interval) Short() string { s := i.String() if strings.HasSuffix(s, "m0s") { s = s[:len(s)-2] } if strings.HasSuffix(s, "h0m") { s = s[:len(s)-2] } return s } // FillMissingDataWithEmptyEntries amends a kline item to have candle entries // for every interval between its start and end dates derived from ranges func (k *Item) FillMissingDataWithEmptyEntries(i *IntervalRangeHolder) { var anyChanges bool for x := range i.Ranges { for y := range i.Ranges[x].Intervals { if !i.Ranges[x].Intervals[y].HasData { for z := range k.Candles { if i.Ranges[x].Intervals[y].Start.Equal(k.Candles[z].Time) { break } } anyChanges = true k.Candles = append(k.Candles, Candle{ Time: i.Ranges[x].Intervals[y].Start.Time, }) } } } if anyChanges { k.SortCandlesByTimestamp(false) } } // RemoveDuplicates removes any duplicate candles func (k *Item) RemoveDuplicates() { var newCandles []Candle for x := range k.Candles { if x == 0 { newCandles = append(newCandles, k.Candles[x]) continue } if !k.Candles[x].Time.Equal(k.Candles[x-1].Time) { // don't add duplicate newCandles = append(newCandles, k.Candles[x]) } } k.Candles = newCandles } // RemoveOutsideRange removes any candles outside the start and end date func (k *Item) RemoveOutsideRange(start, end time.Time) { var newCandles []Candle for i := range k.Candles { if k.Candles[i].Time.Equal(start) || (k.Candles[i].Time.After(start) && k.Candles[i].Time.Before(end)) { newCandles = append(newCandles, k.Candles[i]) } } k.Candles = newCandles } // SortCandlesByTimestamp sorts candles by timestamp func (k *Item) SortCandlesByTimestamp(desc bool) { sort.Slice(k.Candles, func(i, j int) bool { if desc { return k.Candles[i].Time.After(k.Candles[j].Time) } return k.Candles[i].Time.Before(k.Candles[j].Time) }) } // FormatDates converts all date to UTC time func (k *Item) FormatDates() { for x := range k.Candles { k.Candles[x].Time = k.Candles[x].Time.UTC() } } // durationToWord returns english version of interval func durationToWord(in Interval) string { switch in { case FifteenSecond: return "fifteensecond" case OneMin: return "onemin" case ThreeMin: return "threemin" case FiveMin: return "fivemin" case TenMin: return "tenmin" case FifteenMin: return "fifteenmin" case ThirtyMin: return "thirtymin" case OneHour: return "onehour" case TwoHour: return "twohour" case FourHour: return "fourhour" case SixHour: return "sixhour" case EightHour: return "eighthour" case TwelveHour: return "twelvehour" case OneDay: return "oneday" case ThreeDay: return "threeday" case FifteenDay: return "fifteenday" case OneWeek: return "oneweek" case TwoWeek: return "twoweek" case OneMonth: return "onemonth" case OneYear: return "oneyear" default: return "notfound" } } // TotalCandlesPerInterval turns total candles per period for interval func TotalCandlesPerInterval(start, end time.Time, interval Interval) (out float64) { switch interval { case FifteenSecond: return end.Sub(start).Seconds() / 15 case OneMin: return end.Sub(start).Minutes() case ThreeMin: return end.Sub(start).Minutes() / 3 case FiveMin: return end.Sub(start).Minutes() / 5 case TenMin: return end.Sub(start).Minutes() / 10 case FifteenMin: return end.Sub(start).Minutes() / 15 case ThirtyMin: return end.Sub(start).Minutes() / 30 case OneHour: return end.Sub(start).Hours() case TwoHour: return end.Sub(start).Hours() / 2 case FourHour: return end.Sub(start).Hours() / 4 case SixHour: return end.Sub(start).Hours() / 6 case EightHour: return end.Sub(start).Hours() / 8 case TwelveHour: return end.Sub(start).Hours() / 12 case OneDay: return end.Sub(start).Hours() / 24 case ThreeDay: return end.Sub(start).Hours() / 72 case FifteenDay: return end.Sub(start).Hours() / (24 * 15) case OneWeek: return end.Sub(start).Hours() / (24 * 7) case TwoWeek: return end.Sub(start).Hours() / (24 * 14) case OneMonth: return end.Sub(start).Hours() / (24 * 30) case OneYear: return end.Sub(start).Hours() / 8760 } return -1 } // IntervalsPerYear helps determine the number of intervals in a year // used in CAGR calculation to know the amount of time of an interval in a year func (i *Interval) IntervalsPerYear() float64 { return float64(OneYear.Duration().Nanoseconds()) / float64(i.Duration().Nanoseconds()) } // CalculateCandleDateRanges will calculate the expected candle data in intervals in a date range // If an API is limited in the amount of candles it can make in a request, it will automatically separate // ranges into the limit func CalculateCandleDateRanges(start, end time.Time, interval Interval, limit uint32) IntervalRangeHolder { start = start.Round(interval.Duration()) end = end.Round(interval.Duration()) resp := IntervalRangeHolder{ Start: CreateIntervalTime(start), End: CreateIntervalTime(end), } var intervalsInWholePeriod []IntervalData for i := start; !i.After(end) && !i.Equal(end); i = i.Add(interval.Duration()) { intervalsInWholePeriod = append(intervalsInWholePeriod, IntervalData{ Start: CreateIntervalTime(i.Round(interval.Duration())), End: CreateIntervalTime(i.Round(interval.Duration()).Add(interval.Duration())), }) } if len(intervalsInWholePeriod) < int(limit) || limit == 0 { resp.Ranges = []IntervalRange{{ Start: CreateIntervalTime(start), End: CreateIntervalTime(end), Intervals: intervalsInWholePeriod, }} return resp } var intervals []IntervalData splitIntervalsByLimit := make([][]IntervalData, 0, len(intervalsInWholePeriod)/int(limit)+1) for len(intervalsInWholePeriod) >= int(limit) { intervals, intervalsInWholePeriod = intervalsInWholePeriod[:limit], intervalsInWholePeriod[limit:] splitIntervalsByLimit = append(splitIntervalsByLimit, intervals) } if len(intervalsInWholePeriod) > 0 { splitIntervalsByLimit = append(splitIntervalsByLimit, intervalsInWholePeriod) } for x := range splitIntervalsByLimit { resp.Ranges = append(resp.Ranges, IntervalRange{ Start: splitIntervalsByLimit[x][0].Start, End: splitIntervalsByLimit[x][len(splitIntervalsByLimit[x])-1].End, Intervals: splitIntervalsByLimit[x], }) } return resp } // HasDataAtDate determines whether a there is any data at a set // date inside the existing limits func (h *IntervalRangeHolder) HasDataAtDate(t time.Time) bool { tu := t.Unix() if tu < h.Start.Ticks || tu > h.End.Ticks { return false } for i := range h.Ranges { if tu >= h.Ranges[i].Start.Ticks && tu <= h.Ranges[i].End.Ticks { for j := range h.Ranges[i].Intervals { if tu >= h.Ranges[i].Intervals[j].Start.Ticks && tu < h.Ranges[i].Intervals[j].End.Ticks { return h.Ranges[i].Intervals[j].HasData } if j == len(h.Ranges[i].Intervals)-1 { if tu == h.Ranges[i].Start.Ticks { return h.Ranges[i].Intervals[j].HasData } } } } } return false } // VerifyResultsHaveData will calculate whether there is data in each candle // allowing any missing data from an API request to be highlighted func (h *IntervalRangeHolder) VerifyResultsHaveData(c []Candle) error { var wg sync.WaitGroup wg.Add(len(h.Ranges)) for x := range h.Ranges { go func(iVal int) { for y := range h.Ranges[iVal].Intervals { for z := range c { cu := c[z].Time.Unix() if cu >= h.Ranges[iVal].Intervals[y].Start.Ticks && cu < h.Ranges[iVal].Intervals[y].End.Ticks { h.Ranges[iVal].Intervals[y].HasData = true break } } } wg.Done() }(x) } wg.Wait() var errs common.Errors for x := range h.Ranges { for y := range h.Ranges[x].Intervals { if !h.Ranges[x].Intervals[y].HasData { errs = append(errs, fmt.Errorf("between %v (%v) & %v (%v)", h.Ranges[x].Intervals[y].Start.Time, h.Ranges[x].Intervals[y].Start.Ticks, h.Ranges[x].Intervals[y].End.Time, h.Ranges[x].Intervals[y].End.Ticks)) } } } if len(errs) > 0 { return fmt.Errorf("%w - %v", ErrMissingCandleData, errs) } return nil } // CreateIntervalTime is a simple helper function to set the time twice func CreateIntervalTime(tt time.Time) IntervalTime { return IntervalTime{ Time: tt, Ticks: tt.Unix(), } } // Equal allows for easier unix comparison func (i *IntervalTime) Equal(tt time.Time) bool { return tt.Unix() == i.Ticks }