pivotshort: add roiMinTakeProfitPercentage option and cumulatedVolume option

This commit is contained in:
c9s 2022-06-10 02:39:14 +08:00
parent 53913ede23
commit 35a58268cf
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
3 changed files with 102 additions and 47 deletions

View File

@ -34,17 +34,22 @@ exchangeStrategies:
# roiStopLossPercentage is the stop loss percentage of the position ROI (currently the price change)
roiStopLossPercentage: 1%
# roiTakeProfitPercentage is the take profit percentage of the position ROI (currently the price change)
# roiTakeProfitPercentage is used to force taking profit percentage of the position ROI (currently the price change)
# force to take the profit ROI exceeded the percentage.
roiTakeProfitPercentage: 25%
# lowerShadowRatio is used to force taking profit when the (lower shadow height / low price) > lowerShadowRatio
# roiMinTakeProfitPercentage applies to lowerShadowRatio and cumulatedVolume exit options
roiMinTakeProfitPercentage: 10%
# lowerShadowRatio is used to taking profit when the (lower shadow height / low price) > lowerShadowRatio
# you can grab a simple stats by the following SQL:
# SELECT ((close - low) / close) AS shadow_ratio FROM binance_klines WHERE symbol = 'ETHUSDT' AND `interval` = '5m' AND start_time > '2022-01-01' ORDER BY shadow_ratio DESC LIMIT 20;
lowerShadowRatio: 3%
# cumulatedVolume is used to take profit when the cumulated quote volume from the klines exceeded a threshold
cumulatedVolume:
minVolume: 50_000
enabled: true
minQuoteVolume: 90_000_000
window: 5
marginOrderSideEffect: repay
@ -52,7 +57,7 @@ exchangeStrategies:
backtest:
sessions:
- binance
startTime: "2022-04-01"
startTime: "2022-01-01"
endTime: "2022-06-08"
symbols:
- ETHUSDT

View File

@ -1,26 +1,41 @@
# usage:
#
# go run ./cmd/bbgo optimize --config bollmaker_ethusdt.yaml --optimizer-config optimizer.yaml --debug
# go run ./cmd/bbgo optimize --config config/pivotshort-ETHUSDT.yaml --optimizer-config config/pivotshort_optimizer.yaml --debug
#
---
matrix:
- type: iterate
label: interval
path: '/exchangeStrategies/0/pivotshort/interval'
values: [ "5m", "30m", "1h" ]
- type: range
path: '/exchangeStrategies/0/pivotshort/pivotLength'
label: pivotLength
min: 20.0
max: 200.0
step: 10.0
#- type: iterate
# label: interval
# path: '/exchangeStrategies/0/pivotshort/interval'
# values: [ "5m", "30m" ]
#- type: range
# path: '/exchangeStrategies/0/pivotshort/pivotLength'
# label: pivotLength
# min: 100.0
# max: 200.0
# step: 20.0
- type: range
path: '/exchangeStrategies/0/pivotshort/breakLow/stopEMARange'
label: pivotLength
label: stopEMARange
min: 0%
max: 10%
step: 1%
- type: range
path: '/exchangeStrategies/0/pivotshort/exit/roiStopLossPercentage'
label: roiStopLossPercentage
min: 0.5%
max: 3%
step: 0.5%
- type: range
path: '/exchangeStrategies/0/pivotshort/exit/roiTakeProfitPercentage'
label: roiTakeProfitPercentage
min: 10%
max: 50%
step: 5%

View File

