@reserve-protocol/dtf-rebalance-lib 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +15 -0
- package/LICENSE.md +55 -0
- package/README.md +3 -0
- package/package.json +37 -0
- package/src/index.ts +5 -0
- package/src/numbers.ts +17 -0
- package/src/open-auction.ts +495 -0
- package/src/start-rebalance.ts +150 -0
- package/src/types.ts +35 -0
- package/src/utils.ts +36 -0
- package/test/rebalancing.test.ts +1448 -0
- package/tsconfig.json +14 -0
- package/tsconfig.test.json +9 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
on:
|
2
|
+
push:
|
3
|
+
branches: main
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
publish:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
steps:
|
9
|
+
- uses: actions/checkout@v4
|
10
|
+
- uses: actions/setup-node@v3
|
11
|
+
with:
|
12
|
+
node-version: "20"
|
13
|
+
- uses: JS-DevTools/npm-publish@v3
|
14
|
+
with:
|
15
|
+
token: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# Blue Oak Model License
|
2
|
+
|
3
|
+
Version 1.0.0
|
4
|
+
|
5
|
+
## Purpose
|
6
|
+
|
7
|
+
This license gives everyone as much permission to work with
|
8
|
+
this software as possible, while protecting contributors
|
9
|
+
from liability.
|
10
|
+
|
11
|
+
## Acceptance
|
12
|
+
|
13
|
+
In order to receive this license, you must agree to its
|
14
|
+
rules. The rules of this license are both obligations
|
15
|
+
under that agreement and conditions to your license.
|
16
|
+
You must not do anything with this software that triggers
|
17
|
+
a rule that you cannot or will not follow.
|
18
|
+
|
19
|
+
## Copyright
|
20
|
+
|
21
|
+
Each contributor licenses you to do everything with this
|
22
|
+
software that would otherwise infringe that contributor's
|
23
|
+
copyright in it.
|
24
|
+
|
25
|
+
## Notices
|
26
|
+
|
27
|
+
You must ensure that everyone who gets a copy of
|
28
|
+
any part of this software from you, with or without
|
29
|
+
changes, also gets the text of this license or a link to
|
30
|
+
<https://blueoakcouncil.org/license/1.0.0>.
|
31
|
+
|
32
|
+
## Excuse
|
33
|
+
|
34
|
+
If anyone notifies you in writing that you have not
|
35
|
+
complied with [Notices](#notices), you can keep your
|
36
|
+
license by taking all practical steps to comply within 30
|
37
|
+
days after the notice. If you do not do so, your license
|
38
|
+
ends immediately.
|
39
|
+
|
40
|
+
## Patent
|
41
|
+
|
42
|
+
Each contributor licenses you to do everything with this
|
43
|
+
software that would otherwise infringe any patent claims
|
44
|
+
they can license or become able to license.
|
45
|
+
|
46
|
+
## Reliability
|
47
|
+
|
48
|
+
No contributor can revoke this license.
|
49
|
+
|
50
|
+
## No Liability
|
51
|
+
|
52
|
+
**_As far as the law allows, this software comes as is,
|
53
|
+
without any warranty or condition, and no contributor
|
54
|
+
will be liable to anyone for any damages related to this
|
55
|
+
software or this license, under any kind of legal claim._**
|
package/README.md
ADDED
package/package.json
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
{
|
2
|
+
"name": "@reserve-protocol/dtf-rebalance-lib",
|
3
|
+
"version": "0.0.1",
|
4
|
+
"description": "Rebalancing library for DTFs in typescript",
|
5
|
+
"main": "dist/index.js",
|
6
|
+
"module": "dist/index.js",
|
7
|
+
"types": "dist/index.d.ts",
|
8
|
+
"scripts": {
|
9
|
+
"build": "tsc",
|
10
|
+
"prepublishOnly": "npm run build",
|
11
|
+
"test": "tsc --project tsconfig.test.json && node --test dist/test/*.test.js"
|
12
|
+
},
|
13
|
+
"publishConfig": {
|
14
|
+
"access": "public"
|
15
|
+
},
|
16
|
+
"keywords": [
|
17
|
+
"typescript",
|
18
|
+
"npm",
|
19
|
+
"package"
|
20
|
+
],
|
21
|
+
"author": "Reserve Team",
|
22
|
+
"license": "BlueOak-1.0.0",
|
23
|
+
"repository": {
|
24
|
+
"type": "git",
|
25
|
+
"url": "git+https://github.com/reserve-protocol/dtf-rebalance-lib.git"
|
26
|
+
},
|
27
|
+
"bugs": {
|
28
|
+
"url": "https://github.com/reserve-protocol/dtf-rebalance-lib/issues"
|
29
|
+
},
|
30
|
+
"homepage": "https://github.com/reserve-protocol/dtf-rebalance-lib#readme",
|
31
|
+
"peerDependencies": {
|
32
|
+
"decimal.js-light": "^2.5.1"
|
33
|
+
},
|
34
|
+
"devDependencies": {
|
35
|
+
"typescript": "^5.8.3"
|
36
|
+
}
|
37
|
+
}
|
package/src/index.ts
ADDED
package/src/numbers.ts
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
import Decimal from 'decimal.js-light'
|
2
|
+
|
3
|
+
export const D27n: bigint = 10n ** 27n
|
4
|
+
export const D18n: bigint = 10n ** 18n
|
5
|
+
export const D9n: bigint = 10n ** 9n
|
6
|
+
|
7
|
+
export const D27d: Decimal = new Decimal('1e27')
|
8
|
+
export const D18d: Decimal = new Decimal('1e18')
|
9
|
+
export const D9d: Decimal = new Decimal('1e9')
|
10
|
+
|
11
|
+
export const ZERO = new Decimal('0')
|
12
|
+
export const ONE = new Decimal('1')
|
13
|
+
export const TWO = new Decimal('2')
|
14
|
+
|
15
|
+
export const bn = (str: string | Decimal): bigint => {
|
16
|
+
return BigInt(new Decimal(str).toFixed(0))
|
17
|
+
}
|
@@ -0,0 +1,495 @@
|
|
1
|
+
import Decimal from 'decimal.js-light'
|
2
|
+
|
3
|
+
import { bn, D18d, D27d, ONE, ZERO } from './numbers'
|
4
|
+
|
5
|
+
import {
|
6
|
+
PriceControl,
|
7
|
+
PriceRange,
|
8
|
+
Rebalance,
|
9
|
+
RebalanceLimits,
|
10
|
+
WeightRange,
|
11
|
+
} from './types'
|
12
|
+
|
13
|
+
// Call `getOpenAuction()` to get the current auction round
|
14
|
+
export enum AuctionRound {
|
15
|
+
EJECT = 0,
|
16
|
+
PROGRESS = 1,
|
17
|
+
FINAL = 2,
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Useful metrics to use to visualize things
|
22
|
+
*
|
23
|
+
* @param initialProgression {1} The progression the Folio had when the auction was first proposed
|
24
|
+
* @param absoluteProgression {1} The progression of the auction on an absolute scale
|
25
|
+
* @param relativeProgression {1} The relative progression of the auction
|
26
|
+
* @param target {1} The target of the auction on an absolute scale
|
27
|
+
* @param auctionSize {USD} The total value on sale in the auction
|
28
|
+
* @param surplusTokens The list of tokens in surplus
|
29
|
+
* @param deficitTokens The list of tokens in deficit
|
30
|
+
*/
|
31
|
+
export interface AuctionMetrics {
|
32
|
+
round: AuctionRound
|
33
|
+
initialProgression: number
|
34
|
+
absoluteProgression: number
|
35
|
+
relativeProgression: number
|
36
|
+
target: number
|
37
|
+
auctionSize: number
|
38
|
+
surplusTokens: string[]
|
39
|
+
deficitTokens: string[]
|
40
|
+
}
|
41
|
+
|
42
|
+
// All the args needed to call `folio.openAuction()`
|
43
|
+
export interface OpenAuctionArgs {
|
44
|
+
rebalanceNonce: bigint
|
45
|
+
tokens: string[]
|
46
|
+
newWeights: WeightRange[]
|
47
|
+
newPrices: PriceRange[]
|
48
|
+
newLimits: RebalanceLimits
|
49
|
+
}
|
50
|
+
|
51
|
+
/**
|
52
|
+
* Generator for the `targetBasket` parameter
|
53
|
+
*
|
54
|
+
* Depending on the usecase, pass either:
|
55
|
+
* - TRACKING: CURRENT prices
|
56
|
+
* - NATIVE: HISTORICAL prices
|
57
|
+
*
|
58
|
+
* @param _initialWeights D27{tok/BU} The initial historical weights emitted in the RebalanceStarted event
|
59
|
+
* @param _prices {USD/wholeTok} either CURRENT or HISTORICAL prices
|
60
|
+
* @returns D18{1} The target basket
|
61
|
+
*/
|
62
|
+
const getTargetBasket = (
|
63
|
+
_initialWeights: WeightRange[],
|
64
|
+
_prices: number[],
|
65
|
+
_decimals: bigint[]
|
66
|
+
): bigint[] => {
|
67
|
+
if (_initialWeights.length != _prices.length) {
|
68
|
+
throw new Error('length mismatch')
|
69
|
+
}
|
70
|
+
|
71
|
+
const vals = _initialWeights.map((initialWeight: WeightRange, i: number) => {
|
72
|
+
const price = new Decimal(_prices[i])
|
73
|
+
const decimalScale = new Decimal(`1e${_decimals[i]}`)
|
74
|
+
|
75
|
+
// {USD/wholeBU} = D27{tok/BU} * {BU/wholeBU} / {tok/wholeTok} / D27 * {USD/wholeTok}
|
76
|
+
return new Decimal(initialWeight.spot.toString())
|
77
|
+
.mul(D18d)
|
78
|
+
.div(decimalScale)
|
79
|
+
.div(D27d)
|
80
|
+
.mul(price)
|
81
|
+
})
|
82
|
+
|
83
|
+
const totalValue = vals.reduce((a, b) => a.add(b))
|
84
|
+
|
85
|
+
// D18{1} = {USD/wholeBU} / {USD/wholeBU} * D18
|
86
|
+
return vals.map((val) => bn(val.div(totalValue).mul(D18d)))
|
87
|
+
}
|
88
|
+
|
89
|
+
/**
|
90
|
+
* Get the values needed to call `folio.openAuction()` as the AUCTION_LAUNCHER
|
91
|
+
*
|
92
|
+
* Non-AUCTION_LAUNCHERs should use `folio.openAuctionUnrestricted()`
|
93
|
+
*
|
94
|
+
* @param rebalance The result of calling folio.getRebalance()
|
95
|
+
* @param _supply {share} The totalSupply() of the basket, today
|
96
|
+
* @param _initialFolio D18{tok/share} Initial balances per share, e.g result of folio.toAssets(1e18, 0) at time rebalance was first proposed
|
97
|
+
* @param _targetBasket D18{1} Result of calling `getTargetBasket()`
|
98
|
+
* @param _folio D18{tok/share} Current ratio of token per share, e.g result of folio.toAssets(1e18, 0)
|
99
|
+
* @param _decimals Decimals of each token
|
100
|
+
* @param _prices {USD/wholeTok} USD prices for each *whole* token
|
101
|
+
* @param _priceError {1} Price error to use for each token during auction pricing; should be smaller than price error during startRebalance
|
102
|
+
* @param _finalStageAt {1} The % rebalanced from the initial Folio to determine when is the final stage of the rebalance
|
103
|
+
*/
|
104
|
+
export const getOpenAuction = (
|
105
|
+
rebalance: Rebalance,
|
106
|
+
_supply: bigint,
|
107
|
+
_initialFolio: bigint[] = [],
|
108
|
+
_targetBasket: bigint[] = [],
|
109
|
+
_folio: bigint[],
|
110
|
+
_decimals: bigint[],
|
111
|
+
_prices: number[],
|
112
|
+
_priceError: number[],
|
113
|
+
_finalStageAt: number = 0.9
|
114
|
+
): [OpenAuctionArgs, AuctionMetrics] => {
|
115
|
+
|
116
|
+
if (
|
117
|
+
rebalance.tokens.length != _targetBasket.length ||
|
118
|
+
_targetBasket.length != _folio.length ||
|
119
|
+
_folio.length != _decimals.length ||
|
120
|
+
_decimals.length != _prices.length ||
|
121
|
+
_prices.length != _priceError.length
|
122
|
+
) {
|
123
|
+
throw new Error('length mismatch')
|
124
|
+
}
|
125
|
+
|
126
|
+
if (_finalStageAt >= 1) {
|
127
|
+
throw new Error('finalStageAt must be less than 1')
|
128
|
+
}
|
129
|
+
|
130
|
+
// ================================================================
|
131
|
+
|
132
|
+
// {wholeShare} = {share} / {share/wholeShare}
|
133
|
+
const supply = new Decimal(_supply.toString()).div(D18d)
|
134
|
+
|
135
|
+
// {1} = D18{1} / D18
|
136
|
+
const targetBasket = _targetBasket.map((a) =>
|
137
|
+
new Decimal(a.toString()).div(D18d)
|
138
|
+
)
|
139
|
+
|
140
|
+
// {USD/wholeTok}
|
141
|
+
const prices = _prices.map((a) => new Decimal(a))
|
142
|
+
for (let i = 0; i < prices.length; i++) {
|
143
|
+
if (prices[i].eq(ZERO)) {
|
144
|
+
throw new Error(`missing price for token ${rebalance.tokens[i]}`)
|
145
|
+
}
|
146
|
+
}
|
147
|
+
|
148
|
+
// {1}
|
149
|
+
const priceError = _priceError.map((a) => new Decimal(a.toString()))
|
150
|
+
|
151
|
+
// {tok/wholeTok}
|
152
|
+
const decimalScale = _decimals.map((a) => new Decimal(`1e${a}`))
|
153
|
+
|
154
|
+
// {wholeTok/wholeShare} = D18{tok/share} * {share/wholeShare} / {tok/wholeTok} / D18
|
155
|
+
const initialFolio = _initialFolio.map((c: bigint, i: number) =>
|
156
|
+
new Decimal(c.toString()).div(decimalScale[i])
|
157
|
+
)
|
158
|
+
|
159
|
+
// {wholeTok/wholeShare} = D18{tok/share} * {share/wholeShare} / {tok/wholeTok} / D18
|
160
|
+
const folio = _folio.map((c: bigint, i: number) =>
|
161
|
+
new Decimal(c.toString()).div(decimalScale[i])
|
162
|
+
)
|
163
|
+
|
164
|
+
// {wholeTok/wholeBU} = D27{tok/BU} * {BU/wholeBU} / {tok/wholeTok} / D27
|
165
|
+
let weightRanges = rebalance.weights.map((range: WeightRange, i: number) => {
|
166
|
+
return {
|
167
|
+
low: new Decimal(range.low.toString())
|
168
|
+
.mul(D18d)
|
169
|
+
.div(decimalScale[i])
|
170
|
+
.div(D27d),
|
171
|
+
spot: new Decimal(range.spot.toString())
|
172
|
+
.mul(D18d)
|
173
|
+
.div(decimalScale[i])
|
174
|
+
.div(D27d),
|
175
|
+
high: new Decimal(range.high.toString())
|
176
|
+
.mul(D18d)
|
177
|
+
.div(decimalScale[i])
|
178
|
+
.div(D27d),
|
179
|
+
}
|
180
|
+
})
|
181
|
+
|
182
|
+
const finalStageAt = new Decimal(_finalStageAt.toString())
|
183
|
+
|
184
|
+
|
185
|
+
// ================================================================
|
186
|
+
|
187
|
+
// calculate ideal spot limit, the actual BU<->share ratio
|
188
|
+
|
189
|
+
// {USD/wholeShare} = {wholeTok/wholeShare} * {USD/wholeTok}
|
190
|
+
const shareValue = folio
|
191
|
+
.map((f: Decimal, i: number) => f.mul(prices[i]))
|
192
|
+
.reduce((a, b) => a.add(b))
|
193
|
+
|
194
|
+
// {USD/wholeBU} = {wholeTok/wholeBU} * {USD/wholeTok}
|
195
|
+
const buValue = weightRanges
|
196
|
+
.map((weightRange, i) => weightRange.spot.mul(prices[i]))
|
197
|
+
.reduce((a, b) => a.add(b))
|
198
|
+
|
199
|
+
|
200
|
+
// ================================================================
|
201
|
+
|
202
|
+
// calculate rebalanceTarget
|
203
|
+
|
204
|
+
const ejectionIndices: number[] = []
|
205
|
+
for (let i = 0; i < rebalance.weights.length; i++) {
|
206
|
+
if (rebalance.weights[i].spot == 0n) {
|
207
|
+
ejectionIndices.push(i)
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
211
|
+
// {1} = {wholeTok/wholeShare} * {USD/wholeTok} / {USD/wholeShare}
|
212
|
+
const portionBeingEjected = ejectionIndices
|
213
|
+
.map((i) => folio[i].mul(prices[i]))
|
214
|
+
.reduce((a, b) => a.add(b), ZERO)
|
215
|
+
.div(shareValue)
|
216
|
+
|
217
|
+
|
218
|
+
// {1} = {USD/wholeShare} / {USD/wholeShare}
|
219
|
+
let progression = folio
|
220
|
+
.map((actualBalance, i) => {
|
221
|
+
// {wholeTok/wholeShare} = {USD/wholeShare} * {1} / {USD/wholeTok}
|
222
|
+
const balanceExpected = shareValue.mul(targetBasket[i]).div(prices[i])
|
223
|
+
|
224
|
+
// {wholeTok/wholeShare} = {wholeTok/wholeBU} * {wholeBU/wholeShare}
|
225
|
+
const balanceInBU = balanceExpected.gt(actualBalance)
|
226
|
+
? actualBalance
|
227
|
+
: balanceExpected
|
228
|
+
|
229
|
+
// {USD/wholeShare} = {wholeTok/wholeShare} * {USD/wholeTok}
|
230
|
+
return balanceInBU.mul(prices[i])
|
231
|
+
})
|
232
|
+
.reduce((a, b) => a.add(b))
|
233
|
+
.div(shareValue)
|
234
|
+
|
235
|
+
// {1} = {USD/wholeShare} / {USD/wholeShare}
|
236
|
+
const initialProgression = initialFolio
|
237
|
+
.map((initialBalance, i) => {
|
238
|
+
// {wholeTok/wholeShare} = {USD/wholeShare} * {1} / {USD/wholeTok}
|
239
|
+
const balanceExpected = shareValue.mul(targetBasket[i]).div(prices[i])
|
240
|
+
|
241
|
+
// {wholeTok/wholeShare} = {wholeTok/wholeBU} * {wholeBU/wholeShare}
|
242
|
+
const balanceInBU = balanceExpected.gt(initialBalance)
|
243
|
+
? initialBalance
|
244
|
+
: balanceExpected
|
245
|
+
|
246
|
+
// {USD/wholeShare} = {wholeTok/wholeShare} * {USD/wholeTok}
|
247
|
+
return balanceInBU.mul(prices[i])
|
248
|
+
})
|
249
|
+
.reduce((a, b) => a.add(b))
|
250
|
+
.div(shareValue)
|
251
|
+
|
252
|
+
if (progression < initialProgression) {
|
253
|
+
progression = initialProgression // don't go backwards
|
254
|
+
}
|
255
|
+
|
256
|
+
|
257
|
+
// {1} = {1} / {1}
|
258
|
+
const relativeProgression = initialProgression.eq(ONE)
|
259
|
+
? ONE
|
260
|
+
: progression.sub(initialProgression).div(ONE.sub(initialProgression))
|
261
|
+
|
262
|
+
let rebalanceTarget = ONE
|
263
|
+
let round: AuctionRound = AuctionRound.FINAL
|
264
|
+
|
265
|
+
// make it an eject auction if there is 1 bps or more of value to eject
|
266
|
+
if (portionBeingEjected.gte(1e-4)) {
|
267
|
+
round = AuctionRound.EJECT
|
268
|
+
|
269
|
+
rebalanceTarget = progression.add(portionBeingEjected.mul(1.5)) // set rebalanceTarget to 50% more than needed, to ensure ejection completes
|
270
|
+
if (rebalanceTarget.gt(ONE)) {
|
271
|
+
rebalanceTarget = ONE
|
272
|
+
}
|
273
|
+
} else if (relativeProgression.lt(finalStageAt.sub(0.02))) {
|
274
|
+
// wiggle room to prevent having to re-run an auction at the same stage after price movement
|
275
|
+
round = AuctionRound.PROGRESS
|
276
|
+
|
277
|
+
rebalanceTarget = finalStageAt
|
278
|
+
}
|
279
|
+
|
280
|
+
// {1}
|
281
|
+
const delta = ONE.sub(rebalanceTarget)
|
282
|
+
|
283
|
+
// ================================================================
|
284
|
+
|
285
|
+
// get new limits, constrained by extremes
|
286
|
+
|
287
|
+
// {wholeBU/wholeShare} = {USD/wholeShare} / {USD/wholeBU}
|
288
|
+
const spotLimit = shareValue.div(buValue)
|
289
|
+
|
290
|
+
|
291
|
+
// D18{BU/share} = {wholeBU/wholeShare} * D18 * {1}
|
292
|
+
const newLimits = {
|
293
|
+
low: bn(spotLimit.sub(spotLimit.mul(delta)).mul(D18d)),
|
294
|
+
spot: bn(spotLimit.mul(D18d)),
|
295
|
+
high: bn(spotLimit.add(spotLimit.mul(delta)).mul(D18d)),
|
296
|
+
}
|
297
|
+
|
298
|
+
// low
|
299
|
+
if (newLimits.low < rebalance.limits.low) {
|
300
|
+
newLimits.low = rebalance.limits.low
|
301
|
+
}
|
302
|
+
if (newLimits.low > rebalance.limits.high) {
|
303
|
+
newLimits.low = rebalance.limits.high
|
304
|
+
}
|
305
|
+
|
306
|
+
// spot
|
307
|
+
if (newLimits.spot < rebalance.limits.low) {
|
308
|
+
newLimits.spot = rebalance.limits.low
|
309
|
+
}
|
310
|
+
if (newLimits.spot > rebalance.limits.high) {
|
311
|
+
newLimits.spot = rebalance.limits.high
|
312
|
+
}
|
313
|
+
|
314
|
+
// high
|
315
|
+
if (newLimits.high < rebalance.limits.low) {
|
316
|
+
newLimits.high = rebalance.limits.low
|
317
|
+
}
|
318
|
+
if (newLimits.high > rebalance.limits.high) {
|
319
|
+
newLimits.high = rebalance.limits.high
|
320
|
+
}
|
321
|
+
|
322
|
+
// ================================================================
|
323
|
+
|
324
|
+
// get new weights, constrained by extremes
|
325
|
+
|
326
|
+
// {wholeBU/wholeShare} = D18{BU/share} / D18
|
327
|
+
const actualLimits = {
|
328
|
+
low: new Decimal(newLimits.low.toString()).div(D18d),
|
329
|
+
spot: new Decimal(newLimits.spot.toString()).div(D18d),
|
330
|
+
high: new Decimal(newLimits.high.toString()).div(D18d),
|
331
|
+
}
|
332
|
+
|
333
|
+
// D27{tok/BU}
|
334
|
+
const newWeights = rebalance.weights.map((weightRange, i) => {
|
335
|
+
// {wholeTok/wholeBU} = {USD/wholeShare} * {1} / {wholeBU/wholeShare} / {USD/wholeTok}
|
336
|
+
const idealWeight = shareValue
|
337
|
+
.mul(targetBasket[i])
|
338
|
+
.div(actualLimits.spot)
|
339
|
+
.div(prices[i])
|
340
|
+
|
341
|
+
// D27{tok/BU} = {wholeTok/wholeBU} * D27 * {tok/wholeTok} / {BU/wholeBU}
|
342
|
+
const newWeightsD27 = {
|
343
|
+
low: bn(
|
344
|
+
idealWeight
|
345
|
+
.mul(rebalanceTarget.div(actualLimits.low.div(actualLimits.spot))) // add remaining delta into weight
|
346
|
+
.mul(D27d)
|
347
|
+
.mul(decimalScale[i])
|
348
|
+
.div(D18d)
|
349
|
+
),
|
350
|
+
spot: bn(idealWeight.mul(D27d).mul(decimalScale[i]).div(D18d)),
|
351
|
+
high: bn(
|
352
|
+
idealWeight
|
353
|
+
.mul(ONE.add(delta).div(actualLimits.high.div(actualLimits.spot))) // add remaining delta into weight
|
354
|
+
.mul(D27d)
|
355
|
+
.mul(decimalScale[i])
|
356
|
+
.div(D18d)
|
357
|
+
),
|
358
|
+
}
|
359
|
+
|
360
|
+
if (newWeightsD27.low < weightRange.low) {
|
361
|
+
newWeightsD27.low = weightRange.low
|
362
|
+
} else if (newWeightsD27.low > weightRange.high) {
|
363
|
+
newWeightsD27.low = weightRange.high
|
364
|
+
}
|
365
|
+
|
366
|
+
if (newWeightsD27.spot < weightRange.low) {
|
367
|
+
newWeightsD27.spot = weightRange.low
|
368
|
+
} else if (newWeightsD27.spot > weightRange.high) {
|
369
|
+
newWeightsD27.spot = weightRange.high
|
370
|
+
}
|
371
|
+
|
372
|
+
if (newWeightsD27.high < weightRange.low) {
|
373
|
+
newWeightsD27.high = weightRange.low
|
374
|
+
} else if (newWeightsD27.high > weightRange.high) {
|
375
|
+
newWeightsD27.high = weightRange.high
|
376
|
+
}
|
377
|
+
|
378
|
+
return newWeightsD27
|
379
|
+
})
|
380
|
+
|
381
|
+
// ================================================================
|
382
|
+
|
383
|
+
// get new prices, constrained by extremes
|
384
|
+
|
385
|
+
|
386
|
+
// D27{USD/tok}
|
387
|
+
const newPrices = rebalance.initialPrices.map((initialPrice, i) => {
|
388
|
+
// revert if price out of bounds
|
389
|
+
const spotPrice = bn(prices[i].mul(D27d).div(decimalScale[i]))
|
390
|
+
if (spotPrice < initialPrice.low || spotPrice > initialPrice.high) {
|
391
|
+
throw new Error('spot price out of bounds! auction launcher MUST closeRebalance to prevent loss!')
|
392
|
+
}
|
393
|
+
|
394
|
+
if (rebalance.priceControl == PriceControl.NONE) {
|
395
|
+
return initialPrice
|
396
|
+
}
|
397
|
+
|
398
|
+
// D27{USD/tok} = {USD/wholeTok} * D27 / {tok/wholeTok}
|
399
|
+
const pricesD27 = {
|
400
|
+
low: bn(
|
401
|
+
prices[i].mul(ONE.sub(priceError[i])).mul(D27d).div(decimalScale[i])
|
402
|
+
),
|
403
|
+
high: bn(
|
404
|
+
prices[i].div(ONE.sub(priceError[i])).mul(D27d).div(decimalScale[i])
|
405
|
+
),
|
406
|
+
}
|
407
|
+
|
408
|
+
|
409
|
+
// low
|
410
|
+
if (pricesD27.low < initialPrice.low) {
|
411
|
+
pricesD27.low = initialPrice.low
|
412
|
+
}
|
413
|
+
if (pricesD27.low > initialPrice.high) {
|
414
|
+
pricesD27.low = initialPrice.high
|
415
|
+
}
|
416
|
+
|
417
|
+
// high
|
418
|
+
if (pricesD27.high < initialPrice.low) {
|
419
|
+
pricesD27.high = initialPrice.low
|
420
|
+
}
|
421
|
+
if (pricesD27.high > initialPrice.high) {
|
422
|
+
pricesD27.high = initialPrice.high
|
423
|
+
}
|
424
|
+
|
425
|
+
if (pricesD27.low == pricesD27.high) {
|
426
|
+
throw new Error('no price range')
|
427
|
+
}
|
428
|
+
|
429
|
+
return pricesD27
|
430
|
+
})
|
431
|
+
|
432
|
+
// ================================================================
|
433
|
+
|
434
|
+
// calculate metrics
|
435
|
+
|
436
|
+
// {USD} = {1} * {USD/wholeShare} * {wholeShare}
|
437
|
+
const valueBeingTraded = rebalanceTarget
|
438
|
+
.sub(progression)
|
439
|
+
.mul(shareValue)
|
440
|
+
.mul(supply)
|
441
|
+
|
442
|
+
const surplusTokens: string[] = []
|
443
|
+
const deficitTokens: string[] = []
|
444
|
+
|
445
|
+
// update Decimal weightRanges
|
446
|
+
// {wholeTok/wholeBU} = D27{tok/BU} * {BU/wholeBU} / {tok/wholeTok} / D27
|
447
|
+
weightRanges = newWeights.map((range, i) => {
|
448
|
+
return {
|
449
|
+
low: new Decimal(range.low.toString())
|
450
|
+
.mul(D18d)
|
451
|
+
.div(decimalScale[i])
|
452
|
+
.div(D27d),
|
453
|
+
spot: new Decimal(range.spot.toString())
|
454
|
+
.mul(D18d)
|
455
|
+
.div(decimalScale[i])
|
456
|
+
.div(D27d),
|
457
|
+
high: new Decimal(range.high.toString())
|
458
|
+
.mul(D18d)
|
459
|
+
.div(decimalScale[i])
|
460
|
+
.div(D27d),
|
461
|
+
}
|
462
|
+
})
|
463
|
+
|
464
|
+
rebalance.tokens.forEach((token, i) => {
|
465
|
+
// {wholeTok/wholeShare} = {wholeTok/wholeBU} * {wholeBU/wholeShare}
|
466
|
+
const buyUpTo = weightRanges[i].low.mul(actualLimits.low)
|
467
|
+
const sellDownTo = weightRanges[i].high.mul(actualLimits.high)
|
468
|
+
|
469
|
+
if (folio[i].lt(buyUpTo)) {
|
470
|
+
deficitTokens.push(token)
|
471
|
+
} else if (folio[i].gt(sellDownTo)) {
|
472
|
+
surplusTokens.push(token)
|
473
|
+
}
|
474
|
+
})
|
475
|
+
|
476
|
+
return [
|
477
|
+
{
|
478
|
+
rebalanceNonce: rebalance.nonce,
|
479
|
+
tokens: rebalance.tokens, // full set of tokens, not pruned to the active buy/sells
|
480
|
+
newWeights: newWeights,
|
481
|
+
newPrices: newPrices,
|
482
|
+
newLimits: newLimits,
|
483
|
+
},
|
484
|
+
{
|
485
|
+
round: round,
|
486
|
+
initialProgression: initialProgression.toNumber(),
|
487
|
+
absoluteProgression: progression.toNumber(),
|
488
|
+
relativeProgression: relativeProgression.toNumber(),
|
489
|
+
target: rebalanceTarget.toNumber(),
|
490
|
+
auctionSize: valueBeingTraded.toNumber(),
|
491
|
+
surplusTokens: surplusTokens,
|
492
|
+
deficitTokens: deficitTokens,
|
493
|
+
},
|
494
|
+
]
|
495
|
+
}
|