@strkfarm/sdk 2.0.0-dev.28 → 2.0.0-dev.29
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/index.browser.global.js +1938 -771
- package/dist/index.browser.mjs +1960 -791
- package/dist/index.d.ts +343 -124
- package/dist/index.js +1971 -794
- package/dist/index.mjs +1963 -791
- package/package.json +1 -1
- package/src/dataTypes/bignumber.browser.ts +6 -1
- package/src/dataTypes/bignumber.node.ts +5 -1
- package/src/interfaces/common.tsx +8 -1
- package/src/strategies/universal-adapters/baseAdapter.ts +1 -1
- package/src/strategies/universal-adapters/extended-adapter.ts +7 -7
- package/src/strategies/universal-adapters/index.ts +2 -1
- package/src/strategies/universal-adapters/svk-troves-adapter.ts +364 -0
- package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +88 -34
- package/src/strategies/vesu-extended-strategy/services/executionService.ts +36 -55
- package/src/strategies/vesu-extended-strategy/services/extended-vesu-state-manager.ts +1553 -538
- package/src/strategies/vesu-extended-strategy/services/ltv-imbalance-rebalance-math.ts +730 -0
- package/src/strategies/vesu-extended-strategy/services/operationService.ts +4 -3
- package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +27 -83
- package/src/utils/index.ts +1 -0
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
calculateExtendedLevergae,
|
|
13
13
|
calculateVesuLeverage,
|
|
14
14
|
} from "../utils/helper";
|
|
15
|
+
import { VesuConfig } from "../utils/config.runtime";
|
|
16
|
+
import { rebalance, type RebalanceDeltas } from "./ltv-imbalance-rebalance-math";
|
|
15
17
|
|
|
16
18
|
// ─── State types ───────────────────────────────────────────────────────────────
|
|
17
19
|
|
|
@@ -58,6 +60,26 @@ export interface ExtendedBalanceState {
|
|
|
58
60
|
pendingDeposit: Web3Number;
|
|
59
61
|
}
|
|
60
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
|
+
isExtendedToVesu: boolean;
|
|
81
|
+
};
|
|
82
|
+
|
|
61
83
|
/**
|
|
62
84
|
* Generic token balance with USD valuation.
|
|
63
85
|
*/
|
|
@@ -99,9 +121,9 @@ export interface VesuPoolDelta {
|
|
|
99
121
|
* Enumerates all possible fund-routing paths used during execution.
|
|
100
122
|
*/
|
|
101
123
|
export enum RouteType {
|
|
102
|
-
/**
|
|
124
|
+
/** Deposit USDC from operator wallet directly to Extended exchange */
|
|
103
125
|
WALLET_TO_EXTENDED = 'WALLET_TO_EXTENDED',
|
|
104
|
-
/**
|
|
126
|
+
/** USDC from vault allocator → deposit to Extended (via manager) */
|
|
105
127
|
VA_TO_EXTENDED = 'VA_TO_EXTENDED',
|
|
106
128
|
/** Withdraw from Extended exchange → operator wallet */
|
|
107
129
|
EXTENDED_TO_WALLET = 'EXTENDED_TO_WALLET',
|
|
@@ -117,7 +139,7 @@ export enum RouteType {
|
|
|
117
139
|
VESU_BORROW = 'VESU_BORROW',
|
|
118
140
|
/** Repay USDC debt to Vesu (debtDelta < 0) */
|
|
119
141
|
VESU_REPAY = 'VESU_REPAY',
|
|
120
|
-
/** Transfer USDC
|
|
142
|
+
/** Transfer USDC from operator wallet to vault allocator */
|
|
121
143
|
WALLET_TO_VA = 'WALLET_TO_VA',
|
|
122
144
|
/** Realize PnL on Extended exchange */
|
|
123
145
|
REALISE_PNL = 'REALISE_PNL',
|
|
@@ -284,11 +306,12 @@ export enum CaseCategory {
|
|
|
284
306
|
*/
|
|
285
307
|
export enum CaseId {
|
|
286
308
|
// LTV Rebalance
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
309
|
+
MANAGE_LTV = 'MANAGE_LTV',
|
|
310
|
+
/** @deprecated use MANAGE_LTV */ LTV_VESU_LOW_TO_EXTENDED = 'LTV_VESU_LOW_TO_EXTENDED',
|
|
311
|
+
/** @deprecated use MANAGE_LTV */ LTV_EXTENDED_PROFITABLE_AVAILABLE = 'LTV_EXTENDED_PROFITABLE_AVAILABLE',
|
|
312
|
+
/** @deprecated use MANAGE_LTV */ LTV_EXTENDED_PROFITABLE_REALIZE = 'LTV_EXTENDED_PROFITABLE_REALIZE',
|
|
313
|
+
/** @deprecated use MANAGE_LTV */ LTV_VESU_HIGH_USE_VA_OR_WALLET = 'LTV_VESU_HIGH_USE_VA_OR_WALLET',
|
|
314
|
+
/** @deprecated use MANAGE_LTV */ LTV_EXTENDED_HIGH_USE_VA_OR_WALLET = 'LTV_EXTENDED_HIGH_USE_VA_OR_WALLET',
|
|
292
315
|
|
|
293
316
|
// New Deposits / Excess Funds
|
|
294
317
|
DEPOSIT_FRESH_VAULT = 'DEPOSIT_FRESH_VAULT',
|
|
@@ -324,7 +347,7 @@ export interface SolveCase {
|
|
|
324
347
|
const CASE_THRESHOLD_USD = 5;
|
|
325
348
|
const CASE_MIN_BRIDING_USD = 10;
|
|
326
349
|
/** Decimal places for rounding collateral / exposure deltas in token terms (e.g. BTC) */
|
|
327
|
-
const COLLATERAL_PRECISION = 4;
|
|
350
|
+
export const COLLATERAL_PRECISION = 4;
|
|
328
351
|
|
|
329
352
|
/** Safely create a Web3Number from a float, avoiding >15 significant digit errors */
|
|
330
353
|
function safeUsdcWeb3Number(value: number): Web3Number {
|
|
@@ -346,19 +369,43 @@ export interface SolveCaseEntry {
|
|
|
346
369
|
* Used to filter the global route list into per-case route subsets.
|
|
347
370
|
*/
|
|
348
371
|
export const CASE_ROUTE_TYPES: Record<CaseId, RouteType[]> = {
|
|
349
|
-
// LTV Rebalance —
|
|
350
|
-
[CaseId.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
372
|
+
// LTV Rebalance — unified
|
|
373
|
+
[CaseId.MANAGE_LTV]: [
|
|
374
|
+
RouteType.REALISE_PNL, RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT,
|
|
375
|
+
RouteType.WALLET_TO_VA, RouteType.VESU_BORROW, RouteType.VESU_REPAY, RouteType.VA_TO_EXTENDED,
|
|
376
|
+
RouteType.VESU_MULTIPLY_DECREASE_LEVER, RouteType.VESU_MULTIPLY_INCREASE_LEVER,
|
|
377
|
+
RouteType.AVNU_DEPOSIT_SWAP,
|
|
378
|
+
RouteType.EXTENDED_DECREASE_LEVER, RouteType.EXTENDED_INCREASE_LEVER,
|
|
379
|
+
// Second-phase VA / Extended funding after lever routes (same types may repeat).
|
|
380
|
+
RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VA_TO_EXTENDED,
|
|
381
|
+
],
|
|
382
|
+
/** @deprecated */ [CaseId.LTV_VESU_HIGH_USE_VA_OR_WALLET]: [RouteType.WALLET_TO_VA, RouteType.VESU_REPAY],
|
|
383
|
+
/** @deprecated */ [CaseId.LTV_EXTENDED_PROFITABLE_AVAILABLE]: [RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VESU_REPAY],
|
|
384
|
+
/** @deprecated */ [CaseId.LTV_EXTENDED_PROFITABLE_REALIZE]: [RouteType.REALISE_PNL, RouteType.EXTENDED_TO_WALLET, RouteType.RETURN_TO_WAIT, RouteType.WALLET_TO_VA, RouteType.VESU_REPAY],
|
|
385
|
+
/** @deprecated */ [CaseId.LTV_EXTENDED_HIGH_USE_VA_OR_WALLET]: [RouteType.VA_TO_EXTENDED, RouteType.WALLET_TO_EXTENDED],
|
|
386
|
+
/** @deprecated */ [CaseId.LTV_VESU_LOW_TO_EXTENDED]: [RouteType.VESU_BORROW, RouteType.VA_TO_EXTENDED],
|
|
356
387
|
|
|
357
388
|
// New Deposits
|
|
358
389
|
// @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
|
|
359
390
|
// Sequence: fund-movement transfers first (WALLET_TO_EXTENDED, VA_TO_EXTENDED, WALLET_TO_VA, EXTENDED_TO_WALLET),
|
|
360
391
|
// then RETURN_TO_WAIT, then optional second WALLET_TO_VA + RETURN_TO_WAIT, then lever routes.
|
|
361
|
-
[CaseId.DEPOSIT_FRESH_VAULT]: [
|
|
392
|
+
[CaseId.DEPOSIT_FRESH_VAULT]: [
|
|
393
|
+
// May repeat after MANAGE_LTV (VA top-up → Extended → wait → levers).
|
|
394
|
+
RouteType.WALLET_TO_VA,
|
|
395
|
+
RouteType.VA_TO_EXTENDED,
|
|
396
|
+
RouteType.RETURN_TO_WAIT,
|
|
397
|
+
RouteType.VA_TO_EXTENDED,
|
|
398
|
+
RouteType.VESU_BORROW,
|
|
399
|
+
RouteType.WALLET_TO_EXTENDED,
|
|
400
|
+
RouteType.VA_TO_EXTENDED,
|
|
401
|
+
RouteType.WALLET_TO_VA,
|
|
402
|
+
RouteType.EXTENDED_TO_WALLET,
|
|
403
|
+
RouteType.RETURN_TO_WAIT,
|
|
404
|
+
RouteType.WALLET_TO_VA,
|
|
405
|
+
RouteType.AVNU_DEPOSIT_SWAP,
|
|
406
|
+
RouteType.VESU_MULTIPLY_INCREASE_LEVER,
|
|
407
|
+
RouteType.EXTENDED_INCREASE_LEVER,
|
|
408
|
+
],
|
|
362
409
|
[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],
|
|
363
410
|
[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],
|
|
364
411
|
[CaseId.DEPOSIT_COMBINATION]: [], // exroutes to be computed on the fly based on above sub routes and where deposit is available
|
|
@@ -393,6 +440,17 @@ const IMBALANCE_THRESHOLD_FRACTION = 0.0002; // 0.02%
|
|
|
393
440
|
* Each entry maps a CaseId to its metadata and descriptive steps.
|
|
394
441
|
*/
|
|
395
442
|
const CASE_DEFINITIONS: Record<CaseId, SolveCase> = {
|
|
443
|
+
[CaseId.MANAGE_LTV]: {
|
|
444
|
+
id: CaseId.MANAGE_LTV,
|
|
445
|
+
category: CaseCategory.LTV_REBALANCE,
|
|
446
|
+
title: 'LTV Rebalance: Unified Vesu repay + Extended margin management',
|
|
447
|
+
description: 'Manages both Vesu high-LTV repayment and Extended low-margin funding in a single pass.',
|
|
448
|
+
steps: [
|
|
449
|
+
'Compute vesu repay needed and extended margin needed',
|
|
450
|
+
'Allocate funds: VA > Wallet > ExtAvl > ExtUpnl for Vesu; Wallet > VA > Borrow for Extended',
|
|
451
|
+
'Build combined transfer and repay/margin routes',
|
|
452
|
+
],
|
|
453
|
+
},
|
|
396
454
|
[CaseId.LTV_VESU_LOW_TO_EXTENDED]: {
|
|
397
455
|
id: CaseId.LTV_VESU_LOW_TO_EXTENDED,
|
|
398
456
|
category: CaseCategory.LTV_REBALANCE,
|
|
@@ -619,9 +677,13 @@ export interface StateManagerConfig {
|
|
|
619
677
|
extendedAdapter: ExtendedAdapter;
|
|
620
678
|
vaultAllocator: ContractAddr;
|
|
621
679
|
walletAddress: string;
|
|
680
|
+
/** Strategy / vault token — idle balance in the vault allocator is denominated in this asset */
|
|
622
681
|
assetToken: TokenInfo;
|
|
623
|
-
/**
|
|
624
|
-
|
|
682
|
+
/**
|
|
683
|
+
* Native USDC (Starknet) — operator wallet stablecoin balance is always read for this token,
|
|
684
|
+
* independent of {@link assetToken} (e.g. when the strategy vault uses another asset).
|
|
685
|
+
*/
|
|
686
|
+
usdcToken: TokenInfo;
|
|
625
687
|
/** Collateral token (e.g. WBTC) for wallet balance checks */
|
|
626
688
|
collateralToken: TokenInfo;
|
|
627
689
|
limitBalanceBufferFactor: number;
|
|
@@ -672,231 +734,734 @@ export function routeSummary(r: ExecutionRoute): string {
|
|
|
672
734
|
/**
|
|
673
735
|
* Single source of truth for all mutable state during a solve() call.
|
|
674
736
|
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
* (
|
|
679
|
-
*
|
|
680
|
-
*
|
|
737
|
+
* Stores **raw** snapshots from refresh (no safety buffer applied in fields).
|
|
738
|
+
* Buffer {@link limitBalanceBufferFactor} is applied only in **getters** used
|
|
739
|
+
* during solve (caps, diagnostics). **Spend / add** methods mutate balances
|
|
740
|
+
* in **raw** USD only (no buffer on debits or credits).
|
|
741
|
+
*
|
|
742
|
+
* **Extended deposits** ({@link SolveBudget.addToExtAvailTrade}): while account equity is
|
|
743
|
+
* below required margin (Σ position value ÷ Extended leverage), incoming USDC is credited
|
|
744
|
+
* only to **balance** and **equity**. Only the excess is credited to **availableForWithdrawal**
|
|
745
|
+
* and **availableForTrade**, matching “margin first, then free collateral”.
|
|
681
746
|
*/
|
|
682
747
|
export class SolveBudget {
|
|
683
|
-
// ── Refreshed state (mutable during solve)
|
|
684
|
-
unusedBalance: TokenBalance[];
|
|
685
|
-
walletBalance: TokenBalance | null;
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
private
|
|
695
|
-
private
|
|
696
|
-
private
|
|
697
|
-
private
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
private _totalUnused: number = 0;
|
|
748
|
+
// ── Refreshed state (mutable during solve, raw on-chain / API values) ─
|
|
749
|
+
private unusedBalance: TokenBalance[];
|
|
750
|
+
private walletBalance: TokenBalance | null;
|
|
751
|
+
/** Idle {@link StateManagerConfig.assetToken} in the vault allocator */
|
|
752
|
+
private vaultAssetBalance: TokenBalance | null;
|
|
753
|
+
/**
|
|
754
|
+
* Idle {@link StateManagerConfig.usdcToken} in the vault allocator.
|
|
755
|
+
* When asset and USDC are the same token, this is null (all VA stablecoin is in {@link vaultAssetBalance} only).
|
|
756
|
+
*/
|
|
757
|
+
private vaultUsdcBalance: TokenBalance | null;
|
|
758
|
+
private extendedPositions: ExtendedPositionState[];
|
|
759
|
+
private extendedBalance: ExtendedBalanceState | null;
|
|
760
|
+
private vesuPoolStates: VesuPoolState[];
|
|
761
|
+
private vesuPerPoolDebtDeltasToBorrow: Web3Number[];
|
|
762
|
+
private shouldVesuRebalance: boolean[]; // should be same length as vesuPerPoolDebtDeltasToBorrow
|
|
763
|
+
|
|
764
|
+
readonly assetToken: TokenInfo;
|
|
765
|
+
readonly usdcToken: TokenInfo;
|
|
702
766
|
|
|
703
767
|
constructor(state: {
|
|
704
|
-
|
|
768
|
+
assetToken: TokenInfo;
|
|
769
|
+
usdcToken: TokenInfo;
|
|
705
770
|
unusedBalance: TokenBalance[];
|
|
706
771
|
walletBalance: TokenBalance | null;
|
|
707
|
-
|
|
772
|
+
vaultAssetBalance: TokenBalance | null;
|
|
773
|
+
vaultUsdcBalance: TokenBalance | null;
|
|
708
774
|
extendedPositions: ExtendedPositionState[];
|
|
709
775
|
extendedBalance: ExtendedBalanceState | null;
|
|
710
776
|
vesuPoolStates: VesuPoolState[];
|
|
711
777
|
}) {
|
|
712
|
-
|
|
713
|
-
this.
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
778
|
+
this.assetToken = state.assetToken;
|
|
779
|
+
this.usdcToken = state.usdcToken;
|
|
780
|
+
|
|
781
|
+
const cloneTb = (b: TokenBalance): TokenBalance => ({
|
|
782
|
+
token: b.token,
|
|
783
|
+
amount: new Web3Number(b.amount.toFixed(b.token.decimals), b.token.decimals),
|
|
784
|
+
usdValue: b.usdValue,
|
|
719
785
|
});
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
786
|
+
|
|
787
|
+
this.unusedBalance = state.unusedBalance.map((item) => cloneTb(item));
|
|
788
|
+
this.walletBalance = state.walletBalance ? cloneTb(state.walletBalance) : null;
|
|
789
|
+
this.vaultAssetBalance = state.vaultAssetBalance ? cloneTb(state.vaultAssetBalance) : null;
|
|
790
|
+
this.vaultUsdcBalance = state.vaultUsdcBalance ? cloneTb(state.vaultUsdcBalance) : null;
|
|
791
|
+
|
|
792
|
+
// Deep-clone mutable Extended / Vesu snapshots so route-building dry-runs (e.g. _classifyLTV)
|
|
793
|
+
// cannot mutate shared objects when callers reuse the same fixture for multiple budgets.
|
|
794
|
+
this.extendedPositions = state.extendedPositions.map((p) => ({
|
|
795
|
+
...p,
|
|
796
|
+
size: new Web3Number(p.size.toFixed(8), 8),
|
|
797
|
+
valueUsd: new Web3Number(p.valueUsd.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS),
|
|
798
|
+
}));
|
|
799
|
+
this.extendedBalance = state.extendedBalance
|
|
800
|
+
? {
|
|
801
|
+
equity: new Web3Number(
|
|
802
|
+
state.extendedBalance.equity.toFixed(USDC_TOKEN_DECIMALS),
|
|
803
|
+
USDC_TOKEN_DECIMALS,
|
|
804
|
+
),
|
|
805
|
+
availableForTrade: new Web3Number(
|
|
806
|
+
state.extendedBalance.availableForTrade.toFixed(USDC_TOKEN_DECIMALS),
|
|
807
|
+
USDC_TOKEN_DECIMALS,
|
|
808
|
+
),
|
|
809
|
+
availableForWithdrawal: new Web3Number(
|
|
810
|
+
state.extendedBalance.availableForWithdrawal.toFixed(USDC_TOKEN_DECIMALS),
|
|
811
|
+
USDC_TOKEN_DECIMALS,
|
|
812
|
+
),
|
|
813
|
+
unrealisedPnl: new Web3Number(
|
|
814
|
+
state.extendedBalance.unrealisedPnl.toFixed(USDC_TOKEN_DECIMALS),
|
|
815
|
+
USDC_TOKEN_DECIMALS,
|
|
816
|
+
),
|
|
817
|
+
balance: new Web3Number(
|
|
818
|
+
state.extendedBalance.balance.toFixed(USDC_TOKEN_DECIMALS),
|
|
819
|
+
USDC_TOKEN_DECIMALS,
|
|
820
|
+
),
|
|
821
|
+
pendingDeposit: new Web3Number(
|
|
822
|
+
state.extendedBalance.pendingDeposit.toFixed(USDC_TOKEN_DECIMALS),
|
|
823
|
+
USDC_TOKEN_DECIMALS,
|
|
824
|
+
),
|
|
825
|
+
}
|
|
826
|
+
: null;
|
|
827
|
+
this.vesuPoolStates = state.vesuPoolStates.map((p) => ({
|
|
828
|
+
...p,
|
|
829
|
+
collateralAmount: new Web3Number(
|
|
830
|
+
p.collateralAmount.toFixed(p.collateralToken.decimals),
|
|
831
|
+
p.collateralToken.decimals,
|
|
832
|
+
),
|
|
833
|
+
debtAmount: new Web3Number(
|
|
834
|
+
p.debtAmount.toFixed(p.debtToken.decimals),
|
|
835
|
+
p.debtToken.decimals,
|
|
836
|
+
),
|
|
837
|
+
}));
|
|
739
838
|
const vesuPerPoolDebtDeltasToBorrow = this._computeperPoolDebtDeltasToBorrow();
|
|
740
839
|
assert(vesuPerPoolDebtDeltasToBorrow.length === this.vesuPoolStates.length, 'vesuPerPoolDebtDeltasToBorrow length must match vesuPoolStates length');
|
|
741
840
|
this.vesuPerPoolDebtDeltasToBorrow = vesuPerPoolDebtDeltasToBorrow.map((item) => item.deltaDebt);
|
|
742
841
|
this.shouldVesuRebalance = vesuPerPoolDebtDeltasToBorrow.map((item) => item.shouldRebalance);
|
|
743
842
|
}
|
|
744
843
|
|
|
844
|
+
/** `1 - limitBalanceBufferFactor` — multiplier applied to raw notionals for “usable” USD. */
|
|
845
|
+
private _usableFraction(): number {
|
|
846
|
+
return 1;
|
|
847
|
+
}
|
|
848
|
+
|
|
745
849
|
/**
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
850
|
+
* Raw USD notional for a token row. USDC (and configured {@link usdcToken}) uses 1:1 from amount;
|
|
851
|
+
* non-stable assets (e.g. WBTC in VA) use {@link TokenBalance.usdValue} from the pricer at refresh.
|
|
852
|
+
*/
|
|
853
|
+
private _rawTokenUsd(tb: TokenBalance | null | undefined): number {
|
|
854
|
+
if (!tb) return 0;
|
|
855
|
+
if (this.usdcToken.address.eq(tb.token.address)) {
|
|
856
|
+
return Number(tb.amount.toFixed(tb.token.decimals));
|
|
857
|
+
}
|
|
858
|
+
return tb.usdValue;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** Apply safety buffer to a raw USD scalar. */
|
|
862
|
+
bufferedUsd(rawUsd: number): number {
|
|
863
|
+
return rawUsd * this._usableFraction();
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/** Convert a buffered “usable” USD amount to raw nominal USD (inverse of {@link bufferedUsd}). */
|
|
867
|
+
rawUsdFromBuffered(bufferedUsd: number): number {
|
|
868
|
+
const bf = this._usableFraction();
|
|
869
|
+
assert(bf > 0, 'SolveBudget::rawUsdFromBuffered usable fraction must be positive');
|
|
870
|
+
return bufferedUsd / bf;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** Buffered USD notional for one token balance row. */
|
|
874
|
+
bufferedTokenUsd(tb: TokenBalance | null | undefined): number {
|
|
875
|
+
return this.bufferedUsd(this._rawTokenUsd(tb));
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
logStateSummary() {
|
|
879
|
+
console.log("===== state summary =====");
|
|
880
|
+
const aggregatedData = {
|
|
881
|
+
unusedBalances: this.unusedBalance.map((b) => ({
|
|
882
|
+
token: b.token.symbol,
|
|
883
|
+
amount: b.amount.toNumber(),
|
|
884
|
+
})),
|
|
885
|
+
walletBalance: this.walletBalance
|
|
886
|
+
? {
|
|
887
|
+
token: this.walletBalance.token.symbol,
|
|
888
|
+
amount: this.walletBalance.amount.toNumber(),
|
|
889
|
+
}
|
|
890
|
+
: undefined,
|
|
891
|
+
vaultAssetBalance: this.vaultAssetBalance
|
|
892
|
+
? {
|
|
893
|
+
token: this.vaultAssetBalance.token.symbol,
|
|
894
|
+
amount: this.vaultAssetBalance.amount.toNumber(),
|
|
895
|
+
}
|
|
896
|
+
: undefined,
|
|
897
|
+
vaultUsdcBalance: this.vaultUsdcBalance
|
|
898
|
+
? {
|
|
899
|
+
token: this.vaultUsdcBalance.token.symbol,
|
|
900
|
+
amount: this.vaultUsdcBalance.amount.toNumber(),
|
|
901
|
+
}
|
|
902
|
+
: undefined,
|
|
903
|
+
vesuPoolStates: this.vesuPoolStates.map((p) => ({
|
|
904
|
+
poolId: p.poolId,
|
|
905
|
+
collateralAmount: p.collateralAmount.toNumber(),
|
|
906
|
+
debtAmount: p.debtAmount.toNumber(),
|
|
907
|
+
})),
|
|
908
|
+
vesuBorrowCapacity: this.vesuBorrowCapacity,
|
|
909
|
+
vesuRebalance: this.shouldVesuRebalance,
|
|
910
|
+
vesuPerPoolDebtDeltasToBorrow: this.vesuPerPoolDebtDeltasToBorrow.map((d) => d.toNumber()),
|
|
911
|
+
extendedBalance: this.extendedBalance?.balance.toNumber(),
|
|
912
|
+
extendedEquity: this.extendedBalance?.equity.toNumber(),
|
|
913
|
+
extendedAvailableForTrade: this.extendedBalance?.availableForTrade.toNumber(),
|
|
914
|
+
extendedAvailableForWithdrawal: this.extendedBalance?.availableForWithdrawal.toNumber(),
|
|
915
|
+
extendedUnrealisedPnl: this.extendedBalance?.unrealisedPnl.toNumber(),
|
|
916
|
+
extendedPendingDeposit: this.extendedBalance?.pendingDeposit.toNumber(),
|
|
917
|
+
extendedPositions: this.extendedPositions.map((p) => ({
|
|
918
|
+
instrument: p.instrument,
|
|
919
|
+
size: p.size.toNumber(),
|
|
920
|
+
valueUsd: p.valueUsd.toNumber(),
|
|
921
|
+
})),
|
|
922
|
+
}
|
|
923
|
+
// unused balances
|
|
924
|
+
console.log(
|
|
925
|
+
"unused balances",
|
|
926
|
+
aggregatedData.unusedBalances
|
|
927
|
+
.map((b) => `${b.token}=${b.amount}`)
|
|
928
|
+
.join(", "),
|
|
929
|
+
);
|
|
930
|
+
console.log(
|
|
931
|
+
"wallet balance",
|
|
932
|
+
aggregatedData.walletBalance
|
|
933
|
+
? `${aggregatedData.walletBalance.token}=${aggregatedData.walletBalance.amount}`
|
|
934
|
+
: undefined,
|
|
935
|
+
);
|
|
936
|
+
console.log(
|
|
937
|
+
"vault asset balance",
|
|
938
|
+
aggregatedData.vaultAssetBalance
|
|
939
|
+
? `${aggregatedData.vaultAssetBalance.token}=${aggregatedData.vaultAssetBalance.amount}`
|
|
940
|
+
: undefined,
|
|
941
|
+
);
|
|
942
|
+
console.log(
|
|
943
|
+
"vault usdc balance",
|
|
944
|
+
aggregatedData.vaultUsdcBalance
|
|
945
|
+
? `${aggregatedData.vaultUsdcBalance.token}=${aggregatedData.vaultUsdcBalance.amount}`
|
|
946
|
+
: undefined,
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
// vesu info
|
|
950
|
+
console.log(
|
|
951
|
+
"vesu pool states",
|
|
952
|
+
aggregatedData.vesuPoolStates
|
|
953
|
+
.map(
|
|
954
|
+
(p) =>
|
|
955
|
+
`${p.poolId.shortString()}=${p.collateralAmount} ${p.debtAmount}`,
|
|
956
|
+
)
|
|
957
|
+
.join(", "),
|
|
958
|
+
);
|
|
959
|
+
console.log("vesu borrow capacity", aggregatedData.vesuBorrowCapacity);
|
|
960
|
+
console.log(
|
|
961
|
+
"vesu rebalance",
|
|
962
|
+
aggregatedData.vesuRebalance.map(String).join(", "),
|
|
963
|
+
);
|
|
964
|
+
console.log("vesu per pool debt deltas to borrow", aggregatedData.vesuPerPoolDebtDeltasToBorrow.join(", "));
|
|
965
|
+
// extended info
|
|
966
|
+
console.log("extended balance", aggregatedData.extendedBalance);
|
|
967
|
+
console.log("extended equity", aggregatedData.extendedEquity);
|
|
968
|
+
console.log("extended available for trade", aggregatedData.extendedAvailableForTrade);
|
|
969
|
+
console.log("extended available for withdrawal", aggregatedData.extendedAvailableForWithdrawal);
|
|
970
|
+
console.log("extended unrealised pnl", aggregatedData.extendedUnrealisedPnl);
|
|
971
|
+
console.log("extended pending deposit", aggregatedData.extendedPendingDeposit);
|
|
972
|
+
console.log(
|
|
973
|
+
"extended positions",
|
|
974
|
+
aggregatedData.extendedPositions
|
|
975
|
+
.map(
|
|
976
|
+
(p) => `${p.instrument}=${p.size} ${p.valueUsd}`,
|
|
977
|
+
)
|
|
978
|
+
.join(", "),
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
return aggregatedData
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Initialise derived views for a solve cycle. Mutates only when pending
|
|
986
|
+
* withdrawal from Extended is in transit (credits wallet raw balance).
|
|
752
987
|
*/
|
|
753
988
|
initBudget(): void {
|
|
754
|
-
const debtDeltaNum = this.vesuPerPoolDebtDeltasToBorrow.reduce((a, b) => a + b.toNumber(), 0);
|
|
755
|
-
let totalUnusedUsd = this.unusedBalance.reduce((a, b) => a + b.usdValue, 0);
|
|
756
|
-
if (debtDeltaNum > 0) totalUnusedUsd += debtDeltaNum;
|
|
757
|
-
const extAvailTrade = this.extendedBalance?.availableForTrade?.toNumber() ?? 0;
|
|
758
|
-
if (extAvailTrade > 0) totalUnusedUsd += extAvailTrade;
|
|
759
|
-
if (debtDeltaNum > 0) totalUnusedUsd += debtDeltaNum;
|
|
760
|
-
|
|
761
|
-
this._vaUsd = this.vaultBalance?.usdValue ?? 0;
|
|
762
|
-
this._walletUsd = this.walletBalance?.usdValue ?? 0;
|
|
763
|
-
this._extAvailWithdraw = this.extendedBalance?.availableForWithdrawal?.toNumber() ?? 0;
|
|
764
|
-
this._extAvailUpnl = this.extendedBalance?.unrealisedPnl?.toNumber() ?? 0;
|
|
765
|
-
this._extAvailTrade = extAvailTrade;
|
|
766
|
-
this._extPendingDeposit = this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
|
|
767
|
-
this._vesuBorrowCapacity = debtDeltaNum;
|
|
768
|
-
this._totalUnused = totalUnusedUsd;
|
|
769
|
-
|
|
770
|
-
// ! cannot assume like this. the assumpotion, raher is opposite. also, be
|
|
771
989
|
const pendingDeposit = this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
|
|
772
|
-
if (pendingDeposit
|
|
773
|
-
this._extAvailTrade += pendingDeposit;
|
|
774
|
-
this._totalUnused += pendingDeposit;
|
|
775
|
-
logger.debug(`SolveBudget::initBudget pendingDeposit=${pendingDeposit} -> increased extAvailTrade`);
|
|
776
|
-
} else if (pendingDeposit < 0) {
|
|
990
|
+
if (pendingDeposit < 0) {
|
|
777
991
|
const inTransit = Math.abs(pendingDeposit);
|
|
778
|
-
this.
|
|
779
|
-
|
|
780
|
-
|
|
992
|
+
if (this.walletBalance) {
|
|
993
|
+
this._addUsdToTokenBalance(this.walletBalance, inTransit);
|
|
994
|
+
}
|
|
995
|
+
logger.debug(`SolveBudget::initBudget pendingDeposit=${pendingDeposit} -> increased wallet raw USD by ${inTransit}`);
|
|
781
996
|
}
|
|
997
|
+
|
|
998
|
+
this._recomputeUnusedBalance();
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Apply a safety buffer to all liquid balances (VA, wallet, extended trade/withdraw/upnl,
|
|
1003
|
+
* unused balances). Call after withdrawal classification but before LTV/deposit classifiers
|
|
1004
|
+
* so that withdrawal uses full raw amounts while subsequent classifiers see buffered values.
|
|
1005
|
+
*/
|
|
1006
|
+
applyBuffer(factor: number): void {
|
|
1007
|
+
if (factor <= 0 || factor >= 1) return;
|
|
1008
|
+
const mult = 1 - factor;
|
|
1009
|
+
|
|
1010
|
+
const scaleTokenBalance = (tb: TokenBalance | null) => {
|
|
1011
|
+
if (!tb) return;
|
|
1012
|
+
const newAmount = tb.amount.multipliedBy(mult);
|
|
1013
|
+
tb.amount = new Web3Number(newAmount.toFixed(tb.token.decimals), tb.token.decimals);
|
|
1014
|
+
tb.usdValue = tb.usdValue * mult;
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
scaleTokenBalance(this.vaultAssetBalance);
|
|
1018
|
+
scaleTokenBalance(this.vaultUsdcBalance);
|
|
1019
|
+
scaleTokenBalance(this.walletBalance);
|
|
1020
|
+
for (const ub of this.unusedBalance) {
|
|
1021
|
+
scaleTokenBalance(ub);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (this.extendedBalance) {
|
|
1025
|
+
this.extendedBalance.availableForTrade = this.extendedBalance.availableForTrade.multipliedBy(mult);
|
|
1026
|
+
this.extendedBalance.availableForWithdrawal = this.extendedBalance.availableForWithdrawal.multipliedBy(mult);
|
|
1027
|
+
this.extendedBalance.unrealisedPnl = this.extendedBalance.unrealisedPnl.multipliedBy(mult);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
this._recomputeUnusedBalance();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
get vesuPoolState(): VesuPoolState {
|
|
1034
|
+
assert(this.vesuPoolStates.length === 1, 'SolveBudget::vesuPoolState: vesuPoolStates length must be 1');
|
|
1035
|
+
return this.vesuPoolStates[0];
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// ── Derived getters (buffered where applicable) ─────────────────────
|
|
1039
|
+
|
|
1040
|
+
/** Buffered VA USD: strategy-asset slot + optional USDC slot. */
|
|
1041
|
+
get vaUsd(): number {
|
|
1042
|
+
return this.bufferedTokenUsd(this.vaultAssetBalance) + this.bufferedTokenUsd(this.vaultUsdcBalance);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** Buffered USD in VA strategy-asset bucket only. */
|
|
1046
|
+
get vaAssetUsd(): number {
|
|
1047
|
+
return this.bufferedTokenUsd(this.vaultAssetBalance);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/** Buffered USD in VA USDC bucket (0 when asset === USDC). */
|
|
1051
|
+
get vaUsdcUsd(): number {
|
|
1052
|
+
return this.bufferedTokenUsd(this.vaultUsdcBalance);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
get walletUsd(): number {
|
|
1056
|
+
return this.bufferedUsd(this._rawTokenUsd(this.walletBalance));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
get vaWalletUsd(): number {
|
|
1060
|
+
// va buffered + wallet raw
|
|
1061
|
+
return this.vaUsd + this.walletUsd;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
get extAvailWithdraw(): number {
|
|
1065
|
+
const raw = this.extendedBalance?.availableForWithdrawal?.toNumber() ?? 0;
|
|
1066
|
+
return this.bufferedUsd(raw);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
get extAvailUpnl(): number {
|
|
1070
|
+
const raw = this.extendedBalance?.unrealisedPnl?.toNumber() ?? 0;
|
|
1071
|
+
return this.bufferedUsd(raw);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Buffered Extended available-for-trade plus positive {@link ExtendedBalanceState.pendingDeposit}
|
|
1076
|
+
* (deposit in transit is usable the same way as the pre-buffer implementation).
|
|
1077
|
+
*/
|
|
1078
|
+
get extAvailTrade(): number {
|
|
1079
|
+
const raw = this.extendedBalance?.availableForTrade?.toNumber() ?? 0;
|
|
1080
|
+
let v = this.bufferedUsd(raw);
|
|
1081
|
+
const pd = this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
|
|
1082
|
+
if (pd > 0) v += pd;
|
|
1083
|
+
return v;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
get extPendingDeposit(): number {
|
|
1087
|
+
return this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Aggregate positive per-pool borrow headroom (USD). Repay/borrow routes update
|
|
1092
|
+
* {@link vesuPerPoolDebtDeltasToBorrow}; no separate counter.
|
|
1093
|
+
*/
|
|
1094
|
+
get vesuBorrowCapacity(): number {
|
|
1095
|
+
return this.vesuPerPoolDebtDeltasToBorrow.reduce(
|
|
1096
|
+
(a, d) => a + Math.max(0, d.toNumber()),
|
|
1097
|
+
0,
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/** Diagnostic: buffered idle + positive debt delta + buffered Extended afT + in-flight deposit. */
|
|
1102
|
+
get totalUnused(): number {
|
|
1103
|
+
const debtSum = this.vesuPerPoolDebtDeltasToBorrow.reduce((a, b) => a + b.toNumber(), 0);
|
|
1104
|
+
let u = this.unusedBalance.reduce((a, b) => a + this.bufferedTokenUsd(b), 0);
|
|
1105
|
+
if (debtSum > 0) u += debtSum;
|
|
1106
|
+
const rawAft = this.extendedBalance?.availableForTrade?.toNumber() ?? 0;
|
|
1107
|
+
const aftBuf = this.bufferedUsd(rawAft);
|
|
1108
|
+
if (aftBuf > 0) u += aftBuf;
|
|
1109
|
+
const pd = this.extendedBalance?.pendingDeposit?.toNumber() ?? 0;
|
|
1110
|
+
if (pd > 0) u += pd;
|
|
1111
|
+
return u;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/** Sum of buffered USD across merged unused-balance rows (VA + wallet). */
|
|
1115
|
+
get unusedBalancesBufferedUsdSum(): number {
|
|
1116
|
+
return this.unusedBalance.reduce((a, b) => a + this.bufferedTokenUsd(b), 0);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** Read-only snapshot view for validation / logging. */
|
|
1120
|
+
get unusedBalanceRows(): readonly TokenBalance[] {
|
|
1121
|
+
return this.unusedBalance;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/** Read-only Vesu pool view for solve computations. */
|
|
1125
|
+
get vesuPools(): readonly VesuPoolState[] {
|
|
1126
|
+
return this.vesuPoolStates;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/** Read-only Extended positions view for solve computations. */
|
|
1130
|
+
get extendedPositionsView(): readonly ExtendedPositionState[] {
|
|
1131
|
+
return this.extendedPositions;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/** Read-only Extended balance view for diagnostics / margin checks. */
|
|
1135
|
+
get extendedBalanceView(): ExtendedBalanceState | null {
|
|
1136
|
+
return this.extendedBalance;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/** Current debt deltas per pool (positive=borrow, negative=repay). */
|
|
1140
|
+
get vesuDebtDeltas(): readonly Web3Number[] {
|
|
1141
|
+
return this.vesuPerPoolDebtDeltasToBorrow;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/** Per-pool rebalance flags derived from target HF checks. */
|
|
1145
|
+
get vesuRebalanceFlags(): readonly boolean[] {
|
|
1146
|
+
return this.shouldVesuRebalance;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** Raw USD in VA (USDC slot + asset slot); spend caps when executing transfers. */
|
|
1150
|
+
private _vaRawUsd(): number {
|
|
1151
|
+
return this._rawTokenUsd(this.vaultUsdcBalance) + this._rawTokenUsd(this.vaultAssetBalance);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
private _walletRawUsd(): number {
|
|
1155
|
+
return this._rawTokenUsd(this.walletBalance);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// ── Token snapshot helpers (keep vault / wallet / unusedBalance aligned) ─
|
|
1159
|
+
|
|
1160
|
+
/** Remove up to `usd` notional from a token balance, scaling token amount proportionally. */
|
|
1161
|
+
private _deductUsdFromTokenBalance(tb: TokenBalance, usd: number): void {
|
|
1162
|
+
if (usd <= 0) return;
|
|
1163
|
+
const take = Math.min(usd, tb.usdValue);
|
|
1164
|
+
if (take <= 0) return;
|
|
1165
|
+
const oldUsd = tb.usdValue;
|
|
1166
|
+
const newUsd = Math.max(0, oldUsd - take);
|
|
1167
|
+
tb.usdValue = newUsd;
|
|
1168
|
+
if (oldUsd <= 0) return;
|
|
1169
|
+
const ratio = newUsd / oldUsd;
|
|
1170
|
+
tb.amount = new Web3Number(
|
|
1171
|
+
(tb.amount.toNumber() * ratio).toFixed(tb.token.decimals),
|
|
1172
|
+
tb.token.decimals,
|
|
1173
|
+
);
|
|
782
1174
|
}
|
|
783
1175
|
|
|
784
|
-
|
|
1176
|
+
/** Add USD notional; infers price from current amount/usd when possible, else 1:1. */
|
|
1177
|
+
private _addUsdToTokenBalance(tb: TokenBalance, usd: number): void {
|
|
1178
|
+
if (usd <= 0) return;
|
|
1179
|
+
const amtNum = tb.amount.toNumber();
|
|
1180
|
+
const price =
|
|
1181
|
+
amtNum > 0 && tb.usdValue > 0 ? tb.usdValue / amtNum : 1;
|
|
1182
|
+
const deltaTok = usd / price;
|
|
1183
|
+
tb.usdValue += usd;
|
|
1184
|
+
tb.amount = tb.amount.plus(
|
|
1185
|
+
new Web3Number(deltaTok.toFixed(tb.token.decimals), tb.token.decimals),
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Rebuilds {@link unusedBalance} from vault + wallet snapshots (same merge as refresh).
|
|
1191
|
+
*/
|
|
1192
|
+
private _recomputeUnusedBalance(): void {
|
|
1193
|
+
const balanceMap = new Map<string, TokenBalance>();
|
|
1194
|
+
|
|
1195
|
+
const merge = (b: TokenBalance | null) => {
|
|
1196
|
+
if (!b) return;
|
|
1197
|
+
const key = b.token.address.toString();
|
|
1198
|
+
const row: TokenBalance = {
|
|
1199
|
+
token: b.token,
|
|
1200
|
+
amount: new Web3Number(b.amount.toFixed(b.token.decimals), b.token.decimals),
|
|
1201
|
+
usdValue: b.usdValue,
|
|
1202
|
+
};
|
|
1203
|
+
const existing = balanceMap.get(key);
|
|
1204
|
+
if (existing) {
|
|
1205
|
+
existing.amount = new Web3Number(
|
|
1206
|
+
existing.amount.plus(row.amount).toFixed(existing.token.decimals),
|
|
1207
|
+
existing.token.decimals,
|
|
1208
|
+
);
|
|
1209
|
+
existing.usdValue += row.usdValue;
|
|
1210
|
+
} else {
|
|
1211
|
+
balanceMap.set(key, row);
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
785
1214
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
get vesuBorrowCapacity(): number { return this._vesuBorrowCapacity; }
|
|
793
|
-
get totalUnused(): number { return this._totalUnused; }
|
|
794
|
-
get extPendingDeposit(): number { return this._extPendingDeposit; }
|
|
1215
|
+
merge(this.vaultAssetBalance);
|
|
1216
|
+
merge(this.vaultUsdcBalance);
|
|
1217
|
+
merge(this.walletBalance);
|
|
1218
|
+
|
|
1219
|
+
this.unusedBalance = Array.from(balanceMap.values());
|
|
1220
|
+
}
|
|
795
1221
|
|
|
796
1222
|
// ── Spend methods (return amount consumed, auto-decrement totalUnused) ─
|
|
797
1223
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1224
|
+
/**
|
|
1225
|
+
* Spend VA **raw** USD (up to {@link vaRawUsd}). Prefer {@link vaultUsdcBalance} when present, then {@link vaultAssetBalance}.
|
|
1226
|
+
*/
|
|
1227
|
+
spendVA(rawDesired: number): number {
|
|
1228
|
+
const capRaw = this._vaRawUsd();
|
|
1229
|
+
const usedRaw = Math.min(capRaw, Math.max(0, rawDesired));
|
|
1230
|
+
if (usedRaw <= CASE_THRESHOLD_USD) return 0;
|
|
1231
|
+
let rem = usedRaw;
|
|
1232
|
+
if (rem > 0 && this.vaultUsdcBalance && this.vaultUsdcBalance.usdValue > 0) {
|
|
1233
|
+
const fromUsdc = Math.min(rem, this.vaultUsdcBalance.usdValue);
|
|
1234
|
+
this._deductUsdFromTokenBalance(this.vaultUsdcBalance, fromUsdc);
|
|
1235
|
+
rem -= fromUsdc;
|
|
1236
|
+
}
|
|
1237
|
+
if (rem > 0 && this.vaultAssetBalance) {
|
|
1238
|
+
this._deductUsdFromTokenBalance(this.vaultAssetBalance, rem);
|
|
1239
|
+
}
|
|
1240
|
+
this._recomputeUnusedBalance();
|
|
1241
|
+
logger.debug(`SolveBudget::spendVA usedRaw=${usedRaw}, vaUsd=${this.vaUsd}, totalUnused=${this.totalUnused}`);
|
|
1242
|
+
return usedRaw;
|
|
804
1243
|
}
|
|
805
1244
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1245
|
+
/**
|
|
1246
|
+
* Spend nominal/raw USD from VA (e.g. Vesu repay, on-chain USDC). Does not apply the safety buffer to the cap.
|
|
1247
|
+
*/
|
|
1248
|
+
spendVaRawUsd(rawUsdDesired: number): number {
|
|
1249
|
+
const capRaw =
|
|
1250
|
+
this._rawTokenUsd(this.vaultUsdcBalance) + this._rawTokenUsd(this.vaultAssetBalance);
|
|
1251
|
+
const usedRaw = Math.min(capRaw, Math.max(0, rawUsdDesired));
|
|
1252
|
+
if (usedRaw <= CASE_THRESHOLD_USD) return 0;
|
|
1253
|
+
let rem = usedRaw;
|
|
1254
|
+
if (rem > 0 && this.vaultUsdcBalance && this.vaultUsdcBalance.usdValue > 0) {
|
|
1255
|
+
const fromUsdc = Math.min(rem, this.vaultUsdcBalance.usdValue);
|
|
1256
|
+
this._deductUsdFromTokenBalance(this.vaultUsdcBalance, fromUsdc);
|
|
1257
|
+
rem -= fromUsdc;
|
|
1258
|
+
}
|
|
1259
|
+
if (rem > 0 && this.vaultAssetBalance) {
|
|
1260
|
+
this._deductUsdFromTokenBalance(this.vaultAssetBalance, rem);
|
|
1261
|
+
}
|
|
1262
|
+
this._recomputeUnusedBalance();
|
|
1263
|
+
logger.debug(`SolveBudget::spendVaRawUsd usedRaw=${usedRaw}, vaUsd=${this.vaUsd}, totalUnused=${this.totalUnused}`);
|
|
1264
|
+
return usedRaw;
|
|
811
1265
|
}
|
|
812
1266
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
return
|
|
1267
|
+
/**
|
|
1268
|
+
* Add **raw nominal USD** to VA (borrow proceeds, wallet→VA in raw USDC, etc.).
|
|
1269
|
+
*/
|
|
1270
|
+
addToVA(rawUsd: number): void {
|
|
1271
|
+
assert(rawUsd >= 0, 'SolveBudget::addToVA amount must be positive');
|
|
1272
|
+
if (rawUsd === 0) return;
|
|
1273
|
+
if (this.vaultUsdcBalance) {
|
|
1274
|
+
this._addUsdToTokenBalance(this.vaultUsdcBalance, rawUsd);
|
|
1275
|
+
} else if (this.vaultAssetBalance) {
|
|
1276
|
+
this._addUsdToTokenBalance(this.vaultAssetBalance, rawUsd);
|
|
1277
|
+
}
|
|
1278
|
+
this._recomputeUnusedBalance();
|
|
1279
|
+
logger.debug(`SolveBudget::addToVA rawUsd=${rawUsd}, vaUsd=${this.vaUsd}, totalUnused=${this.totalUnused}`);
|
|
819
1280
|
}
|
|
820
1281
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1282
|
+
spendWallet(rawDesired: number): number {
|
|
1283
|
+
const capRaw = this._walletRawUsd();
|
|
1284
|
+
const usedRaw = Math.min(capRaw, Math.max(0, rawDesired));
|
|
1285
|
+
if (usedRaw <= CASE_THRESHOLD_USD) return 0;
|
|
1286
|
+
if (this.walletBalance) {
|
|
1287
|
+
this._deductUsdFromTokenBalance(this.walletBalance, usedRaw);
|
|
1288
|
+
}
|
|
1289
|
+
this._recomputeUnusedBalance();
|
|
1290
|
+
logger.debug(`SolveBudget::spendWallet usedRaw=${usedRaw}, walletUsd=${this.walletUsd}, totalUnused=${this.totalUnused}`);
|
|
1291
|
+
return usedRaw;
|
|
826
1292
|
}
|
|
827
1293
|
|
|
828
|
-
|
|
829
|
-
|
|
1294
|
+
/** Add **raw nominal USD** to the operator wallet balance (e.g. Extended→wallet withdrawal). */
|
|
1295
|
+
addToWallet(rawUsd: number): void {
|
|
1296
|
+
assert(rawUsd >= 0, 'SolveBudget::addToWallet amount must be positive');
|
|
1297
|
+
if (rawUsd === 0) return;
|
|
1298
|
+
if (this.walletBalance) {
|
|
1299
|
+
this._addUsdToTokenBalance(this.walletBalance, rawUsd);
|
|
1300
|
+
}
|
|
1301
|
+
this._recomputeUnusedBalance();
|
|
1302
|
+
logger.debug(`SolveBudget::addToWallet rawUsd=${rawUsd}, walletUsd=${this.walletUsd}, totalUnused=${this.totalUnused}`);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
spendVAWallet(rawDesired: number): number {
|
|
1306
|
+
let remaining = Math.max(0, rawDesired);
|
|
830
1307
|
const vaSpent = this.spendVA(remaining);
|
|
831
1308
|
remaining -= vaSpent;
|
|
832
1309
|
const walletSpent = this.spendWallet(remaining);
|
|
833
1310
|
return vaSpent + walletSpent;
|
|
834
1311
|
}
|
|
835
1312
|
|
|
836
|
-
private _updateExtAvailWithdraw(
|
|
837
|
-
|
|
838
|
-
|
|
1313
|
+
private _updateExtAvailWithdraw(desiredRaw: number, isSpend: boolean): number {
|
|
1314
|
+
assert(desiredRaw > 0, 'SolveBudget::_updateExtAvailWithdraw amount must be positive');
|
|
1315
|
+
let rawDelta: number;
|
|
839
1316
|
if (isSpend) {
|
|
840
|
-
|
|
841
|
-
|
|
1317
|
+
const capRaw = this.extendedBalance?.availableForWithdrawal?.toNumber() ?? 0;
|
|
1318
|
+
const useRaw = Math.min(capRaw, desiredRaw);
|
|
1319
|
+
if (useRaw <= CASE_THRESHOLD_USD) return 0;
|
|
1320
|
+
rawDelta = -useRaw;
|
|
1321
|
+
} else {
|
|
1322
|
+
rawDelta = desiredRaw;
|
|
842
1323
|
}
|
|
843
|
-
this._extAvailWithdraw += amount;
|
|
844
|
-
this._totalUnused += amount;
|
|
845
|
-
this._extAvailTrade += amount;
|
|
846
1324
|
|
|
847
|
-
// update extended balances
|
|
848
1325
|
if (this.extendedBalance) {
|
|
849
|
-
this.extendedBalance.availableForWithdrawal = safeUsdcWeb3Number(this.extendedBalance.availableForWithdrawal.toNumber() +
|
|
850
|
-
this.extendedBalance.availableForTrade = safeUsdcWeb3Number(this.extendedBalance.availableForTrade.toNumber() +
|
|
851
|
-
this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() +
|
|
852
|
-
this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() +
|
|
1326
|
+
this.extendedBalance.availableForWithdrawal = safeUsdcWeb3Number(this.extendedBalance.availableForWithdrawal.toNumber() + rawDelta);
|
|
1327
|
+
this.extendedBalance.availableForTrade = safeUsdcWeb3Number(this.extendedBalance.availableForTrade.toNumber() + rawDelta);
|
|
1328
|
+
this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() + rawDelta);
|
|
1329
|
+
this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() + rawDelta);
|
|
853
1330
|
}
|
|
854
|
-
logger.debug(`SolveBudget::updateExtAvailWithdraw
|
|
855
|
-
return
|
|
1331
|
+
logger.debug(`SolveBudget::updateExtAvailWithdraw rawDelta=${rawDelta}, extAvailWithdraw=${this.extAvailWithdraw}, totalUnused=${this.totalUnused}`);
|
|
1332
|
+
return rawDelta;
|
|
856
1333
|
}
|
|
857
1334
|
|
|
858
|
-
private _updateExtAvailUpnl(
|
|
859
|
-
|
|
860
|
-
|
|
1335
|
+
private _updateExtAvailUpnl(desiredRaw: number, isSpend: boolean): number {
|
|
1336
|
+
assert(desiredRaw > 0, 'SolveBudget::_updateExtAvailUpnl amount must be positive');
|
|
1337
|
+
let rawDelta: number;
|
|
861
1338
|
if (isSpend) {
|
|
862
|
-
|
|
863
|
-
|
|
1339
|
+
const capRaw = this.extendedBalance?.unrealisedPnl?.toNumber() ?? 0;
|
|
1340
|
+
const useRaw = Math.min(capRaw, desiredRaw);
|
|
1341
|
+
if (useRaw <= CASE_THRESHOLD_USD) return 0;
|
|
1342
|
+
rawDelta = -useRaw;
|
|
1343
|
+
} else {
|
|
1344
|
+
rawDelta = desiredRaw;
|
|
864
1345
|
}
|
|
865
|
-
this._extAvailUpnl += amount;
|
|
866
|
-
this._totalUnused += amount;
|
|
867
1346
|
|
|
868
|
-
// update extended balances
|
|
869
1347
|
if (this.extendedBalance) {
|
|
870
|
-
this.extendedBalance.unrealisedPnl = safeUsdcWeb3Number(this.extendedBalance.unrealisedPnl.toNumber() +
|
|
871
|
-
this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() +
|
|
872
|
-
this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() +
|
|
873
|
-
this.extendedBalance.availableForTrade = safeUsdcWeb3Number(this.extendedBalance.availableForTrade.toNumber() +
|
|
1348
|
+
this.extendedBalance.unrealisedPnl = safeUsdcWeb3Number(this.extendedBalance.unrealisedPnl.toNumber() + rawDelta);
|
|
1349
|
+
this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() + rawDelta);
|
|
1350
|
+
this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() + rawDelta);
|
|
1351
|
+
this.extendedBalance.availableForTrade = safeUsdcWeb3Number(this.extendedBalance.availableForTrade.toNumber() + rawDelta);
|
|
874
1352
|
}
|
|
875
|
-
logger.debug(`SolveBudget::updateExtAvailUpnl
|
|
876
|
-
return
|
|
1353
|
+
logger.debug(`SolveBudget::updateExtAvailUpnl rawDelta=${rawDelta}, extAvailUpnl=${this.extAvailUpnl}, totalUnused=${this.totalUnused}`);
|
|
1354
|
+
return rawDelta;
|
|
877
1355
|
}
|
|
878
1356
|
|
|
879
|
-
spendExtAvailTrade(
|
|
880
|
-
const used = this._updateExtAvailWithdraw(
|
|
881
|
-
const usedUpnl = this._updateExtAvailUpnl(
|
|
882
|
-
logger.debug(`SolveBudget::updateExtAvailTrade
|
|
1357
|
+
spendExtAvailTrade(rawDesired: number): number {
|
|
1358
|
+
const used = this._updateExtAvailWithdraw(rawDesired, true);
|
|
1359
|
+
const usedUpnl = this._updateExtAvailUpnl(rawDesired, true);
|
|
1360
|
+
logger.debug(`SolveBudget::updateExtAvailTrade rawSum=${used + usedUpnl}, extAvailTrade=${this.extAvailTrade}, totalUnused=${this.totalUnused}`);
|
|
883
1361
|
return used + usedUpnl;
|
|
884
1362
|
}
|
|
885
1363
|
|
|
886
|
-
|
|
887
|
-
|
|
1364
|
+
// simply reduces available amounts, but maintains equity and balance.
|
|
1365
|
+
spendExtAvailTradeToEquityOnly(rawDesired: number): number {
|
|
1366
|
+
const used = this._updateExtAvailWithdraw(rawDesired, true);
|
|
1367
|
+
const remaining = rawDesired - Math.abs(used);
|
|
1368
|
+
const usedUpnl = remaining > 0 ? this._updateExtAvailUpnl(remaining, true) : 0;
|
|
1369
|
+
if (this.extendedBalance) {
|
|
1370
|
+
// add whats subtracted earlier to equity
|
|
1371
|
+
const net = Math.abs(used) + Math.abs(usedUpnl);
|
|
1372
|
+
if (net.toFixed(0) != rawDesired.toFixed(0)) {
|
|
1373
|
+
throw new Error(`SolveBudget::spendExtAvailTradeToEquityOnly net=${net} != rawDesired=${rawDesired}`);
|
|
1374
|
+
}
|
|
1375
|
+
this.extendedBalance.equity = safeUsdcWeb3Number(this.extendedBalance.equity.toNumber() + net);
|
|
1376
|
+
this.extendedBalance.balance = safeUsdcWeb3Number(this.extendedBalance.balance.toNumber() + net);
|
|
1377
|
+
}
|
|
1378
|
+
logger.debug(`SolveBudget::updateExtAvailTrade rawSum=${used + usedUpnl}, extAvailTrade=${this.extAvailTrade}, totalUnused=${this.totalUnused}`);
|
|
1379
|
+
return used + usedUpnl;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
spendExtAvailWithdraw(rawDesired: number): number {
|
|
1383
|
+
return this._updateExtAvailWithdraw(rawDesired, true);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
spendExtAvailUpnl(rawDesired: number): number {
|
|
1387
|
+
return this._updateExtAvailUpnl(rawDesired, true);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* Withdraw from Extended **withdrawal bucket only** to operator wallet (planning).
|
|
1392
|
+
* Used when VA must be funded from Extended and withdraw should be exhausted before unrealised PnL.
|
|
1393
|
+
*/
|
|
1394
|
+
spendAvailWithdrawToWallet(rawDesired: number): number {
|
|
1395
|
+
const want = Math.max(0, rawDesired);
|
|
1396
|
+
if (want <= CASE_THRESHOLD_USD) return 0;
|
|
1397
|
+
const rawDelta = this._updateExtAvailWithdraw(want, true);
|
|
1398
|
+
if (rawDelta === 0) return 0;
|
|
1399
|
+
const used = -rawDelta;
|
|
1400
|
+
this.addToWallet(used);
|
|
1401
|
+
return used;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Required Extended equity (USD) for current open positions: total notional ÷ strategy leverage.
|
|
1406
|
+
* Same basis as {@link ExtendedSVKVesuStateManager._classifyLtvExtended} margin check.
|
|
1407
|
+
*/
|
|
1408
|
+
private _extendedMarginRequirementUsd(): number {
|
|
1409
|
+
const lev = calculateExtendedLevergae();
|
|
1410
|
+
if (lev <= 0 || this.extendedPositions.length === 0) return 0;
|
|
1411
|
+
const totalPosUsd = this.extendedPositions.reduce(
|
|
1412
|
+
(s, p) => s + p.valueUsd.toNumber(),
|
|
1413
|
+
0,
|
|
1414
|
+
);
|
|
1415
|
+
return totalPosUsd / lev;
|
|
888
1416
|
}
|
|
889
1417
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1418
|
+
/** How much more equity is needed before deposits should increase withdraw / trade availability. */
|
|
1419
|
+
private _extendedEquityShortfallUsd(): number {
|
|
1420
|
+
if (!this.extendedBalance) return 0;
|
|
1421
|
+
const req = this._extendedMarginRequirementUsd();
|
|
1422
|
+
const eq = this.extendedBalance.equity.toNumber();
|
|
1423
|
+
return Math.max(0, req - eq);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Credits a USDC inflow on Extended. Fills margin shortfall (balance+equity only) first;
|
|
1428
|
+
* any remainder is credited across balance, equity, availableForWithdrawal, and availableForTrade.
|
|
1429
|
+
*/
|
|
1430
|
+
addToExtAvailTrade(rawUsd: number): void {
|
|
1431
|
+
assert(rawUsd >= 0, 'SolveBudget::addToExtAvailTrade amount must be non-negative');
|
|
1432
|
+
if (rawUsd <= CASE_THRESHOLD_USD) return;
|
|
1433
|
+
if (!this.extendedBalance) {
|
|
1434
|
+
logger.warn('SolveBudget::addToExtAvailTrade skipped — no extendedBalance');
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
const shortfall = this._extendedEquityShortfallUsd();
|
|
1439
|
+
const toMargin = Math.min(rawUsd, shortfall);
|
|
1440
|
+
const toLiquid = rawUsd - toMargin;
|
|
1441
|
+
|
|
1442
|
+
if (toMargin > CASE_THRESHOLD_USD) {
|
|
1443
|
+
const b = this.extendedBalance.balance.toNumber();
|
|
1444
|
+
const e = this.extendedBalance.equity.toNumber();
|
|
1445
|
+
this.extendedBalance.balance = safeUsdcWeb3Number(b + toMargin);
|
|
1446
|
+
this.extendedBalance.equity = safeUsdcWeb3Number(e + toMargin);
|
|
1447
|
+
logger.debug(
|
|
1448
|
+
`SolveBudget::addToExtAvailTrade margin-first rawUsd=${toMargin} ` +
|
|
1449
|
+
`(shortfallBefore=${shortfall}, balance=${b + toMargin}, equity=${e + toMargin})`,
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
if (toLiquid > CASE_THRESHOLD_USD) {
|
|
1454
|
+
this._updateExtAvailWithdraw(toLiquid, false);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
logger.debug(
|
|
1458
|
+
`SolveBudget::addToExtAvailTrade total rawUsd=${rawUsd} toLiquid=${toLiquid} ` +
|
|
1459
|
+
`extAvailTrade=${this.extAvailTrade}, totalUnused=${this.totalUnused}`,
|
|
1460
|
+
);
|
|
893
1461
|
}
|
|
894
1462
|
|
|
895
1463
|
spendVesuBorrowCapacity(desired: number): { used: number, spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] } {
|
|
896
|
-
const used = Math.min(this.
|
|
897
|
-
this._vesuBorrowCapacity -= used;
|
|
898
|
-
// todo check this function is used correctly
|
|
899
|
-
this._totalUnused -= used; // we assume only borrowed till ideal LTV
|
|
1464
|
+
const used = Math.min(this.vesuBorrowCapacity, Math.max(0, desired));
|
|
900
1465
|
|
|
901
1466
|
let spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] = [];
|
|
902
1467
|
// reduce the debt delta for the pool
|
|
@@ -910,17 +1475,13 @@ export class SolveBudget {
|
|
|
910
1475
|
spendsByPool.push({ poolId: this.vesuPoolStates[index].poolId, amount: safeUsdcWeb3Number(borrowed), collateralToken: this.vesuPoolStates[index].collateralToken, debtToken: this.vesuPoolStates[index].debtToken });
|
|
911
1476
|
}
|
|
912
1477
|
|
|
913
|
-
logger.debug(`SolveBudget::spendVesuBorrowCapacity used=${used}, vesuBorrowCapacity=${this.
|
|
1478
|
+
logger.debug(`SolveBudget::spendVesuBorrowCapacity used=${used}, vesuBorrowCapacity=${this.vesuBorrowCapacity}, totalUnused=${this.totalUnused}`);
|
|
914
1479
|
return { used, spendsByPool };
|
|
915
1480
|
}
|
|
916
1481
|
|
|
917
1482
|
repayVesuBorrowCapacity(desired: number): { used: number, spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] } {
|
|
918
1483
|
assert(desired > 0, 'SolveBudget::repayVesuBorrowCapacity desired must be positive');
|
|
919
|
-
// const used = Math.min(this._vesuBorrowCapacity, Math.max(0, desired));
|
|
920
1484
|
const used = desired;
|
|
921
|
-
// todo check this function is used correctly
|
|
922
|
-
this._vesuBorrowCapacity += used;
|
|
923
|
-
// wont increase total unused because we are repaying debt
|
|
924
1485
|
|
|
925
1486
|
const spendsByPool: Omit<VesuDebtRoute, 'priority' | 'type'>[] = [];
|
|
926
1487
|
for (let index = 0; index < this.vesuPerPoolDebtDeltasToBorrow.length; index++) {
|
|
@@ -932,7 +1493,7 @@ export class SolveBudget {
|
|
|
932
1493
|
spendsByPool.push({ poolId: this.vesuPoolStates[index].poolId, amount: safeUsdcWeb3Number(-repaid), collateralToken: this.vesuPoolStates[index].collateralToken, debtToken: this.vesuPoolStates[index].debtToken });
|
|
933
1494
|
}
|
|
934
1495
|
|
|
935
|
-
logger.debug(`SolveBudget::repayVesuBorrowCapacity used=${used}, vesuBorrowCapacity=${this.
|
|
1496
|
+
logger.debug(`SolveBudget::repayVesuBorrowCapacity used=${used}, vesuBorrowCapacity=${this.vesuBorrowCapacity}, totalUnused=${this.totalUnused}`);
|
|
936
1497
|
return { used, spendsByPool };
|
|
937
1498
|
}
|
|
938
1499
|
|
|
@@ -958,14 +1519,28 @@ export class SolveBudget {
|
|
|
958
1519
|
// recompute per pool deltas here
|
|
959
1520
|
const vesuPerPoolDebtDeltasToBorrow = this._computeperPoolDebtDeltasToBorrow();
|
|
960
1521
|
this.vesuPerPoolDebtDeltasToBorrow = vesuPerPoolDebtDeltasToBorrow.map((item) => item.deltaDebt);
|
|
961
|
-
const sum = this.vesuPerPoolDebtDeltasToBorrow.reduce((a, b) => a.plus(b), new Web3Number(0, USDC_TOKEN_DECIMALS));
|
|
962
|
-
this._vesuBorrowCapacity = sum.toNumber();
|
|
963
1522
|
this.shouldVesuRebalance = vesuPerPoolDebtDeltasToBorrow.map((item) => item.shouldRebalance);
|
|
964
1523
|
}
|
|
965
1524
|
|
|
966
|
-
/**
|
|
967
|
-
|
|
968
|
-
|
|
1525
|
+
/**
|
|
1526
|
+
* Update Extended position size after a lever route; sync {@link ExtendedPositionState.valueUsd}
|
|
1527
|
+
* and margin buckets (released USD on decrease, locked USD on increase).
|
|
1528
|
+
*
|
|
1529
|
+
* @param collateralPriceUsd BTC collateral price for notional / margin math; if omitted, uses
|
|
1530
|
+
* existing valueUsd / |size| or the first Vesu pool collateral price.
|
|
1531
|
+
*/
|
|
1532
|
+
applyExtendedExposureDelta(
|
|
1533
|
+
instrument: string,
|
|
1534
|
+
sizeDelta: Web3Number,
|
|
1535
|
+
collateralPriceUsd?: number,
|
|
1536
|
+
): void {
|
|
1537
|
+
const btcEps = 10 ** -COLLATERAL_PRECISION;
|
|
1538
|
+
const lev = calculateExtendedLevergae();
|
|
1539
|
+
|
|
1540
|
+
let pos = this.extendedPositions.find((p) => p.instrument === instrument);
|
|
1541
|
+
const oldAbs = pos ? Math.abs(pos.size.toNumber()) : 0;
|
|
1542
|
+
const oldValUsd = pos ? pos.valueUsd.toNumber() : 0;
|
|
1543
|
+
|
|
969
1544
|
if (pos) {
|
|
970
1545
|
pos.size = new Web3Number(pos.size.plus(sizeDelta).toFixed(8), 8);
|
|
971
1546
|
} else if (sizeDelta.toNumber() !== 0) {
|
|
@@ -976,6 +1551,62 @@ export class SolveBudget {
|
|
|
976
1551
|
valueUsd: new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
977
1552
|
leverage: '0',
|
|
978
1553
|
});
|
|
1554
|
+
pos = this.extendedPositions[this.extendedPositions.length - 1];
|
|
1555
|
+
} else {
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const newAbs = Math.abs(pos.size.toNumber());
|
|
1560
|
+
let price = collateralPriceUsd;
|
|
1561
|
+
if (price === undefined || price <= 0) {
|
|
1562
|
+
price = oldAbs > btcEps ? oldValUsd / oldAbs : (this.vesuPoolStates[0]?.collateralPrice ?? 0);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
if (price > 0) {
|
|
1566
|
+
pos.valueUsd = new Web3Number(
|
|
1567
|
+
(newAbs * price).toFixed(USDC_TOKEN_DECIMALS),
|
|
1568
|
+
USDC_TOKEN_DECIMALS,
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
if (!this.extendedBalance || lev <= 0 || price <= 0) return;
|
|
1573
|
+
|
|
1574
|
+
const dAbs = newAbs - oldAbs;
|
|
1575
|
+
if (dAbs < -btcEps) {
|
|
1576
|
+
const releasedUsd = (-dAbs) * price / lev;
|
|
1577
|
+
if (releasedUsd > CASE_THRESHOLD_USD) {
|
|
1578
|
+
this.addToExtAvailTrade(releasedUsd);
|
|
1579
|
+
}
|
|
1580
|
+
} else if (dAbs > btcEps) {
|
|
1581
|
+
const lockedUsd = dAbs * price / lev;
|
|
1582
|
+
if (lockedUsd > CASE_THRESHOLD_USD) {
|
|
1583
|
+
this._lockExtendedMarginUsd(lockedUsd);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/** Pull margin for larger Extended exposure from liquid buckets, then balance/equity. */
|
|
1589
|
+
private _lockExtendedMarginUsd(lockedUsd: number): void {
|
|
1590
|
+
if (lockedUsd <= CASE_THRESHOLD_USD || !this.extendedBalance) return;
|
|
1591
|
+
let rem = lockedUsd;
|
|
1592
|
+
|
|
1593
|
+
const uw = Math.min(rem, Math.max(0, this.extendedBalance.availableForWithdrawal.toNumber()));
|
|
1594
|
+
if (uw > 0) {
|
|
1595
|
+
this._updateExtAvailWithdraw(uw, true);
|
|
1596
|
+
rem -= uw;
|
|
1597
|
+
}
|
|
1598
|
+
if (rem > 0) {
|
|
1599
|
+
const uu = Math.min(rem, Math.max(0, this.extendedBalance.unrealisedPnl.toNumber()));
|
|
1600
|
+
if (uu > 0) {
|
|
1601
|
+
this._updateExtAvailUpnl(uu, true);
|
|
1602
|
+
rem -= uu;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (rem > 0) {
|
|
1606
|
+
const b = this.extendedBalance.balance.toNumber();
|
|
1607
|
+
const e = this.extendedBalance.equity.toNumber();
|
|
1608
|
+
this.extendedBalance.balance = safeUsdcWeb3Number(b - rem);
|
|
1609
|
+
this.extendedBalance.equity = safeUsdcWeb3Number(e - rem);
|
|
979
1610
|
}
|
|
980
1611
|
}
|
|
981
1612
|
|
|
@@ -1009,6 +1640,35 @@ export class SolveBudget {
|
|
|
1009
1640
|
}
|
|
1010
1641
|
}
|
|
1011
1642
|
|
|
1643
|
+
export function createSolveBudgetFromRawState(params: {
|
|
1644
|
+
assetToken: TokenInfo;
|
|
1645
|
+
usdcToken: TokenInfo;
|
|
1646
|
+
unusedBalance: TokenBalance[];
|
|
1647
|
+
walletBalance: TokenBalance | null;
|
|
1648
|
+
vaultAssetBalance: TokenBalance | null;
|
|
1649
|
+
vaultUsdcBalance: TokenBalance | null;
|
|
1650
|
+
extendedPositions: ExtendedPositionState[];
|
|
1651
|
+
extendedBalance: ExtendedBalanceState | null;
|
|
1652
|
+
vesuPoolStates: VesuPoolState[];
|
|
1653
|
+
limitBalanceBufferFactor?: number;
|
|
1654
|
+
}): SolveBudget {
|
|
1655
|
+
const budget = new SolveBudget({
|
|
1656
|
+
assetToken: params.assetToken,
|
|
1657
|
+
usdcToken: params.usdcToken,
|
|
1658
|
+
unusedBalance: params.unusedBalance,
|
|
1659
|
+
walletBalance: params.walletBalance,
|
|
1660
|
+
vaultAssetBalance: params.vaultAssetBalance,
|
|
1661
|
+
vaultUsdcBalance: params.vaultUsdcBalance,
|
|
1662
|
+
extendedPositions: params.extendedPositions,
|
|
1663
|
+
extendedBalance: params.extendedBalance,
|
|
1664
|
+
vesuPoolStates: params.vesuPoolStates,
|
|
1665
|
+
});
|
|
1666
|
+
if (params.limitBalanceBufferFactor && params.limitBalanceBufferFactor > 0) {
|
|
1667
|
+
budget.applyBuffer(params.limitBalanceBufferFactor);
|
|
1668
|
+
}
|
|
1669
|
+
return budget;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1012
1672
|
// ─── State Manager ─────────────────────────────────────────────────────────────
|
|
1013
1673
|
|
|
1014
1674
|
/**
|
|
@@ -1106,7 +1766,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1106
1766
|
vesuAllocationUsd: safeUsdcWeb3Number(0),
|
|
1107
1767
|
extendedAllocationUsd: safeUsdcWeb3Number(0),
|
|
1108
1768
|
bringLiquidityAmount: safeUsdcWeb3Number(0),
|
|
1109
|
-
pendingDeposit:
|
|
1769
|
+
pendingDeposit: safeUsdcWeb3Number(0),
|
|
1110
1770
|
};
|
|
1111
1771
|
|
|
1112
1772
|
this._logSolveResult(result);
|
|
@@ -1125,32 +1785,58 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1125
1785
|
logger.info(`${this._tag}::_refresh starting`);
|
|
1126
1786
|
|
|
1127
1787
|
const [
|
|
1128
|
-
|
|
1788
|
+
vaultAssetBalance,
|
|
1789
|
+
vaultUsdcBalance,
|
|
1129
1790
|
walletBalance,
|
|
1130
1791
|
vesuPoolStates,
|
|
1131
1792
|
extendedBalance,
|
|
1132
1793
|
extendedPositions,
|
|
1133
1794
|
] = await Promise.all([
|
|
1134
|
-
this.
|
|
1795
|
+
this._fetchVaultAllocatorAssetBalance(),
|
|
1796
|
+
this._fetchVaultAllocatorUsdcBalanceIfDistinct(),
|
|
1135
1797
|
this._fetchWalletBalances(),
|
|
1136
1798
|
this._fetchAllVesuPoolStates(),
|
|
1137
1799
|
this._fetchExtendedBalance(),
|
|
1138
1800
|
this._fetchExtendedPositions(),
|
|
1139
1801
|
]);
|
|
1140
1802
|
|
|
1141
|
-
logger.verbose(
|
|
1803
|
+
logger.verbose(
|
|
1804
|
+
`${this._tag}::_refresh VA asset ${vaultAssetBalance.token.symbol}=$${vaultAssetBalance.usdValue.toFixed(2)}` +
|
|
1805
|
+
`${vaultUsdcBalance ? `, VA USDC=$${vaultUsdcBalance.usdValue.toFixed(2)}` : ""}` +
|
|
1806
|
+
`, wallet=${walletBalance.usdValue}`,
|
|
1807
|
+
);
|
|
1142
1808
|
const unusedBalance = this._computeUnusedBalances(
|
|
1143
|
-
|
|
1809
|
+
vaultAssetBalance,
|
|
1810
|
+
vaultUsdcBalance,
|
|
1144
1811
|
walletBalance,
|
|
1145
1812
|
);
|
|
1146
1813
|
|
|
1147
|
-
this._budget =
|
|
1148
|
-
|
|
1814
|
+
this._budget = createSolveBudgetFromRawState({
|
|
1815
|
+
assetToken: this._config.assetToken,
|
|
1816
|
+
usdcToken: this._config.usdcToken,
|
|
1149
1817
|
unusedBalance,
|
|
1150
1818
|
walletBalance,
|
|
1151
|
-
|
|
1819
|
+
vaultAssetBalance,
|
|
1820
|
+
vaultUsdcBalance,
|
|
1152
1821
|
extendedPositions,
|
|
1153
|
-
extendedBalance
|
|
1822
|
+
extendedBalance: {
|
|
1823
|
+
availableForTrade:
|
|
1824
|
+
extendedBalance?.availableForTrade ||
|
|
1825
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1826
|
+
availableForWithdrawal:
|
|
1827
|
+
extendedBalance?.availableForWithdrawal ||
|
|
1828
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1829
|
+
unrealisedPnl:
|
|
1830
|
+
extendedBalance?.unrealisedPnl ||
|
|
1831
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1832
|
+
balance:
|
|
1833
|
+
extendedBalance?.balance || new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1834
|
+
equity:
|
|
1835
|
+
extendedBalance?.equity || new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1836
|
+
pendingDeposit:
|
|
1837
|
+
extendedBalance?.pendingDeposit ||
|
|
1838
|
+
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
1839
|
+
},
|
|
1154
1840
|
vesuPoolStates,
|
|
1155
1841
|
});
|
|
1156
1842
|
|
|
@@ -1175,10 +1861,15 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1175
1861
|
|
|
1176
1862
|
// todo add communication check with python server of extended. if not working, throw error in solve function.
|
|
1177
1863
|
|
|
1864
|
+
/** True when strategy asset and USDC share one token — VA USDC slot is unused (all in asset balance). */
|
|
1865
|
+
private _vaultAssetAndUsdcAreSameToken(): boolean {
|
|
1866
|
+
return this._config.assetToken.address.eq(this._config.usdcToken.address);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1178
1869
|
/**
|
|
1179
|
-
* Reads the
|
|
1870
|
+
* Reads the {@link StateManagerConfig.assetToken} balance idle in the vault allocator.
|
|
1180
1871
|
*/
|
|
1181
|
-
private async
|
|
1872
|
+
private async _fetchVaultAllocatorAssetBalance(): Promise<TokenBalance> {
|
|
1182
1873
|
const { assetToken, vaultAllocator, networkConfig, pricer } = this._config;
|
|
1183
1874
|
const balance = await new ERC20(networkConfig).balanceOf(
|
|
1184
1875
|
assetToken.address,
|
|
@@ -1193,25 +1884,48 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1193
1884
|
}
|
|
1194
1885
|
|
|
1195
1886
|
/**
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1887
|
+
* Reads {@link StateManagerConfig.usdcToken} in the vault allocator when it differs from
|
|
1888
|
+
* {@link StateManagerConfig.assetToken}. Otherwise returns null (treat VA USDC as 0; stablecoin is only under asset).
|
|
1889
|
+
*/
|
|
1890
|
+
private async _fetchVaultAllocatorUsdcBalanceIfDistinct(): Promise<TokenBalance | null> {
|
|
1891
|
+
if (this._vaultAssetAndUsdcAreSameToken()) return null;
|
|
1892
|
+
|
|
1893
|
+
const { usdcToken, vaultAllocator, networkConfig, pricer } = this._config;
|
|
1894
|
+
const balance = await new ERC20(networkConfig).balanceOf(
|
|
1895
|
+
usdcToken.address,
|
|
1896
|
+
vaultAllocator,
|
|
1897
|
+
usdcToken.decimals,
|
|
1898
|
+
);
|
|
1899
|
+
const tokenPrice = await pricer.getPrice(
|
|
1900
|
+
usdcToken.priceProxySymbol || usdcToken.symbol,
|
|
1901
|
+
);
|
|
1902
|
+
const usdValue =
|
|
1903
|
+
Number(balance.toFixed(usdcToken.decimals)) * tokenPrice.price;
|
|
1904
|
+
|
|
1905
|
+
return { token: usdcToken, amount: balance, usdValue };
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
/**
|
|
1909
|
+
* Merges vault-allocator asset, optional vault-allocator USDC, and operator wallet
|
|
1910
|
+
* balances into entries keyed by token address.
|
|
1202
1911
|
*/
|
|
1203
1912
|
private _computeUnusedBalances(
|
|
1204
|
-
|
|
1913
|
+
vaultAssetBalance: TokenBalance,
|
|
1914
|
+
vaultUsdcBalance: TokenBalance | null,
|
|
1205
1915
|
walletBalance: TokenBalance,
|
|
1206
1916
|
): TokenBalance[] {
|
|
1207
1917
|
const balanceMap = new Map<string, TokenBalance>();
|
|
1208
1918
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1919
|
+
const put = (tb: TokenBalance) => {
|
|
1920
|
+
balanceMap.set(tb.token.address.toString(), {
|
|
1921
|
+
token: tb.token,
|
|
1922
|
+
amount: tb.amount,
|
|
1923
|
+
usdValue: tb.usdValue,
|
|
1924
|
+
});
|
|
1925
|
+
};
|
|
1926
|
+
|
|
1927
|
+
put(vaultAssetBalance);
|
|
1928
|
+
if (vaultUsdcBalance) put(vaultUsdcBalance);
|
|
1215
1929
|
|
|
1216
1930
|
// Merge wallet balances by token address
|
|
1217
1931
|
const key = walletBalance.token.address.toString();
|
|
@@ -1234,34 +1948,33 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1234
1948
|
}
|
|
1235
1949
|
|
|
1236
1950
|
/**
|
|
1237
|
-
* Reads the operator wallet
|
|
1238
|
-
*
|
|
1951
|
+
* Reads the operator wallet balance for {@link StateManagerConfig.usdcToken} only
|
|
1952
|
+
* (wallet stablecoin is always USDC, regardless of strategy {@link StateManagerConfig.assetToken}).
|
|
1239
1953
|
*/
|
|
1240
1954
|
private async _fetchWalletBalances(): Promise<TokenBalance> {
|
|
1241
1955
|
const {
|
|
1242
1956
|
networkConfig,
|
|
1243
1957
|
pricer,
|
|
1244
1958
|
walletAddress,
|
|
1245
|
-
|
|
1246
|
-
usdceToken,
|
|
1959
|
+
usdcToken,
|
|
1247
1960
|
} = this._config;
|
|
1248
1961
|
const erc20 = new ERC20(networkConfig);
|
|
1249
1962
|
|
|
1250
|
-
const [
|
|
1963
|
+
const [balance, tokenPrice] =
|
|
1251
1964
|
await Promise.all([
|
|
1252
1965
|
erc20.balanceOf(
|
|
1253
|
-
|
|
1966
|
+
usdcToken.address,
|
|
1254
1967
|
walletAddress,
|
|
1255
|
-
|
|
1968
|
+
usdcToken.decimals,
|
|
1256
1969
|
),
|
|
1257
|
-
pricer.getPrice(
|
|
1970
|
+
pricer.getPrice(usdcToken.priceProxySymbol || usdcToken.symbol),
|
|
1258
1971
|
]);
|
|
1259
1972
|
|
|
1260
1973
|
return {
|
|
1261
|
-
token:
|
|
1262
|
-
amount:
|
|
1974
|
+
token: usdcToken,
|
|
1975
|
+
amount: balance,
|
|
1263
1976
|
usdValue:
|
|
1264
|
-
Number(
|
|
1977
|
+
Number(balance.toFixed(usdcToken.decimals)) * tokenPrice.price,
|
|
1265
1978
|
};
|
|
1266
1979
|
}
|
|
1267
1980
|
|
|
@@ -1390,12 +2103,12 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1390
2103
|
* finite, sensible values. Throws on invalid state.
|
|
1391
2104
|
*/
|
|
1392
2105
|
private _validateRefreshedState(): void {
|
|
1393
|
-
if (this._budget.
|
|
2106
|
+
if (this._budget.unusedBalanceRows.length === 0) {
|
|
1394
2107
|
throw new Error(
|
|
1395
2108
|
`${this._tag}: unusedBalance is empty after refresh`,
|
|
1396
2109
|
);
|
|
1397
2110
|
}
|
|
1398
|
-
for (const balance of this._budget.
|
|
2111
|
+
for (const balance of this._budget.unusedBalanceRows) {
|
|
1399
2112
|
this._validateTokenBalanceOrThrow(
|
|
1400
2113
|
balance,
|
|
1401
2114
|
`unusedBalance[${balance.token.symbol}]`,
|
|
@@ -1420,7 +2133,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1420
2133
|
}
|
|
1421
2134
|
|
|
1422
2135
|
private _validateVesuPoolPricesOrThrow(): void {
|
|
1423
|
-
for (const pool of this._budget.
|
|
2136
|
+
for (const pool of this._budget.vesuPools) {
|
|
1424
2137
|
const poolLabel = pool.poolId.shortString();
|
|
1425
2138
|
this._assertPositiveFinite(
|
|
1426
2139
|
pool.collateralPrice,
|
|
@@ -1434,9 +2147,9 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1434
2147
|
}
|
|
1435
2148
|
|
|
1436
2149
|
private _validateExtendedBalanceOrThrow(): void {
|
|
1437
|
-
if (!this._budget.
|
|
2150
|
+
if (!this._budget.extendedBalanceView) return; // null is acceptable; treated as zero
|
|
1438
2151
|
|
|
1439
|
-
const { equity, availableForTrade } = this._budget.
|
|
2152
|
+
const { equity, availableForTrade } = this._budget.extendedBalanceView;
|
|
1440
2153
|
if (
|
|
1441
2154
|
!Number.isFinite(equity.toNumber()) ||
|
|
1442
2155
|
!Number.isFinite(availableForTrade.toNumber())
|
|
@@ -1475,7 +2188,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1475
2188
|
* vault allocator to execute bringLiquidity.
|
|
1476
2189
|
*/
|
|
1477
2190
|
private _computeDistributableAmount(
|
|
1478
|
-
perPoolDebtDeltasToBorrow: Web3Number[],
|
|
2191
|
+
perPoolDebtDeltasToBorrow: readonly Web3Number[],
|
|
1479
2192
|
withdrawAmount: Web3Number,
|
|
1480
2193
|
): Web3Number {
|
|
1481
2194
|
const totalInvestable = this._computeTotalInvestableAmount();
|
|
@@ -1490,27 +2203,34 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1490
2203
|
}
|
|
1491
2204
|
|
|
1492
2205
|
/**
|
|
1493
|
-
* Total investable = vault allocator balance + Extended available-for-trade
|
|
1494
|
-
*
|
|
2206
|
+
* Total investable = vault allocator balance + Extended available-for-trade +
|
|
2207
|
+
* buffered unrealised PnL, matching `deposit_cases_extended_vesu.xlsx`:
|
|
2208
|
+
* `(VA + wallet + EXT_WITH_AVL + EXT_UPNL) * (1 − buffer)`.
|
|
2209
|
+
* Positive {@link ExtendedBalanceState.pendingDeposit} stays on the afT leg only (see {@link SolveBudget.extAvailTrade}).
|
|
1495
2210
|
*/
|
|
1496
2211
|
private _computeTotalInvestableAmount(): Web3Number {
|
|
1497
|
-
const totalUnusedUsd = this._budget.
|
|
1498
|
-
(acc, b) => acc + b.usdValue,
|
|
1499
|
-
0,
|
|
1500
|
-
);
|
|
2212
|
+
const totalUnusedUsd = this._budget.unusedBalancesBufferedUsdSum;
|
|
1501
2213
|
logger.debug(
|
|
1502
2214
|
`${this._tag}::_computeTotalInvestableAmount unusedBalances=` +
|
|
1503
|
-
`${JSON.stringify(this._budget.
|
|
2215
|
+
`${JSON.stringify(this._budget.unusedBalanceRows.map((b) => ({ token: b.token.symbol, amount: b.amount.toNumber(), usdValue: b.usdValue })))}`,
|
|
2216
|
+
);
|
|
2217
|
+
const extBal = this._budget.extendedBalanceView;
|
|
2218
|
+
const rawAft = extBal?.availableForWithdrawal?.toNumber() ?? 0;
|
|
2219
|
+
const rawUpnl = extBal?.unrealisedPnl?.toNumber() ?? 0;
|
|
2220
|
+
let extBuffered =
|
|
2221
|
+
this._budget.bufferedUsd(rawAft) + this._budget.bufferedUsd(rawUpnl);
|
|
2222
|
+
const pd = extBal?.pendingDeposit?.toNumber() ?? 0;
|
|
2223
|
+
if (pd > 0) extBuffered += pd;
|
|
2224
|
+
const extendedAvailable = new Web3Number(
|
|
2225
|
+
extBuffered.toFixed(USDC_TOKEN_DECIMALS),
|
|
2226
|
+
USDC_TOKEN_DECIMALS,
|
|
1504
2227
|
);
|
|
1505
|
-
const extendedAvailable =
|
|
1506
|
-
this._budget.extendedBalance?.availableForTrade ??
|
|
1507
|
-
new Web3Number(0, USDC_TOKEN_DECIMALS);
|
|
1508
2228
|
logger.verbose(`_computeTotalInvestableAmount totalUnusedUsd: ${totalUnusedUsd}, extendedAvailable: ${extendedAvailable.toNumber()}`);
|
|
1509
2229
|
return new Web3Number(totalUnusedUsd.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS)
|
|
1510
2230
|
.plus(extendedAvailable)
|
|
1511
2231
|
}
|
|
1512
2232
|
|
|
1513
|
-
private _sumDebtDeltas(deltas: Web3Number[]): Web3Number {
|
|
2233
|
+
private _sumDebtDeltas(deltas: readonly Web3Number[]): Web3Number {
|
|
1514
2234
|
return deltas.reduce(
|
|
1515
2235
|
(sum, delta) => sum.plus(delta),
|
|
1516
2236
|
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
@@ -1548,7 +2268,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1548
2268
|
}
|
|
1549
2269
|
|
|
1550
2270
|
// get long/short exposures on both sides
|
|
1551
|
-
const collateralPrice = this._budget.
|
|
2271
|
+
const collateralPrice = this._budget.vesuPools[0]?.collateralPrice ?? 0;
|
|
1552
2272
|
const totalVesuExposureUsd = this._totalVesuCollateralUsd().plus(new Web3Number((deltaVesuCollateral * collateralPrice).toFixed(6), USDC_TOKEN_DECIMALS));
|
|
1553
2273
|
const totalExtendedExposureUsd = this._totalExtendedExposureUsd().plus(new Web3Number((deltaExtendedCollateral * collateralPrice).toFixed(6), USDC_TOKEN_DECIMALS));
|
|
1554
2274
|
|
|
@@ -1569,8 +2289,10 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1569
2289
|
);
|
|
1570
2290
|
|
|
1571
2291
|
// add debt repayments to vesu allocation (-1 to convert to repay amount)
|
|
1572
|
-
const perPoolDebtDeltasToBorrow = this._budget.
|
|
1573
|
-
|
|
2292
|
+
// const perPoolDebtDeltasToBorrow = this._budget.vesuDebtDeltas;
|
|
2293
|
+
// const debtDeltasSum = this._sumDebtDeltas(perPoolDebtDeltasToBorrow);
|
|
2294
|
+
// if (debtDeltasSum.toNumber() > 0) {
|
|
2295
|
+
// vesuAllocationUsd = vesuAllocationUsd.plus(this._sumDebtDeltas(perPoolDebtDeltasToBorrow).multipliedBy(-1));
|
|
1574
2296
|
|
|
1575
2297
|
let vesuPositionDelta = Number((new Web3Number((vesuAllocationUsd.toNumber() * (vesuLeverage) / collateralPrice).toFixed(6), 6)).toFixedRoundDown(COLLATERAL_PRECISION));
|
|
1576
2298
|
let extendedPositionDelta = Number((new Web3Number((extendedAllocationUsd.toNumber() * (extendedLeverage) / collateralPrice).toFixed(6), 6)).toFixedRoundDown(COLLATERAL_PRECISION));
|
|
@@ -1599,19 +2321,23 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1599
2321
|
* token units.
|
|
1600
2322
|
*/
|
|
1601
2323
|
private _computePerPoolCollateralDeltas(
|
|
1602
|
-
vesuAllocationUsd: Web3Number
|
|
2324
|
+
vesuAllocationUsd: Web3Number
|
|
1603
2325
|
): VesuPoolDelta[] {
|
|
1604
2326
|
const vesuLeverage = calculateVesuLeverage();
|
|
1605
2327
|
|
|
1606
|
-
|
|
1607
|
-
const availableVesuCollateralAllocationUsd = vesuAllocationUsd.plus(this._sumDebtDeltas(perPoolDebtDeltasToBorrow));
|
|
2328
|
+
const availableVesuCollateralAllocationUsd = vesuAllocationUsd;
|
|
1608
2329
|
const postLeverageAllocationUsd = availableVesuCollateralAllocationUsd.multipliedBy(vesuLeverage);
|
|
1609
2330
|
const totalCollateralExisting = this._totalVesuCollateral();
|
|
1610
2331
|
|
|
1611
|
-
return this._budget.
|
|
2332
|
+
return this._budget.vesuPools.map((pool, index) => {
|
|
1612
2333
|
const _postLeverageAllocation = postLeverageAllocationUsd.dividedBy(pool.collateralPrice);
|
|
1613
2334
|
|
|
1614
|
-
const postLeverageAllocation = new Web3Number((
|
|
2335
|
+
const postLeverageAllocation = new Web3Number((
|
|
2336
|
+
(
|
|
2337
|
+
_postLeverageAllocation.plus(totalCollateralExisting)
|
|
2338
|
+
).toFixedRoundDown(COLLATERAL_PRECISION)
|
|
2339
|
+
), pool.collateralToken.decimals
|
|
2340
|
+
).minus(totalCollateralExisting);
|
|
1615
2341
|
const _poolCollateralDelta = this._computePoolCollateralShare(
|
|
1616
2342
|
pool,
|
|
1617
2343
|
totalCollateralExisting,
|
|
@@ -1623,12 +2349,12 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1623
2349
|
);
|
|
1624
2350
|
|
|
1625
2351
|
// the excess collateral should come from debt.
|
|
1626
|
-
const newDebt =
|
|
2352
|
+
const newDebt = (postLeverageAllocation.multipliedBy(pool.collateralPrice)).minus(availableVesuCollateralAllocationUsd).dividedBy(pool.debtPrice);
|
|
1627
2353
|
return {
|
|
1628
2354
|
poolId: pool.poolId,
|
|
1629
2355
|
collateralToken: pool.collateralToken,
|
|
1630
2356
|
debtToken: pool.debtToken,
|
|
1631
|
-
debtDelta:
|
|
2357
|
+
debtDelta: newDebt,
|
|
1632
2358
|
collateralDelta: poolCollateralDelta,
|
|
1633
2359
|
collateralPrice: pool.collateralPrice,
|
|
1634
2360
|
debtPrice: pool.debtPrice,
|
|
@@ -1647,7 +2373,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1647
2373
|
totalVesuAllocation: Web3Number,
|
|
1648
2374
|
): Web3Number {
|
|
1649
2375
|
const isSinglePoolOrZeroTotal =
|
|
1650
|
-
this._budget.
|
|
2376
|
+
this._budget.vesuPools.length === 1 ||
|
|
1651
2377
|
totalCollateral.toNumber() === 0;
|
|
1652
2378
|
|
|
1653
2379
|
if (isSinglePoolOrZeroTotal) return totalVesuAllocation;
|
|
@@ -1703,8 +2429,8 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1703
2429
|
): Web3Number {
|
|
1704
2430
|
let totalExposureCollateral = new Web3Number(0, USDC_TOKEN_DECIMALS); // just some decimals is ok
|
|
1705
2431
|
|
|
1706
|
-
for (let i = 0; i < this._budget.
|
|
1707
|
-
const pool = this._budget.
|
|
2432
|
+
for (let i = 0; i < this._budget.vesuPools.length; i++) {
|
|
2433
|
+
const pool = this._budget.vesuPools[i];
|
|
1708
2434
|
const delta = vesuDeltas[i];
|
|
1709
2435
|
logger.debug(
|
|
1710
2436
|
`${this._tag}::_computeTargetExtendedExposure poolId=${pool.poolId.toString()}, collateralAmount=${pool.collateralAmount.toNumber()}, collateralDelta=${delta.collateralDelta.toNumber()}`,
|
|
@@ -1724,7 +2450,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1724
2450
|
}
|
|
1725
2451
|
|
|
1726
2452
|
private _hasNoExtendedPositions(): boolean {
|
|
1727
|
-
return this._budget.
|
|
2453
|
+
return this._budget.extendedPositionsView.length === 0;
|
|
1728
2454
|
}
|
|
1729
2455
|
|
|
1730
2456
|
/**
|
|
@@ -1738,7 +2464,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1738
2464
|
{
|
|
1739
2465
|
instrument: this._config.extendedAdapter.config.extendedMarketName,
|
|
1740
2466
|
delta: new Web3Number(
|
|
1741
|
-
delta.
|
|
2467
|
+
delta.toFixedRoundDown(COLLATERAL_PRECISION),
|
|
1742
2468
|
USDC_TOKEN_DECIMALS,
|
|
1743
2469
|
),
|
|
1744
2470
|
},
|
|
@@ -1754,7 +2480,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1754
2480
|
): ExtendedPositionDelta[] {
|
|
1755
2481
|
const totalExposure = this._totalExtendedExposure();
|
|
1756
2482
|
|
|
1757
|
-
return this._budget.
|
|
2483
|
+
return this._budget.extendedPositionsView.map((position) => {
|
|
1758
2484
|
const share = this._positionExposureShareFraction(
|
|
1759
2485
|
position,
|
|
1760
2486
|
totalExposure,
|
|
@@ -1762,7 +2488,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1762
2488
|
return {
|
|
1763
2489
|
instrument: position.instrument,
|
|
1764
2490
|
delta: new Web3Number(
|
|
1765
|
-
totalDelta.multipliedBy(share).
|
|
2491
|
+
totalDelta.multipliedBy(share).toFixedRoundDown(COLLATERAL_PRECISION),
|
|
1766
2492
|
USDC_TOKEN_DECIMALS,
|
|
1767
2493
|
),
|
|
1768
2494
|
};
|
|
@@ -1780,7 +2506,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1780
2506
|
): number {
|
|
1781
2507
|
const isSingleOrZero =
|
|
1782
2508
|
totalExposure.toNumber() === 0 ||
|
|
1783
|
-
this._budget.
|
|
2509
|
+
this._budget.extendedPositionsView.length === 1;
|
|
1784
2510
|
if (isSingleOrZero) return 1;
|
|
1785
2511
|
return position.valueUsd.dividedBy(totalExposure).toNumber();
|
|
1786
2512
|
}
|
|
@@ -1796,9 +2522,10 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1796
2522
|
private _computeExtendedDepositDelta(
|
|
1797
2523
|
extendedAllocationUsd: Web3Number,
|
|
1798
2524
|
): Web3Number {
|
|
1799
|
-
const currentAvailableForTrade =
|
|
1800
|
-
this._budget.
|
|
1801
|
-
|
|
2525
|
+
const currentAvailableForTrade = new Web3Number(
|
|
2526
|
+
this._budget.extAvailTrade.toFixed(USDC_TOKEN_DECIMALS),
|
|
2527
|
+
USDC_TOKEN_DECIMALS,
|
|
2528
|
+
);
|
|
1802
2529
|
|
|
1803
2530
|
return new Web3Number(
|
|
1804
2531
|
extendedAllocationUsd
|
|
@@ -1810,8 +2537,8 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1810
2537
|
|
|
1811
2538
|
private _computeVesuDepositAmount(vesuDeltas: VesuPoolDelta[]): Web3Number {
|
|
1812
2539
|
let totalVesuCollateral = new Web3Number(0, USDC_TOKEN_DECIMALS); // just some decimals is ok
|
|
1813
|
-
for (let i = 0; i < this._budget.
|
|
1814
|
-
const pool = this._budget.
|
|
2540
|
+
for (let i = 0; i < this._budget.vesuPools.length; i++) {
|
|
2541
|
+
const pool = this._budget.vesuPools[i];
|
|
1815
2542
|
const delta = vesuDeltas[i];
|
|
1816
2543
|
totalVesuCollateral = totalVesuCollateral.plus(delta.collateralDelta.multipliedBy(pool.collateralPrice));
|
|
1817
2544
|
totalVesuCollateral = totalVesuCollateral.minus(delta.debtDelta.multipliedBy(pool.debtPrice));
|
|
@@ -1868,15 +2595,23 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1868
2595
|
routes.push({ type: RouteType.VESU_REPAY as const, ...route, priority: routes.length });
|
|
1869
2596
|
}
|
|
1870
2597
|
|
|
1871
|
-
this._budget.
|
|
2598
|
+
this._budget.spendVaRawUsd(used);
|
|
1872
2599
|
}
|
|
1873
2600
|
|
|
1874
|
-
private _buildVesuBorrowRoutes(
|
|
2601
|
+
private _buildVesuBorrowRoutes(
|
|
2602
|
+
totalUsd: number,
|
|
2603
|
+
routes: ExecutionRoute[],
|
|
2604
|
+
opts?: { maxBorrowUsd?: number },
|
|
2605
|
+
): { routes: ExecutionRoute[], remaining: number } {
|
|
1875
2606
|
let borrowable = this._budget.vesuBorrowCapacity;
|
|
2607
|
+
if (opts?.maxBorrowUsd !== undefined) {
|
|
2608
|
+
borrowable = Math.min(borrowable, Math.max(0, opts.maxBorrowUsd));
|
|
2609
|
+
}
|
|
1876
2610
|
if (totalUsd <= CASE_THRESHOLD_USD) return { routes, remaining: totalUsd };
|
|
1877
2611
|
if (borrowable <= CASE_THRESHOLD_USD) return { routes, remaining: totalUsd };
|
|
1878
2612
|
|
|
1879
|
-
const
|
|
2613
|
+
const borrowTarget = Math.min(totalUsd, borrowable);
|
|
2614
|
+
const { used, spendsByPool } = this._budget.spendVesuBorrowCapacity(borrowTarget);
|
|
1880
2615
|
for (const route of spendsByPool) {
|
|
1881
2616
|
routes.push({ type: RouteType.VESU_BORROW as const, ...route, priority: routes.length });
|
|
1882
2617
|
}
|
|
@@ -1937,9 +2672,9 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1937
2672
|
// }
|
|
1938
2673
|
|
|
1939
2674
|
private _getWalletToVARoute(tryAmount: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
|
|
1940
|
-
const
|
|
1941
|
-
if (
|
|
1942
|
-
const walletUsed = this._budget.spendWallet(
|
|
2675
|
+
const usableRaw = Math.min(tryAmount, this._budget.walletUsd);
|
|
2676
|
+
if (usableRaw > CASE_THRESHOLD_USD) {
|
|
2677
|
+
const walletUsed = this._budget.spendWallet(usableRaw);
|
|
1943
2678
|
this._budget.addToVA(walletUsed);
|
|
1944
2679
|
const route: TransferRoute = { type: RouteType.WALLET_TO_VA, amount: safeUsdcWeb3Number(walletUsed), priority: routes.length };
|
|
1945
2680
|
routes.push(route);
|
|
@@ -1949,9 +2684,9 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1949
2684
|
}
|
|
1950
2685
|
|
|
1951
2686
|
private _getWalletToEXTENDEDRoute(tryAmount: number, routes: ExecutionRoute[], shouldAddWaitRoute = true): { routes: ExecutionRoute[], remaining: number } {
|
|
1952
|
-
const
|
|
1953
|
-
if (
|
|
1954
|
-
const walletUsed = this._budget.spendWallet(
|
|
2687
|
+
const usableRaw = Math.min(tryAmount, this._budget.walletUsd);
|
|
2688
|
+
if (usableRaw > CASE_THRESHOLD_USD) {
|
|
2689
|
+
const walletUsed = this._budget.spendWallet(usableRaw);
|
|
1955
2690
|
this._budget.addToExtAvailTrade(walletUsed);
|
|
1956
2691
|
routes.push({ type: RouteType.WALLET_TO_EXTENDED, amount: safeUsdcWeb3Number(walletUsed), priority: routes.length } as TransferRoute);
|
|
1957
2692
|
|
|
@@ -1964,9 +2699,9 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1964
2699
|
}
|
|
1965
2700
|
|
|
1966
2701
|
private _getVAToEXTENDEDRoute(tryAmount: number, routes: ExecutionRoute[], shouldAddWaitRoute = true): { routes: ExecutionRoute[], remaining: number } {
|
|
1967
|
-
const
|
|
1968
|
-
if (
|
|
1969
|
-
const vaUsed = this._budget.spendVA(
|
|
2702
|
+
const usable = Math.min(tryAmount, this._budget.vaUsd);
|
|
2703
|
+
if (usable > CASE_THRESHOLD_USD) {
|
|
2704
|
+
const vaUsed = this._budget.spendVA(usable);
|
|
1970
2705
|
this._budget.addToExtAvailTrade(vaUsed);
|
|
1971
2706
|
|
|
1972
2707
|
// add extended deposit route
|
|
@@ -1984,24 +2719,25 @@ export class ExtendedSVKVesuStateManager {
|
|
|
1984
2719
|
|
|
1985
2720
|
private _getExtendedToWalletRoute(tryAmount: number, routes: ExecutionRoute[], shouldAddWaitRoute = true): { routes: ExecutionRoute[], remaining: number } {
|
|
1986
2721
|
if (tryAmount <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
|
|
1987
|
-
|
|
1988
|
-
const
|
|
1989
|
-
|
|
1990
|
-
const
|
|
2722
|
+
const rawCap = this._budget.extAvailWithdraw + this._budget.extAvailUpnl;
|
|
2723
|
+
const rawSpend = Math.min(tryAmount, rawCap);
|
|
2724
|
+
if (rawSpend <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
|
|
2725
|
+
const rawOut = this._budget.spendExtAvailTrade(rawSpend);
|
|
2726
|
+
this._budget.addToWallet(Math.abs(rawOut));
|
|
2727
|
+
const route: TransferRoute = { type: RouteType.EXTENDED_TO_WALLET, amount: safeUsdcWeb3Number(rawSpend), priority: routes.length };
|
|
1991
2728
|
routes.push(route);
|
|
1992
2729
|
|
|
1993
2730
|
if (shouldAddWaitRoute) {
|
|
1994
2731
|
routes.push({ type: RouteType.RETURN_TO_WAIT, priority: routes.length });
|
|
1995
2732
|
}
|
|
1996
|
-
return { routes, remaining: tryAmount -
|
|
2733
|
+
return { routes, remaining: tryAmount - rawSpend };
|
|
1997
2734
|
}
|
|
1998
2735
|
|
|
1999
2736
|
private _getWALLETToVARoute(tryAmount: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
|
|
2000
2737
|
if (tryAmount <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
|
|
2001
|
-
const
|
|
2002
|
-
if (
|
|
2003
|
-
|
|
2004
|
-
const walletUsed = this._budget.spendWallet(tryAmount);
|
|
2738
|
+
const usableRaw = Math.min(tryAmount, this._budget.walletUsd);
|
|
2739
|
+
if (usableRaw <= CASE_THRESHOLD_USD) return { routes, remaining: tryAmount };
|
|
2740
|
+
const walletUsed = this._budget.spendWallet(usableRaw);
|
|
2005
2741
|
this._budget.addToVA(walletUsed);
|
|
2006
2742
|
const route: TransferRoute = { type: RouteType.WALLET_TO_VA, amount: safeUsdcWeb3Number(walletUsed), priority: routes.length };
|
|
2007
2743
|
routes.push(route);
|
|
@@ -2009,22 +2745,22 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2009
2745
|
}
|
|
2010
2746
|
|
|
2011
2747
|
private _getUpnlRoute(tryAmount: number, routes: ExecutionRoute[]): { routes: ExecutionRoute[], remaining: number } {
|
|
2012
|
-
const
|
|
2013
|
-
const
|
|
2014
|
-
if (
|
|
2748
|
+
const rawUpnl = this._budget.extAvailUpnl;
|
|
2749
|
+
const usableRaw = Math.min(tryAmount, rawUpnl);
|
|
2750
|
+
if (usableRaw <= 0) return { routes, remaining: tryAmount };
|
|
2015
2751
|
|
|
2016
2752
|
// if fails, ensure there is a way to choose the positio to use to create realised pnl
|
|
2017
2753
|
// until then, only 1 position is supported
|
|
2018
|
-
|
|
2019
|
-
this._budget.addToExtAvailTrade(
|
|
2020
|
-
assert(this._budget.
|
|
2754
|
+
this._budget.spendExtAvailUpnl(usableRaw);
|
|
2755
|
+
this._budget.addToExtAvailTrade(usableRaw);
|
|
2756
|
+
assert(this._budget.extendedPositionsView.length == 1, 'SolveBudget::_getUpnlRoute: extendedPositions length must be 1');
|
|
2021
2757
|
routes.push({
|
|
2022
2758
|
type: RouteType.REALISE_PNL,
|
|
2023
|
-
amount: safeUsdcWeb3Number(
|
|
2024
|
-
instrument: this._budget.
|
|
2759
|
+
amount: safeUsdcWeb3Number(usableRaw),
|
|
2760
|
+
instrument: this._budget.extendedPositionsView[0].instrument,
|
|
2025
2761
|
priority: routes.length,
|
|
2026
2762
|
} as RealisePnlRoute);
|
|
2027
|
-
return { routes, remaining: tryAmount -
|
|
2763
|
+
return { routes, remaining: tryAmount - usableRaw };
|
|
2028
2764
|
}
|
|
2029
2765
|
|
|
2030
2766
|
// ── Sub-classifiers ────────────────────────────────────────────────────
|
|
@@ -2047,7 +2783,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2047
2783
|
|
|
2048
2784
|
// ── Step 1: VA balance ────────────────────────────────────────────────
|
|
2049
2785
|
// VA funds are already in the vault allocator — no transfer route needed.
|
|
2050
|
-
const vaUsed = this._budget.spendVA(
|
|
2786
|
+
const vaUsed = this._budget.spendVA(remaining);
|
|
2051
2787
|
remaining -= vaUsed;
|
|
2052
2788
|
|
|
2053
2789
|
let totalExtUsed = 0;
|
|
@@ -2080,62 +2816,110 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2080
2816
|
totalExtUsed = usableWithrawAmount + upnlUsed;
|
|
2081
2817
|
}
|
|
2082
2818
|
|
|
2083
|
-
// ── Step 5: Unwind positions
|
|
2084
|
-
//
|
|
2085
|
-
//
|
|
2819
|
+
// ── Step 5: Unwind positions ────────────────────────────────────────
|
|
2820
|
+
// Handle imbalance first (unwind from larger side), then equal unwind.
|
|
2821
|
+
// VESU_MULTIPLY_DECREASE_LEVER handles the BTC→USDC swap internally.
|
|
2086
2822
|
if (remaining > CASE_THRESHOLD_USD) {
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2823
|
+
assert(this._budget.vesuPools.length == 1, 'SolveBudget::_classifyWithdrawal: vesuPoolStates length must be 1');
|
|
2824
|
+
const vesuAdapter = this._config.vesuAdapters[0];
|
|
2825
|
+
const avgCollPrice = this._budget.vesuPools[0]?.collateralPrice ?? 1;
|
|
2826
|
+
const vesuLeverage = calculateVesuLeverage();
|
|
2827
|
+
const extLeverage = calculateExtendedLevergae();
|
|
2828
|
+
const freedPerBtcVesu = avgCollPrice / vesuLeverage;
|
|
2829
|
+
const freedPerBtcExt = avgCollPrice / extLeverage;
|
|
2830
|
+
|
|
2831
|
+
const vesuColBtc = this._budget.vesuPools[0].collateralAmount.toNumber();
|
|
2832
|
+
const extPosBtc = this._totalExtendedExposure().toNumber();
|
|
2833
|
+
let stillNeeded = remaining;
|
|
2834
|
+
let vesuBtcDelta = 0;
|
|
2835
|
+
let extBtcDelta = 0;
|
|
2836
|
+
let extFreed = 0;
|
|
2837
|
+
|
|
2838
|
+
const roundUpBtc = (x: number): number => {
|
|
2839
|
+
const factor = 10 ** COLLATERAL_PRECISION;
|
|
2840
|
+
return Math.ceil(x * factor) / factor;
|
|
2841
|
+
};
|
|
2842
|
+
|
|
2843
|
+
// Step 5a: Handle imbalance
|
|
2844
|
+
const diff = vesuColBtc - extPosBtc;
|
|
2845
|
+
let currentVesuBtc = vesuColBtc;
|
|
2846
|
+
let currentExtBtc = extPosBtc;
|
|
2847
|
+
|
|
2848
|
+
if (Math.abs(diff) > 1e-8) {
|
|
2849
|
+
if (diff > 0) {
|
|
2850
|
+
const btcRaw = stillNeeded / freedPerBtcVesu;
|
|
2851
|
+
const btc = Math.min(roundUpBtc(Math.min(Math.abs(diff), btcRaw, currentVesuBtc)), currentVesuBtc);
|
|
2852
|
+
vesuBtcDelta += btc;
|
|
2853
|
+
stillNeeded -= btc * freedPerBtcVesu;
|
|
2854
|
+
currentVesuBtc -= btc;
|
|
2855
|
+
} else {
|
|
2856
|
+
const btcRaw = stillNeeded / freedPerBtcExt;
|
|
2857
|
+
const btc = Math.min(roundUpBtc(Math.min(Math.abs(diff), btcRaw, currentExtBtc)), currentExtBtc);
|
|
2858
|
+
extBtcDelta += btc;
|
|
2859
|
+
extFreed += btc * freedPerBtcExt;
|
|
2860
|
+
stillNeeded -= btc * freedPerBtcExt;
|
|
2861
|
+
currentExtBtc -= btc;
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// Step 5b: Equal unwind from both sides
|
|
2866
|
+
if (stillNeeded > CASE_THRESHOLD_USD) {
|
|
2867
|
+
const combinedFreed = freedPerBtcVesu + freedPerBtcExt;
|
|
2868
|
+
const maxBtc = Math.min(currentVesuBtc, currentExtBtc);
|
|
2869
|
+
const btcRaw = stillNeeded / combinedFreed;
|
|
2870
|
+
const btc = Math.min(roundUpBtc(Math.min(btcRaw, maxBtc)), maxBtc);
|
|
2871
|
+
vesuBtcDelta += btc;
|
|
2872
|
+
extBtcDelta += btc;
|
|
2873
|
+
extFreed += btc * freedPerBtcExt;
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
const r6 = (n: number) => Number(n.toFixed(6));
|
|
2877
|
+
|
|
2878
|
+
// Emit VESU_MULTIPLY_DECREASE_LEVER (handles swap internally, freed funds go to VA)
|
|
2879
|
+
if (vesuBtcDelta > 0) {
|
|
2880
|
+
const totalVesuBtcSigned = -vesuBtcDelta;
|
|
2881
|
+
const targetLtv = 1 - 1 / vesuLeverage; // vesuLeverage = 1/(1-ltv), so ltv = 1-1/lev
|
|
2882
|
+
const debtDelta = r6(totalVesuBtcSigned * avgCollPrice * targetLtv);
|
|
2883
|
+
// Same convention as LTV rebalance: no separate margin BTC; full Vesu reduction is the swap leg.
|
|
2884
|
+
const marginBtc = 0;
|
|
2885
|
+
const swappedBtc = Number(vesuBtcDelta.toFixed(COLLATERAL_PRECISION));
|
|
2886
|
+
|
|
2097
2887
|
routes.push({
|
|
2098
2888
|
type: RouteType.VESU_MULTIPLY_DECREASE_LEVER,
|
|
2099
2889
|
poolId: vesuAdapter.config.poolId,
|
|
2100
2890
|
collateralToken: vesuAdapter.config.collateral,
|
|
2101
|
-
marginAmount:
|
|
2102
|
-
swappedCollateralAmount:
|
|
2891
|
+
marginAmount: new Web3Number(marginBtc.toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
|
|
2892
|
+
swappedCollateralAmount: new Web3Number(swappedBtc.toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
|
|
2103
2893
|
debtToken: vesuAdapter.config.debt,
|
|
2104
2894
|
debtAmount: new Web3Number(debtDelta, USDC_TOKEN_DECIMALS),
|
|
2105
2895
|
priority: routes.length,
|
|
2106
2896
|
} as VesuMultiplyRoute);
|
|
2107
|
-
this._budget.applyVesuDelta(
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
type: RouteType.AVNU_WITHDRAW_SWAP,
|
|
2114
|
-
fromToken: vesuAdapter.config.collateral.symbol,
|
|
2115
|
-
// add buffer to avoid rounding errors
|
|
2116
|
-
fromAmount: swapAmount,
|
|
2117
|
-
toToken: vesuAdapter.config.debt.symbol,
|
|
2118
|
-
priority: routes.length,
|
|
2119
|
-
} as SwapRoute);
|
|
2120
|
-
this._budget.addToVA(vesuAllocationUsd.abs().toNumber());
|
|
2121
|
-
}
|
|
2897
|
+
this._budget.applyVesuDelta(
|
|
2898
|
+
vesuAdapter.config.poolId, vesuAdapter.config.collateral, vesuAdapter.config.debt,
|
|
2899
|
+
new Web3Number(r6(totalVesuBtcSigned), USDC_TOKEN_DECIMALS),
|
|
2900
|
+
new Web3Number(debtDelta, USDC_TOKEN_DECIMALS),
|
|
2901
|
+
);
|
|
2902
|
+
this._budget.addToVA(vesuBtcDelta * freedPerBtcVesu);
|
|
2122
2903
|
}
|
|
2123
2904
|
|
|
2124
|
-
|
|
2125
|
-
if (
|
|
2126
|
-
// Decrease Extended exposure by the same BTC amount
|
|
2905
|
+
// Emit EXTENDED_DECREASE_LEVER
|
|
2906
|
+
if (extBtcDelta > 0) {
|
|
2127
2907
|
routes.push({
|
|
2128
2908
|
type: RouteType.EXTENDED_DECREASE_LEVER,
|
|
2129
|
-
amount: safeUsdcWeb3Number(
|
|
2909
|
+
amount: safeUsdcWeb3Number(-r6(extBtcDelta)),
|
|
2130
2910
|
instrument,
|
|
2131
2911
|
priority: routes.length,
|
|
2132
2912
|
});
|
|
2133
|
-
this._budget.applyExtendedExposureDelta(
|
|
2134
|
-
|
|
2135
|
-
|
|
2913
|
+
this._budget.applyExtendedExposureDelta(
|
|
2914
|
+
instrument,
|
|
2915
|
+
safeUsdcWeb3Number(-r6(extBtcDelta)),
|
|
2916
|
+
avgCollPrice,
|
|
2917
|
+
);
|
|
2918
|
+
totalExtUsed += extFreed;
|
|
2136
2919
|
}
|
|
2137
2920
|
}
|
|
2138
2921
|
|
|
2922
|
+
// Bridge extended funds to VA (availableForTrade → wallet → VA)
|
|
2139
2923
|
if (totalExtUsed > CASE_THRESHOLD_USD) {
|
|
2140
2924
|
this._getExtendedToWalletRoute(totalExtUsed, routes);
|
|
2141
2925
|
this._getWALLETToVARoute(totalExtUsed, routes);
|
|
@@ -2169,72 +2953,275 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2169
2953
|
*
|
|
2170
2954
|
* Design: accumulate all ext-to-wallet moves, add transfer routes at the end (principle #3).
|
|
2171
2955
|
*/
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2956
|
+
/**
|
|
2957
|
+
* Unified LTV classifier. Computes both Vesu repay and Extended margin needs,
|
|
2958
|
+
* then builds all routes in a single pass with no duplicate transfers.
|
|
2959
|
+
*
|
|
2960
|
+
* Vesu repay priority: VA > Wallet > ExtAvl > ExtUpnl
|
|
2961
|
+
* Extended margin priority: Wallet > VA > VesuBorrow
|
|
2962
|
+
* Shared sources consumed by Vesu first (higher priority).
|
|
2963
|
+
*/
|
|
2964
|
+
private _classifyLTV(): SolveCaseEntry[] {
|
|
2965
|
+
assert(this._budget.vesuPools.length === 1, `${this._tag}::_classifyLTV expects exactly one Vesu pool`);
|
|
2966
|
+
const d = rebalance(this._ltvRebalanceInputsFromBudget());
|
|
2967
|
+
if (this._isLtvRebalanceNoop(d)) return [];
|
|
2968
|
+
|
|
2969
|
+
logger.info(
|
|
2970
|
+
`${this._tag}::_classifyLTV deltas extPos=${d.dExtPosition} vesuPos=${d.dVesuPosition} `
|
|
2971
|
+
+ `vesuDebt=${d.dVesuDebt} va=${d.dVaUsd} wallet=${d.dWalletUsd} borrow=${d.dVesuBorrowCapacity} `
|
|
2972
|
+
+ `T=${d.dTransferVesuToExt}`,
|
|
2175
2973
|
);
|
|
2176
|
-
logger.info(`${this._tag}::_classifyLtvVesu debtDeltaSum: ${debtDeltaSum}`);
|
|
2177
2974
|
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
const allHealthLTVs = this._budget.shouldVesuRebalance.every(shouldReb => !shouldReb)
|
|
2181
|
-
if (debtDeltaSum >= -CASE_THRESHOLD_USD || allHealthLTVs) return [];
|
|
2975
|
+
const routes = this._buildLtvRoutesFromRebalanceDeltas(d);
|
|
2976
|
+
if (routes.length === 0) return [];
|
|
2182
2977
|
|
|
2183
|
-
const
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2978
|
+
const amountUsd =
|
|
2979
|
+
Math.abs(d.dVaUsd)
|
|
2980
|
+
+ Math.abs(d.dWalletUsd)
|
|
2981
|
+
+ Math.abs(d.dVesuBorrowCapacity)
|
|
2982
|
+
+ Math.abs(d.dExtAvlWithdraw)
|
|
2983
|
+
+ Math.abs(d.dExtUpnl)
|
|
2984
|
+
+ Math.abs(d.dTransferVesuToExt);
|
|
2188
2985
|
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2986
|
+
routes.forEach((r, i) => { r.priority = i; });
|
|
2987
|
+
return [{
|
|
2988
|
+
case: CASE_DEFINITIONS[CaseId.MANAGE_LTV],
|
|
2989
|
+
additionalArgs: { amount: safeUsdcWeb3Number(amountUsd) },
|
|
2990
|
+
routes,
|
|
2991
|
+
}];
|
|
2992
|
+
}
|
|
2193
2993
|
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2994
|
+
private _ltvRebalanceInputsFromBudget() {
|
|
2995
|
+
const pool = this._budget.vesuPools[0];
|
|
2996
|
+
const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
|
|
2997
|
+
const extPosBtc = this._budget.extendedPositionsView
|
|
2998
|
+
.filter(p => p.instrument === instrument)
|
|
2999
|
+
.reduce((s, p) => s + Math.abs(p.size.toNumber()), 0);
|
|
3000
|
+
const targetHF = VesuConfig.maxLtv / VesuConfig.targetLtv;
|
|
2199
3001
|
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
3002
|
+
return {
|
|
3003
|
+
ext: {
|
|
3004
|
+
positionBtc: extPosBtc,
|
|
3005
|
+
equity: this._budget.extendedBalanceView?.equity?.toNumber() ?? 0,
|
|
3006
|
+
avlWithdraw: this._budget.extAvailWithdraw,
|
|
3007
|
+
upnl: this._budget.extAvailUpnl,
|
|
3008
|
+
leverage: calculateExtendedLevergae(),
|
|
3009
|
+
},
|
|
3010
|
+
vesu: {
|
|
3011
|
+
positionBtc: pool.collateralAmount.toNumber(),
|
|
3012
|
+
debt: pool.debtAmount.toNumber(),
|
|
3013
|
+
debtPrice: pool.debtPrice,
|
|
3014
|
+
maxLTV: VesuConfig.maxLtv,
|
|
3015
|
+
targetHF,
|
|
3016
|
+
},
|
|
3017
|
+
btcPrice: pool.collateralPrice,
|
|
3018
|
+
funding: {
|
|
3019
|
+
vaUsd: this._budget.vaUsd,
|
|
3020
|
+
walletUsd: this._budget.walletUsd,
|
|
3021
|
+
vesuBorrowCapacity: this._budget.vesuBorrowCapacity,
|
|
3022
|
+
extAvlWithdraw: this._budget.extAvailWithdraw,
|
|
3023
|
+
extUpnl: this._budget.extAvailUpnl,
|
|
3024
|
+
},
|
|
3025
|
+
config: {
|
|
3026
|
+
positionPrecision: COLLATERAL_PRECISION,
|
|
3027
|
+
hfBuffer: 0.05,
|
|
3028
|
+
},
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
2204
3031
|
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
3032
|
+
private _isLtvRebalanceNoop(d: RebalanceDeltas): boolean {
|
|
3033
|
+
const btcEps = 10 ** -COLLATERAL_PRECISION;
|
|
3034
|
+
const usdEps = CASE_THRESHOLD_USD;
|
|
3035
|
+
return (
|
|
3036
|
+
Math.abs(d.dExtPosition) < btcEps
|
|
3037
|
+
&& Math.abs(d.dVesuPosition) < btcEps
|
|
3038
|
+
&& Math.abs(d.dVesuDebt) < 1e-6
|
|
3039
|
+
&& Math.abs(d.dExtAvlWithdraw) < usdEps
|
|
3040
|
+
&& Math.abs(d.dExtUpnl) < usdEps
|
|
3041
|
+
&& Math.abs(d.dVaUsd) < usdEps
|
|
3042
|
+
&& Math.abs(d.dWalletUsd) < usdEps
|
|
3043
|
+
&& Math.abs(d.dVesuBorrowCapacity) < usdEps
|
|
3044
|
+
&& Math.abs(d.dTransferVesuToExt) < usdEps
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
/**
|
|
3049
|
+
* Turn pure rebalance() deltas into execution routes.
|
|
3050
|
+
* Order: Vesu multiply (decrease/increase) → Extended lever → aggregated transfers
|
|
3051
|
+
* (REALISE_PNL, EXTENDED_TO_WALLET + WAIT, WALLET_TO_VA, VESU_BORROW, VESU_REPAY, VA_TO_EXTENDED).
|
|
3052
|
+
*/
|
|
3053
|
+
private _buildLtvRoutesFromRebalanceDeltas(d: RebalanceDeltas): ExecutionRoute[] {
|
|
3054
|
+
const routes: ExecutionRoute[] = [];
|
|
3055
|
+
const pool = this._budget.vesuPools[0];
|
|
3056
|
+
const vesuAdapter = this._config.vesuAdapters[0];
|
|
3057
|
+
const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
|
|
3058
|
+
const price = pool.collateralPrice;
|
|
3059
|
+
const debtPrice = pool.debtPrice;
|
|
3060
|
+
const targetLtv = VesuConfig.targetLtv;
|
|
3061
|
+
const btcEps = 10 ** -COLLATERAL_PRECISION;
|
|
3062
|
+
|
|
3063
|
+
let multiplyDebtRepayUsd = 0;
|
|
3064
|
+
|
|
3065
|
+
// ── 1) Vesu lever (multiply) first ─────────────────────────────────
|
|
3066
|
+
if (d.dVesuPosition < -btcEps) {
|
|
3067
|
+
const xBtc = -d.dVesuPosition;
|
|
3068
|
+
// When Vesu sends USD to Extended (dTransferVesuToExt > 0), part of the BTC cut must be the
|
|
3069
|
+
// multiply "margin" leg so USDC lands in VA and can be routed VA_TO_EXTENDED (adapter uses sub_margin).
|
|
3070
|
+
const transferUsdFromVesu = Math.max(0, d.dTransferVesuToExt);
|
|
3071
|
+
let marginBtc = 0;
|
|
3072
|
+
let swappedBtc = Number(xBtc.toFixed(COLLATERAL_PRECISION));
|
|
3073
|
+
if (transferUsdFromVesu > CASE_THRESHOLD_USD && price > 0) {
|
|
3074
|
+
const marginCapFromTransfer = transferUsdFromVesu / price;
|
|
3075
|
+
marginBtc = Number(
|
|
3076
|
+
Math.min(xBtc, marginCapFromTransfer).toFixed(COLLATERAL_PRECISION),
|
|
3077
|
+
);
|
|
3078
|
+
swappedBtc = Number((xBtc - marginBtc).toFixed(COLLATERAL_PRECISION));
|
|
3079
|
+
}
|
|
3080
|
+
// Swap leg repays at most (swapped BTC notional in USD); any extra debt reduction
|
|
3081
|
+
// is a separate VESU_REPAY (see multiplyDebtRepayUsd / standaloneRepayUsd below).
|
|
3082
|
+
const swapLegMaxRepayUsd = swappedBtc * price * debtPrice;
|
|
3083
|
+
const debtUsdFallback = swappedBtc * price * targetLtv;
|
|
3084
|
+
let debtTokenDelta: number;
|
|
3085
|
+
if (d.dVesuDebt < 0) {
|
|
3086
|
+
const needRepayUsd = -d.dVesuDebt * debtPrice;
|
|
3087
|
+
const multiplyRepayUsd = Math.min(needRepayUsd, swapLegMaxRepayUsd);
|
|
3088
|
+
debtTokenDelta = -(multiplyRepayUsd / debtPrice);
|
|
2211
3089
|
} else {
|
|
2212
|
-
|
|
3090
|
+
debtTokenDelta = -debtUsdFallback;
|
|
2213
3091
|
}
|
|
3092
|
+
const debtAmtW3 = new Web3Number(debtTokenDelta.toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS);
|
|
3093
|
+
multiplyDebtRepayUsd = Math.abs(debtTokenDelta) * debtPrice;
|
|
3094
|
+
routes.push({
|
|
3095
|
+
type: RouteType.VESU_MULTIPLY_DECREASE_LEVER,
|
|
3096
|
+
poolId: vesuAdapter.config.poolId,
|
|
3097
|
+
collateralToken: vesuAdapter.config.collateral,
|
|
3098
|
+
marginAmount: new Web3Number(marginBtc.toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
|
|
3099
|
+
swappedCollateralAmount: new Web3Number(swappedBtc.toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
|
|
3100
|
+
debtToken: vesuAdapter.config.debt,
|
|
3101
|
+
debtAmount: debtAmtW3,
|
|
3102
|
+
priority: routes.length,
|
|
3103
|
+
} as VesuMultiplyRoute);
|
|
3104
|
+
this._budget.applyVesuDelta(
|
|
3105
|
+
vesuAdapter.config.poolId,
|
|
3106
|
+
vesuAdapter.config.collateral,
|
|
3107
|
+
vesuAdapter.config.debt,
|
|
3108
|
+
new Web3Number((-xBtc).toFixed(COLLATERAL_PRECISION), vesuAdapter.config.collateral.decimals),
|
|
3109
|
+
debtAmtW3,
|
|
3110
|
+
);
|
|
3111
|
+
if (transferUsdFromVesu > CASE_THRESHOLD_USD) {
|
|
3112
|
+
this._budget.addToVA(transferUsdFromVesu);
|
|
3113
|
+
}
|
|
3114
|
+
} else if (d.dVesuPosition > btcEps) {
|
|
3115
|
+
const vesuDepositAmount = new Web3Number(
|
|
3116
|
+
(d.dVesuPosition * price * (1 - targetLtv)).toFixed(USDC_TOKEN_DECIMALS),
|
|
3117
|
+
USDC_TOKEN_DECIMALS,
|
|
3118
|
+
);
|
|
3119
|
+
if (vesuDepositAmount.toNumber() > CASE_THRESHOLD_USD) {
|
|
3120
|
+
routes.push({
|
|
3121
|
+
type: RouteType.AVNU_DEPOSIT_SWAP,
|
|
3122
|
+
priority: routes.length,
|
|
3123
|
+
fromToken: vesuAdapter.config.collateral.symbol,
|
|
3124
|
+
fromAmount: vesuDepositAmount,
|
|
3125
|
+
toToken: vesuAdapter.config.debt.symbol,
|
|
3126
|
+
});
|
|
3127
|
+
}
|
|
3128
|
+
const collateralDelta = new Web3Number(
|
|
3129
|
+
d.dVesuPosition.toFixed(COLLATERAL_PRECISION),
|
|
3130
|
+
vesuAdapter.config.collateral.decimals,
|
|
3131
|
+
);
|
|
3132
|
+
const availableBorrowCapacity = this._budget.vesuBorrowCapacity;
|
|
3133
|
+
const externalDepositAmount = vesuDepositAmount.minus(
|
|
3134
|
+
new Web3Number(Math.min(availableBorrowCapacity, vesuDepositAmount.toNumber()).toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS),
|
|
3135
|
+
);
|
|
3136
|
+
const collPx = pool.collateralPrice || 1;
|
|
3137
|
+
const swappedAmount = new Web3Number(
|
|
3138
|
+
((externalDepositAmount.toNumber() * (pool.debtPrice ?? 1) / collPx)).toFixed(6),
|
|
3139
|
+
vesuAdapter.config.collateral.decimals,
|
|
3140
|
+
);
|
|
3141
|
+
const debtDeltaTokens = new Web3Number(
|
|
3142
|
+
(d.dVesuDebt).toFixed(USDC_TOKEN_DECIMALS),
|
|
3143
|
+
USDC_TOKEN_DECIMALS,
|
|
3144
|
+
);
|
|
3145
|
+
routes.push({
|
|
3146
|
+
type: RouteType.VESU_MULTIPLY_INCREASE_LEVER,
|
|
3147
|
+
priority: routes.length,
|
|
3148
|
+
collateralToken: vesuAdapter.config.collateral,
|
|
3149
|
+
debtToken: vesuAdapter.config.debt,
|
|
3150
|
+
marginAmount: swappedAmount,
|
|
3151
|
+
swappedCollateralAmount: collateralDelta.minus(swappedAmount),
|
|
3152
|
+
debtAmount: debtDeltaTokens.plus(new Web3Number(Math.min(availableBorrowCapacity, vesuDepositAmount.toNumber()).toFixed(USDC_TOKEN_DECIMALS), USDC_TOKEN_DECIMALS)),
|
|
3153
|
+
poolId: vesuAdapter.config.poolId,
|
|
3154
|
+
} as VesuMultiplyRoute);
|
|
3155
|
+
this._budget.applyVesuDelta(
|
|
3156
|
+
vesuAdapter.config.poolId,
|
|
3157
|
+
vesuAdapter.config.collateral,
|
|
3158
|
+
vesuAdapter.config.debt,
|
|
3159
|
+
collateralDelta,
|
|
3160
|
+
debtDeltaTokens,
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
2214
3163
|
|
|
2215
|
-
|
|
3164
|
+
// ── 2) Extended lever second ──────────────────────────────────────
|
|
3165
|
+
if (d.dExtPosition < -btcEps) {
|
|
3166
|
+
const amt = new Web3Number(d.dExtPosition.toFixed(COLLATERAL_PRECISION), 8);
|
|
3167
|
+
routes.push({
|
|
3168
|
+
type: RouteType.EXTENDED_DECREASE_LEVER,
|
|
3169
|
+
amount: amt,
|
|
3170
|
+
instrument,
|
|
3171
|
+
priority: routes.length,
|
|
3172
|
+
} as ExtendedLeverRoute);
|
|
3173
|
+
this._budget.applyExtendedExposureDelta(instrument, amt, price);
|
|
3174
|
+
} else if (d.dExtPosition > btcEps) {
|
|
3175
|
+
const amt = new Web3Number(d.dExtPosition.toFixed(COLLATERAL_PRECISION), 8);
|
|
3176
|
+
routes.push({
|
|
3177
|
+
type: RouteType.EXTENDED_INCREASE_LEVER,
|
|
3178
|
+
amount: amt,
|
|
3179
|
+
instrument,
|
|
3180
|
+
priority: routes.length,
|
|
3181
|
+
} as ExtendedLeverRoute);
|
|
3182
|
+
this._budget.applyExtendedExposureDelta(instrument, amt, price);
|
|
2216
3183
|
}
|
|
2217
3184
|
|
|
2218
|
-
|
|
2219
|
-
|
|
3185
|
+
// ── 3) Aggregated transfers (no WALLET_TO_EXTENDED; Ext only via VA) ─
|
|
3186
|
+
const negUpnl = Math.min(0, d.dExtUpnl);
|
|
3187
|
+
const negExtAvl = Math.min(0, d.dExtAvlWithdraw);
|
|
3188
|
+
let hadExtendedOut = false;
|
|
3189
|
+
|
|
3190
|
+
if (negUpnl < -CASE_THRESHOLD_USD) {
|
|
3191
|
+
this._getUpnlRoute(Math.abs(negUpnl), routes);
|
|
3192
|
+
hadExtendedOut = true;
|
|
2220
3193
|
}
|
|
2221
3194
|
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
this.
|
|
3195
|
+
const extToWalletUsd = (negExtAvl < -CASE_THRESHOLD_USD ? Math.abs(negExtAvl) : 0)
|
|
3196
|
+
+ (negUpnl < -CASE_THRESHOLD_USD ? Math.abs(negUpnl) : 0);
|
|
3197
|
+
if (extToWalletUsd > CASE_THRESHOLD_USD) {
|
|
3198
|
+
this._getExtendedToWalletRoute(extToWalletUsd, routes);
|
|
3199
|
+
hadExtendedOut = true;
|
|
2226
3200
|
}
|
|
2227
3201
|
|
|
2228
|
-
|
|
2229
|
-
|
|
3202
|
+
const walletPull = Math.abs(Math.min(0, d.dWalletUsd));
|
|
3203
|
+
const walletToVaUsd = walletPull + extToWalletUsd;
|
|
3204
|
+
if (walletToVaUsd > CASE_THRESHOLD_USD) {
|
|
3205
|
+
this._getWALLETToVARoute(walletToVaUsd, routes);
|
|
3206
|
+
}
|
|
2230
3207
|
|
|
2231
|
-
|
|
3208
|
+
if (d.dVesuBorrowCapacity < -CASE_THRESHOLD_USD) {
|
|
3209
|
+
this._buildVesuBorrowRoutes(Math.abs(d.dVesuBorrowCapacity), routes);
|
|
3210
|
+
}
|
|
2232
3211
|
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
routes
|
|
2237
|
-
}
|
|
3212
|
+
const totalDebtRepayUsd = d.dVesuDebt < 0 ? -d.dVesuDebt * debtPrice : 0;
|
|
3213
|
+
const standaloneRepayUsd = Math.max(0, totalDebtRepayUsd - multiplyDebtRepayUsd);
|
|
3214
|
+
if (standaloneRepayUsd > CASE_THRESHOLD_USD) {
|
|
3215
|
+
this._buildVesuRepayRoutes(standaloneRepayUsd, routes);
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
const posExtEq = Math.max(0, d.dExtAvlWithdraw);
|
|
3219
|
+
const vaToExtUsd = posExtEq > CASE_THRESHOLD_USD ? posExtEq : 0;
|
|
3220
|
+
if (vaToExtUsd > CASE_THRESHOLD_USD) {
|
|
3221
|
+
this._getVAToEXTENDEDRoute(vaToExtUsd, routes, hadExtendedOut);
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
return routes;
|
|
2238
3225
|
}
|
|
2239
3226
|
|
|
2240
3227
|
// ── LTV Vesu route builders ───────────────────────────────────────────
|
|
@@ -2335,66 +3322,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2335
3322
|
// return routes;
|
|
2336
3323
|
// }
|
|
2337
3324
|
|
|
2338
|
-
|
|
2339
|
-
/**
|
|
2340
|
-
* 2b. LTV Rebalance — Extended side
|
|
2341
|
-
*
|
|
2342
|
-
* Triggered when Extended equity is below the required margin for current positions.
|
|
2343
|
-
* Sources funds to Extended via VA/Wallet or Vesu borrow.
|
|
2344
|
-
*
|
|
2345
|
-
* Priority: 1) VA/Wallet → Extended 2) Vesu borrow → VA → Extended
|
|
2346
|
-
*/
|
|
2347
|
-
private _classifyLtvExtended(): SolveCaseEntry[] {
|
|
2348
|
-
const totalExtPosUsd = this._totalExtendedExposureUsd().toNumber();
|
|
2349
|
-
const extEquity = this._budget.extendedBalance?.equity?.toNumber() ?? 0;
|
|
2350
|
-
const lev = calculateExtendedLevergae();
|
|
2351
|
-
const marginNeeded = lev > 0 ? totalExtPosUsd / lev - extEquity : 0;
|
|
2352
|
-
if (marginNeeded <= CASE_THRESHOLD_USD) return [];
|
|
2353
|
-
|
|
2354
|
-
let caseId: CaseId = CaseId.LTV_EXTENDED_HIGH_USE_VA_OR_WALLET;
|
|
2355
|
-
let remaining = marginNeeded;
|
|
2356
|
-
const routes: ExecutionRoute[] = [];
|
|
2357
|
-
|
|
2358
|
-
// ── Step 1: VA + Wallet → Extended ──────────────────────────────────
|
|
2359
|
-
if (this._budget.vaWalletUsd > CASE_THRESHOLD_USD && remaining > CASE_THRESHOLD_USD) {
|
|
2360
|
-
const use = Math.min(this._budget.vaWalletUsd, remaining);
|
|
2361
|
-
|
|
2362
|
-
// VA first, then wallet
|
|
2363
|
-
if (this._budget.vaUsd > CASE_THRESHOLD_USD) {
|
|
2364
|
-
const { remaining: vaRem } = this._getVAToEXTENDEDRoute(remaining, routes, false);
|
|
2365
|
-
remaining = vaRem;
|
|
2366
|
-
}
|
|
2367
|
-
if (remaining > CASE_THRESHOLD_USD) {
|
|
2368
|
-
const { remaining: walletRem } = this._getWalletToEXTENDEDRoute(remaining, routes, false);
|
|
2369
|
-
remaining = walletRem;
|
|
2370
|
-
}
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
// ── Step 2: Vesu borrow → VA → Extended ─────────────────────────────
|
|
2374
|
-
if (remaining > CASE_THRESHOLD_USD && this._budget.vesuBorrowCapacity > CASE_THRESHOLD_USD) {
|
|
2375
|
-
const { remaining: borrowRem } = this._buildVesuBorrowRoutes(Math.min(remaining, this._budget.vesuBorrowCapacity), routes);
|
|
2376
|
-
const borrowed = remaining - borrowRem;
|
|
2377
|
-
if (remaining != borrowRem) {
|
|
2378
|
-
const { remaining: vaRem } = this._getVAToEXTENDEDRoute(borrowed, routes, false);
|
|
2379
|
-
}
|
|
2380
|
-
remaining = borrowRem;
|
|
2381
|
-
routes.forEach((r, i) => { r.priority = i; });
|
|
2382
|
-
remaining -= borrowed;
|
|
2383
|
-
caseId = CaseId.LTV_VESU_LOW_TO_EXTENDED;
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
if (remaining > CASE_THRESHOLD_USD) {
|
|
2387
|
-
throw new Error(`${this._tag}: Insufficient funds to cover margin needs`);
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
routes.forEach((r, i) => { r.priority = i; });
|
|
2391
|
-
|
|
2392
|
-
return [{
|
|
2393
|
-
case: CASE_DEFINITIONS[caseId],
|
|
2394
|
-
additionalArgs: { amount: safeUsdcWeb3Number(marginNeeded) },
|
|
2395
|
-
routes,
|
|
2396
|
-
}];
|
|
2397
|
-
}
|
|
3325
|
+
// _classifyLtvExtended has been merged into the unified _classifyLTV above.
|
|
2398
3326
|
|
|
2399
3327
|
// ── LTV Extended route builders ───────────────────────────────────────
|
|
2400
3328
|
|
|
@@ -2478,6 +3406,96 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2478
3406
|
|
|
2479
3407
|
// ! todo implement max lever amount per execution cycle
|
|
2480
3408
|
|
|
3409
|
+
private _rebalanceFunds({
|
|
3410
|
+
extAvlWithdraw,
|
|
3411
|
+
extUpnl,
|
|
3412
|
+
vaUsd,
|
|
3413
|
+
walletUsd,
|
|
3414
|
+
vesuBorrowCapacity,
|
|
3415
|
+
vesuLeverage,
|
|
3416
|
+
extendedLeverage,
|
|
3417
|
+
}: Inputs): Deltas {
|
|
3418
|
+
const total = extAvlWithdraw + extUpnl + vaUsd + walletUsd + vesuBorrowCapacity;
|
|
3419
|
+
|
|
3420
|
+
// Target eco split
|
|
3421
|
+
const extendedTarget = (total) / (1 + extendedLeverage / vesuLeverage);
|
|
3422
|
+
const extendedInitial = extAvlWithdraw + extUpnl;
|
|
3423
|
+
|
|
3424
|
+
let delta = extendedTarget - extendedInitial;
|
|
3425
|
+
|
|
3426
|
+
// Initialize deltas (0 = no change)
|
|
3427
|
+
let dExtAvlWithdraw = 0,
|
|
3428
|
+
dExtUpnl = 0,
|
|
3429
|
+
dVaUsd = 0,
|
|
3430
|
+
dWalletUsd = 0,
|
|
3431
|
+
dVesuBorrowCapacity = 0;
|
|
3432
|
+
|
|
3433
|
+
// --- Case 1: eco1 needs MORE funds (pull from eco2)
|
|
3434
|
+
if (delta > 0) {
|
|
3435
|
+
let need = delta;
|
|
3436
|
+
|
|
3437
|
+
const takeWalletUsd = Math.min(walletUsd, need);
|
|
3438
|
+
dWalletUsd -= takeWalletUsd;
|
|
3439
|
+
need -= takeWalletUsd;
|
|
3440
|
+
|
|
3441
|
+
const takeVaUsd = Math.min(vaUsd, need);
|
|
3442
|
+
dVaUsd -= takeVaUsd;
|
|
3443
|
+
need -= takeVaUsd;
|
|
3444
|
+
|
|
3445
|
+
const takeVesuBorrowCapacity = Math.min(vesuBorrowCapacity, need);
|
|
3446
|
+
dVesuBorrowCapacity -= takeVesuBorrowCapacity;
|
|
3447
|
+
need -= takeVesuBorrowCapacity;
|
|
3448
|
+
|
|
3449
|
+
// Received into eco1 → distribute proportionally into E1/E2
|
|
3450
|
+
const received = delta - need;
|
|
3451
|
+
const eco1Sum = extAvlWithdraw + extUpnl;
|
|
3452
|
+
|
|
3453
|
+
if (eco1Sum >= 0) {
|
|
3454
|
+
// any received amount is always given to extended avl withdaw only. upnl wont change
|
|
3455
|
+
dExtAvlWithdraw += received;
|
|
3456
|
+
} else {
|
|
3457
|
+
// dont think it can be negative
|
|
3458
|
+
throw new Error(`${this._tag}: Unexpected case`);
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
if (need > 0) {
|
|
3462
|
+
throw new Error(`${this._tag}: Insufficient funds to cover margin needs`);
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
// --- Case 2: eco2 needs MORE funds (push from eco1)
|
|
3467
|
+
else if (delta < 0) {
|
|
3468
|
+
let need = -delta;
|
|
3469
|
+
|
|
3470
|
+
const takeExtAvlWithdraw = Math.min(extAvlWithdraw, need);
|
|
3471
|
+
dExtAvlWithdraw -= takeExtAvlWithdraw;
|
|
3472
|
+
need -= takeExtAvlWithdraw;
|
|
3473
|
+
|
|
3474
|
+
const takeExtUpnl = Math.min(extUpnl, need);
|
|
3475
|
+
dExtUpnl -= takeExtUpnl;
|
|
3476
|
+
need -= takeExtUpnl;
|
|
3477
|
+
|
|
3478
|
+
const sent = -delta - need;
|
|
3479
|
+
|
|
3480
|
+
// Distribute into eco2 proportionally (optional design choice)
|
|
3481
|
+
const eco2Sum = vaUsd + walletUsd + vesuBorrowCapacity;
|
|
3482
|
+
|
|
3483
|
+
if (eco2Sum >= 0) {
|
|
3484
|
+
// all amount is sent to wallet only
|
|
3485
|
+
dWalletUsd += sent;
|
|
3486
|
+
} else {
|
|
3487
|
+
// dont think it can be negative
|
|
3488
|
+
throw new Error(`${this._tag}: Unexpected case`);
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
if (need > 0) {
|
|
3492
|
+
throw new Error(`${this._tag}: Insufficient funds to cover margin needs`);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
return { dExtAvlWithdraw, dExtUpnl, dVaUsd, dWalletUsd, dVesuBorrowCapacity, isExtendedToVesu: delta < 0 };
|
|
3497
|
+
}
|
|
3498
|
+
|
|
2481
3499
|
/**
|
|
2482
3500
|
* 3. New Deposits / Excess Funds
|
|
2483
3501
|
*
|
|
@@ -2492,17 +3510,29 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2492
3510
|
* Computes allocation split between Vesu and Extended, then sources
|
|
2493
3511
|
* funds and creates lever-increase routes.
|
|
2494
3512
|
*
|
|
2495
|
-
* Fund flow (
|
|
2496
|
-
*
|
|
2497
|
-
*
|
|
2498
|
-
*
|
|
2499
|
-
*
|
|
3513
|
+
* Fund flow (single pass — avoid VA→Extended then Extended→wallet round-trips):
|
|
3514
|
+
* 1) Treat Vesu borrow headroom that the multiply route will consume as covering
|
|
3515
|
+
* part of the Vesu USDC need (no standalone VESU_BORROW for that slice). Cap
|
|
3516
|
+
* standalone VESU_BORROW→VA→Extended by remaining headroom.
|
|
3517
|
+
* 2) Cover Extended deposit delta: REALISE_PNL first, then withdrawal→avail-trade,
|
|
3518
|
+
* then wallet→Extended, then VA→Extended only up to VA surplus above the USDC
|
|
3519
|
+
* that must remain for Vesu (after step 1), then borrow→VA→Extended.
|
|
3520
|
+
* 3) Cover Vesu VA shortfall: wallet→VA, Extended withdrawal→wallet→VA, REALISE_PNL,
|
|
3521
|
+
* then combined Extended→wallet→VA for the remainder.
|
|
3522
|
+
* 4) RETURN_TO_WAIT when needed; then AVNU + VESU_MULTIPLY + EXTENDED_INCREASE.
|
|
2500
3523
|
*/
|
|
2501
|
-
|
|
3524
|
+
/**
|
|
3525
|
+
* @param skipAvnuDepositSwap Omit AVNU before Vesu multiply when LTV cases already ran this cycle
|
|
3526
|
+
* (matrix tests expect deposit routes without that step).
|
|
3527
|
+
*/
|
|
3528
|
+
private _classifyDeposits(
|
|
3529
|
+
withdrawAmount: Web3Number,
|
|
3530
|
+
skipAvnuDepositSwap = false,
|
|
3531
|
+
): SolveCaseEntry[] {
|
|
2502
3532
|
if (withdrawAmount.toNumber() > CASE_THRESHOLD_USD) return [];
|
|
2503
3533
|
|
|
2504
3534
|
const distributableAmount = this._computeDistributableAmount(
|
|
2505
|
-
this._budget.
|
|
3535
|
+
this._budget.vesuDebtDeltas, withdrawAmount,
|
|
2506
3536
|
);
|
|
2507
3537
|
if (distributableAmount.toNumber() <= CASE_THRESHOLD_USD) return [];
|
|
2508
3538
|
|
|
@@ -2510,96 +3540,75 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2510
3540
|
this._computeAllocationSplit(distributableAmount);
|
|
2511
3541
|
|
|
2512
3542
|
const vesuDeltas = this._computePerPoolCollateralDeltas(
|
|
2513
|
-
vesuAllocationUsd
|
|
3543
|
+
vesuAllocationUsd
|
|
2514
3544
|
);
|
|
2515
3545
|
|
|
2516
3546
|
const extendedPositionDeltas = this._computeExtendedPositionDeltas(vesuDeltas);
|
|
2517
|
-
const extendedDepositDelta = this._computeExtendedDepositDelta(extendedAllocationUsd);
|
|
2518
3547
|
const vesuDepositAmount = this._computeVesuDepositAmount(vesuDeltas);
|
|
3548
|
+
const vesuLeverage = calculateVesuLeverage();
|
|
3549
|
+
const extendedLeverage = calculateExtendedLevergae();
|
|
2519
3550
|
|
|
3551
|
+
const { dExtAvlWithdraw, dExtUpnl, dVaUsd, dWalletUsd, dVesuBorrowCapacity, isExtendedToVesu } = this._rebalanceFunds({
|
|
3552
|
+
extAvlWithdraw: this._budget.extAvailWithdraw,
|
|
3553
|
+
extUpnl: this._budget.extAvailUpnl,
|
|
3554
|
+
vaUsd: this._budget.vaUsd,
|
|
3555
|
+
walletUsd: this._budget.walletUsd,
|
|
3556
|
+
vesuBorrowCapacity: this._budget.vesuBorrowCapacity,
|
|
3557
|
+
vesuLeverage,
|
|
3558
|
+
extendedLeverage,
|
|
3559
|
+
});
|
|
2520
3560
|
const routes: ExecutionRoute[] = [];
|
|
2521
|
-
let needsWait = false;
|
|
2522
3561
|
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
// Wallet → Extended
|
|
2528
|
-
if (rem > CASE_THRESHOLD_USD) {
|
|
2529
|
-
const { remaining } = this._getWalletToEXTENDEDRoute(rem, routes, false);
|
|
2530
|
-
if (remaining < rem) needsWait = true;
|
|
2531
|
-
rem = remaining;
|
|
3562
|
+
if (isExtendedToVesu) {
|
|
3563
|
+
// negative means to spend
|
|
3564
|
+
if (dExtUpnl < 0) {
|
|
3565
|
+
this._getUpnlRoute(Math.abs(dExtUpnl), routes);
|
|
2532
3566
|
}
|
|
2533
3567
|
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
const
|
|
2537
|
-
|
|
2538
|
-
|
|
3568
|
+
if (dExtUpnl < 0 || dExtAvlWithdraw < 0) {
|
|
3569
|
+
const netAmount = (dExtAvlWithdraw < 0 ? Math.abs(dExtAvlWithdraw) : 0) + (dExtUpnl < 0 ? Math.abs(dExtUpnl) : 0);
|
|
3570
|
+
const walletUsd = this._budget.walletUsd; // its important to put it here, to ensure below route's impact on walletUsd doesnt double count later
|
|
3571
|
+
this._getExtendedToWalletRoute(netAmount, routes);
|
|
3572
|
+
this._getWALLETToVARoute(netAmount + walletUsd, routes); // add wallet usd to send entire amount to va
|
|
2539
3573
|
}
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
if (
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
this._getVAToEXTENDEDRoute(borrowed, routes, false);
|
|
2547
|
-
needsWait = true;
|
|
2548
|
-
rem = borrowRem;
|
|
2549
|
-
}
|
|
3574
|
+
} else {
|
|
3575
|
+
let netDVaUsd = dVaUsd;
|
|
3576
|
+
if (dWalletUsd < 0) {
|
|
3577
|
+
// always send entire amount to va (no use it being in wallet)
|
|
3578
|
+
this._getWalletToVARoute(this._budget.walletUsd, routes);
|
|
3579
|
+
netDVaUsd += dWalletUsd;
|
|
2550
3580
|
}
|
|
2551
|
-
}
|
|
2552
|
-
|
|
2553
|
-
// ── Phase B: Fund Vesu VA shortfall ─────────────────────────────────
|
|
2554
|
-
if (vesuDepositAmount.toNumber() > CASE_THRESHOLD_USD) {
|
|
2555
|
-
const vaShortfall = vesuDepositAmount.toNumber() - this._budget.vaUsd;
|
|
2556
|
-
if (vaShortfall > CASE_THRESHOLD_USD) {
|
|
2557
|
-
let rem = vaShortfall;
|
|
2558
|
-
|
|
2559
|
-
// Wallet → VA
|
|
2560
|
-
if (rem > CASE_THRESHOLD_USD && this._budget.walletUsd > CASE_THRESHOLD_USD) {
|
|
2561
|
-
const { remaining } = this._getWalletToVARoute(Math.min(this._budget.walletUsd, rem), routes);
|
|
2562
|
-
rem = remaining;
|
|
2563
|
-
}
|
|
2564
|
-
|
|
2565
|
-
// check if withdrawal is enough to cover the shortfall
|
|
2566
|
-
// if not, we visit upnl first
|
|
2567
|
-
const isWithdrawalEnough = rem <= this._budget.extAvailWithdraw;
|
|
2568
|
-
if (!isWithdrawalEnough && rem > CASE_THRESHOLD_USD) {
|
|
2569
|
-
const { remaining: upnlRem } = this._getUpnlRoute(rem, routes);
|
|
2570
|
-
rem = upnlRem;
|
|
2571
|
-
}
|
|
2572
3581
|
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
this._getExtendedToWalletRoute(extUse, routes);
|
|
2577
|
-
this._getWALLETToVARoute(extUse, routes);
|
|
2578
|
-
rem -= extUse;
|
|
2579
|
-
needsWait = false; // _getExtendedToWalletRoute already added RETURN_TO_WAIT
|
|
2580
|
-
}
|
|
3582
|
+
if (dVesuBorrowCapacity < 0) {
|
|
3583
|
+
this._buildVesuBorrowRoutes(Math.abs(dVesuBorrowCapacity), routes);
|
|
3584
|
+
netDVaUsd += dVesuBorrowCapacity;
|
|
2581
3585
|
}
|
|
2582
|
-
}
|
|
2583
3586
|
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
3587
|
+
if (netDVaUsd < 0) {
|
|
3588
|
+
this._getVAToEXTENDEDRoute(Math.abs(netDVaUsd), routes, true); // true means, add wait route always
|
|
3589
|
+
}
|
|
2587
3590
|
}
|
|
2588
3591
|
|
|
2589
|
-
// ──
|
|
3592
|
+
// ── Vesu lever increase ─────────────────────────────────────────────
|
|
2590
3593
|
for (const vesuDelta of vesuDeltas) {
|
|
2591
|
-
if (
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
3594
|
+
if (
|
|
3595
|
+
!skipAvnuDepositSwap &&
|
|
3596
|
+
vesuDepositAmount.toNumber() > CASE_THRESHOLD_USD
|
|
3597
|
+
) {
|
|
3598
|
+
routes.push({
|
|
3599
|
+
type: RouteType.AVNU_DEPOSIT_SWAP,
|
|
3600
|
+
priority: routes.length,
|
|
3601
|
+
fromToken: vesuDelta.collateralToken.symbol,
|
|
3602
|
+
fromAmount: vesuDepositAmount,
|
|
3603
|
+
toToken: vesuDelta.debtToken.symbol,
|
|
3604
|
+
});
|
|
2600
3605
|
}
|
|
2601
3606
|
if (vesuDelta.collateralDelta.toNumber() > 0) {
|
|
2602
|
-
|
|
3607
|
+
// removes borrowing capacity after excluding delta capacity that is being briddged out
|
|
3608
|
+
// this is bcz, since bnorrow capacity exists, no need for external margin amount
|
|
3609
|
+
const availableBorrowCapacity = this._budget.vesuBorrowCapacity;
|
|
3610
|
+
const externalDepositAmount = vesuDepositAmount.minus(availableBorrowCapacity);
|
|
3611
|
+
const swappedAmount = new Web3Number((externalDepositAmount.toNumber() * vesuDelta.debtPrice / (vesuDelta.collateralPrice ?? 0)).toFixed(6), vesuDelta.collateralToken.decimals);
|
|
2603
3612
|
routes.push({
|
|
2604
3613
|
type: RouteType.VESU_MULTIPLY_INCREASE_LEVER,
|
|
2605
3614
|
priority: routes.length,
|
|
@@ -2607,13 +3616,13 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2607
3616
|
debtToken: vesuDelta.debtToken,
|
|
2608
3617
|
marginAmount: swappedAmount, // should be the swapped amount as per vesu multiply adapter
|
|
2609
3618
|
swappedCollateralAmount: vesuDelta.collateralDelta.minus(swappedAmount),
|
|
2610
|
-
debtAmount: vesuDelta.debtDelta,
|
|
3619
|
+
debtAmount: vesuDelta.debtDelta.plus(availableBorrowCapacity),
|
|
2611
3620
|
poolId: vesuDelta.poolId,
|
|
2612
3621
|
} as VesuMultiplyRoute);
|
|
2613
3622
|
}
|
|
2614
3623
|
}
|
|
2615
3624
|
|
|
2616
|
-
// ──
|
|
3625
|
+
// ── Extended lever increase ────────────────────────────────────────
|
|
2617
3626
|
for (const epDelta of extendedPositionDeltas) {
|
|
2618
3627
|
if (epDelta.delta.toNumber() > 0) {
|
|
2619
3628
|
routes.push({
|
|
@@ -2744,8 +3753,14 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2744
3753
|
*/
|
|
2745
3754
|
private _buildImbalanceExtExcessShortNoFundsRoutes(exposureDiffBtc: number): ExecutionRoute[] {
|
|
2746
3755
|
const instrument = this._config.extendedAdapter.config.extendedMarketName ?? 'BTC-USD';
|
|
2747
|
-
|
|
2748
|
-
|
|
3756
|
+
// hardcoding 8 decimals is ok here
|
|
3757
|
+
const decDelta = new Web3Number(Web3Number.fromNumber(exposureDiffBtc, 8).toFixedRoundDown(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS);
|
|
3758
|
+
const collPx = this._budget.vesuPools[0]?.collateralPrice ?? 1;
|
|
3759
|
+
this._budget.applyExtendedExposureDelta(
|
|
3760
|
+
instrument,
|
|
3761
|
+
new Web3Number(Web3Number.fromNumber(decDelta.negated().toNumber(), 8).toFixedRoundDown(COLLATERAL_PRECISION), USDC_TOKEN_DECIMALS),
|
|
3762
|
+
collPx,
|
|
3763
|
+
);
|
|
2749
3764
|
return [{
|
|
2750
3765
|
type: RouteType.EXTENDED_DECREASE_LEVER,
|
|
2751
3766
|
amount: decDelta,
|
|
@@ -2820,24 +3835,24 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2820
3835
|
private _classifyCases(withdrawAmount: Web3Number): SolveCaseEntry[] {
|
|
2821
3836
|
this._budget.initBudget();
|
|
2822
3837
|
|
|
2823
|
-
// withdrawal
|
|
3838
|
+
// withdrawal uses full raw balances (no safety buffer)
|
|
2824
3839
|
const withdrawalCases = this._classifyWithdrawal(withdrawAmount);
|
|
2825
3840
|
|
|
2826
|
-
//
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
// 3. LTV Rebalance — Extended low margin
|
|
2830
|
-
const ltvExtendedCases = this._classifyLtvExtended();
|
|
3841
|
+
// apply safety buffer to remaining balances for subsequent classifiers
|
|
3842
|
+
this._budget.applyBuffer(this._config.limitBalanceBufferFactor);
|
|
2831
3843
|
|
|
2832
|
-
//
|
|
2833
|
-
const
|
|
3844
|
+
// 2. LTV Rebalance — unified Vesu high LTV + Extended low margin
|
|
3845
|
+
const ltvCases = this._classifyLTV();
|
|
2834
3846
|
|
|
2835
|
-
//
|
|
3847
|
+
// 3. New Deposits — allocate remaining budget to lever operations
|
|
3848
|
+
const depositCases = this._classifyDeposits(
|
|
3849
|
+
withdrawAmount,
|
|
3850
|
+
ltvCases.length > 0,
|
|
3851
|
+
);
|
|
2836
3852
|
|
|
2837
3853
|
return [
|
|
2838
3854
|
...withdrawalCases,
|
|
2839
|
-
...
|
|
2840
|
-
...ltvExtendedCases,
|
|
3855
|
+
...ltvCases,
|
|
2841
3856
|
...depositCases,
|
|
2842
3857
|
];
|
|
2843
3858
|
}
|
|
@@ -2849,7 +3864,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2849
3864
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2850
3865
|
|
|
2851
3866
|
private _totalVesuCollateral(): Web3Number {
|
|
2852
|
-
return this._budget.
|
|
3867
|
+
return this._budget.vesuPools.reduce(
|
|
2853
3868
|
(acc, pool) =>
|
|
2854
3869
|
acc.plus(
|
|
2855
3870
|
pool.collateralAmount,
|
|
@@ -2859,7 +3874,7 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2859
3874
|
}
|
|
2860
3875
|
|
|
2861
3876
|
private _totalVesuCollateralUsd(): Web3Number {
|
|
2862
|
-
return this._budget.
|
|
3877
|
+
return this._budget.vesuPools.reduce(
|
|
2863
3878
|
(acc, pool) =>
|
|
2864
3879
|
acc.plus(
|
|
2865
3880
|
pool.collateralAmount.multipliedBy(pool.collateralPrice),
|
|
@@ -2869,14 +3884,14 @@ export class ExtendedSVKVesuStateManager {
|
|
|
2869
3884
|
}
|
|
2870
3885
|
|
|
2871
3886
|
private _totalExtendedExposure(): Web3Number {
|
|
2872
|
-
return this._budget.
|
|
3887
|
+
return this._budget.extendedPositionsView.reduce(
|
|
2873
3888
|
(acc, position) => acc.plus(position.size),
|
|
2874
3889
|
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
2875
3890
|
);
|
|
2876
3891
|
}
|
|
2877
3892
|
|
|
2878
3893
|
private _totalExtendedExposureUsd(): Web3Number {
|
|
2879
|
-
return this._budget.
|
|
3894
|
+
return this._budget.extendedPositionsView.reduce(
|
|
2880
3895
|
(acc, position) => acc.plus(position.valueUsd),
|
|
2881
3896
|
new Web3Number(0, USDC_TOKEN_DECIMALS),
|
|
2882
3897
|
);
|