qbtrade/pkg/types/kline.go
2024-06-27 22:42:38 +08:00

693 lines
16 KiB
Go

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()
}