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.

Files changed (124) hide show
  1. wayfinder_paths/__init__.py +2 -0
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
  3. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  4. wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
  5. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  6. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  7. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  8. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  9. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  10. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  11. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  12. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  13. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  14. wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
  15. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  16. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
  17. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  18. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
  19. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  20. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
  21. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  22. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  23. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  24. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  27. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  28. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  29. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  30. wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
  31. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  32. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
  33. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  34. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  35. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  36. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  37. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  38. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  39. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  40. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  41. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  42. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  43. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  44. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  45. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  46. wayfinder_paths/conftest.py +24 -17
  47. wayfinder_paths/core/__init__.py +2 -0
  48. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  49. wayfinder_paths/core/adapters/models.py +17 -7
  50. wayfinder_paths/core/clients/BRAPClient.py +1 -1
  51. wayfinder_paths/core/clients/TokenClient.py +47 -1
  52. wayfinder_paths/core/clients/WayfinderClient.py +1 -2
  53. wayfinder_paths/core/clients/protocols.py +21 -22
  54. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  55. wayfinder_paths/core/config.py +12 -0
  56. wayfinder_paths/core/constants/__init__.py +15 -0
  57. wayfinder_paths/core/constants/base.py +6 -1
  58. wayfinder_paths/core/constants/contracts.py +39 -26
  59. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  60. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  61. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  62. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  63. wayfinder_paths/core/engine/manifest.py +66 -0
  64. wayfinder_paths/core/strategies/Strategy.py +0 -61
  65. wayfinder_paths/core/strategies/__init__.py +10 -1
  66. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  67. wayfinder_paths/core/utils/test_transaction.py +289 -0
  68. wayfinder_paths/core/utils/transaction.py +44 -1
  69. wayfinder_paths/core/utils/web3.py +3 -0
  70. wayfinder_paths/mcp/__init__.py +5 -0
  71. wayfinder_paths/mcp/preview.py +185 -0
  72. wayfinder_paths/mcp/scripting.py +84 -0
  73. wayfinder_paths/mcp/server.py +52 -0
  74. wayfinder_paths/mcp/state/profile_store.py +195 -0
  75. wayfinder_paths/mcp/state/store.py +89 -0
  76. wayfinder_paths/mcp/test_scripting.py +267 -0
  77. wayfinder_paths/mcp/tools/__init__.py +0 -0
  78. wayfinder_paths/mcp/tools/balances.py +290 -0
  79. wayfinder_paths/mcp/tools/discovery.py +158 -0
  80. wayfinder_paths/mcp/tools/execute.py +770 -0
  81. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  82. wayfinder_paths/mcp/tools/quotes.py +288 -0
  83. wayfinder_paths/mcp/tools/run_script.py +286 -0
  84. wayfinder_paths/mcp/tools/strategies.py +188 -0
  85. wayfinder_paths/mcp/tools/tokens.py +46 -0
  86. wayfinder_paths/mcp/tools/wallets.py +354 -0
  87. wayfinder_paths/mcp/utils.py +129 -0
  88. wayfinder_paths/policies/hyperliquid.py +1 -1
  89. wayfinder_paths/policies/lifi.py +18 -0
  90. wayfinder_paths/policies/util.py +8 -2
  91. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
  92. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  93. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  94. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  95. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  96. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  97. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  98. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  99. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  100. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  101. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  102. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  103. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  104. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  105. wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
  106. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  107. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  108. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
  109. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
  110. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
  111. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  112. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
  113. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
  114. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  115. wayfinder_paths/tests/test_test_coverage.py +1 -4
  116. wayfinder_paths-0.1.25.dist-info/METADATA +377 -0
  117. wayfinder_paths-0.1.25.dist-info/RECORD +185 -0
  118. wayfinder_paths/scripts/create_strategy.py +0 -139
  119. wayfinder_paths/scripts/make_wallets.py +0 -142
  120. wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
  121. wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
  122. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  123. {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/LICENSE +0 -0
  124. {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/WHEEL +0 -0
@@ -0,0 +1,642 @@
1
+ """
2
+ Hyperliquid 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
+ from typing import Any
10
+
11
+ from loguru import logger
12
+
13
+ from wayfinder_paths.adapters.hyperliquid_adapter.adapter import (
14
+ HYPERLIQUID_BRIDGE_ADDRESS,
15
+ )
16
+ from wayfinder_paths.adapters.hyperliquid_adapter.paired_filler import (
17
+ MIN_NOTIONAL_USD,
18
+ PairedFiller,
19
+ )
20
+
21
+ from .constants import MAX_HL_LEVERAGE, USDC_ARB
22
+ from .types import Inventory
23
+
24
+
25
+ class BorosHypeHyperliquidOpsMixin:
26
+ async def _get_hype_asset_ids(self) -> tuple[int, int]:
27
+ if not self.hyperliquid_adapter:
28
+ raise RuntimeError("Hyperliquid adapter not configured")
29
+
30
+ perp_asset_id = self.hyperliquid_adapter.coin_to_asset.get("HYPE")
31
+ if perp_asset_id is None:
32
+ raise RuntimeError("HYPE perp asset ID not found")
33
+
34
+ spot_asset_id = await self.hyperliquid_adapter.get_spot_asset_id("HYPE", "USDC")
35
+ if spot_asset_id is None:
36
+ success, spot_assets = await self.hyperliquid_adapter.get_spot_assets()
37
+ if success and isinstance(spot_assets, dict):
38
+ spot_asset_id = spot_assets.get("HYPE/USDC")
39
+
40
+ if spot_asset_id is None:
41
+ raise RuntimeError("HYPE/USDC spot asset ID not found")
42
+
43
+ return int(spot_asset_id), int(perp_asset_id)
44
+
45
+ async def _ensure_hl_hype_leverage_set(self, address: str) -> tuple[bool, str]:
46
+ if self.simulation:
47
+ self._planner_runtime.leverage_set_for_hype = True
48
+ return True, "[SIMULATION] HYPE leverage set"
49
+
50
+ if not self.hyperliquid_adapter:
51
+ return False, "Hyperliquid adapter not configured"
52
+ if not self._sign_callback:
53
+ return False, "No strategy wallet signing callback configured"
54
+
55
+ if self._planner_runtime.leverage_set_for_hype:
56
+ return True, "HYPE leverage already set"
57
+
58
+ perp_asset_id = self.hyperliquid_adapter.coin_to_asset.get("HYPE")
59
+ if perp_asset_id is None:
60
+ return False, "HYPE perp asset ID not found"
61
+
62
+ ok_lev, lev_res = await self.hyperliquid_adapter.update_leverage(
63
+ asset_id=int(perp_asset_id),
64
+ leverage=int(MAX_HL_LEVERAGE),
65
+ is_cross=True,
66
+ address=address,
67
+ )
68
+ if not ok_lev:
69
+ return False, f"Failed to update Hyperliquid leverage: {lev_res}"
70
+
71
+ self._planner_runtime.leverage_set_for_hype = True
72
+ logger.info(f"Set Hyperliquid HYPE leverage to {int(MAX_HL_LEVERAGE)}x (cross)")
73
+ return True, f"Set Hyperliquid HYPE leverage to {int(MAX_HL_LEVERAGE)}x (cross)"
74
+
75
+ async def _cancel_lingering_orders(
76
+ self, pointers: list[dict[str, Any]], address: str
77
+ ) -> None:
78
+ if not self.hyperliquid_adapter:
79
+ return
80
+
81
+ for pointer in pointers:
82
+ metadata = pointer.get("metadata") if isinstance(pointer, dict) else None
83
+ if not isinstance(metadata, dict):
84
+ continue
85
+ asset_id = metadata.get("asset_id")
86
+ cloid = metadata.get("client_id")
87
+ if asset_id is None or not cloid:
88
+ continue
89
+ try:
90
+ await self.hyperliquid_adapter.cancel_order_by_cloid(
91
+ int(asset_id), str(cloid), address
92
+ )
93
+ except Exception as exc:
94
+ logger.debug(
95
+ f"Failed to cancel lingering order: asset_id={asset_id}, cloid={cloid}, err={exc}"
96
+ )
97
+
98
+ async def _cancel_hl_open_orders_for_hype(self, address: str) -> None:
99
+ if not self.hyperliquid_adapter:
100
+ return
101
+
102
+ try:
103
+ spot_asset_id, perp_asset_id = await self._get_hype_asset_ids()
104
+ except Exception as exc: # noqa: BLE001
105
+ logger.warning(f"Failed to resolve HYPE asset ids for cancel: {exc}")
106
+ return
107
+
108
+ spot_coin = f"@{spot_asset_id - 10000}" if spot_asset_id >= 10000 else None
109
+
110
+ success, open_orders = await self.hyperliquid_adapter.get_frontend_open_orders(
111
+ address
112
+ )
113
+ if not success or not isinstance(open_orders, list):
114
+ logger.warning("Could not fetch Hyperliquid open orders to cancel")
115
+ return
116
+
117
+ canceled = 0
118
+ for order in open_orders:
119
+ if not isinstance(order, dict):
120
+ continue
121
+ order_coin = order.get("coin") or order.get("asset") or ""
122
+ order_id = order.get("oid") or order.get("orderId") or order.get("id")
123
+ if not order_id:
124
+ continue
125
+
126
+ try:
127
+ if str(order_coin) == "HYPE":
128
+ await self.hyperliquid_adapter.cancel_order(
129
+ asset_id=int(perp_asset_id),
130
+ order_id=order_id,
131
+ address=address,
132
+ )
133
+ canceled += 1
134
+ elif spot_coin and str(order_coin) == spot_coin:
135
+ await self.hyperliquid_adapter.cancel_order(
136
+ asset_id=int(spot_asset_id),
137
+ order_id=order_id,
138
+ address=address,
139
+ )
140
+ canceled += 1
141
+ except Exception as exc: # noqa: BLE001
142
+ logger.debug(
143
+ f"Failed cancel HL order: coin={order_coin} oid={order_id} err={exc}"
144
+ )
145
+
146
+ if canceled:
147
+ logger.info(f"Canceled {canceled} Hyperliquid HYPE order(s)")
148
+
149
+ async def _deploy_excess_hl_margin(
150
+ self, params: dict[str, Any], inventory: Inventory
151
+ ) -> tuple[bool, str]:
152
+ # Flow: Transfer USDC perp→spot, buy HYPE on spot, bridge to HyperEVM
153
+ excess_margin = params.get("excess_margin_usd", 0)
154
+
155
+ if excess_margin < 5:
156
+ return True, "Skipping small excess margin deployment"
157
+
158
+ if not self.hyperliquid_adapter:
159
+ return False, "Hyperliquid adapter not configured"
160
+
161
+ strategy_wallet = self._config.get("strategy_wallet", {})
162
+ address = strategy_wallet.get("address")
163
+ if not address:
164
+ return False, "No strategy wallet address configured"
165
+
166
+ if self.simulation:
167
+ return (
168
+ True,
169
+ f"[SIMULATION] Deployed ${excess_margin:.2f} excess HL margin to spot",
170
+ )
171
+
172
+ hype_price = float(inventory.hype_price_usd or 0.0)
173
+ if hype_price <= 0:
174
+ hype_price = 25.0
175
+
176
+ success, result = await self.hyperliquid_adapter.transfer_perp_to_spot(
177
+ amount=excess_margin,
178
+ address=address,
179
+ )
180
+
181
+ if not success:
182
+ error_msg = result if isinstance(result, str) else str(result)
183
+ return False, f"Perp to spot transfer failed: {error_msg}"
184
+
185
+ logger.info(f"Transferred ${excess_margin:.2f} from perp margin to spot")
186
+
187
+ if excess_margin < MIN_NOTIONAL_USD:
188
+ return True, f"Excess margin ${excess_margin:.2f} too small for paired fill"
189
+
190
+ hype_to_buy = (excess_margin * 0.98) / hype_price # 2% buffer
191
+
192
+ spot_asset_id, perp_asset_id = await self._get_hype_asset_ids()
193
+ paired_filler = PairedFiller(adapter=self.hyperliquid_adapter, address=address)
194
+
195
+ ok_lev, lev_msg = await self._ensure_hl_hype_leverage_set(address)
196
+ if not ok_lev:
197
+ return False, lev_msg
198
+
199
+ try:
200
+ (
201
+ filled_spot,
202
+ filled_perp,
203
+ spot_notional,
204
+ perp_notional,
205
+ spot_pointers,
206
+ perp_pointers,
207
+ ) = await paired_filler.fill_pair_units(
208
+ coin="HYPE",
209
+ spot_asset_id=spot_asset_id,
210
+ perp_asset_id=perp_asset_id,
211
+ total_units=hype_to_buy,
212
+ direction="long_spot_short_perp",
213
+ builder_fee=self.builder_fee,
214
+ )
215
+ logger.info(
216
+ f"Paired fill from excess margin complete: "
217
+ f"spot={filled_spot:.4f} (${spot_notional:.2f}), "
218
+ f"perp={filled_perp:.4f} (${perp_notional:.2f})"
219
+ )
220
+ await self._cancel_lingering_orders(spot_pointers + perp_pointers, address)
221
+ except Exception as exc:
222
+ logger.error(f"Paired fill from excess margin failed: {exc}")
223
+ return False, f"Paired fill failed: {exc}"
224
+
225
+ try:
226
+ success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
227
+ address
228
+ )
229
+ if not success or not isinstance(spot_state, dict):
230
+ return (
231
+ True,
232
+ f"Deployed ${excess_margin:.2f} (bridge pending spot state)",
233
+ )
234
+
235
+ balances = spot_state.get("balances", [])
236
+ actual_hype = 0.0
237
+ for bal in balances:
238
+ coin = bal.get("coin") or bal.get("token")
239
+ if coin != "HYPE":
240
+ continue
241
+ hold = float(bal.get("hold", 0))
242
+ total = float(bal.get("total", 0))
243
+ actual_hype = max(0.0, total - hold)
244
+ break
245
+
246
+ amount_to_bridge = actual_hype - 0.001
247
+ if amount_to_bridge < 0.1:
248
+ return True, f"Deployed ${excess_margin:.2f} (no HYPE to bridge)"
249
+
250
+ ok, res = await self.hyperliquid_adapter.hypercore_to_hyperevm(
251
+ amount=amount_to_bridge,
252
+ address=address,
253
+ )
254
+ if ok:
255
+ return (
256
+ True,
257
+ f"Deployed ${excess_margin:.2f} excess margin → {amount_to_bridge:.4f} HYPE to HyperEVM",
258
+ )
259
+
260
+ err = res if isinstance(res, str) else str(res)
261
+ return False, f"Bridge failed: {err}"
262
+
263
+ except Exception as exc:
264
+ return False, f"Bridge failed: {exc}"
265
+
266
+ async def _transfer_hl_spot_to_hyperevm(
267
+ self, params: dict[str, Any], inventory: Inventory
268
+ ) -> tuple[bool, str]:
269
+ # HL spot HYPE withdrawal goes directly to HyperEVM (native chain) via L1 withdrawal
270
+ hype_amount = params.get("hype_amount", 0)
271
+
272
+ if hype_amount < 0.1:
273
+ return True, "Skipping small HYPE transfer"
274
+
275
+ if not self.hyperliquid_adapter:
276
+ return False, "Hyperliquid adapter not configured"
277
+
278
+ strategy_wallet = self._config.get("strategy_wallet", {})
279
+ address = strategy_wallet.get("address")
280
+ if not address:
281
+ return False, "No strategy wallet address configured"
282
+
283
+ if self.simulation:
284
+ return (
285
+ True,
286
+ f"[SIMULATION] Transferred {hype_amount:.4f} HYPE from HL spot to HyperEVM",
287
+ )
288
+
289
+ try:
290
+ success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
291
+ address
292
+ )
293
+ if not success or not isinstance(spot_state, dict):
294
+ return False, "Failed to read HL spot balances"
295
+
296
+ balances = spot_state.get("balances", [])
297
+ actual_hype = 0.0
298
+ for bal in balances:
299
+ coin = bal.get("coin") or bal.get("token")
300
+ if coin != "HYPE":
301
+ continue
302
+ hold = float(bal.get("hold", 0))
303
+ total = float(bal.get("total", 0))
304
+ actual_hype = max(0.0, total - hold)
305
+ break
306
+
307
+ transfer_amount = actual_hype - 0.001
308
+ if transfer_amount < 0.1:
309
+ return True, "Insufficient HYPE balance to transfer"
310
+
311
+ ok, res = await self.hyperliquid_adapter.hypercore_to_hyperevm(
312
+ amount=transfer_amount,
313
+ address=address,
314
+ )
315
+ if ok:
316
+ return True, f"Transferred {transfer_amount:.4f} HYPE to HyperEVM"
317
+
318
+ err = res if isinstance(res, str) else str(res)
319
+ return False, f"Transfer failed: {err}"
320
+
321
+ except Exception as exc:
322
+ logger.error(f"HYPE spot transfer failed: {exc}")
323
+ return False, f"HL spot transfer failed: {exc}"
324
+
325
+ async def _ensure_hl_short(
326
+ self, params: dict[str, Any], inventory: Inventory
327
+ ) -> tuple[bool, str]:
328
+ # Safety: 2x leverage, venue-valid rounding, check free margin before increasing
329
+ target_size = float(params.get("target_size") or 0.0)
330
+ current_size = float(params.get("current_size") or 0.0)
331
+
332
+ # If both target and current are negligible, do nothing.
333
+ # If target is ~0 but we still have a short, fall through so we can close it.
334
+ if target_size < 0.01 and abs(current_size) < 0.01:
335
+ return True, "No HYPE exposure to hedge"
336
+
337
+ tol = max(
338
+ self._planner_config.delta_neutral_abs_tol_hype,
339
+ target_size * self._planner_config.delta_neutral_rel_tol,
340
+ )
341
+ delta = target_size - current_size
342
+ diff = abs(delta)
343
+ if diff <= tol:
344
+ return (
345
+ True,
346
+ f"HYPE hedge within tolerance: Δ={diff:.4f} tol={tol:.4f} "
347
+ f"(spot={target_size:.4f}, short={current_size:.4f})",
348
+ )
349
+
350
+ if not self.hyperliquid_adapter:
351
+ return False, "Hyperliquid adapter not configured"
352
+
353
+ strategy_wallet = self._config.get("strategy_wallet", {})
354
+ address = strategy_wallet.get("address")
355
+ if not address:
356
+ return False, "No strategy wallet address configured"
357
+
358
+ perp_asset_id = self.hyperliquid_adapter.coin_to_asset.get("HYPE")
359
+ if perp_asset_id is None:
360
+ return False, "HYPE perp asset ID not found"
361
+
362
+ ok_lev, lev_msg = await self._ensure_hl_hype_leverage_set(address)
363
+ if not ok_lev:
364
+ return False, lev_msg
365
+
366
+ hype_price = float(inventory.hype_price_usd or 0.0)
367
+ if hype_price <= 0:
368
+ return False, "Cannot hedge: HYPE price unavailable"
369
+
370
+ # delta > 0 => need to INCREASE short (sell more)
371
+ if delta > 0:
372
+ min_increase_needed = max(0.0, diff - tol)
373
+ free_margin = float(inventory.hl_withdrawable_usd or 0.0)
374
+ max_increase_by_margin = (
375
+ (free_margin * MAX_HL_LEVERAGE) / hype_price if hype_price > 0 else 0.0
376
+ )
377
+ desired_increase = min(diff, max_increase_by_margin)
378
+ required_margin = (min_increase_needed * hype_price) / MAX_HL_LEVERAGE
379
+
380
+ if free_margin < required_margin * 0.9:
381
+ return (
382
+ False,
383
+ f"Insufficient free margin (${free_margin:.2f}) to increase short by {diff:.4f} HYPE "
384
+ f"(need ${required_margin:.2f}, tol={tol:.4f}). Consider trimming spot.",
385
+ )
386
+
387
+ rounded_size = self.hyperliquid_adapter.get_valid_order_size(
388
+ int(perp_asset_id), desired_increase
389
+ )
390
+ if rounded_size <= 0:
391
+ return (
392
+ False,
393
+ f"Hedge mismatch Δ={diff:.4f} tol={tol:.4f} but order size rounds to 0.",
394
+ )
395
+ if rounded_size + 1e-9 < min_increase_needed:
396
+ return (
397
+ False,
398
+ "Insufficient free margin to hedge within tolerance after rounding "
399
+ f"(need ≥{min_increase_needed:.4f} HYPE, got {rounded_size:.4f}).",
400
+ )
401
+
402
+ order_value_usd = rounded_size * hype_price
403
+ if order_value_usd < float(MIN_NOTIONAL_USD):
404
+ return True, (
405
+ f"Delta {diff:.4f} HYPE below HL ${MIN_NOTIONAL_USD:.0f} minimum, acceptable"
406
+ )
407
+
408
+ if self.simulation:
409
+ return True, (
410
+ f"[SIMULATION] Increased HYPE short by {rounded_size:.4f} "
411
+ f"(target={target_size:.4f}, current={current_size:.4f})"
412
+ )
413
+
414
+ ok, res = await self.hyperliquid_adapter.place_market_order(
415
+ asset_id=int(perp_asset_id),
416
+ is_buy=False, # sell to increase short
417
+ slippage=0.05,
418
+ size=float(rounded_size),
419
+ address=address,
420
+ builder=self.builder_fee,
421
+ )
422
+ if not ok:
423
+ return False, f"Failed to increase HYPE short: {res}"
424
+
425
+ return True, f"Increased HYPE short by {rounded_size:.4f}"
426
+
427
+ # delta < 0 => need to REDUCE short (buy back)
428
+ reduce_units = min(diff, current_size)
429
+ rounded_size = self.hyperliquid_adapter.get_valid_order_size(
430
+ int(perp_asset_id), reduce_units
431
+ )
432
+ if rounded_size <= 0:
433
+ return True, "No short position to reduce"
434
+
435
+ order_value_usd = rounded_size * hype_price
436
+ if order_value_usd < float(MIN_NOTIONAL_USD):
437
+ return True, (
438
+ f"Delta {diff:.4f} HYPE below HL ${MIN_NOTIONAL_USD:.0f} minimum, acceptable"
439
+ )
440
+
441
+ if self.simulation:
442
+ return True, (
443
+ f"[SIMULATION] Reduced HYPE short by {rounded_size:.4f} "
444
+ f"(target={target_size:.4f}, current={current_size:.4f})"
445
+ )
446
+
447
+ ok, res = await self.hyperliquid_adapter.place_market_order(
448
+ asset_id=int(perp_asset_id),
449
+ is_buy=True, # buy to reduce short
450
+ slippage=0.05,
451
+ size=float(rounded_size),
452
+ address=address,
453
+ reduce_only=True,
454
+ builder=self.builder_fee,
455
+ )
456
+ if not ok:
457
+ return False, f"Failed to reduce HYPE short: {res}"
458
+
459
+ return True, f"Reduced HYPE short by {rounded_size:.4f}"
460
+
461
+ async def _send_usdc_to_hl(
462
+ self, params: dict[str, Any], inventory: Inventory
463
+ ) -> tuple[bool, str]:
464
+ amount_usd = params.get("amount_usd", 0.0)
465
+
466
+ if amount_usd < self._planner_config.min_usdc_action:
467
+ return True, f"Skipping small USDC send (${amount_usd:.2f})"
468
+
469
+ if not self.balance_adapter:
470
+ return False, "Balance adapter not configured"
471
+
472
+ if not self.hyperliquid_adapter:
473
+ return False, "Hyperliquid adapter not configured"
474
+
475
+ strategy_wallet = self._config.get("strategy_wallet", {})
476
+ address = strategy_wallet.get("address")
477
+ if not address:
478
+ return False, "No strategy wallet address configured"
479
+
480
+ if self.simulation:
481
+ return True, f"[SIMULATION] Sent ${amount_usd:.2f} USDC to Hyperliquid"
482
+
483
+ usdc_raw = int(float(amount_usd) * 1e6)
484
+ success, result = await self.balance_adapter.send_to_address(
485
+ token_id=USDC_ARB,
486
+ amount=usdc_raw,
487
+ from_wallet=strategy_wallet,
488
+ to_address=HYPERLIQUID_BRIDGE_ADDRESS,
489
+ signing_callback=self._sign_callback,
490
+ )
491
+ if not success:
492
+ return False, f"Failed to send USDC to HL bridge: {result}"
493
+
494
+ confirmed, final_balance = await self.hyperliquid_adapter.wait_for_deposit(
495
+ address=address,
496
+ expected_increase=float(amount_usd),
497
+ timeout_s=240,
498
+ poll_interval_s=10,
499
+ )
500
+ if not confirmed:
501
+ return False, (
502
+ f"USDC sent to bridge but not confirmed on Hyperliquid within timeout. "
503
+ f"Current HL balance: ${final_balance:.2f}"
504
+ )
505
+
506
+ return (
507
+ True,
508
+ f"Sent ${amount_usd:.2f} USDC to Hyperliquid (balance=${final_balance:.2f})",
509
+ )
510
+
511
+ async def _bridge_to_hyperevm(
512
+ self, params: dict[str, Any], inventory: Inventory
513
+ ) -> tuple[bool, str]:
514
+ # Assumes Arb→HL deposit handled by SEND_USDC_TO_HL. We: 1) xfer perp→spot,
515
+ # 2) paired fill (long spot / short perp), 3) bridge spot HYPE to HyperEVM.
516
+ desired_usd = float(params.get("amount_usd") or 0.0)
517
+ reserve_hl_margin_usd = float(params.get("reserve_hl_margin_usd") or 0.0)
518
+
519
+ if desired_usd < max(self._planner_config.min_usdc_action, MIN_NOTIONAL_USD):
520
+ # Return False to not trigger re-observation when nothing changes
521
+ return False, f"Skipping small bridge (${desired_usd:.2f})"
522
+
523
+ if not self.hyperliquid_adapter:
524
+ return False, "Hyperliquid adapter not configured"
525
+
526
+ strategy_wallet = self._config.get("strategy_wallet", {})
527
+ address = strategy_wallet.get("address")
528
+ if not address:
529
+ return False, "No strategy wallet address configured"
530
+
531
+ if self.simulation:
532
+ return True, f"[SIMULATION] Bridged ${desired_usd:.2f} to HyperEVM"
533
+
534
+ hype_price = float(inventory.hype_price_usd or 0.0)
535
+ if hype_price <= 0:
536
+ success, prices = await self.hyperliquid_adapter.get_all_mid_prices()
537
+ if success and isinstance(prices, dict):
538
+ hype_price = float(prices.get("HYPE", 0.0))
539
+ if hype_price <= 0:
540
+ return False, "Could not determine HYPE price"
541
+
542
+ # Transfer from perp→spot, but never below the reserved perp margin.
543
+ spot_usdc_before = float(inventory.hl_spot_usdc or 0.0)
544
+ perp_margin = float(inventory.hl_perp_margin or 0.0)
545
+ withdrawable = float(inventory.hl_withdrawable_usd or 0.0)
546
+
547
+ transferable_from_perp = max(
548
+ 0.0,
549
+ min(
550
+ withdrawable,
551
+ perp_margin - reserve_hl_margin_usd,
552
+ ),
553
+ )
554
+
555
+ need_from_perp = max(0.0, desired_usd - spot_usdc_before)
556
+ xfer_usd = min(need_from_perp, transferable_from_perp)
557
+
558
+ if xfer_usd >= self._planner_config.min_usdc_action:
559
+ xfer_ok, xfer_res = await self.hyperliquid_adapter.transfer_perp_to_spot(
560
+ amount=float(xfer_usd),
561
+ address=address,
562
+ )
563
+ if not xfer_ok:
564
+ return False, f"Perp→spot transfer failed: {xfer_res}"
565
+ logger.info(
566
+ f"Transferred ${xfer_usd:.2f} from HL perp→spot "
567
+ f"(reserve_perp=${reserve_hl_margin_usd:.2f})"
568
+ )
569
+
570
+ deployable_usd = min(desired_usd, spot_usdc_before + xfer_usd)
571
+ if deployable_usd < MIN_NOTIONAL_USD:
572
+ return False, (
573
+ "Insufficient deployable HL spot USDC after reserving perp margin "
574
+ f"(${deployable_usd:.2f} < ${MIN_NOTIONAL_USD:.2f}; "
575
+ f"reserve=${reserve_hl_margin_usd:.2f}, perp=${perp_margin:.2f}, "
576
+ f"withdrawable=${withdrawable:.2f}, spot=${spot_usdc_before:.2f})"
577
+ )
578
+
579
+ # Atomic Paired Fill: buy spot HYPE, short perp HYPE
580
+ hype_units = (deployable_usd * 0.98) / hype_price # 2% buffer
581
+ spot_asset_id, perp_asset_id = await self._get_hype_asset_ids()
582
+ paired_filler = PairedFiller(adapter=self.hyperliquid_adapter, address=address)
583
+
584
+ ok_lev, lev_msg = await self._ensure_hl_hype_leverage_set(address)
585
+ if not ok_lev:
586
+ return False, lev_msg
587
+
588
+ try:
589
+ (
590
+ filled_spot,
591
+ filled_perp,
592
+ spot_notional,
593
+ perp_notional,
594
+ spot_pointers,
595
+ perp_pointers,
596
+ ) = await paired_filler.fill_pair_units(
597
+ coin="HYPE",
598
+ spot_asset_id=spot_asset_id,
599
+ perp_asset_id=perp_asset_id,
600
+ total_units=hype_units,
601
+ direction="long_spot_short_perp",
602
+ builder_fee=self.builder_fee,
603
+ )
604
+ logger.info(
605
+ f"Paired fill complete: spot={filled_spot:.4f} (${spot_notional:.2f}), "
606
+ f"perp={filled_perp:.4f} (${perp_notional:.2f})"
607
+ )
608
+ await self._cancel_lingering_orders(spot_pointers + perp_pointers, address)
609
+ except Exception as exc:
610
+ logger.error(f"Paired fill failed: {exc}")
611
+ return False, f"Paired fill failed: {exc}"
612
+
613
+ success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
614
+ address
615
+ )
616
+ if not success or not isinstance(spot_state, dict):
617
+ return False, "Failed to read HL spot balances after paired fill"
618
+
619
+ balances = spot_state.get("balances", [])
620
+ actual_hype = 0.0
621
+ for bal in balances:
622
+ coin = bal.get("coin") or bal.get("token")
623
+ if coin != "HYPE":
624
+ continue
625
+ hold = float(bal.get("hold", 0))
626
+ total = float(bal.get("total", 0))
627
+ actual_hype = max(0.0, total - hold)
628
+ break
629
+
630
+ amount_to_bridge = actual_hype - 0.001
631
+ if amount_to_bridge < 0.1:
632
+ return False, "No HYPE available to bridge to HyperEVM"
633
+
634
+ ok, res = await self.hyperliquid_adapter.hypercore_to_hyperevm(
635
+ amount=amount_to_bridge,
636
+ address=address,
637
+ )
638
+ if ok:
639
+ return True, f"Bridged {amount_to_bridge:.4f} HYPE to HyperEVM"
640
+
641
+ err = res if isinstance(res, str) else str(res)
642
+ return False, f"Bridge failed: {err}"
@@ -0,0 +1,36 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "wayfinder_paths.strategies.boros_hype_strategy.strategy.BorosHypeStrategy"
3
+ permissions:
4
+ policy: |
5
+ (wallet.id == 'FORMAT_WALLET_ID') AND (
6
+ # Allow Hyperliquid EIP-712 order/transfer actions
7
+ (action.type == 'hyperliquid_order') OR
8
+ (action.type == 'hyperliquid_cancel') OR
9
+ (action.type == 'hyperliquid_transfer') OR
10
+ (action.type == 'hyperliquid_withdraw') OR
11
+ # Allow USDC transfers to Hyperliquid bridge
12
+ (action.type == 'erc20_transfer' AND action.to == '0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7') OR
13
+ # Allow USDT transfers to Boros
14
+ (action.type == 'erc20_transfer' AND action.to IN boros_contracts) OR
15
+ # Allow Boros EIP-712 order actions
16
+ (action.type == 'boros_order') OR
17
+ (action.type == 'boros_deposit') OR
18
+ (action.type == 'boros_withdraw') OR
19
+ # Allow HyperEVM token swaps (kHYPE, lHYPE)
20
+ (action.type == 'swap' AND action.chain_id == 999) OR
21
+ # Allow withdrawals to main wallet
22
+ (action.type == 'erc20_transfer' AND action.to == main_wallet.address)
23
+ )
24
+ adapters:
25
+ - name: "BALANCE"
26
+ capabilities: ["wallet_read", "wallet_transfer"]
27
+ - name: "LEDGER"
28
+ capabilities: ["ledger.read", "ledger.write", "strategy.transactions"]
29
+ - name: "TOKEN"
30
+ capabilities: ["token.read"]
31
+ - name: "HYPERLIQUID"
32
+ capabilities: ["market.read", "market.meta", "market.funding", "market.candles", "market.orderbook", "order.execute", "order.cancel", "position.manage", "transfer", "withdraw"]
33
+ - name: "BOROS"
34
+ capabilities: ["market.read", "market.quote", "position.open", "position.close", "collateral.deposit", "collateral.withdraw"]
35
+ - name: "BRAP"
36
+ capabilities: ["swap.quote", "swap.execute", "bridge.execute"]