wayfinder-paths 0.1.19__py3-none-any.whl → 0.1.21__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/__init__.py +0 -2
- wayfinder_paths/adapters/balance_adapter/README.md +59 -45
- wayfinder_paths/adapters/balance_adapter/adapter.py +1 -22
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -14
- wayfinder_paths/adapters/brap_adapter/README.md +61 -184
- wayfinder_paths/adapters/brap_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/brap_adapter/adapter.py +1 -148
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +0 -15
- wayfinder_paths/adapters/hyperlend_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +1 -10
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +0 -17
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +3 -312
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +1 -71
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +0 -57
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +0 -17
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +2 -42
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +1 -9
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +15 -47
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +0 -7
- wayfinder_paths/adapters/ledger_adapter/README.md +54 -74
- wayfinder_paths/adapters/ledger_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/ledger_adapter/adapter.py +0 -106
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +0 -12
- wayfinder_paths/adapters/moonwell_adapter/README.md +67 -106
- wayfinder_paths/adapters/moonwell_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +10 -122
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +84 -83
- wayfinder_paths/adapters/pool_adapter/README.md +30 -51
- wayfinder_paths/adapters/pool_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/pool_adapter/adapter.py +0 -19
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -8
- wayfinder_paths/adapters/token_adapter/README.md +41 -49
- wayfinder_paths/adapters/token_adapter/adapter.py +0 -32
- wayfinder_paths/adapters/token_adapter/test_adapter.py +1 -12
- wayfinder_paths/conftest.py +0 -8
- wayfinder_paths/core/__init__.py +0 -2
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -22
- wayfinder_paths/core/adapters/__init__.py +0 -5
- wayfinder_paths/core/adapters/models.py +0 -5
- wayfinder_paths/core/analytics/__init__.py +0 -2
- wayfinder_paths/core/analytics/bootstrap.py +0 -16
- wayfinder_paths/core/analytics/stats.py +0 -7
- wayfinder_paths/core/analytics/test_analytics.py +5 -34
- wayfinder_paths/core/clients/BRAPClient.py +0 -35
- wayfinder_paths/core/clients/ClientManager.py +0 -51
- wayfinder_paths/core/clients/HyperlendClient.py +0 -77
- wayfinder_paths/core/clients/LedgerClient.py +2 -122
- wayfinder_paths/core/clients/PoolClient.py +0 -2
- wayfinder_paths/core/clients/TokenClient.py +0 -39
- wayfinder_paths/core/clients/WalletClient.py +0 -15
- wayfinder_paths/core/clients/WayfinderClient.py +0 -24
- wayfinder_paths/core/clients/__init__.py +0 -4
- wayfinder_paths/core/clients/protocols.py +25 -98
- wayfinder_paths/core/config.py +0 -24
- wayfinder_paths/core/constants/__init__.py +0 -7
- wayfinder_paths/core/constants/base.py +2 -9
- wayfinder_paths/core/constants/erc20_abi.py +0 -5
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -7
- wayfinder_paths/core/constants/moonwell_abi.py +0 -35
- wayfinder_paths/core/engine/StrategyJob.py +0 -32
- wayfinder_paths/core/strategies/Strategy.py +0 -99
- wayfinder_paths/core/strategies/__init__.py +0 -2
- wayfinder_paths/core/utils/__init__.py +0 -1
- wayfinder_paths/core/utils/evm_helpers.py +0 -50
- wayfinder_paths/core/utils/{erc20_service.py → tokens.py} +25 -21
- wayfinder_paths/core/utils/transaction.py +0 -1
- wayfinder_paths/run_strategy.py +0 -46
- wayfinder_paths/scripts/create_strategy.py +0 -17
- wayfinder_paths/scripts/make_wallets.py +1 -4
- wayfinder_paths/strategies/basis_trading_strategy/README.md +71 -163
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +0 -24
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +36 -400
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +15 -64
- wayfinder_paths/strategies/basis_trading_strategy/types.py +0 -4
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +65 -56
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +4 -27
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -10
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +71 -72
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +23 -227
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +120 -113
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +64 -59
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +4 -44
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +2 -35
- wayfinder_paths/templates/adapter/README.md +107 -46
- wayfinder_paths/templates/adapter/adapter.py +0 -9
- wayfinder_paths/templates/adapter/test_adapter.py +0 -19
- wayfinder_paths/templates/strategy/README.md +113 -59
- wayfinder_paths/templates/strategy/strategy.py +0 -22
- wayfinder_paths/templates/strategy/test_strategy.py +0 -28
- wayfinder_paths/tests/test_test_coverage.py +2 -12
- wayfinder_paths/tests/test_utils.py +1 -31
- wayfinder_paths-0.1.21.dist-info/METADATA +355 -0
- wayfinder_paths-0.1.21.dist-info/RECORD +129 -0
- {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.21.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/adapters/base.py +0 -5
- wayfinder_paths-0.1.19.dist-info/METADATA +0 -592
- wayfinder_paths-0.1.19.dist-info/RECORD +0 -130
- {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.21.dist-info}/LICENSE +0 -0
|
@@ -1,89 +1,150 @@
|
|
|
1
|
-
|
|
1
|
+
# Adapter Template
|
|
2
2
|
|
|
3
|
-
Adapters expose protocol-specific capabilities to strategies. They
|
|
3
|
+
Adapters expose protocol-specific capabilities to strategies. They wrap one or more clients from `wayfinder_paths.core.clients`.
|
|
4
4
|
|
|
5
|
-
## Quick
|
|
5
|
+
## Quick Start
|
|
6
6
|
|
|
7
7
|
1. Copy the template:
|
|
8
|
-
```
|
|
8
|
+
```bash
|
|
9
9
|
cp -r wayfinder_paths/templates/adapter wayfinder_paths/adapters/my_adapter
|
|
10
10
|
```
|
|
11
11
|
2. Rename `MyAdapter` in `adapter.py` to match your adapter's purpose.
|
|
12
|
-
3.
|
|
13
|
-
4.
|
|
12
|
+
3. Set `adapter_type` to a unique identifier (e.g., `"MY_PROTOCOL"`).
|
|
13
|
+
4. Implement your public methods.
|
|
14
|
+
5. Add tests in `test_adapter.py`.
|
|
14
15
|
|
|
15
|
-
##
|
|
16
|
+
## Directory Structure
|
|
16
17
|
|
|
17
18
|
```
|
|
18
19
|
my_adapter/
|
|
19
20
|
├── adapter.py # Adapter implementation
|
|
20
|
-
├── examples.json # Example payloads (optional
|
|
21
|
-
├── test_adapter.py # Pytest
|
|
22
|
-
└── README.md # Adapter
|
|
21
|
+
├── examples.json # Example payloads (optional)
|
|
22
|
+
├── test_adapter.py # Pytest tests
|
|
23
|
+
└── README.md # Adapter documentation
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
##
|
|
26
|
+
## Adapter Structure
|
|
26
27
|
|
|
27
28
|
```python
|
|
28
29
|
from typing import Any
|
|
29
30
|
|
|
30
31
|
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
31
|
-
from wayfinder_paths.core.clients.
|
|
32
|
+
from wayfinder_paths.core.clients.SomeClient import SomeClient
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
class MyAdapter(BaseAdapter):
|
|
35
|
-
|
|
36
|
+
"""Adapter for MyProtocol operations."""
|
|
37
|
+
|
|
38
|
+
adapter_type = "MY_PROTOCOL"
|
|
36
39
|
|
|
37
40
|
def __init__(self, config: dict[str, Any] | None = None):
|
|
38
41
|
super().__init__("my_adapter", config)
|
|
39
|
-
self.
|
|
42
|
+
self.client = SomeClient()
|
|
40
43
|
|
|
41
44
|
async def connect(self) -> bool:
|
|
42
|
-
"""Optional:
|
|
45
|
+
"""Optional: Establish connectivity."""
|
|
43
46
|
return True
|
|
44
47
|
|
|
45
|
-
async def
|
|
46
|
-
"""
|
|
48
|
+
async def do_something(self, param: str) -> tuple[bool, Any]:
|
|
49
|
+
"""
|
|
50
|
+
Execute an operation.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
param: Operation parameter
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Tuple of (success, data) where data is result or error message
|
|
57
|
+
"""
|
|
47
58
|
try:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
self.logger.error(f"Failed to fetch pools: {exc}")
|
|
54
|
-
return (False, str(exc))
|
|
59
|
+
result = await self.client.call(param)
|
|
60
|
+
return (True, result)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
self.logger.error(f"Operation failed: {e}")
|
|
63
|
+
return (False, str(e))
|
|
55
64
|
```
|
|
56
65
|
|
|
57
|
-
|
|
66
|
+
## Key Conventions
|
|
67
|
+
|
|
68
|
+
1. **Return tuples**: All methods return `(success: bool, data: Any)`
|
|
69
|
+
2. **Adapter type**: Set `adapter_type` for registry lookups
|
|
70
|
+
3. **Config access**: Use `self.config` for configuration
|
|
71
|
+
4. **Logging**: Use `self.logger` for consistent logging
|
|
72
|
+
5. **Error handling**: Catch exceptions and return `(False, error_message)`
|
|
73
|
+
|
|
74
|
+
## BaseAdapter Interface
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
class BaseAdapter(ABC):
|
|
78
|
+
adapter_type: str | None = None
|
|
79
|
+
|
|
80
|
+
def __init__(self, name: str, config: dict | None = None):
|
|
81
|
+
self.name = name
|
|
82
|
+
self.config = config or {}
|
|
83
|
+
self.logger = logger.bind(adapter=self.__class__.__name__)
|
|
84
|
+
|
|
85
|
+
async def connect(self) -> bool:
|
|
86
|
+
"""Establish connectivity (default: True)."""
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
async def get_balance(self, asset: str) -> dict:
|
|
90
|
+
"""Get balance (raises NotImplementedError by default)."""
|
|
91
|
+
raise NotImplementedError
|
|
92
|
+
|
|
93
|
+
async def health_check(self) -> dict:
|
|
94
|
+
"""Check adapter health."""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
async def close(self) -> None:
|
|
98
|
+
"""Clean up resources."""
|
|
99
|
+
pass
|
|
100
|
+
```
|
|
58
101
|
|
|
59
102
|
## Testing
|
|
60
103
|
|
|
61
|
-
`test_adapter.py
|
|
104
|
+
Create `test_adapter.py`:
|
|
62
105
|
|
|
63
106
|
```python
|
|
64
107
|
import pytest
|
|
65
108
|
from unittest.mock import AsyncMock, patch
|
|
109
|
+
from .adapter import MyAdapter
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TestMyAdapter:
|
|
113
|
+
@pytest.fixture
|
|
114
|
+
def adapter(self):
|
|
115
|
+
return MyAdapter()
|
|
116
|
+
|
|
117
|
+
@pytest.mark.asyncio
|
|
118
|
+
async def test_do_something_success(self, adapter):
|
|
119
|
+
with patch.object(adapter, "client") as mock_client:
|
|
120
|
+
mock_client.call = AsyncMock(return_value={"result": "ok"})
|
|
121
|
+
|
|
122
|
+
success, data = await adapter.do_something(param="test")
|
|
123
|
+
|
|
124
|
+
assert success
|
|
125
|
+
assert data["result"] == "ok"
|
|
126
|
+
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_do_something_failure(self, adapter):
|
|
129
|
+
with patch.object(adapter, "client") as mock_client:
|
|
130
|
+
mock_client.call = AsyncMock(side_effect=Exception("API error"))
|
|
131
|
+
|
|
132
|
+
success, data = await adapter.do_something(param="test")
|
|
133
|
+
|
|
134
|
+
assert not success
|
|
135
|
+
assert "error" in data.lower()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Run tests:
|
|
66
139
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@pytest.mark.asyncio
|
|
71
|
-
async def test_get_pools():
|
|
72
|
-
with patch(
|
|
73
|
-
"wayfinder_paths.adapters.my_adapter.adapter.PoolClient",
|
|
74
|
-
return_value=AsyncMock(
|
|
75
|
-
get_pools_by_ids=AsyncMock(return_value={"pools": []})
|
|
76
|
-
),
|
|
77
|
-
):
|
|
78
|
-
adapter = MyAdapter(config={})
|
|
79
|
-
success, data = await adapter.get_pools(["pool-1"])
|
|
80
|
-
assert success
|
|
81
|
-
assert "pools" in data
|
|
140
|
+
```bash
|
|
141
|
+
poetry run pytest wayfinder_paths/adapters/my_adapter/ -v
|
|
82
142
|
```
|
|
83
143
|
|
|
84
|
-
## Best
|
|
144
|
+
## Best Practices
|
|
85
145
|
|
|
86
|
-
- Keep adapters
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
146
|
+
- Keep adapters thin - business logic belongs in strategies
|
|
147
|
+
- Mock clients in tests, not adapters
|
|
148
|
+
- Document each public method with Args/Returns docstrings
|
|
149
|
+
- Use type hints for all parameters and return values
|
|
150
|
+
- Log errors with context for debugging
|
|
@@ -4,22 +4,13 @@ from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class MyAdapter(BaseAdapter):
|
|
7
|
-
"""
|
|
8
|
-
Template adapter for a protocol/exchange integration.
|
|
9
|
-
Copy this folder, rename it (e.g., my_adapter), and implement your adapter methods.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
7
|
adapter_type: str = "MY_ADAPTER"
|
|
13
8
|
|
|
14
9
|
def __init__(self, config: dict[str, Any] | None = None):
|
|
15
10
|
super().__init__("my_adapter", config)
|
|
16
11
|
|
|
17
12
|
async def connect(self) -> bool:
|
|
18
|
-
"""Establish connectivity to remote service(s) if needed."""
|
|
19
13
|
return True
|
|
20
14
|
|
|
21
15
|
async def example_operation(self, **kwargs) -> tuple[bool, str]:
|
|
22
|
-
"""
|
|
23
|
-
Example operation. Replace with your adapter's real API.
|
|
24
|
-
"""
|
|
25
16
|
return (True, "example.op executed")
|
|
@@ -1,49 +1,30 @@
|
|
|
1
|
-
"""Test template for adapters.
|
|
2
|
-
|
|
3
|
-
Quick setup:
|
|
4
|
-
1. Replace MyAdapter with your actual adapter class name
|
|
5
|
-
2. Implement test_basic_functionality with your adapter's core methods
|
|
6
|
-
3. Add client mocking if your adapter uses external clients
|
|
7
|
-
4. Run: pytest wayfinder_paths/adapters/your_adapter/ -v
|
|
8
|
-
|
|
9
|
-
Note: examples.json is optional for adapters (not required).
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
1
|
import pytest
|
|
13
2
|
|
|
14
3
|
# TODO: Replace MyAdapter with your actual adapter class name
|
|
15
4
|
from .adapter import MyAdapter
|
|
16
5
|
|
|
17
6
|
# For mocking clients, uncomment when needed:
|
|
18
|
-
# from unittest.mock import AsyncMock, patch
|
|
19
7
|
|
|
20
8
|
|
|
21
9
|
class TestMyAdapter:
|
|
22
|
-
"""Test cases for MyAdapter"""
|
|
23
|
-
|
|
24
10
|
@pytest.fixture
|
|
25
11
|
def adapter(self):
|
|
26
|
-
"""Create adapter instance for testing."""
|
|
27
12
|
return MyAdapter(config={})
|
|
28
13
|
|
|
29
14
|
@pytest.mark.asyncio
|
|
30
15
|
async def test_health_check(self, adapter):
|
|
31
|
-
"""Test adapter health check"""
|
|
32
16
|
health = await adapter.health_check()
|
|
33
17
|
assert isinstance(health, dict)
|
|
34
18
|
assert health.get("status") in {"healthy", "unhealthy", "error"}
|
|
35
19
|
|
|
36
20
|
@pytest.mark.asyncio
|
|
37
21
|
async def test_connect(self, adapter):
|
|
38
|
-
"""Test adapter connection"""
|
|
39
22
|
ok = await adapter.connect()
|
|
40
23
|
assert isinstance(ok, bool)
|
|
41
24
|
|
|
42
25
|
def test_capabilities(self, adapter):
|
|
43
|
-
"""Test adapter capabilities"""
|
|
44
26
|
assert hasattr(adapter, "adapter_type")
|
|
45
27
|
|
|
46
28
|
@pytest.mark.asyncio
|
|
47
29
|
async def test_basic_functionality(self, adapter):
|
|
48
|
-
"""REQUIRED: Test your adapter's core functionality."""
|
|
49
30
|
assert adapter is not None
|
|
@@ -1,48 +1,65 @@
|
|
|
1
1
|
# Strategy Template
|
|
2
2
|
|
|
3
|
-
This template provides
|
|
3
|
+
This template provides scaffolding for a new strategy.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
|
-
1. Copy the template
|
|
8
|
-
```
|
|
7
|
+
1. Copy the template:
|
|
8
|
+
```bash
|
|
9
9
|
cp -r wayfinder_paths/templates/strategy wayfinder_paths/strategies/my_strategy
|
|
10
10
|
```
|
|
11
|
+
Or use the convenience command:
|
|
12
|
+
```bash
|
|
13
|
+
just create-strategy "My Strategy Name"
|
|
14
|
+
```
|
|
11
15
|
2. Rename the class in `strategy.py` to match your strategy name.
|
|
12
|
-
3.
|
|
13
|
-
4.
|
|
16
|
+
3. Implement the required methods (`deposit`, `update`, `exit`, `_status`).
|
|
17
|
+
4. Add tests in `test_strategy.py`.
|
|
18
|
+
5. Fill out `examples.json` with sample CLI invocations.
|
|
14
19
|
|
|
15
|
-
##
|
|
20
|
+
## Directory Structure
|
|
16
21
|
|
|
17
22
|
```
|
|
18
23
|
my_strategy/
|
|
19
24
|
├── strategy.py # Strategy implementation
|
|
20
25
|
├── examples.json # Example CLI payloads
|
|
21
|
-
├── test_strategy.py # Pytest
|
|
22
|
-
└── README.md # Strategy
|
|
26
|
+
├── test_strategy.py # Pytest tests
|
|
27
|
+
└── README.md # Strategy documentation
|
|
23
28
|
```
|
|
24
29
|
|
|
25
|
-
## Required
|
|
30
|
+
## Required Methods
|
|
26
31
|
|
|
27
32
|
```python
|
|
28
33
|
async def deposit(self, main_token_amount: float, gas_token_amount: float) -> StatusTuple:
|
|
29
|
-
"""Move funds from
|
|
34
|
+
"""Move funds from main wallet into strategy wallet and deploy capital."""
|
|
30
35
|
|
|
31
36
|
async def update(self) -> StatusTuple:
|
|
32
|
-
"""Periodic rebalance/
|
|
37
|
+
"""Periodic rebalance/optimization loop."""
|
|
38
|
+
|
|
39
|
+
async def exit(self, **kwargs) -> StatusTuple:
|
|
40
|
+
"""Transfer funds from strategy wallet back to main wallet."""
|
|
33
41
|
|
|
34
42
|
async def _status(self) -> StatusDict:
|
|
35
|
-
"""Return portfolio_value, net_deposit, and strategy_status
|
|
43
|
+
"""Return portfolio_value, net_deposit, and strategy_status."""
|
|
36
44
|
```
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
## Optional Methods
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
```python
|
|
49
|
+
async def withdraw(self, **kwargs) -> StatusTuple:
|
|
50
|
+
"""Unwind positions. Default implementation unwinds ledger operations."""
|
|
51
|
+
|
|
52
|
+
async def partial_liquidate(self, usd_value: float) -> tuple[bool, LiquidationResult]:
|
|
53
|
+
"""Liquidate a portion of the position by USD value."""
|
|
41
54
|
|
|
42
|
-
|
|
55
|
+
async def setup(self) -> None:
|
|
56
|
+
"""Post-construction initialization."""
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
async def health_check(self) -> dict:
|
|
59
|
+
"""Check strategy and adapter health."""
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Strategy Structure
|
|
46
63
|
|
|
47
64
|
```python
|
|
48
65
|
from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
|
|
@@ -50,83 +67,120 @@ from wayfinder_paths.adapters.balance_adapter.adapter import BalanceAdapter
|
|
|
50
67
|
|
|
51
68
|
|
|
52
69
|
class MyStrategy(Strategy):
|
|
53
|
-
name = "
|
|
70
|
+
name = "My Strategy"
|
|
54
71
|
|
|
55
|
-
def __init__(self, config: dict | None = None):
|
|
56
|
-
super().__init__()
|
|
72
|
+
def __init__(self, config: dict | None = None, **kwargs):
|
|
73
|
+
super().__init__(config, **kwargs)
|
|
57
74
|
self.config = config or {}
|
|
58
|
-
|
|
75
|
+
|
|
76
|
+
# Initialize and register adapters
|
|
77
|
+
balance_adapter = BalanceAdapter(
|
|
78
|
+
self.config,
|
|
79
|
+
main_wallet_signing_callback=kwargs.get("main_wallet_signing_callback"),
|
|
80
|
+
strategy_wallet_signing_callback=kwargs.get("strategy_wallet_signing_callback"),
|
|
81
|
+
)
|
|
59
82
|
self.register_adapters([balance_adapter])
|
|
60
83
|
self.balance_adapter = balance_adapter
|
|
61
84
|
|
|
62
85
|
async def deposit(
|
|
63
86
|
self, main_token_amount: float = 0.0, gas_token_amount: float = 0.0
|
|
64
87
|
) -> StatusTuple:
|
|
65
|
-
"""Perform validation, move funds, and optionally deploy capital."""
|
|
66
88
|
if main_token_amount <= 0:
|
|
67
89
|
return (False, "Nothing to deposit")
|
|
68
90
|
|
|
69
|
-
|
|
70
|
-
query=self.config.get("token_id"),
|
|
71
|
-
wallet_address=self.config.get("main_wallet", {}).get("address"),
|
|
72
|
-
)
|
|
73
|
-
if not success:
|
|
74
|
-
return (False, "Unable to fetch balances")
|
|
75
|
-
|
|
76
|
-
self.last_deposit = main_token_amount
|
|
91
|
+
# Implement deposit logic
|
|
77
92
|
return (True, f"Deposited {main_token_amount} tokens")
|
|
78
93
|
|
|
79
94
|
async def update(self) -> StatusTuple:
|
|
80
|
-
|
|
81
|
-
return (True, "
|
|
95
|
+
# Implement rebalancing logic
|
|
96
|
+
return (True, "Update complete")
|
|
97
|
+
|
|
98
|
+
async def exit(self, **kwargs) -> StatusTuple:
|
|
99
|
+
# Implement exit logic
|
|
100
|
+
return (True, "Exit complete")
|
|
82
101
|
|
|
83
102
|
async def _status(self) -> StatusDict:
|
|
84
|
-
"""Surface state back to run_strategy.py."""
|
|
85
|
-
success, balance = await self.balance_adapter.get_balance(
|
|
86
|
-
query=self.config.get("token_id"),
|
|
87
|
-
wallet_address=self.config.get("strategy_wallet", {}).get("address"),
|
|
88
|
-
)
|
|
89
103
|
return {
|
|
90
|
-
"portfolio_value":
|
|
91
|
-
"net_deposit":
|
|
92
|
-
"strategy_status": {"message": "healthy"
|
|
104
|
+
"portfolio_value": 0.0,
|
|
105
|
+
"net_deposit": 0.0,
|
|
106
|
+
"strategy_status": {"message": "healthy"},
|
|
107
|
+
"gas_available": 0.0,
|
|
108
|
+
"gassed_up": True,
|
|
93
109
|
}
|
|
94
110
|
```
|
|
95
111
|
|
|
96
112
|
## Testing
|
|
97
113
|
|
|
98
|
-
`test_strategy.py`
|
|
114
|
+
Create `test_strategy.py` using `examples.json`:
|
|
99
115
|
|
|
100
116
|
```python
|
|
101
117
|
import pytest
|
|
102
|
-
from
|
|
118
|
+
from pathlib import Path
|
|
119
|
+
from tests.test_utils import load_strategy_examples
|
|
120
|
+
from .strategy import MyStrategy
|
|
103
121
|
|
|
104
122
|
|
|
105
123
|
@pytest.mark.asyncio
|
|
106
|
-
async def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
124
|
+
async def test_smoke():
|
|
125
|
+
"""Basic strategy lifecycle test."""
|
|
126
|
+
examples = load_strategy_examples(Path(__file__))
|
|
127
|
+
smoke_example = examples["smoke"]
|
|
128
|
+
|
|
129
|
+
s = MyStrategy()
|
|
130
|
+
|
|
131
|
+
# Deposit
|
|
132
|
+
deposit_params = smoke_example.get("deposit", {})
|
|
133
|
+
ok, _ = await s.deposit(**deposit_params)
|
|
134
|
+
assert ok
|
|
135
|
+
|
|
136
|
+
# Update
|
|
137
|
+
ok, _ = await s.update()
|
|
138
|
+
assert ok
|
|
139
|
+
|
|
140
|
+
# Status
|
|
141
|
+
st = await s._status()
|
|
142
|
+
assert "portfolio_value" in st
|
|
143
|
+
assert "net_deposit" in st
|
|
144
|
+
assert "strategy_status" in st
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Run tests:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
poetry run pytest wayfinder_paths/strategies/my_strategy/ -v
|
|
110
151
|
```
|
|
111
152
|
|
|
112
|
-
## Running the
|
|
153
|
+
## Running the Strategy
|
|
113
154
|
|
|
114
155
|
```bash
|
|
115
|
-
# Install dependencies
|
|
156
|
+
# Install dependencies
|
|
116
157
|
poetry install
|
|
117
|
-
# Creates a main wallet (or use 'just create-strategy' which auto-creates wallets)
|
|
118
|
-
poetry run python wayfinder_paths/scripts/make_wallets.py -n 1
|
|
119
158
|
|
|
120
|
-
#
|
|
121
|
-
|
|
159
|
+
# Generate wallets
|
|
160
|
+
just create-wallets
|
|
161
|
+
just create-wallet my_strategy
|
|
162
|
+
|
|
163
|
+
# Configure API key in config.json
|
|
164
|
+
|
|
165
|
+
# Check status
|
|
166
|
+
poetry run python wayfinder_paths/run_strategy.py my_strategy --action status --config config.json
|
|
167
|
+
|
|
168
|
+
# Deposit
|
|
169
|
+
poetry run python wayfinder_paths/run_strategy.py my_strategy \
|
|
170
|
+
--action deposit --main-token-amount 100 --gas-token-amount 0.01 --config config.json
|
|
171
|
+
|
|
172
|
+
# Run update
|
|
173
|
+
poetry run python wayfinder_paths/run_strategy.py my_strategy --action update --config config.json
|
|
122
174
|
|
|
123
|
-
#
|
|
124
|
-
poetry run python wayfinder_paths/run_strategy.py my_strategy --action
|
|
175
|
+
# Withdraw
|
|
176
|
+
poetry run python wayfinder_paths/run_strategy.py my_strategy --action withdraw --config config.json
|
|
125
177
|
```
|
|
126
178
|
|
|
127
|
-
## Best
|
|
179
|
+
## Best Practices
|
|
128
180
|
|
|
129
|
-
- Return `(success: bool, message: str)` tuples from
|
|
130
|
-
- Always populate `portfolio_value`, `net_deposit`, and `strategy_status`
|
|
131
|
-
- Register adapters via `register_adapters` in
|
|
132
|
-
-
|
|
181
|
+
- Return `(success: bool, message: str)` tuples from all action methods
|
|
182
|
+
- Always populate `portfolio_value`, `net_deposit`, and `strategy_status` in `_status`
|
|
183
|
+
- Register adapters via `register_adapters()` in `__init__`
|
|
184
|
+
- Use adapters for external operations, not clients directly
|
|
185
|
+
- Keep strategy logic clear and well-documented
|
|
186
|
+
- Add error handling with informative messages
|
|
@@ -10,41 +10,20 @@ class MyStrategy(Strategy):
|
|
|
10
10
|
super().__init__()
|
|
11
11
|
|
|
12
12
|
async def setup(self):
|
|
13
|
-
"""Optional initialization logic."""
|
|
14
13
|
return None
|
|
15
14
|
|
|
16
15
|
async def deposit(
|
|
17
16
|
self, main_token_amount: float, gas_token_amount: float
|
|
18
17
|
) -> StatusTuple:
|
|
19
|
-
"""Deposit funds into the strategy.
|
|
20
|
-
|
|
21
|
-
Args:
|
|
22
|
-
main_token_amount: Amount of the main token to deposit (e.g., USDC, USDT0)
|
|
23
|
-
gas_token_amount: Amount of gas token to deposit (e.g., ETH, HYPE)
|
|
24
|
-
"""
|
|
25
18
|
return (True, "Deposit successful")
|
|
26
19
|
|
|
27
20
|
async def withdraw(self, amount: float | None = None) -> StatusTuple:
|
|
28
|
-
"""Withdraw funds from the strategy.
|
|
29
|
-
|
|
30
|
-
This method is required. The base Strategy class provides a default
|
|
31
|
-
implementation that unwinds all ledger operations. You can either:
|
|
32
|
-
1. Call the parent implementation (as shown here, recommended for most cases)
|
|
33
|
-
2. Override this method for custom withdrawal logic (e.g., unwinding specific positions,
|
|
34
|
-
converting tokens, handling partial withdrawals)
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
amount: Optional amount to withdraw. If None, withdraws all funds.
|
|
38
|
-
"""
|
|
39
|
-
# Call parent implementation which unwinds all ledger operations
|
|
40
21
|
return await super().withdraw(amount=amount)
|
|
41
22
|
|
|
42
23
|
async def update(self) -> StatusTuple:
|
|
43
|
-
"""Rebalance or update positions."""
|
|
44
24
|
return (True, "Update successful")
|
|
45
25
|
|
|
46
26
|
async def _status(self) -> StatusDict:
|
|
47
|
-
"""Report strategy status."""
|
|
48
27
|
return {
|
|
49
28
|
"portfolio_value": 0.0,
|
|
50
29
|
"net_deposit": 0.0,
|
|
@@ -53,5 +32,4 @@ class MyStrategy(Strategy):
|
|
|
53
32
|
|
|
54
33
|
@staticmethod
|
|
55
34
|
def policies() -> list[str]:
|
|
56
|
-
"""Return policy strings used to scope on-chain permissions."""
|
|
57
35
|
return []
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
"""Test template for strategies.
|
|
2
|
-
|
|
3
|
-
REQUIRED: This template uses examples.json for all test data.
|
|
4
|
-
Replace 'MyStrategy' with your actual strategy class name.
|
|
5
|
-
|
|
6
|
-
Quick setup:
|
|
7
|
-
1. Replace MyStrategy with your strategy class name
|
|
8
|
-
2. Create examples.json with a 'smoke' example (see TESTING.md)
|
|
9
|
-
3. Add mocking if your strategy uses adapters
|
|
10
|
-
4. Run: pytest wayfinder_paths/strategies/your_strategy/ -v
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
1
|
import sys
|
|
14
2
|
from pathlib import Path
|
|
15
3
|
|
|
@@ -31,7 +19,6 @@ elif sys.path.index(_wayfinder_path_str) > 0:
|
|
|
31
19
|
|
|
32
20
|
import pytest # noqa: E402
|
|
33
21
|
|
|
34
|
-
# Import test utilities
|
|
35
22
|
try:
|
|
36
23
|
from tests.test_utils import get_canonical_examples, load_strategy_examples
|
|
37
24
|
except ImportError:
|
|
@@ -48,7 +35,6 @@ except ImportError:
|
|
|
48
35
|
|
|
49
36
|
@pytest.fixture
|
|
50
37
|
def strategy():
|
|
51
|
-
"""Create a strategy instance for testing with minimal config."""
|
|
52
38
|
mock_config = {
|
|
53
39
|
"main_wallet": {"address": "0x1234567890123456789012345678901234567890"},
|
|
54
40
|
"strategy_wallet": {"address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"},
|
|
@@ -69,10 +55,7 @@ def strategy():
|
|
|
69
55
|
# def get_balance_side_effect(query, wallet_address, **kwargs):
|
|
70
56
|
# token_id = query if isinstance(query, str) else (query or {}).get("token_id")
|
|
71
57
|
# if token_id == "usd-coin-base" or token_id == "usd-coin":
|
|
72
|
-
# return usdc_balance_mock.return_value
|
|
73
58
|
# elif token_id == "ethereum-base" or token_id == "ethereum":
|
|
74
|
-
# return gas_balance_mock.return_value
|
|
75
|
-
# return (True, 1000000000)
|
|
76
59
|
#
|
|
77
60
|
# s.balance_adapter.get_balance = AsyncMock(
|
|
78
61
|
# side_effect=get_balance_side_effect
|
|
@@ -94,19 +77,15 @@ def strategy():
|
|
|
94
77
|
# Example for transaction adapters:
|
|
95
78
|
# if hasattr(s, "tx_adapter") and s.tx_adapter:
|
|
96
79
|
# s.tx_adapter.move_from_main_wallet_to_strategy_wallet = AsyncMock(
|
|
97
|
-
# return_value=(True, "Transfer successful (simulated)")
|
|
98
80
|
# )
|
|
99
81
|
# s.tx_adapter.move_from_strategy_wallet_to_main_wallet = AsyncMock(
|
|
100
|
-
# return_value=(True, "Transfer successful (simulated)")
|
|
101
82
|
# )
|
|
102
83
|
|
|
103
84
|
# Example for ledger_adapter:
|
|
104
85
|
# if hasattr(s, "ledger_adapter") and s.ledger_adapter:
|
|
105
86
|
# s.ledger_adapter.get_strategy_net_deposit = AsyncMock(
|
|
106
|
-
# return_value=(True, {"net_deposit": 0})
|
|
107
87
|
# )
|
|
108
88
|
# s.ledger_adapter.get_strategy_transactions = AsyncMock(
|
|
109
|
-
# return_value=(True, {"transactions": []})
|
|
110
89
|
# )
|
|
111
90
|
|
|
112
91
|
return s
|
|
@@ -115,7 +94,6 @@ def strategy():
|
|
|
115
94
|
@pytest.mark.asyncio
|
|
116
95
|
@pytest.mark.smoke
|
|
117
96
|
async def test_smoke(strategy):
|
|
118
|
-
"""REQUIRED: Basic smoke test - verifies strategy lifecycle."""
|
|
119
97
|
examples = load_strategy_examples(Path(__file__))
|
|
120
98
|
smoke_data = examples["smoke"]
|
|
121
99
|
|
|
@@ -137,11 +115,6 @@ async def test_smoke(strategy):
|
|
|
137
115
|
|
|
138
116
|
@pytest.mark.asyncio
|
|
139
117
|
async def test_canonical_usage(strategy):
|
|
140
|
-
"""REQUIRED: Test canonical usage examples from examples.json (minimum).
|
|
141
|
-
|
|
142
|
-
Canonical usage = all positive usage examples (excluding error cases).
|
|
143
|
-
This is the MINIMUM requirement - feel free to add more test cases here.
|
|
144
|
-
"""
|
|
145
118
|
examples = load_strategy_examples(Path(__file__))
|
|
146
119
|
canonical = get_canonical_examples(examples)
|
|
147
120
|
|
|
@@ -164,7 +137,6 @@ async def test_canonical_usage(strategy):
|
|
|
164
137
|
|
|
165
138
|
@pytest.mark.asyncio
|
|
166
139
|
async def test_error_cases(strategy):
|
|
167
|
-
"""OPTIONAL: Test error scenarios from examples.json."""
|
|
168
140
|
examples = load_strategy_examples(Path(__file__))
|
|
169
141
|
|
|
170
142
|
for example_name, example_data in examples.items():
|