wayfinder-paths 0.1.9__py3-none-any.whl → 0.1.11__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.

Files changed (54) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +1 -2
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +4 -4
  3. wayfinder_paths/adapters/brap_adapter/adapter.py +139 -23
  4. wayfinder_paths/adapters/moonwell_adapter/README.md +174 -0
  5. wayfinder_paths/adapters/moonwell_adapter/__init__.py +7 -0
  6. wayfinder_paths/adapters/moonwell_adapter/adapter.py +1226 -0
  7. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +635 -0
  8. wayfinder_paths/core/clients/AuthClient.py +3 -0
  9. wayfinder_paths/core/clients/WayfinderClient.py +2 -2
  10. wayfinder_paths/core/constants/__init__.py +0 -2
  11. wayfinder_paths/core/constants/base.py +6 -2
  12. wayfinder_paths/core/constants/moonwell_abi.py +411 -0
  13. wayfinder_paths/core/engine/StrategyJob.py +3 -0
  14. wayfinder_paths/core/services/local_evm_txn.py +182 -217
  15. wayfinder_paths/core/services/local_token_txn.py +46 -26
  16. wayfinder_paths/core/strategies/descriptors.py +1 -1
  17. wayfinder_paths/core/utils/evm_helpers.py +0 -27
  18. wayfinder_paths/run_strategy.py +34 -74
  19. wayfinder_paths/scripts/create_strategy.py +2 -27
  20. wayfinder_paths/scripts/run_strategy.py +37 -7
  21. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +1 -1
  22. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -15
  23. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +1 -1
  24. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +108 -0
  25. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/examples.json +11 -0
  26. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2975 -0
  27. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +886 -0
  28. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -7
  29. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1 -1
  30. wayfinder_paths/templates/adapter/README.md +5 -21
  31. wayfinder_paths/templates/adapter/adapter.py +1 -2
  32. wayfinder_paths/templates/adapter/test_adapter.py +1 -1
  33. wayfinder_paths/templates/strategy/README.md +4 -21
  34. wayfinder_paths/tests/test_smoke_manifest.py +17 -2
  35. {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.11.dist-info}/METADATA +60 -187
  36. {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.11.dist-info}/RECORD +38 -45
  37. wayfinder_paths/CONFIG_GUIDE.md +0 -390
  38. wayfinder_paths/adapters/balance_adapter/manifest.yaml +0 -8
  39. wayfinder_paths/adapters/brap_adapter/manifest.yaml +0 -11
  40. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +0 -10
  41. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +0 -8
  42. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +0 -11
  43. wayfinder_paths/adapters/pool_adapter/manifest.yaml +0 -10
  44. wayfinder_paths/adapters/token_adapter/manifest.yaml +0 -6
  45. wayfinder_paths/config.example.json +0 -22
  46. wayfinder_paths/core/engine/manifest.py +0 -97
  47. wayfinder_paths/scripts/validate_manifests.py +0 -213
  48. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +0 -23
  49. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +0 -7
  50. wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +0 -17
  51. wayfinder_paths/templates/adapter/manifest.yaml +0 -6
  52. wayfinder_paths/templates/strategy/manifest.yaml +0 -8
  53. {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.11.dist-info}/LICENSE +0 -0
  54. {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.11.dist-info}/WHEEL +0 -0
@@ -13,7 +13,6 @@ from pathlib import Path
13
13
  from loguru import logger
14
14
 
15
15
  from wayfinder_paths.core.config import StrategyJobConfig
16
- from wayfinder_paths.core.engine.manifest import load_manifest, validate_manifest
17
16
  from wayfinder_paths.core.engine.StrategyJob import StrategyJob
18
17
 
19
18
 
@@ -24,7 +23,7 @@ def load_strategy(
24
23
  api_key: str | None = None,
25
24
  ):