@ -3,6 +3,7 @@ package pivotshort
import (
"context"
"fmt"
"os"
"sync"
"github.com/sirupsen/logrus"
@ -39,7 +40,11 @@ func (s *TradeStats) Add(pnl fixedpoint.Value) {
s.MostLossTrade = fixedpoint.Min(s.MostLossTrade, pnl)
}
s.WinningRatio = fixedpoint.NewFromFloat(float64(s.NumOfProfitTrade) / float64(s.NumOfLossTrade))
if s.NumOfLossTrade == 0 && s.NumOfProfitTrade > 0 {
s.WinningRatio = fixedpoint.One
} else {
s.WinningRatio = fixedpoint.NewFromFloat(float64(s.NumOfProfitTrade) / float64(s.NumOfLossTrade))
}
}
func (s *TradeStats) String() string {
@ -76,12 +81,21 @@ type Entry struct {
MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"`
}
type CumulatedVolume struct {
Enabled bool `json:"enabled"`
MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"`
Window int `json:"window"`
}
type Exit struct {
RoiStopLossPercentage fixedpoint.Value `json:"roiStopLossPercentage"`
RoiTakeProfitPercentage fixedpoint.Value `json:"roiTakeProfitPercentage"`
RoiStopLossPercentage fixedpoint.Value `json:"roiStopLossPercentage"`
RoiTakeProfitPercentage fixedpoint.Value `json:"roiTakeProfitPercentage"`
RoiMinTakeProfitPercentage fixedpoint.Value `json:"roiMinTakeProfitPercentage"`
LowerShadowRatio fixedpoint.Value `json:"lowerShadowRatio"`
CumulatedVolume *CumulatedVolume `json:"cumulatedVolume"`
MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"`
}
@ -287,36 +301,47 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
// TODO: apply quantity to this formula
roi := s.Position.AverageCost.Sub(kline.Close).Div(s.Position.AverageCost)
if roi.Compare(s.Exit.RoiStopLossPercentage.Neg()) < 0 {
// SL
s.Notify("%s ROI StopLoss triggered at price %f, ROI = %s", s.Symbol, kline.Close.Float64(), roi.Percentage())
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
if err := s.ClosePosition(ctx, fixedpoint.One); err != nil {
log.WithError(err).Errorf("close position error")
}
// stop loss
s.Notify("%s ROI StopLoss triggered at price %f: Loss %s", s.Symbol, kline.Close.Float64(), roi.Percentage())
s.closePosition(ctx)
return
} else if roi.Compare(s.Exit.RoiTakeProfitPercentage) > 0 { // disable this condition temporarily
s.Notify("%s TakeProfit triggered at price %f, ROI take profit percentage by %s", s.Symbol, kline.Close.Float64(), roi.Percentage(), kline)
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
} else {
// take profit
if roi.Compare(s.Exit.RoiTakeProfitPercentage) > 0 { // force take profit
s.Notify("%s TakeProfit triggered at price %f: by ROI percentage %s", s.Symbol, kline.Close.Float64(), roi.Percentage(), kline)
s.closePosition(ctx)
return
} else if !s.Exit.RoiMinTakeProfitPercentage.IsZero() && roi.Compare(s.Exit.RoiMinTakeProfitPercentage) > 0 {
if !s.Exit.LowerShadowRatio.IsZero() && kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Exit.LowerShadowRatio) > 0 {
s.Notify("%s TakeProfit triggered at price %f: by shadow ratio %f",
s.Symbol,
kline.Close.Float64(),
kline.GetLowerShadowRatio().Float64(), kline)
s.closePosition(ctx)
return
} else if s.Exit.CumulatedVolume != nil && s.Exit.CumulatedVolume.Enabled {
if klines, ok := store.KLinesOfInterval(s.Interval); ok {
var cbv = fixedpoint.Zero
var cqv = fixedpoint.Zero
for i := 0; i < s.Exit.CumulatedVolume.Window; i++ {
last := (*klines)[len(*klines)-1-i]
cqv = cqv.Add(last.QuoteVolume)
cbv = cbv.Add(last.Volume)
}
if err := s.ClosePosition(ctx, fixedpoint.One); err != nil {
log.WithError(err).Errorf("close position error")
if cqv.Compare(s.Exit.CumulatedVolume.MinQuoteVolume) > 0 {
s.Notify("%s TakeProfit triggered at price %f: by cumulated volume (window: %d) %f > %f",
s.Symbol,
kline.Close.Float64(),
s.Exit.CumulatedVolume.Window,
cqv.Float64(),
s.Exit.CumulatedVolume.MinQuoteVolume.Float64())
s.closePosition(ctx)
return
}
}
}
}
} else if !s.Exit.LowerShadowRatio.IsZero() && kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Exit.LowerShadowRatio) > 0 {
s.Notify("%s TakeProfit triggered at price %f: shadow ratio %f", s.Symbol, kline.Close.Float64(), kline.GetLowerShadowRatio().Float64(), kline)
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
if err := s.ClosePosition(ctx, fixedpoint.One); err != nil {
log.WithError(err).Errorf("close position error")
}
return
}
}
@ -379,13 +404,23 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
})
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
log.Info(s.TradeStats.String())
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
wg.Done()
})
return nil
}
func (s *Strategy) closePosition(ctx context.Context) {
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error")
}
if err := s.ClosePosition(ctx, fixedpoint.One); err != nil {
log.WithError(err).Errorf("close position error")
}
}
func (s *Strategy) findHigherPivotLow(price fixedpoint.Value) (fixedpoint.Value, bool) {
for l := len(s.pivotLowPrices) - 1; l > 0; l-- {
if s.pivotLowPrices[l].Compare(price) > 0 {