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,88 +1,93 @@
|
|
|
1
1
|
# Stablecoin Yield Strategy
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
- Examples: `examples.json`
|
|
5
|
-
- Tests: `test_strategy.py`
|
|
3
|
+
Automated USDC yield optimization on Base chain.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
- **Module**: `wayfinder_paths.strategies.stablecoin_yield_strategy.strategy.StablecoinYieldStrategy`
|
|
6
|
+
- **Chain**: Base (8453)
|
|
7
|
+
- **Token**: USDC
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Overview
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
This strategy actively manages USDC deposits by:
|
|
12
|
+
1. Transferring USDC (plus ETH gas buffer) from main wallet to strategy wallet
|
|
13
|
+
2. Searching Base-native pools for the best USD-denominated APY
|
|
14
|
+
3. Monitoring DeFi Llama feeds and Wayfinder pool analytics
|
|
15
|
+
4. Rebalancing to higher-yield pools when APY improvements exceed thresholds
|
|
16
|
+
5. Respecting rotation cooldowns to avoid excessive churn
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
## Key Parameters
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
| Parameter | Value | Description |
|
|
21
|
+
|-----------|-------|-------------|
|
|
22
|
+
| `MIN_AMOUNT_USDC` | 2 | Minimum deposit amount |
|
|
23
|
+
| `MIN_TVL` | 1,000,000 | Minimum pool TVL |
|
|
24
|
+
| `ROTATION_MIN_INTERVAL` | 14 days | Cooldown between rotations |
|
|
25
|
+
| `DUST_APY` | 0.01 (1%) | APY threshold below which pools are ignored |
|
|
26
|
+
| `SEARCH_DEPTH` | 10 | Number of pools to examine |
|
|
27
|
+
| `MIN_GAS` | 0.001 ETH | Minimum gas buffer |
|
|
28
|
+
| `GAS_MAXIMUM` | 0.02 ETH | Maximum gas per deposit |
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
- `MIN_TVL = 1_000_000` → pools below $1M TVL are ignored.
|
|
23
|
-
- `ROTATION_MIN_INTERVAL = 14 days` → once rotated, the strategy waits ~2 weeks unless the new candidate dramatically outperforms.
|
|
24
|
-
- `DUST_APY = 0.01` (1%) → pools below this APY are treated as dust.
|
|
25
|
-
- `SEARCH_DEPTH = 10` → how many pools to examine when selecting candidates.
|
|
26
|
-
- `MIN_GAS = 0.001` and `GAS_MAXIMUM = 0.02` Base ETH → minimum buffer required in the strategy wallet plus the upper bound accepted per deposit.
|
|
30
|
+
## Adapters Used
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- `TokenAdapter` for metadata (gas token, USDC info).
|
|
34
|
-
- `LedgerAdapter` for net-deposit tracking and cooldown enforcement.
|
|
32
|
+
- **BalanceAdapter**: Wallet/pool balances, cross-wallet transfers
|
|
33
|
+
- **PoolAdapter**: Pool metadata, yield analytics
|
|
34
|
+
- **BRAPAdapter**: Swap quotes and execution
|
|
35
|
+
- **TokenAdapter**: Token metadata (gas token, USDC info)
|
|
36
|
+
- **LedgerAdapter**: Net deposit tracking, cooldown enforcement
|
|
35
37
|
|
|
36
38
|
## Actions
|
|
37
39
|
|
|
38
40
|
### Deposit
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
```bash
|
|
43
|
+
poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy \
|
|
44
|
+
--action deposit --main-token-amount 60 --gas-token-amount 0.001 --config config.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- Validates `main_token_amount >= MIN_AMOUNT_USDC`
|
|
48
|
+
- Validates `gas_token_amount <= GAS_MAXIMUM`
|
|
49
|
+
- Transfers ETH and USDC to strategy wallet
|
|
50
|
+
- Initializes position tracking
|
|
44
51
|
|
|
45
52
|
### Update
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
```bash
|
|
55
|
+
poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy \
|
|
56
|
+
--action update --config config.json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- Fetches current balances and active pool
|
|
60
|
+
- Runs `_find_best_pool()` to score candidate pools
|
|
61
|
+
- Checks rotation cooldown via LedgerAdapter
|
|
62
|
+
- Executes rotation if APY improvement threshold met
|
|
63
|
+
- Sweeps idle balances into target token
|
|
52
64
|
|
|
53
65
|
### Status
|
|
54
66
|
|
|
55
|
-
|
|
67
|
+
```bash
|
|
68
|
+
poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy \
|
|
69
|
+
--action status --config config.json
|
|
70
|
+
```
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
- `
|
|
59
|
-
- `
|
|
72
|
+
Returns:
|
|
73
|
+
- `portfolio_value`: Current pool balance
|
|
74
|
+
- `net_deposit`: From LedgerAdapter
|
|
75
|
+
- `strategy_status`: Active pool, APY, wallet balances
|
|
60
76
|
|
|
61
77
|
### Withdraw
|
|
62
78
|
|
|
63
|
-
- Requires a prior deposit (the strategy tracks `self.DEPOSIT_USDC`).
|
|
64
|
-
- Reads the pool balance via `BalanceAdapter.get_balance` (with pool address and chain_id), unwinds via BRAP swaps back to USDC, and moves USDC from the strategy wallet to the main wallet via `BalanceAdapter.move_from_strategy_wallet_to_main_wallet`.
|
|
65
|
-
- Updates the ledger and clears cached pool state.
|
|
66
|
-
|
|
67
|
-
## Running locally
|
|
68
|
-
|
|
69
79
|
```bash
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# Generate main wallet (writes config.json)
|
|
74
|
-
# Creates a main wallet (or use 'just create-strategy' which auto-creates wallets)
|
|
75
|
-
poetry run python wayfinder_paths/scripts/make_wallets.py -n 1
|
|
80
|
+
poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy \
|
|
81
|
+
--action withdraw --config config.json
|
|
82
|
+
```
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
- Unwinds current position via BRAP swaps
|
|
85
|
+
- Converts all holdings back to USDC
|
|
86
|
+
- Transfers USDC to main wallet
|
|
87
|
+
- Clears cached pool state
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy --action status --config $(pwd)/config.json
|
|
89
|
+
## Testing
|
|
82
90
|
|
|
83
|
-
|
|
84
|
-
poetry run
|
|
85
|
-
poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy --action update --config $(pwd)/config.json
|
|
91
|
+
```bash
|
|
92
|
+
poetry run pytest wayfinder_paths/strategies/stablecoin_yield_strategy/ -v
|
|
86
93
|
```
|
|
87
|
-
|
|
88
|
-
Wallet addresses are auto-populated from `config.json` when you run `wayfinder_paths/scripts/make_wallets.py`. Set `NETWORK=testnet` in `config.json` to dry-run operations against mocked services.
|
|
@@ -32,12 +32,12 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
32
32
|
MINIMUM_DAYS_UNTIL_PROFIT = 7
|
|
33
33
|
MIN_TVL = 1_000_000
|
|
34
34
|
DUST_APY = 0.01
|
|
35
|
-
MIN_GAS = 10e-4
|
|
35
|
+
MIN_GAS = 10e-4
|
|
36
36
|
SEARCH_DEPTH = 10
|
|
37
37
|
SUPPORTED_NETWORK_CODES = {"base"}
|
|
38
38
|
ROTATION_MIN_INTERVAL = timedelta(days=14)
|
|
39
39
|
MINIMUM_APY_IMPROVEMENT = 0.01
|
|
40
|
-
GAS_MAXIMUM = 10e-4
|
|
40
|
+
GAS_MAXIMUM = 10e-4
|
|
41
41
|
GAS_SAFETY_FRACTION = 1 / 3
|
|
42
42
|
|
|
43
43
|
INFO = StratDescriptor(
|
|
@@ -226,7 +226,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
226
226
|
pass
|
|
227
227
|
|
|
228
228
|
def _get_strategy_wallet_address(self) -> str:
|
|
229
|
-
"""Get strategy wallet address with validation."""
|
|
230
229
|
strategy_wallet = self.config.get("strategy_wallet")
|
|
231
230
|
if not strategy_wallet or not isinstance(strategy_wallet, dict):
|
|
232
231
|
raise ValueError("strategy_wallet not configured in strategy config")
|
|
@@ -236,7 +235,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
236
235
|
return str(address)
|
|
237
236
|
|
|
238
237
|
def _get_main_wallet_address(self) -> str:
|
|
239
|
-
"""Get main wallet address with validation."""
|
|
240
238
|
main_wallet = self.config.get("main_wallet")
|
|
241
239
|
if not main_wallet or not isinstance(main_wallet, dict):
|
|
242
240
|
raise ValueError("main_wallet not configured in strategy config")
|
|
@@ -246,21 +244,18 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
246
244
|
return str(address)
|
|
247
245
|
|
|
248
246
|
def _track_token(self, token_id: str, balance_wei: int = 0):
|
|
249
|
-
"""Track a token that the strategy holds or might hold."""
|
|
250
247
|
if token_id:
|
|
251
248
|
self.tracked_token_ids.add(token_id)
|
|
252
249
|
if balance_wei > 0:
|
|
253
250
|
self.tracked_balances[token_id] = balance_wei
|
|
254
251
|
|
|
255
252
|
def _update_balance(self, token_id: str, balance_wei: int):
|
|
256
|
-
"""Update the tracked balance for a token."""
|
|
257
253
|
if token_id:
|
|
258
254
|
self.tracked_balances[token_id] = balance_wei
|
|
259
255
|
if balance_wei > 0:
|
|
260
256
|
self.tracked_token_ids.add(token_id)
|
|
261
257
|
|
|
262
258
|
async def _refresh_tracked_balances(self):
|
|
263
|
-
"""Refresh balances for all tracked tokens from on-chain data."""
|
|
264
259
|
strategy_address = self._get_strategy_wallet_address()
|
|
265
260
|
for token_id in self.tracked_token_ids:
|
|
266
261
|
try:
|
|
@@ -277,7 +272,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
277
272
|
self.tracked_balances[token_id] = 0
|
|
278
273
|
|
|
279
274
|
def _get_non_zero_tracked_tokens(self) -> list[tuple[str, int]]:
|
|
280
|
-
"""Get list of (token_id, balance_wei) for tokens with non-zero balances."""
|
|
281
275
|
return [
|
|
282
276
|
(token_id, balance)
|
|
283
277
|
for token_id, balance in self.tracked_balances.items()
|
|
@@ -291,7 +285,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
291
285
|
await super().setup()
|
|
292
286
|
self.current_combined_apy_pct = 0.0
|
|
293
287
|
|
|
294
|
-
# Get strategy net deposit
|
|
295
288
|
try:
|
|
296
289
|
logger.info("Fetching strategy net deposit from ledger")
|
|
297
290
|
strategy_address = self._get_strategy_wallet_address()
|
|
@@ -308,7 +301,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
308
301
|
logger.error(f"Failed to fetch strategy net deposit: {e}")
|
|
309
302
|
self.DEPOSIT_USDC = 0
|
|
310
303
|
|
|
311
|
-
# Get USDC token info
|
|
312
304
|
try:
|
|
313
305
|
logger.info("Fetching USDC token information")
|
|
314
306
|
success, self.usdc_token_info = await self.token_adapter.get_token(
|
|
@@ -340,11 +332,10 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
340
332
|
|
|
341
333
|
self.current_pool_data = None
|
|
342
334
|
|
|
343
|
-
chain_code = "base"
|
|
335
|
+
chain_code = "base"
|
|
344
336
|
if self.current_pool and self.current_pool.get("chain"):
|
|
345
337
|
chain_code = self.current_pool.get("chain").get("code", "base")
|
|
346
338
|
|
|
347
|
-
# Get gas token info
|
|
348
339
|
try:
|
|
349
340
|
logger.info(f"Fetching gas token for chain: {chain_code}")
|
|
350
341
|
success, gas_token_data = await self.token_adapter.get_gas_token(chain_code)
|
|
@@ -368,7 +359,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
368
359
|
self.current_pool_balance = 0
|
|
369
360
|
return
|
|
370
361
|
|
|
371
|
-
# Get strategy transactions to determine current position and build tracked token set
|
|
372
362
|
try:
|
|
373
363
|
logger.info("Fetching strategy transaction history to build state")
|
|
374
364
|
success, txns_data = await self.ledger_adapter.get_strategy_transactions(
|
|
@@ -382,7 +372,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
382
372
|
]
|
|
383
373
|
logger.info(f"Found {len(txns)} non-deposit transactions")
|
|
384
374
|
|
|
385
|
-
# Build tracked token set from transaction history
|
|
386
375
|
for txn in txns:
|
|
387
376
|
op_data = txn.get("data", {}).get("op_data", {})
|
|
388
377
|
# Track any token that was swapped TO
|
|
@@ -585,7 +574,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
585
574
|
return total_usd
|
|
586
575
|
|
|
587
576
|
async def _infer_active_pool_from_tracked_tokens(self):
|
|
588
|
-
"""Infer the active pool from tracked tokens with non-zero balances."""
|
|
589
577
|
try:
|
|
590
578
|
# Refresh balances for tracked tokens
|
|
591
579
|
await self._refresh_tracked_balances()
|
|
@@ -613,12 +601,10 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
613
601
|
if not best_token_id:
|
|
614
602
|
return None
|
|
615
603
|
|
|
616
|
-
# Fetch token info
|
|
617
604
|
success, token = await self.token_adapter.get_token(best_token_id)
|
|
618
605
|
if not success:
|
|
619
606
|
return None
|
|
620
607
|
|
|
621
|
-
# Get fresh on-chain balance
|
|
622
608
|
strategy_address = self._get_strategy_wallet_address()
|
|
623
609
|
try:
|
|
624
610
|
success, onchain_balance = await self.balance_adapter.get_balance(
|
|
@@ -640,11 +626,9 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
640
626
|
return None
|
|
641
627
|
|
|
642
628
|
def _is_gas_balance_entry(self, balance: dict[str, Any]) -> bool:
|
|
643
|
-
"""Check if a balance entry represents a gas token."""
|
|
644
629
|
if not self.gas_token:
|
|
645
630
|
return False
|
|
646
631
|
|
|
647
|
-
# Check by token ID
|
|
648
632
|
token_id = balance.get("token_id")
|
|
649
633
|
if (
|
|
650
634
|
isinstance(token_id, str)
|
|
@@ -652,13 +636,11 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
652
636
|
):
|
|
653
637
|
return True
|
|
654
638
|
|
|
655
|
-
# Check by token address and network
|
|
656
639
|
token_address = balance.get("tokenAddress")
|
|
657
640
|
if isinstance(token_address, str):
|
|
658
641
|
if token_address.lower() == self.gas_token.get("address", "").lower():
|
|
659
642
|
return True
|
|
660
643
|
|
|
661
|
-
# Check address + network combination
|
|
662
644
|
network = (balance.get("network") or "").lower()
|
|
663
645
|
chain_code = self.current_pool.get("chain", {}).get("code", "").lower()
|
|
664
646
|
if (
|
|
@@ -698,7 +680,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
698
680
|
f"Current pool set to: {token_info.get('symbol')} on {token_info.get('chain', {}).get('name')}"
|
|
699
681
|
)
|
|
700
682
|
|
|
701
|
-
# Check main wallet USDC balance if depositing main token
|
|
702
683
|
if main_token_amount > 0:
|
|
703
684
|
logger.info("Checking main wallet USDC balance")
|
|
704
685
|
(
|
|
@@ -735,7 +716,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
735
716
|
f"Minimum deposit is {self.MIN_AMOUNT_USDC} USDC on Base. Received: {main_token_amount}",
|
|
736
717
|
)
|
|
737
718
|
|
|
738
|
-
# Check gas token amount if provided
|
|
739
719
|
if gas_token_amount > 0:
|
|
740
720
|
if gas_token_amount > self.GAS_MAXIMUM:
|
|
741
721
|
return (
|
|
@@ -766,7 +746,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
766
746
|
f"Main wallet {gas_symbol} balance is less than the deposit amount: {main_gas_native} < {gas_token_amount}",
|
|
767
747
|
)
|
|
768
748
|
|
|
769
|
-
# Check gas balances for minimum requirement (only if depositing main token)
|
|
770
749
|
if main_token_amount > 0:
|
|
771
750
|
logger.info("Checking gas token balances for operations")
|
|
772
751
|
gas_decimals = self.gas_token.get("decimals")
|
|
@@ -838,7 +817,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
838
817
|
return (False, f"USDC transfer to strategy failed: {msg}")
|
|
839
818
|
logger.info("USDC transfer completed successfully")
|
|
840
819
|
|
|
841
|
-
# Update tracked state
|
|
842
820
|
self._track_token(self.usdc_token_info.get("token_id"))
|
|
843
821
|
self._update_balance(
|
|
844
822
|
self.usdc_token_info.get("token_id"),
|
|
@@ -847,7 +825,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
847
825
|
|
|
848
826
|
# Transfer gas if provided or if strategy needs top-up
|
|
849
827
|
if gas_token_amount > 0:
|
|
850
|
-
# Get gas symbol if not already defined
|
|
851
828
|
if main_token_amount == 0:
|
|
852
829
|
gas_symbol = self.gas_token.get("symbol")
|
|
853
830
|
|
|
@@ -902,7 +879,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
902
879
|
False,
|
|
903
880
|
"Nothing to withdraw from strategy, wallet should be empty already. If not, an error has happened please manually remove funds",
|
|
904
881
|
)
|
|
905
|
-
# Get current pool balance
|
|
906
882
|
logger.info("Fetching current pool balance")
|
|
907
883
|
try:
|
|
908
884
|
(
|
|
@@ -918,7 +894,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
918
894
|
logger.error(f"Failed to fetch pool balance: {e}")
|
|
919
895
|
self.current_pool_balance = 0
|
|
920
896
|
|
|
921
|
-
# Check if we need to swap out of current position
|
|
922
897
|
if (
|
|
923
898
|
self.current_pool.get("token_id") != self.usdc_token_info.get("token_id")
|
|
924
899
|
and self.current_pool_balance
|
|
@@ -951,7 +926,7 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
951
926
|
break
|
|
952
927
|
except Exception as e:
|
|
953
928
|
logger.warning(f"Quote attempt {attempt + 1} failed: {e}")
|
|
954
|
-
if attempt == 3:
|
|
929
|
+
if attempt == 3:
|
|
955
930
|
logger.error("All quote attempts failed")
|
|
956
931
|
|
|
957
932
|
best_quote = quotes.get("best_quote") if isinstance(quotes, dict) else None
|
|
@@ -1006,7 +981,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1006
981
|
|
|
1007
982
|
await self._sweep_wallet(self.usdc_token_info)
|
|
1008
983
|
|
|
1009
|
-
# Get final USDC balance in strategy wallet
|
|
1010
984
|
status, raw_balance = await self.balance_adapter.get_balance(
|
|
1011
985
|
query=self.usdc_token_info.get("token_id"),
|
|
1012
986
|
wallet_address=self._get_strategy_wallet_address(),
|
|
@@ -1017,7 +991,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1017
991
|
"decimals"
|
|
1018
992
|
)
|
|
1019
993
|
|
|
1020
|
-
# Get gas balance in strategy wallet
|
|
1021
994
|
gas_amount = 0.0
|
|
1022
995
|
if self.gas_token:
|
|
1023
996
|
status, raw_gas = await self.balance_adapter.get_balance(
|
|
@@ -1047,7 +1020,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1047
1020
|
)
|
|
1048
1021
|
|
|
1049
1022
|
async def exit(self, **kwargs) -> StatusTuple:
|
|
1050
|
-
"""Transfer funds from strategy wallet to main wallet."""
|
|
1051
1023
|
logger.info("EXIT: Transferring remaining funds to main wallet")
|
|
1052
1024
|
|
|
1053
1025
|
strategy_address = self._get_strategy_wallet_address()
|
|
@@ -1299,7 +1271,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1299
1271
|
pass
|
|
1300
1272
|
|
|
1301
1273
|
async def _sweep_wallet(self, target_token):
|
|
1302
|
-
"""Sweep all tracked non-target tokens into the target token."""
|
|
1303
1274
|
# Refresh tracked balances
|
|
1304
1275
|
await self._refresh_tracked_balances()
|
|
1305
1276
|
|
|
@@ -1322,7 +1293,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1322
1293
|
if token_id == target_token_id:
|
|
1323
1294
|
continue
|
|
1324
1295
|
|
|
1325
|
-
# Get fresh balance to ensure accuracy
|
|
1326
1296
|
try:
|
|
1327
1297
|
success, fresh_balance = await self.balance_adapter.get_balance(
|
|
1328
1298
|
query=token_id,
|
|
@@ -1351,7 +1321,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1351
1321
|
strategy_name=self.name,
|
|
1352
1322
|
)
|
|
1353
1323
|
if success:
|
|
1354
|
-
# Update tracked state: source token now has 0 balance
|
|
1355
1324
|
self._update_balance(token_id, 0)
|
|
1356
1325
|
logger.info(f"Successfully swept {token_id} to {target_token_id}")
|
|
1357
1326
|
else:
|
|
@@ -1454,7 +1423,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1454
1423
|
)
|
|
1455
1424
|
|
|
1456
1425
|
async def _get_non_gas_balances(self) -> list[dict[str, Any]]:
|
|
1457
|
-
"""Get non-gas balances from tracked tokens."""
|
|
1458
1426
|
# Refresh tracked balances
|
|
1459
1427
|
await self._refresh_tracked_balances()
|
|
1460
1428
|
|
|
@@ -1470,7 +1438,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1470
1438
|
if balance_wei <= 0:
|
|
1471
1439
|
continue
|
|
1472
1440
|
|
|
1473
|
-
# Fetch token info to get address and chain
|
|
1474
1441
|
try:
|
|
1475
1442
|
success, token_info = await self.token_adapter.get_token(token_id)
|
|
1476
1443
|
if not success or not token_info:
|
|
@@ -1634,7 +1601,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1634
1601
|
return float(gas_price) * float(amount) / (10 ** token.get("decimals"))
|
|
1635
1602
|
|
|
1636
1603
|
async def _status(self) -> StatusDict:
|
|
1637
|
-
# Get ETH gas balance
|
|
1638
1604
|
gas_success, gas_balance_wei = await self.balance_adapter.get_balance(
|
|
1639
1605
|
query=self.gas_token.get("token_id"),
|
|
1640
1606
|
wallet_address=self._get_strategy_wallet_address(),
|
|
@@ -1663,7 +1629,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1663
1629
|
# Refresh tracked balances
|
|
1664
1630
|
await self._refresh_tracked_balances()
|
|
1665
1631
|
|
|
1666
|
-
# Calculate total value from tracked non-gas balances
|
|
1667
1632
|
total_value = 0.0
|
|
1668
1633
|
gas_token_id = self.gas_token.get("token_id") if self.gas_token else None
|
|
1669
1634
|
|
|
@@ -1674,7 +1639,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1674
1639
|
continue
|
|
1675
1640
|
|
|
1676
1641
|
try:
|
|
1677
|
-
# Get token price to calculate USD value
|
|
1678
1642
|
success, price_data = await self.token_adapter.get_token_price(token_id)
|
|
1679
1643
|
if not success:
|
|
1680
1644
|
continue
|
|
@@ -1728,7 +1692,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1728
1692
|
return [f"({wallet_id}) && (({approve_enso}) || ({swap_enso})) "]
|
|
1729
1693
|
|
|
1730
1694
|
async def partial_liquidate(self, usd_value: float) -> StatusTuple:
|
|
1731
|
-
"""Liquidate strategy assets to reach target USD value in USDC."""
|
|
1732
1695
|
# Refresh tracked balances
|
|
1733
1696
|
await self._refresh_tracked_balances()
|
|
1734
1697
|
|
|
@@ -1736,7 +1699,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1736
1699
|
usdc_decimals = self.usdc_token_info.get("decimals")
|
|
1737
1700
|
gas_token_id = self.gas_token.get("token_id") if self.gas_token else None
|
|
1738
1701
|
|
|
1739
|
-
# Check current USDC balance
|
|
1740
1702
|
available_usdc_wei = self.tracked_balances.get(usdc_token_id, 0)
|
|
1741
1703
|
available_usdc_usd = float(available_usdc_wei) / (10**usdc_decimals)
|
|
1742
1704
|
|
|
@@ -1757,7 +1719,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1757
1719
|
if balance_wei <= 0:
|
|
1758
1720
|
continue
|
|
1759
1721
|
|
|
1760
|
-
# Get token info and price
|
|
1761
1722
|
try:
|
|
1762
1723
|
success, token_info = await self.token_adapter.get_token(token_id)
|
|
1763
1724
|
if not success:
|
|
@@ -1789,7 +1750,6 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1789
1750
|
if success:
|
|
1790
1751
|
swapped_usd = (amount_to_swap / (10**decimals)) * price
|
|
1791
1752
|
available_usdc_usd += swapped_usd
|
|
1792
|
-
# Update tracked state
|
|
1793
1753
|
self._update_balance(token_id, balance_wei - amount_to_swap)
|
|
1794
1754
|
else:
|
|
1795
1755
|
logger.warning(f"Failed to liquidate {token_id}: {msg}")
|
|
@@ -15,7 +15,6 @@ elif sys.path.index(_wayfinder_path_str) > 0:
|
|
|
15
15
|
|
|
16
16
|
import pytest # noqa: E402
|
|
17
17
|
|
|
18
|
-
# Import test utilities
|
|
19
18
|
try:
|
|
20
19
|
from tests.test_utils import get_canonical_examples, load_strategy_examples
|
|
21
20
|
except ImportError:
|
|
@@ -36,7 +35,6 @@ from wayfinder_paths.strategies.stablecoin_yield_strategy.strategy import ( # n
|
|
|
36
35
|
|
|
37
36
|
@pytest.fixture
|
|
38
37
|
def strategy():
|
|
39
|
-
"""Create a strategy instance for testing with minimal config."""
|
|
40
38
|
mock_config = {
|
|
41
39
|
"main_wallet": {"address": "0x1234567890123456789012345678901234567890"},
|
|
42
40
|
"strategy_wallet": {"address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"},
|
|
@@ -260,7 +258,6 @@ def strategy():
|
|
|
260
258
|
@pytest.mark.asyncio
|
|
261
259
|
@pytest.mark.smoke
|
|
262
260
|
async def test_smoke(strategy):
|
|
263
|
-
"""REQUIRED: Basic smoke test - verifies strategy lifecycle."""
|
|
264
261
|
examples = load_strategy_examples(Path(__file__))
|
|
265
262
|
smoke_data = examples["smoke"]
|
|
266
263
|
|
|
@@ -282,11 +279,6 @@ async def test_smoke(strategy):
|
|
|
282
279
|
|
|
283
280
|
@pytest.mark.asyncio
|
|
284
281
|
async def test_canonical_usage(strategy):
|
|
285
|
-
"""REQUIRED: Test canonical usage examples from examples.json (minimum).
|
|
286
|
-
|
|
287
|
-
Canonical usage = all positive usage examples (excluding error cases).
|
|
288
|
-
This is the MINIMUM requirement - feel free to add more test cases here.
|
|
289
|
-
"""
|
|
290
282
|
examples = load_strategy_examples(Path(__file__))
|
|
291
283
|
canonical = get_canonical_examples(examples)
|
|
292
284
|
|
|
@@ -309,7 +301,6 @@ async def test_canonical_usage(strategy):
|
|
|
309
301
|
|
|
310
302
|
@pytest.mark.asyncio
|
|
311
303
|
async def test_error_cases(strategy):
|
|
312
|
-
"""OPTIONAL: Test error scenarios from examples.json."""
|
|
313
304
|
examples = load_strategy_examples(Path(__file__))
|
|
314
305
|
|
|
315
306
|
for example_name, example_data in examples.items():
|
|
@@ -341,7 +332,6 @@ async def test_error_cases(strategy):
|
|
|
341
332
|
|
|
342
333
|
@pytest.mark.asyncio
|
|
343
334
|
async def test_token_tracking_initialization(strategy):
|
|
344
|
-
"""Test that tracked_token_ids and tracked_balances are initialized."""
|
|
345
335
|
assert hasattr(strategy, "tracked_token_ids")
|
|
346
336
|
assert hasattr(strategy, "tracked_balances")
|
|
347
337
|
assert isinstance(strategy.tracked_token_ids, set)
|
|
@@ -350,7 +340,6 @@ async def test_token_tracking_initialization(strategy):
|
|
|
350
340
|
|
|
351
341
|
@pytest.mark.asyncio
|
|
352
342
|
async def test_track_token(strategy):
|
|
353
|
-
"""Test that _track_token adds tokens to tracked state."""
|
|
354
343
|
test_token_id = "test-token-base"
|
|
355
344
|
test_balance = 1000000
|
|
356
345
|
|
|
@@ -362,7 +351,6 @@ async def test_track_token(strategy):
|
|
|
362
351
|
|
|
363
352
|
@pytest.mark.asyncio
|
|
364
353
|
async def test_update_balance(strategy):
|
|
365
|
-
"""Test that _update_balance updates tracked balances."""
|
|
366
354
|
test_token_id = "test-token-base"
|
|
367
355
|
initial_balance = 1000000
|
|
368
356
|
updated_balance = 2000000
|
|
@@ -376,7 +364,6 @@ async def test_update_balance(strategy):
|
|
|
376
364
|
|
|
377
365
|
@pytest.mark.asyncio
|
|
378
366
|
async def test_get_non_zero_tracked_tokens(strategy):
|
|
379
|
-
"""Test that _get_non_zero_tracked_tokens returns only non-zero balances."""
|
|
380
367
|
strategy._track_token("token-1", 1000000)
|
|
381
368
|
strategy._track_token("token-2", 0)
|
|
382
369
|
strategy._track_token("token-3", 5000000)
|
|
@@ -392,7 +379,6 @@ async def test_get_non_zero_tracked_tokens(strategy):
|
|
|
392
379
|
|
|
393
380
|
@pytest.mark.asyncio
|
|
394
381
|
async def test_refresh_tracked_balances(strategy):
|
|
395
|
-
"""Test that _refresh_tracked_balances updates all tracked token balances."""
|
|
396
382
|
# Track some tokens
|
|
397
383
|
strategy._track_token("usd-coin-base")
|
|
398
384
|
strategy._track_token("ethereum-base")
|
|
@@ -407,7 +393,6 @@ async def test_refresh_tracked_balances(strategy):
|
|
|
407
393
|
|
|
408
394
|
@pytest.mark.asyncio
|
|
409
395
|
async def test_deposit_tracks_usdc(strategy):
|
|
410
|
-
"""Test that deposit operation tracks USDC token."""
|
|
411
396
|
# Clear tracked state
|
|
412
397
|
strategy.tracked_token_ids.clear()
|
|
413
398
|
strategy.tracked_balances.clear()
|
|
@@ -423,8 +408,6 @@ async def test_deposit_tracks_usdc(strategy):
|
|
|
423
408
|
|
|
424
409
|
@pytest.mark.asyncio
|
|
425
410
|
async def test_sweep_wallet_uses_tracked_tokens(strategy):
|
|
426
|
-
"""Test that _sweep_wallet only swaps tracked tokens."""
|
|
427
|
-
# Import the real implementation to restore it
|
|
428
411
|
from wayfinder_paths.strategies.stablecoin_yield_strategy.strategy import (
|
|
429
412
|
StablecoinYieldStrategy,
|
|
430
413
|
)
|
|
@@ -434,7 +417,6 @@ async def test_sweep_wallet_uses_tracked_tokens(strategy):
|
|
|
434
417
|
strategy, StablecoinYieldStrategy
|
|
435
418
|
)
|
|
436
419
|
|
|
437
|
-
# Setup: track some tokens with balances
|
|
438
420
|
strategy._track_token("token-1", 1000000)
|
|
439
421
|
strategy._track_token("token-2", 2000000)
|
|
440
422
|
|
|
@@ -447,10 +429,8 @@ async def test_sweep_wallet_uses_tracked_tokens(strategy):
|
|
|
447
429
|
async def get_balance_mock(query, **kwargs):
|
|
448
430
|
token_id = query if isinstance(query, str) else (query or {}).get("token_id")
|
|
449
431
|
balance = strategy.tracked_balances.get(token_id, 0)
|
|
450
|
-
# Return the balance, ensuring it's an int
|
|
451
432
|
return (True, int(balance) if balance else 0)
|
|
452
433
|
|
|
453
|
-
# Create a new AsyncMock that will track calls
|
|
454
434
|
new_mock = AsyncMock(side_effect=get_balance_mock)
|
|
455
435
|
strategy.balance_adapter.get_balance = new_mock
|
|
456
436
|
|
|
@@ -465,7 +445,6 @@ async def test_sweep_wallet_uses_tracked_tokens(strategy):
|
|
|
465
445
|
"chain": {"code": "base", "name": "Base"},
|
|
466
446
|
}
|
|
467
447
|
|
|
468
|
-
# Call sweep
|
|
469
448
|
await strategy._sweep_wallet(target_token)
|
|
470
449
|
|
|
471
450
|
# Verify that swap was called for tracked tokens (should be called twice, once for each token)
|
|
@@ -482,8 +461,6 @@ async def test_sweep_wallet_uses_tracked_tokens(strategy):
|
|
|
482
461
|
|
|
483
462
|
@pytest.mark.asyncio
|
|
484
463
|
async def test_get_non_gas_balances_uses_tracked_state(strategy):
|
|
485
|
-
"""Test that _get_non_gas_balances only checks tracked tokens."""
|
|
486
|
-
# Setup tracked tokens
|
|
487
464
|
usdc_token_id = "usd-coin-base"
|
|
488
465
|
pool_token_id = "test-pool-base"
|
|
489
466
|
|
|
@@ -497,7 +474,6 @@ async def test_get_non_gas_balances_uses_tracked_state(strategy):
|
|
|
497
474
|
|
|
498
475
|
strategy.balance_adapter.get_balance = AsyncMock(side_effect=_get_balance_effect)
|
|
499
476
|
|
|
500
|
-
# Get non-gas balances
|
|
501
477
|
balances = await strategy._get_non_gas_balances()
|
|
502
478
|
|
|
503
479
|
# Verify only tracked tokens are returned (excluding gas)
|
|
@@ -508,10 +484,8 @@ async def test_get_non_gas_balances_uses_tracked_state(strategy):
|
|
|
508
484
|
|
|
509
485
|
@pytest.mark.asyncio
|
|
510
486
|
async def test_partial_liquidate_uses_tracked_tokens(strategy):
|
|
511
|
-
""
|
|
512
|
-
|
|
513
|
-
strategy._track_token("usd-coin-base", 50000000) # 50 USDC
|
|
514
|
-
strategy._track_token("test-pool-base", 100000000000000000000) # 100 POOL tokens
|
|
487
|
+
strategy._track_token("usd-coin-base", 50000000)
|
|
488
|
+
strategy._track_token("test-pool-base", 100000000000000000000)
|
|
515
489
|
|
|
516
490
|
# Mock balance and token adapters
|
|
517
491
|
def _get_balance_effect_partial(query, **kwargs):
|
|
@@ -530,7 +504,6 @@ async def test_partial_liquidate_uses_tracked_tokens(strategy):
|
|
|
530
504
|
return_value=(True, "Swap successful")
|
|
531
505
|
)
|
|
532
506
|
|
|
533
|
-
# Call partial liquidate
|
|
534
507
|
ok, msg = await strategy.partial_liquidate(usd_value=75.0)
|
|
535
508
|
|
|
536
509
|
# Verify success
|
|
@@ -540,12 +513,6 @@ async def test_partial_liquidate_uses_tracked_tokens(strategy):
|
|
|
540
513
|
|
|
541
514
|
@pytest.mark.asyncio
|
|
542
515
|
async def test_setup_handles_float_net_deposit(strategy):
|
|
543
|
-
"""Test that setup() correctly handles float from get_strategy_net_deposit.
|
|
544
|
-
|
|
545
|
-
The ledger adapter returns (success, float) not (success, dict).
|
|
546
|
-
This test ensures the strategy doesn't try to call .get() on the float,
|
|
547
|
-
which would raise "'float' object has no attribute 'get'".
|
|
548
|
-
"""
|
|
549
516
|
# Mock get_strategy_net_deposit to return float (not dict)
|
|
550
517
|
strategy.ledger_adapter.get_strategy_net_deposit = AsyncMock(
|
|
551
518
|
return_value=(True, 1500.0)
|