wayfinder-paths 0.1.11__py3-none-any.whl → 0.1.14__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 (66) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +13 -14
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +36 -39
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
  4. wayfinder_paths/adapters/brap_adapter/README.md +11 -16
  5. wayfinder_paths/adapters/brap_adapter/adapter.py +87 -75
  6. wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
  7. wayfinder_paths/adapters/brap_adapter/test_adapter.py +121 -59
  8. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +22 -23
  9. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +114 -60
  10. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1 -1
  11. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +44 -5
  12. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +104 -0
  13. wayfinder_paths/adapters/moonwell_adapter/adapter.py +0 -3
  14. wayfinder_paths/adapters/pool_adapter/README.md +11 -27
  15. wayfinder_paths/adapters/pool_adapter/adapter.py +11 -37
  16. wayfinder_paths/adapters/pool_adapter/examples.json +6 -7
  17. wayfinder_paths/adapters/pool_adapter/test_adapter.py +8 -8
  18. wayfinder_paths/adapters/token_adapter/README.md +2 -14
  19. wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
  20. wayfinder_paths/adapters/token_adapter/examples.json +4 -8
  21. wayfinder_paths/adapters/token_adapter/test_adapter.py +5 -3
  22. wayfinder_paths/core/clients/BRAPClient.py +103 -62
  23. wayfinder_paths/core/clients/ClientManager.py +1 -68
  24. wayfinder_paths/core/clients/HyperlendClient.py +127 -66
  25. wayfinder_paths/core/clients/LedgerClient.py +1 -4
  26. wayfinder_paths/core/clients/PoolClient.py +126 -88
  27. wayfinder_paths/core/clients/TokenClient.py +92 -37
  28. wayfinder_paths/core/clients/WalletClient.py +28 -58
  29. wayfinder_paths/core/clients/WayfinderClient.py +33 -166
  30. wayfinder_paths/core/clients/__init__.py +0 -2
  31. wayfinder_paths/core/clients/protocols.py +35 -52
  32. wayfinder_paths/core/clients/sdk_example.py +37 -22
  33. wayfinder_paths/core/config.py +60 -224
  34. wayfinder_paths/core/engine/StrategyJob.py +7 -55
  35. wayfinder_paths/core/services/local_evm_txn.py +28 -10
  36. wayfinder_paths/core/services/local_token_txn.py +1 -1
  37. wayfinder_paths/core/strategies/Strategy.py +3 -5
  38. wayfinder_paths/core/strategies/descriptors.py +7 -0
  39. wayfinder_paths/core/utils/evm_helpers.py +7 -3
  40. wayfinder_paths/core/utils/wallets.py +12 -19
  41. wayfinder_paths/core/wallets/README.md +1 -1
  42. wayfinder_paths/run_strategy.py +8 -17
  43. wayfinder_paths/scripts/create_strategy.py +5 -5
  44. wayfinder_paths/scripts/make_wallets.py +5 -5
  45. wayfinder_paths/scripts/run_strategy.py +3 -3
  46. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -1
  47. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +206 -526
  48. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +228 -11
  49. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
  50. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +41 -25
  51. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
  52. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +1 -1
  53. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +10 -9
  54. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +12 -6
  55. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +3 -3
  56. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +110 -78
  57. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +44 -21
  58. wayfinder_paths/templates/adapter/README.md +1 -1
  59. wayfinder_paths/templates/strategy/README.md +3 -3
  60. wayfinder_paths/templates/strategy/test_strategy.py +3 -2
  61. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/METADATA +21 -59
  62. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/RECORD +64 -65
  63. wayfinder_paths/core/clients/AuthClient.py +0 -83
  64. wayfinder_paths/core/settings.py +0 -61
  65. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/LICENSE +0 -0
  66. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/WHEEL +0 -0
@@ -282,8 +282,25 @@ class TestBasisTradingStrategy:
282
282
  assert success is False, "Expected deposit to fail"
283
283
 
284
284
  @pytest.mark.asyncio
285
- async def test_update_without_deposit(self, strategy):
285
+ async def test_update_without_deposit(self, strategy, mock_hyperliquid_adapter):
286
286
  """Test update fails without deposit."""
