wayfinder-paths 0.1.15__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.

Files changed (47) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +19 -20
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +66 -37
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +2 -8
  4. wayfinder_paths/adapters/brap_adapter/README.md +22 -19
  5. wayfinder_paths/adapters/brap_adapter/adapter.py +33 -34
  6. wayfinder_paths/adapters/brap_adapter/test_adapter.py +2 -18
  7. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +40 -56
  8. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +1 -8
  9. wayfinder_paths/adapters/moonwell_adapter/README.md +29 -31
  10. wayfinder_paths/adapters/moonwell_adapter/adapter.py +301 -662
  11. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +275 -179
  12. wayfinder_paths/core/config.py +8 -47
  13. wayfinder_paths/core/constants/base.py +0 -1
  14. wayfinder_paths/core/constants/erc20_abi.py +13 -13
  15. wayfinder_paths/core/strategies/Strategy.py +6 -2
  16. wayfinder_paths/core/utils/erc20_service.py +100 -0
  17. wayfinder_paths/core/utils/evm_helpers.py +1 -1
  18. wayfinder_paths/core/utils/transaction.py +191 -0
  19. wayfinder_paths/core/utils/web3.py +66 -0
  20. wayfinder_paths/run_strategy.py +37 -6
  21. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +200 -224
  22. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +128 -151
  23. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -1
  24. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +52 -78
  25. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -12
  26. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +0 -1
  27. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +39 -64
  28. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -1
  29. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +42 -85
  30. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -8
  31. wayfinder_paths/templates/strategy/README.md +1 -5
  32. {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/METADATA +3 -41
  33. {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/RECORD +35 -44
  34. {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/WHEEL +1 -1
  35. wayfinder_paths/core/clients/sdk_example.py +0 -125
  36. wayfinder_paths/core/engine/__init__.py +0 -5
  37. wayfinder_paths/core/services/__init__.py +0 -0
  38. wayfinder_paths/core/services/base.py +0 -131
  39. wayfinder_paths/core/services/local_evm_txn.py +0 -350
  40. wayfinder_paths/core/services/local_token_txn.py +0 -238
  41. wayfinder_paths/core/services/web3_service.py +0 -43
  42. wayfinder_paths/core/wallets/README.md +0 -88
  43. wayfinder_paths/core/wallets/WalletManager.py +0 -56
  44. wayfinder_paths/core/wallets/__init__.py +0 -7
  45. wayfinder_paths/scripts/run_strategy.py +0 -152
  46. wayfinder_paths/strategies/config.py +0 -85
  47. {wayfinder_paths-0.1.15.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
- LIQUIDATION_REBALANCE_THRESHOLD = 0.75 # Trigger rebalance at 75% to liquidation
120
- LIQUIDATION_STOP_LOSS_THRESHOLD = 0.90 # Stop-loss at 90% to liquidation (closer)
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, web3_service=web3_service
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
- query="ethereum-arbitrum", # Native ETH on Arbitrum
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, then send USDC to bridge.
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
- query=USDC_ARBITRUM_TOKEN_ID,
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
- self.logger.info(
539
- f"Sending {main_token_amount} USDC from strategy wallet ({strategy_address}) "
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"Deposited {main_token_amount} USDC to Hyperliquid L1. "
604
- f"Balance: ${final_balance:.2f}. Call update() to open positions.",
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
- # If deposit_amount not set, try to detect from Hyperliquid USDC (spot + perp)
626
- if self.deposit_amount <= 0:
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
- total_usdc = perp_margin + spot_usdc
629
- if total_usdc > 1.0:
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"Detected ${total_usdc:.2f} USDC on Hyperliquid (spot+perp), using as deposit amount"
599
+ f"Found ${strategy_usdc:.2f} USDC in strategy wallet, bridging to Hyperliquid"
632
600
  )
633
- self.deposit_amount = total_usdc
634
601
 
635
- if self.deposit_amount <= 0:
636
- return (False, "No deposit to manage. Call deposit() first.")
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 return funds to main wallet.
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
- # Step 0: Send any USDC already in strategy wallet to main wallet
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
- if total_withdrawn > 0:
888
- return (
889
- True,
890
- f"Sent ${total_withdrawn:.2f} USDC to main wallet ({main_address})",
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
- success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
902
- address
903
- )
904
- if success:
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
- spot_usdc = float(bal.get("total", 0))
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
- await self.hyperliquid_adapter.transfer_spot_to_perp(
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,58 +1042,13 @@ 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
- # Step 6: Send USDC from strategy wallet to main wallet
1022
- amount_to_send = final_balance # Send full amount
1023
-
1024
- if amount_to_send > 1.0 and main_address.lower() != address.lower():
1025
- self.logger.info(
1026
- f"Sending ${amount_to_send:.2f} USDC from strategy wallet to main wallet"
1027
- )
1028
-
1029
- try:
1030
- (
1031
- send_success,
1032
- send_result,
1033
- ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
1034
- query=usdc_token_id,
1035
- amount=amount_to_send,
1036
- strategy_name=self.name,
1037
- skip_ledger=False, # Record in ledger
1038
- )
1039
-
1040
- if send_success:
1041
- self.logger.info(
1042
- f"Successfully sent ${amount_to_send:.2f} USDC to main wallet"
1043
- )
1044
- total_withdrawn += amount_to_send
1045
- else:
1046
- self.logger.warning(
1047
- f"Failed to send USDC to main wallet: {send_result}"
1048
- )
1049
- return (
1050
- True,
1051
- f"Withdrew ${withdrawable:.2f} from Hyperliquid but failed to send to main wallet: {send_result}. "
1052
- f"USDC is in strategy wallet ({address}).",
1053
- )
1054
- except Exception as e:
1055
- self.logger.error(f"Error sending to main wallet: {e}")
1056
- return (
1057
- True,
1058
- f"Withdrew ${withdrawable:.2f} from Hyperliquid but error sending to main wallet: {e}. "
1059
- f"USDC is in strategy wallet ({address}).",
1060
- )
1061
- elif main_address.lower() == address.lower():
1062
- self.logger.info("Main wallet is strategy wallet, no transfer needed")
1063
- total_withdrawn += withdrawable
1064
- else:
1065
- self.logger.warning(f"Amount too small to transfer: ${amount_to_send:.2f}")
1066
-
1067
1045
  self.deposit_amount = 0
1068
1046
  self.current_position = None
1069
1047
 
1070
1048
  return (
1071
1049
  True,
1072
- f"Withdrew ${total_withdrawn:.2f} total to main wallet ({main_address}).",
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.",
1073
1052
  )
1074
1053
 
1075
1054
  async def exit(self, **kwargs) -> StatusTuple:
@@ -1453,7 +1432,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1453
1432
 
1454
1433
  # Get entry price from current mid
1455
1434
  success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
1456
- entry_price = mids.get(coin, 0.0) if success else 0.0
1435
+ entry_price = self._resolve_mid_price(coin, mids) if success else 0.0
1457
1436
 
1458
1437
  # Step 5: Get liquidation price and place stop-loss
1459
1438
  success, user_state = await self.hyperliquid_adapter.get_user_state(address)
@@ -1643,7 +1622,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1643
1622
  if not success:
1644
1623
  return False, "Failed to get mid prices"
1645
1624
 
1646
- price = mids.get(pos.coin, 0.0)
1625
+ price = self._resolve_mid_price(pos.coin, mids)
1647
1626
  if price <= 0:
1648
1627
  return False, f"Invalid price for {pos.coin}"
1649
1628
 
@@ -1910,7 +1889,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1910
1889
  if not success:
1911
1890
  return False, "Failed to fetch mid prices"
1912
1891
 
1913
- mid_px = float(mids.get(coin, 0.0) or 0.0)
1892
+ mid_px = float(self._resolve_mid_price(coin, mids) or 0.0)
1914
1893
  if mid_px <= 0:
1915
1894
  return False, "Missing mid price"
1916
1895
 
@@ -1957,7 +1936,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1957
1936
  spot_size = 0.0
1958
1937
  if success:
1959
1938
  for bal in spot_state.get("balances", []):
1960
- if bal.get("coin") == coin:
1939
+ if self._coins_match(bal.get("coin", ""), coin):
1961
1940
  spot_size = float(bal.get("total", 0))
1962
1941
  break
1963
1942
 
@@ -2016,7 +1995,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2016
1995
  spot_size = 0.0
2017
1996
  if success:
2018
1997
  for bal in spot_state.get("balances", []):
2019
- if bal.get("coin") == coin:
1998
+ if self._coins_match(bal.get("coin", ""), coin):
2020
1999
  spot_size = float(bal.get("total", 0))
2021
2000
  break
2022
2001
 
@@ -2028,7 +2007,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2028
2007
  success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
2029
2008
  if not success:
2030
2009
  return False, "Failed to get mid prices"
2031
- price = mids.get(coin, 0)
2010
+ price = self._resolve_mid_price(coin, mids)
2032
2011
  if price <= 0:
2033
2012
  return False, f"Invalid price for {coin}"
2034
2013
 
@@ -2324,10 +2303,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2324
2303
  balances = spot_state.get("balances", [])
2325
2304
  for bal in balances:
2326
2305
  coin = bal.get("coin", "")
2327
- # Match by stripping /USDC suffix or checking against coin name
2328
- if coin == self.current_position.coin or coin.startswith(
2329
- self.current_position.coin
2330
- ):
2306
+ if self._coins_match(coin, self.current_position.coin):
2331
2307
  bal["asset_id"] = self.current_position.spot_asset_id
2332
2308
  return bal
2333
2309
 
@@ -2622,7 +2598,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2622
2598
  hl_value += total
2623
2599
  else:
2624
2600
  # Look up mid price for non-USDC assets
2625
- mid_price = mid_prices.get(coin, 0.0)
2601
+ mid_price = self._resolve_mid_price(coin, mid_prices)
2626
2602
  if mid_price > 0:
2627
2603
  hl_value += total * mid_price
2628
2604
  else: