bbgo_origin/pkg/strategy/bollmaker/dynamic_spread.go

227 lines
8.6 KiB
Go
Raw Normal View History

2022-06-02 09:55:14 +00:00
package bollmaker
import (
"github.com/pkg/errors"
"math"
2022-06-02 09:55:14 +00:00
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
type DynamicSpreadSettings struct {
AmpSpreadSettings *DynamicSpreadAmpSettings `json:"amplitude"`
WeightedBollWidthRatioSpreadSettings *DynamicSpreadBollWidthRatioSettings `json:"weightedBollWidth"`
2022-06-02 09:55:14 +00:00
// deprecated
Enabled *bool `json:"enabled"`
// deprecated
types.IntervalWindow
2022-06-02 09:55:14 +00:00
// deprecated. AskSpreadScale is used to define the ask spread range with the given percentage.
2022-06-02 09:55:14 +00:00
AskSpreadScale *bbgo.PercentageScale `json:"askSpreadScale"`
// deprecated. BidSpreadScale is used to define the bid spread range with the given percentage.
2022-06-02 09:55:14 +00:00
BidSpreadScale *bbgo.PercentageScale `json:"bidSpreadScale"`
}
2022-06-02 09:55:14 +00:00
// 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
2022-06-02 09:55:14 +00:00
}
// 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.
2022-06-02 09:55:14 +00:00
}
}
2022-06-02 09:55:14 +00:00
// 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")
}
}
2022-06-02 09:55:14 +00:00
// 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()
2022-06-02 09:55:14 +00:00
default:
return 0, errors.New("dynamic spread is not enabled")
2022-06-02 09:55:14 +00:00
}
}
type DynamicSpreadAmpSettings struct {
types.IntervalWindow
// 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
}
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])
}
}
}
func (ds *DynamicSpreadAmpSettings) update(kline types.KLine) {
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)
2022-06-02 09:55:14 +00:00
}
}
2022-06-02 09:55:14 +00:00
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())
2022-06-02 09:55:14 +00:00
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")
}
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())
2022-06-02 09:55:14 +00:00
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 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.
}