wayfinder-paths 0.1.11__py3-none-any.whl → 0.1.13__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 (46) hide show
  1. wayfinder_paths/adapters/balance_adapter/adapter.py +3 -7
  2. wayfinder_paths/adapters/brap_adapter/adapter.py +10 -13
  3. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +6 -9
  4. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1 -1
  5. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +44 -5
  6. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +104 -0
  7. wayfinder_paths/adapters/moonwell_adapter/adapter.py +0 -3
  8. wayfinder_paths/adapters/pool_adapter/README.md +4 -19
  9. wayfinder_paths/adapters/pool_adapter/adapter.py +4 -29
  10. wayfinder_paths/adapters/pool_adapter/examples.json +6 -7
  11. wayfinder_paths/adapters/pool_adapter/test_adapter.py +8 -8
  12. wayfinder_paths/core/clients/AuthClient.py +2 -2
  13. wayfinder_paths/core/clients/BRAPClient.py +2 -2
  14. wayfinder_paths/core/clients/HyperlendClient.py +2 -2
  15. wayfinder_paths/core/clients/PoolClient.py +18 -54
  16. wayfinder_paths/core/clients/TokenClient.py +3 -3
  17. wayfinder_paths/core/clients/WalletClient.py +2 -2
  18. wayfinder_paths/core/clients/WayfinderClient.py +9 -10
  19. wayfinder_paths/core/clients/protocols.py +1 -7
  20. wayfinder_paths/core/config.py +60 -224
  21. wayfinder_paths/core/services/local_evm_txn.py +22 -4
  22. wayfinder_paths/core/strategies/Strategy.py +3 -3
  23. wayfinder_paths/core/strategies/descriptors.py +7 -0
  24. wayfinder_paths/core/utils/evm_helpers.py +5 -1
  25. wayfinder_paths/core/utils/wallets.py +12 -19
  26. wayfinder_paths/core/wallets/README.md +1 -1
  27. wayfinder_paths/run_strategy.py +10 -8
  28. wayfinder_paths/scripts/create_strategy.py +5 -5
  29. wayfinder_paths/scripts/make_wallets.py +5 -5
  30. wayfinder_paths/scripts/run_strategy.py +3 -3
  31. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -1
  32. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +196 -515
  33. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +228 -11
  34. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
  35. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +1 -0
  36. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +1 -1
  37. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +8 -7
  38. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +2 -2
  39. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +25 -25
  40. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +28 -9
  41. wayfinder_paths/templates/adapter/README.md +1 -1
  42. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.13.dist-info}/METADATA +9 -12
  43. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.13.dist-info}/RECORD +45 -45
  44. wayfinder_paths/core/settings.py +0 -61
  45. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.13.dist-info}/LICENSE +0 -0
  46. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.13.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",
