@reserve-protocol/dtf-rebalance-lib 3.2.1 → 3.3.0
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/README.md +87 -1
- package/dist/index.d.ts +2 -0
- package/docs/auction-algorithm.md +357 -0
- package/package.json +15 -4
package/README.md
CHANGED
|
@@ -1,3 +1,89 @@
|
|
|
1
1
|
# dtf-rebalance-lib
|
|
2
2
|
|
|
3
|
-
Rebalancing library for DTFs in typescript.
|
|
3
|
+
Rebalancing library for DTFs in typescript. Computes the parameters needed to rebalance a DTF portfolio through a series of on-chain auctions, converging from its current composition toward a target basket.
|
|
4
|
+
|
|
5
|
+
For detailed formulas and worked examples, see [docs/auction-algorithm.md](docs/auction-algorithm.md).
|
|
6
|
+
|
|
7
|
+
## Repository layout
|
|
8
|
+
|
|
9
|
+
The root package, `@reserve-protocol/dtf-rebalance-lib`, is the SDK-independent core and the only package released from this repository. It stays deterministic and does not depend on `@reserve-protocol/sdk`, RPCs, subgraphs, Reserve API, Hardhat, or deployed DTF metadata.
|
|
10
|
+
|
|
11
|
+
Internal SDK-aware operational code lives under `packages/tools`. It is a private workspace used by repo-local scripts, fork tests, and Hardhat task adapters. It is not published to npm and is not part of the `@reserve-protocol/dtf-rebalance-lib` release artifact.
|
|
12
|
+
|
|
13
|
+
## How rebalancing works
|
|
14
|
+
|
|
15
|
+
Rebalancing is a two-phase process:
|
|
16
|
+
|
|
17
|
+
1. **Start** -- Call `getStartRebalance()` once to open a new rebalance. It computes initial weight ranges, price ranges, limits, and per-token auction size caps based on the current portfolio, target basket, and market prices.
|
|
18
|
+
|
|
19
|
+
2. **Auction rounds** -- Call `getOpenAuction()` repeatedly (once per round) to produce tightening parameters that progressively move the portfolio toward its target. Each round narrows the weight/price bounds and advances a _progression_ metric from 0 toward 1.
|
|
20
|
+
|
|
21
|
+
## Key concepts
|
|
22
|
+
|
|
23
|
+
**Weights and limits.** Each token's expected balance per share is `weight * limit`. Weights describe _how much_ of each token a basket unit contains; limits describe _how many_ basket units a share is worth.
|
|
24
|
+
|
|
25
|
+
**Low/high bounds.** Every weight and limit has a low, spot, and high value. The low bound defines what you buy _up to_ and the high bound defines what you sell _down to_, creating a corridor within which the auction clears.
|
|
26
|
+
|
|
27
|
+
**Progression.** A 0-to-1 metric measuring how close the portfolio is to its target composition. Each auction round advances progression by a controlled step, and the final round pushes it to 1.
|
|
28
|
+
|
|
29
|
+
**maxAuctionSize.** Caps the USD value each token can trade in a single auction round, limiting market impact. Set per-token in `getStartRebalance()`.
|
|
30
|
+
|
|
31
|
+
## Auction rounds
|
|
32
|
+
|
|
33
|
+
Each call to `getOpenAuction()` produces one of three round types:
|
|
34
|
+
|
|
35
|
+
- **EJECT** -- Removes tokens that are being dropped from the basket entirely (weight target is zero). Runs first if applicable.
|
|
36
|
+
- **PROGRESS** -- The main phase. Moves the portfolio toward the target in controlled steps, with each step bounded by `maxAuctionSize` and a progression target.
|
|
37
|
+
- **FINAL** -- Once progression crosses the `finalStageAt` threshold, tightens bounds to zero spread and finishes the rebalance.
|
|
38
|
+
|
|
39
|
+
## Tracking vs Native
|
|
40
|
+
|
|
41
|
+
**Tracking rebalances** (`weightControl = false`) keep weights fixed and move only the limits. This changes the _scale_ of the portfolio -- how many basket units each share represents -- without changing composition. The target basket is computed from current market prices.
|
|
42
|
+
|
|
43
|
+
**Native rebalances** (`weightControl = true`) keep limits fixed and move only the weights. This changes the _composition_ of the portfolio -- which tokens and in what proportions -- without changing scale. The target basket is computed from the prices at rebalance start.
|
|
44
|
+
|
|
45
|
+
## Parameters
|
|
46
|
+
|
|
47
|
+
### `getStartRebalance()`
|
|
48
|
+
|
|
49
|
+
| Parameter | Type | Description |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `version` | `FolioVersion` | Protocol version (`V4` or `V5`) |
|
|
52
|
+
| `_supply` | `bigint` | Current total share supply |
|
|
53
|
+
| `tokens` | `string[]` | Token addresses in the basket |
|
|
54
|
+
| `_assets` | `bigint[]` | Current token balances |
|
|
55
|
+
| `decimals` | `bigint[]` | Decimals for each token |
|
|
56
|
+
| `_targetBasket` | `bigint[]` | D18 ideal basket proportions |
|
|
57
|
+
| `_prices` | `number[]` | USD price per whole token |
|
|
58
|
+
| `_priceError` | `number[]` | Price error fraction per token |
|
|
59
|
+
| `_maxAuctionSizes` | `number[]` | Max USD auction size per token |
|
|
60
|
+
| `weightControl` | `boolean` | `false` = tracking, `true` = native |
|
|
61
|
+
| `deferWeights` | `boolean` | Use full weight range (native only) |
|
|
62
|
+
| `debug` | `boolean?` | Log debug output |
|
|
63
|
+
|
|
64
|
+
**Returns** `StartRebalanceArgsPartial` -- contains `tokens` (with weight ranges, price ranges, and max auction sizes per token) and `limits` (low/spot/high).
|
|
65
|
+
|
|
66
|
+
### `getOpenAuction()`
|
|
67
|
+
|
|
68
|
+
| Parameter | Type | Description |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| `version` | `FolioVersion` | Protocol version |
|
|
71
|
+
| `_rebalance` | `Rebalance` | On-chain rebalance state |
|
|
72
|
+
| `_supply` | `bigint` | Current total share supply |
|
|
73
|
+
| `_initialSupply` | `bigint` | Supply at rebalance start |
|
|
74
|
+
| `_initialAssets` | `bigint[]` | Token balances at rebalance start |
|
|
75
|
+
| `_targetBasket` | `bigint[]` | D18 ideal basket proportions |
|
|
76
|
+
| `_assets` | `bigint[]` | Current token balances |
|
|
77
|
+
| `_decimals` | `bigint[]` | Token decimals |
|
|
78
|
+
| `_prices` | `number[]` | Current USD prices per whole token |
|
|
79
|
+
| `_priceError` | `number[]` | Price error fraction per token |
|
|
80
|
+
| `_finalStageAt` | `number` | Progression threshold to enter FINAL (e.g. 0.9) |
|
|
81
|
+
| `debug` | `boolean?` | Log debug output |
|
|
82
|
+
|
|
83
|
+
**Returns** `[OpenAuctionArgs, AuctionMetrics]` -- the on-chain call arguments and a metrics object describing the round type, progression, and per-token surplus/deficit sizes.
|
|
84
|
+
|
|
85
|
+
## Utility functions
|
|
86
|
+
|
|
87
|
+
- **`getTargetBasket(weights, prices, decimals)`** -- Computes the D18 target basket proportions from initial weights and prices.
|
|
88
|
+
- **`getBasketDistribution(balances, prices, decimals)`** -- Returns the D18 value distribution across tokens given current balances and prices.
|
|
89
|
+
- **`getBasketAccuracy(balances, prices, decimals, weights)`** -- Returns a 0-to-1 score measuring how closely current balances match the target weights.
|
package/dist/index.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ export * from "./numbers";
|
|
|
3
3
|
export * from "./open-auction";
|
|
4
4
|
export * from "./start-rebalance";
|
|
5
5
|
export * from "./utils";
|
|
6
|
+
export type { Rebalance as RebalanceV4, StartRebalanceArgsPartial as StartRebalanceArgsPartialV4, } from "./4.0.0/types";
|
|
7
|
+
export type { Rebalance as RebalanceV5, StartRebalanceArgsPartial as StartRebalanceArgsPartialV5, } from "./types";
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# Auction Algorithm
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The auction algorithm is a sophisticated mechanism for rebalancing portfolios (called "Folios") through a series of auction rounds. The algorithm determines what tokens to buy and sell, at what prices, and in what quantities to transition a portfolio from its current state to a target composition.
|
|
6
|
+
|
|
7
|
+
The algorithm is designed to handle any number of tokens and supports three types of rebalances, each with different approaches to manipulating weights and limits.
|
|
8
|
+
|
|
9
|
+
Initially `getStartRebalance()` is called to prepare the initial rebalance parameters. Any number of auctions (serially) are then launched via successive calls to `getOpenAuction()`.
|
|
10
|
+
|
|
11
|
+
## Key Concepts
|
|
12
|
+
|
|
13
|
+
### Core Components
|
|
14
|
+
|
|
15
|
+
1. **Weights** (`WeightRange`): Represent the amount of tokens per basket unit (BU). Expressed as D27 values with low/spot/high ranges.
|
|
16
|
+
|
|
17
|
+
- Formula: `D27{tok/BU}`
|
|
18
|
+
- Used to define the target composition of the basket
|
|
19
|
+
|
|
20
|
+
2. **Limits** (`RebalanceLimits`): Define the relationship between basket units and shares. Expressed as D18 values with low/spot/high ranges.
|
|
21
|
+
|
|
22
|
+
- Formula: `D18{BU/share}`
|
|
23
|
+
- Used to scale the basket composition to actual share holdings
|
|
24
|
+
|
|
25
|
+
3. **Prices** (`PriceRange`): Token prices in nanoUSD (D27 format) with low/high bounds for auction pricing.
|
|
26
|
+
|
|
27
|
+
- Formula: `D27{nanoUSD/tok}`
|
|
28
|
+
- Used to calculate values and determine trading ranges
|
|
29
|
+
|
|
30
|
+
4. **Target Basket**: A normalized representation (D18 format) of the portfolio's target composition by value percentage.
|
|
31
|
+
- Different calculation methods for tracking vs native rebalances
|
|
32
|
+
|
|
33
|
+
### Fundamental Relationship
|
|
34
|
+
|
|
35
|
+
The core relationship between these components is:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
expected balance = weight × limit
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Where:
|
|
42
|
+
|
|
43
|
+
- `balance`: tokens per share `{tok/share}`
|
|
44
|
+
- `weight`: tokens per basket unit `{tok/BU}`
|
|
45
|
+
- `limit`: basket units per share `{BU/share}`
|
|
46
|
+
|
|
47
|
+
**Understanding High and Low Bounds:**
|
|
48
|
+
|
|
49
|
+
The high and low bounds define the trading boundaries for the auction:
|
|
50
|
+
|
|
51
|
+
- **Low Bounds**: Define **deficits** - what we **buy up to**
|
|
52
|
+
|
|
53
|
+
- `buyUpTo = weight.low × limit.low`
|
|
54
|
+
- If current balance < buyUpTo, the token has a deficit
|
|
55
|
+
- The auction will purchase tokens to reach this threshold
|
|
56
|
+
|
|
57
|
+
- **High Bounds**: Define **surpluses** - what we **sell down to**
|
|
58
|
+
- `sellDownTo = weight.high × limit.high`
|
|
59
|
+
- If current balance > sellDownTo, the token has a surplus
|
|
60
|
+
- The auction will sell tokens down to this threshold
|
|
61
|
+
|
|
62
|
+
This asymmetric design creates a trading range that gradually narrows in on a final spot target that is constantly updated throughout the auction in response to changing prices and variable slippage.
|
|
63
|
+
|
|
64
|
+
## Types of Rebalances
|
|
65
|
+
|
|
66
|
+
The algorithm supports three types of rebalances. First, let's understand the two simple, disjoint cases:
|
|
67
|
+
|
|
68
|
+
### 1. Tracking Rebalance (Simple Case)
|
|
69
|
+
|
|
70
|
+
- **What it does**: Adjusts portfolio scale while keeping token ratios constant
|
|
71
|
+
- **What changes**: Only limits vary
|
|
72
|
+
- **What stays fixed**: Weights remain constant (low = spot = high)
|
|
73
|
+
- **Weight Control**: `false` in `getStartRebalance()`
|
|
74
|
+
- **Target Basket**: Uses CURRENT market prices
|
|
75
|
+
- **Real-world analogy**: Like zooming in/out on a photograph - proportions stay the same, only scale changes
|
|
76
|
+
- **Example**: Portfolio maintains 60% ETH, 40% BTC ratio but adjusts total value
|
|
77
|
+
|
|
78
|
+
### 2. Native Rebalance (Simple Case)
|
|
79
|
+
|
|
80
|
+
- **What it does**: Changes portfolio composition while keeping scale constant
|
|
81
|
+
- **What changes**: Only weights vary
|
|
82
|
+
- **What stays fixed**: Limits remain at initial values
|
|
83
|
+
- **Weight Control**: `true` in `getStartRebalance()`
|
|
84
|
+
- **Target Basket**: Uses HISTORICAL prices from rebalance start
|
|
85
|
+
- **Real-world analogy**: Like rearranging furniture in a room - the room size stays the same, but contents change
|
|
86
|
+
- **Example**: Changing from 100% USDC to 50% DAI, 50% USDT
|
|
87
|
+
|
|
88
|
+
### 3. Hybrid Rebalance (Complex Case)
|
|
89
|
+
|
|
90
|
+
- **What it does**: Changes both composition AND scale simultaneously
|
|
91
|
+
- **What changes**: Both weights AND limits vary
|
|
92
|
+
- **Implementation**: Uses manually constructed `Rebalance` objects
|
|
93
|
+
- **Use Case**: Complex scenarios requiring simultaneous composition and scale changes
|
|
94
|
+
- **Real-world analogy**: Like both rearranging furniture AND changing room size
|
|
95
|
+
- **Example**: Reducing USDC from 100% to 33% while also doubling portfolio scale
|
|
96
|
+
|
|
97
|
+
## Auction Rounds
|
|
98
|
+
|
|
99
|
+
The algorithm progresses through three types of auction rounds:
|
|
100
|
+
|
|
101
|
+
### 1. Eject Round (`EJECT`)
|
|
102
|
+
|
|
103
|
+
- **Purpose**: Remove tokens with zero target weight from the portfolio
|
|
104
|
+
- **Trigger**: When `portionBeingEjected > 0` (tokens have `weight.spot == 0`)
|
|
105
|
+
- **Special Handling**:
|
|
106
|
+
- Adds 10% buffer to high limits and weights
|
|
107
|
+
- Prevents selling all surpluses upfront
|
|
108
|
+
- Allows ejected tokens to fill deficits
|
|
109
|
+
- **Target**: Either approaches `finalStageAt` or completes ejection
|
|
110
|
+
|
|
111
|
+
### 2. Progress Round (`PROGRESS`)
|
|
112
|
+
|
|
113
|
+
- **Purpose**: Move the portfolio towards the `finalStageAt` threshold
|
|
114
|
+
- **Trigger**: When progression < 99% AND relative progression < (finalStageAt - 0.02)
|
|
115
|
+
- **Target**: `initialProgression + (1 - initialProgression) × finalStageAt`
|
|
116
|
+
- **Behavior**: Gradual rebalancing to avoid market impact
|
|
117
|
+
|
|
118
|
+
### 3. Final Round (`FINAL`)
|
|
119
|
+
|
|
120
|
+
- **Purpose**: Complete the rebalance to 100%
|
|
121
|
+
- **Trigger**: When approaching or exceeding `finalStageAt` threshold
|
|
122
|
+
- **Target**: 100% completion (rebalanceTarget = 1)
|
|
123
|
+
- **Delta**: 0 (no spread between low/high bounds)
|
|
124
|
+
|
|
125
|
+
## Algorithm Flow
|
|
126
|
+
|
|
127
|
+
### Input Parameters
|
|
128
|
+
|
|
129
|
+
1. **rebalance**: Current rebalance state from `folio.getRebalance()`
|
|
130
|
+
2. **\_supply**: Total supply of shares
|
|
131
|
+
3. **\_initialFolio**: Initial token balances when rebalance started
|
|
132
|
+
4. **\_targetBasket**: Target composition by value percentage
|
|
133
|
+
5. **\_folio**: Current token balances
|
|
134
|
+
6. **\_decimals**: Token decimal places
|
|
135
|
+
7. **\_prices**: Current USD prices per whole token
|
|
136
|
+
8. **\_priceError**: Price error margins for auction pricing
|
|
137
|
+
9. **\_finalStageAt**: Progression threshold (e.g., 0.9 = 95%)
|
|
138
|
+
|
|
139
|
+
### Initial Setup with getStartRebalance
|
|
140
|
+
|
|
141
|
+
Before any auction can begin, `getStartRebalance()` prepares the initial rebalance parameters:
|
|
142
|
+
|
|
143
|
+
**For Tracking Rebalances (`weightControl = false`):**
|
|
144
|
+
|
|
145
|
+
- Weights: All three values (low/spot/high) are set identically based on target basket
|
|
146
|
+
- Limits: Calculated using price error to create asymmetric bounds
|
|
147
|
+
- `totalPortion = Σ(targetBasket[i] × priceError[i])`
|
|
148
|
+
- `low = (1 - totalPortion) × 1e18`
|
|
149
|
+
- `high = 1 / (1 - totalPortion) × 1e18`
|
|
150
|
+
- The division in `high` creates asymmetry (e.g., 10% error → 90% low, 111% high)
|
|
151
|
+
- Prices: Standard low/high based on price error
|
|
152
|
+
|
|
153
|
+
**For Native Rebalances (`weightControl = true`):**
|
|
154
|
+
|
|
155
|
+
- Weights: Vary based on price error
|
|
156
|
+
- `low = spotWeight × (1 - priceError)`
|
|
157
|
+
- `high = spotWeight / (1 - priceError)`
|
|
158
|
+
- Limits: Fixed at 1e18 for all (low/spot/high)
|
|
159
|
+
- Prices: Same calculation as tracking
|
|
160
|
+
|
|
161
|
+
### Processing Steps
|
|
162
|
+
|
|
163
|
+
1. **Calculate Current State**
|
|
164
|
+
|
|
165
|
+
- Convert all values to common decimal format
|
|
166
|
+
- Calculate share value and basket unit value
|
|
167
|
+
- Determine ideal spot limit: `shareValue / buValue`
|
|
168
|
+
|
|
169
|
+
2. **Calculate Progression**
|
|
170
|
+
|
|
171
|
+
- **Absolute Progression**: Percentage of balances in correct position (0-100%)
|
|
172
|
+
- **Relative Progression**: Progress from initial to target
|
|
173
|
+
- Formula: `(current - initial) / (1 - initial)`
|
|
174
|
+
|
|
175
|
+
3. **Determine Auction Round**
|
|
176
|
+
|
|
177
|
+
- Check for ejections first
|
|
178
|
+
- Then check progression thresholds
|
|
179
|
+
- Default to FINAL if near completion
|
|
180
|
+
|
|
181
|
+
4. **Calculate New Limits**
|
|
182
|
+
|
|
183
|
+
- Base calculation: `spotLimit × (1 ± delta)`
|
|
184
|
+
- Constrained by initial rebalance limits
|
|
185
|
+
- Delta derived from target progression
|
|
186
|
+
|
|
187
|
+
5. **Calculate New Weights**
|
|
188
|
+
|
|
189
|
+
- Ideal weight: `shareValue × targetBasket / actualLimits.spot / price`
|
|
190
|
+
- Adjusted for delta while avoiding double-counting uncertainty
|
|
191
|
+
- Formula: `idealWeight × (1 ± delta) / (limitRatio)`
|
|
192
|
+
- The `limitRatio` division prevents propagating uncertainty twice
|
|
193
|
+
- Constrained by initial weight ranges
|
|
194
|
+
|
|
195
|
+
6. **Calculate New Prices**
|
|
196
|
+
|
|
197
|
+
- Based on current prices ± price error
|
|
198
|
+
- Constrained by initial price ranges
|
|
199
|
+
- Only adjusted if `priceControl != NONE`
|
|
200
|
+
|
|
201
|
+
7. **Filter Tradeable Tokens**
|
|
202
|
+
- Include only tokens with surpluses or deficits
|
|
203
|
+
- Exclude tokens not in rebalance
|
|
204
|
+
- Minimum trade value: $1
|
|
205
|
+
|
|
206
|
+
### Output
|
|
207
|
+
|
|
208
|
+
The algorithm returns:
|
|
209
|
+
|
|
210
|
+
1. **OpenAuctionArgs**: Parameters for calling `folio.openAuction()`
|
|
211
|
+
|
|
212
|
+
- rebalanceNonce
|
|
213
|
+
- tokens (filtered list)
|
|
214
|
+
- newWeights
|
|
215
|
+
- newPrices
|
|
216
|
+
- newLimits
|
|
217
|
+
|
|
218
|
+
2. **AuctionMetrics**: Useful metrics for monitoring
|
|
219
|
+
- round type
|
|
220
|
+
- progression values (initial, absolute, relative)
|
|
221
|
+
- target values
|
|
222
|
+
- auction size in USD
|
|
223
|
+
- surplus/deficit token lists
|
|
224
|
+
|
|
225
|
+
## Example Walkthroughs
|
|
226
|
+
|
|
227
|
+
### Tracking Rebalance: 100% USDC → 50% DAI, 50% USDT
|
|
228
|
+
|
|
229
|
+
This example shows how limits change while weights stay constant throughout.
|
|
230
|
+
|
|
231
|
+
**Initial Setup:**
|
|
232
|
+
|
|
233
|
+
- Starting Folio: [1 USDC, 0 DAI, 0 USDT] per share
|
|
234
|
+
- Target: [0%, 50%, 50%] by value (using current prices)
|
|
235
|
+
- Weights (constant throughout): USDC=0, DAI=5e26, USDT=5e14 (in D27 format)
|
|
236
|
+
- Price Error: 10% for each token
|
|
237
|
+
- Initial Limits calculation:
|
|
238
|
+
- totalPortion = (0×0.1) + (0.5×0.1) + (0.5×0.1) = 0.1
|
|
239
|
+
- low = (1 - 0.1) × 1e18 = 9e17
|
|
240
|
+
- high = 1/(1 - 0.1) × 1e18 = 1.111...e18
|
|
241
|
+
- Note the asymmetry: high uses division, not simple addition
|
|
242
|
+
|
|
243
|
+
**Scenario 1 - Ejection Round:**
|
|
244
|
+
|
|
245
|
+
- Current Folio: [1 USDC, 0 DAI, 0 USDT] (still at start)
|
|
246
|
+
- Current Limits: {low: 9.5e17, spot: 1e18, high: 1.11e18}
|
|
247
|
+
- USDC marked for ejection (weight = 0)
|
|
248
|
+
- Progression: 0%
|
|
249
|
+
- Target: 95% (approaching finalStageAt)
|
|
250
|
+
- New Limits: {low: 9.5e17, spot: 1e18, high: 1.11e18} (adjusted for delta)
|
|
251
|
+
- Action: Sell USDC using limit-based pricing, buy DAI/USDT
|
|
252
|
+
|
|
253
|
+
**Scenario 2 - Final Round:**
|
|
254
|
+
|
|
255
|
+
- Current Folio: [0.05 USDC, 0.475 DAI, 0.475 USDT] by value
|
|
256
|
+
- Low limit of 0.9 constrained the first auction
|
|
257
|
+
- Progression: 95% (reached finalStageAt threshold)
|
|
258
|
+
- Round: FINAL (skipped PROGRESS since we hit finalStageAt)
|
|
259
|
+
- New Limits: {low: 1e18, spot: 1e18, high: 1e18} (delta = 0)
|
|
260
|
+
- Action: Sell remaining USDC, reach exact 50/50 DAI/USDT split
|
|
261
|
+
- Final Result: [0 USDC, 0.5 DAI, 0.5 USDT] by value
|
|
262
|
+
|
|
263
|
+
**Key Insight**: Throughout this rebalance, weights never changed. Only limits varied to achieve the target composition through the relationship `balance = weight × limit`.
|
|
264
|
+
|
|
265
|
+
### Native Rebalance: 100% USDC → 50% DAI, 50% USDT
|
|
266
|
+
|
|
267
|
+
This example shows how weights change while limits stay constant.
|
|
268
|
+
|
|
269
|
+
**Initial Setup:**
|
|
270
|
+
|
|
271
|
+
- Starting Folio: [1 USDC, 0 DAI, 0 USDT] per share
|
|
272
|
+
- Target Basket: [0%, 50%, 50%] by value (using historical prices)
|
|
273
|
+
- Limits (constant throughout): {low: 1e18, spot: 1e18, high: 1e18}
|
|
274
|
+
- Price Error: 10% for each token
|
|
275
|
+
- Initial Weights calculation:
|
|
276
|
+
- USDC: {low: 0, spot: 0, high: 0} (marked for ejection)
|
|
277
|
+
- DAI: {low: 4.5e26, spot: 5e26, high: 5.55e26} (D27 format)
|
|
278
|
+
- USDT: {low: 4.5e14, spot: 5e14, high: 5.55e14} (D27 format)
|
|
279
|
+
|
|
280
|
+
**Scenario 1 - Ejection Round:**
|
|
281
|
+
|
|
282
|
+
- Current Folio: [1 USDC, 0 DAI, 0 USDT] (still at start)
|
|
283
|
+
- Current Weights: USDC=0, DAI and USDT have ranges as above
|
|
284
|
+
- Progression: 0%
|
|
285
|
+
- Target: 95% (approaching finalStageAt)
|
|
286
|
+
- New Weights (with 5% delta applied):
|
|
287
|
+
- USDC: remains at 0
|
|
288
|
+
- DAI/USDT: adjusted ranges to enable trading
|
|
289
|
+
- Action: Sell USDC using weight-based pricing, buy DAI/USDT
|
|
290
|
+
|
|
291
|
+
**Scenario 2 - Final Round:**
|
|
292
|
+
|
|
293
|
+
- Current Folio: [0.05 USDC, 0.475 DAI, 0.475 USDT] by value
|
|
294
|
+
- Progression: 95% (reached finalStageAt threshold)
|
|
295
|
+
- Round: FINAL (skipped PROGRESS since we hit finalStageAt)
|
|
296
|
+
- New Weights (delta = 0):
|
|
297
|
+
- USDC: {low: 0, spot: 0, high: 0}
|
|
298
|
+
- DAI: {low: 5e26, spot: 5e26, high: 5e26} (converged to spot)
|
|
299
|
+
- USDT: {low: 5e14, spot: 5e14, high: 5e14} (converged to spot)
|
|
300
|
+
- Action: Sell remaining USDC, reach exact 50/50 DAI/USDT split
|
|
301
|
+
- Final Result: [0 USDC, 0.5 DAI, 0.5 USDT] by value
|
|
302
|
+
|
|
303
|
+
**Key Insight**: Throughout this rebalance, limits never changed from 1e18. Only weights varied to achieve the target composition.
|
|
304
|
+
|
|
305
|
+
## Price Control
|
|
306
|
+
|
|
307
|
+
The `PriceControl` enum affects price adjustments:
|
|
308
|
+
|
|
309
|
+
- **NONE**: No price manipulation allowed; prices remain at initial ranges
|
|
310
|
+
- **PARTIAL**: Prices can be adjusted within bounds based on current market prices and price error
|
|
311
|
+
|
|
312
|
+
## Key Formulas
|
|
313
|
+
|
|
314
|
+
### Target Basket Calculation
|
|
315
|
+
|
|
316
|
+
```javascript
|
|
317
|
+
targetBasket[i] = (initialWeight[i] × price[i]) / totalValue
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Progression Calculation
|
|
321
|
+
|
|
322
|
+
```javascript
|
|
323
|
+
absoluteProgression = Σ(min(actual[i], expected[i]) × price[i]) / shareValue
|
|
324
|
+
relativeProgression = (absolute - initial) / (1 - initial)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Delta Application and Uncertainty Propagation
|
|
328
|
+
|
|
329
|
+
The delta represents the uncertainty or spread in the rebalancing process. It's crucial to avoid double-counting this uncertainty:
|
|
330
|
+
|
|
331
|
+
- **For Limits**: `limit × (1 ± delta)`
|
|
332
|
+
|
|
333
|
+
- Uncertainty is directly applied to limits
|
|
334
|
+
|
|
335
|
+
- **For Weights**: `weight × (1 ± delta) / (limitRatio)`
|
|
336
|
+
- Where `limitRatio = actualLimit / spotLimit`
|
|
337
|
+
- The division by `limitRatio` accounts for uncertainty already propagated to limits
|
|
338
|
+
- This prevents double-counting the delta uncertainty
|
|
339
|
+
- Since `balance = weight × limit`, if limits already contain uncertainty, weights must be adjusted to avoid compounding it
|
|
340
|
+
|
|
341
|
+
## Error Handling
|
|
342
|
+
|
|
343
|
+
The algorithm includes several safety checks:
|
|
344
|
+
|
|
345
|
+
1. Spot prices must remain within initial price bounds
|
|
346
|
+
2. Progression should not go backwards (except for rounding)
|
|
347
|
+
3. BU value and share value should not differ by more than 10x
|
|
348
|
+
4. All array lengths must match
|
|
349
|
+
5. Minimum $1 trade value per token
|
|
350
|
+
|
|
351
|
+
## Usage Notes
|
|
352
|
+
|
|
353
|
+
1. **AUCTION_LAUNCHER** role uses `getOpenAuction()` results with `folio.openAuction()`
|
|
354
|
+
2. Non-launchers should use `folio.openAuctionUnrestricted()`
|
|
355
|
+
3. The algorithm is stateless - each call recalculates from current state
|
|
356
|
+
4. Prices should be passed differently for tracking vs native rebalances
|
|
357
|
+
5. The 10% buffer in ejection rounds prevents premature surplus depletion
|
package/package.json
CHANGED
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reserve-protocol/dtf-rebalance-lib",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Rebalancing library for DTFs in typescript",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
8
11
|
"files": [
|
|
9
12
|
"dist",
|
|
13
|
+
"docs",
|
|
10
14
|
"LICENSE.md",
|
|
11
15
|
"README.md"
|
|
12
16
|
],
|
|
17
|
+
"workspaces": [
|
|
18
|
+
"packages/*"
|
|
19
|
+
],
|
|
13
20
|
"scripts": {
|
|
14
21
|
"clean": "rm -rf dist",
|
|
15
22
|
"compile": "hardhat compile",
|
|
23
|
+
"compile:dtf-artifacts": "forge compile --root node_modules/@reserve-protocol/reserve-index-dtf -R forge-std=$PWD/node_modules/forge-std/src/ -R @prb/math=$PWD/node_modules/@prb/math/ -R @reserve-protocol/trusted-fillers=$PWD/node_modules/@reserve-protocol/trusted-fillers/ contracts/Folio.sol contracts/periphery/FolioLens.sol contracts/utils/RebalancingLib.sol contracts/folio/FolioProxy.sol",
|
|
16
24
|
"build": "npm run clean && tsc --project tsconfig.build.json",
|
|
25
|
+
"build:internal-tools": "npm run build -w @reserve-protocol/dtf-rebalance-tools",
|
|
26
|
+
"build:tools": "npm run build:internal-tools",
|
|
17
27
|
"prepublishOnly": "npm run build",
|
|
18
28
|
"test": "npm run test:unit && npm run test:e2e",
|
|
19
29
|
"test:unit": "node --test --require ts-node/register test/**/unit.test.ts",
|
|
20
|
-
"
|
|
30
|
+
"pretest:e2e": "npm run compile:dtf-artifacts",
|
|
31
|
+
"test:e2e": "hardhat test packages/tools/test/*.test.ts --bail"
|
|
21
32
|
},
|
|
22
33
|
"publishConfig": {
|
|
23
34
|
"access": "public"
|
|
@@ -43,8 +54,8 @@
|
|
|
43
54
|
"devDependencies": {
|
|
44
55
|
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
|
|
45
56
|
"@openzeppelin/contracts": "^5.3.0",
|
|
46
|
-
"@reserve-protocol/reserve-index-dtf": "github:reserve-protocol/reserve-index-dtf#
|
|
47
|
-
"@types/node": "^
|
|
57
|
+
"@reserve-protocol/reserve-index-dtf": "github:reserve-protocol/reserve-index-dtf#5db814757dc0b0649483a1c2c29e54e687010003",
|
|
58
|
+
"@types/node": "^24.0.0",
|
|
48
59
|
"dotenv": "^16.5.0",
|
|
49
60
|
"hardhat": "^2.24.1",
|
|
50
61
|
"prettier": "^3.5.3",
|