287
+ strategy.deposit_amount = 0.0
288
+
289
+ # No USDC in perp withdrawable or spot.
290
+ mock_hyperliquid_adapter.get_user_state = AsyncMock(
291
+ return_value=(
292
+ True,
293
+ {
294
+ "marginSummary": {"accountValue": "0"},
295
+ "withdrawable": "0",
296
+ "assetPositions": [],
297
+ },
298
+ )
299
+ )
300
+ mock_hyperliquid_adapter.get_spot_user_state = AsyncMock(
301
+ return_value=(True, {"balances": []})
302
+ )
303
+
287
304
  success, msg = await strategy.update()
288
305
  assert success is False
289
306
  assert "No deposit" in msg
@@ -347,15 +364,6 @@ class TestBasisTradingStrategy:
347
364
  z = strategy._z_from_conf(0.99)
348
365
  assert 2.5 < z < 2.6 # ~2.576 for 99% two-sided confidence
349
366
 
350
- def test_calculate_funding_stats(self, strategy):
351
- """Test funding statistics calculation."""
352
- hourly_funding = [0.0001, 0.0002, -0.0001, 0.0003, 0.0001]
353
- stats = strategy._calculate_funding_stats(hourly_funding)
354
-
355
- assert stats["points"] == 5
356
- assert stats["mean_hourly"] > 0
357
- assert stats["neg_hour_fraction"] == 0.2 # 1/5 negative
358
-
359
367
  @pytest.mark.asyncio
360
368
  async def test_build_batch_snapshot_and_filter(self, strategy):
361
369
  snap = await strategy.build_batch_snapshot(
@@ -521,7 +529,7 @@ class TestBasisTradingStrategy:
521
529
  )
522
530
  )
523
531
  mock_hyperliquid_adapter.get_valid_order_size = MagicMock(
524
- side_effect=lambda aid, sz: sz
532
+ side_effect=lambda _aid, sz: sz
525
533
  )
