diff --git a/pkg/strategy/pivotshort/cumulated_volume_take_profit.go b/pkg/strategy/pivotshort/cumulated_volume_take_profit.go new file mode 100644 index 000000000..b27c19d37 --- /dev/null +++ b/pkg/strategy/pivotshort/cumulated_volume_take_profit.go @@ -0,0 +1,64 @@ +package pivotshort + +import ( + "context" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type CumulatedVolumeTakeProfit struct { + types.IntervalWindow + Ratio fixedpoint.Value `json:"ratio"` + MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor +} + +func (s *CumulatedVolumeTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + position := orderExecutor.Position() + + store, _ := session.MarketDataStore(position.Symbol) + + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m { + return + } + + closePrice := kline.Close + if position.IsClosed() || position.IsDust(closePrice) { + return + } + + roi := position.ROI(closePrice) + if roi.Sign() < 0 { + return + } + + if klines, ok := store.KLinesOfInterval(s.Interval); ok { + var cbv = fixedpoint.Zero + var cqv = fixedpoint.Zero + for i := 0; i < s.Window; i++ { + last := (*klines)[len(*klines)-1-i] + cqv = cqv.Add(last.QuoteVolume) + cbv = cbv.Add(last.Volume) + } + + if cqv.Compare(s.MinQuoteVolume) > 0 { + bbgo.Notify("%s TakeProfit triggered by cumulated volume (window: %d) %f > %f, price = %f", + position.Symbol, + s.Window, + cqv.Float64(), + s.MinQuoteVolume.Float64(), kline.Close.Float64()) + + _ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One) + return + } + } + }) +} diff --git a/pkg/strategy/pivotshort/exit.go b/pkg/strategy/pivotshort/exit.go new file mode 100644 index 000000000..3451ac70b --- /dev/null +++ b/pkg/strategy/pivotshort/exit.go @@ -0,0 +1,28 @@ +package pivotshort + +import "github.com/c9s/bbgo/pkg/bbgo" + +type ExitMethod struct { + RoiStopLoss *RoiStopLoss `json:"roiStopLoss"` + ProtectionStopLoss *ProtectionStopLoss `json:"protectionStopLoss"` + + RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"` + LowerShadowTakeProfit *LowerShadowTakeProfit `json:"lowerShadowTakeProfit"` + + CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"` + // MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"` +} + +func (m *ExitMethod) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + if m.ProtectionStopLoss != nil { + m.ProtectionStopLoss.Bind(session, orderExecutor) + } else if m.RoiStopLoss != nil { + m.RoiStopLoss.Bind(session, orderExecutor) + } else if m.RoiTakeProfit != nil { + m.RoiTakeProfit.Bind(session, orderExecutor) + } else if m.LowerShadowTakeProfit != nil { + m.LowerShadowTakeProfit.Bind(session, orderExecutor) + } else if m.CumulatedVolumeTakeProfit != nil { + m.CumulatedVolumeTakeProfit.Bind(session, orderExecutor) + } +} diff --git a/pkg/strategy/pivotshort/lower_shadow_take_profit.go b/pkg/strategy/pivotshort/lower_shadow_take_profit.go new file mode 100644 index 000000000..014b9f135 --- /dev/null +++ b/pkg/strategy/pivotshort/lower_shadow_take_profit.go @@ -0,0 +1,53 @@ +package pivotshort + +import ( + "context" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type LowerShadowTakeProfit struct { + Ratio fixedpoint.Value `json:"ratio"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor +} + +func (s *LowerShadowTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + position := orderExecutor.Position() + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m { + return + } + + closePrice := kline.Close + if position.IsClosed() || position.IsDust(closePrice) { + return + } + + roi := position.ROI(closePrice) + if roi.Sign() < 0 { + return + } + + if s.Ratio.IsZero() { + return + } + + if kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Ratio) > 0 { + bbgo.Notify("%s TakeProfit triggered by shadow ratio %f, price = %f", + position.Symbol, + kline.GetLowerShadowRatio().Float64(), + kline.Close.Float64(), + kline) + + _ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One) + return + } + }) +} diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 76df9de2c..de77a983d 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -74,20 +74,6 @@ type CumulatedVolume struct { Window int `json:"window"` } -type Exit struct { - RoiMinTakeProfitPercentage fixedpoint.Value `json:"roiMinTakeProfitPercentage"` - - RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"` - RoiStopLoss *RoiStopLoss `json:"roiStopLoss"` - ProtectionStopLoss *ProtectionStopLoss `json:"protectionStopLoss"` - - LowerShadowRatio fixedpoint.Value `json:"lowerShadowRatio"` - - CumulatedVolume *CumulatedVolume `json:"cumulatedVolume"` - - MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"` -} - type Strategy struct { *bbgo.Graceful @@ -107,8 +93,8 @@ type Strategy struct { BounceShort *BounceShort `json:"bounceShort"` - Entry Entry `json:"entry"` - Exit Exit `json:"exit"` + Entry Entry `json:"entry"` + ExitMethods []ExitMethod `json:"exits"` session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor @@ -278,16 +264,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) - if s.Exit.ProtectionStopLoss != nil { - s.Exit.ProtectionStopLoss.Bind(session, s.orderExecutor) - } - - if s.Exit.RoiStopLoss != nil { - s.Exit.RoiStopLoss.Bind(session, s.orderExecutor) - } - - if s.Exit.RoiTakeProfit != nil { - s.Exit.RoiTakeProfit.Bind(session, s.orderExecutor) + for _, method := range s.ExitMethods { + method.Bind(session, s.orderExecutor) } // Always check whether you can open a short position or not @@ -301,42 +279,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } isPositionOpened := !s.Position.IsClosed() && !s.Position.IsDust(kline.Close) - if isPositionOpened && s.Position.IsShort() { - roi := s.Position.ROI(kline.Close) - if !s.Exit.RoiMinTakeProfitPercentage.IsZero() { - if roi.Compare(s.Exit.RoiMinTakeProfitPercentage) > 0 { - if !s.Exit.LowerShadowRatio.IsZero() && kline.GetLowerShadowHeight().Div(kline.Close).Compare(s.Exit.LowerShadowRatio) > 0 { - bbgo.Notify("%s TakeProfit triggered at price %f: by shadow ratio %f", - s.Symbol, - kline.Close.Float64(), - kline.GetLowerShadowRatio().Float64(), kline) - _ = s.ClosePosition(ctx, fixedpoint.One) - 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 { - bbgo.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, fixedpoint.One) - return - } - } - } - } - } + return } if len(s.pivotLowPrices) == 0 {