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.
- wayfinder_paths/adapters/balance_adapter/README.md +13 -14
- wayfinder_paths/adapters/balance_adapter/adapter.py +36 -39
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
- wayfinder_paths/adapters/brap_adapter/README.md +11 -16
- wayfinder_paths/adapters/brap_adapter/adapter.py +87 -75
- wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +121 -59
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +22 -23
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +114 -60
- 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 +11 -27
- wayfinder_paths/adapters/pool_adapter/adapter.py +11 -37
- wayfinder_paths/adapters/pool_adapter/examples.json +6 -7
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +8 -8
- wayfinder_paths/adapters/token_adapter/README.md +2 -14
- wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
- wayfinder_paths/adapters/token_adapter/examples.json +4 -8
- wayfinder_paths/adapters/token_adapter/test_adapter.py +5 -3
- wayfinder_paths/core/clients/BRAPClient.py +103 -62
- wayfinder_paths/core/clients/ClientManager.py +1 -68
- wayfinder_paths/core/clients/HyperlendClient.py +127 -66
- wayfinder_paths/core/clients/LedgerClient.py +1 -4
- wayfinder_paths/core/clients/PoolClient.py +126 -88
- wayfinder_paths/core/clients/TokenClient.py +92 -37
- wayfinder_paths/core/clients/WalletClient.py +28 -58
- wayfinder_paths/core/clients/WayfinderClient.py +33 -166
- wayfinder_paths/core/clients/__init__.py +0 -2
- wayfinder_paths/core/clients/protocols.py +35 -52
- wayfinder_paths/core/clients/sdk_example.py +37 -22
- wayfinder_paths/core/config.py +60 -224
- wayfinder_paths/core/engine/StrategyJob.py +7 -55
- wayfinder_paths/core/services/local_evm_txn.py +28 -10
- wayfinder_paths/core/services/local_token_txn.py +1 -1
- wayfinder_paths/core/strategies/Strategy.py +3 -5
- wayfinder_paths/core/strategies/descriptors.py +7 -0
- wayfinder_paths/core/utils/evm_helpers.py +7 -3
- wayfinder_paths/core/utils/wallets.py +12 -19
- wayfinder_paths/core/wallets/README.md +1 -1
- wayfinder_paths/run_strategy.py +8 -17
- 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 +206 -526
- 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 +41 -25
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +1 -1
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +10 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +12 -6
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +3 -3
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +110 -78
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +44 -21
- wayfinder_paths/templates/adapter/README.md +1 -1
- wayfinder_paths/templates/strategy/README.md +3 -3
- wayfinder_paths/templates/strategy/test_strategy.py +3 -2
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/METADATA +21 -59
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/RECORD +64 -65
- wayfinder_paths/core/clients/AuthClient.py +0 -83
- wayfinder_paths/core/settings.py +0 -61
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/LICENSE +0 -0
- {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
|
|
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",
|
|
@@ -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__(
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
719
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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=(
|
|
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
|
|
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,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
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
|
-
|
|
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
|
-
|
|
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")
|