pivotshort: refactor take profit and stop loss methods

Signed-off-by: c9s <yoanlin93@gmail.com>
This commit is contained in:
c9s 2022-06-26 16:13:58 +08:00
parent 4c02d8f729
commit 47677e303f
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
8 changed files with 305 additions and 26 deletions

View File

@ -56,7 +56,7 @@ type SimplePriceMatching struct {
mu sync.Mutex mu sync.Mutex
bidOrders []types.Order bidOrders []types.Order
askOrders []types.Order askOrders []types.Order
closedOrders []types.Order closedOrders map[uint64]types.Order
LastPrice fixedpoint.Value LastPrice fixedpoint.Value
LastKLine types.KLine LastKLine types.KLine
@ -375,8 +375,9 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
trades = append(trades, trade) trades = append(trades, trade)
m.EmitOrderUpdate(o) m.EmitOrderUpdate(o)
m.closedOrders[o.OrderID] = o
} }
m.closedOrders = append(m.closedOrders, closedOrders...)
return closedOrders, trades return closedOrders, trades
} }
@ -445,12 +446,33 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
trades = append(trades, trade) trades = append(trades, trade)
m.EmitOrderUpdate(o) m.EmitOrderUpdate(o)
m.closedOrders[o.OrderID] = o
} }
m.closedOrders = append(m.closedOrders, closedOrders...)
return closedOrders, trades return closedOrders, trades
} }
func (m *SimplePriceMatching) getOrder(orderID uint64) (types.Order, bool) {
if o, ok := m.closedOrders[orderID]; ok {
return o, true
}
for _, o := range m.bidOrders {
if o.OrderID == orderID {
return o, true
}
}
for _, o := range m.askOrders {
if o.OrderID == orderID {
return o, true
}
}
return types.Order{}, false
}
func (m *SimplePriceMatching) processKLine(kline types.KLine) { func (m *SimplePriceMatching) processKLine(kline types.KLine) {
m.CurrentTime = kline.EndTime.Time() m.CurrentTime = kline.EndTime.Time()
m.LastKLine = kline m.LastKLine = kline

View File

@ -14,11 +14,6 @@ import (
type OrderExecutor interface { type OrderExecutor interface {
SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error)
CancelOrders(ctx context.Context, orders ...types.Order) error CancelOrders(ctx context.Context, orders ...types.Order) error
OnTradeUpdate(cb func(trade types.Trade))
OnOrderUpdate(cb func(order types.Order))
EmitTradeUpdate(trade types.Trade)
EmitOrderUpdate(order types.Order)
} }
type OrderExecutionRouter interface { type OrderExecutionRouter interface {

View File

@ -85,6 +85,11 @@ func (e *GeneralOrderExecutor) Bind() {
e.tradeCollector.BindStream(e.session.UserDataStream) e.tradeCollector.BindStream(e.session.UserDataStream)
} }
func (e *GeneralOrderExecutor) CancelOrders(ctx context.Context, orders ...types.Order) error {
err := e.session.Exchange.CancelOrders(ctx, orders...)
return err
}
func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) {
formattedOrders, err := e.session.FormatOrders(submitOrders) formattedOrders, err := e.session.FormatOrders(submitOrders)
if err != nil { if err != nil {
@ -125,3 +130,11 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix
func (e *GeneralOrderExecutor) TradeCollector() *TradeCollector { func (e *GeneralOrderExecutor) TradeCollector() *TradeCollector {
return e.tradeCollector return e.tradeCollector
} }
func (e *GeneralOrderExecutor) Session() *ExchangeSession {
return e.session
}
func (e *GeneralOrderExecutor) Position() *types.Position {
return e.position
}

View File

@ -0,0 +1,155 @@
package pivotshort
import (
"context"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
type ProtectionStopLoss struct {
// ActivationRatio is the trigger condition of this ROI protection stop loss
// When the price goes lower (for short position) with the ratio, the protection stop will be activated.
// This number should be positive to protect the profit
ActivationRatio fixedpoint.Value `json:"activationRatio"`
// StopLossRatio is the ratio for stop loss. This number should be positive to protect the profit.
// negative ratio will cause loss.
StopLossRatio fixedpoint.Value `json:"stopLossRatio"`
// PlaceStopOrder places the stop order on exchange and lock the balance
PlaceStopOrder bool `json:"placeStopOrder"`
session *bbgo.ExchangeSession
orderExecutor *bbgo.GeneralOrderExecutor
stopLossPrice fixedpoint.Value
stopLossOrder *types.Order
}
func (s *ProtectionStopLoss) shouldActivate(position *types.Position, closePrice fixedpoint.Value) bool {
if position.IsLong() {
r := one.Add(s.ActivationRatio)
activationPrice := position.AverageCost.Mul(r)
return closePrice.Compare(activationPrice) > 0
} else if position.IsShort() {
r := one.Sub(s.ActivationRatio)
activationPrice := position.AverageCost.Mul(r)
// for short position, if the close price is less than the activation price then this is a profit position.
return closePrice.Compare(activationPrice) < 0
}
return false
}
func (s *ProtectionStopLoss) placeStopOrder(ctx context.Context, position *types.Position, orderExecutor bbgo.OrderExecutor) error {
if s.stopLossOrder != nil {
if err := orderExecutor.CancelOrders(ctx, *s.stopLossOrder); err != nil {
log.WithError(err).Errorf("failed to cancel stop limit order: %+v", s.stopLossOrder)
}
s.stopLossOrder = nil
}
createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: position.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeStopLimit,
Quantity: position.GetQuantity(),
Price: s.stopLossPrice.Mul(one.Add(fixedpoint.NewFromFloat(0.005))), // +0.5% from the trigger price, slippage protection
StopPrice: s.stopLossPrice,
Market: position.Market,
})
if len(createdOrders) > 0 {
s.stopLossOrder = &createdOrders[0]
}
return err
}
func (s *ProtectionStopLoss) shouldStop(closePrice fixedpoint.Value) bool {
if s.stopLossPrice.IsZero() {
return false
}
return closePrice.Compare(s.stopLossPrice) >= 0
}
func (s *ProtectionStopLoss) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
s.session = session
s.orderExecutor = orderExecutor
orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
if position.IsClosed() {
s.stopLossOrder = nil
s.stopLossPrice = zero
}
})
session.UserDataStream.OnOrderUpdate(func(order types.Order) {
if s.stopLossOrder == nil {
return
}
if order.OrderID == s.stopLossOrder.OrderID {
switch order.Status {
case types.OrderStatusFilled, types.OrderStatusCanceled:
s.stopLossOrder = nil
s.stopLossPrice = zero
}
}
})
position := orderExecutor.Position()
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
if kline.Symbol != position.Symbol || kline.Interval != types.Interval1m {
return
}
isPositionOpened := !position.IsClosed() && !position.IsDust(kline.Close)
if isPositionOpened && position.IsShort() {
s.handleChange(context.Background(), position, kline.Close, s.orderExecutor)
}
})
}
func (s *ProtectionStopLoss) handleChange(ctx context.Context, position *types.Position, closePrice fixedpoint.Value, orderExecutor *bbgo.GeneralOrderExecutor) {
if s.stopLossOrder != nil {
// use RESTful to query the order status
// orderQuery := orderExecutor.Session().Exchange.(types.ExchangeOrderQueryService)
// order, err := orderQuery.QueryOrder(ctx, types.OrderQuery{
// Symbol: s.stopLossOrder.Symbol,
// OrderID: strconv.FormatUint(s.stopLossOrder.OrderID, 10),
// })
// if err != nil {
// log.WithError(err).Errorf("query order failed")
// }
}
if s.stopLossPrice.IsZero() {
if s.shouldActivate(position, closePrice) {
// calculate stop loss price
if position.IsShort() {
s.stopLossPrice = position.AverageCost.Mul(one.Sub(s.StopLossRatio))
} else if position.IsLong() {
s.stopLossPrice = position.AverageCost.Mul(one.Add(s.StopLossRatio))
}
log.Infof("[ProtectionStopLoss] %s protection stop loss activated, current price = %f, average cost = %f, stop loss price = %f",
position.Symbol, closePrice.Float64(), position.AverageCost.Float64(), s.stopLossPrice.Float64())
} else {
// not activated, skip setup stop order
return
}
}
if s.PlaceStopOrder {
if err := s.placeStopOrder(ctx, position, orderExecutor); err != nil {
log.WithError(err).Errorf("failed to place stop limit order")
}
} else if s.shouldStop(closePrice) {
log.Infof("[ProtectionStopLoss] protection stop order is triggered at price %f, position = %+v", closePrice.Float64(), position)
if err := orderExecutor.ClosePosition(ctx, one); err != nil {
log.WithError(err).Errorf("failed to close position")
}
}
}