@@ -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,
@@ -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
+ token_id=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(
@@ -71,7 +71,7 @@ Transactions are scoped to the strategy wallet and Enso Router approval/swap cal
71
71
  # Install dependencies
72
72
  poetry install
73
73
 
74
- # Generate main wallet (writes wallets.json)
74
+ # Generate main wallet (writes config.json)
75
75
  # Creates a main wallet (or use 'just create-strategy' which auto-creates wallets)
76
76
  poetry run python wayfinder_paths/scripts/make_wallets.py -n 1
77
77
 
@@ -86,4 +86,4 @@ poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy --ac
86
86
  poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy --action update --config $(pwd)/config.json
87
87
  ```
88
88
 
89
- Wallet addresses are auto-populated from `wallets.json` when you run `wayfinder_paths/scripts/make_wallets.py`. Set `NETWORK=testnet` in `config.json` to dry-run operations against mocked services.
89
+ 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.
@@ -65,6 +65,7 @@ class StablecoinYieldStrategy(Strategy):
65
65
  f"Continuously optimizes positions for maximum stable yield while avoiding impermanent loss. "
66
66
  f"Min: {MIN_AMOUNT_USDC} USDC + ETH gas. Filters for ${MIN_TVL:,}+ TVL protocols."
67
67
  ),
68
+ risk_description=f"Protocol risk is always present when engaging with DeFi strategies, this includes underlying DeFi protocols and Wayfinder itself. Additional risks include temporary yield fluctuations, gas costs during rebalancing, and potential brief capital lock-up during protocol transitions. Strategy filters for protocols with a minimum TVL of ${MIN_TVL:,} to ensure low-risk exposure.",
68
69
  gas_token_symbol="ETH",
69
70
  gas_token_id="ethereum-base",
70
71
  deposit_token_id="usd-coin-base",
@@ -303,8 +304,8 @@ class StablecoinYieldStrategy(Strategy):
303
304
  success, deposit_data = await self.ledger_adapter.get_strategy_net_deposit(
304
305
  wallet_address=strategy_address,
305
306
  )
306
- if success:
307
- self.DEPOSIT_USDC = deposit_data.get("net_deposit", 0)
307
+ if success and deposit_data is not None:
308
+ self.DEPOSIT_USDC = float(deposit_data)
308
309
  logger.info(f"Strategy net deposit: {self.DEPOSIT_USDC} USDC")
309
310
  else:
310
311
  logger.error(f"Failed to fetch strategy net deposit: {deposit_data}")
@@ -427,30 +428,29 @@ class StablecoinYieldStrategy(Strategy):
427
428
  self._track_token(token_info.get("token_id"))
428
429
 
429
430
  success, reports = await self.pool_adapter.get_pools_by_ids(
430
- pool_ids=[self.current_pool.get("token_id")],
431
- merge_external=False,
431
+ pool_ids=[self.current_pool.get("token_id")]
432
432
  )
433
433
  if success and reports.get("pools"):
434
434
  self.current_pool_data = reports.get("pools", [])[0]
435
435
 
436
- identifiers = []
436
+ pool_ids = []
437
437
  pool_id = self.current_pool.get("token_id", None)
438
438
  if isinstance(pool_id, str):
439
- identifiers.append(pool_id)
439
+ pool_ids.append(pool_id)
440
440
 
441
441
  pool_address = self.current_pool.get("address", None)
442
442
  pool_chain = self.current_pool.get("chain", None)
443
443
  chain_code = ((pool_chain or {}).get("code")) or None
444
444
  if isinstance(pool_address, str) and isinstance(chain_code, str):
445
- identifiers.append(f"{chain_code.lower()}_{pool_address.lower()}")
445
+ pool_ids.append(f"{chain_code.lower()}_{pool_address.lower()}")
446
446
 
447
447
  llama_report = None
448
- if identifiers:
449
- success, llama_reports = await self.pool_adapter.get_llama_reports(
450
- identifiers=identifiers
448
+ if pool_ids:
449
+ success, llama_reports = await self.pool_adapter.get_pools_by_ids(
450
+ pool_ids=pool_ids
451
451
  )
452
452
  if success:
453
- for identifier in identifiers:
453
+ for identifier in pool_ids:
454
454
  if not isinstance(identifier, str):
455
455
  continue
456
456
  report = llama_reports.get(identifier.lower(), None)
@@ -464,12 +464,12 @@ class StablecoinYieldStrategy(Strategy):
464
464
  "llama_report": llama_report,
465
465
  }
466
466
 
467
- if llama_report and llama_report.get("llama_combined_apy_pct") is not None:
467
+ if llama_report and llama_report.get("combined_apy_pct") is not None:
468
468
  self.current_combined_apy_pct = (
469
- llama_report.get("llama_combined_apy_pct", 0) / 100
469
+ llama_report.get("combined_apy_pct", 0) / 100
470
470
  )
471
- elif llama_report and llama_report.get("llama_apy_pct") is not None:
472
- self.current_combined_apy_pct = llama_report.get("llama_apy_pct", 0) / 100
471
+ elif llama_report and llama_report.get("apy") is not None:
472
+ self.current_combined_apy_pct = llama_report.get("apy", 0) / 100
473
473
  elif self.current_pool_data:
474
474
  self.current_combined_apy_pct = self.current_pool_data.get("apy", 0)
475
475
 
@@ -525,8 +525,8 @@ class StablecoinYieldStrategy(Strategy):
525
525
  self.current_pool_balance = inferred_balance
526
526
  if inferred_entry:
527
527
  self.current_pool_data = inferred_entry
528
- llama_combined = inferred_entry.get("llama_combined_apy_pct")
529
- llama_apy = inferred_entry.get("llama_apy_pct")
528
+ llama_combined = inferred_entry.get("combined_apy_pct")
529
+ llama_apy = inferred_entry.get("apy")
530
530
  if llama_combined is not None:
531
531
  self.current_combined_apy_pct = float(llama_combined) / 100
532
532
  elif llama_apy is not None:
@@ -1209,7 +1209,7 @@ class StablecoinYieldStrategy(Strategy):
1209
1209
  self.current_combined_apy_pct = self.current_pool_data.get("apy", 0)
1210
1210
  else:
1211
1211
  self.current_combined_apy_pct = (
1212
- target_pool_data.get("llama_combined_apy_pct", 0) / 100
1212
+ target_pool_data.get("combined_apy_pct", 0) / 100
1213
1213
  if target_pool_data
1214
1214
  else 0
1215
1215
  )
@@ -1440,21 +1440,21 @@ class StablecoinYieldStrategy(Strategy):
1440
1440
  return results
1441
1441
 
1442
1442
  async def _find_best_pool(self) -> tuple[bool, dict[str, Any]]:
1443
- success, llama_data = await self.pool_adapter.get_llama_matches()
1443
+ success, llama_data = await self.pool_adapter.get_pools()
1444
1444
  if not success:
1445
1445
  return False, {"message": f"Failed to fetch Llama data: {llama_data}"}
1446
1446
 
1447
1447
  llama_pools = [
1448
1448
  pool
1449
1449
  for pool in llama_data.get("matches", [])
1450
- if pool.get("llama_stablecoin")
1451
- and pool.get("llama_il_risk") == "no"
1452
- and pool.get("llama_tvl_usd") > self.MIN_TVL
1453
- and pool.get("llama_apy_pct") > self.DUST_APY
1450
+ if pool.get("stablecoin")
1451
+ and pool.get("ilRisk") == "no"
1452
+ and pool.get("tvlUsd") > self.MIN_TVL
1453
+ and pool.get("combined_apy_pct") > self.DUST_APY
1454
1454
  and pool.get("network", "").lower() in self.SUPPORTED_NETWORK_CODES
1455
1455
  ]
1456
1456
  llama_pools = sorted(
1457
- llama_pools, key=lambda pool: pool.get("llama_apy_pct"), reverse=True
1457
+ llama_pools, key=lambda pool: pool.get("combined_apy_pct"), reverse=True
1458
1458
  )
1459
1459
  if not llama_pools:
1460
1460
  return False, {"message": "No suitable pools found."}
@@ -1515,7 +1515,7 @@ class StablecoinYieldStrategy(Strategy):
1515
1515
  return None
1516
1516
 
1517
1517
  try:
1518
- combined_apy_pct = pool_data.get("llama_combined_apy_pct") / 100
1518
+ combined_apy_pct = pool_data.get("combined_apy_pct") / 100
1519
1519
  success, quotes = await self.brap_adapter.get_swap_quote(
1520
1520
  from_token_address=current_token.get("address"),
1521
1521
  to_token_address=token.get("address"),
@@ -121,9 +121,8 @@ def strategy():
121
121
  )
122
122
 
123
123
  if hasattr(s, "ledger_adapter") and s.ledger_adapter:
124
- s.ledger_adapter.get_strategy_net_deposit = AsyncMock(
125
- return_value=(True, {"net_deposit": 0})
126
- )
124
+ # NOTE: The real LedgerClient returns float, not dict!
125
+ s.ledger_adapter.get_strategy_net_deposit = AsyncMock(return_value=(True, 0.0))
127
126
  s.ledger_adapter.get_strategy_transactions = AsyncMock(
128
127
  return_value=(True, {"transactions": []})
129
128
  )
@@ -135,21 +134,21 @@ def strategy():
135
134
  {"pools": [{"id": "test-pool-base", "apy": 15.0, "symbol": "POOL"}]},
136
135
  )
137
136
  )
138
- s.pool_adapter.get_llama_matches = AsyncMock(
137
+ s.pool_adapter.get_pools = AsyncMock(
139
138
  return_value=(
140
139
  True,
141
140
  {
142
141
  "matches": [
143
142
  {
144
- "llama_stablecoin": True,
145
- "llama_il_risk": "no",
146
- "llama_tvl_usd": 2000000,
147
- "llama_apy_pct": 5.0,
143
+ "stablecoin": True,
144
+ "ilRisk": "no",
145
+ "tvlUsd": 2000000,
146
+ "apy": 5.0,
148
147
  "network": "base",
149
148
  "address": "0x1234567890123456789012345678901234567890",
150
149
  "token_id": "test-pool-base",
151
150
  "pool_id": "test-pool-base",
152
- "llama_combined_apy_pct": 15.0,
151
+ "combined_apy_pct": 15.0,
153
152
  }
154
153
  ]
155
154
  },
@@ -541,3 +540,23 @@ async def test_partial_liquidate_uses_tracked_tokens(strategy):
541
540
  # Verify success
542
541
  assert ok
543
542
  assert "liquidation completed" in msg.lower()
543
+
544
+
545
+ @pytest.mark.asyncio
546
+ async def test_setup_handles_float_net_deposit(strategy):
547
+ """Test that setup() correctly handles float from get_strategy_net_deposit.
548
+
549
+ The ledger adapter returns (success, float) not (success, dict).
550
+ This test ensures the strategy doesn't try to call .get() on the float,
551
+ which would raise "'float' object has no attribute 'get'".
552
+ """
553
+ # Mock get_strategy_net_deposit to return float (not dict)
554
+ strategy.ledger_adapter.get_strategy_net_deposit = AsyncMock(
555
+ return_value=(True, 1500.0)
556
+ )
557
+
558
+ # Run setup - should not raise AttributeError
559
+ await strategy.setup()
560
+
561
+ # Verify DEPOSIT_USDC was set from the float
562
+ assert strategy.DEPOSIT_USDC == 1500.0
@@ -46,7 +46,7 @@ class MyAdapter(BaseAdapter):
46
46
  """Example capability that proxies PoolClient."""
47
47
  try:
48
48
  data = await self.pool_client.get_pools_by_ids(
49
- pool_ids=",".join(pool_ids), merge_external=True
49
+ pool_ids=pool_ids
50
50
  )
51
51
  return (True, data)
52
52
  except Exception as exc: # noqa: BLE001
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wayfinder-paths
3
- Version: 0.1.11
3
+ Version: 0.1.13
4
4
  Summary: Wayfinder Path: strategies and adapters
5
5
  Author: Wayfinder
6
6
  Author-email: dev@wayfinder.ai
@@ -15,8 +15,6 @@ Requires-Dist: loguru (>=0.7.3,<0.8.0)
15
15
  Requires-Dist: numpy (>=1.26.0,<2.0.0)
16
16
  Requires-Dist: pandas (>=2.2.0,<3.0.0)
17
17
  Requires-Dist: pydantic (>=2.11.9,<3.0.0)
18
- Requires-Dist: pydantic-settings (>=2.7.0,<3.0.0)
19
- Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
20
18
  Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
21
19
  Requires-Dist: web3 (>=7.13.0,<8.0.0)
22
20
  Description-Content-Type: text/markdown
@@ -43,7 +41,7 @@ curl -sSL https://install.python-poetry.org | python3 -
43
41
  poetry install
44
42
 
45
43
  # ⚠️ Generate test wallets FIRST (required!)
46
- # This creates wallets.json with a main wallet for local testing
44
+ # This creates config.json with a main wallet for local testing
47
45
  just create-wallets
48
46
  # Or manually: poetry run python wayfinder_paths/scripts/make_wallets.py -n 1
49
47
 
@@ -92,8 +90,7 @@ wayfinder_paths/
92
90
  │ ├── config.example.json # Example configuration
93
91
  │ ├── scripts/ # Utility scripts
94
92
  │ └── run_strategy.py # Strategy runner script
95
- ├── config.json # Your local config (created by you)
96
- ├── wallets.json # Generated dev wallets
93
+ ├── config.json # Your local config with credentials and wallets
97
94
  ├── pyproject.toml # Poetry configuration
98
95
  └── README.md # This file
99
96
  ```
@@ -302,7 +299,7 @@ class MyAdapter(BaseAdapter):
302
299
 
303
300
  async def get_pools(self, pool_ids: list[str]):
304
301
  data = await self.pool_client.get_pools_by_ids(
305
- pool_ids=",".join(pool_ids), merge_external=True
302
+ pool_ids=",".join(pool_ids)
306
303
  )
307
304
  return (True, data)
308
305
  ```
@@ -491,7 +488,7 @@ poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy --de
491
488
 
492
489
  ### Wallet Generation for Testing
493
490
 
494
- **Before running any strategies, generate test wallets.** This creates `wallets.json` in the repository root with throwaway wallets for local testing:
491
+ **Before running any strategies, generate test wallets.** This creates `config.json` in the repository root with throwaway wallets for local testing:
495
492
 
496
493
  ```bash
497
494
  # Essential: Create main wallet for testing
@@ -501,8 +498,8 @@ just create-wallets
501
498
 
502
499
  This creates:
503
500
 
504
- - `main` wallet - your main wallet for testing (labeled "main" in wallets.json)
505
- - `wallets.json` - wallet addresses and private keys for local testing
501
+ - `main` wallet - your main wallet for testing (labeled "main" in config.json)
502
+ - `config.json` - wallet addresses and private keys for local testing
506
503
 
507
504
  **Note:** Strategy-specific wallets are automatically created when you use `just create-strategy "Strategy Name"`. For manual creation, use `just create-wallet "strategy_name"` or `poetry run python wayfinder_paths/scripts/make_wallets.py --label "strategy_name"`.
508
505
 
@@ -538,9 +535,9 @@ cp wayfinder_paths/config.example.json config.json
538
535
  # - user.username: Your Wayfinder username
539
536
  # - user.password: Your Wayfinder password
540
537
  # - OR user.refresh_token: Your refresh token
541
- # - system.wallets_path: Path to wallets.json (default: "wallets.json")
538
+ # - system.wallets_path: Path to config.json (default: "config.json")
542
539
  #
543
- # Wallet addresses are auto-loaded from wallets.json by default.
540
+ # Wallet addresses are auto-loaded from config.json by default.
544
541
  # Then run with:
545
542
  poetry run python wayfinder_paths/run_strategy.py stablecoin_yield_strategy --config config.json
546
543
  ```