@reserve-protocol/dtf-rebalance-lib 3.2.0 → 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 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.
@@ -24,12 +24,13 @@ const getOpenAuction = (rebalance, _supply, _initialSupply, _initialAssets = [],
24
24
  if (debug) {
25
25
  console.log("getOpenAuction", rebalance, _supply, _initialSupply, _initialAssets, _targetBasket, _assets, _decimals, _prices, _priceError, _finalStageAt);
26
26
  }
27
- if (rebalance.tokens.length != _targetBasket.length ||
27
+ if (rebalance.tokens.length != _initialAssets.length ||
28
+ _initialAssets.length != _targetBasket.length ||
28
29
  _targetBasket.length != _assets.length ||
29
30
  _assets.length != _decimals.length ||
30
31
  _decimals.length != _prices.length ||
31
32
  _prices.length != _priceError.length) {
32
- throw new Error("length mismatch");
33
+ throw new Error("getOpenAuction: length mismatch");
33
34
  }
34
35
  if (_finalStageAt > 1) {
35
36
  throw new Error("finalStageAt must be less than 1");
@@ -23,6 +23,13 @@ const getStartRebalance = (_supply, tokens, _assets, decimals, _targetBasket, _p
23
23
  if (debug) {
24
24
  console.log("getStartRebalance", _supply, tokens, _assets, decimals, _targetBasket, _prices, _priceError, weightControl, deferWeights);
25
25
  }
26
+ if (tokens.length != _assets.length ||
27
+ _assets.length != decimals.length ||
28
+ decimals.length != _targetBasket.length ||
29
+ _targetBasket.length != _prices.length ||
30
+ _prices.length != _priceError.length) {
31
+ throw new Error("getStartRebalance: length mismatch");
32
+ }
26
33
  if (deferWeights && !weightControl) {
27
34
  throw new Error("deferWeights is not supported for tracking DTFs");
28
35
  }
@@ -24,12 +24,13 @@ const getOpenAuction = (rebalance, _supply, _initialSupply, _initialAssets = [],
24
24
  if (debug) {
25
25
  console.log("getOpenAuction", rebalance, _supply, _initialSupply, _initialAssets, _targetBasket, _assets, _decimals, _prices, _priceError, _finalStageAt);
26
26
  }
27
- if (rebalance.tokens.length != _targetBasket.length ||
27
+ if (rebalance.tokens.length != _initialAssets.length ||
28
+ _initialAssets.length != _targetBasket.length ||
28
29
  _targetBasket.length != _assets.length ||
29
30
  _assets.length != _decimals.length ||
30
31
  _decimals.length != _prices.length ||
31
32
  _prices.length != _priceError.length) {
32
- throw new Error("length mismatch");
33
+ throw new Error("getOpenAuction: length mismatch");
33
34
  }
34
35
  if (_finalStageAt > 1) {
35
36
  throw new Error("finalStageAt must be less than 1");
@@ -24,6 +24,14 @@ const getStartRebalance = (_supply, tokens, _assets, decimals, _targetBasket, _p
24
24
  if (debug) {
25
25
  console.log("getStartRebalance", _supply, tokens, _assets, decimals, _targetBasket, _prices, _priceError, _maxAuctionSizes, weightControl, deferWeights);
26
26
  }
27
+ if (tokens.length != _assets.length ||
28
+ _assets.length != decimals.length ||
29
+ decimals.length != _targetBasket.length ||
30
+ _targetBasket.length != _prices.length ||
31
+ _prices.length != _priceError.length ||
32
+ _priceError.length != _maxAuctionSizes.length) {
33
+ throw new Error("getStartRebalance: length mismatch");
34
+ }
27
35
  if (deferWeights && !weightControl) {
28
36
  throw new Error("deferWeights is not supported for tracking DTFs");
29
37
  }
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";
package/dist/utils.js CHANGED
@@ -18,6 +18,9 @@ exports.Decimal = decimal_js_light_1.default.clone({ precision: 100 });
18
18
  * @returns D18{1} Current basket, total will be around 1e18 but not exactly
19
19
  */
20
20
  const getBasketDistribution = (_bals, _prices, decimals) => {
21
+ if (_bals.length !== _prices.length || _bals.length !== decimals.length) {
22
+ throw new Error("getBasketDistribution: length mismatch");
23
+ }
21
24
  const decimalScale = decimals.map((d) => new exports.Decimal(`1e${d}`));
22
25
  // {wholeTok} = {tok} / {tok/wholeTok}
23
26
  const bals = _bals.map((bal, i) => new exports.Decimal(bal.toString()).div(decimalScale[i]));
@@ -39,6 +42,9 @@ exports.getBasketDistribution = getBasketDistribution;
39
42
  * @returns {1} Basket accuracy
40
43
  */
41
44
  const getBasketAccuracy = (_bals, _prices, _decimals, _weights) => {
45
+ if (_bals.length !== _prices.length || _bals.length !== _decimals.length || _bals.length !== _weights.length) {
46
+ throw new Error("getBasketAccuracy: length mismatch");
47
+ }
42
48
  const decimalScale = _decimals.map((d) => new exports.Decimal(`1e${d}`));
43
49
  // {USD/wholeTok} = {USD/wholeTok}
44
50
  const prices = _prices.map((a) => new exports.Decimal(a.toString()));
@@ -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.2.0",
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
- "test:e2e": "hardhat test test/*.test.ts --bail"
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#a240822d7a02520def5564668ac7699b0520bdae",
47
- "@types/node": "^20.0.0",
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",