@strkfarm/sdk 2.0.0-dev.3 → 2.0.0-dev.31
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/dist/cli.js +190 -36
- package/dist/cli.mjs +188 -34
- package/dist/index.browser.global.js +78475 -45620
- package/dist/index.browser.mjs +19580 -9901
- package/dist/index.d.ts +3763 -1424
- package/dist/index.js +20977 -11063
- package/dist/index.mjs +20945 -11087
- package/package.json +1 -1
- package/src/data/avnu.abi.json +840 -0
- package/src/data/ekubo-price-fethcer.abi.json +265 -0
- package/src/dataTypes/_bignumber.ts +13 -4
- package/src/dataTypes/bignumber.browser.ts +6 -1
- package/src/dataTypes/bignumber.node.ts +5 -1
- package/src/dataTypes/index.ts +3 -2
- package/src/dataTypes/mynumber.ts +141 -0
- package/src/global.ts +76 -41
- package/src/index.browser.ts +2 -1
- package/src/interfaces/common.tsx +175 -3
- package/src/modules/ExtendedWrapperSDk/types.ts +28 -5
- package/src/modules/ExtendedWrapperSDk/wrapper.ts +275 -59
- package/src/modules/apollo-client-config.ts +28 -0
- package/src/modules/avnu.ts +4 -4
- package/src/modules/ekubo-pricer.ts +79 -0
- package/src/modules/ekubo-quoter.ts +48 -30
- package/src/modules/erc20.ts +17 -0
- package/src/modules/harvests.ts +43 -29
- package/src/modules/pragma.ts +23 -8
- package/src/modules/pricer-from-api.ts +156 -15
- package/src/modules/pricer-lst.ts +1 -1
- package/src/modules/pricer.ts +40 -4
- package/src/modules/pricerBase.ts +2 -1
- package/src/node/deployer.ts +36 -1
- package/src/node/pricer-redis.ts +2 -1
- package/src/strategies/base-strategy.ts +78 -10
- package/src/strategies/ekubo-cl-vault.tsx +906 -347
- package/src/strategies/factory.ts +159 -0
- package/src/strategies/index.ts +7 -1
- package/src/strategies/registry.ts +239 -0
- package/src/strategies/sensei.ts +335 -7
- package/src/strategies/svk-strategy.ts +97 -27
- package/src/strategies/types.ts +4 -0
- package/src/strategies/universal-adapters/adapter-utils.ts +2 -1
- package/src/strategies/universal-adapters/avnu-adapter.ts +180 -265
- package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
- package/src/strategies/universal-adapters/common-adapter.ts +206 -203
- package/src/strategies/universal-adapters/extended-adapter.ts +490 -316
- package/src/strategies/universal-adapters/index.ts +11 -8
- package/src/strategies/universal-adapters/svk-troves-adapter.ts +364 -0
- package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
- package/src/strategies/universal-adapters/usdc<>usdce-adapter.ts +200 -0
- package/src/strategies/universal-adapters/vesu-adapter.ts +120 -82
- package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +476 -0
- package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +1067 -704
- package/src/strategies/universal-adapters/vesu-position-common.ts +251 -0
- package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
- package/src/strategies/universal-lst-muliplier-strategy.tsx +397 -204
- package/src/strategies/universal-strategy.tsx +1426 -1173
- package/src/strategies/vesu-extended-strategy/services/executionService.ts +2233 -0
- package/src/strategies/vesu-extended-strategy/services/extended-vesu-state-manager.ts +4087 -0
- package/src/strategies/vesu-extended-strategy/services/ltv-imbalance-rebalance-math.ts +783 -0
- package/src/strategies/vesu-extended-strategy/services/operationService.ts +38 -16
- package/src/strategies/vesu-extended-strategy/types/transaction-metadata.ts +88 -0
- package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +1 -0
- package/src/strategies/vesu-extended-strategy/utils/constants.ts +5 -6
- package/src/strategies/vesu-extended-strategy/utils/helper.ts +259 -103
- package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +688 -817
- package/src/strategies/vesu-rebalance.tsx +255 -152
- package/src/utils/cacheClass.ts +11 -2
- package/src/utils/health-factor-math.ts +4 -1
- package/src/utils/index.ts +3 -1
- package/src/utils/logger.browser.ts +22 -4
- package/src/utils/logger.node.ts +259 -24
- package/src/utils/starknet-call-parser.ts +1036 -0
- package/src/utils/strategy-utils.ts +61 -0
- package/src/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// rebalance.ts — Delta-Neutral Rebalancing Solver
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// Manages two matched BTC positions across Extended (leveraged perp)
|
|
6
|
+
// and Vesu (collateral/debt lending), ensuring:
|
|
7
|
+
// 1. Margin health on both protocols
|
|
8
|
+
// 2. Equal BTC exposure on both sides
|
|
9
|
+
//
|
|
10
|
+
// Architecture:
|
|
11
|
+
// Phase 1 — Try to fix margin deficits using 5 funding sources
|
|
12
|
+
// Phase 2 — Unified position + margin solver
|
|
13
|
+
// Simultaneously resolves position imbalance AND any
|
|
14
|
+
// remaining margin deficits from Phase 1 by finding
|
|
15
|
+
// the optimal common position F that satisfies both
|
|
16
|
+
// Extended margin and Vesu HF constraints.
|
|
17
|
+
//
|
|
18
|
+
// Key leverage mechanics:
|
|
19
|
+
// Extended: depositing $x of margin creates $x × leverage of position.
|
|
20
|
+
// → cost to create gapBtc = gapBtc × price / leverage
|
|
21
|
+
// → margin released by closing = closeBtc × price / leverage
|
|
22
|
+
//
|
|
23
|
+
// Vesu: depositing $x of equity creates $x / (1 - targetLTV) of position.
|
|
24
|
+
// → cost to create gapBtc = gapBtc × price × (1 - targetLTV)
|
|
25
|
+
// → equity freed by closing = closeBtc × price × (1 - currentLTV)
|
|
26
|
+
// (because debt proportional to the close must be repaid first)
|
|
27
|
+
//
|
|
28
|
+
// All functions are pure except drawFunds which mutates the pool
|
|
29
|
+
// to track remaining balances across sequential draws.
|
|
30
|
+
// ============================================================
|
|
31
|
+
|
|
32
|
+
import { logger } from "@/utils";
|
|
33
|
+
|
|
34
|
+
// ---- Types ----
|
|
35
|
+
|
|
36
|
+
export interface RebalanceConfig {
|
|
37
|
+
positionPrecision: number; // decimal places for BTC rounding, e.g. 4
|
|
38
|
+
hfBuffer: number; // HF tolerance before acting, e.g. 0.05
|
|
39
|
+
/**
|
|
40
|
+
* USD below this cannot be executed as a standalone transfer/borrow/repay step
|
|
41
|
+
* (matches execution-layer dust threshold). Default 0 keeps pure math tests unchanged.
|
|
42
|
+
*/
|
|
43
|
+
minRoutableUsd?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ExtendedState {
|
|
47
|
+
positionBtc: number; // current BTC position size
|
|
48
|
+
equity: number; // current equity in USD
|
|
49
|
+
avlWithdraw: number; // withdrawable surplus in USD (0 if equity < idealMargin)
|
|
50
|
+
upnl: number; // unrealized PnL in USD (0 if equity < idealMargin)
|
|
51
|
+
leverage: number; // target leverage, typically 3
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface VesuState {
|
|
55
|
+
positionBtc: number; // BTC collateral position
|
|
56
|
+
debt: number; // outstanding debt in debt-token units
|
|
57
|
+
debtPrice: number; // USD price per debt token
|
|
58
|
+
maxLTV: number; // liquidation LTV threshold, e.g. 0.8
|
|
59
|
+
targetHF: number; // target health factor, e.g. 1.25
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Mutable pool of available funding sources, drawn in priority order. */
|
|
63
|
+
export interface FundingPool {
|
|
64
|
+
vaUsd: number; // yield aggregator (VA / Troves) balance
|
|
65
|
+
walletUsd: number; // hot wallet balance
|
|
66
|
+
vesuBorrowCapacity: number; // remaining borrow headroom on Vesu, in USD
|
|
67
|
+
extAvlWithdraw: number; // withdrawable from Extended
|
|
68
|
+
extUpnl: number; // realizable UPNL from Extended
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type FundKey = keyof FundingPool;
|
|
72
|
+
|
|
73
|
+
interface RebalanceInputs {
|
|
74
|
+
ext: ExtendedState;
|
|
75
|
+
vesu: VesuState;
|
|
76
|
+
btcPrice: number;
|
|
77
|
+
funding: FundingPool;
|
|
78
|
+
config: RebalanceConfig;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** All deltas are signed: negative = reducing/withdrawing, positive = increasing/depositing. */
|
|
82
|
+
interface RebalanceDeltas {
|
|
83
|
+
dExtPosition: number; // BTC change on Extended (negative = close)
|
|
84
|
+
dVesuPosition: number; // BTC change on Vesu (negative = close)
|
|
85
|
+
dVesuDebt: number; // debt-token change on Vesu (negative = repay)
|
|
86
|
+
dExtAvlWithdraw: number; // USD change in Extended withdrawable equity
|
|
87
|
+
dExtUpnl: number; // USD change in Extended UPNL (negative = realized)
|
|
88
|
+
dVaUsd: number; // USD change in VA balance (negative = withdrawn)
|
|
89
|
+
dWalletUsd: number; // USD change in wallet (negative = withdrawn)
|
|
90
|
+
dVesuBorrowCapacity: number; // USD change in Vesu borrow room (negative = used)
|
|
91
|
+
dTransferVesuToExt: number; // net USD transferred from Vesu to Extended
|
|
92
|
+
// positive = Vesu sends to Extended
|
|
93
|
+
// negative = Extended sends to Vesu
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---- Precision helpers ----
|
|
97
|
+
|
|
98
|
+
/** Ceil a BTC amount up to `precision` decimal places (used when closing positions). */
|
|
99
|
+
function ceilBtc(v: number, precision: number): number {
|
|
100
|
+
const f = 10 ** precision;
|
|
101
|
+
return Math.ceil(v * f) / f;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Floor a BTC amount down to `precision` decimal places (used for target positions). */
|
|
105
|
+
function floorBtc(v: number, precision: number): number {
|
|
106
|
+
const f = 10 ** precision;
|
|
107
|
+
return Math.floor(v * f) / f;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Check if a BTC amount is below the smallest representable unit. */
|
|
111
|
+
function isNegligible(btc: number, precision: number): boolean {
|
|
112
|
+
return Math.abs(btc) < 10 ** -precision;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- Health computations ----
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Minimum equity required on Extended for a given position.
|
|
119
|
+
* idealMargin = positionValue / leverage
|
|
120
|
+
*/
|
|
121
|
+
function computeExtIdealMargin(posBtc: number, leverage: number, price: number): number {
|
|
122
|
+
return (posBtc * price) / leverage;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* How much USD equity Extended is short of its ideal margin.
|
|
127
|
+
* Returns 0 if equity is sufficient.
|
|
128
|
+
*/
|
|
129
|
+
function computeExtDeficit(ext: ExtendedState, price: number): number {
|
|
130
|
+
return Math.max(0, computeExtIdealMargin(ext.positionBtc, ext.leverage, price) - ext.equity);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Vesu health factor: HF = maxLTV / currentLTV.
|
|
135
|
+
* Higher is healthier. Returns Infinity if no debt or no collateral.
|
|
136
|
+
*/
|
|
137
|
+
function computeVesuHF(vesu: VesuState, price: number): number {
|
|
138
|
+
const collateralUsd = vesu.positionBtc * price;
|
|
139
|
+
if (collateralUsd === 0) return Infinity;
|
|
140
|
+
const ltv = (vesu.debt * vesu.debtPrice) / collateralUsd;
|
|
141
|
+
if (ltv === 0) return Infinity;
|
|
142
|
+
return vesu.maxLTV / ltv;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Debt (in token units) that must be repaid to bring Vesu HF back to targetHF,
|
|
147
|
+
* assuming collateral position stays constant.
|
|
148
|
+
*
|
|
149
|
+
* Derivation: targetHF = maxLTV / targetLTV
|
|
150
|
+
* → targetLTV = maxLTV / targetHF
|
|
151
|
+
* → requiredDebt = targetLTV × collateralUsd / debtPrice
|
|
152
|
+
* → repay = currentDebt - requiredDebt
|
|
153
|
+
*/
|
|
154
|
+
function computeVesuDebtRepay(vesu: VesuState, price: number): number {
|
|
155
|
+
const targetLTV = vesu.maxLTV / vesu.targetHF;
|
|
156
|
+
const collateralUsd = vesu.positionBtc * price;
|
|
157
|
+
const requiredDebt = (targetLTV * collateralUsd) / vesu.debtPrice;
|
|
158
|
+
return Math.max(0, vesu.debt - requiredDebt);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Compute the target LTV for Vesu based on its targetHF.
|
|
163
|
+
* targetLTV = maxLTV / targetHF
|
|
164
|
+
*
|
|
165
|
+
* This is the LTV we aim to maintain after any position changes.
|
|
166
|
+
* Used to compute equity costs, debt changes, and the unified solver.
|
|
167
|
+
*/
|
|
168
|
+
function computeVesuTargetLTV(vesu: VesuState): number {
|
|
169
|
+
return vesu.maxLTV / vesu.targetHF;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Compute the USD equity cost to grow Vesu position by `gapBtc`.
|
|
174
|
+
*
|
|
175
|
+
* Vesu is a leveraged lending position. Depositing $x as collateral,
|
|
176
|
+
* borrowing at targetLTV, recycling proceeds → converges to:
|
|
177
|
+
*
|
|
178
|
+
* totalPosition = equity / (1 - targetLTV)
|
|
179
|
+
* → equityCost = gapBtc × btcPrice × (1 - targetLTV)
|
|
180
|
+
*/
|
|
181
|
+
function computeVesuGrowthCost(gapBtc: number, vesu: VesuState, price: number): number {
|
|
182
|
+
const targetLTV = computeVesuTargetLTV(vesu);
|
|
183
|
+
return gapBtc * price * (1 - targetLTV);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Compute new debt (in token units) created when growing Vesu by `gapBtc`.
|
|
188
|
+
* newDebtTokens = gapBtc × btcPrice × targetLTV / debtPrice
|
|
189
|
+
*/
|
|
190
|
+
function computeVesuGrowthDebt(gapBtc: number, vesu: VesuState, price: number): number {
|
|
191
|
+
const targetLTV = computeVesuTargetLTV(vesu);
|
|
192
|
+
return (gapBtc * price * targetLTV) / vesu.debtPrice;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Given available USD equity, compute max BTC position growth on Vesu.
|
|
197
|
+
* gapBtc = equityUsd / (btcPrice × (1 - targetLTV))
|
|
198
|
+
*/
|
|
199
|
+
function computeVesuGrowthFromEquity(equityUsd: number, vesu: VesuState, price: number): number {
|
|
200
|
+
const targetLTV = computeVesuTargetLTV(vesu);
|
|
201
|
+
return equityUsd / (price * (1 - targetLTV));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---- Delta helpers ----
|
|
205
|
+
|
|
206
|
+
function emptyDeltas(): RebalanceDeltas {
|
|
207
|
+
return {
|
|
208
|
+
dExtPosition: 0, dVesuPosition: 0, dVesuDebt: 0,
|
|
209
|
+
dExtAvlWithdraw: 0, dExtUpnl: 0, dVaUsd: 0,
|
|
210
|
+
dWalletUsd: 0, dVesuBorrowCapacity: 0, dTransferVesuToExt: 0,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Element-wise addition of two delta sets. */
|
|
215
|
+
function mergeDeltas(a: RebalanceDeltas, b: RebalanceDeltas): RebalanceDeltas {
|
|
216
|
+
return {
|
|
217
|
+
dExtPosition: a.dExtPosition + b.dExtPosition,
|
|
218
|
+
dVesuPosition: a.dVesuPosition + b.dVesuPosition,
|
|
219
|
+
dVesuDebt: a.dVesuDebt + b.dVesuDebt,
|
|
220
|
+
dExtAvlWithdraw: a.dExtAvlWithdraw + b.dExtAvlWithdraw,
|
|
221
|
+
dExtUpnl: a.dExtUpnl + b.dExtUpnl,
|
|
222
|
+
dVaUsd: a.dVaUsd + b.dVaUsd,
|
|
223
|
+
dWalletUsd: a.dWalletUsd + b.dWalletUsd,
|
|
224
|
+
dVesuBorrowCapacity: a.dVesuBorrowCapacity + b.dVesuBorrowCapacity,
|
|
225
|
+
dTransferVesuToExt: a.dTransferVesuToExt + b.dTransferVesuToExt,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---- Fund drawing engine ----
|
|
230
|
+
|
|
231
|
+
interface DrawResult {
|
|
232
|
+
draws: Partial<Record<FundKey, number>>; // how much was taken from each source
|
|
233
|
+
filled: number; // total USD successfully drawn
|
|
234
|
+
unmet: number; // remaining USD still needed
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Portion of `need` we can take from `avail` such that execution can actually move it.
|
|
239
|
+
* Chunks at or below `minRoutable` are skipped (caller may satisfy `need` from another source
|
|
240
|
+
* or leave it unmet); matches VA/wallet/borrow dust rules in the state manager.
|
|
241
|
+
*/
|
|
242
|
+
function routableDrawAmount(avail: number, need: number, minRoutable: number): number {
|
|
243
|
+
if (avail <= 0 || need <= 0) return 0;
|
|
244
|
+
const raw = Math.min(avail, need);
|
|
245
|
+
if (raw <= 0) return 0;
|
|
246
|
+
if (minRoutable <= 0) return raw;
|
|
247
|
+
return raw > minRoutable ? raw : 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Draw `need` USD from funding sources in strict priority order.
|
|
252
|
+
* Mutates `pool` balances so subsequent draws see reduced availability.
|
|
253
|
+
* Each source contributes a routable amount (see {@link routableDrawAmount}).
|
|
254
|
+
*/
|
|
255
|
+
function drawFunds(need: number, keys: FundKey[], pool: FundingPool, minRoutable: number): DrawResult {
|
|
256
|
+
const draws: Partial<Record<FundKey, number>> = {};
|
|
257
|
+
let unmet = need;
|
|
258
|
+
for (const k of keys) {
|
|
259
|
+
if (unmet <= 0) break;
|
|
260
|
+
const avail = pool[k];
|
|
261
|
+
const take = routableDrawAmount(avail, unmet, minRoutable);
|
|
262
|
+
if (take <= 0) continue;
|
|
263
|
+
draws[k] = take;
|
|
264
|
+
pool[k] -= take;
|
|
265
|
+
unmet -= take;
|
|
266
|
+
}
|
|
267
|
+
return { draws, filled: need - unmet, unmet };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Withdraw + uPnL are separate buckets but execution moves them in one EXTENDED_TO_WALLET leg
|
|
272
|
+
* (waterfall inside the budget). Apply {@link routableDrawAmount} to their **sum**, then drain
|
|
273
|
+
* avl-withdraw first, then uPnL — matches spendExtAvailTrade in the state-manager budget.
|
|
274
|
+
*/
|
|
275
|
+
function drawExtendedAggregated(need: number, pool: FundingPool, minRoutable: number): DrawResult {
|
|
276
|
+
const draws: Partial<Record<FundKey, number>> = {};
|
|
277
|
+
if (need <= 0) return { draws, filled: 0, unmet: 0 };
|
|
278
|
+
const totalCap = Math.max(0, pool.extAvlWithdraw) + Math.max(0, pool.extUpnl);
|
|
279
|
+
const want = routableDrawAmount(totalCap, need, minRoutable);
|
|
280
|
+
if (want <= 0) return { draws, filled: 0, unmet: need };
|
|
281
|
+
const fromAvl = Math.min(Math.max(0, pool.extAvlWithdraw), want);
|
|
282
|
+
pool.extAvlWithdraw -= fromAvl;
|
|
283
|
+
const fromUpnl = Math.min(Math.max(0, pool.extUpnl), want - fromAvl);
|
|
284
|
+
pool.extUpnl -= fromUpnl;
|
|
285
|
+
if (fromAvl > 0) draws.extAvlWithdraw = fromAvl;
|
|
286
|
+
if (fromUpnl > 0) draws.extUpnl = fromUpnl;
|
|
287
|
+
const filled = fromAvl + fromUpnl;
|
|
288
|
+
return { draws, filled, unmet: need - filled };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Sum draw amounts for a subset of keys (e.g. all Extended-sourced draws). */
|
|
292
|
+
function sumKeys(draws: Partial<Record<FundKey, number>>, keys: FundKey[]): number {
|
|
293
|
+
return keys.reduce((s, k) => s + (draws[k] ?? 0), 0);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Record fund draws as output deltas.
|
|
298
|
+
* sign = -1: sources are being drained (normal case).
|
|
299
|
+
* sign = +1: sources are being credited (e.g. freed margin returning).
|
|
300
|
+
*/
|
|
301
|
+
function applyDrawsToDeltas(d: RebalanceDeltas, draws: Partial<Record<FundKey, number>>, sign: number) {
|
|
302
|
+
d.dVaUsd += sign * (draws.vaUsd ?? 0);
|
|
303
|
+
d.dWalletUsd += sign * (draws.walletUsd ?? 0);
|
|
304
|
+
d.dVesuBorrowCapacity += sign * (draws.vesuBorrowCapacity ?? 0);
|
|
305
|
+
d.dExtAvlWithdraw += sign * (draws.extAvlWithdraw ?? 0);
|
|
306
|
+
d.dExtUpnl += sign * (draws.extUpnl ?? 0);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================================
|
|
310
|
+
// PHASE 1: Margin health (best-effort from funding sources)
|
|
311
|
+
// ============================================================
|
|
312
|
+
//
|
|
313
|
+
// Step 1a: Extended equity deficit → draw wallet → VA → vesuBorrowable
|
|
314
|
+
// Step 1b: Vesu HF below threshold → repay debt from VA → wallet → extAvlWith → extUpnl
|
|
315
|
+
//
|
|
316
|
+
// Returns deltas + any unmet deficit amounts. Phase 2 handles the rest.
|
|
317
|
+
// ============================================================
|
|
318
|
+
|
|
319
|
+
interface Phase1Result {
|
|
320
|
+
deltas: RebalanceDeltas;
|
|
321
|
+
extDeficitRemaining: number; // USD still needed for Extended margin
|
|
322
|
+
vesuRepayRemaining: number; // USD still needed for Vesu debt repayment
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Fix Extended margin deficit by drawing from external sources.
|
|
327
|
+
* Priority: wallet → VA → vesuBorrowCapacity.
|
|
328
|
+
* Funds received increase Extended equity (dExtAvlWithdraw).
|
|
329
|
+
*/
|
|
330
|
+
function fixExtMargin(ext: ExtendedState, price: number, pool: FundingPool, config: RebalanceConfig): { d: RebalanceDeltas; unmet: number } {
|
|
331
|
+
const d = emptyDeltas();
|
|
332
|
+
const deficit = computeExtDeficit(ext, price);
|
|
333
|
+
if (deficit <= 0) return { d, unmet: 0 };
|
|
334
|
+
|
|
335
|
+
const minR = config.minRoutableUsd ?? 0;
|
|
336
|
+
const { draws, filled, unmet } = drawFunds(deficit, ['walletUsd', 'vaUsd', 'vesuBorrowCapacity'], pool, minR);
|
|
337
|
+
applyDrawsToDeltas(d, draws, -1);
|
|
338
|
+
|
|
339
|
+
// All filled amount lands as Extended equity
|
|
340
|
+
d.dExtAvlWithdraw += filled;
|
|
341
|
+
|
|
342
|
+
// Funds borrowed from Vesu and sent to Extended = positive transfer
|
|
343
|
+
const fromVesu = draws.vesuBorrowCapacity ?? 0;
|
|
344
|
+
if (fromVesu > 0) d.dTransferVesuToExt += fromVesu;
|
|
345
|
+
|
|
346
|
+
return { d, unmet };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Fix Vesu health factor by repaying debt from available sources.
|
|
351
|
+
* Only triggers if HF < targetHF - hfBuffer (avoids churn near target).
|
|
352
|
+
* Priority: VA → wallet → extAvlWithdraw → extUpnl.
|
|
353
|
+
* Repaid amount reduces Vesu debt (dVesuDebt).
|
|
354
|
+
*/
|
|
355
|
+
function fixVesuMargin(vesu: VesuState, price: number, pool: FundingPool, hfBuffer: number, config: RebalanceConfig): { d: RebalanceDeltas; unmet: number } {
|
|
356
|
+
const d = emptyDeltas();
|
|
357
|
+
const hf = computeVesuHF(vesu, price);
|
|
358
|
+
|
|
359
|
+
// Only act if HF is meaningfully below target (buffer prevents churn)
|
|
360
|
+
if (hf >= vesu.targetHF - hfBuffer) return { d, unmet: 0 };
|
|
361
|
+
|
|
362
|
+
const repayTokens = computeVesuDebtRepay(vesu, price);
|
|
363
|
+
const repayUsd = repayTokens * vesu.debtPrice;
|
|
364
|
+
|
|
365
|
+
const minR = config.minRoutableUsd ?? 0;
|
|
366
|
+
const rMain = drawFunds(repayUsd, ['vaUsd', 'walletUsd'], pool, minR);
|
|
367
|
+
applyDrawsToDeltas(d, rMain.draws, -1);
|
|
368
|
+
const rExt = drawExtendedAggregated(rMain.unmet, pool, minR);
|
|
369
|
+
applyDrawsToDeltas(d, rExt.draws, -1);
|
|
370
|
+
const filled = rMain.filled + rExt.filled;
|
|
371
|
+
const unmet = rExt.unmet;
|
|
372
|
+
|
|
373
|
+
// Convert filled USD back to debt tokens for the repayment delta
|
|
374
|
+
d.dVesuDebt -= filled / vesu.debtPrice;
|
|
375
|
+
|
|
376
|
+
// Funds withdrawn from Extended and sent to Vesu = negative transfer
|
|
377
|
+
const fromExt = sumKeys(rExt.draws, ['extAvlWithdraw', 'extUpnl']);
|
|
378
|
+
if (fromExt > 0) d.dTransferVesuToExt -= fromExt;
|
|
379
|
+
|
|
380
|
+
return { d, unmet };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Phase 1 orchestrator: fix Extended margin, then Vesu margin.
|
|
385
|
+
* Extended draws wallet/VA first; Vesu draws from remaining pool.
|
|
386
|
+
* Returns combined deltas and any unresolved deficits.
|
|
387
|
+
*/
|
|
388
|
+
function phase1(ext: ExtendedState, vesu: VesuState, price: number, pool: FundingPool, config: RebalanceConfig): Phase1Result {
|
|
389
|
+
const extResult = fixExtMargin(ext, price, pool, config);
|
|
390
|
+
const vesuResult = fixVesuMargin(vesu, price, pool, config.hfBuffer, config);
|
|
391
|
+
return {
|
|
392
|
+
deltas: mergeDeltas(extResult.d, vesuResult.d),
|
|
393
|
+
extDeficitRemaining: extResult.unmet,
|
|
394
|
+
vesuRepayRemaining: vesuResult.unmet,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ============================================================
|
|
399
|
+
// PHASE 2: Unified position + margin solver
|
|
400
|
+
// ============================================================
|
|
401
|
+
//
|
|
402
|
+
// This phase simultaneously resolves:
|
|
403
|
+
// a) Position imbalance (extPos ≠ vesuPos)
|
|
404
|
+
// b) Any remaining margin deficits from Phase 1
|
|
405
|
+
//
|
|
406
|
+
// It works by finding F, the optimal final common position for both
|
|
407
|
+
// sides, such that both Extended margin and Vesu HF are satisfied.
|
|
408
|
+
//
|
|
409
|
+
// The approach has two steps:
|
|
410
|
+
// Step 1: Try to close the gap using available funding sources
|
|
411
|
+
// (grow the lagging side toward the leading side).
|
|
412
|
+
// Step 2: Whatever gap or deficit remains, solve for F using the
|
|
413
|
+
// unified equation that accounts for both protocols'
|
|
414
|
+
// leverage, existing positions, and existing equity/debt.
|
|
415
|
+
//
|
|
416
|
+
// ---- DERIVATION OF THE UNIFIED EQUATION ----
|
|
417
|
+
//
|
|
418
|
+
// After Phase 1, we have effective state:
|
|
419
|
+
// extPos, vesuPos — BTC positions
|
|
420
|
+
// extEquity — Extended equity in USD
|
|
421
|
+
// vesuDebt — Vesu debt in token units
|
|
422
|
+
//
|
|
423
|
+
// We want to find F (final common position) such that both sides
|
|
424
|
+
// can sustain it. Both dx = F - extPos and dy = F - vesuPos can
|
|
425
|
+
// be positive (grow) or negative (shrink).
|
|
426
|
+
//
|
|
427
|
+
// CONSTRAINT 1 — Position equality:
|
|
428
|
+
// extPos + dx = vesuPos + dy = F (by definition)
|
|
429
|
+
//
|
|
430
|
+
// CONSTRAINT 2 — Extended margin health:
|
|
431
|
+
// The equity available to Extended after the rebalance must cover
|
|
432
|
+
// the ideal margin on the final position.
|
|
433
|
+
//
|
|
434
|
+
// extEquity + T ≥ F × price / extLeverage
|
|
435
|
+
//
|
|
436
|
+
// Where T is the net transfer from Vesu to Extended.
|
|
437
|
+
// - If T > 0: Vesu sends surplus equity to Extended
|
|
438
|
+
// - If T < 0: Extended sends surplus margin to Vesu
|
|
439
|
+
//
|
|
440
|
+
// CONSTRAINT 3 — Vesu LTV at target:
|
|
441
|
+
// After the rebalance, remaining debt on remaining collateral
|
|
442
|
+
// must be at targetLTV:
|
|
443
|
+
//
|
|
444
|
+
// (vesuDebt - debtRepay) × debtPrice = targetLTV × F × price
|
|
445
|
+
//
|
|
446
|
+
// Solving for debtRepay:
|
|
447
|
+
// debtRepay = vesuDebt - (targetLTV × F × price / debtPrice)
|
|
448
|
+
//
|
|
449
|
+
// CONSTRAINT 4 — Vesu fund conservation:
|
|
450
|
+
// When Vesu position changes by dy, the collateral value change
|
|
451
|
+
// must balance between debt change and transfer:
|
|
452
|
+
//
|
|
453
|
+
// (vesuPos - F) × price = debtRepay × debtPrice + T
|
|
454
|
+
//
|
|
455
|
+
// This says: collateral freed (if closing) = debt repaid + transfer out.
|
|
456
|
+
// (If growing, both sides flip sign and it still holds.)
|
|
457
|
+
//
|
|
458
|
+
// SOLVING:
|
|
459
|
+
// Substitute debtRepay from C3 into C4:
|
|
460
|
+
//
|
|
461
|
+
// T = (vesuPos - F) × price - (vesuDebt - targetLTV × F × price / debtPrice) × debtPrice
|
|
462
|
+
// T = (vesuPos - F) × price - vesuDebt × debtPrice + targetLTV × F × price
|
|
463
|
+
// T = vesuPos × price - F × price - vesuDebt × debtPrice + targetLTV × F × price
|
|
464
|
+
// T = vesuPos × price - vesuDebt × debtPrice - F × price × (1 - targetLTV)
|
|
465
|
+
//
|
|
466
|
+
// Now substitute T into C2 (as equality for the optimal F):
|
|
467
|
+
//
|
|
468
|
+
// extEquity + vesuPos × price - vesuDebt × debtPrice - F × price × (1 - targetLTV)
|
|
469
|
+
// = F × price / extLeverage
|
|
470
|
+
//
|
|
471
|
+
// extEquity + vesuPos × price - vesuDebt × debtPrice
|
|
472
|
+
// = F × price × (1 - targetLTV + 1/extLeverage)
|
|
473
|
+
//
|
|
474
|
+
// Let k = 1 - targetLTV + 1/extLeverage
|
|
475
|
+
//
|
|
476
|
+
// F = (extEquity + vesuPos × price - vesuDebt × debtPrice) / (price × k)
|
|
477
|
+
//
|
|
478
|
+
// INTUITION:
|
|
479
|
+
// The numerator is the total "real equity" in the system:
|
|
480
|
+
// extEquity (what Extended actually owns)
|
|
481
|
+
// + vesuPos × price (Vesu collateral value)
|
|
482
|
+
// - vesuDebt × debtPrice (minus what Vesu owes)
|
|
483
|
+
// This is the combined net worth across both protocols.
|
|
484
|
+
//
|
|
485
|
+
// The denominator (price × k) represents the "equity cost per BTC
|
|
486
|
+
// of matched position" — maintaining 1 BTC on Extended costs
|
|
487
|
+
// price/leverage in margin, and maintaining 1 BTC on Vesu costs
|
|
488
|
+
// price × (1 - targetLTV) in equity. Together: price × k.
|
|
489
|
+
//
|
|
490
|
+
// So F = totalEquity / costPerBtc — the maximum matched position
|
|
491
|
+
// the system can afford.
|
|
492
|
+
//
|
|
493
|
+
// DERIVED QUANTITIES:
|
|
494
|
+
// Once F is known:
|
|
495
|
+
// dx = F - extPos (Extended position change)
|
|
496
|
+
// dy = F - vesuPos (Vesu position change)
|
|
497
|
+
// debtRepay = vesuDebt - targetLTV × F × price / debtPrice
|
|
498
|
+
// T = vesuPos × price - vesuDebt × debtPrice - F × price × (1 - targetLTV)
|
|
499
|
+
//
|
|
500
|
+
// If debtRepay < 0, Vesu takes on more debt (position grew).
|
|
501
|
+
// If T < 0, Extended sends funds to Vesu.
|
|
502
|
+
// dx and dy can each be positive or negative independently.
|
|
503
|
+
// ============================================================
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Solve for the optimal final common position F.
|
|
507
|
+
*
|
|
508
|
+
* F = (extEquity + vesuPos × price - vesuDebt × debtPrice) / (price × k)
|
|
509
|
+
* where k = 1 - targetLTV + 1/extLeverage
|
|
510
|
+
*
|
|
511
|
+
* This is the largest matched position that both protocols can sustain
|
|
512
|
+
* given their combined net equity.
|
|
513
|
+
*/
|
|
514
|
+
function solveUnifiedF(
|
|
515
|
+
extPos: number, vesuPos: number, extEquity: number,
|
|
516
|
+
vesuDebt: number, vesu: VesuState, extLev: number, price: number,
|
|
517
|
+
): number {
|
|
518
|
+
const targetLTV = computeVesuTargetLTV(vesu);
|
|
519
|
+
|
|
520
|
+
// k = equity cost per BTC of matched position, divided by price
|
|
521
|
+
// Extended needs price/lev per BTC, Vesu needs price×(1-tLTV) per BTC
|
|
522
|
+
const k = 1 - targetLTV + 1 / extLev;
|
|
523
|
+
|
|
524
|
+
// Numerator = total real equity across both protocols
|
|
525
|
+
const totalEquity = extEquity + vesuPos * price - vesuDebt * vesu.debtPrice;
|
|
526
|
+
|
|
527
|
+
return totalEquity / (price * k);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Compute the net transfer T given the final position F.
|
|
532
|
+
*
|
|
533
|
+
* T = vesuPos × price - vesuDebt × debtPrice - F × price × (1 - targetLTV)
|
|
534
|
+
*
|
|
535
|
+
* Positive T means Vesu sends surplus to Extended.
|
|
536
|
+
* Negative T means Extended sends surplus to Vesu.
|
|
537
|
+
*/
|
|
538
|
+
function solveTransfer(
|
|
539
|
+
vesuPos: number, vesuDebt: number, F: number,
|
|
540
|
+
vesu: VesuState, price: number,
|
|
541
|
+
): number {
|
|
542
|
+
const targetLTV = computeVesuTargetLTV(vesu);
|
|
543
|
+
return vesuPos * price - vesuDebt * vesu.debtPrice - F * price * (1 - targetLTV);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Compute the debt repayment (in token units) for the final position F.
|
|
548
|
+
*
|
|
549
|
+
* debtRepay = vesuDebt - targetLTV × F × price / debtPrice
|
|
550
|
+
*
|
|
551
|
+
* Positive = debt repaid. Negative = new debt taken on (position grew).
|
|
552
|
+
*/
|
|
553
|
+
function solveDebtRepay(
|
|
554
|
+
vesuDebt: number, F: number,
|
|
555
|
+
vesu: VesuState, price: number,
|
|
556
|
+
): number {
|
|
557
|
+
const targetLTV = computeVesuTargetLTV(vesu);
|
|
558
|
+
return vesuDebt - (targetLTV * F * price / vesu.debtPrice);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Apply rounding to the final position F.
|
|
563
|
+
*
|
|
564
|
+
* For positions that shrink: ceil the close amount (close slightly more, safer).
|
|
565
|
+
* For positions that grow: floor the growth (grow slightly less, safer).
|
|
566
|
+
*
|
|
567
|
+
* After rounding, both sides must still land on the same final position.
|
|
568
|
+
* If rounding causes mismatch, align to the more conservative (smaller) value.
|
|
569
|
+
*/
|
|
570
|
+
function roundFinalPosition(
|
|
571
|
+
extPos: number, vesuPos: number, rawF: number, precision: number,
|
|
572
|
+
): number {
|
|
573
|
+
// If F < extPos (Extended shrinks), the close amount is ceiled,
|
|
574
|
+
// so effective F is reduced: extPos - ceil(extPos - F)
|
|
575
|
+
// If F < vesuPos (Vesu shrinks), same logic.
|
|
576
|
+
// If F > a position (growth), floor the growth.
|
|
577
|
+
|
|
578
|
+
let fFromExt: number;
|
|
579
|
+
if (rawF < extPos) {
|
|
580
|
+
const closeExt = ceilBtc(extPos - rawF, precision);
|
|
581
|
+
fFromExt = extPos - closeExt;
|
|
582
|
+
} else {
|
|
583
|
+
const growExt = floorBtc(rawF - extPos, precision);
|
|
584
|
+
fFromExt = extPos + growExt;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let fFromVesu: number;
|
|
588
|
+
if (rawF < vesuPos) {
|
|
589
|
+
const closeVesu = ceilBtc(vesuPos - rawF, precision);
|
|
590
|
+
fFromVesu = vesuPos - closeVesu;
|
|
591
|
+
} else {
|
|
592
|
+
const growVesu = floorBtc(rawF - vesuPos, precision);
|
|
593
|
+
fFromVesu = vesuPos + growVesu;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Align to the more conservative (smaller) final position
|
|
597
|
+
return Math.max(0, Math.min(fFromExt, fFromVesu));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Phase 2: Unified position + margin solver.
|
|
602
|
+
*
|
|
603
|
+
* First tries to close the position gap using available funding sources.
|
|
604
|
+
* Then solves the unified equation for any remaining gap + margin deficits.
|
|
605
|
+
*
|
|
606
|
+
* The function handles all combinations:
|
|
607
|
+
* - Gap only (margins healthy): grows lagging side from funds, or converges
|
|
608
|
+
* - Deficit only (positions matched): equally reduces both sides
|
|
609
|
+
* - Both gap and deficit: finds F that resolves both simultaneously
|
|
610
|
+
*/
|
|
611
|
+
function phase2(
|
|
612
|
+
extPos: number, vesuPos: number, extEquity: number,
|
|
613
|
+
vesuDebt: number, vesu: VesuState, extLev: number, price: number,
|
|
614
|
+
pool: FundingPool, config: RebalanceConfig,
|
|
615
|
+
): RebalanceDeltas {
|
|
616
|
+
const d = emptyDeltas();
|
|
617
|
+
const precision = config.positionPrecision;
|
|
618
|
+
const minR = config.minRoutableUsd ?? 0;
|
|
619
|
+
const targetLTV = computeVesuTargetLTV(vesu);
|
|
620
|
+
|
|
621
|
+
// ---- Step 1: Try to close gap using available funding sources ----
|
|
622
|
+
//
|
|
623
|
+
// Attempt to grow the lagging side to match the leading side,
|
|
624
|
+
// drawing from the funding pool. This is the "easy" path —
|
|
625
|
+
// if funds are sufficient, no position reduction needed.
|
|
626
|
+
|
|
627
|
+
const imbalance = extPos - vesuPos; // positive = Extended is larger
|
|
628
|
+
let fundedGrowthBtc = 0; // how much the lagging side actually grew
|
|
629
|
+
|
|
630
|
+
if (!isNegligible(imbalance, precision)) {
|
|
631
|
+
if (imbalance > 0) {
|
|
632
|
+
// Extended is larger — try to grow Vesu
|
|
633
|
+
// Vesu growth is leveraged: equity cost = gapBtc × price × (1 - targetLTV)
|
|
634
|
+
const equityCostUsd = computeVesuGrowthCost(imbalance, vesu, price);
|
|
635
|
+
const rMain = drawFunds(equityCostUsd, ['vaUsd', 'walletUsd'], pool, minR);
|
|
636
|
+
applyDrawsToDeltas(d, rMain.draws, -1);
|
|
637
|
+
const rExt = drawExtendedAggregated(rMain.unmet, pool, minR);
|
|
638
|
+
applyDrawsToDeltas(d, rExt.draws, -1);
|
|
639
|
+
const filled = rMain.filled + rExt.filled;
|
|
640
|
+
|
|
641
|
+
// Convert filled equity into leveraged position growth
|
|
642
|
+
const grownBtc = computeVesuGrowthFromEquity(filled, vesu, price);
|
|
643
|
+
d.dVesuPosition += grownBtc;
|
|
644
|
+
fundedGrowthBtc = grownBtc;
|
|
645
|
+
|
|
646
|
+
// New debt created alongside the leveraged growth
|
|
647
|
+
d.dVesuDebt += computeVesuGrowthDebt(grownBtc, vesu, price);
|
|
648
|
+
|
|
649
|
+
// Track ext → vesu fund movement
|
|
650
|
+
const fromExt = sumKeys(rExt.draws, ['extAvlWithdraw', 'extUpnl']);
|
|
651
|
+
if (fromExt > 0) d.dTransferVesuToExt -= fromExt;
|
|
652
|
+
|
|
653
|
+
} else {
|
|
654
|
+
// Vesu is larger — try to grow Extended
|
|
655
|
+
// Extended growth is leveraged: margin cost = gapBtc × price / extLeverage
|
|
656
|
+
const absImbalance = -imbalance;
|
|
657
|
+
const marginCostUsd = absImbalance * price / extLev;
|
|
658
|
+
const { draws, filled } = drawFunds(marginCostUsd, ['walletUsd', 'vaUsd', 'vesuBorrowCapacity'], pool, minR);
|
|
659
|
+
applyDrawsToDeltas(d, draws, -1);
|
|
660
|
+
|
|
661
|
+
// Each $1 of margin creates extLeverage/price BTC of position
|
|
662
|
+
const grownBtc = filled * extLev / price;
|
|
663
|
+
d.dExtPosition += grownBtc;
|
|
664
|
+
fundedGrowthBtc = grownBtc;
|
|
665
|
+
|
|
666
|
+
// Track vesu → ext fund movement
|
|
667
|
+
const fromVesu = draws.vesuBorrowCapacity ?? 0;
|
|
668
|
+
if (fromVesu > 0) d.dTransferVesuToExt += fromVesu;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ---- Step 2: Solve unified equation for remaining gap + deficits ----
|
|
673
|
+
//
|
|
674
|
+
// After Step 1, compute the effective state. There may still be:
|
|
675
|
+
// a) Remaining position imbalance (funds weren't enough to fully close gap)
|
|
676
|
+
// b) Margin deficits (carried over from Phase 1)
|
|
677
|
+
// c) Both
|
|
678
|
+
//
|
|
679
|
+
// The unified equation solves for the optimal F that handles everything.
|
|
680
|
+
// If Step 1 fully resolved the gap AND margins were already healthy,
|
|
681
|
+
// F will equal the current (now-matched) position and no further action needed.
|
|
682
|
+
|
|
683
|
+
const effExtPos = extPos + d.dExtPosition;
|
|
684
|
+
const effVesuPos = vesuPos + d.dVesuPosition;
|
|
685
|
+
const effExtEquity = extEquity; // equity hasn't changed (draws came from pool, not equity)
|
|
686
|
+
const effVesuDebt = vesuDebt + d.dVesuDebt;
|
|
687
|
+
|
|
688
|
+
// Compute what F the system can actually sustain
|
|
689
|
+
const rawF = solveUnifiedF(effExtPos, effVesuPos, effExtEquity, effVesuDebt, vesu, extLev, price);
|
|
690
|
+
|
|
691
|
+
// If F ≥ both effective positions, everything is already healthy — no reduction needed.
|
|
692
|
+
// F might even be larger (meaning we could grow more, but we've exhausted funding sources).
|
|
693
|
+
// Cap F at the larger of the two effective positions (can't grow without funds).
|
|
694
|
+
const cappedF = Math.min(rawF, Math.max(effExtPos, effVesuPos));
|
|
695
|
+
|
|
696
|
+
// Also cap F at the smaller of the two if there's no remaining imbalance to close
|
|
697
|
+
// (if positions are matched, F can't grow either side without funds)
|
|
698
|
+
const maxGrowableTo = Math.max(effExtPos, effVesuPos);
|
|
699
|
+
const targetF = Math.max(0, Math.min(cappedF, maxGrowableTo));
|
|
700
|
+
|
|
701
|
+
// Check if further action is needed
|
|
702
|
+
const remainingImbalance = effExtPos - effVesuPos;
|
|
703
|
+
const extNeedsMore = effExtEquity < computeExtIdealMargin(effExtPos, extLev, price);
|
|
704
|
+
const vesuCurrentLTV = effVesuPos > 0
|
|
705
|
+
? (effVesuDebt * vesu.debtPrice) / (effVesuPos * price)
|
|
706
|
+
: 0;
|
|
707
|
+
const vesuNeedsMore = vesuCurrentLTV > targetLTV;
|
|
708
|
+
|
|
709
|
+
const needsFurtherAction = !isNegligible(remainingImbalance, precision) || extNeedsMore || vesuNeedsMore;
|
|
710
|
+
|
|
711
|
+
if (!needsFurtherAction) {
|
|
712
|
+
return d;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Apply rounding to F
|
|
716
|
+
const F = roundFinalPosition(effExtPos, effVesuPos, targetF, precision);
|
|
717
|
+
|
|
718
|
+
// Compute position changes relative to effective (post-Step 1) positions
|
|
719
|
+
const dx = F - effExtPos; // Extended position change (negative = shrink)
|
|
720
|
+
const dy = F - effVesuPos; // Vesu position change (negative = shrink)
|
|
721
|
+
|
|
722
|
+
if (isNegligible(dx, precision) && isNegligible(dy, precision)) {
|
|
723
|
+
return d;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
d.dExtPosition += dx;
|
|
727
|
+
d.dVesuPosition += dy;
|
|
728
|
+
|
|
729
|
+
// Compute debt change: positive debtRepay = debt reduced
|
|
730
|
+
// debtRepay = currentDebt - targetLTV × F × price / debtPrice
|
|
731
|
+
const debtRepay = solveDebtRepay(effVesuDebt, F, vesu, price);
|
|
732
|
+
d.dVesuDebt -= debtRepay; // negative debtRepay means new debt (dVesuDebt increases)
|
|
733
|
+
|
|
734
|
+
// Compute transfer: positive = Vesu → Extended
|
|
735
|
+
const T = solveTransfer(effVesuPos, effVesuDebt, F, vesu, price);
|
|
736
|
+
d.dTransferVesuToExt += T;
|
|
737
|
+
|
|
738
|
+
// Record the transfer's effect on Extended equity
|
|
739
|
+
// T > 0: Extended receives funds → equity increases
|
|
740
|
+
// T < 0: Extended sends funds → equity decreases
|
|
741
|
+
d.dExtAvlWithdraw += T;
|
|
742
|
+
|
|
743
|
+
return d;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ============================================================
|
|
747
|
+
// ORCHESTRATOR
|
|
748
|
+
// ============================================================
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Main entry point.
|
|
752
|
+
*
|
|
753
|
+
* Phase 1: best-effort margin repair from funding sources.
|
|
754
|
+
* Phase 2: unified solver for position matching + remaining deficits.
|
|
755
|
+
*
|
|
756
|
+
* The funding pool is cloned so the caller's data is not mutated.
|
|
757
|
+
* Phase 1 drains from the pool; Phase 2 draws from whatever remains,
|
|
758
|
+
* then solves the unified equation for anything left.
|
|
759
|
+
*/
|
|
760
|
+
function rebalance(inputs: RebalanceInputs): RebalanceDeltas {
|
|
761
|
+
logger.info(`ltv-imbalance-rebalance-math::rebalance inputs=${JSON.stringify(inputs)}`);
|
|
762
|
+
const { ext, vesu, btcPrice, config } = inputs;
|
|
763
|
+
const pool: FundingPool = { ...inputs.funding };
|
|
764
|
+
|
|
765
|
+
// Phase 1: best-effort margin repair from funding sources
|
|
766
|
+
const p1 = phase1(ext, vesu, btcPrice, pool, config);
|
|
767
|
+
|
|
768
|
+
// Phase 2: unified position + margin solver
|
|
769
|
+
// Uses effective state after Phase 1, plus remaining pool for gap-closing
|
|
770
|
+
const effExtPos = ext.positionBtc + p1.deltas.dExtPosition;
|
|
771
|
+
const effVesuPos = vesu.positionBtc + p1.deltas.dVesuPosition;
|
|
772
|
+
const effExtEquity = ext.equity + p1.deltas.dExtAvlWithdraw + p1.deltas.dExtUpnl;
|
|
773
|
+
const effVesuDebt = vesu.debt + p1.deltas.dVesuDebt;
|
|
774
|
+
|
|
775
|
+
const p2 = phase2(
|
|
776
|
+
effExtPos, effVesuPos, effExtEquity, effVesuDebt,
|
|
777
|
+
vesu, ext.leverage, btcPrice, pool, config,
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
return mergeDeltas(p1.deltas, p2);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
export { rebalance, RebalanceInputs, RebalanceDeltas, routableDrawAmount };
|