wayfinder-paths 0.1.9__py3-none-any.whl → 0.1.10__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 (53) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +1 -1
  2. wayfinder_paths/adapters/balance_adapter/README.md +1 -2
  3. wayfinder_paths/adapters/balance_adapter/adapter.py +4 -4
  4. wayfinder_paths/adapters/brap_adapter/adapter.py +139 -23
  5. wayfinder_paths/adapters/moonwell_adapter/README.md +174 -0
  6. wayfinder_paths/adapters/moonwell_adapter/__init__.py +7 -0
  7. wayfinder_paths/adapters/moonwell_adapter/adapter.py +1226 -0
  8. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +635 -0
  9. wayfinder_paths/core/clients/AuthClient.py +3 -0
  10. wayfinder_paths/core/clients/WayfinderClient.py +2 -2
  11. wayfinder_paths/core/constants/__init__.py +0 -2
  12. wayfinder_paths/core/constants/base.py +6 -2
  13. wayfinder_paths/core/constants/moonwell_abi.py +411 -0
  14. wayfinder_paths/core/engine/StrategyJob.py +3 -0
  15. wayfinder_paths/core/services/base.py +55 -0
  16. wayfinder_paths/core/services/local_evm_txn.py +288 -208
  17. wayfinder_paths/core/services/local_token_txn.py +46 -26
  18. wayfinder_paths/core/strategies/descriptors.py +1 -1
  19. wayfinder_paths/run_strategy.py +34 -74
  20. wayfinder_paths/scripts/create_strategy.py +2 -27
  21. wayfinder_paths/scripts/run_strategy.py +37 -7
  22. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +1 -1
  23. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -15
  24. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +1 -1
  25. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +108 -0
  26. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/examples.json +11 -0
  27. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2975 -0
  28. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +886 -0
  29. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -7
  30. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1 -1
  31. wayfinder_paths/templates/adapter/README.md +5 -21
  32. wayfinder_paths/templates/adapter/adapter.py +1 -2
  33. wayfinder_paths/templates/adapter/test_adapter.py +1 -1
  34. wayfinder_paths/templates/strategy/README.md +4 -21
  35. wayfinder_paths/tests/test_smoke_manifest.py +17 -2
  36. {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.10.dist-info}/METADATA +60 -187
  37. {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.10.dist-info}/RECORD +39 -44
  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/core/engine/manifest.py +0 -97
  46. wayfinder_paths/scripts/validate_manifests.py +0 -213
  47. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +0 -23
  48. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +0 -7
  49. wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +0 -17
  50. wayfinder_paths/templates/adapter/manifest.yaml +0 -6
  51. wayfinder_paths/templates/strategy/manifest.yaml +0 -8
  52. {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.10.dist-info}/LICENSE +0 -0
  53. {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.10.dist-info}/WHEEL +0 -0
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  from decimal import ROUND_DOWN, Decimal
4
5
  from typing import Any
5
6
 
@@ -27,7 +28,6 @@ class LocalTokenTxnService(TokenTxn):
27
28
  self.wallet_provider = wallet_provider
28
29
  self.logger = logger.bind(service="DefaultEvmTransactionService")
29
30
  self.token_client = TokenClient()
30
- self.builder = _EvmTransactionBuilder(wallet_provider)
31
31
 
