package types import ( "fmt" "time" "github.com/slack-go/slack" "git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint" "git.qtrade.icu/lychiyu/qbtrade/pkg/style" ) var Two = fixedpoint.NewFromInt(2) var Three = fixedpoint.NewFromInt(3) type Direction int const DirectionUp = 1 const DirectionNone = 0 const DirectionDown = -1 type KLineOrWindow interface { GetInterval() string Direction() Direction GetChange() fixedpoint.Value GetMaxChange() fixedpoint.Value GetThickness() fixedpoint.Value Mid() fixedpoint.Value GetOpen() fixedpoint.Value GetClose() fixedpoint.Value GetHigh() fixedpoint.Value GetLow() fixedpoint.Value BounceUp() bool BounceDown() bool GetUpperShadowRatio() fixedpoint.Value GetLowerShadowRatio() fixedpoint.Value SlackAttachment() slack.Attachment } type KLineQueryOptions struct { Limit int StartTime *time.Time EndTime *time.Time } // KLine uses binance's kline as the standard structure type KLine struct { GID uint64 `json:"gid" db:"gid"` Exchange ExchangeName `json:"exchange" db:"exchange"` Symbol string `json:"symbol" db:"symbol"` StartTime Time `json:"startTime" db:"start_time"` // EndTime follows the binance rule, to avoid endTime overlapping with the next startTime. So if your end time (2023-01-01 01:00:00) // are overlapping with next start time interval (2023-01-01 01:00:00), you should subtract -1 time.millisecond on EndTime. EndTime Time `json:"endTime" db:"end_time"` Interval Interval `json:"interval" db:"interval"` Open fixedpoint.Value `json:"open" db:"open"` Close fixedpoint.Value `json:"close" db:"close"` High fixedpoint.Value `json:"high" db:"high"` Low fixedpoint.Value `json:"low" db:"low"` Volume fixedpoint.Value `json:"volume" db:"volume"` QuoteVolume fixedpoint.Value `json:"quoteVolume" db:"quote_volume"` TakerBuyBaseAssetVolume fixedpoint.Value `json:"takerBuyBaseAssetVolume" db:"taker_buy_base_volume"` TakerBuyQuoteAssetVolume fixedpoint.Value `json:"takerBuyQuoteAssetVolume" db:"taker_buy_quote_volume"` LastTradeID uint64 `json:"lastTradeID" db:"last_trade_id"` NumberOfTrades uint64 `json:"numberOfTrades" db:"num_trades"` Closed bool `json:"closed" db:"closed"` } func (k *KLine) Set(o *KLine) { k.GID = o.GID k.Exchange = o.Exchange k.Symbol = o.Symbol k.StartTime = o.StartTime k.EndTime = o.EndTime k.Interval = o.Interval k.Open = o.Open k.Close = o.Close k.High = o.High k.Low = o.Low k.Volume = o.Volume k.QuoteVolume = o.QuoteVolume k.TakerBuyBaseAssetVolume = o.TakerBuyBaseAssetVolume k.TakerBuyQuoteAssetVolume = o.TakerBuyQuoteAssetVolume k.LastTradeID = o.LastTradeID k.NumberOfTrades = o.NumberOfTrades k.Closed = o.Closed } func (k *KLine) Merge(o *KLine) { k.EndTime = o.EndTime k.Close = o.Close k.High = fixedpoint.Max(k.High, o.High) k.Low = fixedpoint.Min(k.Low, o.Low) k.Volume = k.Volume.Add(o.Volume) k.QuoteVolume = k.QuoteVolume.Add(o.QuoteVolume) k.TakerBuyBaseAssetVolume = k.TakerBuyBaseAssetVolume.Add(o.TakerBuyBaseAssetVolume) k.TakerBuyQuoteAssetVolume = k.TakerBuyQuoteAssetVolume.Add(o.TakerBuyQuoteAssetVolume) k.LastTradeID = o.LastTradeID k.NumberOfTrades += o.NumberOfTrades k.Closed = o.Closed } func (k *KLine) GetStartTime() Time { return k.StartTime } func (k *KLine) GetEndTime() Time { return k.EndTime } func (k *KLine) GetInterval() Interval { return k.Interval } func (k *KLine) Mid() fixedpoint.Value { return k.High.Add(k.Low).Div(Two) } // green candle with open and close near high price func (k *KLine) BounceUp() bool { mid := k.Mid() trend := k.Direction() return trend > 0 && k.Open.Compare(mid) > 0 && k.Close.Compare(mid) > 0 } // red candle with open and close near low price func (k *KLine) BounceDown() bool { mid := k.Mid() trend := k.Direction() return trend > 0 && k.Open.Compare(mid) < 0 && k.Close.Compare(mid) < 0 } func (k *KLine) Direction() Direction { o := k.GetOpen() c := k.GetClose() if c.Compare(o) > 0 { return DirectionUp } else if c.Compare(o) < 0 { return DirectionDown } return DirectionNone } func (k *KLine) GetHigh() fixedpoint.Value { return k.High } func (k *KLine) GetLow() fixedpoint.Value { return k.Low } func (k *KLine) GetOpen() fixedpoint.Value { return k.Open } func (k *KLine) GetClose() fixedpoint.Value { return k.Close } func (k *KLine) GetMaxChange() fixedpoint.Value { return k.GetHigh().Sub(k.GetLow()) } func (k *KLine) GetAmplification() fixedpoint.Value { return k.GetMaxChange().Div(k.GetLow()) } // GetThickness returns the thickness of the kline. 1 => thick, 0.1 => thin func (k *KLine) GetThickness() fixedpoint.Value { out := k.GetChange().Div(k.GetMaxChange()) if out.Sign() < 0 { return out.Neg() } return out } func (k *KLine) GetUpperShadowRatio() fixedpoint.Value { out := k.GetUpperShadowHeight().Div(k.GetMaxChange()) if out.Sign() < 0 { return out.Neg() } return out } func (k *KLine) GetUpperShadowHeight() fixedpoint.Value { high := k.GetHigh() open := k.GetOpen() clos := k.GetClose() if open.Compare(clos) > 0 { return high.Sub(open) } return high.Sub(clos) } func (k *KLine) GetLowerShadowRatio() fixedpoint.Value { out := k.GetLowerShadowHeight().Div(k.GetMaxChange()) if out.Sign() < 0 { return out.Neg() } return out } func (k *KLine) GetLowerShadowHeight() fixedpoint.Value { low := k.Low if k.Open.Compare(k.Close) < 0 { // uptrend return k.Open.Sub(low) } // downtrend return k.Close.Sub(low) } // GetBody returns the height of the candle real body func (k *KLine) GetBody() fixedpoint.Value { return k.GetChange() } // GetChange returns Close price - Open price. func (k *KLine) GetChange() fixedpoint.Value { return k.Close.Sub(k.Open) } func (k *KLine) Color() string { if k.Direction() > 0 { return style.GreenColor } else if k.Direction() < 0 { return style.RedColor } return style.GrayColor } func (k *KLine) String() string { return fmt.Sprintf("%s %s %s %s O: %.4f H: %.4f L: %.4f C: %.4f CHG: %.4f MAXCHG: %.4f V: %.4f QV: %.2f TBBV: %.2f", k.Exchange.String(), k.StartTime.Time().Format("2006-01-02 15:04"), k.Symbol, k.Interval, k.Open.Float64(), k.High.Float64(), k.Low.Float64(), k.Close.Float64(), k.GetChange().Float64(), k.GetMaxChange().Float64(), k.Volume.Float64(), k.QuoteVolume.Float64(), k.TakerBuyBaseAssetVolume.Float64()) } func (k *KLine) PlainText() string { return k.String() } func (k *KLine) SlackAttachment() slack.Attachment { return slack.Attachment{ Text: fmt.Sprintf("*%s* KLine %s", k.Symbol, k.Interval), Color: k.Color(), Fields: []slack.AttachmentField{ {Title: "Open", Value: k.Open.FormatString(2), Short: true}, {Title: "High", Value: k.High.FormatString(2), Short: true}, {Title: "Low", Value: k.Low.FormatString(2), Short: true}, {Title: "Close", Value: k.Close.FormatString(2), Short: true}, {Title: "Mid", Value: k.Mid().FormatString(2), Short: true}, {Title: "Change", Value: k.GetChange().FormatString(2), Short: true}, {Title: "Volume", Value: k.Volume.FormatString(2), Short: true}, {Title: "Taker Buy Base Volume", Value: k.TakerBuyBaseAssetVolume.FormatString(2), Short: true}, {Title: "Taker Buy Quote Volume", Value: k.TakerBuyQuoteAssetVolume.FormatString(2), Short: true}, {Title: "Max Change", Value: k.GetMaxChange().FormatString(2), Short: true}, { Title: "Thickness", Value: k.GetThickness().FormatString(4), Short: true, }, { Title: "UpperShadowRatio", Value: k.GetUpperShadowRatio().FormatString(4), Short: true, }, { Title: "LowerShadowRatio", Value: k.GetLowerShadowRatio().FormatString(4), Short: true, }, }, Footer: "", FooterIcon: "", } } type KLineWindow []KLine // ReduceClose reduces the closed prices func (k KLineWindow) ReduceClose() fixedpoint.Value { s := fixedpoint.Zero for _, kline := range k { s = s.Add(kline.GetClose()) } return s } func (k KLineWindow) Len() int { return len(k) } func (k KLineWindow) First() KLine { return k[0] } func (k KLineWindow) Last() KLine { return k[len(k)-1] } func (k KLineWindow) GetInterval() Interval { return k.First().Interval } func (k KLineWindow) GetOpen() fixedpoint.Value { first := k.First() return first.GetOpen() } func (k KLineWindow) GetClose() fixedpoint.Value { end := len(k) - 1 return k[end].GetClose() } func (k KLineWindow) GetHigh() fixedpoint.Value { first := k.First() high := first.GetHigh() for _, line := range k { high = fixedpoint.Max(high, line.GetHigh()) } return high } func (k KLineWindow) GetLow() fixedpoint.Value { first := k.First() low := first.GetLow() for _, line := range k { low = fixedpoint.Min(low, line.GetLow()) } return low } func (k KLineWindow) GetChange() fixedpoint.Value { return k.GetClose().Sub(k.GetOpen()) } func (k KLineWindow) GetMaxChange() fixedpoint.Value { return k.GetHigh().Sub(k.GetLow()) } func (k KLineWindow) GetAmplification() fixedpoint.Value { return k.GetMaxChange().Div(k.GetLow()) } func (k KLineWindow) AllDrop() bool { for _, n := range k { if n.Direction() >= 0 { return false } } return true } func (k KLineWindow) AllRise() bool { for _, n := range k { if n.Direction() <= 0 { return false } } return true } func (k KLineWindow) GetTrend() int { o := k.GetOpen() c := k.GetClose() if c.Compare(o) > 0 { return 1 } else if c.Compare(o) < 0 { return -1 } return 0 } func (k KLineWindow) Color() string { if k.GetTrend() > 0 { return style.GreenColor } else if k.GetTrend() < 0 { return style.RedColor } return style.GrayColor } // Mid price func (k KLineWindow) Mid() fixedpoint.Value { return k.GetHigh().Add(k.GetLow()).Div(Two) } // BounceUp returns true if it's green candle with open and close near high price func (k KLineWindow) BounceUp() bool { mid := k.Mid() trend := k.GetTrend() return trend > 0 && k.GetOpen().Compare(mid) > 0 && k.GetClose().Compare(mid) > 0 } // BounceDown returns true red candle with open and close near low price func (k KLineWindow) BounceDown() bool { mid := k.Mid() trend := k.GetTrend() return trend > 0 && k.GetOpen().Compare(mid) < 0 && k.GetClose().Compare(mid) < 0 } func (k *KLineWindow) Add(line KLine) { *k = append(*k, line) } func (k KLineWindow) Take(size int) KLineWindow { return k[:size] } func (k KLineWindow) Tail(size int) KLineWindow { length := len(k) if length <= size { win := make(KLineWindow, length) copy(win, k) return win } win := make(KLineWindow, size) copy(win, k[length-size:]) return win } // Truncate removes the old klines from the window func (k *KLineWindow) Truncate(size int) { if len(*k) <= size { return } end := len(*k) start := end - size if start < 0 { start = 0 } kn := (*k)[start:] *k = kn } func (k KLineWindow) GetBody() fixedpoint.Value { return k.GetChange() } func (k KLineWindow) GetThickness() fixedpoint.Value { out := k.GetChange().Div(k.GetMaxChange()) if out.Sign() < 0 { return out.Neg() } return out } func (k KLineWindow) GetUpperShadowRatio() fixedpoint.Value { out := k.GetUpperShadowHeight().Div(k.GetMaxChange()) if out.Sign() < 0 { return out.Neg() } return out } func (k KLineWindow) GetUpperShadowHeight() fixedpoint.Value { high := k.GetHigh() open := k.GetOpen() clos := k.GetClose() if open.Compare(clos) > 0 { return high.Sub(open) } return high.Sub(clos) } func (k KLineWindow) GetLowerShadowRatio() fixedpoint.Value { out := k.GetLowerShadowHeight().Div(k.GetMaxChange()) if out.Sign() < 0 { return out.Neg() } return out } func (k KLineWindow) GetLowerShadowHeight() fixedpoint.Value { low := k.GetLow() open := k.GetOpen() clos := k.GetClose() if open.Compare(clos) < 0 { return open.Sub(low) } return clos.Sub(low) } func (k KLineWindow) SlackAttachment() slack.Attachment { var first KLine var end KLine var windowSize = len(k) if windowSize > 0 { first = k[0] end = k[windowSize-1] } return slack.Attachment{ Text: fmt.Sprintf("*%s* KLineWindow %s x %d", first.Symbol, first.Interval, windowSize), Color: k.Color(), Fields: []slack.AttachmentField{ {Title: "Open", Value: k.GetOpen().FormatString(2), Short: true}, {Title: "High", Value: k.GetHigh().FormatString(2), Short: true}, {Title: "Low", Value: k.GetLow().FormatString(2), Short: true}, {Title: "Close", Value: k.GetClose().FormatString(2), Short: true}, {Title: "Mid", Value: k.Mid().FormatPercentage(2), Short: true}, { Title: "Change", Value: k.GetChange().FormatString(2), Short: true, }, { Title: "Max Change", Value: k.GetMaxChange().FormatString(2), Short: true, }, { Title: "Thickness", Value: k.GetThickness().FormatString(4), Short: true, }, { Title: "UpperShadowRatio", Value: k.GetUpperShadowRatio().FormatString(4), Short: true, }, { Title: "LowerShadowRatio", Value: k.GetLowerShadowRatio().FormatString(4), Short: true, }, }, Footer: fmt.Sprintf("Since %s til %s", first.StartTime, end.EndTime), FooterIcon: "", } } type KLineCallback func(k KLine) type KValueType int const ( kOpUnknown KValueType = iota kOpenValue kCloseValue kHighValue kLowValue kVolumeValue ) func (k *KLineWindow) High() Series { return &KLineSeries{ lines: k, kv: kHighValue, } } func (k *KLineWindow) Low() Series { return &KLineSeries{ lines: k, kv: kLowValue, } } func (k *KLineWindow) Open() Series { return &KLineSeries{ lines: k, kv: kOpenValue, } } func (k *KLineWindow) Close() Series { return &KLineSeries{ lines: k, kv: kCloseValue, } } func (k *KLineWindow) Volume() Series { return &KLineSeries{ lines: k, kv: kVolumeValue, } } type KLineSeries struct { lines *KLineWindow kv KValueType } func (k *KLineSeries) Index(i int) float64 { return k.Last(i) } func (k *KLineSeries) Last(i int) float64 { length := len(*k.lines) if length == 0 || length-i-1 < 0 { return 0 } switch k.kv { case kOpenValue: return (*k.lines)[length-i-1].GetOpen().Float64() case kCloseValue: return (*k.lines)[length-i-1].GetClose().Float64() case kLowValue: return (*k.lines)[length-i-1].GetLow().Float64() case kHighValue: return (*k.lines)[length-i-1].GetHigh().Float64() case kVolumeValue: return (*k.lines)[length-i-1].Volume.Float64() } return 0 } func (k *KLineSeries) Length() int { return len(*k.lines) } var _ Series = &KLineSeries{} func TradeWith(symbol string, f func(trade Trade)) func(trade Trade) { return func(trade Trade) { if symbol != "" && trade.Symbol != symbol { return } f(trade) } } func KLineWith(symbol string, interval Interval, callback KLineCallback) KLineCallback { return func(k KLine) { if k.Symbol != symbol || (k.Interval != "" && k.Interval != interval) { return } callback(k) } } type KLineValueMapper func(k KLine) float64 func KLineOpenPriceMapper(k KLine) float64 { return k.Open.Float64() } func KLineClosePriceMapper(k KLine) float64 { return k.Close.Float64() } func KLineTypicalPriceMapper(k KLine) float64 { return (k.High.Float64() + k.Low.Float64() + k.Close.Float64()) / 3. } func KLinePriceVolumeMapper(k KLine) float64 { return k.Close.Mul(k.Volume).Float64() } func KLineVolumeMapper(k KLine) float64 { return k.Volume.Float64() } func KLineHLC3Mapper(k KLine) float64 { return k.High.Add(k.Low).Add(k.Close).Div(Three).Float64() } func MapKLinePrice(kLines []KLine, f KLineValueMapper) (prices []float64) { for _, k := range kLines { prices = append(prices, f(k)) } return prices } func KLineLowPriceMapper(k KLine) float64 { return k.Low.Float64() } func KLineHighPriceMapper(k KLine) float64 { return k.High.Float64() }