2020-06-18 15:46:59 +00:00
|
|
|
package bbgo
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"github.com/adshao/go-binance"
|
|
|
|
"github.com/c9s/bbgo/pkg/bbgo/types"
|
2020-06-19 01:24:22 +00:00
|
|
|
"github.com/slack-go/slack"
|
2020-06-18 15:46:59 +00:00
|
|
|
"math"
|
2020-06-19 01:24:22 +00:00
|
|
|
"strconv"
|
2020-06-18 15:46:59 +00:00
|
|
|
)
|
|
|
|
|
2020-06-19 03:16:25 +00:00
|
|
|
const epsilon = 0.0000001
|
2020-06-18 15:46:59 +00:00
|
|
|
|
|
|
|
func NotZero(v float64) bool {
|
2020-06-19 03:16:25 +00:00
|
|
|
return math.Abs(v) > epsilon
|
2020-06-18 15:46:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type KLineDetector struct {
|
2020-06-19 03:21:17 +00:00
|
|
|
Name string `json:"name"`
|
2020-06-18 15:46:59 +00:00
|
|
|
Interval string `json:"interval"`
|
|
|
|
MinPriceChange float64 `json:"minPriceChange"`
|
|
|
|
MaxPriceChange float64 `json:"maxPriceChange"`
|
|
|
|
|
|
|
|
EnableMinThickness bool `json:"enableMinThickness"`
|
|
|
|
MinThickness float64 `json:"minThickness"`
|
|
|
|
|
|
|
|
EnableMaxShadowRatio bool `json:"enableMaxShadowRatio"`
|
|
|
|
MaxShadowRatio float64 `json:"maxShadowRatio"`
|
|
|
|
|
|
|
|
EnableLookBack bool `json:"enableLookBack"`
|
|
|
|
LookBackFrames int `json:"lookBackFrames"`
|
|
|
|
|
|
|
|
DelayMilliseconds int `json:"delayMsec"`
|
|
|
|
Stop bool `json:"stop"`
|
|
|
|
}
|
|
|
|
|
2020-06-19 01:24:22 +00:00
|
|
|
func (d *KLineDetector) SlackAttachment() slack.Attachment {
|
2020-06-19 03:21:17 +00:00
|
|
|
var name = "Detector "
|
|
|
|
|
|
|
|
if len(d.Name) > 0 {
|
|
|
|
name += " " + d.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
name += fmt.Sprintf(" %s", d.Interval)
|
|
|
|
|
2020-06-19 01:24:22 +00:00
|
|
|
if d.EnableLookBack {
|
2020-06-19 03:21:17 +00:00
|
|
|
name += fmt.Sprintf(" x %d", d.LookBackFrames)
|
2020-06-19 01:24:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if NotZero(d.MaxPriceChange) {
|
|
|
|
name += fmt.Sprintf(" MaxPriceChange %.2f ~ %.2f", d.MinPriceChange, d.MaxPriceChange)
|
|
|
|
} else {
|
|
|
|
name += fmt.Sprintf(" MaxPriceChange %.2f ~ NO LIMIT", d.MinPriceChange)
|
|
|
|
}
|
|
|
|
|
2020-06-19 03:21:17 +00:00
|
|
|
var fields = []slack.AttachmentField{
|
|
|
|
{
|
|
|
|
Title: "Interval",
|
|
|
|
Value: d.Interval,
|
|
|
|
Short: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Title: "MinMaxPriceChange",
|
|
|
|
Value: formatFloat(d.MinPriceChange, 2),
|
|
|
|
Short: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Title: "MaxMaxPriceChange",
|
|
|
|
Value: formatFloat(d.MaxPriceChange, 2),
|
|
|
|
Short: true,
|
|
|
|
},
|
|
|
|
}
|
2020-06-19 01:24:22 +00:00
|
|
|
|
|
|
|
if d.EnableMinThickness {
|
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "MinThickness",
|
|
|
|
Value: formatFloat(d.MinThickness, 4),
|
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if d.EnableMaxShadowRatio {
|
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "MaxShadowRatio",
|
|
|
|
Value: formatFloat(d.MaxShadowRatio, 4),
|
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if d.EnableLookBack {
|
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "LookBackFrames",
|
|
|
|
Value: strconv.Itoa(d.LookBackFrames),
|
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return slack.Attachment{
|
|
|
|
Color: "",
|
|
|
|
Fallback: "",
|
|
|
|
ID: 0,
|
|
|
|
Title: name,
|
|
|
|
Pretext: "",
|
|
|
|
Text: "",
|
|
|
|
Fields: fields,
|
|
|
|
Footer: "",
|
|
|
|
FooterIcon: "",
|
|
|
|
Ts: "",
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *KLineDetector) String() string {
|
|
|
|
var name = fmt.Sprintf("Detector %s (%f < x < %f)", d.Interval, d.MinPriceChange, d.MaxPriceChange)
|
2020-06-18 15:46:59 +00:00
|
|
|
|
|
|
|
if d.EnableMinThickness {
|
|
|
|
name += fmt.Sprintf(" [MinThickness: %f]", d.MinThickness)
|
|
|
|
}
|
|
|
|
|
|
|
|
if d.EnableLookBack {
|
|
|
|
name += fmt.Sprintf(" [LookBack: %d]", d.LookBackFrames)
|
|
|
|
}
|
|
|
|
if d.EnableMaxShadowRatio {
|
|
|
|
name += fmt.Sprintf(" [MaxShadowRatio: %f]", d.MaxShadowRatio)
|
|
|
|
}
|
|
|
|
return name
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *KLineDetector) NewOrder(e *KLineEvent, tradingCtx *TradingContext) Order {
|
|
|
|
var kline types.KLine = e.KLine
|
|
|
|
if d.EnableLookBack {
|
|
|
|
klineWindow := tradingCtx.KLineWindows[e.KLine.Interval]
|
|
|
|
if len(klineWindow) >= d.LookBackFrames {
|
|
|
|
kline = klineWindow.Tail(d.LookBackFrames)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var trend = kline.GetTrend()
|
|
|
|
|
|
|
|
var side binance.SideType
|
|
|
|
if trend < 0 {
|
|
|
|
side = binance.SideTypeBuy
|
|
|
|
} else if trend > 0 {
|
|
|
|
side = binance.SideTypeSell
|
|
|
|
}
|
|
|
|
|
2020-06-18 16:07:05 +00:00
|
|
|
var volume = tradingCtx.Market.FormatVolume(VolumeByPriceChange(tradingCtx.Market, kline.GetClose(), kline.GetChange(), side))
|
2020-06-18 15:46:59 +00:00
|
|
|
return Order{
|
|
|
|
Symbol: e.KLine.Symbol,
|
|
|
|
Type: binance.OrderTypeMarket,
|
|
|
|
Side: side,
|
|
|
|
VolumeStr: volume,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *KLineDetector) Detect(e *KLineEvent, tradingCtx *TradingContext) (reason string, ok bool) {
|
|
|
|
var kline types.KLine = e.KLine
|
|
|
|
|
|
|
|
// if the 3m trend is drop, do not buy, let 5m window handle it.
|
|
|
|
if d.EnableLookBack {
|
|
|
|
klineWindow := tradingCtx.KLineWindows[e.KLine.Interval]
|
|
|
|
if len(klineWindow) >= d.LookBackFrames {
|
|
|
|
kline = klineWindow.Tail(d.LookBackFrames)
|
|
|
|
}
|
|
|
|
/*
|
|
|
|
if lookbackKline.AllDrop() {
|
|
|
|
trader.Infof("1m window all drop down (%d frames), do not buy: %+v", d.LookBackFrames, klineWindow)
|
|
|
|
} else if lookbackKline.AllRise() {
|
|
|
|
trader.Infof("1m window all rise up (%d frames), do not sell: %+v", d.LookBackFrames, klineWindow)
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
}
|
|
|
|
|
|
|
|
var maxChange = math.Abs(kline.GetMaxChange())
|
|
|
|
|
|
|
|
if maxChange < d.MinPriceChange {
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
|
|
|
|
if NotZero(d.MaxPriceChange) && maxChange > d.MaxPriceChange {
|
2020-06-19 03:11:16 +00:00
|
|
|
return fmt.Sprintf("1m lookback window (x %d) max price change %f > %f", d.LookBackFrames, maxChange, d.MaxPriceChange), false
|
2020-06-18 15:46:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if d.EnableMinThickness {
|
|
|
|
if kline.GetThickness() < d.MinThickness {
|
2020-06-19 02:07:27 +00:00
|
|
|
return fmt.Sprintf("kline too thin %f (1m) < min kline thickness %f, skip to the next round", kline.GetThickness(), d.MinThickness), false
|
2020-06-18 15:46:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var trend = kline.GetTrend()
|
|
|
|
if d.EnableMaxShadowRatio {
|
|
|
|
if trend > 0 {
|
|
|
|
if kline.GetUpperShadowRatio() > d.MaxShadowRatio {
|
2020-06-19 02:07:27 +00:00
|
|
|
return fmt.Sprintf("kline upper shadow ratio too high. %f > %f (MaxShadowRatio)", kline.GetUpperShadowRatio(), d.MaxShadowRatio), false
|
2020-06-18 15:46:59 +00:00
|
|
|
}
|
|
|
|
} else if trend < 0 {
|
|
|
|
if kline.GetLowerShadowRatio() > d.MaxShadowRatio {
|
2020-06-19 02:07:27 +00:00
|
|
|
return fmt.Sprintf("kline lower shadow ratio too high. %f > %f (MaxShadowRatio)", kline.GetLowerShadowRatio(), d.MaxShadowRatio), false
|
2020-06-18 15:46:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if trend > 0 && kline.BounceUp() { // trend up, ignore bounce up
|
|
|
|
|
2020-06-19 02:07:27 +00:00
|
|
|
return fmt.Sprintf("bounce up, do not sell, kline mid: %f", kline.Mid()), false
|
2020-06-18 15:46:59 +00:00
|
|
|
|
|
|
|
} else if trend < 0 && kline.BounceDown() { // trend down, ignore bounce down
|
|
|
|
|
2020-06-19 02:07:27 +00:00
|
|
|
return fmt.Sprintf("bounce down, do not buy, kline mid: %f", kline.Mid()), false
|
2020-06-18 15:46:59 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
if toPrice(kline.GetClose()) == toPrice(kline.GetLow()) {
|
|
|
|
return fmt.Sprintf("close near the lowest price, the price might continue to drop."), false
|
|
|
|
}
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
return "", true
|
|
|
|
}
|