32
32
  async def build_send(
33
33
  self,
@@ -61,7 +61,7 @@ class LocalTokenTxnService(TokenTxn):
61
61
  amount_wei = self._to_base_units(amount, decimals)
62
62
 
63
63
  try:
64
- tx = await self.builder.build_send_transaction(
64
+ tx = await self.build_send_transaction(
65
65
  from_address=from_address,
66
66
  to_address=to_address,
67
67
  token_address=token_address,
@@ -93,7 +93,7 @@ class LocalTokenTxnService(TokenTxn):
93
93
  except (TypeError, ValueError) as exc:
94
94
  return False, str(exc)
95
95
 
96
- approve_tx = self.builder.build_erc20_approval_transaction(
96
+ approve_tx = self.build_erc20_approval_transaction(
97
97
  chain_id=chain_id,
98
98
  token_address=token_checksum,
99
99
  from_address=from_checksum,
@@ -104,28 +104,53 @@ class LocalTokenTxnService(TokenTxn):
104
104
  return True, approve_tx
105
105
 
106
106
  async def read_erc20_allowance(
107
- self, chain: Any, token_address: str, from_address: str, spender_address: str
107
+ self,
108
+ chain: Any,
109
+ token_address: str,
110
+ from_address: str,
111
+ spender_address: str,
112
+ max_retries: int = 3,
108
113
  ) -> dict[str, Any]:
109
114
  try:
110
115
  chain_id = self._chain_id(chain)
111
116
  except (TypeError, ValueError) as exc:
112
117
  return {"error": str(exc), "allowance": 0}
113
118
 
114
- w3 = self.get_web3(chain_id)
115
- try:
116
- contract = w3.eth.contract(
117
- address=to_checksum_address(token_address), abi=ERC20_APPROVAL_ABI
118
- )
119
- allowance = await contract.functions.allowance(
120
- to_checksum_address(from_address),
121
- to_checksum_address(spender_address),
122
- ).call()
123
- return (True, {"allowance": int(allowance)})
124
- except Exception as exc: # noqa: BLE001
125
- self.logger.error(f"Failed to read allowance: {exc}")
126
- return {"error": f"Allowance query failed: {exc}", "allowance": 0}
127
- finally:
128
- await self._close_web3(w3)
119
+ last_error = None
120
+ for attempt in range(max_retries):
121
+ w3 = self.wallet_provider.get_web3(chain_id)
122
+ try:
123
+ contract = w3.eth.contract(
124
+ address=to_checksum_address(token_address), abi=ERC20_APPROVAL_ABI
125
+ )
126
+ allowance = await contract.functions.allowance(
127
+ to_checksum_address(from_address),
128
+ to_checksum_address(spender_address),
129
+ ).call()
130
+ return {"allowance": int(allowance)}
131
+ except Exception as exc: # noqa: BLE001
132
+ last_error = exc
133
+ error_str = str(exc)
134
+ if "429" in error_str or "Too Many Requests" in error_str:
135
+ if attempt < max_retries - 1:
136
+ wait_time = 3.0 * (2**attempt) # 3, 6, 12 seconds
137
+ self.logger.warning(
138
+ f"Rate limited reading allowance, retrying in {wait_time}s..."
139
+ )
140
+ await asyncio.sleep(wait_time)
141
+ continue
142
+ self.logger.error(f"Failed to read allowance: {exc}")
143
+ return {"error": f"Allowance query failed: {exc}", "allowance": 0}
144
+ finally:
145
+ await self.wallet_provider._close_web3(w3)
146
+
147
+ self.logger.error(
148
+ f"Failed to read allowance after {max_retries} retries: {last_error}"
149
+ )
150
+ return {
151
+ "error": f"Allowance query failed after retries: {last_error}",
152
+ "allowance": 0,
153
+ }
129
154
 
130
155
  def _chain_id(self, chain: Any) -> int:
131
156
  if isinstance(chain, dict):
@@ -144,13 +169,6 @@ class LocalTokenTxnService(TokenTxn):
144
169
  )
145
170
  return int(quantized)
146
171
 
147
-
148
- class _EvmTransactionBuilder:
149
- """Helpers that only build transaction dictionaries for sends and approvals."""
150
-
151
- def __init__(self, wallet_provider: EvmTxn) -> None:
152
- self.wallet_provider = wallet_provider
153
-
154
172
  async def build_send_transaction(
155
173
  self,
156
174
  *,
@@ -200,6 +218,7 @@ class _EvmTransactionBuilder:
200
218
  web3: AsyncWeb3,
201
219
  ) -> dict[str, Any]:
202
220
  """Build an ERC20 approval transaction dict."""
221
+ del web3 # Use sync Web3 for encoding (AsyncContract doesn't have encodeABI)
203
222
  token_checksum = to_checksum_address(token_address)
204
223
  spender_checksum = to_checksum_address(spender)
205
224
  from_checksum = to_checksum_address(from_address)
@@ -208,6 +227,7 @@ class _EvmTransactionBuilder:
208
227
  # Use synchronous Web3 for encoding (encodeABI doesn't exist in web3.py v7)
209
228
  w3_sync = Web3()
210
229
  contract = w3_sync.eth.contract(address=token_checksum, abi=ERC20_APPROVAL_ABI)
230
+
211
231
  # In web3.py v7, use _encode_transaction_data to encode without network calls
212
232
  data = contract.functions.approve(
213
233
  spender_checksum, amount_int
@@ -66,7 +66,7 @@ class StratDescriptor(BaseModel):
66
66
 
67
67
  # risk indicators
68
68
  volatility: Volatility
69
- volatility_description_short: str
69
+ volatility_description: str
70
70
  directionality: Directionality
71
71
  directionality_description: str
72
72
  complexity: Complexity
@@ -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
+ }