wayfinder-paths 0.1.29__py3-none-any.whl → 0.1.31__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 (43) hide show
  1. wayfinder_paths/adapters/boros_adapter/adapter.py +313 -12
  2. wayfinder_paths/adapters/boros_adapter/test_adapter.py +125 -14
  3. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +17 -3
  4. wayfinder_paths/adapters/hyperliquid_adapter/exchange.py +5 -5
  5. wayfinder_paths/adapters/hyperliquid_adapter/local_signer.py +2 -33
  6. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1 -1
  7. wayfinder_paths/adapters/hyperliquid_adapter/test_cancel_order.py +57 -0
  8. wayfinder_paths/adapters/hyperliquid_adapter/test_exchange_mid_prices.py +52 -0
  9. wayfinder_paths/adapters/hyperliquid_adapter/test_hyperliquid_sdk_live.py +64 -0
  10. wayfinder_paths/adapters/hyperliquid_adapter/util.py +9 -10
  11. wayfinder_paths/core/clients/PoolClient.py +1 -1
  12. wayfinder_paths/core/constants/hype_oft_abi.py +151 -0
  13. wayfinder_paths/core/strategies/Strategy.py +1 -2
  14. wayfinder_paths/mcp/tools/execute.py +48 -16
  15. wayfinder_paths/mcp/tools/hyperliquid.py +1 -13
  16. wayfinder_paths/mcp/tools/quotes.py +38 -124
  17. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +24 -0
  18. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +249 -29
  19. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +125 -15
  20. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +57 -201
  21. wayfinder_paths/strategies/boros_hype_strategy/constants.py +1 -152
  22. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +29 -0
  23. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2 -0
  24. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/manifest.yaml +33 -0
  25. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2 -0
  26. wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +23 -0
  27. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +2 -0
  28. wayfinder_paths/tests/test_manifests.py +93 -0
  29. wayfinder_paths/tests/test_mcp_balances.py +73 -0
  30. wayfinder_paths/tests/test_mcp_discovery.py +34 -0
  31. wayfinder_paths/tests/test_mcp_execute.py +146 -0
  32. wayfinder_paths/tests/test_mcp_hyperliquid_execute.py +69 -0
  33. wayfinder_paths/tests/test_mcp_idempotency_store.py +14 -0
  34. wayfinder_paths/tests/test_mcp_quote_swap.py +60 -0
  35. wayfinder_paths/tests/test_mcp_run_script.py +47 -0
  36. wayfinder_paths/tests/test_mcp_tokens.py +49 -0
  37. wayfinder_paths/tests/test_mcp_utils.py +35 -0
  38. wayfinder_paths/tests/test_mcp_wallets.py +38 -0
  39. {wayfinder_paths-0.1.29.dist-info → wayfinder_paths-0.1.31.dist-info}/METADATA +2 -2
  40. {wayfinder_paths-0.1.29.dist-info → wayfinder_paths-0.1.31.dist-info}/RECORD +42 -25
  41. {wayfinder_paths-0.1.29.dist-info → wayfinder_paths-0.1.31.dist-info}/WHEEL +1 -1
  42. wayfinder_paths/core/types.py +0 -19
  43. {wayfinder_paths-0.1.29.dist-info → wayfinder_paths-0.1.31.dist-info}/LICENSE +0 -0
@@ -429,35 +429,34 @@ class TestBasisTradingStrategy:
429
429
  coin="ETH",
430
430
  spot_asset_id=10000,
431
431
  perp_asset_id=1,
432
- spot_amount=1.0,
433
- perp_amount=1.0,
432
+ spot_amount=0.03,
433
+ perp_amount=0.03,
434
434
  entry_price=2000.0,
435
435
  leverage=2,
436
436
  entry_timestamp=1700000000000,
437
437
  funding_collected=0.0,
438
438
  )
439
439
 
