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.

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 +1 -22
  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 +1 -148
  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 +1 -10
  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 +10 -122
  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/evm_helpers.py +0 -50
  65. wayfinder_paths/core/utils/{erc20_service.py → tokens.py} +25 -21
  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.21.dist-info/METADATA +355 -0
  93. wayfinder_paths-0.1.21.dist-info/RECORD +129 -0
  94. {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.21.dist-info}/WHEEL +1 -1
  95. wayfinder_paths/core/adapters/base.py +0 -5
  96. wayfinder_paths-0.1.19.dist-info/METADATA +0 -592
  97. wayfinder_paths-0.1.19.dist-info/RECORD +0 -130
  98. {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.21.dist-info}/LICENSE +0 -0
@@ -1,5 +1,3 @@
1
- """Tests for BasisTradingStrategy."""
2
-
3
1
  import json
4
2
  from pathlib import Path
5
3
  from unittest.mock import AsyncMock, MagicMock, patch
@@ -12,21 +10,17 @@ from wayfinder_paths.tests.test_utils import load_strategy_examples
12
10
 
13
11
 
14
12
  def load_examples():
15
- """Load test examples from examples.json using shared utility."""
16
13
  return load_strategy_examples(Path(__file__))
17
14
 
18
15
 
19
16
  class TestBasisTradingStrategy:
20
- """Tests for BasisTradingStrategy."""
21
-
22
17
  @pytest.fixture
23
18
  def mock_hyperliquid_adapter(self):
24
- """Create mock HyperliquidAdapter."""
25
19
  mock = MagicMock()
26
20
  # Provide enough points to satisfy the strategy's lookback checks without making tests too slow.
27
21
  n_points = 1200
28
22
  start_ms = 1700000000000
