wayfinder-paths 0.1.19__py3-none-any.whl → 0.1.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wayfinder-paths might be problematic. Click here for more details.

Files changed (98) hide show
  1. wayfinder_paths/__init__.py +0 -2
  2. wayfinder_paths/adapters/balance_adapter/README.md +59 -45
  3. wayfinder_paths/adapters/balance_adapter/adapter.py +0 -21
  4. wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -14
  5. wayfinder_paths/adapters/brap_adapter/README.md +61 -184
  6. wayfinder_paths/adapters/brap_adapter/__init__.py +0 -4
  7. wayfinder_paths/adapters/brap_adapter/adapter.py +0 -147
  8. wayfinder_paths/adapters/brap_adapter/test_adapter.py +0 -15
  9. wayfinder_paths/adapters/hyperlend_adapter/__init__.py +0 -4
  10. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +0 -9
  11. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +0 -17
  12. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +3 -312
  13. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +1 -71
  14. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +0 -57
  15. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +0 -17
  16. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +2 -42
  17. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +1 -9
  18. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +15 -47
  19. wayfinder_paths/adapters/hyperliquid_adapter/utils.py +0 -7
  20. wayfinder_paths/adapters/ledger_adapter/README.md +54 -74
  21. wayfinder_paths/adapters/ledger_adapter/__init__.py +0 -4
  22. wayfinder_paths/adapters/ledger_adapter/adapter.py +0 -106
  23. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +0 -12
  24. wayfinder_paths/adapters/moonwell_adapter/README.md +67 -106
  25. wayfinder_paths/adapters/moonwell_adapter/__init__.py +0 -4
  26. wayfinder_paths/adapters/moonwell_adapter/adapter.py +9 -121
  27. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +84 -83
  28. wayfinder_paths/adapters/pool_adapter/README.md +30 -51
  29. wayfinder_paths/adapters/pool_adapter/__init__.py +0 -4
  30. wayfinder_paths/adapters/pool_adapter/adapter.py +0 -19
  31. wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -8
  32. wayfinder_paths/adapters/token_adapter/README.md +41 -49
  33. wayfinder_paths/adapters/token_adapter/adapter.py +0 -32
  34. wayfinder_paths/adapters/token_adapter/test_adapter.py +1 -12
  35. wayfinder_paths/conftest.py +0 -8
  36. wayfinder_paths/core/__init__.py +0 -2
  37. wayfinder_paths/core/adapters/BaseAdapter.py +0 -22
  38. wayfinder_paths/core/adapters/__init__.py +0 -5
  39. wayfinder_paths/core/adapters/models.py +0 -5
  40. wayfinder_paths/core/analytics/__init__.py +0 -2
  41. wayfinder_paths/core/analytics/bootstrap.py +0 -16
  42. wayfinder_paths/core/analytics/stats.py +0 -7
  43. wayfinder_paths/core/analytics/test_analytics.py +5 -34
  44. wayfinder_paths/core/clients/BRAPClient.py +0 -35
  45. wayfinder_paths/core/clients/ClientManager.py +0 -51
  46. wayfinder_paths/core/clients/HyperlendClient.py +0 -77
  47. wayfinder_paths/core/clients/LedgerClient.py +2 -122
  48. wayfinder_paths/core/clients/PoolClient.py +0 -2
  49. wayfinder_paths/core/clients/TokenClient.py +0 -39
  50. wayfinder_paths/core/clients/WalletClient.py +0 -15
  51. wayfinder_paths/core/clients/WayfinderClient.py +0 -24
  52. wayfinder_paths/core/clients/__init__.py +0 -4
  53. wayfinder_paths/core/clients/protocols.py +25 -98
  54. wayfinder_paths/core/config.py +0 -24
  55. wayfinder_paths/core/constants/__init__.py +0 -7
  56. wayfinder_paths/core/constants/base.py +2 -9
  57. wayfinder_paths/core/constants/erc20_abi.py +0 -5
  58. wayfinder_paths/core/constants/hyperlend_abi.py +0 -7
  59. wayfinder_paths/core/constants/moonwell_abi.py +0 -35
  60. wayfinder_paths/core/engine/StrategyJob.py +0 -32
  61. wayfinder_paths/core/strategies/Strategy.py +0 -99
  62. wayfinder_paths/core/strategies/__init__.py +0 -2
  63. wayfinder_paths/core/utils/__init__.py +0 -1
  64. wayfinder_paths/core/utils/erc20_service.py +0 -1
  65. wayfinder_paths/core/utils/evm_helpers.py +0 -50
  66. wayfinder_paths/core/utils/transaction.py +0 -1
  67. wayfinder_paths/run_strategy.py +0 -46
  68. wayfinder_paths/scripts/create_strategy.py +0 -17
  69. wayfinder_paths/scripts/make_wallets.py +1 -4
  70. wayfinder_paths/strategies/basis_trading_strategy/README.md +71 -163
  71. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +0 -24
  72. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +36 -400
  73. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +15 -64
  74. wayfinder_paths/strategies/basis_trading_strategy/types.py +0 -4
  75. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +65 -56
  76. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +4 -27
  77. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -10
  78. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +71 -72
  79. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +23 -227
  80. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +120 -113
  81. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +64 -59
  82. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +4 -44
  83. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +2 -35
  84. wayfinder_paths/templates/adapter/README.md +107 -46
  85. wayfinder_paths/templates/adapter/adapter.py +0 -9
  86. wayfinder_paths/templates/adapter/test_adapter.py +0 -19
  87. wayfinder_paths/templates/strategy/README.md +113 -59
  88. wayfinder_paths/templates/strategy/strategy.py +0 -22
  89. wayfinder_paths/templates/strategy/test_strategy.py +0 -28
  90. wayfinder_paths/tests/test_test_coverage.py +2 -12
  91. wayfinder_paths/tests/test_utils.py +1 -31
  92. wayfinder_paths-0.1.20.dist-info/METADATA +355 -0
  93. wayfinder_paths-0.1.20.dist-info/RECORD +129 -0
  94. wayfinder_paths/core/adapters/base.py +0 -5
  95. wayfinder_paths-0.1.19.dist-info/METADATA +0 -592
  96. wayfinder_paths-0.1.19.dist-info/RECORD +0 -130
  97. {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.20.dist-info}/LICENSE +0 -0
  98. {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.20.dist-info}/WHEEL +0 -0
