@@ -2,69 +2,125 @@ package bollmaker
2
2
3
3
import (
4
4
"github.com/pkg/errors"
5
+ "math"
5
6
6
7
"github.com/c9s/bbgo/pkg/bbgo"
7
8
"github.com/c9s/bbgo/pkg/indicator"
8
9
"github.com/c9s/bbgo/pkg/types"
9
10
)
10
11
11
12
type DynamicSpreadSettings struct {
12
- Enabled bool `json:"enabled"`
13
+ AmpSpreadSettings * DynamicSpreadAmpSettings `json:"amplitude"`
14
+ WeightedBollWidthRatioSpreadSettings * DynamicSpreadBollWidthRatioSettings `json:"weightedBollWidth"`
13
15
16
+ // deprecated
17
+ Enabled * bool `json:"enabled"`
18
+
19
+ // deprecated
14
20
types.IntervalWindow
15
21
16
- // AskSpreadScale is used to define the ask spread range with the given percentage.
22
+ // deprecated. AskSpreadScale is used to define the ask spread range with the given percentage.
17
23
AskSpreadScale * bbgo.PercentageScale `json:"askSpreadScale"`
18
24
19
- // BidSpreadScale is used to define the bid spread range with the given percentage.
25
+ // deprecated. BidSpreadScale is used to define the bid spread range with the given percentage.
20
26
BidSpreadScale * bbgo.PercentageScale `json:"bidSpreadScale"`
27
+ }
21
28
22
- DynamicAskSpread * indicator.SMA
23
- DynamicBidSpread * indicator.SMA
29
+ // Initialize dynamic spreads and preload SMAs
30
+ func (ds * DynamicSpreadSettings ) Initialize (symbol string , session * bbgo.ExchangeSession , neutralBoll , defaultBoll * indicator.BOLL ) {
31
+ switch {
32
+ case ds .Enabled != nil && ! * ds .Enabled :
33
+ // do nothing
34
+ case ds .AmpSpreadSettings != nil :
35
+ ds .AmpSpreadSettings .initialize (symbol , session )
36
+ case ds .WeightedBollWidthRatioSpreadSettings != nil :
37
+ ds .WeightedBollWidthRatioSpreadSettings .initialize (neutralBoll , defaultBoll )
38
+ }
39
+ }
40
+
41
+ func (ds * DynamicSpreadSettings ) IsEnabled () bool {
42
+ return ds .AmpSpreadSettings != nil || ds .WeightedBollWidthRatioSpreadSettings != nil
24
43
}
25
44
26
45
// Update dynamic spreads
27
46
func (ds * DynamicSpreadSettings ) Update (kline types.KLine ) {
28
- if ! ds .Enabled {
29
- return
47
+ switch {
48
+ case ds .AmpSpreadSettings != nil :
49
+ ds .AmpSpreadSettings .update (kline )
50
+ case ds .WeightedBollWidthRatioSpreadSettings != nil :
51
+ // Boll bands are updated outside of settings. Do nothing.
52
+ default :
53
+ // Disabled. Do nothing.
30
54
}
55
+ }
31
56
32
- ampl := (kline .GetHigh ().Float64 () - kline .GetLow ().Float64 ()) / kline .GetOpen ().Float64 ()
57
+ // GetAskSpread returns current ask spread
58
+ func (ds * DynamicSpreadSettings ) GetAskSpread () (askSpread float64 , err error ) {
59
+ switch {
60
+ case ds .AmpSpreadSettings != nil :
61
+ return ds .AmpSpreadSettings .getAskSpread ()
62
+ case ds .WeightedBollWidthRatioSpreadSettings != nil :
63
+ return ds .WeightedBollWidthRatioSpreadSettings .getAskSpread ()
64
+ default :
65
+ return 0 , errors .New ("dynamic spread is not enabled" )
66
+ }
67
+ }
33
68
34
- switch kline . Direction () {
35
- case types . DirectionUp :
36
- ds . DynamicAskSpread . Update ( ampl )
37
- ds .DynamicBidSpread . Update ( 0 )
38
- case types . DirectionDown :
39
- ds .DynamicBidSpread . Update ( ampl )
40
- ds .DynamicAskSpread . Update ( 0 )
69
+ // GetBidSpread returns current dynamic bid spread
70
+ func ( ds * DynamicSpreadSettings ) GetBidSpread () ( bidSpread float64 , err error ) {
71
+ switch {
72
+ case ds .AmpSpreadSettings != nil :
73
+ return ds . AmpSpreadSettings . getBidSpread ()
74
+ case ds .WeightedBollWidthRatioSpreadSettings != nil :
75
+ return ds .WeightedBollWidthRatioSpreadSettings . getBidSpread ( )
41
76
default :
42
- ds .DynamicAskSpread .Update (0 )
43
- ds .DynamicBidSpread .Update (0 )
77
+ return 0 , errors .New ("dynamic spread is not enabled" )
44
78
}
45
79
}
46
80
47
- // Initialize dynamic spreads and preload SMAs
48
- func (ds * DynamicSpreadSettings ) Initialize (symbol string , session * bbgo.ExchangeSession ) {
49
- ds .DynamicBidSpread = & indicator.SMA {IntervalWindow : types.IntervalWindow {Interval : ds .Interval , Window : ds .Window }}
50
- ds .DynamicAskSpread = & indicator.SMA {IntervalWindow : types.IntervalWindow {Interval : ds .Interval , Window : ds .Window }}
81
+ type DynamicSpreadAmpSettings struct {
82
+ types.IntervalWindow
51
83
84
+ // AskSpreadScale is used to define the ask spread range with the given percentage.
85
+ AskSpreadScale * bbgo.PercentageScale `json:"askSpreadScale"`
86
+
87
+ // BidSpreadScale is used to define the bid spread range with the given percentage.
88
+ BidSpreadScale * bbgo.PercentageScale `json:"bidSpreadScale"`
89
+
90
+ dynamicAskSpread * indicator.SMA
91
+ dynamicBidSpread * indicator.SMA
92
+ }
93
+
94
+ func (ds * DynamicSpreadAmpSettings ) initialize (symbol string , session * bbgo.ExchangeSession ) {
95
+ ds .dynamicBidSpread = & indicator.SMA {IntervalWindow : types.IntervalWindow {Interval : ds .Interval , Window : ds .Window }}
96
+ ds .dynamicAskSpread = & indicator.SMA {IntervalWindow : types.IntervalWindow {Interval : ds .Interval , Window : ds .Window }}
52
97
kLineStore , _ := session .MarketDataStore (symbol )
53
98
if klines , ok := kLineStore .KLinesOfInterval (ds .Interval ); ok {
54
99
for i := 0 ; i < len (* klines ); i ++ {
55
- ds .Update ((* klines )[i ])
100
+ ds .update ((* klines )[i ])
56
101
}
57
102
}
58
103
}
59
104
60
- // GetAskSpread returns current ask spread
61
- func (ds * DynamicSpreadSettings ) GetAskSpread () (askSpread float64 , err error ) {
62
- if ! ds .Enabled {
63
- return 0 , errors .New ("dynamic spread is not enabled" )
105
+ func (ds * DynamicSpreadAmpSettings ) update (kline types.KLine ) {
106
+ ampl := (kline .GetHigh ().Float64 () - kline .GetLow ().Float64 ()) / kline .GetOpen ().Float64 ()
107
+
108
+ switch kline .Direction () {
109
+ case types .DirectionUp :
110
+ ds .dynamicAskSpread .Update (ampl )
111
+ ds .dynamicBidSpread .Update (0 )
112
+ case types .DirectionDown :
113
+ ds .dynamicBidSpread .Update (ampl )
114
+ ds .dynamicAskSpread .Update (0 )
115
+ default :
116
+ ds .dynamicAskSpread .Update (0 )
117
+ ds .dynamicBidSpread .Update (0 )
64
118
}
119
+ }
65
120
66
- if ds .AskSpreadScale != nil && ds .DynamicAskSpread .Length () >= ds .Window {
67
- askSpread , err = ds .AskSpreadScale .Scale (ds .DynamicAskSpread .Last ())
121
+ func (ds * DynamicSpreadAmpSettings ) getAskSpread () (askSpread float64 , err error ) {
122
+ if ds .AskSpreadScale != nil && ds .dynamicAskSpread .Length () >= ds .Window {
123
+ askSpread , err = ds .AskSpreadScale .Scale (ds .dynamicAskSpread .Last ())
68
124
if err != nil {
69
125
log .WithError (err ).Errorf ("can not calculate dynamicAskSpread" )
70
126
return 0 , err
@@ -76,14 +132,9 @@ func (ds *DynamicSpreadSettings) GetAskSpread() (askSpread float64, err error) {
76
132
return 0 , errors .New ("incomplete dynamic spread settings or not enough data yet" )
77
133
}
78
134
79
- // GetBidSpread returns current dynamic bid spread
80
- func (ds * DynamicSpreadSettings ) GetBidSpread () (bidSpread float64 , err error ) {
81
- if ! ds .Enabled {
82
- return 0 , errors .New ("dynamic spread is not enabled" )
83
- }
84
-
85
- if ds .BidSpreadScale != nil && ds .DynamicBidSpread .Length () >= ds .Window {
86
- bidSpread , err = ds .BidSpreadScale .Scale (ds .DynamicBidSpread .Last ())
135
+ func (ds * DynamicSpreadAmpSettings ) getBidSpread () (bidSpread float64 , err error ) {
136
+ if ds .BidSpreadScale != nil && ds .dynamicBidSpread .Length () >= ds .Window {
137
+ bidSpread , err = ds .BidSpreadScale .Scale (ds .dynamicBidSpread .Last ())
87
138
if err != nil {
88
139
log .WithError (err ).Errorf ("can not calculate dynamicBidSpread" )
89
140
return 0 , err
@@ -94,3 +145,82 @@ func (ds *DynamicSpreadSettings) GetBidSpread() (bidSpread float64, err error) {
94
145
95
146
return 0 , errors .New ("incomplete dynamic spread settings or not enough data yet" )
96
147
}
148
+
149
+ type DynamicSpreadBollWidthRatioSettings struct {
150
+ // AskSpreadScale is used to define the ask spread range with the given percentage.
151
+ AskSpreadScale * bbgo.PercentageScale `json:"askSpreadScale"`
152
+
153
+ // BidSpreadScale is used to define the bid spread range with the given percentage.
154
+ BidSpreadScale * bbgo.PercentageScale `json:"bidSpreadScale"`
155
+
156
+ neutralBoll * indicator.BOLL
157
+ defaultBoll * indicator.BOLL
158
+ }
159
+
160
+ func (ds * DynamicSpreadBollWidthRatioSettings ) initialize (neutralBoll , defaultBoll * indicator.BOLL ) {
161
+ ds .neutralBoll = neutralBoll
162
+ ds .defaultBoll = defaultBoll
163
+ }
164
+
165
+ func (ds * DynamicSpreadBollWidthRatioSettings ) getAskSpread () (askSpread float64 , err error ) {
166
+ askSpread , err = ds .AskSpreadScale .Scale (ds .getWeightedBBWidthRatio (true ))
167
+ if err != nil {
168
+ log .WithError (err ).Errorf ("can not calculate dynamicAskSpread" )
169
+ return 0 , err
170
+ }
171
+
172
+ return askSpread , nil
173
+ }
174
+
175
+ func (ds * DynamicSpreadBollWidthRatioSettings ) getBidSpread () (bidSpread float64 , err error ) {
176
+ bidSpread , err = ds .BidSpreadScale .Scale (ds .getWeightedBBWidthRatio (false ))
177
+ if err != nil {
178
+ log .WithError (err ).Errorf ("can not calculate dynamicAskSpread" )
179
+ return 0 , err
180
+ }
181
+
182
+ return bidSpread , nil
183
+ }
184
+
185
+ func (ds * DynamicSpreadBollWidthRatioSettings ) getWeightedBBWidthRatio (positiveSigmoid bool ) float64 {
186
+ // Weight the width of Boll bands with sigmoid function and calculate the ratio after integral.
187
+ //
188
+ // Given the default band: moving average default_BB_mid, band from default_BB_lower to default_BB_upper.
189
+ // And the neutral band: from neutral_BB_lower to neutral_BB_upper.
190
+ //
191
+ // 1 x - default_BB_mid
192
+ // sigmoid weighting function f(y) = ------------- where y = --------------------
193
+ // 1 + exp(-y) default_BB_width
194
+ // Set the sigmoid weighting function:
195
+ // - to ask spread, the weighting density function d_weight(x) is sigmoid((x - default_BB_mid) / (default_BB_upper - default_BB_lower))
196
+ // - to bid spread, the weighting density function d_weight(x) is sigmoid((default_BB_mid - x) / (default_BB_upper - default_BB_lower))
197
+ //
198
+ // Then calculate the weighted band width ratio by taking integral of d_weight(x) from bx_lower to bx_upper:
199
+ // infinite integral of ask spread sigmoid weighting density function F(y) = ln(1 + exp(y))
200
+ // infinite integral of bid spread sigmoid weighting density function F(y) = y - ln(1 + exp(y))
201
+ // Note that we've rescaled the sigmoid function to fit default BB,
202
+ // the weighted default BB width is always calculated by integral(f of y from -1 to 1) = F(1) - F(-1)
203
+ // F(y_upper) - F(y_lower) F(y_upper) - F(y_lower)
204
+ // weighted ratio = ------------------------- = -------------------------
205
+ // F(1) - F(-1) 1
206
+ // where y_upper = (neutral_BB_upper - default_BB_mid) / default_BB_width
207
+ // y_lower = (neutral_BB_lower - default_BB_mid) / default_BB_width
208
+ // - The wider neutral band get greater ratio
209
+ // - To ask spread, the higher neutral band get greater ratio
210
+ // - To bid spread, the lower neutral band get greater ratio
211
+
212
+ defaultMid := ds .defaultBoll .SMA .Last ()
213
+ defaultWidth := ds .defaultBoll .UpBand .Last () - ds .defaultBoll .DownBand .Last ()
214
+ yUpper := (ds .neutralBoll .UpBand .Last () - defaultMid ) / defaultWidth
215
+ yLower := (ds .neutralBoll .DownBand .Last () - defaultMid ) / defaultWidth
216
+ var weightedUpper , weightedLower float64
217
+ if positiveSigmoid {
218
+ weightedUpper = math .Log (1 + math .Pow (math .E , yUpper ))
219
+ weightedLower = math .Log (1 + math .Pow (math .E , yLower ))
220
+ } else {
221
+ weightedUpper = yUpper - math .Log (1 + math .Pow (math .E , yUpper ))
222
+ weightedLower = yLower - math .Log (1 + math .Pow (math .E , yLower ))
223
+ }
224
+ // The weighted ratio always positive, and may be greater than 1 if neutral band is wider than default band.
225
+ return (weightedUpper - weightedLower ) / 1.
226
+ }
0 commit comments