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.
- wayfinder_paths/CONFIG_GUIDE.md +1 -1
- wayfinder_paths/adapters/balance_adapter/README.md +1 -2
- wayfinder_paths/adapters/balance_adapter/adapter.py +4 -4
- wayfinder_paths/adapters/brap_adapter/adapter.py +139 -23
- wayfinder_paths/adapters/moonwell_adapter/README.md +174 -0
- wayfinder_paths/adapters/moonwell_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +1226 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +635 -0
- wayfinder_paths/core/clients/AuthClient.py +3 -0
- wayfinder_paths/core/clients/WayfinderClient.py +2 -2
- wayfinder_paths/core/constants/__init__.py +0 -2
- wayfinder_paths/core/constants/base.py +6 -2
- wayfinder_paths/core/constants/moonwell_abi.py +411 -0
- wayfinder_paths/core/engine/StrategyJob.py +3 -0
- wayfinder_paths/core/services/base.py +55 -0
- wayfinder_paths/core/services/local_evm_txn.py +288 -208
- wayfinder_paths/core/services/local_token_txn.py +46 -26
- wayfinder_paths/core/strategies/descriptors.py +1 -1
- wayfinder_paths/run_strategy.py +34 -74
- wayfinder_paths/scripts/create_strategy.py +2 -27
- wayfinder_paths/scripts/run_strategy.py +37 -7
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +1 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -15
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +1 -1
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +108 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/examples.json +11 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2975 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +886 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1 -1
- wayfinder_paths/templates/adapter/README.md +5 -21
- wayfinder_paths/templates/adapter/adapter.py +1 -2
- wayfinder_paths/templates/adapter/test_adapter.py +1 -1
- wayfinder_paths/templates/strategy/README.md +4 -21
- wayfinder_paths/tests/test_smoke_manifest.py +17 -2
- {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.10.dist-info}/METADATA +60 -187
- {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.10.dist-info}/RECORD +39 -44
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +0 -8
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +0 -11
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +0 -10
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +0 -8
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +0 -11
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +0 -10
- wayfinder_paths/adapters/token_adapter/manifest.yaml +0 -6
- wayfinder_paths/core/engine/manifest.py +0 -97
- wayfinder_paths/scripts/validate_manifests.py +0 -213
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +0 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +0 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +0 -17
- wayfinder_paths/templates/adapter/manifest.yaml +0 -6
- wayfinder_paths/templates/strategy/manifest.yaml +0 -8
- {wayfinder_paths-0.1.9.dist-info → wayfinder_paths-0.1.10.dist-info}/LICENSE +0 -0
- {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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
wayfinder_paths/run_strategy.py
CHANGED
|
@@ -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
|
|
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
|
-
#
|
|
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
|
|
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 / "
|
|
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
|
-
#
|
|
55
|
-
|
|
56
|
-
module_path,
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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=
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
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(
|
|
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=[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
```
|