@@ -1,89 +1,150 @@
1
- # Adapter Template
1
+ # Adapter Template
2
2
 
3
- Adapters expose protocol-specific capabilities to strategies. They should be thin, async wrappers around one or more clients from `wayfinder_paths.core.clients`.
3
+ Adapters expose protocol-specific capabilities to strategies. They wrap one or more clients from `wayfinder_paths.core.clients`.
4
4
 
5
- ## Quick start
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. Implement the public methods that provide your adapter's capabilities.
13
- 4. Add tests in `test_adapter.py`.
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
- ## Layout
16
+ ## Directory Structure
16
17
 
17
18
  ```
18
19
  my_adapter/
19
20
  ├── adapter.py # Adapter implementation
20
- ├── examples.json # Example payloads (optional but encouraged)
21
- ├── test_adapter.py # Pytest smoke tests
22
- └── README.md # Adapter-specific notes
21
+ ├── examples.json # Example payloads (optional)
22
+ ├── test_adapter.py # Pytest tests
23
+ └── README.md # Adapter documentation
23
24
  ```
24
25
 
25
- ## Skeleton adapter
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.PoolClient import PoolClient
32
+ from wayfinder_paths.core.clients.SomeClient import SomeClient
32
33
 
33
34
 
34
35
  class MyAdapter(BaseAdapter):
35
- adapter_type = "MY_ADAPTER"
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.pool_client = PoolClient()
42
+ self.client = SomeClient()
40
43
 
41
44
  async def connect(self) -> bool:
42
- """Optional: prime caches / test connectivity."""
45
+ """Optional: Establish connectivity."""
43
46
  return True
44
47
 