526
534
  mock_hyperliquid_adapter.transfer_perp_to_spot = AsyncMock(
527
535
  return_value=(True, "ok")
@@ -763,3 +771,212 @@ class TestBasisTradingStrategy:
763
771
 
764
772
  # deposit_amount should now be set from detected balance
765
773
  assert strategy.deposit_amount == 50.0
774
+
775
+ @pytest.mark.asyncio
776
+ async def test_update_spot_usdc_only_rebalances_before_open(
777
+ self, strategy, mock_hyperliquid_adapter
778
+ ):
779
+ """
780
+ If funds are mostly in spot USDC (e.g., after liquidation), update() should:
781
+ - detect deposit from spot+perp USDC
782
+ - transfer spot->perp to reach the target split before opening.
783
+ """
784
+ strategy.deposit_amount = 0.0
785
+ strategy.current_position = None
786
+
787
+ # Perp account has $0 withdrawable, spot has $100 USDC
788
+ mock_hyperliquid_adapter.get_user_state = AsyncMock(
789
+ return_value=(
790
+ True,
791
+ {
792
+ "marginSummary": {"accountValue": "0"},
793
+ "withdrawable": "0",
794
+ "assetPositions": [],
795
+ },
796
+ )
797
+ )
798
+ mock_hyperliquid_adapter.get_spot_user_state = AsyncMock(
799
+ return_value=(
800
+ True,
801
+ {"balances": [{"coin": "USDC", "total": "100"}]},
802
+ )
803
+ )
804
+
805
+ # Avoid running the full solver; return a deterministic best trade.
806
+ strategy.find_best_trade_with_backtest = AsyncMock(
807
+ return_value={
808
+ "coin": "ETH",
809
+ "spot_asset_id": 10000,
810
+ "perp_asset_id": 1,
811
+ "net_apy": 0.1,
812
+ "best_L": 2,
813
+ "safe": {
814
+ "7": {
815
+ "safe_leverage": 2,
816
+ "spot_usdc": 66.67,
817
+ "spot_amount": 0.033335,
818
+ "perp_amount": 0.033335,
819
+ }
820
+ },
821
+ }
822
+ )
823
+
824
+ # Mock the paired filler to avoid actual execution
825
+ with patch(
826
+ "wayfinder_paths.strategies.basis_trading_strategy.strategy.PairedFiller"
827
+ ) as mock_filler_class:
828
+ mock_filler = MagicMock()
829
+ mock_filler.fill_pair_units = AsyncMock(
830
+ return_value=(0.5, 0.5, 1000.0, 1000.0, [], [])
831
+ )
832
+ mock_filler_class.return_value = mock_filler
833
+
834
+ success, _ = await strategy.update()
835
+ assert success is True
836
+
837
+ # Target spot was $66.67, so we should transfer $33.33 spot->perp.
838
+ mock_hyperliquid_adapter.transfer_spot_to_perp.assert_called_once()
839
+ _, kwargs = mock_hyperliquid_adapter.transfer_spot_to_perp.call_args
840
+ assert kwargs["address"] == "0x5678"
841
+ assert abs(kwargs["amount"] - 33.33) < 1e-6
842
+
843
+ # Should not attempt perp->spot when spot already has sufficient USDC.
844
+ mock_hyperliquid_adapter.transfer_perp_to_spot.assert_not_called()
845
+
846
+ assert strategy.deposit_amount == 100.0
847
+
848
+ @pytest.mark.asyncio
849
+ async def test_update_near_liquidation_closes_and_redeploys(
850
+ self, strategy, mock_hyperliquid_adapter
851
+ ):
852
+ """Near-liquidation should trigger an emergency close+redeploy (bypasses cooldown)."""
853
+ from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
854
+ BasisPosition,
855
+ )
856
+
857
+ strategy.deposit_amount = 100.0
858
+ strategy.current_position = BasisPosition(
859
+ coin="HYPE",
860
+ spot_asset_id=10107,
861
+ perp_asset_id=7,
862
+ spot_amount=1.0,
863
+ perp_amount=1.0,
864
+ entry_price=100.0,
865
+ leverage=2,
866
+ entry_timestamp=1700000000000,
867
+ funding_collected=0.0,
868
+ )
869
+
870
+ # Price is exactly 75% of the way from entry -> liquidation:
871
+ # (175 - 100) / (200 - 100) = 0.75
872
+ mock_hyperliquid_adapter.get_all_mid_prices = AsyncMock(
873
+ return_value=(True, {"HYPE": 175.0})
874
+ )
875
+ mock_hyperliquid_adapter.get_user_state = AsyncMock(
876
+ return_value=(
877
+ True,
878
+ {
879
+ "marginSummary": {
880
+ "accountValue": "100",
881
+ "withdrawable": "0",
882
+ "totalNtlPos": "100",
883
+ },
884
+ "assetPositions": [
885
+ {
886
+ "position": {
887
+ "coin": "HYPE",
888
+ "szi": "-1.0",
889
+ "entryPx": "100",
890
+ "liquidationPx": "200",
891
+ }
892
+ }
893
+ ],
894
+ },
895
+ )
896
+ )
897
+ mock_hyperliquid_adapter.get_spot_user_state = AsyncMock(
898
+ return_value=(
899
+ True,
900
+ {"balances": [{"coin": "HYPE", "total": "1.0"}]},
901
+ )
902
+ )
903
+
904
+ # Ensure cooldown would block a normal rebalance, but emergency should bypass it.
905
+ strategy._is_rotation_allowed = AsyncMock(return_value=(False, "cooldown"))
906
+ strategy._close_position = AsyncMock(return_value=(True, "closed"))
907
+ strategy._find_and_open_position = AsyncMock(return_value=(True, "redeployed"))
908
+
909
+ success, msg = await strategy.update()
910
+ assert success is True
911
+ assert msg == "redeployed"
912
+ strategy._close_position.assert_awaited_once()
913
+ strategy._find_and_open_position.assert_awaited_once()
914
+ strategy._is_rotation_allowed.assert_not_called()
915
+
916
+ @pytest.mark.asyncio
917
+ async def test_net_deposit_handles_float_return(self, strategy):
918
+ """Test that strategy correctly handles float from get_strategy_net_deposit.
919
+
920
+ The ledger adapter returns (success, float) not (success, dict).
921
+ This test ensures the strategy doesn't try to call .get() on the float,
922
+ which would raise "'float' object has no attribute 'get'".
923
+ """
924
+ # Mock ledger adapter to return a float (not a dict)
925
+ strategy.ledger_adapter.get_strategy_net_deposit = AsyncMock(
926
+ return_value=(True, 1500.0)
927
+ )
928
+
929
+ # Call status() which internally uses get_strategy_net_deposit
930
+ status = await strategy.status()
931
+
932
+ # Verify net_deposit is correctly set from the float
933
+ assert status["net_deposit"] == 1500.0
934
+
935
+ @pytest.mark.asyncio
936
+ async def test_setup_handles_float_net_deposit(
937
+ self, mock_hyperliquid_adapter, ledger_adapter
938
+ ):
939
+ """Test that setup() correctly handles float from get_strategy_net_deposit.
940
+
941
+ This catches if code is changed to expect a dict with .get('net_deposit').
942
+ """
943
+ with patch(
944
+ "wayfinder_paths.strategies.basis_trading_strategy.strategy.HyperliquidAdapter",
945
+ return_value=mock_hyperliquid_adapter,
946
+ ):
947
+ with patch(
948
+ "wayfinder_paths.strategies.basis_trading_strategy.strategy.BalanceAdapter"
949
+ ):
950
+ with patch(
951
+ "wayfinder_paths.strategies.basis_trading_strategy.strategy.TokenAdapter"
952
+ ):
953
+ with patch(
954
+ "wayfinder_paths.strategies.basis_trading_strategy.strategy.LedgerAdapter",
955
+ return_value=ledger_adapter,
956
+ ):
957
+ with patch(
958
+ "wayfinder_paths.strategies.basis_trading_strategy.strategy.WalletManager"
959
+ ):
960
+ from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
961
+ BasisTradingStrategy,
962
+ )
963
+
964
+ s = BasisTradingStrategy(
965
+ config={
966
+ "main_wallet": {"address": "0x1234"},
967
+ "strategy_wallet": {"address": "0x5678"},
968
+ },
969
+ )
970
+ s.hyperliquid_adapter = mock_hyperliquid_adapter
971
+ s.ledger_adapter = ledger_adapter
972
+
973
+ # Mock get_strategy_net_deposit to return float (not dict)
974
+ s.ledger_adapter.get_strategy_net_deposit = AsyncMock(
975
+ return_value=(True, 2500.0)
976
+ )
977
+
978
+ # Run setup - should not raise AttributeError
979
+ await s.setup()
980
+
981
+ # Verify deposit_amount was set from the float
982
+ assert s.deposit_amount == 2500.0
@@ -66,7 +66,7 @@ Allocates USDT0 on HyperEVM across HyperLend stablecoin markets. The strategy:
66
66
  # Install dependencies
