@strkfarm/sdk 2.0.0-dev.3 → 2.0.0-dev.30

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.
Files changed (75) hide show
  1. package/dist/cli.js +190 -36
  2. package/dist/cli.mjs +188 -34
  3. package/dist/index.browser.global.js +78478 -45620
  4. package/dist/index.browser.mjs +19583 -9901
  5. package/dist/index.d.ts +3763 -1424
  6. package/dist/index.js +20980 -11063
  7. package/dist/index.mjs +20948 -11087
  8. package/package.json +1 -1
  9. package/src/data/avnu.abi.json +840 -0
  10. package/src/data/ekubo-price-fethcer.abi.json +265 -0
  11. package/src/dataTypes/_bignumber.ts +13 -4
  12. package/src/dataTypes/bignumber.browser.ts +6 -1
  13. package/src/dataTypes/bignumber.node.ts +5 -1
  14. package/src/dataTypes/index.ts +3 -2
  15. package/src/dataTypes/mynumber.ts +141 -0
  16. package/src/global.ts +76 -41
  17. package/src/index.browser.ts +2 -1
  18. package/src/interfaces/common.tsx +175 -3
  19. package/src/modules/ExtendedWrapperSDk/types.ts +28 -5
  20. package/src/modules/ExtendedWrapperSDk/wrapper.ts +275 -59
  21. package/src/modules/apollo-client-config.ts +28 -0
  22. package/src/modules/avnu.ts +4 -4
  23. package/src/modules/ekubo-pricer.ts +79 -0
  24. package/src/modules/ekubo-quoter.ts +48 -30
  25. package/src/modules/erc20.ts +17 -0
  26. package/src/modules/harvests.ts +43 -29
  27. package/src/modules/pragma.ts +23 -8
  28. package/src/modules/pricer-from-api.ts +156 -15
  29. package/src/modules/pricer-lst.ts +1 -1
  30. package/src/modules/pricer.ts +40 -4
  31. package/src/modules/pricerBase.ts +2 -1
  32. package/src/node/deployer.ts +36 -1
  33. package/src/node/pricer-redis.ts +2 -1
  34. package/src/strategies/base-strategy.ts +78 -10
  35. package/src/strategies/ekubo-cl-vault.tsx +906 -347
  36. package/src/strategies/factory.ts +159 -0
  37. package/src/strategies/index.ts +7 -1
  38. package/src/strategies/registry.ts +239 -0
  39. package/src/strategies/sensei.ts +335 -7
  40. package/src/strategies/svk-strategy.ts +97 -27
  41. package/src/strategies/types.ts +4 -0
  42. package/src/strategies/universal-adapters/adapter-utils.ts +2 -1
  43. package/src/strategies/universal-adapters/avnu-adapter.ts +180 -265
  44. package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
  45. package/src/strategies/universal-adapters/common-adapter.ts +206 -203
  46. package/src/strategies/universal-adapters/extended-adapter.ts +490 -316
  47. package/src/strategies/universal-adapters/index.ts +11 -8
  48. package/src/strategies/universal-adapters/svk-troves-adapter.ts +364 -0
  49. package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
  50. package/src/strategies/universal-adapters/usdc<>usdce-adapter.ts +200 -0
  51. package/src/strategies/universal-adapters/vesu-adapter.ts +120 -82
  52. package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +476 -0
  53. package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +1067 -704
  54. package/src/strategies/universal-adapters/vesu-position-common.ts +251 -0
  55. package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
  56. package/src/strategies/universal-lst-muliplier-strategy.tsx +397 -204
  57. package/src/strategies/universal-strategy.tsx +1426 -1173
  58. package/src/strategies/vesu-extended-strategy/services/executionService.ts +2233 -0
  59. package/src/strategies/vesu-extended-strategy/services/extended-vesu-state-manager.ts +4087 -0
  60. package/src/strategies/vesu-extended-strategy/services/ltv-imbalance-rebalance-math.ts +783 -0
  61. package/src/strategies/vesu-extended-strategy/services/operationService.ts +38 -16
  62. package/src/strategies/vesu-extended-strategy/types/transaction-metadata.ts +88 -0
  63. package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +1 -0
  64. package/src/strategies/vesu-extended-strategy/utils/constants.ts +5 -6
  65. package/src/strategies/vesu-extended-strategy/utils/helper.ts +259 -103
  66. package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +688 -817
  67. package/src/strategies/vesu-rebalance.tsx +255 -152
  68. package/src/utils/cacheClass.ts +11 -2
  69. package/src/utils/health-factor-math.ts +4 -1
  70. package/src/utils/index.ts +3 -1
  71. package/src/utils/logger.browser.ts +22 -4
  72. package/src/utils/logger.node.ts +259 -24
  73. package/src/utils/starknet-call-parser.ts +1036 -0
  74. package/src/utils/strategy-utils.ts +61 -0
  75. 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 };