Merge pull request #617 from andycheng123/improve/bollmaker-dynamic-spread

feature: bollmaker dynamic spread
This commit is contained in:
Yo-An Lin 2022-05-27 16:55:20 +08:00 committed by GitHub
commit 424c235b43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 126 additions and 0 deletions

View File

@ -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

View File

@ -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")
}