29
- step_ms = 3600 * 1000 # 1h
23
+ step_ms = 3600 * 1000
30
24
  mock.get_meta_and_asset_ctxs = AsyncMock(
31
25
  return_value=(
32
26
  True,
@@ -93,8 +87,8 @@ class TestBasisTradingStrategy:
93
87
  True,
94
88
  {
95
89
  "levels": [
96
- [{"px": "1999", "sz": "100", "n": 10}], # bids
97
- [{"px": "2001", "sz": "100", "n": 10}], # asks
90
+ [{"px": "1999", "sz": "100", "n": 10}],
91
+ [{"px": "2001", "sz": "100", "n": 10}],
98
92
  ],
99
93
  "midPx": "2000",
100
94
  },
@@ -120,7 +114,7 @@ class TestBasisTradingStrategy:
120
114
  True,
121
115
  {
122
116
  "marginSummary": {"accountValue": "0"},
123
- "withdrawable": "100.0", # Top-level withdrawable for withdraw() method
117
+ "withdrawable": "100.0",
124
118
  "assetPositions": [],
125
119
  },
126
120
  )
@@ -139,9 +133,7 @@ class TestBasisTradingStrategy:
139
133
  mock.get_open_orders = AsyncMock(return_value=(True, []))
140
134
  mock.get_frontend_open_orders = AsyncMock(return_value=(True, []))
141
135
  mock.get_valid_order_size = MagicMock(side_effect=lambda _asset, size: size)
142
- mock.wait_for_deposit = AsyncMock(
143
- return_value=(True, 100.0) # (deposit_confirmed, final_balance)
144
- )
136
+ mock.wait_for_deposit = AsyncMock(return_value=(True, 100.0))
145
137
  mock.wait_for_withdrawal = AsyncMock(
146
138
  # tx_hash -> amount (float)
147
139
  return_value=(True, {"0x123456": 100.0})
@@ -150,13 +142,11 @@ class TestBasisTradingStrategy:
150
142
 
151
143
  @pytest.fixture
152
144
  def ledger_adapter(self, tmp_path):
153
- """Create real LedgerAdapter with temp directory."""
154
145
  ledger_client = LedgerClient(ledger_dir=tmp_path)
155
146
  return LedgerAdapter(ledger_client=ledger_client)
156
147
 
157
148
  @pytest.fixture
158
149
  def strategy(self, mock_hyperliquid_adapter, ledger_adapter):
159
- """Create strategy with mocked market adapters but real ledger."""
160
150
  with patch(
161
151
  "wayfinder_paths.strategies.basis_trading_strategy.strategy.HyperliquidAdapter",
162
152
  return_value=mock_hyperliquid_adapter,
@@ -222,7 +212,6 @@ class TestBasisTradingStrategy:
222
212
  @pytest.mark.asyncio
223
213
  @pytest.mark.smoke
224
214
  async def test_smoke(self, strategy):
225
- """Smoke test: deposit → update → status → withdraw lifecycle."""
226
215
  examples = load_examples()
227
216
  smoke = examples["smoke"]
228
217
 
@@ -241,7 +230,6 @@ class TestBasisTradingStrategy:
241
230
  success, msg = await strategy.deposit(**deposit_params)
242
231
  assert success, f"Deposit failed: {msg}"
243
232
 
244
- # Update
245
233
  success, msg = await strategy.update()
246
234
  assert success, f"Update failed: {msg}"
247
235
 
@@ -255,7 +243,6 @@ class TestBasisTradingStrategy:
255
243
 
256
244
  @pytest.mark.asyncio
257
245
  async def test_deposit_minimum(self, strategy):
258
- """Test minimum deposit validation."""
259
246
  examples = load_examples()
260
247
  min_fail = examples.get("min_deposit_fail", {})
261
248
 
@@ -269,7 +256,6 @@ class TestBasisTradingStrategy:
269
256
 
270
257
  @pytest.mark.asyncio
271
258
  async def test_update_without_deposit(self, strategy, mock_hyperliquid_adapter):
272
- """Test update fails without deposit."""
273
259
  strategy.deposit_amount = 0.0
274
260
 
275
261
  # No USDC in perp withdrawable or spot.
@@ -293,13 +279,11 @@ class TestBasisTradingStrategy:
293
279
 
294
280
  @pytest.mark.asyncio
295
281
  async def test_withdraw_without_deposit(self, strategy):
296
- """Test withdraw fails without deposit."""
297
282
  success, msg = await strategy.withdraw()
298
283
  assert success is False
299
284
 
300
285
  @pytest.mark.asyncio
301
286
  async def test_status(self, strategy):
302
- """Test status returns expected fields."""
303
287
  status = await strategy.status()
304
288
  assert "portfolio_value" in status
305
289
  assert "net_deposit" in status
@@ -307,8 +291,6 @@ class TestBasisTradingStrategy:
307
291
 
308
292
  @pytest.mark.asyncio
309
293
  async def test_ledger_records_snapshot(self, strategy, tmp_path):
310
- """Test that status() records a snapshot to the ledger."""
311
- # Get status (should record snapshot)
312
294
  status = await strategy.status()
313
295
  assert status is not None
314
296
 
@@ -325,30 +307,27 @@ class TestBasisTradingStrategy:
325
307
  assert snapshot["portfolio_value"] == status["portfolio_value"]
326
308
 
327
309
  def test_maintenance_rate(self):
328
- """Test maintenance rate calculation."""
329
310
  from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
330
311
  BasisTradingStrategy,
331
312
  )
332
313
 
333
314
  rate = BasisTradingStrategy.maintenance_rate_from_max_leverage(50)
334
- assert rate == 0.01 # 0.5 / 50
315
+ assert rate == 0.01
335
316
 
336
317
  rate = BasisTradingStrategy.maintenance_rate_from_max_leverage(10)
337
- assert rate == 0.05 # 0.5 / 10
318
+ assert rate == 0.05
338
319
 
339
320
  def test_rolling_min_sum(self, strategy):
340
- """Test rolling minimum sum calculation."""
341
321
  arr = [1, -2, 3, -4, 5]
342
322
  result = strategy._rolling_min_sum(arr, 2)
343
- assert result == -1 # min of [1-2, -2+3, 3-4, -4+5] = [-1, 1, -1, 1]
323
+ assert result == -1
344
324
 
345
325
  def test_z_from_conf(self, strategy):
346
- """Test z-score calculation."""
347
326
  z = strategy._z_from_conf(0.95)
348
- assert 1.9 < z < 2.0 # ~1.96 for 95% two-sided confidence
327
+ assert 1.9 < z < 2.0
349
328
 
350
329
  z = strategy._z_from_conf(0.99)
351
- assert 2.5 < z < 2.6 # ~2.576 for 99% two-sided confidence
330
+ assert 2.5 < z < 2.6
352
331
 
353
332
  @pytest.mark.asyncio
354
333
  async def test_build_batch_snapshot_and_filter(self, strategy):
@@ -373,7 +352,6 @@ class TestBasisTradingStrategy:
373
352
  async def test_get_undeployed_capital_empty(
374
353
  self, strategy, mock_hyperliquid_adapter
375
354
  ):
376
- """Test _get_undeployed_capital with no capital."""
377
355
  mock_hyperliquid_adapter.get_user_state = AsyncMock(
378
356
  return_value=(
379
357
  True,
@@ -395,7 +373,6 @@ class TestBasisTradingStrategy:
395
373
  async def test_get_undeployed_capital_with_margin(
396
374
  self, strategy, mock_hyperliquid_adapter
397
375
  ):
398
- """Test _get_undeployed_capital with withdrawable margin."""
399
376
  mock_hyperliquid_adapter.get_user_state = AsyncMock(
400
377
  return_value=(
401
378
  True,
@@ -418,7 +395,6 @@ class TestBasisTradingStrategy:
418
395
 
419
396
  @pytest.mark.asyncio
420
397
  async def test_scale_up_position_no_position(self, strategy):
421
- """Test _scale_up_position fails without existing position."""
422
398
  success, msg = await strategy._scale_up_position(100.0)
423
399
  assert success is False
424
400
  assert "No position to scale up" in msg
@@ -427,7 +403,6 @@ class TestBasisTradingStrategy:
427
403
  async def test_scale_up_position_below_minimum(
428
404
  self, strategy, mock_hyperliquid_adapter
429
405
  ):
430
- """Test _scale_up_position rejects below minimum notional."""
431
406
  from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
432
407
  BasisPosition,
433
408
  )
@@ -447,14 +422,13 @@ class TestBasisTradingStrategy:
447
422
  # Try to scale with $5 (below $10 minimum notional)
448
423
  # With 2x leverage, order_usd = 5 * (2/3) = 3.33, below $10
449
424
  success, msg = await strategy._scale_up_position(5.0)
450
- assert success # Returns success=True but with message
425
+ assert success
451
426
  assert "below minimum notional" in msg
452
427
 
453
428
  @pytest.mark.asyncio
454
429
  async def test_update_with_idle_capital_scales_up(
455
430
  self, strategy, mock_hyperliquid_adapter
456
431
  ):
457
- """Test update() calls _scale_up_position when idle capital exists."""
458
432
  from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
459
433
  BasisPosition,
460
434
  )
@@ -486,7 +460,7 @@ class TestBasisTradingStrategy:
486
460
  "marginSummary": {
487
461
  "accountValue": "120",
488
462
  "withdrawable": "12",
489
- "totalNtlPos": "112", # Deployed capital
463
+ "totalNtlPos": "112",
490
464
  },
491
465
  "assetPositions": [
492
466
  {
@@ -555,7 +529,6 @@ class TestBasisTradingStrategy:
555
529
  async def test_ensure_builder_fee_approved_already_approved(
556
530
  self, mock_hyperliquid_adapter, ledger_adapter
557
531
  ):
558
- """Test ensure_builder_fee_approved when already approved."""
559
532
  with patch(
560
533
  "wayfinder_paths.strategies.basis_trading_strategy.strategy.HyperliquidAdapter",
561
534
  return_value=mock_hyperliquid_adapter,
@@ -588,7 +561,7 @@ class TestBasisTradingStrategy:
588
561
  return_value=(
589
562
  True,
590
563
  30,
591
- ) # Already approved for 30 tenths bp
564
+ )
592
565
  )
593
566
  mock_hyperliquid_adapter.approve_builder_fee = AsyncMock(
594
567
  return_value=(True, {"status": "ok"})
@@ -604,7 +577,6 @@ class TestBasisTradingStrategy:
604
577
  async def test_ensure_builder_fee_approved_needs_approval(
605
578
  self, mock_hyperliquid_adapter, ledger_adapter
606
579
  ):
607
- """Test ensure_builder_fee_approved when approval is needed."""
608
580
  with patch(
609
581
  "wayfinder_paths.strategies.basis_trading_strategy.strategy.HyperliquidAdapter",
610
582
  return_value=mock_hyperliquid_adapter,
@@ -634,7 +606,7 @@ class TestBasisTradingStrategy:
634
606
 
635
607
  # Mock get_max_builder_fee returning insufficient approval
636
608
  mock_hyperliquid_adapter.get_max_builder_fee = AsyncMock(
637
- return_value=(True, 0) # Not approved yet
609
+ return_value=(True, 0)
638
610
  )
639
611
  mock_hyperliquid_adapter.approve_builder_fee = AsyncMock(
640
612
  return_value=(True, {"status": "ok"})
@@ -650,7 +622,6 @@ class TestBasisTradingStrategy:
650
622
  async def test_portfolio_value_includes_spot_holdings(
651
623
  self, strategy, mock_hyperliquid_adapter
652
624
  ):
653
- """Portfolio value should include non-USDC spot holdings."""
654
625
  # Perp account has $100
655
626
  mock_hyperliquid_adapter.get_user_state = AsyncMock(
656
627
  return_value=(
@@ -678,11 +649,10 @@ class TestBasisTradingStrategy:
678
649
  total, hl_value, vault_value = await strategy._get_total_portfolio_value()
679
650
  # 100 (perp) + 50 (USDC) + 0.5*2000 (ETH) = 1150
680
651
  assert hl_value == 1150.0
681
- assert total == 1150.0 # No vault balance
652
+ assert total == 1150.0
682
653
 
683
654
  @pytest.mark.asyncio
684
655
  async def test_portfolio_value_usdc_only(self, strategy, mock_hyperliquid_adapter):
685
- """Portfolio value with only USDC spot balance."""
686
656
  mock_hyperliquid_adapter.get_user_state = AsyncMock(
687
657
  return_value=(
688
658
  True,
@@ -704,7 +674,6 @@ class TestBasisTradingStrategy:
704
674
 
705
675
  @pytest.mark.asyncio
706
676
  async def test_withdraw_detects_spot_usdc(self, strategy, mock_hyperliquid_adapter):
707
- """Withdraw should detect funds in spot USDC (not perp margin)."""
708
677
  # Perp is empty
709
678
  mock_hyperliquid_adapter.get_user_state = AsyncMock(
710
679
  return_value=(
@@ -732,7 +701,6 @@ class TestBasisTradingStrategy:
732
701
  async def test_update_detects_hl_balance_when_deposit_zero(
733
702
  self, strategy, mock_hyperliquid_adapter
734
703
  ):
735
- """Update should detect Hyperliquid balance when deposit_amount is 0."""
736
704
  strategy.deposit_amount = 0
737
705
 
738
706
  # Hyperliquid has $50 in perp account
@@ -756,11 +724,6 @@ class TestBasisTradingStrategy:
756
724
  async def test_update_spot_usdc_only_rebalances_before_open(
757
725
  self, strategy, mock_hyperliquid_adapter
758
726
  ):
759
- """
760
- If funds are mostly in spot USDC (e.g., after liquidation), update() should:
761
- - detect deposit from spot+perp USDC
762
- - transfer spot->perp to reach the target split before opening.
763
- """
764
727
  strategy.deposit_amount = 0.0
765
728
  strategy.current_position = None
766
729
 
@@ -829,7 +792,6 @@ class TestBasisTradingStrategy:
829
792
  async def test_update_near_liquidation_closes_and_redeploys(
830
793
  self, strategy, mock_hyperliquid_adapter
831
794
  ):
832
- """Near-liquidation should trigger an emergency close+redeploy (bypasses cooldown)."""
833
795
  from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
834
796
  BasisPosition,
835
797
  )
@@ -895,18 +857,11 @@ class TestBasisTradingStrategy:
895
857
 
896
858
  @pytest.mark.asyncio
897
859
  async def test_net_deposit_handles_float_return(self, strategy):
898
- """Test that strategy correctly handles float from get_strategy_net_deposit.
899
-
900
- The ledger adapter returns (success, float) not (success, dict).
901
- This test ensures the strategy doesn't try to call .get() on the float,
902
- which would raise "'float' object has no attribute 'get'".
903
- """
904
860
  # Mock ledger adapter to return a float (not a dict)
905
861
  strategy.ledger_adapter.get_strategy_net_deposit = AsyncMock(
906
862
  return_value=(True, 1500.0)
907
863
  )
908
864
 
909
- # Call status() which internally uses get_strategy_net_deposit
910
865
  status = await strategy.status()
911
866
 
912
867
  # Verify net_deposit is correctly set from the float
@@ -916,10 +871,6 @@ class TestBasisTradingStrategy:
916
871
  async def test_setup_handles_float_net_deposit(
917
872
  self, mock_hyperliquid_adapter, ledger_adapter
918
873
  ):
919
- """Test that setup() correctly handles float from get_strategy_net_deposit.
920
-
921
- This catches if code is changed to expect a dict with .get('net_deposit').
922
- """
923
874
  with patch(
924
875
  "wayfinder_paths.strategies.basis_trading_strategy.strategy.HyperliquidAdapter",
925
876
  return_value=mock_hyperliquid_adapter,
@@ -6,8 +6,6 @@ from typing import Any
6
6
 
7
7
  @dataclass
8
8
  class BasisCandidate:
9
- """Represents a potential basis trading opportunity."""
10
-
11
9
  coin: str
12
10
  spot_pair: str
13
11
  spot_asset_id: int
@@ -26,8 +24,6 @@ class BasisCandidate:
26
24
 
27
25
  @dataclass
28
26
  class BasisPosition:
29
- """Tracks an active basis position."""
30
-
31
27
  coin: str
32
28
  spot_asset_id: int
33
29
  perp_asset_id: int
@@ -1,84 +1,93 @@
1
- # Hyperlend Stable Yield Strategy
1
+ # HyperLend Stable Yield Strategy
2
2
 
3
- - Entrypoint: `strategies.hyperlend_stable_yield_strategy.strategy.HyperlendStableYieldStrategy`
4
- - Examples: `examples.json`
5
- - Tests: `test_strategy.py`
3
+ Stablecoin yield optimization on HyperLend (HyperEVM).
6
4
 
7
- ## What it does
5
+ - **Module**: `wayfinder_paths.strategies.hyperlend_stable_yield_strategy.strategy.HyperlendStableYieldStrategy`
6
+ - **Chain**: HyperEVM
7
+ - **Token**: USDT0
8
8
 
9
- Allocates USDT0 on HyperEVM across HyperLend stablecoin markets. The strategy:
9
+ ## Overview
10
10
 
11
- 1. Pulls USDT0 (plus a configurable HYPE gas buffer) from the main wallet into the strategy wallet.
12
- 2. Samples HyperLend hourly rate history, applies a bootstrap tournament (horizon = 6h, blocks = 6h, 4,000 trials, 7-day half-life) to estimate which stablecoin should outperform.
13
- 3. Tops up the small HYPE gas buffer if needed, swaps USDT0 into the target stablecoin, and supplies it to HyperLend.
14
- 4. Enforces a hysteresis rotation policy so minor APY noise does not churn capital.
11
+ This strategy allocates USDT0 across HyperLend stablecoin markets by:
12
+ 1. Transferring USDT0 (plus HYPE gas buffer) from main wallet to strategy wallet
13
+ 2. Sampling HyperLend hourly rate history
14
+ 3. Running bootstrap tournament analysis to identify best-performing stablecoin
15
+ 4. Swapping and supplying to HyperLend
16
+ 5. Enforcing hysteresis rotation policy to prevent excessive churn
15
17
 
16
- ## Key parameters
18
+ ## Key Parameters
17
19
 
18
- - `MIN_USDT0_DEPOSIT_AMOUNT = 1`
19
- - `GAS_MAXIMUM = 0.1` HYPE (max accepted per deposit)
20
- - `HORIZON_HOURS = 6`, `BLOCK_LEN = 6`, `TRIALS = 4000`
21
- - `HYSTERESIS_DWELL_HOURS = 168`, `HYSTERESIS_Z = 1.15`
22
- - `ROTATION_COOLDOWN = 168 hours`
23
- - `APY_REBALANCE_THRESHOLD = 0.0035` (35 bps edge required to rotate when not short-circuiting)
24
- - `MIN_STABLE_SWAP_TOKENS = 1e-3` dust threshold when sweeping balances
20
+ | Parameter | Value | Description |
21
+ |-----------|-------|-------------|
22
+ | `MIN_USDT0_DEPOSIT_AMOUNT` | 1 | Minimum deposit amount |
23
+ | `GAS_MAXIMUM` | 0.1 HYPE | Maximum gas per deposit |
24
+ | `HORIZON_HOURS` | 6 | Analysis horizon |
25
+ | `TRIALS` | 4000 | Bootstrap simulation trials |
26
+ | `HYSTERESIS_DWELL_HOURS` | 168 | Rotation cooldown |
27
+ | `HYSTERESIS_Z` | 1.15 | APY improvement threshold |
28
+ | `ROTATION_COOLDOWN` | 168 hours | Minimum time between rotations |
29
+ | `APY_REBALANCE_THRESHOLD` | 0.0035 | 35 bps edge required to rotate |
25
30
 
26
- ## Adapters used
31
+ ## Adapters Used
27
32
 
28
- - `BalanceAdapter` for token/pool balances and orchestrating wallet transfers with ledger tracking.
29
- - `TokenAdapter` for metadata (USDT0, HYPE, wrapping info).
30
- - `LedgerAdapter` for net deposit + rotation history.
31
- - `BRAPAdapter` to source quotes/swap stablecoins.
32
- - `HyperlendAdapter` for asset views, lend/withdraw ops, supply caps.
33
+ - **BalanceAdapter**: Token/pool balances, wallet transfers
34
+ - **TokenAdapter**: Token metadata (USDT0, HYPE)
35
+ - **LedgerAdapter**: Net deposit, rotation history
36
+ - **BRAPAdapter**: Swap quotes and execution
37
+ - **HyperlendAdapter**: Asset views, lend/withdraw operations
33
38
 
34
39
  ## Actions
35
40
 
36
41
  ### Deposit
37
42
 
38
- - Validates USDT0 and HYPE balances in the main wallet.
39
- - Transfers HYPE into the strategy wallet when a top-up is required, ensuring the strategy maintains the configured buffer.
40
- - Moves USDT0 from the main wallet into the strategy wallet through `BalanceAdapter.move_from_main_wallet_to_strategy_wallet`.
41
- - Clears cached asset snapshots so the next update starts from on-chain reality.
43
+ ```bash
44
+ poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy \
45
+ --action deposit --main-token-amount 25 --gas-token-amount 0.02 --config config.json
46
+ ```
47
+
48
+ - Validates USDT0 and HYPE balances in main wallet
49
+ - Transfers HYPE for gas buffer
50
+ - Moves USDT0 to strategy wallet
51
+ - Clears cached asset snapshots
42
52
 
43
53
  ### Update
44
54
 
45
- - Refreshes HyperLend asset snapshots, calculates tournament winners, and filters markets that respect supply caps + buffer requirements.
46
- - Reads rotation history through `LedgerAdapter.get_strategy_latest_transactions` to enforce the cooldown (unless the short-circuit policy is triggered).
47
- - If a new asset wins the tournament and passes hysteresis checks, BRAP quotes are fetched and executed to rotate into the better performer.
48
- - Sweeps residual stable balances, lends via `HyperlendAdapter`, and records ledger operations.
55
+ ```bash
56
+ poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy \
57
+ --action update --config config.json
58
+ ```
59
+
60
+ - Refreshes HyperLend asset snapshots
61
+ - Runs tournament analysis to find winner
62
+ - Enforces cooldown (unless short-circuit triggered)
63
+ - Executes rotation via BRAP if new asset wins
64
+ - Sweeps residual balances and lends via HyperlendAdapter
49
65
 
50
66
  ### Status
51
67
 
52
- `_status()` returns:
68
+ ```bash
69
+ poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy \
70
+ --action status --config config.json
71
+ ```
53
72
 
54
- - `portfolio_value`: active lend balance (converted to float),
55
- - `net_deposit`: fetched from `LedgerAdapter`,
56
- - `strategy_status`: includes current lent asset, APY, idle balances, and tournament projections.
73
+ Returns:
74
+ - `portfolio_value`: Active lend balance
75
+ - `net_deposit`: From LedgerAdapter
76
+ - `strategy_status`: Current asset, APY, balances, tournament projections
57
77
 
58
78
  ### Withdraw
59
79
 
60
- - Unwinds existing HyperLend positions, swaps back to USDT0 when necessary, returns USDT0 and residual HYPE to the main wallet via `BalanceAdapter`, and clears cached state.
61
-
62
- ## Running locally
63
-
64
80
  ```bash
65
- # Install dependencies
66
- poetry install
67
-
68
- # Generate main wallet (writes config.json)
69
- # Creates a main wallet (or use 'just create-strategy' which auto-creates wallets)
70
- poetry run python wayfinder_paths/scripts/make_wallets.py -n 1
81
+ poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy \
82
+ --action withdraw --config config.json
83
+ ```
71
84
 
72
- # Copy config and edit credentials
73
- cp wayfinder_paths/config.example.json config.json
85
+ - Unwinds HyperLend positions
86
+ - Swaps back to USDT0 if needed
87
+ - Returns USDT0 and residual HYPE to main wallet
74
88
 
75
- # Check status / health
76
- poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy --action status --config $(pwd)/config.json
89
+ ## Testing
77
90
 
78
- # Perform a deposit/update/withdraw cycle
79
- poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy --action deposit --main-token-amount 25 --gas-token-amount 0.02 --config $(pwd)/config.json
80
- poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy --action update --config $(pwd)/config.json
81
- poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy --action withdraw --config $(pwd)/config.json
91
+ ```bash
92
+ poetry run pytest wayfinder_paths/strategies/hyperlend_stable_yield_strategy/ -v
82
93
  ```
83
-
84
- Wallet addresses/labels are auto-resolved from `config.json`. Set `NETWORK=testnet` in your config to run the orchestration without touching live HyperEVM endpoints.
@@ -74,12 +74,12 @@ class HyperlendStableYieldStrategy(Strategy):
74
74
  ASSETS_SNAPSHOT_TTL_SECONDS = 20.0
75
75
  DEFAULT_LOOKBACK_HOURS = 24 * 7
76
76
  APY_REBALANCE_THRESHOLD = 0.0035
77
- TOURNAMENT_MODE = "joint" # "joint" or "independent"
77
+ TOURNAMENT_MODE = "joint"
78
78
  ROTATION_COOLDOWN = timedelta(hours=168)
79
79
  P_BEST_ROTATION_THRESHOLD = 0.4
80
80
  MAX_CANDIDATES = 5
81
81
  MIN_STABLE_SWAP_TOKENS = 1e-3
82
- MAX_GAS = 0.1 # hype float
82
+ MAX_GAS = 0.1
83
83
 
84
84
  INFO = StratDescriptor(
85
85
  description=f"""Multi-strategy allocator that converts USDT0 into the most consistently rewarding HyperLend stablecoin and continuously checks if a rotation is justified.
@@ -104,7 +104,7 @@ class HyperlendStableYieldStrategy(Strategy):
104
104
  gas_token_id="hyperliquid-hyperevm",
105
105
  deposit_token_id="usdt0-hyperevm",
106
106
  minimum_net_deposit=10,
107
- gas_maximum=MAX_GAS, # hype float
107
+ gas_maximum=MAX_GAS,
108
108
  gas_threshold=MAX_GAS / 3,
109
109
  # risk indicators
110
110
  volatility=Volatility.LOW,
@@ -218,7 +218,6 @@ class HyperlendStableYieldStrategy(Strategy):
218
218
  main_wallet_cfg = self.config.get("main_wallet")
219
219
  strategy_wallet_cfg = self.config.get("strategy_wallet")
220
220
 
221
- # Validate wallets are configured
222
221
  if not strategy_wallet_cfg or not strategy_wallet_cfg.get("address"):
223
222
  raise ValueError(
224
223
  "strategy_wallet not configured. Provide strategy_wallet address in config or ensure wallet is properly configured for your wallet provider"
@@ -428,8 +427,6 @@ class HyperlendStableYieldStrategy(Strategy):
428
427
  return total_tokens
429
428
 
430
429
  def _amount_to_wei(self, token: dict[str, Any], amount: Decimal) -> int:
431
- """Convert ``amount`` tokens into base units using existing helpers."""
432
-
433
430
  if amount <= 0:
434
431
  return 0
435
432
 
@@ -746,21 +743,6 @@ class HyperlendStableYieldStrategy(Strategy):
746
743
  def _get_token_address(
747
744
  self, token: dict[str, Any] | None, chain_code: str = "hyperevm"
748
745
  ) -> str | None:
749
- """
750
- Extract token address from various token data structures.
751
-
752
- Handles:
753
- 1. Top-level 'address' field (e.g., hype_token_info)
754
- 2. 'addresses' dict with chain_code key (e.g., addresses: {'hyperevm': '0x...'})
755
- 3. 'chain_addresses' dict with chain_code key (e.g., chain_addresses: {'hyperevm': {'address': '0x...'}})
756
-
757
- Args:
758
- token: Token dictionary with address information
759
- chain_code: Chain code to look up in nested structures (default: 'hyperevm')
760
-
761
- Returns:
762
- Token address string or None if not found
763
- """
764
746
  if not token:
765
747
  return None
766
748
 
@@ -910,7 +892,6 @@ class HyperlendStableYieldStrategy(Strategy):
910
892
  return (True, ". ".join(messages))
911
893
 
912
894
  async def exit(self, **kwargs) -> StatusTuple:
913
- """Transfer funds from strategy wallet to main wallet."""
914
895
  self.logger.info("EXIT: Transferring remaining funds to main wallet")
915
896
 
916
897
  strategy_address = self._get_strategy_wallet_address()
@@ -953,7 +934,7 @@ class HyperlendStableYieldStrategy(Strategy):
953
934
  )
954
935
  if hype_ok and hype_raw:
955
936
  hype_balance = float(hype_raw.get("balance", 0))
956
- tx_fee_reserve = 0.1 # Reserve 0.1 HYPE for tx fees
937
+ tx_fee_reserve = 0.1
957
938
  transferable_hype = hype_balance - tx_fee_reserve
958
939
  if transferable_hype > 0.01:
959
940
  self.logger.info(
@@ -1050,8 +1031,6 @@ class HyperlendStableYieldStrategy(Strategy):
1050
1031
  return actions
1051
1032
 
1052
1033
  async def update(self) -> StatusTuple:
1053
- """Rebalance or update positions."""
1054
-
1055
1034
  await self._hydrate_position_from_chain()
1056
1035
 
1057
1036
  redeploy_tokens = await self._estimate_redeploy_tokens()
@@ -2194,7 +2173,6 @@ class HyperlendStableYieldStrategy(Strategy):
2194
2173
 
2195
2174
  raw_balance_wei = asset.get("underlying_wallet_balance_wei")
2196
2175
  try:
2197
- # Handle both "decimals" (plural) and "decimal" (singular) from API
2198
2176
  token_decimals = token.get("decimals") or token.get("decimal")
2199
2177
  asset_decimals = asset.get("decimals") or asset.get("decimal")
2200
2178
  if token_decimals is not None:
@@ -2392,7 +2370,6 @@ class HyperlendStableYieldStrategy(Strategy):
2392
2370
 
2393
2371
  @staticmethod
2394
2372
  async def policies() -> list[str]:
2395
- """Return policy strings used to scope on-chain permissions."""
2396
2373
  return [
2397
2374
  any_hyperliquid_l1_payload(),
2398
2375
  any_hyperliquid_user_payload(),
@@ -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.hyperlend_stable_yield_strategy.strategy import
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"},
@@ -339,7 +337,6 @@ def strategy():
339
337
  @pytest.mark.asyncio
340
338
  @pytest.mark.smoke
341
339
  async def test_smoke(strategy):
342
- """REQUIRED: Basic smoke test - verifies strategy lifecycle."""
343
340
  examples = load_strategy_examples(Path(__file__))
344
341
  smoke_data = examples["smoke"]
345
342
 
@@ -355,7 +352,6 @@ async def test_smoke(strategy):
355
352
  assert isinstance(msg, str)
356
353
 
357
354
  result = await strategy.update(**smoke_data.get("update", {}))
358
- # update() returns (ok, msg, should_notify) or (ok, msg)
359
355
  ok = result[0]
360
356
  assert isinstance(ok, bool)
361
357
 
@@ -365,11 +361,6 @@ async def test_smoke(strategy):
365
361
 
366
362
  @pytest.mark.asyncio
367
363
  async def test_canonical_usage(strategy):
368
- """REQUIRED: Test canonical usage examples from examples.json (minimum).
369
-
370
- Canonical usage = all positive usage examples (excluding error cases).
371
- This is the MINIMUM requirement - feel free to add more test cases here.
372
- """
373
364
  examples = load_strategy_examples(Path(__file__))
374
365
  canonical = get_canonical_examples(examples)
375
366
 
@@ -394,7 +385,6 @@ async def test_canonical_usage(strategy):
394
385
 
395
386
  @pytest.mark.asyncio
396
387
  async def test_error_cases(strategy):
397
- """OPTIONAL: Test error scenarios from examples.json."""
398
388
  examples = load_strategy_examples(Path(__file__))
399
389
 
400
390
  for example_name, example_data in examples.items():