diff --git a/config/drift.yaml b/config/drift.yaml index 412a3fe93..b2459d22e 100644 --- a/config/drift.yaml +++ b/config/drift.yaml @@ -31,25 +31,25 @@ exchangeStrategies: stoploss: 0.3% source: close predictOffset: 2 - # the init value of takeProfitFactor Series, position avg +- takeProfitFactor * atr as take profit price - takeProfitFactor: 6 - profitFactorWindow: 8 noTrailingStopLoss: false trailingStopLossType: kline # stddev on high/low-source hlVarianceMultiplier: 0.23 hlRangeWindow: 5 + window1m: 24 + smootherWindow1m: 24 + fisherTransformWindow1m: 162 smootherWindow: 1 fisherTransformWindow: 9 atrWindow: 14 # orders not been traded will be canceled after `pendingMinutes` minutes - pendingMinutes: 4 + pendingMinutes: 3 noRebalance: true trendWindow: 12 rebalanceFilter: 1.5 - trailingActivationRatio: [0.008, 0.015] - trailingCallbackRate: [0.002, 0.001] + trailingActivationRatio: [0.004] + trailingCallbackRate: [0.001] generateGraph: true graphPNLDeductFee: true @@ -88,7 +88,7 @@ sync: backtest: startTime: "2022-01-01" - endTime: "2022-07-30" + endTime: "2022-08-30" symbols: - ETHBUSD sessions: [binance] diff --git a/config/driftBTC.yaml b/config/driftBTC.yaml index 93ba91d7d..32324019e 100644 --- a/config/driftBTC.yaml +++ b/config/driftBTC.yaml @@ -38,6 +38,9 @@ exchangeStrategies: hlRangeWindow: 5 smootherWindow: 1 fisherTransformWindow: 9 + window1m: 28 + smootherWindow1m: 14 + fisherTransformWindow1m: 162 # the init value of takeProfitFactor Series, the coefficient of ATR as TP takeProfitFactor: 4 profitFactorWindow: 5 @@ -51,11 +54,11 @@ exchangeStrategies: # 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.011, 0.02, 0.05] - trailingActivationRatio: [0.001, 0.002, 0.004] + trailingActivationRatio: [0.002, 0.02, 0.04] #trailingActivationRatio: [] #trailingCallbackRate: [] #trailingCallbackRate: [0.002, 0.001, 0.002, 0.001] - trailingCallbackRate: [0.0005, 0.0008, 0.002] + trailingCallbackRate: [0.0005, 0.001, 0.003] generateGraph: true graphPNLDeductFee: false @@ -128,5 +131,5 @@ backtest: makerFeeRate: 0.000 #takerFeeRate: 0.000 balances: - BTC: 1 - USDT: 5000 + BTC: 0 + USDT: 21 diff --git a/go.mod b/go.mod index d9c8597cc..f6904f458 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 + github.com/jedib0t/go-pretty/v6 v6.3.6 github.com/jmoiron/sqlx v1.3.4 github.com/joho/godotenv v1.3.0 github.com/leekchan/accounting v0.0.0-20191218023648-17a4ce5f94d4 @@ -83,7 +84,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 diff --git a/go.sum b/go.sum index 5ce4df09c..5feae444a 100644 --- a/go.sum +++ b/go.sum @@ -81,7 +81,6 @@ github.com/c9s/requestgen v1.3.0/go.mod h1:5n9FU3hr5307IiXAmbMiZbHYaPiys1u9jCWYe github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b h1:wT8c03PHLv7+nZUIGqxAzRvIfYHNxMCNVWwvdGkOXTs= github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b/go.mod h1:EKObf66Cp7erWxym2de+07qNN5T1N9PXxHdh97N44EQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= @@ -149,7 +148,6 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= @@ -441,7 +439,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 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= @@ -603,6 +600,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= 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= @@ -611,7 +609,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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= @@ -622,7 +619,6 @@ github.com/tebeka/strftime v0.1.3 h1:5HQXOqWKYRFfNyBMNVc9z5+QzuBtIXy03psIhtdJYto github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.3 h1:WbFSXLxDFKVN69Sk8t+XHGzVCD7R8UoAATR8NqZgTbk= github.com/ugorji/go v1.2.3/go.mod h1:5l8GZ8hZvmL4uMdy+mhCO1LjswGRYco9Q3HfuisB21A= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.3 h1:/mVYEV+Jo3IZKeA5gBngN0AvNnQltEDkR+eQikkWQu0= @@ -642,7 +638,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= @@ -775,8 +770,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0= golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -855,7 +848,6 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -928,7 +920,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/indicator/fisher.go b/pkg/indicator/fisher.go index b4b6430cd..cd9ce4dac 100644 --- a/pkg/indicator/fisher.go +++ b/pkg/indicator/fisher.go @@ -34,6 +34,10 @@ func (inc *FisherTransform) Update(value float64) { inc.prices.Update(value) highest := inc.prices.Highest(inc.Window) lowest := inc.prices.Lowest(inc.Window) + if highest == lowest { + inc.Values.Update(0) + return + } x := 2*((value-lowest)/(highest-lowest)) - 1 if x == 1 { x = 0.9999 diff --git a/pkg/strategy/drift/output.go b/pkg/strategy/drift/output.go index 34fbd663f..6bdca8e71 100644 --- a/pkg/strategy/drift/output.go +++ b/pkg/strategy/drift/output.go @@ -43,6 +43,7 @@ func (s *Strategy) ParamDump(f io.Writer, seriesLength ...int) { } fieldName := t.Name typeName := field.Type().String() + log.Infof("fieldName %s typeName %s", fieldName, typeName) value := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem() isSeries := true lastFunc := value.MethodByName("Last") @@ -82,7 +83,7 @@ func (s *Strategy) ParamDump(f io.Writer, seriesLength ...int) { fmt.Fprintf(f, "%s: %v", fieldName, field.Interface()) } else if field.Type().Kind() == reflect.Map { fmt.Fprintf(f, "%s: {", fieldName) - iter := field.MapRange() + iter := value.MapRange() for iter.Next() { k := iter.Key().Interface() v := iter.Value().Interface() @@ -203,10 +204,7 @@ func (s *Strategy) Print(f io.Writer, pretty bool, withColor ...bool) { } } 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()) } } diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index c0f132668..029af5bd4 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -73,12 +73,7 @@ type Strategy struct { beta float64 - // This stores the maximum TP coefficient of ATR multiplier of each entry point - takeProfitFactor types.UpdatableSeriesExtend - Source string `json:"source,omitempty"` - TakeProfitFactor float64 `json:"takeProfitFactor"` - ProfitFactorWindow int `json:"profitFactorWindow"` StopLoss fixedpoint.Value `json:"stoploss"` CanvasPath string `json:"canvasPath"` PredictOffset int `json:"predictOffset"` @@ -86,6 +81,9 @@ type Strategy struct { 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"` + Window1m int `json:"window1m"` + SmootherWindow1m int `json:"smootherWindow1m"` + FisherTransformWindow1m int `json:"fisherTransformWindow1m"` SmootherWindow int `json:"smootherWindow"` FisherTransformWindow int `json:"fisherTransformWindow"` ATRWindow int `json:"atrWindow"` @@ -225,24 +223,20 @@ func (s *Strategy) initIndicators(priceLines *types.Queue) error { 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}, + MA: &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1m, Window: s.Window1m}}, + IntervalWindow: types.IntervalWindow{Interval: types.Interval1m, Window: s.Window1m}, }, ma1: &indicator.EWMA{ - IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 24}, + IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.SmootherWindow1m}, }, ma2: &indicator.FisherTransform{ - IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.FisherTransformWindow * 15}, + IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.FisherTransformWindow1m}, }, } 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++ { - s.takeProfitFactor.Update(s.TakeProfitFactor) - } store, _ := s.Session.MarketDataStore(s.Symbol) klines, ok := store.KLinesOfInterval(s.Interval) if !ok { @@ -271,6 +265,9 @@ func (s *Strategy) initIndicators(priceLines *types.Queue) error { for _, kline := range *klines { source := s.getSource(&kline).Float64() s.drift1m.Update(source) + if s.drift1m.Last() != s.drift1m.Last() { + panic(fmt.Sprintf("%f %v %f %f", source, s.drift1m.drift.Values.Index(1), s.drift1m.ma2.Last(), s.drift1m.drift.LastValue)) + } } if s.kline1m != nil && klines != nil { s.kline1m.Set(&(*klines)[len(*klines)-1]) @@ -278,7 +275,7 @@ func (s *Strategy) initIndicators(priceLines *types.Queue) error { return nil } -func (s *Strategy) smartCancel(ctx context.Context, pricef, atr, takeProfitFactor float64) (int, error) { +func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, error) { nonTraded := s.GeneralOrderExecutor.ActiveMakerOrders().Orders() if len(nonTraded) > 0 { if len(nonTraded) > 1 { @@ -364,39 +361,23 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { bestAsk := ticker.Sell var pricef, atr float64 - if util.TryLock(&s.lock) { - if !bestAsk.IsZero() && !bestBid.IsZero() { - s.midPrice = bestAsk.Add(bestBid).Div(Two) - } else if !bestAsk.IsZero() { - s.midPrice = bestAsk - } else { - s.midPrice = bestBid - } - pricef = s.midPrice.Float64() - } else { + if !util.TryLock(&s.lock) { return } + if !bestAsk.IsZero() && !bestBid.IsZero() { + s.midPrice = bestAsk.Add(bestBid).Div(Two) + } else if !bestAsk.IsZero() { + s.midPrice = bestAsk + } else { + s.midPrice = bestBid + } + pricef = s.midPrice.Float64() s.lock.Unlock() - // for trailing stoploss during the realtime - if s.NoTrailingStopLoss || s.TrailingStopLossType == "kline" { - return - } - stoploss := s.StopLoss.Float64() - 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") + if !util.TryLock(&s.positionLock) { return } - if numPending > 0 { - return - } - - s.positionLock.Lock() if s.highestPrice > 0 && s.highestPrice < pricef { s.highestPrice = pricef @@ -404,18 +385,29 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { if s.lowestPrice > 0 && s.lowestPrice > pricef { s.lowestPrice = pricef } + // for trailing stoploss during the realtime + if s.NoTrailingStopLoss || s.TrailingStopLossType == "kline" { + s.positionLock.Unlock() + return + } - exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= pricef || s.sellPrice-atr*takeProfitFactor >= pricef || + stoploss := s.StopLoss.Float64() + atr = s.atr.Last() + numPending := 0 + var err error + if numPending, err = s.smartCancel(ctx, pricef, atr); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return + } + if numPending > 0 { + return + } + + exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= pricef || s.trailingCheck(pricef, "short")) - exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= pricef || s.buyPrice+atr*takeProfitFactor <= pricef || + exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= pricef || s.trailingCheck(pricef, "long")) if exitShortCondition || exitLongCondition { - if exitLongCondition && s.highestPrice > s.buyPrice { - s.takeProfitFactor.Update((s.highestPrice - s.buyPrice) / atr * 1.5) - } - if exitShortCondition && s.sellPrice > s.lowestPrice { - s.takeProfitFactor.Update((s.sellPrice - s.lowestPrice) / atr * 1.5) - } log.Infof("Close position by orderbook changes") s.positionLock.Unlock() _ = s.ClosePosition(ctx, fixedpoint.One) @@ -453,12 +445,20 @@ func (s *Strategy) DrawIndicators(time types.Time, priceLine types.SeriesExtend, highestPrice := priceLine.Minus(mean).Abs().Highest(Length) highestDrift := s.drift.Abs().Highest(Length) hi := s.drift.drift.Abs().Highest(Length) + h1m := s.drift1m.Abs().Highest(Length * s.Interval.Minutes()) ratio := highestPrice / highestDrift + for i := 0; i < s.drift1m.Length(); i++ { + if s.drift1m.Index(i) != s.drift1m.Index(i) { + log.Infof("%d, %f", i, s.drift1m.Index(i-1)) + } + } + log.Infof("%d, %v", s.drift1m.Length(), s.drift1m.Array(10)) canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, Length) canvas.Plot("ma", s.ma, time, Length) canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, Length) canvas.Plot("drift", s.drift.Mul(ratio).Add(mean), time, Length) canvas.Plot("driftOrig", s.drift.drift.Mul(highestPrice/hi).Add(mean), time, Length) + canvas.Plot("drift1m", s.drift1m.Mul(highestPrice/h1m).Add(mean), time, Length*s.Interval.Minutes(), types.Interval1m) canvas.Plot("zero", types.NumberSeries(mean), time, Length) canvas.Plot("price", priceLine, time, Length) canvas.Plot("zeroPoint", zeroPoints, time, Length) @@ -821,10 +821,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se 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 { + if numPending, err = s.smartCancel(ctx, pricef, atr); err != nil { log.WithError(err).Errorf("cannot cancel orders") return } @@ -842,17 +841,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if s.highestPrice > 0 && highf > s.highestPrice { s.highestPrice = highf } - exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= highf || s.sellPrice-atr*takeProfitFactor >= lowf || - s.trailingCheck(highf, "short")) - exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= lowf || s.buyPrice+atr*takeProfitFactor <= highf || - s.trailingCheck(lowf, "long")) + exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= highf || + s.trailingCheck(highf, "short") || s.drift1m.Last() > 0) + exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= lowf || + s.trailingCheck(lowf, "long") || s.drift1m.Last() < 0) if exitShortCondition || exitLongCondition { - if exitLongCondition && s.highestPrice > s.buyPrice { - s.takeProfitFactor.Update((s.highestPrice - s.buyPrice) / atr * 1.5) - } - if exitShortCondition && s.sellPrice > s.lowestPrice { - s.takeProfitFactor.Update((s.sellPrice - s.lowestPrice) / atr * 1.5) - } s.positionLock.Unlock() _ = s.ClosePosition(ctx, fixedpoint.One) } else { @@ -901,15 +894,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if s.highestPrice > 0 && highf > s.highestPrice { s.highestPrice = highf } - takeProfitFactor := s.takeProfitFactor.Predict(2) if !s.NoRebalance { s.Rebalance(ctx) } balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() - bbgo.Notify("source: %.4f, price: %.4f, driftPred: %.4f, ddriftPred: %.4f, drift[1]: %.4f, ddrift[1]: %.4f, atr: %.4f, takeProfitFact: %.4f, lowf %.4f, highf: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f", - sourcef, pricef, driftPred, ddriftPred, drift[1], ddrift[1], atr, takeProfitFactor, lowf, highf, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice) + bbgo.Notify("source: %.4f, price: %.4f, driftPred: %.4f, ddriftPred: %.4f, drift[1]: %.4f, ddrift[1]: %.4f, atr: %.4f, lowf %.4f, highf: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f", + sourcef, pricef, driftPred, ddriftPred, drift[1], ddrift[1], atr, lowf, highf, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice) // Notify will parse args to strings and process separately bbgo.Notify("balances: [Base] %s(%vU) [Quote] %s", balances[s.Market.BaseCurrency].String(), @@ -927,12 +919,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } exitShortCondition := s.sellPrice > 0 && ( //!shortCondition && !longCondition || s.sellPrice*(1.+stoploss) <= highf || - s.trailingCheck(pricef, "short") || - s.sellPrice-atr*takeProfitFactor >= s.lowestPrice) + s.trailingCheck(pricef, "short")) exitLongCondition := s.buyPrice > 0 && ( //!longCondition && !shortCondition || s.buyPrice*(1.-stoploss) >= lowf || - s.trailingCheck(pricef, "long") || - s.buyPrice+atr*takeProfitFactor <= s.highestPrice) + s.trailingCheck(pricef, "long")) if exitShortCondition || exitLongCondition { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { @@ -940,15 +930,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.positionLock.Unlock() return } - if exitShortCondition && s.sellPrice > s.lowestPrice { - s.takeProfitFactor.Update((s.sellPrice - s.lowestPrice) / atr * 1.5) - } - if exitLongCondition && s.buyPrice < s.highestPrice { - s.takeProfitFactor.Update((s.highestPrice - s.buyPrice) / atr * 1.5) - } - if s.takeProfitFactor.Last() == 0 { - log.Errorf("exit %f %f %v", s.highestPrice, s.lowestPrice, s.takeProfitFactor.Array(10)) - } s.positionLock.Unlock() _ = s.ClosePosition(ctx, fixedpoint.One) return @@ -976,9 +957,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.positionLock.Unlock() return } - if s.sellPrice > s.lowestPrice && s.lowestPrice > 0 { - s.takeProfitFactor.Update((s.sellPrice - s.lowestPrice) / atr * 1.5) - } s.positionLock.Unlock() quantity := quoteBalance.Available.Div(source) createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ @@ -1018,9 +996,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.positionLock.Unlock() return } - if s.buyPrice < s.highestPrice && s.buyPrice > 0 { - s.takeProfitFactor.Update((s.highestPrice - s.buyPrice) / atr * 1.5) - } s.positionLock.Unlock() // Cleanup pending StopOrders quantity := baseBalance.Available @@ -1039,6 +1014,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter return } + s.positionLock.Unlock() }) bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { @@ -1046,6 +1022,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se var buffer bytes.Buffer s.Print(&buffer, true, true) + + fmt.Fprintln(&buffer, "--- NonProfitable Dates ---") + for _, daypnl := range s.TradeStats.IntervalProfits[types.Interval1d].GetNonProfitableIntervals() { + fmt.Fprintf(&buffer, "%s\n", daypnl) + } fmt.Fprintln(&buffer, s.TradeStats.BriefString()) os.Stdout.Write(buffer.Bytes()) diff --git a/pkg/types/trade_stats.go b/pkg/types/trade_stats.go index 921a2016a..de6f203a4 100644 --- a/pkg/types/trade_stats.go +++ b/pkg/types/trade_stats.go @@ -1,6 +1,8 @@ package types import ( + "encoding/json" + "log" "math" "time" @@ -10,13 +12,14 @@ import ( ) type IntervalProfitCollector struct { - Interval Interval `json:"interval"` - Profits *Float64Slice `json:"profits"` - tmpTime time.Time `json:"tmpTime"` + Interval Interval `json:"interval"` + Profits *Float64Slice `json:"profits"` + Timestamp *Float64Slice `json:"timestamp"` + tmpTime time.Time `json:"tmpTime"` } func NewIntervalProfitCollector(i Interval, startTime time.Time) *IntervalProfitCollector { - return &IntervalProfitCollector{Interval: i, tmpTime: startTime, Profits: &Float64Slice{1.}} + return &IntervalProfitCollector{Interval: i, tmpTime: startTime, Profits: &Float64Slice{1.}, Timestamp: &Float64Slice{float64(startTime.Unix())}} } // Update the collector by every traded profit @@ -31,6 +34,7 @@ func (s *IntervalProfitCollector) Update(profit *Profit) { for { s.Profits.Update(1.) s.tmpTime = s.tmpTime.Add(duration) + s.Timestamp.Update(float64(s.tmpTime.Unix())) if profit.TradedAt.Before(s.tmpTime.Add(duration)) { (*s.Profits)[len(*s.Profits)-1] *= 1. + profit.NetProfitMargin.Float64() break @@ -40,6 +44,48 @@ func (s *IntervalProfitCollector) Update(profit *Profit) { } } +type ProfitReport struct { + StartTime time.Time `json:"startTime"` + Profit float64 `json:"profit"` + Interval Interval `json:"interval"` +} + +func (s ProfitReport) String() string { + b, err := json.MarshalIndent(s, "", "\t") + if err != nil { + log.Fatal(err) + } + return string(b) +} + +// Get all none-profitable intervals +func (s *IntervalProfitCollector) GetNonProfitableIntervals() (result []ProfitReport) { + if s.Profits == nil { + return result + } + l := s.Profits.Length() + for i := 0; i < l; i++ { + if s.Profits.Index(i) <= 1. { + result = append(result, ProfitReport{StartTime: time.Unix(int64(s.Timestamp.Index(i)), 0), Profit: s.Profits.Index(i), Interval: s.Interval}) + } + } + return result +} + +// Get all profitable intervals +func (s *IntervalProfitCollector) GetProfitableIntervals() (result []ProfitReport) { + if s.Profits == nil { + return result + } + l := s.Profits.Length() + for i := 0; i < l; i++ { + if s.Profits.Index(i) > 1. { + result = append(result, ProfitReport{StartTime: time.Unix(int64(s.Timestamp.Index(i)), 0), Profit: s.Profits.Index(i), Interval: s.Interval}) + } + } + return result +} + // Get number of profitable traded intervals func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) { if s.Profits == nil {