wayfinder-paths 0.1.23__py3-none-any.whl → 0.1.24__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.

Files changed (122) hide show
  1. wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
  2. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
  4. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  5. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  6. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  7. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  8. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  9. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  10. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  11. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  12. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  13. wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
  14. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  15. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
  16. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  17. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
  18. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  19. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
  20. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  21. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  22. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  23. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  24. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  27. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  28. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  29. wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
  30. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  31. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
  32. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  33. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  34. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  35. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  36. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  37. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  38. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  39. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  40. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  41. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  42. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  43. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  44. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  45. wayfinder_paths/conftest.py +24 -17
  46. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  47. wayfinder_paths/core/adapters/models.py +17 -7
  48. wayfinder_paths/core/clients/BRAPClient.py +1 -1
  49. wayfinder_paths/core/clients/TokenClient.py +47 -1
  50. wayfinder_paths/core/clients/WayfinderClient.py +1 -2
  51. wayfinder_paths/core/clients/protocols.py +21 -22
  52. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  53. wayfinder_paths/core/config.py +12 -0
  54. wayfinder_paths/core/constants/__init__.py +15 -0
  55. wayfinder_paths/core/constants/base.py +6 -1
  56. wayfinder_paths/core/constants/contracts.py +39 -26
  57. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  58. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  59. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  60. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  61. wayfinder_paths/core/engine/manifest.py +66 -0
  62. wayfinder_paths/core/strategies/Strategy.py +0 -61
  63. wayfinder_paths/core/strategies/__init__.py +10 -1
  64. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  65. wayfinder_paths/core/utils/test_transaction.py +289 -0
  66. wayfinder_paths/core/utils/transaction.py +44 -1
  67. wayfinder_paths/core/utils/web3.py +3 -0
  68. wayfinder_paths/mcp/__init__.py +5 -0
  69. wayfinder_paths/mcp/preview.py +185 -0
  70. wayfinder_paths/mcp/scripting.py +84 -0
  71. wayfinder_paths/mcp/server.py +52 -0
  72. wayfinder_paths/mcp/state/profile_store.py +195 -0
  73. wayfinder_paths/mcp/state/store.py +89 -0
  74. wayfinder_paths/mcp/test_scripting.py +267 -0
  75. wayfinder_paths/mcp/tools/__init__.py +0 -0
  76. wayfinder_paths/mcp/tools/balances.py +290 -0
  77. wayfinder_paths/mcp/tools/discovery.py +158 -0
  78. wayfinder_paths/mcp/tools/execute.py +770 -0
  79. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  80. wayfinder_paths/mcp/tools/quotes.py +288 -0
  81. wayfinder_paths/mcp/tools/run_script.py +286 -0
  82. wayfinder_paths/mcp/tools/strategies.py +188 -0
  83. wayfinder_paths/mcp/tools/tokens.py +46 -0
  84. wayfinder_paths/mcp/tools/wallets.py +354 -0
  85. wayfinder_paths/mcp/utils.py +129 -0
  86. wayfinder_paths/policies/hyperliquid.py +1 -1
  87. wayfinder_paths/policies/lifi.py +18 -0
  88. wayfinder_paths/policies/util.py +8 -2
  89. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
  90. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  91. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  92. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  93. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  94. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  95. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  96. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  97. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  98. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  99. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  100. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  101. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  102. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  103. wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
  104. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  105. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  106. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
  107. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
  108. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
  109. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  110. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
  111. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
  112. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  113. wayfinder_paths/tests/test_test_coverage.py +1 -4
  114. wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
  115. wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
  116. {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
  117. wayfinder_paths/scripts/create_strategy.py +0 -139
  118. wayfinder_paths/scripts/make_wallets.py +0 -142
  119. wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
  120. wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
  121. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  122. {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +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
+ )