2022-06-02 09:55:14 +00:00
package bollmaker
import (
"github.com/pkg/errors"
2022-08-29 05:38:42 +00:00
"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 {
2022-08-29 05:38:42 +00:00
AmpSpreadSettings * DynamicSpreadAmpSettings ` json:"amplitude" `
WeightedBollWidthRatioSpreadSettings * DynamicSpreadBollWidthRatioSettings ` json:"weightedBollWidth" `
2022-06-02 09:55:14 +00:00
2022-08-29 05:38:42 +00:00
// deprecated
Enabled * bool ` json:"enabled" `
// deprecated
2022-08-25 05:44:38 +00:00
types . IntervalWindow
2022-06-02 09:55:14 +00:00
2022-08-29 05:38:42 +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" `
2022-08-29 05:38:42 +00:00
// 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-08-29 05:38:42 +00:00
}
2022-06-02 09:55:14 +00:00
2022-08-29 05:38:42 +00:00
// Initialize dynamic spreads and preload SMAs
func ( ds * DynamicSpreadSettings ) Initialize ( symbol string , session * bbgo . ExchangeSession , neutralBoll , defaultBoll * indicator . BOLL ) {
switch {
2022-09-11 16:27:11 +00:00
case ds . AmpSpreadSettings != nil :
ds . AmpSpreadSettings . initialize ( symbol , session )
case ds . WeightedBollWidthRatioSpreadSettings != nil :
ds . WeightedBollWidthRatioSpreadSettings . initialize ( neutralBoll , defaultBoll )
case ds . Enabled != nil && * ds . Enabled :
2022-09-11 12:56:43 +00:00
// backward compatibility
ds . AmpSpreadSettings = & DynamicSpreadAmpSettings {
IntervalWindow : ds . IntervalWindow ,
AskSpreadScale : ds . AskSpreadScale ,
BidSpreadScale : ds . BidSpreadScale ,
}
ds . AmpSpreadSettings . initialize ( symbol , session )
2022-08-29 05:38:42 +00:00
}
}
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 ) {
2022-08-29 05:38:42 +00:00
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-08-29 05:38:42 +00:00
}
2022-06-02 09:55:14 +00:00
2022-08-29 05:38:42 +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
2022-08-29 05:38:42 +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 :
2022-08-29 05:38:42 +00:00
return 0 , errors . New ( "dynamic spread is not enabled" )
2022-06-02 09:55:14 +00:00
}
}
2022-08-29 05:38:42 +00:00
type DynamicSpreadAmpSettings struct {
types . IntervalWindow
2022-08-25 05:44:38 +00:00
2022-08-29 05:38:42 +00:00
// 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 } }
2022-08-25 05:44:38 +00:00
kLineStore , _ := session . MarketDataStore ( symbol )
if klines , ok := kLineStore . KLinesOfInterval ( ds . Interval ) ; ok {
for i := 0 ; i < len ( * klines ) ; i ++ {
2022-08-29 05:38:42 +00:00
ds . update ( ( * klines ) [ i ] )
2022-08-25 05:44:38 +00:00
}
}
}
2022-08-29 05:38:42 +00:00
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-08-29 05:38:42 +00:00
}
2022-06-02 09:55:14 +00:00
2022-08-29 05:38:42 +00:00
func ( ds * DynamicSpreadAmpSettings ) getAskSpread ( ) ( askSpread float64 , err error ) {
if ds . AskSpreadScale != nil && ds . dynamicAskSpread . Length ( ) >= ds . Window {
2023-05-31 11:35:44 +00:00
askSpread , err = ds . AskSpreadScale . Scale ( ds . dynamicAskSpread . Last ( 0 ) )
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" )
}
2022-08-29 05:38:42 +00:00
func ( ds * DynamicSpreadAmpSettings ) getBidSpread ( ) ( bidSpread float64 , err error ) {
if ds . BidSpreadScale != nil && ds . dynamicBidSpread . Length ( ) >= ds . Window {
2023-05-31 11:35:44 +00:00
bidSpread , err = ds . BidSpreadScale . Scale ( ds . dynamicBidSpread . Last ( 0 ) )
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" )
}
2022-08-29 05:38:42 +00:00
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" `
2022-08-30 14:39:03 +00:00
// Sensitivity factor of the weighting function: 1 / (1 + exp(-(x - mid) * sensitivity / width))
// A positive number. The greater factor, the sharper weighting function. Default set to 1.0 .
Sensitivity float64 ` json:"sensitivity" `
2022-08-29 05:38:42 +00:00
neutralBoll * indicator . BOLL
defaultBoll * indicator . BOLL
}
func ( ds * DynamicSpreadBollWidthRatioSettings ) initialize ( neutralBoll , defaultBoll * indicator . BOLL ) {
ds . neutralBoll = neutralBoll
ds . defaultBoll = defaultBoll
2022-08-30 14:39:03 +00:00
if ds . Sensitivity <= 0. {
ds . Sensitivity = 1.
}
2022-08-29 05:38:42 +00:00
}
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.
2022-08-30 14:39:03 +00:00
// And a sensitivity factor alpha, which is a positive constant.
//
// width of default BB w = default_BB_upper - default_BB_lower
2022-08-29 05:38:42 +00:00
//
// 1 x - default_BB_mid
// sigmoid weighting function f(y) = ------------- where y = --------------------
2022-08-30 14:39:03 +00:00
// 1 + exp(-y) w / alpha
2022-08-29 05:38:42 +00:00
// Set the sigmoid weighting function:
2022-08-30 14:39:03 +00:00
// - To ask spread, the weighting density function d_weight(x) is sigmoid((x - default_BB_mid) / (w / alpha))
// - To bid spread, the weighting density function d_weight(x) is sigmoid((default_BB_mid - x) / (w / alpha))
// - The higher sensitivity factor alpha, the sharper weighting function.
2022-08-29 05:38:42 +00:00
//
2022-08-30 14:39:03 +00:00
// Then calculate the weighted band width ratio by taking integral of d_weight(x) from neutral_BB_lower to neutral_BB_upper:
// infinite integral of ask spread sigmoid weighting density function F(x) = (w / alpha) * ln(exp(x / (w / alpha)) + exp(default_BB_mid / (w / alpha)))
// infinite integral of bid spread sigmoid weighting density function F(x) = x - (w / alpha) * ln(exp(x / (w / alpha)) + exp(default_BB_mid / (w / alpha)))
2022-08-29 05:38:42 +00:00
// Note that we've rescaled the sigmoid function to fit default BB,
2022-08-30 14:39:03 +00:00
// the weighted default BB width is always calculated by integral(f of x from default_BB_lower to default_BB_upper)
// F(neutral_BB_upper) - F(neutral_BB_lower)
// weighted ratio = -------------------------------------------
// F(default_BB_upper) - F(default_BB_lower)
2022-08-29 05:38:42 +00:00
// - 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
2023-05-31 11:35:44 +00:00
defaultMid := ds . defaultBoll . SMA . Last ( 0 )
defaultUpper := ds . defaultBoll . UpBand . Last ( 0 )
defaultLower := ds . defaultBoll . DownBand . Last ( 0 )
2022-08-30 14:39:03 +00:00
defaultWidth := defaultUpper - defaultLower
2023-05-31 11:35:44 +00:00
neutralUpper := ds . neutralBoll . UpBand . Last ( 0 )
neutralLower := ds . neutralBoll . DownBand . Last ( 0 )
2022-08-30 14:39:03 +00:00
factor := defaultWidth / ds . Sensitivity
var weightedUpper , weightedLower , weightedDivUpper , weightedDivLower float64
2022-08-29 05:38:42 +00:00
if positiveSigmoid {
2022-08-30 14:39:03 +00:00
weightedUpper = factor * math . Log ( math . Exp ( neutralUpper / factor ) + math . Exp ( defaultMid / factor ) )
weightedLower = factor * math . Log ( math . Exp ( neutralLower / factor ) + math . Exp ( defaultMid / factor ) )
weightedDivUpper = factor * math . Log ( math . Exp ( defaultUpper / factor ) + math . Exp ( defaultMid / factor ) )
weightedDivLower = factor * math . Log ( math . Exp ( defaultLower / factor ) + math . Exp ( defaultMid / factor ) )
2022-08-29 05:38:42 +00:00
} else {
2022-08-30 14:39:03 +00:00
weightedUpper = neutralUpper - factor * math . Log ( math . Exp ( neutralUpper / factor ) + math . Exp ( defaultMid / factor ) )
weightedLower = neutralLower - factor * math . Log ( math . Exp ( neutralLower / factor ) + math . Exp ( defaultMid / factor ) )
weightedDivUpper = defaultUpper - factor * math . Log ( math . Exp ( defaultUpper / factor ) + math . Exp ( defaultMid / factor ) )
weightedDivLower = defaultLower - factor * math . Log ( math . Exp ( defaultLower / factor ) + math . Exp ( defaultMid / factor ) )
2022-08-29 05:38:42 +00:00
}
2022-08-30 14:39:03 +00:00
return ( weightedUpper - weightedLower ) / ( weightedDivUpper - weightedDivLower )
2022-08-29 05:38:42 +00:00
}