mirror of
https://github.com/c9s/bbgo.git
synced 2024-09-20 08:11:08 +00:00
strategy: bollmaker: dynamic spread by weighted Bollinger width ratio
This commit is contained in:
parent
f17249ba89
commit
6314a31554
|
@ -60,28 +60,59 @@ exchangeStrategies:
|
|||
# Dynamic spread is an experimental feature. Use at your own risk!
|
||||
#
|
||||
# dynamicSpread enables the automatic adjustment to bid and ask spread.
|
||||
# Choose one of the scaling strategy to enable dynamicSpread:
|
||||
# - amplitude: scales by K-line amplitude
|
||||
# - weightedBollWidth: scales by weighted Bollinger band width (explained below)
|
||||
# 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 ]
|
||||
# amplitude: # delete other scaling strategy if this is defined
|
||||
# # 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 ]
|
||||
# weightedBollWidth: # delete other scaling strategy if this is defined
|
||||
# # Scale spread base on weighted Bollinger band width ratio between default and neutral bands.
|
||||
# # Given the default band: moving average bd_mid, band from bd_lower to bd_upper.
|
||||
# # And the neutral band: from bn_lower to bn_upper
|
||||
# # Set the sigmoid weighting function:
|
||||
# # - to ask spread, the weighting density function d_weight(x) is sigmoid((x - bd_mid) / (bd_upper - bd_lower))
|
||||
# # - to bid spread, the weighting density function d_weight(x) is sigmoid((bd_mid - x) / (bd_upper - bd_lower))
|
||||
# # Then calculate the weighted band width ratio by taking integral of d_weight(x) from bx_lower to bx_upper:
|
||||
# # - weighted_ratio = integral(d_weight from bn_lower to bn_upper) / integral(d_weight from bd_lower to bd_upper)
|
||||
# # - The wider neutral band get greater ratio
|
||||
# # - To ask spread, the higher neutral band get greater ratio
|
||||
# # - To bid spread, the lower neutral band get greater ratio
|
||||
# # The weighted ratio always positive, and may be greater than 1 if neutral band is wider than default band.
|
||||
# askSpreadScale:
|
||||
# byPercentage:
|
||||
# # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale
|
||||
# linear:
|
||||
# # from down to up
|
||||
# domain: [ 0.1, 0.5 ]
|
||||
# range: [ 0.001, 0.002 ]
|
||||
# bidSpreadScale:
|
||||
# byPercentage:
|
||||
# # exp means we want to use exponential scale, you can replace "exp" with "linear" for linear scale
|
||||
# linear:
|
||||
# # from down to up
|
||||
# domain: [ 0.1, 0.5 ]
|
||||
# range: [ 0.001, 0.002 ]
|
||||
|
||||
|
||||
# maxExposurePosition is the maximum position you can hold
|
||||
# +10 means you can hold 10 ETH long position by maximum
|
||||
|
|
|
@ -2,6 +2,7 @@ package bollmaker
|
|||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"math"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/indicator"
|
||||
|
@ -9,8 +10,75 @@ import (
|
|||
)
|
||||
|
||||
type DynamicSpreadSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AmpSpreadSettings *DynamicSpreadAmpSettings `json:"amplitude"`
|
||||
WeightedBollWidthRatioSpreadSettings *DynamicSpreadBollWidthRatioSettings `json:"weightedBollWidth"`
|
||||
|
||||
// deprecated
|
||||
Enabled *bool `json:"enabled"`
|
||||
|
||||
// deprecated
|
||||
types.IntervalWindow
|
||||
|
||||
// deprecated. AskSpreadScale is used to define the ask spread range with the given percentage.
|
||||
AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"`
|
||||
|
||||
// deprecated. BidSpreadScale is used to define the bid spread range with the given percentage.
|
||||
BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"`
|
||||
}
|
||||
|
||||
// Initialize dynamic spreads and preload SMAs
|
||||
func (ds *DynamicSpreadSettings) Initialize(symbol string, session *bbgo.ExchangeSession, neutralBoll, defaultBoll *indicator.BOLL) {
|
||||
switch {
|
||||
case ds.Enabled != nil && !*ds.Enabled:
|
||||
// do nothing
|
||||
case ds.AmpSpreadSettings != nil:
|
||||
ds.AmpSpreadSettings.initialize(symbol, session)
|
||||
case ds.WeightedBollWidthRatioSpreadSettings != nil:
|
||||
ds.WeightedBollWidthRatioSpreadSettings.initialize(neutralBoll, defaultBoll)
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DynamicSpreadSettings) IsEnabled() bool {
|
||||
return ds.AmpSpreadSettings != nil || ds.WeightedBollWidthRatioSpreadSettings != nil
|
||||
}
|
||||
|
||||
// Update dynamic spreads
|
||||
func (ds *DynamicSpreadSettings) Update(kline types.KLine) {
|
||||
switch {
|
||||
case ds.AmpSpreadSettings != nil:
|
||||
ds.AmpSpreadSettings.update(kline)
|
||||
case ds.WeightedBollWidthRatioSpreadSettings != nil:
|
||||
// Boll bands are updated outside of settings. Do nothing.
|
||||
default:
|
||||
// Disabled. Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
// GetAskSpread returns current ask spread
|
||||
func (ds *DynamicSpreadSettings) GetAskSpread() (askSpread float64, err error) {
|
||||
switch {
|
||||
case ds.AmpSpreadSettings != nil:
|
||||
return ds.AmpSpreadSettings.getAskSpread()
|
||||
case ds.WeightedBollWidthRatioSpreadSettings != nil:
|
||||
return ds.WeightedBollWidthRatioSpreadSettings.getAskSpread()
|
||||
default:
|
||||
return 0, errors.New("dynamic spread is not enabled")
|
||||
}
|
||||
}
|
||||
|
||||
// GetBidSpread returns current dynamic bid spread
|
||||
func (ds *DynamicSpreadSettings) GetBidSpread() (bidSpread float64, err error) {
|
||||
switch {
|
||||
case ds.AmpSpreadSettings != nil:
|
||||
return ds.AmpSpreadSettings.getBidSpread()
|
||||
case ds.WeightedBollWidthRatioSpreadSettings != nil:
|
||||
return ds.WeightedBollWidthRatioSpreadSettings.getBidSpread()
|
||||
default:
|
||||
return 0, errors.New("dynamic spread is not enabled")
|
||||
}
|
||||
}
|
||||
|
||||
type DynamicSpreadAmpSettings struct {
|
||||
types.IntervalWindow
|
||||
|
||||
// AskSpreadScale is used to define the ask spread range with the given percentage.
|
||||
|
@ -19,52 +87,40 @@ type DynamicSpreadSettings struct {
|
|||
// BidSpreadScale is used to define the bid spread range with the given percentage.
|
||||
BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"`
|
||||
|
||||
DynamicAskSpread *indicator.SMA
|
||||
DynamicBidSpread *indicator.SMA
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize dynamic spreads and preload SMAs
|
||||
func (ds *DynamicSpreadSettings) Initialize(symbol string, session *bbgo.ExchangeSession) {
|
||||
ds.DynamicBidSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}}
|
||||
ds.DynamicAskSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}}
|
||||
|
||||
func (ds *DynamicSpreadAmpSettings) initialize(symbol string, session *bbgo.ExchangeSession) {
|
||||
ds.dynamicBidSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}}
|
||||
ds.dynamicAskSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: ds.Interval, Window: ds.Window}}
|
||||
kLineStore, _ := session.MarketDataStore(symbol)
|
||||
if klines, ok := kLineStore.KLinesOfInterval(ds.Interval); ok {
|
||||
for i := 0; i < len(*klines); i++ {
|
||||
ds.Update((*klines)[i])
|
||||
ds.update((*klines)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
func (ds *DynamicSpreadAmpSettings) update(kline types.KLine) {
|
||||
ampl := (kline.GetHigh().Float64() - kline.GetLow().Float64()) / kline.GetOpen().Float64()
|
||||
|
||||
if ds.AskSpreadScale != nil && ds.DynamicAskSpread.Length() >= ds.Window {
|
||||
askSpread, err = ds.AskSpreadScale.Scale(ds.DynamicAskSpread.Last())
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DynamicSpreadAmpSettings) getAskSpread() (askSpread float64, err error) {
|
||||
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
|
||||
|
@ -76,14 +132,9 @@ func (ds *DynamicSpreadSettings) GetAskSpread() (askSpread float64, err error) {
|
|||
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())
|
||||
func (ds *DynamicSpreadAmpSettings) getBidSpread() (bidSpread float64, err error) {
|
||||
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
|
||||
|
@ -94,3 +145,82 @@ func (ds *DynamicSpreadSettings) GetBidSpread() (bidSpread float64, err error) {
|
|||
|
||||
return 0, errors.New("incomplete dynamic spread settings or not enough data yet")
|
||||
}
|
||||
|
||||
type DynamicSpreadBollWidthRatioSettings struct {
|
||||
// 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"`
|
||||
|
||||
neutralBoll *indicator.BOLL
|
||||
defaultBoll *indicator.BOLL
|
||||
}
|
||||
|
||||
func (ds *DynamicSpreadBollWidthRatioSettings) initialize(neutralBoll, defaultBoll *indicator.BOLL) {
|
||||
ds.neutralBoll = neutralBoll
|
||||
ds.defaultBoll = defaultBoll
|
||||
}
|
||||
|
||||
func (ds *DynamicSpreadBollWidthRatioSettings) getAskSpread() (askSpread float64, err error) {
|
||||
askSpread, err = ds.AskSpreadScale.Scale(ds.getWeightedBBWidthRatio(true))
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("can not calculate dynamicAskSpread")
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return askSpread, nil
|
||||
}
|
||||
|
||||
func (ds *DynamicSpreadBollWidthRatioSettings) getBidSpread() (bidSpread float64, err error) {
|
||||
bidSpread, err = ds.BidSpreadScale.Scale(ds.getWeightedBBWidthRatio(false))
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("can not calculate dynamicAskSpread")
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return bidSpread, nil
|
||||
}
|
||||
|
||||
func (ds *DynamicSpreadBollWidthRatioSettings) getWeightedBBWidthRatio(positiveSigmoid bool) float64 {
|
||||
// Weight the width of Boll bands with sigmoid function and calculate the ratio after integral.
|
||||
//
|
||||
// Given the default band: moving average default_BB_mid, band from default_BB_lower to default_BB_upper.
|
||||
// And the neutral band: from neutral_BB_lower to neutral_BB_upper.
|
||||
//
|
||||
// 1 x - default_BB_mid
|
||||
// sigmoid weighting function f(y) = ------------- where y = --------------------
|
||||
// 1 + exp(-y) default_BB_width
|
||||
// Set the sigmoid weighting function:
|
||||
// - to ask spread, the weighting density function d_weight(x) is sigmoid((x - default_BB_mid) / (default_BB_upper - default_BB_lower))
|
||||
// - to bid spread, the weighting density function d_weight(x) is sigmoid((default_BB_mid - x) / (default_BB_upper - default_BB_lower))
|
||||
//
|
||||
// Then calculate the weighted band width ratio by taking integral of d_weight(x) from bx_lower to bx_upper:
|
||||
// infinite integral of ask spread sigmoid weighting density function F(y) = ln(1 + exp(y))
|
||||
// infinite integral of bid spread sigmoid weighting density function F(y) = y - ln(1 + exp(y))
|
||||
// Note that we've rescaled the sigmoid function to fit default BB,
|
||||
// the weighted default BB width is always calculated by integral(f of y from -1 to 1) = F(1) - F(-1)
|
||||
// F(y_upper) - F(y_lower) F(y_upper) - F(y_lower)
|
||||
// weighted ratio = ------------------------- = -------------------------
|
||||
// F(1) - F(-1) 1
|
||||
// where y_upper = (neutral_BB_upper - default_BB_mid) / default_BB_width
|
||||
// y_lower = (neutral_BB_lower - default_BB_mid) / default_BB_width
|
||||
// - The wider neutral band get greater ratio
|
||||
// - To ask spread, the higher neutral band get greater ratio
|
||||
// - To bid spread, the lower neutral band get greater ratio
|
||||
|
||||
defaultMid := ds.defaultBoll.SMA.Last()
|
||||
defaultWidth := ds.defaultBoll.UpBand.Last() - ds.defaultBoll.DownBand.Last()
|
||||
yUpper := (ds.neutralBoll.UpBand.Last() - defaultMid) / defaultWidth
|
||||
yLower := (ds.neutralBoll.DownBand.Last() - defaultMid) / defaultWidth
|
||||
var weightedUpper, weightedLower float64
|
||||
if positiveSigmoid {
|
||||
weightedUpper = math.Log(1 + math.Pow(math.E, yUpper))
|
||||
weightedLower = math.Log(1 + math.Pow(math.E, yLower))
|
||||
} else {
|
||||
weightedUpper = yUpper - math.Log(1+math.Pow(math.E, yUpper))
|
||||
weightedLower = yLower - math.Log(1+math.Pow(math.E, yLower))
|
||||
}
|
||||
// The weighted ratio always positive, and may be greater than 1 if neutral band is wider than default band.
|
||||
return (weightedUpper - weightedLower) / 1.
|
||||
}
|
||||
|
|
|
@ -435,12 +435,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
// StrategyController
|
||||
s.Status = types.StrategyStatusRunning
|
||||
|
||||
s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth)
|
||||
s.defaultBoll = s.StandardIndicatorSet.BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth)
|
||||
|
||||
// Setup dynamic spread
|
||||
if s.DynamicSpread.Enabled {
|
||||
if s.DynamicSpread.IsEnabled() {
|
||||
if s.DynamicSpread.Interval == "" {
|
||||
s.DynamicSpread.Interval = s.Interval
|
||||
}
|
||||
s.DynamicSpread.Initialize(s.Symbol, s.session)
|
||||
s.DynamicSpread.Initialize(s.Symbol, s.session, s.neutralBoll, s.defaultBoll)
|
||||
}
|
||||
|
||||
if s.DisableShort {
|
||||
|
@ -463,9 +466,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
s.ShadowProtectionRatio = fixedpoint.NewFromFloat(0.01)
|
||||
}
|
||||
|
||||
s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth)
|
||||
s.defaultBoll = s.StandardIndicatorSet.BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth)
|
||||
|
||||
// calculate group id for orders
|
||||
instanceID := s.InstanceID()
|
||||
s.groupID = util.FNV32(instanceID)
|
||||
|
@ -538,7 +538,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
}
|
||||
|
||||
// Update spreads with dynamic spread
|
||||
if s.DynamicSpread.Enabled {
|
||||
if s.DynamicSpread.IsEnabled() {
|
||||
s.DynamicSpread.Update(kline)
|
||||
dynamicBidSpread, err := s.DynamicSpread.GetBidSpread()
|
||||
if err == nil && dynamicBidSpread > 0 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user