From 62aff676da60539221d8c803c16bc672d375826a Mon Sep 17 00:00:00 2001 From: Yo-An Lin Date: Tue, 9 Aug 2022 16:25:36 +0800 Subject: [PATCH] Revert "feature: add smart cancel to drift" --- .../components/TradingViewChart.tsx | 2 +- config/drift.yaml | 17 +- config/driftBTC.yaml | 33 +- go.mod | 5 +- go.sum | 9 - pkg/bbgo/interact.go | 21 - pkg/bbgo/notification.go | 28 - pkg/notifier/slacknotifier/slack.go | 9 - pkg/notifier/telegramnotifier/telegram.go | 43 - pkg/strategy/drift/strategy.go | 918 ++++-------------- pkg/types/indicator.go | 31 +- pkg/types/trade_stats.go | 2 - 12 files changed, 219 insertions(+), 899 deletions(-) diff --git a/apps/backtest-report/components/TradingViewChart.tsx b/apps/backtest-report/components/TradingViewChart.tsx index 31ed484ad..0532127f4 100644 --- a/apps/backtest-report/components/TradingViewChart.tsx +++ b/apps/backtest-report/components/TradingViewChart.tsx @@ -71,7 +71,7 @@ const parseOrder = () => { case "update_time": case "creation_time": case "time": - d[key] = moment(d[key], 'dddd, DD MMM YYYY h:mm:ss').toDate(); + d[key] = new Date(d[key]); break; } } diff --git a/config/drift.yaml b/config/drift.yaml index 412a3fe93..c5c684b56 100644 --- a/config/drift.yaml +++ b/config/drift.yaml @@ -35,21 +35,12 @@ exchangeStrategies: takeProfitFactor: 6 profitFactorWindow: 8 noTrailingStopLoss: false - trailingStopLossType: kline # stddev on high/low-source - hlVarianceMultiplier: 0.23 + hlVarianceMultiplier: 0.22 hlRangeWindow: 5 - smootherWindow: 1 - fisherTransformWindow: 9 + smootherWindow: 2 + fisherTransformWindow: 8 atrWindow: 14 - # orders not been traded will be canceled after `pendingMinutes` minutes - pendingMinutes: 4 - noRebalance: true - trendWindow: 12 - rebalanceFilter: 1.5 - - trailingActivationRatio: [0.008, 0.015] - trailingCallbackRate: [0.002, 0.001] generateGraph: true graphPNLDeductFee: true @@ -88,7 +79,7 @@ sync: backtest: startTime: "2022-01-01" - endTime: "2022-07-30" + endTime: "2022-07-29" symbols: - ETHBUSD sessions: [binance] diff --git a/config/driftBTC.yaml b/config/driftBTC.yaml index c32957dff..c199e0b0c 100644 --- a/config/driftBTC.yaml +++ b/config/driftBTC.yaml @@ -24,7 +24,7 @@ exchangeStrategies: - on: binance drift: canvasPath: "./output.png" - symbol: BTCUSDT + symbol: BTCBUSD # kline interval for indicators interval: 15m window: 2 @@ -32,28 +32,15 @@ exchangeStrategies: source: close predictOffset: 2 noTrailingStopLoss: false - trailingStopLossType: kline # stddev on high/low-source - hlVarianceMultiplier: 0.23 + hlVarianceMultiplier: 0.22 hlRangeWindow: 5 - smootherWindow: 1 + smootherWindow: 2 fisherTransformWindow: 9 # the init value of takeProfitFactor Series, the coefficient of ATR as TP - takeProfitFactor: 1.2 - profitFactorWindow: 5 - atrWindow: 12 - # orders not been traded will be canceled after `pendingMinutes` minutes - pendingMinutes: 3 - noRebalance: true - trendWindow: 12 - rebalanceFilter: 3 - - # ActivationRatio should be increasing order - # when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop - #trailingActivationRatio: [0.007, 0.015, 0.02, 0.05] - trailingActivationRatio: [0.007, 0.011] - #trailingCallbackRate: [0.005, 0.003, 0.002, 0.001] - trailingCallbackRate: [0.002, 0.001] + takeProfitFactor: 6 + profitFactorWindow: 8 + atrWindow: 14 generateGraph: true graphPNLDeductFee: true @@ -113,13 +100,13 @@ sync: sessions: - binance symbols: - - BTCUSDT + - BTCBUSD backtest: startTime: "2022-01-01" - endTime: "2022-07-26" + endTime: "2022-07-29" symbols: - - BTCUSDT + - BTCBUSD sessions: [binance] accounts: binance: @@ -127,4 +114,4 @@ backtest: takerFeeRate: 0.00075 balances: BTC: 1 - USDT: 5000.0 + BUSD: 5000.0 diff --git a/go.mod b/go.mod index bfb2ab6c4..f47f5b63b 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/spf13/cobra v1.1.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 - github.com/stretchr/testify v1.7.4 + github.com/stretchr/testify v1.7.0 github.com/valyala/fastjson v1.5.1 github.com/wcharczuk/go-chart/v2 v2.1.0 github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 @@ -83,7 +83,6 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/jedib0t/go-pretty/v6 v6.3.6 // indirect github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect github.com/json-iterator/go v1.1.11 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect @@ -94,7 +93,7 @@ require ( github.com/magiconair/properties v1.8.4 // indirect github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-runewidth v0.0.12 // indirect github.com/mattn/go-sqlite3 v1.14.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect diff --git a/go.sum b/go.sum index 8e792e62e..4effe7aca 100644 --- a/go.sum +++ b/go.sum @@ -352,8 +352,6 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jedib0t/go-pretty/v6 v6.3.6 h1:A6w2BuyPMtf7M82BGRBys9bAba2C26ZX9lrlrZ7uH6U= -github.com/jedib0t/go-pretty/v6 v6.3.6/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4= github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -444,8 +442,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= @@ -507,7 +503,6 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -604,7 +599,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -614,9 +608,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= -github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tebeka/strftime v0.1.3 h1:5HQXOqWKYRFfNyBMNVc9z5+QzuBtIXy03psIhtdJYto= diff --git a/pkg/bbgo/interact.go b/pkg/bbgo/interact.go index 820097219..3ace4cf72 100644 --- a/pkg/bbgo/interact.go +++ b/pkg/bbgo/interact.go @@ -51,27 +51,6 @@ func NewCoreInteraction(environment *Environment, trader *Trader) *CoreInteracti } } -type SimpleInteraction struct { - Command string - Description string - F interface{} - Cmd *interact.Command -} - -func (it *SimpleInteraction) Commands(i *interact.Interact) { - it.Cmd = i.PrivateCommand(it.Command, it.Description, it.F) -} - -func RegisterCommand(command, desc string, f interface{}) *interact.Command { - it := &SimpleInteraction{ - Command: command, - Description: desc, - F: f, - } - interact.AddCustomInteraction(it) - return it.Cmd -} - func getStrategySignatures(exchangeStrategies map[string]SingleExchangeStrategy) []string { var strategies []string for signature := range exchangeStrategies { diff --git a/pkg/bbgo/notification.go b/pkg/bbgo/notification.go index 2d8420297..db63d7448 100644 --- a/pkg/bbgo/notification.go +++ b/pkg/bbgo/notification.go @@ -1,8 +1,6 @@ package bbgo import ( - "bytes" - "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/util" @@ -22,19 +20,9 @@ func NotifyTo(channel string, obj interface{}, args ...interface{}) { Notification.NotifyTo(channel, obj, args...) } -func SendPhoto(buffer *bytes.Buffer) { - Notification.SendPhoto(buffer) -} - -func SendPhotoTo(channel string, buffer *bytes.Buffer) { - Notification.SendPhotoTo(channel, buffer) -} - type Notifier interface { NotifyTo(channel string, obj interface{}, args ...interface{}) Notify(obj interface{}, args ...interface{}) - SendPhotoTo(channel string, buffer *bytes.Buffer) - SendPhoto(buffer *bytes.Buffer) } type NullNotifier struct{} @@ -43,10 +31,6 @@ func (n *NullNotifier) NotifyTo(channel string, obj interface{}, args ...interfa func (n *NullNotifier) Notify(obj interface{}, args ...interface{}) {} -func (n *NullNotifier) SendPhoto(buffer *bytes.Buffer) {} - -func (n *NullNotifier) SendPhotoTo(channel string, buffer *bytes.Buffer) {} - type Notifiability struct { notifiers []Notifier SessionChannelRouter *PatternChannelRouter `json:"-"` @@ -99,15 +83,3 @@ func (m *Notifiability) NotifyTo(channel string, obj interface{}, args ...interf n.NotifyTo(channel, obj, args...) } } - -func (m *Notifiability) SendPhoto(buffer *bytes.Buffer) { - for _, n := range m.notifiers { - n.SendPhoto(buffer) - } -} - -func (m *Notifiability) SendPhotoTo(channel string, buffer *bytes.Buffer) { - for _, n := range m.notifiers { - n.SendPhotoTo(channel, buffer) - } -} diff --git a/pkg/notifier/slacknotifier/slack.go b/pkg/notifier/slacknotifier/slack.go index 333e3f1e5..69e229257 100644 --- a/pkg/notifier/slacknotifier/slack.go +++ b/pkg/notifier/slacknotifier/slack.go @@ -1,7 +1,6 @@ package slacknotifier import ( - "bytes" "context" "fmt" "time" @@ -151,14 +150,6 @@ func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{} } } -func (n *Notifier) SendPhoto(buffer *bytes.Buffer) { - n.SendPhotoTo(n.channel, buffer) -} - -func (n *Notifier) SendPhotoTo(channel string, buffer *bytes.Buffer) { - // TODO -} - /* func (n *Notifier) NotifyTrade(trade *types.Trade) { _, _, err := n.client.PostMessageContext(context.Background(), n.TradeChannel, diff --git a/pkg/notifier/telegramnotifier/telegram.go b/pkg/notifier/telegramnotifier/telegram.go index 0afe4566f..36927319e 100644 --- a/pkg/notifier/telegramnotifier/telegram.go +++ b/pkg/notifier/telegramnotifier/telegram.go @@ -1,7 +1,6 @@ package telegramnotifier import ( - "bytes" "fmt" "reflect" "strconv" @@ -130,48 +129,6 @@ func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{} } } -func (n *Notifier) SendPhoto(buffer *bytes.Buffer) { - n.SendPhotoTo("", buffer) -} - -func photoFromBuffer(buffer *bytes.Buffer) telebot.InputMedia { - reader := bytes.NewReader(buffer.Bytes()) - return &telebot.Photo{ - File: telebot.FromReader(reader), - } -} - -func (n *Notifier) SendPhotoTo(channel string, buffer *bytes.Buffer) { - if n.broadcast { - if n.Subscribers == nil { - return - } - - for chatID := range n.Subscribers { - chat, err := n.bot.ChatByID(strconv.FormatInt(chatID, 10)) - if err != nil { - log.WithError(err).Error("can not get chat by ID") - continue - } - album := telebot.Album{ - photoFromBuffer(buffer), - } - if _, err := n.bot.SendAlbum(chat, album); err != nil { - log.WithError(err).Error("failed to send message") - } - } - } else if n.Chats != nil { - for _, chat := range n.Chats { - album := telebot.Album{ - photoFromBuffer(buffer), - } - if _, err := n.bot.SendAlbum(chat, album); err != nil { - log.WithError(err).Error("telegram send error") - } - } - } -} - func (n *Notifier) AddChat(c *telebot.Chat) { if n.Chats == nil { n.Chats = make(map[int64]*telebot.Chat) diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 914cdae2c..f3a31f137 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -1,15 +1,13 @@ package drift import ( - "bytes" + "bufio" "context" + "encoding/json" "errors" "fmt" - "io" "math" "os" - "reflect" - "sort" "strings" "sync" @@ -20,20 +18,12 @@ import ( "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" - "github.com/c9s/bbgo/pkg/interact" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" - "github.com/jedib0t/go-pretty/v6/table" - "github.com/jedib0t/go-pretty/v6/text" ) const ID = "drift" -const DDriftFilterNeg = -0.7 -const DDriftFilterPos = 0.7 -const DriftFilterNeg = -1.85 -const DriftFilterPos = 1.85 - var log = logrus.WithField("strategy", ID) var Four fixedpoint.Value = fixedpoint.NewFromInt(4) var Three fixedpoint.Value = fixedpoint.NewFromInt(3) @@ -59,21 +49,13 @@ type Strategy struct { *types.ProfitStats `persistence:"profit_stats"` *types.TradeStats `persistence:"trade_stats"` - p *types.Position - - trendLine types.UpdatableSeriesExtend - ma types.UpdatableSeriesExtend - stdevHigh *indicator.StdDev - stdevLow *indicator.StdDev - drift *DriftMA - drift1m *DriftMA - atr *indicator.ATR - midPrice fixedpoint.Value - lock sync.RWMutex - minutesCounter int - orderPendingCounter map[uint64]int - - beta float64 + ma types.UpdatableSeriesExtend + stdevHigh *indicator.StdDev + stdevLow *indicator.StdDev + drift *DriftMA + atr *indicator.ATR + midPrice fixedpoint.Value + lock sync.RWMutex // This stores the maximum TP coefficient of ATR multiplier of each entry point takeProfitFactor types.UpdatableSeriesExtend @@ -86,22 +68,15 @@ type Strategy struct { PredictOffset int `json:"predictOffset"` HighLowVarianceMultiplier float64 `json:"hlVarianceMultiplier"` NoTrailingStopLoss bool `json:"noTrailingStopLoss"` - TrailingStopLossType string `json:"trailingStopLossType"` // trailing stop sources. Possible options are `kline` for 1m kline and `realtime` from order updates HLRangeWindow int `json:"hlRangeWindow"` SmootherWindow int `json:"smootherWindow"` FisherTransformWindow int `json:"fisherTransformWindow"` ATRWindow int `json:"atrWindow"` - PendingMinutes int `json:"pendingMinutes"` // if order not be traded for pendingMinutes of time, cancel it. - NoRebalance bool `json:"noRebalance"` // disable rebalance - TrendWindow int `json:"trendWindow"` // trendLine is used for rebalancing the position. When trendLine goes up, hold base, otherwise hold quote - RebalanceFilter float64 `json:"rebalanceFilter"` // beta filter on the Linear Regression of trendLine - TrailingCallbackRate []float64 `json:"trailingCallbackRate"` - TrailingActivationRatio []float64 `json:"trailingActivationRatio"` - buyPrice float64 `persistence:"buy_price"` - sellPrice float64 `persistence:"sell_price"` - highestPrice float64 `persistence:"highest_price"` - lowestPrice float64 `persistence:"lowest_price"` + buyPrice float64 + sellPrice float64 + highestPrice float64 + lowestPrice float64 // This is not related to trade but for statistics graph generation // Will deduct fee in percentage from every trade @@ -119,122 +94,32 @@ type Strategy struct { getSource SourceFunc } -type jsonStruct struct { - key string - json string - tp string - value interface{} -} -type jsonArr []jsonStruct - -func (a jsonArr) Len() int { return len(a) } -func (a jsonArr) Less(i, j int) bool { return a[i].key < a[j].key } -func (a jsonArr) Swap(i, j int) { a[i], a[j] = a[j], a[i] } - -func (s *Strategy) Print(f io.Writer, pretty bool, withColor ...bool) { - //b, _ := json.MarshalIndent(s.ExitMethods, " ", " ") - - t := table.NewWriter() - style := table.Style{ - Name: "StyleRounded", - Box: table.StyleBoxRounded, - Color: table.ColorOptionsDefault, - Format: table.FormatOptionsDefault, - HTML: table.DefaultHTMLOptions, - Options: table.OptionsDefault, - Title: table.TitleOptionsDefault, - } - var hiyellow func(io.Writer, string, ...interface{}) - if len(withColor) > 0 && withColor[0] { - if pretty { - style.Color = table.ColorOptionsYellowWhiteOnBlack - style.Color.Row = text.Colors{text.FgHiYellow, text.BgHiBlack} - style.Color.RowAlternate = text.Colors{text.FgYellow, text.BgBlack} - } - hiyellow = color.New(color.FgHiYellow).FprintfFunc() - } else { - hiyellow = func(a io.Writer, format string, args ...interface{}) { - fmt.Fprintf(a, format, args...) - } - } - if pretty { - t.SetOutputMirror(f) - t.SetStyle(style) - t.AppendHeader(table.Row{"json", "struct field name", "type", "value"}) - } +func (s *Strategy) Print(o *os.File) { + f := bufio.NewWriter(o) + defer f.Flush() + b, _ := json.MarshalIndent(s.ExitMethods, " ", " ") + hiyellow := color.New(color.FgHiYellow).FprintfFunc() hiyellow(f, "------ %s Settings ------\n", s.InstanceID()) - - embeddedWhiteSet := map[string]struct{}{"Window": {}, "Interval": {}, "Symbol": {}} - redundantSet := map[string]struct{}{} - var rows []table.Row - val := reflect.ValueOf(*s) - var values jsonArr - for i := 0; i < val.Type().NumField(); i++ { - t := val.Type().Field(i) - if !t.IsExported() { - continue - } - fieldName := t.Name - switch jsonTag := t.Tag.Get("json"); jsonTag { - case "-": - case "": - if t.Anonymous { - var target reflect.Type - if t.Type.Kind() == reflect.Pointer { - target = t.Type.Elem() - } else { - target = t.Type - } - for j := 0; j < target.NumField(); j++ { - tt := target.Field(j) - if !tt.IsExported() { - continue - } - fieldName := tt.Name - if _, ok := embeddedWhiteSet[fieldName]; !ok { - continue - } - - if jtag := tt.Tag.Get("json"); jtag != "" && jtag != "-" { - name := strings.Split(jtag, ",")[0] - if _, ok := redundantSet[name]; ok { - continue - } - redundantSet[name] = struct{}{} - var value interface{} - if t.Type.Kind() == reflect.Pointer { - value = val.Field(i).Elem().Field(j).Interface() - } else { - value = val.Field(i).Field(j).Interface() - } - values = append(values, jsonStruct{key: fieldName, json: name, tp: tt.Type.String(), value: value}) - } - } - } - default: - name := strings.Split(jsonTag, ",")[0] - if _, ok := redundantSet[name]; ok { - continue - } - redundantSet[name] = struct{}{} - values = append(values, jsonStruct{key: fieldName, json: name, tp: t.Type.String(), value: val.Field(i).Interface()}) - } - } - sort.Sort(values) - for _, value := range values { - if pretty { - rows = append(rows, table.Row{value.json, value.key, value.tp, value.value}) - } else { - hiyellow(f, "%s: %v\n", value.json, value.value) - } - } - if pretty { - rows = append(rows, table.Row{"takeProfitFactor(last)", "takeProfitFactor", "float64", s.takeProfitFactor.Last()}) - t.AppendRows(rows) - t.Render() - } else { - hiyellow(f, "takeProfitFactor(last): %f\n", s.takeProfitFactor.Last()) - } + hiyellow(f, "generateGraph: %v\n", s.GenerateGraph) + hiyellow(f, "canvasPath: %s\n", s.CanvasPath) + hiyellow(f, "graphPNLPath: %s\n", s.GraphPNLPath) + hiyellow(f, "graphCumPNLPath: %s\n", s.GraphCumPNLPath) + hiyellow(f, "source: %s\n", s.Source) + hiyellow(f, "stoploss: %v\n", s.StopLoss) + hiyellow(f, "takeProfitFactor(last): %f, (init): %f\n", s.takeProfitFactor.Last(), s.TakeProfitFactor) + hiyellow(f, "profitFactorWindow: %d\n", s.ProfitFactorWindow) + hiyellow(f, "predictOffset: %d\n", s.PredictOffset) + hiyellow(f, "exits:\n %s\n", string(b)) + hiyellow(f, "symbol: %s\n", s.Symbol) + hiyellow(f, "interval: %s\n", s.Interval) + hiyellow(f, "window: %d\n", s.Window) + hiyellow(f, "noTrailingStopLoss: %v\n", s.NoTrailingStopLoss) + hiyellow(f, "hlVarianceMutiplier: %f\n", s.HighLowVarianceMultiplier) + hiyellow(f, "hlRangeWindow: %d\n", s.HLRangeWindow) + hiyellow(f, "smootherWindow: %d\n", s.SmootherWindow) + hiyellow(f, "fisherTransformWindow: %d\n", s.FisherTransformWindow) + hiyellow(f, "atrWindow: %d\n", s.ATRWindow) + hiyellow(f, "\n") } func (s *Strategy) ID() string { @@ -242,7 +127,7 @@ func (s *Strategy) ID() string { } func (s *Strategy) InstanceID() string { - return fmt.Sprintf("%s:%s:%v", ID, "" /*s.Symbol*/, bbgo.IsBackTesting) + return fmt.Sprintf("%s:%s", ID, s.Symbol) } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { @@ -264,13 +149,13 @@ func (s *Strategy) CurrentPosition() *types.Position { } func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { - order := s.p.NewMarketCloseOrder(percentage) + order := s.Position.NewMarketCloseOrder(percentage) if order == nil { return nil } order.Tag = "close" order.TimeInForce = "" - balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + balances := s.Session.GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Available price := s.getLastPrice() if order.Side == types.SideTypeBuy { @@ -371,7 +256,7 @@ func (s *DriftMA) TestUpdate(v float64) *DriftMA { return out } -func (s *Strategy) initIndicators(kline *types.KLine, priceLines *types.Queue) error { +func (s *Strategy) initIndicators() error { s.ma = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.HLRangeWindow}} s.stdevHigh = &indicator.StdDev{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.HLRangeWindow}} s.stdevLow = &indicator.StdDev{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.HLRangeWindow}} @@ -388,24 +273,9 @@ func (s *Strategy) initIndicators(kline *types.KLine, priceLines *types.Queue) e }, } s.drift.SeriesBase.Series = s.drift - s.drift1m = &DriftMA{ - drift: &indicator.Drift{ - MA: &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1m, Window: 2}}, - IntervalWindow: types.IntervalWindow{Interval: types.Interval1m, Window: 2}, - }, - ma1: &indicator.EWMA{ - IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 24}, - }, - ma2: &indicator.FisherTransform{ - IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.FisherTransformWindow * 15}, - }, - } - s.drift1m.SeriesBase.Series = s.drift1m s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.ATRWindow}} s.takeProfitFactor = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.ProfitFactorWindow}} - s.trendLine = &indicator.EWMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.TrendWindow}} - - for i := 0; i <= s.ProfitFactorWindow; i++ { + for i := 0; i < s.ProfitFactorWindow; i++ { s.takeProfitFactor.Update(s.TakeProfitFactor) } store, _ := s.Session.MarketDataStore(s.Symbol) @@ -422,94 +292,11 @@ func (s *Strategy) initIndicators(kline *types.KLine, priceLines *types.Queue) e s.stdevHigh.Update(high - s.ma.Last()) s.stdevLow.Update(s.ma.Last() - low) s.drift.Update(source) - s.trendLine.Update(source) s.atr.PushK(kline) - priceLines.Update(source) - } - if kline != nil && klines != nil { - kline.Set(&(*klines)[len(*klines)-1]) - } - klines, ok = store.KLinesOfInterval(types.Interval1m) - if !ok { - return errors.New("klines not exists") - } - for _, kline := range *klines { - source := s.getSource(&kline).Float64() - s.drift1m.Update(source) } return nil } -func (s *Strategy) smartCancel(ctx context.Context, pricef, atr, takeProfitFactor float64) (int, error) { - nonTraded := s.GeneralOrderExecutor.ActiveMakerOrders().Orders() - if len(nonTraded) > 0 { - if len(nonTraded) > 1 { - log.Errorf("should only have one order to cancel, got %d", len(nonTraded)) - } - toCancel := false - - drift := s.drift1m.Array(2) - for _, order := range nonTraded { - if s.minutesCounter-s.orderPendingCounter[order.OrderID] > s.PendingMinutes { - if order.Side == types.SideTypeBuy && drift[1] > drift[0] { - continue - } else if order.Side == types.SideTypeSell && drift[1] < drift[0] { - continue - } - toCancel = true - } else if order.Side == types.SideTypeBuy { - // 75% of the probability - if order.Price.Float64()+s.stdevHigh.Last()*2 <= pricef { - toCancel = true - } - } else if order.Side == types.SideTypeSell { - // 75% of the probability - if order.Price.Float64()-s.stdevLow.Last()*2 >= pricef { - toCancel = true - } - } else { - panic("not supported side for the order") - } - } - if toCancel { - err := s.GeneralOrderExecutor.GracefulCancel(ctx) - // TODO: clean orderPendingCounter on cancel/trade - if err == nil { - for _, order := range nonTraded { - delete(s.orderPendingCounter, order.OrderID) - } - } - return 0, err - } - } - return len(nonTraded), nil -} - -func (s *Strategy) trailingCheck(price float64, direction string) bool { - avg := s.buyPrice + s.sellPrice - if s.highestPrice > 0 && s.highestPrice < price { - s.highestPrice = price - } - if s.lowestPrice > 0 && s.lowestPrice > price { - s.lowestPrice = price - } - isShort := direction == "short" - for i := len(s.TrailingCallbackRate) - 1; i >= 0; i-- { - trailingCallbackRate := s.TrailingCallbackRate[i] - trailingActivationRatio := s.TrailingActivationRatio[i] - if isShort { - if (avg-s.lowestPrice)/s.lowestPrice > trailingActivationRatio { - return (price-s.lowestPrice)/s.lowestPrice > trailingCallbackRate - } - } else { - if (s.highestPrice-avg)/avg > trailingActivationRatio { - return (s.highestPrice-price)/price > trailingCallbackRate - } - } - } - return false -} - func (s *Strategy) initTickerFunctions(ctx context.Context) { if s.IsBackTesting() { s.getLastPrice = func() fixedpoint.Value { @@ -539,48 +326,40 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { } else { return } - - defer s.lock.Unlock() - // for trailing stoploss during the realtime - if s.NoTrailingStopLoss || s.TrailingStopLossType == "kline" { - return - } - - atr = s.atr.Last() - takeProfitFactor := s.takeProfitFactor.Predict(2) - numPending := 0 - var err error - if numPending, err = s.smartCancel(ctx, pricef, atr, takeProfitFactor); err != nil { - log.WithError(err).Errorf("cannot cancel orders") - return - } - if numPending > 0 { - return - } - if s.highestPrice > 0 && s.highestPrice < pricef { s.highestPrice = pricef } if s.lowestPrice > 0 && s.lowestPrice > pricef { s.lowestPrice = pricef } - avg = s.buyPrice + s.sellPrice - exitShortCondition := ( /*avg*(1.+stoploss) <= pricef || (ddrift > 0 && drift > DDriftFilterPos) ||*/ avg-atr*takeProfitFactor >= pricef || - s.trailingCheck(pricef, "short")) && - (s.p.IsShort() && !s.p.IsDust(price)) - exitLongCondition := ( /*avg*(1.-stoploss) >= pricef || (ddrift < 0 && drift < DDriftFilterNeg) ||*/ avg+atr*takeProfitFactor <= pricef || - s.trailingCheck(pricef, "long")) && - (!s.p.IsLong() && !s.p.IsDust(price)) + // for trailing stoploss during the realtime + if s.NoTrailingStopLoss || s.GeneralOrderExecutor.ActiveMakerOrders().NumOfOrders() > 0 { + s.lock.Unlock() + return + } + atr = s.atr.Last() + avg = s.buyPrice + s.sellPrice + d := s.drift.TestUpdate(pricef) + drift := d.Last() + ddrift := d.drift.Last() + takeProfitFactor := s.takeProfitFactor.Predict(2) + exitShortCondition := ( /*avg+atr/2 <= pricef || avg*(1.+stoploss) <= pricef ||*/ (drift > 0 && ddrift > 0.6) || avg-atr*takeProfitFactor >= pricef || + ((pricef-s.lowestPrice)/s.lowestPrice > 0.003 && (avg-s.lowestPrice)/s.lowestPrice > 0.015)) && + (s.Position.IsShort() && !s.Position.IsDust(price)) + exitLongCondition := ( /*avg-atr/2 >= pricef || avg*(1.-stoploss) >= pricef ||*/ (drift < 0 && ddrift < -0.6) || avg+atr*takeProfitFactor <= pricef || + ((s.highestPrice-pricef)/pricef > 0.003 && (s.highestPrice-avg)/avg > 0.015)) && + (!s.Position.IsLong() && !s.Position.IsDust(price)) if exitShortCondition || exitLongCondition { if exitLongCondition && s.highestPrice > avg { - s.takeProfitFactor.Update((s.highestPrice - avg) / atr * 1.5) + s.takeProfitFactor.Update((s.highestPrice - avg) / atr * 4) } else if exitShortCondition && avg > s.lowestPrice { - s.takeProfitFactor.Update((avg - s.lowestPrice) / atr * 1.5) + s.takeProfitFactor.Update((avg - s.lowestPrice) / atr * 4) } - log.Infof("Close position by orderbook changes") _ = s.ClosePosition(ctx, fixedpoint.One) } + s.lock.Unlock() + }) s.getLastPrice = func() (lastPrice fixedpoint.Value) { var ok bool @@ -589,7 +368,6 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { lastPrice, ok = s.Session.LastPrice(s.Symbol) if !ok { log.Error("cannot get lastprice") - s.lock.RUnlock() return lastPrice } } else { @@ -602,13 +380,12 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { } -func (s *Strategy) DrawIndicators(time types.Time, priceLine types.SeriesExtend, zeroPoints types.Series) *types.Canvas { +func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit types.Series, cumProfit types.Series, zeroPoints types.Series) { canvas := types.NewCanvas(s.InstanceID(), s.Interval) Length := priceLine.Length() if Length > 300 { Length = 300 } - log.Infof("draw indicators with %d data", Length) mean := priceLine.Mean(Length) highestPrice := priceLine.Minus(mean).Abs().Highest(Length) highestDrift := s.drift.Abs().Highest(Length) @@ -622,50 +399,6 @@ func (s *Strategy) DrawIndicators(time types.Time, priceLine types.SeriesExtend, canvas.Plot("zero", types.NumberSeries(mean), time, Length) canvas.Plot("price", priceLine, time, Length) canvas.Plot("zeroPoint", zeroPoints, time, Length) - return canvas -} - -func (s *Strategy) DrawPNL(profit types.Series) *types.Canvas { - canvas := types.NewCanvas(s.InstanceID()) - log.Errorf("pnl Highest: %f, Lowest: %f", types.Highest(profit, profit.Length()), types.Lowest(profit, profit.Length())) - length := profit.Length() - if s.GraphPNLDeductFee { - canvas.PlotRaw("pnl % (with Fee Deducted)", profit, length) - } else { - canvas.PlotRaw("pnl %", profit, length) - } - canvas.YAxis = chart.YAxis{ - ValueFormatter: func(v interface{}) string { - if vf, isFloat := v.(float64); isFloat { - return fmt.Sprintf("%.4f", vf) - } - return "" - }, - } - canvas.PlotRaw("1", types.NumberSeries(1), length) - return canvas -} - -func (s *Strategy) DrawCumPNL(cumProfit types.Series) *types.Canvas { - canvas := types.NewCanvas(s.InstanceID()) - if s.GraphPNLDeductFee { - canvas.PlotRaw("cummulative pnl % (with Fee Deducted)", cumProfit, cumProfit.Length()) - } else { - canvas.PlotRaw("cummulative pnl %", cumProfit, cumProfit.Length()) - } - canvas.YAxis = chart.YAxis{ - ValueFormatter: func(v interface{}) string { - if vf, isFloat := v.(float64); isFloat { - return fmt.Sprintf("%.4f", vf) - } - return "" - }, - } - return canvas -} - -func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit types.Series, cumProfit types.Series, zeroPoints types.Series) { - canvas := s.DrawIndicators(time, priceLine, zeroPoints) f, err := os.Create(s.CanvasPath) if err != nil { log.WithError(err).Errorf("cannot create on %s", s.CanvasPath) @@ -676,7 +409,12 @@ func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit ty log.WithError(err).Errorf("cannot render in drift") } - canvas = s.DrawPNL(profit) + canvas = types.NewCanvas(s.InstanceID()) + if s.GraphPNLDeductFee { + canvas.PlotRaw("pnl % (with Fee Deducted)", profit, profit.Length()) + } else { + canvas.PlotRaw("pnl %", profit, profit.Length()) + } f, err = os.Create(s.GraphPNLPath) if err != nil { log.WithError(err).Errorf("open pnl") @@ -687,7 +425,12 @@ func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit ty log.WithError(err).Errorf("render pnl") } - canvas = s.DrawCumPNL(cumProfit) + canvas = types.NewCanvas(s.InstanceID()) + if s.GraphPNLDeductFee { + canvas.PlotRaw("cummulative pnl % (with Fee Deducted)", cumProfit, cumProfit.Length()) + } else { + canvas.PlotRaw("cummulative pnl %", cumProfit, cumProfit.Length()) + } f, err = os.Create(s.GraphCumPNLPath) if err != nil { log.WithError(err).Errorf("open cumpnl") @@ -699,120 +442,11 @@ func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit ty } } -// Sending new rebalance orders cost too much. -// Modify the position instead to expect the strategy itself rebalance on Close -func (s *Strategy) Rebalance(ctx context.Context, orderTagHistory map[uint64]string) { - price := s.getLastPrice() - _, beta := types.LinearRegression(s.trendLine, 3) - if math.Abs(beta) > s.RebalanceFilter && math.Abs(s.beta) > s.RebalanceFilter || math.Abs(s.beta) < s.RebalanceFilter && math.Abs(beta) < s.RebalanceFilter { - return - } - balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() - baseBalance := balances[s.Market.BaseCurrency].Total() - quoteBalance := balances[s.Market.QuoteCurrency].Total() - total := baseBalance.Add(quoteBalance.Div(price)) - percentage := fixedpoint.One.Sub(Delta) - log.Infof("rebalance beta %f %v", beta, s.p) - if beta > s.RebalanceFilter { - if total.Mul(percentage).Compare(baseBalance) > 0 { - q := total.Mul(percentage).Sub(baseBalance) - s.p.Lock() - defer s.p.Unlock() - s.p.Base = q.Neg() - s.p.Quote = q.Mul(price) - s.p.AverageCost = price - } - /*if total.Mul(percentage).Compare(baseBalance) > 0 { - q := total.Mul(percentage).Sub(baseBalance) - if s.Market.IsDustQuantity(q, price) { - return - } - err := s.GeneralOrderExecutor.GracefulCancel(ctx) - if err != nil { - panic(fmt.Sprintf("%s", err)) - } - orders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeMarket, - Price: price, - Quantity: q, - Tag: "rebalance", - }) - if err == nil { - orderTagHistory[orders[0].OrderID] = "rebalance" - } else { - log.WithError(err).Errorf("rebalance %v %v %v %v", total, q, balances[s.Market.QuoteCurrency].Available, quoteBalance) - } - }*/ - } else if beta <= -s.RebalanceFilter { - if total.Mul(percentage).Compare(quoteBalance.Div(price)) > 0 { - q := total.Mul(percentage).Sub(quoteBalance.Div(price)) - s.p.Lock() - defer s.p.Unlock() - s.p.Base = q - s.p.Quote = q.Mul(price).Neg() - s.p.AverageCost = price - } - /*if total.Mul(percentage).Compare(quoteBalance.Div(price)) > 0 { - q := total.Mul(percentage).Sub(quoteBalance.Div(price)) - if s.Market.IsDustQuantity(q, price) { - return - } - err := s.GeneralOrderExecutor.GracefulCancel(ctx) - if err != nil { - panic(fmt.Sprintf("%s", err)) - } - orders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeMarket, - Price: price, - Quantity: q, - Tag: "rebalance", - }) - if err == nil { - orderTagHistory[orders[0].OrderID] = "rebalance" - } else { - log.WithError(err).Errorf("rebalance %v %v %v %v", total, q, balances[s.Market.BaseCurrency].Available, baseBalance) - } - }*/ - } else { - if total.Div(Two).Compare(quoteBalance.Div(price)) > 0 { - q := total.Div(Two).Sub(quoteBalance.Div(price)) - s.p.Lock() - defer s.p.Unlock() - s.p.Base = q - s.p.Quote = q.Mul(price).Neg() - s.p.AverageCost = price - } else if total.Div(Two).Compare(baseBalance) > 0 { - q := total.Div(Two).Sub(baseBalance) - s.p.Lock() - defer s.p.Unlock() - s.p.Base = q.Neg() - s.p.Quote = q.Mul(price) - s.p.AverageCost = price - } else { - s.p.Lock() - defer s.p.Unlock() - s.p.Reset() - } - } - log.Infof("rebalanceafter %v %v %v", baseBalance, quoteBalance, s.p) - s.beta = beta -} - func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() // Will be set by persistence if there's any from DB if s.Position == nil { s.Position = types.NewPositionFromMarket(s.Market) - s.p = types.NewPositionFromMarket(s.Market) - } else { - s.p = types.NewPositionFromMarket(s.Market) - s.p.Base = s.Position.Base - s.p.Quote = s.Position.Quote - s.p.AverageCost = s.Position.AverageCost } if s.ProfitStats == nil { s.ProfitStats = types.NewProfitStats(s.Market) @@ -847,9 +481,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.GeneralOrderExecutor.Bind() - s.orderPendingCounter = make(map[uint64]int) - s.minutesCounter = 0 - // Exit methods from config for _, method := range s.ExitMethods { method.Bind(session, s.GeneralOrderExecutor) @@ -860,6 +491,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se profit := types.Float64Slice{} cumProfit := types.Float64Slice{1.} orderTagHistory := make(map[uint64]string) + s.buyPrice = 0 + s.sellPrice = 0 s.Session.UserDataStream.OnOrderUpdate(func(order types.Order) { orderTagHistory[order.OrderID] = order.Tag }) @@ -872,116 +505,44 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } s.Session.UserDataStream.OnTradeUpdate(func(trade types.Trade) { - s.p.AddTrade(trade) tag, ok := orderTagHistory[trade.OrderID] if !ok { panic(fmt.Sprintf("cannot find order: %v", trade)) } - bp := buyPrice - vol := Volume - sp := sellPrice - resetPrice := false if tag == "close" { if !buyPrice.IsZero() { - if trade.Side == types.SideTypeSell { - if trade.Quantity.Compare(Volume) > 0 { - profit.Update(modify(trade.Price.Div(buyPrice)).Float64()) - } else { - profit.Update(modify(trade.Price.Div(buyPrice)). - Sub(fixedpoint.One). - Mul(trade.Quantity). - Div(Volume). - Add(fixedpoint.One). - Float64()) - } - cumProfit.Update(cumProfit.Last() * profit.Last()) - Volume = Volume.Sub(trade.Quantity) - if Volume.Sign() < 0 { - sellPrice = trade.Price - buyPrice = fixedpoint.Zero - resetPrice = true - } else if Volume.Sign() == 0 { - buyPrice = fixedpoint.Zero - resetPrice = true - } - } else { - buyPrice = buyPrice.Mul(Volume).Add(trade.Price.Mul(trade.Quantity)).Div(Volume.Add(trade.Quantity)) - Volume = Volume.Add(trade.Quantity) + profit.Update(modify(trade.Price.Div(buyPrice)). + Sub(fixedpoint.One). + Mul(trade.Quantity). + Div(Volume). + Add(fixedpoint.One). + Float64()) + cumProfit.Update(cumProfit.Last() * profit.Last()) + Volume = Volume.Sub(trade.Quantity) + if Volume.IsZero() { + buyPrice = fixedpoint.Zero + } + if !sellPrice.IsZero() { + panic("sellprice shouldn't be zero") } } else if !sellPrice.IsZero() { - if trade.Side == types.SideTypeBuy { - if trade.Quantity.Compare(Volume.Neg()) > 0 { - profit.Update(modify(sellPrice.Div(trade.Price)).Float64()) - } else { - profit.Update(modify(sellPrice.Div(trade.Price)). - Sub(fixedpoint.One). - Mul(trade.Quantity). - Div(Volume). - Neg(). - Add(fixedpoint.One). - Float64()) - } - cumProfit.Update(cumProfit.Last() * profit.Last()) - Volume = Volume.Add(trade.Quantity) - if Volume.Sign() > 0 { - buyPrice = trade.Price - sellPrice = fixedpoint.Zero - resetPrice = true - } else if Volume.Sign() == 0 { - sellPrice = fixedpoint.Zero - resetPrice = true - } - } else { - sellPrice = sellPrice.Mul(Volume).Sub(trade.Price.Mul(trade.Quantity)).Div(Volume.Sub(trade.Quantity)) - Volume = Volume.Sub(trade.Quantity) + profit.Update(modify(sellPrice.Div(trade.Price)). + Sub(fixedpoint.One). + Mul(trade.Quantity). + Div(Volume). + Neg(). + Add(fixedpoint.One). + Float64()) + cumProfit.Update(cumProfit.Last() * profit.Last()) + Volume = Volume.Add(trade.Quantity) + if Volume.IsZero() { + sellPrice = fixedpoint.Zero + } + if !buyPrice.IsZero() { + panic("buyprice shouldn't be zero") } } else { - // position changed by strategy - oldSign := Volume.Sign() - if trade.Side == types.SideTypeBuy { - Volume = Volume.Add(trade.Quantity) - if Volume.Sign() > 0 { - buyPrice = trade.Price - sellPrice = fixedpoint.Zero - if oldSign <= 0 { - resetPrice = true - } - } else if Volume.Sign() < 0 { - sellPrice = trade.Price - buyPrice = fixedpoint.Zero - if oldSign >= 0 { - resetPrice = true - } - } else { - buyPrice = fixedpoint.Zero - sellPrice = fixedpoint.Zero - if oldSign != 0 { - resetPrice = true - } - } - } else if trade.Side == types.SideTypeSell { - sellPrice = trade.Price - Volume = Volume.Sub(trade.Quantity) - if Volume.Sign() > 0 { - buyPrice = trade.Price - sellPrice = fixedpoint.Zero - if oldSign <= 0 { - resetPrice = true - } - } else if Volume.Sign() < 0 { - sellPrice = trade.Price - buyPrice = fixedpoint.Zero - if oldSign >= 0 { - resetPrice = true - } - } else { - buyPrice = fixedpoint.Zero - sellPrice = fixedpoint.Zero - if oldSign != 0 { - resetPrice = true - } - } - } + panic("no price available") } } else if tag == "short" { if buyPrice.IsZero() { @@ -998,7 +559,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se sellPrice = trade.Price } Volume = Volume.Sub(trade.Quantity) - resetPrice = true } else if tag == "long" { if sellPrice.IsZero() { if !buyPrice.IsZero() { @@ -1014,84 +574,23 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Volume = fixedpoint.Zero } Volume = Volume.Add(trade.Quantity) - resetPrice = true - } else if tag == "rebalance" { - if sellPrice.IsZero() { - profit.Update(modify(sellPrice.Div(trade.Price)).Float64()) - } else { - profit.Update(modify(trade.Price.Div(buyPrice)).Float64()) - } - resetPrice = true - cumProfit.Update(cumProfit.Last() * profit.Last()) - sellPrice = fixedpoint.Zero - buyPrice = fixedpoint.Zero - Volume = fixedpoint.Zero - s.p.Lock() - defer s.p.Unlock() - s.p.Reset() } s.buyPrice = buyPrice.Float64() + s.highestPrice = s.buyPrice s.sellPrice = sellPrice.Float64() - if resetPrice { - s.highestPrice = s.buyPrice - s.lowestPrice = s.sellPrice - } - bbgo.Notify("tag %s %v %s volafter: %v, quantity: %v, bp: %v, sp: %v, volbefore: %v, bpafter: %v, spafter: %v", tag, trade.Price, trade.Side, Volume, trade.Quantity, bp, sp, vol, s.buyPrice, s.sellPrice) + s.lowestPrice = s.sellPrice }) - dynamicKLine := &types.KLine{} - priceLine := types.NewQueue(300) - if err := s.initIndicators(dynamicKLine, priceLine); err != nil { + if err := s.initIndicators(); err != nil { log.WithError(err).Errorf("initIndicator failed") return nil } s.initTickerFunctions(ctx) + dynamicKLine := &types.KLine{} + priceLine := types.NewQueue(300) zeroPoints := types.NewQueue(300) stoploss := s.StopLoss.Float64() - // default value: use 1m kline - if !s.NoTrailingStopLoss && s.IsBackTesting() || s.TrailingStopLossType == "" { - s.TrailingStopLossType = "kline" - } - - bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) { - canvas := s.DrawIndicators(dynamicKLine.StartTime, priceLine, zeroPoints) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render indicators in drift") - reply.Message(fmt.Sprintf("[error] cannot render indicators in drift: %v", err)) - return - } - bbgo.SendPhoto(&buffer) - }) - - bbgo.RegisterCommand("/pnl", "Draw PNL per trade", func(reply interact.Reply) { - canvas := s.DrawPNL(&profit) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render pnl in drift") - reply.Message(fmt.Sprintf("[error] cannot render pnl in drift: %v", err)) - return - } - bbgo.SendPhoto(&buffer) - }) - - bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL", func(reply interact.Reply) { - canvas := s.DrawCumPNL(&cumProfit) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render cumpnl in drift") - reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) - return - } - bbgo.SendPhoto(&buffer) - }) - - bbgo.RegisterCommand("/config", "Show latest config", func(reply interact.Reply) { - var buffer bytes.Buffer - s.Print(&buffer, false) - reply.Message(buffer.String()) - }) session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { if s.Status != types.StrategyStatusRunning { @@ -1107,29 +606,19 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } if kline.Interval == types.Interval1m { - s.drift1m.Update(s.getSource(&kline).Float64()) - s.minutesCounter += 1 - if s.NoTrailingStopLoss || s.TrailingStopLossType == "realtime" { + if s.NoTrailingStopLoss || !s.IsBackTesting() { return } // for doing the trailing stoploss during backtesting atr = s.atr.Last() price := s.getLastPrice() pricef := price.Float64() - - takeProfitFactor := s.takeProfitFactor.Predict(2) - var err error - numPending := 0 - if numPending, err = s.smartCancel(ctx, pricef, atr, takeProfitFactor); err != nil { - log.WithError(err).Errorf("cannot cancel orders") - return - } - if numPending > 0 { - return - } - lowf := math.Min(kline.Low.Float64(), pricef) highf := math.Max(kline.High.Float64(), pricef) + d := s.drift.TestUpdate(pricef) + drift := d.Last() + ddrift := d.drift.Last() + if s.lowestPrice > 0 && lowf < s.lowestPrice { s.lowestPrice = lowf } @@ -1137,34 +626,35 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.highestPrice = highf } avg := s.buyPrice + s.sellPrice - exitShortCondition := ( /*avg*(1.+stoploss) <= pricef || (drift > 0 || ddrift > DDriftFilterPos) ||*/ avg-atr*takeProfitFactor >= pricef || - s.trailingCheck(highf, "short")) && - (s.p.IsShort() && !s.p.IsDust(price)) - exitLongCondition := ( /*avg*(1.-stoploss) >= pricef || (drift < 0 || ddrift < DDriftFilterNeg) ||*/ avg+atr*takeProfitFactor <= pricef || - s.trailingCheck(lowf, "long")) && - (s.p.IsLong() && !s.p.IsDust(price)) + + if s.GeneralOrderExecutor.ActiveMakerOrders().NumOfOrders() > 0 { + return + } + + takeProfitFactor := s.takeProfitFactor.Predict(2) + exitShortCondition := ( /*avg+atr/2 <= highf || avg*(1.+stoploss) <= pricef ||*/ (drift > 0 && ddrift > 0.6) || avg-atr*takeProfitFactor >= pricef || + ((highf-s.lowestPrice)/s.lowestPrice > 0.003 && (avg-s.lowestPrice)/s.lowestPrice > 0.015)) && + (s.Position.IsShort() && !s.Position.IsDust(price)) + exitLongCondition := ( /*avg-atr/2 >= lowf || avg*(1.-stoploss) >= pricef || */ (drift < 0 && ddrift < -0.6) || avg+atr*takeProfitFactor <= pricef || + ((s.highestPrice-lowf)/lowf > 0.003 && (s.highestPrice-avg)/avg > 0.015)) && + (s.Position.IsLong() && !s.Position.IsDust(price)) if exitShortCondition || exitLongCondition { if exitLongCondition && s.highestPrice > avg { - s.takeProfitFactor.Update((s.highestPrice - avg) / atr * 1.5) + s.takeProfitFactor.Update((s.highestPrice - avg) / atr * 4) } else if exitShortCondition && avg > s.lowestPrice { - s.takeProfitFactor.Update((avg - s.lowestPrice) / atr * 1.5) + s.takeProfitFactor.Update((avg - s.lowestPrice) / atr * 4) } _ = s.ClosePosition(ctx, fixedpoint.One) } return } - if kline.Interval != s.Interval { - return - } dynamicKLine.Set(&kline) source := s.getSource(dynamicKLine) sourcef := source.Float64() priceLine.Update(sourcef) s.ma.Update(sourcef) - s.trendLine.Update(sourcef) s.drift.Update(sourcef) - zeroPoint := s.drift.ZeroPoint() zeroPoints.Update(zeroPoint) s.atr.PushK(kline) @@ -1181,8 +671,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.stdevLow.Update(lowdiff) highdiff := highf - s.ma.Last() s.stdevHigh.Update(highdiff) - - log.Errorf("highdiff: %3.2f ma: %.2f, close: %8v, high: %8v, low: %8v, time: %v", s.stdevHigh.Last(), s.ma.Last(), kline.Close, kline.High, kline.Low, kline.StartTime) if s.lowestPrice > 0 && lowf < s.lowestPrice { s.lowestPrice = lowf } @@ -1192,48 +680,38 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se avg := s.buyPrice + s.sellPrice takeProfitFactor := s.takeProfitFactor.Predict(2) - if !s.NoRebalance { - s.Rebalance(ctx, orderTagHistory) + if !s.IsBackTesting() { + balances := s.Session.GetAccount().Balances() + bbgo.Notify("zeroPoint: %.4f, source: %.4f, price: %.4f, driftPred: %.4f, drift: %.4f, drift[1]: %.4f, atr: %.4f, avg: %.4f", + zeroPoint, sourcef, pricef, driftPred, drift[0], drift[1], atr, avg) + // Notify will parse args to strings and process separately + bbgo.Notify("balances: [Base] %s [Quote] %s", balances[s.Market.BaseCurrency].String(), balances[s.Market.QuoteCurrency].String()) } - //if !s.IsBackTesting() { - balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() - bbgo.Notify("source: %.4f, price: %.4f, driftPred: %.4f, ddriftPred: %.4f, drift[1]: %.4f, ddrift[1]: %.4f, atr: %.4f, avg: %.4f, takeProfitFact: %.4f, lowf %.4f, highf: %.4f", - sourcef, pricef, driftPred, ddriftPred, drift[1], ddrift[1], atr, avg, takeProfitFactor, lowf, highf) - // Notify will parse args to strings and process separately - bbgo.Notify("balances: [Base] %s(%vU) [Quote] %s", - balances[s.Market.BaseCurrency].String(), - balances[s.Market.BaseCurrency].Total().Mul(price), - balances[s.Market.QuoteCurrency].String()) - //} - - shortCondition := (drift[1] >= DriftFilterNeg || ddrift[1] >= 0) && (driftPred <= DDriftFilterNeg || ddriftPred <= 0) || drift[1] < 0 && drift[0] < 0 - longCondition := (drift[1] <= DriftFilterPos || ddrift[1] <= 0) && (driftPred >= DDriftFilterPos || ddriftPred >= 0) || drift[1] > 0 && drift[0] > 0 - exitShortCondition := ((drift[0] >= DDriftFilterPos && ddrift[0] >= 0) || + //shortCondition := (sourcef <= zeroPoint && driftPred <= drift[0] && drift[0] <= 0 && drift[1] > 0 && drift[2] > drift[1]) + //longCondition := (sourcef >= zeroPoint && driftPred >= drift[0] && drift[0] >= 0 && drift[1] < 0 && drift[2] < drift[1]) + //bothUp := ddrift[1] < ddrift[0] && drift[1] < drift[0] + //bothDown := ddrift[1] > ddrift[0] && drift[1] > drift[0] + shortCondition := (drift[1] >= -0.9 || ddrift[1] >= 0) && (driftPred <= -0.6 || ddriftPred <= 0) + longCondition := (drift[1] <= 0.9 || ddrift[1] <= 0) && (driftPred >= 0.6 || ddriftPred >= 0) + exitShortCondition := ((drift[0] >= 0.6 && ddrift[0] >= 0) || avg*(1.+stoploss) <= pricef || avg-atr*takeProfitFactor >= pricef) && - s.p.IsShort() - exitLongCondition := ((drift[0] <= DDriftFilterNeg && ddrift[0] <= 0) || + s.Position.IsShort() && !longCondition && !shortCondition + exitLongCondition := ((drift[0] <= -0.6 && ddrift[0] <= 0) || avg*(1.-stoploss) >= pricef || avg+atr*takeProfitFactor <= pricef) && - s.p.IsLong() - if shortCondition && longCondition { - if drift[1] > drift[0] { - longCondition = false - } else { - shortCondition = false - } - } + s.Position.IsLong() && !shortCondition && !longCondition - if (exitShortCondition || exitLongCondition) && s.p.IsOpened(price) && !shortCondition && !longCondition { + if (exitShortCondition || exitLongCondition) && s.Position.IsOpened(price) { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { log.WithError(err).Errorf("cannot cancel orders") return } if exitShortCondition && avg > s.lowestPrice { - s.takeProfitFactor.Update((avg - s.lowestPrice) / atr * 1.5) + s.takeProfitFactor.Update((avg - s.lowestPrice) / atr * 4) } else if exitLongCondition && avg < s.highestPrice { - s.takeProfitFactor.Update((s.highestPrice - avg) / atr * 1.5) + s.takeProfitFactor.Update((s.highestPrice - avg) / atr * 4) } if s.takeProfitFactor.Last() == 0 { log.Errorf("exit %f %f %f %v", s.highestPrice, s.lowestPrice, avg, s.takeProfitFactor.Array(10)) @@ -1241,46 +719,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se _ = s.ClosePosition(ctx, fixedpoint.One) return } - if longCondition { - if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { - log.WithError(err).Errorf("cannot cancel orders") - return - } - source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last() * s.HighLowVarianceMultiplier)) - if source.Compare(price) > 0 { - source = price - } - sourcef = source.Float64() - - quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) - if !ok { - log.Errorf("unable to get quoteCurrency") - return - } - if s.Market.IsDustQuantity( - quoteBalance.Available.Div(source), source) { - return - } - if avg > s.lowestPrice && s.Position.IsShort() { - s.takeProfitFactor.Update((avg - s.lowestPrice) / atr * 1.5) - } - quantity := quoteBalance.Available.Div(source) - createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Price: source, - Quantity: quantity, - Tag: "long", - }) - if err != nil { - log.WithError(err).Errorf("cannot place buy order") - return - } - orderTagHistory[createdOrders[0].OrderID] = "long" - s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter - return - } if shortCondition { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { log.WithError(err).Errorf("cannot cancel orders") @@ -1301,7 +739,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } if avg < s.highestPrice && avg > 0 && s.Position.IsLong() { - s.takeProfitFactor.Update((s.highestPrice - avg) / atr * 1.5) + s.takeProfitFactor.Update((s.highestPrice - avg) / atr * 4) + if s.takeProfitFactor.Last() == 0 { + log.Errorf("short %f %f", s.highestPrice, avg) + } } // Cleanup pending StopOrders quantity := baseBalance.Available @@ -1318,19 +759,56 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } orderTagHistory[createdOrders[0].OrderID] = "short" - s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter - return + } + if longCondition { + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return + } + source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last() * s.HighLowVarianceMultiplier)) + if source.Compare(price) > 0 { + source = price + } + sourcef = source.Float64() + + quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) + if !ok { + log.Errorf("unable to get quoteCurrency") + return + } + if s.Market.IsDustQuantity( + quoteBalance.Available.Div(source), source) { + return + } + if avg > s.lowestPrice && s.Position.IsShort() { + s.takeProfitFactor.Update((avg - s.lowestPrice) / atr * 4) + if s.takeProfitFactor.Last() == 0 { + log.Errorf("long %f %f", s.lowestPrice, avg) + } + + } + quantity := quoteBalance.Available.Div(source) + createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Price: source, + Quantity: quantity, + Tag: "long", + }) + if err != nil { + log.WithError(err).Errorf("cannot place buy order") + return + } + orderTagHistory[createdOrders[0].OrderID] = "long" } }) bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { - var buffer bytes.Buffer + defer s.Print(os.Stdout) - s.Print(&buffer, true, true) - fmt.Fprintln(&buffer, s.TradeStats.BriefString()) - - os.Stdout.Write(buffer.Bytes()) + defer fmt.Fprintln(os.Stdout, s.TradeStats.BriefString()) if s.GenerateGraph { s.Draw(dynamicKLine.StartTime, priceLine, &profit, &cumProfit, zeroPoints) diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index ca1822135..5c67fbc21 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -1192,35 +1192,16 @@ func NewCanvas(title string, intervals ...Interval) *Canvas { return out } -func expand(a []float64, length int, defaultVal float64) []float64 { - l := len(a) - if l >= length { - return a - } - for i := 0; i < length-l; i++ { - a = append([]float64{defaultVal}, a...) - } - return a -} - -func (canvas *Canvas) Plot(tag string, a Series, endTime Time, length int, intervals ...Interval) { +func (canvas *Canvas) Plot(tag string, a Series, endTime Time, length int) { var timeline []time.Time e := endTime.Time() - if a.Length() == 0 { - return - } - oldest := a.Index(a.Length() - 1) - interval := canvas.Interval - if len(intervals) > 0 { - interval = intervals[0] - } for i := length - 1; i >= 0; i-- { - shiftedT := e.Add(-time.Duration(i*interval.Minutes()) * time.Minute) + shiftedT := e.Add(-time.Duration(i*canvas.Interval.Minutes()) * time.Minute) timeline = append(timeline, shiftedT) } canvas.Series = append(canvas.Series, chart.TimeSeries{ Name: tag, - YValues: expand(Reverse(a, length), length, oldest), + YValues: Reverse(a, length), XValues: timeline, }) } @@ -1230,14 +1211,10 @@ func (canvas *Canvas) PlotRaw(tag string, a Series, length int) { for i := 0; i < length; i++ { x = append(x, float64(i)) } - if a.Length() == 0 { - return - } - oldest := a.Index(a.Length() - 1) canvas.Series = append(canvas.Series, chart.ContinuousSeries{ Name: tag, XValues: x, - YValues: expand(Reverse(a, length), length, oldest), + YValues: Reverse(a, length), }) } diff --git a/pkg/types/trade_stats.go b/pkg/types/trade_stats.go index 921a2016a..c4aab1c51 100644 --- a/pkg/types/trade_stats.go +++ b/pkg/types/trade_stats.go @@ -226,8 +226,6 @@ func (s *TradeStats) BriefString() string { GrossLoss: s.GrossLoss, LargestProfitTrade: s.LargestProfitTrade, LargestLossTrade: s.LargestLossTrade, - AverageProfitTrade: s.AverageProfitTrade, - AverageLossTrade: s.AverageLossTrade, ProfitFactor: s.ProfitFactor, TotalNetProfit: s.TotalNetProfit, IntervalProfits: s.IntervalProfits,