mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Merge pull request #842 from c9s/feature/bollmaker-exit
feature: bollmaker exit methods
This commit is contained in:
commit
48ab33dfa3
|
@ -17,7 +17,7 @@ backtest:
|
|||
# see here for more details
|
||||
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
|
||||
startTime: "2022-01-01"
|
||||
endTime: "2022-05-12"
|
||||
endTime: "2022-07-18"
|
||||
sessions:
|
||||
- binance
|
||||
symbols:
|
||||
|
@ -133,28 +133,24 @@ exchangeStrategies:
|
|||
# buyBelowNeutralSMA: when this set, it will only place buy order when the current price is below the SMA line.
|
||||
buyBelowNeutralSMA: false
|
||||
|
||||
# Set up your stop order, this is optional
|
||||
# sometimes the stop order might decrease your total profit.
|
||||
# you can setup multiple stop,
|
||||
stops:
|
||||
# use trailing stop order
|
||||
- trailingStop:
|
||||
# callbackRate: when the price reaches -1% from the previous highest, we trigger the stop
|
||||
callbackRate: 5.1%
|
||||
exits:
|
||||
|
||||
# closePosition is how much position do you want to close
|
||||
closePosition: 20%
|
||||
# roiTakeProfit is used to force taking profit by percentage of the position ROI (currently the price change)
|
||||
# force to take the profit ROI exceeded the percentage.
|
||||
- roiTakeProfit:
|
||||
percentage: 3%
|
||||
|
||||
# minProfit is how much profit you want to take.
|
||||
# if you set this option, your stop will only be triggered above the average cost.
|
||||
minProfit: 5%
|
||||
- protectiveStopLoss:
|
||||
activationRatio: 1%
|
||||
stopLossRatio: 0.2%
|
||||
placeStopOrder: false
|
||||
|
||||
# interval is the time interval for checking your stop
|
||||
interval: 1m
|
||||
|
||||
# virtual means we don't place a a REAL stop order
|
||||
# when virtual is on
|
||||
# the strategy won't place a REAL stop order, instead if watches the close price,
|
||||
# and if the condition matches, it submits a market order to close your position.
|
||||
virtual: true
|
||||
- protectiveStopLoss:
|
||||
activationRatio: 2%
|
||||
stopLossRatio: 1%
|
||||
placeStopOrder: false
|
||||
|
||||
- protectiveStopLoss:
|
||||
activationRatio: 5%
|
||||
stopLossRatio: 3%
|
||||
placeStopOrder: false
|
||||
|
|
|
@ -42,6 +42,7 @@ func TestTrailingStop_ShortPosition(t *testing.T) {
|
|||
Market: market,
|
||||
Quantity: fixedpoint.NewFromFloat(1.0),
|
||||
Tag: "trailingStop",
|
||||
MarginSideEffect: types.SideEffectTypeAutoRepay,
|
||||
})
|
||||
|
||||
session := NewExchangeSession("test", mockEx)
|
||||
|
@ -119,6 +120,7 @@ func TestTrailingStop_LongPosition(t *testing.T) {
|
|||
Market: market,
|
||||
Quantity: fixedpoint.NewFromFloat(1.0),
|
||||
Tag: "trailingStop",
|
||||
MarginSideEffect: types.SideEffectTypeAutoRepay,
|
||||
})
|
||||
|
||||
session := NewExchangeSession("test", mockEx)
|
||||
|
|
|
@ -1,287 +0,0 @@
|
|||
package bbgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
type TrailingStop struct {
|
||||
// CallbackRate is the callback rate from the previous high price
|
||||
CallbackRate fixedpoint.Value `json:"callbackRate,omitempty"`
|
||||
|
||||
// ClosePosition is a percentage of the position to be closed
|
||||
ClosePosition fixedpoint.Value `json:"closePosition,omitempty"`
|
||||
|
||||
// MinProfit is the percentage of the minimum profit ratio.
|
||||
// Stop order will be activiated only when the price reaches above this threshold.
|
||||
MinProfit fixedpoint.Value `json:"minProfit,omitempty"`
|
||||
|
||||
// Interval is the time resolution to update the stop order
|
||||
// KLine per Interval will be used for updating the stop order
|
||||
Interval types.Interval `json:"interval,omitempty"`
|
||||
|
||||
// Virtual is used when you don't want to place the real order on the exchange and lock the balance.
|
||||
// You want to handle the stop order by the strategy itself.
|
||||
Virtual bool `json:"virtual,omitempty"`
|
||||
}
|
||||
|
||||
type TrailingStopController struct {
|
||||
*TrailingStop
|
||||
|
||||
Symbol string
|
||||
|
||||
position *types.Position
|
||||
latestHigh fixedpoint.Value
|
||||
averageCost fixedpoint.Value
|
||||
|
||||
// activated: when the price reaches the min profit price, we set the activated to true to enable trailing stop
|
||||
activated bool
|
||||
}
|
||||
|
||||
func NewTrailingStopController(symbol string, config *TrailingStop) *TrailingStopController {
|
||||
return &TrailingStopController{
|
||||
TrailingStop: config,
|
||||
Symbol: symbol,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TrailingStopController) Subscribe(session *ExchangeSession) {
|
||||
session.Subscribe(types.KLineChannel, c.Symbol, types.SubscribeOptions{
|
||||
Interval: c.Interval,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *TrailingStopController) Run(ctx context.Context, session *ExchangeSession, tradeCollector *TradeCollector) {
|
||||
// store the position
|
||||
c.position = tradeCollector.Position()
|
||||
c.averageCost = c.position.AverageCost
|
||||
|
||||
// Use trade collector to get the position update event
|
||||
tradeCollector.OnPositionUpdate(func(position *types.Position) {
|
||||
// update average cost if we have it.
|
||||
c.averageCost = position.AverageCost
|
||||
})
|
||||
|
||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
||||
if kline.Symbol != c.Symbol || kline.Interval != c.Interval {
|
||||
return
|
||||
}
|
||||
|
||||
// if average cost is zero, we don't need trailing stop
|
||||
if c.averageCost.IsZero() || c.position == nil {
|
||||
return
|
||||
}
|
||||
|
||||
closePrice := kline.Close
|
||||
|
||||
// if we don't hold position, we just skip dust position
|
||||
if c.position.Base.Abs().Compare(c.position.Market.MinQuantity) < 0 || c.position.Base.Abs().Mul(closePrice).Compare(c.position.Market.MinNotional) < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if c.MinProfit.Sign() <= 0 {
|
||||
// when minProfit is not set, we should always activate the trailing stop order
|
||||
c.activated = true
|
||||
} else if closePrice.Compare(c.averageCost) > 0 ||
|
||||
changeRate(closePrice, c.averageCost).Compare(c.MinProfit) > 0 {
|
||||
|
||||
if !c.activated {
|
||||
log.Infof("%s trailing stop activated at price %s", c.Symbol, closePrice.String())
|
||||
c.activated = true
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.activated {
|
||||
return
|
||||
}
|
||||
|
||||
// if the trailing stop order is activated, we should update the latest high
|
||||
// update the latest high
|
||||
c.latestHigh = fixedpoint.Max(closePrice, c.latestHigh)
|
||||
|
||||
// if it's in the callback rate, we don't want to trigger stop
|
||||
if closePrice.Compare(c.latestHigh) < 0 && changeRate(closePrice, c.latestHigh).Compare(c.CallbackRate) < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if c.Virtual {
|
||||
// if the profit rate is defined, and it is less than our minimum profit rate, we skip stop
|
||||
if c.MinProfit.Sign() > 0 &&
|
||||
closePrice.Compare(c.averageCost) < 0 ||
|
||||
changeRate(closePrice, c.averageCost).Compare(c.MinProfit) < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("%s trailing stop emitted, latest high: %s, closed price: %s, average cost: %s, profit spread: %s",
|
||||
c.Symbol,
|
||||
c.latestHigh.String(),
|
||||
closePrice.String(),
|
||||
c.averageCost.String(),
|
||||
closePrice.Sub(c.averageCost).String())
|
||||
|
||||
log.Infof("current %s position: %s", c.Symbol, c.position.String())
|
||||
|
||||
marketOrder := c.position.NewMarketCloseOrder(c.ClosePosition)
|
||||
if marketOrder != nil {
|
||||
log.Infof("submitting %s market order to stop: %+v", c.Symbol, marketOrder)
|
||||
|
||||
// skip dust order
|
||||
if marketOrder.Quantity.Mul(closePrice).Compare(c.position.Market.MinNotional) < 0 {
|
||||
log.Warnf("%s market order quote quantity %s < min notional %s, skip placing order", c.Symbol, marketOrder.Quantity.Mul(closePrice).String(), c.position.Market.MinNotional.String())
|
||||
return
|
||||
}
|
||||
|
||||
createdOrders, err := session.Exchange.SubmitOrders(ctx, *marketOrder)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("stop market order place error")
|
||||
return
|
||||
}
|
||||
tradeCollector.OrderStore().Add(createdOrders...)
|
||||
tradeCollector.Process()
|
||||
|
||||
// reset the state
|
||||
c.latestHigh = fixedpoint.Zero
|
||||
c.activated = false
|
||||
}
|
||||
} else {
|
||||
// place stop order only when the closed price is greater than the current average cost
|
||||
if c.MinProfit.Sign() > 0 && closePrice.Compare(c.averageCost) > 0 &&
|
||||
changeRate(closePrice, c.averageCost).Compare(c.MinProfit) >= 0 {
|
||||
|
||||
stopPrice := c.averageCost.Mul(fixedpoint.One.Add(c.MinProfit))
|
||||
orderForm := c.GenerateStopOrder(stopPrice, c.averageCost)
|
||||
if orderForm != nil {
|
||||
log.Infof("updating %s stop limit order to simulate trailing stop order...", c.Symbol)
|
||||
|
||||
createdOrders, err := session.Exchange.SubmitOrders(ctx, *orderForm)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("%s stop order place error", c.Symbol)
|
||||
return
|
||||
}
|
||||
|
||||
tradeCollector.OrderStore().Add(createdOrders...)
|
||||
tradeCollector.Process()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *TrailingStopController) GenerateStopOrder(stopPrice, price fixedpoint.Value) *types.SubmitOrder {
|
||||
base := c.position.GetBase()
|
||||
if base.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
quantity := base.Abs()
|
||||
quoteQuantity := price.Mul(quantity)
|
||||
|
||||
if c.ClosePosition.Sign() > 0 {
|
||||
quantity = quantity.Mul(c.ClosePosition)
|
||||
}
|
||||
|
||||
// skip dust orders
|
||||
if quantity.Compare(c.position.Market.MinQuantity) < 0 ||
|
||||
quoteQuantity.Compare(c.position.Market.MinNotional) < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
side := types.SideTypeSell
|
||||
if base.Sign() < 0 {
|
||||
side = types.SideTypeBuy
|
||||
}
|
||||
|
||||
return &types.SubmitOrder{
|
||||
Symbol: c.Symbol,
|
||||
Market: c.position.Market,
|
||||
Type: types.OrderTypeStopLimit,
|
||||
Side: side,
|
||||
StopPrice: stopPrice,
|
||||
Price: price,
|
||||
Quantity: quantity,
|
||||
}
|
||||
}
|
||||
|
||||
type FixedStop struct{}
|
||||
|
||||
type Stop struct {
|
||||
TrailingStop *TrailingStop `json:"trailingStop,omitempty"`
|
||||
FixedStop *FixedStop `json:"fixedStop,omitempty"`
|
||||
}
|
||||
|
||||
// SmartStops shares the stop order logics between different strategies
|
||||
//
|
||||
// See also:
|
||||
// - Stop-Loss order: https://www.investopedia.com/terms/s/stop-lossorder.asp
|
||||
// - Trailing Stop-loss order: https://www.investopedia.com/articles/trading/08/trailing-stop-loss.asp
|
||||
//
|
||||
// How to integrate this into your strategy?
|
||||
//
|
||||
// To use the stop controllers, you can embed this struct into your Strategy struct
|
||||
//
|
||||
// func (s *Strategy) Initialize() error {
|
||||
// return s.SmartStops.InitializeStopControllers(s.Symbol)
|
||||
// }
|
||||
// func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||
// s.SmartStops.Subscribe(session)
|
||||
// }
|
||||
//
|
||||
// func (s *Strategy) Run() {
|
||||
// s.SmartStops.RunStopControllers(ctx, session, s.tradeCollector)
|
||||
// }
|
||||
//
|
||||
type SmartStops struct {
|
||||
// Stops is the slice of the stop order config
|
||||
Stops []Stop `json:"stops,omitempty"`
|
||||
|
||||
// StopControllers are constructed from the stop config
|
||||
StopControllers []StopController `json:"-"`
|
||||
}
|
||||
|
||||
type StopController interface {
|
||||
Subscribe(session *ExchangeSession)
|
||||
Run(ctx context.Context, session *ExchangeSession, tradeCollector *TradeCollector)
|
||||
}
|
||||
|
||||
func (s *SmartStops) newStopController(symbol string, config Stop) (StopController, error) {
|
||||
if config.TrailingStop != nil {
|
||||
return NewTrailingStopController(symbol, config.TrailingStop), nil
|
||||
}
|
||||
|
||||
return nil, errors.New("incorrect stop controller setup")
|
||||
}
|
||||
|
||||
func (s *SmartStops) InitializeStopControllers(symbol string) error {
|
||||
for _, stop := range s.Stops {
|
||||
controller, err := s.newStopController(symbol, stop)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.StopControllers = append(s.StopControllers, controller)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SmartStops) Subscribe(session *ExchangeSession) {
|
||||
for _, stopController := range s.StopControllers {
|
||||
stopController.Subscribe(session)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SmartStops) RunStopControllers(ctx context.Context, session *ExchangeSession, tradeCollector *TradeCollector) {
|
||||
for _, stopController := range s.StopControllers {
|
||||
stopController.Run(ctx, session, tradeCollector)
|
||||
}
|
||||
}
|
||||
|
||||
func changeRate(a, b fixedpoint.Value) fixedpoint.Value {
|
||||
return a.Sub(b).Div(b).Abs()
|
||||
}
|
|
@ -23,8 +23,6 @@ import (
|
|||
|
||||
const ID = "bollmaker"
|
||||
|
||||
const stateKey = "state-v1"
|
||||
|
||||
var notionModifier = fixedpoint.NewFromFloat(1.1)
|
||||
var two = fixedpoint.NewFromInt(2)
|
||||
|
||||
|
@ -58,8 +56,7 @@ type Strategy struct {
|
|||
// Symbol is the market symbol you want to trade
|
||||
Symbol string `json:"symbol"`
|
||||
|
||||
// Interval is how long do you want to update your order price and quantity
|
||||
Interval types.Interval `json:"interval"`
|
||||
types.IntervalWindow
|
||||
|
||||
bbgo.QuantityOrAmount
|
||||
|
||||
|
@ -142,12 +139,10 @@ type Strategy struct {
|
|||
ShadowProtection bool `json:"shadowProtection"`
|
||||
ShadowProtectionRatio fixedpoint.Value `json:"shadowProtectionRatio"`
|
||||
|
||||
bbgo.SmartStops
|
||||
|
||||
session *bbgo.ExchangeSession
|
||||
book *types.StreamOrderBook
|
||||
|
||||
state *State
|
||||
ExitMethods bbgo.ExitMethodSet `json:"exits"`
|
||||
|
||||
// persistence fields
|
||||
Position *types.Position `json:"position,omitempty" persistence:"position"`
|
||||
|
@ -175,10 +170,6 @@ func (s *Strategy) InstanceID() string {
|
|||
return fmt.Sprintf("%s:%s", ID, s.Symbol)
|
||||
}
|
||||
|
||||
func (s *Strategy) Initialize() error {
|
||||
return s.SmartStops.InitializeStopControllers(s.Symbol)
|
||||
}
|
||||
|
||||
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{
|
||||
Interval: s.Interval,
|
||||
|
@ -196,7 +187,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
|||
})
|
||||
}
|
||||
|
||||
s.SmartStops.Subscribe(session)
|
||||
s.ExitMethods.SetAndSubscribe(session, s)
|
||||
}
|
||||
|
||||
func (s *Strategy) Validate() error {
|
||||
|
@ -449,18 +440,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
s.DynamicSpread.DynamicAskSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, s.DynamicSpread.Window}}
|
||||
}
|
||||
|
||||
s.OnSuspend(func() {
|
||||
s.Status = types.StrategyStatusStopped
|
||||
_ = s.orderExecutor.GracefulCancel(ctx)
|
||||
bbgo.Sync(s)
|
||||
})
|
||||
|
||||
s.OnEmergencyStop(func() {
|
||||
// Close 100% position
|
||||
percentage := fixedpoint.NewFromFloat(1.0)
|
||||
_ = s.ClosePosition(ctx, percentage)
|
||||
})
|
||||
|
||||
if s.DisableShort {
|
||||
s.Long = &[]bool{true}[0]
|
||||
}
|
||||
|
@ -515,18 +494,27 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
s.orderExecutor.BindEnvironment(s.Environment)
|
||||
s.orderExecutor.BindProfitStats(s.ProfitStats)
|
||||
s.orderExecutor.Bind()
|
||||
|
||||
s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
|
||||
bbgo.Sync(s)
|
||||
})
|
||||
|
||||
s.SmartStops.RunStopControllers(ctx, session, s.orderExecutor.TradeCollector())
|
||||
s.ExitMethods.Bind(session, s.orderExecutor)
|
||||
|
||||
if bbgo.IsBackTesting {
|
||||
log.Warn("turning of useTickerPrice option in the back-testing environment...")
|
||||
s.UseTickerPrice = false
|
||||
}
|
||||
|
||||
s.OnSuspend(func() {
|
||||
_ = s.orderExecutor.GracefulCancel(ctx)
|
||||
bbgo.Sync(s)
|
||||
})
|
||||
|
||||
s.OnEmergencyStop(func() {
|
||||
// Close 100% position
|
||||
percentage := fixedpoint.NewFromFloat(1.0)
|
||||
_ = s.ClosePosition(ctx, percentage)
|
||||
})
|
||||
|
||||
session.UserDataStream.OnStart(func() {
|
||||
if s.UseTickerPrice {
|
||||
ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol)
|
||||
|
@ -543,28 +531,24 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
}
|
||||
})
|
||||
|
||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
||||
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
|
||||
// StrategyController
|
||||
if s.Status != types.StrategyStatusRunning {
|
||||
return
|
||||
}
|
||||
|
||||
if kline.Symbol != s.Symbol || kline.Interval != s.Interval {
|
||||
return
|
||||
}
|
||||
|
||||
// Update spreads with dynamic spread
|
||||
if s.DynamicSpread.Enabled {
|
||||
s.DynamicSpread.Update(kline)
|
||||
dynamicBidSpread, err := s.DynamicSpread.GetBidSpread()
|
||||
if err == nil && dynamicBidSpread > 0 {
|
||||
s.BidSpread = fixedpoint.NewFromFloat(dynamicBidSpread)
|
||||
log.Infof("new bid spread: %v", s.BidSpread.Percentage())
|
||||
log.Infof("%s dynamic bid spread updated: %s", s.Symbol, s.BidSpread.Percentage())
|
||||
}
|
||||
dynamicAskSpread, err := s.DynamicSpread.GetAskSpread()
|
||||
if err == nil && dynamicAskSpread > 0 {
|
||||
s.AskSpread = fixedpoint.NewFromFloat(dynamicAskSpread)
|
||||
log.Infof("new ask spread: %v", s.AskSpread.Percentage())
|
||||
log.Infof("%s dynamic ask spread updated: %s", s.Symbol, s.AskSpread.Percentage())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -582,7 +566,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
} else {
|
||||
s.placeOrders(ctx, kline.Close, &kline)
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
// s.book = types.NewStreamBook(s.Symbol)
|
||||
// s.book.BindStreamForBackground(session.MarketDataStream)
|
||||
|
|
|
@ -121,8 +121,6 @@ type Strategy struct {
|
|||
ProfitStats *types.ProfitStats `persistence:"profit_stats"`
|
||||
TradeStats *types.TradeStats `persistence:"trade_stats"`
|
||||
|
||||
bbgo.SmartStops
|
||||
|
||||
session *bbgo.ExchangeSession
|
||||
orderExecutor *bbgo.GeneralOrderExecutor
|
||||
book *types.StreamOrderBook
|
||||
|
@ -143,15 +141,10 @@ func (s *Strategy) ID() string {
|
|||
return ID
|
||||
}
|
||||
|
||||
func (s *Strategy) Initialize() error {
|
||||
return s.SmartStops.InitializeStopControllers(s.Symbol)
|
||||
}
|
||||
|
||||
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{
|
||||
Interval: s.Interval,
|
||||
})
|
||||
// s.SmartStops.Subscribe(session)
|
||||
}
|
||||
|
||||
func (s *Strategy) Validate() error {
|
||||
|
@ -430,8 +423,6 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
|
|||
s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth)
|
||||
s.defaultBoll = s.StandardIndicatorSet.BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth)
|
||||
|
||||
// s.SmartStops.RunStopControllers(ctx, session, s.tradeCollector)
|
||||
|
||||
var klines []*types.KLine
|
||||
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
|
||||
// StrategyController
|
||||
|
|
Loading…
Reference in New Issue
Block a user