wayfinder-paths 0.1.1__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 (115) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +394 -0
  2. wayfinder_paths/__init__.py +21 -0
  3. wayfinder_paths/config.example.json +20 -0
  4. wayfinder_paths/conftest.py +31 -0
  5. wayfinder_paths/core/__init__.py +13 -0
  6. wayfinder_paths/core/adapters/BaseAdapter.py +48 -0
  7. wayfinder_paths/core/adapters/__init__.py +5 -0
  8. wayfinder_paths/core/adapters/base.py +5 -0
  9. wayfinder_paths/core/clients/AuthClient.py +83 -0
  10. wayfinder_paths/core/clients/BRAPClient.py +90 -0
  11. wayfinder_paths/core/clients/ClientManager.py +231 -0
  12. wayfinder_paths/core/clients/HyperlendClient.py +151 -0
  13. wayfinder_paths/core/clients/LedgerClient.py +222 -0
  14. wayfinder_paths/core/clients/PoolClient.py +96 -0
  15. wayfinder_paths/core/clients/SimulationClient.py +180 -0
  16. wayfinder_paths/core/clients/TokenClient.py +73 -0
  17. wayfinder_paths/core/clients/TransactionClient.py +47 -0
  18. wayfinder_paths/core/clients/WalletClient.py +90 -0
  19. wayfinder_paths/core/clients/WayfinderClient.py +258 -0
  20. wayfinder_paths/core/clients/__init__.py +48 -0
  21. wayfinder_paths/core/clients/protocols.py +295 -0
  22. wayfinder_paths/core/clients/sdk_example.py +115 -0
  23. wayfinder_paths/core/config.py +369 -0
  24. wayfinder_paths/core/constants/__init__.py +26 -0
  25. wayfinder_paths/core/constants/base.py +25 -0
  26. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  27. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  28. wayfinder_paths/core/engine/VaultJob.py +182 -0
  29. wayfinder_paths/core/engine/__init__.py +5 -0
  30. wayfinder_paths/core/engine/manifest.py +97 -0
  31. wayfinder_paths/core/services/__init__.py +0 -0
  32. wayfinder_paths/core/services/base.py +177 -0
  33. wayfinder_paths/core/services/local_evm_txn.py +429 -0
  34. wayfinder_paths/core/services/local_token_txn.py +231 -0
  35. wayfinder_paths/core/services/web3_service.py +45 -0
  36. wayfinder_paths/core/settings.py +61 -0
  37. wayfinder_paths/core/strategies/Strategy.py +183 -0
  38. wayfinder_paths/core/strategies/__init__.py +5 -0
  39. wayfinder_paths/core/strategies/base.py +7 -0
  40. wayfinder_paths/core/utils/__init__.py +1 -0
  41. wayfinder_paths/core/utils/evm_helpers.py +165 -0
  42. wayfinder_paths/core/utils/wallets.py +77 -0
  43. wayfinder_paths/core/wallets/README.md +91 -0
  44. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  45. wayfinder_paths/core/wallets/__init__.py +7 -0
  46. wayfinder_paths/run_strategy.py +409 -0
  47. wayfinder_paths/scripts/__init__.py +0 -0
  48. wayfinder_paths/scripts/create_strategy.py +181 -0
  49. wayfinder_paths/scripts/make_wallets.py +160 -0
  50. wayfinder_paths/scripts/validate_manifests.py +213 -0
  51. wayfinder_paths/tests/__init__.py +0 -0
  52. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  53. wayfinder_paths/tests/test_test_coverage.py +212 -0
  54. wayfinder_paths/tests/test_utils.py +64 -0
  55. wayfinder_paths/vaults/__init__.py +0 -0
  56. wayfinder_paths/vaults/adapters/__init__.py +0 -0
  57. wayfinder_paths/vaults/adapters/balance_adapter/README.md +104 -0
  58. wayfinder_paths/vaults/adapters/balance_adapter/adapter.py +257 -0
  59. wayfinder_paths/vaults/adapters/balance_adapter/examples.json +6 -0
  60. wayfinder_paths/vaults/adapters/balance_adapter/manifest.yaml +8 -0
  61. wayfinder_paths/vaults/adapters/balance_adapter/test_adapter.py +83 -0
  62. wayfinder_paths/vaults/adapters/brap_adapter/README.md +249 -0
  63. wayfinder_paths/vaults/adapters/brap_adapter/__init__.py +7 -0
  64. wayfinder_paths/vaults/adapters/brap_adapter/adapter.py +717 -0
  65. wayfinder_paths/vaults/adapters/brap_adapter/examples.json +175 -0
  66. wayfinder_paths/vaults/adapters/brap_adapter/manifest.yaml +11 -0
  67. wayfinder_paths/vaults/adapters/brap_adapter/test_adapter.py +288 -0
  68. wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +7 -0
  69. wayfinder_paths/vaults/adapters/hyperlend_adapter/adapter.py +298 -0
  70. wayfinder_paths/vaults/adapters/hyperlend_adapter/manifest.yaml +10 -0
  71. wayfinder_paths/vaults/adapters/hyperlend_adapter/test_adapter.py +267 -0
  72. wayfinder_paths/vaults/adapters/ledger_adapter/README.md +158 -0
  73. wayfinder_paths/vaults/adapters/ledger_adapter/__init__.py +7 -0
  74. wayfinder_paths/vaults/adapters/ledger_adapter/adapter.py +286 -0
  75. wayfinder_paths/vaults/adapters/ledger_adapter/examples.json +131 -0
  76. wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +11 -0
  77. wayfinder_paths/vaults/adapters/ledger_adapter/test_adapter.py +202 -0
  78. wayfinder_paths/vaults/adapters/pool_adapter/README.md +218 -0
  79. wayfinder_paths/vaults/adapters/pool_adapter/__init__.py +7 -0
  80. wayfinder_paths/vaults/adapters/pool_adapter/adapter.py +289 -0
  81. wayfinder_paths/vaults/adapters/pool_adapter/examples.json +143 -0
  82. wayfinder_paths/vaults/adapters/pool_adapter/manifest.yaml +10 -0
  83. wayfinder_paths/vaults/adapters/pool_adapter/test_adapter.py +222 -0
  84. wayfinder_paths/vaults/adapters/token_adapter/README.md +101 -0
  85. wayfinder_paths/vaults/adapters/token_adapter/__init__.py +3 -0
  86. wayfinder_paths/vaults/adapters/token_adapter/adapter.py +92 -0
  87. wayfinder_paths/vaults/adapters/token_adapter/examples.json +26 -0
  88. wayfinder_paths/vaults/adapters/token_adapter/manifest.yaml +6 -0
  89. wayfinder_paths/vaults/adapters/token_adapter/test_adapter.py +135 -0
  90. wayfinder_paths/vaults/strategies/__init__.py +0 -0
  91. wayfinder_paths/vaults/strategies/config.py +85 -0
  92. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/README.md +99 -0
  93. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/examples.json +16 -0
  94. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  95. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/strategy.py +2328 -0
  96. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/test_strategy.py +319 -0
  97. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/README.md +95 -0
  98. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/examples.json +17 -0
  99. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  100. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/strategy.py +1684 -0
  101. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/test_strategy.py +350 -0
  102. wayfinder_paths/vaults/templates/adapter/README.md +105 -0
  103. wayfinder_paths/vaults/templates/adapter/adapter.py +26 -0
  104. wayfinder_paths/vaults/templates/adapter/examples.json +8 -0
  105. wayfinder_paths/vaults/templates/adapter/manifest.yaml +6 -0
  106. wayfinder_paths/vaults/templates/adapter/test_adapter.py +49 -0
  107. wayfinder_paths/vaults/templates/strategy/README.md +152 -0
  108. wayfinder_paths/vaults/templates/strategy/examples.json +11 -0
  109. wayfinder_paths/vaults/templates/strategy/manifest.yaml +8 -0
  110. wayfinder_paths/vaults/templates/strategy/strategy.py +57 -0
  111. wayfinder_paths/vaults/templates/strategy/test_strategy.py +197 -0
  112. wayfinder_paths-0.1.1.dist-info/LICENSE +21 -0
  113. wayfinder_paths-0.1.1.dist-info/METADATA +727 -0
  114. wayfinder_paths-0.1.1.dist-info/RECORD +115 -0
  115. wayfinder_paths-0.1.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,152 @@