26
25
  """
27
- Dynamically load a strategy by name using its manifest
26
+ Dynamically load a strategy by name
28
27
 
29
28
  Args:
30
29
  strategy_name: Name of the strategy to load (directory name in strategies/)
@@ -34,28 +33,42 @@ def load_strategy(
34
33
  Returns:
35
34
  Strategy instance
36
35
  """
37
- # Find strategy manifest by scanning for manifest.yaml in the strategy directory
36
+ # Build the expected module path from strategy name
38
37
  strategies_dir = Path(__file__).parent / "strategies"
39
38
  strategy_dir = strategies_dir / strategy_name
40
- manifest_path = strategy_dir / "manifest.yaml"
41
39
 
42
- if not manifest_path.exists():
40
+ if not strategy_dir.exists():
43
41
  # List available strategies for better error message
44
42
  available = []
45
43
  if strategies_dir.exists():
46
44
  for path in strategies_dir.iterdir():
47
- if path.is_dir() and (path / "manifest.yaml").exists():
45
+ if path.is_dir() and (path / "strategy.py").exists():
48
46
  available.append(path.name)
49
47
  available_str = ", ".join(available) if available else "none"
50
48
  raise ValueError(
51
49
  f"Unknown strategy: {strategy_name}. Available strategies: {available_str}"
52
50
  )
53
51
 
54
- # Load manifest and use its entrypoint
55
- manifest = load_manifest(str(manifest_path))
56
- module_path, class_name = manifest.entrypoint.rsplit(".", 1)
57
- module = __import__(module_path, fromlist=[class_name])
58
- strategy_class = getattr(module, class_name)
52
+ # Import strategy module and find Strategy class
53
+ module_path = f"strategies.{strategy_name}.strategy"
54
+ module = __import__(module_path, fromlist=[""])
55
+
56
+ # Find the Strategy subclass in the module
57
+ from wayfinder_paths.core.strategies.Strategy import Strategy
58
+
59
+ strategy_class = None
60
+ for attr_name in dir(module):
61
+ attr = getattr(module, attr_name)
62
+ if (
63
+ isinstance(attr, type)
64
+ and issubclass(attr, Strategy)
65
+ and attr is not Strategy
66
+ ):
67
+ strategy_class = attr
68
+ break
69
+
70
+ if strategy_class is None:
71
+ raise ValueError(f"No Strategy class found in {module_path}")
59
72
 
60
73
  return strategy_class(config=strategy_config, api_key=api_key)
61
74
 
@@ -102,7 +115,6 @@ async def run_strategy(
102
115
  strategy_name: str | None = None,
103
116
  config_path: str | None = None,
104
117
  action: str = "run",
105
- manifest_path: str | None = None,
106
118
  **kwargs,
107
119
  ):
108
120
  """
@@ -115,37 +127,14 @@ async def run_strategy(
115
127
  **kwargs: Additional arguments for the action
116
128
  """
117
129
  try:
118
- # Determine strategy name for wallet lookup BEFORE loading config
119
- # This ensures wallets are properly matched during config initialization
120
- manifest = None
121
- strategy_name_for_wallet = None
122
- if manifest_path:
123
- logger.debug(f"Loading strategy via manifest: {manifest_path}")
124
- manifest = load_manifest(manifest_path)
125
- validate_manifest(manifest)
126
- # Extract directory name from manifest path for wallet lookup
127
- # Use the directory name (strategy identifier) for wallet lookup
128
- manifest_dir = Path(manifest_path).parent
129
- strategies_dir = Path(__file__).parent / "strategies"
130
- try:
131
- # Try to get relative path - if it's under strategies_dir, use directory name
132
- rel_path = manifest_dir.relative_to(strategies_dir)
133
- strategy_name_for_wallet = (
134
- rel_path.parts[0] if rel_path.parts else manifest_dir.name
135
- )
136
- except ValueError:
137
- # Not under strategies_dir, fallback to directory name or manifest name
138
- strategy_name_for_wallet = manifest_dir.name or manifest.name
139
- else:
140
- if not strategy_name:
141
- raise ValueError("Either strategy_name or --manifest must be provided")
142
- logger.debug(f"Loading strategy by name: {strategy_name}")
143
- # Use directory name (strategy_name) directly for wallet lookup
144
- strategy_name_for_wallet = strategy_name
130
+ if not strategy_name:
131
+ raise ValueError("strategy_name is required")
132
+
133
+ logger.debug(f"Loading strategy by name: {strategy_name}")
145
134
 
