forked from stellar-deprecated/kelp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbalancedLevelProvider.go
205 lines (180 loc) · 8.64 KB
/
balancedLevelProvider.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package plugins
import (
"log"
"math/rand"
"time"
"github.com/lightyeario/kelp/api"
"github.com/lightyeario/kelp/model"
"github.com/lightyeario/kelp/support/utils"
"github.com/stellar/go/clients/horizon"
)
// balancedLevelProvider provides levels based on an exponential curve wrt. the number of assets held in the account.
// This strategy does not allow using the balance of a single asset for more strategies other than this one because
// that would require building in some trade tracking along with asset balance tracking for this strategy. The support
// for this can always be added later.
type balancedLevelProvider struct {
spread float64
useMaxQuoteInTargetAmountCalc bool // else use maxBase
minAmountSpread float64 // % that we take off the top of each amount order size which effectively serves as our spread when multiple levels are consumed
maxAmountSpread float64 // % that we take off the top of each amount order size which effectively serves as our spread when multiple levels are consumed
maxLevels int16
levelDensity float64
ensureFirstNLevels int16 // always adds the first N levels, meaningless if levelDensity = 1.0
minAmountCarryoverSpread float64 // the minimum spread % we take off the amountCarryover before placing the orders
maxAmountCarryoverSpread float64 // the maximum spread % we take off the amountCarryover before placing the orders
carryoverInclusionProbability float64 // probability of including the carryover at a level that will be added
virtualBalanceBase float64 // virtual balance to use so we can smoothen out the curve
virtualBalanceQuote float64 // virtual balance to use so we can smoothen out the curve
// precomputed before construction
randGen *rand.Rand
}
// ensure it implements LevelProvider
var _ api.LevelProvider = &balancedLevelProvider{}
// makeBalancedLevelProvider is the factory method
func makeBalancedLevelProvider(
spread float64,
useMaxQuoteInTargetAmountCalc bool,
minAmountSpread float64,
maxAmountSpread float64,
maxLevels int16,
levelDensity float64,
ensureFirstNLevels int16,
minAmountCarryoverSpread float64,
maxAmountCarryoverSpread float64,
carryoverInclusionProbability float64,
virtualBalanceBase float64,
virtualBalanceQuote float64,
) api.LevelProvider {
if minAmountSpread <= 0 {
log.Fatalf("minAmountSpread (%.7f) needs to be > 0 for the algorithm to work sustainably\n", minAmountSpread)
}
validateSpread(minAmountSpread)
validateSpread(maxAmountSpread)
if minAmountSpread > maxAmountSpread {
log.Fatalf("minAmountSpread (%.7f) needs to be <= maxAmountSpread (%.7f)\n", minAmountSpread, maxAmountSpread)
}
validateSpread(minAmountCarryoverSpread)
validateSpread(maxAmountCarryoverSpread)
if minAmountCarryoverSpread > maxAmountCarryoverSpread {
log.Fatalf("minAmountCarryoverSpread (%.7f) needs to be <= maxAmountCarryoverSpread (%.7f)\n", minAmountCarryoverSpread, maxAmountCarryoverSpread)
}
// carryoverInclusionProbability is a value between 0 and 1
validateSpread(carryoverInclusionProbability)
randGen := rand.New(rand.NewSource(time.Now().UnixNano()))
return &balancedLevelProvider{
spread: spread,
useMaxQuoteInTargetAmountCalc: useMaxQuoteInTargetAmountCalc,
minAmountSpread: minAmountSpread,
maxAmountSpread: maxAmountSpread,
maxLevels: maxLevels,
levelDensity: levelDensity,
ensureFirstNLevels: ensureFirstNLevels,
minAmountCarryoverSpread: minAmountCarryoverSpread,
maxAmountCarryoverSpread: maxAmountCarryoverSpread,
carryoverInclusionProbability: carryoverInclusionProbability,
virtualBalanceBase: virtualBalanceBase,
virtualBalanceQuote: virtualBalanceQuote,
randGen: randGen,
}
}
func validateSpread(spread float64) {
if spread > 1.0 || spread < 0.0 {
log.Fatalf("spread values need to be inclusively between 0 and 1: %.7f\n", spread)
}
}
// GetLevels impl.
func (p *balancedLevelProvider) GetLevels(maxAssetBase float64, maxAssetQuote float64, buyingAOffers []horizon.Offer, sellingAOffers []horizon.Offer) ([]api.Level, error) {
_maxAssetBase := maxAssetBase + p.virtualBalanceBase
_maxAssetQuote := maxAssetQuote + p.virtualBalanceQuote
// represents the amount that was meant to be included in a previous level that we excluded because we skipped that level
amountCarryover := 0.0
levels := []api.Level{}
for i := int16(0); i < p.maxLevels; i++ {
level, e := p.getLevel(_maxAssetBase, _maxAssetQuote)
if e != nil {
return nil, e
}
// always update _maxAssetBase and _maxAssetQuote to account for the level we just calculated, ensures price moves across levels regardless of inclusion of prior levels
_maxAssetBase, _maxAssetQuote = updateAssetBalances(level, p.useMaxQuoteInTargetAmountCalc, _maxAssetBase, _maxAssetQuote)
// always take a spread off the amountCarryover
amountCarryoverSpread := p.getRandomSpread(p.minAmountCarryoverSpread, p.maxAmountCarryoverSpread)
amountCarryover *= (1 - amountCarryoverSpread)
if !p.shouldIncludeLevel(i) {
// accummulate targetAmount into amountCarryover
amountCarryover += level.Amount.AsFloat()
continue
}
if p.shouldIncludeCarryover() {
level, amountCarryover = p.computeNewLevelWithCarryover(level, amountCarryover)
}
levels = append(levels, level)
}
return levels, nil
}
func (p *balancedLevelProvider) computeNewLevelWithCarryover(level api.Level, amountCarryover float64) (api.Level, float64) {
// include a partial amount of the carryover
amountCarryoverToInclude := p.randGen.Float64() * amountCarryover
// update amountCarryover to reflect inclusion in the level
amountCarryover -= amountCarryoverToInclude
// include the amountCarryover we computed, price of the level remains unchanged
level = api.Level{
Price: level.Price,
Amount: *model.NumberFromFloat(level.Amount.AsFloat()+amountCarryoverToInclude, level.Amount.Precision()),
}
return level, amountCarryover
}
func updateAssetBalances(level api.Level, useMaxQuoteInTargetAmountCalc bool, maxAssetBase float64, maxAssetQuote float64) (float64, float64) {
// targetPrice is always quote/base
var baseDecreased float64
var quoteIncreased float64
if useMaxQuoteInTargetAmountCalc {
// targetAmount is in quote so divide by price (quote/base) to give base
baseDecreased = level.Amount.AsFloat() / level.Price.AsFloat()
// targetAmount is in quote so use directly
quoteIncreased = level.Amount.AsFloat()
} else {
// targetAmount is in base so use directly
baseDecreased = level.Amount.AsFloat()
// targetAmount is in base so multiply by price (quote/base) to give quote
quoteIncreased = level.Amount.AsFloat() * level.Price.AsFloat()
}
// subtract because we had to sell that many units to reach the next level
newMaxAssetBase := maxAssetBase - baseDecreased
// add because we had to buy these many units to reach the next level
newMaxAssetQuote := maxAssetQuote + quoteIncreased
return newMaxAssetBase, newMaxAssetQuote
}
func (p *balancedLevelProvider) shouldIncludeLevel(levelIndex int16) bool {
includeLevelUsingProbability := p.randGen.Float64() < p.levelDensity
includeLevelUsingConstraint := levelIndex < p.ensureFirstNLevels
return includeLevelUsingConstraint || includeLevelUsingProbability
}
func (p *balancedLevelProvider) shouldIncludeCarryover() bool {
return p.randGen.Float64() < p.carryoverInclusionProbability
}
// getRandomSpread returns a random value between the two params (inclusive)
func (p *balancedLevelProvider) getRandomSpread(minSpread float64, maxSpread float64) float64 {
// generates a float between 0 and 1
randFloat := p.randGen.Float64()
// reduce to a float between 0 and diffSpread
diffSpread := maxSpread - minSpread
spreadAboveMin := diffSpread * randFloat
// convert to a float between minSpread and maxSpread
return minSpread + spreadAboveMin
}
func (p *balancedLevelProvider) getLevel(maxAssetBase float64, maxAssetQuote float64) (api.Level, error) {
centerPrice := maxAssetQuote / maxAssetBase
// price always adds the spread
targetPrice := centerPrice * (1 + p.spread/2)
targetAmount := (2 * maxAssetBase * p.spread) / (4 + p.spread)
if p.useMaxQuoteInTargetAmountCalc {
targetAmount = (2 * maxAssetQuote * p.spread) / (4 + p.spread)
}
// since targetAmount needs to be less then what we've set above based on the inequality formula, let's reduce it by 5%
targetAmount *= (1 - p.getRandomSpread(p.minAmountSpread, p.maxAmountSpread))
level := api.Level{
Price: *model.NumberFromFloat(targetPrice, utils.SdexPrecision),
Amount: *model.NumberFromFloat(targetAmount, utils.SdexPrecision),
}
return level, nil
}