mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-19 07:26:49 +00:00
* BTSE: bump API version to 3.2 * BTSE: added AccountFee endpoint * BTSE: ratelimit, modified GetFee() to use GetFeeInformation, CreateWalletAddress support * BTSE: gofmt * BTSE: renamed ratelimits to ratelimit * BTSE: comments on exported methods, reworked const * BTSE: remove verbose * BTSE: increased test coverage * BTSE: removed futures test * BTSE: comments, correct types, moon * Add support for futures ticker/orderbook, pass data from OHLCV to append because data is important :D * BTSE: update futures test pair * BTSE: updated creatorder test to use negative numbers * BTSE: updated test wording * BTSE: no BTSEx anymore * BTSE: use GetEnabledPairs * BTSE: updated order structu * BTSE: goimport test package * BTSE: added orderIntToType method * BTSE: added extra params to Trade/OpenOrders, kline format method added * BTSE: CreateOrder and IndexOrderPeg updates * removed binary * BTSE: type fixes for orderid, comments * BTSE: remove float tos tring conversion correct casing * BTSE: updated return types * BTSE: return slice * BTSE: update type to string, fixed comment on Price(), removed verbose flag * BTSE: use FormatExchangeCurrency() * BTSE: status -> string * BTSE: added withinLimit method to confirm order is within valid limits * BTSE: gofmt * BTSE: updated comment * BTSE: comment update * BTSE: init map for cancelallexcahgneorders * BTSE: updated json structs for trade history, reworked withinlimits and ordersizelimits to use sync.map * BTSE: test other currencies to confirm matching values for incerement * BTSE: comment, changed type * BTSE: added ordersizelimits seed data to test * BTSE: fpair -> fPair naming & kline sort update * BTSE: removed format call & asset param from withinLimits * BTSE: range over pairs for active orders * BTSE: verbose removal pass * BTSE: ticker batching support * BTSE: remove old pair formatter
309 lines
7.6 KiB
Go
309 lines
7.6 KiB
Go
package kline
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"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
|
|
}
|
|
|
|
// 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 uint32) {
|
|
switch interval {
|
|
case FifteenSecond:
|
|
out = uint32(end.Sub(start).Seconds() / 15)
|
|
case OneMin:
|
|
out = uint32(end.Sub(start).Minutes())
|
|
case ThreeMin:
|
|
out = uint32(end.Sub(start).Minutes() / 3)
|
|
case FiveMin:
|
|
out = uint32(end.Sub(start).Minutes() / 5)
|
|
case TenMin:
|
|
out = uint32(end.Sub(start).Minutes() / 10)
|
|
case FifteenMin:
|
|
out = uint32(end.Sub(start).Minutes() / 15)
|
|
case ThirtyMin:
|
|
out = uint32(end.Sub(start).Minutes() / 30)
|
|
case OneHour:
|
|
out = uint32(end.Sub(start).Hours())
|
|
case TwoHour:
|
|
out = uint32(end.Sub(start).Hours() / 2)
|
|
case FourHour:
|
|
out = uint32(end.Sub(start).Hours() / 4)
|
|
case SixHour:
|
|
out = uint32(end.Sub(start).Hours() / 6)
|
|
case EightHour:
|
|
out = uint32(end.Sub(start).Hours() / 8)
|
|
case TwelveHour:
|
|
out = uint32(end.Sub(start).Hours() / 12)
|
|
case OneDay:
|
|
out = uint32(end.Sub(start).Hours() / 24)
|
|
case ThreeDay:
|
|
out = uint32(end.Sub(start).Hours() / 72)
|
|
case FifteenDay:
|
|
out = uint32(end.Sub(start).Hours() / (24 * 15))
|
|
case OneWeek:
|
|
out = uint32(end.Sub(start).Hours()) / (24 * 7)
|
|
case TwoWeek:
|
|
out = uint32(end.Sub(start).Hours() / (24 * 14))
|
|
case OneMonth:
|
|
out = uint32(end.Sub(start).Hours() / (24 * 30))
|
|
case OneYear:
|
|
out = uint32(end.Sub(start).Hours() / 8760)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// CalcDateRanges returns slice of start/end times based on start & end date
|
|
func CalcDateRanges(start, end time.Time, interval Interval, limit uint32) (out []DateRange) {
|
|
total := TotalCandlesPerInterval(start, end, interval)
|
|
if total < limit {
|
|
return []DateRange{{
|
|
Start: start,
|
|
End: end,
|
|
},
|
|
}
|
|
}
|
|
var allDateIntervals []time.Time
|
|
var y uint32
|
|
var lastNum int
|
|
for d := start; !d.After(end); d = d.Add(interval.Duration()) {
|
|
allDateIntervals = append(allDateIntervals, d)
|
|
}
|
|
for x := range allDateIntervals {
|
|
if y == limit {
|
|
out = append(out, DateRange{
|
|
allDateIntervals[x-int(limit)],
|
|
allDateIntervals[x],
|
|
})
|
|
y = 0
|
|
lastNum = x
|
|
}
|
|
y++
|
|
}
|
|
if allDateIntervals != nil && lastNum+1 < len(allDateIntervals) {
|
|
out = append(out, DateRange{
|
|
Start: allDateIntervals[lastNum+1],
|
|
End: allDateIntervals[len(allDateIntervals)-1],
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|