wayfinder-paths 0.1.23__py3-none-any.whl → 0.1.25__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/__init__.py +2 -0
- wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
- wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
- wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
- wayfinder_paths/adapters/boros_adapter/client.py +476 -0
- wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
- wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
- wayfinder_paths/adapters/boros_adapter/types.py +70 -0
- wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
- wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
- wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
- wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
- wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
- wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
- wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
- wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/examples.json +0 -4
- wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
- wayfinder_paths/conftest.py +24 -17
- wayfinder_paths/core/__init__.py +2 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +1 -1
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -2
- wayfinder_paths/core/clients/protocols.py +21 -22
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +12 -0
- wayfinder_paths/core/constants/__init__.py +15 -0
- wayfinder_paths/core/constants/base.py +6 -1
- wayfinder_paths/core/constants/contracts.py +39 -26
- wayfinder_paths/core/constants/erc20_abi.py +0 -1
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
- wayfinder_paths/core/constants/hyperliquid.py +16 -0
- wayfinder_paths/core/constants/moonwell_abi.py +0 -15
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -61
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/transaction.py +44 -1
- wayfinder_paths/core/utils/web3.py +3 -0
- wayfinder_paths/mcp/__init__.py +5 -0
- wayfinder_paths/mcp/preview.py +185 -0
- wayfinder_paths/mcp/scripting.py +84 -0
- wayfinder_paths/mcp/server.py +52 -0
- wayfinder_paths/mcp/state/profile_store.py +195 -0
- wayfinder_paths/mcp/state/store.py +89 -0
- wayfinder_paths/mcp/test_scripting.py +267 -0
- wayfinder_paths/mcp/tools/__init__.py +0 -0
- wayfinder_paths/mcp/tools/balances.py +290 -0
- wayfinder_paths/mcp/tools/discovery.py +158 -0
- wayfinder_paths/mcp/tools/execute.py +770 -0
- wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
- wayfinder_paths/mcp/tools/quotes.py +288 -0
- wayfinder_paths/mcp/tools/run_script.py +286 -0
- wayfinder_paths/mcp/tools/strategies.py +188 -0
- wayfinder_paths/mcp/tools/tokens.py +46 -0
- wayfinder_paths/mcp/tools/wallets.py +354 -0
- wayfinder_paths/mcp/utils.py +129 -0
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
- wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
- wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
- wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
- wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
- wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
- wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.25.dist-info/METADATA +377 -0
- wayfinder_paths-0.1.25.dist-info/RECORD +185 -0
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
- wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Withdrawal operations for BorosHypeStrategy.
|
|
3
|
+
|
|
4
|
+
Kept as a mixin so the main strategy file stays readable without changing behavior.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from wayfinder_paths.adapters.hyperliquid_adapter.adapter import HyperliquidAdapter
|
|
15
|
+
from wayfinder_paths.adapters.hyperliquid_adapter.paired_filler import MIN_NOTIONAL_USD
|
|
16
|
+
from wayfinder_paths.core.strategies import StatusTuple
|
|
17
|
+
from wayfinder_paths.core.utils.transaction import encode_call, send_transaction
|
|
18
|
+
|
|
19
|
+
from .constants import (
|
|
20
|
+
ARBITRUM_CHAIN_ID,
|
|
21
|
+
BOROS_HYPE_MARKET_ID,
|
|
22
|
+
BOROS_HYPE_TOKEN_ID,
|
|
23
|
+
HYPE_NATIVE,
|
|
24
|
+
HYPE_OFT_ADDRESS,
|
|
25
|
+
HYPEREVM_CHAIN_ID,
|
|
26
|
+
KHYPE_LST,
|
|
27
|
+
LOOPED_HYPE,
|
|
28
|
+
MIN_HYPE_GAS,
|
|
29
|
+
USDC_ARB,
|
|
30
|
+
USDT_ARB,
|
|
31
|
+
WHYPE,
|
|
32
|
+
WHYPE_ABI,
|
|
33
|
+
WHYPE_ADDRESS,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BorosHypeWithdrawMixin:
|
|
38
|
+
async def withdraw(self, **kwargs) -> StatusTuple:
|
|
39
|
+
# Liquidates to USDC on Arb but does NOT transfer to main wallet (call exit() after)
|
|
40
|
+
if self.simulation:
|
|
41
|
+
return True, "[SIMULATION] Withdrawal complete"
|
|
42
|
+
|
|
43
|
+
max_wait_s = int(
|
|
44
|
+
kwargs.get("max_wait_s") or kwargs.get("max_wait_seconds") or 20 * 60
|
|
45
|
+
)
|
|
46
|
+
poll_interval_s = int(kwargs.get("poll_interval_s") or 10)
|
|
47
|
+
if max_wait_s < 0:
|
|
48
|
+
max_wait_s = 0
|
|
49
|
+
if poll_interval_s < 1:
|
|
50
|
+
poll_interval_s = 1
|
|
51
|
+
withdraw_start_ts = time.time()
|
|
52
|
+
deadline_ts = withdraw_start_ts + max_wait_s
|
|
53
|
+
|
|
54
|
+
if not self.balance_adapter:
|
|
55
|
+
return False, "Balance adapter not configured"
|
|
56
|
+
if not self.hyperliquid_adapter:
|
|
57
|
+
return False, "Hyperliquid adapter not configured"
|
|
58
|
+
if not self.brap_adapter:
|
|
59
|
+
return False, "BRAP adapter not configured"
|
|
60
|
+
if not self.boros_adapter:
|
|
61
|
+
return False, "Boros adapter not configured"
|
|
62
|
+
if not self._sign_callback:
|
|
63
|
+
return False, "No strategy wallet signing callback configured"
|
|
64
|
+
|
|
65
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
66
|
+
address = strategy_wallet.get("address")
|
|
67
|
+
if not address:
|
|
68
|
+
return False, "No strategy wallet address configured"
|
|
69
|
+
|
|
70
|
+
# Ensure builder fee is approved before placing any orders
|
|
71
|
+
if self.hyperliquid_adapter and self.builder_fee:
|
|
72
|
+
ok, msg = await self.hyperliquid_adapter.ensure_builder_fee_approved(
|
|
73
|
+
address=address,
|
|
74
|
+
builder_fee=self.builder_fee,
|
|
75
|
+
)
|
|
76
|
+
if not ok:
|
|
77
|
+
return False, f"Builder fee approval failed: {msg}"
|
|
78
|
+
logger.info(f"Builder fee status: {msg}")
|
|
79
|
+
|
|
80
|
+
# Get inventory once - use it for initial decisions/logging.
|
|
81
|
+
inv = await self.observe()
|
|
82
|
+
isolated_usd = float(inv.boros_idle_collateral_isolated or 0.0) * float(
|
|
83
|
+
inv.hype_price_usd or 0.0
|
|
84
|
+
)
|
|
85
|
+
cross_usd = float(inv.boros_idle_collateral_cross or 0.0) * float(
|
|
86
|
+
inv.hype_price_usd or 0.0
|
|
87
|
+
)
|
|
88
|
+
logger.info(
|
|
89
|
+
"Withdraw starting. Inventory: "
|
|
90
|
+
f"hl_perp_margin=${inv.hl_perp_margin:.2f}, "
|
|
91
|
+
f"boros_collateral=${inv.boros_collateral_usd:.2f} "
|
|
92
|
+
f"(isolated={inv.boros_idle_collateral_isolated:.6f} HYPE ≈${isolated_usd:.2f}, "
|
|
93
|
+
f"cross={inv.boros_idle_collateral_cross:.6f} HYPE ≈${cross_usd:.2f}), "
|
|
94
|
+
f"boros_position_size=${inv.boros_position_size:.2f}, "
|
|
95
|
+
f"boros_market_ids={inv.boros_position_market_ids}, "
|
|
96
|
+
f"spot=${inv.spot_value_usd:.2f}, "
|
|
97
|
+
f"hl_short={inv.hl_short_size_hype:.4f} HYPE"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
await self._cancel_hl_open_orders_for_hype(address)
|
|
102
|
+
except Exception as exc: # noqa: BLE001
|
|
103
|
+
logger.debug(f"Failed to cancel HL open orders pre-withdraw: {exc}")
|
|
104
|
+
|
|
105
|
+
# ─────────────────────────────────────────────────────────────────
|
|
106
|
+
# STEP 1: Close all Boros positions (settles to USDT, no delta risk)
|
|
107
|
+
# ─────────────────────────────────────────────────────────────────
|
|
108
|
+
market_ids_to_close = inv.boros_position_market_ids or []
|
|
109
|
+
if not market_ids_to_close and inv.boros_position_size > 0:
|
|
110
|
+
try:
|
|
111
|
+
ok_pos, positions = await self.boros_adapter.get_active_positions()
|
|
112
|
+
if ok_pos and isinstance(positions, list):
|
|
113
|
+
mids: set[int] = set()
|
|
114
|
+
for pos in positions:
|
|
115
|
+
mid = pos.get("marketId") or pos.get("market_id")
|
|
116
|
+
try:
|
|
117
|
+
mid_int = int(mid) if mid is not None else None
|
|
118
|
+
except (TypeError, ValueError):
|
|
119
|
+
mid_int = None
|
|
120
|
+
if mid_int and mid_int > 0:
|
|
121
|
+
mids.add(mid_int)
|
|
122
|
+
market_ids_to_close = sorted(mids)
|
|
123
|
+
except Exception as exc: # noqa: BLE001
|
|
124
|
+
logger.warning(f"Failed to fetch Boros market IDs for close: {exc}")
|
|
125
|
+
|
|
126
|
+
if inv.boros_position_size > 0 and not market_ids_to_close:
|
|
127
|
+
logger.warning(
|
|
128
|
+
f"Boros position size > 0 but no market IDs found; trying default {BOROS_HYPE_MARKET_ID}"
|
|
129
|
+
)
|
|
130
|
+
market_ids_to_close = [BOROS_HYPE_MARKET_ID]
|
|
131
|
+
|
|
132
|
+
boros_position_closed = False
|
|
133
|
+
for market_id in market_ids_to_close:
|
|
134
|
+
try:
|
|
135
|
+
ok_close, res_close = await self.boros_adapter.close_positions_market(
|
|
136
|
+
market_id, token_id=BOROS_HYPE_TOKEN_ID
|
|
137
|
+
)
|
|
138
|
+
if ok_close:
|
|
139
|
+
boros_position_closed = True
|
|
140
|
+
logger.info(f"Closed Boros position in market {market_id}")
|
|
141
|
+
else:
|
|
142
|
+
logger.warning(
|
|
143
|
+
f"Failed to close Boros position in market {market_id}: {res_close}"
|
|
144
|
+
)
|
|
145
|
+
except Exception as exc: # noqa: BLE001
|
|
146
|
+
logger.warning(
|
|
147
|
+
f"Failed to close Boros position in market {market_id}: {exc}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if boros_position_closed:
|
|
151
|
+
await asyncio.sleep(5)
|
|
152
|
+
|
|
153
|
+
# ─────────────────────────────────────────────────────────────────
|
|
154
|
+
# STEP 2: Move isolated HYPE collateral to cross margin, then withdraw
|
|
155
|
+
# ─────────────────────────────────────────────────────────────────
|
|
156
|
+
boros_wait_min_hype_raw: int | None = None
|
|
157
|
+
try:
|
|
158
|
+
ok_bal, balances = await self.boros_adapter.get_account_balances(
|
|
159
|
+
token_id=BOROS_HYPE_TOKEN_ID
|
|
160
|
+
)
|
|
161
|
+
if ok_bal and isinstance(balances, dict):
|
|
162
|
+
isolated_hype = float(balances.get("isolated", 0.0))
|
|
163
|
+
cross_hype = float(balances.get("cross", 0.0))
|
|
164
|
+
total_hype = float(balances.get("total", 0.0))
|
|
165
|
+
isolated_positions = balances.get("isolated_positions", [])
|
|
166
|
+
logger.info(
|
|
167
|
+
"Boros balances after position close: "
|
|
168
|
+
f"isolated={isolated_hype:.6f}, cross={cross_hype:.6f}, total={total_hype:.6f}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
for iso_pos in isolated_positions:
|
|
172
|
+
iso_market_id = iso_pos.get("market_id")
|
|
173
|
+
iso_balance = float(iso_pos.get("balance", 0) or 0.0)
|
|
174
|
+
if iso_market_id and iso_balance > 0.001:
|
|
175
|
+
iso_wei = int(
|
|
176
|
+
iso_balance * 1e18
|
|
177
|
+
) # Boros cashTransfer uses 1e18 cash units
|
|
178
|
+
logger.info(
|
|
179
|
+
f"Moving {iso_balance:.6f} collateral from isolated market {iso_market_id} to cross"
|
|
180
|
+
)
|
|
181
|
+
ok_xfer, res_xfer = await self.boros_adapter.cash_transfer(
|
|
182
|
+
market_id=iso_market_id, # Use actual market ID
|
|
183
|
+
amount_wei=iso_wei,
|
|
184
|
+
is_deposit=False, # isolated -> cross
|
|
185
|
+
)
|
|
186
|
+
if ok_xfer:
|
|
187
|
+
await asyncio.sleep(2)
|
|
188
|
+
else:
|
|
189
|
+
logger.warning(
|
|
190
|
+
f"Failed Boros isolated->cross transfer for market {iso_market_id}: {res_xfer}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Re-fetch balances after transfers
|
|
194
|
+
if isolated_positions:
|
|
195
|
+
ok_bal, balances = await self.boros_adapter.get_account_balances(
|
|
196
|
+
token_id=BOROS_HYPE_TOKEN_ID
|
|
197
|
+
)
|
|
198
|
+
if ok_bal and isinstance(balances, dict):
|
|
199
|
+
cross_hype = float(balances.get("cross", 0.0))
|
|
200
|
+
|
|
201
|
+
if cross_hype > 0.001:
|
|
202
|
+
withdraw_native = int(
|
|
203
|
+
cross_hype * 1e18
|
|
204
|
+
) # HYPE native decimals (18)
|
|
205
|
+
|
|
206
|
+
(
|
|
207
|
+
ok_hype0,
|
|
208
|
+
res0,
|
|
209
|
+
) = await self.balance_adapter.get_wallet_balances_multicall(
|
|
210
|
+
assets=[
|
|
211
|
+
{
|
|
212
|
+
"token_address": HYPE_OFT_ADDRESS,
|
|
213
|
+
"chain_id": ARBITRUM_CHAIN_ID,
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
)
|
|
217
|
+
hype_raw_before_int = (
|
|
218
|
+
int(res0[0].get("balance_raw") or 0)
|
|
219
|
+
if ok_hype0
|
|
220
|
+
and isinstance(res0, list)
|
|
221
|
+
and res0
|
|
222
|
+
and res0[0].get("success")
|
|
223
|
+
else 0
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
ok_wd, res_wd = await self.boros_adapter.withdraw_collateral(
|
|
227
|
+
token_id=BOROS_HYPE_TOKEN_ID,
|
|
228
|
+
amount_native=withdraw_native,
|
|
229
|
+
)
|
|
230
|
+
if ok_wd:
|
|
231
|
+
logger.info(
|
|
232
|
+
f"Withdrew {cross_hype:.6f} HYPE collateral from Boros"
|
|
233
|
+
)
|
|
234
|
+
min_expected = max(
|
|
235
|
+
0, int(withdraw_native * 0.99)
|
|
236
|
+
) # 1% tolerance
|
|
237
|
+
boros_wait_min_hype_raw = hype_raw_before_int + min_expected
|
|
238
|
+
else:
|
|
239
|
+
logger.warning(f"Failed to withdraw Boros collateral: {res_wd}")
|
|
240
|
+
except Exception as exc: # noqa: BLE001
|
|
241
|
+
logger.warning(f"Boros collateral withdrawal step failed: {exc}")
|
|
242
|
+
|
|
243
|
+
# ─────────────────────────────────────────────────────────────────
|
|
244
|
+
# STEP 3: Sell spot positions on HyperEVM to HYPE (hedge still active)
|
|
245
|
+
# ─────────────────────────────────────────────────────────────────
|
|
246
|
+
try:
|
|
247
|
+
ok_khype, khype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
248
|
+
KHYPE_LST
|
|
249
|
+
)
|
|
250
|
+
if ok_khype and khype_raw > 0:
|
|
251
|
+
ok, res = await self.brap_adapter.swap_from_token_ids(
|
|
252
|
+
from_token_id=KHYPE_LST,
|
|
253
|
+
to_token_id=HYPE_NATIVE,
|
|
254
|
+
from_address=address,
|
|
255
|
+
amount=str(int(khype_raw)),
|
|
256
|
+
slippage=0.01,
|
|
257
|
+
strategy_name="boros_hype_strategy",
|
|
258
|
+
)
|
|
259
|
+
if ok:
|
|
260
|
+
logger.info(f"Sold kHYPE → HYPE: {khype_raw / 1e18:.4f} kHYPE")
|
|
261
|
+
await asyncio.sleep(2)
|
|
262
|
+
else:
|
|
263
|
+
logger.warning(f"Failed to sell kHYPE → HYPE: {res}")
|
|
264
|
+
|
|
265
|
+
ok_lhype, lhype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
266
|
+
LOOPED_HYPE
|
|
267
|
+
)
|
|
268
|
+
if ok_lhype and lhype_raw > 0:
|
|
269
|
+
ok, res = await self.brap_adapter.swap_from_token_ids(
|
|
270
|
+
from_token_id=LOOPED_HYPE,
|
|
271
|
+
to_token_id=HYPE_NATIVE,
|
|
272
|
+
from_address=address,
|
|
273
|
+
amount=str(int(lhype_raw)),
|
|
274
|
+
slippage=0.01,
|
|
275
|
+
strategy_name="boros_hype_strategy",
|
|
276
|
+
)
|
|
277
|
+
if ok:
|
|
278
|
+
logger.info(
|
|
279
|
+
f"Sold looped HYPE → HYPE: {lhype_raw / 1e18:.4f} lHYPE"
|
|
280
|
+
)
|
|
281
|
+
await asyncio.sleep(2)
|
|
282
|
+
else:
|
|
283
|
+
logger.warning(f"Failed to sell looped HYPE → HYPE: {res}")
|
|
284
|
+
|
|
285
|
+
# Check for WHYPE and unwrap if present (swap may output WHYPE instead of native HYPE)
|
|
286
|
+
ok_whype, whype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
287
|
+
WHYPE
|
|
288
|
+
)
|
|
289
|
+
if ok_whype and whype_raw > 0:
|
|
290
|
+
logger.info(
|
|
291
|
+
f"Unwrapping {float(whype_raw) / 1e18:.4f} WHYPE to native HYPE"
|
|
292
|
+
)
|
|
293
|
+
ok_unwrap, unwrap_res = await self._unwrap_whype(
|
|
294
|
+
address, int(whype_raw)
|
|
295
|
+
)
|
|
296
|
+
if ok_unwrap:
|
|
297
|
+
await asyncio.sleep(2)
|
|
298
|
+
else:
|
|
299
|
+
logger.warning(f"WHYPE unwrap failed: {unwrap_res}")
|
|
300
|
+
except Exception as exc: # noqa: BLE001
|
|
301
|
+
logger.warning(f"Failed selling HyperEVM spot to HYPE: {exc}")
|
|
302
|
+
|
|
303
|
+
# ─────────────────────────────────────────────────────────────────
|
|
304
|
+
# STEP 4: Transfer HYPE from HyperEVM to Hyperliquid spot (keep gas)
|
|
305
|
+
# ─────────────────────────────────────────────────────────────────
|
|
306
|
+
sent_hype_to_hl = False
|
|
307
|
+
sent_hype_to_hl_amount_hype = 0.0
|
|
308
|
+
try:
|
|
309
|
+
ok_hype, hype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
310
|
+
HYPE_NATIVE
|
|
311
|
+
)
|
|
312
|
+
hype_raw_int = int(hype_raw) if ok_hype and hype_raw and hype_raw > 0 else 0
|
|
313
|
+
gas_reserve_wei = int(float(MIN_HYPE_GAS) * 1e18)
|
|
314
|
+
hype_to_transfer_raw = max(0, hype_raw_int - gas_reserve_wei)
|
|
315
|
+
hype_to_transfer = float(hype_to_transfer_raw) / 1e18
|
|
316
|
+
|
|
317
|
+
if hype_to_transfer_raw > int(0.01 * 1e18):
|
|
318
|
+
destination = HyperliquidAdapter.hypercore_index_to_system_address(
|
|
319
|
+
150
|
|
320
|
+
) # native HYPE
|
|
321
|
+
ok_send, send_res = await self.balance_adapter.send_to_address(
|
|
322
|
+
token_id=HYPE_NATIVE,
|
|
323
|
+
amount=int(hype_to_transfer_raw),
|
|
324
|
+
from_wallet=strategy_wallet,
|
|
325
|
+
to_address=destination,
|
|
326
|
+
signing_callback=self._sign_callback,
|
|
327
|
+
)
|
|
328
|
+
if ok_send:
|
|
329
|
+
logger.info(
|
|
330
|
+
f"Transferred {hype_to_transfer:.4f} HYPE to Hyperliquid spot (kept {MIN_HYPE_GAS} for gas)"
|
|
331
|
+
)
|
|
332
|
+
sent_hype_to_hl = True
|
|
333
|
+
sent_hype_to_hl_amount_hype = float(hype_to_transfer)
|
|
334
|
+
await asyncio.sleep(3)
|
|
335
|
+
else:
|
|
336
|
+
logger.warning(
|
|
337
|
+
f"Failed to transfer HYPE to Hyperliquid: {send_res}"
|
|
338
|
+
)
|
|
339
|
+
except Exception as exc: # noqa: BLE001
|
|
340
|
+
logger.warning(f"Failed to transfer HYPE to Hyperliquid: {exc}")
|
|
341
|
+
|
|
342
|
+
# ─────────────────────────────────────────────────────────────────
|
|
343
|
+
# SAFETY CHECK: Verify delta-neutral before closing perp
|
|
344
|
+
# If significant HyperEVM value AND perp exists, wait for spot to arrive
|
|
345
|
+
# ─────────────────────────────────────────────────────────────────
|
|
346
|
+
hyperevm_hype_value = 0.0
|
|
347
|
+
try:
|
|
348
|
+
ok_hype, hype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
349
|
+
HYPE_NATIVE
|
|
350
|
+
)
|
|
351
|
+
ok_whype, whype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
352
|
+
WHYPE
|
|
353
|
+
)
|
|
354
|
+
ok_khype, khype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
355
|
+
KHYPE_LST
|
|
356
|
+
)
|
|
357
|
+
ok_lhype, lhype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
358
|
+
LOOPED_HYPE
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Calculate total HYPE-equivalent value on HyperEVM (above gas reserve)
|
|
362
|
+
native_hype = (float(hype_raw) / 1e18) if ok_hype and hype_raw > 0 else 0.0
|
|
363
|
+
whype_bal = (float(whype_raw) / 1e18) if ok_whype and whype_raw > 0 else 0.0
|
|
364
|
+
khype_bal = (float(khype_raw) / 1e18) if ok_khype and khype_raw > 0 else 0.0
|
|
365
|
+
lhype_bal = (float(lhype_raw) / 1e18) if ok_lhype and lhype_raw > 0 else 0.0
|
|
366
|
+
|
|
367
|
+
# Native HYPE above gas reserve
|
|
368
|
+
hedgeable_hype = max(0.0, native_hype - MIN_HYPE_GAS)
|
|
369
|
+
# WHYPE is 1:1, LSTs use approximate ratio (close enough for safety check)
|
|
370
|
+
hyperevm_hype_value = (
|
|
371
|
+
hedgeable_hype + whype_bal + khype_bal * 1.1 + lhype_bal * 1.1
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
logger.info(
|
|
375
|
+
f"HyperEVM balances: native={native_hype:.4f}, whype={whype_bal:.4f}, "
|
|
376
|
+
f"khype={khype_bal:.4f}, lhype={lhype_bal:.4f}, total={hyperevm_hype_value:.4f}"
|
|
377
|
+
)
|
|
378
|
+
except Exception as exc:
|
|
379
|
+
logger.debug(f"Error checking HyperEVM balances: {exc}")
|
|
380
|
+
|
|
381
|
+
spot_hype_for_check = 0.0
|
|
382
|
+
perp_short_for_check = 0.0
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
(
|
|
386
|
+
ok_spot_chk,
|
|
387
|
+
spot_state_chk,
|
|
388
|
+
) = await self.hyperliquid_adapter.get_spot_user_state(address)
|
|
389
|
+
if ok_spot_chk and isinstance(spot_state_chk, dict):
|
|
390
|
+
for bal in spot_state_chk.get("balances", []):
|
|
391
|
+
if (bal.get("coin") or bal.get("token")) == "HYPE":
|
|
392
|
+
spot_hype_for_check = float(bal.get("total", 0)) - float(
|
|
393
|
+
bal.get("hold", 0)
|
|
394
|
+
)
|
|
395
|
+
break
|
|
396
|
+
except Exception as exc:
|
|
397
|
+
logger.debug(f"Error checking HL spot state: {exc}")
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
ok_perp_chk, perp_state_chk = await self.hyperliquid_adapter.get_user_state(
|
|
401
|
+
address
|
|
402
|
+
)
|
|
403
|
+
if ok_perp_chk and isinstance(perp_state_chk, dict):
|
|
404
|
+
for pos in perp_state_chk.get("assetPositions", []):
|
|
405
|
+
p = pos.get("position", {}) if isinstance(pos, dict) else {}
|
|
406
|
+
if p.get("coin") == "HYPE" and float(p.get("szi", 0)) < 0:
|
|
407
|
+
perp_short_for_check = abs(float(p.get("szi", 0)))
|
|
408
|
+
break
|
|
409
|
+
except Exception as exc:
|
|
410
|
+
logger.debug(f"Error checking perp state: {exc}")
|
|
411
|
+
|
|
412
|
+
logger.info(
|
|
413
|
+
f"Pre-unwind check: hl_spot_hype={spot_hype_for_check:.4f}, "
|
|
414
|
+
f"perp_short={perp_short_for_check:.4f}, hyperevm_hype_value={hyperevm_hype_value:.4f}"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
dust_threshold = 0.1
|
|
418
|
+
|
|
419
|
+
if sent_hype_to_hl and spot_hype_for_check < 0.01:
|
|
420
|
+
logger.info(
|
|
421
|
+
f"Sent {sent_hype_to_hl_amount_hype:.4f} HYPE to HL spot but balance not visible yet; waiting..."
|
|
422
|
+
)
|
|
423
|
+
for attempt in range(6): # 60s max additional wait
|
|
424
|
+
await asyncio.sleep(10)
|
|
425
|
+
try:
|
|
426
|
+
(
|
|
427
|
+
ok_spot_chk,
|
|
428
|
+
spot_state_chk,
|
|
429
|
+
) = await self.hyperliquid_adapter.get_spot_user_state(address)
|
|
430
|
+
if ok_spot_chk and isinstance(spot_state_chk, dict):
|
|
431
|
+
for bal in spot_state_chk.get("balances", []):
|
|
432
|
+
if (bal.get("coin") or bal.get("token")) == "HYPE":
|
|
433
|
+
spot_hype_for_check = float(
|
|
434
|
+
bal.get("total", 0)
|
|
435
|
+
) - float(bal.get("hold", 0))
|
|
436
|
+
break
|
|
437
|
+
if spot_hype_for_check > 0.01:
|
|
438
|
+
logger.info(
|
|
439
|
+
f"HYPE arrived on HL spot: {spot_hype_for_check:.4f}"
|
|
440
|
+
)
|
|
441
|
+
break
|
|
442
|
+
except Exception as exc: # noqa: BLE001
|
|
443
|
+
logger.debug(f"Error re-checking HL spot: {exc}")
|
|
444
|
+
logger.info(
|
|
445
|
+
f"Still waiting for HYPE to arrive on HL spot (attempt {attempt + 1}/6)"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
if (
|
|
449
|
+
hyperevm_hype_value > dust_threshold
|
|
450
|
+
and perp_short_for_check > dust_threshold
|
|
451
|
+
):
|
|
452
|
+
logger.warning(
|
|
453
|
+
f"Significant value on HyperEVM ({hyperevm_hype_value:.4f} HYPE) "
|
|
454
|
+
f"but perp short still open ({perp_short_for_check:.4f}). "
|
|
455
|
+
"Waiting for spot to arrive on HL before closing perp..."
|
|
456
|
+
)
|
|
457
|
+
for attempt in range(6): # 60s max additional wait
|
|
458
|
+
await asyncio.sleep(10)
|
|
459
|
+
try:
|
|
460
|
+
(
|
|
461
|
+
ok_spot_chk,
|
|
462
|
+
spot_state_chk,
|
|
463
|
+
) = await self.hyperliquid_adapter.get_spot_user_state(address)
|
|
464
|
+
if ok_spot_chk and isinstance(spot_state_chk, dict):
|
|
465
|
+
for bal in spot_state_chk.get("balances", []):
|
|
466
|
+
if (bal.get("coin") or bal.get("token")) == "HYPE":
|
|
467
|
+
spot_hype_for_check = float(
|
|
468
|
+
bal.get("total", 0)
|
|
469
|
+
) - float(bal.get("hold", 0))
|
|
470
|
+
break
|
|
471
|
+
if spot_hype_for_check > dust_threshold:
|
|
472
|
+
logger.info(
|
|
473
|
+
f"HYPE arrived on HL spot: {spot_hype_for_check:.4f}"
|
|
474
|
+
)
|
|
475
|
+
break
|
|
476
|
+
except Exception as exc:
|
|
477
|
+
logger.debug(f"Error re-checking HL spot: {exc}")
|
|
478
|
+
logger.info(
|
|
479
|
+
f"Still waiting for HYPE to arrive on HL spot (attempt {attempt + 1}/6)"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
if (
|
|
483
|
+
spot_hype_for_check > dust_threshold
|
|
484
|
+
and perp_short_for_check < dust_threshold
|
|
485
|
+
):
|
|
486
|
+
logger.info(
|
|
487
|
+
"Spot HYPE present, perp already closed - proceeding with spot sale only"
|
|
488
|
+
)
|
|
489
|
+
elif (
|
|
490
|
+
perp_short_for_check > dust_threshold
|
|
491
|
+
and spot_hype_for_check < dust_threshold
|
|
492
|
+
):
|
|
493
|
+
if hyperevm_hype_value < dust_threshold:
|
|
494
|
+
logger.info(
|
|
495
|
+
"Perp short present, no spot - proceeding with perp close only (one-leg scenario)"
|
|
496
|
+
)
|
|
497
|
+
else:
|
|
498
|
+
logger.warning(
|
|
499
|
+
f"Perp short present but spot HYPE not yet on HL "
|
|
500
|
+
f"(HyperEVM value: {hyperevm_hype_value:.4f}) - delta risk!"
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# ─────────────────────────────────────────────────────────────────
|
|
504
|
+
# STEP 5: Sell HYPE to USDC on Hyperliquid spot (still hedged)
|
|
505
|
+
# ─────────────────────────────────────────────────────────────────
|
|
506
|
+
sold_hl_spot_hype = False
|
|
507
|
+
try:
|
|
508
|
+
ok_spot, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
|
|
509
|
+
address
|
|
510
|
+
)
|
|
511
|
+
spot_hype_balance = 0.0
|
|
512
|
+
if ok_spot and isinstance(spot_state, dict):
|
|
513
|
+
for bal in spot_state.get("balances", []):
|
|
514
|
+
token = bal.get("coin") or bal.get("token")
|
|
515
|
+
hold = float(bal.get("hold", 0))
|
|
516
|
+
total = float(bal.get("total", 0))
|
|
517
|
+
available = total - hold
|
|
518
|
+
if token == "HYPE":
|
|
519
|
+
spot_hype_balance = available
|
|
520
|
+
break
|
|
521
|
+
|
|
522
|
+
if spot_hype_balance > 0.01:
|
|
523
|
+
await self.hyperliquid_adapter.get_spot_assets()
|
|
524
|
+
spot_asset_id = await self.hyperliquid_adapter.get_spot_asset_id(
|
|
525
|
+
"HYPE", "USDC"
|
|
526
|
+
)
|
|
527
|
+
if spot_asset_id is None:
|
|
528
|
+
raise ValueError("Missing Hyperliquid spot asset id for HYPE/USDC")
|
|
529
|
+
|
|
530
|
+
rounded_size = self.hyperliquid_adapter.get_valid_order_size(
|
|
531
|
+
spot_asset_id, spot_hype_balance
|
|
532
|
+
)
|
|
533
|
+
if rounded_size > 0:
|
|
534
|
+
(
|
|
535
|
+
ok_sell,
|
|
536
|
+
res_sell,
|
|
537
|
+
) = await self.hyperliquid_adapter.place_market_order(
|
|
538
|
+
asset_id=spot_asset_id,
|
|
539
|
+
is_buy=False, # selling HYPE
|
|
540
|
+
slippage=0.10,
|
|
541
|
+
size=rounded_size,
|
|
542
|
+
address=address,
|
|
543
|
+
builder=self.builder_fee,
|
|
544
|
+
)
|
|
545
|
+
if ok_sell:
|
|
546
|
+
logger.info(f"Sold {rounded_size:.4f} HYPE to USDC on HL spot")
|
|
547
|
+
sold_hl_spot_hype = True
|
|
548
|
+
await asyncio.sleep(
|
|
549
|
+
10
|
|
550
|
+
) # HL spot trades need time to clear hold
|
|
551
|
+
else:
|
|
552
|
+
logger.warning(f"Failed to sell spot HYPE: {res_sell}")
|
|
553
|
+
except Exception as exc: # noqa: BLE001
|
|
554
|
+
logger.warning(f"Failed to sell HYPE on HL spot: {exc}")
|
|
555
|
+
|
|
556
|
+
# ─────────────────────────────────────────────────────────────────
|
|
557
|
+
# STEP 6: Close HL perp short (safe now - we hold USDC, not HYPE)
|
|
558
|
+
# ─────────────────────────────────────────────────────────────────
|
|
559
|
+
try:
|
|
560
|
+
await self._cancel_hl_open_orders_for_hype(address)
|
|
561
|
+
except Exception as exc: # noqa: BLE001
|
|
562
|
+
logger.debug(f"Failed to cancel HL open orders before hedge close: {exc}")
|
|
563
|
+
|
|
564
|
+
try:
|
|
565
|
+
ok_state, user_state = await self.hyperliquid_adapter.get_user_state(
|
|
566
|
+
address
|
|
567
|
+
)
|
|
568
|
+
current_short_size = 0.0
|
|
569
|
+
hype_price_usd = float(inv.hype_price_usd or 0.0)
|
|
570
|
+
if ok_state and isinstance(user_state, dict):
|
|
571
|
+
for pos in user_state.get("assetPositions", []):
|
|
572
|
+
p = pos.get("position", {}) if isinstance(pos, dict) else {}
|
|
573
|
+
if p.get("coin") == "HYPE":
|
|
574
|
+
szi = float(p.get("szi", 0))
|
|
575
|
+
if szi < 0:
|
|
576
|
+
current_short_size = abs(szi)
|
|
577
|
+
break
|
|
578
|
+
|
|
579
|
+
if current_short_size > 0.01:
|
|
580
|
+
hype_asset_id = self.hyperliquid_adapter.coin_to_asset.get("HYPE")
|
|
581
|
+
if hype_asset_id is None:
|
|
582
|
+
raise ValueError("Missing Hyperliquid perp asset id for HYPE")
|
|
583
|
+
|
|
584
|
+
rounded_size = self.hyperliquid_adapter.get_valid_order_size(
|
|
585
|
+
hype_asset_id, current_short_size
|
|
586
|
+
)
|
|
587
|
+
if (
|
|
588
|
+
rounded_size > 0
|
|
589
|
+
and (rounded_size * hype_price_usd) >= MIN_NOTIONAL_USD
|
|
590
|
+
):
|
|
591
|
+
(
|
|
592
|
+
ok_close,
|
|
593
|
+
res_close,
|
|
594
|
+
) = await self.hyperliquid_adapter.place_market_order(
|
|
595
|
+
asset_id=hype_asset_id,
|
|
596
|
+
is_buy=True, # buy to close short
|
|
597
|
+
slippage=0.01,
|
|
598
|
+
size=rounded_size,
|
|
599
|
+
address=address,
|
|
600
|
+
reduce_only=True,
|
|
601
|
+
builder=self.builder_fee,
|
|
602
|
+
)
|
|
603
|
+
if not ok_close:
|
|
604
|
+
return False, f"Failed to close HL hedge: {res_close}"
|
|
605
|
+
logger.info(f"Closed HL perp short: {rounded_size:.4f} HYPE")
|
|
606
|
+
await asyncio.sleep(2)
|
|
607
|
+
except Exception as exc: # noqa: BLE001
|
|
608
|
+
return False, f"Failed to close HL hedge: {exc}"
|
|
609
|
+
|
|
610
|
+
# ─────────────────────────────────────────────────────────────────
|
|
611
|
+
# STEP 7: Move all USDC from spot to perp margin (poll until cleared)
|
|
612
|
+
# ─────────────────────────────────────────────────────────────────
|
|
613
|
+
usdc_sz_decimals = await self.hyperliquid_adapter.get_spot_token_sz_decimals(
|
|
614
|
+
"USDC"
|
|
615
|
+
)
|
|
616
|
+
if usdc_sz_decimals is None:
|
|
617
|
+
usdc_sz_decimals = 2
|
|
618
|
+
|
|
619
|
+
spot_transfer_succeeded = False
|
|
620
|
+
did_transfer_spot_usdc_to_perp = False
|
|
621
|
+
observed_spot_usdc_after_sell = not sold_hl_spot_hype
|
|
622
|
+
spot_total = 0.0
|
|
623
|
+
spot_hold = 0.0
|
|
624
|
+
spot_usdc = 0.0
|
|
625
|
+
attempt = 0
|
|
626
|
+
while True:
|
|
627
|
+
attempt += 1
|
|
628
|
+
try:
|
|
629
|
+
spot_total = 0.0
|
|
630
|
+
spot_hold = 0.0
|
|
631
|
+
spot_usdc = 0.0
|
|
632
|
+
spot_total_s = "0"
|
|
633
|
+
spot_hold_s = "0"
|
|
634
|
+
|
|
635
|
+
(
|
|
636
|
+
ok_spot,
|
|
637
|
+
spot_state,
|
|
638
|
+
) = await self.hyperliquid_adapter.get_spot_user_state(address)
|
|
639
|
+
if ok_spot and isinstance(spot_state, dict):
|
|
640
|
+
for bal in spot_state.get("balances", []):
|
|
641
|
+
token = bal.get("coin") or bal.get("token")
|
|
642
|
+
if token == "USDC":
|
|
643
|
+
spot_total_s = str(bal.get("total", "0") or "0")
|
|
644
|
+
spot_hold_s = str(bal.get("hold", "0") or "0")
|
|
645
|
+
spot_hold = float(spot_hold_s)
|
|
646
|
+
spot_total = float(spot_total_s)
|
|
647
|
+
spot_usdc = spot_total - spot_hold
|
|
648
|
+
break
|
|
649
|
+
|
|
650
|
+
logger.info(
|
|
651
|
+
f"Spot USDC balance (attempt {attempt}): "
|
|
652
|
+
f"total={spot_total:.2f}, hold={spot_hold:.2f}, available={spot_usdc:.2f}"
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
if not observed_spot_usdc_after_sell:
|
|
656
|
+
if spot_total > 1.0 or spot_hold > 0.5 or spot_usdc > 1.0:
|
|
657
|
+
observed_spot_usdc_after_sell = True
|
|
658
|
+
else:
|
|
659
|
+
if time.time() >= deadline_ts:
|
|
660
|
+
break
|
|
661
|
+
logger.info(
|
|
662
|
+
"Waiting for HL spot USDC to settle after HYPE sale..."
|
|
663
|
+
)
|
|
664
|
+
await asyncio.sleep(poll_interval_s)
|
|
665
|
+
continue
|
|
666
|
+
|
|
667
|
+
if spot_total <= 1.0:
|
|
668
|
+
# No significant USDC remaining on spot (including hold), nothing to transfer.
|
|
669
|
+
spot_transfer_succeeded = True
|
|
670
|
+
break
|
|
671
|
+
|
|
672
|
+
if spot_usdc > 1.0:
|
|
673
|
+
# Compute a safe amount using Decimal math and szDecimals, leaving 1 tick.
|
|
674
|
+
spot_usdc_to_xfer = (
|
|
675
|
+
self.hyperliquid_adapter.max_transferable_amount(
|
|
676
|
+
spot_total_s,
|
|
677
|
+
spot_hold_s,
|
|
678
|
+
sz_decimals=int(usdc_sz_decimals),
|
|
679
|
+
leave_one_tick=True,
|
|
680
|
+
)
|
|
681
|
+
)
|
|
682
|
+
# Fallback: some Hyperliquid client versions effectively round to 2dp
|
|
683
|
+
# internally for usdClassTransfer. If we get an "insufficient balance"
|
|
684
|
+
# error, retry with 2dp floor.
|
|
685
|
+
fallback_2dp = (
|
|
686
|
+
self.hyperliquid_adapter.max_transferable_amount(
|
|
687
|
+
spot_total_s,
|
|
688
|
+
spot_hold_s,
|
|
689
|
+
sz_decimals=2,
|
|
690
|
+
leave_one_tick=True,
|
|
691
|
+
)
|
|
692
|
+
if int(usdc_sz_decimals) != 2
|
|
693
|
+
else 0.0
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
if spot_usdc_to_xfer <= 1.0:
|
|
697
|
+
if time.time() >= deadline_ts:
|
|
698
|
+
break
|
|
699
|
+
await asyncio.sleep(poll_interval_s)
|
|
700
|
+
continue
|
|
701
|
+
|
|
702
|
+
# Transfer the full available amount (fresh balance query each attempt)
|
|
703
|
+
(
|
|
704
|
+
ok_xfer,
|
|
705
|
+
res_xfer,
|
|
706
|
+
) = await self.hyperliquid_adapter.transfer_spot_to_perp(
|
|
707
|
+
amount=float(spot_usdc_to_xfer),
|
|
708
|
+
address=address,
|
|
709
|
+
)
|
|
710
|
+
if ok_xfer:
|
|
711
|
+
logger.info(
|
|
712
|
+
f"Transferred ${spot_usdc_to_xfer:.2f} USDC from spot to perp"
|
|
713
|
+
)
|
|
714
|
+
did_transfer_spot_usdc_to_perp = True
|
|
715
|
+
await asyncio.sleep(3)
|
|
716
|
+
else:
|
|
717
|
+
res_s = str(res_xfer)
|
|
718
|
+
if fallback_2dp > 1.0 and (
|
|
719
|
+
"insufficient balance" in res_s.lower()
|
|
720
|
+
):
|
|
721
|
+
(
|
|
722
|
+
ok_xfer2,
|
|
723
|
+
res_xfer2,
|
|
724
|
+
) = await self.hyperliquid_adapter.transfer_spot_to_perp(
|
|
725
|
+
amount=float(fallback_2dp),
|
|
726
|
+
address=address,
|
|
727
|
+
)
|
|
728
|
+
if ok_xfer2:
|
|
729
|
+
logger.info(
|
|
730
|
+
f"Transferred ${fallback_2dp:.2f} USDC from spot to perp (2dp fallback)"
|
|
731
|
+
)
|
|
732
|
+
did_transfer_spot_usdc_to_perp = True
|
|
733
|
+
await asyncio.sleep(3)
|
|
734
|
+
continue
|
|
735
|
+
res_xfer = res_xfer2
|
|
736
|
+
|
|
737
|
+
logger.warning(
|
|
738
|
+
f"Failed to move USDC spot→perp (attempt {attempt}): {res_xfer}"
|
|
739
|
+
)
|
|
740
|
+
else:
|
|
741
|
+
# USDC exists but is still held (trade settlement). Wait and retry.
|
|
742
|
+
if time.time() >= deadline_ts:
|
|
743
|
+
break
|
|
744
|
+
await asyncio.sleep(poll_interval_s)
|
|
745
|
+
except Exception as exc: # noqa: BLE001
|
|
746
|
+
logger.warning(
|
|
747
|
+
f"Failed to move USDC spot→perp (attempt {attempt}): {exc}"
|
|
748
|
+
)
|
|
749
|
+
if time.time() >= deadline_ts:
|
|
750
|
+
break
|
|
751
|
+
await asyncio.sleep(poll_interval_s)
|
|
752
|
+
|
|
753
|
+
remaining_spot_usdc = 0.0
|
|
754
|
+
if not spot_transfer_succeeded:
|
|
755
|
+
remaining_spot_usdc = spot_total
|
|
756
|
+
logger.error(
|
|
757
|
+
f"Failed to transfer spot USDC to perp before timeout. "
|
|
758
|
+
f"Spot USDC may still be on Hyperliquid spot account (total=${spot_total:.2f}, hold=${spot_hold:.2f})."
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
# ─────────────────────────────────────────────────────────────────
|
|
762
|
+
# STEP 8: Withdraw all from Hyperliquid to Arbitrum
|
|
763
|
+
# ─────────────────────────────────────────────────────────────────
|
|
764
|
+
hl_wait_min_usdc_raw: int | None = None
|
|
765
|
+
try:
|
|
766
|
+
attempt = 0
|
|
767
|
+
while True:
|
|
768
|
+
attempt += 1
|
|
769
|
+
ok_state, user_state = await self.hyperliquid_adapter.get_user_state(
|
|
770
|
+
address
|
|
771
|
+
)
|
|
772
|
+
perp_balance = (
|
|
773
|
+
self.hyperliquid_adapter.get_perp_margin_amount(user_state)
|
|
774
|
+
if ok_state and isinstance(user_state, dict)
|
|
775
|
+
else 0.0
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
if perp_balance > 1.0:
|
|
779
|
+
(
|
|
780
|
+
ok_usdc,
|
|
781
|
+
usdc_raw_before,
|
|
782
|
+
) = await self.balance_adapter.get_vault_wallet_balance(USDC_ARB)
|
|
783
|
+
usdc_raw_before_int = int(usdc_raw_before) if ok_usdc else 0
|
|
784
|
+
expected_usdc_raw = max(0, int(float(perp_balance) * 1e6))
|
|
785
|
+
ok_wd, res_wd = await self.hyperliquid_adapter.withdraw(
|
|
786
|
+
amount=float(perp_balance),
|
|
787
|
+
address=address,
|
|
788
|
+
)
|
|
789
|
+
if ok_wd:
|
|
790
|
+
min_expected = max(int(1e6), int(expected_usdc_raw * 0.99))
|
|
791
|
+
hl_wait_min_usdc_raw = usdc_raw_before_int + min_expected
|
|
792
|
+
break
|
|
793
|
+
logger.warning(f"Failed to withdraw from Hyperliquid: {res_wd}")
|
|
794
|
+
if time.time() >= deadline_ts:
|
|
795
|
+
break
|
|
796
|
+
await asyncio.sleep(poll_interval_s)
|
|
797
|
+
continue
|
|
798
|
+
|
|
799
|
+
# No perp balance yet. If we just moved spot→perp, wait for it to reflect.
|
|
800
|
+
if not did_transfer_spot_usdc_to_perp:
|
|
801
|
+
break
|
|
802
|
+
if time.time() >= deadline_ts:
|
|
803
|
+
break
|
|
804
|
+
logger.info(
|
|
805
|
+
f"Waiting for spot→perp transfer to reflect in HL margin (attempt {attempt})..."
|
|
806
|
+
)
|
|
807
|
+
await asyncio.sleep(poll_interval_s)
|
|
808
|
+
except Exception as exc: # noqa: BLE001
|
|
809
|
+
logger.warning(f"Failed Hyperliquid withdrawal: {exc}")
|
|
810
|
+
|
|
811
|
+
# ─────────────────────────────────────────────────────────────────
|
|
812
|
+
# WAIT: Boros + Hyperliquid withdrawals concurrently
|
|
813
|
+
# ─────────────────────────────────────────────────────────────────
|
|
814
|
+
async def _wait_for_wallet_balance_at_least(
|
|
815
|
+
*,
|
|
816
|
+
token_id: str | None = None,
|
|
817
|
+
token_address: str | None = None,
|
|
818
|
+
chain_id: int | None = None,
|
|
819
|
+
min_raw: int,
|
|
820
|
+
) -> tuple[bool, int]:
|
|
821
|
+
if token_address and chain_id is not None:
|
|
822
|
+
assets = [{"token_address": token_address, "chain_id": int(chain_id)}]
|
|
823
|
+
|
|
824
|
+
async def _get_raw() -> tuple[bool, int]:
|
|
825
|
+
ok, res = await self.balance_adapter.get_wallet_balances_multicall(
|
|
826
|
+
assets=assets
|
|
827
|
+
)
|
|
828
|
+
if not ok or not isinstance(res, list) or not res:
|
|
829
|
+
return False, 0
|
|
830
|
+
item = res[0]
|
|
831
|
+
if not item.get("success"):
|
|
832
|
+
return False, 0
|
|
833
|
+
return True, int(item.get("balance_raw") or 0)
|
|
834
|
+
|
|
835
|
+
if min_raw <= 0:
|
|
836
|
+
return await _get_raw()
|
|
837
|
+
else:
|
|
838
|
+
if token_id is None:
|
|
839
|
+
return False, 0
|
|
840
|
+
|
|
841
|
+
async def _get_raw() -> tuple[bool, int]:
|
|
842
|
+
ok, raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
843
|
+
token_id
|
|
844
|
+
)
|
|
845
|
+
return bool(ok), int(raw) if ok else 0
|
|
846
|
+
|
|
847
|
+
if min_raw <= 0:
|
|
848
|
+
return await _get_raw()
|
|
849
|
+
|
|
850
|
+
deadline = deadline_ts
|
|
851
|
+
last_raw = 0
|
|
852
|
+
while True:
|
|
853
|
+
ok, raw = await _get_raw()
|
|
854
|
+
if ok:
|
|
855
|
+
last_raw = int(raw)
|
|
856
|
+
if last_raw >= int(min_raw):
|
|
857
|
+
return True, last_raw
|
|
858
|
+
if time.time() >= deadline:
|
|
859
|
+
return False, last_raw
|
|
860
|
+
await asyncio.sleep(poll_interval_s)
|
|
861
|
+
|
|
862
|
+
wait_tasks: list[asyncio.Task] = []
|
|
863
|
+
if max_wait_s > 0 and time.time() < deadline_ts:
|
|
864
|
+
if boros_wait_min_hype_raw is not None:
|
|
865
|
+
wait_tasks.append(
|
|
866
|
+
asyncio.create_task(
|
|
867
|
+
_wait_for_wallet_balance_at_least(
|
|
868
|
+
token_address=HYPE_OFT_ADDRESS,
|
|
869
|
+
chain_id=ARBITRUM_CHAIN_ID,
|
|
870
|
+
min_raw=int(boros_wait_min_hype_raw),
|
|
871
|
+
)
|
|
872
|
+
)
|
|
873
|
+
)
|
|
874
|
+
if hl_wait_min_usdc_raw is not None:
|
|
875
|
+
wait_tasks.append(
|
|
876
|
+
asyncio.create_task(
|
|
877
|
+
_wait_for_wallet_balance_at_least(
|
|
878
|
+
token_id=USDC_ARB,
|
|
879
|
+
min_raw=int(hl_wait_min_usdc_raw),
|
|
880
|
+
)
|
|
881
|
+
)
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
if wait_tasks:
|
|
885
|
+
try:
|
|
886
|
+
await asyncio.gather(*wait_tasks)
|
|
887
|
+
except Exception as exc: # noqa: BLE001
|
|
888
|
+
logger.warning(f"Withdrawal wait phase errored: {exc}")
|
|
889
|
+
|
|
890
|
+
# ─────────────────────────────────────────────────────────────────
|
|
891
|
+
# STEP 9: Swap any USDT to USDC on Arbitrum
|
|
892
|
+
# ─────────────────────────────────────────────────────────────────
|
|
893
|
+
try:
|
|
894
|
+
ok_usdt, usdt_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
895
|
+
USDT_ARB
|
|
896
|
+
)
|
|
897
|
+
if ok_usdt and usdt_raw > 0:
|
|
898
|
+
ok_swap, swap_res = await self.brap_adapter.swap_from_token_ids(
|
|
899
|
+
from_token_id=USDT_ARB,
|
|
900
|
+
to_token_id=USDC_ARB,
|
|
901
|
+
from_address=address,
|
|
902
|
+
amount=str(int(usdt_raw)),
|
|
903
|
+
slippage=0.005,
|
|
904
|
+
strategy_name="boros_hype_strategy",
|
|
905
|
+
)
|
|
906
|
+
if ok_swap:
|
|
907
|
+
await asyncio.sleep(2)
|
|
908
|
+
else:
|
|
909
|
+
logger.warning(f"Failed to swap USDT→USDC: {swap_res}")
|
|
910
|
+
except Exception as exc: # noqa: BLE001
|
|
911
|
+
logger.warning(f"Failed to swap USDT to USDC: {exc}")
|
|
912
|
+
|
|
913
|
+
ok_usdc, vault_usdc_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
914
|
+
USDC_ARB
|
|
915
|
+
)
|
|
916
|
+
usdc_tokens = (
|
|
917
|
+
float(vault_usdc_raw) / 1e6 if ok_usdc and vault_usdc_raw > 0 else 0.0
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
try:
|
|
921
|
+
elapsed_s = int(time.time() - withdraw_start_ts)
|
|
922
|
+
inv_final = await self.observe()
|
|
923
|
+
remaining_hype_usd = 0.0
|
|
924
|
+
if inv_final.whype_balance > 0.001:
|
|
925
|
+
remaining_hype_usd += inv_final.whype_value_usd
|
|
926
|
+
if inv_final.khype_balance > 0.001:
|
|
927
|
+
remaining_hype_usd += inv_final.khype_value_usd
|
|
928
|
+
if inv_final.looped_hype_balance > 0.001:
|
|
929
|
+
remaining_hype_usd += inv_final.looped_hype_value_usd
|
|
930
|
+
|
|
931
|
+
hedgeable_hype = max(0.0, inv_final.hype_hyperevm_balance - MIN_HYPE_GAS)
|
|
932
|
+
if hedgeable_hype > 0.001:
|
|
933
|
+
remaining_hype_usd += hedgeable_hype * float(inv_final.hype_price_usd)
|
|
934
|
+
|
|
935
|
+
if inv_final.hl_spot_hype > 0.01:
|
|
936
|
+
remaining_hype_usd += inv_final.hl_spot_hype_value_usd
|
|
937
|
+
|
|
938
|
+
remaining_non_usdc_arb_usd = 0.0
|
|
939
|
+
if inv_final.hl_spot_usdc > 1.0:
|
|
940
|
+
remaining_non_usdc_arb_usd += inv_final.hl_spot_usdc
|
|
941
|
+
if inv_final.hl_perp_margin > 1.0:
|
|
942
|
+
remaining_non_usdc_arb_usd += inv_final.hl_perp_margin
|
|
943
|
+
if inv_final.boros_collateral_usd > 1.0:
|
|
944
|
+
remaining_non_usdc_arb_usd += inv_final.boros_collateral_usd
|
|
945
|
+
if inv_final.boros_pending_withdrawal_usd > 1.0:
|
|
946
|
+
remaining_non_usdc_arb_usd += inv_final.boros_pending_withdrawal_usd
|
|
947
|
+
if inv_final.usdt_arb_idle > 1.0:
|
|
948
|
+
remaining_non_usdc_arb_usd += inv_final.usdt_arb_idle
|
|
949
|
+
if inv_final.hype_oft_arb_value_usd > 1.0:
|
|
950
|
+
remaining_non_usdc_arb_usd += inv_final.hype_oft_arb_value_usd
|
|
951
|
+
|
|
952
|
+
if remaining_hype_usd > 1.0:
|
|
953
|
+
return False, (
|
|
954
|
+
f"Withdrawal incomplete after {elapsed_s}s: "
|
|
955
|
+
f"~${remaining_hype_usd:.2f} still in HYPE/WHYPE on HyperEVM/HL spot. "
|
|
956
|
+
"Run withdraw again (or increase max_wait_s)."
|
|
957
|
+
)
|
|
958
|
+
if remaining_non_usdc_arb_usd > 1.0:
|
|
959
|
+
return False, (
|
|
960
|
+
f"Withdrawal incomplete after {elapsed_s}s: "
|
|
961
|
+
f"~${remaining_non_usdc_arb_usd:.2f} still not in Arbitrum USDC (HL/Boros/HYPE/USDT). "
|
|
962
|
+
"Run withdraw again (or increase max_wait_s)."
|
|
963
|
+
)
|
|
964
|
+
except Exception as exc: # noqa: BLE001
|
|
965
|
+
logger.debug(f"Final withdrawal inventory check failed: {exc}")
|
|
966
|
+
|
|
967
|
+
if remaining_spot_usdc > 1.0:
|
|
968
|
+
return False, (
|
|
969
|
+
f"Withdrawal incomplete: ${remaining_spot_usdc:.2f} USDC still on Hyperliquid spot. "
|
|
970
|
+
"Run withdraw again (or increase max_wait_s) to retry the spot→perp transfer."
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
return (
|
|
974
|
+
True,
|
|
975
|
+
f"Fully unwound all positions. USDC balance: ${usdc_tokens:.2f}. Call exit() to transfer to main wallet.",
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
async def _unwrap_whype(self, address: str, amount_wei: int) -> tuple[bool, str]:
|
|
979
|
+
try:
|
|
980
|
+
if not self._sign_callback:
|
|
981
|
+
return False, "No signing callback configured"
|
|
982
|
+
|
|
983
|
+
tx = await encode_call(
|
|
984
|
+
target=WHYPE_ADDRESS,
|
|
985
|
+
abi=WHYPE_ABI,
|
|
986
|
+
fn_name="withdraw",
|
|
987
|
+
args=[int(amount_wei)],
|
|
988
|
+
from_address=address,
|
|
989
|
+
chain_id=HYPEREVM_CHAIN_ID,
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
txn_hash = await send_transaction(
|
|
993
|
+
tx, self._sign_callback, wait_for_receipt=True
|
|
994
|
+
)
|
|
995
|
+
return True, txn_hash
|
|
996
|
+
except Exception as exc: # noqa: BLE001
|
|
997
|
+
return False, str(exc)
|