440
- # Mock user state with perp position and idle capital
441
- # totalNtlPos represents position notional value, set high to avoid rebalance trigger
442
- # unused_usd = accountValue - totalNtlPos = 120 - 112 = 8
443
- # threshold for rebalance = epsilon * 2 = max(5, 0.02 * 100) * 2 = 10
444
- # 8 < 10 so no rebalance
445
- # total_idle = withdrawable (12) + spot_usdc (8) = 20 > min_deploy (5) so will scale up
446
- # order_usd = 20 * (2/3) = 13.33 > MIN_NOTIONAL_USD (10)
440
+ # Make sure there's deployable idle USDC without relying on marginSummary.withdrawable.
441
+ # With 2x leverage and ~0.03 ETH:
442
+ # - spot value 0.03 * 2000 = 60
443
+ # - perp contrib (2000*(1+1/2) - 2000) * 0.03 = 30
444
+ # - bankroll 30 + 60 + 20 = 110
445
+ # - allocated 30 + 60 = 90
446
+ # - unused 20 (deployable USDC) -> scale up
447
447
  mock_hyperliquid_adapter.get_user_state = AsyncMock(
448
448
  return_value=(
449
449
  True,
450
450
  {
451
451
  "marginSummary": {
452
- "accountValue": "120",
452
+ "accountValue": "30",
453
453
  "withdrawable": "12",
454
- "totalNtlPos": "112",
455
454
  },
456
455
  "assetPositions": [
457
456
  {
458
457
  "position": {
459
458
  "coin": "ETH",
460
- "szi": "-1.0",
459
+ "szi": "-0.03",
461
460
  "leverage": {"value": "2"},
462
461
  "liquidationPx": "2500",
463
462
  "entryPx": "2000",
@@ -467,14 +466,14 @@ class TestBasisTradingStrategy:
467
466
  },
468
467
  )
469
468
  )
470
- # Include ETH spot balance for leg balance check, plus USDC for idle capital
469
+ # Include ETH spot balance for leg balance check, plus USDC to deploy.
471
470
  mock_hyperliquid_adapter.get_spot_user_state = AsyncMock(
472
471
  return_value=(
473
472
  True,
474
473
  {
475
474
  "balances": [
476
- {"coin": "ETH", "total": "1.0"},
477
- {"coin": "USDC", "total": "8"},
475
+ {"coin": "ETH", "total": "0.03"},
476
+ {"coin": "USDC", "total": "20"},
478
477
  ]
479
478
  },
480
479
  )
@@ -516,6 +515,117 @@ class TestBasisTradingStrategy:
516
515
  assert mock_filler.fill_pair_units.called
517
516
  assert success
518
517
 
518
+ @pytest.mark.asyncio
519
+ async def test_update_does_not_scale_on_perp_pnl_margin_release(
520
+ self, strategy, mock_hyperliquid_adapter
521
+ ):
522
+ """A favorable perp move can increase withdrawable margin; it should not trigger scale-up."""
523
+ strategy.deposit_amount = 100.0
524
+ strategy.current_position = BasisPosition(
525
+ coin="ETH",
526
+ spot_asset_id=10000,
527
+ perp_asset_id=1,
528
+ spot_amount=0.03,
529
+ perp_amount=0.03,
530
+ entry_price=2000.0,
531
+ leverage=2,
532
+ entry_timestamp=1700000000000,
533
+ funding_collected=0.0,
534
+ )
535
+
536
+ # Price down benefits the short perp; withdrawable may rise, but unused cash is ~0.
537
+ mock_hyperliquid_adapter.get_all_mid_prices = AsyncMock(
538
+ return_value=(True, {"ETH": 1800.0, "BTC": 50000.0})
539
+ )
540
+ mock_hyperliquid_adapter.get_user_state = AsyncMock(
541
+ return_value=(
542
+ True,
543
+ {
544
+ "marginSummary": {
545
+ "accountValue": "36",
546
+ "withdrawable": "12",
547
+ },
548
+ "assetPositions": [
549
+ {
550
+ "position": {
551
+ "coin": "ETH",
552
+ "szi": "-0.03",
553
+ "leverage": {"value": "2"},
554
+ "liquidationPx": "2500",
555
+ "entryPx": "2000",
556
+ }
557
+ }
558
+ ],
559
+ },
560
+ )
561
+ )
562
+ mock_hyperliquid_adapter.get_spot_user_state = AsyncMock(
563
+ return_value=(True, {"balances": [{"coin": "ETH", "total": "0.03"}]})
564
+ )
565
+
566
+ strategy._scale_up_position = AsyncMock(return_value=(True, "scaled"))
567
+
568
+ success, _ = await strategy.update()
569
+ assert success
570
+ strategy._scale_up_position.assert_not_awaited()
571
+
572
+ @pytest.mark.asyncio
573
+ async def test_update_includes_rotation_cooldown_hint(
574
+ self, strategy, mock_hyperliquid_adapter
575
+ ):
576
+ strategy.deposit_amount = 100.0
577
+ strategy.current_position = BasisPosition(
578
+ coin="ETH",
579
+ spot_asset_id=10000,
580
+ perp_asset_id=1,
581
+ spot_amount=0.03,
582
+ perp_amount=0.03,
583
+ entry_price=2000.0,
584
+ leverage=2,
585
+ entry_timestamp=1700000000000,
586
+ funding_collected=0.0,
587
+ )
588
+
589
+ mock_hyperliquid_adapter.get_user_state = AsyncMock(
590
+ return_value=(
591
+ True,
592
+ {
593
+ "marginSummary": {
594
+ "accountValue": "0",
595
+ "withdrawable": "0",
596
+ },
597
+ "assetPositions": [
598
+ {
599
+ "position": {
600
+ "coin": "ETH",
601
+ "szi": "-0.03",
602
+ "leverage": {"value": "2"},
603
+ "liquidationPx": "2500",
604
+ "entryPx": "2000",
605
+ }
606
+ }
607
+ ],
608
+ },
609
+ )
610
+ )
611
+ mock_hyperliquid_adapter.get_spot_user_state = AsyncMock(
612
+ return_value=(True, {"balances": [{"coin": "ETH", "total": "0.03"}]})
613
+ )
614
+
615
+ strategy._needs_new_position = AsyncMock(
616
+ return_value=(False, "Position healthy")
617
+ )
618
+ strategy._verify_leg_balance = AsyncMock(return_value=(True, "ok"))
619
+ strategy._unused_usd_now = AsyncMock(return_value=(0.0, 100.0))
620
+ strategy._ensure_stop_loss_valid = AsyncMock(
621
+ return_value=(True, "Stop-loss ok")
622
+ )
623
+ strategy._rotation_cooldown_hint = AsyncMock(return_value="3d 4h remaining")
624
+
625
+ success, msg = await strategy.update()
626
+ assert success
627
+ assert "Rotation: 3d 4h remaining" in msg
628
+
519
629
  @pytest.mark.asyncio
520
630
  async def test_ensure_builder_fee_approved_already_approved(
521
631
  self, mock_hyperliquid_adapter, ledger_adapter
@@ -11,22 +11,18 @@ from datetime import datetime
11
11
  from typing import Any
12
12
 
13
13
  from loguru import logger
14
- from web3 import AsyncWeb3
15
14
 
16
15
  from wayfinder_paths.core.utils.transaction import encode_call, send_transaction
17
- from wayfinder_paths.core.utils.web3 import web3_from_chain_id
18
16
 
19
17
  from .constants import (
20
18
  BOROS_HYPE_MARKET_ID,
21
19
  BOROS_HYPE_TOKEN_ID,
22
20
  BOROS_MIN_DEPOSIT_HYPE,
23
21
  HYPE_NATIVE,
24
- HYPE_OFT_ABI,
25
22
  HYPE_OFT_ADDRESS,
26
23
  HYPEREVM_CHAIN_ID,
27
24
  KHYPE_LST,
28
25
  LOOPED_HYPE,
29
- LZ_EID_ARBITRUM,
30
26
  MIN_HYPE_GAS,
31
27
  USDC_ARB,
32
28
  USDT_ARB,
@@ -37,11 +33,6 @@ from .constants import (
37
33
  from .types import Inventory
38
34
 
39
35
 
40
- def _pad_address_bytes32(address: str) -> bytes:
41
- checksum = AsyncWeb3.to_checksum_address(address)
42
- return bytes.fromhex(checksum[2:]).rjust(32, b"\x00")
43
-
44
-
45
36
  class BorosHypeBorosOpsMixin:
46
37
  async def _fund_boros(
47
38
  self, params: dict[str, Any], inventory: Inventory
@@ -267,84 +258,21 @@ class BorosHypeBorosOpsMixin:
267
258
  bridge_amount_wei = min(target_wei, max_value_wei)
268
259
  if bridge_amount_wei <= 0:
269
260
  return True, "Boros funding: nothing to bridge"
270
-
271
- to_bytes32 = _pad_address_bytes32(wallet_address)
272
-
273
- async with web3_from_chain_id(HYPEREVM_CHAIN_ID) as w3:
274
- contract = w3.eth.contract(
275
- address=w3.to_checksum_address(HYPE_OFT_ADDRESS),
276
- abi=HYPE_OFT_ABI,
277
- )
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
-
289
- # Quote fee, then clamp amount to fit balance (amount + fee) while
290
- # still leaving MIN_HYPE_GAS behind for future gas.
291
- send_params = (
292
- int(LZ_EID_ARBITRUM),
293
- to_bytes32,
294
- int(bridge_amount_wei),
295
- 0,
296
- b"",
297
- b"",
298
- b"",
299
- )
300
- fee = await contract.functions.quoteSend(send_params, False).call()
301
- native_fee = int(fee[0])
302
- lz_token_fee = int(fee[1])
303
-
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
309
- if bridge_amount_wei > max_send_amount_wei:
310
- bridge_amount_wei = max_send_amount_wei
311
- if bridge_amount_wei <= 0:
312
- return False, "Insufficient HyperEVM HYPE to cover OFT bridge fee"
313
-
314
- send_params = (
315
- int(LZ_EID_ARBITRUM),
316
- to_bytes32,
317
- int(bridge_amount_wei),
318
- 0,
319
- b"",
320
- b"",
321
- b"",
322
- )
323
- fee = await contract.functions.quoteSend(send_params, False).call()
324
- native_fee = int(fee[0])
325
- lz_token_fee = int(fee[1])
326
-
327
- total_value_wei = int(bridge_amount_wei) + int(native_fee)
328
- if total_value_wei > max_value_wei:
329
- return False, "Insufficient HyperEVM HYPE to bridge after fee quote"
330
-
331
- tx = await encode_call(
332
- target=HYPE_OFT_ADDRESS,
333
- abi=HYPE_OFT_ABI,
334
- fn_name="send",
335
- args=[
336
- send_params,
337
- (int(native_fee), int(lz_token_fee)),
338
- AsyncWeb3.to_checksum_address(wallet_address),
339
- ],
340
- from_address=wallet_address,
341
- chain_id=HYPEREVM_CHAIN_ID,
342
- value=total_value_wei,
261
+ (
262
+ ok_bridge,
263
+ bridge_res,
264
+ ) = await self.boros_adapter.bridge_hype_oft_hyperevm_to_arbitrum(
265
+ amount_wei=int(bridge_amount_wei),
266
+ max_value_wei=int(max_value_wei),
267
+ to_address=str(wallet_address),
268
+ from_address=str(wallet_address),
343
269
  )
270
+ if not ok_bridge:
271
+ return False, f"OFT bridge failed: {bridge_res}"
344
272
 
345
- tx_hash = await send_transaction(tx, self._sign_callback, wait_for_receipt=True)
346
-
347
- bridged_hype = float(bridge_amount_wei) / 1e18
273
+ tx_hash = str(bridge_res.get("tx_hash") or "")
274
+ bridged_wei = int(bridge_res.get("amount_wei") or 0)
275
+ bridged_hype = float(bridged_wei) / 1e18
348
276
  bridged_usd = bridged_hype * hype_price
349
277
 
350
278
  # Track in-flight amount so planner doesn't double-fund while the bridge settles.
@@ -360,7 +288,7 @@ class BorosHypeBorosOpsMixin:
360
288
 
361
289
  return True, (
362
290
  f"Bridging {bridged_hype:.6f} HYPE (≈${bridged_usd:.2f}) HyperEVM→Arbitrum via OFT; "
363
- f"tx={tx_hash} (LayerZero: https://layerzeroscan.com/tx/{tx_hash}). "
291
+ f"tx={tx_hash} (LayerZero: {bridge_res.get('layerzeroscan')}). "
364
292
  "Once bridged HYPE lands on Arbitrum, the next tick will deposit it to Boros."
365
293
  )
366
294
 
@@ -431,41 +359,15 @@ class BorosHypeBorosOpsMixin:
431
359
  f"({depositable_hype:.6f} HYPE)",
432
360
  )
433
361
 
434
- # 0) Move any isolated collateral to cross margin (cleanup).
435
- # Boros markets expire, so we need to get the actual market ID from isolated positions.
362
+ # 0) Cleanup: sweep any isolated collateral back to cross margin.
436
363
  try:
437
- ok_bal, balances = await self.boros_adapter.get_account_balances(
364
+ ok_sweep, sweep_res = await self.boros_adapter.sweep_isolated_to_cross(
438
365
  token_id=int(token_id)
439
366
  )
440
- if ok_bal and isinstance(balances, dict):
441
- isolated_positions = balances.get("isolated_positions", [])
442
- logger.debug(
443
- f"Boros position check: isolated={balances.get('isolated', 0):.6f}, "
444
- f"cross={balances.get('cross', 0):.6f}, "
445
- f"total={balances.get('total', 0):.6f}, "
446
- f"isolated_positions={isolated_positions}"
447
- )
448
- for iso_pos in isolated_positions:
449
- iso_market_id = iso_pos.get("market_id")
450
- iso_balance = float(iso_pos.get("balance", 0) or 0.0)
451
- if iso_market_id and iso_balance > 0.001:
452
- iso_wei = int(iso_balance * 1e18) # Boros cash units
453
- logger.info(
454
- f"Moving {iso_balance:.6f} collateral from isolated market {iso_market_id} to cross"
455
- )
456
- ok_xfer, res_xfer = await self.boros_adapter.cash_transfer(
457
- market_id=int(iso_market_id),
458
- amount_wei=iso_wei,
459
- is_deposit=False, # isolated -> cross
460
- )
461
- if ok_xfer:
462
- await asyncio.sleep(2)
463
- else:
464
- logger.warning(
465
- f"Failed Boros isolated->cross transfer for market {iso_market_id}: {res_xfer}"
466
- )
367
+ if not ok_sweep:
368
+ logger.warning(f"Failed Boros isolated->cross sweep: {sweep_res}")
467
369
  except Exception as exc: # noqa: BLE001
468
- logger.warning(f"Failed Boros isolated->cross transfer: {exc}")
370
+ logger.warning(f"Failed Boros isolated->cross sweep: {exc}")
469
371
 
470
372
  # 1) Best-effort: if any OFT HYPE is sitting idle on Arbitrum, deposit it to cross margin.
471
373
  if inventory.hype_oft_arb_balance > 0.0:
@@ -491,104 +393,58 @@ class BorosHypeBorosOpsMixin:
491
393
  except Exception as exc: # noqa: BLE001
492
394
  logger.warning(f"Failed to deposit OFT HYPE to Boros: {exc}")
493
395
 
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
-
526
396
  # 2) Rollover: close positions in other markets (best effort).
527
397
  try:
528
- for mid in inventory.boros_position_market_ids or []:
529
- try:
530
- mid_int = int(mid)
531
- except (TypeError, ValueError):
532
- continue
533
- if mid_int <= 0 or mid_int == int(market_id):
534
- continue
535
- try:
536
- await self.boros_adapter.close_positions_market(
537
- mid_int, token_id=int(token_id)
538
- )
539
- await asyncio.sleep(2)
540
- except Exception as exc: # noqa: BLE001
541
- logger.warning(f"Failed to close Boros market {mid_int}: {exc}")
398
+ ok_roll, roll_res = await self.boros_adapter.close_positions_except(
399
+ keep_market_id=int(market_id),
400
+ token_id=int(token_id),
401
+ market_ids=inventory.boros_position_market_ids or [],
402
+ best_effort=True,
403
+ )
404
+ if not ok_roll:
405
+ logger.warning(f"Failed Boros rollover close: {roll_res}")
542
406
  except Exception as exc: # noqa: BLE001
543
407
  logger.warning(f"Failed Boros rollover close: {exc}")
544
408
 
545
- success, positions = await self.boros_adapter.get_active_positions(
546
- market_id=int(market_id)
409
+ yu_to_usd = (
410
+ float(inventory.hype_price_usd or 0.0)
411
+ if int(token_id) == BOROS_HYPE_TOKEN_ID
412
+ else 1.0
547
413
  )
548
- if not success:
549
- return False, "Failed to get Boros positions"
550
-
551
- if positions:
552
- pos = positions[0]
553
- current_size_yu = abs(float(pos.get("size", 0) or 0.0))
554
- else:
555
- current_size_yu = 0.0
556
-
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)"
561
-
562
- size_yu_wei = int(abs(diff_yu) * 1e18) # Boros YU wei
563
-
564
- if diff_yu > 0:
565
- # Open/increase SHORT side (receive fixed)
566
- ok_open, open_res = await self.boros_adapter.place_rate_order(
567
- market_id=int(market_id),
568
- token_id=int(token_id),
569
- size_yu_wei=size_yu_wei,
570
- side="short",
571
- tif="IOC",
414
+ ok_set, set_res = await self.boros_adapter.ensure_position_size_yu(
415
+ market_id=int(market_id),
416
+ token_id=int(token_id),
417
+ target_size_yu=float(target_size_yu),
418
+ tif="IOC",
419
+ min_resize_excess_usd=float(
420
+ self._planner_config.boros_resize_min_excess_usd
421
+ ),
422
+ yu_to_usd=float(yu_to_usd),
423
+ )
424
+ if not ok_set:
425
+ return False, f"Failed to ensure Boros position: {set_res}"
426
+
427
+ action = str(set_res.get("action") or "unknown")
428
+ diff_yu = float(set_res.get("diff_yu") or 0.0)
429
+ if action == "no_op":
430
+ return (
431
+ True,
432
+ f"Boros position already at target ({set_res.get('current_size_yu', 0.0):.4f} YU)",
572
433
  )
573
- if not ok_open:
574
- return False, f"Failed to open Boros position: {open_res}"
434
+ if action == "increase_short":
575
435
  return (
576
436
  True,
577
437
  f"Boros position increased by {diff_yu:.4f} YU on market {market_id}",
578
438
  )
579
-
580
- # Close/decrease position
581
- ok_close, close_res = await self.boros_adapter.close_positions_market(
582
- market_id=int(market_id),
583
- token_id=int(token_id),
584
- size_yu_wei=size_yu_wei,
585
- )
586
- if not ok_close:
587
- return False, f"Failed to close Boros position: {close_res}"
439
+ if action == "decrease":
440
+ return (
441
+ True,
442
+ f"Boros position decreased by {abs(diff_yu):.4f} YU on market {market_id}",
443
+ )
588
444
 
589
445
  return (
590
446
  True,
591
- f"Boros position decreased by {abs(diff_yu):.4f} YU on market {market_id}",
447
+ f"Boros position adjusted ({action}) on market {market_id}: Δ={diff_yu:.4f} YU",
592
448
  )
593
449
 
594
450
  async def _complete_pending_withdrawal(
@@ -109,158 +109,7 @@ BOROS_ENABLE_MIN_TOTAL_USD = 80.0 # Skip Boros if capital below this
109
109
 
110
110
  # LayerZero OFT bridge (HyperEVM native HYPE -> Arbitrum OFT HYPE)
111
111
  # HYPE_OFT_ADDRESS imported from contracts.py
112
- LZ_EID_ARBITRUM = 30110
113
-
114
- # Minimal IOFT ABI for quoting + sending.
115
- HYPE_OFT_ABI = [
116
- {
117
- "inputs": [
118
- {
119
- "components": [
120
- {"internalType": "uint32", "name": "dstEid", "type": "uint32"},
121
- {"internalType": "bytes32", "name": "to", "type": "bytes32"},
122
- {"internalType": "uint256", "name": "amountLD", "type": "uint256"},
123
- {
124
- "internalType": "uint256",
125
- "name": "minAmountLD",
126
- "type": "uint256",
127
- },
128
- {
129
- "internalType": "bytes",
130
- "name": "extraOptions",
131
- "type": "bytes",
132
- },
133
- {"internalType": "bytes", "name": "composeMsg", "type": "bytes"},
134
- {"internalType": "bytes", "name": "oftCmd", "type": "bytes"},
135
- ],
136
- "internalType": "struct SendParam",
137
- "name": "_sendParam",
138
- "type": "tuple",
139
- },
140
- {"internalType": "bool", "name": "_payInLzToken", "type": "bool"},
141
- ],
142
- "name": "quoteSend",
143
- "outputs": [
144
- {
145
- "components": [
146
- {"internalType": "uint256", "name": "nativeFee", "type": "uint256"},
147
- {
148
- "internalType": "uint256",
149
- "name": "lzTokenFee",
150
- "type": "uint256",
151
- },
152
- ],
153
- "internalType": "struct MessagingFee",
154
- "name": "",
155
- "type": "tuple",
156
- }
157
- ],
158
- "stateMutability": "view",
159
- "type": "function",
160
- },
161
- {
162
- "inputs": [
163
- {
164
- "components": [
165
- {"internalType": "uint32", "name": "dstEid", "type": "uint32"},
166
- {"internalType": "bytes32", "name": "to", "type": "bytes32"},
167
- {"internalType": "uint256", "name": "amountLD", "type": "uint256"},
168
- {
169
- "internalType": "uint256",
170
- "name": "minAmountLD",
171
- "type": "uint256",
172
- },
173
- {
174
- "internalType": "bytes",
175
- "name": "extraOptions",
176
- "type": "bytes",
177
- },
178
- {"internalType": "bytes", "name": "composeMsg", "type": "bytes"},
179
- {"internalType": "bytes", "name": "oftCmd", "type": "bytes"},
180
- ],
181
- "internalType": "struct SendParam",
182
- "name": "_sendParam",
183
- "type": "tuple",
184
- },
185
- {
186
- "components": [
187
- {"internalType": "uint256", "name": "nativeFee", "type": "uint256"},
188
- {
189
- "internalType": "uint256",
190
- "name": "lzTokenFee",
191
- "type": "uint256",
192
- },
193
- ],
194
- "internalType": "struct MessagingFee",
195
- "name": "_fee",
196
- "type": "tuple",
197
- },
198
- {"internalType": "address", "name": "_refundAddress", "type": "address"},
199
- ],
200
- "name": "send",
201
- "outputs": [
202
- {
203
- "components": [
204
- {"internalType": "bytes32", "name": "guid", "type": "bytes32"},
205
- {"internalType": "uint64", "name": "nonce", "type": "uint64"},
206
- {
207
- "components": [
208
- {
209
- "internalType": "uint256",
210
- "name": "nativeFee",
211
- "type": "uint256",
212
- },
213
- {
214
- "internalType": "uint256",
215
- "name": "lzTokenFee",
216
- "type": "uint256",
217
- },
218
- ],
219
- "internalType": "struct MessagingFee",
220
- "name": "fee",
221
- "type": "tuple",
222
- },
223
- ],
224
- "internalType": "struct MessagingReceipt",
225
- "name": "",
226
- "type": "tuple",
227
- },
228
- {
229
- "components": [
230
- {
231
- "internalType": "uint256",
232
- "name": "amountSentLD",
233
- "type": "uint256",
234
- },
235
- {
236
- "internalType": "uint256",
237
- "name": "amountReceivedLD",
238
- "type": "uint256",
239
- },
240
- ],
241
- "internalType": "struct OFTReceipt",
242
- "name": "",
243
- "type": "tuple",
244
- },
245
- ],
246
- "stateMutability": "payable",
247
- "type": "function",
248
- },
249
- {
250
- "inputs": [],
251
- "name": "sharedDecimals",
252
- "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}],
253
- "stateMutability": "view",
254
- "type": "function",
255
- },
256
- {
257
- "inputs": [],
258
- "name": "decimalConversionRate",
259
- "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
260
- "stateMutability": "view",
261
- "type": "function",
262
- },
263
- ]
112
+ # ABI lives in `wayfinder_paths/core/constants/hype_oft_abi.py`.
264
113
 
265
114
  # ─────────────────────────────────────────────────────────────────────────────
266
115
  # GAS CONFIGURATION