From dbe0fbcd4cce4945e76b3d617c23a407daf64191 Mon Sep 17 00:00:00 2001 From: zenix Date: Tue, 24 May 2022 16:19:00 +0900 Subject: [PATCH 1/5] fix: split implementation, fix code comments, add explanation on ewo params --- config/ewo_dgtrd.yaml | 23 +-- pkg/strategy/ewoDgtrd/heikinashi.go | 86 ++++++++++ pkg/strategy/ewoDgtrd/strategy.go | 239 ++++++++++------------------ 3 files changed, 184 insertions(+), 164 deletions(-) create mode 100644 pkg/strategy/ewoDgtrd/heikinashi.go diff --git a/config/ewo_dgtrd.yaml b/config/ewo_dgtrd.yaml index 7c47b4614..cac6f1362 100644 --- a/config/ewo_dgtrd.yaml +++ b/config/ewo_dgtrd.yaml @@ -10,20 +10,25 @@ exchangeStrategies: - on: binance ewo_dgtrd: symbol: MATICUSDT + # kline interval for indicators interval: 2h + # use ema as MA useEma: false + # use sma as MA, used when ema is false + # if both sma and ema are false, use EVMA useSma: false + # ewo signal line window size sigWin: 8 - stoploss: 10% + # SL percentage from entry price + stoploss: 15% + # use HeikinAshi klines instead of normal OHLC useHeikinAshi: true + # disable SL when short disableShortStop: false - #stops: - #- trailingStop: - # callbackRate: 5.1% - # closePosition: 20% - # minProfit: 1% - # interval: 1m - # virtual: true + # CCI Stochastic Indicator high filter + ccistochFilterHigh: 80 + # CCI Stochastic Indicator low filter + ccistochFilterLow: 20 sync: userDataStream: @@ -36,7 +41,7 @@ sync: backtest: startTime: "2022-05-01" - endTime: "2022-05-11" + endTime: "2022-05-24" symbols: - MATICUSDT sessions: [binance] diff --git a/pkg/strategy/ewoDgtrd/heikinashi.go b/pkg/strategy/ewoDgtrd/heikinashi.go new file mode 100644 index 000000000..1b81442af --- /dev/null +++ b/pkg/strategy/ewoDgtrd/heikinashi.go @@ -0,0 +1,86 @@ +package ewoDgtrd + +import ( + "fmt" + "math" + + "github.com/c9s/bbgo/pkg/types" +) + +type Queue struct { + arr []float64 + size int +} + +func NewQueue(size int) *Queue { + return &Queue{ + arr: make([]float64, 0, size), + size: size, + } +} + +func (inc *Queue) Last() float64 { + if len(inc.arr) == 0 { + return 0 + } + return inc.arr[len(inc.arr)-1] +} + +func (inc *Queue) Index(i int) float64 { + if len(inc.arr)-i-1 < 0 { + return 0 + } + return inc.arr[len(inc.arr)-i-1] +} + +func (inc *Queue) Length() int { + return len(inc.arr) +} + +func (inc *Queue) Update(v float64) { + inc.arr = append(inc.arr, v) + if len(inc.arr) > inc.size { + inc.arr = inc.arr[len(inc.arr)-inc.size:] + } +} + +type HeikinAshi struct { + Close *Queue + Open *Queue + High *Queue + Low *Queue + Volume *Queue +} + +func NewHeikinAshi(size int) *HeikinAshi { + return &HeikinAshi{ + Close: NewQueue(size), + Open: NewQueue(size), + High: NewQueue(size), + Low: NewQueue(size), + Volume: NewQueue(size), + } +} + +func (s *HeikinAshi) Print() string { + return fmt.Sprintf("Heikin c: %.3f, o: %.3f, h: %.3f, l: %.3f, v: %.3f", + s.Close.Last(), + s.Open.Last(), + s.High.Last(), + s.Low.Last(), + s.Volume.Last()) +} + +func (inc *HeikinAshi) Update(kline types.KLine) { + open := kline.Open.Float64() + cloze := kline.Close.Float64() + high := kline.High.Float64() + low := kline.Low.Float64() + newClose := (open + high + low + cloze) / 4. + newOpen := (inc.Open.Last() + inc.Close.Last()) / 2. + inc.Close.Update(newClose) + inc.Open.Update(newOpen) + inc.High.Update(math.Max(math.Max(high, newOpen), newClose)) + inc.Low.Update(math.Min(math.Min(low, newOpen), newClose)) + inc.Volume.Update(kline.Volume.Float64()) +} diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index 4315e4fb0..252d21f1b 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -3,7 +3,6 @@ package ewoDgtrd import ( "context" "fmt" - "math" "sync" "github.com/sirupsen/logrus" @@ -23,8 +22,8 @@ func init() { } type Strategy struct { - Position *types.Position `json:"position,omitempty", persistence:"position"` - ProfitStats *types.ProfitStats `json:"profitStats,omitempty", persistence:"profit_stats"` + Position *types.Position `json:"position,omitempty" persistence:"position"` + ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` Market types.Market Session *bbgo.ExchangeSession @@ -32,10 +31,12 @@ type Strategy struct { Stoploss fixedpoint.Value `json:"stoploss"` Symbol string `json:"symbol"` Interval types.Interval `json:"interval"` - UseEma bool `json:"useEma"` // use exponential ma or not - UseSma bool `json:"useSma"` // if UseEma == false, use simple ma or not - SignalWindow int `json:"sigWin"` // signal window - DisableShortStop bool `json:"disableShortStop"` // disable TP/SL on short + UseEma bool `json:"useEma"` // use exponential ma or not + UseSma bool `json:"useSma"` // if UseEma == false, use simple ma or not + SignalWindow int `json:"sigWin"` // signal window + DisableShortStop bool `json:"disableShortStop"` // disable TP/SL on short + FilterHigh float64 `json:"ccistochFilterHigh"` // high filter for CCI Stochastic indicator + FilterLow float64 `json:"ccistochFilterLow"` // low filter for CCI Stochastic indicator KLineStartTime types.Time KLineEndTime types.Time @@ -44,7 +45,6 @@ type Strategy struct { *bbgo.Notifiability *bbgo.Persistence *bbgo.Graceful - bbgo.SmartStops bbgo.StrategyController activeMakerOrders *bbgo.LocalActiveOrderBook @@ -76,7 +76,7 @@ func (s *Strategy) InstanceID() string { } func (s *Strategy) Initialize() error { - return s.SmartStops.InitializeStopControllers(s.Symbol) + return nil } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { @@ -85,8 +85,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) - - s.SmartStops.Subscribe(session) } type UpdatableSeries interface { @@ -96,19 +94,23 @@ type UpdatableSeries interface { // Refer: https://tw.tradingview.com/script/XZyG5SOx-CCI-Stochastic-and-a-quick-lesson-on-Scalping-Trading-Systems/ type CCISTOCH struct { - cci *indicator.CCI - stoch *indicator.STOCH - ma *indicator.SMA + cci *indicator.CCI + stoch *indicator.STOCH + ma *indicator.SMA + filterHigh float64 + filterLow float64 } -func NewCCISTOCH(i types.Interval) *CCISTOCH { - cci := &indicator.CCI{IntervalWindow: types.IntervalWindow{i, 28}} - stoch := &indicator.STOCH{IntervalWindow: types.IntervalWindow{i, 28}} - ma := &indicator.SMA{IntervalWindow: types.IntervalWindow{i, 3}} +func NewCCISTOCH(i types.Interval, filterHigh, filterLow float64) *CCISTOCH { + cci := &indicator.CCI{IntervalWindow: types.IntervalWindow{Interval: i, Window: 28}} + stoch := &indicator.STOCH{IntervalWindow: types.IntervalWindow{Interval: i, Window: 28}} + ma := &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: i, Window: 3}} return &CCISTOCH{ - cci: cci, - stoch: stoch, - ma: ma, + cci: cci, + stoch: stoch, + ma: ma, + filterHigh: filterHigh, + filterLow: filterLow, } } @@ -122,14 +124,14 @@ func (inc *CCISTOCH) BuySignal() bool { hasGrey := false for i := 0; i < len(inc.ma.Values); i++ { v := inc.ma.Index(i) - if v > 80 { + if v > inc.filterHigh { return false } - if v >= 20 && v <= 80 { + if v >= inc.filterLow && v <= inc.filterHigh { hasGrey = true continue } - if v < 20 { + if v < inc.filterLow { return hasGrey } } @@ -140,14 +142,14 @@ func (inc *CCISTOCH) SellSignal() bool { hasGrey := false for i := 0; i < len(inc.ma.Values); i++ { v := inc.ma.Index(i) - if v < 20 { + if v < inc.filterLow { return false } - if v >= 20 && v <= 80 { + if v >= inc.filterLow && v <= inc.filterHigh { hasGrey = true continue } - if v > 80 { + if v > inc.filterHigh { return hasGrey } } @@ -193,84 +195,6 @@ func (inc *VWEMA) UpdateVal(price float64, vol float64) { inc.V.Update(vol) } -type Queue struct { - arr []float64 - size int -} - -func NewQueue(size int) *Queue { - return &Queue{ - arr: make([]float64, 0, size), - size: size, - } -} - -func (inc *Queue) Last() float64 { - if len(inc.arr) == 0 { - return 0 - } - return inc.arr[len(inc.arr)-1] -} - -func (inc *Queue) Index(i int) float64 { - if len(inc.arr)-i-1 < 0 { - return 0 - } - return inc.arr[len(inc.arr)-i-1] -} - -func (inc *Queue) Length() int { - return len(inc.arr) -} - -func (inc *Queue) Update(v float64) { - inc.arr = append(inc.arr, v) - if len(inc.arr) > inc.size { - inc.arr = inc.arr[len(inc.arr)-inc.size:] - } -} - -type HeikinAshi struct { - Close *Queue - Open *Queue - High *Queue - Low *Queue - Volume *Queue -} - -func NewHeikinAshi(size int) *HeikinAshi { - return &HeikinAshi{ - Close: NewQueue(size), - Open: NewQueue(size), - High: NewQueue(size), - Low: NewQueue(size), - Volume: NewQueue(size), - } -} - -func (s *HeikinAshi) Print() string { - return fmt.Sprintf("Heikin c: %.3f, o: %.3f, h: %.3f, l: %.3f, v: %.3f", - s.Close.Last(), - s.Open.Last(), - s.High.Last(), - s.Low.Last(), - s.Volume.Last()) -} - -func (inc *HeikinAshi) Update(kline types.KLine) { - open := kline.Open.Float64() - cloze := kline.Close.Float64() - high := kline.High.Float64() - low := kline.Low.Float64() - newClose := (open + high + low + cloze) / 4. - newOpen := (inc.Open.Last() + inc.Close.Last()) / 2. - inc.Close.Update(newClose) - inc.Open.Update(newOpen) - inc.High.Update(math.Max(math.Max(high, newOpen), newClose)) - inc.Low.Update(math.Min(math.Min(low, newOpen), newClose)) - inc.Volume.Update(kline.Volume.Float64()) -} - func (s *Strategy) SetupIndicators() { store, ok := s.Session.MarketDataStore(s.Symbol) if !ok { @@ -278,8 +202,10 @@ func (s *Strategy) SetupIndicators() { return } - s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{s.Interval, 34}} - s.ccis = NewCCISTOCH(s.Interval) + window5 := types.IntervalWindow{Interval: s.Interval, Window: 5} + window34 := types.IntervalWindow{Interval: s.Interval, Window: 34} + s.atr = &indicator.ATR{IntervalWindow: window34} + s.ccis = NewCCISTOCH(s.Interval, s.FilterHigh, s.FilterLow) if s.UseHeikinAshi { s.heikinAshi = NewHeikinAshi(50) @@ -316,8 +242,8 @@ func (s *Strategy) SetupIndicators() { } }) if s.UseEma { - ema5 := &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 5}} - ema34 := &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 34}} + ema5 := &indicator.EWMA{IntervalWindow: window5} + ema34 := &indicator.EWMA{IntervalWindow: window34} store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { if s.Interval != interval { return @@ -337,8 +263,8 @@ func (s *Strategy) SetupIndicators() { s.ma5 = ema5 s.ma34 = ema34 } else if s.UseSma { - sma5 := &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, 5}} - sma34 := &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, 34}} + sma5 := &indicator.SMA{IntervalWindow: window5} + sma34 := &indicator.SMA{IntervalWindow: window34} store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { if s.Interval != interval { return @@ -359,12 +285,12 @@ func (s *Strategy) SetupIndicators() { s.ma34 = sma34 } else { evwma5 := &VWEMA{ - PV: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 5}}, - V: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 5}}, + PV: &indicator.EWMA{IntervalWindow: window5}, + V: &indicator.EWMA{IntervalWindow: window5}, } evwma34 := &VWEMA{ - PV: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 34}}, - V: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 34}}, + PV: &indicator.EWMA{IntervalWindow: window34}, + V: &indicator.EWMA{IntervalWindow: window34}, } store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { if s.Interval != interval { @@ -408,19 +334,19 @@ func (s *Strategy) SetupIndicators() { } }) if s.UseEma { - s.ma5 = indicatorSet.EWMA(types.IntervalWindow{s.Interval, 5}) - s.ma34 = indicatorSet.EWMA(types.IntervalWindow{s.Interval, 34}) + s.ma5 = indicatorSet.EWMA(window5) + s.ma34 = indicatorSet.EWMA(window34) } else if s.UseSma { - s.ma5 = indicatorSet.SMA(types.IntervalWindow{s.Interval, 5}) - s.ma34 = indicatorSet.SMA(types.IntervalWindow{s.Interval, 34}) + s.ma5 = indicatorSet.SMA(window5) + s.ma34 = indicatorSet.SMA(window34) } else { evwma5 := &VWEMA{ - PV: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 5}}, - V: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 5}}, + PV: &indicator.EWMA{IntervalWindow: window5}, + V: &indicator.EWMA{IntervalWindow: window5}, } evwma34 := &VWEMA{ - PV: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 34}}, - V: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, 34}}, + PV: &indicator.EWMA{IntervalWindow: window34}, + V: &indicator.EWMA{IntervalWindow: window34}, } store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { if s.Interval != interval { @@ -442,8 +368,9 @@ func (s *Strategy) SetupIndicators() { } s.ewo = types.Mul(types.Minus(types.Div(s.ma5, s.ma34), 1.0), 100.) + windowSignal := types.IntervalWindow{Interval: s.Interval, Window: s.SignalWindow} if s.UseEma { - sig := &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, s.SignalWindow}} + sig := &indicator.EWMA{IntervalWindow: windowSignal} store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { if interval != s.Interval { return @@ -461,7 +388,7 @@ func (s *Strategy) SetupIndicators() { }) s.ewoSignal = sig } else if s.UseSma { - sig := &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, s.SignalWindow}} + sig := &indicator.SMA{IntervalWindow: windowSignal} store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { if interval != s.Interval { return @@ -480,8 +407,8 @@ func (s *Strategy) SetupIndicators() { s.ewoSignal = sig } else { sig := &VWEMA{ - PV: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, s.SignalWindow}}, - V: &indicator.EWMA{IntervalWindow: types.IntervalWindow{s.Interval, s.SignalWindow}}, + PV: &indicator.EWMA{IntervalWindow: windowSignal}, + V: &indicator.EWMA{IntervalWindow: windowSignal}, } store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { if interval != s.Interval { @@ -691,15 +618,17 @@ func (s *Strategy) CancelAll(ctx context.Context) { // Trading Rules: // - buy / sell the whole asset -// - SL/TP by atr (buyprice - 2 * atr, sellprice + 2 * atr) +// - SL by atr (lastprice < buyprice - atr) || (lastprice > sellprice + atr) +// - TP by cci stoch invert or ewo invert +// - TP by (lastprice < peak price - atr) || (lastprice > bottom price + atr) // - SL by s.Stoploss (Abs(price_diff / price) > s.Stoploss) // - entry condition on ewo(Elliott wave oscillator) Crosses ewoSignal(ma on ewo, signalWindow) // * buy signal on crossover // * sell signal on crossunder // - and filtered by the following rules: -// * buy: prev buy signal ON and current sell signal OFF, kline Close > Open, Close > ma(Window=5), CCI Stochastic Buy signal -// * sell: prev buy signal OFF and current sell signal ON, kline Close < Open, Close < ma(Window=5), CCI Stochastic Sell signal -// Cancel non-fully filed orders every bar +// * buy: buy signal ON, kline Close > Open, Close > ma(Window=5), CCI Stochastic Buy signal +// * sell: sell signal ON, kline Close < Open, Close < ma(Window=5), CCI Stochastic Sell signal +// Cancel non-fully filled orders every s.Interval // // ps: kline might refer to heikinashi or normal ohlc func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { @@ -769,14 +698,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) s.tradeCollector.BindStream(session.UserDataStream) - s.SmartStops.RunStopControllers(ctx, session, s.tradeCollector) - s.SetupIndicators() sellOrderTPSL := func(price fixedpoint.Value) { balances := session.GetAccount().Balances() quoteBalance := balances[s.Market.QuoteCurrency].Available - atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) + atr := fixedpoint.NewFromFloat(s.atr.Last()) lastPrice := price var ok bool if s.Environment.IsBackTesting() { @@ -796,14 +723,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bottomBack := s.bottomPrice spBack := s.sellPrice if !quoteBalance.IsZero() && !s.sellPrice.IsZero() && !s.DisableShortStop { - // longSignal := types.CrossOver(s.ewo, s.ewoSignal) + longSignal := types.CrossOver(s.ewo, s.ewoSignal) // TP - /*if lastPrice.Compare(s.sellPrice) < 0 && (s.ccis.BuySignal() || longSignal.Last()) { + if lastPrice.Compare(s.sellPrice) < 0 && (s.ccis.BuySignal() || longSignal.Last()) { buyall = true s.bottomPrice = fixedpoint.Zero takeProfit = true - }*/ - if !atrx2.IsZero() && s.bottomPrice.Add(atrx2).Compare(lastPrice) >= 0 && + } + if !atr.IsZero() && s.bottomPrice.Add(atr).Compare(lastPrice) >= 0 && lastPrice.Compare(s.sellPrice) < 0 { buyall = true s.bottomPrice = fixedpoint.Zero @@ -811,7 +738,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } // SL - /*if (!atrx2.IsZero() && s.bottomPrice.Add(atrx2).Compare(lastPrice) <= 0) || + /*if (!atr.IsZero() && s.bottomPrice.Add(atr).Compare(lastPrice) <= 0) || lastPrice.Sub(s.bottomPrice).Div(lastPrice).Compare(s.Stoploss) > 0 { if lastPrice.Compare(s.sellPrice) < 0 { takeProfit = true @@ -819,7 +746,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se buyall = true s.bottomPrice = fixedpoint.Zero }*/ - if (!atrx2.IsZero() && s.sellPrice.Add(atrx2).Compare(lastPrice) <= 0) || + if (!atr.IsZero() && s.sellPrice.Add(atr).Compare(lastPrice) <= 0) || lastPrice.Sub(s.sellPrice).Div(s.sellPrice).Compare(s.Stoploss) > 0 { buyall = true s.bottomPrice = fixedpoint.Zero @@ -829,9 +756,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.Warnf("buyall TPSL %v %v", s.Position.GetBase(), quoteBalance) if s.ClosePosition(ctx) { if takeProfit { - log.Errorf("takeprofit buy at %v, avg %v, l: %v, atrx2: %v", lastPrice, spBack, bottomBack, atrx2) + log.Errorf("takeprofit buy at %v, avg %v, l: %v, atr: %v", lastPrice, spBack, bottomBack, atr) } else { - log.Errorf("stoploss buy at %v, avg %v, l: %v, atrx2: %v", lastPrice, spBack, bottomBack, atrx2) + log.Errorf("stoploss buy at %v, avg %v, l: %v, atr: %v", lastPrice, spBack, bottomBack, atr) } } } @@ -839,7 +766,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se buyOrderTPSL := func(price fixedpoint.Value) { balances := session.GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Available - atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) + atr := fixedpoint.NewFromFloat(s.atr.Last()) lastPrice := price var ok bool if s.Environment.IsBackTesting() { @@ -861,7 +788,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if !baseBalance.IsZero() && !s.buyPrice.IsZero() { shortSignal := types.CrossUnder(s.ewo, s.ewoSignal) // TP - if !atrx2.IsZero() && s.peakPrice.Sub(atrx2).Compare(lastPrice) >= 0 && + if !atr.IsZero() && s.peakPrice.Sub(atr).Compare(lastPrice) >= 0 && lastPrice.Compare(s.buyPrice) > 0 { sellall = true s.peakPrice = fixedpoint.Zero @@ -875,7 +802,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // SL /*if s.peakPrice.Sub(lastPrice).Div(s.peakPrice).Compare(s.Stoploss) > 0 || - (!atrx2.IsZero() && s.peakPrice.Sub(atrx2).Compare(lastPrice) >= 0) { + (!atr.IsZero() && s.peakPrice.Sub(atr).Compare(lastPrice) >= 0) { if lastPrice.Compare(s.buyPrice) > 0 { takeProfit = true } @@ -883,7 +810,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.peakPrice = fixedpoint.Zero }*/ if s.buyPrice.Sub(lastPrice).Div(s.buyPrice).Compare(s.Stoploss) > 0 || - (!atrx2.IsZero() && s.buyPrice.Sub(atrx2).Compare(lastPrice) >= 0) { + (!atr.IsZero() && s.buyPrice.Sub(atr).Compare(lastPrice) >= 0) { sellall = true s.peakPrice = fixedpoint.Zero } @@ -893,9 +820,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.Warnf("sellall TPSL %v", s.Position.GetBase()) if s.ClosePosition(ctx) { if takeProfit { - log.Errorf("takeprofit sell at %v, avg %v, h: %v, atrx2: %v", lastPrice, bpBack, peakBack, atrx2) + log.Errorf("takeprofit sell at %v, avg %v, h: %v, atr: %v", lastPrice, bpBack, peakBack, atr) } else { - log.Errorf("stoploss sell at %v, avg %v, h: %v, atrx2: %v", lastPrice, bpBack, peakBack, atrx2) + log.Errorf("stoploss sell at %v, avg %v, h: %v, atr: %v", lastPrice, bpBack, peakBack, atr) } } } @@ -911,7 +838,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bestAsk := ticker.Sell var midPrice fixedpoint.Value - // TODO: for go1.18 we can use TryLock, use build flag to support this if tryLock(&s.lock) { if !bestAsk.IsZero() && !bestBid.IsZero() { s.midPrice = bestAsk.Add(bestBid).Div(types.Two) @@ -965,9 +891,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se balances := session.GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Available quoteBalance := balances[s.Market.QuoteCurrency].Available - atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) - log.Infof("Get last price: %v, ewo %f, ewoSig %f, ccis: %f, atrx2 %v, kline: %v, balance[base]: %v balance[quote]: %v", - lastPrice, s.ewo.Last(), s.ewoSignal.Last(), s.ccis.ma.Last(), atrx2, kline, baseBalance, quoteBalance) + atr := fixedpoint.NewFromFloat(s.atr.Last()) + log.Infof("Get last price: %v, ewo %f, ewoSig %f, ccis: %f, atr %v, kline: %v, balance[base]: %v balance[quote]: %v", + lastPrice, s.ewo.Last(), s.ewoSignal.Last(), s.ccis.ma.Last(), atr, kline, baseBalance, quoteBalance) } if kline.Interval != s.Interval { @@ -1005,10 +931,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se IsBear, !bull, shortSignal.Index(1), longSignal.Last()) } - price := lastPrice - if longSignal.Index(1) && !shortSignal.Last() && IsBull { + if longSignal.Last() && IsBull { + // on long, set price to lastPrice - atr / 2 + price := lastPrice.Sub(fixedpoint.NewFromFloat(s.atr.Last() / 2)) s.PlaceBuyOrder(ctx, price) - } else if shortSignal.Index(1) && !longSignal.Last() && IsBear { + } else if shortSignal.Last() && IsBear { + // on short, set price to lastPrice + atr / 2 + price := lastPrice.Add(fixedpoint.NewFromFloat(s.atr.Last() / 2)) s.PlaceSellOrder(ctx, price) } }) From 99122f44bcd7ed70f4a84caec3d2de63caffc46e Mon Sep 17 00:00:00 2001 From: zenix Date: Tue, 24 May 2022 22:55:43 +0900 Subject: [PATCH 2/5] fix: backtest kline prev close -> new open, pass exchange as pointer (for Mutex passing), ewo add filter ma34 --- config/ewo_dgtrd.yaml | 12 +- pkg/backtest/exchange.go | 4 +- pkg/backtest/matching.go | 9 ++ pkg/strategy/ewoDgtrd/strategy.go | 180 +++++++++++++++++------------- 4 files changed, 119 insertions(+), 86 deletions(-) diff --git a/config/ewo_dgtrd.yaml b/config/ewo_dgtrd.yaml index cac6f1362..310126414 100644 --- a/config/ewo_dgtrd.yaml +++ b/config/ewo_dgtrd.yaml @@ -11,20 +11,22 @@ exchangeStrategies: ewo_dgtrd: symbol: MATICUSDT # kline interval for indicators - interval: 2h + interval: 15m # use ema as MA - useEma: false + useEma: true # use sma as MA, used when ema is false # if both sma and ema are false, use EVMA useSma: false # ewo signal line window size - sigWin: 8 + sigWin: 5 # SL percentage from entry price stoploss: 15% # use HeikinAshi klines instead of normal OHLC useHeikinAshi: true # disable SL when short disableShortStop: false + # disable SL when long + disableLongStop: false # CCI Stochastic Indicator high filter ccistochFilterHigh: 80 # CCI Stochastic Indicator low filter @@ -50,5 +52,5 @@ backtest: #makerFeeRate: 0 #takerFeeRate: 15 balances: - MATIC: 5000.0 - USDT: 10000 + MATIC: 000.0 + USDT: 15000.0 diff --git a/pkg/backtest/exchange.go b/pkg/backtest/exchange.go index d0703c282..f6e325619 100644 --- a/pkg/backtest/exchange.go +++ b/pkg/backtest/exchange.go @@ -281,11 +281,11 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { return e.markets, nil } -func (e Exchange) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []types.Deposit, err error) { +func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []types.Deposit, err error) { return nil, nil } -func (e Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []types.Withdraw, err error) { +func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []types.Withdraw, err error) { return nil, nil } diff --git a/pkg/backtest/matching.go b/pkg/backtest/matching.go index f212ef4e5..ddb80b07f 100644 --- a/pkg/backtest/matching.go +++ b/pkg/backtest/matching.go @@ -410,6 +410,15 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders func (m *SimplePriceMatching) processKLine(kline types.KLine) { m.CurrentTime = kline.EndTime.Time() m.LastKLine = kline + if m.LastPrice.IsZero() { + m.LastPrice = kline.Open + } else { + if m.LastPrice.Compare(kline.Open) > 0 { + m.BuyToPrice(kline.Open) + } else { + m.SellToPrice(kline.Open) + } + } switch kline.Direction() { case types.DirectionDown: diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index 252d21f1b..52218fb72 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -2,6 +2,7 @@ package ewoDgtrd import ( "context" + "errors" "fmt" "sync" @@ -34,7 +35,8 @@ type Strategy struct { UseEma bool `json:"useEma"` // use exponential ma or not UseSma bool `json:"useSma"` // if UseEma == false, use simple ma or not SignalWindow int `json:"sigWin"` // signal window - DisableShortStop bool `json:"disableShortStop"` // disable TP/SL on short + DisableShortStop bool `json:"disableShortStop"` // disable SL on short + DisableLongStop bool `json:"disableLongStop"` // disable SL on long FilterHigh float64 `json:"ccistochFilterHigh"` // high filter for CCI Stochastic indicator FilterLow float64 `json:"ccistochFilterLow"` // low filter for CCI Stochastic indicator @@ -441,15 +443,15 @@ func (s *Strategy) SetupIndicators() { } } -func (s *Strategy) validateOrder(order *types.SubmitOrder) bool { +func (s *Strategy) validateOrder(order *types.SubmitOrder) error { if order.Type == types.OrderTypeMarket && order.TimeInForce != "" { - return false + return errors.New("wrong field: market vs TimeInForce") } if order.Side == types.SideTypeSell { baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) if !ok { log.Error("cannot get account") - return false + return errors.New("cannot get account") } if order.Quantity.Compare(baseBalance.Available) > 0 { order.Quantity = baseBalance.Available @@ -459,7 +461,7 @@ func (s *Strategy) validateOrder(order *types.SubmitOrder) bool { price, ok = s.Session.LastPrice(s.Symbol) if !ok { log.Error("no price") - return false + return errors.New("no price") } } orderAmount := order.Quantity.Mul(price) @@ -467,39 +469,39 @@ func (s *Strategy) validateOrder(order *types.SubmitOrder) bool { order.Quantity.Compare(s.Market.MinQuantity) < 0 || orderAmount.Compare(s.Market.MinNotional) < 0 { log.Debug("amount fail") - return false + return errors.New("amount fail") } - return true + return nil } else if order.Side == types.SideTypeBuy { quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) if !ok { log.Error("cannot get account") - return false + return errors.New("cannot get account") } price := order.Price if price.IsZero() { price, ok = s.Session.LastPrice(s.Symbol) if !ok { log.Error("no price") - return false + return errors.New("no price") } } totalQuantity := quoteBalance.Available.Div(price) if order.Quantity.Compare(totalQuantity) > 0 { log.Error("qty > avail") - return false + return errors.New("qty > avail") } orderAmount := order.Quantity.Mul(price) if order.Quantity.Sign() <= 0 || orderAmount.Compare(s.Market.MinNotional) < 0 || order.Quantity.Compare(s.Market.MinQuantity) < 0 { log.Debug("amount fail") - return false + return errors.New("amount fail") } - return true + return nil } log.Error("side error") - return false + return errors.New("side error") } @@ -524,8 +526,8 @@ func (s *Strategy) PlaceBuyOrder(ctx context.Context, price fixedpoint.Value) { Market: s.Market, TimeInForce: types.TimeInForceGTC, } - if !s.validateOrder(&order) { - log.Debugf("validation failed %v", order) + if err := s.validateOrder(&order); err != nil { + log.Infof("validation failed %v: %v", order, err) return } // strong long @@ -557,8 +559,8 @@ func (s *Strategy) PlaceSellOrder(ctx context.Context, price fixedpoint.Value) { Price: price, TimeInForce: types.TimeInForceGTC, } - if !s.validateOrder(&order) { - log.Debugf("validation failed %v", order) + if err := s.validateOrder(&order); err != nil { + log.Infof("validation failed %v: %v", order, err) return } @@ -583,8 +585,8 @@ func (s *Strategy) ClosePosition(ctx context.Context) bool { return true } order.TimeInForce = "" - if !s.validateOrder(order) { - log.Errorf("cannot place close order %v", order) + if err := s.validateOrder(order); err != nil { + log.Errorf("cannot place close order %v: %v", order, err) return false } @@ -600,10 +602,10 @@ func (s *Strategy) ClosePosition(ctx context.Context) bool { return true } -func (s *Strategy) CancelAll(ctx context.Context) { +func (s *Strategy) CancelAll(ctx context.Context, side types.SideType) { var toCancel []types.Order for _, order := range s.orderStore.Orders() { - if order.Status == types.OrderStatusNew || order.Status == types.OrderStatusPartiallyFilled { + if order.Status == types.OrderStatusNew || order.Status == types.OrderStatusPartiallyFilled && order.Side == side { toCancel = append(toCancel, order) } } @@ -618,9 +620,9 @@ func (s *Strategy) CancelAll(ctx context.Context) { // Trading Rules: // - buy / sell the whole asset -// - SL by atr (lastprice < buyprice - atr) || (lastprice > sellprice + atr) +// - SL by atr (lastprice < buyprice - atr * 2) || (lastprice > sellprice + atr * 2) // - TP by cci stoch invert or ewo invert -// - TP by (lastprice < peak price - atr) || (lastprice > bottom price + atr) +// - TP by (lastprice < peak price - atr * 2) || (lastprice > bottom price + atr * 2) // - SL by s.Stoploss (Abs(price_diff / price) > s.Stoploss) // - entry condition on ewo(Elliott wave oscillator) Crosses ewoSignal(ma on ewo, signalWindow) // * buy signal on crossover @@ -674,16 +676,18 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se sign := s.Position.GetBase().Sign() if sign > 0 { log.Infof("base become positive, %v", trade) - s.buyPrice = trade.Price - s.peakPrice = trade.Price + s.buyPrice = s.Position.AverageCost + s.sellPrice = fixedpoint.Zero + s.peakPrice = s.Position.AverageCost } else if sign == 0 { log.Infof("base become zero") s.buyPrice = fixedpoint.Zero s.sellPrice = fixedpoint.Zero } else { log.Infof("base become negative, %v", trade) - s.sellPrice = trade.Price - s.bottomPrice = trade.Price + s.buyPrice = fixedpoint.Zero + s.sellPrice = s.Position.AverageCost + s.bottomPrice = s.Position.AverageCost } } else { log.Infof("base become zero") @@ -701,9 +705,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.SetupIndicators() sellOrderTPSL := func(price fixedpoint.Value) { + if s.Position.GetBase().IsZero() { + return + } + if s.sellPrice.IsZero() { + return + } balances := session.GetAccount().Balances() quoteBalance := balances[s.Market.QuoteCurrency].Available - atr := fixedpoint.NewFromFloat(s.atr.Last()) + atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) lastPrice := price var ok bool if s.Environment.IsBackTesting() { @@ -714,15 +724,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } buyall := false - if !s.sellPrice.IsZero() { - if s.bottomPrice.IsZero() || s.bottomPrice.Compare(price) > 0 { - s.bottomPrice = price - } + if s.bottomPrice.IsZero() || s.bottomPrice.Compare(price) > 0 { + s.bottomPrice = price } takeProfit := false bottomBack := s.bottomPrice spBack := s.sellPrice - if !quoteBalance.IsZero() && !s.sellPrice.IsZero() && !s.DisableShortStop { + if !quoteBalance.IsZero() { longSignal := types.CrossOver(s.ewo, s.ewoSignal) // TP if lastPrice.Compare(s.sellPrice) < 0 && (s.ccis.BuySignal() || longSignal.Last()) { @@ -730,7 +738,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.bottomPrice = fixedpoint.Zero takeProfit = true } - if !atr.IsZero() && s.bottomPrice.Add(atr).Compare(lastPrice) >= 0 && + if !atrx2.IsZero() && s.bottomPrice.Add(atrx2).Compare(lastPrice) >= 0 && lastPrice.Compare(s.sellPrice) < 0 { buyall = true s.bottomPrice = fixedpoint.Zero @@ -738,7 +746,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } // SL - /*if (!atr.IsZero() && s.bottomPrice.Add(atr).Compare(lastPrice) <= 0) || + /*if (!atrx2.IsZero() && s.bottomPrice.Add(atrx2).Compare(lastPrice) <= 0) || lastPrice.Sub(s.bottomPrice).Div(lastPrice).Compare(s.Stoploss) > 0 { if lastPrice.Compare(s.sellPrice) < 0 { takeProfit = true @@ -746,8 +754,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se buyall = true s.bottomPrice = fixedpoint.Zero }*/ - if (!atr.IsZero() && s.sellPrice.Add(atr).Compare(lastPrice) <= 0) || - lastPrice.Sub(s.sellPrice).Div(s.sellPrice).Compare(s.Stoploss) > 0 { + if !s.DisableShortStop && ((!atrx2.IsZero() && s.sellPrice.Add(atrx2).Compare(lastPrice) <= 0) || + lastPrice.Sub(s.sellPrice).Div(s.sellPrice).Compare(s.Stoploss) > 0) { buyall = true s.bottomPrice = fixedpoint.Zero } @@ -756,17 +764,23 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.Warnf("buyall TPSL %v %v", s.Position.GetBase(), quoteBalance) if s.ClosePosition(ctx) { if takeProfit { - log.Errorf("takeprofit buy at %v, avg %v, l: %v, atr: %v", lastPrice, spBack, bottomBack, atr) + log.Errorf("takeprofit buy at %v, avg %v, l: %v, atrx2: %v", lastPrice, spBack, bottomBack, atrx2) } else { - log.Errorf("stoploss buy at %v, avg %v, l: %v, atr: %v", lastPrice, spBack, bottomBack, atr) + log.Errorf("stoploss buy at %v, avg %v, l: %v, atrx2: %v", lastPrice, spBack, bottomBack, atrx2) } } } } buyOrderTPSL := func(price fixedpoint.Value) { + if s.Position.GetBase().IsZero() { + return + } + if s.buyPrice.IsZero() { + return + } balances := session.GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Available - atr := fixedpoint.NewFromFloat(s.atr.Last()) + atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) lastPrice := price var ok bool if s.Environment.IsBackTesting() { @@ -777,18 +791,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } sellall := false - if !s.buyPrice.IsZero() { - if s.peakPrice.IsZero() || s.peakPrice.Compare(price) < 0 { - s.peakPrice = price - } + if s.peakPrice.IsZero() || s.peakPrice.Compare(price) < 0 { + s.peakPrice = price } takeProfit := false peakBack := s.peakPrice bpBack := s.buyPrice - if !baseBalance.IsZero() && !s.buyPrice.IsZero() { + if !baseBalance.IsZero() { shortSignal := types.CrossUnder(s.ewo, s.ewoSignal) // TP - if !atr.IsZero() && s.peakPrice.Sub(atr).Compare(lastPrice) >= 0 && + if !atrx2.IsZero() && s.peakPrice.Sub(atrx2).Compare(lastPrice) >= 0 && lastPrice.Compare(s.buyPrice) > 0 { sellall = true s.peakPrice = fixedpoint.Zero @@ -802,15 +814,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // SL /*if s.peakPrice.Sub(lastPrice).Div(s.peakPrice).Compare(s.Stoploss) > 0 || - (!atr.IsZero() && s.peakPrice.Sub(atr).Compare(lastPrice) >= 0) { + (!atrx2.IsZero() && s.peakPrice.Sub(atrx2).Compare(lastPrice) >= 0) { if lastPrice.Compare(s.buyPrice) > 0 { takeProfit = true } sellall = true s.peakPrice = fixedpoint.Zero }*/ - if s.buyPrice.Sub(lastPrice).Div(s.buyPrice).Compare(s.Stoploss) > 0 || - (!atr.IsZero() && s.buyPrice.Sub(atr).Compare(lastPrice) >= 0) { + if !s.DisableLongStop && (s.buyPrice.Sub(lastPrice).Div(s.buyPrice).Compare(s.Stoploss) > 0 || + (!atrx2.IsZero() && s.buyPrice.Sub(atrx2).Compare(lastPrice) >= 0)) { sellall = true s.peakPrice = fixedpoint.Zero } @@ -820,9 +832,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.Warnf("sellall TPSL %v", s.Position.GetBase()) if s.ClosePosition(ctx) { if takeProfit { - log.Errorf("takeprofit sell at %v, avg %v, h: %v, atr: %v", lastPrice, bpBack, peakBack, atr) + log.Errorf("takeprofit sell at %v, avg %v, h: %v, atrx2: %v", lastPrice, bpBack, peakBack, atrx2) } else { - log.Errorf("stoploss sell at %v, avg %v, h: %v, atr: %v", lastPrice, bpBack, peakBack, atr) + log.Errorf("stoploss sell at %v, avg %v, h: %v, atrx2: %v", lastPrice, bpBack, peakBack, atrx2) } } } @@ -884,14 +896,22 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } else { s.lock.RLock() - lastPrice = s.midPrice + if s.midPrice.IsZero() { + lastPrice, ok = session.LastPrice(s.Symbol) + if !ok { + log.Errorf("cannot get last price") + return + } + } else { + lastPrice = s.midPrice + } s.lock.RUnlock() } + balances := session.GetAccount().Balances() + baseBalance := balances[s.Market.BaseCurrency].Available + quoteBalance := balances[s.Market.QuoteCurrency].Available + atr := fixedpoint.NewFromFloat(s.atr.Last()) if !s.Environment.IsBackTesting() { - balances := session.GetAccount().Balances() - baseBalance := balances[s.Market.BaseCurrency].Available - quoteBalance := balances[s.Market.QuoteCurrency].Available - atr := fixedpoint.NewFromFloat(s.atr.Last()) log.Infof("Get last price: %v, ewo %f, ewoSig %f, ccis: %f, atr %v, kline: %v, balance[base]: %v balance[quote]: %v", lastPrice, s.ewo.Last(), s.ewoSignal.Last(), s.ccis.ma.Last(), atr, kline, baseBalance, quoteBalance) } @@ -900,10 +920,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } - s.CancelAll(ctx) - // To get the threshold for ewo - // mean := types.Mean(s.ewo, 10) + // mean := types.Mean(types.Abs(s.ewo), 10) // std := types.Stdev(s.ewo, 10) longSignal := types.CrossOver(s.ewo, s.ewoSignal) @@ -912,33 +930,37 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se var bull, breakThrough, breakDown bool if s.UseHeikinAshi { bull = s.heikinAshi.Close.Last() > s.heikinAshi.Open.Last() - breakThrough = s.heikinAshi.Close.Last() > s.ma5.Last() - breakDown = s.heikinAshi.Close.Last() < s.ma5.Last() + breakThrough = s.heikinAshi.Close.Last() > s.ma5.Last() && s.heikinAshi.Close.Last() > s.ma34.Last() + breakDown = s.heikinAshi.Close.Last() < s.ma5.Last() && s.heikinAshi.Close.Last() < s.ma34.Last() + } else { bull = kline.Close.Compare(kline.Open) > 0 - breakThrough = kline.Close.Float64() > s.ma5.Last() - breakDown = kline.Close.Float64() < s.ma5.Last() + breakThrough = kline.Close.Float64() > s.ma5.Last() && kline.Close.Float64() > s.ma34.Last() + breakDown = kline.Close.Float64() < s.ma5.Last() && kline.Close.Float64() < s.ma34.Last() } - // kline breakthrough ma5, ma50 trend up, and ewo > threshold - IsBull := bull && breakThrough && s.ccis.BuySignal() // && s.ewo.Last() > mean + 2 * std - // kline downthrough ma5, ma50 trend down, and ewo < threshold - IsBear := !bull && breakDown && s.ccis.SellSignal() // .ewo.Last() < mean - 2 * std + // kline breakthrough ma5, ma34 trend up, and cci Stochastic bull + IsBull := bull && breakThrough && s.ccis.BuySignal() + // kline downthrough ma5, ma34 trend down, and cci Stochastic bear + IsBear := !bull && breakDown && s.ccis.SellSignal() if !s.Environment.IsBackTesting() { - log.Infof("IsBull: %v, bull: %v, longSignal[1]: %v, shortSignal: %v", - IsBull, bull, longSignal.Index(1), shortSignal.Last()) - log.Infof("IsBear: %v, bear: %v, shortSignal[1]: %v, longSignal: %v", - IsBear, !bull, shortSignal.Index(1), longSignal.Last()) + log.Infof("IsBull: %v, bull: %v, longSignal[1]: %v, shortSignal: %v, lastPrice: %v", + IsBull, bull, longSignal.Index(1), shortSignal.Last(), lastPrice) + log.Infof("IsBear: %v, bear: %v, shortSignal[1]: %v, longSignal: %v, lastPrice: %v", + IsBear, !bull, shortSignal.Index(1), longSignal.Last(), lastPrice) } - if longSignal.Last() && IsBull { - // on long, set price to lastPrice - atr / 2 - price := lastPrice.Sub(fixedpoint.NewFromFloat(s.atr.Last() / 2)) - s.PlaceBuyOrder(ctx, price) - } else if shortSignal.Last() && IsBear { - // on short, set price to lastPrice + atr / 2 - price := lastPrice.Add(fixedpoint.NewFromFloat(s.atr.Last() / 2)) - s.PlaceSellOrder(ctx, price) + if longSignal.Index(1) && !shortSignal.Last() && IsBull { + s.CancelAll(ctx, types.SideTypeBuy) + if quoteBalance.Div(lastPrice).Compare(s.Market.MinQuantity) >= 0 && quoteBalance.Compare(s.Market.MinNotional) >= 0 { + s.PlaceBuyOrder(ctx, lastPrice) + } + } + if shortSignal.Index(1) && !longSignal.Last() && IsBear { + s.CancelAll(ctx, types.SideTypeSell) + if baseBalance.Mul(lastPrice).Compare(s.Market.MinNotional) >= 0 && baseBalance.Compare(s.Market.MinQuantity) >= 0 { + s.PlaceSellOrder(ctx, lastPrice) + } } }) s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { From c6bad0ba08c7f2628a73b93bf0ae53088924c6ec Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 25 May 2022 01:48:14 +0900 Subject: [PATCH 3/5] fix: tv chart, price direction in backtest --- apps/backtest-report/components/TradingViewChart.tsx | 6 +++--- pkg/backtest/matching.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/backtest-report/components/TradingViewChart.tsx b/apps/backtest-report/components/TradingViewChart.tsx index ea2fb4390..b91d5bda3 100644 --- a/apps/backtest-report/components/TradingViewChart.tsx +++ b/apps/backtest-report/components/TradingViewChart.tsx @@ -155,7 +155,7 @@ const ordersToMarkets = (interval: string, orders: Array | void): Array 0 ? markers[markers.length - 1] : null; if (lastMarker) { let remainder = lastMarker.time % intervalSecs; @@ -264,7 +264,7 @@ const positionBaseHistoryToLineData = (interval: string, hs: Array 0 && hs[i].time.getTime() === hs[i - 1].time.getTime()) { continue } @@ -296,7 +296,7 @@ const positionAverageCostHistoryToLineData = (interval: string, hs: Array 0 && hs[i].time.getTime() === hs[i - 1].time.getTime()) { continue } diff --git a/pkg/backtest/matching.go b/pkg/backtest/matching.go index ddb80b07f..54bf9c86c 100644 --- a/pkg/backtest/matching.go +++ b/pkg/backtest/matching.go @@ -414,9 +414,9 @@ func (m *SimplePriceMatching) processKLine(kline types.KLine) { m.LastPrice = kline.Open } else { if m.LastPrice.Compare(kline.Open) > 0 { - m.BuyToPrice(kline.Open) - } else { m.SellToPrice(kline.Open) + } else { + m.BuyToPrice(kline.Open) } } From e81216e6787e11f508d3f0843ee78c476b14f59e Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 25 May 2022 16:11:19 +0900 Subject: [PATCH 4/5] fix: TP compare direction in ewo, feature: add trade report fro ewo, fix: db lock on insertion in sqlite3 --- config/ewo_dgtrd.yaml | 8 +- pkg/service/backtest.go | 8 +- pkg/strategy/ewoDgtrd/strategy.go | 180 ++++++++++++++++++++++++++---- 3 files changed, 167 insertions(+), 29 deletions(-) diff --git a/config/ewo_dgtrd.yaml b/config/ewo_dgtrd.yaml index 310126414..392189c6a 100644 --- a/config/ewo_dgtrd.yaml +++ b/config/ewo_dgtrd.yaml @@ -13,14 +13,14 @@ exchangeStrategies: # kline interval for indicators interval: 15m # use ema as MA - useEma: true + useEma: false # use sma as MA, used when ema is false # if both sma and ema are false, use EVMA useSma: false # ewo signal line window size sigWin: 5 # SL percentage from entry price - stoploss: 15% + stoploss: 2% # use HeikinAshi klines instead of normal OHLC useHeikinAshi: true # disable SL when short @@ -43,7 +43,7 @@ sync: backtest: startTime: "2022-05-01" - endTime: "2022-05-24" + endTime: "2022-05-25" symbols: - MATICUSDT sessions: [binance] @@ -52,5 +52,5 @@ backtest: #makerFeeRate: 0 #takerFeeRate: 15 balances: - MATIC: 000.0 + MATIC: 00.0 USDT: 15000.0 diff --git a/pkg/service/backtest.go b/pkg/service/backtest.go index a7fa4337d..f1a1877bd 100644 --- a/pkg/service/backtest.go +++ b/pkg/service/backtest.go @@ -34,7 +34,6 @@ func (s *BacktestService) SyncKLineByInterval(ctx context.Context, exchange type if err := s.BatchInsert(klines); err != nil { return err } - count += len(klines) } log.Debugf("inserted klines %s %s data: %d", symbol, interval.String(), count) @@ -321,8 +320,11 @@ func (s *BacktestService) BatchInsert(kline []types.KLine) error { sql := fmt.Sprintf("INSERT INTO `%s` (`exchange`, `start_time`, `end_time`, `symbol`, `interval`, `open`, `high`, `low`, `close`, `closed`, `volume`, `quote_volume`, `taker_buy_base_volume`, `taker_buy_quote_volume`)"+ " values (:exchange, :start_time, :end_time, :symbol, :interval, :open, :high, :low, :close, :closed, :volume, :quote_volume, :taker_buy_base_volume, :taker_buy_quote_volume); ", tableName) - _, err := s.DB.NamedExec(sql, kline) - return err + tx := s.DB.MustBegin() + if _, err := tx.NamedExec(sql, kline); err != nil { + return err + } + return tx.Commit() } func (s *BacktestService) _deleteDuplicatedKLine(k types.KLine) error { diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index 52218fb72..6312454b6 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -6,6 +6,7 @@ import ( "fmt" "sync" + "github.com/fatih/color" "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" @@ -128,12 +129,10 @@ func (inc *CCISTOCH) BuySignal() bool { v := inc.ma.Index(i) if v > inc.filterHigh { return false - } - if v >= inc.filterLow && v <= inc.filterHigh { + } else if v >= inc.filterLow && v <= inc.filterHigh { hasGrey = true continue - } - if v < inc.filterLow { + } else if v < inc.filterLow { return hasGrey } } @@ -146,12 +145,10 @@ func (inc *CCISTOCH) SellSignal() bool { v := inc.ma.Index(i) if v < inc.filterLow { return false - } - if v >= inc.filterLow && v <= inc.filterHigh { + } else if v >= inc.filterLow && v <= inc.filterHigh { hasGrey = true continue - } - if v > inc.filterHigh { + } else if v > inc.filterHigh { return hasGrey } } @@ -639,6 +636,21 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.peakPrice = fixedpoint.Zero s.bottomPrice = fixedpoint.Zero + counterTPfromPeak := 0 + percentAvgTPfromPeak := 0.0 + counterTPfromCCI := 0 + percentAvgTPfromCCI := 0.0 + counterTPfromLongShort := 0 + percentAvgTPfromLongShort := 0.0 + counterTPfromAtr := 0 + percentAvgTPfromAtr := 0.0 + counterTPfromOrder := 0 + percentAvgTPfromOrder := 0.0 + counterSLfromSL := 0 + percentAvgSLfromSL := 0.0 + counterSLfromOrder := 0 + percentAvgSLfromOrder := 0.0 + s.activeMakerOrders = bbgo.NewLocalActiveOrderBook(s.Symbol) s.activeMakerOrders.BindStream(session.UserDataStream) @@ -713,6 +725,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } balances := session.GetAccount().Balances() quoteBalance := balances[s.Market.QuoteCurrency].Available + atr := fixedpoint.NewFromFloat(s.atr.Last()) atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) lastPrice := price var ok bool @@ -732,17 +745,42 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se spBack := s.sellPrice if !quoteBalance.IsZero() { longSignal := types.CrossOver(s.ewo, s.ewoSignal) + base := fixedpoint.NewFromFloat(s.ma34.Last()) // TP - if lastPrice.Compare(s.sellPrice) < 0 && (s.ccis.BuySignal() || longSignal.Last()) { + if lastPrice.Compare(s.sellPrice) < 0 && (s.ccis.BuySignal() || longSignal.Last() || (!atrx2.IsZero() && base.Sub(atrx2).Compare(lastPrice) >= 0)) { buyall = true s.bottomPrice = fixedpoint.Zero takeProfit = true + + // calculate report + pnlRate := s.sellPrice.Sub(lastPrice).Div(lastPrice).Float64() + if s.ccis.BuySignal() { + percentAvgTPfromCCI = percentAvgTPfromCCI*float64(counterTPfromCCI) + pnlRate + counterTPfromCCI += 1 + percentAvgTPfromCCI /= float64(counterTPfromCCI) + } else if longSignal.Last() { + percentAvgTPfromLongShort = percentAvgTPfromLongShort*float64(counterTPfromLongShort) + pnlRate + counterTPfromLongShort += 1 + percentAvgTPfromLongShort /= float64(counterTPfromLongShort) + } else { + percentAvgTPfromAtr = percentAvgTPfromAtr*float64(counterTPfromAtr) + pnlRate + counterTPfromAtr += 1 + percentAvgTPfromAtr /= float64(counterTPfromAtr) + + } + } - if !atrx2.IsZero() && s.bottomPrice.Add(atrx2).Compare(lastPrice) >= 0 && + if !atr.IsZero() && s.bottomPrice.Add(atr).Compare(lastPrice) <= 0 && lastPrice.Compare(s.sellPrice) < 0 { buyall = true s.bottomPrice = fixedpoint.Zero takeProfit = true + + // calculate report + pnlRate := s.sellPrice.Sub(lastPrice).Div(lastPrice).Float64() + percentAvgTPfromPeak = percentAvgTPfromPeak*float64(counterTPfromPeak) + pnlRate + counterTPfromPeak += 1 + percentAvgTPfromPeak /= float64(counterTPfromPeak) } // SL @@ -754,10 +792,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se buyall = true s.bottomPrice = fixedpoint.Zero }*/ - if !s.DisableShortStop && ((!atrx2.IsZero() && s.sellPrice.Add(atrx2).Compare(lastPrice) <= 0) || + if !s.DisableShortStop && ((!atr.IsZero() && s.sellPrice.Sub(atr).Compare(lastPrice) >= 0) || lastPrice.Sub(s.sellPrice).Div(s.sellPrice).Compare(s.Stoploss) > 0) { buyall = true s.bottomPrice = fixedpoint.Zero + + // calculate report + pnlRate := lastPrice.Sub(s.sellPrice).Div(lastPrice).Float64() + percentAvgSLfromSL = percentAvgSLfromSL*float64(counterSLfromSL) + pnlRate + counterSLfromSL += 1 + percentAvgSLfromSL /= float64(counterSLfromSL) } } if buyall { @@ -780,6 +824,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } balances := session.GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Available + atr := fixedpoint.NewFromFloat(s.atr.Last()) atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) lastPrice := price var ok bool @@ -800,16 +845,39 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if !baseBalance.IsZero() { shortSignal := types.CrossUnder(s.ewo, s.ewoSignal) // TP - if !atrx2.IsZero() && s.peakPrice.Sub(atrx2).Compare(lastPrice) >= 0 && + if !atr.IsZero() && s.peakPrice.Sub(atr).Compare(lastPrice) >= 0 && lastPrice.Compare(s.buyPrice) > 0 { sellall = true s.peakPrice = fixedpoint.Zero takeProfit = true + + // calculate report + pnlRate := lastPrice.Sub(s.buyPrice).Div(s.buyPrice).Float64() + percentAvgTPfromPeak = percentAvgTPfromPeak*float64(counterTPfromPeak) + pnlRate + counterTPfromPeak += 1 + percentAvgTPfromPeak /= float64(counterTPfromPeak) } - if lastPrice.Compare(s.buyPrice) > 0 && (s.ccis.SellSignal() || shortSignal.Last()) { + base := fixedpoint.NewFromFloat(s.ma34.Last()) + if lastPrice.Compare(s.buyPrice) > 0 && (s.ccis.SellSignal() || shortSignal.Last() || (!atrx2.IsZero() && base.Add(atrx2).Compare(lastPrice) <= 0)) { sellall = true s.peakPrice = fixedpoint.Zero takeProfit = true + + // calculate report + pnlRate := lastPrice.Sub(s.buyPrice).Div(s.buyPrice).Float64() + if s.ccis.SellSignal() { + percentAvgTPfromCCI = percentAvgTPfromCCI*float64(counterTPfromCCI) + pnlRate + counterTPfromCCI += 1 + percentAvgTPfromCCI /= float64(counterTPfromCCI) + } else if shortSignal.Last() { + percentAvgTPfromLongShort = percentAvgTPfromLongShort*float64(counterTPfromLongShort) + pnlRate + counterTPfromLongShort += 1 + percentAvgTPfromLongShort /= float64(counterTPfromLongShort) + } else { + percentAvgTPfromAtr = percentAvgTPfromAtr*float64(counterTPfromAtr) + pnlRate + counterTPfromAtr += 1 + percentAvgTPfromAtr /= float64(counterTPfromAtr) + } } // SL @@ -822,9 +890,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.peakPrice = fixedpoint.Zero }*/ if !s.DisableLongStop && (s.buyPrice.Sub(lastPrice).Div(s.buyPrice).Compare(s.Stoploss) > 0 || - (!atrx2.IsZero() && s.buyPrice.Sub(atrx2).Compare(lastPrice) >= 0)) { + (!atr.IsZero() && s.buyPrice.Sub(atr).Compare(lastPrice) >= 0)) { sellall = true s.peakPrice = fixedpoint.Zero + + // calculate report + pnlRate := s.buyPrice.Sub(lastPrice).Div(s.buyPrice).Float64() + percentAvgSLfromSL = percentAvgSLfromSL*float64(counterSLfromSL) + pnlRate + counterSLfromSL += 1 + percentAvgSLfromSL /= float64(counterSLfromSL) } } @@ -908,8 +982,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.lock.RUnlock() } balances := session.GetAccount().Balances() - baseBalance := balances[s.Market.BaseCurrency].Available - quoteBalance := balances[s.Market.QuoteCurrency].Available + baseBalance := balances[s.Market.BaseCurrency].Total() + quoteBalance := balances[s.Market.QuoteCurrency].Total() atr := fixedpoint.NewFromFloat(s.atr.Last()) if !s.Environment.IsBackTesting() { log.Infof("Get last price: %v, ewo %f, ewoSig %f, ccis: %f, atr %v, kline: %v, balance[base]: %v balance[quote]: %v", @@ -951,15 +1025,51 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if longSignal.Index(1) && !shortSignal.Last() && IsBull { - s.CancelAll(ctx, types.SideTypeBuy) - if quoteBalance.Div(lastPrice).Compare(s.Market.MinQuantity) >= 0 && quoteBalance.Compare(s.Market.MinNotional) >= 0 { - s.PlaceBuyOrder(ctx, lastPrice) + price := kline.Close.Sub(atr.Div(types.Two)) + // if total asset (including locked) could be used to buy + if quoteBalance.Div(price).Compare(s.Market.MinQuantity) >= 0 && quoteBalance.Compare(s.Market.MinNotional) >= 0 { + // cancel all buy orders to release lock + s.CancelAll(ctx, types.SideTypeBuy) + + // calculate report + if !s.sellPrice.IsZero() { + if price.Compare(s.sellPrice) > 0 { + pnlRate := price.Sub(s.sellPrice).Div(price).Float64() + percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate + counterTPfromOrder += 1 + percentAvgTPfromOrder /= float64(counterTPfromOrder) + } else { + pnlRate := s.sellPrice.Sub(price).Div(price).Float64() + percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate + counterSLfromOrder += 1 + percentAvgSLfromOrder /= float64(counterSLfromOrder) + } + } + s.PlaceBuyOrder(ctx, price) } } if shortSignal.Index(1) && !longSignal.Last() && IsBear { - s.CancelAll(ctx, types.SideTypeSell) - if baseBalance.Mul(lastPrice).Compare(s.Market.MinNotional) >= 0 && baseBalance.Compare(s.Market.MinQuantity) >= 0 { - s.PlaceSellOrder(ctx, lastPrice) + price := kline.Close.Add(atr.Div(types.Two)) + // if total asset (including locked) could be used to sell + if baseBalance.Mul(price).Compare(s.Market.MinNotional) >= 0 && baseBalance.Compare(s.Market.MinQuantity) >= 0 { + // cancel all sell orders to release lock + s.CancelAll(ctx, types.SideTypeSell) + + // calculate report + if !s.buyPrice.IsZero() { + if price.Compare(s.buyPrice) > 0 { + pnlRate := price.Sub(s.buyPrice).Div(s.buyPrice).Float64() + percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate + counterTPfromOrder += 1 + percentAvgTPfromOrder /= float64(counterTPfromOrder) + } else { + pnlRate := s.buyPrice.Sub(price).Div(s.buyPrice).Float64() + percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate + counterSLfromOrder += 1 + percentAvgSLfromOrder /= float64(counterSLfromOrder) + } + } + s.PlaceSellOrder(ctx, price) } } }) @@ -978,6 +1088,32 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.WithError(err).Errorf("cancel order error") } s.tradeCollector.Process() + color.HiBlue("---- Trade Report (Without Fee) ----") + color.HiBlue("TP:") + color.Blue("\tpeak / bottom with atr: %d, avg pnl rate: %f", counterTPfromPeak, percentAvgTPfromPeak) + color.Blue("\tCCI Stochastic: %d, avg pnl rate: %f", counterTPfromCCI, percentAvgTPfromCCI) + color.Blue("\tLongSignal/ShortSignal: %d, avg pnl rate: %f", counterTPfromLongShort, percentAvgTPfromLongShort) + color.Blue("\tma34 and Atrx2: %d, avg pnl rate: %f", counterTPfromAtr, percentAvgTPfromAtr) + color.Blue("\tActive Order: %d, avg pnl rate: %f", counterTPfromOrder, percentAvgTPfromOrder) + + totalTP := counterTPfromPeak + counterTPfromCCI + counterTPfromLongShort + counterTPfromAtr + counterTPfromOrder + avgProfit := (float64(counterTPfromPeak)*percentAvgTPfromPeak + + float64(counterTPfromCCI)*percentAvgTPfromCCI + + float64(counterTPfromLongShort)*percentAvgTPfromLongShort + + float64(counterTPfromAtr)*percentAvgTPfromAtr + + float64(counterTPfromOrder)*percentAvgTPfromOrder) / float64(totalTP) + color.HiBlue("\tSum: %d, avg pnl rate: %f", totalTP, avgProfit) + + color.HiBlue("SL:") + color.Blue("\tentry SL: %d, avg pnl rate: -%f", counterSLfromSL, percentAvgSLfromSL) + color.Blue("\tActive Order: %d, avg pnl rate: -%f", counterSLfromOrder, percentAvgSLfromOrder) + + totalSL := counterSLfromSL + counterSLfromOrder + avgLoss := (float64(counterSLfromSL)*percentAvgSLfromSL + float64(counterSLfromOrder)*percentAvgSLfromOrder) / float64(totalSL) + color.HiBlue("\tSum: %d, avg pnl rate: -%f", totalSL, avgLoss) + + color.HiBlue("WinRate: %f", float64(totalTP)/float64(totalTP+totalSL)) + }) return nil } From e3a8ef4e699c5561e25e11289df57f13e295ba37 Mon Sep 17 00:00:00 2001 From: zenix Date: Mon, 30 May 2022 12:45:52 +0900 Subject: [PATCH 5/5] fix: statistics on entry/exit on signal changes, fix position check --- config/ewo_dgtrd.yaml | 6 +- pkg/strategy/ewoDgtrd/strategy.go | 470 +++++++++++++++++++--------- pkg/strategy/ewoDgtrd/trylock.go | 5 + pkg/strategy/ewoDgtrd/trylock_18.go | 4 + pkg/types/position.go | 2 +- 5 files changed, 329 insertions(+), 158 deletions(-) diff --git a/config/ewo_dgtrd.yaml b/config/ewo_dgtrd.yaml index 392189c6a..bc4b43f10 100644 --- a/config/ewo_dgtrd.yaml +++ b/config/ewo_dgtrd.yaml @@ -31,6 +31,8 @@ exchangeStrategies: ccistochFilterHigh: 80 # CCI Stochastic Indicator low filter ccistochFilterLow: 20 + # print record exit point in log messages + record: false sync: userDataStream: @@ -43,7 +45,7 @@ sync: backtest: startTime: "2022-05-01" - endTime: "2022-05-25" + endTime: "2022-05-27" symbols: - MATICUSDT sessions: [binance] @@ -52,5 +54,5 @@ backtest: #makerFeeRate: 0 #takerFeeRate: 15 balances: - MATIC: 00.0 + MATIC: 000.0 USDT: 15000.0 diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index 6312454b6..4c3defc07 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -17,6 +17,8 @@ import ( const ID = "ewo_dgtrd" +const record = false + var log = logrus.WithField("strategy", ID) func init() { @@ -41,6 +43,8 @@ type Strategy struct { FilterHigh float64 `json:"ccistochFilterHigh"` // high filter for CCI Stochastic indicator FilterLow float64 `json:"ccistochFilterLow"` // low filter for CCI Stochastic indicator + Record bool `json:"record"` // print record messages on position exit point + KLineStartTime types.Time KLineEndTime types.Time @@ -53,6 +57,8 @@ type Strategy struct { activeMakerOrders *bbgo.LocalActiveOrderBook orderStore *bbgo.OrderStore tradeCollector *bbgo.TradeCollector + entryPrice fixedpoint.Value + waitForTrade bool atr *indicator.ATR ccis *CCISTOCH @@ -194,6 +200,7 @@ func (inc *VWEMA) UpdateVal(price float64, vol float64) { inc.V.Update(vol) } +// Setup the Indicators going to be used func (s *Strategy) SetupIndicators() { store, ok := s.Session.MarketDataStore(s.Symbol) if !ok { @@ -440,6 +447,7 @@ func (s *Strategy) SetupIndicators() { } } +// Utility to evaluate if the order is valid or not to send to the exchange func (s *Strategy) validateOrder(order *types.SubmitOrder) error { if order.Type == types.OrderTypeMarket && order.TimeInForce != "" { return errors.New("wrong field: market vs TimeInForce") @@ -451,7 +459,8 @@ func (s *Strategy) validateOrder(order *types.SubmitOrder) error { return errors.New("cannot get account") } if order.Quantity.Compare(baseBalance.Available) > 0 { - order.Quantity = baseBalance.Available + log.Errorf("qty %v > avail %v", order.Quantity, baseBalance.Available) + return errors.New("qty > avail") } price := order.Price if price.IsZero() { @@ -466,7 +475,7 @@ func (s *Strategy) validateOrder(order *types.SubmitOrder) error { order.Quantity.Compare(s.Market.MinQuantity) < 0 || orderAmount.Compare(s.Market.MinNotional) < 0 { log.Debug("amount fail") - return errors.New("amount fail") + return errors.New(fmt.Sprintf("amount fail: quantity: %v, amount: %v", order.Quantity, orderAmount)) } return nil } else if order.Side == types.SideTypeBuy { @@ -485,7 +494,7 @@ func (s *Strategy) validateOrder(order *types.SubmitOrder) error { } totalQuantity := quoteBalance.Available.Div(price) if order.Quantity.Compare(totalQuantity) > 0 { - log.Error("qty > avail") + log.Errorf("qty %v > avail %v", order.Quantity, totalQuantity) return errors.New("qty > avail") } orderAmount := order.Quantity.Mul(price) @@ -493,7 +502,7 @@ func (s *Strategy) validateOrder(order *types.SubmitOrder) error { orderAmount.Compare(s.Market.MinNotional) < 0 || order.Quantity.Compare(s.Market.MinQuantity) < 0 { log.Debug("amount fail") - return errors.New("amount fail") + return errors.New(fmt.Sprintf("amount fail: quantity: %v, amount: %v", order.Quantity, orderAmount)) } return nil } @@ -502,15 +511,27 @@ func (s *Strategy) validateOrder(order *types.SubmitOrder) error { } -func (s *Strategy) PlaceBuyOrder(ctx context.Context, price fixedpoint.Value) { - if s.Position.GetBase().Add(s.Market.MinQuantity).Sign() < 0 && !s.ClosePosition(ctx) { - log.Errorf("sell position %v remained not closed, skip placing order", s.Position.GetBase()) - return +func (s *Strategy) PlaceBuyOrder(ctx context.Context, price fixedpoint.Value) (*types.Order, *types.Order) { + var closeOrder *types.Order + var ok bool + waitForTrade := false + base := s.Position.GetBase() + if base.Abs().Compare(s.Market.MinQuantity) >= 0 && base.Mul(s.GetLastPrice()).Abs().Compare(s.Market.MinNotional) >= 0 && base.Sign() < 0 { + if closeOrder, ok = s.ClosePosition(ctx); !ok { + log.Errorf("sell position %v remained not closed, skip placing order", base) + return closeOrder, nil + } + } + if s.Position.GetBase().Sign() < 0 { + // we are not able to make close trade at this moment, + // will close the rest of the position by normal limit order + // s.entryPrice is set in the last trade + waitForTrade = true } quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) if !ok { log.Infof("buy order at price %v failed", price) - return + return closeOrder, nil } quantityAmount := quoteBalance.Available totalQuantity := quantityAmount.Div(price) @@ -525,84 +546,126 @@ func (s *Strategy) PlaceBuyOrder(ctx context.Context, price fixedpoint.Value) { } if err := s.validateOrder(&order); err != nil { log.Infof("validation failed %v: %v", order, err) - return + return closeOrder, nil } - // strong long - log.Warnf("long at %v, timestamp: %s", price, s.KLineStartTime) + log.Warnf("long at %v, position %v, closeOrder %v, timestamp: %s", price, s.Position.GetBase(), closeOrder, s.KLineStartTime) createdOrders, err := s.Session.Exchange.SubmitOrders(ctx, order) if err != nil { log.WithError(err).Errorf("cannot place order") - return + return closeOrder, nil } - log.Infof("post order %v", createdOrders) + + log.Infof("post order c: %v, entryPrice: %v o: %v", waitForTrade, s.entryPrice, createdOrders) + s.waitForTrade = waitForTrade s.orderStore.Add(createdOrders...) s.activeMakerOrders.Add(createdOrders...) s.tradeCollector.Process() + return closeOrder, &createdOrders[0] } -func (s *Strategy) PlaceSellOrder(ctx context.Context, price fixedpoint.Value) { - if s.Position.GetBase().Compare(s.Market.MinQuantity) > 0 && !s.ClosePosition(ctx) { - log.Errorf("buy position %v remained not closed, skip placing order", s.Position.GetBase()) - return +func (s *Strategy) PlaceSellOrder(ctx context.Context, price fixedpoint.Value) (*types.Order, *types.Order) { + var closeOrder *types.Order + var ok bool + waitForTrade := false + base := s.Position.GetBase() + if base.Abs().Compare(s.Market.MinQuantity) >= 0 && base.Abs().Mul(s.GetLastPrice()).Compare(s.Market.MinNotional) >= 0 && base.Sign() > 0 { + if closeOrder, ok = s.ClosePosition(ctx); !ok { + log.Errorf("buy position %v remained not closed, skip placing order", base) + return closeOrder, nil + } + } + if s.Position.GetBase().Sign() > 0 { + // we are not able to make close trade at this moment, + // will close the rest of the position by normal limit order + // s.entryPrice is set in the last trade + waitForTrade = true + } + baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) + if !ok { + return closeOrder, nil } - balances := s.Session.GetAccount().Balances() - baseBalance := balances[s.Market.BaseCurrency].Available order := types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeLimit, Market: s.Market, - Quantity: baseBalance, + Quantity: baseBalance.Available, Price: price, TimeInForce: types.TimeInForceGTC, } if err := s.validateOrder(&order); err != nil { log.Infof("validation failed %v: %v", order, err) - return + return closeOrder, nil } - log.Warnf("short at %v, timestamp: %s", price, s.KLineStartTime) + log.Warnf("short at %v, position %v closeOrder %v, timestamp: %s", price, s.Position.GetBase(), closeOrder, s.KLineStartTime) createdOrders, err := s.Session.Exchange.SubmitOrders(ctx, order) if err != nil { log.WithError(err).Errorf("cannot place order") - return + return closeOrder, nil } - log.Infof("post order %v", createdOrders) + log.Infof("post order, c: %v, entryPrice: %v o: %v", waitForTrade, s.entryPrice, createdOrders) + s.waitForTrade = waitForTrade s.orderStore.Add(createdOrders...) s.activeMakerOrders.Add(createdOrders...) s.tradeCollector.Process() + return closeOrder, &createdOrders[0] } -func (s *Strategy) ClosePosition(ctx context.Context) bool { +// ClosePosition(context.Context) -> (closeOrder *types.Order, ok bool) +// this will decorate the generated order from NewClosePositionOrder +// add do necessary checks +// if available quantity is zero, will return (nil, true) +// if any of the checks failed, will return (nil, false) +// otherwise, return the created close order and true +func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) { order := s.Position.NewClosePositionOrder(fixedpoint.One) + // no position exists if order == nil { // no base s.sellPrice = fixedpoint.Zero s.buyPrice = fixedpoint.Zero - return true + return nil, true } order.TimeInForce = "" + // If there's any order not yet been traded in the orderbook, + // we need this additional check to make sure we have enough balance to post a close order + balances := s.Session.GetAccount().Balances() + baseBalance := balances[s.Market.BaseCurrency].Available + if order.Side == types.SideTypeBuy { + price := s.GetLastPrice() + quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price) + if order.Quantity.Compare(quoteAmount) > 0 { + order.Quantity = quoteAmount + } + } else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 { + order.Quantity = baseBalance + } + // if no available balance... + if order.Quantity.IsZero() { + return nil, true + } if err := s.validateOrder(order); err != nil { log.Errorf("cannot place close order %v: %v", order, err) - return false + return nil, false } createdOrders, err := s.Session.Exchange.SubmitOrders(ctx, *order) if err != nil { log.WithError(err).Errorf("cannot place close order") - return false + return nil, false } log.Infof("close order %v", createdOrders) s.orderStore.Add(createdOrders...) s.activeMakerOrders.Add(createdOrders...) s.tradeCollector.Process() - return true + return &createdOrders[0], true } -func (s *Strategy) CancelAll(ctx context.Context, side types.SideType) { +func (s *Strategy) CancelAll(ctx context.Context) { var toCancel []types.Order for _, order := range s.orderStore.Orders() { - if order.Status == types.OrderStatusNew || order.Status == types.OrderStatusPartiallyFilled && order.Side == side { + if order.Status == types.OrderStatusNew || order.Status == types.OrderStatusPartiallyFilled { toCancel = append(toCancel, order) } } @@ -610,11 +673,35 @@ func (s *Strategy) CancelAll(ctx context.Context, side types.SideType) { if err := s.Session.Exchange.CancelOrders(ctx, toCancel...); err != nil { log.WithError(err).Errorf("cancel order error") } - - s.tradeCollector.Process() + s.waitForTrade = false } } +func (s *Strategy) GetLastPrice() fixedpoint.Value { + var lastPrice fixedpoint.Value + var ok bool + if s.Environment.IsBackTesting() { + lastPrice, ok = s.Session.LastPrice(s.Symbol) + if !ok { + log.Errorf("cannot get last price") + return lastPrice + } + } else { + s.lock.RLock() + if s.midPrice.IsZero() { + lastPrice, ok = s.Session.LastPrice(s.Symbol) + if !ok { + log.Errorf("cannot get last price") + return lastPrice + } + } else { + lastPrice = s.midPrice + } + s.lock.RUnlock() + } + return lastPrice +} + // Trading Rules: // - buy / sell the whole asset // - SL by atr (lastprice < buyprice - atr * 2) || (lastprice > sellprice + atr * 2) @@ -684,7 +771,42 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } else { s.Environment.RecordPosition(s.Position, trade, nil) } - if s.Position.GetBase().Abs().Compare(s.Market.MinQuantity) > 0 { + // calculate report for the position that cannot be closed by close order (amount too small) + if s.waitForTrade { + price := s.entryPrice + if price.IsZero() { + panic("no price found") + } + pnlRate := trade.Price.Sub(price).Abs().Div(trade.Price).Float64() + if s.Record { + log.Errorf("record avg %v trade %v", price, trade) + } + if trade.Side == types.SideTypeBuy { + if trade.Price.Compare(price) < 0 { + percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate + counterTPfromOrder += 1 + percentAvgTPfromOrder /= float64(counterTPfromOrder) + } else { + percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate + counterSLfromOrder += 1 + percentAvgSLfromOrder /= float64(counterSLfromOrder) + } + } else if trade.Side == types.SideTypeSell { + if trade.Price.Compare(price) > 0 { + percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate + counterTPfromOrder += 1 + percentAvgTPfromOrder /= float64(counterTPfromOrder) + } else { + percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate + counterSLfromOrder += 1 + percentAvgSLfromOrder /= float64(counterSLfromOrder) + } + } else { + panic(fmt.Sprintf("no sell(%v) or buy price(%v), %v", s.sellPrice, s.buyPrice, trade)) + } + s.waitForTrade = false + } + if s.Position.GetBase().Abs().Compare(s.Market.MinQuantity) >= 0 && s.Position.GetBase().Abs().Mul(trade.Price).Compare(s.Market.MinNotional) >= 0 { sign := s.Position.GetBase().Sign() if sign > 0 { log.Infof("base become positive, %v", trade) @@ -692,19 +814,23 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.sellPrice = fixedpoint.Zero s.peakPrice = s.Position.AverageCost } else if sign == 0 { - log.Infof("base become zero") - s.buyPrice = fixedpoint.Zero - s.sellPrice = fixedpoint.Zero + panic("not going to happen") } else { log.Infof("base become negative, %v", trade) s.buyPrice = fixedpoint.Zero s.sellPrice = s.Position.AverageCost s.bottomPrice = s.Position.AverageCost } + s.entryPrice = trade.Price } else { - log.Infof("base become zero") + log.Infof("base become zero, rest of base: %v", s.Position.GetBase()) + if s.Position.GetBase().IsZero() { + s.entryPrice = fixedpoint.Zero + } s.buyPrice = fixedpoint.Zero s.sellPrice = fixedpoint.Zero + s.peakPrice = fixedpoint.Zero + s.bottomPrice = fixedpoint.Zero } }) @@ -717,7 +843,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.SetupIndicators() sellOrderTPSL := func(price fixedpoint.Value) { - if s.Position.GetBase().IsZero() { + lastPrice := s.GetLastPrice() + base := s.Position.GetBase().Abs() + if base.Mul(lastPrice).Compare(s.Market.MinNotional) < 0 || base.Compare(s.Market.MinQuantity) < 0 { return } if s.sellPrice.IsZero() { @@ -727,15 +855,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se quoteBalance := balances[s.Market.QuoteCurrency].Available atr := fixedpoint.NewFromFloat(s.atr.Last()) atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) - lastPrice := price - var ok bool - if s.Environment.IsBackTesting() { - lastPrice, ok = session.LastPrice(s.Symbol) - if !ok { - log.Errorf("cannot get last price") - return - } - } buyall := false if s.bottomPrice.IsZero() || s.bottomPrice.Compare(price) > 0 { s.bottomPrice = price @@ -743,44 +862,30 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se takeProfit := false bottomBack := s.bottomPrice spBack := s.sellPrice - if !quoteBalance.IsZero() { + reason := -1 + if quoteBalance.Div(lastPrice).Compare(s.Market.MinQuantity) >= 0 && quoteBalance.Compare(s.Market.MinNotional) >= 0 { longSignal := types.CrossOver(s.ewo, s.ewoSignal) base := fixedpoint.NewFromFloat(s.ma34.Last()) // TP if lastPrice.Compare(s.sellPrice) < 0 && (s.ccis.BuySignal() || longSignal.Last() || (!atrx2.IsZero() && base.Sub(atrx2).Compare(lastPrice) >= 0)) { buyall = true - s.bottomPrice = fixedpoint.Zero takeProfit = true // calculate report - pnlRate := s.sellPrice.Sub(lastPrice).Div(lastPrice).Float64() if s.ccis.BuySignal() { - percentAvgTPfromCCI = percentAvgTPfromCCI*float64(counterTPfromCCI) + pnlRate - counterTPfromCCI += 1 - percentAvgTPfromCCI /= float64(counterTPfromCCI) + reason = 0 } else if longSignal.Last() { - percentAvgTPfromLongShort = percentAvgTPfromLongShort*float64(counterTPfromLongShort) + pnlRate - counterTPfromLongShort += 1 - percentAvgTPfromLongShort /= float64(counterTPfromLongShort) + reason = 1 } else { - percentAvgTPfromAtr = percentAvgTPfromAtr*float64(counterTPfromAtr) + pnlRate - counterTPfromAtr += 1 - percentAvgTPfromAtr /= float64(counterTPfromAtr) - + reason = 2 } } if !atr.IsZero() && s.bottomPrice.Add(atr).Compare(lastPrice) <= 0 && lastPrice.Compare(s.sellPrice) < 0 { buyall = true - s.bottomPrice = fixedpoint.Zero takeProfit = true - - // calculate report - pnlRate := s.sellPrice.Sub(lastPrice).Div(lastPrice).Float64() - percentAvgTPfromPeak = percentAvgTPfromPeak*float64(counterTPfromPeak) + pnlRate - counterTPfromPeak += 1 - percentAvgTPfromPeak /= float64(counterTPfromPeak) + reason = 3 } // SL @@ -795,28 +900,64 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if !s.DisableShortStop && ((!atr.IsZero() && s.sellPrice.Sub(atr).Compare(lastPrice) >= 0) || lastPrice.Sub(s.sellPrice).Div(s.sellPrice).Compare(s.Stoploss) > 0) { buyall = true - s.bottomPrice = fixedpoint.Zero - - // calculate report - pnlRate := lastPrice.Sub(s.sellPrice).Div(lastPrice).Float64() - percentAvgSLfromSL = percentAvgSLfromSL*float64(counterSLfromSL) + pnlRate - counterSLfromSL += 1 - percentAvgSLfromSL /= float64(counterSLfromSL) + reason = 4 } } if buyall { log.Warnf("buyall TPSL %v %v", s.Position.GetBase(), quoteBalance) - if s.ClosePosition(ctx) { + p := s.sellPrice + if order, ok := s.ClosePosition(ctx); order != nil && ok { if takeProfit { log.Errorf("takeprofit buy at %v, avg %v, l: %v, atrx2: %v", lastPrice, spBack, bottomBack, atrx2) } else { log.Errorf("stoploss buy at %v, avg %v, l: %v, atrx2: %v", lastPrice, spBack, bottomBack, atrx2) } + + // calculate report + if s.Record { + log.Error("record ba") + } + var pnlRate float64 + if takeProfit { + pnlRate = p.Sub(lastPrice).Div(lastPrice).Float64() + } else { + pnlRate = lastPrice.Sub(p).Div(lastPrice).Float64() + } + switch reason { + case 0: + percentAvgTPfromCCI = percentAvgTPfromCCI*float64(counterTPfromCCI) + pnlRate + counterTPfromCCI += 1 + percentAvgTPfromCCI /= float64(counterTPfromCCI) + break + case 1: + percentAvgTPfromLongShort = percentAvgTPfromLongShort*float64(counterTPfromLongShort) + pnlRate + counterTPfromLongShort += 1 + percentAvgTPfromLongShort /= float64(counterTPfromLongShort) + break + case 2: + percentAvgTPfromAtr = percentAvgTPfromAtr*float64(counterTPfromAtr) + pnlRate + counterTPfromAtr += 1 + percentAvgTPfromAtr /= float64(counterTPfromAtr) + break + case 3: + percentAvgTPfromPeak = percentAvgTPfromPeak*float64(counterTPfromPeak) + pnlRate + counterTPfromPeak += 1 + percentAvgTPfromPeak /= float64(counterTPfromPeak) + break + case 4: + percentAvgSLfromSL = percentAvgSLfromSL*float64(counterSLfromSL) + pnlRate + counterSLfromSL += 1 + percentAvgSLfromSL /= float64(counterSLfromSL) + break + + } } } } buyOrderTPSL := func(price fixedpoint.Value) { - if s.Position.GetBase().IsZero() { + lastPrice := s.GetLastPrice() + base := s.Position.GetBase().Abs() + if base.Mul(lastPrice).Compare(s.Market.MinNotional) < 0 || base.Compare(s.Market.MinQuantity) < 0 { return } if s.buyPrice.IsZero() { @@ -826,15 +967,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se baseBalance := balances[s.Market.BaseCurrency].Available atr := fixedpoint.NewFromFloat(s.atr.Last()) atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) - lastPrice := price - var ok bool - if s.Environment.IsBackTesting() { - lastPrice, ok = session.LastPrice(s.Symbol) - if !ok { - log.Errorf("cannot get last price") - return - } - } sellall := false if s.peakPrice.IsZero() || s.peakPrice.Compare(price) < 0 { s.peakPrice = price @@ -842,43 +974,30 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se takeProfit := false peakBack := s.peakPrice bpBack := s.buyPrice - if !baseBalance.IsZero() { + reason := -1 + if baseBalance.Compare(s.Market.MinQuantity) >= 0 && baseBalance.Mul(lastPrice).Compare(s.Market.MinNotional) >= 0 { shortSignal := types.CrossUnder(s.ewo, s.ewoSignal) // TP - if !atr.IsZero() && s.peakPrice.Sub(atr).Compare(lastPrice) >= 0 && - lastPrice.Compare(s.buyPrice) > 0 { - sellall = true - s.peakPrice = fixedpoint.Zero - takeProfit = true - - // calculate report - pnlRate := lastPrice.Sub(s.buyPrice).Div(s.buyPrice).Float64() - percentAvgTPfromPeak = percentAvgTPfromPeak*float64(counterTPfromPeak) + pnlRate - counterTPfromPeak += 1 - percentAvgTPfromPeak /= float64(counterTPfromPeak) - } base := fixedpoint.NewFromFloat(s.ma34.Last()) if lastPrice.Compare(s.buyPrice) > 0 && (s.ccis.SellSignal() || shortSignal.Last() || (!atrx2.IsZero() && base.Add(atrx2).Compare(lastPrice) <= 0)) { sellall = true - s.peakPrice = fixedpoint.Zero takeProfit = true // calculate report - pnlRate := lastPrice.Sub(s.buyPrice).Div(s.buyPrice).Float64() if s.ccis.SellSignal() { - percentAvgTPfromCCI = percentAvgTPfromCCI*float64(counterTPfromCCI) + pnlRate - counterTPfromCCI += 1 - percentAvgTPfromCCI /= float64(counterTPfromCCI) + reason = 0 } else if shortSignal.Last() { - percentAvgTPfromLongShort = percentAvgTPfromLongShort*float64(counterTPfromLongShort) + pnlRate - counterTPfromLongShort += 1 - percentAvgTPfromLongShort /= float64(counterTPfromLongShort) + reason = 1 } else { - percentAvgTPfromAtr = percentAvgTPfromAtr*float64(counterTPfromAtr) + pnlRate - counterTPfromAtr += 1 - percentAvgTPfromAtr /= float64(counterTPfromAtr) + reason = 2 } } + if !atr.IsZero() && s.peakPrice.Sub(atr).Compare(lastPrice) >= 0 && + lastPrice.Compare(s.buyPrice) > 0 { + sellall = true + takeProfit = true + reason = 3 + } // SL /*if s.peakPrice.Sub(lastPrice).Div(s.peakPrice).Compare(s.Stoploss) > 0 || @@ -892,24 +1011,56 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if !s.DisableLongStop && (s.buyPrice.Sub(lastPrice).Div(s.buyPrice).Compare(s.Stoploss) > 0 || (!atr.IsZero() && s.buyPrice.Sub(atr).Compare(lastPrice) >= 0)) { sellall = true - s.peakPrice = fixedpoint.Zero - - // calculate report - pnlRate := s.buyPrice.Sub(lastPrice).Div(s.buyPrice).Float64() - percentAvgSLfromSL = percentAvgSLfromSL*float64(counterSLfromSL) + pnlRate - counterSLfromSL += 1 - percentAvgSLfromSL /= float64(counterSLfromSL) + reason = 4 } } if sellall { log.Warnf("sellall TPSL %v", s.Position.GetBase()) - if s.ClosePosition(ctx) { + p := s.buyPrice + if order, ok := s.ClosePosition(ctx); order != nil && ok { if takeProfit { log.Errorf("takeprofit sell at %v, avg %v, h: %v, atrx2: %v", lastPrice, bpBack, peakBack, atrx2) } else { log.Errorf("stoploss sell at %v, avg %v, h: %v, atrx2: %v", lastPrice, bpBack, peakBack, atrx2) } + // calculate report + if s.Record { + log.Error("record sa") + } + var pnlRate float64 + if takeProfit { + pnlRate = lastPrice.Sub(p).Div(p).Float64() + } else { + pnlRate = p.Sub(lastPrice).Div(p).Float64() + } + switch reason { + case 0: + percentAvgTPfromCCI = percentAvgTPfromCCI*float64(counterTPfromCCI) + pnlRate + counterTPfromCCI += 1 + percentAvgTPfromCCI /= float64(counterTPfromCCI) + break + case 1: + percentAvgTPfromLongShort = percentAvgTPfromLongShort*float64(counterTPfromLongShort) + pnlRate + counterTPfromLongShort += 1 + percentAvgTPfromLongShort /= float64(counterTPfromLongShort) + break + case 2: + percentAvgTPfromAtr = percentAvgTPfromAtr*float64(counterTPfromAtr) + pnlRate + counterTPfromAtr += 1 + percentAvgTPfromAtr /= float64(counterTPfromAtr) + break + case 3: + percentAvgTPfromPeak = percentAvgTPfromPeak*float64(counterTPfromPeak) + pnlRate + counterTPfromPeak += 1 + percentAvgTPfromPeak /= float64(counterTPfromPeak) + break + case 4: + percentAvgSLfromSL = percentAvgSLfromSL*float64(counterSLfromSL) + pnlRate + counterSLfromSL += 1 + percentAvgSLfromSL /= float64(counterSLfromSL) + break + } } } } @@ -1028,65 +1179,74 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se price := kline.Close.Sub(atr.Div(types.Two)) // if total asset (including locked) could be used to buy if quoteBalance.Div(price).Compare(s.Market.MinQuantity) >= 0 && quoteBalance.Compare(s.Market.MinNotional) >= 0 { - // cancel all buy orders to release lock - s.CancelAll(ctx, types.SideTypeBuy) + // cancel all orders to release lock + s.CancelAll(ctx) + + // backup, since the s.sellPrice will be cleared when doing ClosePosition + sellPrice := s.sellPrice // calculate report - if !s.sellPrice.IsZero() { - if price.Compare(s.sellPrice) > 0 { - pnlRate := price.Sub(s.sellPrice).Div(price).Float64() - percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate - counterTPfromOrder += 1 - percentAvgTPfromOrder /= float64(counterTPfromOrder) + if closeOrder, _ := s.PlaceBuyOrder(ctx, price); closeOrder != nil { + if s.Record { + log.Error("record l") + } + if !sellPrice.IsZero() { + if lastPrice.Compare(sellPrice) > 0 { + pnlRate := lastPrice.Sub(sellPrice).Div(lastPrice).Float64() + percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate + counterTPfromOrder += 1 + percentAvgTPfromOrder /= float64(counterTPfromOrder) + } else { + pnlRate := sellPrice.Sub(lastPrice).Div(lastPrice).Float64() + percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate + counterSLfromOrder += 1 + percentAvgSLfromOrder /= float64(counterSLfromOrder) + } } else { - pnlRate := s.sellPrice.Sub(price).Div(price).Float64() - percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate - counterSLfromOrder += 1 - percentAvgSLfromOrder /= float64(counterSLfromOrder) + panic("no sell price") } } - s.PlaceBuyOrder(ctx, price) } } if shortSignal.Index(1) && !longSignal.Last() && IsBear { price := kline.Close.Add(atr.Div(types.Two)) // if total asset (including locked) could be used to sell if baseBalance.Mul(price).Compare(s.Market.MinNotional) >= 0 && baseBalance.Compare(s.Market.MinQuantity) >= 0 { - // cancel all sell orders to release lock - s.CancelAll(ctx, types.SideTypeSell) + // cancel all orders to release lock + s.CancelAll(ctx) + + // backup, since the s.buyPrice will be cleared when doing ClosePosition + buyPrice := s.buyPrice // calculate report - if !s.buyPrice.IsZero() { - if price.Compare(s.buyPrice) > 0 { - pnlRate := price.Sub(s.buyPrice).Div(s.buyPrice).Float64() - percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate - counterTPfromOrder += 1 - percentAvgTPfromOrder /= float64(counterTPfromOrder) + if closeOrder, _ := s.PlaceSellOrder(ctx, price); closeOrder != nil { + if s.Record { + log.Error("record s") + } + if !buyPrice.IsZero() { + if lastPrice.Compare(buyPrice) > 0 { + pnlRate := lastPrice.Sub(buyPrice).Div(buyPrice).Float64() + percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate + counterTPfromOrder += 1 + percentAvgTPfromOrder /= float64(counterTPfromOrder) + } else { + pnlRate := buyPrice.Sub(lastPrice).Div(buyPrice).Float64() + percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate + counterSLfromOrder += 1 + percentAvgSLfromOrder /= float64(counterSLfromOrder) + } } else { - pnlRate := s.buyPrice.Sub(price).Div(s.buyPrice).Float64() - percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate - counterSLfromOrder += 1 - percentAvgSLfromOrder /= float64(counterSLfromOrder) + panic("no buy price") } } - s.PlaceSellOrder(ctx, price) } } }) s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling active orders...") + s.CancelAll(ctx) - var toCancel []types.Order - for _, order := range s.orderStore.Orders() { - if order.Status == types.OrderStatusNew || order.Status == types.OrderStatusPartiallyFilled { - toCancel = append(toCancel, order) - } - } - - if err := orderExecutor.CancelOrders(ctx, toCancel...); err != nil { - log.WithError(err).Errorf("cancel order error") - } s.tradeCollector.Process() color.HiBlue("---- Trade Report (Without Fee) ----") color.HiBlue("TP:") diff --git a/pkg/strategy/ewoDgtrd/trylock.go b/pkg/strategy/ewoDgtrd/trylock.go index ec3e875a9..f3e6e551a 100644 --- a/pkg/strategy/ewoDgtrd/trylock.go +++ b/pkg/strategy/ewoDgtrd/trylock.go @@ -9,3 +9,8 @@ func tryLock(lock *sync.RWMutex) bool { lock.Lock() return true } + +func tryRLock(lock *sync.RWMutex) bool { + lock.RLock() + return true +} diff --git a/pkg/strategy/ewoDgtrd/trylock_18.go b/pkg/strategy/ewoDgtrd/trylock_18.go index f092ebcdb..1511766ae 100644 --- a/pkg/strategy/ewoDgtrd/trylock_18.go +++ b/pkg/strategy/ewoDgtrd/trylock_18.go @@ -8,3 +8,7 @@ import "sync" func tryLock(lock *sync.RWMutex) bool { return lock.TryLock() } + +func tryRLock(lock *sync.RWMutex) bool { + return lock.TryRLock() +} diff --git a/pkg/types/position.go b/pkg/types/position.go index 644d96ebf..c02c358ac 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -122,7 +122,7 @@ func (p *Position) NewProfit(trade Trade, profit, netProfit fixedpoint.Value) Pr func (p *Position) NewClosePositionOrder(percentage fixedpoint.Value) *SubmitOrder { base := p.GetBase() - quantity := base.Mul(percentage) + quantity := base.Mul(percentage).Abs() if quantity.Compare(p.Market.MinQuantity) < 0 { return nil }