wayfinder-paths 0.1.14__py3-none-any.whl → 0.1.16__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/balance_adapter/README.md +19 -20
- wayfinder_paths/adapters/balance_adapter/adapter.py +91 -22
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +5 -11
- wayfinder_paths/adapters/brap_adapter/README.md +22 -19
- wayfinder_paths/adapters/brap_adapter/adapter.py +95 -45
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +8 -24
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +40 -42
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +8 -15
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/moonwell_adapter/README.md +29 -31
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +326 -364
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +285 -189
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
- wayfinder_paths/adapters/token_adapter/test_adapter.py +4 -4
- wayfinder_paths/core/config.py +8 -47
- wayfinder_paths/core/constants/base.py +0 -1
- wayfinder_paths/core/constants/erc20_abi.py +13 -24
- wayfinder_paths/core/engine/StrategyJob.py +3 -1
- wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
- wayfinder_paths/core/strategies/Strategy.py +22 -4
- wayfinder_paths/core/utils/erc20_service.py +100 -0
- wayfinder_paths/core/utils/evm_helpers.py +1 -8
- wayfinder_paths/core/utils/transaction.py +191 -0
- wayfinder_paths/core/utils/web3.py +66 -0
- wayfinder_paths/policies/erc20.py +1 -1
- wayfinder_paths/run_strategy.py +42 -6
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +263 -220
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +132 -155
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +123 -80
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -12
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -6
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2270 -1328
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +282 -121
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +107 -85
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -8
- wayfinder_paths/templates/adapter/README.md +1 -1
- wayfinder_paths/templates/strategy/README.md +1 -5
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/METADATA +3 -41
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/RECORD +45 -54
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/WHEEL +1 -1
- wayfinder_paths/abis/generic/erc20.json +0 -383
- wayfinder_paths/core/clients/sdk_example.py +0 -125
- wayfinder_paths/core/engine/__init__.py +0 -5
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +0 -130
- wayfinder_paths/core/services/local_evm_txn.py +0 -334
- wayfinder_paths/core/services/local_token_txn.py +0 -242
- wayfinder_paths/core/services/web3_service.py +0 -43
- wayfinder_paths/core/wallets/README.md +0 -88
- wayfinder_paths/core/wallets/WalletManager.py +0 -56
- wayfinder_paths/core/wallets/__init__.py +0 -7
- wayfinder_paths/scripts/run_strategy.py +0 -152
- wayfinder_paths/strategies/config.py +0 -85
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/LICENSE +0 -0
|
@@ -11,6 +11,7 @@ import asyncio
|
|
|
11
11
|
import math
|
|
12
12
|
import random
|
|
13
13
|
import time
|
|
14
|
+
from collections.abc import Awaitable, Callable
|
|
14
15
|
from datetime import UTC, datetime, timedelta
|
|
15
16
|
from decimal import ROUND_DOWN, ROUND_UP, Decimal, getcontext
|
|
16
17
|
from pathlib import Path
|
|
@@ -60,9 +61,6 @@ from wayfinder_paths.core.analytics import (
|
|
|
60
61
|
from wayfinder_paths.core.analytics import (
|
|
61
62
|
z_from_conf as analytics_z_from_conf,
|
|
62
63
|
)
|
|
63
|
-
from wayfinder_paths.core.services.base import Web3Service
|
|
64
|
-
from wayfinder_paths.core.services.local_token_txn import LocalTokenTxnService
|
|
65
|
-
from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
|
|
66
64
|
from wayfinder_paths.core.strategies.descriptors import (
|
|
67
65
|
Complexity,
|
|
68
66
|
Directionality,
|
|
@@ -72,7 +70,6 @@ from wayfinder_paths.core.strategies.descriptors import (
|
|
|
72
70
|
Volatility,
|
|
73
71
|
)
|
|
74
72
|
from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
|
|
75
|
-
from wayfinder_paths.core.wallets.WalletManager import WalletManager
|
|
76
73
|
from wayfinder_paths.strategies.basis_trading_strategy.constants import (
|
|
77
74
|
USDC_ARBITRUM_TOKEN_ID,
|
|
78
75
|
)
|
|
@@ -116,8 +113,10 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
116
113
|
DEFAULT_BOOTSTRAP_BLOCK_HOURS = 48
|
|
117
114
|
|
|
118
115
|
# Liquidation and rebalance thresholds
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
# Trigger rebalance at 75% to liquidation
|
|
117
|
+
LIQUIDATION_REBALANCE_THRESHOLD = 0.75
|
|
118
|
+
# Stop-loss at 90% to liquidation (closer)
|
|
119
|
+
LIQUIDATION_STOP_LOSS_THRESHOLD = 0.90
|
|
121
120
|
FUNDING_REBALANCE_THRESHOLD = 0.02 # Rebalance when funding hits 2% gains
|
|
122
121
|
|
|
123
122
|
# Position tolerances
|
|
@@ -201,10 +200,17 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
201
200
|
*,
|
|
202
201
|
main_wallet: dict[str, Any] | None = None,
|
|
203
202
|
strategy_wallet: dict[str, Any] | None = None,
|
|
204
|
-
web3_service: Web3Service | None = None,
|
|
205
203
|
hyperliquid_executor: HyperliquidExecutor | None = None,
|
|
204
|
+
api_key: str | None = None,
|
|
205
|
+
main_wallet_signing_callback: Callable[[dict], Awaitable[str]] | None = None,
|
|
206
|
+
strategy_wallet_signing_callback: Callable[[dict], Awaitable[str]]
|
|
207
|
+
| None = None,
|
|
206
208
|
) -> None:
|
|
207
|
-
super().__init__(
|
|
209
|
+
super().__init__(
|
|
210
|
+
api_key=api_key,
|
|
211
|
+
main_wallet_signing_callback=main_wallet_signing_callback,
|
|
212
|
+
strategy_wallet_signing_callback=strategy_wallet_signing_callback,
|
|
213
|
+
)
|
|
208
214
|
|
|
209
215
|
merged_config = dict(config or {})
|
|
210
216
|
if main_wallet:
|
|
@@ -262,19 +268,10 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
262
268
|
|
|
263
269
|
# Other adapters require a configured wallet provider / web3 service.
|
|
264
270
|
try:
|
|
265
|
-
if web3_service is None:
|
|
266
|
-
wallet_provider = WalletManager.get_provider(adapter_config)
|
|
267
|
-
tx_adapter = LocalTokenTxnService(
|
|
268
|
-
adapter_config,
|
|
269
|
-
wallet_provider=wallet_provider,
|
|
270
|
-
)
|
|
271
|
-
web3_service = DefaultWeb3Service(
|
|
272
|
-
wallet_provider=wallet_provider, evm_transactions=tx_adapter
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
self.web3_service = web3_service
|
|
276
271
|
self.balance_adapter = BalanceAdapter(
|
|
277
|
-
adapter_config,
|
|
272
|
+
adapter_config,
|
|
273
|
+
main_wallet_signing_callback=self.main_wallet_signing_callback,
|
|
274
|
+
strategy_wallet_signing_callback=self.strategy_wallet_signing_callback,
|
|
278
275
|
)
|
|
279
276
|
self.token_adapter = TokenAdapter()
|
|
280
277
|
self.ledger_adapter = LedgerAdapter()
|
|
@@ -478,20 +475,18 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
478
475
|
gas_ok,
|
|
479
476
|
gas_res,
|
|
480
477
|
) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
|
|
481
|
-
|
|
478
|
+
token_id="ethereum-arbitrum", # Native ETH on Arbitrum
|
|
482
479
|
amount=gas_token_amount,
|
|
483
480
|
strategy_name=self.name or "basis_trading_strategy",
|
|
484
|
-
skip_ledger=True,
|
|
485
481
|
)
|
|
486
482
|
if not gas_ok:
|
|
487
483
|
self.logger.error(f"Failed to transfer ETH for gas: {gas_res}")
|
|
488
484
|
return (False, f"Failed to transfer ETH for gas: {gas_res}")
|
|
489
485
|
self.logger.info(f"Gas transfer successful: {gas_res}")
|
|
490
486
|
|
|
491
|
-
# Real deposit: ensure funds are in the strategy wallet
|
|
487
|
+
# Real deposit: ensure funds are in the strategy wallet.
|
|
492
488
|
try:
|
|
493
489
|
main_address = self._get_main_wallet_address()
|
|
494
|
-
strategy_wallet = self.config.get("strategy_wallet")
|
|
495
490
|
strategy_address = self._get_strategy_wallet_address()
|
|
496
491
|
|
|
497
492
|
# Check if strategy wallet already has sufficient USDC
|
|
@@ -517,10 +512,9 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
517
512
|
move_ok,
|
|
518
513
|
move_res,
|
|
519
514
|
) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
|
|
520
|
-
|
|
515
|
+
token_id=USDC_ARBITRUM_TOKEN_ID,
|
|
521
516
|
amount=need_to_move,
|
|
522
517
|
strategy_name=self.name or "basis_trading_strategy",
|
|
523
|
-
skip_ledger=True,
|
|
524
518
|
)
|
|
525
519
|
if not move_ok:
|
|
526
520
|
self.logger.error(
|
|
@@ -535,73 +529,14 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
535
529
|
f"Strategy wallet already has {strategy_usdc:.2f} USDC, skipping transfer from main"
|
|
536
530
|
)
|
|
537
531
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
f"to Hyperliquid bridge ({HYPERLIQUID_BRIDGE_ADDRESS})"
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
# Send USDC to bridge address (deposit credits the sender address on Hyperliquid)
|
|
544
|
-
success, result = await self.balance_adapter.send_to_address(
|
|
545
|
-
query=USDC_ARBITRUM_TOKEN_ID,
|
|
546
|
-
amount=main_token_amount,
|
|
547
|
-
from_wallet=strategy_wallet,
|
|
548
|
-
to_address=HYPERLIQUID_BRIDGE_ADDRESS,
|
|
549
|
-
skip_ledger=True, # We'll record after HL credits the deposit
|
|
550
|
-
)
|
|
551
|
-
|
|
552
|
-
if not success:
|
|
553
|
-
self.logger.error(f"Failed to send USDC to bridge: {result}")
|
|
554
|
-
return (False, f"Failed to send USDC to bridge: {result}")
|
|
555
|
-
|
|
556
|
-
self.logger.info(f"USDC sent to bridge, tx: {result}")
|
|
557
|
-
|
|
558
|
-
# Wait for Hyperliquid to credit the deposit
|
|
559
|
-
self.logger.info("Waiting for Hyperliquid to credit the deposit...")
|
|
560
|
-
|
|
561
|
-
(
|
|
562
|
-
deposit_confirmed,
|
|
563
|
-
final_balance,
|
|
564
|
-
) = await self.hyperliquid_adapter.wait_for_deposit(
|
|
565
|
-
address=strategy_address,
|
|
566
|
-
expected_increase=main_token_amount,
|
|
567
|
-
timeout_s=180, # 3 minutes for initial deposit
|
|
568
|
-
poll_interval_s=10,
|
|
569
|
-
)
|
|
570
|
-
|
|
571
|
-
if not deposit_confirmed:
|
|
572
|
-
self.logger.warning(
|
|
573
|
-
f"Deposit not confirmed within timeout. "
|
|
574
|
-
f"Current HL balance: ${final_balance:.2f}. "
|
|
575
|
-
f"Deposit may still be processing."
|
|
576
|
-
)
|
|
577
|
-
# Still track the deposit amount since we sent it
|
|
578
|
-
self.deposit_amount = main_token_amount
|
|
579
|
-
return (
|
|
580
|
-
True,
|
|
581
|
-
f"Sent {main_token_amount} USDC to bridge. Deposit still processing. "
|
|
582
|
-
f"Current HL balance: ${final_balance:.2f}",
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
self.deposit_amount = main_token_amount
|
|
586
|
-
|
|
587
|
-
# Record in ledger
|
|
588
|
-
try:
|
|
589
|
-
await self.ledger_adapter.record_deposit(
|
|
590
|
-
wallet_address=strategy_address,
|
|
591
|
-
chain_id=42161, # Arbitrum
|
|
592
|
-
token_address="hyperliquid-vault-usd", # Synthetic address for HL USD
|
|
593
|
-
token_amount=str(main_token_amount),
|
|
594
|
-
usd_value=main_token_amount,
|
|
595
|
-
data={"destination": "hyperliquid_l1"},
|
|
596
|
-
strategy_name=self.name,
|
|
597
|
-
)
|
|
598
|
-
except Exception as e:
|
|
599
|
-
self.logger.warning(f"Failed to record deposit in ledger: {e}")
|
|
532
|
+
# Accumulate deposit amount for bridging in update()
|
|
533
|
+
self.deposit_amount += main_token_amount
|
|
600
534
|
|
|
601
535
|
return (
|
|
602
536
|
True,
|
|
603
|
-
f"
|
|
604
|
-
f"
|
|
537
|
+
f"Transferred {main_token_amount} USDC to strategy wallet ({strategy_address}). "
|
|
538
|
+
f"Total deposits: ${self.deposit_amount:.2f}. "
|
|
539
|
+
f"Call update() to bridge to Hyperliquid and open positions.",
|
|
605
540
|
)
|
|
606
541
|
|
|
607
542
|
except Exception as e:
|
|
@@ -622,18 +557,92 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
622
557
|
Returns:
|
|
623
558
|
StatusTuple (success, message)
|
|
624
559
|
"""
|
|
625
|
-
#
|
|
626
|
-
|
|
560
|
+
# Check actual balances instead of relying on in-memory deposit_amount
|
|
561
|
+
strategy_address = self._get_strategy_wallet_address()
|
|
562
|
+
strategy_wallet = self.config.get("strategy_wallet")
|
|
563
|
+
|
|
564
|
+
# Check strategy wallet USDC balance on Arbitrum
|
|
565
|
+
strategy_usdc = 0.0
|
|
566
|
+
try:
|
|
567
|
+
(
|
|
568
|
+
strategy_balance_ok,
|
|
569
|
+
strategy_balance,
|
|
570
|
+
) = await self.balance_adapter.get_balance(
|
|
571
|
+
query=USDC_ARBITRUM_TOKEN_ID,
|
|
572
|
+
wallet_address=strategy_address,
|
|
573
|
+
)
|
|
574
|
+
if strategy_balance_ok and strategy_balance:
|
|
575
|
+
strategy_usdc = float(strategy_balance) / 1e6
|
|
576
|
+
except Exception as e:
|
|
577
|
+
self.logger.warning(f"Could not check strategy wallet balance: {e}")
|
|
578
|
+
|
|
579
|
+
# Check Hyperliquid USDC balance (spot + perp)
|
|
580
|
+
hl_usdc = 0.0
|
|
581
|
+
try:
|
|
627
582
|
perp_margin, spot_usdc = await self._get_undeployed_capital()
|
|
628
|
-
|
|
629
|
-
|
|
583
|
+
hl_usdc = perp_margin + spot_usdc
|
|
584
|
+
except Exception as e:
|
|
585
|
+
self.logger.warning(f"Could not check Hyperliquid balance: {e}")
|
|
586
|
+
|
|
587
|
+
# Update deposit_amount from actual balances
|
|
588
|
+
total_available = strategy_usdc + hl_usdc
|
|
589
|
+
if total_available > 1.0:
|
|
590
|
+
self.deposit_amount = max(self.deposit_amount, total_available)
|
|
591
|
+
|
|
592
|
+
if total_available < 1.0 and self.current_position is None:
|
|
593
|
+
return (False, "No funds to manage. Call deposit() first.")
|
|
594
|
+
|
|
595
|
+
# Bridge USDC from strategy wallet to Hyperliquid if needed
|
|
596
|
+
if strategy_usdc > 10.0:
|
|
597
|
+
try:
|
|
630
598
|
self.logger.info(
|
|
631
|
-
f"
|
|
599
|
+
f"Found ${strategy_usdc:.2f} USDC in strategy wallet, bridging to Hyperliquid"
|
|
632
600
|
)
|
|
633
|
-
self.deposit_amount = total_usdc
|
|
634
601
|
|
|
635
|
-
|
|
636
|
-
|
|
602
|
+
# Send USDC to bridge address (internal operation, not a deposit event)
|
|
603
|
+
success, result = await self.balance_adapter.send_to_address(
|
|
604
|
+
token_id=USDC_ARBITRUM_TOKEN_ID,
|
|
605
|
+
amount=strategy_usdc,
|
|
606
|
+
from_wallet=strategy_wallet,
|
|
607
|
+
to_address=HYPERLIQUID_BRIDGE_ADDRESS,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
if not success:
|
|
611
|
+
self.logger.error(f"Failed to send USDC to bridge: {result}")
|
|
612
|
+
return (False, f"Failed to bridge USDC to Hyperliquid: {result}")
|
|
613
|
+
|
|
614
|
+
self.logger.info(f"USDC sent to bridge, tx: {result}")
|
|
615
|
+
|
|
616
|
+
# Wait for Hyperliquid to credit the deposit
|
|
617
|
+
self.logger.info("Waiting for Hyperliquid to credit the deposit...")
|
|
618
|
+
|
|
619
|
+
(
|
|
620
|
+
deposit_confirmed,
|
|
621
|
+
final_balance,
|
|
622
|
+
) = await self.hyperliquid_adapter.wait_for_deposit(
|
|
623
|
+
address=strategy_address,
|
|
624
|
+
expected_increase=strategy_usdc,
|
|
625
|
+
timeout_s=180, # 3 minutes
|
|
626
|
+
poll_interval_s=10,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
if not deposit_confirmed:
|
|
630
|
+
self.logger.warning(
|
|
631
|
+
f"Deposit not confirmed within timeout. "
|
|
632
|
+
f"Current HL balance: ${final_balance:.2f}. "
|
|
633
|
+
f"Deposit may still be processing."
|
|
634
|
+
)
|
|
635
|
+
return (
|
|
636
|
+
True,
|
|
637
|
+
f"Sent ${strategy_usdc:.2f} USDC to bridge. Deposit still processing. "
|
|
638
|
+
f"Current HL balance: ${final_balance:.2f}. Call update() again.",
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
self.logger.info(
|
|
642
|
+
f"Successfully bridged ${strategy_usdc:.2f} USDC to Hyperliquid"
|
|
643
|
+
)
|
|
644
|
+
except Exception as e:
|
|
645
|
+
self.logger.warning(f"Failed to bridge USDC to Hyperliquid: {e}")
|
|
637
646
|
|
|
638
647
|
# If no position, find and open one
|
|
639
648
|
if self.current_position is None:
|
|
@@ -797,14 +806,55 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
797
806
|
return nested.get(key, default)
|
|
798
807
|
return default
|
|
799
808
|
|
|
809
|
+
def _resolve_mid_price(self, coin: str, mid_prices: dict[str, float]) -> float:
|
|
810
|
+
"""Resolve mid price with U-prefix handling for universal spot tokens.
|
|
811
|
+
|
|
812
|
+
Spot balances may return U-prefixed coin names (e.g., UXPL) while mid prices
|
|
813
|
+
are keyed by non-prefixed names (e.g., XPL). This helper handles the mismatch.
|
|
814
|
+
"""
|
|
815
|
+
# Direct match
|
|
816
|
+
if coin in mid_prices:
|
|
817
|
+
return mid_prices[coin]
|
|
818
|
+
|
|
819
|
+
# Case variations
|
|
820
|
+
for key in [coin.upper(), coin.lower()]:
|
|
821
|
+
if key in mid_prices:
|
|
822
|
+
return mid_prices[key]
|
|
823
|
+
|
|
824
|
+
# Strip U-prefix (UXPL -> XPL)
|
|
825
|
+
if coin.startswith("U") and len(coin) > 1:
|
|
826
|
+
stripped = coin[1:]
|
|
827
|
+
for key in [stripped, stripped.upper(), stripped.lower()]:
|
|
828
|
+
if key in mid_prices:
|
|
829
|
+
return mid_prices[key]
|
|
830
|
+
|
|
831
|
+
# Add U-prefix (XPL -> UXPL)
|
|
832
|
+
prefixed = f"U{coin}"
|
|
833
|
+
for key in [prefixed, prefixed.upper(), prefixed.lower()]:
|
|
834
|
+
if key in mid_prices:
|
|
835
|
+
return mid_prices[key]
|
|
836
|
+
|
|
837
|
+
return 0.0
|
|
838
|
+
|
|
839
|
+
def _coins_match(self, coin1: str, coin2: str) -> bool:
|
|
840
|
+
"""Check if two coin names match, handling U-prefix (UXPL == XPL)."""
|
|
841
|
+
if coin1 == coin2:
|
|
842
|
+
return True
|
|
843
|
+
# Strip U-prefix from either and compare
|
|
844
|
+
c1 = coin1[1:] if coin1.startswith("U") and len(coin1) > 1 else coin1
|
|
845
|
+
c2 = coin2[1:] if coin2.startswith("U") and len(coin2) > 1 else coin2
|
|
846
|
+
return c1 == c2
|
|
847
|
+
|
|
800
848
|
async def withdraw(self, amount: float | None = None) -> StatusTuple:
|
|
801
849
|
"""
|
|
802
|
-
Close all positions and
|
|
850
|
+
Close all positions and liquidate to strategy wallet.
|
|
803
851
|
|
|
804
852
|
Handles funds in:
|
|
805
853
|
1. Strategy wallet on Arbitrum (USDC)
|
|
806
854
|
2. Hyperliquid L1 (positions + margin)
|
|
807
855
|
|
|
856
|
+
Does NOT transfer to main wallet - call exit() for that.
|
|
857
|
+
|
|
808
858
|
Args:
|
|
809
859
|
amount: Amount to withdraw (None = all)
|
|
810
860
|
|
|
@@ -812,9 +862,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
812
862
|
StatusTuple (success, message)
|
|
813
863
|
"""
|
|
814
864
|
address = self._get_strategy_wallet_address()
|
|
815
|
-
main_address = self._get_main_wallet_address()
|
|
816
865
|
usdc_token_id = "usd-coin-arbitrum"
|
|
817
|
-
total_withdrawn = 0.0
|
|
818
866
|
|
|
819
867
|
# Check for USDC already in strategy wallet on Arbitrum
|
|
820
868
|
strategy_usdc = 0.0
|
|
@@ -852,44 +900,14 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
852
900
|
if strategy_usdc < 1.0 and hl_value < 1.0 and self.current_position is None:
|
|
853
901
|
return (False, "Nothing to withdraw")
|
|
854
902
|
|
|
855
|
-
#
|
|
856
|
-
if strategy_usdc > 1.0 and main_address.lower() != address.lower():
|
|
857
|
-
amount_to_send = strategy_usdc # Send full amount
|
|
858
|
-
self.logger.info(
|
|
859
|
-
f"Found ${strategy_usdc:.2f} USDC in strategy wallet, "
|
|
860
|
-
f"sending ${amount_to_send:.2f} to main wallet"
|
|
861
|
-
)
|
|
862
|
-
|
|
863
|
-
try:
|
|
864
|
-
(
|
|
865
|
-
send_success,
|
|
866
|
-
send_result,
|
|
867
|
-
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
868
|
-
query=usdc_token_id,
|
|
869
|
-
amount=amount_to_send,
|
|
870
|
-
strategy_name=self.name,
|
|
871
|
-
skip_ledger=False,
|
|
872
|
-
)
|
|
873
|
-
|
|
874
|
-
if send_success:
|
|
875
|
-
self.logger.info(f"Sent ${amount_to_send:.2f} USDC to main wallet")
|
|
876
|
-
total_withdrawn += amount_to_send
|
|
877
|
-
else:
|
|
878
|
-
self.logger.warning(
|
|
879
|
-
f"Failed to send USDC to main wallet: {send_result}"
|
|
880
|
-
)
|
|
881
|
-
except Exception as e:
|
|
882
|
-
self.logger.error(f"Error sending USDC to main wallet: {e}")
|
|
883
|
-
|
|
884
|
-
# If nothing on Hyperliquid, we're done
|
|
903
|
+
# If nothing on Hyperliquid, we're done - funds already in strategy wallet
|
|
885
904
|
if hl_value < 1.0 and self.current_position is None:
|
|
886
905
|
self.deposit_amount = 0
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
return (True, "No funds on Hyperliquid to withdraw")
|
|
906
|
+
return (
|
|
907
|
+
True,
|
|
908
|
+
f"${strategy_usdc:.2f} USDC in strategy wallet ({address}). "
|
|
909
|
+
f"Call exit() to transfer to main wallet.",
|
|
910
|
+
)
|
|
893
911
|
|
|
894
912
|
# Close any open position
|
|
895
913
|
if self.current_position is not None:
|
|
@@ -898,23 +916,50 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
898
916
|
return (False, f"Failed to close position: {close_msg}")
|
|
899
917
|
|
|
900
918
|
# Step 1: Transfer any spot USDC to perp for withdrawal
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
919
|
+
# Wait for spot sale to settle before checking balance
|
|
920
|
+
await asyncio.sleep(5)
|
|
921
|
+
|
|
922
|
+
for _transfer_attempt in range(3):
|
|
923
|
+
success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
|
|
924
|
+
address
|
|
925
|
+
)
|
|
926
|
+
if not success:
|
|
927
|
+
continue
|
|
928
|
+
|
|
905
929
|
spot_balances = spot_state.get("balances", [])
|
|
906
930
|
for bal in spot_balances:
|
|
907
931
|
if bal.get("coin") == "USDC":
|
|
908
|
-
|
|
932
|
+
# Use available balance (total - hold), not total
|
|
933
|
+
total = float(bal.get("total", 0))
|
|
934
|
+
hold = float(bal.get("hold", 0))
|
|
935
|
+
available = total - hold
|
|
936
|
+
|
|
937
|
+
# Floor to 2 decimal places to avoid precision issues
|
|
938
|
+
spot_usdc = math.floor(available * 100) / 100
|
|
939
|
+
|
|
909
940
|
if spot_usdc > 1.0: # Only transfer if meaningful amount
|
|
910
941
|
self.logger.info(
|
|
911
|
-
f"Transferring ${spot_usdc:.2f} from spot to perp"
|
|
942
|
+
f"Transferring ${spot_usdc:.2f} from spot to perp "
|
|
943
|
+
f"(available={available:.8f}, floored={spot_usdc:.2f})"
|
|
912
944
|
)
|
|
913
|
-
|
|
945
|
+
(
|
|
946
|
+
transfer_ok,
|
|
947
|
+
transfer_result,
|
|
948
|
+
) = await self.hyperliquid_adapter.transfer_spot_to_perp(
|
|
914
949
|
amount=spot_usdc,
|
|
915
950
|
address=address,
|
|
916
951
|
)
|
|
952
|
+
if transfer_ok:
|
|
953
|
+
self.logger.info("Spot to perp transfer successful")
|
|
954
|
+
else:
|
|
955
|
+
self.logger.warning(
|
|
956
|
+
f"Spot to perp transfer failed: {transfer_result}. "
|
|
957
|
+
f"Retrying after delay..."
|
|
958
|
+
)
|
|
959
|
+
await asyncio.sleep(5)
|
|
960
|
+
continue
|
|
917
961
|
break
|
|
962
|
+
break
|
|
918
963
|
|
|
919
964
|
# Step 2: Get updated perp balance for withdrawal (with retry)
|
|
920
965
|
# Wait a moment for transfers to settle
|
|
@@ -982,31 +1027,10 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
982
1027
|
f"Withdrawal confirmed: tx={tx_hash}, amount=${withdrawn_amount:.2f}"
|
|
983
1028
|
)
|
|
984
1029
|
|
|
985
|
-
# Record withdrawal in ledger
|
|
986
|
-
try:
|
|
987
|
-
await self.ledger_adapter.record_withdrawal(
|
|
988
|
-
wallet_address=address,
|
|
989
|
-
chain_id=42161, # Arbitrum
|
|
990
|
-
token_address="hyperliquid-vault-usd",
|
|
991
|
-
token_amount=str(withdrawn_amount),
|
|
992
|
-
usd_value=withdrawn_amount,
|
|
993
|
-
data={
|
|
994
|
-
"source": "hyperliquid_l1",
|
|
995
|
-
"destination": "arbitrum",
|
|
996
|
-
"tx_hash": tx_hash,
|
|
997
|
-
},
|
|
998
|
-
strategy_name=self.name,
|
|
999
|
-
)
|
|
1000
|
-
self.logger.info(
|
|
1001
|
-
f"Recorded withdrawal of ${withdrawn_amount:.2f} in ledger"
|
|
1002
|
-
)
|
|
1003
|
-
except Exception as e:
|
|
1004
|
-
self.logger.warning(f"Failed to record withdrawal in ledger: {e}")
|
|
1005
|
-
|
|
1006
1030
|
# Step 5: Wait a bit for the USDC to be credited on Arbitrum
|
|
1007
1031
|
await asyncio.sleep(10)
|
|
1008
1032
|
|
|
1009
|
-
# Get final USDC balance
|
|
1033
|
+
# Get final USDC balance in strategy wallet
|
|
1010
1034
|
final_balance = 0.0
|
|
1011
1035
|
try:
|
|
1012
1036
|
success, balance_data = await self.balance_adapter.get_balance(
|
|
@@ -1018,59 +1042,81 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1018
1042
|
except Exception as e:
|
|
1019
1043
|
self.logger.warning(f"Could not get final balance: {e}")
|
|
1020
1044
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1045
|
+
self.deposit_amount = 0
|
|
1046
|
+
self.current_position = None
|
|
1047
|
+
|
|
1048
|
+
return (
|
|
1049
|
+
True,
|
|
1050
|
+
f"Withdrew ${withdrawn_amount:.2f} from Hyperliquid to strategy wallet ({address}). "
|
|
1051
|
+
f"Current balance: ${final_balance:.2f}. Call exit() to transfer to main wallet.",
|
|
1052
|
+
)
|
|
1023
1053
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
)
|
|
1054
|
+
async def exit(self, **kwargs) -> StatusTuple:
|
|
1055
|
+
"""Transfer funds from strategy wallet to main wallet."""
|
|
1056
|
+
self.logger.info("EXIT: Transferring remaining funds to main wallet")
|
|
1028
1057
|
|
|
1029
|
-
|
|
1058
|
+
strategy_address = self._get_strategy_wallet_address()
|
|
1059
|
+
main_address = self._get_main_wallet_address()
|
|
1060
|
+
|
|
1061
|
+
if strategy_address.lower() == main_address.lower():
|
|
1062
|
+
return (True, "Main wallet is strategy wallet, no transfer needed")
|
|
1063
|
+
|
|
1064
|
+
transferred_items = []
|
|
1065
|
+
|
|
1066
|
+
# Transfer USDC to main wallet
|
|
1067
|
+
usdc_ok, usdc_raw = await self.balance_adapter.get_balance(
|
|
1068
|
+
token_id=USDC_ARBITRUM_TOKEN_ID,
|
|
1069
|
+
wallet_address=strategy_address,
|
|
1070
|
+
)
|
|
1071
|
+
if usdc_ok and usdc_raw:
|
|
1072
|
+
usdc_balance = float(usdc_raw.get("balance", 0))
|
|
1073
|
+
if usdc_balance > 1.0:
|
|
1074
|
+
self.logger.info(f"Transferring {usdc_balance:.2f} USDC to main wallet")
|
|
1030
1075
|
(
|
|
1031
|
-
|
|
1032
|
-
|
|
1076
|
+
success,
|
|
1077
|
+
msg,
|
|
1033
1078
|
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
1034
|
-
query=
|
|
1035
|
-
amount=
|
|
1079
|
+
query=USDC_ARBITRUM_TOKEN_ID,
|
|
1080
|
+
amount=usdc_balance,
|
|
1036
1081
|
strategy_name=self.name,
|
|
1037
|
-
skip_ledger=False,
|
|
1082
|
+
skip_ledger=False,
|
|
1038
1083
|
)
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
self.logger.info(
|
|
1042
|
-
f"Successfully sent ${amount_to_send:.2f} USDC to main wallet"
|
|
1043
|
-
)
|
|
1044
|
-
total_withdrawn += amount_to_send
|
|
1084
|
+
if success:
|
|
1085
|
+
transferred_items.append(f"{usdc_balance:.2f} USDC")
|
|
1045
1086
|
else:
|
|
1046
|
-
self.logger.warning(
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
f"
|
|
1087
|
+
self.logger.warning(f"USDC transfer failed: {msg}")
|
|
1088
|
+
|
|
1089
|
+
# Transfer ETH (minus reserve for tx fees) to main wallet
|
|
1090
|
+
eth_ok, eth_raw = await self.balance_adapter.get_balance(
|
|
1091
|
+
token_id="ethereum-arbitrum",
|
|
1092
|
+
wallet_address=strategy_address,
|
|
1093
|
+
)
|
|
1094
|
+
if eth_ok and eth_raw:
|
|
1095
|
+
eth_balance = float(eth_raw.get("balance", 0))
|
|
1096
|
+
tx_fee_reserve = 0.0002
|
|
1097
|
+
transferable_eth = eth_balance - tx_fee_reserve
|
|
1098
|
+
if transferable_eth > 0.0001:
|
|
1099
|
+
self.logger.info(
|
|
1100
|
+
f"Transferring {transferable_eth:.6f} ETH to main wallet"
|
|
1060
1101
|
)
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1102
|
+
(
|
|
1103
|
+
success,
|
|
1104
|
+
msg,
|
|
1105
|
+
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
1106
|
+
query="ethereum-arbitrum",
|
|
1107
|
+
amount=transferable_eth,
|
|
1108
|
+
strategy_name=self.name,
|
|
1109
|
+
skip_ledger=False,
|
|
1110
|
+
)
|
|
1111
|
+
if success:
|
|
1112
|
+
transferred_items.append(f"{transferable_eth:.6f} ETH")
|
|
1113
|
+
else:
|
|
1114
|
+
self.logger.warning(f"ETH transfer failed: {msg}")
|
|
1066
1115
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1116
|
+
if not transferred_items:
|
|
1117
|
+
return (True, "No funds to transfer to main wallet")
|
|
1069
1118
|
|
|
1070
|
-
return (
|
|
1071
|
-
True,
|
|
1072
|
-
f"Withdrew ${total_withdrawn:.2f} total to main wallet ({main_address}).",
|
|
1073
|
-
)
|
|
1119
|
+
return (True, f"Transferred to main wallet: {', '.join(transferred_items)}")
|
|
1074
1120
|
|
|
1075
1121
|
async def _status(self) -> StatusDict:
|
|
1076
1122
|
"""Return portfolio value and strategy status with live data."""
|
|
@@ -1386,7 +1432,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1386
1432
|
|
|
1387
1433
|
# Get entry price from current mid
|
|
1388
1434
|
success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
|
|
1389
|
-
entry_price =
|
|
1435
|
+
entry_price = self._resolve_mid_price(coin, mids) if success else 0.0
|
|
1390
1436
|
|
|
1391
1437
|
# Step 5: Get liquidation price and place stop-loss
|
|
1392
1438
|
success, user_state = await self.hyperliquid_adapter.get_user_state(address)
|
|
@@ -1576,7 +1622,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1576
1622
|
if not success:
|
|
1577
1623
|
return False, "Failed to get mid prices"
|
|
1578
1624
|
|
|
1579
|
-
price =
|
|
1625
|
+
price = self._resolve_mid_price(pos.coin, mids)
|
|
1580
1626
|
if price <= 0:
|
|
1581
1627
|
return False, f"Invalid price for {pos.coin}"
|
|
1582
1628
|
|
|
@@ -1843,7 +1889,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1843
1889
|
if not success:
|
|
1844
1890
|
return False, "Failed to fetch mid prices"
|
|
1845
1891
|
|
|
1846
|
-
mid_px = float(
|
|
1892
|
+
mid_px = float(self._resolve_mid_price(coin, mids) or 0.0)
|
|
1847
1893
|
if mid_px <= 0:
|
|
1848
1894
|
return False, "Missing mid price"
|
|
1849
1895
|
|
|
@@ -1890,7 +1936,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1890
1936
|
spot_size = 0.0
|
|
1891
1937
|
if success:
|
|
1892
1938
|
for bal in spot_state.get("balances", []):
|
|
1893
|
-
if bal.get("coin")
|
|
1939
|
+
if self._coins_match(bal.get("coin", ""), coin):
|
|
1894
1940
|
spot_size = float(bal.get("total", 0))
|
|
1895
1941
|
break
|
|
1896
1942
|
|
|
@@ -1949,7 +1995,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1949
1995
|
spot_size = 0.0
|
|
1950
1996
|
if success:
|
|
1951
1997
|
for bal in spot_state.get("balances", []):
|
|
1952
|
-
if bal.get("coin")
|
|
1998
|
+
if self._coins_match(bal.get("coin", ""), coin):
|
|
1953
1999
|
spot_size = float(bal.get("total", 0))
|
|
1954
2000
|
break
|
|
1955
2001
|
|
|
@@ -1961,7 +2007,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1961
2007
|
success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
|
|
1962
2008
|
if not success:
|
|
1963
2009
|
return False, "Failed to get mid prices"
|
|
1964
|
-
price =
|
|
2010
|
+
price = self._resolve_mid_price(coin, mids)
|
|
1965
2011
|
if price <= 0:
|
|
1966
2012
|
return False, f"Invalid price for {coin}"
|
|
1967
2013
|
|
|
@@ -2257,10 +2303,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2257
2303
|
balances = spot_state.get("balances", [])
|
|
2258
2304
|
for bal in balances:
|
|
2259
2305
|
coin = bal.get("coin", "")
|
|
2260
|
-
|
|
2261
|
-
if coin == self.current_position.coin or coin.startswith(
|
|
2262
|
-
self.current_position.coin
|
|
2263
|
-
):
|
|
2306
|
+
if self._coins_match(coin, self.current_position.coin):
|
|
2264
2307
|
bal["asset_id"] = self.current_position.spot_asset_id
|
|
2265
2308
|
return bal
|
|
2266
2309
|
|
|
@@ -2555,7 +2598,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2555
2598
|
hl_value += total
|
|
2556
2599
|
else:
|
|
2557
2600
|
# Look up mid price for non-USDC assets
|
|
2558
|
-
mid_price =
|
|
2601
|
+
mid_price = self._resolve_mid_price(coin, mid_prices)
|
|
2559
2602
|
if mid_price > 0:
|
|
2560
2603
|
hl_value += total * mid_price
|
|
2561
2604
|
else:
|