View File

@ -0,0 +1,41 @@
package pivotshort
import (
"context"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
type RoiStopLoss struct {
Percentage fixedpoint.Value `json:"percentage"`
session *bbgo.ExchangeSession
orderExecutor *bbgo.GeneralOrderExecutor
}
func (s *RoiStopLoss) 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.Compare(s.Percentage.Neg()) < 0 {
// stop loss
bbgo.Notify("[RoiStopLoss] %s stop loss triggered by ROI %s/%s, price: %f", position.Symbol, roi.Percentage(), s.Percentage.Neg().Percentage(), kline.Close.Float64())
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One)
return
}
})
}

View File

@ -0,0 +1,41 @@
package pivotshort
import (
"context"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
type RoiTakeProfit struct {
Percentage fixedpoint.Value `json:"percentage"`
session *bbgo.ExchangeSession
orderExecutor *bbgo.GeneralOrderExecutor
}
func (s *RoiTakeProfit) 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.Compare(s.Percentage) > 0 {
// stop loss
bbgo.Notify("[RoiTakeProfit] %s take profit is triggered by ROI %s/%s, price: %f", position.Symbol, roi.Percentage(), s.Percentage.Percentage(), kline.Close.Float64())
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One)
return
}
})
}

View File

@ -17,6 +17,9 @@ import (
const ID = "pivotshort" const ID = "pivotshort"
var one = fixedpoint.One
var zero = fixedpoint.Zero
var log = logrus.WithField("strategy", ID) var log = logrus.WithField("strategy", ID)
func init() { func init() {
@ -72,10 +75,12 @@ type CumulatedVolume struct {
} }
type Exit struct { type Exit struct {
RoiStopLossPercentage fixedpoint.Value `json:"roiStopLossPercentage"`
RoiTakeProfitPercentage fixedpoint.Value `json:"roiTakeProfitPercentage"`
RoiMinTakeProfitPercentage fixedpoint.Value `json:"roiMinTakeProfitPercentage"` RoiMinTakeProfitPercentage fixedpoint.Value `json:"roiMinTakeProfitPercentage"`
RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"`
RoiStopLoss *RoiStopLoss `json:"roiStopLoss"`
ProtectionStopLoss *ProtectionStopLoss `json:"protectionStopLoss"`
LowerShadowRatio fixedpoint.Value `json:"lowerShadowRatio"` LowerShadowRatio fixedpoint.Value `json:"lowerShadowRatio"`
CumulatedVolume *CumulatedVolume `json:"cumulatedVolume"` CumulatedVolume *CumulatedVolume `json:"cumulatedVolume"`
@ -108,6 +113,7 @@ type Strategy struct {
session *bbgo.ExchangeSession session *bbgo.ExchangeSession
orderExecutor *bbgo.GeneralOrderExecutor orderExecutor *bbgo.GeneralOrderExecutor
stopLossPrice fixedpoint.Value
lastLow fixedpoint.Value lastLow fixedpoint.Value
pivot *indicator.Pivot pivot *indicator.Pivot
resistancePivot *indicator.Pivot resistancePivot *indicator.Pivot
@ -178,6 +184,7 @@ func (s *Strategy) CurrentPosition() *types.Position {
} }
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
bbgo.Notify("Closing position", s.Position)
return s.orderExecutor.ClosePosition(ctx, percentage) return s.orderExecutor.ClosePosition(ctx, percentage)
} }
@ -271,9 +278,20 @@ 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)
}
// Always check whether you can open a short position or not // Always check whether you can open a short position or not
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
// StrategyController
if s.Status != types.StrategyStatusRunning { if s.Status != types.StrategyStatusRunning {
return return
} }
@ -285,21 +303,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
isPositionOpened := !s.Position.IsClosed() && !s.Position.IsDust(kline.Close) isPositionOpened := !s.Position.IsClosed() && !s.Position.IsDust(kline.Close)
if isPositionOpened && s.Position.IsShort() { if isPositionOpened && s.Position.IsShort() {
// calculate return rate roi := s.Position.ROI(kline.Close)
// TODO: apply quantity to this formula if !s.Exit.RoiMinTakeProfitPercentage.IsZero() {
roi := s.Position.AverageCost.Sub(kline.Close).Div(s.Position.AverageCost) if roi.Compare(s.Exit.RoiMinTakeProfitPercentage) > 0 {
if roi.Compare(s.Exit.RoiStopLossPercentage.Neg()) < 0 {
// stop loss
bbgo.Notify("%s ROI StopLoss triggered at price %f: Loss %s", s.Symbol, kline.Close.Float64(), roi.Percentage())
_ = s.ClosePosition(ctx, fixedpoint.One)
return
} else {
// take profit
if roi.Compare(s.Exit.RoiTakeProfitPercentage) > 0 { // force take profit
bbgo.Notify("%s TakeProfit triggered at price %f: by ROI percentage %s", s.Symbol, kline.Close.Float64(), roi.Percentage(), kline)
_ = s.ClosePosition(ctx, fixedpoint.One)
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 { 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", bbgo.Notify("%s TakeProfit triggered at price %f: by shadow ratio %f",
s.Symbol, s.Symbol,
@ -334,6 +340,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
} }
if len(s.pivotLowPrices) == 0 { if len(s.pivotLowPrices) == 0 {
log.Infof("currently there is no pivot low prices, skip placing orders...")
return return
} }

View File

@ -174,6 +174,11 @@ func (p *Position) GetBase() (base fixedpoint.Value) {
return base return base
} }
func (p *Position) GetQuantity() fixedpoint.Value {
base := p.GetBase()
return base.Abs()
}
func (p *Position) UnrealizedProfit(price fixedpoint.Value) fixedpoint.Value { func (p *Position) UnrealizedProfit(price fixedpoint.Value) fixedpoint.Value {
quantity := p.GetBase().Abs() quantity := p.GetBase().Abs()