1
+ # Strategy Template
2
+
3
+ This template provides the scaffolding for a new vault strategy. It mirrors the structure in `wayfinder_paths/vaults/strategies/...` and wires into the runner via a manifest.
4
+
5
+ ## Quick Start
6
+
7
+ 1. Copy the template to a new folder:
8
+ ```
9
+ cp -r wayfinder_paths/vaults/templates/strategy wayfinder_paths/vaults/strategies/my_strategy
10
+ ```
11
+ 2. Rename the class in `strategy.py` and update `manifest.yaml` so `entrypoint` points to your new module (for example `vaults.strategies.my_strategy.strategy.MyStrategy`).
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`).
14
+
15
+ ## Layout
16
+
17
+ ```
18
+ my_strategy/
19
+ ├── strategy.py # Strategy implementation
20
+ ├── manifest.yaml # Entrypoint + adapter requirements + policy
21
+ ├── examples.json # Example CLI payloads
22
+ ├── test_strategy.py # Pytest-based smoke tests
23
+ └── README.md # Strategy-specific documentation
24
+ ```
25
+
26
+ ## Required methods
27
+
28
+ ```python
29
+ async def deposit(self, main_token_amount: float, gas_token_amount: float) -> StatusTuple:
30
+ """Move funds from the main wallet into the vault wallet and prepare on-chain positions."""
31
+
32
+ async def update(self) -> StatusTuple:
33
+ """Periodic rebalance/update loop."""
34
+
35
+ async def _status(self) -> StatusDict:
36
+ """Return portfolio_value, net_deposit, and strategy_status payloads."""
37
+ ```
38
+
39
+ `Strategy.withdraw` already unwinds ledger operations. Override it only if you need custom exit logic.
40
+
41
+ ## Wiring adapters
42
+
43
+ Strategies typically:
44
+
45
+ 1. Build a `DefaultWeb3Service` so every adapter shares a wallet provider.
46
+ 2. Instantiate adapters (balance, ledger, protocol specific, etc.).
47
+ 3. Register adapters via `self.register_adapters([...])` and keep references as attributes.
48
+
49
+ ```python
50
+ from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
51
+ from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
52
+ from wayfinder_paths.vaults.adapters.balance_adapter.adapter import BalanceAdapter
53
+
54
+
55
+ class MyStrategy(Strategy):
56
+ name = "Demo Strategy"
57
+
58
+ def __init__(self, config: dict | None = None):
59
+ super().__init__()
60
+ self.config = config or {}
61
+ web3_service = DefaultWeb3Service(self.config)
62
+ balance_adapter = BalanceAdapter(self.config, web3_service=web3_service)
63
+ self.register_adapters([balance_adapter])
64
+ self.balance_adapter = balance_adapter
65
+
66
+ async def deposit(
67
+ self, main_token_amount: float = 0.0, gas_token_amount: float = 0.0
68
+ ) -> StatusTuple:
69
+ """Perform validation, move funds, and optionally deploy capital."""
70
+ if main_token_amount <= 0:
71
+ return (False, "Nothing to deposit")
72
+
73
+ success, _ = await self.balance_adapter.get_balance(
74
+ token_id=self.config.get("token_id"),
75
+ wallet_address=self.config.get("main_wallet", {}).get("address"),
76
+ )
77
+ if not success:
78
+ return (False, "Unable to fetch balances")
79
+
80
+ # Use BalanceAdapter (which leverages LocalTokenTxnService builders) for transfers.
81
+ self.last_deposit = main_token_amount
82
+ return (True, f"Deposited {main_token_amount} tokens")
83
+
84
+ async def update(self) -> StatusTuple:
85
+ """Execute your strategy logic periodically."""
86
+ return (True, "No-op update")
87
+
88
+ async def _status(self) -> StatusDict:
89
+ """Surface state back to run_strategy.py."""
90
+ success, balance = await self.balance_adapter.get_balance(
91
+ token_id=self.config.get("token_id"),
92
+ wallet_address=self.config.get("vault_wallet", {}).get("address"),
93
+ )
94
+ return {
95
+ "portfolio_value": float(balance or 0),
96
+ "net_deposit": float(getattr(self, "last_deposit", 0.0)),
97
+ "strategy_status": {"message": "healthy" if success else "unknown"},
98
+ }
99
+ ```
100
+
101
+ ## Manifest configuration
102
+
103
+ Your `manifest.yaml` should define:
104
+
105
+ ```yaml
106
+ schema_version: "0.1"
107
+ entrypoint: "vaults.strategies.my_strategy.strategy.MyStrategy"
108
+ permissions:
109
+ policy: "(wallet.id == 'FORMAT_WALLET_ID') && (eth.tx.to == '0x...')"
110
+ adapters:
111
+ - name: "BALANCE"
112
+ capabilities: ["wallet_read", "wallet_transfer"]
113
+ - name: "POOL"
114
+ capabilities: ["pool.read", "pool.analytics"]
115
+ ```
116
+
117
+ ## Testing
118
+
119
+ `test_strategy.py` should cover at least deposit/update/status. Use `pytest.mark.asyncio` and patch adapters or services as needed.
120
+
121
+ ```python
122
+ import pytest
123
+ from wayfinder_paths.vaults.strategies.my_strategy.strategy import MyStrategy
124
+
125
+
126
+ @pytest.mark.asyncio
127
+ async def test_status_shape():
128
+ strat = MyStrategy(config={})
129
+ status = await strat._status()
130
+ assert set(status) == {"portfolio_value", "net_deposit", "strategy_status"}
131
+ ```
132
+
133
+ ## Running the strategy locally
134
+
135
+ ```bash
136
+ # Install dependencies & create wallets first
137
+ poetry install
138
+ poetry run python wayfinder_paths/scripts/make_wallets.py --default --vault
139
+
140
+ # Copy config and edit credentials
141
+ cp wayfinder_paths/config.example.json config.json
142
+
143
+ # Run your strategy
144
+ poetry run python wayfinder_paths/run_strategy.py my_strategy --action status --config $(pwd)/config.json
145
+ ```
146
+
147
+ ## Best practices
148
+
149
+ - Return `(success: bool, message: str)` tuples from `deposit`/`update`.
150
+ - Always populate `portfolio_value`, `net_deposit`, and `strategy_status` keys in `_status`.
151
+ - Use the adapters declared in `manifest.yaml`—they are injected via `register_adapters`.
152
+ - Keep on-chain permissions tight by scoping the manifest policy to the strategy wallet and expected contracts.
@@ -0,0 +1,11 @@
1
+ {
2
+ "deposit": {
3
+ "main_token_amount": 100,
4
+ "gas_token_amount": 0.001
5
+ },
6
+ "update": {},
7
+ "status": {},
8
+ "withdraw": {}
9
+ }
10
+
11
+
@@ -0,0 +1,8 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "vaults.strategies.my_strategy.strategy.MyStrategy"
3
+ name: "my_strategy" # Unique name for this strategy instance (used for wallet lookup)
4
+ permissions:
5
+ policy: "(wallet.id == 'FORMAT_WALLET_ID')"
6
+ adapters:
7
+ - name: "BALANCE"
8
+ capabilities: ["wallet_read"]
@@ -0,0 +1,57 @@
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
+ """Optional initialization logic."""
14
+ return None
15
+
16
+ async def deposit(
17
+ self, main_token_amount: float, gas_token_amount: float
18
+ ) -> 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
+ return (True, "Deposit successful")
26
+
27
+ 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
+ return await super().withdraw(amount=amount)
41
+
42
+ async def update(self) -> StatusTuple:
43
+ """Rebalance or update positions."""
44
+ return (True, "Update successful")
45
+
46
+ async def _status(self) -> StatusDict:
47
+ """Report strategy status."""
48
+ return {
49
+ "portfolio_value": 0.0,
50
+ "net_deposit": 0.0,
51
+ "strategy_status": {},
52
+ }
53
+
54
+ @staticmethod
55
+ def policies() -> list[str]:
56
+ """Return policy strings used to scope on-chain permissions."""
57
+ return []
@@ -0,0 +1,197 @@
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 vaults/strategies/your_strategy/ -v
11
+ """
12
+
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ # TODO: Replace MyStrategy with your actual strategy class name
17
+ from wayfinder_paths.vaults.strategies.your_strategy.strategy import (
18
+ MyStrategy, # noqa: E402
19
+ )
20
+
21
+ # Ensure wayfinder-paths is on path for tests.test_utils import
22
+ # This is a workaround until conftest loading order is resolved
23
+ _wayfinder_path_dir = Path(__file__).parent.parent.parent.parent.resolve()
24
+ _wayfinder_path_str = str(_wayfinder_path_dir)
25
+ if _wayfinder_path_str not in sys.path:
26
+ sys.path.insert(0, _wayfinder_path_str)
27
+ elif sys.path.index(_wayfinder_path_str) > 0:
28
+ # Move to front to take precedence
29
+ sys.path.remove(_wayfinder_path_str)
30
+ sys.path.insert(0, _wayfinder_path_str)
31
+
32
+ import pytest # noqa: E402
33
+
34
+ # Import test utilities
35
+ try:
36
+ from tests.test_utils import get_canonical_examples, load_strategy_examples
37
+ except ImportError:
38
+ # Fallback if path setup didn't work
39
+ import importlib.util
40
+
41
+ test_utils_path = Path(_wayfinder_path_dir) / "tests" / "test_utils.py"
42
+ spec = importlib.util.spec_from_file_location("tests.test_utils", test_utils_path)
43
+ test_utils = importlib.util.module_from_spec(spec)
44
+ spec.loader.exec_module(test_utils)
45
+ get_canonical_examples = test_utils.get_canonical_examples
46
+ load_strategy_examples = test_utils.load_strategy_examples
47
+
48
+
49
+ @pytest.fixture
50
+ def strategy():
51
+ """Create a strategy instance for testing with minimal config."""
52
+ mock_config = {
53
+ "main_wallet": {"address": "0x1234567890123456789012345678901234567890"},
54
+ "vault_wallet": {"address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"},
55
+ }
56
+
57
+ s = MyStrategy(
58
+ config=mock_config,
59
+ main_wallet=mock_config["main_wallet"],
60
+ vault_wallet=mock_config["vault_wallet"],
61
+ simulation=True,
62
+ )
63
+
64
+ # TODO: Add mocking for your adapters here if needed
65
+ # Example for balance_adapter:
66
+ # if hasattr(s, "balance_adapter") and s.balance_adapter:
67
+ # usdc_balance_mock = AsyncMock(return_value=(True, 60000000))
68
+ # gas_balance_mock = AsyncMock(return_value=(True, 2000000000000000))
69
+ #
70
+ # def get_balance_side_effect(token_id, wallet_address, **kwargs):
71
+ # if token_id == "usd-coin-base" or token_id == "usd-coin":
72
+ # return usdc_balance_mock.return_value
73
+ # elif token_id == "ethereum-base" or token_id == "ethereum":
74
+ # return gas_balance_mock.return_value
75
+ # return (True, 1000000000)
76
+ #
77
+ # s.balance_adapter.get_token_balance_for_wallet = AsyncMock(
78
+ # side_effect=get_balance_side_effect
79
+ # )
80
+ # s.balance_adapter.get_all_enriched_token_balances_for_wallet = AsyncMock(
81
+ # return_value=(True, {"balances": []})
82
+ # )
83
+
84
+ # Example for token_adapter:
85
+ # if hasattr(s, "token_adapter") and s.token_adapter:
86
+ # default_token = {
87
+ # "id": "usd-coin-base",
88
+ # "symbol": "USDC",
89
+ # "name": "USD Coin",
90
+ # "decimals": 6,
91
+ # "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
92
+ # "chain": {"code": "base", "id": 8453, "name": "Base"},
93
+ # }
94
+ # s.token_adapter.get_token = AsyncMock(return_value=(True, default_token))
95
+ # s.token_adapter.get_gas_token = AsyncMock(return_value=(True, default_token))
96
+
97
+ # Example for transaction adapters:
98
+ # if hasattr(s, "tx_adapter") and s.tx_adapter:
99
+ # s.tx_adapter.move_from_main_wallet_to_vault_wallet = AsyncMock(
100
+ # return_value=(True, "Transfer successful (simulated)")
101
+ # )
102
+ # s.tx_adapter.move_from_vault_wallet_to_main_wallet = AsyncMock(
103
+ # return_value=(True, "Transfer successful (simulated)")
104
+ # )
105
+
106
+ # Example for ledger_adapter:
107
+ # if hasattr(s, "ledger_adapter") and s.ledger_adapter:
108
+ # s.ledger_adapter.get_vault_net_deposit = AsyncMock(
109
+ # return_value=(True, {"net_deposit": 0})
110
+ # )
111
+ # s.ledger_adapter.get_vault_transactions = AsyncMock(
112
+ # return_value=(True, {"transactions": []})
113
+ # )
114
+
115
+ return s
116
+
117
+
118
+ @pytest.mark.asyncio
119
+ @pytest.mark.smoke
120
+ async def test_smoke(strategy):
121
+ """REQUIRED: Basic smoke test - verifies strategy lifecycle."""
122
+ examples = load_strategy_examples(Path(__file__))
123
+ smoke_data = examples["smoke"]
124
+
125
+ st = await strategy.status()
126
+ assert isinstance(st, dict)
127
+ assert "portfolio_value" in st or "net_deposit" in st or "strategy_status" in st
128
+
129
+ deposit_params = smoke_data.get("deposit", {})
130
+ ok, msg = await strategy.deposit(**deposit_params)
131
+ assert isinstance(ok, bool)
132
+ assert isinstance(msg, str)
133
+
134
+ ok, msg = await strategy.update(**smoke_data.get("update", {}))
135
+ assert isinstance(ok, bool)
136
+
137
+ ok, msg = await strategy.withdraw(**smoke_data.get("withdraw", {}))
138
+ assert isinstance(ok, bool)
139
+
140
+
141
+ @pytest.mark.asyncio
142
+ async def test_canonical_usage(strategy):
143
+ """REQUIRED: Test canonical usage examples from examples.json (minimum).
144
+
145
+ Canonical usage = all positive usage examples (excluding error cases).
146
+ This is the MINIMUM requirement - feel free to add more test cases here.
147
+ """
148
+ examples = load_strategy_examples(Path(__file__))
149
+ canonical = get_canonical_examples(examples)
150
+
151
+ for example_name, example_data in canonical.items():
152
+ if "deposit" in example_data:
153
+ deposit_params = example_data.get("deposit", {})
154
+ ok, _ = await strategy.deposit(**deposit_params)
155
+ assert ok, f"Canonical example '{example_name}' deposit failed"
156
+
157
+ if "update" in example_data:
158
+ ok, msg = await strategy.update()
159
+ assert ok, f"Canonical example '{example_name}' update failed: {msg}"
160
+
161
+ if "status" in example_data:
162
+ st = await strategy.status()
163
+ assert isinstance(st, dict), (
164
+ f"Canonical example '{example_name}' status failed"
165
+ )
166
+
167
+
168
+ @pytest.mark.asyncio
169
+ async def test_error_cases(strategy):
170
+ """OPTIONAL: Test error scenarios from examples.json."""
171
+ examples = load_strategy_examples(Path(__file__))
172
+
173
+ for example_name, example_data in examples.items():
174
+ if isinstance(example_data, dict) and "expect" in example_data:
175
+ expect = example_data.get("expect", {})
176
+
177
+ if "deposit" in example_data:
178
+ deposit_params = example_data.get("deposit", {})
179
+ ok, _ = await strategy.deposit(**deposit_params)
180
+
181
+ if expect.get("success") is False:
182
+ assert ok is False, (
183
+ f"Expected {example_name} deposit to fail but it succeeded"
184
+ )
185
+ elif expect.get("success") is True:
186
+ assert ok is True, (
187
+ f"Expected {example_name} deposit to succeed but it failed"
188
+ )
189
+
190
+ if "update" in example_data:
191
+ ok, _ = await strategy.update()
192
+ if "success" in expect:
193
+ expected_success = expect.get("success")
194
+ assert ok == expected_success, (
195
+ f"Expected {example_name} update to "
196
+ f"{'succeed' if expected_success else 'fail'} but got opposite"
197
+ )
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Wayfinder
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.