wayfinder-paths 0.1.10__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.
- wayfinder_paths/adapters/balance_adapter/adapter.py +3 -7
- wayfinder_paths/adapters/brap_adapter/adapter.py +10 -13
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +6 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +44 -5
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +104 -0
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +0 -3
- wayfinder_paths/adapters/pool_adapter/README.md +4 -19
- wayfinder_paths/adapters/pool_adapter/adapter.py +4 -29
- wayfinder_paths/adapters/pool_adapter/examples.json +6 -7
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +8 -8
- wayfinder_paths/core/clients/AuthClient.py +2 -2
- wayfinder_paths/core/clients/BRAPClient.py +2 -2
- wayfinder_paths/core/clients/HyperlendClient.py +2 -2
- wayfinder_paths/core/clients/PoolClient.py +18 -54
- wayfinder_paths/core/clients/TokenClient.py +3 -3
- wayfinder_paths/core/clients/WalletClient.py +2 -2
- wayfinder_paths/core/clients/WayfinderClient.py +9 -10
- wayfinder_paths/core/clients/protocols.py +1 -7
- wayfinder_paths/core/config.py +60 -224
- wayfinder_paths/core/services/base.py +0 -55
- wayfinder_paths/core/services/local_evm_txn.py +37 -134
- wayfinder_paths/core/strategies/Strategy.py +3 -3
- wayfinder_paths/core/strategies/descriptors.py +7 -0
- wayfinder_paths/core/utils/evm_helpers.py +5 -28
- wayfinder_paths/core/utils/wallets.py +12 -19
- wayfinder_paths/core/wallets/README.md +1 -1
- wayfinder_paths/run_strategy.py +10 -8
- wayfinder_paths/scripts/create_strategy.py +5 -5
- wayfinder_paths/scripts/make_wallets.py +5 -5
- wayfinder_paths/scripts/run_strategy.py +3 -3
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +196 -515
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +228 -11
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +1 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +1 -1
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +8 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +2 -2
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +25 -25
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +28 -9
- wayfinder_paths/templates/adapter/README.md +1 -1
- {wayfinder_paths-0.1.10.dist-info → wayfinder_paths-0.1.13.dist-info}/METADATA +15 -18
- {wayfinder_paths-0.1.10.dist-info → wayfinder_paths-0.1.13.dist-info}/RECORD +46 -48
- wayfinder_paths/CONFIG_GUIDE.md +0 -390
- wayfinder_paths/config.example.json +0 -22
- wayfinder_paths/core/settings.py +0 -61
- {wayfinder_paths-0.1.10.dist-info → wayfinder_paths-0.1.13.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.10.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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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
|
|
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
|
-
|
|
436
|
+
pool_ids = []
|
|
437
437
|
pool_id = self.current_pool.get("token_id", None)
|
|
438
438
|
if isinstance(pool_id, str):
|
|
439
|
-
|
|
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
|
-
|
|
445
|
+
pool_ids.append(f"{chain_code.lower()}_{pool_address.lower()}")
|
|
446
446
|
|
|
447
447
|
llama_report = None
|
|
448
|
-
if
|
|
449
|
-
success, llama_reports = await self.pool_adapter.
|
|
450
|
-
|
|
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
|
|
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("
|
|
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("
|
|
469
|
+
llama_report.get("combined_apy_pct", 0) / 100
|
|
470
470
|
)
|
|
471
|
-
elif llama_report and llama_report.get("
|
|
472
|
-
self.current_combined_apy_pct = llama_report.get("
|
|
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("
|
|
529
|
-
llama_apy = inferred_entry.get("
|
|
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("
|
|
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.
|
|
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("
|
|
1451
|
-
and pool.get("
|
|
1452
|
-
and pool.get("
|
|
1453
|
-
and pool.get("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
125
|
-
|
|
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.
|
|
137
|
+
s.pool_adapter.get_pools = AsyncMock(
|
|
139
138
|
return_value=(
|
|
140
139
|
True,
|
|
141
140
|
{
|
|
142
141
|
"matches": [
|
|
143
142
|
{
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
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
|
-
"
|
|
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=
|
|
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.
|
|
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,12 +41,12 @@ curl -sSL https://install.python-poetry.org | python3 -
|
|
|
43
41
|
poetry install
|
|
44
42
|
|
|
45
43
|
# ⚠️ Generate test wallets FIRST (required!)
|
|
46
|
-
# This creates
|
|
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
|
|
|
50
48
|
# To test a specific strategy
|
|
51
|
-
just
|
|
49
|
+
just create-wallet stablecoin_yield_strategy
|
|
52
50
|
|
|
53
51
|
# Copy and configure
|
|
54
52
|
cp wayfinder_paths/config.example.json config.json
|
|
@@ -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
|
|
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)
|
|
302
|
+
pool_ids=",".join(pool_ids)
|
|
306
303
|
)
|
|
307
304
|
return (True, data)
|
|
308
305
|
```
|
|
@@ -374,11 +371,11 @@ class MyStrategy(Strategy):
|
|
|
374
371
|
|
|
375
372
|
The following strategies are available and can be run using the CLI:
|
|
376
373
|
|
|
377
|
-
| Strategy
|
|
378
|
-
|
|
379
|
-
| `basis_trading_strategy`
|
|
380
|
-
| `hyperlend_stable_yield_strategy` | Stable yield on HyperLend
|
|
381
|
-
| `moonwell_wsteth_loop_strategy`
|
|
374
|
+
| Strategy | Description | Chain |
|
|
375
|
+
| --------------------------------- | --------------------------- | -------- |
|
|
376
|
+
| `basis_trading_strategy` | Delta-neutral basis trading | - |
|
|
377
|
+
| `hyperlend_stable_yield_strategy` | Stable yield on HyperLend | HyperEVM |
|
|
378
|
+
| `moonwell_wsteth_loop_strategy` | Leveraged wstETH yield loop | Base |
|
|
382
379
|
|
|
383
380
|
#### Running Strategies
|
|
384
381
|
|
|
@@ -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 `
|
|
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
|
|
505
|
-
- `
|
|
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
|
|
538
|
+
# - system.wallets_path: Path to config.json (default: "config.json")
|
|
542
539
|
#
|
|
543
|
-
# Wallet addresses are auto-loaded from
|
|
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
|
```
|