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,886 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Risk and recovery 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
|
+
from typing import Any
|
|
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 PairedFiller
|
|
16
|
+
|
|
17
|
+
from .constants import (
|
|
18
|
+
BOROS_HYPE_MARKET_ID,
|
|
19
|
+
HYPE_NATIVE,
|
|
20
|
+
KHYPE_LST,
|
|
21
|
+
LOOPED_HYPE,
|
|
22
|
+
MIN_HYPE_GAS,
|
|
23
|
+
MIN_NET_DEPOSIT,
|
|
24
|
+
USDC_ARB,
|
|
25
|
+
USDT_ARB,
|
|
26
|
+
)
|
|
27
|
+
from .types import Inventory
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BorosHypeRiskOpsMixin:
|
|
31
|
+
async def _close_and_redeploy(
|
|
32
|
+
self, params: dict[str, Any], inventory: Inventory
|
|
33
|
+
) -> tuple[bool, str]:
|
|
34
|
+
if self.simulation:
|
|
35
|
+
return True, "[SIMULATION] Close and redeploy executed"
|
|
36
|
+
|
|
37
|
+
if not self.balance_adapter:
|
|
38
|
+
return False, "Balance adapter not configured"
|
|
39
|
+
if not self.hyperliquid_adapter:
|
|
40
|
+
return False, "Hyperliquid adapter not configured"
|
|
41
|
+
if not self.brap_adapter:
|
|
42
|
+
return False, "BRAP adapter not configured"
|
|
43
|
+
if not self._sign_callback:
|
|
44
|
+
return False, "No strategy wallet signing callback configured"
|
|
45
|
+
|
|
46
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
47
|
+
address = strategy_wallet.get("address")
|
|
48
|
+
if not address:
|
|
49
|
+
return False, "No strategy wallet address configured"
|
|
50
|
+
|
|
51
|
+
logger.warning("Emergency close and redeploy triggered")
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
await self._cancel_hl_open_orders_for_hype(address)
|
|
55
|
+
except Exception as exc: # noqa: BLE001
|
|
56
|
+
logger.debug(f"Failed to cancel HL open orders pre-redeploy: {exc}")
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
ok_state, user_state = await self.hyperliquid_adapter.get_user_state(
|
|
60
|
+
address
|
|
61
|
+
)
|
|
62
|
+
current_short_size = 0.0
|
|
63
|
+
if ok_state and isinstance(user_state, dict):
|
|
64
|
+
for pos in user_state.get("assetPositions", []):
|
|
65
|
+
p = pos.get("position", {}) if isinstance(pos, dict) else {}
|
|
66
|
+
if p.get("coin") == "HYPE":
|
|
67
|
+
szi = float(p.get("szi", 0))
|
|
68
|
+
if szi < 0:
|
|
69
|
+
current_short_size = abs(szi)
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
if current_short_size > 0.01:
|
|
73
|
+
perp_asset_id = self.hyperliquid_adapter.coin_to_asset.get("HYPE")
|
|
74
|
+
if perp_asset_id is None:
|
|
75
|
+
logger.warning(
|
|
76
|
+
"Missing Hyperliquid perp asset id for HYPE; cannot close hedge"
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
rounded_size = self.hyperliquid_adapter.get_valid_order_size(
|
|
80
|
+
int(perp_asset_id), current_short_size
|
|
81
|
+
)
|
|
82
|
+
if rounded_size > 0:
|
|
83
|
+
(
|
|
84
|
+
ok_close,
|
|
85
|
+
res_close,
|
|
86
|
+
) = await self.hyperliquid_adapter.place_market_order(
|
|
87
|
+
asset_id=int(perp_asset_id),
|
|
88
|
+
is_buy=True,
|
|
89
|
+
slippage=0.05,
|
|
90
|
+
size=float(rounded_size),
|
|
91
|
+
address=address,
|
|
92
|
+
reduce_only=True,
|
|
93
|
+
builder=self.builder_fee,
|
|
94
|
+
)
|
|
95
|
+
if not ok_close:
|
|
96
|
+
logger.warning(f"Failed to close HL short: {res_close}")
|
|
97
|
+
await asyncio.sleep(2)
|
|
98
|
+
except Exception as exc: # noqa: BLE001
|
|
99
|
+
logger.warning(f"Failed closing HL hedge: {exc}")
|
|
100
|
+
|
|
101
|
+
if self.boros_adapter:
|
|
102
|
+
try:
|
|
103
|
+
ok_pos, positions = await self.boros_adapter.get_active_positions()
|
|
104
|
+
if ok_pos and isinstance(positions, list):
|
|
105
|
+
for pos in positions:
|
|
106
|
+
mid = pos.get("marketId") or pos.get("market_id")
|
|
107
|
+
try:
|
|
108
|
+
mid_int = int(mid) if mid is not None else None
|
|
109
|
+
except (TypeError, ValueError):
|
|
110
|
+
mid_int = None
|
|
111
|
+
if mid_int and mid_int > 0:
|
|
112
|
+
try:
|
|
113
|
+
await self.boros_adapter.close_positions_market(mid_int)
|
|
114
|
+
except Exception as exc: # noqa: BLE001
|
|
115
|
+
logger.warning(
|
|
116
|
+
f"Failed to close Boros market {mid_int}: {exc}"
|
|
117
|
+
)
|
|
118
|
+
except Exception as exc: # noqa: BLE001
|
|
119
|
+
logger.warning(f"Failed to close Boros positions: {exc}")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
ok_khype, khype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
123
|
+
KHYPE_LST
|
|
124
|
+
)
|
|
125
|
+
if ok_khype and khype_raw > 0:
|
|
126
|
+
await self.brap_adapter.swap_from_token_ids(
|
|
127
|
+
from_token_id=KHYPE_LST,
|
|
128
|
+
to_token_id=HYPE_NATIVE,
|
|
129
|
+
from_address=address,
|
|
130
|
+
amount=str(int(khype_raw)),
|
|
131
|
+
slippage=0.01,
|
|
132
|
+
strategy_name="boros_hype_strategy",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
ok_lhype, lhype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
136
|
+
LOOPED_HYPE
|
|
137
|
+
)
|
|
138
|
+
if ok_lhype and lhype_raw > 0:
|
|
139
|
+
await self.brap_adapter.swap_from_token_ids(
|
|
140
|
+
from_token_id=LOOPED_HYPE,
|
|
141
|
+
to_token_id=HYPE_NATIVE,
|
|
142
|
+
from_address=address,
|
|
143
|
+
amount=str(int(lhype_raw)),
|
|
144
|
+
slippage=0.01,
|
|
145
|
+
strategy_name="boros_hype_strategy",
|
|
146
|
+
)
|
|
147
|
+
except Exception as exc: # noqa: BLE001
|
|
148
|
+
logger.warning(f"Failed selling spot LSTs to HYPE: {exc}")
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
ok_hype, hype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
152
|
+
HYPE_NATIVE
|
|
153
|
+
)
|
|
154
|
+
hype_raw_int = int(hype_raw) if ok_hype and hype_raw and hype_raw > 0 else 0
|
|
155
|
+
gas_reserve_wei = int(float(MIN_HYPE_GAS) * 1e18)
|
|
156
|
+
hype_to_transfer_raw = max(0, hype_raw_int - gas_reserve_wei)
|
|
157
|
+
|
|
158
|
+
if hype_to_transfer_raw > int(0.01 * 1e18):
|
|
159
|
+
destination = HyperliquidAdapter.hypercore_index_to_system_address(150)
|
|
160
|
+
ok_send, send_res = await self.balance_adapter.send_to_address(
|
|
161
|
+
token_id=HYPE_NATIVE,
|
|
162
|
+
amount=int(hype_to_transfer_raw),
|
|
163
|
+
from_wallet=strategy_wallet,
|
|
164
|
+
to_address=destination,
|
|
165
|
+
signing_callback=self._sign_callback,
|
|
166
|
+
)
|
|
167
|
+
if not ok_send:
|
|
168
|
+
logger.warning(
|
|
169
|
+
f"Failed to transfer HYPE to Hyperliquid: {send_res}"
|
|
170
|
+
)
|
|
171
|
+
await asyncio.sleep(3)
|
|
172
|
+
except Exception as exc: # noqa: BLE001
|
|
173
|
+
logger.warning(f"Failed to transfer HYPE to Hyperliquid: {exc}")
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
ok_spot, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
|
|
177
|
+
address
|
|
178
|
+
)
|
|
179
|
+
spot_hype_balance = 0.0
|
|
180
|
+
if ok_spot and isinstance(spot_state, dict):
|
|
181
|
+
for bal in spot_state.get("balances", []):
|
|
182
|
+
token = bal.get("coin") or bal.get("token")
|
|
183
|
+
hold = float(bal.get("hold", 0))
|
|
184
|
+
total = float(bal.get("total", 0))
|
|
185
|
+
available = total - hold
|
|
186
|
+
if token == "HYPE":
|
|
187
|
+
spot_hype_balance = available
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
if spot_hype_balance > 0.01:
|
|
191
|
+
spot_asset_id, _ = await self._get_hype_asset_ids()
|
|
192
|
+
rounded_size = self.hyperliquid_adapter.get_valid_order_size(
|
|
193
|
+
int(spot_asset_id), spot_hype_balance
|
|
194
|
+
)
|
|
195
|
+
if rounded_size > 0:
|
|
196
|
+
await self.hyperliquid_adapter.place_market_order(
|
|
197
|
+
asset_id=int(spot_asset_id),
|
|
198
|
+
is_buy=False,
|
|
199
|
+
slippage=0.10,
|
|
200
|
+
size=float(rounded_size),
|
|
201
|
+
address=address,
|
|
202
|
+
builder=self.builder_fee,
|
|
203
|
+
)
|
|
204
|
+
await asyncio.sleep(2)
|
|
205
|
+
except Exception as exc: # noqa: BLE001
|
|
206
|
+
logger.warning(f"Failed to sell spot HYPE: {exc}")
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
usdc_sz_decimals = (
|
|
210
|
+
await self.hyperliquid_adapter.get_spot_token_sz_decimals("USDC")
|
|
211
|
+
)
|
|
212
|
+
if usdc_sz_decimals is None:
|
|
213
|
+
usdc_sz_decimals = 2
|
|
214
|
+
|
|
215
|
+
ok_spot, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
|
|
216
|
+
address
|
|
217
|
+
)
|
|
218
|
+
spot_usdc = 0.0
|
|
219
|
+
spot_total_s = "0"
|
|
220
|
+
spot_hold_s = "0"
|
|
221
|
+
if ok_spot and isinstance(spot_state, dict):
|
|
222
|
+
for bal in spot_state.get("balances", []):
|
|
223
|
+
token = bal.get("coin") or bal.get("token")
|
|
224
|
+
if token != "USDC":
|
|
225
|
+
continue
|
|
226
|
+
spot_total_s = str(bal.get("total", "0") or "0")
|
|
227
|
+
spot_hold_s = str(bal.get("hold", "0") or "0")
|
|
228
|
+
hold = float(spot_hold_s)
|
|
229
|
+
total = float(spot_total_s)
|
|
230
|
+
spot_usdc = max(0.0, total - hold)
|
|
231
|
+
break
|
|
232
|
+
if spot_usdc > 1.0:
|
|
233
|
+
amount = self.hyperliquid_adapter.max_transferable_amount(
|
|
234
|
+
spot_total_s,
|
|
235
|
+
spot_hold_s,
|
|
236
|
+
sz_decimals=int(usdc_sz_decimals),
|
|
237
|
+
leave_one_tick=True,
|
|
238
|
+
)
|
|
239
|
+
(
|
|
240
|
+
ok_xfer,
|
|
241
|
+
res_xfer,
|
|
242
|
+
) = await self.hyperliquid_adapter.transfer_spot_to_perp(
|
|
243
|
+
amount=float(amount),
|
|
244
|
+
address=address,
|
|
245
|
+
)
|
|
246
|
+
if (not ok_xfer) and int(usdc_sz_decimals) != 2:
|
|
247
|
+
if "insufficient balance" in str(res_xfer).lower():
|
|
248
|
+
fallback_2dp = self.hyperliquid_adapter.max_transferable_amount(
|
|
249
|
+
spot_total_s,
|
|
250
|
+
spot_hold_s,
|
|
251
|
+
sz_decimals=2,
|
|
252
|
+
leave_one_tick=True,
|
|
253
|
+
)
|
|
254
|
+
if fallback_2dp > 1.0:
|
|
255
|
+
await self.hyperliquid_adapter.transfer_spot_to_perp(
|
|
256
|
+
amount=float(fallback_2dp),
|
|
257
|
+
address=address,
|
|
258
|
+
)
|
|
259
|
+
except Exception as exc: # noqa: BLE001
|
|
260
|
+
logger.warning(f"Failed to move spot USDC to perp: {exc}")
|
|
261
|
+
|
|
262
|
+
hl_perp_balance = 0.0
|
|
263
|
+
try:
|
|
264
|
+
ok_state, user_state = await self.hyperliquid_adapter.get_user_state(
|
|
265
|
+
address
|
|
266
|
+
)
|
|
267
|
+
if ok_state and isinstance(user_state, dict):
|
|
268
|
+
hl_perp_balance = float(
|
|
269
|
+
self.hyperliquid_adapter.get_perp_margin_amount(user_state)
|
|
270
|
+
)
|
|
271
|
+
except Exception as exc: # noqa: BLE001
|
|
272
|
+
logger.warning(f"Failed to read HL perp balance: {exc}")
|
|
273
|
+
|
|
274
|
+
ok_arb_usdc, arb_usdc_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
275
|
+
USDC_ARB, wallet_address=address
|
|
276
|
+
)
|
|
277
|
+
arb_usdc_tokens = (
|
|
278
|
+
(int(arb_usdc_raw) / 1e6) if ok_arb_usdc and arb_usdc_raw else 0.0
|
|
279
|
+
)
|
|
280
|
+
total_usdc = hl_perp_balance + arb_usdc_tokens
|
|
281
|
+
|
|
282
|
+
if total_usdc < MIN_NET_DEPOSIT:
|
|
283
|
+
return True, "Closed all positions. Insufficient capital to redeploy."
|
|
284
|
+
|
|
285
|
+
spot_target = self.hedge_cfg.spot_pct * total_usdc
|
|
286
|
+
boros_target = self.hedge_cfg.boros_pct * total_usdc
|
|
287
|
+
|
|
288
|
+
if spot_target > self._planner_config.min_usdc_action:
|
|
289
|
+
try:
|
|
290
|
+
await self.hyperliquid_adapter.transfer_perp_to_spot(
|
|
291
|
+
amount=float(spot_target),
|
|
292
|
+
address=address,
|
|
293
|
+
)
|
|
294
|
+
await asyncio.sleep(2)
|
|
295
|
+
|
|
296
|
+
success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
|
|
297
|
+
hype_price = (
|
|
298
|
+
float(mids.get("HYPE", 0.0))
|
|
299
|
+
if success and isinstance(mids, dict)
|
|
300
|
+
else 0.0
|
|
301
|
+
)
|
|
302
|
+
if hype_price <= 0:
|
|
303
|
+
hype_price = float(inventory.hype_price_usd or 0.0)
|
|
304
|
+
|
|
305
|
+
if hype_price > 0:
|
|
306
|
+
hype_to_buy = float(spot_target) / hype_price
|
|
307
|
+
spot_asset_id, _ = await self._get_hype_asset_ids()
|
|
308
|
+
rounded_size = self.hyperliquid_adapter.get_valid_order_size(
|
|
309
|
+
int(spot_asset_id), hype_to_buy
|
|
310
|
+
)
|
|
311
|
+
if rounded_size > 0:
|
|
312
|
+
await self.hyperliquid_adapter.place_market_order(
|
|
313
|
+
asset_id=int(spot_asset_id),
|
|
314
|
+
is_buy=True,
|
|
315
|
+
slippage=0.10,
|
|
316
|
+
size=float(rounded_size),
|
|
317
|
+
address=address,
|
|
318
|
+
builder=self.builder_fee,
|
|
319
|
+
)
|
|
320
|
+
await asyncio.sleep(2)
|
|
321
|
+
|
|
322
|
+
(
|
|
323
|
+
ok_spot,
|
|
324
|
+
spot_state,
|
|
325
|
+
) = await self.hyperliquid_adapter.get_spot_user_state(address)
|
|
326
|
+
spot_hype = 0.0
|
|
327
|
+
if ok_spot and isinstance(spot_state, dict):
|
|
328
|
+
for bal in spot_state.get("balances", []):
|
|
329
|
+
token = bal.get("coin") or bal.get("token")
|
|
330
|
+
if token == "HYPE":
|
|
331
|
+
hold = float(bal.get("hold", 0))
|
|
332
|
+
total = float(bal.get("total", 0))
|
|
333
|
+
spot_hype = max(0.0, total - hold)
|
|
334
|
+
break
|
|
335
|
+
|
|
336
|
+
amount_to_bridge = spot_hype - 0.001
|
|
337
|
+
if amount_to_bridge > 0.1:
|
|
338
|
+
await self.hyperliquid_adapter.hypercore_to_hyperevm(
|
|
339
|
+
amount=float(amount_to_bridge),
|
|
340
|
+
address=address,
|
|
341
|
+
)
|
|
342
|
+
except Exception as exc: # noqa: BLE001
|
|
343
|
+
logger.warning(f"Failed to redeploy spot: {exc}")
|
|
344
|
+
|
|
345
|
+
if boros_target > self._planner_config.min_usdt_action:
|
|
346
|
+
try:
|
|
347
|
+
ok_wd, wd_res = await self.hyperliquid_adapter.withdraw(
|
|
348
|
+
amount=float(boros_target),
|
|
349
|
+
address=address,
|
|
350
|
+
)
|
|
351
|
+
if ok_wd:
|
|
352
|
+
await self.hyperliquid_adapter.wait_for_withdrawal(
|
|
353
|
+
address=address,
|
|
354
|
+
max_poll_time_s=300,
|
|
355
|
+
poll_interval_s=10,
|
|
356
|
+
)
|
|
357
|
+
(
|
|
358
|
+
ok_arb,
|
|
359
|
+
arb_raw,
|
|
360
|
+
) = await self.balance_adapter.get_vault_wallet_balance(
|
|
361
|
+
USDC_ARB, wallet_address=address
|
|
362
|
+
)
|
|
363
|
+
if ok_arb and arb_raw and int(arb_raw) > 0:
|
|
364
|
+
await self.brap_adapter.swap_from_token_ids(
|
|
365
|
+
from_token_id=USDC_ARB,
|
|
366
|
+
to_token_id=USDT_ARB,
|
|
367
|
+
from_address=address,
|
|
368
|
+
amount=str(int(arb_raw)),
|
|
369
|
+
slippage=0.005,
|
|
370
|
+
strategy_name="boros_hype_strategy",
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
logger.warning(
|
|
374
|
+
f"Failed to withdraw from HL for Boros redeploy: {wd_res}"
|
|
375
|
+
)
|
|
376
|
+
except Exception as exc: # noqa: BLE001
|
|
377
|
+
logger.warning(f"Failed to redeploy Boros: {exc}")
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
inv_after = await self.observe()
|
|
381
|
+
swappable_hype = max(
|
|
382
|
+
0.0, float(inv_after.hype_hyperevm_balance or 0.0) - MIN_HYPE_GAS
|
|
383
|
+
)
|
|
384
|
+
if swappable_hype > self._planner_config.min_hype_swap:
|
|
385
|
+
await self._swap_hype_to_lst({"hype_amount": swappable_hype}, inv_after)
|
|
386
|
+
except Exception as exc: # noqa: BLE001
|
|
387
|
+
logger.warning(f"Failed to allocate spot HYPE after redeploy: {exc}")
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
inv_final = await self.observe()
|
|
391
|
+
ok_short, msg_short = await self._ensure_hl_short(
|
|
392
|
+
{
|
|
393
|
+
"target_size": inv_final.total_hype_exposure,
|
|
394
|
+
"current_size": inv_final.hl_short_size_hype,
|
|
395
|
+
},
|
|
396
|
+
inv_final,
|
|
397
|
+
)
|
|
398
|
+
if not ok_short:
|
|
399
|
+
logger.error(
|
|
400
|
+
f"[RECOVERY_FAIL] Could not re-open HL hedge after redeploy: {msg_short}"
|
|
401
|
+
)
|
|
402
|
+
return await self._failsafe_liquidate_all(
|
|
403
|
+
f"Post-liquidation hedge rebuild failed: {msg_short}"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# If we cannot get Boros back into a sane state, melt down rather than
|
|
407
|
+
# continuing with a partially functioning multi-venue position.
|
|
408
|
+
try:
|
|
409
|
+
spot_usd = float(inv_final.total_hype_exposure) * float(
|
|
410
|
+
inv_final.hype_price_usd
|
|
411
|
+
)
|
|
412
|
+
boros_enabled = (
|
|
413
|
+
float(inv_final.total_value)
|
|
414
|
+
>= float(self._planner_config.min_total_for_boros)
|
|
415
|
+
and float(inv_final.boros_pending_withdrawal_usd) <= 0.0
|
|
416
|
+
)
|
|
417
|
+
if boros_enabled and spot_usd >= 10.0 and self.boros_adapter:
|
|
418
|
+
target_usd = spot_usd * float(
|
|
419
|
+
self._planner_config.boros_coverage_target
|
|
420
|
+
)
|
|
421
|
+
market_id = (
|
|
422
|
+
self._planner_runtime.current_boros_market_id
|
|
423
|
+
or BOROS_HYPE_MARKET_ID
|
|
424
|
+
)
|
|
425
|
+
ok_boros, msg_boros = await self._ensure_boros_position(
|
|
426
|
+
{
|
|
427
|
+
"market_id": int(market_id),
|
|
428
|
+
"target_size_usd": float(target_usd),
|
|
429
|
+
},
|
|
430
|
+
inv_final,
|
|
431
|
+
)
|
|
432
|
+
if not ok_boros:
|
|
433
|
+
return await self._failsafe_liquidate_all(
|
|
434
|
+
f"Post-liquidation Boros recovery failed: {msg_boros}"
|
|
435
|
+
)
|
|
436
|
+
except Exception as exc: # noqa: BLE001
|
|
437
|
+
return await self._failsafe_liquidate_all(
|
|
438
|
+
f"Post-liquidation Boros recovery raised: {exc}"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
True,
|
|
443
|
+
f"Redeployed. Spot={inv_final.total_hype_exposure:.4f} HYPE, short re-opened.",
|
|
444
|
+
)
|
|
445
|
+
except Exception as exc: # noqa: BLE001
|
|
446
|
+
logger.warning(f"Failed to verify hedge after redeploy: {exc}")
|
|
447
|
+
return True, "Redeployed (hedge verification pending)"
|
|
448
|
+
|
|
449
|
+
async def _failsafe_liquidate_all(self, reason: str) -> tuple[bool, str]:
|
|
450
|
+
# Called when critical operations fail; close all positions to stable assets
|
|
451
|
+
logger.error(f"[FAILSAFE] Initiating full liquidation: {reason}")
|
|
452
|
+
self._failsafe_triggered = True
|
|
453
|
+
|
|
454
|
+
if self.simulation:
|
|
455
|
+
msg = f"[SIMULATION] [FAILSAFE] Would liquidate all: {reason}"
|
|
456
|
+
self._failsafe_message = msg
|
|
457
|
+
return False, msg
|
|
458
|
+
|
|
459
|
+
messages: list[str] = []
|
|
460
|
+
|
|
461
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
462
|
+
address = strategy_wallet.get("address")
|
|
463
|
+
if not address:
|
|
464
|
+
msg = f"[FAILSAFE] No wallet address: {reason}"
|
|
465
|
+
self._failsafe_message = msg
|
|
466
|
+
return False, msg
|
|
467
|
+
if not self._sign_callback:
|
|
468
|
+
msg = f"[FAILSAFE] No signing callback: {reason}"
|
|
469
|
+
self._failsafe_message = msg
|
|
470
|
+
return False, msg
|
|
471
|
+
|
|
472
|
+
if self.hyperliquid_adapter:
|
|
473
|
+
try:
|
|
474
|
+
ok_state, user_state = await self.hyperliquid_adapter.get_user_state(
|
|
475
|
+
address
|
|
476
|
+
)
|
|
477
|
+
current_short_size = 0.0
|
|
478
|
+
if ok_state and isinstance(user_state, dict):
|
|
479
|
+
for pos in user_state.get("assetPositions", []):
|
|
480
|
+
p = pos.get("position", {}) if isinstance(pos, dict) else {}
|
|
481
|
+
if p.get("coin") == "HYPE":
|
|
482
|
+
szi = float(p.get("szi", 0))
|
|
483
|
+
if szi < 0:
|
|
484
|
+
current_short_size = abs(szi)
|
|
485
|
+
break
|
|
486
|
+
|
|
487
|
+
if current_short_size > 0.01:
|
|
488
|
+
perp_asset_id = self.hyperliquid_adapter.coin_to_asset.get("HYPE")
|
|
489
|
+
if perp_asset_id is not None:
|
|
490
|
+
rounded_size = self.hyperliquid_adapter.get_valid_order_size(
|
|
491
|
+
int(perp_asset_id), current_short_size
|
|
492
|
+
)
|
|
493
|
+
if rounded_size > 0:
|
|
494
|
+
(
|
|
495
|
+
ok_close,
|
|
496
|
+
res_close,
|
|
497
|
+
) = await self.hyperliquid_adapter.place_market_order(
|
|
498
|
+
asset_id=int(perp_asset_id),
|
|
499
|
+
is_buy=True,
|
|
500
|
+
slippage=0.05,
|
|
501
|
+
size=float(rounded_size),
|
|
502
|
+
address=address,
|
|
503
|
+
reduce_only=True,
|
|
504
|
+
builder=self.builder_fee,
|
|
505
|
+
)
|
|
506
|
+
if ok_close:
|
|
507
|
+
messages.append(f"HL short closed: {rounded_size:.4f}")
|
|
508
|
+
else:
|
|
509
|
+
messages.append(f"HL close failed: {res_close}")
|
|
510
|
+
await asyncio.sleep(2)
|
|
511
|
+
else:
|
|
512
|
+
messages.append("HL short: none")
|
|
513
|
+
except Exception as e:
|
|
514
|
+
messages.append(f"HL close error: {e}")
|
|
515
|
+
|
|
516
|
+
if self.boros_adapter:
|
|
517
|
+
try:
|
|
518
|
+
ok_pos, positions = await self.boros_adapter.get_active_positions()
|
|
519
|
+
if ok_pos and isinstance(positions, list) and positions:
|
|
520
|
+
for pos in positions:
|
|
521
|
+
mid = pos.get("marketId") or pos.get("market_id")
|
|
522
|
+
try:
|
|
523
|
+
mid_int = int(mid) if mid is not None else None
|
|
524
|
+
except (TypeError, ValueError):
|
|
525
|
+
mid_int = None
|
|
526
|
+
if mid_int and mid_int > 0:
|
|
527
|
+
try:
|
|
528
|
+
await self.boros_adapter.close_positions_market(mid_int)
|
|
529
|
+
messages.append(f"Boros {mid_int} closed")
|
|
530
|
+
except Exception as exc:
|
|
531
|
+
messages.append(f"Boros {mid_int} close failed: {exc}")
|
|
532
|
+
else:
|
|
533
|
+
messages.append("Boros positions: none")
|
|
534
|
+
except Exception as e:
|
|
535
|
+
messages.append(f"Boros close error: {e}")
|
|
536
|
+
|
|
537
|
+
if self.brap_adapter and self.balance_adapter:
|
|
538
|
+
try:
|
|
539
|
+
# Swap kHYPE to HYPE
|
|
540
|
+
(
|
|
541
|
+
ok_khype,
|
|
542
|
+
khype_raw,
|
|
543
|
+
) = await self.balance_adapter.get_vault_wallet_balance(KHYPE_LST)
|
|
544
|
+
if ok_khype and khype_raw and int(khype_raw) > 0:
|
|
545
|
+
await self.brap_adapter.swap_from_token_ids(
|
|
546
|
+
from_token_id=KHYPE_LST,
|
|
547
|
+
to_token_id=HYPE_NATIVE,
|
|
548
|
+
from_address=address,
|
|
549
|
+
amount=str(int(khype_raw)),
|
|
550
|
+
slippage=0.02,
|
|
551
|
+
strategy_name="boros_hype_strategy",
|
|
552
|
+
)
|
|
553
|
+
messages.append("kHYPE swapped to HYPE")
|
|
554
|
+
|
|
555
|
+
# Swap lHYPE to HYPE
|
|
556
|
+
(
|
|
557
|
+
ok_lhype,
|
|
558
|
+
lhype_raw,
|
|
559
|
+
) = await self.balance_adapter.get_vault_wallet_balance(LOOPED_HYPE)
|
|
560
|
+
if ok_lhype and lhype_raw and int(lhype_raw) > 0:
|
|
561
|
+
await self.brap_adapter.swap_from_token_ids(
|
|
562
|
+
from_token_id=LOOPED_HYPE,
|
|
563
|
+
to_token_id=HYPE_NATIVE,
|
|
564
|
+
from_address=address,
|
|
565
|
+
amount=str(int(lhype_raw)),
|
|
566
|
+
slippage=0.02,
|
|
567
|
+
strategy_name="boros_hype_strategy",
|
|
568
|
+
)
|
|
569
|
+
messages.append("lHYPE swapped to HYPE")
|
|
570
|
+
except Exception as e:
|
|
571
|
+
messages.append(f"Spot swap error: {e}")
|
|
572
|
+
|
|
573
|
+
if self.hyperliquid_adapter and self.balance_adapter:
|
|
574
|
+
try:
|
|
575
|
+
ok_hype, hype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
576
|
+
HYPE_NATIVE
|
|
577
|
+
)
|
|
578
|
+
hype_raw_int = (
|
|
579
|
+
int(hype_raw) if ok_hype and hype_raw and int(hype_raw) > 0 else 0
|
|
580
|
+
)
|
|
581
|
+
gas_reserve_wei = int(float(MIN_HYPE_GAS) * 1e18)
|
|
582
|
+
hype_to_transfer_raw = max(0, hype_raw_int - gas_reserve_wei)
|
|
583
|
+
hype_to_transfer = float(hype_to_transfer_raw) / 1e18
|
|
584
|
+
|
|
585
|
+
if hype_to_transfer_raw > int(0.01 * 1e18):
|
|
586
|
+
destination = HyperliquidAdapter.hypercore_index_to_system_address(
|
|
587
|
+
150
|
|
588
|
+
)
|
|
589
|
+
ok_send, _ = await self.balance_adapter.send_to_address(
|
|
590
|
+
token_id=HYPE_NATIVE,
|
|
591
|
+
amount=int(hype_to_transfer_raw),
|
|
592
|
+
from_wallet=strategy_wallet,
|
|
593
|
+
to_address=destination,
|
|
594
|
+
signing_callback=self._sign_callback,
|
|
595
|
+
)
|
|
596
|
+
if ok_send:
|
|
597
|
+
await asyncio.sleep(3)
|
|
598
|
+
messages.append(
|
|
599
|
+
f"HYPE transferred to HL: {hype_to_transfer:.4f}"
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# Sell HYPE for USDC on HL spot
|
|
603
|
+
(
|
|
604
|
+
ok_spot,
|
|
605
|
+
spot_state,
|
|
606
|
+
) = await self.hyperliquid_adapter.get_spot_user_state(address)
|
|
607
|
+
spot_hype = 0.0
|
|
608
|
+
if ok_spot and isinstance(spot_state, dict):
|
|
609
|
+
for bal in spot_state.get("balances", []):
|
|
610
|
+
if (
|
|
611
|
+
bal.get("coin") == "HYPE"
|
|
612
|
+
or bal.get("token") == "HYPE"
|
|
613
|
+
):
|
|
614
|
+
spot_hype = float(bal.get("total", 0)) - float(
|
|
615
|
+
bal.get("hold", 0)
|
|
616
|
+
)
|
|
617
|
+
break
|
|
618
|
+
|
|
619
|
+
if spot_hype > 0.01:
|
|
620
|
+
spot_asset_id, _ = await self._get_hype_asset_ids()
|
|
621
|
+
rounded_size = (
|
|
622
|
+
self.hyperliquid_adapter.get_valid_order_size(
|
|
623
|
+
int(spot_asset_id), spot_hype
|
|
624
|
+
)
|
|
625
|
+
)
|
|
626
|
+
if rounded_size > 0:
|
|
627
|
+
await self.hyperliquid_adapter.place_market_order(
|
|
628
|
+
asset_id=int(spot_asset_id),
|
|
629
|
+
is_buy=False,
|
|
630
|
+
slippage=0.10,
|
|
631
|
+
size=float(rounded_size),
|
|
632
|
+
address=address,
|
|
633
|
+
builder=self.builder_fee,
|
|
634
|
+
)
|
|
635
|
+
messages.append(
|
|
636
|
+
f"HYPE sold for USDC: {rounded_size:.4f}"
|
|
637
|
+
)
|
|
638
|
+
except Exception as e:
|
|
639
|
+
messages.append(f"HYPE liquidation error: {e}")
|
|
640
|
+
|
|
641
|
+
result_msg = f"[FAILSAFE] {reason} | {'; '.join(messages)}"
|
|
642
|
+
logger.error(result_msg)
|
|
643
|
+
self._failsafe_message = result_msg
|
|
644
|
+
|
|
645
|
+
return False, result_msg
|
|
646
|
+
|
|
647
|
+
async def _partial_trim_spot(
|
|
648
|
+
self, params: dict[str, Any], inventory: Inventory
|
|
649
|
+
) -> tuple[bool, str]:
|
|
650
|
+
trim_pct = float(params.get("trim_pct") or 0.25)
|
|
651
|
+
|
|
652
|
+
if inventory.spot_value_usd < 10.0:
|
|
653
|
+
return True, "No spot to trim"
|
|
654
|
+
|
|
655
|
+
if self.simulation:
|
|
656
|
+
return True, f"[SIMULATION] Trimmed {trim_pct:.0%} of spot to add margin"
|
|
657
|
+
|
|
658
|
+
if not self.balance_adapter:
|
|
659
|
+
return False, "Balance adapter not configured"
|
|
660
|
+
if not self.hyperliquid_adapter:
|
|
661
|
+
return False, "Hyperliquid adapter not configured"
|
|
662
|
+
if not self.brap_adapter:
|
|
663
|
+
return False, "BRAP adapter not configured"
|
|
664
|
+
if not self._sign_callback:
|
|
665
|
+
return False, "No strategy wallet signing callback configured"
|
|
666
|
+
|
|
667
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
668
|
+
address = strategy_wallet.get("address")
|
|
669
|
+
if not address:
|
|
670
|
+
return False, "No strategy wallet address configured"
|
|
671
|
+
|
|
672
|
+
hype_price = float(inventory.hype_price_usd or 0.0)
|
|
673
|
+
if hype_price <= 0:
|
|
674
|
+
ok_mid, mids = await self.hyperliquid_adapter.get_all_mid_prices()
|
|
675
|
+
if ok_mid and isinstance(mids, dict):
|
|
676
|
+
hype_price = float(mids.get("HYPE", 0.0))
|
|
677
|
+
if hype_price <= 0:
|
|
678
|
+
return False, "Could not determine HYPE price for trim"
|
|
679
|
+
|
|
680
|
+
trim_usd = float(inventory.spot_value_usd) * float(trim_pct)
|
|
681
|
+
|
|
682
|
+
# Sell kHYPE first (more liquid), then looped HYPE if needed.
|
|
683
|
+
if inventory.khype_value_usd > 0 and trim_usd > 1.0:
|
|
684
|
+
khype_trim_usd = min(float(inventory.khype_value_usd), trim_usd)
|
|
685
|
+
if inventory.khype_to_hype_ratio > 0:
|
|
686
|
+
khype_trim_tokens = (
|
|
687
|
+
khype_trim_usd / hype_price / float(inventory.khype_to_hype_ratio)
|
|
688
|
+
)
|
|
689
|
+
khype_trim_wei = int(khype_trim_tokens * 1e18)
|
|
690
|
+
if khype_trim_wei > 0:
|
|
691
|
+
try:
|
|
692
|
+
await self.brap_adapter.swap_from_token_ids(
|
|
693
|
+
from_token_id=KHYPE_LST,
|
|
694
|
+
to_token_id=HYPE_NATIVE,
|
|
695
|
+
from_address=address,
|
|
696
|
+
amount=str(int(khype_trim_wei)),
|
|
697
|
+
slippage=0.01,
|
|
698
|
+
strategy_name="boros_hype_strategy",
|
|
699
|
+
)
|
|
700
|
+
trim_usd -= khype_trim_usd
|
|
701
|
+
await asyncio.sleep(2)
|
|
702
|
+
except Exception as exc: # noqa: BLE001
|
|
703
|
+
logger.warning(f"Failed to sell kHYPE: {exc}")
|
|
704
|
+
|
|
705
|
+
if trim_usd > 1.0 and inventory.looped_hype_value_usd > 0:
|
|
706
|
+
lhype_trim_usd = min(float(inventory.looped_hype_value_usd), trim_usd)
|
|
707
|
+
if inventory.looped_hype_to_hype_ratio > 0:
|
|
708
|
+
lhype_trim_tokens = (
|
|
709
|
+
lhype_trim_usd
|
|
710
|
+
/ hype_price
|
|
711
|
+
/ float(inventory.looped_hype_to_hype_ratio)
|
|
712
|
+
)
|
|
713
|
+
lhype_trim_wei = int(lhype_trim_tokens * 1e18)
|
|
714
|
+
if lhype_trim_wei > 0:
|
|
715
|
+
try:
|
|
716
|
+
await self.brap_adapter.swap_from_token_ids(
|
|
717
|
+
from_token_id=LOOPED_HYPE,
|
|
718
|
+
to_token_id=HYPE_NATIVE,
|
|
719
|
+
from_address=address,
|
|
720
|
+
amount=str(int(lhype_trim_wei)),
|
|
721
|
+
slippage=0.01,
|
|
722
|
+
strategy_name="boros_hype_strategy",
|
|
723
|
+
)
|
|
724
|
+
await asyncio.sleep(2)
|
|
725
|
+
except Exception as exc: # noqa: BLE001
|
|
726
|
+
logger.warning(f"Failed to sell looped HYPE: {exc}")
|
|
727
|
+
|
|
728
|
+
ok_hype, hype_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
729
|
+
HYPE_NATIVE
|
|
730
|
+
)
|
|
731
|
+
hype_raw_int = (
|
|
732
|
+
int(hype_raw) if ok_hype and hype_raw and int(hype_raw) > 0 else 0
|
|
733
|
+
)
|
|
734
|
+
gas_reserve_wei = int(float(MIN_HYPE_GAS) * 1e18)
|
|
735
|
+
hype_to_transfer_raw = max(0, hype_raw_int - gas_reserve_wei)
|
|
736
|
+
if hype_to_transfer_raw < int(0.01 * 1e18):
|
|
737
|
+
return True, "No HYPE to transfer after trim"
|
|
738
|
+
|
|
739
|
+
destination = HyperliquidAdapter.hypercore_index_to_system_address(150)
|
|
740
|
+
ok_send, send_res = await self.balance_adapter.send_to_address(
|
|
741
|
+
token_id=HYPE_NATIVE,
|
|
742
|
+
amount=int(hype_to_transfer_raw),
|
|
743
|
+
from_wallet=strategy_wallet,
|
|
744
|
+
to_address=destination,
|
|
745
|
+
signing_callback=self._sign_callback,
|
|
746
|
+
)
|
|
747
|
+
if not ok_send:
|
|
748
|
+
return False, f"Failed to transfer HYPE to Hyperliquid: {send_res}"
|
|
749
|
+
await asyncio.sleep(3)
|
|
750
|
+
|
|
751
|
+
try:
|
|
752
|
+
spot_asset_id, perp_asset_id = await self._get_hype_asset_ids()
|
|
753
|
+
ok_spot, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
|
|
754
|
+
address
|
|
755
|
+
)
|
|
756
|
+
spot_hype_balance = 0.0
|
|
757
|
+
if ok_spot and isinstance(spot_state, dict):
|
|
758
|
+
for bal in spot_state.get("balances", []):
|
|
759
|
+
token = bal.get("coin") or bal.get("token")
|
|
760
|
+
hold = float(bal.get("hold", 0))
|
|
761
|
+
total = float(bal.get("total", 0))
|
|
762
|
+
available = total - hold
|
|
763
|
+
if token == "HYPE":
|
|
764
|
+
spot_hype_balance = available
|
|
765
|
+
break
|
|
766
|
+
|
|
767
|
+
if spot_hype_balance > 0.01:
|
|
768
|
+
rounded_units = self.hyperliquid_adapter.get_valid_order_size(
|
|
769
|
+
int(spot_asset_id), spot_hype_balance
|
|
770
|
+
)
|
|
771
|
+
if rounded_units > 0:
|
|
772
|
+
if (
|
|
773
|
+
inventory.hl_short_size_hype > 0.1
|
|
774
|
+
and inventory.hl_short_size_hype
|
|
775
|
+
>= inventory.total_hype_exposure
|
|
776
|
+
):
|
|
777
|
+
ok_lev, lev_msg = await self._ensure_hl_hype_leverage_set(
|
|
778
|
+
address
|
|
779
|
+
)
|
|
780
|
+
if not ok_lev:
|
|
781
|
+
return False, lev_msg
|
|
782
|
+
paired_filler = PairedFiller(
|
|
783
|
+
adapter=self.hyperliquid_adapter, address=address
|
|
784
|
+
)
|
|
785
|
+
(
|
|
786
|
+
_filled_spot,
|
|
787
|
+
_filled_perp,
|
|
788
|
+
_spot_notional,
|
|
789
|
+
_perp_notional,
|
|
790
|
+
spot_pointers,
|
|
791
|
+
perp_pointers,
|
|
792
|
+
) = await paired_filler.fill_pair_units(
|
|
793
|
+
coin="HYPE",
|
|
794
|
+
spot_asset_id=int(spot_asset_id),
|
|
795
|
+
perp_asset_id=int(perp_asset_id),
|
|
796
|
+
total_units=float(rounded_units),
|
|
797
|
+
direction="short_spot_long_perp",
|
|
798
|
+
builder_fee=self.builder_fee,
|
|
799
|
+
)
|
|
800
|
+
await self._cancel_lingering_orders(
|
|
801
|
+
spot_pointers + perp_pointers, address
|
|
802
|
+
)
|
|
803
|
+
await asyncio.sleep(2)
|
|
804
|
+
else:
|
|
805
|
+
await self.hyperliquid_adapter.place_market_order(
|
|
806
|
+
asset_id=int(spot_asset_id),
|
|
807
|
+
is_buy=False,
|
|
808
|
+
slippage=0.10,
|
|
809
|
+
size=float(rounded_units),
|
|
810
|
+
address=address,
|
|
811
|
+
builder=self.builder_fee,
|
|
812
|
+
)
|
|
813
|
+
await asyncio.sleep(2)
|
|
814
|
+
except Exception as exc: # noqa: BLE001
|
|
815
|
+
logger.warning(f"Failed to sell spot HYPE: {exc}")
|
|
816
|
+
|
|
817
|
+
try:
|
|
818
|
+
usdc_sz_decimals = (
|
|
819
|
+
await self.hyperliquid_adapter.get_spot_token_sz_decimals("USDC")
|
|
820
|
+
)
|
|
821
|
+
if usdc_sz_decimals is None:
|
|
822
|
+
usdc_sz_decimals = 2
|
|
823
|
+
|
|
824
|
+
ok_spot, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
|
|
825
|
+
address
|
|
826
|
+
)
|
|
827
|
+
spot_usdc = 0.0
|
|
828
|
+
spot_total_s = "0"
|
|
829
|
+
spot_hold_s = "0"
|
|
830
|
+
if ok_spot and isinstance(spot_state, dict):
|
|
831
|
+
for bal in spot_state.get("balances", []):
|
|
832
|
+
token = bal.get("coin") or bal.get("token")
|
|
833
|
+
if token != "USDC":
|
|
834
|
+
continue
|
|
835
|
+
spot_total_s = str(bal.get("total", "0") or "0")
|
|
836
|
+
spot_hold_s = str(bal.get("hold", "0") or "0")
|
|
837
|
+
hold = float(spot_hold_s)
|
|
838
|
+
total = float(spot_total_s)
|
|
839
|
+
spot_usdc = max(0.0, total - hold)
|
|
840
|
+
break
|
|
841
|
+
|
|
842
|
+
if spot_usdc > 1.0:
|
|
843
|
+
amount = self.hyperliquid_adapter.max_transferable_amount(
|
|
844
|
+
spot_total_s,
|
|
845
|
+
spot_hold_s,
|
|
846
|
+
sz_decimals=int(usdc_sz_decimals),
|
|
847
|
+
leave_one_tick=True,
|
|
848
|
+
)
|
|
849
|
+
(
|
|
850
|
+
ok_xfer,
|
|
851
|
+
res_xfer,
|
|
852
|
+
) = await self.hyperliquid_adapter.transfer_spot_to_perp(
|
|
853
|
+
amount=float(amount),
|
|
854
|
+
address=address,
|
|
855
|
+
)
|
|
856
|
+
if (not ok_xfer) and int(usdc_sz_decimals) != 2:
|
|
857
|
+
if "insufficient balance" in str(res_xfer).lower():
|
|
858
|
+
fallback_2dp = self.hyperliquid_adapter.max_transferable_amount(
|
|
859
|
+
spot_total_s,
|
|
860
|
+
spot_hold_s,
|
|
861
|
+
sz_decimals=2,
|
|
862
|
+
leave_one_tick=True,
|
|
863
|
+
)
|
|
864
|
+
if fallback_2dp > 1.0:
|
|
865
|
+
await self.hyperliquid_adapter.transfer_spot_to_perp(
|
|
866
|
+
amount=float(fallback_2dp),
|
|
867
|
+
address=address,
|
|
868
|
+
)
|
|
869
|
+
except Exception as exc: # noqa: BLE001
|
|
870
|
+
logger.warning(f"Failed to move USDC spot→perp: {exc}")
|
|
871
|
+
|
|
872
|
+
inv_after = await self.observe()
|
|
873
|
+
ok_short, msg_short = await self._ensure_hl_short(
|
|
874
|
+
{
|
|
875
|
+
"target_size": inv_after.total_hype_exposure,
|
|
876
|
+
"current_size": inv_after.hl_short_size_hype,
|
|
877
|
+
},
|
|
878
|
+
inv_after,
|
|
879
|
+
)
|
|
880
|
+
if not ok_short:
|
|
881
|
+
return False, f"Failed to resize short after trim: {msg_short}"
|
|
882
|
+
|
|
883
|
+
return (
|
|
884
|
+
True,
|
|
885
|
+
f"Trimmed spot and resized short to {inv_after.total_hype_exposure:.4f} HYPE.",
|
|
886
|
+
)
|