67
67
  poetry install
68
68
 
69
- # Generate main wallet (writes wallets.json)
69
+ # Generate main wallet (writes config.json)
70
70
  # Creates a main wallet (or use 'just create-strategy' which auto-creates wallets)
71
71
  poetry run python wayfinder_paths/scripts/make_wallets.py -n 1
72
72
 
@@ -82,4 +82,4 @@ poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strateg
82
82
  poetry run python wayfinder_paths/run_strategy.py hyperlend_stable_yield_strategy --action withdraw --config $(pwd)/config.json
83
83
  ```
84
84
 
85
- Wallet addresses/labels are auto-resolved from `wallets.json`. Set `NETWORK=testnet` in your config to run the orchestration without touching live HyperEVM endpoints.
85
+ Wallet addresses/labels are auto-resolved from `config.json`. Set `NETWORK=testnet` in your config to run the orchestration without touching live HyperEVM endpoints.
@@ -104,6 +104,7 @@ class HyperlendStableYieldStrategy(Strategy):
104
104
  f"rotation band (dwell={HYSTERESIS_DWELL_HOURS}h, z={HYSTERESIS_Z:.2f}) to avoid churn while still "
105
105
  "short-circuiting when yield gaps are extreme."
106
106
  ),
107
+ risk_description="Protocol risk is always present when engaging with DeFi strategies, this includes underlying DeFi protocols and Wayfinder itself. Additional risk includes rate volatility between sampling windows, swap slippage on HYPE ⇄ stable legs, HyperLend protocol risk, and rotation gas costs eroding yield if APY edges are thin. Strategy requires a small HYPE balance for gas on HyperEVM.",
107
108
  gas_token_symbol="HYPE",
108
109
  gas_token_id="hyperliquid-hyperevm",
109
110
  deposit_token_id="usdt0-hyperevm",
@@ -196,9 +197,8 @@ class HyperlendStableYieldStrategy(Strategy):
196
197
  main_wallet: dict[str, Any] | None = None,
197
198
  strategy_wallet: dict[str, Any] | None = None,
198
199
  web3_service: Web3Service = None,
199
- api_key: str | None = None,
200
200
  ):
201
- super().__init__(api_key=api_key)
201
+ super().__init__()
202
202
  merged_config: dict[str, Any] = dict(config or {})
203
203
  if main_wallet is not None:
204
204
  merged_config["main_wallet"] = main_wallet
@@ -349,7 +349,7 @@ class HyperlendStableYieldStrategy(Strategy):
349
349
  success,
350
350
  main_usdt0_balance,
351
351
  ) = await self.balance_adapter.get_balance(
352
- token_id=self.usdt_token_info.get("token_id"),
352
+ query=self.usdt_token_info.get("token_id"),
353
353
  wallet_address=self._get_main_wallet_address(),
354
354
  )
355
355
  if not success:
@@ -362,7 +362,7 @@ class HyperlendStableYieldStrategy(Strategy):
362
362
  success,
363
363
  main_hype_balance,
364
364
  ) = await self.balance_adapter.get_balance(
365
- token_id=self.hype_token_info.get("token_id"),
365
+ query=self.hype_token_info.get("token_id"),
366
366
  wallet_address=self._get_main_wallet_address(),
367
367
  )
368
368
  if not success:
@@ -529,11 +529,10 @@ class HyperlendStableYieldStrategy(Strategy):
529
529
  return self._assets_snapshot
530
530
 
531
531
  _, snapshot = await self.hyperlend_adapter.get_assets_view(
532
- chain_id=self.hype_token_info.get("chain").get("id"),
533
532
  user_address=self._get_strategy_wallet_address(),
534
533
  )
535
534
 
536
- assets = snapshot.get("assets_view", {}).get("assets", [])
535
+ assets = snapshot.get("assets", [])
537
536
  asset_map = {}
538
537
 
539
538
  for asset in assets:
@@ -581,11 +580,9 @@ class HyperlendStableYieldStrategy(Strategy):
581
580
 
582
581
  try:
583
582
  _, data = await self.hyperlend_adapter.get_stable_markets(
584
- chain_id=self.hype_token_info.get("chain").get("id"),
585
583
  required_underlying_tokens=required_tokens,
586
584
  buffer_bps=self.SUPPLY_CAP_BUFFER_BPS,
587
585
  min_buffer_tokens=self.SUPPLY_CAP_MIN_BUFFER_TOKENS,
588
- is_stable_symbol=True,
589
586
  )
590
587
  markets = data.get("markets", {}) if isinstance(data, dict) else {}
591
588
  except Exception:
@@ -608,7 +605,7 @@ class HyperlendStableYieldStrategy(Strategy):
608
605
  async def _get_lent_positions(self, snapshot=None) -> dict[str, dict[str, Any]]:
609
606
  if not snapshot:
610
607
  snapshot = await self._get_assets_snapshot()
611
- assets = snapshot.get("assets_view", {}).get("assets", None)
608
+ assets = snapshot.get("assets", None)
612
609
 
613
610
  if not assets:
614
611
  return {}
@@ -629,7 +626,14 @@ class HyperlendStableYieldStrategy(Strategy):
629
626
  continue
630
627
 
631
628
  try:
632
- success, token = await self.token_adapter.get_token(checksum)
629
+ chain_id = None
630
+ try:
631
+ chain_id = int((self.hype_token_info.get("chain") or {}).get("id"))
632
+ except Exception:
633
+ chain_id = None
634
+ success, token = await self.token_adapter.get_token(
635
+ checksum, chain_id=chain_id
636
+ )
633
637
  if not success or not isinstance(token, dict):
634
638
  logger.info(f"Error getting token for asset: {asset}")
635
639
  continue
@@ -715,8 +719,8 @@ class HyperlendStableYieldStrategy(Strategy):
715
719
  result,
716
720
  tx_data,
717
721
  ) = await self.brap_adapter.swap_from_token_ids(
718
- from_token_id=from_token_id,
719
- to_token_id=to_token_id,
722
+ from_query=from_token_id,
723
+ to_query=to_token_id,
720
724
  from_address=strategy_address,
721
725
  amount=amount_wei_str,
722
726
  slippage=slippage,
@@ -861,7 +865,7 @@ class HyperlendStableYieldStrategy(Strategy):
861
865
 
862
866
  try:
863
867
  _, total_usdt_wei = await self.balance_adapter.get_balance(
864
- token_id=self.usdt_token_info.get("token_id"),
868
+ query=self.usdt_token_info.get("token_id"),
865
869
  wallet_address=self._get_strategy_wallet_address(),
866
870
  )
867
871
  except Exception:
@@ -890,7 +894,7 @@ class HyperlendStableYieldStrategy(Strategy):
890
894
 
891
895
  try:
892
896
  _, total_hype_wei = await self.balance_adapter.get_balance(
893
- token_id=self.hype_token_info.get("token_id"),
897
+ query=self.hype_token_info.get("token_id"),
894
898
  wallet_address=self._get_strategy_wallet_address(),
895
899
  )
896
900
  except Exception:
@@ -986,7 +990,7 @@ class HyperlendStableYieldStrategy(Strategy):
986
990
  success,
987
991
  message,
988
992
  ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
989
- token_id=token_info.get("token_id"),
993
+ query=token_info.get("token_id"),
990
994
  amount=amount_tokens,
991
995
  strategy_name=self.name,
992
996
  )
@@ -1548,11 +1552,9 @@ class HyperlendStableYieldStrategy(Strategy):
1548
1552
  )
1549
1553
 
1550
1554
  _, stable_markets = await self.hyperlend_adapter.get_stable_markets(
1551
- chain_id=self.hype_token_info.get("chain").get("id"),
1552
1555
  required_underlying_tokens=required_underlying_tokens,
1553
1556
  buffer_bps=self.SUPPLY_CAP_BUFFER_BPS,
1554
1557
  min_buffer_tokens=self.SUPPLY_CAP_MIN_BUFFER_TOKENS,
1555
- is_stable_symbol=True,
1556
1558
  )
1557
1559
  filtered_notes = stable_markets.get("notes", [])
1558
1560
  filtered_map = stable_markets.get("markets", {})
@@ -1584,8 +1586,7 @@ class HyperlendStableYieldStrategy(Strategy):
1584
1586
  if current_checksum_lower not in existing_addresses:
1585
1587
  try:
1586
1588
  _, current_entry = await self.hyperlend_adapter.get_market_entry(
1587
- chain_id=self.hype_token_info.get("chain").get("id"),
1588
- token_address=current_checksum_value,
1589
+ token=current_checksum_value,
1589
1590
  )
1590
1591
  except Exception:
1591
1592
  current_entry = None
@@ -1632,8 +1633,7 @@ class HyperlendStableYieldStrategy(Strategy):
1632
1633
  histories = await asyncio.gather(
1633
1634
  *[
1634
1635
  self.hyperlend_adapter.get_lend_rate_history(
1635
- chain_id=self.hype_token_info.get("chain").get("id"),
1636
- token_address=addr,
1636
+ token=addr,
1637
1637
  lookback_hours=lookback_hours,
1638
1638
  )
1639
1639
  for addr, _ in filtered
@@ -1655,7 +1655,7 @@ class HyperlendStableYieldStrategy(Strategy):
1655
1655
  if not history_status:
1656
1656
  continue
1657
1657
  history_data = history[1]
1658
- for row in history_data.get("rate_history", []):
1658
+ for row in history_data.get("history", []):
1659
1659
  ts_ms = row.get("timestamp_ms")
1660
1660
  if ts_ms is None:
1661
1661
  continue
@@ -1811,7 +1811,14 @@ class HyperlendStableYieldStrategy(Strategy):
1811
1811
  token = None
1812
1812
  if address:
1813
1813
  try:
1814
- success, token = await self.token_adapter.get_token(address.lower())
1814
+ chain_id = None
1815
+ try:
1816
+ chain_id = int((self.hype_token_info.get("chain") or {}).get("id"))
1817
+ except Exception:
1818
+ chain_id = None
1819
+ success, token = await self.token_adapter.get_token(
1820
+ address.lower(), chain_id=chain_id
1821
+ )
1815
1822
  except Exception:
1816
1823
  token = None
1817
1824
  if not success:
@@ -2127,7 +2134,16 @@ class HyperlendStableYieldStrategy(Strategy):
2127
2134
  continue
2128
2135
 
2129
2136
  try:
2130
- success, token = await self.token_adapter.get_token(checksum)
2137
+ chain_id = None
2138
+ try:
2139
+ chain_id = int(
2140
+ (self.hype_token_info.get("chain") or {}).get("id")
2141
+ )
2142
+ except Exception:
2143
+ chain_id = None
2144
+ success, token = await self.token_adapter.get_token(
2145
+ checksum, chain_id=chain_id
2146
+ )
2131
2147
  if not success or not isinstance(token, dict):
2132
2148
  continue
2133
2149
  except Exception:
@@ -2238,7 +2254,7 @@ class HyperlendStableYieldStrategy(Strategy):
2238
2254
  success,
2239
2255
  strategy_hype_balance_wei,
2240
2256
  ) = await self.balance_adapter.get_balance(
2241
- token_id=self.hype_token_info.get("token_id"),
2257
+ query=self.hype_token_info.get("token_id"),
2242
2258
  wallet_address=self._get_strategy_wallet_address(),
2243
2259
  )
2244
2260
  hype_price = asset_map.get(WRAPPED_HYPE_ADDRESS, {}).get("price_usd") or 0.0
@@ -50,7 +50,10 @@ def strategy():
50
50
 
51
51
  if hasattr(s, "balance_adapter") and s.balance_adapter:
52
52
  # Mock balances: 1000 USDT0 (with 6 decimals) and 2 HYPE (with 18 decimals)
53
- def get_balance_side_effect(token_id, wallet_address, **kwargs):
53
+ def get_balance_side_effect(query, wallet_address, **kwargs):
54
+ token_id = (
55
+ query if isinstance(query, str) else (query or {}).get("token_id")
56
+ )
54
57
  token_id_str = str(token_id).lower() if token_id else ""
55
58
  if "usdt0" in token_id_str or token_id_str == "usdt0":
56
59
  # 1000 USDT0 with 6 decimals = 1000 * 10^6 = 1000000000
@@ -209,7 +212,31 @@ def strategy():
209
212
 
210
213
  if hasattr(s, "hyperlend_adapter") and s.hyperlend_adapter:
211
214
  s.hyperlend_adapter.get_assets_view = AsyncMock(
212
- return_value=(True, {"assets_view": {"assets": []}})
215
+ return_value=(
216
+ True,
217
+ {
218
+ "block_number": 12345,
219
+ "user": "0x0",
220
+ "native_balance_wei": 0,
221
+ "native_balance": 0.0,
222
+ "assets": [],
223
+ "account_data": {
224
+ "total_collateral_base": 0,
225
+ "total_debt_base": 0,
226
+ "available_borrows_base": 0,
227
+ "current_liquidation_threshold": 0,
228
+ "ltv": 0,
229
+ "health_factor_wad": 0,
230
+ "health_factor": 0.0,
231
+ },
232
+ "base_currency_info": {
233
+ "marketReferenceCurrencyUnit": 100000000,
234
+ "marketReferenceCurrencyPriceInUsd": 100000000,
235
+ "networkBaseTokenPriceInUsd": 0,
236
+ "networkBaseTokenPriceDecimals": 8,
237
+ },
238
+ },
239
+ )
213
240
  )
214
241
  s.hyperlend_adapter.get_stable_markets = AsyncMock(
215
242
  return_value=(
@@ -232,14 +259,24 @@ def strategy():
232
259
  },
233
260
  )
234
261
  )
262
+ # Block bootstrap needs at least BLOCK_LEN (6) rows; provide enough history
263
+ _history_base = {
264
+ "timestamp_ms": 1700000000000,
265
+ "timestamp": 1700000000.0,
266
+ "supply_apr": 0.05,
267
+ "supply_apy": 0.05,
268
+ "borrow_apr": 0.07,
269
+ "borrow_apy": 0.07,
270
+ "token": "0x1234567890123456789012345678901234567890",
271
+ "symbol": "usdt0",
272
+ "display_symbol": "USDT0",
273
+ }
274
+ history_rows = [
275
+ {**_history_base, "timestamp_ms": 1700000000000 + i * 3600000}
276
+ for i in range(24)
277
+ ]
235
278
  s.hyperlend_adapter.get_lend_rate_history = AsyncMock(
236
- return_value=(
237
- True,
238
- {
239
- "rates": [{"rate": 5.0, "timestamp": 1700000000}],
240
- "avg_rate": 5.0,
241
- },
242
- )
279
+ return_value=(True, {"history": history_rows})
243
280
  )
244
281
 
245
282
  s.usdt_token_info = {
@@ -259,6 +296,14 @@ def strategy():
259
296
  "chain": {"code": "hyperevm", "id": 9999, "name": "HyperEVM"},
260
297
  }
261
298
  s.current_token = None
299
+ # Attributes normally set in setup()
300
+ s.rotation_policy = "hysteresis"
301
+ s.hys_dwell_hours = 168
302
+ s.hys_z = 1.15
303
+ s.rotation_tx_cost = 0.002
304
+ s.last_summary = None
305
+ s.last_dominance = None
306
+ s.last_samples = None
262
307
 
263
308
  if hasattr(s, "token_adapter") and s.token_adapter:
264
309
  if not hasattr(s.token_adapter, "get_token_price"):
@@ -86,7 +86,7 @@ The position is **delta-neutral**: WETH debt offsets wstETH collateral, so PnL i
86
86
  # Install dependencies
87
87
  poetry install
88
88
 
89
- # Generate wallets (writes wallets.json)
89
+ # Generate wallets (writes config.json)
90
90
  poetry run python wayfinder_paths/scripts/make_wallets.py -n 1
91
91
 
92
92
  # Copy config and edit credentials
@@ -81,7 +81,8 @@ class MoonwellWstethLoopStrategy(Strategy):
81
81
  summary = "Loop wstETH on Moonwell for amplified staking yields."
82
82
 
83
83
  # Strategy parameters
84
- MIN_GAS = 0.002 # Minimum Base ETH (in ETH) required for gas fees (Base L2)
84
+ # Minimum Base ETH (in ETH) required for gas fees (Base L2)
85
+ MIN_GAS = 0.002
85
86
  MAINTENANCE_GAS = MIN_GAS / 10
86
87
  # When wrapping ETH to WETH for swaps/repayment, avoid draining gas below this floor.
87
88
  # We can dip below MIN_GAS temporarily, but should not wipe the wallet.
@@ -110,6 +111,7 @@ class MoonwellWstethLoopStrategy(Strategy):
110
111
  description="Leveraged wstETH carry: loops USDC → borrow WETH → swap wstETH → lend. "
111
112
  "Depeg-aware sizing with safety factor. ETH-neutral: WETH debt vs wstETH collateral.",
112
113
  summary="Leveraged wstETH carry on Base with depeg-aware sizing.",
114
+ risk_description=f"Protocol risk is always present when engaging with DeFi strategies, this includes underlying DeFi protocols and Wayfinder itself. Additional risks include weth/wsteth depegging (this strategy tracks the peg and is robust up to {int(MAX_DEPEG * 100)}% depeg). The rate spread between weth borrow and wsteth lend may also turn negative. This will likely only be temporary and is very rare. If this persists manual withdraw may be needed.",
113
115
  gas_token_symbol="ETH",
114
116
  gas_token_id=ETH_TOKEN_ID,
115
117
  deposit_token_id=USDC_TOKEN_ID,
@@ -506,7 +508,7 @@ class MoonwellWstethLoopStrategy(Strategy):
506
508
  if self.balance_adapter is None:
507
509
  return 0
508
510
  success, raw = await self.balance_adapter.get_balance(
509
- token_id=token_id,
511
+ query=token_id,
510
512
  wallet_address=wallet_address,
511
513
  )
512
514
  return self._parse_balance(raw) if success else 0
@@ -532,11 +534,9 @@ class MoonwellWstethLoopStrategy(Strategy):
532
534
  return 0
533
535
 
534
536
  try:
535
- evm = getattr(self.web3_service, "evm_transactions", None)
536
- if evm is None:
537
- return 0
538
- ok, bal = await evm.get_balance(
539
- wallet_address, token_address, BASE_CHAIN_ID, block_identifier
537
+ ok, bal = await self.balance_adapter.get_balance(
538
+ query=token_id,
539
+ wallet_address=wallet_address,
540
540
  )
541
541
  return int(bal) if ok else 0
542
542
  except Exception as exc:
@@ -1340,7 +1340,8 @@ class MoonwellWstethLoopStrategy(Strategy):
1340
1340
  for mtoken in mtoken_list:
1341
1341
  pos = await self.moonwell_adapter.get_pos(mtoken=mtoken)
1342
1342
  positions.append(pos)
1343
- await asyncio.sleep(2.0) # 2s delay between positions for public RPC
1343
+ # 2s delay between positions for public RPC
1344
+ await asyncio.sleep(2.0)
1344
1345
 
1345
1346
  # Token data can be fetched in parallel (uses cache, minimal RPC)
1346
1347
  token_data = await asyncio.gather(
@@ -1718,7 +1719,7 @@ class MoonwellWstethLoopStrategy(Strategy):
1718
1719
 
1719
1720
  # Get actual wstETH balance
1720
1721
  wsteth_success, wsteth_bal_raw = await self.balance_adapter.get_balance(
1721
- token_id=WSTETH_TOKEN_ID, wallet_address=self._get_strategy_wallet_address()
1722
+ query=WSTETH_TOKEN_ID, wallet_address=self._get_strategy_wallet_address()
1722
1723
  )
1723
1724
  if not wsteth_success:
1724
1725
  raise Exception("Failed to get wstETH balance after swap")