mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-14 11:03:53 +00:00
pivotshort: add roiMinTakeProfitPercentage option and cumulatedVolume option
This commit is contained in:
parent
53913ede23
commit
35a58268cf
|
@ -34,17 +34,22 @@ exchangeStrategies:
|
||||||
# roiStopLossPercentage is the stop loss percentage of the position ROI (currently the price change)
|
# roiStopLossPercentage is the stop loss percentage of the position ROI (currently the price change)
|
||||||
roiStopLossPercentage: 1%
|
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.
|
# force to take the profit ROI exceeded the percentage.
|
||||||
roiTakeProfitPercentage: 25%
|
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:
|
# 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;
|
# 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%
|
lowerShadowRatio: 3%
|
||||||
|
|
||||||
|
# cumulatedVolume is used to take profit when the cumulated quote volume from the klines exceeded a threshold
|
||||||
cumulatedVolume:
|
cumulatedVolume:
|
||||||
minVolume: 50_000
|
enabled: true
|
||||||
|
minQuoteVolume: 90_000_000
|
||||||
window: 5
|
window: 5
|
||||||
|
|
||||||
marginOrderSideEffect: repay
|
marginOrderSideEffect: repay
|
||||||
|
@ -52,7 +57,7 @@ exchangeStrategies:
|
||||||
backtest:
|
backtest:
|
||||||
sessions:
|
sessions:
|
||||||
- binance
|
- binance
|
||||||
startTime: "2022-04-01"
|
startTime: "2022-01-01"
|
||||||
endTime: "2022-06-08"
|
endTime: "2022-06-08"
|
||||||
symbols:
|
symbols:
|
||||||
- ETHUSDT
|
- ETHUSDT
|
||||||
|
|
|
@ -1,26 +1,41 @@
|
||||||
# usage:
|
# 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:
|
matrix:
|
||||||
- type: iterate
|
|
||||||
label: interval
|
|
||||||
path: '/exchangeStrategies/0/pivotshort/interval'
|
|
||||||
values: [ "5m", "30m", "1h" ]
|
|
||||||
|
|
||||||
- type: range
|
#- type: iterate
|
||||||
path: '/exchangeStrategies/0/pivotshort/pivotLength'
|
# label: interval
|
||||||
label: pivotLength
|
# path: '/exchangeStrategies/0/pivotshort/interval'
|
||||||
min: 20.0
|
# values: [ "5m", "30m" ]
|
||||||
max: 200.0
|
|
||||||
step: 10.0
|
#- type: range
|
||||||
|
# path: '/exchangeStrategies/0/pivotshort/pivotLength'
|
||||||
|
# label: pivotLength
|
||||||
|
# min: 100.0
|
||||||
|
# max: 200.0
|
||||||
|
# step: 20.0
|
||||||
|
|
||||||
- type: range
|
- type: range
|
||||||
path: '/exchangeStrategies/0/pivotshort/breakLow/stopEMARange'
|
path: '/exchangeStrategies/0/pivotshort/breakLow/stopEMARange'
|
||||||
label: pivotLength
|
label: stopEMARange
|
||||||
min: 0%
|
min: 0%
|
||||||
max: 10%
|
max: 10%
|
||||||
|
step: 1%
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/exit/roiStopLossPercentage'
|
||||||
|
label: roiStopLossPercentage
|
||||||
|
min: 0.5%
|
||||||
|
max: 3%
|
||||||
step: 0.5%
|
step: 0.5%
|
||||||
|
|
||||||
|
- type: range
|
||||||
|
path: '/exchangeStrategies/0/pivotshort/exit/roiTakeProfitPercentage'
|
||||||
|
label: roiTakeProfitPercentage
|
||||||
|
min: 10%
|
||||||
|
max: 50%
|
||||||
|
step: 5%
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package pivotshort
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
@ -39,7 +40,11 @@ func (s *TradeStats) Add(pnl fixedpoint.Value) {
|
||||||
s.MostLossTrade = fixedpoint.Min(s.MostLossTrade, pnl)
|
s.MostLossTrade = fixedpoint.Min(s.MostLossTrade, pnl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.NumOfLossTrade == 0 && s.NumOfProfitTrade > 0 {
|
||||||
|
s.WinningRatio = fixedpoint.One
|
||||||
|
} else {
|
||||||
s.WinningRatio = fixedpoint.NewFromFloat(float64(s.NumOfProfitTrade) / float64(s.NumOfLossTrade))
|
s.WinningRatio = fixedpoint.NewFromFloat(float64(s.NumOfProfitTrade) / float64(s.NumOfLossTrade))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TradeStats) String() string {
|
func (s *TradeStats) String() string {
|
||||||
|
@ -76,12 +81,21 @@ type Entry struct {
|
||||||
MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"`
|
MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CumulatedVolume struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"`
|
||||||
|
Window int `json:"window"`
|
||||||
|
}
|
||||||
|
|
||||||
type Exit struct {
|
type Exit struct {
|
||||||
RoiStopLossPercentage fixedpoint.Value `json:"roiStopLossPercentage"`
|
RoiStopLossPercentage fixedpoint.Value `json:"roiStopLossPercentage"`
|
||||||
RoiTakeProfitPercentage fixedpoint.Value `json:"roiTakeProfitPercentage"`
|
RoiTakeProfitPercentage fixedpoint.Value `json:"roiTakeProfitPercentage"`
|
||||||
|
RoiMinTakeProfitPercentage fixedpoint.Value `json:"roiMinTakeProfitPercentage"`
|
||||||
|
|
||||||
LowerShadowRatio fixedpoint.Value `json:"lowerShadowRatio"`
|
LowerShadowRatio fixedpoint.Value `json:"lowerShadowRatio"`
|
||||||
|
|
||||||
|
CumulatedVolume *CumulatedVolume `json:"cumulatedVolume"`
|
||||||
|
|
||||||
MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"`
|
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
|
// TODO: apply quantity to this formula
|
||||||
roi := s.Position.AverageCost.Sub(kline.Close).Div(s.Position.AverageCost)
|
roi := s.Position.AverageCost.Sub(kline.Close).Div(s.Position.AverageCost)
|
||||||
if roi.Compare(s.Exit.RoiStopLossPercentage.Neg()) < 0 {
|
if roi.Compare(s.Exit.RoiStopLossPercentage.Neg()) < 0 {
|
||||||
// SL
|
// stop loss
|
||||||
s.Notify("%s ROI StopLoss triggered at price %f, ROI = %s", s.Symbol, kline.Close.Float64(), roi.Percentage())
|
s.Notify("%s ROI StopLoss triggered at price %f: Loss %s", s.Symbol, kline.Close.Float64(), roi.Percentage())
|
||||||
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
|
s.closePosition(ctx)
|
||||||
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
|
return
|
||||||
} else if roi.Compare(s.Exit.RoiTakeProfitPercentage) > 0 { // disable this condition temporarily
|
} else {
|
||||||
s.Notify("%s TakeProfit triggered at price %f, ROI take profit percentage by %s", s.Symbol, kline.Close.Float64(), roi.Percentage(), kline)
|
// take profit
|
||||||
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
|
if roi.Compare(s.Exit.RoiTakeProfitPercentage) > 0 { // force take profit
|
||||||
log.WithError(err).Errorf("graceful cancel order error")
|
s.Notify("%s TakeProfit triggered at price %f: by ROI percentage %s", s.Symbol, kline.Close.Float64(), roi.Percentage(), kline)
|
||||||
}
|
s.closePosition(ctx)
|
||||||
|
|
||||||
if err := s.ClosePosition(ctx, fixedpoint.One); err != nil {
|
|
||||||
log.WithError(err).Errorf("close position error")
|
|
||||||
}
|
|
||||||
} 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
|
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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
log.Info(s.TradeStats.String())
|
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
|
||||||
wg.Done()
|
wg.Done()
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
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) {
|
func (s *Strategy) findHigherPivotLow(price fixedpoint.Value) (fixedpoint.Value, bool) {
|
||||||
for l := len(s.pivotLowPrices) - 1; l > 0; l-- {
|
for l := len(s.pivotLowPrices) - 1; l > 0; l-- {
|
||||||
if s.pivotLowPrices[l].Compare(price) > 0 {
|
if s.pivotLowPrices[l].Compare(price) > 0 {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user