wayfinder-paths 0.1.4__py3-none-any.whl → 0.1.6__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 +14 -14
- wayfinder_paths/__init__.py +4 -3
- wayfinder_paths/adapters/balance_adapter/README.md +10 -10
- wayfinder_paths/adapters/balance_adapter/adapter.py +10 -9
- wayfinder_paths/adapters/balance_adapter/examples.json +1 -1
- wayfinder_paths/adapters/brap_adapter/README.md +1 -1
- wayfinder_paths/adapters/brap_adapter/adapter.py +28 -21
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +33 -26
- wayfinder_paths/adapters/ledger_adapter/README.md +26 -39
- wayfinder_paths/adapters/ledger_adapter/adapter.py +78 -75
- wayfinder_paths/adapters/ledger_adapter/examples.json +10 -4
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +4 -4
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +31 -26
- wayfinder_paths/adapters/pool_adapter/README.md +1 -13
- wayfinder_paths/adapters/pool_adapter/adapter.py +12 -19
- wayfinder_paths/adapters/token_adapter/adapter.py +8 -4
- wayfinder_paths/core/__init__.py +9 -4
- wayfinder_paths/core/adapters/BaseAdapter.py +20 -3
- wayfinder_paths/core/adapters/models.py +41 -0
- wayfinder_paths/core/clients/BRAPClient.py +21 -2
- wayfinder_paths/core/clients/ClientManager.py +42 -63
- wayfinder_paths/core/clients/HyperlendClient.py +46 -5
- wayfinder_paths/core/clients/LedgerClient.py +350 -124
- wayfinder_paths/core/clients/PoolClient.py +51 -19
- wayfinder_paths/core/clients/SimulationClient.py +16 -4
- wayfinder_paths/core/clients/TokenClient.py +34 -18
- wayfinder_paths/core/clients/TransactionClient.py +18 -2
- wayfinder_paths/core/clients/WalletClient.py +35 -4
- wayfinder_paths/core/clients/WayfinderClient.py +16 -5
- wayfinder_paths/core/clients/protocols.py +69 -62
- wayfinder_paths/core/clients/sdk_example.py +0 -5
- wayfinder_paths/core/config.py +192 -103
- wayfinder_paths/core/constants/base.py +17 -0
- wayfinder_paths/core/engine/{VaultJob.py → StrategyJob.py} +25 -19
- wayfinder_paths/core/engine/__init__.py +2 -2
- wayfinder_paths/core/services/base.py +6 -4
- wayfinder_paths/core/services/local_evm_txn.py +3 -2
- wayfinder_paths/core/settings.py +2 -2
- wayfinder_paths/core/strategies/Strategy.py +135 -38
- wayfinder_paths/core/strategies/descriptors.py +1 -0
- wayfinder_paths/core/utils/evm_helpers.py +12 -10
- wayfinder_paths/core/wallets/README.md +3 -3
- wayfinder_paths/core/wallets/WalletManager.py +3 -3
- wayfinder_paths/run_strategy.py +26 -24
- wayfinder_paths/scripts/make_wallets.py +6 -6
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +6 -6
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +36 -156
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +6 -6
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +11 -11
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +1 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +92 -92
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +6 -6
- wayfinder_paths/templates/adapter/README.md +1 -1
- wayfinder_paths/templates/adapter/test_adapter.py +1 -1
- wayfinder_paths/templates/strategy/README.md +4 -4
- wayfinder_paths/templates/strategy/test_strategy.py +7 -7
- wayfinder_paths/tests/test_test_coverage.py +5 -5
- {wayfinder_paths-0.1.4.dist-info → wayfinder_paths-0.1.6.dist-info}/METADATA +46 -47
- {wayfinder_paths-0.1.4.dist-info → wayfinder_paths-0.1.6.dist-info}/RECORD +61 -60
- {wayfinder_paths-0.1.4.dist-info → wayfinder_paths-0.1.6.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.4.dist-info → wayfinder_paths-0.1.6.dist-info}/WHEEL +0 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
import traceback
|
|
3
5
|
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
4
7
|
from typing import Any, TypedDict
|
|
5
8
|
|
|
6
9
|
from loguru import logger
|
|
7
10
|
|
|
11
|
+
from wayfinder_paths.core.clients.TokenClient import TokenDetails
|
|
8
12
|
from wayfinder_paths.core.services.base import Web3Service
|
|
9
13
|
from wayfinder_paths.core.strategies.descriptors import StratDescriptor
|
|
10
14
|
|
|
@@ -13,32 +17,59 @@ class StatusDict(TypedDict):
|
|
|
13
17
|
portfolio_value: float
|
|
14
18
|
net_deposit: float
|
|
15
19
|
strategy_status: Any
|
|
20
|
+
gas_available: float
|
|
21
|
+
gassed_up: bool
|
|
16
22
|
|
|
17
23
|
|
|
18
24
|
StatusTuple = tuple[bool, str]
|
|
19
25
|
|
|
20
26
|
|
|
27
|
+
class WalletConfig(TypedDict, total=False):
|
|
28
|
+
"""Wallet configuration structure - allows additional fields for flexibility"""
|
|
29
|
+
|
|
30
|
+
address: str
|
|
31
|
+
private_key: str | None
|
|
32
|
+
private_key_hex: str | None
|
|
33
|
+
wallet_type: str | None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StrategyConfig(TypedDict, total=False):
|
|
37
|
+
"""Base strategy configuration structure - allows additional fields for flexibility"""
|
|
38
|
+
|
|
39
|
+
main_wallet: WalletConfig | None
|
|
40
|
+
strategy_wallet: WalletConfig | None
|
|
41
|
+
wallet_type: str | None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LiquidationResult(TypedDict):
|
|
45
|
+
usd_value: float
|
|
46
|
+
token: TokenDetails
|
|
47
|
+
amt: int
|
|
48
|
+
|
|
49
|
+
|
|
21
50
|
class Strategy(ABC):
|
|
22
|
-
name: str = None
|
|
23
|
-
INFO: StratDescriptor = None
|
|
51
|
+
name: str | None = None
|
|
52
|
+
INFO: StratDescriptor | None = None
|
|
24
53
|
|
|
25
54
|
def __init__(
|
|
26
55
|
self,
|
|
27
|
-
config: dict[str, Any] | None = None,
|
|
56
|
+
config: StrategyConfig | dict[str, Any] | None = None,
|
|
28
57
|
*,
|
|
29
|
-
main_wallet: dict[str, Any] | None = None,
|
|
30
|
-
|
|
58
|
+
main_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
59
|
+
strategy_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
31
60
|
simulation: bool = False,
|
|
32
|
-
web3_service: Web3Service = None,
|
|
61
|
+
web3_service: Web3Service | None = None,
|
|
33
62
|
api_key: str | None = None,
|
|
34
63
|
):
|
|
35
64
|
self.adapters = {}
|
|
36
|
-
self.
|
|
65
|
+
self.ledger_adapter = None
|
|
37
66
|
self.logger = logger.bind(strategy=self.__class__.__name__)
|
|
38
67
|
if api_key:
|
|
39
68
|
os.environ["WAYFINDER_API_KEY"] = api_key
|
|
40
69
|
|
|
41
|
-
|
|
70
|
+
self.config = config
|
|
71
|
+
|
|
72
|
+
async def setup(self) -> None:
|
|
42
73
|
"""Initialize strategy-specific setup after construction"""
|
|
43
74
|
pass
|
|
44
75
|
|
|
@@ -46,38 +77,72 @@ class Strategy(ABC):
|
|
|
46
77
|
"""Log messages - can be overridden by subclasses"""
|
|
47
78
|
self.logger.info(msg)
|
|
48
79
|
|
|
49
|
-
async def
|
|
50
|
-
"""Hook for temporary UI messages (e.g., progress) to the chat window."""
|
|
51
|
-
# No-op by default; strategies/hosts can override
|
|
52
|
-
return None
|
|
53
|
-
|
|
54
|
-
async def quote(self):
|
|
80
|
+
async def quote(self) -> None:
|
|
55
81
|
"""Get quotes for potential trades - optional for strategies"""
|
|
56
82
|
pass
|
|
57
83
|
|
|
84
|
+
def _get_strategy_wallet_address(self) -> str:
|
|
85
|
+
"""Get strategy wallet address with validation."""
|
|
86
|
+
strategy_wallet = self.config.get("strategy_wallet")
|
|
87
|
+
if not strategy_wallet or not isinstance(strategy_wallet, dict):
|
|
88
|
+
raise ValueError("strategy_wallet not configured in strategy config")
|
|
89
|
+
address = strategy_wallet.get("address")
|
|
90
|
+
if not address:
|
|
91
|
+
raise ValueError("strategy_wallet address not found in config")
|
|
92
|
+
return str(address)
|
|
93
|
+
|
|
94
|
+
def _get_main_wallet_address(self) -> str:
|
|
95
|
+
"""Get main wallet address with validation."""
|
|
96
|
+
main_wallet = self.config.get("main_wallet")
|
|
97
|
+
if not main_wallet or not isinstance(main_wallet, dict):
|
|
98
|
+
raise ValueError("main_wallet not configured in strategy config")
|
|
99
|
+
address = main_wallet.get("address")
|
|
100
|
+
if not address:
|
|
101
|
+
raise ValueError("main_wallet address not found in config")
|
|
102
|
+
return str(address)
|
|
103
|
+
|
|
58
104
|
@abstractmethod
|
|
59
105
|
async def deposit(self, **kwargs) -> StatusTuple:
|
|
60
106
|
"""
|
|
61
|
-
Deposit funds into the strategy
|
|
62
|
-
|
|
107
|
+
Deposit funds into the strategy.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
**kwargs: Strategy-specific deposit parameters. Common parameters include:
|
|
111
|
+
- main_token_amount: Amount of main token to deposit (float)
|
|
112
|
+
- gas_token_amount: Amount of gas token to deposit (float)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Tuple of (success: bool, message: str)
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: If required parameters are missing or invalid.
|
|
63
119
|
"""
|
|
64
120
|
pass
|
|
65
121
|
|
|
66
122
|
async def withdraw(self, **kwargs) -> StatusTuple:
|
|
67
123
|
"""
|
|
68
|
-
Withdraw funds from the strategy
|
|
69
|
-
Default implementation unwinds all operations
|
|
70
|
-
|
|
124
|
+
Withdraw funds from the strategy.
|
|
125
|
+
Default implementation unwinds all operations.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
**kwargs: Strategy-specific withdrawal parameters (optional).
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Tuple of (success: bool, message: str)
|
|
132
|
+
|
|
133
|
+
Note:
|
|
134
|
+
Subclasses may override this method to add validation or custom
|
|
135
|
+
withdrawal logic. The base implementation unwinds all ledger operations.
|
|
71
136
|
"""
|
|
72
|
-
if hasattr(self, "
|
|
73
|
-
while self.
|
|
74
|
-
node = self.
|
|
137
|
+
if hasattr(self, "ledger_adapter") and self.ledger_adapter:
|
|
138
|
+
while self.ledger_adapter.positions.operations:
|
|
139
|
+
node = self.ledger_adapter.positions.operations[-1]
|
|
75
140
|
adapter = self.adapters.get(node.adapter)
|
|
76
141
|
if adapter and hasattr(adapter, "unwind_op"):
|
|
77
142
|
await adapter.unwind_op(node)
|
|
78
|
-
self.
|
|
143
|
+
self.ledger_adapter.positions.operations.pop()
|
|
79
144
|
|
|
80
|
-
await self.
|
|
145
|
+
await self.ledger_adapter.save()
|
|
81
146
|
|
|
82
147
|
return (True, "Withdrawal complete")
|
|
83
148
|
|
|
@@ -108,7 +173,14 @@ class Strategy(ABC):
|
|
|
108
173
|
Wrapper to compute and return strategy status. In Django, this also snapshots.
|
|
109
174
|
Here we simply delegate to _status for compatibility.
|
|
110
175
|
"""
|
|
111
|
-
|
|
176
|
+
|
|
177
|
+
status = await self._status()
|
|
178
|
+
await self.ledger_adapter.record_strategy_snapshot(
|
|
179
|
+
wallet_address=self._get_strategy_wallet_address(),
|
|
180
|
+
strategy_status=status,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return status
|
|
112
184
|
|
|
113
185
|
def register_adapters(self, adapters: list[Any]) -> None:
|
|
114
186
|
"""Register adapters for use by the strategy"""
|
|
@@ -119,13 +191,15 @@ class Strategy(ABC):
|
|
|
119
191
|
elif hasattr(adapter, "__class__"):
|
|
120
192
|
self.adapters[adapter.__class__.__name__] = adapter
|
|
121
193
|
|
|
122
|
-
def unwind_on_error(
|
|
194
|
+
def unwind_on_error(
|
|
195
|
+
self, func: Callable[..., Awaitable[StatusTuple]]
|
|
196
|
+
) -> Callable[..., Awaitable[StatusTuple]]:
|
|
123
197
|
"""
|
|
124
198
|
Decorator to unwind operations on error
|
|
125
199
|
Useful for deposit operations that need cleanup on failure
|
|
126
200
|
"""
|
|
127
201
|
|
|
128
|
-
async def wrapper(*args, **kwargs):
|
|
202
|
+
async def wrapper(*args: Any, **kwargs: Any) -> StatusTuple:
|
|
129
203
|
try:
|
|
130
204
|
return await func(*args, **kwargs)
|
|
131
205
|
except Exception:
|
|
@@ -143,21 +217,28 @@ class Strategy(ABC):
|
|
|
143
217
|
f"Strategy failed and unwinding also failed. Operation error: {trace}. Unwind error: {trace2}",
|
|
144
218
|
)
|
|
145
219
|
finally:
|
|
146
|
-
if hasattr(self, "
|
|
147
|
-
await self.
|
|
220
|
+
if hasattr(self, "ledger_adapter") and self.ledger_adapter:
|
|
221
|
+
await self.ledger_adapter.save()
|
|
148
222
|
|
|
149
223
|
return wrapper
|
|
150
224
|
|
|
151
225
|
@classmethod
|
|
152
226
|
def get_metadata(cls) -> dict[str, Any]:
|
|
153
227
|
"""
|
|
154
|
-
Return metadata about this strategy
|
|
155
|
-
Can be overridden to provide discovery information
|
|
228
|
+
Return metadata about this strategy.
|
|
229
|
+
Can be overridden to provide discovery information.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Dictionary containing strategy metadata. The following keys are optional
|
|
233
|
+
and will be None if not defined on the class:
|
|
234
|
+
- name: Strategy name
|
|
235
|
+
- description: Strategy description
|
|
236
|
+
- summary: Strategy summary
|
|
156
237
|
"""
|
|
157
238
|
return {
|
|
158
|
-
"name": cls
|
|
159
|
-
"description": cls
|
|
160
|
-
"summary": cls
|
|
239
|
+
"name": getattr(cls, "name", None),
|
|
240
|
+
"description": getattr(cls, "description", None),
|
|
241
|
+
"summary": getattr(cls, "summary", None),
|
|
161
242
|
}
|
|
162
243
|
|
|
163
244
|
async def health_check(self) -> dict[str, Any]:
|
|
@@ -174,10 +255,26 @@ class Strategy(ABC):
|
|
|
174
255
|
|
|
175
256
|
return health
|
|
176
257
|
|
|
177
|
-
async def partial_liquidate(
|
|
258
|
+
async def partial_liquidate(
|
|
259
|
+
self, usd_value: float
|
|
260
|
+
) -> tuple[bool, LiquidationResult]:
|
|
178
261
|
"""
|
|
179
|
-
Partially liquidate strategy positions by USD value
|
|
180
|
-
Optional method that can be overridden by subclasses
|
|
181
|
-
|
|
262
|
+
Partially liquidate strategy positions by USD value.
|
|
263
|
+
Optional method that can be overridden by subclasses.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
usd_value: USD value to liquidate (must be positive).
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Tuple of (success: bool, message: str)
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ValueError: If usd_value is not positive.
|
|
273
|
+
|
|
274
|
+
Note:
|
|
275
|
+
Base implementation returns failure. Subclasses should override
|
|
276
|
+
to implement partial liquidation logic.
|
|
182
277
|
"""
|
|
278
|
+
if usd_value <= 0:
|
|
279
|
+
raise ValueError(f"usd_value must be positive, got {usd_value}")
|
|
183
280
|
return (False, "Partial liquidation not implemented for this strategy")
|
|
@@ -130,41 +130,43 @@ def resolve_private_key_for_from_address(
|
|
|
130
130
|
"""
|
|
131
131
|
from_addr_norm = (from_address or "").lower()
|
|
132
132
|
main_wallet = config.get("main_wallet")
|
|
133
|
-
|
|
133
|
+
strategy_wallet = config.get("strategy_wallet")
|
|
134
134
|
|
|
135
135
|
main_pk = None
|
|
136
|
-
|
|
136
|
+
strategy_pk = None
|
|
137
137
|
try:
|
|
138
138
|
if isinstance(main_wallet, dict):
|
|
139
139
|
main_pk = main_wallet.get("private_key") or main_wallet.get(
|
|
140
140
|
"private_key_hex"
|
|
141
141
|
)
|
|
142
|
-
if isinstance(
|
|
143
|
-
|
|
142
|
+
if isinstance(strategy_wallet, dict):
|
|
143
|
+
strategy_pk = strategy_wallet.get("private_key") or strategy_wallet.get(
|
|
144
144
|
"private_key_hex"
|
|
145
145
|
)
|
|
146
146
|
except (AttributeError, TypeError) as e:
|
|
147
147
|
logger.debug("Error resolving private keys from wallet config: %s", e)
|
|
148
148
|
|
|
149
149
|
main_addr = None
|
|
150
|
-
|
|
150
|
+
strategy_addr = None
|
|
151
151
|
try:
|
|
152
152
|
main_addr = (main_wallet or {}).get("address") or (
|
|
153
153
|
(main_wallet or {}).get("evm") or {}
|
|
154
154
|
).get("address")
|
|
155
|
-
|
|
156
|
-
(
|
|
155
|
+
strategy_addr = (strategy_wallet or {}).get("address") or (
|
|
156
|
+
(strategy_wallet or {}).get("evm") or {}
|
|
157
157
|
).get("address")
|
|
158
158
|
except (AttributeError, TypeError) as e:
|
|
159
159
|
logger.debug("Error resolving addresses from wallet config: %s", e)
|
|
160
160
|
|
|
161
161
|
if main_addr and from_addr_norm == (main_addr or "").lower():
|
|
162
162
|
return main_pk or os.getenv("PRIVATE_KEY")
|
|
163
|
-
if
|
|
164
|
-
return
|
|
163
|
+
if strategy_addr and from_addr_norm == (strategy_addr or "").lower():
|
|
164
|
+
return (
|
|
165
|
+
strategy_pk or os.getenv("PRIVATE_KEY_STRATEGY") or os.getenv("PRIVATE_KEY")
|
|
166
|
+
)
|
|
165
167
|
|
|
166
168
|
# Fallback to environment variables
|
|
167
|
-
return os.getenv("
|
|
169
|
+
return os.getenv("PRIVATE_KEY_STRATEGY") or os.getenv("PRIVATE_KEY")
|
|
168
170
|
|
|
169
171
|
|
|
170
172
|
async def _get_abi(chain_id: int, address: str) -> str | None:
|
|
@@ -15,7 +15,7 @@ Wayfinder strategies interact with blockchains through a single abstraction: the
|
|
|
15
15
|
from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
|
|
16
16
|
from wayfinder_paths.core.wallets.WalletManager import WalletManager
|
|
17
17
|
|
|
18
|
-
config = {...} # contains main_wallet /
|
|
18
|
+
config = {...} # contains main_wallet / strategy_wallet entries
|
|
19
19
|
wallet_provider = WalletManager.get_provider(config)
|
|
20
20
|
web3_service = DefaultWeb3Service(config, wallet_provider=wallet_provider)
|
|
21
21
|
|
|
@@ -70,14 +70,14 @@ web3_service = DefaultWeb3Service(config, wallet_provider=custom_wallet)
|
|
|
70
70
|
|
|
71
71
|
## Configuration hints
|
|
72
72
|
|
|
73
|
-
`WalletManager.get_provider` looks for `wallet_type` on the top-level config, `main_wallet`, and `
|
|
73
|
+
`WalletManager.get_provider` looks for `wallet_type` on the top-level config, `main_wallet`, and `strategy_wallet`. Example:
|
|
74
74
|
|
|
75
75
|
```json
|
|
76
76
|
{
|
|
77
77
|
"strategy": {
|
|
78
78
|
"wallet_type": "local",
|
|
79
79
|
"main_wallet": {"address": "0x...", "wallet_type": "local"},
|
|
80
|
-
"
|
|
80
|
+
"strategy_wallet": {"address": "0x..."}
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
```
|
|
@@ -41,9 +41,9 @@ class WalletManager:
|
|
|
41
41
|
wallet_type = main_wallet.get("wallet_type")
|
|
42
42
|
|
|
43
43
|
if not wallet_type:
|
|
44
|
-
|
|
45
|
-
if isinstance(
|
|
46
|
-
wallet_type =
|
|
44
|
+
strategy_wallet = config.get("strategy_wallet")
|
|
45
|
+
if isinstance(strategy_wallet, dict):
|
|
46
|
+
wallet_type = strategy_wallet.get("wallet_type")
|
|
47
47
|
|
|
48
48
|
if not wallet_type or wallet_type == "local":
|
|
49
49
|
logger.debug("Using LocalWalletProvider (default)")
|
wayfinder_paths/run_strategy.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
Strategy Runner
|
|
4
|
-
Main entry point for running
|
|
4
|
+
Main entry point for running strategies locally
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
@@ -12,9 +12,9 @@ from pathlib import Path
|
|
|
12
12
|
|
|
13
13
|
from loguru import logger
|
|
14
14
|
|
|
15
|
-
from wayfinder_paths.core.config import
|
|
15
|
+
from wayfinder_paths.core.config import StrategyJobConfig, load_config_from_env
|
|
16
16
|
from wayfinder_paths.core.engine.manifest import load_manifest, validate_manifest
|
|
17
|
-
from wayfinder_paths.core.engine.
|
|
17
|
+
from wayfinder_paths.core.engine.StrategyJob import StrategyJob
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def load_strategy(
|
|
@@ -66,7 +66,7 @@ def load_strategy(
|
|
|
66
66
|
|
|
67
67
|
def load_config(
|
|
68
68
|
config_path: str | None = None, strategy_name: str | None = None
|
|
69
|
-
) ->
|
|
69
|
+
) -> StrategyJobConfig:
|
|
70
70
|
"""
|
|
71
71
|
Load configuration from file or environment
|
|
72
72
|
|
|
@@ -75,13 +75,13 @@ def load_config(
|
|
|
75
75
|
strategy_name: Optional strategy name for per-strategy wallet lookup
|
|
76
76
|
|
|
77
77
|
Returns:
|
|
78
|
-
|
|
78
|
+
StrategyJobConfig instance
|
|
79
79
|
"""
|
|
80
80
|
if config_path and Path(config_path).exists():
|
|
81
81
|
logger.info(f"Loading config from {config_path}")
|
|
82
82
|
with open(config_path) as f:
|
|
83
83
|
config_data = json.load(f)
|
|
84
|
-
return
|
|
84
|
+
return StrategyJobConfig.from_dict(config_data, strategy_name=strategy_name)
|
|
85
85
|
else:
|
|
86
86
|
logger.info("Loading config from environment variables")
|
|
87
87
|
config = load_config_from_env()
|
|
@@ -100,7 +100,7 @@ async def run_strategy(
|
|
|
100
100
|
**kwargs,
|
|
101
101
|
):
|
|
102
102
|
"""
|
|
103
|
-
Run a
|
|
103
|
+
Run a strategy
|
|
104
104
|
|
|
105
105
|
Args:
|
|
106
106
|
strategy_name: Name of the strategy to run
|
|
@@ -141,13 +141,13 @@ async def run_strategy(
|
|
|
141
141
|
logger.debug(f"Config path provided: {config_path}")
|
|
142
142
|
config = load_config(config_path, strategy_name=strategy_name_for_wallet)
|
|
143
143
|
logger.debug(
|
|
144
|
-
"Loaded config: creds=%s wallets(main=%s
|
|
144
|
+
"Loaded config: creds=%s wallets(main=%s strategy=%s)",
|
|
145
145
|
"yes"
|
|
146
146
|
if (config.user.username and config.user.password)
|
|
147
147
|
or config.user.refresh_token
|
|
148
148
|
else "no",
|
|
149
149
|
(config.user.main_wallet_address or "none"),
|
|
150
|
-
(config.user.
|
|
150
|
+
(config.user.strategy_wallet_address or "none"),
|
|
151
151
|
)
|
|
152
152
|
|
|
153
153
|
# Validate required configuration
|
|
@@ -173,11 +173,11 @@ async def run_strategy(
|
|
|
173
173
|
)
|
|
174
174
|
logger.info(f"Loaded strategy: {strategy.name}")
|
|
175
175
|
|
|
176
|
-
# Create
|
|
177
|
-
|
|
176
|
+
# Create strategy job
|
|
177
|
+
strategy_job = StrategyJob(strategy, config)
|
|
178
178
|
|
|
179
|
-
# Setup
|
|
180
|
-
logger.info("Setting up
|
|
179
|
+
# Setup strategy job
|
|
180
|
+
logger.info("Setting up strategy job...")
|
|
181
181
|
logger.debug(
|
|
182
182
|
"Auth mode: %s",
|
|
183
183
|
"credentials"
|
|
@@ -185,12 +185,12 @@ async def run_strategy(
|
|
|
185
185
|
or config.user.refresh_token
|
|
186
186
|
else "missing",
|
|
187
187
|
)
|
|
188
|
-
await
|
|
188
|
+
await strategy_job.setup()
|
|
189
189
|
|
|
190
190
|
# Execute action
|
|
191
191
|
if action == "run":
|
|
192
192
|
logger.info("Starting continuous execution...")
|
|
193
|
-
await
|
|
193
|
+
await strategy_job.run_continuous(interval_seconds=kwargs.get("interval"))
|
|
194
194
|
|
|
195
195
|
elif action == "deposit":
|
|
196
196
|
main_token_amount = kwargs.get("main_token_amount")
|
|
@@ -207,7 +207,7 @@ async def run_strategy(
|
|
|
207
207
|
if gas_token_amount is None:
|
|
208
208
|
gas_token_amount = 0.0
|
|
209
209
|
|
|
210
|
-
result = await
|
|
210
|
+
result = await strategy_job.execute_strategy(
|
|
211
211
|
"deposit",
|
|
212
212
|
main_token_amount=main_token_amount,
|
|
213
213
|
gas_token_amount=gas_token_amount,
|
|
@@ -216,22 +216,22 @@ async def run_strategy(
|
|
|
216
216
|
|
|
217
217
|
elif action == "withdraw":
|
|
218
218
|
amount = kwargs.get("amount")
|
|
219
|
-
result = await
|
|
219
|
+
result = await strategy_job.execute_strategy("withdraw", amount=amount)
|
|
220
220
|
logger.info(f"Withdraw result: {result}")
|
|
221
221
|
|
|
222
222
|
elif action == "status":
|
|
223
|
-
result = await
|
|
223
|
+
result = await strategy_job.execute_strategy("status")
|
|
224
224
|
logger.info(f"Status: {json.dumps(result, indent=2)}")
|
|
225
225
|
|
|
226
226
|
elif action == "update":
|
|
227
|
-
result = await
|
|
227
|
+
result = await strategy_job.execute_strategy("update")
|
|
228
228
|
logger.info(f"Update result: {result}")
|
|
229
229
|
|
|
230
230
|
elif action == "partial-liquidate":
|
|
231
231
|
usd_value = kwargs.get("amount")
|
|
232
232
|
if not usd_value:
|
|
233
233
|
raise ValueError("Amount (USD value) required for partial-liquidate")
|
|
234
|
-
result = await
|
|
234
|
+
result = await strategy_job.execute_strategy(
|
|
235
235
|
"partial_liquidate", usd_value=usd_value
|
|
236
236
|
)
|
|
237
237
|
logger.info(f"Partial liquidation result: {result}")
|
|
@@ -289,7 +289,9 @@ async def run_strategy(
|
|
|
289
289
|
duration = kwargs.get("duration") or 300
|
|
290
290
|
logger.info(f"Running script mode for {duration}s...")
|
|
291
291
|
task = asyncio.create_task(
|
|
292
|
-
|
|
292
|
+
strategy_job.run_continuous(
|
|
293
|
+
interval_seconds=kwargs.get("interval") or 60
|
|
294
|
+
)
|
|
293
295
|
)
|
|
294
296
|
await asyncio.sleep(duration)
|
|
295
297
|
task.cancel()
|
|
@@ -307,13 +309,13 @@ async def run_strategy(
|
|
|
307
309
|
logger.error(f"Error: {e}")
|
|
308
310
|
sys.exit(1)
|
|
309
311
|
finally:
|
|
310
|
-
if "
|
|
311
|
-
await
|
|
312
|
+
if "strategy_job" in locals():
|
|
313
|
+
await strategy_job.stop()
|
|
312
314
|
|
|
313
315
|
|
|
314
316
|
def main():
|
|
315
317
|
"""Main entry point"""
|
|
316
|
-
parser = argparse.ArgumentParser(description="Run
|
|
318
|
+
parser = argparse.ArgumentParser(description="Run strategy strategies")
|
|
317
319
|
parser.add_argument(
|
|
318
320
|
"strategy",
|
|
319
321
|
nargs="?",
|
|
@@ -22,18 +22,18 @@ def write_env(rows: list[dict[str, str]], out_dir: Path) -> None:
|
|
|
22
22
|
main_w = (
|
|
23
23
|
label_to_wallet.get("main") or label_to_wallet.get("default") or rows[0]
|
|
24
24
|
)
|
|
25
|
-
|
|
25
|
+
strategy_w = label_to_wallet.get("strategy")
|
|
26
26
|
|
|
27
27
|
f.write("RPC_URL=https://rpc.ankr.com/eth\n")
|
|
28
28
|
# Back-compat defaults
|
|
29
29
|
f.write(f"PRIVATE_KEY={main_w['private_key_hex']}\n")
|
|
30
30
|
f.write(f"FROM_ADDRESS={main_w['address']}\n")
|
|
31
|
-
# Explicit main/
|
|
31
|
+
# Explicit main/strategy variables
|
|
32
32
|
f.write(f"MAIN_WALLET_ADDRESS={main_w['address']}\n")
|
|
33
|
-
if
|
|
34
|
-
f.write(f"
|
|
35
|
-
# Optional: expose
|
|
36
|
-
f.write(f"
|
|
33
|
+
if strategy_w:
|
|
34
|
+
f.write(f"STRATEGY_WALLET_ADDRESS={strategy_w['address']}\n")
|
|
35
|
+
# Optional: expose strategy private key for local dev only
|
|
36
|
+
f.write(f"PRIVATE_KEY_STRATEGY={strategy_w['private_key_hex']}\n")
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def main():
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
Allocates USDT0 on HyperEVM across HyperLend stablecoin markets. The strategy:
|
|
11
11
|
|
|
12
|
-
1. Pulls USDT0 (plus a configurable HYPE gas buffer) from the main wallet into the
|
|
12
|
+
1. Pulls USDT0 (plus a configurable HYPE gas buffer) from the main wallet into the strategy wallet.
|
|
13
13
|
2. Samples HyperLend hourly rate history, applies a bootstrap tournament (horizon = 6h, blocks = 6h, 4,000 trials, 7-day half-life) to estimate which stablecoin should outperform.
|
|
14
14
|
3. Tops up the small HYPE gas buffer if needed, swaps USDT0 into the target stablecoin, and supplies it to HyperLend.
|
|
15
15
|
4. Enforces a hysteresis rotation policy so minor APY noise does not churn capital.
|
|
16
16
|
|
|
17
17
|
## Policy
|
|
18
18
|
|
|
19
|
-
The manifest policy simply locks transactions to the
|
|
19
|
+
The manifest policy simply locks transactions to the strategy wallet ID:
|
|
20
20
|
|
|
21
21
|
```
|
|
22
22
|
(wallet.id == 'FORMAT_WALLET_ID')
|
|
@@ -46,14 +46,14 @@ The manifest policy simply locks transactions to the vault wallet ID:
|
|
|
46
46
|
### Deposit
|
|
47
47
|
|
|
48
48
|
- Validates USDT0 and HYPE balances in the main wallet.
|
|
49
|
-
- Transfers HYPE into the
|
|
50
|
-
- Moves USDT0 from the main wallet into the
|
|
49
|
+
- Transfers HYPE into the strategy wallet when a top-up is required, ensuring the strategy maintains the configured buffer.
|
|
50
|
+
- Moves USDT0 from the main wallet into the strategy wallet through `BalanceAdapter.move_from_main_wallet_to_strategy_wallet`.
|
|
51
51
|
- Clears cached asset snapshots so the next update starts from on-chain reality.
|
|
52
52
|
|
|
53
53
|
### Update
|
|
54
54
|
|
|
55
55
|
- Refreshes HyperLend asset snapshots, calculates tournament winners, and filters markets that respect supply caps + buffer requirements.
|
|
56
|
-
- Reads rotation history through `LedgerAdapter.
|
|
56
|
+
- Reads rotation history through `LedgerAdapter.get_strategy_latest_transactions` to enforce the cooldown (unless the short-circuit policy is triggered).
|
|
57
57
|
- If a new asset wins the tournament and passes hysteresis checks, BRAP quotes are fetched and executed to rotate into the better performer.
|
|
58
58
|
- Sweeps residual stable balances, lends via `HyperlendAdapter`, and records ledger operations.
|
|
59
59
|
|
|
@@ -94,7 +94,7 @@ poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strateg
|
|
|
94
94
|
Use the manifest directly if you prefer:
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
|
-
poetry run python wayfinder_paths/run_strategy.py --manifest wayfinder_paths/
|
|
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
98
|
```
|
|
99
99
|
|
|
100
100
|
Wallet addresses/labels are auto-resolved from `wallets.json`. Set `NETWORK=testnet` in your config to run the orchestration without touching live HyperEVM endpoints.
|