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.
- wayfinder_paths/adapters/boros_adapter/adapter.py +313 -12
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +125 -14
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +17 -3
- wayfinder_paths/adapters/hyperliquid_adapter/exchange.py +5 -5
- wayfinder_paths/adapters/hyperliquid_adapter/local_signer.py +2 -33
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1 -1
- wayfinder_paths/adapters/hyperliquid_adapter/test_cancel_order.py +57 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_exchange_mid_prices.py +52 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_hyperliquid_sdk_live.py +64 -0
- wayfinder_paths/adapters/hyperliquid_adapter/util.py +9 -10
- wayfinder_paths/core/clients/PoolClient.py +1 -1
- wayfinder_paths/core/constants/hype_oft_abi.py +151 -0
- wayfinder_paths/core/strategies/Strategy.py +1 -2
- wayfinder_paths/mcp/tools/execute.py +48 -16
- wayfinder_paths/mcp/tools/hyperliquid.py +1 -13
- wayfinder_paths/mcp/tools/quotes.py +38 -124
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +24 -0
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +249 -29
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +125 -15
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +57 -201
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +1 -152
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +29 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/manifest.yaml +33 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +23 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +2 -0
- wayfinder_paths/tests/test_manifests.py +93 -0
- wayfinder_paths/tests/test_mcp_balances.py +73 -0
- wayfinder_paths/tests/test_mcp_discovery.py +34 -0
- wayfinder_paths/tests/test_mcp_execute.py +146 -0
- wayfinder_paths/tests/test_mcp_hyperliquid_execute.py +69 -0
- wayfinder_paths/tests/test_mcp_idempotency_store.py +14 -0
- wayfinder_paths/tests/test_mcp_quote_swap.py +60 -0
- wayfinder_paths/tests/test_mcp_run_script.py +47 -0
- wayfinder_paths/tests/test_mcp_tokens.py +49 -0
- wayfinder_paths/tests/test_mcp_utils.py +35 -0
- wayfinder_paths/tests/test_mcp_wallets.py +38 -0
- {wayfinder_paths-0.1.29.dist-info → wayfinder_paths-0.1.31.dist-info}/METADATA +2 -2
- {wayfinder_paths-0.1.29.dist-info → wayfinder_paths-0.1.31.dist-info}/RECORD +42 -25
- {wayfinder_paths-0.1.29.dist-info → wayfinder_paths-0.1.31.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/types.py +0 -19
- {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=
|
|
433
|
-
perp_amount=
|
|
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
|
-
#
|
|
441
|
-
#
|
|
442
|
-
#
|
|
443
|
-
#
|
|
444
|
-
#
|
|
445
|
-
#
|
|
446
|
-
#
|
|
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": "
|
|
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": "-
|
|
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
|
|
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": "
|
|
477
|
-
{"coin": "USDC", "total": "
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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 =
|
|
346
|
-
|
|
347
|
-
bridged_hype = float(
|
|
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:
|
|
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)
|
|
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
|
-
|
|
364
|
+
ok_sweep, sweep_res = await self.boros_adapter.sweep_isolated_to_cross(
|
|
438
365
|
token_id=int(token_id)
|
|
439
366
|
)
|
|
440
|
-
if
|
|
441
|
-
|
|
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
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
546
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
|
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
|
-
|
|
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
|