45
- async def get_pools(self, pool_ids: list[str]) -> tuple[bool, Any]:
46
- """Example capability that proxies PoolClient."""
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
- data = await self.pool_client.get_pools_by_ids(
49
- pool_ids=pool_ids
50
- )
51
- return (True, data)
52
- except Exception as exc: # noqa: BLE001
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
- Your adapter should return `(success, payload)` tuples for every operation, just like the built-in adapters do.
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` should cover the public methods you expose. Patch out remote clients with `unittest.mock.AsyncMock` so tests run offline.
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
- from wayfinder_paths.adapters.my_adapter.adapter import MyAdapter
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 practices
144
+ ## Best Practices
85
145
 
86
- - Keep adapters stateless and idempotent—strategies may reuse instances across operations.
87
- - Use `self.logger` for contextual logging (BaseAdapter has already bound the adapter name).
88
- - Return `(success, payload)` tuples consistently for all operations.
89
- - Raise `NotImplementedError` for capabilities you intentionally do not support yet.
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 the scaffolding for a new strategy. It mirrors the structure in `wayfinder_paths/strategies/...`.
3
+ This template provides scaffolding for a new strategy.
4
4
 
5
5
  ## Quick Start
6
6
 
7
- 1. Copy the template to a new folder:
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. Fill out `examples.json` with sample CLI invocations and `test_strategy.py` with at least one smoke test.
13
- 4. Implement the required strategy methods (`deposit`, `update`, `_status`, optionally override `withdraw`).
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
- ## Layout
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-based smoke tests
22
- └── README.md # Strategy-specific documentation
26
+ ├── test_strategy.py # Pytest tests
27
+ └── README.md # Strategy documentation
23
28
  ```
24
29
 
25
- ## Required methods
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 the main wallet into the strategy wallet and prepare on-chain positions."""
34
+ """Move funds from main wallet into strategy wallet and deploy capital."""
30
35
 
31
36
  async def update(self) -> StatusTuple:
32
- """Periodic rebalance/update loop."""
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 payloads."""
43
+ """Return portfolio_value, net_deposit, and strategy_status."""
36
44
  ```
37
45
 
38
- `Strategy.withdraw` already unwinds ledger operations. Override it only if you need custom exit logic.
46
+ ## Optional Methods
39
47
 
40
- ## Wiring adapters
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
- Strategies typically:
55
+ async def setup(self) -> None:
56
+ """Post-construction initialization."""
43
57
 
44
- 2. Instantiate adapters (balance, ledger, protocol specific, etc.).
45
- 3. Register adapters via `self.register_adapters([...])` and keep references as attributes.
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 = "Demo Strategy"
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
- balance_adapter = BalanceAdapter(self.config)
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
- success, _ = await self.balance_adapter.get_balance(
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
- """Execute your strategy logic periodically."""
81
- return (True, "No-op update")
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": float(balance or 0),
91
- "net_deposit": float(getattr(self, "last_deposit", 0.0)),
92
- "strategy_status": {"message": "healthy" if success else "unknown"},
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` should cover at least deposit/update/status. Use `pytest.mark.asyncio` and patch adapters or services as needed.
114
+ Create `test_strategy.py` using `examples.json`:
99
115
 
100
116
  ```python
101
117
  import pytest
102
- from wayfinder_paths.strategies.my_strategy.strategy import MyStrategy
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 test_status_shape():
107
- strat = MyStrategy(config={})
108
- status = await strat._status()
109
- assert set(status) == {"portfolio_value", "net_deposit", "strategy_status"}
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 strategy locally
153
+ ## Running the Strategy
113
154
 
114
155
  ```bash
115
- # Install dependencies & create wallets first
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
- # Copy config and edit credentials
121
- cp wayfinder_paths/config.example.json config.json
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
- # Run your strategy
124
- poetry run python wayfinder_paths/run_strategy.py my_strategy --action status --config $(pwd)/config.json
175
+ # Withdraw
176
+ poetry run python wayfinder_paths/run_strategy.py my_strategy --action withdraw --config config.json
125
177
  ```
126
178
 
127
- ## Best practices
179
+ ## Best Practices
128
180
 
129
- - Return `(success: bool, message: str)` tuples from `deposit`/`update`.
130
- - Always populate `portfolio_value`, `net_deposit`, and `strategy_status` keys in `_status`.
131
- - Register adapters via `register_adapters` in your `__init__` method.
132
- - Keep strategy logic clear and well-documented.
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():