diff --git a/config/bollmaker.yaml b/config/bollmaker.yaml index 748ac2878..f4b253d3f 100644 --- a/config/bollmaker.yaml +++ b/config/bollmaker.yaml @@ -56,6 +56,33 @@ exchangeStrategies: # For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) minProfitSpread: 0.1% + # EXPERIMENTAL + # Dynamic spread is an experimental feature. Use at your own risk! + # + # dynamicSpread enables the automatic adjustment to bid and ask spread. + # dynamicSpread: + # enabled: true + # # window is the window of the SMAs of spreads + # window: 1 + # askSpreadScale: + # byPercentage: + # # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + # exp: + # # from down to up + # domain: [ 0.0001, 0.005 ] + # # when in down band, holds 1.0 by maximum + # # when in up band, holds 0.05 by maximum + # range: [ 0.001, 0.002 ] + # bidSpreadScale: + # byPercentage: + # # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale + # exp: + # # from down to up + # domain: [ 0.0001, 0.005 ] + # # when in down band, holds 1.0 by maximum + # # when in up band, holds 0.05 by maximum + # range: [ 0.001, 0.002 ] + # maxExposurePosition is the maximum position you can hold # +10 means you can hold 10 ETH long position by maximum # -10 means you can hold -10 ETH short position by maximum diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index f134ba107..038f2c901 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -49,6 +49,81 @@ type BollingerSetting struct { BandWidth float64 `json:"bandWidth"` } +type DynamicSpreadSettings struct { + Enabled bool `json:"enabled"` + + // Window is the window of the SMAs of spreads + Window int `json:"window"` + + // AskSpreadScale is used to define the ask spread range with the given percentage. + AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"` + + // BidSpreadScale is used to define the bid spread range with the given percentage. + BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"` + + DynamicAskSpread *indicator.SMA + DynamicBidSpread *indicator.SMA +} + +// Update dynamic spreads +func (ds *DynamicSpreadSettings) Update(kline types.KLine) { + if !ds.Enabled { + return + } + + ampl := (kline.GetHigh().Float64() - kline.GetLow().Float64()) / kline.GetOpen().Float64() + + switch kline.Direction() { + case types.DirectionUp: + ds.DynamicAskSpread.Update(ampl) + ds.DynamicBidSpread.Update(0) + case types.DirectionDown: + ds.DynamicBidSpread.Update(ampl) + ds.DynamicAskSpread.Update(0) + default: + ds.DynamicAskSpread.Update(0) + ds.DynamicBidSpread.Update(0) + } +} + +// GetAskSpread returns current ask spread +func (ds *DynamicSpreadSettings) GetAskSpread() (askSpread float64, err error) { + if !ds.Enabled { + return 0, errors.New("dynamic spread is not enabled") + } + + if ds.AskSpreadScale != nil && ds.DynamicAskSpread.Length() >= ds.Window { + askSpread, err = ds.AskSpreadScale.Scale(ds.DynamicAskSpread.Last()) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicAskSpread") + return 0, err + } + + return askSpread, nil + } + + return 0, errors.New("incomplete dynamic spread settings or not enough data yet") +} + +// GetBidSpread returns current dynamic bid spread +func (ds *DynamicSpreadSettings) GetBidSpread() (bidSpread float64, err error) { + if !ds.Enabled { + return 0, errors.New("dynamic spread is not enabled") + } + + if ds.BidSpreadScale != nil && ds.DynamicBidSpread.Length() >= ds.Window { + bidSpread, err = ds.BidSpreadScale.Scale(ds.DynamicBidSpread.Last()) + if err != nil { + log.WithError(err).Errorf("can not calculate dynamicBidSpread") + return 0, err + } + + return bidSpread, nil + } + + return 0, errors.New("incomplete dynamic spread settings or not enough data yet") +} + type Strategy struct { *bbgo.Graceful *bbgo.Notifiability @@ -78,6 +153,9 @@ type Strategy struct { // AskSpread overrides the spread setting, this spread will be used for the sell order AskSpread fixedpoint.Value `json:"askSpread,omitempty"` + // DynamicSpread enables the automatic adjustment to bid and ask spread. + DynamicSpread DynamicSpreadSettings `json:"dynamicSpread,omitempty"` + // MinProfitSpread is the minimal order price spread from the current average cost. // For long position, you will only place sell order above the price (= average cost * (1 + minProfitSpread)) // For short position, you will only place buy order below the price (= average cost * (1 - minProfitSpread)) @@ -507,6 +585,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // StrategyController s.Status = types.StrategyStatusRunning + // Setup dynamic spread + if s.DynamicSpread.Enabled { + s.DynamicSpread.DynamicBidSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, s.DynamicSpread.Window}} + s.DynamicSpread.DynamicAskSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, s.DynamicSpread.Window}} + } + s.OnSuspend(func() { s.Status = types.StrategyStatusStopped @@ -670,6 +754,21 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se 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()) + } + dynamicAskSpread, err := s.DynamicSpread.GetAskSpread() + if err == nil && dynamicAskSpread > 0 { + s.AskSpread = fixedpoint.NewFromFloat(dynamicAskSpread) + log.Infof("new ask spread: %v", s.AskSpread.Percentage()) + } + } + if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { log.WithError(err).Errorf("graceful cancel order error") }