wayfinder-paths 0.1.22__py3-none-any.whl → 0.1.23__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 -4
- wayfinder_paths/adapters/balance_adapter/README.md +0 -1
- wayfinder_paths/adapters/balance_adapter/adapter.py +65 -169
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -113
- wayfinder_paths/adapters/brap_adapter/README.md +22 -75
- wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
- wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +39 -86
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +5 -1
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +6 -5
- wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
- wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +108 -198
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +37 -23
- wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
- wayfinder_paths/core/__init__.py +0 -3
- wayfinder_paths/core/clients/BRAPClient.py +3 -0
- wayfinder_paths/core/clients/ClientManager.py +0 -7
- wayfinder_paths/core/clients/LedgerClient.py +196 -172
- wayfinder_paths/core/clients/WayfinderClient.py +0 -1
- wayfinder_paths/core/clients/__init__.py +0 -5
- wayfinder_paths/core/clients/protocols.py +0 -13
- wayfinder_paths/core/config.py +0 -164
- wayfinder_paths/core/constants/__init__.py +58 -2
- wayfinder_paths/core/constants/base.py +8 -22
- wayfinder_paths/core/constants/chains.py +36 -0
- wayfinder_paths/core/constants/contracts.py +39 -0
- wayfinder_paths/core/constants/tokens.py +9 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -10
- wayfinder_paths/core/utils/evm_helpers.py +5 -15
- wayfinder_paths/core/utils/tokens.py +28 -0
- wayfinder_paths/core/utils/transaction.py +13 -7
- wayfinder_paths/core/utils/web3.py +5 -3
- wayfinder_paths/policies/enso.py +1 -2
- wayfinder_paths/policies/hyper_evm.py +6 -3
- wayfinder_paths/policies/hyperlend.py +1 -2
- wayfinder_paths/policies/moonwell.py +12 -7
- wayfinder_paths/policies/prjx.py +1 -3
- wayfinder_paths/run_strategy.py +97 -300
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +19 -14
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +12 -11
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +20 -33
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +21 -18
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -130
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +32 -42
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.23.dist-info}/METADATA +2 -3
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.23.dist-info}/RECORD +51 -60
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.23.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/clients/WalletClient.py +0 -41
- wayfinder_paths/core/engine/StrategyJob.py +0 -110
- wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
- wayfinder_paths/templates/adapter/README.md +0 -150
- wayfinder_paths/templates/adapter/adapter.py +0 -16
- wayfinder_paths/templates/adapter/examples.json +0 -8
- wayfinder_paths/templates/adapter/test_adapter.py +0 -30
- wayfinder_paths/templates/strategy/README.md +0 -186
- wayfinder_paths/templates/strategy/examples.json +0 -11
- wayfinder_paths/templates/strategy/strategy.py +0 -35
- wayfinder_paths/templates/strategy/test_strategy.py +0 -166
- wayfinder_paths/tests/test_smoke_manifest.py +0 -63
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.23.dist-info}/LICENSE +0 -0
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from unittest.mock import AsyncMock, MagicMock
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
from wayfinder_paths.core.services.local_evm_txn import BASE_CHAIN_ID, LocalEvmTxn
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class _FakeTxHash:
|
|
11
|
-
def __init__(self, value: str):
|
|
12
|
-
self._value = value
|
|
13
|
-
|
|
14
|
-
def hex(self) -> str:
|
|
15
|
-
return self._value
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@pytest.mark.asyncio
|
|
19
|
-
async def test_base_defaults_to_two_confirmations():
|
|
20
|
-
txn = LocalEvmTxn(config={})
|
|
21
|
-
|
|
22
|
-
fake_web3 = MagicMock()
|
|
23
|
-
fake_web3.eth = MagicMock()
|
|
24
|
-
fake_web3.eth.send_raw_transaction = AsyncMock(return_value=_FakeTxHash("0x1"))
|
|
25
|
-
fake_web3.eth.wait_for_transaction_receipt = AsyncMock(
|
|
26
|
-
return_value={
|
|
27
|
-
"status": 1,
|
|
28
|
-
"blockNumber": 100,
|
|
29
|
-
"transactionHash": "0x1",
|
|
30
|
-
"gasUsed": 21_000,
|
|
31
|
-
"logs": [],
|
|
32
|
-
}
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
txn.get_web3 = MagicMock(return_value=fake_web3)
|
|
36
|
-
txn._validate_transaction = MagicMock(side_effect=lambda tx: tx)
|
|
37
|
-
txn._nonce_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
|
|
38
|
-
txn._gas_limit_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
|
|
39
|
-
txn._gas_price_transaction = AsyncMock(side_effect=lambda tx, _chain_id, _w3: tx)
|
|
40
|
-
txn._sign_transaction = MagicMock(return_value=b"signed")
|
|
41
|
-
txn._close_web3 = AsyncMock()
|
|
42
|
-
txn._wait_for_confirmations = AsyncMock()
|
|
43
|
-
|
|
44
|
-
ok, result = await txn.broadcast_transaction(
|
|
45
|
-
{
|
|
46
|
-
"chainId": BASE_CHAIN_ID,
|
|
47
|
-
"from": "0x0000000000000000000000000000000000000001",
|
|
48
|
-
"to": "0x0000000000000000000000000000000000000002",
|
|
49
|
-
"value": 0,
|
|
50
|
-
},
|
|
51
|
-
wait_for_receipt=True,
|
|
52
|
-
timeout=1,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
assert ok is True
|
|
56
|
-
txn._wait_for_confirmations.assert_awaited_once_with(fake_web3, 100, 2)
|
|
57
|
-
assert result["confirmations"] == 2
|
|
58
|
-
assert result["confirmed_block_number"] == 102
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@pytest.mark.asyncio
|
|
62
|
-
async def test_non_base_defaults_to_zero_confirmations():
|
|
63
|
-
txn = LocalEvmTxn(config={})
|
|
64
|
-
|
|
65
|
-
fake_web3 = MagicMock()
|
|
66
|
-
fake_web3.eth = MagicMock()
|
|
67
|
-
fake_web3.eth.send_raw_transaction = AsyncMock(return_value=_FakeTxHash("0x1"))
|
|
68
|
-
fake_web3.eth.wait_for_transaction_receipt = AsyncMock(
|
|
69
|
-
return_value={
|
|
70
|
-
"status": 1,
|
|
71
|
-
"blockNumber": 100,
|
|
72
|
-
"transactionHash": "0x1",
|
|
73
|
-
"gasUsed": 21_000,
|
|
74
|
-
"logs": [],
|
|
75
|
-
}
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
txn.get_web3 = MagicMock(return_value=fake_web3)
|
|
79
|
-
txn._validate_transaction = MagicMock(side_effect=lambda tx: tx)
|
|
80
|
-
txn._nonce_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
|
|
81
|
-
txn._gas_limit_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
|
|
82
|
-
txn._gas_price_transaction = AsyncMock(side_effect=lambda tx, _chain_id, _w3: tx)
|
|
83
|
-
txn._sign_transaction = MagicMock(return_value=b"signed")
|
|
84
|
-
txn._close_web3 = AsyncMock()
|
|
85
|
-
txn._wait_for_confirmations = AsyncMock()
|
|
86
|
-
|
|
87
|
-
ok, result = await txn.broadcast_transaction(
|
|
88
|
-
{
|
|
89
|
-
"chainId": 1,
|
|
90
|
-
"from": "0x0000000000000000000000000000000000000001",
|
|
91
|
-
"to": "0x0000000000000000000000000000000000000002",
|
|
92
|
-
"value": 0,
|
|
93
|
-
},
|
|
94
|
-
wait_for_receipt=True,
|
|
95
|
-
timeout=1,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
assert ok is True
|
|
99
|
-
txn._wait_for_confirmations.assert_not_awaited()
|
|
100
|
-
assert result["confirmations"] == 0
|
|
101
|
-
assert result["confirmed_block_number"] == 100
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@pytest.mark.asyncio
|
|
105
|
-
async def test_explicit_confirmations_override_defaults():
|
|
106
|
-
txn = LocalEvmTxn(config={})
|
|
107
|
-
|
|
108
|
-
fake_web3 = MagicMock()
|
|
109
|
-
fake_web3.eth = MagicMock()
|
|
110
|
-
fake_web3.eth.send_raw_transaction = AsyncMock(return_value=_FakeTxHash("0x1"))
|
|
111
|
-
fake_web3.eth.wait_for_transaction_receipt = AsyncMock(
|
|
112
|
-
return_value={
|
|
113
|
-
"status": 1,
|
|
114
|
-
"blockNumber": 100,
|
|
115
|
-
"transactionHash": "0x1",
|
|
116
|
-
"gasUsed": 21_000,
|
|
117
|
-
"logs": [],
|
|
118
|
-
}
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
txn.get_web3 = MagicMock(return_value=fake_web3)
|
|
122
|
-
txn._validate_transaction = MagicMock(side_effect=lambda tx: tx)
|
|
123
|
-
txn._nonce_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
|
|
124
|
-
txn._gas_limit_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
|
|
125
|
-
txn._gas_price_transaction = AsyncMock(side_effect=lambda tx, _chain_id, _w3: tx)
|
|
126
|
-
txn._sign_transaction = MagicMock(return_value=b"signed")
|
|
127
|
-
txn._close_web3 = AsyncMock()
|
|
128
|
-
txn._wait_for_confirmations = AsyncMock()
|
|
129
|
-
|
|
130
|
-
ok, result = await txn.broadcast_transaction(
|
|
131
|
-
{
|
|
132
|
-
"chainId": BASE_CHAIN_ID,
|
|
133
|
-
"from": "0x0000000000000000000000000000000000000001",
|
|
134
|
-
"to": "0x0000000000000000000000000000000000000002",
|
|
135
|
-
"value": 0,
|
|
136
|
-
},
|
|
137
|
-
wait_for_receipt=True,
|
|
138
|
-
timeout=1,
|
|
139
|
-
confirmations=0,
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
assert ok is True
|
|
143
|
-
txn._wait_for_confirmations.assert_not_awaited()
|
|
144
|
-
assert result["confirmations"] == 0
|
|
145
|
-
assert result["confirmed_block_number"] == 100
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
# Adapter Template
|
|
2
|
-
|
|
3
|
-
Adapters expose protocol-specific capabilities to strategies. They wrap one or more clients from `wayfinder_paths.core.clients`.
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
1. Copy the template:
|
|
8
|
-
```bash
|
|
9
|
-
cp -r wayfinder_paths/templates/adapter wayfinder_paths/adapters/my_adapter
|
|
10
|
-
```
|
|
11
|
-
2. Rename `MyAdapter` in `adapter.py` to match your adapter's purpose.
|
|
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`.
|
|
15
|
-
|
|
16
|
-
## Directory Structure
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
my_adapter/
|
|
20
|
-
├── adapter.py # Adapter implementation
|
|
21
|
-
├── examples.json # Example payloads (optional)
|
|
22
|
-
├── test_adapter.py # Pytest tests
|
|
23
|
-
└── README.md # Adapter documentation
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Adapter Structure
|
|
27
|
-
|
|
28
|
-
```python
|
|
29
|
-
from typing import Any
|
|
30
|
-
|
|
31
|
-
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
32
|
-
from wayfinder_paths.core.clients.SomeClient import SomeClient
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class MyAdapter(BaseAdapter):
|
|
36
|
-
"""Adapter for MyProtocol operations."""
|
|
37
|
-
|
|
38
|
-
adapter_type = "MY_PROTOCOL"
|
|
39
|
-
|
|
40
|
-
def __init__(self, config: dict[str, Any] | None = None):
|
|
41
|
-
super().__init__("my_adapter", config)
|
|
42
|
-
self.client = SomeClient()
|
|
43
|
-
|
|
44
|
-
async def connect(self) -> bool:
|
|
45
|
-
"""Optional: Establish connectivity."""
|
|
46
|
-
return True
|
|
47
|
-
|
|
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
|
-
"""
|
|
58
|
-
try:
|
|
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))
|
|
64
|
-
```
|
|
65
|
-
|
|
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
|
-
```
|
|
101
|
-
|
|
102
|
-
## Testing
|
|
103
|
-
|
|
104
|
-
Create `test_adapter.py`:
|
|
105
|
-
|
|
106
|
-
```python
|
|
107
|
-
import pytest
|
|
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:
|
|
139
|
-
|
|
140
|
-
```bash
|
|
141
|
-
poetry run pytest wayfinder_paths/adapters/my_adapter/ -v
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
## Best Practices
|
|
145
|
-
|
|
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
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class MyAdapter(BaseAdapter):
|
|
7
|
-
adapter_type: str = "MY_ADAPTER"
|
|
8
|
-
|
|
9
|
-
def __init__(self, config: dict[str, Any] | None = None):
|
|
10
|
-
super().__init__("my_adapter", config)
|
|
11
|
-
|
|
12
|
-
async def connect(self) -> bool:
|
|
13
|
-
return True
|
|
14
|
-
|
|
15
|
-
async def example_operation(self, **kwargs) -> tuple[bool, str]:
|
|
16
|
-
return (True, "example.op executed")
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
# TODO: Replace MyAdapter with your actual adapter class name
|
|
4
|
-
from .adapter import MyAdapter
|
|
5
|
-
|
|
6
|
-
# For mocking clients, uncomment when needed:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class TestMyAdapter:
|
|
10
|
-
@pytest.fixture
|
|
11
|
-
def adapter(self):
|
|
12
|
-
return MyAdapter(config={})
|
|
13
|
-
|
|
14
|
-
@pytest.mark.asyncio
|
|
15
|
-
async def test_health_check(self, adapter):
|
|
16
|
-
health = await adapter.health_check()
|
|
17
|
-
assert isinstance(health, dict)
|
|
18
|
-
assert health.get("status") in {"healthy", "unhealthy", "error"}
|
|
19
|
-
|
|
20
|
-
@pytest.mark.asyncio
|
|
21
|
-
async def test_connect(self, adapter):
|
|
22
|
-
ok = await adapter.connect()
|
|
23
|
-
assert isinstance(ok, bool)
|
|
24
|
-
|
|
25
|
-
def test_capabilities(self, adapter):
|
|
26
|
-
assert hasattr(adapter, "adapter_type")
|
|
27
|
-
|
|
28
|
-
@pytest.mark.asyncio
|
|
29
|
-
async def test_basic_functionality(self, adapter):
|
|
30
|
-
assert adapter is not None
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
# Strategy Template
|
|
2
|
-
|
|
3
|
-
This template provides scaffolding for a new strategy.
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
1. Copy the template:
|
|
8
|
-
```bash
|
|
9
|
-
cp -r wayfinder_paths/templates/strategy wayfinder_paths/strategies/my_strategy
|
|
10
|
-
```
|
|
11
|
-
Or use the convenience command:
|
|
12
|
-
```bash
|
|
13
|
-
just create-strategy "My Strategy Name"
|
|
14
|
-
```
|
|
15
|
-
2. Rename the class in `strategy.py` to match your strategy name.
|
|
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.
|
|
19
|
-
|
|
20
|
-
## Directory Structure
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
my_strategy/
|
|
24
|
-
├── strategy.py # Strategy implementation
|
|
25
|
-
├── examples.json # Example CLI payloads
|
|
26
|
-
├── test_strategy.py # Pytest tests
|
|
27
|
-
└── README.md # Strategy documentation
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Required Methods
|
|
31
|
-
|
|
32
|
-
```python
|
|
33
|
-
async def deposit(self, main_token_amount: float, gas_token_amount: float) -> StatusTuple:
|
|
34
|
-
"""Move funds from main wallet into strategy wallet and deploy capital."""
|
|
35
|
-
|
|
36
|
-
async def update(self) -> StatusTuple:
|
|
37
|
-
"""Periodic rebalance/optimization loop."""
|
|
38
|
-
|
|
39
|
-
async def exit(self, **kwargs) -> StatusTuple:
|
|
40
|
-
"""Transfer funds from strategy wallet back to main wallet."""
|
|
41
|
-
|
|
42
|
-
async def _status(self) -> StatusDict:
|
|
43
|
-
"""Return portfolio_value, net_deposit, and strategy_status."""
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Optional Methods
|
|
47
|
-
|
|
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."""
|
|
54
|
-
|
|
55
|
-
async def setup(self) -> None:
|
|
56
|
-
"""Post-construction initialization."""
|
|
57
|
-
|
|
58
|
-
async def health_check(self) -> dict:
|
|
59
|
-
"""Check strategy and adapter health."""
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Strategy Structure
|
|
63
|
-
|
|
64
|
-
```python
|
|
65
|
-
from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
|
|
66
|
-
from wayfinder_paths.adapters.balance_adapter.adapter import BalanceAdapter
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class MyStrategy(Strategy):
|
|
70
|
-
name = "My Strategy"
|
|
71
|
-
|
|
72
|
-
def __init__(self, config: dict | None = None, **kwargs):
|
|
73
|
-
super().__init__(config, **kwargs)
|
|
74
|
-
self.config = config or {}
|
|
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
|
-
)
|
|
82
|
-
self.register_adapters([balance_adapter])
|
|
83
|
-
self.balance_adapter = balance_adapter
|
|
84
|
-
|
|
85
|
-
async def deposit(
|
|
86
|
-
self, main_token_amount: float = 0.0, gas_token_amount: float = 0.0
|
|
87
|
-
) -> StatusTuple:
|
|
88
|
-
if main_token_amount <= 0:
|
|
89
|
-
return (False, "Nothing to deposit")
|
|
90
|
-
|
|
91
|
-
# Implement deposit logic
|
|
92
|
-
return (True, f"Deposited {main_token_amount} tokens")
|
|
93
|
-
|
|
94
|
-
async def update(self) -> StatusTuple:
|
|
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")
|
|
101
|
-
|
|
102
|
-
async def _status(self) -> StatusDict:
|
|
103
|
-
return {
|
|
104
|
-
"portfolio_value": 0.0,
|
|
105
|
-
"net_deposit": 0.0,
|
|
106
|
-
"strategy_status": {"message": "healthy"},
|
|
107
|
-
"gas_available": 0.0,
|
|
108
|
-
"gassed_up": True,
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## Testing
|
|
113
|
-
|
|
114
|
-
Create `test_strategy.py` using `examples.json`:
|
|
115
|
-
|
|
116
|
-
```python
|
|
117
|
-
import pytest
|
|
118
|
-
from pathlib import Path
|
|
119
|
-
from tests.test_utils import load_strategy_examples
|
|
120
|
-
from .strategy import MyStrategy
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
@pytest.mark.asyncio
|
|
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
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
## Running the Strategy
|
|
154
|
-
|
|
155
|
-
```bash
|
|
156
|
-
# Install dependencies
|
|
157
|
-
poetry install
|
|
158
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
# Withdraw
|
|
176
|
-
poetry run python wayfinder_paths/run_strategy.py my_strategy --action withdraw --config config.json
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
## Best Practices
|
|
180
|
-
|
|
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
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class MyStrategy(Strategy):
|
|
5
|
-
name = "My Strategy"
|
|
6
|
-
description = "Short description of what the strategy does."
|
|
7
|
-
summary = "One-line summary for discovery."
|
|
8
|
-
|
|
9
|
-
def __init__(self):
|
|
10
|
-
super().__init__()
|
|
11
|
-
|
|
12
|
-
async def setup(self):
|
|
13
|
-
return None
|
|
14
|
-
|
|
15
|
-
async def deposit(
|
|
16
|
-
self, main_token_amount: float, gas_token_amount: float
|
|
17
|
-
) -> StatusTuple:
|
|
18
|
-
return (True, "Deposit successful")
|
|
19
|
-
|
|
20
|
-
async def withdraw(self, amount: float | None = None) -> StatusTuple:
|
|
21
|
-
return await super().withdraw(amount=amount)
|
|
22
|
-
|
|
23
|
-
async def update(self) -> StatusTuple:
|
|
24
|
-
return (True, "Update successful")
|
|
25
|
-
|
|
26
|
-
async def _status(self) -> StatusDict:
|
|
27
|
-
return {
|
|
28
|
-
"portfolio_value": 0.0,
|
|
29
|
-
"net_deposit": 0.0,
|
|
30
|
-
"strategy_status": {},
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@staticmethod
|
|
34
|
-
def policies() -> list[str]:
|
|
35
|
-
return []
|