wayfinder-paths 0.1.19__py3-none-any.whl → 0.1.20__py3-none-any.whl
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.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/__init__.py +0 -2
- wayfinder_paths/adapters/balance_adapter/README.md +59 -45
- wayfinder_paths/adapters/balance_adapter/adapter.py +0 -21
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -14
- wayfinder_paths/adapters/brap_adapter/README.md +61 -184
- wayfinder_paths/adapters/brap_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/brap_adapter/adapter.py +0 -147
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +0 -15
- wayfinder_paths/adapters/hyperlend_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +0 -9
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +0 -17
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +3 -312
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +1 -71
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +0 -57
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +0 -17
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +2 -42
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +1 -9
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +15 -47
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +0 -7
- wayfinder_paths/adapters/ledger_adapter/README.md +54 -74
- wayfinder_paths/adapters/ledger_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/ledger_adapter/adapter.py +0 -106
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +0 -12
- wayfinder_paths/adapters/moonwell_adapter/README.md +67 -106
- wayfinder_paths/adapters/moonwell_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +9 -121
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +84 -83
- wayfinder_paths/adapters/pool_adapter/README.md +30 -51
- wayfinder_paths/adapters/pool_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/pool_adapter/adapter.py +0 -19
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -8
- wayfinder_paths/adapters/token_adapter/README.md +41 -49
- wayfinder_paths/adapters/token_adapter/adapter.py +0 -32
- wayfinder_paths/adapters/token_adapter/test_adapter.py +1 -12
- wayfinder_paths/conftest.py +0 -8
- wayfinder_paths/core/__init__.py +0 -2
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -22
- wayfinder_paths/core/adapters/__init__.py +0 -5
- wayfinder_paths/core/adapters/models.py +0 -5
- wayfinder_paths/core/analytics/__init__.py +0 -2
- wayfinder_paths/core/analytics/bootstrap.py +0 -16
- wayfinder_paths/core/analytics/stats.py +0 -7
- wayfinder_paths/core/analytics/test_analytics.py +5 -34
- wayfinder_paths/core/clients/BRAPClient.py +0 -35
- wayfinder_paths/core/clients/ClientManager.py +0 -51
- wayfinder_paths/core/clients/HyperlendClient.py +0 -77
- wayfinder_paths/core/clients/LedgerClient.py +2 -122
- wayfinder_paths/core/clients/PoolClient.py +0 -2
- wayfinder_paths/core/clients/TokenClient.py +0 -39
- wayfinder_paths/core/clients/WalletClient.py +0 -15
- wayfinder_paths/core/clients/WayfinderClient.py +0 -24
- wayfinder_paths/core/clients/__init__.py +0 -4
- wayfinder_paths/core/clients/protocols.py +25 -98
- wayfinder_paths/core/config.py +0 -24
- wayfinder_paths/core/constants/__init__.py +0 -7
- wayfinder_paths/core/constants/base.py +2 -9
- wayfinder_paths/core/constants/erc20_abi.py +0 -5
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -7
- wayfinder_paths/core/constants/moonwell_abi.py +0 -35
- wayfinder_paths/core/engine/StrategyJob.py +0 -32
- wayfinder_paths/core/strategies/Strategy.py +0 -99
- wayfinder_paths/core/strategies/__init__.py +0 -2
- wayfinder_paths/core/utils/__init__.py +0 -1
- wayfinder_paths/core/utils/erc20_service.py +0 -1
- wayfinder_paths/core/utils/evm_helpers.py +0 -50
- wayfinder_paths/core/utils/transaction.py +0 -1
- wayfinder_paths/run_strategy.py +0 -46
- wayfinder_paths/scripts/create_strategy.py +0 -17
- wayfinder_paths/scripts/make_wallets.py +1 -4
- wayfinder_paths/strategies/basis_trading_strategy/README.md +71 -163
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +0 -24
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +36 -400
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +15 -64
- wayfinder_paths/strategies/basis_trading_strategy/types.py +0 -4
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +65 -56
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +4 -27
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -10
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +71 -72
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +23 -227
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +120 -113
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +64 -59
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +4 -44
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +2 -35
- wayfinder_paths/templates/adapter/README.md +107 -46
- wayfinder_paths/templates/adapter/adapter.py +0 -9
- wayfinder_paths/templates/adapter/test_adapter.py +0 -19
- wayfinder_paths/templates/strategy/README.md +113 -59
- wayfinder_paths/templates/strategy/strategy.py +0 -22
- wayfinder_paths/templates/strategy/test_strategy.py +0 -28
- wayfinder_paths/tests/test_test_coverage.py +2 -12
- wayfinder_paths/tests/test_utils.py +1 -31
- wayfinder_paths-0.1.20.dist-info/METADATA +355 -0
- wayfinder_paths-0.1.20.dist-info/RECORD +129 -0
- wayfinder_paths/core/adapters/base.py +0 -5
- wayfinder_paths-0.1.19.dist-info/METADATA +0 -592
- wayfinder_paths-0.1.19.dist-info/RECORD +0 -130
- {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.20.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.20.dist-info}/WHEEL +0 -0
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Moonwell wstETH Loop Strategy
|
|
3
|
-
|
|
4
|
-
A leveraged liquid-staking carry on Base that loops USDC → borrow WETH → swap to wstETH → lend wstETH.
|
|
5
|
-
The loop repeats while keeping debt as a fraction F of borrow capacity, chosen conservatively
|
|
6
|
-
so the position remains safe under a stETH/ETH depeg.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
1
|
import asyncio
|
|
10
2
|
import time
|
|
11
3
|
from collections.abc import Awaitable, Callable
|
|
@@ -67,16 +59,10 @@ COLLATERAL_SAFETY_FACTOR = 0.98
|
|
|
67
59
|
|
|
68
60
|
|
|
69
61
|
class SwapOutcomeUnknownError(RuntimeError):
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
In this case we must not retry (risk duplicate fills) and should halt the strategy
|
|
73
|
-
so the caller can inspect on-chain state.
|
|
74
|
-
"""
|
|
62
|
+
"Raised when the outcome of a swap operation is unknown."
|
|
75
63
|
|
|
76
64
|
|
|
77
65
|
class MoonwellWstethLoopStrategy(Strategy):
|
|
78
|
-
"""Leveraged wstETH yield strategy using Moonwell lending protocol on Base."""
|
|
79
|
-
|
|
80
66
|
name = "Moonwell wstETH Loop Strategy"
|
|
81
67
|
description = "Leveraged wstETH yield strategy using Moonwell lending protocol."
|
|
82
68
|
summary = "Loop wstETH on Moonwell for amplified staking yields."
|
|
@@ -88,15 +74,15 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
88
74
|
# When wrapping ETH to WETH for swaps/repayment, avoid draining gas below this floor.
|
|
89
75
|
# We can dip below MIN_GAS temporarily, but should not wipe the wallet.
|
|
90
76
|
WRAP_GAS_RESERVE = 0.0014
|
|
91
|
-
MIN_USDC_DEPOSIT = 10.0
|
|
92
|
-
MIN_REWARD_CLAIM_USD = 0.30
|
|
93
|
-
MAX_DEPEG = 0.01
|
|
77
|
+
MIN_USDC_DEPOSIT = 10.0
|
|
78
|
+
MIN_REWARD_CLAIM_USD = 0.30
|
|
79
|
+
MAX_DEPEG = 0.01
|
|
94
80
|
MAX_HEALTH_FACTOR = 1.5
|
|
95
81
|
MIN_HEALTH_FACTOR = 1.2
|
|
96
82
|
# Operational target HF (keep some buffer above MIN_HEALTH_FACTOR).
|
|
97
83
|
TARGET_HEALTH_FACTOR = 1.25
|
|
98
84
|
# Lever up if HF is more than this amount above TARGET_HEALTH_FACTOR
|
|
99
|
-
HF_LEVER_UP_BUFFER = 0.05
|
|
85
|
+
HF_LEVER_UP_BUFFER = 0.05
|
|
100
86
|
# Deleverage if HF drops below (TARGET - buffer).
|
|
101
87
|
HF_DELEVERAGE_BUFFER = 0.05
|
|
102
88
|
# Deleverage if leverage multiplier exceeds target by this much.
|
|
@@ -111,20 +97,20 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
111
97
|
# Full-exit (withdraw) dust behavior: do fewer, larger actions so we can repay_full in one go.
|
|
112
98
|
FULL_EXIT_BUFFER_MULT = 1.05
|
|
113
99
|
FULL_EXIT_MIN_BATCH_USD = 10.0
|
|
114
|
-
_MAX_LOOP_LIMIT = 30
|
|
100
|
+
_MAX_LOOP_LIMIT = 30
|
|
115
101
|
|
|
116
102
|
# Parameters
|
|
117
|
-
leverage_limit = 10
|
|
103
|
+
leverage_limit = 10
|
|
118
104
|
min_withdraw_usd = 2
|
|
119
105
|
sweep_min_usd = 0.20
|
|
120
|
-
max_swap_retries = 3
|
|
121
|
-
swap_slippage_tolerance = 0.005
|
|
122
|
-
MAX_SLIPPAGE_TOLERANCE = 0.03
|
|
123
|
-
PRICE_STALENESS_THRESHOLD = 300
|
|
106
|
+
max_swap_retries = 3
|
|
107
|
+
swap_slippage_tolerance = 0.005
|
|
108
|
+
MAX_SLIPPAGE_TOLERANCE = 0.03
|
|
109
|
+
PRICE_STALENESS_THRESHOLD = 300
|
|
124
110
|
|
|
125
111
|
# 50 basis points (0.0050) - minimum leverage gain per loop iteration to continue
|
|
126
112
|
# If marginal gain drops below this, stop looping as gas costs outweigh benefit
|
|
127
|
-
_MIN_LEVERAGE_GAIN_BPS = 50e-4
|
|
113
|
+
_MIN_LEVERAGE_GAIN_BPS = 50e-4
|
|
128
114
|
|
|
129
115
|
INFO = StratDescriptor(
|
|
130
116
|
description="Leveraged wstETH carry: loops USDC → borrow WETH → swap wstETH → lend. "
|
|
@@ -188,7 +174,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
188
174
|
*,
|
|
189
175
|
main_wallet: dict | None = None,
|
|
190
176
|
strategy_wallet: dict | None = None,
|
|
191
|
-
simulation: bool = False,
|
|
192
177
|
api_key: str | None = None,
|
|
193
178
|
main_wallet_signing_callback: Callable[[dict], Awaitable[str]] | None = None,
|
|
194
179
|
strategy_wallet_signing_callback: Callable[[dict], Awaitable[str]]
|
|
@@ -206,7 +191,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
206
191
|
merged_config["strategy_wallet"] = strategy_wallet
|
|
207
192
|
|
|
208
193
|
self.config = merged_config
|
|
209
|
-
self.simulation = simulation
|
|
210
194
|
|
|
211
195
|
# Adapter references
|
|
212
196
|
self.balance_adapter: BalanceAdapter | None = None
|
|
@@ -235,10 +219,8 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
235
219
|
"strategy": self.config,
|
|
236
220
|
}
|
|
237
221
|
|
|
238
|
-
# Initialize adapters
|
|
239
222
|
balance = BalanceAdapter(
|
|
240
223
|
adapter_config,
|
|
241
|
-
simulation=self.simulation,
|
|
242
224
|
main_wallet_signing_callback=self.main_wallet_signing_callback,
|
|
243
225
|
strategy_wallet_signing_callback=self.strategy_wallet_signing_callback,
|
|
244
226
|
)
|
|
@@ -246,12 +228,10 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
246
228
|
ledger_adapter = LedgerAdapter()
|
|
247
229
|
brap_adapter = BRAPAdapter(
|
|
248
230
|
adapter_config,
|
|
249
|
-
simulation=self.simulation,
|
|
250
231
|
strategy_wallet_signing_callback=self.strategy_wallet_signing_callback,
|
|
251
232
|
)
|
|
252
233
|
moonwell_adapter = MoonwellAdapter(
|
|
253
234
|
adapter_config,
|
|
254
|
-
simulation=self.simulation,
|
|
255
235
|
strategy_wallet_signing_callback=self.strategy_wallet_signing_callback,
|
|
256
236
|
)
|
|
257
237
|
|
|
@@ -276,16 +256,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
276
256
|
raise
|
|
277
257
|
|
|
278
258
|
def _max_safe_F(self, cf_w: float) -> float:
|
|
279
|
-
"""Max safe debt fraction vs borrow capacity under a depeg.
|
|
280
|
-
|
|
281
|
-
Let a = 1 - MAX_DEPEG. If the position is sized at par (a=1) with debt
|
|
282
|
-
fraction F = Debt / BorrowCapacity, then after an instantaneous depeg to a
|
|
283
|
-
the borrow capacity shrinks by cf_w * (1-a) * Debt. Requiring Debt to
|
|
284
|
-
remain <= new capacity yields:
|
|
285
|
-
F_max = 1 / (1 + cf_w * (1 - a))
|
|
286
|
-
|
|
287
|
-
Returns F_max clipped to [0, 1].
|
|
288
|
-
"""
|
|
289
259
|
a = 1 - self.MAX_DEPEG
|
|
290
260
|
if not (0 < a):
|
|
291
261
|
return 0.0
|
|
@@ -298,20 +268,14 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
298
268
|
return max(0.0, min(1.0, min(f_bound, f_feasible, 1.0)))
|
|
299
269
|
|
|
300
270
|
def _get_strategy_wallet_address(self) -> str:
|
|
301
|
-
"""Get the strategy wallet address."""
|
|
302
271
|
wallet = self.config.get("strategy_wallet", {})
|
|
303
272
|
return wallet.get("address", "")
|
|
304
273
|
|
|
305
274
|
def _get_main_wallet_address(self) -> str:
|
|
306
|
-
"""Get the main wallet address."""
|
|
307
275
|
wallet = self.config.get("main_wallet", {})
|
|
308
276
|
return wallet.get("address", "")
|
|
309
277
|
|
|
310
278
|
def _gas_keep_wei(self) -> int:
|
|
311
|
-
"""
|
|
312
|
-
Hard ETH reserve in the strategy wallet.
|
|
313
|
-
Never spend below this when wrapping/swapping ETH.
|
|
314
|
-
"""
|
|
315
279
|
# Extra buffer for a couple txs (wrap + swap/repay).
|
|
316
280
|
tx_buffer = int(0.0003 * 10**18)
|
|
317
281
|
return max(
|
|
@@ -327,9 +291,9 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
327
291
|
wallet_wsteth: int
|
|
328
292
|
wallet_usdc: int
|
|
329
293
|
|
|
330
|
-
usdc_supplied: int
|
|
331
|
-
wsteth_supplied: int
|
|
332
|
-
weth_debt: int
|
|
294
|
+
usdc_supplied: int
|
|
295
|
+
wsteth_supplied: int
|
|
296
|
+
weth_debt: int
|
|
333
297
|
|
|
334
298
|
# prices/decimals
|
|
335
299
|
eth_price: float
|
|
@@ -363,10 +327,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
363
327
|
async def _accounting_snapshot(
|
|
364
328
|
self, collateral_factors: tuple[float, float] | None = None
|
|
365
329
|
) -> tuple["MoonwellWstethLoopStrategy.AccountingSnapshot", tuple[float, float]]:
|
|
366
|
-
"""
|
|
367
|
-
One snapshot for decisions.
|
|
368
|
-
Keep it deterministic and re-usable across rebalance/unwind paths.
|
|
369
|
-
"""
|
|
370
330
|
if collateral_factors is None:
|
|
371
331
|
collateral_factors = await self._get_collateral_factors()
|
|
372
332
|
cf_u, cf_w = collateral_factors
|
|
@@ -484,7 +444,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
484
444
|
async def _ensure_markets_for_state(
|
|
485
445
|
self, snap: "MoonwellWstethLoopStrategy.AccountingSnapshot"
|
|
486
446
|
) -> tuple[bool, str]:
|
|
487
|
-
"""Idempotently ensure Moonwell markets are entered based on current positions."""
|
|
488
447
|
errors: list[str] = []
|
|
489
448
|
|
|
490
449
|
if snap.usdc_supplied > 0:
|
|
@@ -509,9 +468,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
509
468
|
def _debt_gap_report(
|
|
510
469
|
self, snap: "MoonwellWstethLoopStrategy.AccountingSnapshot"
|
|
511
470
|
) -> dict[str, float]:
|
|
512
|
-
"""
|
|
513
|
-
Mode-agnostic accounting: how much USDC needs to be raised to repay debt.
|
|
514
|
-
"""
|
|
515
471
|
eth_usable_usd = (
|
|
516
472
|
(snap.eth_usable_wei / (10**snap.eth_dec)) * snap.eth_price
|
|
517
473
|
if snap.eth_price
|
|
@@ -544,10 +500,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
544
500
|
def _delta_mismatch_usd(
|
|
545
501
|
self, snap: "MoonwellWstethLoopStrategy.AccountingSnapshot"
|
|
546
502
|
) -> float:
|
|
547
|
-
"""
|
|
548
|
-
Positive => net SHORT (debt > wstETH collateral) in USD terms.
|
|
549
|
-
Negative => net LONG (wstETH collateral > debt).
|
|
550
|
-
"""
|
|
551
503
|
wsteth_coll_usd = float(snap.totals_usd.get(f"Base_{M_WSTETH}", 0.0))
|
|
552
504
|
return float(snap.debt_usd) - wsteth_coll_usd
|
|
553
505
|
|
|
@@ -560,17 +512,13 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
560
512
|
hf_floor: float,
|
|
561
513
|
precision_usd: float = 0.50,
|
|
562
514
|
) -> float:
|
|
563
|
-
"""
|
|
564
|
-
Maximum USD amount of a given collateral you can remove while keeping HF >= hf_floor,
|
|
565
|
-
considering only the withdrawal step (before the subsequent swap+repay improves HF).
|
|
566
|
-
"""
|
|
567
515
|
current_val = float(totals_usd.get(withdraw_key, 0.0))
|
|
568
516
|
if current_val <= 0:
|
|
569
517
|
return 0.0
|
|
570
518
|
|
|
571
519
|
debt_usd = abs(float(totals_usd.get(f"Base_{WETH}", 0.0)))
|
|
572
520
|
if debt_usd <= 0:
|
|
573
|
-
return current_val
|
|
521
|
+
return current_val
|
|
574
522
|
|
|
575
523
|
cf_u, cf_w = collateral_factors
|
|
576
524
|
usdc_key = f"Base_{M_USDC}"
|
|
@@ -608,15 +556,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
608
556
|
mode: str = "operate",
|
|
609
557
|
prior_error: Exception | None = None,
|
|
610
558
|
) -> tuple[bool, str]:
|
|
611
|
-
"""
|
|
612
|
-
Always-run finalizer. Attempts to restore:
|
|
613
|
-
- HF >= MIN_HEALTH_FACTOR
|
|
614
|
-
- Not net-short ETH (wstETH collateral >= WETH debt, within tolerance)
|
|
615
|
-
|
|
616
|
-
mode:
|
|
617
|
-
- "operate": keep strategy running normally after guard
|
|
618
|
-
- "exit": conservative (don't buy/lend; only reconcile wallet collateral + delever)
|
|
619
|
-
"""
|
|
620
559
|
logger.info("-" * 40)
|
|
621
560
|
logger.info(f"POST-RUN GUARD: mode={mode}")
|
|
622
561
|
if prior_error:
|
|
@@ -695,7 +634,7 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
695
634
|
tol = max(float(self.DELTA_TOL_USD), float(self.min_withdraw_usd))
|
|
696
635
|
|
|
697
636
|
for _ in range(int(self.POST_RUN_MAX_PASSES)):
|
|
698
|
-
mismatch = self._delta_mismatch_usd(snap)
|
|
637
|
+
mismatch = self._delta_mismatch_usd(snap)
|
|
699
638
|
short_usd = max(0.0, float(mismatch))
|
|
700
639
|
long_usd = max(0.0, float(-mismatch))
|
|
701
640
|
|
|
@@ -820,10 +759,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
820
759
|
collateral_factors: tuple[float, float],
|
|
821
760
|
target_hf: float | None = None,
|
|
822
761
|
) -> float:
|
|
823
|
-
"""Compute the target leverage multiplier implied by HF + collateral factors.
|
|
824
|
-
|
|
825
|
-
In this strategy, leverage is defined as (wstETH_supply_usd / usdc_supply_usd) + 1.
|
|
826
|
-
"""
|
|
827
762
|
cf_u, cf_w = collateral_factors
|
|
828
763
|
hf = (
|
|
829
764
|
float(target_hf)
|
|
@@ -849,7 +784,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
849
784
|
max_batch_usd: float = 4000.0,
|
|
850
785
|
max_steps: int = 10,
|
|
851
786
|
) -> tuple[bool, str]:
|
|
852
|
-
"""Reduce leverage by withdrawing wstETH collateral and repaying WETH debt."""
|
|
853
787
|
band = (
|
|
854
788
|
float(max_over_leverage)
|
|
855
789
|
if max_over_leverage is not None
|
|
@@ -989,12 +923,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
989
923
|
collateral_factors: tuple[float, float],
|
|
990
924
|
max_batch_usd: float = 5000.0,
|
|
991
925
|
) -> tuple[bool, str]:
|
|
992
|
-
"""
|
|
993
|
-
Operate-mode reconciliation:
|
|
994
|
-
- Always lend loose wallet wstETH (no swaps).
|
|
995
|
-
- If there is WETH debt and wstETH collateral is short, use wallet WETH then wallet ETH
|
|
996
|
-
(above gas reserve) to buy wstETH and lend it.
|
|
997
|
-
"""
|
|
998
926
|
snap, _ = await self._accounting_snapshot(collateral_factors=collateral_factors)
|
|
999
927
|
addr = self._get_strategy_wallet_address()
|
|
1000
928
|
|
|
@@ -1129,7 +1057,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
1129
1057
|
hf_floor: float,
|
|
1130
1058
|
max_batch_usd: float = 8000.0,
|
|
1131
1059
|
) -> tuple[bool, str]:
|
|
1132
|
-
"""Reduce net long wstETH exposure by converting mwstETH collateral into mUSDC."""
|
|
1133
1060
|
if unwind_usd <= 0:
|
|
1134
1061
|
return (True, "No wstETH long to unwind")
|
|
1135
1062
|
|
|
@@ -1245,17 +1172,10 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
1245
1172
|
target_debt_usd: float,
|
|
1246
1173
|
target_hf: float | None = None,
|
|
1247
1174
|
collateral_factors: tuple[float, float],
|
|
1248
|
-
mode: str,
|
|
1175
|
+
mode: str,
|
|
1249
1176
|
max_batch_usd: float = 4000.0,
|
|
1250
1177
|
max_steps: int = 20,
|
|
1251
1178
|
) -> tuple[bool, str]:
|
|
1252
|
-
"""
|
|
1253
|
-
Reduce debt until the specified target is met.
|
|
1254
|
-
Uses wallet assets first; if insufficient, redeems collateral in HF-safe batches.
|
|
1255
|
-
|
|
1256
|
-
mode="operate": be conservative about collateral withdrawals (HF floor ~ MIN_HEALTH_FACTOR)
|
|
1257
|
-
mode="exit": allow lower HF floor during the withdraw step (still > 1.05), since repay comes right after.
|
|
1258
|
-
"""
|
|
1259
1179
|
logger.info(
|
|
1260
1180
|
f"SETTLE DEBT: target_debt=${target_debt_usd:.2f}, target_hf={target_hf}, mode={mode}"
|
|
1261
1181
|
)
|
|
@@ -1431,7 +1351,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
1431
1351
|
|
|
1432
1352
|
# Choose the collateral source that can actually be withdrawn (HF-safe + cash bound)
|
|
1433
1353
|
# in the largest size. This avoids getting stuck redeeming ever-smaller amounts when
|
|
1434
|
-
# a market (often wstETH) is cash-limited.
|
|
1435
1354
|
snap, _ = await self._accounting_snapshot(
|
|
1436
1355
|
collateral_factors=collateral_factors
|
|
1437
1356
|
)
|
|
@@ -1632,7 +1551,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
1632
1551
|
return (False, f"Exceeded max_steps={max_steps} while settling debt")
|
|
1633
1552
|
|
|
1634
1553
|
async def setup(self):
|
|
1635
|
-
"""Initialize token info and validate configuration."""
|
|
1636
1554
|
if self.token_adapter is None:
|
|
1637
1555
|
raise RuntimeError("Token adapter not initialized.")
|
|
1638
1556
|
|
|
@@ -1646,7 +1564,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
1646
1564
|
logger.warning(f"Failed to fetch token info for {token_id}: {e}")
|
|
1647
1565
|
|
|
1648
1566
|
async def _get_token_info(self, token_id: str) -> dict:
|
|
1649
|
-
"""Get token info from cache or fetch it."""
|
|
1650
1567
|
if token_id in self._token_info_cache:
|
|
1651
1568
|
return self._token_info_cache[token_id]
|
|
1652
1569
|
|
|
@@ -1657,10 +1574,8 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
1657
1574
|
return {}
|
|
1658
1575
|
|
|
1659
1576
|
async def _get_token_price(self, token_id: str) -> float:
|
|
1660
|
-
"""Get token price with staleness check."""
|
|
1661
1577
|
now = time.time()
|
|
1662
1578
|
|
|
1663
|
-
# Check cache with staleness
|
|
1664
1579
|
if token_id in self._token_price_cache:
|
|
1665
1580
|
timestamp = self._token_price_timestamps.get(token_id, 0)
|
|
1666
1581
|
if now - timestamp < self.PRICE_STALENESS_THRESHOLD:
|
|
@@ -1682,12 +1597,10 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
1682
1597
|
return 0.0
|
|
1683
1598
|
|
|
1684
1599
|
def _clear_price_cache(self):
|
|
1685
|
-
"""Clear the price cache to force refresh."""
|
|
1686
1600
|
self._token_price_cache.clear()
|
|
1687
1601
|
self._token_price_timestamps.clear()
|
|
1688
1602
|
|
|
1689
1603
|
async def _get_token_data(self, token_id: str) -> tuple[float, int]:
|
|
1690
|
-
"""Get price and decimals for a token in one call."""
|
|
1691
1604
|
price = await self._get_token_price(token_id)
|
|
1692
1605
|
info = await self._get_token_info(token_id)
|
|
1693
1606
|
return price, info.get("decimals", 18)
|
|
@@ -1701,13 +1614,11 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
1701
1614
|
base_slippage: float | None = None,
|
|
1702
1615
|
preferred_providers: list[str] | None = None,
|
|
1703
1616
|
) -> dict | None:
|
|
1704
|
-
"""Swap with retries, progressive slippage, and exponential backoff."""
|
|
1705
1617
|
if max_retries is None:
|
|
1706
1618
|
max_retries = self.max_swap_retries
|
|
1707
1619
|
if base_slippage is None:
|
|
1708
1620
|
base_slippage = self.swap_slippage_tolerance
|
|
1709
1621
|
|
|
1710
|
-
# Get token info for logging
|
|
1711
1622
|
from_decimals = (
|
|
1712
1623
|
18 if from_token_id in (ETH_TOKEN_ID, WETH_TOKEN_ID, WSTETH_TOKEN_ID) else 6
|
|
1713
1624
|
)
|
|
@@ -1842,7 +1753,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
1842
1753
|
return None
|
|
1843
1754
|
|
|
1844
1755
|
def _parse_balance(self, raw: Any) -> int:
|
|
1845
|
-
"""Parse balance value to integer, handling various formats."""
|
|
1846
1756
|
if raw is None:
|
|
1847
1757
|
return 0
|
|
1848
1758
|
if isinstance(raw, dict):
|
|
@@ -1873,29 +1783,9 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
1873
1783
|
wallet_address: str,
|
|
1874
1784
|
block_identifier: int | str | None = None,
|
|
1875
1785
|
) -> int:
|
|
1876
|
-
"""Read a wallet balance directly from chain (falls back to adapter in simulation).
|
|
1877
|
-
|
|
1878
|
-
Args:
|
|
1879
|
-
token_id: Token identifier (e.g., WETH_TOKEN_ID)
|
|
1880
|
-
wallet_address: Address to query balance for
|
|
1881
|
-
block_identifier: Block to query at. Can be:
|
|
1882
|
-
- int: specific block number (for pinning to tx block)
|
|
1883
|
-
- "safe": OP Stack safe block (data posted to L1)
|
|
1884
|
-
- None/"latest": current head (default)
|
|
1885
|
-
"""
|
|
1886
1786
|
if not token_id or not wallet_address:
|
|
1887
1787
|
return 0
|
|
1888
1788
|
|
|
1889
|
-
# Tests/simulations patch adapters; avoid RPC calls there.
|
|
1890
|
-
if self.simulation:
|
|
1891
|
-
if self.balance_adapter is None:
|
|
1892
|
-
return 0
|
|
1893
|
-
success, raw = await self.balance_adapter.get_balance(
|
|
1894
|
-
token_id=token_id,
|
|
1895
|
-
wallet_address=wallet_address,
|
|
1896
|
-
)
|
|
1897
|
-
return self._parse_balance(raw) if success else 0
|
|
1898
|
-
|
|
1899
1789
|
token_address = self._token_address_for_id(token_id)
|
|
1900
1790
|
if token_id != ETH_TOKEN_ID and not token_address:
|
|
1901
1791
|
# Try to resolve address via token metadata (not a balance read).
|
|
@@ -1959,11 +1849,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
1959
1849
|
return 0
|
|
1960
1850
|
|
|
1961
1851
|
def _normalize_usd_value(self, raw: Any) -> float:
|
|
1962
|
-
"""Normalize a USD value that may be 18-decimal scaled (Compound/Moonwell style).
|
|
1963
|
-
|
|
1964
|
-
Moonwell's Comptroller `getAccountLiquidity` returns USD with 18 decimals as an int.
|
|
1965
|
-
Some mocks may return a float already in USD.
|
|
1966
|
-
"""
|
|
1967
1852
|
if raw is None:
|
|
1968
1853
|
return 0.0
|
|
1969
1854
|
|
|
@@ -1983,7 +1868,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
1983
1868
|
def _mtoken_amount_for_underlying(
|
|
1984
1869
|
self, withdraw_info: dict[str, Any], underlying_raw: int
|
|
1985
1870
|
) -> int:
|
|
1986
|
-
"""Convert desired underlying (raw) to mToken amount (raw), capped by max withdrawable."""
|
|
1987
1871
|
if underlying_raw <= 0:
|
|
1988
1872
|
return 0
|
|
1989
1873
|
|
|
@@ -2011,7 +1895,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2011
1895
|
return min(int(ctokens_needed), max_ctokens)
|
|
2012
1896
|
|
|
2013
1897
|
def _pinned_block(self, tx_result: Any) -> int | None:
|
|
2014
|
-
"""Extract a deterministic pinned block number from an adapter tx result (best-effort)."""
|
|
2015
1898
|
if not isinstance(tx_result, dict):
|
|
2016
1899
|
return None
|
|
2017
1900
|
|
|
@@ -2035,7 +1918,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2035
1918
|
min_expected: int = 1,
|
|
2036
1919
|
attempts: int = 5,
|
|
2037
1920
|
) -> int:
|
|
2038
|
-
"""Read a balance at a pinned block, retrying briefly to avoid RPC indexing lag."""
|
|
2039
1921
|
bal = 0
|
|
2040
1922
|
for i in range(int(attempts)):
|
|
2041
1923
|
bal = await self._get_balance_raw(
|
|
@@ -2049,19 +1931,16 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2049
1931
|
return int(bal)
|
|
2050
1932
|
|
|
2051
1933
|
async def _get_gas_balance(self) -> int:
|
|
2052
|
-
"""Get ETH balance in strategy wallet (raw wei)."""
|
|
2053
1934
|
return await self._get_balance_raw(
|
|
2054
1935
|
token_id=ETH_TOKEN_ID, wallet_address=self._get_strategy_wallet_address()
|
|
2055
1936
|
)
|
|
2056
1937
|
|
|
2057
1938
|
async def _get_usdc_balance(self) -> int:
|
|
2058
|
-
"""Get USDC balance in strategy wallet (raw wei)."""
|
|
2059
1939
|
return await self._get_balance_raw(
|
|
2060
1940
|
token_id=USDC_TOKEN_ID, wallet_address=self._get_strategy_wallet_address()
|
|
2061
1941
|
)
|
|
2062
1942
|
|
|
2063
1943
|
async def _validate_gas_balance(self) -> tuple[bool, str]:
|
|
2064
|
-
"""Validate gas balance meets minimum requirements."""
|
|
2065
1944
|
gas_balance = await self._get_gas_balance()
|
|
2066
1945
|
main_gas = await self._get_balance_raw(
|
|
2067
1946
|
token_id=ETH_TOKEN_ID, wallet_address=self._get_main_wallet_address()
|
|
@@ -2080,7 +1959,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2080
1959
|
async def _validate_usdc_deposit(
|
|
2081
1960
|
self, usdc_amount: float
|
|
2082
1961
|
) -> tuple[bool, str, float]:
|
|
2083
|
-
"""Validate USDC deposit amount."""
|
|
2084
1962
|
actual_balance = await self._get_balance_raw(
|
|
2085
1963
|
token_id=USDC_TOKEN_ID, wallet_address=self._get_main_wallet_address()
|
|
2086
1964
|
)
|
|
@@ -2100,7 +1978,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2100
1978
|
return (True, "USDC deposit amount validated", usdc_amount)
|
|
2101
1979
|
|
|
2102
1980
|
async def _check_quote_profitability(self) -> tuple[bool, str]:
|
|
2103
|
-
"""Check if the quote APY is profitable."""
|
|
2104
1981
|
quote = await self.quote()
|
|
2105
1982
|
if quote.get("apy", 0) < 0:
|
|
2106
1983
|
return (
|
|
@@ -2110,7 +1987,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2110
1987
|
return (True, "Quote is profitable")
|
|
2111
1988
|
|
|
2112
1989
|
async def _transfer_usdc_to_vault(self, usdc_amount: float) -> tuple[bool, str]:
|
|
2113
|
-
"""Transfer USDC from main wallet to vault wallet."""
|
|
2114
1990
|
(
|
|
2115
1991
|
success,
|
|
2116
1992
|
msg,
|
|
@@ -2122,7 +1998,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2122
1998
|
return (True, "USDC transferred to vault")
|
|
2123
1999
|
|
|
2124
2000
|
async def _transfer_gas_to_vault(self) -> tuple[bool, str]:
|
|
2125
|
-
"""Transfer gas from main wallet to vault if needed."""
|
|
2126
2001
|
vault_gas = await self._get_gas_balance()
|
|
2127
2002
|
min_gas_wei = int(self._gas_keep_wei())
|
|
2128
2003
|
if vault_gas < min_gas_wei:
|
|
@@ -2145,7 +2020,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2145
2020
|
*,
|
|
2146
2021
|
strict: bool = False,
|
|
2147
2022
|
) -> tuple[bool, str]:
|
|
2148
|
-
"""Sweep miscellaneous tokens above min_usd_value to target token."""
|
|
2149
2023
|
if exclude is None:
|
|
2150
2024
|
exclude = set()
|
|
2151
2025
|
|
|
@@ -2204,11 +2078,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2204
2078
|
return (True, f"Swept {swept_count} tokens totaling ${total_swept_usd:.2f}")
|
|
2205
2079
|
|
|
2206
2080
|
async def _claim_and_reinvest_rewards(self) -> tuple[bool, str]:
|
|
2207
|
-
"""Claim WELL rewards, swap to USDC, and lend directly to mUSDC (no leverage).
|
|
2208
|
-
|
|
2209
|
-
This deposits rewards as unleveraged USDC collateral rather than running
|
|
2210
|
-
them through the leverage loop, preserving the strategy's debt ratio.
|
|
2211
|
-
"""
|
|
2212
2081
|
# Claim rewards if above threshold
|
|
2213
2082
|
claimed_ok, claimed = await self.moonwell_adapter.claim_rewards(
|
|
2214
2083
|
min_rewards_usd=self.MIN_REWARD_CLAIM_USD
|
|
@@ -2217,11 +2086,9 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2217
2086
|
logger.warning(f"Failed to claim rewards: {claimed}")
|
|
2218
2087
|
return (True, "Reward claim failed, skipping reinvestment")
|
|
2219
2088
|
|
|
2220
|
-
# Check if we actually got rewards (claimed is dict of token -> amount)
|
|
2221
2089
|
if not claimed or not isinstance(claimed, dict):
|
|
2222
2090
|
return (True, "No rewards to reinvest")
|
|
2223
2091
|
|
|
2224
|
-
# Check WELL balance in wallet
|
|
2225
2092
|
well_balance = await self._get_balance_raw(
|
|
2226
2093
|
token_id=WELL_TOKEN_ID,
|
|
2227
2094
|
wallet_address=self._get_strategy_wallet_address(),
|
|
@@ -2229,7 +2096,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2229
2096
|
if well_balance <= 0:
|
|
2230
2097
|
return (True, "No WELL balance to reinvest")
|
|
2231
2098
|
|
|
2232
|
-
# Get WELL price and check value
|
|
2233
2099
|
well_price, well_decimals = await self._get_token_data(WELL_TOKEN_ID)
|
|
2234
2100
|
well_value_usd = (well_balance / 10**well_decimals) * well_price
|
|
2235
2101
|
|
|
@@ -2257,7 +2123,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2257
2123
|
logger.warning(f"WELL swap error: {e}")
|
|
2258
2124
|
return (True, f"WELL swap error: {e}")
|
|
2259
2125
|
|
|
2260
|
-
# Get resulting USDC balance to lend
|
|
2261
2126
|
usdc_balance = await self._get_balance_raw(
|
|
2262
2127
|
token_id=USDC_TOKEN_ID,
|
|
2263
2128
|
wallet_address=self._get_strategy_wallet_address(),
|
|
@@ -2283,19 +2148,15 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2283
2148
|
async def deposit(
|
|
2284
2149
|
self, main_token_amount: float = 0.0, gas_token_amount: float = 0.0
|
|
2285
2150
|
) -> StatusTuple:
|
|
2286
|
-
"""Deposit USDC and execute leverage loop."""
|
|
2287
2151
|
self._clear_price_cache()
|
|
2288
2152
|
|
|
2289
|
-
# Validate deposit amount is positive
|
|
2290
2153
|
if main_token_amount <= 0:
|
|
2291
2154
|
return (False, "Deposit amount must be positive")
|
|
2292
2155
|
|
|
2293
|
-
# Check quote profitability
|
|
2294
2156
|
success, message = await self._check_quote_profitability()
|
|
2295
2157
|
if not success:
|
|
2296
2158
|
return (False, message)
|
|
2297
2159
|
|
|
2298
|
-
# Validate USDC deposit amount
|
|
2299
2160
|
success, message, validated_amount = await self._validate_usdc_deposit(
|
|
2300
2161
|
main_token_amount
|
|
2301
2162
|
)
|
|
@@ -2303,7 +2164,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2303
2164
|
return (False, message)
|
|
2304
2165
|
usdc_amount = validated_amount
|
|
2305
2166
|
|
|
2306
|
-
# Validate gas balance
|
|
2307
2167
|
success, message = await self._validate_gas_balance()
|
|
2308
2168
|
if not success:
|
|
2309
2169
|
return (False, message)
|
|
@@ -2324,10 +2184,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2324
2184
|
)
|
|
2325
2185
|
|
|
2326
2186
|
async def _get_collateral_factors(self) -> tuple[float, float]:
|
|
2327
|
-
"""Fetch both collateral factors (USDC and wstETH), using adapter cache.
|
|
2328
|
-
|
|
2329
|
-
Returns (cf_usdc, cf_wsteth).
|
|
2330
|
-
"""
|
|
2331
2187
|
cf_u_result, cf_w_result = await asyncio.gather(
|
|
2332
2188
|
self.moonwell_adapter.get_collateral_factor(mtoken=M_USDC),
|
|
2333
2189
|
self.moonwell_adapter.get_collateral_factor(mtoken=M_WSTETH),
|
|
@@ -2341,12 +2197,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2341
2197
|
snap: Optional["MoonwellWstethLoopStrategy.AccountingSnapshot"] = None,
|
|
2342
2198
|
collateral_factors: tuple[float, float] | None = None,
|
|
2343
2199
|
) -> tuple[float, float, float]:
|
|
2344
|
-
"""Returns (usdc_lend_value, wsteth_lend_value, current_leverage).
|
|
2345
|
-
|
|
2346
|
-
Args:
|
|
2347
|
-
snap: Optional accounting snapshot. If provided, skips snapshot fetches.
|
|
2348
|
-
collateral_factors: Optional (cf_usdc, cf_wsteth) tuple; only used if `snap` is None.
|
|
2349
|
-
"""
|
|
2350
2200
|
if snap is None:
|
|
2351
2201
|
snap, _ = await self._accounting_snapshot(
|
|
2352
2202
|
collateral_factors=collateral_factors
|
|
@@ -2364,7 +2214,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2364
2214
|
return (usdc_lend_value, wsteth_lend_value, initial_leverage)
|
|
2365
2215
|
|
|
2366
2216
|
async def _get_steth_apy(self) -> float | None:
|
|
2367
|
-
"""Fetch wstETH APY from Lido API."""
|
|
2368
2217
|
url = "https://eth-api.lido.fi/v1/protocol/steth/apr/sma"
|
|
2369
2218
|
try:
|
|
2370
2219
|
async with httpx.AsyncClient(timeout=60) as client:
|
|
@@ -2380,8 +2229,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2380
2229
|
return None
|
|
2381
2230
|
|
|
2382
2231
|
async def quote(self) -> dict:
|
|
2383
|
-
"""Calculate projected APY for the strategy."""
|
|
2384
|
-
# Get APYs and collateral factors in parallel
|
|
2385
2232
|
(
|
|
2386
2233
|
usdc_apy_result,
|
|
2387
2234
|
weth_apy_result,
|
|
@@ -2413,7 +2260,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2413
2260
|
if not cf_u or cf_u <= 0:
|
|
2414
2261
|
return {"apy": 0, "information": "Invalid collateral factor", "data": {}}
|
|
2415
2262
|
|
|
2416
|
-
# Calculate target borrow and leverage
|
|
2417
2263
|
denominator = self.TARGET_HEALTH_FACTOR - cf_w
|
|
2418
2264
|
if denominator <= 0:
|
|
2419
2265
|
return {"apy": 0, "information": "Invalid health factor params", "data": {}}
|
|
@@ -2436,7 +2282,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2436
2282
|
}
|
|
2437
2283
|
|
|
2438
2284
|
async def _atomic_deposit_iteration(self, borrow_amt_wei: int) -> int:
|
|
2439
|
-
"""One atomic iteration: borrow WETH → swap wstETH → lend. Returns wstETH lent."""
|
|
2440
2285
|
safe_borrow_amt = int(borrow_amt_wei * COLLATERAL_SAFETY_FACTOR)
|
|
2441
2286
|
strategy_address = self._get_strategy_wallet_address()
|
|
2442
2287
|
|
|
@@ -2458,7 +2303,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2458
2303
|
if not success:
|
|
2459
2304
|
raise Exception(f"Borrow failed: {borrow_result}")
|
|
2460
2305
|
|
|
2461
|
-
# Extract a deterministic pinned block number from the transaction result.
|
|
2462
2306
|
# On Base we wait +2 blocks by default; `confirmed_block_number` is safe to pin reads to.
|
|
2463
2307
|
pinned_block = self._pinned_block(borrow_result)
|
|
2464
2308
|
|
|
@@ -2614,7 +2458,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2614
2458
|
) from repay_exc
|
|
2615
2459
|
raise Exception("Atomic deposit failed at swap step after all retries")
|
|
2616
2460
|
|
|
2617
|
-
# Parse to_amount from swap result (may be int or string)
|
|
2618
2461
|
raw_to_amount = (
|
|
2619
2462
|
swap_result.get("to_amount", 0) if isinstance(swap_result, dict) else 0
|
|
2620
2463
|
)
|
|
@@ -2623,7 +2466,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2623
2466
|
except (ValueError, TypeError):
|
|
2624
2467
|
to_amount_wei = 0
|
|
2625
2468
|
|
|
2626
|
-
# Get actual wstETH balance
|
|
2627
2469
|
wsteth_success, wsteth_bal_raw = await self.balance_adapter.get_balance(
|
|
2628
2470
|
token_id=WSTETH_TOKEN_ID, wallet_address=self._get_strategy_wallet_address()
|
|
2629
2471
|
)
|
|
@@ -2640,7 +2482,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2640
2482
|
if lend_amt_wei <= 0:
|
|
2641
2483
|
logger.warning("Swap resulted in 0 wstETH. Rolling back borrow...")
|
|
2642
2484
|
try:
|
|
2643
|
-
# Get WETH balance to repay (swap may have returned WETH or nothing)
|
|
2644
2485
|
weth_bal = await self._get_balance_raw(
|
|
2645
2486
|
token_id=WETH_TOKEN_ID, wallet_address=strategy_address
|
|
2646
2487
|
)
|
|
@@ -2766,7 +2607,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2766
2607
|
return lend_amt_wei
|
|
2767
2608
|
|
|
2768
2609
|
async def partial_liquidate(self, usd_value: float) -> StatusTuple:
|
|
2769
|
-
"""Create USDC liquidity in the strategy wallet by safely redeeming collateral."""
|
|
2770
2610
|
if usd_value <= 0:
|
|
2771
2611
|
raise ValueError(f"usd_value must be positive, got {usd_value}")
|
|
2772
2612
|
|
|
@@ -2961,19 +2801,16 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
2961
2801
|
)
|
|
2962
2802
|
|
|
2963
2803
|
async def _execute_deposit_loop(self, usdc_amount: float) -> tuple[bool, Any, int]:
|
|
2964
|
-
"""Execute the recursive leverage loop."""
|
|
2965
2804
|
token_info = await self._get_token_info(USDC_TOKEN_ID)
|
|
2966
2805
|
decimals = token_info.get("decimals", 6)
|
|
2967
2806
|
initial_deposit = int(usdc_amount * 10**decimals)
|
|
2968
2807
|
|
|
2969
|
-
# Fetch prices and collateral factors in parallel (use cache, minimal RPC)
|
|
2970
2808
|
wsteth_price, weth_price, collateral_factors = await asyncio.gather(
|
|
2971
2809
|
self._get_token_price(WSTETH_TOKEN_ID),
|
|
2972
2810
|
self._get_token_price(WETH_TOKEN_ID),
|
|
2973
2811
|
self._get_collateral_factors(),
|
|
2974
2812
|
)
|
|
2975
2813
|
|
|
2976
|
-
# Fetch position separately to avoid overwhelming public RPC
|
|
2977
2814
|
weth_pos = await self.moonwell_adapter.get_pos(mtoken=M_WETH)
|
|
2978
2815
|
|
|
2979
2816
|
current_borrowed_value = 0.0
|
|
@@ -3020,12 +2857,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3020
2857
|
wsteth_lend_value: float,
|
|
3021
2858
|
collateral_factors: tuple[float, float] | None = None,
|
|
3022
2859
|
) -> tuple[bool, Any, int]:
|
|
3023
|
-
"""Execute leverage loop until target health factor reached.
|
|
3024
|
-
|
|
3025
|
-
Args:
|
|
3026
|
-
collateral_factors: Optional (cf_usdc, cf_wsteth) tuple.
|
|
3027
|
-
If provided, skips collateral factor fetches.
|
|
3028
|
-
"""
|
|
3029
2860
|
# Ensure USDC and wstETH markets are entered as collateral before borrowing
|
|
3030
2861
|
# This is idempotent - if already entered, Moonwell just returns success
|
|
3031
2862
|
if usdc_lend_value > 0:
|
|
@@ -3069,7 +2900,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3069
2900
|
else:
|
|
3070
2901
|
cf_u, cf_w = await self._get_collateral_factors()
|
|
3071
2902
|
|
|
3072
|
-
# Calculate depeg-aware max safe leverage fraction
|
|
3073
2903
|
max_safe_f = self._max_safe_F(cf_w)
|
|
3074
2904
|
|
|
3075
2905
|
# Guard against division by zero/negative denominator
|
|
@@ -3080,7 +2910,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3080
2910
|
)
|
|
3081
2911
|
return (False, initial_leverage, -1)
|
|
3082
2912
|
|
|
3083
|
-
# Calculate target borrow value
|
|
3084
2913
|
target_borrow_value = (
|
|
3085
2914
|
usdc_lend_value * cf_u / denominator - current_borrowed_value
|
|
3086
2915
|
)
|
|
@@ -3104,7 +2933,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3104
2933
|
leverage_tracker: list[float] = [initial_leverage]
|
|
3105
2934
|
|
|
3106
2935
|
for i in range(self._MAX_LOOP_LIMIT):
|
|
3107
|
-
# Get borrowable amount (returns USD with 18 decimals)
|
|
3108
2936
|
borrowable_result = await self.moonwell_adapter.get_borrowable_amount()
|
|
3109
2937
|
if not borrowable_result[0]:
|
|
3110
2938
|
logger.warning("Failed to get borrowable amount")
|
|
@@ -3121,7 +2949,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3121
2949
|
|
|
3122
2950
|
weth_info = await self._get_token_info(WETH_TOKEN_ID)
|
|
3123
2951
|
weth_decimals = weth_info.get("decimals", 18)
|
|
3124
|
-
# Convert USD to WETH wei: (USD / price) * 10^decimals
|
|
3125
2952
|
max_borrow_wei = int(borrowable_usd / weth_price * 10**weth_decimals)
|
|
3126
2953
|
|
|
3127
2954
|
# remaining_value is how much more we need to borrow/lend THIS session
|
|
@@ -3175,7 +3002,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3175
3002
|
return (True, leverage_tracker[-1], len(leverage_tracker) - 1)
|
|
3176
3003
|
|
|
3177
3004
|
async def update(self) -> StatusTuple:
|
|
3178
|
-
"""Rebalance positions, then run a post-run safety guard."""
|
|
3179
3005
|
logger.info("")
|
|
3180
3006
|
logger.info("*" * 60)
|
|
3181
3007
|
logger.info("* MOONWELL STRATEGY UPDATE CALLED")
|
|
@@ -3208,12 +3034,10 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3208
3034
|
)
|
|
3209
3035
|
|
|
3210
3036
|
async def _update_impl(self) -> StatusTuple:
|
|
3211
|
-
"""Update implementation (called by update() wrapper)."""
|
|
3212
3037
|
logger.info("=" * 60)
|
|
3213
3038
|
logger.info("UPDATE START")
|
|
3214
3039
|
logger.info("=" * 60)
|
|
3215
3040
|
|
|
3216
|
-
# Check gas balance - deposit() should have provided sufficient gas
|
|
3217
3041
|
gas_amt = await self._get_gas_balance()
|
|
3218
3042
|
logger.info(
|
|
3219
3043
|
f"Gas balance: {gas_amt / 10**18:.6f} ETH (min: {self.MAINTENANCE_GAS} ETH)"
|
|
@@ -3392,18 +3216,15 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3392
3216
|
reward_ok, reward_msg = await self._claim_and_reinvest_rewards()
|
|
3393
3217
|
logger.info(f" Rewards: {reward_msg}")
|
|
3394
3218
|
|
|
3395
|
-
# Check profitability
|
|
3396
3219
|
success, msg = await self._check_quote_profitability()
|
|
3397
3220
|
if not success:
|
|
3398
3221
|
return (False, msg)
|
|
3399
3222
|
|
|
3400
|
-
# Get USDC balance in wallet
|
|
3401
3223
|
usdc_balance_wei = await self._get_usdc_balance()
|
|
3402
3224
|
token_info = await self._get_token_info(USDC_TOKEN_ID)
|
|
3403
3225
|
decimals = token_info.get("decimals", 6)
|
|
3404
3226
|
usdc_balance = usdc_balance_wei / 10**decimals
|
|
3405
3227
|
|
|
3406
|
-
# Get lend values from the snapshot
|
|
3407
3228
|
usdc_key = f"Base_{M_USDC}"
|
|
3408
3229
|
wsteth_key = f"Base_{M_WSTETH}"
|
|
3409
3230
|
usdc_lend_value = totals_usd.get(usdc_key, 0)
|
|
@@ -3506,7 +3327,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3506
3327
|
)
|
|
3507
3328
|
|
|
3508
3329
|
async def _repay_weth(self, amount: int, remaining_debt: int) -> int:
|
|
3509
|
-
"""Repay WETH debt. Returns amount actually repaid."""
|
|
3510
3330
|
if amount <= 0 or remaining_debt <= 0:
|
|
3511
3331
|
return 0
|
|
3512
3332
|
|
|
@@ -3515,9 +3335,7 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3515
3335
|
|
|
3516
3336
|
# Only use repay_full when we have a small buffer above the observed debt.
|
|
3517
3337
|
# This avoids cases where debt accrues between snapshot and execution and leaves dust.
|
|
3518
|
-
full_repay_buffer_wei = max(
|
|
3519
|
-
10_000, remaining_debt // 10_000
|
|
3520
|
-
) # 0.01% or 10k wei
|
|
3338
|
+
full_repay_buffer_wei = max(10_000, remaining_debt // 10_000)
|
|
3521
3339
|
can_repay_full = amount >= (remaining_debt + full_repay_buffer_wei)
|
|
3522
3340
|
|
|
3523
3341
|
if can_repay_full:
|
|
@@ -3543,7 +3361,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3543
3361
|
async def _swap_to_weth_and_repay(
|
|
3544
3362
|
self, token_id: str, amount: int, remaining_debt: int
|
|
3545
3363
|
) -> int:
|
|
3546
|
-
"""Swap token to WETH and repay. Returns amount repaid."""
|
|
3547
3364
|
swap_result = await self._swap_with_retries(
|
|
3548
3365
|
from_token_id=token_id,
|
|
3549
3366
|
to_token_id=WETH_TOKEN_ID,
|
|
@@ -3580,7 +3397,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3580
3397
|
return await self._repay_weth(weth_bal, remaining_debt)
|
|
3581
3398
|
|
|
3582
3399
|
async def withdraw(self, amount: float | None = None) -> StatusTuple:
|
|
3583
|
-
"""Withdraw funds; on failure, run a post-run safety guard."""
|
|
3584
3400
|
logger.info("")
|
|
3585
3401
|
logger.info("*" * 60)
|
|
3586
3402
|
logger.info("* MOONWELL STRATEGY WITHDRAW CALLED")
|
|
@@ -3613,12 +3429,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3613
3429
|
return (False, f"{status[1]} | {suffix}")
|
|
3614
3430
|
|
|
3615
3431
|
async def _withdraw_impl(self, amount: float | None = None) -> StatusTuple:
|
|
3616
|
-
"""Withdraw implementation (called by withdraw() wrapper).
|
|
3617
|
-
|
|
3618
|
-
Logic:
|
|
3619
|
-
1. Liquidate any Moonwell positions to USDC (if any exist)
|
|
3620
|
-
2. Transfer any USDC > 0 to main wallet
|
|
3621
|
-
"""
|
|
3622
3432
|
# NOTE: amount is currently unused; withdraw() is all-or-nothing in this strategy.
|
|
3623
3433
|
logger.info("=" * 60)
|
|
3624
3434
|
logger.info("WITHDRAW START - Full position unwind")
|
|
@@ -3626,7 +3436,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3626
3436
|
|
|
3627
3437
|
collateral_factors = await self._get_collateral_factors()
|
|
3628
3438
|
|
|
3629
|
-
# Get initial state for logging
|
|
3630
3439
|
snap, _ = await self._accounting_snapshot(collateral_factors=collateral_factors)
|
|
3631
3440
|
logger.info("INITIAL STATE:")
|
|
3632
3441
|
logger.info(f" USDC supplied: ${snap.usdc_supplied / 10**6:.2f}")
|
|
@@ -3670,9 +3479,7 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3670
3479
|
(
|
|
3671
3480
|
ok,
|
|
3672
3481
|
msg,
|
|
3673
|
-
) = (
|
|
3674
|
-
await self._unlend_remaining_positions()
|
|
3675
|
-
) # swaps wstETH->USDC internally
|
|
3482
|
+
) = await self._unlend_remaining_positions()
|
|
3676
3483
|
except SwapOutcomeUnknownError as exc:
|
|
3677
3484
|
return (False, f"Swap outcome unknown while unlending positions: {exc}")
|
|
3678
3485
|
except Exception as exc: # noqa: BLE001
|
|
@@ -3723,7 +3530,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3723
3530
|
)
|
|
3724
3531
|
|
|
3725
3532
|
async def exit(self, **kwargs) -> StatusTuple:
|
|
3726
|
-
"""Transfer funds from strategy wallet to main wallet."""
|
|
3727
3533
|
logger.info("")
|
|
3728
3534
|
logger.info("*" * 60)
|
|
3729
3535
|
logger.info("* MOONWELL STRATEGY EXIT CALLED")
|
|
@@ -3757,7 +3563,7 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3757
3563
|
gas_balance = await self._get_balance_raw(
|
|
3758
3564
|
token_id=ETH_TOKEN_ID, wallet_address=self._get_strategy_wallet_address()
|
|
3759
3565
|
)
|
|
3760
|
-
tx_fee_reserve = int(0.0002 * 10**18)
|
|
3566
|
+
tx_fee_reserve = int(0.0002 * 10**18)
|
|
3761
3567
|
transferable_gas = gas_balance - tx_fee_reserve
|
|
3762
3568
|
if transferable_gas > 0:
|
|
3763
3569
|
gas_amount = transferable_gas / 10**18
|
|
@@ -3780,7 +3586,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3780
3586
|
return (True, f"Transferred to main wallet: {', '.join(transferred_items)}")
|
|
3781
3587
|
|
|
3782
3588
|
async def _unlend_remaining_positions(self) -> tuple[bool, str]:
|
|
3783
|
-
"""Unlend remaining collateral and convert to USDC."""
|
|
3784
3589
|
logger.info("UNLEND: Redeeming remaining Moonwell positions...")
|
|
3785
3590
|
|
|
3786
3591
|
# Unlend remaining wstETH
|
|
@@ -3828,7 +3633,7 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3828
3633
|
# Sweep any remaining tokens to USDC
|
|
3829
3634
|
ok, msg = await self._sweep_token_balances(
|
|
3830
3635
|
target_token_id=USDC_TOKEN_ID,
|
|
3831
|
-
exclude={ETH_TOKEN_ID, WELL_TOKEN_ID},
|
|
3636
|
+
exclude={ETH_TOKEN_ID, WELL_TOKEN_ID},
|
|
3832
3637
|
min_usd_value=float(self.sweep_min_usd),
|
|
3833
3638
|
strict=True,
|
|
3834
3639
|
)
|
|
@@ -3837,7 +3642,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3837
3642
|
return (True, "Unlent remaining positions")
|
|
3838
3643
|
|
|
3839
3644
|
async def get_peg_diff(self) -> float | dict:
|
|
3840
|
-
"""Get stETH/ETH peg difference."""
|
|
3841
3645
|
steth_price = await self._get_token_price(STETH_TOKEN_ID)
|
|
3842
3646
|
weth_price = await self._get_token_price(WETH_TOKEN_ID)
|
|
3843
3647
|
|
|
@@ -3853,22 +3657,18 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3853
3657
|
return peg_diff
|
|
3854
3658
|
|
|
3855
3659
|
async def _status(self) -> StatusDict:
|
|
3856
|
-
"""Report strategy status."""
|
|
3857
3660
|
snap, _ = await self._accounting_snapshot()
|
|
3858
3661
|
totals_usd = dict(snap.totals_usd)
|
|
3859
3662
|
|
|
3860
3663
|
ltv = float(snap.ltv)
|
|
3861
3664
|
hf = (1 / ltv) if ltv and ltv > 0 and not (ltv != ltv) else None
|
|
3862
3665
|
|
|
3863
|
-
# Get gas balance
|
|
3864
3666
|
gas_balance = await self._get_gas_balance()
|
|
3865
3667
|
|
|
3866
|
-
# Get borrowable amount
|
|
3867
3668
|
borrowable_result = await self.moonwell_adapter.get_borrowable_amount()
|
|
3868
3669
|
borrowable_amt_raw = borrowable_result[1] if borrowable_result[0] else 0
|
|
3869
3670
|
borrowable_amt = self._normalize_usd_value(borrowable_amt_raw)
|
|
3870
3671
|
|
|
3871
|
-
# Calculate credit remaining
|
|
3872
3672
|
total_borrowed = float(snap.debt_usd)
|
|
3873
3673
|
credit_remaining = 1.0
|
|
3874
3674
|
if (borrowable_amt + total_borrowed) > 0:
|
|
@@ -3876,13 +3676,10 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3876
3676
|
borrowable_amt / (borrowable_amt + total_borrowed), 4
|
|
3877
3677
|
)
|
|
3878
3678
|
|
|
3879
|
-
# Get peg diff
|
|
3880
3679
|
peg_diff = await self.get_peg_diff()
|
|
3881
3680
|
|
|
3882
|
-
# Calculate portfolio value
|
|
3883
3681
|
portfolio_value = float(snap.net_equity_usd)
|
|
3884
3682
|
|
|
3885
|
-
# Get projected earnings
|
|
3886
3683
|
quote = await self.quote()
|
|
3887
3684
|
|
|
3888
3685
|
strategy_status = {
|
|
@@ -3896,7 +3693,7 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3896
3693
|
|
|
3897
3694
|
return StatusDict(
|
|
3898
3695
|
portfolio_value=portfolio_value,
|
|
3899
|
-
net_deposit=0.0,
|
|
3696
|
+
net_deposit=0.0,
|
|
3900
3697
|
strategy_status=strategy_status,
|
|
3901
3698
|
gas_available=gas_balance / 10**18,
|
|
3902
3699
|
gassed_up=gas_balance >= int(self.MAINTENANCE_GAS * 10**18),
|
|
@@ -3904,7 +3701,6 @@ class MoonwellWstethLoopStrategy(Strategy):
|
|
|
3904
3701
|
|
|
3905
3702
|
@staticmethod
|
|
3906
3703
|
async def policies() -> list[str]:
|
|
3907
|
-
"""Return policy strings used to scope on-chain permissions."""
|
|
3908
3704
|
return [
|
|
3909
3705
|
# Moonwell operations
|
|
3910
3706
|
await musdc_mint_or_approve_or_redeem(),
|