wayfinder-paths 0.1.24__py3-none-any.whl → 0.1.27__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/brap_adapter/adapter.py +7 -47
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +10 -31
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +128 -60
- wayfinder_paths/adapters/hyperliquid_adapter/exchange.py +399 -0
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +74 -0
- wayfinder_paths/adapters/hyperliquid_adapter/local_signer.py +82 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +1 -1
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +1 -1
- wayfinder_paths/adapters/hyperliquid_adapter/util.py +237 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +19 -55
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +14 -46
- wayfinder_paths/core/__init__.py +2 -0
- wayfinder_paths/core/clients/BalanceClient.py +72 -0
- wayfinder_paths/core/clients/TokenClient.py +1 -1
- wayfinder_paths/core/clients/__init__.py +2 -0
- wayfinder_paths/core/strategies/Strategy.py +3 -3
- wayfinder_paths/core/types.py +19 -0
- wayfinder_paths/core/utils/tokens.py +19 -1
- wayfinder_paths/core/utils/transaction.py +9 -7
- wayfinder_paths/mcp/tools/balances.py +122 -214
- wayfinder_paths/mcp/tools/execute.py +63 -41
- wayfinder_paths/mcp/tools/quotes.py +16 -5
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +6 -22
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +227 -33
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +17 -1
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +44 -1
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +87 -32
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +50 -28
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +71 -50
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +3 -1
- wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +0 -2
- wayfinder_paths/strategies/boros_hype_strategy/types.py +4 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +0 -2
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -2
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +0 -2
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +0 -2
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -2
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -2
- wayfinder_paths/tests/test_mcp_quote_swap.py +3 -3
- {wayfinder_paths-0.1.24.dist-info → wayfinder_paths-0.1.27.dist-info}/METADATA +2 -3
- {wayfinder_paths-0.1.24.dist-info → wayfinder_paths-0.1.27.dist-info}/RECORD +44 -39
- {wayfinder_paths-0.1.24.dist-info → wayfinder_paths-0.1.27.dist-info}/WHEEL +1 -1
- {wayfinder_paths-0.1.24.dist-info → wayfinder_paths-0.1.27.dist-info}/LICENSE +0 -0
|
@@ -19,14 +19,20 @@ from wayfinder_paths.core.utils.web3 import web3_from_chain_id
|
|
|
19
19
|
from .constants import (
|
|
20
20
|
BOROS_HYPE_MARKET_ID,
|
|
21
21
|
BOROS_HYPE_TOKEN_ID,
|
|
22
|
-
|
|
22
|
+
BOROS_MIN_DEPOSIT_HYPE,
|
|
23
|
+
HYPE_NATIVE,
|
|
23
24
|
HYPE_OFT_ABI,
|
|
24
25
|
HYPE_OFT_ADDRESS,
|
|
25
26
|
HYPEREVM_CHAIN_ID,
|
|
27
|
+
KHYPE_LST,
|
|
28
|
+
LOOPED_HYPE,
|
|
26
29
|
LZ_EID_ARBITRUM,
|
|
27
30
|
MIN_HYPE_GAS,
|
|
28
31
|
USDC_ARB,
|
|
29
32
|
USDT_ARB,
|
|
33
|
+
WHYPE,
|
|
34
|
+
WHYPE_ABI,
|
|
35
|
+
WHYPE_ADDRESS,
|
|
30
36
|
)
|
|
31
37
|
from .types import Inventory
|
|
32
38
|
|
|
@@ -67,8 +73,8 @@ class BorosHypeBorosOpsMixin:
|
|
|
67
73
|
if not collateral_address:
|
|
68
74
|
collateral_address = HYPE_OFT_ADDRESS
|
|
69
75
|
|
|
70
|
-
if amount_usd <
|
|
71
|
-
return True, f"Skipping
|
|
76
|
+
if amount_usd < 1.0:
|
|
77
|
+
return True, f"Skipping tiny Boros funding (${amount_usd:.2f})"
|
|
72
78
|
|
|
73
79
|
if self.simulation:
|
|
74
80
|
return True, f"[SIMULATION] Funded Boros with ~${amount_usd:.2f} HYPE"
|
|
@@ -96,28 +102,156 @@ class BorosHypeBorosOpsMixin:
|
|
|
96
102
|
if available_oft_hype > 0:
|
|
97
103
|
deposit_hype = min(available_oft_hype, target_hype)
|
|
98
104
|
deposit_usd = deposit_hype * hype_price
|
|
99
|
-
if deposit_usd
|
|
100
|
-
|
|
105
|
+
if deposit_usd >= 1.0:
|
|
106
|
+
deposit_wei = int(deposit_hype * 1e18)
|
|
107
|
+
ok_dep, dep_res = await self.boros_adapter.deposit_to_cross_margin(
|
|
108
|
+
collateral_address=collateral_address,
|
|
109
|
+
amount_wei=deposit_wei,
|
|
110
|
+
token_id=token_id,
|
|
111
|
+
market_id=market_id,
|
|
112
|
+
)
|
|
113
|
+
if not ok_dep:
|
|
114
|
+
return False, f"Boros deposit failed: {dep_res}"
|
|
101
115
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
amount_wei=deposit_wei,
|
|
106
|
-
token_id=token_id,
|
|
107
|
-
market_id=market_id,
|
|
108
|
-
)
|
|
109
|
-
if not ok_dep:
|
|
110
|
-
return False, f"Boros deposit failed: {dep_res}"
|
|
116
|
+
logger.info(
|
|
117
|
+
f"Deposited {deposit_hype:.6f} HYPE (≈${deposit_usd:.2f}) to Boros cross margin"
|
|
118
|
+
)
|
|
111
119
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
# If this fully satisfies the target, stop here.
|
|
121
|
+
if deposit_hype >= target_hype - 1e-9:
|
|
122
|
+
return True, (
|
|
123
|
+
f"Funded Boros with {deposit_hype:.6f} HYPE (≈${deposit_usd:.2f}) "
|
|
124
|
+
"from Arbitrum OFT balance"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Otherwise, bridge the remainder.
|
|
128
|
+
target_hype = max(0.0, target_hype - deposit_hype)
|
|
129
|
+
target_wei = int(target_hype * 1e18)
|
|
130
|
+
else:
|
|
131
|
+
logger.info(
|
|
132
|
+
f"Skipping tiny OFT HYPE deposit (${deposit_usd:.2f}); will top up via bridge"
|
|
133
|
+
)
|
|
118
134
|
|
|
119
135
|
# 2) Bridge native HYPE from HyperEVM to Arbitrum using the OFT contract.
|
|
120
136
|
hype_balance = float(inventory.hype_hyperevm_balance or 0.0)
|
|
137
|
+
whype_balance = float(inventory.whype_balance or 0.0)
|
|
138
|
+
fee_buffer_hype = 0.03 # conservative buffer for OFT native fee
|
|
139
|
+
desired_native_hype = float(MIN_HYPE_GAS) + float(target_hype) + fee_buffer_hype
|
|
140
|
+
if whype_balance > 0.0:
|
|
141
|
+
# On HyperEVM, HYPE can be split between native HYPE and WHYPE. The OFT
|
|
142
|
+
# bridge requires native HYPE (msg.value), so unwrap enough WHYPE to
|
|
143
|
+
# cover (amount + fee) while still leaving MIN_HYPE_GAS for future gas.
|
|
144
|
+
if hype_balance < desired_native_hype:
|
|
145
|
+
unwrap_hype = min(whype_balance, desired_native_hype - hype_balance)
|
|
146
|
+
unwrap_wei = int(unwrap_hype * 1e18)
|
|
147
|
+
if self.balance_adapter:
|
|
148
|
+
(
|
|
149
|
+
ok_whype,
|
|
150
|
+
whype_raw,
|
|
151
|
+
) = await self.balance_adapter.get_vault_wallet_balance(WHYPE)
|
|
152
|
+
if ok_whype and isinstance(whype_raw, int) and whype_raw > 0:
|
|
153
|
+
unwrap_wei = min(int(unwrap_wei), int(whype_raw))
|
|
154
|
+
if unwrap_wei > 0:
|
|
155
|
+
if not self._sign_callback:
|
|
156
|
+
return False, "No signing callback configured"
|
|
157
|
+
|
|
158
|
+
logger.info(
|
|
159
|
+
f"Unwrapping {unwrap_hype:.6f} WHYPE → native HYPE to fund OFT bridge"
|
|
160
|
+
)
|
|
161
|
+
try:
|
|
162
|
+
tx = await encode_call(
|
|
163
|
+
target=WHYPE_ADDRESS,
|
|
164
|
+
abi=WHYPE_ABI,
|
|
165
|
+
fn_name="withdraw",
|
|
166
|
+
args=[int(unwrap_wei)],
|
|
167
|
+
from_address=wallet_address,
|
|
168
|
+
chain_id=HYPEREVM_CHAIN_ID,
|
|
169
|
+
)
|
|
170
|
+
tx_hash = await send_transaction(
|
|
171
|
+
tx, self._sign_callback, wait_for_receipt=True
|
|
172
|
+
)
|
|
173
|
+
logger.info(f"WHYPE unwrap tx={tx_hash}")
|
|
174
|
+
await asyncio.sleep(2)
|
|
175
|
+
except Exception as exc: # noqa: BLE001
|
|
176
|
+
logger.warning(f"WHYPE unwrap failed: {exc}")
|
|
177
|
+
|
|
178
|
+
if self.balance_adapter:
|
|
179
|
+
(
|
|
180
|
+
ok_hype,
|
|
181
|
+
hype_raw,
|
|
182
|
+
) = await self.balance_adapter.get_vault_wallet_balance(
|
|
183
|
+
HYPE_NATIVE
|
|
184
|
+
)
|
|
185
|
+
if ok_hype and hype_raw:
|
|
186
|
+
hype_balance = float(hype_raw) / 1e18
|
|
187
|
+
|
|
188
|
+
# If we still don't have enough native HYPE and there are LSTs on HyperEVM,
|
|
189
|
+
# sell a small amount back to native HYPE so Boros can be funded.
|
|
190
|
+
if (
|
|
191
|
+
hype_balance < desired_native_hype
|
|
192
|
+
and (inventory.khype_value_usd > 0 or inventory.looped_hype_value_usd > 0)
|
|
193
|
+
and self.brap_adapter
|
|
194
|
+
):
|
|
195
|
+
needed_hype = max(0.0, desired_native_hype - hype_balance)
|
|
196
|
+
if needed_hype > 0.01:
|
|
197
|
+
if inventory.khype_value_usd > 0 and inventory.khype_to_hype_ratio > 0:
|
|
198
|
+
max_hype_from_khype = float(inventory.khype_value_usd) / hype_price
|
|
199
|
+
sell_hype = min(needed_hype, max_hype_from_khype)
|
|
200
|
+
sell_khype = sell_hype / float(inventory.khype_to_hype_ratio)
|
|
201
|
+
sell_khype_wei = int(sell_khype * 1e18)
|
|
202
|
+
if sell_khype_wei > 0:
|
|
203
|
+
logger.info(
|
|
204
|
+
f"Selling {sell_khype:.6f} kHYPE → native HYPE to fund Boros"
|
|
205
|
+
)
|
|
206
|
+
ok_swap, res_swap = await self.brap_adapter.swap_from_token_ids(
|
|
207
|
+
from_token_id=KHYPE_LST,
|
|
208
|
+
to_token_id=HYPE_NATIVE,
|
|
209
|
+
from_address=wallet_address,
|
|
210
|
+
amount=str(int(sell_khype_wei)),
|
|
211
|
+
slippage=0.01,
|
|
212
|
+
strategy_name="boros_hype_strategy",
|
|
213
|
+
)
|
|
214
|
+
if ok_swap:
|
|
215
|
+
needed_hype = max(0.0, needed_hype - sell_hype)
|
|
216
|
+
await asyncio.sleep(2)
|
|
217
|
+
else:
|
|
218
|
+
logger.warning(f"Sell kHYPE→HYPE failed: {res_swap}")
|
|
219
|
+
|
|
220
|
+
if (
|
|
221
|
+
needed_hype > 0.01
|
|
222
|
+
and inventory.looped_hype_value_usd > 0
|
|
223
|
+
and inventory.looped_hype_to_hype_ratio > 0
|
|
224
|
+
):
|
|
225
|
+
max_hype_from_lhype = (
|
|
226
|
+
float(inventory.looped_hype_value_usd) / hype_price
|
|
227
|
+
)
|
|
228
|
+
sell_hype = min(needed_hype, max_hype_from_lhype)
|
|
229
|
+
sell_lhype = sell_hype / float(inventory.looped_hype_to_hype_ratio)
|
|
230
|
+
sell_lhype_wei = int(sell_lhype * 1e18)
|
|
231
|
+
if sell_lhype_wei > 0:
|
|
232
|
+
logger.info(
|
|
233
|
+
f"Selling {sell_lhype:.6f} looped HYPE → native HYPE to fund Boros"
|
|
234
|
+
)
|
|
235
|
+
ok_swap, res_swap = await self.brap_adapter.swap_from_token_ids(
|
|
236
|
+
from_token_id=LOOPED_HYPE,
|
|
237
|
+
to_token_id=HYPE_NATIVE,
|
|
238
|
+
from_address=wallet_address,
|
|
239
|
+
amount=str(int(sell_lhype_wei)),
|
|
240
|
+
slippage=0.01,
|
|
241
|
+
strategy_name="boros_hype_strategy",
|
|
242
|
+
)
|
|
243
|
+
if ok_swap:
|
|
244
|
+
await asyncio.sleep(2)
|
|
245
|
+
else:
|
|
246
|
+
logger.warning(f"Sell looped HYPE→HYPE failed: {res_swap}")
|
|
247
|
+
|
|
248
|
+
if self.balance_adapter:
|
|
249
|
+
(
|
|
250
|
+
ok_hype,
|
|
251
|
+
hype_raw,
|
|
252
|
+
) = await self.balance_adapter.get_vault_wallet_balance(HYPE_NATIVE)
|
|
253
|
+
if ok_hype and hype_raw:
|
|
254
|
+
hype_balance = float(hype_raw) / 1e18
|
|
121
255
|
if hype_balance <= MIN_HYPE_GAS + 0.0005:
|
|
122
256
|
return (
|
|
123
257
|
False,
|
|
@@ -142,6 +276,16 @@ class BorosHypeBorosOpsMixin:
|
|
|
142
276
|
abi=HYPE_OFT_ABI,
|
|
143
277
|
)
|
|
144
278
|
|
|
279
|
+
conversion_rate = int(
|
|
280
|
+
await contract.functions.decimalConversionRate().call()
|
|
281
|
+
) # OFT sharedDecimals rounding
|
|
282
|
+
if conversion_rate > 0:
|
|
283
|
+
bridge_amount_wei = (
|
|
284
|
+
bridge_amount_wei // conversion_rate
|
|
285
|
+
) * conversion_rate
|
|
286
|
+
if bridge_amount_wei <= 0:
|
|
287
|
+
return True, "Boros funding: amount too small after OFT rounding"
|
|
288
|
+
|
|
145
289
|
# Quote fee, then clamp amount to fit balance (amount + fee) while
|
|
146
290
|
# still leaving MIN_HYPE_GAS behind for future gas.
|
|
147
291
|
send_params = (
|
|
@@ -158,6 +302,10 @@ class BorosHypeBorosOpsMixin:
|
|
|
158
302
|
lz_token_fee = int(fee[1])
|
|
159
303
|
|
|
160
304
|
max_send_amount_wei = max(0, max_value_wei - native_fee)
|
|
305
|
+
if conversion_rate > 0:
|
|
306
|
+
max_send_amount_wei = (
|
|
307
|
+
max_send_amount_wei // conversion_rate
|
|
308
|
+
) * conversion_rate
|
|
161
309
|
if bridge_amount_wei > max_send_amount_wei:
|
|
162
310
|
bridge_amount_wei = max_send_amount_wei
|
|
163
311
|
if bridge_amount_wei <= 0:
|
|
@@ -230,7 +378,7 @@ class BorosHypeBorosOpsMixin:
|
|
|
230
378
|
or self._planner_runtime.current_boros_token_id
|
|
231
379
|
or BOROS_HYPE_TOKEN_ID
|
|
232
380
|
)
|
|
233
|
-
|
|
381
|
+
target_size_yu = float(params.get("target_size_yu") or 0.0)
|
|
234
382
|
|
|
235
383
|
if inventory.boros_pending_withdrawal_usd > 0:
|
|
236
384
|
return True, (
|
|
@@ -244,14 +392,14 @@ class BorosHypeBorosOpsMixin:
|
|
|
244
392
|
if self.simulation:
|
|
245
393
|
return (
|
|
246
394
|
True,
|
|
247
|
-
f"[SIMULATION] Boros position at market {market_id} set to
|
|
395
|
+
f"[SIMULATION] Boros position at market {market_id} set to {target_size_yu:.4f} YU",
|
|
248
396
|
)
|
|
249
397
|
|
|
250
398
|
try:
|
|
251
399
|
return await self._ensure_boros_position_impl(
|
|
252
400
|
market_id=market_id,
|
|
253
401
|
token_id=token_id,
|
|
254
|
-
|
|
402
|
+
target_size_yu=target_size_yu,
|
|
255
403
|
inventory=inventory,
|
|
256
404
|
)
|
|
257
405
|
except Exception as exc:
|
|
@@ -268,9 +416,21 @@ class BorosHypeBorosOpsMixin:
|
|
|
268
416
|
*,
|
|
269
417
|
market_id: int,
|
|
270
418
|
token_id: int,
|
|
271
|
-
|
|
419
|
+
target_size_yu: float,
|
|
272
420
|
inventory: Inventory,
|
|
273
421
|
) -> tuple[bool, str]:
|
|
422
|
+
# Only try to manage the rate position once we have at least the minimum
|
|
423
|
+
# HYPE collateral funded on Boros (or sitting idle as OFT HYPE on Arbitrum).
|
|
424
|
+
depositable_hype = float(inventory.boros_collateral_hype or 0.0) + float(
|
|
425
|
+
inventory.hype_oft_arb_balance or 0.0
|
|
426
|
+
)
|
|
427
|
+
if depositable_hype < BOROS_MIN_DEPOSIT_HYPE:
|
|
428
|
+
return (
|
|
429
|
+
True,
|
|
430
|
+
"Skipping Boros position: collateral not funded yet "
|
|
431
|
+
f"({depositable_hype:.6f} HYPE)",
|
|
432
|
+
)
|
|
433
|
+
|
|
274
434
|
# 0) Move any isolated collateral to cross margin (cleanup).
|
|
275
435
|
# Boros markets expire, so we need to get the actual market ID from isolated positions.
|
|
276
436
|
try:
|
|
@@ -331,6 +491,38 @@ class BorosHypeBorosOpsMixin:
|
|
|
331
491
|
except Exception as exc: # noqa: BLE001
|
|
332
492
|
logger.warning(f"Failed to deposit OFT HYPE to Boros: {exc}")
|
|
333
493
|
|
|
494
|
+
# Deposits can land as isolated cash for the given market_id; ensure we
|
|
495
|
+
# sweep isolated -> cross again before attempting to trade.
|
|
496
|
+
try:
|
|
497
|
+
ok_bal, balances = await self.boros_adapter.get_account_balances(
|
|
498
|
+
token_id=int(token_id)
|
|
499
|
+
)
|
|
500
|
+
if ok_bal and isinstance(balances, dict):
|
|
501
|
+
isolated_positions = balances.get("isolated_positions", [])
|
|
502
|
+
for iso_pos in isolated_positions:
|
|
503
|
+
iso_market_id = iso_pos.get("market_id")
|
|
504
|
+
iso_balance = float(iso_pos.get("balance", 0) or 0.0)
|
|
505
|
+
if iso_market_id and iso_balance > 0.001:
|
|
506
|
+
iso_wei = int(iso_balance * 1e18) # Boros cash units
|
|
507
|
+
logger.info(
|
|
508
|
+
f"Moving {iso_balance:.6f} collateral from isolated market {iso_market_id} to cross"
|
|
509
|
+
)
|
|
510
|
+
ok_xfer, res_xfer = await self.boros_adapter.cash_transfer(
|
|
511
|
+
market_id=int(iso_market_id),
|
|
512
|
+
amount_wei=iso_wei,
|
|
513
|
+
is_deposit=False, # isolated -> cross
|
|
514
|
+
)
|
|
515
|
+
if ok_xfer:
|
|
516
|
+
await asyncio.sleep(2)
|
|
517
|
+
else:
|
|
518
|
+
logger.warning(
|
|
519
|
+
f"Failed Boros isolated->cross transfer for market {iso_market_id}: {res_xfer}"
|
|
520
|
+
)
|
|
521
|
+
except Exception as exc: # noqa: BLE001
|
|
522
|
+
logger.warning(
|
|
523
|
+
f"Failed Boros isolated->cross transfer after deposit: {exc}"
|
|
524
|
+
)
|
|
525
|
+
|
|
334
526
|
# 2) Rollover: close positions in other markets (best effort).
|
|
335
527
|
try:
|
|
336
528
|
for mid in inventory.boros_position_market_ids or []:
|
|
@@ -356,18 +548,20 @@ class BorosHypeBorosOpsMixin:
|
|
|
356
548
|
if not success:
|
|
357
549
|
return False, "Failed to get Boros positions"
|
|
358
550
|
|
|
359
|
-
current_size_usd = 0.0
|
|
360
551
|
if positions:
|
|
361
552
|
pos = positions[0]
|
|
362
|
-
|
|
553
|
+
current_size_yu = abs(float(pos.get("size", 0) or 0.0))
|
|
554
|
+
else:
|
|
555
|
+
current_size_yu = 0.0
|
|
363
556
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
557
|
+
diff_yu = float(target_size_yu) - float(current_size_yu)
|
|
558
|
+
diff_usd_equiv = abs(diff_yu) * float(inventory.hype_price_usd or 0.0)
|
|
559
|
+
if diff_usd_equiv < self._planner_config.boros_resize_min_excess_usd:
|
|
560
|
+
return True, f"Boros position already at target ({current_size_yu:.4f} YU)"
|
|
367
561
|
|
|
368
|
-
size_yu_wei = int(abs(
|
|
562
|
+
size_yu_wei = int(abs(diff_yu) * 1e18) # Boros YU wei
|
|
369
563
|
|
|
370
|
-
if
|
|
564
|
+
if diff_yu > 0:
|
|
371
565
|
# Open/increase SHORT side (receive fixed)
|
|
372
566
|
ok_open, open_res = await self.boros_adapter.place_rate_order(
|
|
373
567
|
market_id=int(market_id),
|
|
@@ -380,7 +574,7 @@ class BorosHypeBorosOpsMixin:
|
|
|
380
574
|
return False, f"Failed to open Boros position: {open_res}"
|
|
381
575
|
return (
|
|
382
576
|
True,
|
|
383
|
-
f"Boros position increased by
|
|
577
|
+
f"Boros position increased by {diff_yu:.4f} YU on market {market_id}",
|
|
384
578
|
)
|
|
385
579
|
|
|
386
580
|
# Close/decrease position
|
|
@@ -394,7 +588,7 @@ class BorosHypeBorosOpsMixin:
|
|
|
394
588
|
|
|
395
589
|
return (
|
|
396
590
|
True,
|
|
397
|
-
f"Boros position decreased by
|
|
591
|
+
f"Boros position decreased by {abs(diff_yu):.4f} YU on market {market_id}",
|
|
398
592
|
)
|
|
399
593
|
|
|
400
594
|
async def _complete_pending_withdrawal(
|
|
@@ -94,7 +94,9 @@ HORIZON_DAYS = 7 # Planning horizon in days
|
|
|
94
94
|
# entry path uses HYPE collateral (via the LayerZero OFT token on Arbitrum).
|
|
95
95
|
BOROS_HYPE_MARKET_ID = 51 # HYPERLIQUID-HYPE-27FEB2026 (fallback)
|
|
96
96
|
BOROS_HYPE_TOKEN_ID = 5 # HYPE collateral token ID on Boros (current)
|
|
97
|
-
|
|
97
|
+
# Boros requires a minimum cross-margin cash balance per token to place orders.
|
|
98
|
+
# For the HYPE collateral token (tokenId=5) this is currently 0.4 HYPE (MarketHub.getCashFeeData()).
|
|
99
|
+
BOROS_MIN_DEPOSIT_HYPE = 0.4
|
|
98
100
|
BOROS_MIN_TENOR_DAYS = 3 # Roll to new market if < 3 days to expiry
|
|
99
101
|
BOROS_ENABLE_MIN_TOTAL_USD = 80.0 # Skip Boros if capital below this
|
|
100
102
|
|
|
@@ -237,6 +239,20 @@ HYPE_OFT_ABI = [
|
|
|
237
239
|
"stateMutability": "payable",
|
|
238
240
|
"type": "function",
|
|
239
241
|
},
|
|
242
|
+
{
|
|
243
|
+
"inputs": [],
|
|
244
|
+
"name": "sharedDecimals",
|
|
245
|
+
"outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}],
|
|
246
|
+
"stateMutability": "view",
|
|
247
|
+
"type": "function",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"inputs": [],
|
|
251
|
+
"name": "decimalConversionRate",
|
|
252
|
+
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
|
|
253
|
+
"stateMutability": "view",
|
|
254
|
+
"type": "function",
|
|
255
|
+
},
|
|
240
256
|
]
|
|
241
257
|
|
|
242
258
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -6,9 +6,22 @@ Kept as a mixin so the main strategy file stays readable without changing behavi
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import asyncio
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
11
|
-
from
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from wayfinder_paths.core.utils.transaction import encode_call, send_transaction
|
|
15
|
+
|
|
16
|
+
from .constants import (
|
|
17
|
+
HYPE_NATIVE,
|
|
18
|
+
HYPEREVM_CHAIN_ID,
|
|
19
|
+
KHYPE_LST,
|
|
20
|
+
LOOPED_HYPE,
|
|
21
|
+
MIN_HYPE_GAS,
|
|
22
|
+
WHYPE_ABI,
|
|
23
|
+
WHYPE_ADDRESS,
|
|
24
|
+
)
|
|
12
25
|
from .types import Inventory
|
|
13
26
|
|
|
14
27
|
|
|
@@ -21,6 +34,36 @@ class BorosHypeHyperEvmOpsMixin:
|
|
|
21
34
|
if need <= 0.0:
|
|
22
35
|
return True, "HyperEVM gas already sufficient"
|
|
23
36
|
|
|
37
|
+
# If we have WHYPE on HyperEVM, unwrap a small amount to native HYPE for gas.
|
|
38
|
+
if float(inventory.whype_balance or 0.0) > 0.0:
|
|
39
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
40
|
+
address = strategy_wallet.get("address")
|
|
41
|
+
if address and self._sign_callback:
|
|
42
|
+
unwrap_hype = min(
|
|
43
|
+
float(inventory.whype_balance or 0.0), max(0.01, need + 0.002)
|
|
44
|
+
)
|
|
45
|
+
unwrap_wei = int(unwrap_hype * 1e18)
|
|
46
|
+
if unwrap_wei > 0:
|
|
47
|
+
logger.info(
|
|
48
|
+
f"Unwrapping {unwrap_hype:.6f} WHYPE → native HYPE for gas"
|
|
49
|
+
)
|
|
50
|
+
tx = await encode_call(
|
|
51
|
+
target=WHYPE_ADDRESS,
|
|
52
|
+
abi=WHYPE_ABI,
|
|
53
|
+
fn_name="withdraw",
|
|
54
|
+
args=[int(unwrap_wei)],
|
|
55
|
+
from_address=address,
|
|
56
|
+
chain_id=HYPEREVM_CHAIN_ID,
|
|
57
|
+
)
|
|
58
|
+
tx_hash = await send_transaction(
|
|
59
|
+
tx, self._sign_callback, wait_for_receipt=True
|
|
60
|
+
)
|
|
61
|
+
await asyncio.sleep(2)
|
|
62
|
+
return (
|
|
63
|
+
True,
|
|
64
|
+
f"Unwrapped {unwrap_hype:.6f} WHYPE for gas (tx={tx_hash})",
|
|
65
|
+
)
|
|
66
|
+
|
|
24
67
|
# Best-effort: if HYPE exists on HL spot, bridge it over.
|
|
25
68
|
if inventory.hl_spot_hype > max(0.1, need + 0.001):
|
|
26
69
|
return await self._transfer_hl_spot_to_hyperevm(
|
|
@@ -13,7 +13,13 @@ from datetime import datetime
|
|
|
13
13
|
|
|
14
14
|
from wayfinder_paths.adapters.boros_adapter import BorosMarketQuote
|
|
15
15
|
|
|
16
|
-
from .constants import
|
|
16
|
+
from .constants import (
|
|
17
|
+
BOROS_HYPE_TOKEN_ID,
|
|
18
|
+
BOROS_MIN_DEPOSIT_HYPE,
|
|
19
|
+
BOROS_MIN_TENOR_DAYS,
|
|
20
|
+
MAX_HL_LEVERAGE,
|
|
21
|
+
MIN_HYPE_GAS,
|
|
22
|
+
)
|
|
17
23
|
from .types import (
|
|
18
24
|
AllocationStatus,
|
|
19
25
|
DesiredState,
|
|
@@ -147,7 +153,7 @@ def build_plan(
|
|
|
147
153
|
target_hl_margin_usd=0,
|
|
148
154
|
target_boros_collateral_usd=0,
|
|
149
155
|
target_hype_short_size=0,
|
|
150
|
-
|
|
156
|
+
target_boros_position_yu=0,
|
|
151
157
|
),
|
|
152
158
|
)
|
|
153
159
|
plan.messages.append(
|
|
@@ -165,7 +171,7 @@ def build_plan(
|
|
|
165
171
|
target_hl_margin_usd=0,
|
|
166
172
|
target_boros_collateral_usd=0,
|
|
167
173
|
target_hype_short_size=0,
|
|
168
|
-
|
|
174
|
+
target_boros_position_yu=0,
|
|
169
175
|
),
|
|
170
176
|
)
|
|
171
177
|
plan.messages.append(
|
|
@@ -203,9 +209,12 @@ def build_plan(
|
|
|
203
209
|
target_hl = total * hedge_cfg.hyperliquid_pct
|
|
204
210
|
|
|
205
211
|
# Boros target: use MAX of percentage allocation and minimum deposit + buffer.
|
|
206
|
-
# We keep a small buffer so
|
|
212
|
+
# We keep a small buffer so rounding doesn't undershoot the Boros minimum.
|
|
207
213
|
target_boros = (
|
|
208
|
-
max(
|
|
214
|
+
max(
|
|
215
|
+
hedge_cfg.boros_pct * total,
|
|
216
|
+
(BOROS_MIN_DEPOSIT_HYPE + 0.01) * float(inv.hype_price_usd or 0.0),
|
|
217
|
+
)
|
|
209
218
|
if boros_enabled
|
|
210
219
|
else 0.0
|
|
211
220
|
)
|
|
@@ -223,12 +232,19 @@ def build_plan(
|
|
|
223
232
|
# Short target = current HYPE exposure (to hedge what we actually have)
|
|
224
233
|
target_hype_short = inv.total_hype_exposure
|
|
225
234
|
|
|
226
|
-
# Boros
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
235
|
+
# Boros position sizing is in YU, but YU units depend on the collateral token:
|
|
236
|
+
# - HYPE collateral: 1 YU = 1 HYPE
|
|
237
|
+
# - USDT collateral: 1 YU = 1 USDT (≈$1)
|
|
238
|
+
# For HYPE collateral we size to HYPE exposure; for USDT we size to USD notional.
|
|
239
|
+
if boros_enabled and market_id:
|
|
240
|
+
if boros_token_id == BOROS_HYPE_TOKEN_ID:
|
|
241
|
+
target_boros_position_yu = target_hype_short * config.boros_coverage_target
|
|
242
|
+
else:
|
|
243
|
+
target_boros_position_yu = (
|
|
244
|
+
target_hype_short * inv.hype_price_usd * config.boros_coverage_target
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
target_boros_position_yu = 0.0
|
|
232
248
|
|
|
233
249
|
desired = DesiredState(
|
|
234
250
|
mode=mode,
|
|
@@ -236,7 +252,7 @@ def build_plan(
|
|
|
236
252
|
target_hl_margin_usd=target_hl,
|
|
237
253
|
target_boros_collateral_usd=target_boros,
|
|
238
254
|
target_hype_short_size=target_hype_short,
|
|
239
|
-
|
|
255
|
+
target_boros_position_yu=target_boros_position_yu,
|
|
240
256
|
boros_market_id=market_id,
|
|
241
257
|
boros_market_symbol=market_symbol,
|
|
242
258
|
boros_tenor_days=tenor_days,
|
|
@@ -302,19 +318,18 @@ def build_plan(
|
|
|
302
318
|
0.0, target_boros - float(inv.boros_committed_collateral_usd or 0.0)
|
|
303
319
|
)
|
|
304
320
|
if boros_shortfall >= config.min_usdt_action:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
)
|
|
321
|
+
plan.add_step(
|
|
322
|
+
PlanOp.FUND_BOROS,
|
|
323
|
+
priority=10,
|
|
324
|
+
key=f"fund_boros_{boros_shortfall:.2f}",
|
|
325
|
+
params={
|
|
326
|
+
"amount_usd": float(boros_shortfall),
|
|
327
|
+
"market_id": int(market_id) if market_id else None,
|
|
328
|
+
"token_id": int(boros_token_id) if boros_token_id else None,
|
|
329
|
+
"collateral_address": str(boros_collateral_address or ""),
|
|
330
|
+
},
|
|
331
|
+
reason=f"Funding Boros toward target (${target_boros:.2f}) via HYPE collateral",
|
|
332
|
+
)
|
|
318
333
|
|
|
319
334
|
# Send USDC to Hyperliquid ONCE to cover both:
|
|
320
335
|
# - HL margin allocation
|
|
@@ -403,6 +418,32 @@ def build_plan(
|
|
|
403
418
|
target_hype_short * config.delta_neutral_rel_tol,
|
|
404
419
|
)
|
|
405
420
|
|
|
421
|
+
# If we can't hedge to within tolerance due to margin constraints, trim spot first.
|
|
422
|
+
# (This avoids safety-triggered liquidation loops when HL free margin is too low.)
|
|
423
|
+
hedge_increase_needed = float(target_hype_short) - float(current_short)
|
|
424
|
+
if hedge_increase_needed > 0 and target_hype_short > 0.1:
|
|
425
|
+
hype_price = float(inv.hype_price_usd or 0.0)
|
|
426
|
+
free_margin = float(inv.hl_withdrawable_usd or 0.0)
|
|
427
|
+
max_increase_by_margin = (
|
|
428
|
+
(free_margin * MAX_HL_LEVERAGE) / hype_price if hype_price > 0 else 0.0
|
|
429
|
+
)
|
|
430
|
+
if hedge_increase_needed > max_increase_by_margin + tolerance:
|
|
431
|
+
trim_hype = hedge_increase_needed - (max_increase_by_margin + tolerance)
|
|
432
|
+
trim_usd = trim_hype * hype_price
|
|
433
|
+
if inv.spot_value_usd > 0:
|
|
434
|
+
trim_pct = min(0.50, trim_usd / float(inv.spot_value_usd))
|
|
435
|
+
if trim_pct > 0.02:
|
|
436
|
+
plan.add_step(
|
|
437
|
+
PlanOp.PARTIAL_TRIM_SPOT,
|
|
438
|
+
priority=0,
|
|
439
|
+
key="partial_trim_spot_for_margin",
|
|
440
|
+
params={"trim_pct": float(trim_pct)},
|
|
441
|
+
reason=(
|
|
442
|
+
"Insufficient HL free margin to hedge within tolerance; "
|
|
443
|
+
f"trimming spot by ~${trim_usd:.2f}"
|
|
444
|
+
),
|
|
445
|
+
)
|
|
446
|
+
|
|
406
447
|
if short_delta > tolerance and target_hype_short > 0.1:
|
|
407
448
|
plan.add_step(
|
|
408
449
|
PlanOp.ENSURE_HL_SHORT,
|
|
@@ -414,14 +455,28 @@ def build_plan(
|
|
|
414
455
|
|
|
415
456
|
# Priority 30: Rate positions (Boros)
|
|
416
457
|
if boros_enabled and market_id:
|
|
417
|
-
|
|
418
|
-
|
|
458
|
+
# Only try to manage rate positions once collateral is actually available.
|
|
459
|
+
# (In-flight OFT bridges are tracked separately; they can't be used yet.)
|
|
460
|
+
depositable_collateral_hype = float(inv.boros_collateral_hype or 0.0) + float(
|
|
461
|
+
inv.hype_oft_arb_balance or 0.0
|
|
462
|
+
)
|
|
463
|
+
if depositable_collateral_hype < BOROS_MIN_DEPOSIT_HYPE:
|
|
464
|
+
plan.messages.append(
|
|
465
|
+
"Skipping Boros position: collateral not funded yet "
|
|
466
|
+
f"({depositable_collateral_hype:.6f} HYPE)"
|
|
467
|
+
)
|
|
468
|
+
plan.sort_steps()
|
|
469
|
+
return plan
|
|
470
|
+
|
|
471
|
+
current_boros_size_yu = float(inv.boros_position_size or 0.0)
|
|
472
|
+
size_diff_yu = abs(target_boros_position_yu - current_boros_size_yu)
|
|
473
|
+
size_diff_usd = size_diff_yu * float(inv.hype_price_usd or 0.0)
|
|
419
474
|
|
|
420
475
|
# Trigger if:
|
|
421
476
|
# 1. Position size needs adjustment (resize threshold met)
|
|
422
477
|
# 2. There's isolated collateral that needs to move to cross (market expiry/rotation)
|
|
423
478
|
# 3. There's cross collateral but no position (collateral sitting idle)
|
|
424
|
-
needs_position_resize =
|
|
479
|
+
needs_position_resize = size_diff_usd > config.boros_resize_min_excess_usd
|
|
425
480
|
isolated_usd = float(inv.boros_idle_collateral_isolated or 0.0) * float(
|
|
426
481
|
inv.hype_price_usd or 0.0
|
|
427
482
|
)
|
|
@@ -429,10 +484,10 @@ def build_plan(
|
|
|
429
484
|
inv.hype_price_usd or 0.0
|
|
430
485
|
)
|
|
431
486
|
has_stranded_isolated = isolated_usd > 0.5 # $0.50 threshold
|
|
432
|
-
has_idle_cross = cross_usd > 1.0 and
|
|
487
|
+
has_idle_cross = cross_usd > 1.0 and current_boros_size_yu < 0.01
|
|
433
488
|
|
|
434
489
|
if needs_position_resize or has_stranded_isolated or has_idle_cross:
|
|
435
|
-
reason = f"Adjusting Boros position to {
|
|
490
|
+
reason = f"Adjusting Boros position to {target_boros_position_yu:.4f} YU"
|
|
436
491
|
if has_stranded_isolated and not needs_position_resize:
|
|
437
492
|
reason = (
|
|
438
493
|
"Moving "
|
|
@@ -448,10 +503,10 @@ def build_plan(
|
|
|
448
503
|
plan.add_step(
|
|
449
504
|
PlanOp.ENSURE_BOROS_POSITION,
|
|
450
505
|
priority=20, # Same as ENSURE_HL_SHORT so both execute before re-observe
|
|
451
|
-
key=f"ensure_boros_pos_{
|
|
506
|
+
key=f"ensure_boros_pos_{target_boros_position_yu:.4f}",
|
|
452
507
|
params={
|
|
453
508
|
"market_id": market_id,
|
|
454
|
-
"
|
|
509
|
+
"target_size_yu": target_boros_position_yu,
|
|
455
510
|
},
|
|
456
511
|
reason=reason,
|
|
457
512
|
)
|