wayfinder-paths 0.1.13__py3-none-any.whl → 0.1.15__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 +73 -32
- 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 +144 -78
- wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +127 -65
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +30 -14
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +121 -67
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +332 -9
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +13 -13
- wayfinder_paths/adapters/pool_adapter/README.md +9 -10
- wayfinder_paths/adapters/pool_adapter/adapter.py +9 -10
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
- 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 +9 -7
- wayfinder_paths/core/clients/BRAPClient.py +102 -61
- wayfinder_paths/core/clients/ClientManager.py +1 -68
- wayfinder_paths/core/clients/HyperlendClient.py +125 -64
- wayfinder_paths/core/clients/LedgerClient.py +1 -4
- wayfinder_paths/core/clients/PoolClient.py +122 -48
- wayfinder_paths/core/clients/TokenClient.py +91 -36
- wayfinder_paths/core/clients/WalletClient.py +26 -56
- wayfinder_paths/core/clients/WayfinderClient.py +28 -160
- wayfinder_paths/core/clients/__init__.py +0 -2
- wayfinder_paths/core/clients/protocols.py +35 -46
- wayfinder_paths/core/clients/sdk_example.py +37 -22
- wayfinder_paths/core/constants/erc20_abi.py +0 -11
- wayfinder_paths/core/engine/StrategyJob.py +10 -56
- wayfinder_paths/core/services/base.py +1 -0
- wayfinder_paths/core/services/local_evm_txn.py +25 -9
- wayfinder_paths/core/services/local_token_txn.py +2 -6
- wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
- wayfinder_paths/core/strategies/Strategy.py +16 -4
- wayfinder_paths/core/utils/evm_helpers.py +2 -9
- wayfinder_paths/policies/erc20.py +1 -1
- wayfinder_paths/run_strategy.py +13 -19
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +77 -11
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +6 -6
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +107 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -5
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2246 -1279
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +276 -109
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +1 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +153 -56
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +16 -12
- 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.13.dist-info → wayfinder_paths-0.1.15.dist-info}/METADATA +14 -49
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/RECORD +59 -60
- wayfinder_paths/abis/generic/erc20.json +0 -383
- wayfinder_paths/core/clients/AuthClient.py +0 -83
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/WHEEL +0 -0
|
@@ -57,12 +57,10 @@ class Strategy(ABC):
|
|
|
57
57
|
main_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
58
58
|
strategy_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
59
59
|
web3_service: Web3Service | None = None,
|
|
60
|
-
api_key: str | None = None,
|
|
61
60
|
):
|
|
62
61
|
self.adapters = {}
|
|
63
62
|
self.ledger_adapter = None
|
|
64
63
|
self.logger = logger.bind(strategy=self.__class__.__name__)
|
|
65
|
-
# Note: api_key is passed to ClientManager, not set in environment
|
|
66
64
|
self.config = config
|
|
67
65
|
|
|
68
66
|
async def setup(self) -> None:
|
|
@@ -145,8 +143,22 @@ class Strategy(ABC):
|
|
|
145
143
|
@abstractmethod
|
|
146
144
|
async def update(self) -> StatusTuple:
|
|
147
145
|
"""
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
Deploy funds to protocols (no main wallet access).
|
|
147
|
+
Called after deposit() has transferred assets to strategy wallet.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Tuple of (success: bool, message: str)
|
|
151
|
+
"""
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
@abstractmethod
|
|
155
|
+
async def exit(self, **kwargs) -> StatusTuple:
|
|
156
|
+
"""
|
|
157
|
+
Transfer funds from strategy wallet to main wallet.
|
|
158
|
+
Called after withdraw() has liquidated all positions.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Tuple of (success: bool, message: str)
|
|
150
162
|
"""
|
|
151
163
|
pass
|
|
152
164
|
|
|
@@ -7,7 +7,6 @@ across multiple adapters, extracted from evm_transaction_adapter.
|
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
-
from pathlib import Path
|
|
11
10
|
from typing import Any
|
|
12
11
|
|
|
13
12
|
from loguru import logger
|
|
@@ -43,12 +42,12 @@ def resolve_chain_id(token_info: dict[str, Any], logger_instance=None) -> int |
|
|
|
43
42
|
"""
|
|
44
43
|
log = logger_instance or logger
|
|
45
44
|
chain_meta = token_info.get("chain") or {}
|
|
46
|
-
chain_id = chain_meta.get("
|
|
45
|
+
chain_id = chain_meta.get("id")
|
|
47
46
|
try:
|
|
48
47
|
if chain_id is not None:
|
|
49
48
|
return int(chain_id)
|
|
50
49
|
except (ValueError, TypeError):
|
|
51
|
-
log.debug("Invalid chain_id in token_info.
|
|
50
|
+
log.debug("Invalid chain_id in token_info.chain: %s", chain_id)
|
|
52
51
|
return chain_code_to_chain_id(chain_meta.get("code"))
|
|
53
52
|
|
|
54
53
|
|
|
@@ -170,9 +169,3 @@ async def get_abi_filtered(
|
|
|
170
169
|
if item.get("type") == "function" and item.get("name") in function_names
|
|
171
170
|
]
|
|
172
171
|
return filtered_abi
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
with open(Path(__file__).parent.parent.parent.joinpath("abis/generic/erc20.json")) as f:
|
|
176
|
-
erc20_abi_raw = f.read()
|
|
177
|
-
|
|
178
|
-
ERC20_ABI = json.loads(erc20_abi_raw)
|
wayfinder_paths/run_strategy.py
CHANGED
|
@@ -20,7 +20,6 @@ def load_strategy(
|
|
|
20
20
|
strategy_name: str,
|
|
21
21
|
*,
|
|
22
22
|
strategy_config: dict | None = None,
|
|
23
|
-
api_key: str | None = None,
|
|
24
23
|
):
|
|
25
24
|
"""
|
|
26
25
|
Dynamically load a strategy by name
|
|
@@ -28,7 +27,6 @@ def load_strategy(
|
|
|
28
27
|
Args:
|
|
29
28
|
strategy_name: Name of the strategy to load (directory name in strategies/)
|
|
30
29
|
strategy_config: Configuration dict for the strategy
|
|
31
|
-
api_key: Optional API key for service account authentication
|
|
32
30
|
|
|
33
31
|
Returns:
|
|
34
32
|
Strategy instance
|
|
@@ -70,7 +68,7 @@ def load_strategy(
|
|
|
70
68
|
if strategy_class is None:
|
|
71
69
|
raise ValueError(f"No Strategy class found in {module_path}")
|
|
72
70
|
|
|
73
|
-
return strategy_class(config=strategy_config
|
|
71
|
+
return strategy_class(config=strategy_config)
|
|
74
72
|
|
|
75
73
|
|
|
76
74
|
def load_config(
|
|
@@ -135,18 +133,15 @@ async def run_strategy(
|
|
|
135
133
|
# Load configuration with strategy name for wallet lookup
|
|
136
134
|
logger.debug(f"Config path provided: {config_path}")
|
|
137
135
|
config = load_config(config_path, strategy_name=strategy_name)
|
|
138
|
-
creds = (
|
|
139
|
-
"yes"
|
|
140
|
-
if (config.user.username and config.user.password)
|
|
141
|
-
or config.user.refresh_token
|
|
142
|
-
else "no"
|
|
143
|
-
)
|
|
144
|
-
main_wallet = config.user.main_wallet_address or "none"
|
|
145
|
-
strategy_wallet = config.user.strategy_wallet_address or "none"
|
|
146
136
|
logger.debug(
|
|
147
|
-
|
|
137
|
+
"Loaded config: wallets(main={} strategy={})",
|
|
138
|
+
config.user.main_wallet_address or "none",
|
|
139
|
+
config.user.strategy_wallet_address or "none",
|
|
148
140
|
)
|
|
149
141
|
|
|
142
|
+
# Validate required configuration
|
|
143
|
+
# Authentication is via system.api_key in config.json
|
|
144
|
+
|
|
150
145
|
# Load strategy with the enriched config
|
|
151
146
|
strategy = load_strategy(
|
|
152
147
|
strategy_name,
|
|
@@ -159,13 +154,7 @@ async def run_strategy(
|
|
|
159
154
|
|
|
160
155
|
# Setup strategy job
|
|
161
156
|
logger.info("Setting up strategy job...")
|
|
162
|
-
|
|
163
|
-
"credentials"
|
|
164
|
-
if (config.user.username and config.user.password)
|
|
165
|
-
or config.user.refresh_token
|
|
166
|
-
else "missing"
|
|
167
|
-
)
|
|
168
|
-
logger.debug(f"Auth mode: {auth_mode}")
|
|
157
|
+
logger.debug("Auth mode: API key (from system.api_key)")
|
|
169
158
|
await strategy_job.setup()
|
|
170
159
|
|
|
171
160
|
# Execute action
|
|
@@ -208,6 +197,10 @@ async def run_strategy(
|
|
|
208
197
|
result = await strategy_job.execute_strategy("update")
|
|
209
198
|
logger.info(f"Update result: {result}")
|
|
210
199
|
|
|
200
|
+
elif action == "exit":
|
|
201
|
+
result = await strategy_job.execute_strategy("exit")
|
|
202
|
+
logger.info(f"Exit result: {result}")
|
|
203
|
+
|
|
211
204
|
elif action == "partial-liquidate":
|
|
212
205
|
usd_value = kwargs.get("amount")
|
|
213
206
|
if not usd_value:
|
|
@@ -303,6 +296,7 @@ def main():
|
|
|
303
296
|
"withdraw",
|
|
304
297
|
"status",
|
|
305
298
|
"update",
|
|
299
|
+
"exit",
|
|
306
300
|
"policy",
|
|
307
301
|
"script",
|
|
308
302
|
"partial-liquidate",
|
|
@@ -203,9 +203,8 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
203
203
|
strategy_wallet: dict[str, Any] | None = None,
|
|
204
204
|
web3_service: Web3Service | None = None,
|
|
205
205
|
hyperliquid_executor: HyperliquidExecutor | None = None,
|
|
206
|
-
api_key: str | None = None,
|
|
207
206
|
) -> None:
|
|
208
|
-
super().__init__(
|
|
207
|
+
super().__init__()
|
|
209
208
|
|
|
210
209
|
merged_config = dict(config or {})
|
|
211
210
|
if main_wallet:
|
|
@@ -479,7 +478,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
479
478
|
gas_ok,
|
|
480
479
|
gas_res,
|
|
481
480
|
) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
|
|
482
|
-
|
|
481
|
+
query="ethereum-arbitrum", # Native ETH on Arbitrum
|
|
483
482
|
amount=gas_token_amount,
|
|
484
483
|
strategy_name=self.name or "basis_trading_strategy",
|
|
485
484
|
skip_ledger=True,
|
|
@@ -500,7 +499,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
500
499
|
strategy_balance_ok,
|
|
501
500
|
strategy_balance,
|
|
502
501
|
) = await self.balance_adapter.get_balance(
|
|
503
|
-
|
|
502
|
+
query=USDC_ARBITRUM_TOKEN_ID,
|
|
504
503
|
wallet_address=strategy_address,
|
|
505
504
|
)
|
|
506
505
|
strategy_usdc = 0.0
|
|
@@ -518,7 +517,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
518
517
|
move_ok,
|
|
519
518
|
move_res,
|
|
520
519
|
) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
|
|
521
|
-
|
|
520
|
+
query=USDC_ARBITRUM_TOKEN_ID,
|
|
522
521
|
amount=need_to_move,
|
|
523
522
|
strategy_name=self.name or "basis_trading_strategy",
|
|
524
523
|
skip_ledger=True,
|
|
@@ -543,7 +542,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
543
542
|
|
|
544
543
|
# Send USDC to bridge address (deposit credits the sender address on Hyperliquid)
|
|
545
544
|
success, result = await self.balance_adapter.send_to_address(
|
|
546
|
-
|
|
545
|
+
query=USDC_ARBITRUM_TOKEN_ID,
|
|
547
546
|
amount=main_token_amount,
|
|
548
547
|
from_wallet=strategy_wallet,
|
|
549
548
|
to_address=HYPERLIQUID_BRIDGE_ADDRESS,
|
|
@@ -821,7 +820,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
821
820
|
strategy_usdc = 0.0
|
|
822
821
|
try:
|
|
823
822
|
success, balance_data = await self.balance_adapter.get_balance(
|
|
824
|
-
|
|
823
|
+
query=usdc_token_id,
|
|
825
824
|
wallet_address=address,
|
|
826
825
|
)
|
|
827
826
|
if success:
|
|
@@ -866,7 +865,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
866
865
|
send_success,
|
|
867
866
|
send_result,
|
|
868
867
|
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
869
|
-
|
|
868
|
+
query=usdc_token_id,
|
|
870
869
|
amount=amount_to_send,
|
|
871
870
|
strategy_name=self.name,
|
|
872
871
|
skip_ledger=False,
|
|
@@ -1011,7 +1010,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1011
1010
|
final_balance = 0.0
|
|
1012
1011
|
try:
|
|
1013
1012
|
success, balance_data = await self.balance_adapter.get_balance(
|
|
1014
|
-
|
|
1013
|
+
query=usdc_token_id,
|
|
1015
1014
|
wallet_address=address,
|
|
1016
1015
|
)
|
|
1017
1016
|
if success:
|
|
@@ -1032,7 +1031,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1032
1031
|
send_success,
|
|
1033
1032
|
send_result,
|
|
1034
1033
|
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
1035
|
-
|
|
1034
|
+
query=usdc_token_id,
|
|
1036
1035
|
amount=amount_to_send,
|
|
1037
1036
|
strategy_name=self.name,
|
|
1038
1037
|
skip_ledger=False, # Record in ledger
|
|
@@ -1073,6 +1072,73 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1073
1072
|
f"Withdrew ${total_withdrawn:.2f} total to main wallet ({main_address}).",
|
|
1074
1073
|
)
|
|
1075
1074
|
|
|
1075
|
+
async def exit(self, **kwargs) -> StatusTuple:
|
|
1076
|
+
"""Transfer funds from strategy wallet to main wallet."""
|
|
1077
|
+
self.logger.info("EXIT: Transferring remaining funds to main wallet")
|
|
1078
|
+
|
|
1079
|
+
strategy_address = self._get_strategy_wallet_address()
|
|
1080
|
+
main_address = self._get_main_wallet_address()
|
|
1081
|
+
|
|
1082
|
+
if strategy_address.lower() == main_address.lower():
|
|
1083
|
+
return (True, "Main wallet is strategy wallet, no transfer needed")
|
|
1084
|
+
|
|
1085
|
+
transferred_items = []
|
|
1086
|
+
|
|
1087
|
+
# Transfer USDC to main wallet
|
|
1088
|
+
usdc_ok, usdc_raw = await self.balance_adapter.get_balance(
|
|
1089
|
+
token_id=USDC_ARBITRUM_TOKEN_ID,
|
|
1090
|
+
wallet_address=strategy_address,
|
|
1091
|
+
)
|
|
1092
|
+
if usdc_ok and usdc_raw:
|
|
1093
|
+
usdc_balance = float(usdc_raw.get("balance", 0))
|
|
1094
|
+
if usdc_balance > 1.0:
|
|
1095
|
+
self.logger.info(f"Transferring {usdc_balance:.2f} USDC to main wallet")
|
|
1096
|
+
(
|
|
1097
|
+
success,
|
|
1098
|
+
msg,
|
|
1099
|
+
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
1100
|
+
query=USDC_ARBITRUM_TOKEN_ID,
|
|
1101
|
+
amount=usdc_balance,
|
|
1102
|
+
strategy_name=self.name,
|
|
1103
|
+
skip_ledger=False,
|
|
1104
|
+
)
|
|
1105
|
+
if success:
|
|
1106
|
+
transferred_items.append(f"{usdc_balance:.2f} USDC")
|
|
1107
|
+
else:
|
|
1108
|
+
self.logger.warning(f"USDC transfer failed: {msg}")
|
|
1109
|
+
|
|
1110
|
+
# Transfer ETH (minus reserve for tx fees) to main wallet
|
|
1111
|
+
eth_ok, eth_raw = await self.balance_adapter.get_balance(
|
|
1112
|
+
token_id="ethereum-arbitrum",
|
|
1113
|
+
wallet_address=strategy_address,
|
|
1114
|
+
)
|
|
1115
|
+
if eth_ok and eth_raw:
|
|
1116
|
+
eth_balance = float(eth_raw.get("balance", 0))
|
|
1117
|
+
tx_fee_reserve = 0.0002
|
|
1118
|
+
transferable_eth = eth_balance - tx_fee_reserve
|
|
1119
|
+
if transferable_eth > 0.0001:
|
|
1120
|
+
self.logger.info(
|
|
1121
|
+
f"Transferring {transferable_eth:.6f} ETH to main wallet"
|
|
1122
|
+
)
|
|
1123
|
+
(
|
|
1124
|
+
success,
|
|
1125
|
+
msg,
|
|
1126
|
+
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
1127
|
+
query="ethereum-arbitrum",
|
|
1128
|
+
amount=transferable_eth,
|
|
1129
|
+
strategy_name=self.name,
|
|
1130
|
+
skip_ledger=False,
|
|
1131
|
+
)
|
|
1132
|
+
if success:
|
|
1133
|
+
transferred_items.append(f"{transferable_eth:.6f} ETH")
|
|
1134
|
+
else:
|
|
1135
|
+
self.logger.warning(f"ETH transfer failed: {msg}")
|
|
1136
|
+
|
|
1137
|
+
if not transferred_items:
|
|
1138
|
+
return (True, "No funds to transfer to main wallet")
|
|
1139
|
+
|
|
1140
|
+
return (True, f"Transferred to main wallet: {', '.join(transferred_items)}")
|
|
1141
|
+
|
|
1076
1142
|
async def _status(self) -> StatusDict:
|
|
1077
1143
|
"""Return portfolio value and strategy status with live data."""
|
|
1078
1144
|
total_value, hl_value, vault_value = await self._get_total_portfolio_value()
|
|
@@ -2569,7 +2635,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2569
2635
|
try:
|
|
2570
2636
|
strategy_address = self._get_strategy_wallet_address()
|
|
2571
2637
|
success, balance = await self.balance_adapter.get_balance(
|
|
2572
|
-
|
|
2638
|
+
query=USDC_ARBITRUM_TOKEN_ID,
|
|
2573
2639
|
wallet_address=strategy_address,
|
|
2574
2640
|
)
|
|
2575
2641
|
if success and balance:
|
|
@@ -461,7 +461,7 @@ class TestBasisTradingStrategy:
|
|
|
461
461
|
# Try to scale with $5 (below $10 minimum notional)
|
|
462
462
|
# With 2x leverage, order_usd = 5 * (2/3) = 3.33, below $10
|
|
463
463
|
success, msg = await strategy._scale_up_position(5.0)
|
|
464
|
-
assert success
|
|
464
|
+
assert success # Returns success=True but with message
|
|
465
465
|
assert "below minimum notional" in msg
|
|
466
466
|
|
|
467
467
|
@pytest.mark.asyncio
|
|
@@ -563,7 +563,7 @@ class TestBasisTradingStrategy:
|
|
|
563
563
|
|
|
564
564
|
# Should have called fill_pair_units to scale up
|
|
565
565
|
assert mock_filler.fill_pair_units.called
|
|
566
|
-
assert success
|
|
566
|
+
assert success
|
|
567
567
|
|
|
568
568
|
@pytest.mark.asyncio
|
|
569
569
|
async def test_ensure_builder_fee_approved_already_approved(
|
|
@@ -612,7 +612,7 @@ class TestBasisTradingStrategy:
|
|
|
612
612
|
)
|
|
613
613
|
|
|
614
614
|
success, msg = await s.ensure_builder_fee_approved()
|
|
615
|
-
assert success
|
|
615
|
+
assert success
|
|
616
616
|
assert "already approved" in msg.lower()
|
|
617
617
|
# Should not have called approve_builder_fee
|
|
618
618
|
mock_hyperliquid_adapter.approve_builder_fee.assert_not_called()
|
|
@@ -661,7 +661,7 @@ class TestBasisTradingStrategy:
|
|
|
661
661
|
)
|
|
662
662
|
|
|
663
663
|
success, msg = await s.ensure_builder_fee_approved()
|
|
664
|
-
assert success
|
|
664
|
+
assert success
|
|
665
665
|
assert "approved" in msg.lower()
|
|
666
666
|
# Should have called approve_builder_fee
|
|
667
667
|
mock_hyperliquid_adapter.approve_builder_fee.assert_called_once()
|
|
@@ -832,7 +832,7 @@ class TestBasisTradingStrategy:
|
|
|
832
832
|
mock_filler_class.return_value = mock_filler
|
|
833
833
|
|
|
834
834
|
success, _ = await strategy.update()
|
|
835
|
-
assert success
|
|
835
|
+
assert success
|
|
836
836
|
|
|
837
837
|
# Target spot was $66.67, so we should transfer $33.33 spot->perp.
|
|
838
838
|
mock_hyperliquid_adapter.transfer_spot_to_perp.assert_called_once()
|
|
@@ -907,7 +907,7 @@ class TestBasisTradingStrategy:
|
|
|
907
907
|
strategy._find_and_open_position = AsyncMock(return_value=(True, "redeployed"))
|
|
908
908
|
|
|
909
909
|
success, msg = await strategy.update()
|
|
910
|
-
assert success
|
|
910
|
+
assert success
|
|
911
911
|
assert msg == "redeployed"
|
|
912
912
|
strategy._close_position.assert_awaited_once()
|
|
913
913
|
strategy._find_and_open_position.assert_awaited_once()
|
|
@@ -197,9 +197,8 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
197
197
|
main_wallet: dict[str, Any] | None = None,
|
|
198
198
|
strategy_wallet: dict[str, Any] | None = None,
|
|
199
199
|
web3_service: Web3Service = None,
|
|
200
|
-
api_key: str | None = None,
|
|
201
200
|
):
|
|
202
|
-
super().__init__(
|
|
201
|
+
super().__init__()
|
|
203
202
|
merged_config: dict[str, Any] = dict(config or {})
|
|
204
203
|
if main_wallet is not None:
|
|
205
204
|
merged_config["main_wallet"] = main_wallet
|
|
@@ -350,7 +349,7 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
350
349
|
success,
|
|
351
350
|
main_usdt0_balance,
|
|
352
351
|
) = await self.balance_adapter.get_balance(
|
|
353
|
-
|
|
352
|
+
query=self.usdt_token_info.get("token_id"),
|
|
354
353
|
wallet_address=self._get_main_wallet_address(),
|
|
355
354
|
)
|
|
356
355
|
if not success:
|
|
@@ -363,7 +362,7 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
363
362
|
success,
|
|
364
363
|
main_hype_balance,
|
|
365
364
|
) = await self.balance_adapter.get_balance(
|
|
366
|
-
|
|
365
|
+
query=self.hype_token_info.get("token_id"),
|
|
367
366
|
wallet_address=self._get_main_wallet_address(),
|
|
368
367
|
)
|
|
369
368
|
if not success:
|
|
@@ -530,11 +529,10 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
530
529
|
return self._assets_snapshot
|
|
531
530
|
|
|
532
531
|
_, snapshot = await self.hyperlend_adapter.get_assets_view(
|
|
533
|
-
chain_id=self.hype_token_info.get("chain").get("id"),
|
|
534
532
|
user_address=self._get_strategy_wallet_address(),
|
|
535
533
|
)
|
|
536
534
|
|
|
537
|
-
assets = snapshot.get("
|
|
535
|
+
assets = snapshot.get("assets", [])
|
|
538
536
|
asset_map = {}
|
|
539
537
|
|
|
540
538
|
for asset in assets:
|
|
@@ -582,11 +580,9 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
582
580
|
|
|
583
581
|
try:
|
|
584
582
|
_, data = await self.hyperlend_adapter.get_stable_markets(
|
|
585
|
-
chain_id=self.hype_token_info.get("chain").get("id"),
|
|
586
583
|
required_underlying_tokens=required_tokens,
|
|
587
584
|
buffer_bps=self.SUPPLY_CAP_BUFFER_BPS,
|
|
588
585
|
min_buffer_tokens=self.SUPPLY_CAP_MIN_BUFFER_TOKENS,
|
|
589
|
-
is_stable_symbol=True,
|
|
590
586
|
)
|
|
591
587
|
markets = data.get("markets", {}) if isinstance(data, dict) else {}
|
|
592
588
|
except Exception:
|
|
@@ -609,7 +605,7 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
609
605
|
async def _get_lent_positions(self, snapshot=None) -> dict[str, dict[str, Any]]:
|
|
610
606
|
if not snapshot:
|
|
611
607
|
snapshot = await self._get_assets_snapshot()
|
|
612
|
-
assets = snapshot.get("
|
|
608
|
+
assets = snapshot.get("assets", None)
|
|
613
609
|
|
|
614
610
|
if not assets:
|
|
615
611
|
return {}
|
|
@@ -630,7 +626,14 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
630
626
|
continue
|
|
631
627
|
|
|
632
628
|
try:
|
|
633
|
-
|
|
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
|
+
)
|
|
634
637
|
if not success or not isinstance(token, dict):
|
|
635
638
|
logger.info(f"Error getting token for asset: {asset}")
|
|
636
639
|
continue
|
|
@@ -862,7 +865,7 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
862
865
|
|
|
863
866
|
try:
|
|
864
867
|
_, total_usdt_wei = await self.balance_adapter.get_balance(
|
|
865
|
-
|
|
868
|
+
query=self.usdt_token_info.get("token_id"),
|
|
866
869
|
wallet_address=self._get_strategy_wallet_address(),
|
|
867
870
|
)
|
|
868
871
|
except Exception:
|
|
@@ -891,7 +894,7 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
891
894
|
|
|
892
895
|
try:
|
|
893
896
|
_, total_hype_wei = await self.balance_adapter.get_balance(
|
|
894
|
-
|
|
897
|
+
query=self.hype_token_info.get("token_id"),
|
|
895
898
|
wallet_address=self._get_strategy_wallet_address(),
|
|
896
899
|
)
|
|
897
900
|
except Exception:
|
|
@@ -932,6 +935,75 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
932
935
|
|
|
933
936
|
return (True, ". ".join(messages))
|
|
934
937
|
|
|
938
|
+
async def exit(self, **kwargs) -> StatusTuple:
|
|
939
|
+
"""Transfer funds from strategy wallet to main wallet."""
|
|
940
|
+
self.logger.info("EXIT: Transferring remaining funds to main wallet")
|
|
941
|
+
|
|
942
|
+
strategy_address = self._get_strategy_wallet_address()
|
|
943
|
+
main_address = self._get_main_wallet_address()
|
|
944
|
+
|
|
945
|
+
if strategy_address.lower() == main_address.lower():
|
|
946
|
+
return (True, "Main wallet is strategy wallet, no transfer needed")
|
|
947
|
+
|
|
948
|
+
transferred_items = []
|
|
949
|
+
|
|
950
|
+
# Transfer USDT0 to main wallet
|
|
951
|
+
usdt_ok, usdt_raw = await self.balance_adapter.get_balance(
|
|
952
|
+
token_id="usdt0-hyperevm",
|
|
953
|
+
wallet_address=strategy_address,
|
|
954
|
+
)
|
|
955
|
+
if usdt_ok and usdt_raw:
|
|
956
|
+
usdt_balance = float(usdt_raw.get("balance", 0))
|
|
957
|
+
if usdt_balance > 1.0:
|
|
958
|
+
self.logger.info(
|
|
959
|
+
f"Transferring {usdt_balance:.2f} USDT0 to main wallet"
|
|
960
|
+
)
|
|
961
|
+
(
|
|
962
|
+
success,
|
|
963
|
+
msg,
|
|
964
|
+
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
965
|
+
query="usdt0-hyperevm",
|
|
966
|
+
amount=usdt_balance,
|
|
967
|
+
strategy_name=self.name,
|
|
968
|
+
skip_ledger=False,
|
|
969
|
+
)
|
|
970
|
+
if success:
|
|
971
|
+
transferred_items.append(f"{usdt_balance:.2f} USDT0")
|
|
972
|
+
else:
|
|
973
|
+
self.logger.warning(f"USDT0 transfer failed: {msg}")
|
|
974
|
+
|
|
975
|
+
# Transfer HYPE (minus reserve for tx fees) to main wallet
|
|
976
|
+
hype_ok, hype_raw = await self.balance_adapter.get_balance(
|
|
977
|
+
token_id="hyperliquid-hyperevm",
|
|
978
|
+
wallet_address=strategy_address,
|
|
979
|
+
)
|
|
980
|
+
if hype_ok and hype_raw:
|
|
981
|
+
hype_balance = float(hype_raw.get("balance", 0))
|
|
982
|
+
tx_fee_reserve = 0.1 # Reserve 0.1 HYPE for tx fees
|
|
983
|
+
transferable_hype = hype_balance - tx_fee_reserve
|
|
984
|
+
if transferable_hype > 0.01:
|
|
985
|
+
self.logger.info(
|
|
986
|
+
f"Transferring {transferable_hype:.4f} HYPE to main wallet"
|
|
987
|
+
)
|
|
988
|
+
(
|
|
989
|
+
success,
|
|
990
|
+
msg,
|
|
991
|
+
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
992
|
+
query="hyperliquid-hyperevm",
|
|
993
|
+
amount=transferable_hype,
|
|
994
|
+
strategy_name=self.name,
|
|
995
|
+
skip_ledger=False,
|
|
996
|
+
)
|
|
997
|
+
if success:
|
|
998
|
+
transferred_items.append(f"{transferable_hype:.4f} HYPE")
|
|
999
|
+
else:
|
|
1000
|
+
self.logger.warning(f"HYPE transfer failed: {msg}")
|
|
1001
|
+
|
|
1002
|
+
if not transferred_items:
|
|
1003
|
+
return (True, "No funds to transfer to main wallet")
|
|
1004
|
+
|
|
1005
|
+
return (True, f"Transferred to main wallet: {', '.join(transferred_items)}")
|
|
1006
|
+
|
|
935
1007
|
async def _swap_residual_balances_to_token(
|
|
936
1008
|
self, token_info: dict[str, Any], include_native: bool = False
|
|
937
1009
|
) -> list[str]:
|
|
@@ -987,7 +1059,7 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
987
1059
|
success,
|
|
988
1060
|
message,
|
|
989
1061
|
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
990
|
-
|
|
1062
|
+
query=token_info.get("token_id"),
|
|
991
1063
|
amount=amount_tokens,
|
|
992
1064
|
strategy_name=self.name,
|
|
993
1065
|
)
|
|
@@ -1549,11 +1621,9 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
1549
1621
|
)
|
|
1550
1622
|
|
|
1551
1623
|
_, stable_markets = await self.hyperlend_adapter.get_stable_markets(
|
|
1552
|
-
chain_id=self.hype_token_info.get("chain").get("id"),
|
|
1553
1624
|
required_underlying_tokens=required_underlying_tokens,
|
|
1554
1625
|
buffer_bps=self.SUPPLY_CAP_BUFFER_BPS,
|
|
1555
1626
|
min_buffer_tokens=self.SUPPLY_CAP_MIN_BUFFER_TOKENS,
|
|
1556
|
-
is_stable_symbol=True,
|
|
1557
1627
|
)
|
|
1558
1628
|
filtered_notes = stable_markets.get("notes", [])
|
|
1559
1629
|
filtered_map = stable_markets.get("markets", {})
|
|
@@ -1585,8 +1655,7 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
1585
1655
|
if current_checksum_lower not in existing_addresses:
|
|
1586
1656
|
try:
|
|
1587
1657
|
_, current_entry = await self.hyperlend_adapter.get_market_entry(
|
|
1588
|
-
|
|
1589
|
-
token_address=current_checksum_value,
|
|
1658
|
+
token=current_checksum_value,
|
|
1590
1659
|
)
|
|
1591
1660
|
except Exception:
|
|
1592
1661
|
current_entry = None
|
|
@@ -1633,8 +1702,7 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
1633
1702
|
histories = await asyncio.gather(
|
|
1634
1703
|
*[
|
|
1635
1704
|
self.hyperlend_adapter.get_lend_rate_history(
|
|
1636
|
-
|
|
1637
|
-
token_address=addr,
|
|
1705
|
+
token=addr,
|
|
1638
1706
|
lookback_hours=lookback_hours,
|
|
1639
1707
|
)
|
|
1640
1708
|
for addr, _ in filtered
|
|
@@ -1656,7 +1724,7 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
1656
1724
|
if not history_status:
|
|
1657
1725
|
continue
|
|
1658
1726
|
history_data = history[1]
|
|
1659
|
-
for row in history_data.get("
|
|
1727
|
+
for row in history_data.get("history", []):
|
|
1660
1728
|
ts_ms = row.get("timestamp_ms")
|
|
1661
1729
|
if ts_ms is None:
|
|
1662
1730
|
continue
|
|
@@ -1812,7 +1880,14 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
1812
1880
|
token = None
|
|
1813
1881
|
if address:
|
|
1814
1882
|
try:
|
|
1815
|
-
|
|
1883
|
+
chain_id = None
|
|
1884
|
+
try:
|
|
1885
|
+
chain_id = int((self.hype_token_info.get("chain") or {}).get("id"))
|
|
1886
|
+
except Exception:
|
|
1887
|
+
chain_id = None
|
|
1888
|
+
success, token = await self.token_adapter.get_token(
|
|
1889
|
+
address.lower(), chain_id=chain_id
|
|
1890
|
+
)
|
|
1816
1891
|
except Exception:
|
|
1817
1892
|
token = None
|
|
1818
1893
|
if not success:
|
|
@@ -2128,7 +2203,16 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
2128
2203
|
continue
|
|
2129
2204
|
|
|
2130
2205
|
try:
|
|
2131
|
-
|
|
2206
|
+
chain_id = None
|
|
2207
|
+
try:
|
|
2208
|
+
chain_id = int(
|
|
2209
|
+
(self.hype_token_info.get("chain") or {}).get("id")
|
|
2210
|
+
)
|
|
2211
|
+
except Exception:
|
|
2212
|
+
chain_id = None
|
|
2213
|
+
success, token = await self.token_adapter.get_token(
|
|
2214
|
+
checksum, chain_id=chain_id
|
|
2215
|
+
)
|
|
2132
2216
|
if not success or not isinstance(token, dict):
|
|
2133
2217
|
continue
|
|
2134
2218
|
except Exception:
|
|
@@ -2239,7 +2323,7 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
2239
2323
|
success,
|
|
2240
2324
|
strategy_hype_balance_wei,
|
|
2241
2325
|
) = await self.balance_adapter.get_balance(
|
|
2242
|
-
|
|
2326
|
+
query=self.hype_token_info.get("token_id"),
|
|
2243
2327
|
wallet_address=self._get_strategy_wallet_address(),
|
|
2244
2328
|
)
|
|
2245
2329
|
hype_price = asset_map.get(WRAPPED_HYPE_ADDRESS, {}).get("price_usd") or 0.0
|