@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.
- package/dist/cli.js +190 -36
- package/dist/cli.mjs +188 -34
- package/dist/index.browser.global.js +78478 -45620
- package/dist/index.browser.mjs +19583 -9901
- package/dist/index.d.ts +3763 -1424
- package/dist/index.js +20980 -11063
- package/dist/index.mjs +20948 -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,4087 @@
|
|
|
1
|
+
import { ContractAddr, Web3Number } from "@/dataTypes";
|
|
2
|
+
import { IConfig, TokenInfo } from "@/interfaces";
|
|
3
|
+
import { PricerBase } from "@/modules/pricerBase";
|
|
4
|
+
import { ERC20 } from "@/modules";
|
|
5
|
+
import { assert, logger } from "@/utils";
|
|
6
|
+
import { ExtendedAdapter } from "../../universal-adapters/extended-adapter";
|
|
7
|
+
import { VesuMultiplyAdapter } from "../../universal-adapters/vesu-multiply-adapter";
|
|
8
|
+
import { AssetOperationType, AssetOperationStatus } from "@/modules/ExtendedWrapperSDk";
|
|
9
|
+
import { USDC_TOKEN_DECIMALS } from "../utils/constants";
|
|
10
|
+
import {
|
|
11
|
+
calculateDeltaDebtAmount,
|
|
12
|
+
calculateExtendedLevergae,
|
|
13
|
+
calculateVesuLeverage,
|
|
14
|
+
} from "../utils/helper";
|
|
15
|
+
import { VesuConfig } from "../utils/config.runtime";
|
|
16
|
+
import { rebalance, routableDrawAmount, type RebalanceDeltas } from "./ltv-imbalance-rebalance-math";
|
|
17
|
+
|
|
18
|
+
// ─── State types ───────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Snapshot of a single Vesu pool's position: collateral, debt, and their prices.
|
|
22
|
+
*/
|
|
23
|
+
export interface VesuPoolState {
|
|
24
|
+
poolId: ContractAddr;
|
|
25
|
+
collateralToken: TokenInfo;
|
|
26
|
+
debtToken: TokenInfo;
|
|
27
|
+
collateralAmount: Web3Number;
|
|
28
|
+
collateralUsdValue: number;
|
|
29
|
+
debtAmount: Web3Number;
|
|
30
|
+
debtUsdValue: number;
|
|
31
|
+
collateralPrice: number;
|
|
32
|
+
debtPrice: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Snapshot of a single Extended exchange open position.
|
|
37
|
+
*/
|
|
38
|
+
export interface ExtendedPositionState {
|
|
39
|
+
instrument: string;
|
|
40
|
+
side: string;
|
|
41
|
+
size: Web3Number;
|
|
42
|
+
valueUsd: Web3Number;
|
|
43
|
+
leverage: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Snapshot of the Extended exchange account-level balance.
|
|
48
|
+
*/
|
|
49
|
+
export interface ExtendedBalanceState {
|
|
50
|
+
equity: Web3Number;
|
|
51
|
+
availableForTrade: Web3Number;
|
|
52
|
+
availableForWithdrawal: Web3Number;
|
|
53
|
+
unrealisedPnl: Web3Number;
|
|
54
|
+
balance: Web3Number;
|
|
55
|
+
/**
|
|
56
|
+
* Funds in transit to/from Extended.
|
|
57
|
+
* Positive = deposit in transit (funds left wallet, not yet credited on Extended).
|
|
58
|
+
* Negative = withdrawal in transit (funds left Extended, not yet received in wallet).
|
|
59
|
+
*/
|
|
60
|
+
pendingDeposit: Web3Number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
type Inputs = {
|
|
65
|
+
extAvlWithdraw: number;
|
|
66
|
+
extUpnl: number;
|
|
67
|
+
vaUsd: number;
|
|
68
|
+
walletUsd: number;
|
|
69
|
+
vesuBorrowCapacity: number;
|
|
70
|
+
vesuLeverage: number;
|
|
71
|
+
extendedLeverage: number;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type Deltas = {
|
|
75
|
+
dExtAvlWithdraw: number;
|
|
76
|
+
dExtUpnl: number;
|
|
77
|
+
dVaUsd: number;
|
|
78
|
+
dWalletUsd: number;
|
|
79
|
+
dVesuBorrowCapacity: number;
|
|
80
|
+
finalVaUsd: number;
|
|
81
|
+
finalExtended: number;
|
|
82
|
+
isExtendedToVesu: boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generic token balance with USD valuation.
|
|
87
|
+
*/
|
|
88
|
+
export interface TokenBalance {
|
|
89
|
+
token: TokenInfo;
|
|
90
|
+
amount: Web3Number;
|
|
91
|
+
usdValue: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Solve result types ────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Per-position exposure change on the Extended exchange.
|
|
98
|
+
* Positive delta = increase position size, negative = reduce.
|
|
99
|
+
*/
|
|
100
|
+
export interface ExtendedPositionDelta {
|
|
101
|
+
instrument: string;
|
|
102
|
+
delta: Web3Number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Per-pool position change on Vesu.
|
|
107
|
+
* debtDelta: positive = borrow more, negative = repay.
|
|
108
|
+
* collateralDelta: positive = add collateral, negative = remove.
|
|
109
|
+
*/
|
|
110
|
+
export interface VesuPoolDelta {
|
|
111
|
+
poolId: ContractAddr;
|
|
112
|
+
collateralToken: TokenInfo;
|
|
113
|
+
debtToken: TokenInfo;
|
|
114
|
+
debtDelta: Web3Number;
|
|
115
|
+
collateralDelta: Web3Number;
|
|
116
|
+
collateralPrice: number;
|
|
117
|
+
debtPrice: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── ExecutionRoute types ───────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Enumerates all possible fund-routing paths used during execution.
|
|
124
|
+
*/
|
|
125
|
+
export enum RouteType {
|
|
126
|
+
/** Deposit USDC from operator wallet directly to Extended exchange */
|
|
127
|
+
WALLET_TO_EXTENDED = 'WALLET_TO_EXTENDED',
|
|
128
|
+
/** USDC from vault allocator → deposit to Extended (via manager) */
|
|
129
|
+
VA_TO_EXTENDED = 'VA_TO_EXTENDED',
|
|
130
|
+
/** Withdraw from Extended exchange → operator wallet */
|
|
131
|
+
EXTENDED_TO_WALLET = 'EXTENDED_TO_WALLET',
|
|
132
|
+
/** Swap USDC → BTC to deposit to Vesu */
|
|
133
|
+
AVNU_DEPOSIT_SWAP = 'AVNU_DEPOSIT_SWAP',
|
|
134
|
+
/** Increase leverage on Vesu i.e. deposit on vesu, borrow, in one go to create lever */
|
|
135
|
+
VESU_MULTIPLY_INCREASE_LEVER = 'VESU_MULTIPLY_INCREASE_LEVER',
|
|
136
|
+
/** Decrease leverage on Vesu i.e. withdraw from vesu, repay, in one go to reduce leverage */
|
|
137
|
+
VESU_MULTIPLY_DECREASE_LEVER = 'VESU_MULTIPLY_DECREASE_LEVER',
|
|
138
|
+
/** Swap BTC → USDC to withdraw from Vesu */
|
|
139
|
+
AVNU_WITHDRAW_SWAP = 'AVNU_WITHDRAW_SWAP',
|
|
140
|
+
/** Borrow additional USDC from Vesu (when wallet + VA insufficient for Extended) */
|
|
141
|
+
VESU_BORROW = 'VESU_BORROW',
|
|
142
|
+
/** Repay USDC debt to Vesu (debtDelta < 0) */
|
|
143
|
+
VESU_REPAY = 'VESU_REPAY',
|
|
144
|
+
/** Transfer USDC from operator wallet to vault allocator */
|
|
145
|
+
WALLET_TO_VA = 'WALLET_TO_VA',
|
|
146
|
+
/** Realize PnL on Extended exchange */
|
|
147
|
+
REALISE_PNL = 'REALISE_PNL',
|
|
148
|
+
/** Increase leverage on Extended exchange i.e. deposit on extended, in one go to create lever */
|
|
149
|
+
EXTENDED_INCREASE_LEVER = 'EXTENDED_INCREASE_LEVER',
|
|
150
|
+
/** Decrease leverage on Extended exchange i.e. withdraw from extended, in one go to reduce leverage */
|
|
151
|
+
EXTENDED_DECREASE_LEVER = 'EXTENDED_DECREASE_LEVER',
|
|
152
|
+
|
|
153
|
+
/** Increase leverage on Extended exchange to max leverage (e.g. 4x from 3x) */
|
|
154
|
+
CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE = 'CRISIS_INCREASE_EXTENDER_MAX_LEVERAGE',
|
|
155
|
+
/** Undo max leverage on Extended exchange to reduce leverage (e.g. 4x to 3x) */
|
|
156
|
+
CRISIS_UNDO_EXTENDED_MAX_LEVERAGE = 'CRISIS_UNDO_EXTENDED_MAX_LEVERAGE',
|
|
157
|
+
|
|
158
|
+
/** Borrow beyyond target HF (e.g. 1.2 from 1.4) */
|
|
159
|
+
CRISIS_BORROW_BEYOND_TARGET_HF = 'CRISIS_BORROW_BEYOND_TARGET_HF',
|
|
160
|
+
|
|
161
|
+
/** Bring liquidity from vault allocator to vault contract (for user withdrawals) */
|
|
162
|
+
BRING_LIQUIDITY = 'BRING_LIQUIDITY',
|
|
163
|
+
|
|
164
|
+
// often a bridge tx is involved. when the solve cycle runs again
|
|
165
|
+
// bridges funds shall be handled again
|
|
166
|
+
RETURN_TO_WAIT = 'RETURN_TO_WAIT',
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── Per-route-type metadata ─────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/** Common fields shared by every route variant */
|
|
172
|
+
interface RouteBase {
|
|
173
|
+
/** Execution order — lower values execute first */
|
|
174
|
+
priority: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Simple fund-transfer routes: WALLET_TO_EXTENDED, VA_TO_EXTENDED, EXTENDED_TO_WALLET, WALLET_TO_VA */
|
|
178
|
+
export interface TransferRoute extends RouteBase {
|
|
179
|
+
type:
|
|
180
|
+
| RouteType.WALLET_TO_EXTENDED
|
|
181
|
+
| RouteType.VA_TO_EXTENDED
|
|
182
|
+
| RouteType.EXTENDED_TO_WALLET
|
|
183
|
+
| RouteType.WALLET_TO_VA;
|
|
184
|
+
/** Amount to transfer */
|
|
185
|
+
amount: Web3Number;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** AVNU swap routes: AVNU_DEPOSIT_SWAP (USDC→BTC), AVNU_WITHDRAW_SWAP (BTC→USDC) */
|
|
189
|
+
export interface SwapRoute extends RouteBase {
|
|
190
|
+
type: RouteType.AVNU_DEPOSIT_SWAP | RouteType.AVNU_WITHDRAW_SWAP;
|
|
191
|
+
/** Source token symbol */
|
|
192
|
+
fromToken: string;
|
|
193
|
+
/** Source amount */
|
|
194
|
+
fromAmount: Web3Number;
|
|
195
|
+
/** Destination token symbol */
|
|
196
|
+
toToken: string;
|
|
197
|
+
/** Exact output amount (if known; otherwise undefined means best-effort) */
|
|
198
|
+
toAmount?: Web3Number;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Vesu multiply lever routes: deposit+borrow or withdraw+repay in one go */
|
|
202
|
+
export interface VesuMultiplyRoute extends RouteBase {
|
|
203
|
+
type: RouteType.VESU_MULTIPLY_INCREASE_LEVER | RouteType.VESU_MULTIPLY_DECREASE_LEVER;
|
|
204
|
+
/** Pool to interact with */
|
|
205
|
+
poolId: ContractAddr;
|
|
206
|
+
/** Collateral token info */
|
|
207
|
+
collateralToken: TokenInfo;
|
|
208
|
+
/** Collateral amount delta (positive = deposit, negative = withdraw) */
|
|
209
|
+
marginAmount: Web3Number; // in collateral token units
|
|
210
|
+
swappedCollateralAmount: Web3Number; // debt swapped, in collateral token units
|
|
211
|
+
/** Debt token info */
|
|
212
|
+
debtToken: TokenInfo;
|
|
213
|
+
/** Debt amount delta (positive = borrow, negative = repay) */
|
|
214
|
+
debtAmount: Web3Number;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Vesu single-side borrow / repay routes */
|
|
218
|
+
export interface VesuDebtRoute extends RouteBase {
|
|
219
|
+
type: RouteType.VESU_BORROW | RouteType.VESU_REPAY;
|
|
220
|
+
/** Pool to interact with */
|
|
221
|
+
poolId: ContractAddr;
|
|
222
|
+
/** Amount to borrow (positive) or repay (negative) */
|
|
223
|
+
amount: Web3Number;
|
|
224
|
+
/** Collateral token info */
|
|
225
|
+
collateralToken: TokenInfo;
|
|
226
|
+
/** Debt token info */
|
|
227
|
+
debtToken: TokenInfo;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Realise PnL on Extended exchange */
|
|
231
|
+
export interface RealisePnlRoute extends RouteBase {
|
|
232
|
+
type: RouteType.REALISE_PNL;
|
|
233
|
+
/** Amount of PnL to realise (the shortfall beyond available-to-withdraw) */
|
|
234
|
+
amount: Web3Number;
|
|
235
|
+
/** Extended instrument name (e.g. "BTC-USD") */
|
|
236
|
+
instrument: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Increase / decrease leverage on Extended exchange */
|
|
240
|
+
export interface ExtendedLeverRoute extends RouteBase {
|
|
241
|
+
type: RouteType.EXTENDED_INCREASE_LEVER | RouteType.EXTENDED_DECREASE_LEVER;
|
|
242
|
+
/** Change in exposure denominated in the exposure token */
|
|
243
|
+
amount: Web3Number;
|
|
244
|
+
/** Extended instrument name (e.g. "BTC-USD") */
|
|
245
|
+
instrument: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Crisis: temporarily increase / undo max leverage on Extended — no args for now */
|
|
249
|
+
export interface CrisisExtendedLeverRoute extends RouteBase {
|
|
250
|
+
type: RouteType.CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE | RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Crisis: borrow beyond the normal target HF */
|
|
254
|
+
export interface CrisisBorrowRoute extends RouteBase {
|
|
255
|
+
type: RouteType.CRISIS_BORROW_BEYOND_TARGET_HF;
|
|
256
|
+
/** Pool to borrow from */
|
|
257
|
+
poolId: ContractAddr;
|
|
258
|
+
/** Token being borrowed */
|
|
259
|
+
token: string;
|
|
260
|
+
/** Amount to borrow */
|
|
261
|
+
amount: Web3Number;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Bring liquidity from vault allocator to vault contract for user withdrawals */
|
|
265
|
+
export interface BringLiquidityRoute extends RouteBase {
|
|
266
|
+
type: RouteType.BRING_LIQUIDITY;
|
|
267
|
+
/** Amount to bring */
|
|
268
|
+
amount: Web3Number;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Control routes: RETURN_TO_WAIT (stop execution, resume in next cycle) */
|
|
272
|
+
export interface WaitRoute extends RouteBase {
|
|
273
|
+
type: RouteType.RETURN_TO_WAIT;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* A single step in the execution plan produced by the solver.
|
|
278
|
+
* Discriminated union keyed on `type` — each variant carries only
|
|
279
|
+
* the metadata that specific route needs.
|
|
280
|
+
*/
|
|
281
|
+
export type ExecutionRoute =
|
|
282
|
+
| TransferRoute
|
|
283
|
+
| SwapRoute
|
|
284
|
+
| VesuMultiplyRoute
|
|
285
|
+
| VesuDebtRoute
|
|
286
|
+
| RealisePnlRoute
|
|
287
|
+
| ExtendedLeverRoute
|
|
288
|
+
| CrisisExtendedLeverRoute
|
|
289
|
+
| CrisisBorrowRoute
|
|
290
|
+
| BringLiquidityRoute
|
|
291
|
+
| WaitRoute;
|
|
292
|
+
|
|
293
|
+
// ─── Case classification types ──────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Broad category of a detected case. Multiple categories may apply simultaneously.
|
|
297
|
+
*/
|
|
298
|
+
export enum CaseCategory {
|
|
299
|
+
LTV_REBALANCE = 'LTV_REBALANCE',
|
|
300
|
+
NEW_DEPOSITS = 'NEW_DEPOSITS',
|
|
301
|
+
WITHDRAWAL = 'WITHDRAWAL',
|
|
302
|
+
EXPOSURE_IMBALANCE = 'EXPOSURE_IMBALANCE',
|
|
303
|
+
MARGIN_CRISIS = 'MARGIN_CRISIS',
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Specific case IDs corresponding to the situations defined in cases.json.
|
|
308
|
+
*/
|
|
309
|
+
export enum CaseId {
|
|
310
|
+
// LTV Rebalance
|
|
311
|
+
MANAGE_LTV = 'MANAGE_LTV',
|
|
312
|
+
/** @deprecated use MANAGE_LTV */ LTV_VESU_LOW_TO_EXTENDED = 'LTV_VESU_LOW_TO_EXTENDED',
|
|
313
|
+
/** @deprecated use MANAGE_LTV */ LTV_EXTENDED_PROFITABLE_AVAILABLE = 'LTV_EXTENDED_PROFITABLE_AVAILABLE',
|
|
314
|
+
/** @deprecated use MANAGE_LTV */ LTV_EXTENDED_PROFITABLE_REALIZE = 'LTV_EXTENDED_PROFITABLE_REALIZE',
|
|
315
|
+
/** @deprecated use MANAGE_LTV */ LTV_VESU_HIGH_USE_VA_OR_WALLET = 'LTV_VESU_HIGH_USE_VA_OR_WALLET',
|
|
316
|
+
/** @deprecated use MANAGE_LTV */ LTV_EXTENDED_HIGH_USE_VA_OR_WALLET = 'LTV_EXTENDED_HIGH_USE_VA_OR_WALLET',
|
|
317
|
+
|
|
318
|
+
// New Deposits / Excess Funds
|
|
319
|
+
DEPOSIT_FRESH_VAULT = 'DEPOSIT_FRESH_VAULT',
|
|
320
|
+
DEPOSIT_EXTENDED_AVAILABLE = 'DEPOSIT_EXTENDED_AVAILABLE',
|
|
321
|
+
DEPOSIT_VESU_BORROW_CAPACITY = 'DEPOSIT_VESU_BORROW_CAPACITY',
|
|
322
|
+
DEPOSIT_COMBINATION = 'DEPOSIT_COMBINATION',
|
|
323
|
+
// Withdrawal
|
|
324
|
+
WITHDRAWAL_SIMPLE = 'WITHDRAWAL_SIMPLE',
|
|
325
|
+
// Margin Crisis
|
|
326
|
+
MARGIN_CRISIS_EXTENDED = 'MARGIN_CRISIS_EXTENDED',
|
|
327
|
+
MARGIN_CRISIS_VESU = 'MARGIN_CRISIS_VESU',
|
|
328
|
+
// Exposure Imbalance
|
|
329
|
+
IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS = 'IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS',
|
|
330
|
+
IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS = 'IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS',
|
|
331
|
+
IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS = 'IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS',
|
|
332
|
+
IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS = 'IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS',
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* A detected case describing the current system state and the
|
|
337
|
+
* high-level steps required to resolve it.
|
|
338
|
+
*/
|
|
339
|
+
export interface SolveCase {
|
|
340
|
+
id: CaseId;
|
|
341
|
+
category: CaseCategory;
|
|
342
|
+
title: string;
|
|
343
|
+
description: string;
|
|
344
|
+
/** High-level steps describing what needs to happen for this case */
|
|
345
|
+
steps: string[];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Minimum USD amount to consider a case significant */
|
|
349
|
+
const CASE_THRESHOLD_USD = 5;
|
|
350
|
+
const CASE_MIN_BRIDING_USD = 10;
|
|
351
|
+
/** Decimal places for rounding collateral / exposure deltas in token terms (e.g. BTC) */
|
|
352
|
+
export const COLLATERAL_PRECISION = 4;
|
|
353
|
+
|
|
354
|
+
/** Safely create a Web3Number from a float, avoiding >15 significant digit errors */
|
|
355
|
+
function safeUsdcWeb3Number(value: number): Web3Number {
|
|
356
|
+
return new Web3Number(value.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* A single detected case with its metadata, amounts, and the execution
|
|
361
|
+
* routes that belong to this case.
|
|
362
|
+
*/
|
|
363
|
+
export interface SolveCaseEntry {
|
|
364
|
+
case: SolveCase;
|
|
365
|
+
additionalArgs: { amount?: Web3Number };
|
|
366
|
+
routes: ExecutionRoute[];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Maps each CaseId to the RouteTypes that are relevant for resolving it.
|
|
371
|
+
* Used to filter the global route list into per-case route subsets.
|
|
372
|
+
*/
|
|
373
|
+
export const CASE_ROUTE_TYPES: Record<CaseId, RouteType[]> = {
|
|
374
|
+
// LTV Rebalance — unified
|
|
375
|
+
[CaseId.MANAGE_LTV]: [
|
|
376
|
+
RouteType.REALISE_PNL, RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT,
|
|
377
|
+
RouteType.WALLET_TO_VA, RouteType.VESU_BORROW, RouteType.VESU_REPAY, RouteType.VA_TO_EXTENDED,
|
|
378
|
+
RouteType.VESU_MULTIPLY_DECREASE_LEVER, RouteType.VESU_MULTIPLY_INCREASE_LEVER,
|
|
379
|
+
RouteType.AVNU_DEPOSIT_SWAP,
|
|
380
|
+
RouteType.EXTENDED_DECREASE_LEVER, RouteType.EXTENDED_INCREASE_LEVER,
|
|
381
|
+
// Second-phase VA / Extended funding after lever routes (same types may repeat).
|
|
382
|
+
RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VA_TO_EXTENDED,
|
|
383
|
+
],
|
|
384
|
+
/** @deprecated */ [CaseId.LTV_VESU_HIGH_USE_VA_OR_WALLET]: [RouteType.WALLET_TO_VA, RouteType.VESU_REPAY],
|
|
385
|
+
/** @deprecated */ [CaseId.LTV_EXTENDED_PROFITABLE_AVAILABLE]: [RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VESU_REPAY],
|
|
386
|
+
/** @deprecated */ [CaseId.LTV_EXTENDED_PROFITABLE_REALIZE]: [RouteType.REALISE_PNL, RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VESU_REPAY],
|
|
387
|
+
/** @deprecated */ [CaseId.LTV_EXTENDED_HIGH_USE_VA_OR_WALLET]: [RouteType.VA_TO_EXTENDED, RouteType.WALLET_TO_EXTENDED],
|
|
388
|
+
/** @deprecated */ [CaseId.LTV_VESU_LOW_TO_EXTENDED]: [RouteType.VESU_BORROW, RouteType.VA_TO_EXTENDED],
|
|
389
|
+
|
|
390
|
+
// New Deposits
|
|
391
|
+
// @dev, when handling routes, after VA_TO_EXTENDED and/or WALLET_TO_EXTENDED, return. bcz, funds take time to reach extended. Anyways, in next cycle, these funds will be computed to increase lever
|
|
392
|
+
// Sequence: fund-movement transfers first (WALLET_TO_EXTENDED, VA_TO_EXTENDED, WALLET_TO_VA, EXTENDED_TO_WALLET),
|
|
393
|
+
// then RETURN_TO_WAIT, then optional second WALLET_TO_VA + RETURN_TO_WAIT, then lever routes.
|
|
394
|
+
[CaseId.DEPOSIT_FRESH_VAULT]: [
|
|
395
|
+
// May repeat after MANAGE_LTV (VA top-up → Extended → wait → levers).
|
|
396
|
+
RouteType.WALLET_TO_VA,
|
|
397
|
+
RouteType.VA_TO_EXTENDED,
|
|
398
|
+
RouteType.RETURN_TO_WAIT,
|
|
399
|
+
RouteType.VA_TO_EXTENDED,
|
|
400
|
+
RouteType.VESU_BORROW,
|
|
401
|
+
RouteType.WALLET_TO_EXTENDED,
|
|
402
|
+
RouteType.VA_TO_EXTENDED,
|
|
403
|
+
RouteType.WALLET_TO_VA,
|
|
404
|
+
RouteType.EXTENDED_TO_WALLET,
|
|
405
|
+
RouteType.RETURN_TO_WAIT,
|
|
406
|
+
RouteType.WALLET_TO_VA,
|
|
407
|
+
RouteType.AVNU_DEPOSIT_SWAP,
|
|
408
|
+
RouteType.VESU_MULTIPLY_INCREASE_LEVER,
|
|
409
|
+
RouteType.EXTENDED_INCREASE_LEVER,
|
|
410
|
+
],
|
|
411
|
+
[CaseId.DEPOSIT_EXTENDED_AVAILABLE]: [RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.AVNU_DEPOSIT_SWAP, RouteType.VESU_MULTIPLY_INCREASE_LEVER, RouteType.EXTENDED_INCREASE_LEVER],
|
|
412
|
+
[CaseId.DEPOSIT_VESU_BORROW_CAPACITY]: [RouteType.VESU_BORROW, RouteType.VA_TO_EXTENDED, RouteType.RETURN_TO_WAIT, RouteType.AVNU_DEPOSIT_SWAP, RouteType.VESU_MULTIPLY_INCREASE_LEVER, RouteType.EXTENDED_INCREASE_LEVER],
|
|
413
|
+
[CaseId.DEPOSIT_COMBINATION]: [], // exroutes to be computed on the fly based on above sub routes and where deposit is available
|
|
414
|
+
// Withdrawal
|
|
415
|
+
// Sequence: transfer out first (EXTENDED_TO_WALLET, RETURN_TO_WAIT, WALLET_TO_VA), then unwind lever, then BRING_LIQUIDITY
|
|
416
|
+
[CaseId.WITHDRAWAL_SIMPLE]: [RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VESU_MULTIPLY_DECREASE_LEVER, RouteType.AVNU_WITHDRAW_SWAP, RouteType.EXTENDED_DECREASE_LEVER, RouteType.BRING_LIQUIDITY],
|
|
417
|
+
|
|
418
|
+
// Margin Crisis
|
|
419
|
+
// amounts must be chosen such that, when decresing lever, the LTV reached should be adequate, not high not low.
|
|
420
|
+
[CaseId.MARGIN_CRISIS_EXTENDED]: [RouteType.CRISIS_BORROW_BEYOND_TARGET_HF, RouteType.VA_TO_EXTENDED, RouteType.RETURN_TO_WAIT, RouteType.VESU_MULTIPLY_DECREASE_LEVER, RouteType.EXTENDED_DECREASE_LEVER, RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE],
|
|
421
|
+
[CaseId.MARGIN_CRISIS_VESU]: [RouteType.CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE, RouteType.EXTENDED_TO_WALLET, RouteType.WALLET_TO_VA, RouteType.VESU_MULTIPLY_DECREASE_LEVER, RouteType.EXTENDED_DECREASE_LEVER, RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE],
|
|
422
|
+
|
|
423
|
+
// Exposure Imbalance
|
|
424
|
+
// try to use available funds in VA/Wallet to increase lever in vesu. if not possible, reduce lever on extended for remaining
|
|
425
|
+
[CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS]: [RouteType.AVNU_DEPOSIT_SWAP, RouteType.VESU_MULTIPLY_INCREASE_LEVER, RouteType.EXTENDED_DECREASE_LEVER],
|
|
426
|
+
|
|
427
|
+
// no available funds in VA/Wallet to increase lever in vesu. reduce lever on extended for remaining
|
|
428
|
+
[CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS]: [RouteType.EXTENDED_DECREASE_LEVER],
|
|
429
|
+
|
|
430
|
+
// try to use available funds in VA/Wallet to increase lever in extended. if not possible, reduce lever on vesu for remaining
|
|
431
|
+
[CaseId.IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS]: [RouteType.VA_TO_EXTENDED, RouteType.WALLET_TO_EXTENDED, RouteType.RETURN_TO_WAIT, RouteType.EXTENDED_INCREASE_LEVER, RouteType.RETURN_TO_WAIT, RouteType.VESU_MULTIPLY_DECREASE_LEVER],
|
|
432
|
+
|
|
433
|
+
// no available funds in VA/Wallet to increase lever in extended. reduce lever on vesu for remaining
|
|
434
|
+
[CaseId.IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS]: [RouteType.VESU_MULTIPLY_DECREASE_LEVER],
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
/** Minimum exposure imbalance fraction to flag an imbalance case */
|
|
438
|
+
const IMBALANCE_THRESHOLD_FRACTION = 0.0002; // 0.02%
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Static case definitions derived from cases.json.
|
|
442
|
+
* Each entry maps a CaseId to its metadata and descriptive steps.
|
|
443
|
+
*/
|
|
444
|
+
const CASE_DEFINITIONS: Record<CaseId, SolveCase> = {
|
|
445
|
+
[CaseId.MANAGE_LTV]: {
|
|
446
|
+
id: CaseId.MANAGE_LTV,
|
|
447
|
+
category: CaseCategory.LTV_REBALANCE,
|
|
448
|
+
title: 'LTV Rebalance: Unified Vesu repay + Extended margin management',
|
|
449
|
+
description: 'Manages both Vesu high-LTV repayment and Extended low-margin funding in a single pass.',
|
|
450
|
+
steps: [
|
|
451
|
+
'Compute vesu repay needed and extended margin needed',
|
|
452
|
+
'Allocate funds: VA > Wallet > ExtAvl > ExtUpnl for Vesu; Wallet > VA > Borrow for Extended',
|
|
453
|
+
'Build combined transfer and repay/margin routes',
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
[CaseId.LTV_VESU_LOW_TO_EXTENDED]: {
|
|
457
|
+
id: CaseId.LTV_VESU_LOW_TO_EXTENDED,
|
|
458
|
+
category: CaseCategory.LTV_REBALANCE,
|
|
459
|
+
title: 'LTV Rebalance: Vesu Low LTV → Extended (No BTC Imbalance)',
|
|
460
|
+
description: 'Exposures are balanced on both sides, but Vesu has low LTV (can borrow more). Extended needs additional margin.',
|
|
461
|
+
steps: [
|
|
462
|
+
'Borrow additional USDC from Vesu (up to target LTV)',
|
|
463
|
+
'Vault → Extended: Deposit borrowed USDC as margin',
|
|
464
|
+
],
|
|
465
|
+
},
|
|
466
|
+
[CaseId.LTV_EXTENDED_PROFITABLE_AVAILABLE]: {
|
|
467
|
+
id: CaseId.LTV_EXTENDED_PROFITABLE_AVAILABLE,
|
|
468
|
+
category: CaseCategory.LTV_REBALANCE,
|
|
469
|
+
title: 'LTV Rebalance: Extended Profitable → Vesu (Funds Available to Withdraw)',
|
|
470
|
+
description: 'Exposures balanced. Extended is profitable with sufficient available-to-withdraw funds. Vesu needs more collateral.',
|
|
471
|
+
steps: [
|
|
472
|
+
'Withdraw USDC from Extended to Wallet',
|
|
473
|
+
'Wallet → Vault: Transfer USDC',
|
|
474
|
+
'Vault → Vesu: Deposit USDC to repay',
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
[CaseId.LTV_EXTENDED_PROFITABLE_REALIZE]: {
|
|
478
|
+
id: CaseId.LTV_EXTENDED_PROFITABLE_REALIZE,
|
|
479
|
+
category: CaseCategory.LTV_REBALANCE,
|
|
480
|
+
title: 'LTV Rebalance: Extended Profitable → Vesu (Need to Realize PnL)',
|
|
481
|
+
description: 'Exposures balanced. Extended is profitable but available-to-withdraw is insufficient. Unrealized PnL needs to be realized.',
|
|
482
|
+
steps: [
|
|
483
|
+
'Partially close short BTC position on Extended (realize PnL)',
|
|
484
|
+
'Immediately reopen short BTC position on Extended (maintain exposure)',
|
|
485
|
+
'Withdraw realized USDC from Extended to Wallet',
|
|
486
|
+
'Wallet → Vault: Transfer USDC',
|
|
487
|
+
'Vault → Vesu: Deposit USDC as collateral',
|
|
488
|
+
],
|
|
489
|
+
},
|
|
490
|
+
[CaseId.LTV_VESU_HIGH_USE_VA_OR_WALLET]: {
|
|
491
|
+
id: CaseId.LTV_VESU_HIGH_USE_VA_OR_WALLET,
|
|
492
|
+
category: CaseCategory.LTV_REBALANCE,
|
|
493
|
+
title: 'LTV Rebalance: Vesu High LTV (Use VA or Wallet)',
|
|
494
|
+
description: 'Vesu has high LTV (can repay debt). funds available in VA and/or Wallet can be used to repay debt.',
|
|
495
|
+
steps: [
|
|
496
|
+
'Wallet → Vesu -> Vesu: Repay using USDC',
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
[CaseId.LTV_EXTENDED_HIGH_USE_VA_OR_WALLET]: {
|
|
500
|
+
id: CaseId.LTV_EXTENDED_HIGH_USE_VA_OR_WALLET,
|
|
501
|
+
category: CaseCategory.LTV_REBALANCE,
|
|
502
|
+
title: 'LTV Rebalance: Extended High LTV (Use VA or Wallet)',
|
|
503
|
+
description: 'Extended has high LTV (can borrow more). funds available in VA and/or Wallet can be used to add margin',
|
|
504
|
+
steps: [
|
|
505
|
+
'VA or Wallet → Extended -> Extended: Borrow using USDC',
|
|
506
|
+
],
|
|
507
|
+
},
|
|
508
|
+
[CaseId.DEPOSIT_FRESH_VAULT]: {
|
|
509
|
+
id: CaseId.DEPOSIT_FRESH_VAULT,
|
|
510
|
+
category: CaseCategory.NEW_DEPOSITS,
|
|
511
|
+
title: 'New Deposits/Excess Funds: Fresh USDC in Vault',
|
|
512
|
+
description: 'Fresh USDC deposits received in Vault contract.',
|
|
513
|
+
steps: [
|
|
514
|
+
'Unused funds in Vault or in Wallet',
|
|
515
|
+
'Calculate allocation amounts for Extended and Vesu',
|
|
516
|
+
'Vault and/or Wallet → Extended: Deposit allocated USDC',
|
|
517
|
+
'Once extended receives the funds, open short BTC position',
|
|
518
|
+
'Vault and/or Wallet → Vesu: Deposit allocated USDC, borrow USDC, open long BTC position',
|
|
519
|
+
],
|
|
520
|
+
},
|
|
521
|
+
[CaseId.DEPOSIT_EXTENDED_AVAILABLE]: {
|
|
522
|
+
id: CaseId.DEPOSIT_EXTENDED_AVAILABLE,
|
|
523
|
+
category: CaseCategory.NEW_DEPOSITS,
|
|
524
|
+
title: 'New Deposits/Excess Funds: Available to Withdraw from Extended',
|
|
525
|
+
description: 'Extended has excess available-to-withdraw funds that can be deployed without LTV rebalancing or BTC imbalance concerns.',
|
|
526
|
+
steps: [
|
|
527
|
+
'Calculate position increase amounts for both protocols',
|
|
528
|
+
'Withdraw portion of funds from Extended to Wallet (if needed for Vesu). If part of the required funds are in unrealised PnL, convert to realised PnL.',
|
|
529
|
+
'Once wallet receives the funds, use available funds on Extended to increase short BTC position',
|
|
530
|
+
'Wallet → Vault → Vesu: Deposit USDC as collateral',
|
|
531
|
+
'Borrow USDC from Vesu, open additional long BTC position',
|
|
532
|
+
],
|
|
533
|
+
},
|
|
534
|
+
[CaseId.DEPOSIT_VESU_BORROW_CAPACITY]: {
|
|
535
|
+
id: CaseId.DEPOSIT_VESU_BORROW_CAPACITY,
|
|
536
|
+
category: CaseCategory.NEW_DEPOSITS,
|
|
537
|
+
title: 'New Deposits/Excess Funds: Available Borrowing Capacity on Vesu',
|
|
538
|
+
description: 'Vesu has low LTV with excess borrowing capacity that can be deployed for position expansion.',
|
|
539
|
+
steps: [
|
|
540
|
+
'Borrow additional USDC from Vesu (within safe LTV limits)',
|
|
541
|
+
'Calculate allocation for position expansion',
|
|
542
|
+
'Deposit portion of funds on Extended',
|
|
543
|
+
'Once extended receives the funds, open short BTC position',
|
|
544
|
+
'Use portion to open additional long BTC position on Vesu',
|
|
545
|
+
],
|
|
546
|
+
},
|
|
547
|
+
[CaseId.DEPOSIT_COMBINATION]: {
|
|
548
|
+
id: CaseId.DEPOSIT_COMBINATION,
|
|
549
|
+
category: CaseCategory.NEW_DEPOSITS,
|
|
550
|
+
title: 'New Deposits/Excess Funds: Combination case',
|
|
551
|
+
description: 'Combination of new deposits/excess funds on vault/extended/vesu.',
|
|
552
|
+
steps: [
|
|
553
|
+
'Optimally distribute funds from multiple sources and create new positions. Avoid unnecessary routes.',
|
|
554
|
+
],
|
|
555
|
+
},
|
|
556
|
+
[CaseId.WITHDRAWAL_SIMPLE]: {
|
|
557
|
+
id: CaseId.WITHDRAWAL_SIMPLE,
|
|
558
|
+
category: CaseCategory.WITHDRAWAL,
|
|
559
|
+
title: 'User Withdrawal: Simple Position Reduction',
|
|
560
|
+
description: 'User requests withdrawal with sufficient available funds to close positions proportionally.',
|
|
561
|
+
steps: [
|
|
562
|
+
'First check if any available funds can be used to settle the withdrawal (vault, available to withdraw or borrowing from vesu)',
|
|
563
|
+
'If yes, do necessary transfers such that funds are available in vault',
|
|
564
|
+
'If not or if more required, calculate position sizes to close on both sides',
|
|
565
|
+
'Close long BTC position on Vesu (partially or fully)',
|
|
566
|
+
'Repay borrowed USDC on Vesu',
|
|
567
|
+
'Withdraw collateral from Vesu to Vault',
|
|
568
|
+
'Close short BTC position on Extended',
|
|
569
|
+
'Withdraw margin from Extended to Wallet',
|
|
570
|
+
'Wallet → Vault: Transfer USDC',
|
|
571
|
+
'Vault → User: Process withdrawal',
|
|
572
|
+
],
|
|
573
|
+
},
|
|
574
|
+
[CaseId.MARGIN_CRISIS_EXTENDED]: {
|
|
575
|
+
id: CaseId.MARGIN_CRISIS_EXTENDED,
|
|
576
|
+
category: CaseCategory.MARGIN_CRISIS,
|
|
577
|
+
title: 'Margin Crisis: LTV rebalance requires more funds than available, margin required on Extended',
|
|
578
|
+
description: 'LTV rebalance requires more funds than available on vault/extended/vesu. Extended needs margin urgently.',
|
|
579
|
+
steps: [
|
|
580
|
+
'Calculate required position reduction in vesu and extended',
|
|
581
|
+
'Borrow more on vesu such that HF can go max up to 1.2',
|
|
582
|
+
'Use the borrowed funds to settle margin requirements on Extended (vault to extended)',
|
|
583
|
+
'Once funds reach on extended, close portion of long BTC position on Vesu',
|
|
584
|
+
'Convert BTC to USDC',
|
|
585
|
+
'Repay portion of borrowed USDC on Vesu such that HF can go back to target',
|
|
586
|
+
'Close corresponding short BTC position on Extended (match reduced exposure)',
|
|
587
|
+
],
|
|
588
|
+
},
|
|
589
|
+
[CaseId.MARGIN_CRISIS_VESU]: {
|
|
590
|
+
id: CaseId.MARGIN_CRISIS_VESU,
|
|
591
|
+
category: CaseCategory.MARGIN_CRISIS,
|
|
592
|
+
title: 'Margin Crisis: LTV rebalance requires more funds than available, margin required on Vesu',
|
|
593
|
+
description: 'LTV rebalance requires more funds than available on vault/extended/vesu. Vesu needs margin urgently.',
|
|
594
|
+
steps: [
|
|
595
|
+
'Calculate required position reduction in extended',
|
|
596
|
+
'Update leverage to 4x and try to use available to withdraw funds on extended',
|
|
597
|
+
'Wait until funds reach on wallet',
|
|
598
|
+
'Close portion of short BTC position on Extended',
|
|
599
|
+
'Move wallet to vault and use the USDC to repay on vesu',
|
|
600
|
+
'Reduce long position to match the short position on extended',
|
|
601
|
+
],
|
|
602
|
+
},
|
|
603
|
+
[CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS]: {
|
|
604
|
+
id: CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS,
|
|
605
|
+
category: CaseCategory.EXPOSURE_IMBALANCE,
|
|
606
|
+
title: 'BTC Imbalance: Extended Has Excess Short (No funds to match)',
|
|
607
|
+
description: 'Extended has more short BTC exposure than Vesu long. No funds available to increase Vesu long.',
|
|
608
|
+
steps: [
|
|
609
|
+
'Reduce short BTC position on Extended to match the long position on Vesu',
|
|
610
|
+
],
|
|
611
|
+
},
|
|
612
|
+
[CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS]: {
|
|
613
|
+
id: CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS,
|
|
614
|
+
category: CaseCategory.EXPOSURE_IMBALANCE,
|
|
615
|
+
title: 'BTC Imbalance: Extended Has Excess Short (Vault/Wallet Has Funds)',
|
|
616
|
+
description: 'Extended has more short BTC exposure than Vesu long. Use available funds to increase long on Vesu.',
|
|
617
|
+
steps: [
|
|
618
|
+
'Use available funds to increase long on Vesu as much as possible',
|
|
619
|
+
'If still not matched, reduce short BTC position on Extended to match the long position on Vesu',
|
|
620
|
+
],
|
|
621
|
+
},
|
|
622
|
+
[CaseId.IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS]: {
|
|
623
|
+
id: CaseId.IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS,
|
|
624
|
+
category: CaseCategory.EXPOSURE_IMBALANCE,
|
|
625
|
+
title: 'BTC Imbalance: Vesu Has Excess Long (No funds to match)',
|
|
626
|
+
description: 'Vesu has more long BTC exposure than Extended short. Extended does not have enough available to trade funds.',
|
|
627
|
+
steps: [
|
|
628
|
+
'Reduce long BTC position on Vesu to match the short position on Extended',
|
|
629
|
+
],
|
|
630
|
+
},
|
|
631
|
+
[CaseId.IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS]: {
|
|
632
|
+
id: CaseId.IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS,
|
|
633
|
+
category: CaseCategory.EXPOSURE_IMBALANCE,
|
|
634
|
+
title: 'BTC Imbalance: Vesu Has Excess Long (Extended has funds)',
|
|
635
|
+
description: 'Vesu has more long BTC exposure than Extended short. Extended has available to trade funds to match.',
|
|
636
|
+
steps: [
|
|
637
|
+
'Use available funds to increase short on Extended as much as possible',
|
|
638
|
+
'If still not matched, reduce long BTC position on Vesu to match the short position on Extended',
|
|
639
|
+
],
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Complete output from the solver describing all needed state changes.
|
|
645
|
+
*
|
|
646
|
+
* - cases: detected cases describing the system state and needed actions.
|
|
647
|
+
* - extendedDeposit: positive = deposit USDC into Extended, negative = withdraw.
|
|
648
|
+
* - extendedPositionDeltas: per-instrument exposure changes (with instrument id).
|
|
649
|
+
* - vesuDeltas: per-pool debt & collateral changes (with pool id and token pair).
|
|
650
|
+
* - vesuAllocationUsd: USDC directed to Vesu side (negative = unwind from Vesu).
|
|
651
|
+
* - extendedAllocationUsd: USDC directed to Extended side (negative = withdraw from Extended).
|
|
652
|
+
* - bringLiquidityAmount: amount VA should send via bringLiquidity to the vault
|
|
653
|
+
* (= withdrawAmount input; 0 during investment cycles).
|
|
654
|
+
*/
|
|
655
|
+
export interface SolveResult {
|
|
656
|
+
/** Detected cases describing the current situation, amounts, and per-case routes */
|
|
657
|
+
cases: SolveCaseEntry[];
|
|
658
|
+
extendedDeposit: Web3Number;
|
|
659
|
+
extendedPositionDeltas: ExtendedPositionDelta[];
|
|
660
|
+
vesuDepositAmount: Web3Number;
|
|
661
|
+
vesuDeltas: VesuPoolDelta[];
|
|
662
|
+
vesuAllocationUsd: Web3Number;
|
|
663
|
+
extendedAllocationUsd: Web3Number;
|
|
664
|
+
bringLiquidityAmount: Web3Number;
|
|
665
|
+
/**
|
|
666
|
+
* Net pending deposit for Extended.
|
|
667
|
+
* Positive = deposit in transit (wallet → Extended), negative = withdrawal in transit.
|
|
668
|
+
* Used by ExecutionService to avoid double-sending transfers.
|
|
669
|
+
*/
|
|
670
|
+
pendingDeposit: Web3Number;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ─── Config ────────────────────────────────────────────────────────────────────
|
|
674
|
+
|
|
675
|
+
export interface StateManagerConfig {
|
|
676
|
+
pricer: PricerBase;
|
|
677
|
+
networkConfig: IConfig;
|
|
678
|
+
vesuAdapters: VesuMultiplyAdapter[];
|
|
679
|
+
extendedAdapter: ExtendedAdapter;
|
|
680
|
+
vaultAllocator: ContractAddr;
|
|
681
|
+
walletAddress: string;
|
|
682
|
+
/** Strategy / vault token — idle balance in the vault allocator is denominated in this asset */
|
|
683
|
+
assetToken: TokenInfo;
|
|
684
|
+
/**
|
|
685
|
+
* Native USDC (Starknet) — operator wallet stablecoin balance is always read for this token,
|
|
686
|
+
* independent of {@link assetToken} (e.g. when the strategy vault uses another asset).
|
|
687
|
+
*/
|
|
688
|
+
usdcToken: TokenInfo;
|
|
689
|
+
/** Collateral token (e.g. WBTC) for wallet balance checks */
|
|
690
|
+
collateralToken: TokenInfo;
|
|
691
|
+
limitBalanceBufferFactor: number;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// ─── ExecutionRoute helpers ──────────────────────────────────────────────────────────────
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Returns a short human-readable summary of a route's key fields for logging.
|
|
698
|
+
*/
|
|
699
|
+
export function routeSummary(r: ExecutionRoute): string {
|
|
700
|
+
switch (r.type) {
|
|
701
|
+
case RouteType.WALLET_TO_EXTENDED:
|
|
702
|
+
case RouteType.VA_TO_EXTENDED:
|
|
703
|
+
case RouteType.EXTENDED_TO_WALLET:
|
|
704
|
+
case RouteType.WALLET_TO_VA:
|
|
705
|
+
return `${r.amount.toNumber()}`;
|
|
706
|
+
case RouteType.AVNU_DEPOSIT_SWAP:
|
|
707
|
+
case RouteType.AVNU_WITHDRAW_SWAP:
|
|
708
|
+
return `${r.fromAmount.toNumber()} ${r.fromToken}→${r.toToken}`;
|
|
709
|
+
case RouteType.VESU_MULTIPLY_INCREASE_LEVER:
|
|
710
|
+
case RouteType.VESU_MULTIPLY_DECREASE_LEVER:
|
|
711
|
+
return `coll=${r.marginAmount.toNumber()} [${r.marginAmount.decimals}] debt=${r.debtAmount.toNumber()} [${r.debtAmount.decimals}] (swapped ${r.swappedCollateralAmount.toNumber()} [${r.swappedCollateralAmount.decimals}]) pool=${r.poolId.shortString()}`;
|
|
712
|
+
case RouteType.VESU_BORROW:
|
|
713
|
+
case RouteType.VESU_REPAY:
|
|
714
|
+
return `${r.amount.toNumber()} ${r.debtToken.symbol} pool=${r.poolId.shortString()} [Collateral: ${r.collateralToken.symbol}]`;
|
|
715
|
+
case RouteType.REALISE_PNL:
|
|
716
|
+
return `${r.amount.toNumber()} ${r.instrument}`;
|
|
717
|
+
case RouteType.EXTENDED_INCREASE_LEVER:
|
|
718
|
+
case RouteType.EXTENDED_DECREASE_LEVER:
|
|
719
|
+
return `${r.amount.toNumber()} ${r.instrument}`;
|
|
720
|
+
case RouteType.CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE:
|
|
721
|
+
case RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE:
|
|
722
|
+
return '(no args)';
|
|
723
|
+
case RouteType.CRISIS_BORROW_BEYOND_TARGET_HF:
|
|
724
|
+
return `${r.amount.toNumber()} ${r.token} pool=${r.poolId.shortString()}`;
|
|
725
|
+
case RouteType.BRING_LIQUIDITY:
|
|
726
|
+
return `${r.amount.toNumber()}`;
|
|
727
|
+
case RouteType.RETURN_TO_WAIT:
|
|
728
|
+
return '(control)';
|
|
729
|
+
default:
|
|
730
|
+
return '';
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ─── Solve Budget ───────────────────────────────────────────────────────────────
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Single source of truth for all mutable state during a solve() call.
|
|
738
|
+
*
|
|
739
|
+
* Stores **raw** snapshots from refresh (no safety buffer applied in fields).
|
|
740
|
+
* Buffer {@link limitBalanceBufferFactor} is applied only in **getters** used
|
|
741
|
+
* during solve (caps, diagnostics). **Spend / add** methods mutate balances
|
|
742
|
+
* in **raw** USD only (no buffer on debits or credits).
|
|
743
|
+
*
|
|
744
|
+
* **Extended deposits** ({@link SolveBudget.addToExtAvailTrade}): while account equity is
|
|
745
|
+
* below required margin (Σ position value ÷ Extended leverage), incoming USDC is credited
|
|
746
|
+
* only to **balance** and **equity**. Only the excess is credited to **availableForWithdrawal**
|
|
747
|
+
* and **availableForTrade**, matching “margin first, then free collateral”.
|
|
748
|
+
*/
|
|
749
|
+
export class SolveBudget {
|
|
750
|
+
// ── Refreshed state (mutable during solve, raw on-chain / API values) ─
|
|
751
|
+
private unusedBalance: TokenBalance[];
|
|
752
|
+
private walletBalance: TokenBalance | null;
|
|
753
|
+
/**
|
|
754
|
+
* Idle non-stable {@link StateManagerConfig.assetToken} in the vault allocator.
|
|
755
|
+
* Null when asset and USDC are the same token (VA idle stablecoin is only in {@link vaultUsdcBalance}).
|
|
756
|
+
*/
|
|
757
|
+
private vaultAssetBalance: TokenBalance | null;
|
|
758
|
+
/** Idle {@link StateManagerConfig.usdcToken} in the vault allocator (including when it is also the strategy asset). */
|
|
759
|
+
private vaultUsdcBalance: TokenBalance | null;
|
|
760
|
+
private extendedPositions: ExtendedPositionState[];
|
|
761
|
+
private extendedBalance: ExtendedBalanceState | null;
|
|
762
|
+
private vesuPoolStates: VesuPoolState[];
|
|
763
|
+
private vesuPerPoolDebtDeltasToBorrow: Web3Number[];
|
|
764
|
+
private shouldVesuRebalance: boolean[]; // should be same length as vesuPerPoolDebtDeltasToBorrow
|
|
765
|
+
|
|
766
|
+
readonly assetToken: TokenInfo;
|
|
767
|
+
readonly usdcToken: TokenInfo;
|
|
768
|
+
|
|
769
|
+
constructor(state: {
|
|
770
|
+
assetToken: TokenInfo;
|
|
771
|
+
usdcToken: TokenInfo;
|
|
772
|
+
unusedBalance: TokenBalance[];
|
|
773
|
+
walletBalance: TokenBalance | null;
|
|
774
|
+
vaultAssetBalance: TokenBalance | null;
|
|
775
|
+
vaultUsdcBalance: TokenBalance | null;
|
|
776
|
+
extendedPositions: ExtendedPositionState[];
|
|
777
|
+
extendedBalance: ExtendedBalanceState | null;
|
|
778
|
+
vesuPoolStates: VesuPoolState[];
|
|
779
|
+
}) {
|
|
780
|
+
this.assetToken = state.assetToken;
|
|
781
|
+
this.usdcToken = state.usdcToken;
|
|
782
|
+
|
|
783
|
+
const cloneTb = (b: TokenBalance): TokenBalance => ({
|
|
784
|
+
token: b.token,
|
|
785
|
+
amount: new Web3Number(b.amount.toFixed(b.token.decimals), b.token.decimals),
|
|
786
|
+
usdValue: b.usdValue,
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
this.unusedBalance = state.unusedBalance.map((item) => cloneTb(item));
|
|
790
|
+
this.walletBalance = state.walletBalance ? cloneTb(state.walletBalance) : null;
|
|
791
|
+
this.vaultAssetBalance = state.vaultAssetBalance ? cloneTb(state.vaultAssetBalance) : null;
|
|
792
|
+
this.vaultUsdcBalance = state.vaultUsdcBalance ? cloneTb(state.vaultUsdcBalance) : null;
|
|
793
|
+
|
|
794
|
+
// Deep-clone mutable Extended / Vesu snapshots so route-building dry-runs (e.g. _classifyLTV)
|
|
795
|
+
// cannot mutate shared objects when callers reuse the same fixture for multiple budgets.
|
|
796
|
+
this.extendedPositions = state.extendedPositions.map((p) => ({
|
|
797
|
+
...p,
|
|
798
|
+
size: new Web3Number(p.size.toFixed(8), 8),
|
|
799
|
+
valueUsd: new Web3Number(p.valueUsd.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS),
|
|
800
|
+
}));
|
|
801
|
+
this.extendedBalance = state.extendedBalance
|
|
802
|
+
? {
|
|
803
|
+
equity: new Web3Number(
|
|
804
|
+
state.extendedBalance.equity.toFixed(USDC_TOKEN_DECIMALS),
|
|
805
|
+
USDC_TOKEN_DECIMALS,
|
|
806
|
+
),
|
|
807
|
+
availableForTrade: new Web3Number(
|
|
808
|
+
state.extendedBalance.availableForTrade.toFixed(USDC_TOKEN_DECIMALS),
|
|
809
|
+
USDC_TOKEN_DECIMALS,
|
|
810
|
+
),
|
|
811
|
+
availableForWithdrawal: new Web3Number(
|
|
812
|
+
state.extendedBalance.availableForWithdrawal.toFixed(USDC_TOKEN_DECIMALS),
|
|
813
|
+
USDC_TOKEN_DECIMALS,
|
|
814
|
+
),
|
|
815
|
+
unrealisedPnl: new Web3Number(
|
|
816
|
+
state.extendedBalance.unrealisedPnl.toFixed(USDC_TOKEN_DECIMALS),
|
|
817
|
+
USDC_TOKEN_DECIMALS,
|
|
818
|
+
),
|
|
819
|
+
balance: new Web3Number(
|
|
820
|
+
state.extendedBalance.balance.toFixed(USDC_TOKEN_DECIMALS),
|
|
821
|
+
USDC_TOKEN_DECIMALS,
|
|
822
|
+
),
|
|
823
|
+
pendingDeposit: new Web3Number(
|
|
824
|
+
state.extendedBalance.pendingDeposit.toFixed(USDC_TOKEN_DECIMALS),
|
|
825
|
+
USDC_TOKEN_DECIMALS,
|
|
826
|
+
),
|
|
827
|
+
}
|
|
828
|
+
: null;
|
|
829
|
+
this.vesuPoolStates = state.vesuPoolStates.map((p) => ({
|
|
830
|
+
...p,
|
|
831
|
+
collateralAmount: new Web3Number(
|
|
832
|
+
p.collateralAmount.toFixed(p.collateralToken.decimals),
|
|
833
|
+
p.collateralToken.decimals,
|
|
834
|
+
),
|
|
835
|
+
debtAmount: new Web3Number(
|
|
836
|
+
p.debtAmount.toFixed(p.debtToken.decimals),
|
|
837
|
+
p.debtToken.decimals,
|
|
838
|
+
),
|
|
839
|
+
}));
|
|
840
|
+
const vesuPerPoolDebtDeltasToBorrow = this._computeperPoolDebtDeltasToBorrow();
|
|
841
|
+
assert(vesuPerPoolDebtDeltasToBorrow.length === this.vesuPoolStates.length, 'vesuPerPoolDebtDeltasToBorrow length must match vesuPoolStates length');
|
|
842
|
+
this.vesuPerPoolDebtDeltasToBorrow = vesuPerPoolDebtDeltasToBorrow.map((item) => item.deltaDebt);
|
|
843
|
+
this.shouldVesuRebalance = vesuPerPoolDebtDeltasToBorrow.map((item) => item.shouldRebalance);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/** `1 - limitBalanceBufferFactor` — multiplier applied to raw notionals for “usable” USD. */
|
|
847
|
+
private _usableFraction(): number {
|
|
848
|
+
return 1;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Raw USD notional for a token row. USDC (and configured {@link usdcToken}) uses 1:1 from amount;
|
|
853
|
+
* non-stable assets (e.g. WBTC in VA) use {@link TokenBalance.usdValue} from the pricer at refresh.
|
|
854
|
+
*/
|
|
855
|
+
private _rawTokenUsd(tb: TokenBalance | null | undefined): number {
|
|
856
|
+
if (!tb) return 0;
|
|
857
|
+
if (this.usdcToken.address.eq(tb.token.address)) {
|
|
858
|
+
return Number(tb.amount.toFixed(tb.token.decimals));
|
|
859
|
+
}
|
|
860
|
+
return tb.usdValue;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** Apply safety buffer to a raw USD scalar. */
|
|
864
|
+
bufferedUsd(rawUsd: number): number {
|
|
865
|
+
return rawUsd * this._usableFraction();
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/** Convert a buffered “usable” USD amount to raw nominal USD (inverse of {@link bufferedUsd}). */
|
|
869
|
+
rawUsdFromBuffered(bufferedUsd: number): number {
|
|
870
|
+
const bf = this._usableFraction();
|
|
871
|
+
assert(bf > 0, 'SolveBudget::rawUsdFromBuffered usable fraction must be positive');
|
|
872
|
+
return bufferedUsd / bf;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/** Buffered USD notional for one token balance row. */
|
|
876
|
+
bufferedTokenUsd(tb: TokenBalance | null | undefined): number {
|
|
877
|
+
return this.bufferedUsd(this._rawTokenUsd(tb));
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
logStateSummary() {
|
|
881
|
+
console.log("===== state summary =====");
|
|
882
|
+
const aggregatedData = {
|
|
883
|
+
unusedBalances: this.unusedBalance.map((b) => ({
|
|
884
|
+
token: b.token.symbol,
|
|
885
|
+
amount: b.amount.toNumber(),
|
|
886
|
+
})),
|
|
887
|
+
walletBalance: this.walletBalance
|
|
888
|
+
? {
|
|
889
|
+
token: this.walletBalance.token.symbol,
|
|
890
|
+
amount: this.walletBalance.amount.toNumber(),
|
|
891
|
+
}
|
|
892
|
+
: undefined,
|
|
893
|
+
vaultAssetBalance: this.vaultAssetBalance
|
|
894
|
+
? {
|
|
895
|
+
token: this.vaultAssetBalance.token.symbol,
|
|
896
|
+
amount: this.vaultAssetBalance.amount.toNumber(),
|
|
897
|
+
}
|
|
898
|
+
: undefined,
|
|
899
|
+
vaultUsdcBalance: this.vaultUsdcBalance
|
|
900
|
+
? {
|
|
901
|
+
token: this.vaultUsdcBalance.token.symbol,
|
|
902
|
+
amount: this.vaultUsdcBalance.amount.toNumber(),
|
|
903
|
+
}
|
|
904
|
+
: undefined,
|
|
905
|
+
vesuPoolStates: this.vesuPoolStates.map((p) => ({
|
|
906
|
+
poolId: p.poolId,
|
|
907
|
+
collateralAmount: p.collateralAmount.toNumber(),
|
|
908
|
+
debtAmount: p.debtAmount.toNumber(),
|
|
909
|
+
})),
|
|
910
|
+
vesuBorrowCapacity: this.vesuBorrowCapacity,
|
|
911
|
+
vesuRebalance: this.shouldVesuRebalance,
|
|
912
|
+
vesuPerPoolDebtDeltasToBorrow: this.vesuPerPoolDebtDeltasToBorrow.map((d) => d.toNumber()),
|
|
913
|
+
extendedBalance: this.extendedBalance?.balance.toNumber(),
|
|
914
|
+
extendedEquity: this.extendedBalance?.equity.toNumber(),
|
|
915
|
+
extendedAvailableForTrade: this.extendedBalance?.availableForTrade.toNumber(),
|
|
916
|
+
extendedAvailableForWithdrawal: this.extendedBalance?.availableForWithdrawal.toNumber(),
|
|
917
|
+
extendedUnrealisedPnl: this.extendedBalance?.unrealisedPnl.toNumber(),
|
|
918
|
+
extendedPendingDeposit: this.extendedBalance?.pendingDeposit.toNumber(),
|
|
919
|
+
extendedPositions: this.extendedPositions.map((p) => ({
|
|
920
|
+
instrument: p.instrument,
|
|
921
|
+
size: p.size.toNumber(),
|
|
922
|
+
valueUsd: p.valueUsd.toNumber(),
|
|
923
|
+
})),
|
|
924
|
+
}
|
|
925
|
+
// unused balances
|
|
926
|
+
console.log(
|
|
927
|
+
"unused balances",
|
|
928
|
+
aggregatedData.unusedBalances
|
|
929
|
+
.map((b) => `${b.token}=${b.amount}`)
|
|
930
|
+
.join(", "),
|
|
931
|
+
);
|
|
932
|
+
console.log(
|
|
933
|
+
"wallet balance",
|
|
934
|
+
aggregatedData.walletBalance
|
|
935
|
+
? `${aggregatedData.walletBalance.token}=${aggregatedData.walletBalance.amount}`
|
|
936
|
+
: undefined,
|
|
937
|
+
);
|
|
938
|
+
console.log(
|
|
939
|
+
"vault asset balance",
|
|
940
|
+
aggregatedData.vaultAssetBalance
|
|
941
|
+
? `${aggregatedData.vaultAssetBalance.token}=${aggregatedData.vaultAssetBalance.amount}`
|
|
942
|
+
: undefined,
|
|
943
|
+
);
|
|
944
|
+
console.log(
|
|
945
|
+
"vault usdc balance",
|
|
946
|
+
aggregatedData.vaultUsdcBalance
|
|
947
|
+
? `${aggregatedData.vaultUsdcBalance.token}=${aggregatedData.vaultUsdcBalance.amount}`
|
|
948
|
+
: undefined,
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
// vesu info
|
|
952
|
+
console.log(
|
|
953
|
+
"vesu pool states",
|
|
954
|
+
aggregatedData.vesuPoolStates
|
|
955
|
+
.map(
|
|
956
|
+
(p) =>
|
|
957
|
+
`${p.poolId.shortString()}=${p.collateralAmount} ${p.debtAmount}`,
|
|
958
|
+
)
|
|
959
|
+
.join(", "),
|
|
960
|
+
);
|
|
961
|
+
console.log("vesu borrow capacity", aggregatedData.vesuBorrowCapacity);
|
|
962
|
+
console.log(
|
|
963
|
+
"vesu rebalance",
|
|
964
|
+
aggregatedData.vesuRebalance.map(String).join(", "),
|
|
965
|
+
);
|
|
966
|
+
console.log("vesu per pool debt deltas to borrow", aggregatedData.vesuPerPoolDebtDeltasToBorrow.join(", "));
|
|
967
|
+
// extended info
|
|
968
|
+
console.log("extended balance", aggregatedData.extendedBalance);
|
|
969
|
+
console.log("extended equity", aggregatedData.extendedEquity);
|
|
970
|
+
console.log("extended available for trade", aggregatedData.extendedAvailableForTrade);
|
|
971
|
+
console.log("extended available for withdrawal", aggregatedData.extendedAvailableForWithdrawal);
|
|
972
|
+
console.log("extended unrealised pnl", aggregatedData.extendedUnrealisedPnl);
|
|
973
|
+
console.log("extended pending deposit", aggregatedData.extendedPendingDeposit);
|
|
974
|
+
console.log(
|
|
975
|
+
"extended positions",
|
|
976
|
+
aggregatedData.extendedPositions
|
|
977
|
+
.map(
|
|
978
|
+
(p) => `${p.instrument}=${p.size} ${p.valueUsd}`,
|
|
979
|
+
)
|
|
980
|
+
.join(", "),
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
return aggregatedData
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Initialise derived views for a solve cycle. Mutates only when pending
|
|
988
|
+
* withdrawal from Extended is in transit (credits wallet raw balance).
|
|
989
|
+
*/
|
|
990
|
+
initBudget(): void {
|
|
991
|
+
const pendingDeposit = this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
|
|
992
|
+
if (pendingDeposit < 0) {
|
|
993
|
+
const inTransit = Math.abs(pendingDeposit);
|
|
994
|
+
if (this.walletBalance) {
|
|
995
|
+
this._addUsdToTokenBalance(this.walletBalance, inTransit);
|
|
996
|
+
}
|
|
997
|
+
logger.debug(`SolveBudget::initBudget pendingDeposit=${pendingDeposit} -> increased wallet raw USD by ${inTransit}`);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
this._recomputeUnusedBalance();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Apply a safety buffer to all liquid balances (VA, wallet, extended trade/withdraw/upnl,
|
|
1005
|
+
* unused balances). Call after withdrawal classification but before LTV/deposit classifiers
|
|
1006
|
+
* so that withdrawal uses full raw amounts while subsequent classifiers see buffered values.
|
|
1007
|
+
*/
|
|
1008
|
+
applyBuffer(factor: number): void {
|
|
1009
|
+
if (factor <= 0 || factor >= 1) return;
|
|
1010
|
+
const mult = 1 - factor;
|
|
1011
|
+
|
|
1012
|
+
const scaleTokenBalance = (tb: TokenBalance | null) => {
|
|
1013
|
+
if (!tb) return;
|
|
1014
|
+
const newAmount = tb.amount.multipliedBy(mult);
|
|
1015
|
+
tb.amount = new Web3Number(newAmount.toFixed(tb.token.decimals), tb.token.decimals);
|
|
1016
|
+
tb.usdValue = tb.usdValue * mult;
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
scaleTokenBalance(this.vaultAssetBalance);
|
|
1020
|
+
scaleTokenBalance(this.vaultUsdcBalance);
|
|
1021
|
+
scaleTokenBalance(this.walletBalance);
|
|
1022
|
+
for (const ub of this.unusedBalance) {
|
|
1023
|
+
scaleTokenBalance(ub);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (this.extendedBalance) {
|
|
1027
|
+
this.extendedBalance.availableForTrade = this.extendedBalance.availableForTrade.multipliedBy(mult);
|
|
1028
|
+
this.extendedBalance.availableForWithdrawal = this.extendedBalance.availableForWithdrawal.multipliedBy(mult);
|
|
1029
|
+
this.extendedBalance.unrealisedPnl = this.extendedBalance.unrealisedPnl.multipliedBy(mult);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
this._recomputeUnusedBalance();
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
get vesuPoolState(): VesuPoolState {
|
|
1036
|
+
assert(this.vesuPoolStates.length === 1, 'SolveBudget::vesuPoolState: vesuPoolStates length must be 1');
|
|
1037
|
+
return this.vesuPoolStates[0];
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// ── Derived getters (buffered where applicable) ─────────────────────
|
|
1041
|
+
|
|
1042
|
+
/** Buffered VA USD: non-stable asset slot (if any) + USDC slot. */
|
|
1043
|
+
get vaUsd(): number {
|
|
1044
|
+
// return this.bufferedTokenUsd(this.vaultAssetBalance) + this.bufferedTokenUsd(this.vaultUsdcBalance);
|
|
1045
|
+
return this.bufferedTokenUsd(this.vaultUsdcBalance); // we dont have swap routes and is not correct, to consider asset balance directlyt in strategy
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/** Buffered USD in VA strategy-asset bucket only. */
|
|
1049
|
+
get vaAssetUsd(): number {
|
|
1050
|
+
return this.bufferedTokenUsd(this.vaultAssetBalance);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/** Buffered USD in VA USDC bucket (includes full VA idle when asset === USDC). */
|
|
1054
|
+
get vaUsdcUsd(): number {
|
|
1055
|
+
return this.bufferedTokenUsd(this.vaultUsdcBalance);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
get walletUsd(): number {
|
|
1059
|
+
return this.bufferedUsd(this._rawTokenUsd(this.walletBalance));
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
get vaWalletUsd(): number {
|
|
1063
|
+
// va buffered + wallet raw
|
|
1064
|
+
return this.vaUsd + this.walletUsd;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
get extAvailWithdraw(): number {
|
|
1068
|
+
const raw = this.extendedBalance?.availableForWithdrawal?.toNumber() ?? 0;
|
|
1069
|
+
return this.bufferedUsd(raw);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
get extAvailUpnl(): number {
|
|
1073
|
+
const raw = this.extendedBalance?.unrealisedPnl?.toNumber() ?? 0;
|
|
1074
|
+
return this.bufferedUsd(raw);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Buffered Extended available-for-trade plus positive {@link ExtendedBalanceState.pendingDeposit}
|
|
1079
|
+
* (deposit in transit is usable the same way as the pre-buffer implementation).
|
|
1080
|
+
*/
|
|
1081
|
+
get extAvailTrade(): number {
|
|
1082
|
+
const raw = this.extendedBalance?.availableForTrade?.toNumber() ?? 0;
|
|
1083
|
+
let v = this.bufferedUsd(raw);
|
|
1084
|
+
const pd = this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
|
|
1085
|
+
if (pd > 0) v += pd;
|
|
1086
|
+
return v;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
get extPendingDeposit(): number {
|
|
1090
|
+
return this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Aggregate positive per-pool borrow headroom (USD). Repay/borrow routes update
|
|
1095
|
+
* {@link vesuPerPoolDebtDeltasToBorrow}; no separate counter.
|
|
1096
|
+
*/
|
|
1097
|
+
get vesuBorrowCapacity(): number {
|
|
1098
|
+
return this.vesuPerPoolDebtDeltasToBorrow.reduce(
|
|
1099
|
+
(a, d) => a + Math.max(0, d.toNumber()),
|
|
1100
|
+
0,
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** Diagnostic: buffered idle + positive debt delta + buffered Extended afT + in-flight deposit. */
|
|
1105
|
+
get totalUnused(): number {
|
|
1106
|
+
const debtSum = this.vesuPerPoolDebtDeltasToBorrow.reduce((a, b) => a + b.toNumber(), 0);
|
|
1107
|
+
let u = this.unusedBalance.reduce((a, b) => a + this.bufferedTokenUsd(b), 0);
|
|
1108
|
+
if (debtSum > 0) u += debtSum;
|
|
1109
|
+
const rawAft = this.extendedBalance?.availableForTrade?.toNumber() ?? 0;
|
|
1110
|
+
const aftBuf = this.bufferedUsd(rawAft);
|
|
1111
|
+
if (aftBuf > 0) u += aftBuf;
|
|
1112
|
+
const pd = this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
|
|
1113
|
+
if (pd > 0) u += pd;
|
|
1114
|
+
return u;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/** Sum of buffered USD across merged unused-balance rows (VA + wallet). */
|
|
1118
|
+
get unusedBalancesBufferedUsdSum(): number {
|
|
1119
|
+
return this.unusedBalance.reduce((a, b) => a + this.bufferedTokenUsd(b), 0);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/** Read-only snapshot view for validation / logging. */
|
|
1123
|
+
get unusedBalanceRows(): readonly TokenBalance[] {
|
|
1124
|
+
return this.unusedBalance;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/** Read-only Vesu pool view for solve computations. */
|
|
1128
|
+
get vesuPools(): readonly VesuPoolState[] {
|
|
1129
|
+
return this.vesuPoolStates;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/** Read-only Extended positions view for solve computations. */
|
|
1133
|
+
get extendedPositionsView(): readonly ExtendedPositionState[] {
|
|
1134
|
+
return this.extendedPositions;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/** Read-only Extended balance view for diagnostics / margin checks. */
|
|
1138
|
+
get extendedBalanceView(): ExtendedBalanceState | null {
|
|
1139
|
+
return this.extendedBalance;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/** Current debt deltas per pool (positive=borrow, negative=repay). */
|
|
1143
|
+
get vesuDebtDeltas(): readonly Web3Number[] {
|
|
1144
|
+
return this.vesuPerPoolDebtDeltasToBorrow;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/** Per-pool rebalance flags derived from target HF checks. */
|
|
1148
|
+
get vesuRebalanceFlags(): readonly boolean[] {
|
|
1149
|
+
return this.shouldVesuRebalance;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/** Raw USD in VA (USDC slot + non-stable asset slot when distinct); spend caps when executing transfers. */
|
|
1153
|
+
private _vaRawUsd(): number {
|
|
1154
|
+
// return this._rawTokenUsd(this.vaultUsdcBalance) + this._rawTokenUsd(this.vaultAssetBalance);
|
|
1155
|
+
return this._rawTokenUsd(this.vaultUsdcBalance); // we dont have swap routes and is not correct, to consider asset balance directlyt in strategy
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/** VA liquidity usable for repay / {@link spendVaRawUsd} (matches nominal balances after any {@link applyBuffer} scaling). */
|
|
1159
|
+
get vaRawUsd(): number {
|
|
1160
|
+
return this._vaRawUsd();
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
private _walletRawUsd(): number {
|
|
1164
|
+
return this._rawTokenUsd(this.walletBalance);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// ── Token snapshot helpers (keep vault / wallet / unusedBalance aligned) ─
|
|
1168
|
+
|
|
1169
|
+
/** Remove up to `usd` notional from a token balance, scaling token amount proportionally. */
|
|
1170
|
+
private _deductUsdFromTokenBalance(tb: TokenBalance, usd: number): void {
|
|
1171
|
+
if (usd <= 0) return;
|
|
1172
|
+
const take = Math.min(usd, tb.usdValue);
|
|
1173
|
+
if (take <= 0) return;
|
|
1174
|
+
const oldUsd = tb.usdValue;
|
|
1175
|
+
const newUsd = Math.max(0, oldUsd - take);
|
|
1176
|
+
tb.usdValue = newUsd;
|
|
1177
|
+
if (oldUsd <= 0) return;
|
|
1178
|
+
const ratio = newUsd / oldUsd;
|
|
1179
|
+
tb.amount = new Web3Number(
|
|
1180
|
+
(tb.amount.toNumber() * ratio).toFixed(tb.token.decimals),
|
|
1181
|
+
tb.token.decimals,
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/** Add USD notional; infers price from current amount/usd when possible, else 1:1. */
|
|
1186
|
+
private _addUsdToTokenBalance(tb: TokenBalance, usd: number): void {
|
|
1187
|
+
if (usd <= 0) return;
|
|
1188
|
+
const amtNum = tb.amount.toNumber();
|
|
1189
|
+
const price =
|
|
1190
|
+
amtNum > 0 && tb.usdValue > 0 ? tb.usdValue / amtNum : 1;
|
|
1191
|
+
const deltaTok = usd / price;
|
|
1192
|
+
tb.usdValue += usd;
|
|
1193
|
+
tb.amount = tb.amount.plus(
|
|
1194
|
+
new Web3Number(deltaTok.toFixed(tb.token.decimals), tb.token.decimals),
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Rebuilds {@link unusedBalance} from vault + wallet snapshots (same merge as refresh).
|
|
1200
|
+
*/
|
|
1201
|
+
private _recomputeUnusedBalance(): void {
|
|
1202
|
+
const balanceMap = new Map<string, TokenBalance>();
|
|
1203
|
+
|
|
1204
|
+
const merge = (b: TokenBalance | null) => {
|
|
1205
|
+
if (!b) return;
|
|
1206
|
+
const key = b.token.address.toString();
|
|
1207
|
+
const row: TokenBalance = {
|
|
1208
|
+
token: b.token,
|
|
1209
|
+
amount: new Web3Number(b.amount.toFixed(b.token.decimals), b.token.decimals),
|
|
1210
|
+
usdValue: b.usdValue,
|
|
1211
|
+
};
|
|
1212
|
+
const existing = balanceMap.get(key);
|
|
1213
|
+
if (existing) {
|
|
1214
|
+
existing.amount = new Web3Number(
|
|
1215
|
+
existing.amount.plus(row.amount).toFixed(existing.token.decimals),
|
|
1216
|
+
existing.token.decimals,
|
|
1217
|
+
);
|
|
1218
|
+
existing.usdValue += row.usdValue;
|
|
1219
|
+
} else {
|
|
1220
|
+
balanceMap.set(key, row);
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
merge(this.vaultAssetBalance);
|
|
1225
|
+
merge(this.vaultUsdcBalance);
|
|
1226
|
+
merge(this.walletBalance);
|
|
1227
|
+
|
|
1228
|
+
this.unusedBalance = Array.from(balanceMap.values());
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// ── Spend methods (return amount consumed, auto-decrement totalUnused) ─
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Spend VA **raw** USD (up to {@link vaRawUsd}). Prefer {@link vaultUsdcBalance} when present, then {@link vaultAssetBalance}.
|
|
1235
|
+
*/
|
|
1236
|
+
spendVA(rawDesired: number): number {
|
|
1237
|
+
const capRaw = this._vaRawUsd();
|
|
1238
|
+
const usedRaw = Math.min(capRaw, Math.max(0, rawDesired));
|
|
1239
|
+
if (usedRaw <= CASE_THRESHOLD_USD) return 0;
|
|
1240
|
+
let rem = usedRaw;
|
|
1241
|
+
if (rem > 0 && this.vaultUsdcBalance && this.vaultUsdcBalance.usdValue > 0) {
|
|
1242
|
+
const fromUsdc = Math.min(rem, this.vaultUsdcBalance.usdValue);
|
|
1243
|
+
this._deductUsdFromTokenBalance(this.vaultUsdcBalance, fromUsdc);
|
|
1244
|
+
rem -= fromUsdc;
|
|
1245
|
+
}
|
|
1246
|
+
if (rem > 0 && this.vaultAssetBalance) {
|
|
1247
|
+
// this._deductUsdFromTokenBalance(this.vaultAssetBalance, rem);
|
|
1248
|
+
throw new Error(`Not implemented: spendVA with vaultAssetBalance`);
|
|
1249
|
+
}
|
|
1250
|
+
this._recomputeUnusedBalance();
|
|
1251
|
+
logger.debug(`SolveBudget::spendVA usedRaw=${usedRaw}, vaUsd=${this.vaUsd}, totalUnused=${this.totalUnused}`);
|
|
1252
|
+
return usedRaw;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Spend nominal/raw USD from VA (e.g. Vesu repay, on-chain USDC). Does not apply the safety buffer to the cap.
|
|
1257
|
+
*/
|
|
1258
|
+
spendVaRawUsd(rawUsdDesired: number): number {
|
|
1259
|
+
const capRaw =
|
|
1260
|
+
// this._rawTokenUsd(this.vaultUsdcBalance) + this._rawTokenUsd(this.vaultAssetBalance);
|
|
1261
|
+
this._rawTokenUsd(this.vaultUsdcBalance); // we dont have swap routes and is not correct, to consider asset balance directlyt in strategy
|
|
1262
|
+
const usedRaw = Math.min(capRaw, Math.max(0, rawUsdDesired));
|
|
1263
|
+
if (usedRaw <= CASE_THRESHOLD_USD) return 0;
|
|
1264
|
+
let rem = usedRaw;
|
|
1265
|
+
if (rem > 0 && this.vaultUsdcBalance && this.vaultUsdcBalance.usdValue > 0) {
|
|
1266
|
+
const fromUsdc = Math.min(rem, this.vaultUsdcBalance.usdValue);
|
|
1267
|
+
this._deductUsdFromTokenBalance(this.vaultUsdcBalance, fromUsdc);
|
|
1268
|
+
rem -= fromUsdc;
|
|
1269
|
+
}
|
|
1270
|
+
if (rem > 0 && this.vaultAssetBalance) {
|
|
1271
|
+
// this._deductUsdFromTokenBalance(this.vaultAssetBalance, rem);
|
|
1272
|
+
throw new Error(`Not implemented: spendVaRawUsd with vaultAssetBalance`);
|
|
1273
|
+
}
|
|
1274
|
+
this._recomputeUnusedBalance();
|
|
1275
|
+
logger.debug(`SolveBudget::spendVaRawUsd usedRaw=${usedRaw}, vaUsd=${this.vaUsd}, totalUnused=${this.totalUnused}`);
|
|
1276
|
+
return usedRaw;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Add **raw nominal USD** to VA (borrow proceeds, wallet→VA in raw USDC, etc.).
|
|
1281
|
+
*/
|
|
1282
|
+
addToVA(rawUsd: number): void {
|
|
1283
|
+
assert(rawUsd >= 0, 'SolveBudget::addToVA amount must be positive');
|
|
1284
|
+
if (rawUsd === 0) return;
|
|
1285
|
+
if (this.vaultUsdcBalance) {
|
|
1286
|
+
this._addUsdToTokenBalance(this.vaultUsdcBalance, rawUsd);
|
|
1287
|
+
} else if (this.vaultAssetBalance) {
|
|
1288
|
+
// this._addUsdToTokenBalance(this.vaultAssetBalance, rawUsd);
|
|
1289
|
+
throw new Error(`Not implemented: addToVA with vaultAssetBalance`);
|
|
1290
|
+
}
|
|
1291
|
+
this._recomputeUnusedBalance();
|
|
1292
|
+
logger.debug(`SolveBudget::addToVA rawUsd=${rawUsd}, vaUsd=${this.vaUsd}, totalUnused=${this.totalUnused}`);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
spendWallet(rawDesired: number): number {
|
|
1296
|
+
const capRaw = this._walletRawUsd();
|
|
1297
|
+
const usedRaw = Math.min(capRaw, Math.max(0, rawDesired));
|
|
1298
|
+
if (usedRaw <= CASE_THRESHOLD_USD) return 0;
|
|
1299
|
+
if (this.walletBalance) {
|
|
1300
|
+
this._deductUsdFromTokenBalance(this.walletBalance, usedRaw);
|
|
1301
|
+
}
|
|
1302
|
+
this._recomputeUnusedBalance();
|
|
1303
|
+
logger.debug(`SolveBudget::spendWallet usedRaw=${usedRaw}, walletUsd=${this.walletUsd}, totalUnused=${this.totalUnused}`);
|
|
1304
|
+
return usedRaw;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/** Add **raw nominal USD** to the operator wallet balance (e.g. Extended→wallet withdrawal). */
|
|
1308
|
+
addToWallet(rawUsd: number): void {
|
|
1309
|
+
assert(rawUsd >= 0, 'SolveBudget::addToWallet amount must be positive');
|
|
1310
|
+
if (rawUsd === 0) return;
|
|
1311
|
+
if (this.walletBalance) {
|
|
1312
|
+
this._addUsdToTokenBalance(this.walletBalance, rawUsd);
|
|
1313
|
+
}
|
|
1314
|
+
this._recomputeUnusedBalance();
|
|
1315
|
+
logger.debug(`SolveBudget::addToWallet rawUsd=${rawUsd}, walletUsd=${this.walletUsd}, totalUnused=${this.totalUnused}`);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
spendVAWallet(rawDesired: number): number {
|
|
1319
|
+
let remaining = Math.max(0, rawDesired);
|
|
1320
|
+
const vaSpent = this.spendVA(remaining);
|
|
1321
|
+
remaining -= vaSpent;
|
|
1322
|
+
const walletSpent = this.spendWallet(remaining);
|
|
1323
|
+
return vaSpent + walletSpent;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
private _updateExtAvailWithdraw(desiredRaw: number, isSpend: boolean): number {
|
|
1327
|
+
assert(desiredRaw > 0, 'SolveBudget::_updateExtAvailWithdraw amount must be positive');
|
|
1328
|
+
let rawDelta: number;
|
|
1329
|
+
if (isSpend) {
|
|
1330
|
+
const capRaw = this.extendedBalance?.availableForWithdrawal?.toNumber() ?? 0;
|
|
1331
|
+
const useRaw = Math.min(capRaw, desiredRaw);
|
|
1332
|
+
if (useRaw <= 0) return 0;
|
|
1333
|
+
rawDelta = -useRaw;
|
|
1334
|
+
} else {
|
|
1335
|
+
rawDelta = desiredRaw;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (this.extendedBalance) {
|
|
1339
|
+
this.extendedBalance.availableForWithdrawal = safeUsdcWeb3Number(this.extendedBalance.availableForWithdrawal.toNumber() + rawDelta);
|
|
1340
|
+
this.extendedBalance.availableForTrade = safeUsdcWeb3Number(this.extendedBalance.availableForTrade.toNumber() + rawDelta);
|
|
1341
|
+
this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() + rawDelta);
|
|
1342
|
+
this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() + rawDelta);
|
|
1343
|
+
}
|
|
1344
|
+
logger.debug(`SolveBudget::updateExtAvailWithdraw rawDelta=${rawDelta}, extAvailWithdraw=${this.extAvailWithdraw}, totalUnused=${this.totalUnused}`);
|
|
1345
|
+
return rawDelta;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
private _updateExtAvailUpnl(desiredRaw: number, isSpend: boolean): number {
|
|
1349
|
+
assert(desiredRaw > 0, 'SolveBudget::_updateExtAvailUpnl amount must be positive');
|
|
1350
|
+
let rawDelta: number;
|
|
1351
|
+
if (isSpend) {
|
|
1352
|
+
const capRaw = this.extendedBalance?.unrealisedPnl?.toNumber() ?? 0;
|
|
1353
|
+
const useRaw = Math.min(capRaw, desiredRaw);
|
|
1354
|
+
if (useRaw <= 0) return 0;
|
|
1355
|
+
rawDelta = -useRaw;
|
|
1356
|
+
} else {
|
|
1357
|
+
rawDelta = desiredRaw;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
if (this.extendedBalance) {
|
|
1361
|
+
this.extendedBalance.unrealisedPnl = safeUsdcWeb3Number(this.extendedBalance.unrealisedPnl.toNumber() + rawDelta);
|
|
1362
|
+
this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() + rawDelta);
|
|
1363
|
+
this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() + rawDelta);
|
|
1364
|
+
this.extendedBalance.availableForTrade = safeUsdcWeb3Number(this.extendedBalance.availableForTrade.toNumber() + rawDelta);
|
|
1365
|
+
}
|
|
1366
|
+
logger.debug(`SolveBudget::updateExtAvailUpnl rawDelta=${rawDelta}, extAvailUpnl=${this.extAvailUpnl}, totalUnused=${this.totalUnused}`);
|
|
1367
|
+
return rawDelta;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
spendExtAvailTrade(rawDesired: number): number {
|
|
1371
|
+
const usedWd = this._updateExtAvailWithdraw(rawDesired, true);
|
|
1372
|
+
const tookWd = Math.abs(usedWd);
|
|
1373
|
+
const rem = rawDesired - tookWd;
|
|
1374
|
+
const usedUpnl = rem > 0 ? this._updateExtAvailUpnl(rem, true) : 0;
|
|
1375
|
+
logger.debug(`SolveBudget::updateExtAvailTrade rawSum=${usedWd + usedUpnl}, extAvailTrade=${this.extAvailTrade}, totalUnused=${this.totalUnused}`);
|
|
1376
|
+
return usedWd + usedUpnl;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// simply reduces available amounts, but maintains equity and balance.
|
|
1380
|
+
spendExtAvailTradeToEquityOnly(rawDesired: number): number {
|
|
1381
|
+
const used = this._updateExtAvailWithdraw(rawDesired, true);
|
|
1382
|
+
const remaining = rawDesired - Math.abs(used);
|
|
1383
|
+
const usedUpnl = remaining > 0 ? this._updateExtAvailUpnl(remaining, true) : 0;
|
|
1384
|
+
if (this.extendedBalance) {
|
|
1385
|
+
// add whats subtracted earlier to equity
|
|
1386
|
+
const net = Math.abs(used) + Math.abs(usedUpnl);
|
|
1387
|
+
if (net.toFixed(0) != rawDesired.toFixed(0)) {
|
|
1388
|
+
throw new Error(`SolveBudget::spendExtAvailTradeToEquityOnly net=${net} != rawDesired=${rawDesired}`);
|
|
1389
|
+
}
|
|
1390
|
+
this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() + net);
|
|
1391
|
+
this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() + net);
|
|
1392
|
+
}
|
|
1393
|
+
logger.debug(`SolveBudget::updateExtAvailTrade rawSum=${used + usedUpnl}, extAvailTrade=${this.extAvailTrade}, totalUnused=${this.totalUnused}`);
|
|
1394
|
+
return used + usedUpnl;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
spendExtAvailWithdraw(rawDesired: number): number {
|
|
1398
|
+
return this._updateExtAvailWithdraw(rawDesired, true);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
spendExtAvailUpnl(rawDesired: number): number {
|
|
1402
|
+
return this._updateExtAvailUpnl(rawDesired, true);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Withdraw from Extended **withdrawal bucket only** to operator wallet (planning).
|
|
1407
|
+
* Used when VA must be funded from Extended and withdraw should be exhausted before unrealised PnL.
|
|
1408
|
+
*/
|
|
1409
|
+
spendAvailWithdrawToWallet(rawDesired: number): number {
|
|
1410
|
+
const want = Math.max(0, rawDesired);
|
|
1411
|
+
if (want <= CASE_THRESHOLD_USD) return 0;
|
|
1412
|
+
const rawDelta = this._updateExtAvailWithdraw(want, true);
|
|
1413
|
+
if (rawDelta === 0) return 0;
|
|
1414
|
+
const used = -rawDelta;
|
|
1415
|
+
this.addToWallet(used);
|
|
1416
|
+
return used;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Required Extended equity (USD) for current open positions: total notional ÷ strategy leverage.
|
|
1421
|
+
* Same basis as {@link ExtendedSVKVesuStateManager._classifyLtvExtended} margin check.
|
|
1422
|
+
*/
|
|
1423
|
+
private _extendedMarginRequirementUsd(): number {
|
|
1424
|
+
const lev = calculateExtendedLevergae();
|
|
1425
|
+
if (lev <= 0 || this.extendedPositions.length === 0) return 0;
|
|
1426
|
+
const totalPosUsd = this.extendedPositions.reduce(
|
|
1427
|
+
(s, p) => s + p.valueUsd.toNumber(),
|
|
1428
|
+
0,
|
|
1429
|
+
);
|
|
1430
|
+
return totalPosUsd / lev;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/** How much more equity is needed before deposits should increase withdraw / trade availability. */
|
|
1434
|
+
private _extendedEquityShortfallUsd(): number {
|
|
1435
|
+
if (!this.extendedBalance) return 0;
|
|
1436
|
+
const req = this._extendedMarginRequirementUsd();
|
|
1437
|
+
const eq = this.extendedBalance.equity.toNumber();
|
|
1438
|
+
return Math.max(0, req - eq);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Credits a USDC inflow on Extended. Fills margin shortfall (balance+equity only) first;
|
|
1443
|
+
* any remainder is credited across balance, equity, availableForWithdrawal, and availableForTrade.
|
|
1444
|
+
*/
|
|
1445
|
+
addToExtAvailTrade(rawUsd: number): void {
|
|
1446
|
+
assert(rawUsd >= 0, 'SolveBudget::addToExtAvailTrade amount must be non-negative');
|
|
1447
|
+
if (rawUsd <= CASE_THRESHOLD_USD) return;
|
|
1448
|
+
if (!this.extendedBalance) {
|
|
1449
|
+
logger.warn('SolveBudget::addToExtAvailTrade skipped — no extendedBalance');
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const shortfall = this._extendedEquityShortfallUsd();
|
|
1454
|
+
const toMargin = Math.min(rawUsd, shortfall);
|
|
1455
|
+
const toLiquid = rawUsd - toMargin;
|
|
1456
|
+
|
|
1457
|
+
if (toMargin > CASE_THRESHOLD_USD) {
|
|
1458
|
+
const b = this.extendedBalance.balance.toNumber();
|
|
1459
|
+
const e = this.extendedBalance.equity.toNumber();
|
|
1460
|
+
this.extendedBalance.balance = safeUsdcWeb3Number(b + toMargin);
|
|
1461
|
+
this.extendedBalance.equity = safeUsdcWeb3Number(e + toMargin);
|
|
1462
|
+
logger.debug(
|
|
1463
|
+
`SolveBudget::addToExtAvailTrade margin-first rawUsd=${toMargin} ` +
|
|
1464
|
+
`(shortfallBefore=${shortfall}, balance=${b + toMargin}, equity=${e + toMargin})`,
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
if (toLiquid > CASE_THRESHOLD_USD) {
|
|
1469
|
+
this._updateExtAvailWithdraw(toLiquid, false);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
logger.debug(
|
|
1473
|
+
`SolveBudget::addToExtAvailTrade total rawUsd=${rawUsd} toLiquid=${toLiquid} ` +
|
|
1474
|
+
`extAvailTrade=${this.extAvailTrade}, totalUnused=${this.totalUnused}`,
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
spendVesuBorrowCapacity(desired: number): { used: number, spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] } {
|
|
1479
|
+
const used = Math.min(this.vesuBorrowCapacity, Math.max(0, desired));
|
|
1480
|
+
|
|
1481
|
+
let spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] = [];
|
|
1482
|
+
// reduce the debt delta for the pool
|
|
1483
|
+
for (let index = 0; index < this.vesuPerPoolDebtDeltasToBorrow.length; index++) {
|
|
1484
|
+
const d = this.vesuPerPoolDebtDeltasToBorrow[index];
|
|
1485
|
+
if (d.toNumber() <= CASE_THRESHOLD_USD) continue;
|
|
1486
|
+
const borrowed = Math.min(d.toNumber(), used);
|
|
1487
|
+
this.vesuPerPoolDebtDeltasToBorrow[index] = d.minus(safeUsdcWeb3Number(borrowed));
|
|
1488
|
+
this.vesuPoolStates[index].debtAmount = safeUsdcWeb3Number(this.vesuPoolStates[index].debtAmount.toNumber() + borrowed);
|
|
1489
|
+
this.vesuPoolStates[index].debtUsdValue = this.vesuPoolStates[index].debtAmount.toNumber() * this.vesuPoolStates[index].debtPrice;
|
|
1490
|
+
spendsByPool.push({ poolId: this.vesuPoolStates[index].poolId, amount: safeUsdcWeb3Number(borrowed), collateralToken: this.vesuPoolStates[index].collateralToken, debtToken: this.vesuPoolStates[index].debtToken });
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
logger.debug(`SolveBudget::spendVesuBorrowCapacity used=${used}, vesuBorrowCapacity=${this.vesuBorrowCapacity}, totalUnused=${this.totalUnused}`);
|
|
1494
|
+
return { used, spendsByPool };
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
repayVesuBorrowCapacity(desired: number): { used: number, spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] } {
|
|
1498
|
+
assert(desired > 0, 'SolveBudget::repayVesuBorrowCapacity desired must be positive');
|
|
1499
|
+
const used = desired;
|
|
1500
|
+
|
|
1501
|
+
const spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] = [];
|
|
1502
|
+
for (let index = 0; index < this.vesuPerPoolDebtDeltasToBorrow.length; index++) {
|
|
1503
|
+
const d = this.vesuPerPoolDebtDeltasToBorrow[index];
|
|
1504
|
+
if (d.toNumber() >= -CASE_THRESHOLD_USD) continue; // positive means, dont repay
|
|
1505
|
+
const repaid = Math.min(Math.abs(d.toNumber()), used);
|
|
1506
|
+
this.vesuPerPoolDebtDeltasToBorrow[index] = d.plus(safeUsdcWeb3Number(repaid));
|
|
1507
|
+
this.vesuPoolStates[index].debtAmount = safeUsdcWeb3Number(this.vesuPoolStates[index].debtAmount.toNumber() - repaid);
|
|
1508
|
+
spendsByPool.push({ poolId: this.vesuPoolStates[index].poolId, amount: safeUsdcWeb3Number(-repaid), collateralToken: this.vesuPoolStates[index].collateralToken, debtToken: this.vesuPoolStates[index].debtToken });
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
logger.debug(`SolveBudget::repayVesuBorrowCapacity used=${used}, vesuBorrowCapacity=${this.vesuBorrowCapacity}, totalUnused=${this.totalUnused}`);
|
|
1512
|
+
return { used, spendsByPool };
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// ── State mutation ──────────────────────────────────────────────────
|
|
1516
|
+
|
|
1517
|
+
/** Update a Vesu pool's collateral and debt after a lever / borrow / repay route. */
|
|
1518
|
+
applyVesuDelta(poolId: ContractAddr, collToken: TokenInfo, debtToken: TokenInfo, collDelta: Web3Number, debtDelta: Web3Number): void {
|
|
1519
|
+
const pool = this.vesuPoolStates.find(
|
|
1520
|
+
(p) => p.poolId.toString() === poolId.toString() && p.collateralToken.symbol === collToken.symbol && p.debtToken.symbol === debtToken.symbol,
|
|
1521
|
+
);
|
|
1522
|
+
if (!pool) throw new Error(`SolveBudget::applyVesuDelta pool not found: poolId=${poolId.toString()}, collToken=${collToken.symbol}, debtToken=${debtToken.symbol}`);
|
|
1523
|
+
pool.collateralAmount = new Web3Number(
|
|
1524
|
+
pool.collateralAmount.plus(collDelta).toFixed(pool.collateralToken.decimals),
|
|
1525
|
+
pool.collateralToken.decimals,
|
|
1526
|
+
);
|
|
1527
|
+
pool.collateralUsdValue = pool.collateralAmount.toNumber() * pool.collateralPrice;
|
|
1528
|
+
pool.debtAmount = new Web3Number(
|
|
1529
|
+
pool.debtAmount.plus(debtDelta).toFixed(pool.debtToken.decimals),
|
|
1530
|
+
pool.debtToken.decimals,
|
|
1531
|
+
);
|
|
1532
|
+
pool.debtUsdValue = pool.debtAmount.toNumber() * pool.debtPrice;
|
|
1533
|
+
|
|
1534
|
+
// recompute per pool deltas here
|
|
1535
|
+
const vesuPerPoolDebtDeltasToBorrow = this._computeperPoolDebtDeltasToBorrow();
|
|
1536
|
+
this.vesuPerPoolDebtDeltasToBorrow = vesuPerPoolDebtDeltasToBorrow.map((item) => item.deltaDebt);
|
|
1537
|
+
this.shouldVesuRebalance = vesuPerPoolDebtDeltasToBorrow.map((item) => item.shouldRebalance);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* Update Extended position size after a lever route; sync {@link ExtendedPositionState.valueUsd}
|
|
1542
|
+
* and margin buckets (released USD on decrease, locked USD on increase).
|
|
1543
|
+
*
|
|
1544
|
+
* @param collateralPriceUsd BTC collateral price for notional / margin math; if omitted, uses
|
|
1545
|
+
* existing valueUsd / |size| or the first Vesu pool collateral price.
|
|
1546
|
+
*/
|
|
1547
|
+
applyExtendedExposureDelta(
|
|
1548
|
+
instrument: string,
|
|
1549
|
+
sizeDelta: Web3Number,
|
|
1550
|
+
collateralPriceUsd?: number,
|
|
1551
|
+
): void {
|
|
1552
|
+
const btcEps = 10 ** -COLLATERAL_PRECISION;
|
|
1553
|
+
const lev = calculateExtendedLevergae();
|
|
1554
|
+
|
|
1555
|
+
let pos = this.extendedPositions.find((p) => p.instrument === instrument);
|
|
1556
|
+
const oldAbs = pos ? Math.abs(pos.size.toNumber()) : 0;
|
|
1557
|
+
const oldValUsd = pos ? pos.valueUsd.toNumber() : 0;
|
|
1558
|
+
|
|
1559
|
+
if (pos) {
|
|
1560
|
+
pos.size = new Web3Number(pos.size.plus(sizeDelta).toFixed(8), 8);
|
|
1561
|
+
} else if (sizeDelta.toNumber() !== 0) {
|
|
1562
|
+
this.extendedPositions.push({
|
|
1563
|
+
instrument,
|
|
1564
|
+
side: 'SHORT',
|
|
1565
|
+
size: sizeDelta,
|
|
1566
|
+
valueUsd: new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1567
|
+
leverage: '0',
|
|
1568
|
+
});
|
|
1569
|
+
pos = this.extendedPositions[this.extendedPositions.length - 1];
|
|
1570
|
+
} else {
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
const newAbs = Math.abs(pos.size.toNumber());
|
|
1575
|
+
let price = collateralPriceUsd;
|
|
1576
|
+
if (price === undefined || price <= 0) {
|
|
1577
|
+
price = oldAbs > btcEps ? oldValUsd / oldAbs : (this.vesuPoolStates[0]?.collateralPrice ?? 0);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
if (price > 0) {
|
|
1581
|
+
pos.valueUsd = new Web3Number(
|
|
1582
|
+
(newAbs * price).toFixed(USDC_TOKEN_DECIMALS),
|
|
1583
|
+
USDC_TOKEN_DECIMALS,
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (!this.extendedBalance || lev <= 0 || price <= 0) return;
|
|
1588
|
+
|
|
1589
|
+
const dAbs = newAbs - oldAbs;
|
|
1590
|
+
if (dAbs < -btcEps) {
|
|
1591
|
+
const releasedUsd = (-dAbs) * price / lev;
|
|
1592
|
+
if (releasedUsd > CASE_THRESHOLD_USD) {
|
|
1593
|
+
this.addToExtAvailTrade(releasedUsd);
|
|
1594
|
+
}
|
|
1595
|
+
} else if (dAbs > btcEps) {
|
|
1596
|
+
const lockedUsd = dAbs * price / lev;
|
|
1597
|
+
if (lockedUsd > CASE_THRESHOLD_USD) {
|
|
1598
|
+
this._lockExtendedMarginUsd(lockedUsd);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/** Pull margin for larger Extended exposure from liquid buckets, then balance/equity. */
|
|
1604
|
+
private _lockExtendedMarginUsd(lockedUsd: number): void {
|
|
1605
|
+
if (lockedUsd <= CASE_THRESHOLD_USD || !this.extendedBalance) return;
|
|
1606
|
+
let rem = lockedUsd;
|
|
1607
|
+
|
|
1608
|
+
const uw = Math.min(rem, Math.max(0, this.extendedBalance.availableForWithdrawal.toNumber()));
|
|
1609
|
+
if (uw > 0) {
|
|
1610
|
+
this._updateExtAvailWithdraw(uw, true);
|
|
1611
|
+
rem -= uw;
|
|
1612
|
+
}
|
|
1613
|
+
if (rem > 0) {
|
|
1614
|
+
const uu = Math.min(rem, Math.max(0, this.extendedBalance.unrealisedPnl.toNumber()));
|
|
1615
|
+
if (uu > 0) {
|
|
1616
|
+
this._updateExtAvailUpnl(uu, true);
|
|
1617
|
+
rem -= uu;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (rem > 0) {
|
|
1621
|
+
const b = this.extendedBalance.balance.toNumber();
|
|
1622
|
+
const e = this.extendedBalance.equity.toNumber();
|
|
1623
|
+
this.extendedBalance.balance = safeUsdcWeb3Number(b - rem);
|
|
1624
|
+
this.extendedBalance.equity = safeUsdcWeb3Number(e - rem);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* For each Vesu pool, computes the debt delta needed to bring the position
|
|
1631
|
+
* back to the target health factor.
|
|
1632
|
+
*
|
|
1633
|
+
* Positive = can borrow more, negative = need to repay.
|
|
1634
|
+
*/
|
|
1635
|
+
private _computeperPoolDebtDeltasToBorrow(): { deltaDebt: Web3Number, shouldRebalance: boolean }[] {
|
|
1636
|
+
const output = this.vesuPoolStates.map((pool) =>
|
|
1637
|
+
calculateDeltaDebtAmount(
|
|
1638
|
+
pool.collateralAmount,
|
|
1639
|
+
pool.debtAmount,
|
|
1640
|
+
pool.debtPrice,
|
|
1641
|
+
pool.collateralPrice,
|
|
1642
|
+
),
|
|
1643
|
+
);
|
|
1644
|
+
|
|
1645
|
+
// assert al pools share same collateral and debt tokens
|
|
1646
|
+
const collateralToken = this.vesuPoolStates[0].collateralToken;
|
|
1647
|
+
const debtToken = this.vesuPoolStates[0].debtToken;
|
|
1648
|
+
for (const pool of this.vesuPoolStates) {
|
|
1649
|
+
if (pool.collateralToken.symbol !== collateralToken.symbol || pool.debtToken.symbol !== debtToken.symbol) {
|
|
1650
|
+
// not yet handled in code with multiple pool types. will be handled in future.
|
|
1651
|
+
throw new Error(`SolveBudget::_computeperPoolDebtDeltasToBorrow: All pools must share same collateral and debt tokens`);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return output;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
export function createSolveBudgetFromRawState(params: {
|
|
1659
|
+
assetToken: TokenInfo;
|
|
1660
|
+
usdcToken: TokenInfo;
|
|
1661
|
+
unusedBalance: TokenBalance[];
|
|
1662
|
+
walletBalance: TokenBalance | null;
|
|
1663
|
+
vaultAssetBalance: TokenBalance | null;
|
|
1664
|
+
vaultUsdcBalance: TokenBalance | null;
|
|
1665
|
+
extendedPositions: ExtendedPositionState[];
|
|
1666
|
+
extendedBalance: ExtendedBalanceState | null;
|
|
1667
|
+
vesuPoolStates: VesuPoolState[];
|
|
1668
|
+
limitBalanceBufferFactor?: number;
|
|
1669
|
+
}): SolveBudget {
|
|
1670
|
+
const budget = new SolveBudget({
|
|
1671
|
+
assetToken: params.assetToken,
|
|
1672
|
+
usdcToken: params.usdcToken,
|
|
1673
|
+
unusedBalance: params.unusedBalance,
|
|
1674
|
+
walletBalance: params.walletBalance,
|
|
1675
|
+
vaultAssetBalance: params.vaultAssetBalance,
|
|
1676
|
+
vaultUsdcBalance: params.vaultUsdcBalance,
|
|
1677
|
+
extendedPositions: params.extendedPositions,
|
|
1678
|
+
extendedBalance: params.extendedBalance,
|
|
1679
|
+
vesuPoolStates: params.vesuPoolStates,
|
|
1680
|
+
});
|
|
1681
|
+
if (params.limitBalanceBufferFactor && params.limitBalanceBufferFactor > 0) {
|
|
1682
|
+
budget.applyBuffer(params.limitBalanceBufferFactor);
|
|
1683
|
+
}
|
|
1684
|
+
return budget;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// ─── State Manager ─────────────────────────────────────────────────────────────
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Reads all on-chain / off-chain state for the Extended + SVK + Vesu strategy,
|
|
1691
|
+
* then solves for the optimal rebalancing deltas.
|
|
1692
|
+
*
|
|
1693
|
+
* Usage:
|
|
1694
|
+
* const manager = new ExtendedSVKVesuStateManager(config);
|
|
1695
|
+
* const result = await manager.solve();
|
|
1696
|
+
*/
|
|
1697
|
+
export class ExtendedSVKVesuStateManager {
|
|
1698
|
+
private readonly _config: StateManagerConfig;
|
|
1699
|
+
private readonly _tag = "ExtendedSVKVesuStateManager";
|
|
1700
|
+
|
|
1701
|
+
/** Single mutable state holder — initialised by _refresh(), budget by initBudget(). */
|
|
1702
|
+
private _budget!: SolveBudget;
|
|
1703
|
+
|
|
1704
|
+
constructor(config: StateManagerConfig) {
|
|
1705
|
+
this._config = config;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1709
|
+
// Public API
|
|
1710
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1711
|
+
|
|
1712
|
+
/**
|
|
1713
|
+
* Main entry point. Refreshes all state, then computes and returns
|
|
1714
|
+
* the optimal deltas for rebalancing the strategy.
|
|
1715
|
+
*
|
|
1716
|
+
* @param withdrawAmount — amount (in vault asset, e.g. USDC) the user wants
|
|
1717
|
+
* to withdraw. When > 0 the solver shrinks protocol allocations so that the
|
|
1718
|
+
* vault allocator ends up with enough balance to execute bringLiquidity.
|
|
1719
|
+
* Pass 0 (default) for normal investment / rebalancing cycles.
|
|
1720
|
+
*/
|
|
1721
|
+
async solve(
|
|
1722
|
+
withdrawAmount: Web3Number = new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1723
|
+
): Promise<SolveResult | null> {
|
|
1724
|
+
await this._refresh();
|
|
1725
|
+
if (Math.abs(this._budget.extPendingDeposit) > 0) {
|
|
1726
|
+
logger.warn(`${this._tag}::solve extPendingDeposit=${this._budget.extPendingDeposit}`);
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
this._validateRefreshedState();
|
|
1730
|
+
|
|
1731
|
+
// // Step 1: For each Vesu pool, compute debt delta needed for LTV maintenance
|
|
1732
|
+
// const perPoolDebtDeltasToBorrow = this._computeperPoolDebtDeltasToBorrow();
|
|
1733
|
+
|
|
1734
|
+
// // Step 2: Determine distributable capital after debt and withdrawal adjustments
|
|
1735
|
+
// const distributableAmount = this._computeDistributableAmount(
|
|
1736
|
+
// perPoolDebtDeltasToBorrow,
|
|
1737
|
+
// withdrawAmount,
|
|
1738
|
+
// );
|
|
1739
|
+
|
|
1740
|
+
// // Step 3: Split distributable capital between Vesu and Extended allocations
|
|
1741
|
+
// const { vesuAllocationUsd, extendedAllocationUsd } =
|
|
1742
|
+
// this._computeAllocationSplit(distributableAmount, perPoolDebtDeltasToBorrow);
|
|
1743
|
+
|
|
1744
|
+
// // Step 4: Convert Vesu allocation to per-pool collateral deltas
|
|
1745
|
+
// const vesuDeltas =
|
|
1746
|
+
// this._computePerPoolCollateralDeltas(vesuAllocationUsd, perPoolDebtDeltasToBorrow);
|
|
1747
|
+
|
|
1748
|
+
// // Step 5: Compute Extended position deltas for delta-neutral matching
|
|
1749
|
+
// const extendedPositionDeltas =
|
|
1750
|
+
// this._computeExtendedPositionDeltas(vesuDeltas);
|
|
1751
|
+
// console.log({ extendedPositionDeltas: extendedPositionDeltas.map((delta) => ({
|
|
1752
|
+
// instrument: delta.instrument,
|
|
1753
|
+
// delta: delta.delta.toNumber(),
|
|
1754
|
+
// })) });
|
|
1755
|
+
|
|
1756
|
+
// // Step 6: Compute net deposit/withdrawal change for Extended platform
|
|
1757
|
+
// const extendedDeposit =
|
|
1758
|
+
// this._computeExtendedDepositDelta(extendedAllocationUsd);
|
|
1759
|
+
// console.log({ extendedDeposit: extendedDeposit.toNumber() });
|
|
1760
|
+
|
|
1761
|
+
// // Step 7: compute vesu deposit amount (i.e. collateral required to open position may be)
|
|
1762
|
+
// const vesuDepositAmount =
|
|
1763
|
+
// this._computeVesuDepositAmount(vesuDeltas);
|
|
1764
|
+
|
|
1765
|
+
// // Step 7: Compute bringLiquidity amount (= withdrawAmount for the vault)
|
|
1766
|
+
// const bringLiquidityAmount =
|
|
1767
|
+
// this._computeBringLiquidityAmount(withdrawAmount);
|
|
1768
|
+
// console.log({ bringLiquidityAmount: bringLiquidityAmount.toNumber() });
|
|
1769
|
+
|
|
1770
|
+
// Step 8: Classify state into actionable cases — each case carries
|
|
1771
|
+
// its own routes with amounts and state info.
|
|
1772
|
+
const cases = this._classifyCases(withdrawAmount);
|
|
1773
|
+
|
|
1774
|
+
const result: SolveResult = {
|
|
1775
|
+
cases,
|
|
1776
|
+
// ignore these fields for now. only cases are relevant.
|
|
1777
|
+
extendedDeposit: safeUsdcWeb3Number(0),
|
|
1778
|
+
extendedPositionDeltas: [],
|
|
1779
|
+
vesuDepositAmount: safeUsdcWeb3Number(0),
|
|
1780
|
+
vesuDeltas: [],
|
|
1781
|
+
vesuAllocationUsd: safeUsdcWeb3Number(0),
|
|
1782
|
+
extendedAllocationUsd: safeUsdcWeb3Number(0),
|
|
1783
|
+
bringLiquidityAmount: safeUsdcWeb3Number(0),
|
|
1784
|
+
pendingDeposit: safeUsdcWeb3Number(0),
|
|
1785
|
+
};
|
|
1786
|
+
|
|
1787
|
+
this._logSolveResult(result);
|
|
1788
|
+
return result;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1792
|
+
// Private — state refresh
|
|
1793
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1794
|
+
|
|
1795
|
+
/**
|
|
1796
|
+
* Reads all on-chain and off-chain state in parallel and stores
|
|
1797
|
+
* results in private instance variables.
|
|
1798
|
+
*/
|
|
1799
|
+
private async _refresh(): Promise<void> {
|
|
1800
|
+
logger.info(`${this._tag}::_refresh starting`);
|
|
1801
|
+
|
|
1802
|
+
const [
|
|
1803
|
+
vaultAssetBalance,
|
|
1804
|
+
vaultUsdcBalance,
|
|
1805
|
+
walletBalance,
|
|
1806
|
+
vesuPoolStates,
|
|
1807
|
+
extendedBalance,
|
|
1808
|
+
extendedPositions,
|
|
1809
|
+
] = await Promise.all([
|
|
1810
|
+
this._fetchVaultAllocatorAssetBalance(),
|
|
1811
|
+
this._fetchVaultAllocatorUsdcBalanceIfDistinct(),
|
|
1812
|
+
this._fetchWalletBalances(),
|
|
1813
|
+
this._fetchAllVesuPoolStates(),
|
|
1814
|
+
this._fetchExtendedBalance(),
|
|
1815
|
+
this._fetchExtendedPositions(),
|
|
1816
|
+
]);
|
|
1817
|
+
|
|
1818
|
+
logger.verbose(
|
|
1819
|
+
`${this._tag}::_refresh ` +
|
|
1820
|
+
`${vaultAssetBalance ? `VA asset ${vaultAssetBalance.token.symbol}=$${vaultAssetBalance.usdValue.toFixed(2)}, ` : ""}` +
|
|
1821
|
+
`VA USDC=${vaultUsdcBalance.usdValue.toFixed(2)}` +
|
|
1822
|
+
`, wallet=${walletBalance.usdValue}`,
|
|
1823
|
+
);
|
|
1824
|
+
const unusedBalance = this._computeUnusedBalances(
|
|
1825
|
+
vaultAssetBalance,
|
|
1826
|
+
vaultUsdcBalance,
|
|
1827
|
+
walletBalance,
|
|
1828
|
+
);
|
|
1829
|
+
|
|
1830
|
+
this._budget = createSolveBudgetFromRawState({
|
|
1831
|
+
assetToken: this._config.assetToken,
|
|
1832
|
+
usdcToken: this._config.usdcToken,
|
|
1833
|
+
unusedBalance,
|
|
1834
|
+
walletBalance,
|
|
1835
|
+
vaultAssetBalance,
|
|
1836
|
+
vaultUsdcBalance,
|
|
1837
|
+
extendedPositions,
|
|
1838
|
+
extendedBalance: {
|
|
1839
|
+
availableForTrade:
|
|
1840
|
+
extendedBalance?.availableForTrade ||
|
|
1841
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1842
|
+
availableForWithdrawal:
|
|
1843
|
+
extendedBalance?.availableForWithdrawal ||
|
|
1844
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1845
|
+
unrealisedPnl:
|
|
1846
|
+
extendedBalance?.unrealisedPnl ||
|
|
1847
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1848
|
+
balance:
|
|
1849
|
+
extendedBalance?.balance || new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1850
|
+
equity:
|
|
1851
|
+
extendedBalance?.equity || new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1852
|
+
pendingDeposit:
|
|
1853
|
+
extendedBalance?.pendingDeposit ||
|
|
1854
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1855
|
+
},
|
|
1856
|
+
vesuPoolStates,
|
|
1857
|
+
});
|
|
1858
|
+
this._budget.logStateSummary();
|
|
1859
|
+
|
|
1860
|
+
const totalUnusedUsd = unusedBalance.reduce(
|
|
1861
|
+
(acc, b) => acc + b.usdValue,
|
|
1862
|
+
0,
|
|
1863
|
+
);
|
|
1864
|
+
logger.info(
|
|
1865
|
+
`${this._tag}::_refresh completed — ` +
|
|
1866
|
+
`unusedBalances: ${unusedBalance.length} tokens [${unusedBalance.map((b) => `${b.token.symbol}=$${b.usdValue.toFixed(2)}`).join(', ')}], ` +
|
|
1867
|
+
`totalUnusedUsd: ${totalUnusedUsd.toFixed(2)}, ` +
|
|
1868
|
+
`extendedPositions: ${extendedPositions.length} [${extendedPositions.map((p) => `${p.instrument}=${p.size.toFixed(6)} ${p.side}, ${p.valueUsd.toFixed(6)} ${p.instrument}`).join(', ')}], ` +
|
|
1869
|
+
`vesuPools: ${vesuPoolStates.length} [${vesuPoolStates.map((p) => `${p.poolId.shortString()}=${p.debtAmount.toFixed(6)} ${p.debtToken.symbol}, ${p.collateralAmount.toFixed(6)} ${p.collateralToken.symbol}`).join(', ')}], ` +
|
|
1870
|
+
`availableForTrade: ${extendedBalance?.availableForTrade.toNumber()} - ` +
|
|
1871
|
+
`availableForWithdrawal: ${extendedBalance?.availableForWithdrawal.toNumber()} - ` +
|
|
1872
|
+
`unrealisedPnl: ${extendedBalance?.unrealisedPnl.toNumber()} - ` +
|
|
1873
|
+
`extendedBalance::balance: ${extendedBalance?.balance.toNumber()} - ` +
|
|
1874
|
+
`extendedBalance::pendingDeposit: ${extendedBalance?.pendingDeposit.toNumber()} - ` +
|
|
1875
|
+
`extendedBalance::equity: ${extendedBalance?.equity.toNumber()}`,
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// todo add communication check with python server of extended. if not working, throw error in solve function.
|
|
1880
|
+
|
|
1881
|
+
/** True when strategy asset and USDC share one token — VA idle balance is tracked as USDC, not as asset. */
|
|
1882
|
+
private _vaultAssetAndUsdcAreSameToken(): boolean {
|
|
1883
|
+
return this._config.assetToken.address.eq(this._config.usdcToken.address);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/**
|
|
1887
|
+
* Reads idle {@link StateManagerConfig.assetToken} in the vault allocator when it differs from USDC.
|
|
1888
|
+
* When asset and USDC are the same token, returns null (that balance is reported via {@link _fetchVaultAllocatorUsdcBalanceIfDistinct} only).
|
|
1889
|
+
*/
|
|
1890
|
+
private async _fetchVaultAllocatorAssetBalance(): Promise<TokenBalance | null> {
|
|
1891
|
+
if (this._vaultAssetAndUsdcAreSameToken()) {
|
|
1892
|
+
return null;
|
|
1893
|
+
}
|
|
1894
|
+
const { assetToken, vaultAllocator, networkConfig, pricer } = this._config;
|
|
1895
|
+
const balance = await new ERC20(networkConfig).balanceOf(
|
|
1896
|
+
assetToken.address,
|
|
1897
|
+
vaultAllocator,
|
|
1898
|
+
assetToken.decimals,
|
|
1899
|
+
);
|
|
1900
|
+
const price = await pricer.getPrice(assetToken.symbol);
|
|
1901
|
+
const usdValue =
|
|
1902
|
+
Number(balance.toFixed(assetToken.decimals)) * price.price;
|
|
1903
|
+
|
|
1904
|
+
return { token: assetToken, amount: balance, usdValue };
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/**
|
|
1908
|
+
* Reads {@link StateManagerConfig.usdcToken} idle in the vault allocator (always — distinct asset or USDC-as-asset).
|
|
1909
|
+
*/
|
|
1910
|
+
private async _fetchVaultAllocatorUsdcBalanceIfDistinct(): Promise<TokenBalance> {
|
|
1911
|
+
const { usdcToken, vaultAllocator, networkConfig, pricer } = this._config;
|
|
1912
|
+
const balance = await new ERC20(networkConfig).balanceOf(
|
|
1913
|
+
usdcToken.address,
|
|
1914
|
+
vaultAllocator,
|
|
1915
|
+
usdcToken.decimals,
|
|
1916
|
+
);
|
|
1917
|
+
const tokenPrice = await pricer.getPrice(
|
|
1918
|
+
usdcToken.priceProxySymbol || usdcToken.symbol,
|
|
1919
|
+
);
|
|
1920
|
+
const usdValue =
|
|
1921
|
+
Number(balance.toFixed(usdcToken.decimals)) * tokenPrice.price;
|
|
1922
|
+
|
|
1923
|
+
return { token: usdcToken, amount: balance, usdValue };
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/**
|
|
1927
|
+
* Merges vault-allocator asset (if any), vault-allocator USDC, and operator wallet
|
|
1928
|
+
* balances into entries keyed by token address.
|
|
1929
|
+
*/
|
|
1930
|
+
private _computeUnusedBalances(
|
|
1931
|
+
vaultAssetBalance: TokenBalance | null,
|
|
1932
|
+
vaultUsdcBalance: TokenBalance,
|
|
1933
|
+
walletBalance: TokenBalance,
|
|
1934
|
+
): TokenBalance[] {
|
|
1935
|
+
const balanceMap = new Map<string, TokenBalance>();
|
|
1936
|
+
|
|
1937
|
+
const put = (tb: TokenBalance) => {
|
|
1938
|
+
balanceMap.set(tb.token.address.toString(), {
|
|
1939
|
+
token: tb.token,
|
|
1940
|
+
amount: tb.amount,
|
|
1941
|
+
usdValue: tb.usdValue,
|
|
1942
|
+
});
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
if (vaultAssetBalance) put(vaultAssetBalance);
|
|
1946
|
+
put(vaultUsdcBalance);
|
|
1947
|
+
|
|
1948
|
+
// Merge wallet balances by token address
|
|
1949
|
+
const key = walletBalance.token.address.toString();
|
|
1950
|
+
const existing = balanceMap.get(key);
|
|
1951
|
+
if (existing) {
|
|
1952
|
+
existing.amount = new Web3Number(
|
|
1953
|
+
existing.amount.plus(walletBalance.amount).toFixed(existing.token.decimals),
|
|
1954
|
+
existing.token.decimals,
|
|
1955
|
+
);
|
|
1956
|
+
existing.usdValue += walletBalance.usdValue;
|
|
1957
|
+
} else {
|
|
1958
|
+
balanceMap.set(key, {
|
|
1959
|
+
token: walletBalance.token,
|
|
1960
|
+
amount: walletBalance.amount,
|
|
1961
|
+
usdValue: walletBalance.usdValue,
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
return Array.from(balanceMap.values());
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
/**
|
|
1969
|
+
* Reads the operator wallet balance for {@link StateManagerConfig.usdcToken} only
|
|
1970
|
+
* (wallet stablecoin is always USDC, regardless of strategy {@link StateManagerConfig.assetToken}).
|
|
1971
|
+
*/
|
|
1972
|
+
private async _fetchWalletBalances(): Promise<TokenBalance> {
|
|
1973
|
+
const {
|
|
1974
|
+
networkConfig,
|
|
1975
|
+
pricer,
|
|
1976
|
+
walletAddress,
|
|
1977
|
+
usdcToken,
|
|
1978
|
+
} = this._config;
|
|
1979
|
+
const erc20 = new ERC20(networkConfig);
|
|
1980
|
+
|
|
1981
|
+
const [balance, tokenPrice] =
|
|
1982
|
+
await Promise.all([
|
|
1983
|
+
erc20.balanceOf(
|
|
1984
|
+
usdcToken.address,
|
|
1985
|
+
walletAddress,
|
|
1986
|
+
usdcToken.decimals,
|
|
1987
|
+
),
|
|
1988
|
+
pricer.getPrice(usdcToken.priceProxySymbol || usdcToken.symbol),
|
|
1989
|
+
]);
|
|
1990
|
+
|
|
1991
|
+
return {
|
|
1992
|
+
token: usdcToken,
|
|
1993
|
+
amount: balance,
|
|
1994
|
+
usdValue:
|
|
1995
|
+
Number(balance.toFixed(usdcToken.decimals)) * tokenPrice.price,
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* Reads the Extended exchange account-level balance (equity, available for
|
|
2001
|
+
* trade/withdrawal, etc.). Returns null if the API call fails.
|
|
2002
|
+
*/
|
|
2003
|
+
private async _fetchExtendedBalance(): Promise<ExtendedBalanceState | null> {
|
|
2004
|
+
const [holdings, pendingDeposit] = await Promise.all([
|
|
2005
|
+
this._config.extendedAdapter.getExtendedDepositAmount(),
|
|
2006
|
+
this._fetchPendingDeposit(),
|
|
2007
|
+
]);
|
|
2008
|
+
if (!holdings) return null;
|
|
2009
|
+
|
|
2010
|
+
return {
|
|
2011
|
+
equity: new Web3Number(holdings.equity, USDC_TOKEN_DECIMALS),
|
|
2012
|
+
availableForTrade: new Web3Number(
|
|
2013
|
+
holdings.availableForTrade,
|
|
2014
|
+
USDC_TOKEN_DECIMALS,
|
|
2015
|
+
),
|
|
2016
|
+
availableForWithdrawal: new Web3Number(
|
|
2017
|
+
holdings.availableForWithdrawal,
|
|
2018
|
+
USDC_TOKEN_DECIMALS,
|
|
2019
|
+
),
|
|
2020
|
+
unrealisedPnl: new Web3Number(holdings.unrealisedPnl, USDC_TOKEN_DECIMALS),
|
|
2021
|
+
balance: new Web3Number(holdings.balance, USDC_TOKEN_DECIMALS),
|
|
2022
|
+
pendingDeposit,
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
/**
|
|
2027
|
+
* Computes the net pending deposit by subtracting pending withdrawals from
|
|
2028
|
+
* pending deposits. Uses the Extended exchange asset operations API.
|
|
2029
|
+
*
|
|
2030
|
+
* Positive = deposit in transit (wallet → Extended).
|
|
2031
|
+
* Negative = withdrawal in transit (Extended → wallet).
|
|
2032
|
+
*/
|
|
2033
|
+
private async _fetchPendingDeposit(): Promise<Web3Number> {
|
|
2034
|
+
try {
|
|
2035
|
+
const client = this._config.extendedAdapter.client;
|
|
2036
|
+
const [pendingDeposits, pendingWithdrawals] = await Promise.all([
|
|
2037
|
+
client.getAssetOperations({
|
|
2038
|
+
operationsType: [AssetOperationType.DEPOSIT],
|
|
2039
|
+
operationsStatus: [AssetOperationStatus.IN_PROGRESS],
|
|
2040
|
+
}),
|
|
2041
|
+
client.getAssetOperations({
|
|
2042
|
+
operationsType: [AssetOperationType.WITHDRAWAL],
|
|
2043
|
+
operationsStatus: [AssetOperationStatus.IN_PROGRESS],
|
|
2044
|
+
}),
|
|
2045
|
+
]);
|
|
2046
|
+
|
|
2047
|
+
const depositTotal = pendingDeposits.data.reduce(
|
|
2048
|
+
(sum, op) => sum + parseFloat(op.amount || '0'), 0,
|
|
2049
|
+
);
|
|
2050
|
+
const withdrawTotal = pendingWithdrawals.data.reduce(
|
|
2051
|
+
(sum, op) => sum + parseFloat(op.amount || '0'), 0,
|
|
2052
|
+
);
|
|
2053
|
+
|
|
2054
|
+
const net = depositTotal - withdrawTotal;
|
|
2055
|
+
logger.info(
|
|
2056
|
+
`${this._tag}::_fetchPendingDeposit deposits=${depositTotal}, withdrawals=${withdrawTotal}, net=${net}`,
|
|
2057
|
+
);
|
|
2058
|
+
return new Web3Number(net.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS);
|
|
2059
|
+
} catch (err) {
|
|
2060
|
+
logger.error(`${this._tag}::_fetchPendingDeposit error: ${err}`);
|
|
2061
|
+
return new Web3Number(0, USDC_TOKEN_DECIMALS);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/**
|
|
2066
|
+
* Reads all open positions on the Extended exchange. Each position
|
|
2067
|
+
* includes instrument, side, size, USD value, and leverage.
|
|
2068
|
+
*/
|
|
2069
|
+
private async _fetchExtendedPositions(): Promise<ExtendedPositionState[]> {
|
|
2070
|
+
const positions =
|
|
2071
|
+
await this._config.extendedAdapter.getAllOpenPositions();
|
|
2072
|
+
if (!positions) return [];
|
|
2073
|
+
|
|
2074
|
+
return positions.map((position) => ({
|
|
2075
|
+
instrument: position.market,
|
|
2076
|
+
side: position.side,
|
|
2077
|
+
size: new Web3Number(position.size, USDC_TOKEN_DECIMALS), // though in different token terms, this wont matter from DEX perspective, hence 6decimals is ok
|
|
2078
|
+
valueUsd: new Web3Number(position.value, USDC_TOKEN_DECIMALS),
|
|
2079
|
+
leverage: position.leverage,
|
|
2080
|
+
}));
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
/**
|
|
2084
|
+
* Reads a single Vesu pool's position data: collateral amount/price,
|
|
2085
|
+
* debt amount/price.
|
|
2086
|
+
*/
|
|
2087
|
+
private async _fetchSingleVesuPoolState(
|
|
2088
|
+
adapter: VesuMultiplyAdapter,
|
|
2089
|
+
): Promise<VesuPoolState> {
|
|
2090
|
+
const assetPrices = await adapter._vesuAdapter.getAssetPrices();
|
|
2091
|
+
return {
|
|
2092
|
+
poolId: adapter.config.poolId,
|
|
2093
|
+
collateralToken: adapter.config.collateral,
|
|
2094
|
+
debtToken: adapter.config.debt,
|
|
2095
|
+
collateralAmount: assetPrices.collateralTokenAmount,
|
|
2096
|
+
collateralUsdValue: assetPrices.collateralUSDAmount,
|
|
2097
|
+
debtAmount: assetPrices.debtTokenAmount,
|
|
2098
|
+
debtUsdValue: assetPrices.debtUSDAmount,
|
|
2099
|
+
collateralPrice: assetPrices.collateralPrice,
|
|
2100
|
+
debtPrice: assetPrices.debtPrice,
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
/**
|
|
2105
|
+
* Reads all Vesu pool states in parallel (one per configured adapter).
|
|
2106
|
+
*/
|
|
2107
|
+
private async _fetchAllVesuPoolStates(): Promise<VesuPoolState[]> {
|
|
2108
|
+
return Promise.all(
|
|
2109
|
+
this._config.vesuAdapters.map((adapter) =>
|
|
2110
|
+
this._fetchSingleVesuPoolState(adapter),
|
|
2111
|
+
),
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2116
|
+
// Private — validation
|
|
2117
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2118
|
+
|
|
2119
|
+
/**
|
|
2120
|
+
* Validates that all critical refreshed state is present and contains
|
|
2121
|
+
* finite, sensible values. Throws on invalid state.
|
|
2122
|
+
*/
|
|
2123
|
+
private _validateRefreshedState(): void {
|
|
2124
|
+
if (this._budget.unusedBalanceRows.length === 0) {
|
|
2125
|
+
throw new Error(
|
|
2126
|
+
`${this._tag}: unusedBalance is empty after refresh`,
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
for (const balance of this._budget.unusedBalanceRows) {
|
|
2130
|
+
this._validateTokenBalanceOrThrow(
|
|
2131
|
+
balance,
|
|
2132
|
+
`unusedBalance[${balance.token.symbol}]`,
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
this._validateVesuPoolPricesOrThrow();
|
|
2136
|
+
this._validateExtendedBalanceOrThrow();
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
private _validateTokenBalanceOrThrow(
|
|
2140
|
+
balance: TokenBalance | null,
|
|
2141
|
+
label: string,
|
|
2142
|
+
): void {
|
|
2143
|
+
if (!balance) {
|
|
2144
|
+
throw new Error(`${this._tag}: ${label} is null after refresh`);
|
|
2145
|
+
}
|
|
2146
|
+
if (!Number.isFinite(balance.usdValue) || balance.usdValue < 0) {
|
|
2147
|
+
throw new Error(
|
|
2148
|
+
`${this._tag}: ${label} has invalid usdValue: ${balance.usdValue}`,
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
private _validateVesuPoolPricesOrThrow(): void {
|
|
2154
|
+
for (const pool of this._budget.vesuPools) {
|
|
2155
|
+
const poolLabel = pool.poolId.shortString();
|
|
2156
|
+
this._assertPositiveFinite(
|
|
2157
|
+
pool.collateralPrice,
|
|
2158
|
+
`Vesu pool ${poolLabel} collateralPrice`,
|
|
2159
|
+
);
|
|
2160
|
+
this._assertPositiveFinite(
|
|
2161
|
+
pool.debtPrice,
|
|
2162
|
+
`Vesu pool ${poolLabel} debtPrice`,
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
private _validateExtendedBalanceOrThrow(): void {
|
|
2168
|
+
if (!this._budget.extendedBalanceView) return; // null is acceptable; treated as zero
|
|
2169
|
+
|
|
2170
|
+
const { equity, availableForTrade } = this._budget.extendedBalanceView;
|
|
2171
|
+
if (
|
|
2172
|
+
!Number.isFinite(equity.toNumber()) ||
|
|
2173
|
+
!Number.isFinite(availableForTrade.toNumber())
|
|
2174
|
+
) {
|
|
2175
|
+
throw new Error(
|
|
2176
|
+
`${this._tag}: Extended balance contains non-finite values — ` +
|
|
2177
|
+
`equity: ${equity.toNumber()}, availableForTrade: ${availableForTrade.toNumber()}`,
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
private _assertPositiveFinite(value: number, label: string): void {
|
|
2183
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
2184
|
+
throw new Error(
|
|
2185
|
+
`${this._tag}: ${label} is invalid (${value}). Expected finite positive number.`,
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2191
|
+
// Private — solve step 1: per-pool debt deltas (LTV maintenance)
|
|
2192
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2193
|
+
|
|
2194
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2195
|
+
// Private — solve step 2: distributable amount
|
|
2196
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* Computes total distributable capital by combining idle vault balance
|
|
2200
|
+
* and Extended available balance, then adjusting for aggregate debt
|
|
2201
|
+
* delta and any pending withdrawal.
|
|
2202
|
+
*
|
|
2203
|
+
* When withdrawAmount > 0 the distributable pool shrinks, forcing the
|
|
2204
|
+
* allocation split to produce smaller (or negative) allocations — which
|
|
2205
|
+
* in turn causes protocol positions to unwind and free capital for the
|
|
2206
|
+
* vault allocator to execute bringLiquidity.
|
|
2207
|
+
*/
|
|
2208
|
+
private _computeDistributableAmount(
|
|
2209
|
+
perPoolDebtDeltasToBorrow: readonly Web3Number[],
|
|
2210
|
+
withdrawAmount: Web3Number,
|
|
2211
|
+
): Web3Number {
|
|
2212
|
+
const totalInvestable = this._computeTotalInvestableAmount();
|
|
2213
|
+
const aggregateDebtAdjustment = this._sumDebtDeltas(perPoolDebtDeltasToBorrow);
|
|
2214
|
+
return new Web3Number(
|
|
2215
|
+
totalInvestable
|
|
2216
|
+
.plus(aggregateDebtAdjustment)
|
|
2217
|
+
.minus(withdrawAmount)
|
|
2218
|
+
.toFixed(USDC_TOKEN_DECIMALS),
|
|
2219
|
+
USDC_TOKEN_DECIMALS,
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
/**
|
|
2224
|
+
* Total investable = vault allocator balance + Extended available-for-trade +
|
|
2225
|
+
* buffered unrealised PnL, matching `deposit_cases_extended_vesu.xlsx`:
|
|
2226
|
+
* `(VA + wallet + EXT_WITH_AVL + EXT_UPNL) * (1 − buffer)`.
|
|
2227
|
+
* Positive {@link ExtendedBalanceState.pendingDeposit} stays on the afT leg only (see {@link SolveBudget.extAvailTrade}).
|
|
2228
|
+
*/
|
|
2229
|
+
private _computeTotalInvestableAmount(): Web3Number {
|
|
2230
|
+
const totalUnusedUsd = this._budget.unusedBalancesBufferedUsdSum;
|
|
2231
|
+
logger.debug(
|
|
2232
|
+
`${this._tag}::_computeTotalInvestableAmount unusedBalances=` +
|
|
2233
|
+
`${JSON.stringify(this._budget.unusedBalanceRows.map((b) => ({ token: b.token.symbol, amount: b.amount.toNumber(), usdValue: b.usdValue })))}`,
|
|
2234
|
+
);
|
|
2235
|
+
const extBal = this._budget.extendedBalanceView;
|
|
2236
|
+
const rawAft = extBal?.availableForWithdrawal?.toNumber() ?? 0;
|
|
2237
|
+
const rawUpnl = extBal?.unrealisedPnl?.toNumber() ?? 0;
|
|
2238
|
+
let extBuffered =
|
|
2239
|
+
this._budget.bufferedUsd(rawAft) + this._budget.bufferedUsd(rawUpnl);
|
|
2240
|
+
const pd = extBal?.pendingDeposit?.toNumber() ?? 0;
|
|
2241
|
+
if (pd > 0) extBuffered += pd;
|
|
2242
|
+
const extendedAvailable = new Web3Number(
|
|
2243
|
+
extBuffered.toFixed(USDC_TOKEN_DECIMALS),
|
|
2244
|
+
USDC_TOKEN_DECIMALS,
|
|
2245
|
+
);
|
|
2246
|
+
logger.verbose(`_computeTotalInvestableAmount totalUnusedUsd: ${totalUnusedUsd}, extendedAvailable: ${extendedAvailable.toNumber()}`);
|
|
2247
|
+
return new Web3Number(totalUnusedUsd.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS)
|
|
2248
|
+
.plus(extendedAvailable)
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
private _sumDebtDeltas(deltas: readonly Web3Number[]): Web3Number {
|
|
2252
|
+
return deltas.reduce(
|
|
2253
|
+
(sum, delta) => sum.plus(delta),
|
|
2254
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2259
|
+
// Private — solve step 3: allocation split
|
|
2260
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2261
|
+
|
|
2262
|
+
/**
|
|
2263
|
+
* Splits distributable capital between Vesu and Extended using leverage
|
|
2264
|
+
* ratios and existing exposure to match delta-neutral targets.
|
|
2265
|
+
*
|
|
2266
|
+
* Formula (from existing strategy):
|
|
2267
|
+
* ExtendedAlloc = (vL * distributable + vesuExposure - extendedExposure) / (vL + eL)
|
|
2268
|
+
* VesuAlloc = distributable - ExtendedAlloc
|
|
2269
|
+
*/
|
|
2270
|
+
private _computeAllocationSplit(distributableAmount: Web3Number, deltaVesuCollateral = 0, deltaExtendedCollateral = 0, isRecursive = false): {
|
|
2271
|
+
vesuAllocationUsd: Web3Number;
|
|
2272
|
+
extendedAllocationUsd: Web3Number;
|
|
2273
|
+
vesuPositionDelta: number;
|
|
2274
|
+
extendedPositionDelta: number;
|
|
2275
|
+
} {
|
|
2276
|
+
if (this._hasNoVesuAdapters()) {
|
|
2277
|
+
throw new Error(`${this._tag}: No Vesu adapters found`);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
const vesuLeverage = calculateVesuLeverage();
|
|
2281
|
+
const extendedLeverage = calculateExtendedLevergae();
|
|
2282
|
+
const denominator = vesuLeverage + extendedLeverage;
|
|
2283
|
+
|
|
2284
|
+
if (denominator === 0) {
|
|
2285
|
+
throw new Error(`${this._tag}: Denominator is zero`);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// get long/short exposures on both sides
|
|
2289
|
+
const collateralPrice = this._budget.vesuPools[0]?.collateralPrice ?? 0;
|
|
2290
|
+
const totalVesuExposureUsd = this._totalVesuCollateralUsd().plus(new Web3Number((deltaVesuCollateral * collateralPrice).toFixed(6), USDC_TOKEN_DECIMALS));
|
|
2291
|
+
const totalExtendedExposureUsd = this._totalExtendedExposureUsd().plus(new Web3Number((deltaExtendedCollateral * collateralPrice).toFixed(6), USDC_TOKEN_DECIMALS));
|
|
2292
|
+
|
|
2293
|
+
const numerator =
|
|
2294
|
+
vesuLeverage * distributableAmount.toNumber() +
|
|
2295
|
+
totalVesuExposureUsd.toNumber() -
|
|
2296
|
+
totalExtendedExposureUsd.toNumber();
|
|
2297
|
+
|
|
2298
|
+
const extendedAllocationUsd = new Web3Number(
|
|
2299
|
+
(numerator / denominator).toFixed(USDC_TOKEN_DECIMALS),
|
|
2300
|
+
USDC_TOKEN_DECIMALS,
|
|
2301
|
+
);
|
|
2302
|
+
let vesuAllocationUsd = new Web3Number(
|
|
2303
|
+
distributableAmount
|
|
2304
|
+
.minus(extendedAllocationUsd)
|
|
2305
|
+
.toFixed(USDC_TOKEN_DECIMALS),
|
|
2306
|
+
USDC_TOKEN_DECIMALS,
|
|
2307
|
+
);
|
|
2308
|
+
|
|
2309
|
+
// add debt repayments to vesu allocation (-1 to convert to repay amount)
|
|
2310
|
+
// const perPoolDebtDeltasToBorrow = this._budget.vesuDebtDeltas;
|
|
2311
|
+
// const debtDeltasSum = this._sumDebtDeltas(perPoolDebtDeltasToBorrow);
|
|
2312
|
+
// if (debtDeltasSum.toNumber() > 0) {
|
|
2313
|
+
// vesuAllocationUsd = vesuAllocationUsd.plus(this._sumDebtDeltas(perPoolDebtDeltasToBorrow).multipliedBy(-1));
|
|
2314
|
+
|
|
2315
|
+
let vesuPositionDelta = Number((new Web3Number((vesuAllocationUsd.toNumber() * (vesuLeverage) / collateralPrice).toFixed(6), 6)).toFixedRoundDown(COLLATERAL_PRECISION));
|
|
2316
|
+
let extendedPositionDelta = Number((new Web3Number((extendedAllocationUsd.toNumber() * (extendedLeverage) / collateralPrice).toFixed(6), 6)).toFixedRoundDown(COLLATERAL_PRECISION));
|
|
2317
|
+
|
|
2318
|
+
// for the first call, reconsider the newly created position delta to actually recompute the allocation split
|
|
2319
|
+
if (!isRecursive) {
|
|
2320
|
+
return this._computeAllocationSplit(distributableAmount, vesuPositionDelta, extendedPositionDelta, true);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// subtract any deltas passed
|
|
2324
|
+
// throw new Error(`${this._tag}: Recursive allocation vesuPositionDelta: ${vesuPositionDelta}, extendedPositionDelta: ${extendedPositionDelta}, vesuAllocationUsd: ${vesuAllocationUsd.toNumber()}, extendedAllocationUsd: ${extendedAllocationUsd.toNumber()}`);
|
|
2325
|
+
return { vesuAllocationUsd, extendedAllocationUsd, vesuPositionDelta, extendedPositionDelta };
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
private _hasNoVesuAdapters(): boolean {
|
|
2329
|
+
return this._config.vesuAdapters.length === 0;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2333
|
+
// Private — solve step 4: per-pool collateral deltas
|
|
2334
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2335
|
+
|
|
2336
|
+
/**
|
|
2337
|
+
* Distributes the total Vesu USD allocation across pools proportionally
|
|
2338
|
+
* by existing collateral value, then converts each share to collateral
|
|
2339
|
+
* token units.
|
|
2340
|
+
*/
|
|
2341
|
+
private _computePerPoolCollateralDeltas(
|
|
2342
|
+
vesuAllocationUsd: Web3Number
|
|
2343
|
+
): VesuPoolDelta[] {
|
|
2344
|
+
const vesuLeverage = calculateVesuLeverage();
|
|
2345
|
+
|
|
2346
|
+
const availableVesuCollateralAllocationUsd = vesuAllocationUsd;
|
|
2347
|
+
const postLeverageAllocationUsd = availableVesuCollateralAllocationUsd.multipliedBy(vesuLeverage);
|
|
2348
|
+
const totalCollateralExisting = this._totalVesuCollateral();
|
|
2349
|
+
|
|
2350
|
+
return this._budget.vesuPools.map((pool, index) => {
|
|
2351
|
+
const _postLeverageAllocation = postLeverageAllocationUsd.dividedBy(pool.collateralPrice);
|
|
2352
|
+
|
|
2353
|
+
const postLeverageAllocation = new Web3Number((
|
|
2354
|
+
(
|
|
2355
|
+
_postLeverageAllocation.plus(totalCollateralExisting)
|
|
2356
|
+
).toFixedRoundDown(COLLATERAL_PRECISION)
|
|
2357
|
+
), pool.collateralToken.decimals
|
|
2358
|
+
).minus(totalCollateralExisting);
|
|
2359
|
+
const _poolCollateralDelta = this._computePoolCollateralShare(
|
|
2360
|
+
pool,
|
|
2361
|
+
totalCollateralExisting,
|
|
2362
|
+
postLeverageAllocation,
|
|
2363
|
+
);
|
|
2364
|
+
const poolCollateralDelta = new Web3Number(
|
|
2365
|
+
_poolCollateralDelta.toFixed(COLLATERAL_PRECISION),
|
|
2366
|
+
pool.collateralToken.decimals,
|
|
2367
|
+
);
|
|
2368
|
+
|
|
2369
|
+
// the excess collateral should come from debt.
|
|
2370
|
+
const newDebt = (postLeverageAllocation.multipliedBy(pool.collateralPrice)).minus(availableVesuCollateralAllocationUsd).dividedBy(pool.debtPrice);
|
|
2371
|
+
return {
|
|
2372
|
+
poolId: pool.poolId,
|
|
2373
|
+
collateralToken: pool.collateralToken,
|
|
2374
|
+
debtToken: pool.debtToken,
|
|
2375
|
+
debtDelta: newDebt,
|
|
2376
|
+
collateralDelta: poolCollateralDelta,
|
|
2377
|
+
collateralPrice: pool.collateralPrice,
|
|
2378
|
+
debtPrice: pool.debtPrice,
|
|
2379
|
+
};
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
/**
|
|
2384
|
+
* Determines how much of the total Vesu allocation goes to a single pool.
|
|
2385
|
+
* Single-pool or zero-total cases get the entire allocation.
|
|
2386
|
+
* Multi-pool cases split proportionally by current collateral USD value.
|
|
2387
|
+
*/
|
|
2388
|
+
private _computePoolCollateralShare(
|
|
2389
|
+
pool: VesuPoolState,
|
|
2390
|
+
totalCollateral: Web3Number,
|
|
2391
|
+
totalVesuAllocation: Web3Number,
|
|
2392
|
+
): Web3Number {
|
|
2393
|
+
const isSinglePoolOrZeroTotal =
|
|
2394
|
+
this._budget.vesuPools.length === 1 ||
|
|
2395
|
+
totalCollateral.toNumber() === 0;
|
|
2396
|
+
|
|
2397
|
+
if (isSinglePoolOrZeroTotal) return totalVesuAllocation;
|
|
2398
|
+
|
|
2399
|
+
const poolWeight = pool.collateralAmount.dividedBy(totalCollateral);
|
|
2400
|
+
|
|
2401
|
+
return new Web3Number(
|
|
2402
|
+
totalVesuAllocation
|
|
2403
|
+
.multipliedBy(poolWeight)
|
|
2404
|
+
.toFixed(USDC_TOKEN_DECIMALS),
|
|
2405
|
+
USDC_TOKEN_DECIMALS,
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2410
|
+
// Private — solve step 6: extended position deltas
|
|
2411
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2412
|
+
|
|
2413
|
+
/**
|
|
2414
|
+
* Computes per-position exposure deltas for delta neutrality.
|
|
2415
|
+
* Target: total Extended short exposure = total projected Vesu collateral
|
|
2416
|
+
* exposure (current + collateral deltas).
|
|
2417
|
+
*/
|
|
2418
|
+
private _computeExtendedPositionDeltas(
|
|
2419
|
+
vesuDeltas: VesuPoolDelta[],
|
|
2420
|
+
): ExtendedPositionDelta[] {
|
|
2421
|
+
const targetExposure =
|
|
2422
|
+
this._computeTargetExtendedExposure(vesuDeltas);
|
|
2423
|
+
const currentExposure = this._totalExtendedExposure();
|
|
2424
|
+
logger.debug(
|
|
2425
|
+
`${this._tag}::_computeExtendedPositionDeltas targetExposure=${targetExposure.toNumber()}, currentExposure=${currentExposure.toNumber()}`,
|
|
2426
|
+
);
|
|
2427
|
+
const totalExposureDelta = new Web3Number(
|
|
2428
|
+
targetExposure
|
|
2429
|
+
.minus(currentExposure)
|
|
2430
|
+
.toFixed(USDC_TOKEN_DECIMALS),
|
|
2431
|
+
USDC_TOKEN_DECIMALS,
|
|
2432
|
+
);
|
|
2433
|
+
|
|
2434
|
+
if (this._hasNoExtendedPositions()) {
|
|
2435
|
+
return this._singleInstrumentDelta(totalExposureDelta);
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
return this._distributeExposureDeltaAcrossPositions(totalExposureDelta);
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
/**
|
|
2442
|
+
* Target Extended exposure = sum of (current collateral + collateralDelta)
|
|
2443
|
+
* * price, across all Vesu pools.
|
|
2444
|
+
*/
|
|
2445
|
+
private _computeTargetExtendedExposure(
|
|
2446
|
+
vesuDeltas: VesuPoolDelta[],
|
|
2447
|
+
): Web3Number {
|
|
2448
|
+
let totalExposureCollateral = new Web3Number(0, USDC_TOKEN_DECIMALS); // just some decimals is ok
|
|
2449
|
+
|
|
2450
|
+
for (let i = 0; i < this._budget.vesuPools.length; i++) {
|
|
2451
|
+
const pool = this._budget.vesuPools[i];
|
|
2452
|
+
const delta = vesuDeltas[i];
|
|
2453
|
+
logger.debug(
|
|
2454
|
+
`${this._tag}::_computeTargetExtendedExposure poolId=${pool.poolId.toString()}, collateralAmount=${pool.collateralAmount.toNumber()}, collateralDelta=${delta.collateralDelta.toNumber()}`,
|
|
2455
|
+
);
|
|
2456
|
+
const projectedCollateral = pool.collateralAmount.plus(
|
|
2457
|
+
delta.collateralDelta,
|
|
2458
|
+
);
|
|
2459
|
+
totalExposureCollateral = totalExposureCollateral.plus(
|
|
2460
|
+
projectedCollateral,
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
return new Web3Number(
|
|
2465
|
+
totalExposureCollateral.toFixed(USDC_TOKEN_DECIMALS),
|
|
2466
|
+
USDC_TOKEN_DECIMALS,
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
private _hasNoExtendedPositions(): boolean {
|
|
2471
|
+
return this._budget.extendedPositionsView.length === 0;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
/**
|
|
2475
|
+
* Creates a single-element delta array for the default instrument
|
|
2476
|
+
* when no Extended positions currently exist.
|
|
2477
|
+
*/
|
|
2478
|
+
private _singleInstrumentDelta(
|
|
2479
|
+
delta: Web3Number,
|
|
2480
|
+
): ExtendedPositionDelta[] {
|
|
2481
|
+
return [
|
|
2482
|
+
{
|
|
2483
|
+
instrument: this._config.extendedAdapter.config.extendedMarketName,
|
|
2484
|
+
delta: new Web3Number(
|
|
2485
|
+
delta.toFixedRoundDown(COLLATERAL_PRECISION),
|
|
2486
|
+
USDC_TOKEN_DECIMALS,
|
|
2487
|
+
),
|
|
2488
|
+
},
|
|
2489
|
+
];
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
/**
|
|
2493
|
+
* Distributes a total exposure delta proportionally across existing
|
|
2494
|
+
* positions by their current USD value share.
|
|
2495
|
+
*/
|
|
2496
|
+
private _distributeExposureDeltaAcrossPositions(
|
|
2497
|
+
totalDelta: Web3Number,
|
|
2498
|
+
): ExtendedPositionDelta[] {
|
|
2499
|
+
const totalExposure = this._totalExtendedExposure();
|
|
2500
|
+
|
|
2501
|
+
return this._budget.extendedPositionsView.map((position) => {
|
|
2502
|
+
const share = this._positionExposureShareFraction(
|
|
2503
|
+
position,
|
|
2504
|
+
totalExposure,
|
|
2505
|
+
);
|
|
2506
|
+
return {
|
|
2507
|
+
instrument: position.instrument,
|
|
2508
|
+
delta: new Web3Number(
|
|
2509
|
+
totalDelta.multipliedBy(share).toFixedRoundDown(COLLATERAL_PRECISION),
|
|
2510
|
+
USDC_TOKEN_DECIMALS,
|
|
2511
|
+
),
|
|
2512
|
+
};
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
/**
|
|
2517
|
+
* Returns the fraction (0–1) of total Extended exposure held by
|
|
2518
|
+
* a single position. Returns 1 when there is only one position
|
|
2519
|
+
* or when total exposure is zero.
|
|
2520
|
+
*/
|
|
2521
|
+
private _positionExposureShareFraction(
|
|
2522
|
+
position: ExtendedPositionState,
|
|
2523
|
+
totalExposure: Web3Number,
|
|
2524
|
+
): number {
|
|
2525
|
+
const isSingleOrZero =
|
|
2526
|
+
totalExposure.toNumber() === 0 ||
|
|
2527
|
+
this._budget.extendedPositionsView.length === 1;
|
|
2528
|
+
if (isSingleOrZero) return 1;
|
|
2529
|
+
return position.valueUsd.dividedBy(totalExposure).toNumber();
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2533
|
+
// Private — solve step 7: extended deposit delta
|
|
2534
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2535
|
+
|
|
2536
|
+
/**
|
|
2537
|
+
* Net deposit change = allocation target − currently available for trade.
|
|
2538
|
+
* Positive = need to deposit more, negative = can withdraw excess.
|
|
2539
|
+
*/
|
|
2540
|
+
private _computeExtendedDepositDelta(
|
|
2541
|
+
extendedAllocationUsd: Web3Number,
|
|
2542
|
+
): Web3Number {
|
|
2543
|
+
const currentAvailableForTrade = new Web3Number(
|
|
2544
|
+
this._budget.extAvailTrade.toFixed(USDC_TOKEN_DECIMALS),
|
|
2545
|
+
USDC_TOKEN_DECIMALS,
|
|
2546
|
+
);
|
|
2547
|
+
|
|
2548
|
+
return new Web3Number(
|
|
2549
|
+
extendedAllocationUsd
|
|
2550
|
+
.minus(currentAvailableForTrade)
|
|
2551
|
+
.toFixed(USDC_TOKEN_DECIMALS),
|
|
2552
|
+
USDC_TOKEN_DECIMALS,
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
private _computeVesuDepositAmount(vesuDeltas: VesuPoolDelta[]): Web3Number {
|
|
2557
|
+
let totalVesuCollateral = new Web3Number(0, USDC_TOKEN_DECIMALS); // just some decimals is ok
|
|
2558
|
+
for (let i = 0; i < this._budget.vesuPools.length; i++) {
|
|
2559
|
+
const pool = this._budget.vesuPools[i];
|
|
2560
|
+
const delta = vesuDeltas[i];
|
|
2561
|
+
totalVesuCollateral = totalVesuCollateral.plus(delta.collateralDelta.multipliedBy(pool.collateralPrice));
|
|
2562
|
+
totalVesuCollateral = totalVesuCollateral.minus(delta.debtDelta.multipliedBy(pool.debtPrice));
|
|
2563
|
+
}
|
|
2564
|
+
return totalVesuCollateral;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2568
|
+
// Private — solve step 8: wallet delta
|
|
2569
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2570
|
+
|
|
2571
|
+
/**
|
|
2572
|
+
* The wallet is a pass-through for USDC flows between the vault allocator
|
|
2573
|
+
* and the Extended exchange.
|
|
2574
|
+
*
|
|
2575
|
+
* Positive = vault allocator → wallet → Extended (deposit)
|
|
2576
|
+
* Negative = Extended → wallet → vault allocator (withdrawal)
|
|
2577
|
+
*/
|
|
2578
|
+
private _deriveWalletDelta(extendedDeposit: Web3Number): Web3Number {
|
|
2579
|
+
return extendedDeposit;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2583
|
+
// Private — solve step 9: bring liquidity
|
|
2584
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2585
|
+
|
|
2586
|
+
/**
|
|
2587
|
+
* The bringLiquidity amount is the USDC that the vault allocator must
|
|
2588
|
+
* transfer back to the vault contract for the user's withdrawal.
|
|
2589
|
+
* Equals the withdrawAmount passed into solve(); 0 during investment cycles.
|
|
2590
|
+
*/
|
|
2591
|
+
private _computeBringLiquidityAmount(
|
|
2592
|
+
withdrawAmount: Web3Number,
|
|
2593
|
+
): Web3Number {
|
|
2594
|
+
return withdrawAmount;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2598
|
+
// Private — case classification (modular)
|
|
2599
|
+
//
|
|
2600
|
+
// Cases are the source of truth. Each case carries its own routes
|
|
2601
|
+
// with amounts and state info. There is no global routes array —
|
|
2602
|
+
// consumers flatten case routes as needed.
|
|
2603
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2604
|
+
|
|
2605
|
+
|
|
2606
|
+
// ── ExecutionRoute-building helpers ─────────────────────────────────────────────
|
|
2607
|
+
|
|
2608
|
+
// ── Atomic route builders ────────────────────────────────────────────
|
|
2609
|
+
|
|
2610
|
+
private _buildVesuRepayRoutes(totalUsd: number, routes: ExecutionRoute[]): void {
|
|
2611
|
+
// Repay consumes VA USDC/asset; do not plan more than the budget VA holds after prior
|
|
2612
|
+
// routes (borrow→VA, Vesu→VA, wallet→VA, etc.). Otherwise repayVesuBorrowCapacity would
|
|
2613
|
+
// mutate Vesu debt while spendVaRawUsd could not fund it.
|
|
2614
|
+
const vaCap = this._budget.vaRawUsd;
|
|
2615
|
+
const repayUsd = Math.min(totalUsd, vaCap);
|
|
2616
|
+
if (repayUsd <= CASE_THRESHOLD_USD) return;
|
|
2617
|
+
|
|
2618
|
+
const { used, spendsByPool } = this._budget.repayVesuBorrowCapacity(repayUsd);
|
|
2619
|
+
for (const route of spendsByPool) {
|
|
2620
|
+
routes.push({ type: RouteType.VESU_REPAY as const, ...route, priority: routes.length });
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
this._budget.spendVaRawUsd(used);
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
private _buildVesuBorrowRoutes(
|
|
2627
|
+
totalUsd: number,
|
|
2628
|
+
routes: ExecutionRoute[],
|
|
2629
|
+
opts?: { maxBorrowUsd?: number },
|
|
2630
|
+
): { routes: ExecutionRoute[], remaining: number } {
|
|
2631
|
+
let borrowable = this._budget.vesuBorrowCapacity;
|
|
2632
|
+
if (opts?.maxBorrowUsd !== undefined) {
|
|
2633
|
+
borrowable = Math.min(borrowable, Math.max(0, opts.maxBorrowUsd));
|
|
2634
|
+
}
|
|
2635
|
+
if (totalUsd <= CASE_THRESHOLD_USD) return { routes, remaining: totalUsd };
|
|
2636
|
+
if (borrowable <= CASE_THRESHOLD_USD) return { routes, remaining: totalUsd };
|
|
2637
|
+
|
|
2638
|
+
const borrowTarget = Math.min(totalUsd, borrowable);
|
|
2639
|
+
const { used, spendsByPool } = this._budget.spendVesuBorrowCapacity(borrowTarget);
|
|
2640
|
+
for (const route of spendsByPool) {
|
|
2641
|
+
routes.push({ type: RouteType.VESU_BORROW as const, ...route, priority: routes.length });
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
this._budget.addToVA(used);
|
|
2645
|
+
return { routes, remaining: totalUsd - used };
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// private _buildExtendedDepositRoutes(totalUsd: number): TransferRoute[] {
|
|
2649
|
+
// const routes: TransferRoute[] = [];
|
|
2650
|
+
// let rem = totalUsd;
|
|
2651
|
+
// const walletBal = this._budget.walletBalance?.amount?.toNumber() ?? 0;
|
|
2652
|
+
// if (walletBal > 0 && rem > 0) {
|
|
2653
|
+
// const use = Math.min(walletBal, rem);
|
|
2654
|
+
// routes.push({ type: RouteType.WALLET_TO_EXTENDED, amount: safeUsdcWeb3Number(use), priority: routes.length });
|
|
2655
|
+
// this._budget.applyExtendedBalanceChange(use);
|
|
2656
|
+
// rem -= use;
|
|
2657
|
+
// }
|
|
2658
|
+
// if (rem > 0) {
|
|
2659
|
+
// routes.push({ type: RouteType.VA_TO_EXTENDED, amount: safeUsdcWeb3Number(rem), priority: routes.length });
|
|
2660
|
+
// this._budget.applyExtendedBalanceChange(rem);
|
|
2661
|
+
// }
|
|
2662
|
+
// return routes;
|
|
2663
|
+
// }
|
|
2664
|
+
|
|
2665
|
+
// private _buildVesuIncreaseLeverRoutes(vesuDeltas: VesuPoolDelta[]): VesuMultiplyRoute[] {
|
|
2666
|
+
// return vesuDeltas
|
|
2667
|
+
// .filter((d) => d.collateralDelta.greaterThan(0))
|
|
2668
|
+
// .map((d) => {
|
|
2669
|
+
// this._budget.applyVesuDelta(d.poolId, d.collateralDelta, d.debtDelta);
|
|
2670
|
+
// return {
|
|
2671
|
+
// type: RouteType.VESU_MULTIPLY_INCREASE_LEVER as const,
|
|
2672
|
+
// poolId: d.poolId,
|
|
2673
|
+
// collateralToken: d.collateralToken,
|
|
2674
|
+
// collateralAmount: d.collateralDelta,
|
|
2675
|
+
// debtToken: d.debtToken,
|
|
2676
|
+
// debtAmount: d.debtDelta,
|
|
2677
|
+
// priority: 0,
|
|
2678
|
+
// };
|
|
2679
|
+
// });
|
|
2680
|
+
// }
|
|
2681
|
+
|
|
2682
|
+
// private _buildVesuDecreaseLeverRoutes(vesuDeltas: Web3Number[]): VesuMultiplyRoute[] {
|
|
2683
|
+
// return vesuDeltas
|
|
2684
|
+
// .filter((d) => d.collateralDelta.isNegative())
|
|
2685
|
+
// .map((d) => {
|
|
2686
|
+
// this._budget.applyVesuDelta(d.poolId, d.collateralDelta, d.debtDelta);
|
|
2687
|
+
// return {
|
|
2688
|
+
// type: RouteType.VESU_MULTIPLY_DECREASE_LEVER as const,
|
|
2689
|
+
// poolId: d.poolId,
|
|
2690
|
+
// collateralToken: d.collateralToken,
|
|
2691
|
+
// collateralAmount: d.collateralDelta,
|
|
2692
|
+
// debtToken: d.debtToken,
|
|
2693
|
+
// debtAmount: d.debtDelta,
|
|
2694
|
+
// priority: 0,
|
|
2695
|
+
// };
|
|
2696
|
+
// });
|
|
2697
|
+
// }
|
|
2698
|
+
|
|
2699
|
+
private _getWalletToVARoute(tryAmount: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
|
|
2700
|
+
const usableRaw = Math.min(tryAmount, this._budget.walletUsd);
|
|
2701
|
+
if (usableRaw > CASE_THRESHOLD_USD) {
|
|
2702
|
+
const walletUsed = this._budget.spendWallet(usableRaw);
|
|
2703
|
+
this._budget.addToVA(walletUsed);
|
|
2704
|
+
const route: TransferRoute = { type: RouteType.WALLET_TO_VA, amount: safeUsdcWeb3Number(walletUsed), priority: routes.length };
|
|
2705
|
+
routes.push(route);
|
|
2706
|
+
return { routes, remaining: tryAmount - walletUsed };
|
|
2707
|
+
}
|
|
2708
|
+
return { routes, remaining: tryAmount };
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
private _getWalletToEXTENDEDRoute(tryAmount: number, routes: ExecutionRoute[], shouldAddWaitRoute = true): { routes: ExecutionRoute[], remaining: number } {
|
|
2712
|
+
const usableRaw = Math.min(tryAmount, this._budget.walletUsd);
|
|
2713
|
+
if (usableRaw > CASE_THRESHOLD_USD) {
|
|
2714
|
+
const walletUsed = this._budget.spendWallet(usableRaw);
|
|
2715
|
+
this._budget.addToExtAvailTrade(walletUsed);
|
|
2716
|
+
routes.push({ type: RouteType.WALLET_TO_EXTENDED, amount: safeUsdcWeb3Number(walletUsed), priority: routes.length } as TransferRoute);
|
|
2717
|
+
|
|
2718
|
+
if (shouldAddWaitRoute) {
|
|
2719
|
+
routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
|
|
2720
|
+
}
|
|
2721
|
+
return { routes, remaining: tryAmount - walletUsed };
|
|
2722
|
+
}
|
|
2723
|
+
return { routes, remaining: tryAmount };
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
private _getVAToEXTENDEDRoute(tryAmount: number, routes: ExecutionRoute[], shouldAddWaitRoute = true): { routes: ExecutionRoute[], remaining: number } {
|
|
2727
|
+
const usable = Math.min(tryAmount, this._budget.vaUsdcUsd);
|
|
2728
|
+
if (usable > CASE_THRESHOLD_USD) {
|
|
2729
|
+
const vaUsed = this._budget.spendVA(usable);
|
|
2730
|
+
this._budget.addToExtAvailTrade(vaUsed);
|
|
2731
|
+
|
|
2732
|
+
// add extended deposit route
|
|
2733
|
+
const route: TransferRoute = { type: RouteType.VA_TO_EXTENDED, amount: safeUsdcWeb3Number(vaUsed), priority: routes.length };
|
|
2734
|
+
routes.push(route);
|
|
2735
|
+
|
|
2736
|
+
// should follow up by a returning to wait route
|
|
2737
|
+
if (shouldAddWaitRoute) {
|
|
2738
|
+
routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
|
|
2739
|
+
}
|
|
2740
|
+
return { routes, remaining: tryAmount - vaUsed };
|
|
2741
|
+
}
|
|
2742
|
+
return { routes, remaining: tryAmount };
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
private _getExtendedToWalletRoute(tryAmount: number, routes: ExecutionRoute[], shouldAddWaitRoute = true): { routes: ExecutionRoute[], remaining: number } {
|
|
2746
|
+
if (tryAmount <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
|
|
2747
|
+
const rawCap = this._budget.extAvailWithdraw + Math.max(0, this._budget.extAvailUpnl);
|
|
2748
|
+
const rawSpend = Math.min(tryAmount, rawCap);
|
|
2749
|
+
if (rawSpend <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
|
|
2750
|
+
const rawOut = this._budget.spendExtAvailTrade(rawSpend);
|
|
2751
|
+
this._budget.addToWallet(Math.abs(rawOut));
|
|
2752
|
+
const route: TransferRoute = { type: RouteType.EXTENDED_TO_WALLET, amount: safeUsdcWeb3Number(rawSpend), priority: routes.length };
|
|
2753
|
+
routes.push(route);
|
|
2754
|
+
|
|
2755
|
+
if (shouldAddWaitRoute) {
|
|
2756
|
+
routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
|
|
2757
|
+
}
|
|
2758
|
+
return { routes, remaining: tryAmount - rawSpend };
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
private _getWALLETToVARoute(tryAmount: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
|
|
2762
|
+
if (tryAmount <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
|
|
2763
|
+
const usableRaw = Math.min(tryAmount, this._budget.walletUsd);
|
|
2764
|
+
if (usableRaw <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
|
|
2765
|
+
const walletUsed = this._budget.spendWallet(usableRaw);
|
|
2766
|
+
this._budget.addToVA(walletUsed);
|
|
2767
|
+
const route: TransferRoute = { type: RouteType.WALLET_TO_VA, amount: safeUsdcWeb3Number(walletUsed), priority: routes.length };
|
|
2768
|
+
routes.push(route);
|
|
2769
|
+
return { routes, remaining: tryAmount - walletUsed };
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
private _getUpnlRoute(tryAmount: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
|
|
2773
|
+
const rawUpnl = this._budget.extAvailUpnl;
|
|
2774
|
+
const usableRaw = Math.min(tryAmount, rawUpnl);
|
|
2775
|
+
if (usableRaw <= 0) return { routes, remaining: tryAmount };
|
|
2776
|
+
|
|
2777
|
+
// if fails, ensure there is a way to choose the positio to use to create realised pnl
|
|
2778
|
+
// until then, only 1 position is supported
|
|
2779
|
+
this._budget.spendExtAvailUpnl(usableRaw);
|
|
2780
|
+
this._budget.addToExtAvailTrade(usableRaw);
|
|
2781
|
+
assert(this._budget.extendedPositionsView.length == 1, 'SolveBudget::_getUpnlRoute: extendedPositions length must be 1');
|
|
2782
|
+
routes.push({
|
|
2783
|
+
type: RouteType.REALISE_PNL,
|
|
2784
|
+
amount: safeUsdcWeb3Number(usableRaw),
|
|
2785
|
+
instrument: this._budget.extendedPositionsView[0].instrument,
|
|
2786
|
+
priority: routes.length,
|
|
2787
|
+
} as RealisePnlRoute);
|
|
2788
|
+
return { routes, remaining: tryAmount - usableRaw };
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// ── Sub-classifiers ────────────────────────────────────────────────────
|
|
2792
|
+
// Each sub-classifier builds routes directly from contextual data.
|
|
2793
|
+
|
|
2794
|
+
/**
|
|
2795
|
+
* 1. Withdrawal — source funds in priority order:
|
|
2796
|
+
* 1) VA balance 2) Wallet 3) Vesu borrow capacity
|
|
2797
|
+
* 4) Extended available-for-withdrawal + unrealised PnL
|
|
2798
|
+
* 5) Unwind positions on both sides (freed funds handled next cycle)
|
|
2799
|
+
*/
|
|
2800
|
+
private _classifyWithdrawal(
|
|
2801
|
+
withdrawAmount: Web3Number
|
|
2802
|
+
): SolveCaseEntry[] {
|
|
2803
|
+
if (!withdrawAmount.greaterThan(CASE_THRESHOLD_USD)) return [];
|
|
2804
|
+
|
|
2805
|
+
const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
|
|
2806
|
+
const routes: ExecutionRoute[] = [];
|
|
2807
|
+
let remaining = withdrawAmount.toNumber();
|
|
2808
|
+
|
|
2809
|
+
// ── Step 1: VA balance ────────────────────────────────────────────────
|
|
2810
|
+
// VA funds are already in the vault allocator — no transfer route needed.
|
|
2811
|
+
const vaUsed = this._budget.spendVA(remaining);
|
|
2812
|
+
remaining -= vaUsed;
|
|
2813
|
+
|
|
2814
|
+
let totalExtUsed = 0;
|
|
2815
|
+
|
|
2816
|
+
// ── Step 2: Wallet balance → VA ──────────────────────────────────────
|
|
2817
|
+
if (remaining > CASE_THRESHOLD_USD) {
|
|
2818
|
+
const { remaining: walletToVaRemaining } = this._getWALLETToVARoute(remaining, routes);
|
|
2819
|
+
remaining = walletToVaRemaining;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// ── Step 3: Borrow from Vesu (positive debt delta = borrowable) ──────
|
|
2823
|
+
if (remaining > CASE_THRESHOLD_USD) {
|
|
2824
|
+
const { remaining: borrowRemaining } = this._buildVesuBorrowRoutes(remaining, routes);
|
|
2825
|
+
remaining = borrowRemaining;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
// ── Step 4: Extended available-for-withdrawal, then unrealised PnL ───
|
|
2829
|
+
if (remaining > CASE_THRESHOLD_USD) {
|
|
2830
|
+
const usableWithrawAmount = Math.min(remaining, this._budget.extAvailWithdraw);
|
|
2831
|
+
remaining -= usableWithrawAmount;
|
|
2832
|
+
|
|
2833
|
+
let upnlUsed = 0;
|
|
2834
|
+
if (remaining > CASE_THRESHOLD_USD) {
|
|
2835
|
+
const { remaining: upnlRemaining } = this._getUpnlRoute(remaining, routes);
|
|
2836
|
+
upnlUsed = remaining - upnlRemaining;
|
|
2837
|
+
remaining = upnlRemaining;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// keep track of total extended used
|
|
2841
|
+
totalExtUsed = usableWithrawAmount + upnlUsed;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// ── Step 5: Unwind positions ────────────────────────────────────────
|
|
2845
|
+
// Handle imbalance first (unwind from larger side), then equal unwind.
|
|
2846
|
+
// VESU_MULTIPLY_DECREASE_LEVER handles the BTC→USDC swap internally.
|
|
2847
|
+
if (remaining > CASE_THRESHOLD_USD) {
|
|
2848
|
+
assert(this._budget.vesuPools.length == 1, 'SolveBudget::_classifyWithdrawal: vesuPoolStates length must be 1');
|
|
2849
|
+
const vesuAdapter = this._config.vesuAdapters[0];
|
|
2850
|
+
const avgCollPrice = this._budget.vesuPools[0]?.collateralPrice ?? 1;
|
|
2851
|
+
const vesuLeverage = calculateVesuLeverage();
|
|
2852
|
+
const extLeverage = calculateExtendedLevergae();
|
|
2853
|
+
const freedPerBtcVesu = avgCollPrice / vesuLeverage;
|
|
2854
|
+
const freedPerBtcExt = avgCollPrice / extLeverage;
|
|
2855
|
+
|
|
2856
|
+
const vesuColBtc = this._budget.vesuPools[0].collateralAmount.toNumber();
|
|
2857
|
+
const extPosBtc = this._totalExtendedExposure().toNumber();
|
|
2858
|
+
let stillNeeded = remaining;
|
|
2859
|
+
let vesuBtcDelta = 0;
|
|
2860
|
+
let extBtcDelta = 0;
|
|
2861
|
+
let extFreed = 0;
|
|
2862
|
+
|
|
2863
|
+
const roundUpBtc = (x: number): number => {
|
|
2864
|
+
const factor = 10 ** COLLATERAL_PRECISION;
|
|
2865
|
+
return Math.ceil(x * factor) / factor;
|
|
2866
|
+
};
|
|
2867
|
+
|
|
2868
|
+
// Step 5a: Handle imbalance
|
|
2869
|
+
const diff = vesuColBtc - extPosBtc;
|
|
2870
|
+
let currentVesuBtc = vesuColBtc;
|
|
2871
|
+
let currentExtBtc = extPosBtc;
|
|
2872
|
+
|
|
2873
|
+
if (Math.abs(diff) > 1e-8) {
|
|
2874
|
+
if (diff > 0) {
|
|
2875
|
+
const btcRaw = stillNeeded / freedPerBtcVesu;
|
|
2876
|
+
const btc = Math.min(roundUpBtc(Math.min(Math.abs(diff), btcRaw, currentVesuBtc)), currentVesuBtc);
|
|
2877
|
+
vesuBtcDelta += btc;
|
|
2878
|
+
stillNeeded -= btc * freedPerBtcVesu;
|
|
2879
|
+
currentVesuBtc -= btc;
|
|
2880
|
+
} else {
|
|
2881
|
+
const btcRaw = stillNeeded / freedPerBtcExt;
|
|
2882
|
+
const btc = Math.min(roundUpBtc(Math.min(Math.abs(diff), btcRaw, currentExtBtc)), currentExtBtc);
|
|
2883
|
+
extBtcDelta += btc;
|
|
2884
|
+
extFreed += btc * freedPerBtcExt;
|
|
2885
|
+
stillNeeded -= btc * freedPerBtcExt;
|
|
2886
|
+
currentExtBtc -= btc;
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// todo rereviewe logic on how neg upnl gets handled
|
|
2891
|
+
// esp when equity is below ideal margin, 0 ext withdraw,
|
|
2892
|
+
// and how does closing free capital.
|
|
2893
|
+
|
|
2894
|
+
// Step 5b: Equal unwind from both sides
|
|
2895
|
+
if (stillNeeded > CASE_THRESHOLD_USD) {
|
|
2896
|
+
const combinedFreed = freedPerBtcVesu + freedPerBtcExt;
|
|
2897
|
+
const maxBtc = Math.min(currentVesuBtc, currentExtBtc);
|
|
2898
|
+
const btcRaw = stillNeeded / combinedFreed;
|
|
2899
|
+
const btc = Math.min(roundUpBtc(Math.min(btcRaw, maxBtc)), maxBtc);
|
|
2900
|
+
vesuBtcDelta += btc;
|
|
2901
|
+
extBtcDelta += btc;
|
|
2902
|
+
extFreed += btc * freedPerBtcExt;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
const r6 = (n: number) => Number(n.toFixed(6));
|
|
2906
|
+
|
|
2907
|
+
// Emit VESU_MULTIPLY_DECREASE_LEVER (handles swap internally, freed funds go to VA)
|
|
2908
|
+
if (vesuBtcDelta > 0) {
|
|
2909
|
+
const totalVesuBtcSigned = -vesuBtcDelta;
|
|
2910
|
+
const targetLtv = 1 - 1 / vesuLeverage; // vesuLeverage = 1/(1-ltv), so ltv = 1-1/lev
|
|
2911
|
+
const debtDelta = r6(totalVesuBtcSigned * avgCollPrice * targetLtv);
|
|
2912
|
+
// Same convention as LTV rebalance: no separate margin BTC; full Vesu reduction is the swap leg.
|
|
2913
|
+
const marginBtc = 0;
|
|
2914
|
+
const swappedBtc = Number(vesuBtcDelta.toFixed(COLLATERAL_PRECISION));
|
|
2915
|
+
|
|
2916
|
+
routes.push({
|
|
2917
|
+
type: RouteType.VESU_MULTIPLY_DECREASE_LEVER,
|
|
2918
|
+
poolId: vesuAdapter.config.poolId,
|
|
2919
|
+
collateralToken: vesuAdapter.config.collateral,
|
|
2920
|
+
marginAmount: new Web3Number(marginBtc.toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
|
|
2921
|
+
swappedCollateralAmount: new Web3Number(swappedBtc.toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
|
|
2922
|
+
debtToken: vesuAdapter.config.debt,
|
|
2923
|
+
debtAmount: new Web3Number(debtDelta, USDC_TOKEN_DECIMALS),
|
|
2924
|
+
priority: routes.length,
|
|
2925
|
+
} as VesuMultiplyRoute);
|
|
2926
|
+
this._budget.applyVesuDelta(
|
|
2927
|
+
vesuAdapter.config.poolId, vesuAdapter.config.collateral, vesuAdapter.config.debt,
|
|
2928
|
+
new Web3Number(r6(totalVesuBtcSigned), USDC_TOKEN_DECIMALS),
|
|
2929
|
+
new Web3Number(debtDelta, USDC_TOKEN_DECIMALS),
|
|
2930
|
+
);
|
|
2931
|
+
this._budget.addToVA(vesuBtcDelta * freedPerBtcVesu);
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
// Emit EXTENDED_DECREASE_LEVER
|
|
2935
|
+
if (extBtcDelta > 0) {
|
|
2936
|
+
routes.push({
|
|
2937
|
+
type: RouteType.EXTENDED_DECREASE_LEVER,
|
|
2938
|
+
amount: safeUsdcWeb3Number(-r6(extBtcDelta)),
|
|
2939
|
+
instrument,
|
|
2940
|
+
priority: routes.length,
|
|
2941
|
+
});
|
|
2942
|
+
this._budget.applyExtendedExposureDelta(
|
|
2943
|
+
instrument,
|
|
2944
|
+
safeUsdcWeb3Number(-r6(extBtcDelta)),
|
|
2945
|
+
avgCollPrice,
|
|
2946
|
+
);
|
|
2947
|
+
totalExtUsed += extFreed;
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
// Bridge extended funds to VA (availableForTrade → wallet → VA)
|
|
2952
|
+
if (totalExtUsed > CASE_THRESHOLD_USD) {
|
|
2953
|
+
this._getExtendedToWalletRoute(totalExtUsed, routes);
|
|
2954
|
+
this._getWALLETToVARoute(totalExtUsed, routes);
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
// ── Bring liquidity for whatever was sourced in steps 1-4 ────────────
|
|
2958
|
+
routes.push({
|
|
2959
|
+
type: RouteType.BRING_LIQUIDITY,
|
|
2960
|
+
amount: withdrawAmount,
|
|
2961
|
+
priority: routes.length,
|
|
2962
|
+
});
|
|
2963
|
+
this._budget.spendVA(withdrawAmount.toNumber() - vaUsed);
|
|
2964
|
+
|
|
2965
|
+
routes.forEach((r, i) => { r.priority = i; });
|
|
2966
|
+
|
|
2967
|
+
return [{
|
|
2968
|
+
case: CASE_DEFINITIONS[CaseId.WITHDRAWAL_SIMPLE],
|
|
2969
|
+
additionalArgs: { amount: withdrawAmount },
|
|
2970
|
+
routes,
|
|
2971
|
+
}];
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
/**
|
|
2975
|
+
* 2a. LTV Rebalance — Vesu side
|
|
2976
|
+
*
|
|
2977
|
+
* Triggered when vesuPerPoolDebtDeltasToBorrow sum is negative (high LTV, needs repayment).
|
|
2978
|
+
* Sources funds to VA so the subsequent deposit classifier can build the correct lever routes.
|
|
2979
|
+
*
|
|
2980
|
+
* Priority: 1) VA + Wallet (no routes) 2) Extended available-for-withdrawal
|
|
2981
|
+
* 3) Extended uPnL 4) Margin crisis (future)
|
|
2982
|
+
*
|
|
2983
|
+
* Design: accumulate all ext-to-wallet moves, add transfer routes at the end (principle #3).
|
|
2984
|
+
*/
|
|
2985
|
+
/**
|
|
2986
|
+
* Unified LTV / exposure classifier: `rebalance()` drives both LTV (debt, margin,
|
|
2987
|
+
* funding) and Vesu↔Extended position alignment. There is no separate imbalance pass.
|
|
2988
|
+
* Computes both Vesu repay and Extended margin needs,
|
|
2989
|
+
* then builds all routes in a single pass with no duplicate transfers.
|
|
2990
|
+
*
|
|
2991
|
+
* Vesu repay priority: VA > Wallet > ExtAvl > ExtUpnl
|
|
2992
|
+
* Extended margin priority: Wallet > VA > VesuBorrow
|
|
2993
|
+
* Shared sources consumed by Vesu first (higher priority).
|
|
2994
|
+
*/
|
|
2995
|
+
private _classifyLTV(): SolveCaseEntry[] {
|
|
2996
|
+
assert(this._budget.vesuPools.length === 1, `${this._tag}::_classifyLTV expects exactly one Vesu pool`);
|
|
2997
|
+
const d = rebalance(this._ltvRebalanceInputsFromBudget());
|
|
2998
|
+
if (this._isLtvRebalanceNoop(d)) return [];
|
|
2999
|
+
|
|
3000
|
+
logger.info(
|
|
3001
|
+
`${this._tag}::_classifyLTV deltas extPos=${d.dExtPosition} vesuPos=${d.dVesuPosition} `
|
|
3002
|
+
+ `vesuDebt=${d.dVesuDebt} va=${d.dVaUsd} wallet=${d.dWalletUsd} borrow=${d.dVesuBorrowCapacity} `
|
|
3003
|
+
+ `T=${d.dTransferVesuToExt}`,
|
|
3004
|
+
);
|
|
3005
|
+
|
|
3006
|
+
const routes = this._buildLtvRoutesFromRebalanceDeltas(d);
|
|
3007
|
+
if (routes.length === 0) return [];
|
|
3008
|
+
|
|
3009
|
+
const amountUsd =
|
|
3010
|
+
Math.abs(d.dVaUsd)
|
|
3011
|
+
+ Math.abs(d.dWalletUsd)
|
|
3012
|
+
+ Math.abs(d.dVesuBorrowCapacity)
|
|
3013
|
+
+ Math.abs(d.dExtAvlWithdraw)
|
|
3014
|
+
+ Math.abs(d.dExtUpnl)
|
|
3015
|
+
+ Math.abs(d.dTransferVesuToExt);
|
|
3016
|
+
|
|
3017
|
+
routes.forEach((r, i) => { r.priority = i; });
|
|
3018
|
+
return [{
|
|
3019
|
+
case: CASE_DEFINITIONS[CaseId.MANAGE_LTV],
|
|
3020
|
+
additionalArgs: { amount: safeUsdcWeb3Number(amountUsd) },
|
|
3021
|
+
routes,
|
|
3022
|
+
}];
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
private _ltvRebalanceInputsFromBudget() {
|
|
3026
|
+
const pool = this._budget.vesuPools[0];
|
|
3027
|
+
const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
|
|
3028
|
+
const extPosBtc = this._budget.extendedPositionsView
|
|
3029
|
+
.filter(p => p.instrument === instrument)
|
|
3030
|
+
.reduce((s, p) => s + Math.abs(p.size.toNumber()), 0);
|
|
3031
|
+
const targetHF = VesuConfig.maxLtv / VesuConfig.targetLtv;
|
|
3032
|
+
|
|
3033
|
+
return {
|
|
3034
|
+
ext: {
|
|
3035
|
+
positionBtc: extPosBtc,
|
|
3036
|
+
equity: this._budget.extendedBalanceView?.equity?.toNumber() ?? 0,
|
|
3037
|
+
avlWithdraw: this._budget.extAvailWithdraw,
|
|
3038
|
+
upnl: this._budget.extAvailUpnl,
|
|
3039
|
+
leverage: calculateExtendedLevergae(),
|
|
3040
|
+
},
|
|
3041
|
+
vesu: {
|
|
3042
|
+
positionBtc: pool.collateralAmount.toNumber(),
|
|
3043
|
+
debt: pool.debtAmount.toNumber(),
|
|
3044
|
+
debtPrice: pool.debtPrice,
|
|
3045
|
+
maxLTV: VesuConfig.maxLtv,
|
|
3046
|
+
targetHF,
|
|
3047
|
+
},
|
|
3048
|
+
btcPrice: pool.collateralPrice,
|
|
3049
|
+
funding: {
|
|
3050
|
+
vaUsd: this._budget.vaUsd,
|
|
3051
|
+
walletUsd: this._budget.walletUsd,
|
|
3052
|
+
vesuBorrowCapacity: this._budget.vesuBorrowCapacity,
|
|
3053
|
+
extAvlWithdraw: this._budget.extAvailWithdraw,
|
|
3054
|
+
extUpnl: this._budget.extAvailUpnl,
|
|
3055
|
+
},
|
|
3056
|
+
config: {
|
|
3057
|
+
positionPrecision: COLLATERAL_PRECISION,
|
|
3058
|
+
hfBuffer: 0.05,
|
|
3059
|
+
minRoutableUsd: CASE_THRESHOLD_USD,
|
|
3060
|
+
},
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
private _isLtvRebalanceNoop(d: RebalanceDeltas): boolean {
|
|
3065
|
+
const btcEps = 10 ** -COLLATERAL_PRECISION;
|
|
3066
|
+
const usdEps = CASE_THRESHOLD_USD;
|
|
3067
|
+
return (
|
|
3068
|
+
Math.abs(d.dExtPosition) < btcEps
|
|
3069
|
+
&& Math.abs(d.dVesuPosition) < btcEps
|
|
3070
|
+
&& Math.abs(d.dVesuDebt) < 1e-6
|
|
3071
|
+
&& Math.abs(d.dExtAvlWithdraw) < usdEps
|
|
3072
|
+
&& Math.abs(d.dExtUpnl) < usdEps
|
|
3073
|
+
&& Math.abs(d.dVaUsd) < usdEps
|
|
3074
|
+
&& Math.abs(d.dWalletUsd) < usdEps
|
|
3075
|
+
&& Math.abs(d.dVesuBorrowCapacity) < usdEps
|
|
3076
|
+
&& Math.abs(d.dTransferVesuToExt) < usdEps
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
/**
|
|
3081
|
+
* Turn pure rebalance() deltas into execution routes.
|
|
3082
|
+
* Order: Vesu multiply decrease → Extended decrease → aggregated transfers
|
|
3083
|
+
* (REALISE_PNL, EXTENDED_TO_WALLET + WAIT, WALLET_TO_VA, VESU_BORROW, VESU_REPAY, VA_TO_EXTENDED),
|
|
3084
|
+
* then Vesu multiply increase and Extended increase (need VA / Extended funded first).
|
|
3085
|
+
*/
|
|
3086
|
+
private _buildLtvRoutesFromRebalanceDeltas(d: RebalanceDeltas): ExecutionRoute[] {
|
|
3087
|
+
const routes: ExecutionRoute[] = [];
|
|
3088
|
+
const pool = this._budget.vesuPools[0];
|
|
3089
|
+
const vesuAdapter = this._config.vesuAdapters[0];
|
|
3090
|
+
const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
|
|
3091
|
+
const price = pool.collateralPrice;
|
|
3092
|
+
const debtPrice = pool.debtPrice;
|
|
3093
|
+
const targetLtv = VesuConfig.targetLtv;
|
|
3094
|
+
const btcEps = 10 ** -COLLATERAL_PRECISION;
|
|
3095
|
+
|
|
3096
|
+
let multiplyDebtRepayUsd = 0;
|
|
3097
|
+
|
|
3098
|
+
logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas d=${JSON.stringify(d)}`);
|
|
3099
|
+
|
|
3100
|
+
// ── 1) Vesu multiply decrease (free collateral / repay) ─────────────
|
|
3101
|
+
if (d.dVesuPosition < -btcEps) {
|
|
3102
|
+
const xBtc = -d.dVesuPosition;
|
|
3103
|
+
// When Vesu sends USD to Extended (dTransferVesuToExt > 0), part of the BTC cut must be the
|
|
3104
|
+
// multiply "margin" leg so USDC lands in VA and can be routed VA_TO_EXTENDED (adapter uses sub_margin).
|
|
3105
|
+
const transferUsdFromVesu = Math.max(0, d.dTransferVesuToExt);
|
|
3106
|
+
let marginBtc = 0;
|
|
3107
|
+
let swappedBtc = Number(xBtc.toFixed(COLLATERAL_PRECISION));
|
|
3108
|
+
if (transferUsdFromVesu > CASE_THRESHOLD_USD && price > 0) {
|
|
3109
|
+
const marginCapFromTransfer = transferUsdFromVesu / price;
|
|
3110
|
+
marginBtc = Number(
|
|
3111
|
+
Math.min(xBtc, marginCapFromTransfer).toFixed(COLLATERAL_PRECISION),
|
|
3112
|
+
);
|
|
3113
|
+
swappedBtc = Number((xBtc - marginBtc).toFixed(COLLATERAL_PRECISION));
|
|
3114
|
+
}
|
|
3115
|
+
// Swap leg repays at most (swapped BTC notional in USD); any extra debt reduction
|
|
3116
|
+
// is a separate VESU_REPAY (see multiplyDebtRepayUsd / standaloneRepayUsd below).
|
|
3117
|
+
const swapLegMaxRepayUsd = swappedBtc * price * debtPrice;
|
|
3118
|
+
const debtUsdFallback = swappedBtc * price * targetLtv;
|
|
3119
|
+
let debtTokenDelta: number;
|
|
3120
|
+
if (d.dVesuDebt < 0) {
|
|
3121
|
+
const needRepayUsd = -d.dVesuDebt * debtPrice;
|
|
3122
|
+
const multiplyRepayUsd = Math.min(needRepayUsd, swapLegMaxRepayUsd);
|
|
3123
|
+
logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas ${JSON.stringify({
|
|
3124
|
+
needRepayUsd,
|
|
3125
|
+
multiplyRepayUsd,
|
|
3126
|
+
})}`);
|
|
3127
|
+
debtTokenDelta = -(multiplyRepayUsd / debtPrice);
|
|
3128
|
+
} else {
|
|
3129
|
+
debtTokenDelta = -debtUsdFallback;
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
const debtAmtW3 = new Web3Number(debtTokenDelta.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS);
|
|
3133
|
+
multiplyDebtRepayUsd = Math.abs(debtTokenDelta) * debtPrice;
|
|
3134
|
+
logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas ${JSON.stringify({
|
|
3135
|
+
debtTokenDelta,
|
|
3136
|
+
debtUsdFallback,
|
|
3137
|
+
swapLegMaxRepayUsd,
|
|
3138
|
+
xBtc,
|
|
3139
|
+
marginBtc,
|
|
3140
|
+
swappedBtc,
|
|
3141
|
+
transferUsdFromVesu,
|
|
3142
|
+
debtPrice,
|
|
3143
|
+
targetLtv,
|
|
3144
|
+
multiplyDebtRepayUsd,
|
|
3145
|
+
})}`);
|
|
3146
|
+
routes.push({
|
|
3147
|
+
type: RouteType.VESU_MULTIPLY_DECREASE_LEVER,
|
|
3148
|
+
poolId: vesuAdapter.config.poolId,
|
|
3149
|
+
collateralToken: vesuAdapter.config.collateral,
|
|
3150
|
+
marginAmount: new Web3Number(marginBtc.toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
|
|
3151
|
+
swappedCollateralAmount: new Web3Number(swappedBtc.toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
|
|
3152
|
+
debtToken: vesuAdapter.config.debt,
|
|
3153
|
+
debtAmount: debtAmtW3,
|
|
3154
|
+
priority: routes.length,
|
|
3155
|
+
} as VesuMultiplyRoute);
|
|
3156
|
+
this._budget.applyVesuDelta(
|
|
3157
|
+
vesuAdapter.config.poolId,
|
|
3158
|
+
vesuAdapter.config.collateral,
|
|
3159
|
+
vesuAdapter.config.debt,
|
|
3160
|
+
new Web3Number((-xBtc).toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
|
|
3161
|
+
debtAmtW3,
|
|
3162
|
+
);
|
|
3163
|
+
if (transferUsdFromVesu > CASE_THRESHOLD_USD) {
|
|
3164
|
+
this._budget.addToVA(transferUsdFromVesu);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
// ── 2) Extended decrease lever only (increase after VA→Ext funding) ─
|
|
3169
|
+
if (d.dExtPosition < -btcEps) {
|
|
3170
|
+
const amt = new Web3Number(d.dExtPosition.toFixed(COLLATERAL_PRECISION), 8);
|
|
3171
|
+
routes.push({
|
|
3172
|
+
type: RouteType.EXTENDED_DECREASE_LEVER,
|
|
3173
|
+
amount: amt,
|
|
3174
|
+
instrument,
|
|
3175
|
+
priority: routes.length,
|
|
3176
|
+
} as ExtendedLeverRoute);
|
|
3177
|
+
this._budget.applyExtendedExposureDelta(instrument, amt, price);
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
// ── 3) Aggregated transfers (no WALLET_TO_EXTENDED; Ext only via VA) ─
|
|
3181
|
+
const negUpnl = Math.min(0, d.dExtUpnl);
|
|
3182
|
+
const negExtAvl = Math.min(0, d.dExtAvlWithdraw);
|
|
3183
|
+
let hadExtendedOut = false;
|
|
3184
|
+
|
|
3185
|
+
if (negUpnl < -CASE_THRESHOLD_USD) {
|
|
3186
|
+
this._getUpnlRoute(Math.abs(negUpnl), routes);
|
|
3187
|
+
hadExtendedOut = true;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
const extToWalletUsd = (negExtAvl < -CASE_THRESHOLD_USD ? Math.abs(negExtAvl) : 0)
|
|
3191
|
+
+ (negUpnl < -CASE_THRESHOLD_USD ? Math.abs(negUpnl) : 0);
|
|
3192
|
+
logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas extToWalletUsd=${extToWalletUsd}, negExtAvl=${negExtAvl}, negUpnl=${negUpnl}`);
|
|
3193
|
+
if (extToWalletUsd > CASE_THRESHOLD_USD) {
|
|
3194
|
+
this._getExtendedToWalletRoute(extToWalletUsd, routes);
|
|
3195
|
+
hadExtendedOut = true;
|
|
3196
|
+
} else {
|
|
3197
|
+
// too small to withdraw from extended
|
|
3198
|
+
// reduce any vesu debt
|
|
3199
|
+
// else further repay from va will fail
|
|
3200
|
+
if (d.dVesuDebt < 0) {
|
|
3201
|
+
d.dVesuDebt += (negExtAvl < 0 && negExtAvl > -CASE_THRESHOLD_USD ? Math.abs(negExtAvl) : 0)
|
|
3202
|
+
+ (negUpnl < 0 && negUpnl > -CASE_THRESHOLD_USD ? Math.abs(negUpnl) : 0);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas d=${JSON.stringify(d)}`);
|
|
3207
|
+
|
|
3208
|
+
const walletPull = Math.abs(Math.min(0, d.dWalletUsd));
|
|
3209
|
+
const walletToVaUsd = walletPull + extToWalletUsd;
|
|
3210
|
+
if (walletToVaUsd > CASE_THRESHOLD_USD) {
|
|
3211
|
+
this._getWALLETToVARoute(walletToVaUsd, routes);
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
if (d.dVesuBorrowCapacity < -CASE_THRESHOLD_USD) {
|
|
3215
|
+
this._buildVesuBorrowRoutes(Math.abs(d.dVesuBorrowCapacity), routes);
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
const totalDebtRepayUsd = d.dVesuDebt < 0 ? -d.dVesuDebt * debtPrice : 0;
|
|
3219
|
+
logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas totalDebtRepayUsd=${totalDebtRepayUsd}`);
|
|
3220
|
+
let standaloneRepayUsd = Math.max(0, totalDebtRepayUsd - multiplyDebtRepayUsd);
|
|
3221
|
+
logger.info(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas standaloneRepayUsd=${standaloneRepayUsd}`);
|
|
3222
|
+
if (standaloneRepayUsd > this._budget.vaRawUsd) {
|
|
3223
|
+
if (Math.abs(standaloneRepayUsd - this._budget.vaRawUsd) < CASE_THRESHOLD_USD) {
|
|
3224
|
+
standaloneRepayUsd = this._budget.vaRawUsd;
|
|
3225
|
+
} else {
|
|
3226
|
+
throw new Error(`${this._tag}::_buildLtvRoutesFromRebalanceDeltas standaloneRepayUsd=${standaloneRepayUsd} > vaRawUsd=${this._budget.vaRawUsd}`);
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
if (standaloneRepayUsd > CASE_THRESHOLD_USD) {
|
|
3230
|
+
this._buildVesuRepayRoutes(standaloneRepayUsd, routes);
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
const posExtEq = Math.max(0, d.dExtAvlWithdraw);
|
|
3234
|
+
const vaToExtUsd = posExtEq > CASE_THRESHOLD_USD ? posExtEq : 0;
|
|
3235
|
+
if (vaToExtUsd > CASE_THRESHOLD_USD) {
|
|
3236
|
+
this._getVAToEXTENDEDRoute(vaToExtUsd, routes, hadExtendedOut);
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
// ── Vesu / Extended increase lever: require prior VA funding & VA→Extended where applicable
|
|
3240
|
+
if (d.dVesuPosition > btcEps) {
|
|
3241
|
+
const vesuDepositAmount = new Web3Number(
|
|
3242
|
+
(d.dVesuPosition * price * (1 - targetLtv)).toFixed(USDC_TOKEN_DECIMALS),
|
|
3243
|
+
USDC_TOKEN_DECIMALS,
|
|
3244
|
+
);
|
|
3245
|
+
if (vesuDepositAmount.toNumber() > CASE_THRESHOLD_USD) {
|
|
3246
|
+
// routes.push({
|
|
3247
|
+
// type: RouteType.AVNU_DEPOSIT_SWAP,
|
|
3248
|
+
// priority: routes.length,
|
|
3249
|
+
// fromToken: vesuAdapter.config.collateral.symbol,
|
|
3250
|
+
// fromAmount: vesuDepositAmount,
|
|
3251
|
+
// toToken: vesuAdapter.config.debt.symbol,
|
|
3252
|
+
// });
|
|
3253
|
+
}
|
|
3254
|
+
const collateralDelta = new Web3Number(
|
|
3255
|
+
d.dVesuPosition.toFixed(COLLATERAL_PRECISION),
|
|
3256
|
+
vesuAdapter.config.collateral.decimals,
|
|
3257
|
+
);
|
|
3258
|
+
const availableBorrowCapacity = this._budget.vesuBorrowCapacity;
|
|
3259
|
+
const externalDepositAmount = vesuDepositAmount.minus(
|
|
3260
|
+
new Web3Number(Math.min(availableBorrowCapacity, vesuDepositAmount.toNumber()).toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS),
|
|
3261
|
+
);
|
|
3262
|
+
const collPx = pool.collateralPrice || 1;
|
|
3263
|
+
const marginUsdAmount = externalDepositAmount.toNumber() * (pool.debtPrice ?? 1);
|
|
3264
|
+
const swappedAmount = new Web3Number(
|
|
3265
|
+
((marginUsdAmount / collPx)).toFixed(6),
|
|
3266
|
+
vesuAdapter.config.collateral.decimals,
|
|
3267
|
+
);
|
|
3268
|
+
const debtDeltaTokens = new Web3Number(
|
|
3269
|
+
(d.dVesuDebt).toFixed(USDC_TOKEN_DECIMALS),
|
|
3270
|
+
USDC_TOKEN_DECIMALS,
|
|
3271
|
+
);
|
|
3272
|
+
routes.push({
|
|
3273
|
+
type: RouteType.VESU_MULTIPLY_INCREASE_LEVER,
|
|
3274
|
+
priority: routes.length,
|
|
3275
|
+
collateralToken: vesuAdapter.config.collateral,
|
|
3276
|
+
debtToken: vesuAdapter.config.debt,
|
|
3277
|
+
marginAmount: swappedAmount,
|
|
3278
|
+
swappedCollateralAmount: collateralDelta.minus(swappedAmount),
|
|
3279
|
+
debtAmount: debtDeltaTokens.plus(new Web3Number(Math.min(availableBorrowCapacity, vesuDepositAmount.toNumber()).toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS)),
|
|
3280
|
+
poolId: vesuAdapter.config.poolId,
|
|
3281
|
+
} as VesuMultiplyRoute);
|
|
3282
|
+
this._budget.applyVesuDelta(
|
|
3283
|
+
vesuAdapter.config.poolId,
|
|
3284
|
+
vesuAdapter.config.collateral,
|
|
3285
|
+
vesuAdapter.config.debt,
|
|
3286
|
+
collateralDelta,
|
|
3287
|
+
debtDeltaTokens,
|
|
3288
|
+
);
|
|
3289
|
+
this._budget.spendVA(marginUsdAmount);
|
|
3290
|
+
}
|
|
3291
|
+
if (d.dExtPosition > btcEps) {
|
|
3292
|
+
const amt = new Web3Number(d.dExtPosition.toFixed(COLLATERAL_PRECISION), 8);
|
|
3293
|
+
routes.push({
|
|
3294
|
+
type: RouteType.EXTENDED_INCREASE_LEVER,
|
|
3295
|
+
amount: amt,
|
|
3296
|
+
instrument,
|
|
3297
|
+
priority: routes.length,
|
|
3298
|
+
} as ExtendedLeverRoute);
|
|
3299
|
+
this._budget.applyExtendedExposureDelta(instrument, amt, price);
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
return routes;
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
// ── LTV Vesu route builders ───────────────────────────────────────────
|
|
3306
|
+
|
|
3307
|
+
/**
|
|
3308
|
+
* LTV_EXTENDED_PROFITABLE_AVAILABLE:
|
|
3309
|
+
* Extended has enough available-to-withdraw → withdraw, move to VA, repay Vesu.
|
|
3310
|
+
* Routes: [EXTENDED_TO_WALLET, WALLET_TO_VA, VESU_REPAY]
|
|
3311
|
+
*/
|
|
3312
|
+
// private _buildLtvVesuRepayFromExtendedRoutes(amountUsd: number, vesuDeltas: Web3Number[]): ExecutionRoute[] {
|
|
3313
|
+
// const routes: ExecutionRoute[] = [];
|
|
3314
|
+
// const amt = safeUsdcWeb3Number(amountUsd);
|
|
3315
|
+
|
|
3316
|
+
// // Withdraw from Extended → operator wallet
|
|
3317
|
+
// routes.push({ type: RouteType.EXTENDED_TO_WALLET, amount: amt, priority: routes.length });
|
|
3318
|
+
// this._budget.applyExtendedBalanceChange(-amountUsd);
|
|
3319
|
+
|
|
3320
|
+
// // Wait for Extended withdrawal to settle before using wallet funds
|
|
3321
|
+
// routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
|
|
3322
|
+
|
|
3323
|
+
// // Operator wallet → VA
|
|
3324
|
+
// routes.push({ type: RouteType.WALLET_TO_VA, amount: amt, priority: routes.length });
|
|
3325
|
+
|
|
3326
|
+
// // Repay Vesu debt from VA funds
|
|
3327
|
+
// routes.push(...this._buildVesuRepayRoutesss(amountUsd, vesuDeltas)); // wrong
|
|
3328
|
+
|
|
3329
|
+
// routes.forEach((r, i) => { r.priority = i; });
|
|
3330
|
+
// return routes;
|
|
3331
|
+
// }
|
|
3332
|
+
|
|
3333
|
+
/**
|
|
3334
|
+
* LTV_EXTENDED_PROFITABLE_REALIZE:
|
|
3335
|
+
* Extended has unrealised PnL → realise it first, then withdraw + repay.
|
|
3336
|
+
* Routes: [REALISE_PNL, EXTENDED_TO_WALLET, WALLET_TO_VA, VESU_REPAY]
|
|
3337
|
+
*/
|
|
3338
|
+
// private _buildLtvVesuRepayRealiseRoutes(amountUsd: number, availWithdrawUsd: number, vesuDeltas: Web3Number[]): ExecutionRoute[] {
|
|
3339
|
+
// const routes: ExecutionRoute[] = [];
|
|
3340
|
+
// const amt = safeUsdcWeb3Number(amountUsd);
|
|
3341
|
+
// const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
|
|
3342
|
+
// const upnlRequired = amountUsd - availWithdrawUsd;
|
|
3343
|
+
|
|
3344
|
+
// // Realise PnL to make funds available for withdrawal
|
|
3345
|
+
// routes.push({ type: RouteType.REALISE_PNL, amount: safeUsdcWeb3Number(upnlRequired), instrument, priority: routes.length });
|
|
3346
|
+
|
|
3347
|
+
// // Withdraw realised funds from Extended → operator wallet
|
|
3348
|
+
// routes.push({ type: RouteType.EXTENDED_TO_WALLET, amount: amt, priority: routes.length });
|
|
3349
|
+
// this._budget.applyExtendedBalanceChange(-amountUsd);
|
|
3350
|
+
|
|
3351
|
+
// // Wait for Extended withdrawal to settle
|
|
3352
|
+
// routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
|
|
3353
|
+
|
|
3354
|
+
// // Operator wallet → VA
|
|
3355
|
+
// routes.push({ type: RouteType.WALLET_TO_VA, amount: amt, priority: routes.length });
|
|
3356
|
+
|
|
3357
|
+
// // Repay Vesu debt from VA funds
|
|
3358
|
+
// routes.push(...this._buildVesuRepayRoutesss(amountUsd, vesuDeltas)); // wrong
|
|
3359
|
+
|
|
3360
|
+
// routes.forEach((r, i) => { r.priority = i; });
|
|
3361
|
+
// return routes;
|
|
3362
|
+
// }
|
|
3363
|
+
|
|
3364
|
+
/**
|
|
3365
|
+
* MARGIN_CRISIS_VESU:
|
|
3366
|
+
* Neither VA/Wallet nor Extended withdrawal covers the shortfall.
|
|
3367
|
+
* Temporarily increase Extended leverage to free margin, withdraw to VA, then
|
|
3368
|
+
* decrease Vesu lever to bring LTV back in range.
|
|
3369
|
+
* Routes: [CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE, EXTENDED_TO_WALLET, WALLET_TO_VA,
|
|
3370
|
+
* VESU_MULTIPLY_DECREASE_LEVER, EXTENDED_DECREASE_LEVER, CRISIS_UNDO_EXTENDED_MAX_LEVERAGE]
|
|
3371
|
+
*/
|
|
3372
|
+
// private _buildMarginCrisisVesuRoutes(neededUsd: number, vesuDeltas: Web3Number[]): ExecutionRoute[] {
|
|
3373
|
+
// const routes: ExecutionRoute[] = [];
|
|
3374
|
+
// const amt = safeUsdcWeb3Number(neededUsd);
|
|
3375
|
+
// const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
|
|
3376
|
+
|
|
3377
|
+
// // Temporarily boost Extended leverage to free margin
|
|
3378
|
+
// routes.push({ type: RouteType.CRISIS_INCREASE_EXTENDED_MAX_LEVERAGE, priority: routes.length });
|
|
3379
|
+
|
|
3380
|
+
// // Withdraw freed margin from Extended → wallet
|
|
3381
|
+
// routes.push({ type: RouteType.EXTENDED_TO_WALLET, amount: amt, priority: routes.length });
|
|
3382
|
+
// this._budget.applyExtendedBalanceChange(-neededUsd);
|
|
3383
|
+
|
|
3384
|
+
// // Wallet → VA
|
|
3385
|
+
// routes.push({ type: RouteType.WALLET_TO_VA, amount: amt, priority: routes.length });
|
|
3386
|
+
|
|
3387
|
+
// // Decrease Vesu leverage to reduce LTV
|
|
3388
|
+
// // todo to verify if this is correct.
|
|
3389
|
+
// routes.push(...this._buildVesuDecreaseLeverRoutes(vesuDeltas));
|
|
3390
|
+
|
|
3391
|
+
// // Decrease Extended exposure to match reduced Vesu position
|
|
3392
|
+
// // ! todo incorrect, to fix
|
|
3393
|
+
// routes.push({ type: RouteType.EXTENDED_DECREASE_LEVER, amount: safeUsdcWeb3Number(0), instrument, priority: routes.length });
|
|
3394
|
+
// this._budget.applyExtendedExposureDelta(instrument, safeUsdcWeb3Number(0));
|
|
3395
|
+
|
|
3396
|
+
// // Revert Extended leverage back to normal
|
|
3397
|
+
// routes.push({ type: RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE, priority: routes.length });
|
|
3398
|
+
|
|
3399
|
+
// routes.forEach((r, i) => { r.priority = i; });
|
|
3400
|
+
// return routes;
|
|
3401
|
+
// }
|
|
3402
|
+
|
|
3403
|
+
// _classifyLtvExtended has been merged into the unified _classifyLTV above.
|
|
3404
|
+
|
|
3405
|
+
// ── LTV Extended route builders ───────────────────────────────────────
|
|
3406
|
+
|
|
3407
|
+
/**
|
|
3408
|
+
* LTV_EXTENDED_HIGH_USE_VA_OR_WALLET:
|
|
3409
|
+
* VA/Wallet has funds → route them to Extended.
|
|
3410
|
+
* Routes: [VA_TO_EXTENDED, WALLET_TO_EXTENDED] (wallet-first, then VA for remainder)
|
|
3411
|
+
*/
|
|
3412
|
+
// private _buildLtvExtendedDepositFromVARoutes(amountUsd: number): ExecutionRoute[] {
|
|
3413
|
+
// const routes: ExecutionRoute[] = this._buildExtendedDepositRoutes(amountUsd);
|
|
3414
|
+
// routes.forEach((r, i) => { r.priority = i; });
|
|
3415
|
+
// return routes;
|
|
3416
|
+
// }
|
|
3417
|
+
|
|
3418
|
+
/**
|
|
3419
|
+
* LTV_VESU_LOW_TO_EXTENDED:
|
|
3420
|
+
* Borrow USDC from Vesu, route through VA to Extended.
|
|
3421
|
+
* Routes: [VESU_BORROW, VA_TO_EXTENDED]
|
|
3422
|
+
*/
|
|
3423
|
+
// private _buildLtvExtendedBorrowFromVesuRoutes(borrowUsd: number, vesuDeltas: Web3Number[]): ExecutionRoute[] {
|
|
3424
|
+
// const routes: ExecutionRoute[] = [];
|
|
3425
|
+
// const amt = safeUsdcWeb3Number(borrowUsd);
|
|
3426
|
+
|
|
3427
|
+
// // Borrow USDC from Vesu (lands in VA)
|
|
3428
|
+
// const { routes: borrowRoutes } = this._buildVesuBorrowRoutes(borrowUsd, vesuDeltas);
|
|
3429
|
+
// routes.push(...borrowRoutes);
|
|
3430
|
+
|
|
3431
|
+
// // VA → Extended
|
|
3432
|
+
// routes.push({ type: RouteType.VA_TO_EXTENDED, amount: amt, priority: routes.length });
|
|
3433
|
+
// this._budget.applyExtendedBalanceChange(borrowUsd);
|
|
3434
|
+
|
|
3435
|
+
// routes.forEach((r, i) => { r.priority = i; });
|
|
3436
|
+
// return routes;
|
|
3437
|
+
// }
|
|
3438
|
+
|
|
3439
|
+
/**
|
|
3440
|
+
* MARGIN_CRISIS_EXTENDED:
|
|
3441
|
+
* Borrow beyond target HF on Vesu to free USDC, deposit to Extended,
|
|
3442
|
+
* then decrease Vesu/Extended lever, undo crisis leverage.
|
|
3443
|
+
* Routes: [CRISIS_BORROW_BEYOND_TARGET_HF, VA_TO_EXTENDED,
|
|
3444
|
+
* VESU_MULTIPLY_DECREASE_LEVER, EXTENDED_DECREASE_LEVER,
|
|
3445
|
+
* CRISIS_UNDO_EXTENDED_MAX_LEVERAGE]
|
|
3446
|
+
*/
|
|
3447
|
+
// private _buildMarginCrisisExtendedRoutes(neededUsd: number, vesuDeltas: VesuPoolDelta[]): ExecutionRoute[] {
|
|
3448
|
+
// // ! todo need to verify this case properly
|
|
3449
|
+
|
|
3450
|
+
// const routes: ExecutionRoute[] = [];
|
|
3451
|
+
// const amt = safeUsdcWeb3Number(neededUsd);
|
|
3452
|
+
// const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
|
|
3453
|
+
|
|
3454
|
+
// // Borrow beyond normal target HF to free USDC
|
|
3455
|
+
// const pool = vesuDeltas.find((d) => d.debtDelta.greaterThan(0)) ?? vesuDeltas[0];
|
|
3456
|
+
// routes.push({
|
|
3457
|
+
// type: RouteType.CRISIS_BORROW_BEYOND_TARGET_HF,
|
|
3458
|
+
// poolId: pool?.poolId ?? ('' as unknown as ContractAddr),
|
|
3459
|
+
// token: pool?.debtToken?.symbol ?? this._config.assetToken.symbol,
|
|
3460
|
+
// amount: amt,
|
|
3461
|
+
// priority: routes.length,
|
|
3462
|
+
// });
|
|
3463
|
+
|
|
3464
|
+
// // Borrowed USDC (now in VA) → Extended
|
|
3465
|
+
// routes.push({ type: RouteType.VA_TO_EXTENDED, amount: amt, priority: routes.length });
|
|
3466
|
+
// this._budget.applyExtendedBalanceChange(neededUsd);
|
|
3467
|
+
|
|
3468
|
+
// // Wait for Extended deposit to be credited
|
|
3469
|
+
// routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
|
|
3470
|
+
|
|
3471
|
+
// // Decrease Vesu leverage
|
|
3472
|
+
// routes.push(...this._buildVesuDecreaseLeverRoutes(vesuDeltas));
|
|
3473
|
+
|
|
3474
|
+
// // Decrease Extended exposure to match
|
|
3475
|
+
// routes.push({ type: RouteType.EXTENDED_DECREASE_LEVER, amount: amt, instrument, priority: routes.length });
|
|
3476
|
+
// this._budget.applyExtendedExposureDelta(instrument, new Web3Number(amt.negated().toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS));
|
|
3477
|
+
|
|
3478
|
+
// // Revert crisis leverage
|
|
3479
|
+
// routes.push({ type: RouteType.CRISIS_UNDO_EXTENDED_MAX_LEVERAGE, priority: routes.length });
|
|
3480
|
+
|
|
3481
|
+
// routes.forEach((r, i) => { r.priority = i; });
|
|
3482
|
+
// return routes;
|
|
3483
|
+
// }
|
|
3484
|
+
|
|
3485
|
+
// ! todo implement max lever amount per execution cycle
|
|
3486
|
+
|
|
3487
|
+
private _rebalanceFunds({
|
|
3488
|
+
extAvlWithdraw,
|
|
3489
|
+
extUpnl,
|
|
3490
|
+
vaUsd,
|
|
3491
|
+
walletUsd,
|
|
3492
|
+
vesuBorrowCapacity,
|
|
3493
|
+
vesuLeverage,
|
|
3494
|
+
extendedLeverage,
|
|
3495
|
+
}: Inputs): Deltas {
|
|
3496
|
+
const total = extAvlWithdraw + extUpnl + vaUsd + walletUsd + vesuBorrowCapacity;
|
|
3497
|
+
|
|
3498
|
+
// Target eco split
|
|
3499
|
+
const extendedTarget = (total) / (1 + extendedLeverage / vesuLeverage);
|
|
3500
|
+
const extendedInitial = extAvlWithdraw + extUpnl;
|
|
3501
|
+
|
|
3502
|
+
let delta = extendedTarget - extendedInitial;
|
|
3503
|
+
|
|
3504
|
+
// Initialize deltas (0 = no change)
|
|
3505
|
+
let dExtAvlWithdraw = 0,
|
|
3506
|
+
dExtUpnl = 0,
|
|
3507
|
+
dVaUsd = 0,
|
|
3508
|
+
dWalletUsd = 0,
|
|
3509
|
+
dVesuBorrowCapacity = 0,
|
|
3510
|
+
finalVaUsd = vaUsd,
|
|
3511
|
+
finalExtended = extAvlWithdraw + (Math.max(extUpnl, 0));
|
|
3512
|
+
|
|
3513
|
+
// --- Case 1: eco1 needs MORE funds (pull from eco2)
|
|
3514
|
+
if (delta > 0) {
|
|
3515
|
+
let need = delta;
|
|
3516
|
+
const eps = CASE_THRESHOLD_USD;
|
|
3517
|
+
|
|
3518
|
+
const takeWalletUsd = routableDrawAmount(walletUsd, need, eps);
|
|
3519
|
+
dWalletUsd -= takeWalletUsd;
|
|
3520
|
+
need -= takeWalletUsd;
|
|
3521
|
+
|
|
3522
|
+
const takeVaUsd = routableDrawAmount(vaUsd, need, eps);
|
|
3523
|
+
dVaUsd -= takeVaUsd;
|
|
3524
|
+
need -= takeVaUsd;
|
|
3525
|
+
|
|
3526
|
+
finalVaUsd -= takeVaUsd; // remove, bcz its sent to extended and wont be available for vesu multiply
|
|
3527
|
+
finalVaUsd += walletUsd - takeWalletUsd; // add remaining wallet usd to va
|
|
3528
|
+
// dWallet
|
|
3529
|
+
|
|
3530
|
+
const takeVesuBorrowCapacity = routableDrawAmount(vesuBorrowCapacity, need, eps);
|
|
3531
|
+
dVesuBorrowCapacity -= takeVesuBorrowCapacity;
|
|
3532
|
+
need -= takeVesuBorrowCapacity;
|
|
3533
|
+
|
|
3534
|
+
finalVaUsd += vesuBorrowCapacity - takeVesuBorrowCapacity; // add remaining vesu borrow capacity to va
|
|
3535
|
+
|
|
3536
|
+
// Received into eco1 → distribute proportionally into E1/E2
|
|
3537
|
+
const received = delta - need;
|
|
3538
|
+
const eco1Sum = extAvlWithdraw + extUpnl;
|
|
3539
|
+
finalExtended += received;
|
|
3540
|
+
|
|
3541
|
+
if (eco1Sum >= 0) {
|
|
3542
|
+
// any received amount is always given to extended avl withdaw only. upnl wont change
|
|
3543
|
+
dExtAvlWithdraw += received;
|
|
3544
|
+
} else {
|
|
3545
|
+
// Negative Extended "liquid" (withdraw + uPnL): cover uPnL deficit first, then avl withdraw.
|
|
3546
|
+
const hole = -eco1Sum;
|
|
3547
|
+
const fillUpnl = Math.min(received, hole);
|
|
3548
|
+
dExtUpnl += fillUpnl;
|
|
3549
|
+
dExtAvlWithdraw += received - fillUpnl;
|
|
3550
|
+
finalExtended -= fillUpnl;
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
if (need > CASE_THRESHOLD_USD) {
|
|
3554
|
+
throw new Error(`${this._tag}: Insufficient funds to cover margin needs`);
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
// --- Case 2: eco2 needs MORE funds (push from eco1)
|
|
3559
|
+
else if (delta < 0) {
|
|
3560
|
+
let need = -delta;
|
|
3561
|
+
|
|
3562
|
+
const takeExtAvlWithdraw = Math.min(Math.max(extAvlWithdraw, 0), need);
|
|
3563
|
+
dExtAvlWithdraw -= takeExtAvlWithdraw;
|
|
3564
|
+
|
|
3565
|
+
const takeExtUpnl = Math.min(Math.max(extUpnl, 0), need);
|
|
3566
|
+
dExtUpnl -= takeExtUpnl;
|
|
3567
|
+
|
|
3568
|
+
const netDrawableAmount = takeExtAvlWithdraw + takeExtUpnl;
|
|
3569
|
+
if (netDrawableAmount > CASE_THRESHOLD_USD) {
|
|
3570
|
+
need -= netDrawableAmount;
|
|
3571
|
+
finalExtended -= netDrawableAmount;
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
const sent = -delta - need;
|
|
3575
|
+
|
|
3576
|
+
// Distribute into eco2 proportionally (optional design choice)
|
|
3577
|
+
const eco2Sum = vaUsd + walletUsd + vesuBorrowCapacity;
|
|
3578
|
+
|
|
3579
|
+
const netWalletUsd = walletUsd < CASE_THRESHOLD_USD ? 0 : walletUsd;
|
|
3580
|
+
finalVaUsd += sent + netWalletUsd;
|
|
3581
|
+
|
|
3582
|
+
if (eco2Sum >= 0) {
|
|
3583
|
+
// all amount is sent to wallet only
|
|
3584
|
+
dWalletUsd += sent;
|
|
3585
|
+
} else {
|
|
3586
|
+
// dont think it can be negative
|
|
3587
|
+
throw new Error(`${this._tag}: Unexpected case`);
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
if (need > CASE_THRESHOLD_USD) {
|
|
3591
|
+
throw new Error(`${this._tag}: Insufficient funds to cover margin needs`);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
return { dExtAvlWithdraw, dExtUpnl, dVaUsd, dWalletUsd, dVesuBorrowCapacity, finalVaUsd, finalExtended, isExtendedToVesu: delta < 0 };
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
private _scaleVesuPoolDeltasByFactor(deltas: VesuPoolDelta[], scale: number): VesuPoolDelta[] {
|
|
3599
|
+
if (scale >= 1 - 1e-15) return deltas;
|
|
3600
|
+
return deltas.map((d) => ({
|
|
3601
|
+
...d,
|
|
3602
|
+
collateralDelta: new Web3Number(
|
|
3603
|
+
(d.collateralDelta.toNumber() * scale).toFixed(COLLATERAL_PRECISION),
|
|
3604
|
+
d.collateralToken.decimals,
|
|
3605
|
+
),
|
|
3606
|
+
debtDelta: new Web3Number(
|
|
3607
|
+
(d.debtDelta.toNumber() * scale).toFixed(USDC_TOKEN_DECIMALS),
|
|
3608
|
+
USDC_TOKEN_DECIMALS,
|
|
3609
|
+
),
|
|
3610
|
+
}));
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
/**
|
|
3614
|
+
* 3. New Deposits / Excess Funds
|
|
3615
|
+
*
|
|
3616
|
+
* Computes how much exposure to create on Vesu and Extended, then
|
|
3617
|
+
* distributes available funds using shortest-path priority chains:
|
|
3618
|
+
* Extended margin: Wallet → VA → Vesu borrow
|
|
3619
|
+
* Vesu margin: VA → Wallet → Extended
|
|
3620
|
+
*/
|
|
3621
|
+
/**
|
|
3622
|
+
* 3. New Deposits / Excess Funds
|
|
3623
|
+
*
|
|
3624
|
+
* Computes allocation split between Vesu and Extended, then sources
|
|
3625
|
+
* funds and creates lever-increase routes.
|
|
3626
|
+
*
|
|
3627
|
+
* Order: {@link _rebalanceFunds} first → project VA / Extended liquid after the same funding
|
|
3628
|
+
* routes (wallet→VA, borrow→VA, VA→Extended, Extended→wallet→VA) → ideal Vesu/Extended deltas
|
|
3629
|
+
* from distributable split → cap common BTC by min(Vesu fundable, Extended fundable) → scale
|
|
3630
|
+
* Vesu deltas and recompute Extended deltas so both sides stay matched.
|
|
3631
|
+
*
|
|
3632
|
+
* Fund flow (single pass — avoid VA→Extended then Extended→wallet round-trips):
|
|
3633
|
+
* 1) Treat Vesu borrow headroom that the multiply route will consume as covering
|
|
3634
|
+
* part of the Vesu USDC need (no standalone VESU_BORROW for that slice). Cap
|
|
3635
|
+
* standalone VESU_BORROW→VA→Extended by remaining headroom.
|
|
3636
|
+
* 2) Cover Extended deposit delta: REALISE_PNL first, then withdrawal→avail-trade,
|
|
3637
|
+
* then wallet→Extended, then VA→Extended only up to VA surplus above the USDC
|
|
3638
|
+
* that must remain for Vesu (after step 1), then borrow→VA→Extended.
|
|
3639
|
+
* 3) Cover Vesu VA shortfall: wallet→VA, Extended withdrawal→wallet→VA, REALISE_PNL,
|
|
3640
|
+
* then combined Extended→wallet→VA for the remainder.
|
|
3641
|
+
* 4) RETURN_TO_WAIT when needed; then AVNU + VESU_MULTIPLY + EXTENDED_INCREASE.
|
|
3642
|
+
*/
|
|
3643
|
+
/**
|
|
3644
|
+
* @param skipAvnuDepositSwap Omit AVNU before Vesu multiply when LTV cases already ran this cycle
|
|
3645
|
+
* (matrix tests expect deposit routes without that step).
|
|
3646
|
+
*/
|
|
3647
|
+
private _classifyDeposits(
|
|
3648
|
+
withdrawAmount: Web3Number,
|
|
3649
|
+
skipAvnuDepositSwap = false,
|
|
3650
|
+
): SolveCaseEntry[] {
|
|
3651
|
+
if (withdrawAmount.toNumber() > CASE_THRESHOLD_USD) return [];
|
|
3652
|
+
|
|
3653
|
+
const distributableAmount = this._computeDistributableAmount(
|
|
3654
|
+
this._budget.vesuDebtDeltas, withdrawAmount,
|
|
3655
|
+
);
|
|
3656
|
+
if (distributableAmount.toNumber() <= CASE_THRESHOLD_USD) return [];
|
|
3657
|
+
|
|
3658
|
+
const vesuLeverage = calculateVesuLeverage();
|
|
3659
|
+
const extendedLeverage = calculateExtendedLevergae();
|
|
3660
|
+
|
|
3661
|
+
const { dExtAvlWithdraw, dExtUpnl, dVaUsd, dWalletUsd, dVesuBorrowCapacity, isExtendedToVesu, finalVaUsd, finalExtended } = this._rebalanceFunds({
|
|
3662
|
+
extAvlWithdraw: this._budget.extAvailWithdraw,
|
|
3663
|
+
extUpnl: this._budget.extAvailUpnl,
|
|
3664
|
+
vaUsd: this._budget.vaUsd,
|
|
3665
|
+
walletUsd: this._budget.walletUsd,
|
|
3666
|
+
vesuBorrowCapacity: this._budget.vesuBorrowCapacity,
|
|
3667
|
+
vesuLeverage,
|
|
3668
|
+
extendedLeverage,
|
|
3669
|
+
});
|
|
3670
|
+
logger.info(`${this._tag}::_classifyDeposits dExtAvlWithdraw=${dExtAvlWithdraw}, dExtUpnl=${dExtUpnl}, dVaUsd=${dVaUsd}, dWalletUsd=${dWalletUsd}, dVesuBorrowCapacity=${dVesuBorrowCapacity}, isExtendedToVesu=${isExtendedToVesu}, finalVaUsd=${finalVaUsd}`);
|
|
3671
|
+
|
|
3672
|
+
// const { vesuAllocationUsd } = this._computeAllocationSplit(distributableAmount);
|
|
3673
|
+
|
|
3674
|
+
let vesuDeltas = this._computePerPoolCollateralDeltas(new Web3Number(finalVaUsd.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS));
|
|
3675
|
+
const collateralPrice = this._budget.vesuPools[0]?.collateralPrice ?? 0;
|
|
3676
|
+
const collateralDecimals = this._budget.vesuPools[0]?.collateralToken.decimals ?? 0;
|
|
3677
|
+
let _extendedPositionDelta = new Web3Number((finalExtended * extendedLeverage / collateralPrice).toFixed(USDC_TOKEN_DECIMALS), collateralDecimals).toFixedRoundDown(COLLATERAL_PRECISION);
|
|
3678
|
+
logger.info(`${this._tag}::_classifyDeposits extendedPositionDelta=${_extendedPositionDelta}`);
|
|
3679
|
+
logger.info(`${this._tag}::_classifyDeposits vesuDeltas=${JSON.stringify(vesuDeltas)}`);
|
|
3680
|
+
assert(vesuDeltas.length == 1, 'vesuDeltas should have only one delta');
|
|
3681
|
+
const minPositionDelta = Math.min(vesuDeltas[0].collateralDelta.toNumber(), Number(_extendedPositionDelta));
|
|
3682
|
+
logger.info(`${this._tag}::_classifyDeposits minPositionDelta=${minPositionDelta}`);
|
|
3683
|
+
vesuDeltas[0].collateralDelta = new Web3Number(minPositionDelta.toFixed(COLLATERAL_PRECISION), vesuDeltas[0].collateralDelta.decimals);
|
|
3684
|
+
const extendedPositionDeltas: ExtendedPositionDelta[] = [{
|
|
3685
|
+
instrument: this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD',
|
|
3686
|
+
delta: new Web3Number((minPositionDelta).toFixed(COLLATERAL_PRECISION), collateralDecimals),
|
|
3687
|
+
}];
|
|
3688
|
+
const vesuDepositAmount = this._computeVesuDepositAmount(vesuDeltas);
|
|
3689
|
+
logger.info(`${this._tag}::_classifyDeposits vesuDepositAmount=${vesuDepositAmount}`);
|
|
3690
|
+
|
|
3691
|
+
const routes: ExecutionRoute[] = [];
|
|
3692
|
+
|
|
3693
|
+
if (isExtendedToVesu) {
|
|
3694
|
+
// negative means to spend
|
|
3695
|
+
if (dExtUpnl < 0) {
|
|
3696
|
+
this._getUpnlRoute(Math.abs(dExtUpnl), routes);
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
if (dExtUpnl < 0 || dExtAvlWithdraw < 0) {
|
|
3700
|
+
const netAmount = (dExtAvlWithdraw < 0 ? Math.abs(dExtAvlWithdraw) : 0) + (dExtUpnl < 0 ? Math.abs(dExtUpnl) : 0);
|
|
3701
|
+
const walletUsd = this._budget.walletUsd; // its important to put it here, to ensure below route's impact on walletUsd doesnt double count later
|
|
3702
|
+
this._getExtendedToWalletRoute(netAmount, routes);
|
|
3703
|
+
this._getWALLETToVARoute(netAmount + walletUsd, routes); // add wallet usd to send entire amount to va
|
|
3704
|
+
}
|
|
3705
|
+
} else {
|
|
3706
|
+
let netDVaUsd = dVaUsd;
|
|
3707
|
+
if (dWalletUsd < 0) {
|
|
3708
|
+
// always send entire amount to va (no use it being in wallet)
|
|
3709
|
+
this._getWalletToVARoute(this._budget.walletUsd, routes);
|
|
3710
|
+
netDVaUsd += dWalletUsd;
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
if (dVesuBorrowCapacity < 0) {
|
|
3714
|
+
this._buildVesuBorrowRoutes(Math.abs(dVesuBorrowCapacity), routes);
|
|
3715
|
+
netDVaUsd += dVesuBorrowCapacity;
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
if (netDVaUsd < 0) {
|
|
3719
|
+
this._getVAToEXTENDEDRoute(Math.abs(netDVaUsd), routes, true); // true means, add wait route always
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
// ── Vesu lever increase ─────────────────────────────────────────────
|
|
3724
|
+
for (const vesuDelta of vesuDeltas) {
|
|
3725
|
+
if (
|
|
3726
|
+
!skipAvnuDepositSwap &&
|
|
3727
|
+
vesuDepositAmount.toNumber() > CASE_THRESHOLD_USD
|
|
3728
|
+
) {
|
|
3729
|
+
// routes.push({
|
|
3730
|
+
// type: RouteType.AVNU_DEPOSIT_SWAP,
|
|
3731
|
+
// priority: routes.length,
|
|
3732
|
+
// fromToken: vesuDelta.collateralToken.symbol,
|
|
3733
|
+
// fromAmount: vesuDepositAmount,
|
|
3734
|
+
// toToken: vesuDelta.debtToken.symbol,
|
|
3735
|
+
// });
|
|
3736
|
+
}
|
|
3737
|
+
if (vesuDelta.collateralDelta.toNumber() > 0) {
|
|
3738
|
+
// removes borrowing capacity after excluding delta capacity that is being briddged out
|
|
3739
|
+
// this is bcz, since bnorrow capacity exists, no need for external margin amount
|
|
3740
|
+
const availableBorrowCapacity = this._budget.vesuBorrowCapacity;
|
|
3741
|
+
const externalDepositAmount = vesuDepositAmount.minus(availableBorrowCapacity);
|
|
3742
|
+
const swappedAmount = new Web3Number((externalDepositAmount.toNumber() * vesuDelta.debtPrice / (vesuDelta.collateralPrice ?? 0)).toFixed(6), vesuDelta.collateralToken.decimals);
|
|
3743
|
+
routes.push({
|
|
3744
|
+
type: RouteType.VESU_MULTIPLY_INCREASE_LEVER,
|
|
3745
|
+
priority: routes.length,
|
|
3746
|
+
collateralToken: vesuDelta.collateralToken,
|
|
3747
|
+
debtToken: vesuDelta.debtToken,
|
|
3748
|
+
marginAmount: swappedAmount, // should be the swapped amount as per vesu multiply adapter
|
|
3749
|
+
swappedCollateralAmount: vesuDelta.collateralDelta.minus(swappedAmount),
|
|
3750
|
+
debtAmount: vesuDelta.debtDelta.plus(availableBorrowCapacity),
|
|
3751
|
+
poolId: vesuDelta.poolId,
|
|
3752
|
+
} as VesuMultiplyRoute);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
// ── Extended lever increase ────────────────────────────────────────
|
|
3757
|
+
for (const epDelta of extendedPositionDeltas) {
|
|
3758
|
+
if (epDelta.delta.toNumber() > 0) {
|
|
3759
|
+
routes.push({
|
|
3760
|
+
type: RouteType.EXTENDED_INCREASE_LEVER,
|
|
3761
|
+
priority: routes.length,
|
|
3762
|
+
instrument: epDelta.instrument,
|
|
3763
|
+
amount: epDelta.delta,
|
|
3764
|
+
} as ExtendedLeverRoute);
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
if (routes.length === 0) return [];
|
|
3769
|
+
routes.forEach((r, i) => { r.priority = i; });
|
|
3770
|
+
return [{
|
|
3771
|
+
case: CASE_DEFINITIONS[CaseId.DEPOSIT_FRESH_VAULT],
|
|
3772
|
+
additionalArgs: {
|
|
3773
|
+
amount: safeUsdcWeb3Number(distributableAmount.toNumber()),
|
|
3774
|
+
},
|
|
3775
|
+
routes,
|
|
3776
|
+
}];
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
/** 4. Exposure Imbalance */
|
|
3780
|
+
// private _classifyImbalance(
|
|
3781
|
+
// vesuDeltas: VesuPoolDelta[],
|
|
3782
|
+
// ): SolveCaseEntry[] {
|
|
3783
|
+
// const extBtc = this._totalExtendedExposure().toNumber();
|
|
3784
|
+
// const vesuBtc = this._totalVesuCollateral().toNumber();
|
|
3785
|
+
// const maxBtc = Math.max(extBtc, vesuBtc);
|
|
3786
|
+
// if (maxBtc <= 0) return [];
|
|
3787
|
+
// const imb = extBtc - vesuBtc;
|
|
3788
|
+
// if (Math.abs(imb) / maxBtc <= IMBALANCE_THRESHOLD_FRACTION) return [];
|
|
3789
|
+
// const exposureDiffBtc = Math.abs(imb);
|
|
3790
|
+
// const rem = this._budget.totalUnused;
|
|
3791
|
+
|
|
3792
|
+
// if (imb > 0) {
|
|
3793
|
+
// // Extended excess short → need more Vesu collateral
|
|
3794
|
+
// if (rem > CASE_THRESHOLD_USD) {
|
|
3795
|
+
// return [{
|
|
3796
|
+
// case: CASE_DEFINITIONS[CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS],
|
|
3797
|
+
// additionalArgs: {},
|
|
3798
|
+
// routes: this._buildImbalanceExtExcessShortHasFundsRoutes(rem, exposureDiffBtc, vesuDeltas),
|
|
3799
|
+
// }];
|
|
3800
|
+
// }
|
|
3801
|
+
// return [{
|
|
3802
|
+
// case: CASE_DEFINITIONS[CaseId.IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS],
|
|
3803
|
+
// additionalArgs: {},
|
|
3804
|
+
// routes: this._buildImbalanceExtExcessShortNoFundsRoutes(exposureDiffBtc),
|
|
3805
|
+
// }];
|
|
3806
|
+
// }
|
|
3807
|
+
|
|
3808
|
+
// // Vesu excess long → need more Extended exposure
|
|
3809
|
+
// if (rem > CASE_THRESHOLD_USD) {
|
|
3810
|
+
// return [{
|
|
3811
|
+
// case: CASE_DEFINITIONS[CaseId.IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS],
|
|
3812
|
+
// additionalArgs: {},
|
|
3813
|
+
// routes: this._buildImbalanceVesuExcessLongHasFundsRoutes(rem, exposureDiffBtc, vesuDeltas),
|
|
3814
|
+
// }];
|
|
3815
|
+
// }
|
|
3816
|
+
// return [{
|
|
3817
|
+
// case: CASE_DEFINITIONS[CaseId.IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS],
|
|
3818
|
+
// additionalArgs: {},
|
|
3819
|
+
// routes: this._buildImbalanceVesuExcessLongNoFundsRoutes(vesuDeltas),
|
|
3820
|
+
// }];
|
|
3821
|
+
// }
|
|
3822
|
+
|
|
3823
|
+
// ── Imbalance route builders ──────────────────────────────────────────
|
|
3824
|
+
|
|
3825
|
+
/**
|
|
3826
|
+
* IMBALANCE_EXTENDED_EXCESS_SHORT_HAS_FUNDS:
|
|
3827
|
+
* Extended has too much short exposure vs Vesu. Use VA/Wallet funds to
|
|
3828
|
+
* add Vesu collateral and reduce Extended exposure.
|
|
3829
|
+
*
|
|
3830
|
+
* The new Vesu collateral covers part of the imbalance; the remaining
|
|
3831
|
+
* gap is closed by reducing Extended short exposure.
|
|
3832
|
+
*
|
|
3833
|
+
* Routes: [AVNU_DEPOSIT_SWAP, VESU_MULTIPLY_INCREASE_LEVER, EXTENDED_DECREASE_LEVER]
|
|
3834
|
+
*/
|
|
3835
|
+
// private _buildImbalanceExtExcessShortHasFundsRoutes(
|
|
3836
|
+
// fundsUsd: number, exposureDiffBtc: number, vesuDeltas: VesuPoolDelta[],
|
|
3837
|
+
// ): ExecutionRoute[] {
|
|
3838
|
+
// const routes: ExecutionRoute[] = [];
|
|
3839
|
+
// const collSym = this._config.collateralToken.symbol;
|
|
3840
|
+
// const debtSym = this._config.assetToken.symbol;
|
|
3841
|
+
// const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
|
|
3842
|
+
|
|
3843
|
+
// // Swap USDC → BTC for Vesu collateral
|
|
3844
|
+
// routes.push({
|
|
3845
|
+
// type: RouteType.AVNU_DEPOSIT_SWAP,
|
|
3846
|
+
// fromToken: debtSym,
|
|
3847
|
+
// fromAmount: safeUsdcWeb3Number(fundsUsd),
|
|
3848
|
+
// toToken: collSym,
|
|
3849
|
+
// priority: routes.length,
|
|
3850
|
+
// } as SwapRoute);
|
|
3851
|
+
|
|
3852
|
+
// // Increase Vesu lever (deposit collateral + borrow)
|
|
3853
|
+
// routes.push(...this._buildVesuIncreaseLeverRoutes(vesuDeltas));
|
|
3854
|
+
|
|
3855
|
+
// // New Vesu collateral covers part of the imbalance
|
|
3856
|
+
// const newVesuCollBtc = vesuDeltas
|
|
3857
|
+
// .filter((d) => d.collateralDelta.greaterThan(0))
|
|
3858
|
+
// .reduce((sum, d) => sum + d.collateralDelta.toNumber(), 0);
|
|
3859
|
+
|
|
3860
|
+
// // Remaining imbalance that needs to be closed on Extended
|
|
3861
|
+
// const extDecreaseExposureBtc = Math.max(0, exposureDiffBtc - newVesuCollBtc);
|
|
3862
|
+
|
|
3863
|
+
// if (extDecreaseExposureBtc > 0) {
|
|
3864
|
+
// const decDelta = new Web3Number(extDecreaseExposureBtc.toFixed(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS);
|
|
3865
|
+
// routes.push({
|
|
3866
|
+
// type: RouteType.EXTENDED_DECREASE_LEVER,
|
|
3867
|
+
// amount: decDelta,
|
|
3868
|
+
// instrument,
|
|
3869
|
+
// priority: routes.length,
|
|
3870
|
+
// });
|
|
3871
|
+
// this._budget.applyExtendedExposureDelta(instrument, new Web3Number(decDelta.negated().toFixed(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS));
|
|
3872
|
+
|
|
3873
|
+
// }
|
|
3874
|
+
|
|
3875
|
+
// routes.forEach((r, i) => { r.priority = i; });
|
|
3876
|
+
// return routes;
|
|
3877
|
+
// }
|
|
3878
|
+
|
|
3879
|
+
/**
|
|
3880
|
+
* IMBALANCE_EXTENDED_EXCESS_SHORT_NO_FUNDS:
|
|
3881
|
+
* No available funds to add Vesu collateral → reduce Extended exposure
|
|
3882
|
+
* by the full imbalance amount.
|
|
3883
|
+
* Routes: [EXTENDED_DECREASE_LEVER]
|
|
3884
|
+
*/
|
|
3885
|
+
private _buildImbalanceExtExcessShortNoFundsRoutes(exposureDiffBtc: number): ExecutionRoute[] {
|
|
3886
|
+
const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
|
|
3887
|
+
// hardcoding 8 decimals is ok here
|
|
3888
|
+
const decDelta = new Web3Number(Web3Number.fromNumber(exposureDiffBtc, 8).toFixedRoundDown(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS);
|
|
3889
|
+
const collPx = this._budget.vesuPools[0]?.collateralPrice ?? 1;
|
|
3890
|
+
this._budget.applyExtendedExposureDelta(
|
|
3891
|
+
instrument,
|
|
3892
|
+
new Web3Number(Web3Number.fromNumber(decDelta.negated().toNumber(), 8).toFixedRoundDown(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS),
|
|
3893
|
+
collPx,
|
|
3894
|
+
);
|
|
3895
|
+
return [{
|
|
3896
|
+
type: RouteType.EXTENDED_DECREASE_LEVER,
|
|
3897
|
+
amount: decDelta,
|
|
3898
|
+
instrument,
|
|
3899
|
+
priority: 0,
|
|
3900
|
+
}];
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
/**
|
|
3904
|
+
* IMBALANCE_VESU_EXCESS_LONG_HAS_FUNDS:
|
|
3905
|
+
* Vesu has too much long exposure vs Extended. Deposit funds on Extended
|
|
3906
|
+
* to increase short exposure, and decrease Vesu lever for the remainder.
|
|
3907
|
+
*
|
|
3908
|
+
* Routes: [WALLET_TO_EXTENDED / VA_TO_EXTENDED, EXTENDED_INCREASE_LEVER,
|
|
3909
|
+
* VESU_MULTIPLY_DECREASE_LEVER]
|
|
3910
|
+
*/
|
|
3911
|
+
// private _buildImbalanceVesuExcessLongHasFundsRoutes(
|
|
3912
|
+
// fundsUsd: number, exposureDiffBtc: number, vesuDeltas: VesuPoolDelta[],
|
|
3913
|
+
// ): ExecutionRoute[] {
|
|
3914
|
+
// const routes: ExecutionRoute[] = [];
|
|
3915
|
+
// const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
|
|
3916
|
+
|
|
3917
|
+
// // ExecutionRoute funds to Extended (wallet-first, then VA)
|
|
3918
|
+
// routes.push(...this._buildExtendedDepositRoutes(fundsUsd));
|
|
3919
|
+
|
|
3920
|
+
// // Compute how much new Extended exposure the deposited funds create
|
|
3921
|
+
// const extLeverage = calculateExtendedLevergae();
|
|
3922
|
+
// const newExtExposureUsd = fundsUsd * extLeverage;
|
|
3923
|
+
// const avgCollPrice = this._budget.vesuPoolStates[0]?.collateralPrice ?? 1;
|
|
3924
|
+
// const newExtExposureBtc = avgCollPrice > 0 ? newExtExposureUsd / avgCollPrice : 0;
|
|
3925
|
+
|
|
3926
|
+
// // Increase Extended lever by the new exposure (capped to imbalance)
|
|
3927
|
+
// const extIncreaseBtc = Math.min(newExtExposureBtc, exposureDiffBtc);
|
|
3928
|
+
// if (extIncreaseBtc > 0) {
|
|
3929
|
+
// const incDelta = new Web3Number(extIncreaseBtc.toFixed(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS);
|
|
3930
|
+
// routes.push({
|
|
3931
|
+
// type: RouteType.EXTENDED_INCREASE_LEVER,
|
|
3932
|
+
// amount: incDelta,
|
|
3933
|
+
// instrument,
|
|
3934
|
+
// priority: routes.length,
|
|
3935
|
+
// });
|
|
3936
|
+
// this._budget.applyExtendedExposureDelta(instrument, incDelta);
|
|
3937
|
+
// }
|
|
3938
|
+
|
|
3939
|
+
// // Remaining imbalance closed by decreasing Vesu lever
|
|
3940
|
+
// const vesuDecreaseBtc = Math.max(0, exposureDiffBtc - extIncreaseBtc);
|
|
3941
|
+
// if (vesuDecreaseBtc > 0) {
|
|
3942
|
+
// routes.push(...this._buildVesuDecreaseLeverRoutes(vesuDeltas));
|
|
3943
|
+
// }
|
|
3944
|
+
|
|
3945
|
+
// routes.forEach((r, i) => { r.priority = i; });
|
|
3946
|
+
// return routes;
|
|
3947
|
+
// }
|
|
3948
|
+
|
|
3949
|
+
/**
|
|
3950
|
+
* IMBALANCE_VESU_EXCESS_LONG_NO_FUNDS:
|
|
3951
|
+
* No funds to increase Extended → decrease Vesu lever by the full imbalance.
|
|
3952
|
+
* Routes: [VESU_MULTIPLY_DECREASE_LEVER]
|
|
3953
|
+
*/
|
|
3954
|
+
// private _buildImbalanceVesuExcessLongNoFundsRoutes(vesuDeltas: VesuPoolDelta[]): ExecutionRoute[] {
|
|
3955
|
+
// const routes: ExecutionRoute[] = this._buildVesuDecreaseLeverRoutes(vesuDeltas);
|
|
3956
|
+
// routes.forEach((r, i) => { r.priority = i; });
|
|
3957
|
+
// return routes;
|
|
3958
|
+
// }
|
|
3959
|
+
|
|
3960
|
+
// ── Main classifier (orchestrator) ─────────────────────────────────────
|
|
3961
|
+
|
|
3962
|
+
/**
|
|
3963
|
+
* Classifies the current state into actionable cases. Each case carries
|
|
3964
|
+
* its own execution routes with amounts and state info.
|
|
3965
|
+
*/
|
|
3966
|
+
private _classifyCases(withdrawAmount: Web3Number): SolveCaseEntry[] {
|
|
3967
|
+
this._budget.initBudget();
|
|
3968
|
+
|
|
3969
|
+
// withdrawal uses full raw balances (no safety buffer)
|
|
3970
|
+
const withdrawalCases = this._classifyWithdrawal(withdrawAmount);
|
|
3971
|
+
|
|
3972
|
+
// apply safety buffer to remaining balances for subsequent classifiers
|
|
3973
|
+
this._budget.applyBuffer(this._config.limitBalanceBufferFactor);
|
|
3974
|
+
|
|
3975
|
+
// 2. LTV Rebalance — unified Vesu high LTV + Extended low margin
|
|
3976
|
+
const ltvCases = this._classifyLTV();
|
|
3977
|
+
|
|
3978
|
+
// 3. New Deposits — allocate remaining budget to lever operations
|
|
3979
|
+
const depositCases = this._classifyDeposits(
|
|
3980
|
+
withdrawAmount,
|
|
3981
|
+
ltvCases.length > 0,
|
|
3982
|
+
);
|
|
3983
|
+
|
|
3984
|
+
return [
|
|
3985
|
+
...withdrawalCases,
|
|
3986
|
+
...ltvCases,
|
|
3987
|
+
...depositCases,
|
|
3988
|
+
];
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
|
|
3992
|
+
|
|
3993
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3994
|
+
// Private — aggregation helpers
|
|
3995
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3996
|
+
|
|
3997
|
+
private _totalVesuCollateral(): Web3Number {
|
|
3998
|
+
return this._budget.vesuPools.reduce(
|
|
3999
|
+
(acc, pool) =>
|
|
4000
|
+
acc.plus(
|
|
4001
|
+
pool.collateralAmount,
|
|
4002
|
+
),
|
|
4003
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
4004
|
+
);
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
private _totalVesuCollateralUsd(): Web3Number {
|
|
4008
|
+
return this._budget.vesuPools.reduce(
|
|
4009
|
+
(acc, pool) =>
|
|
4010
|
+
acc.plus(
|
|
4011
|
+
pool.collateralAmount.multipliedBy(pool.collateralPrice),
|
|
4012
|
+
),
|
|
4013
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
4014
|
+
);
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
private _totalExtendedExposure(): Web3Number {
|
|
4018
|
+
return this._budget.extendedPositionsView.reduce(
|
|
4019
|
+
(acc, position) => acc.plus(position.size),
|
|
4020
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
4021
|
+
);
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
private _totalExtendedExposureUsd(): Web3Number {
|
|
4025
|
+
return this._budget.extendedPositionsView.reduce(
|
|
4026
|
+
(acc, position) => acc.plus(position.valueUsd),
|
|
4027
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
4028
|
+
);
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4032
|
+
// Private — logging
|
|
4033
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4034
|
+
|
|
4035
|
+
private _logSolveResult(result: SolveResult): void {
|
|
4036
|
+
// Log detected cases
|
|
4037
|
+
logger.info(
|
|
4038
|
+
`${this._tag}::solve detected ${result.cases.length} case(s): ` +
|
|
4039
|
+
result.cases
|
|
4040
|
+
.map((c) => `[${c.case.category}] ${c.case.id}`)
|
|
4041
|
+
.join(', '),
|
|
4042
|
+
);
|
|
4043
|
+
for (const entry of result.cases) {
|
|
4044
|
+
logger.info(
|
|
4045
|
+
`${this._tag}::solve case "${entry.case.title}" — ` +
|
|
4046
|
+
`steps: ${entry.case.steps.length}`,
|
|
4047
|
+
);
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
logger.info(
|
|
4051
|
+
`${this._tag}::solve result — ` +
|
|
4052
|
+
`extendedDeposit: ${result.extendedDeposit.toNumber()}, ` +
|
|
4053
|
+
`vesuAllocUsd: ${result.vesuAllocationUsd.toNumber()}, ` +
|
|
4054
|
+
`extAllocUsd: ${result.extendedAllocationUsd.toNumber()}, ` +
|
|
4055
|
+
`bringLiquidity: ${result.bringLiquidityAmount.toNumber()}`,
|
|
4056
|
+
);
|
|
4057
|
+
|
|
4058
|
+
for (let i = 0; i < result.vesuDeltas.length; i++) {
|
|
4059
|
+
const delta = result.vesuDeltas[i];
|
|
4060
|
+
logger.info(
|
|
4061
|
+
`${this._tag}::solve vesu[${i}] ` +
|
|
4062
|
+
`pool=${delta.poolId.shortString()} ` +
|
|
4063
|
+
`debtDelta=${delta.debtDelta.toNumber()} ` +
|
|
4064
|
+
`collateralDelta=${delta.collateralDelta.toNumber()}`,
|
|
4065
|
+
);
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
for (let i = 0; i < result.extendedPositionDeltas.length; i++) {
|
|
4069
|
+
const delta = result.extendedPositionDeltas[i];
|
|
4070
|
+
logger.info(
|
|
4071
|
+
`${this._tag}::solve extended[${i}] ` +
|
|
4072
|
+
`instrument=${delta.instrument} ` +
|
|
4073
|
+
`delta=${delta.delta.toNumber()}`,
|
|
4074
|
+
);
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
for (const entry of result.cases) {
|
|
4078
|
+
const caseRoutes = entry.routes;
|
|
4079
|
+
logger.info(
|
|
4080
|
+
`${this._tag}::solve case "${entry.case.id}" routes (${caseRoutes.length}): ` +
|
|
4081
|
+
caseRoutes
|
|
4082
|
+
.map((r) => `[${r.priority}] ${r.type} ${routeSummary(r)}`)
|
|
4083
|
+
.join(' → '),
|
|
4084
|
+
);
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
}
|