146
135
  # Load configuration with strategy name for wallet lookup
147
136
  logger.debug(f"Config path provided: {config_path}")
148
- config = load_config(config_path, strategy_name=strategy_name_for_wallet)
137
+ config = load_config(config_path, strategy_name=strategy_name)
149
138
  logger.debug(
150
139
  "Loaded config: creds=%s wallets(main=%s strategy=%s)",
151
140
  "yes"
@@ -156,25 +145,12 @@ async def run_strategy(
156
145
  (config.user.strategy_wallet_address or "none"),
157
146
  )
158
147
 
159
- # Validate required configuration
160
- # No user id required; authentication is via credentials or refresh token
161
-
162
148
  # Load strategy with the enriched config
163
- if manifest_path:
164
- # Load strategy class from manifest
165
- module_path, class_name = manifest.entrypoint.rsplit(".", 1)
166
- module = __import__(module_path, fromlist=[class_name])
167
- strategy_class = getattr(module, class_name)
168
- strategy = strategy_class(config=config.strategy_config)
169
- logger.info(
170
- f"Loaded strategy from manifest: {strategy_name_for_wallet or 'unnamed'}"
171
- )
172
- else:
173
- strategy = load_strategy(
174
- strategy_name,
175
- strategy_config=config.strategy_config,
176
- )
177
- logger.info(f"Loaded strategy: {strategy.name}")
149
+ strategy = load_strategy(
150
+ strategy_name,
151
+ strategy_config=config.strategy_config,
152
+ )
153
+ logger.info(f"Loaded strategy: {strategy.name}")
178
154
 
179
155
  # Create strategy job
180
156
  strategy_job = StrategyJob(strategy, config)
@@ -251,16 +227,6 @@ async def run_strategy(
251
227
  except Exception:
252
228
  pass
253
229
 
254
- if not policies and manifest and getattr(manifest, "permissions", None):
255
- try:
256
- mpol = manifest.permissions.get("policy")
257
- if isinstance(mpol, str):
258
- policies = [mpol]
259
- elif isinstance(mpol, list):
260
- policies = [p for p in mpol if isinstance(p, str)]
261
- except Exception:
262
- pass
263
-
264
230
  seen = set()
265
231
  deduped: list[str] = []
266
232
  for p in policies:
@@ -321,13 +287,8 @@ def main():
321
287
  parser = argparse.ArgumentParser(description="Run strategy strategies")
322
288
  parser.add_argument(
323
289
  "strategy",
324
- nargs="?",
325
290
  help="Strategy to run (stablecoin_yield_strategy)",
326
291
  )
327
- parser.add_argument(
328
- "--manifest",
329
- help="Path to strategy manifest YAML (alternative to strategy name)",
330
- )
331
292
  parser.add_argument(
332
293
  "--config", help="Path to config file (defaults to config.json)"
333
294
  )
@@ -393,7 +354,6 @@ def main():
393
354
  strategy_name=args.strategy,
394
355
  config_path=args.config,
395
356
  action=args.action,
396
- manifest_path=args.manifest,
397
357
  amount=args.amount,
398
358
  main_token_amount=args.main_token_amount,
399
359
  gas_token_amount=args.gas_token_amount,
@@ -5,7 +5,7 @@ Create a new strategy from template and generate a dedicated wallet for it.
5
5
  This script:
6
6
  1. Copies the strategy template to a new directory
7
7
  2. Generates a wallet with label matching the strategy name
8
- 3. Updates the manifest with the strategy name
8
+ 3. Updates the strategy files with the correct names
9
9
  """
10
10
 
11
11
  import argparse
@@ -13,8 +13,6 @@ import re
13
13
  import shutil
14
14
  from pathlib import Path
15
15
 
16
- import yaml
17
-
18
16
  from wayfinder_paths.core.utils.wallets import make_random_wallet, write_wallet_to_json
19
17
 
20
18
 
@@ -29,18 +27,6 @@ def sanitize_name(name: str) -> str:
29
27
  return name.lower()
30
28
 
31
29
 
32
- def update_manifest(manifest_path: Path, strategy_name: str, entrypoint: str) -> None:
33
- """Update manifest.yaml with strategy name and entrypoint."""
34
- with open(manifest_path) as f:
35
- manifest_data = yaml.safe_load(f)
36
-
37
- manifest_data["name"] = strategy_name
38
- manifest_data["entrypoint"] = entrypoint
39
-
40
- with open(manifest_path, "w") as f:
41
- yaml.dump(manifest_data, f, default_flow_style=False, sort_keys=False)
42
-
43
-
44
30
  def update_strategy_file(strategy_path: Path, class_name: str) -> None:
45
31
  """Update strategy.py with new class name."""
46
32
  content = strategy_path.read_text()
@@ -111,7 +97,6 @@ def main():
111
97
  # Copy template files
112
98
  template_files = [
113
99
  "strategy.py",
114
- "manifest.yaml",
115
100
  "test_strategy.py",
116
101
  "examples.json",
117
102
  "README.md",
@@ -135,15 +120,6 @@ def main():
135
120
  update_strategy_file(strategy_file, class_name)
136
121
  print(f" Updated strategy.py with class name: {class_name}")
137
122
 
138
- # Generate entrypoint path
139
- entrypoint = f"strategies.{dir_name}.strategy.{class_name}"
140
-
141
- # Update manifest with name (using directory name) and entrypoint
142
- manifest_path = strategy_dir / "manifest.yaml"
143
- if manifest_path.exists():
144
- update_manifest(manifest_path, dir_name, entrypoint)
145
- print(f" Updated manifest.yaml with name: {dir_name}")
146
-
147
123
  # Generate wallet with label matching directory name (strategy identifier)
148
124
  # If wallets.json doesn't exist, create it with a main wallet first
149
125
  if not args.wallets_file.exists():
@@ -169,11 +145,10 @@ def main():
169
145
  print(f" Directory: {strategy_dir}")
170
146
  print(f" Name: {dir_name}")
171
147
  print(f" Class: {class_name}")
172
- print(f" Entrypoint: {entrypoint}")
173
148
  print(f" Wallet: {wallet['address']}")
174
149
  print("\nNext steps:")
175
150
  print(f" 1. Edit {strategy_dir / 'strategy.py'} to implement your strategy")
176
- print(f" 2. Update {strategy_dir / 'manifest.yaml'} with required adapters")
151
+ print(" 2. Add required adapters in __init__")
177
152
  print(f" 3. Test with: just test-strategy {dir_name}")
178
153
 
179
154
 
@@ -14,6 +14,15 @@ def _load_wallets(path: Path) -> list[dict[str, Any]]:
14
14
  return [w for w in data if isinstance(w, dict)]
15
15
 
16
16
 
17
+ def _load_config(path: Path) -> dict[str, Any]:
18
+ if not path.exists():
19
+ return {}
20
+ data = json.loads(path.read_text(encoding="utf-8"))
21
+ if not isinstance(data, dict):
22
+ raise ValueError(f"Expected a dict in {path}")
23
+ return data
24
+
25
+
17
26
  def _find_wallet(wallets: list[dict[str, Any]], label: str) -> dict[str, Any]:
18
27
  for w in wallets:
19
28
  if w.get("label") == label:
@@ -36,6 +45,13 @@ def _get_strategy_class(strategy: str):
36
45
 
37
46
  return HyperlendStableYieldStrategy
38
47
 
48
+ if strategy == "moonwell_wsteth_loop_strategy":
49
+ from wayfinder_paths.strategies.moonwell_wsteth_loop_strategy.strategy import (
50
+ MoonwellWstethLoopStrategy,
51
+ )
52
+
53
+ return MoonwellWstethLoopStrategy
54
+
39
55
  raise ValueError(f"Unknown strategy: {strategy}")
40
56
 
41
57
 
@@ -44,18 +60,25 @@ async def _run(args: argparse.Namespace) -> int:
44
60
  wallets_path = (
45
61
  Path(args.wallets).resolve() if args.wallets else repo_root / "wallets.json"
46
62
  )
63
+ config_path = (
64
+ Path(args.config).resolve() if args.config else repo_root / "config.json"
65
+ )
66
+
47
67
  wallets = _load_wallets(wallets_path)
68
+ config = _load_config(config_path)
48
69
 
49
70
  main_wallet = _find_wallet(wallets, args.main_wallet_label)
50
71
  strategy_wallet = _find_wallet(wallets, args.strategy_wallet_label)
51
72
 
73
+ # Merge config with wallet info
74
+ strategy_config = {
75
+ "main_wallet": main_wallet,
76
+ "strategy_wallet": strategy_wallet,
77
+ **config.get("strategy", {}),
78
+ }
79
+
52
80
  strategy_class = _get_strategy_class(args.strategy)
53
- s = strategy_class(
54
- {
55
- "main_wallet": main_wallet,
56
- "strategy_wallet": strategy_wallet,
57
- },
58
- )
81
+ s = strategy_class(strategy_config)
59
82
 
60
83
  await s.setup()
61
84
 
@@ -93,11 +116,18 @@ def main() -> int:
93
116
  p.add_argument(
94
117
  "--strategy",
95
118
  default="basis_trading_strategy",
96
- choices=["basis_trading_strategy", "hyperlend_stable_yield_strategy"],
119
+ choices=[
120
+ "basis_trading_strategy",
121
+ "hyperlend_stable_yield_strategy",
122
+ "moonwell_wsteth_loop_strategy",
123
+ ],
97
124
  )
98
125
  p.add_argument(
99
126
  "--wallets", default=None, help="Path to wallets.json (default: repo root)"
100
127
  )
128
+ p.add_argument(
129
+ "--config", default=None, help="Path to config.json (default: repo root)"
130
+ )
101
131
  p.add_argument("--main-wallet-label", default="main")
102
132
  p.add_argument("--strategy-wallet-label", default="basis_trading_strategy")
103
133
 
@@ -164,7 +164,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
164
164
  gas_maximum=GAS_MAXIMUM,
165
165
  gas_threshold=GAS_MAXIMUM / 3,
166
166
  volatility=Volatility.MEDIUM,
167
- volatility_description_short="Delta-neutral but funding can flip negative.",
167
+ volatility_description="Delta-neutral but funding can flip negative.",
168
168
  directionality=Directionality.DELTA_NEUTRAL,
169
169
  directionality_description="Matched spot long and perp short cancels directional exposure.",
170
170
  complexity=Complexity.MEDIUM,
@@ -1,7 +1,6 @@
1
1
  # Hyperlend Stable Yield Strategy
2
2
 
3
3
  - Entrypoint: `strategies.hyperlend_stable_yield_strategy.strategy.HyperlendStableYieldStrategy`
4
- - Manifest: `manifest.yaml`
5
4
  - Examples: `examples.json`
6
5
  - Tests: `test_strategy.py`
7
6
 
@@ -14,14 +13,6 @@ Allocates USDT0 on HyperEVM across HyperLend stablecoin markets. The strategy:
14
13
  3. Tops up the small HYPE gas buffer if needed, swaps USDT0 into the target stablecoin, and supplies it to HyperLend.
15
14
  4. Enforces a hysteresis rotation policy so minor APY noise does not churn capital.
16
15
 
17
- ## Policy
18
-
19
- The manifest policy simply locks transactions to the strategy wallet ID:
20
-
21
- ```
22
- (wallet.id == 'FORMAT_WALLET_ID')
23
- ```
24
-
25
16
  ## Key parameters
26
17
 
27
18
  - `MIN_USDT0_DEPOSIT_AMOUNT = 1`
@@ -91,10 +82,4 @@ poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strateg
91
82
  poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy --action withdraw --config $(pwd)/config.json
92
83
  ```
93
84
 
94
- Use the manifest directly if you prefer:
95
-
96
- ```bash
97
- poetry run python wayfinder_paths/run_strategy.py --manifest wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml --action status --config $(pwd)/config.json
98
- ```
99
-
100
85
  Wallet addresses/labels are auto-resolved from `wallets.json`. Set `NETWORK=testnet` in your config to run the orchestration without touching live HyperEVM endpoints.
@@ -112,7 +112,7 @@ class HyperlendStableYieldStrategy(Strategy):
112
112
  gas_threshold=MAX_GAS / 3,
113
113
  # risk indicators
114
114
  volatility=Volatility.LOW,
115
- volatility_description_short=(
115
+ volatility_description=(
116
116
  "Pure HyperLend stablecoin lending keeps NAV steady aside from rate drift."
117
117
  ),
118
118
  directionality=Directionality.MARKET_NEUTRAL,
@@ -0,0 +1,108 @@
1
+ # Moonwell wstETH Loop Strategy
2
+
3
+ - Entrypoint: `strategies.moonwell_wsteth_loop_strategy.strategy.MoonwellWstethLoopStrategy`
4
+ - Examples: `examples.json`
5
+ - Tests: `test_strategy.py`
6
+
7
+ ## What it does
8
+
9
+ A leveraged liquid-staking carry trade on Base that loops USDC collateral into wstETH exposure. The strategy:
10
+
11
+ 1. Deposits USDC as initial collateral on Moonwell lending protocol.
12
+ 2. Borrows WETH against the USDC collateral.
13
+ 3. Swaps WETH to wstETH via Aerodrome/BRAP routing.
14
+ 4. Lends wstETH back to Moonwell as additional collateral.
15
+ 5. Repeats the loop until target leverage is reached or marginal gains fall below threshold.
16
+
17
+ The position is **delta-neutral**: WETH debt offsets wstETH collateral, so PnL is driven by the spread between wstETH staking yield and WETH borrow cost.
18
+
19
+ ## Key parameters
20
+
21
+ - `MIN_GAS = 0.002` ETH (minimum Base ETH for gas)
22
+ - `MIN_USDC_DEPOSIT = 20` USDC (minimum initial collateral)
23
+ - `MAX_DEPEG = 0.01` (1% max stETH/ETH depeg threshold)
24
+ - `MIN_HEALTH_FACTOR = 1.2` (triggers deleveraging if below)
25
+ - `MAX_HEALTH_FACTOR = 1.5` (triggers leverage loop if above)
26
+ - `leverage_limit = 10` (maximum leverage multiplier)
27
+ - `COLLATERAL_SAFETY_FACTOR = 0.98` (2% safety buffer on borrows)
28
+ - `MAX_SLIPPAGE_TOLERANCE = 0.03` (3% max slippage to prevent MEV)
29
+ - `_MIN_LEVERAGE_GAIN_BPS = 50e-4` (stop looping if marginal gain < 50 bps)
30
+
31
+ ## Safety features
32
+
33
+ - **Depeg guard**: `_max_safe_F()` calculates leverage ceiling based on wstETH collateral factor and max depeg tolerance.
34
+ - **Delta-neutrality**: `_balance_weth_debt()` rebalances when WETH debt exceeds wstETH collateral value.
35
+ - **Swap retries**: `_swap_with_retries()` uses progressive slippage (0.5% → 1% → 1.5%) with exponential backoff.
36
+ - **Health monitoring**: Automatic deleveraging when health factor drops below `MIN_HEALTH_FACTOR`.
37
+ - **Rollback protection**: Checks actual balances before rollback swaps to prevent failed transactions.
38
+
39
+ ## Adapters used
40
+
41
+ - `BalanceAdapter` for token balances and wallet transfers with ledger tracking.
42
+ - `TokenAdapter` for token metadata and price feeds.
43
+ - `LedgerAdapter` for net deposit tracking.
44
+ - `BRAPAdapter` for swap quotes and execution via Aerodrome/routing.
45
+ - `MoonwellAdapter` for lending, borrowing, collateral management, and position queries.
46
+ - `LocalTokenTxnService` via `DefaultWeb3Service` for low-level sends/approvals.
47
+
48
+ ## Actions
49
+
50
+ ### Deposit
51
+
52
+ - Validates USDC and ETH balances in the main wallet.
53
+ - Transfers ETH (gas) into the strategy wallet if needed.
54
+ - Moves USDC from main wallet to strategy wallet.
55
+ - Lends USDC on Moonwell and enables as collateral.
56
+ - Executes leverage loop: borrow WETH → swap to wstETH → lend wstETH → repeat.
57
+
58
+ ### Update
59
+
60
+ - Checks gas balance meets maintenance threshold.
61
+ - Balances WETH debt against wstETH collateral for delta-neutrality.
62
+ - Computes health factor from aggregated positions.
63
+ - If HF < MIN: triggers deleveraging via `_repay_debt_loop()`.
64
+ - If HF > MAX: executes additional leverage loops to optimize yield.
65
+ - Claims WELL rewards if above minimum threshold.
66
+
67
+ ### Status
68
+
69
+ `_status()` returns:
70
+
71
+ - `portfolio_value`: sum of all position values (USDC lent + wstETH lent - WETH debt)
72
+ - `net_deposit`: fetched from LedgerAdapter
73
+ - `strategy_status`: includes current leverage, health factor, LTV, peg diff, credit remaining
74
+
75
+ ### Withdraw
76
+
77
+ - Sweeps miscellaneous token balances to WETH.
78
+ - Repays all WETH debt via `_repay_debt_loop()`.
79
+ - Unlends wstETH, swaps to USDC.
80
+ - Unlends USDC collateral.
81
+ - Returns USDC and remaining ETH to main wallet.
82
+
83
+ ## Running locally
84
+
85
+ ```bash
86
+ # Install dependencies
87
+ poetry install
88
+
89
+ # Generate wallets (writes wallets.json)
90
+ poetry run python wayfinder_paths/scripts/make_wallets.py -n 1
91
+
92
+ # Copy config and edit credentials
93
+ cp wayfinder_paths/config.example.json config.json
94
+
95
+ # Check status / health
96
+ poetry run python wayfinder_paths/run_strategy.py moonwell_wsteth_loop_strategy --action status --config $(pwd)/config.json
97
+
98
+ # Perform a deposit/update/withdraw cycle
99
+ poetry run python wayfinder_paths/run_strategy.py moonwell_wsteth_loop_strategy --action deposit --main-token-amount 100 --gas-token-amount 0.01 --config $(pwd)/config.json
100
+ poetry run python wayfinder_paths/run_strategy.py moonwell_wsteth_loop_strategy --action update --config $(pwd)/config.json
101
+ poetry run python wayfinder_paths/run_strategy.py moonwell_wsteth_loop_strategy --action withdraw --config $(pwd)/config.json
102
+ ```
103
+
104
+ ## Testing
105
+
106
+ ```bash
107
+ poetry run pytest wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/ -v
108
+ ```
@@ -0,0 +1,11 @@
1
+ {
2
+ "smoke": {
3
+ "deposit": {
4
+ "main_token_amount": 0.1,
5
+ "gas_token_amount": 0.001
6
+ },
7
+ "update": {},
8
+ "status": {},
9
+ "withdraw": {}
10
+ }
11
+ }