wayfinder-paths 0.1.11__py3-none-any.whl → 0.1.14__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 (66) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +13 -14
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +36 -39
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
  4. wayfinder_paths/adapters/brap_adapter/README.md +11 -16
  5. wayfinder_paths/adapters/brap_adapter/adapter.py +87 -75
  6. wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
  7. wayfinder_paths/adapters/brap_adapter/test_adapter.py +121 -59
  8. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +22 -23
  9. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +114 -60
  10. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1 -1
  11. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +44 -5
  12. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +104 -0
  13. wayfinder_paths/adapters/moonwell_adapter/adapter.py +0 -3
  14. wayfinder_paths/adapters/pool_adapter/README.md +11 -27
  15. wayfinder_paths/adapters/pool_adapter/adapter.py +11 -37
  16. wayfinder_paths/adapters/pool_adapter/examples.json +6 -7
  17. wayfinder_paths/adapters/pool_adapter/test_adapter.py +8 -8
  18. wayfinder_paths/adapters/token_adapter/README.md +2 -14
  19. wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
  20. wayfinder_paths/adapters/token_adapter/examples.json +4 -8
  21. wayfinder_paths/adapters/token_adapter/test_adapter.py +5 -3
  22. wayfinder_paths/core/clients/BRAPClient.py +103 -62
  23. wayfinder_paths/core/clients/ClientManager.py +1 -68
  24. wayfinder_paths/core/clients/HyperlendClient.py +127 -66
  25. wayfinder_paths/core/clients/LedgerClient.py +1 -4
  26. wayfinder_paths/core/clients/PoolClient.py +126 -88
  27. wayfinder_paths/core/clients/TokenClient.py +92 -37
  28. wayfinder_paths/core/clients/WalletClient.py +28 -58
  29. wayfinder_paths/core/clients/WayfinderClient.py +33 -166
  30. wayfinder_paths/core/clients/__init__.py +0 -2
  31. wayfinder_paths/core/clients/protocols.py +35 -52
  32. wayfinder_paths/core/clients/sdk_example.py +37 -22
  33. wayfinder_paths/core/config.py +60 -224
  34. wayfinder_paths/core/engine/StrategyJob.py +7 -55
  35. wayfinder_paths/core/services/local_evm_txn.py +28 -10
  36. wayfinder_paths/core/services/local_token_txn.py +1 -1
  37. wayfinder_paths/core/strategies/Strategy.py +3 -5
  38. wayfinder_paths/core/strategies/descriptors.py +7 -0
  39. wayfinder_paths/core/utils/evm_helpers.py +7 -3
  40. wayfinder_paths/core/utils/wallets.py +12 -19
  41. wayfinder_paths/core/wallets/README.md +1 -1
  42. wayfinder_paths/run_strategy.py +8 -17
  43. wayfinder_paths/scripts/create_strategy.py +5 -5
  44. wayfinder_paths/scripts/make_wallets.py +5 -5
  45. wayfinder_paths/scripts/run_strategy.py +3 -3
  46. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -1
  47. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +206 -526
  48. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +228 -11
  49. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
  50. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +41 -25
  51. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
  52. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +1 -1
  53. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +10 -9
  54. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +12 -6
  55. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +3 -3
  56. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +110 -78
  57. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +44 -21
  58. wayfinder_paths/templates/adapter/README.md +1 -1
  59. wayfinder_paths/templates/strategy/README.md +3 -3
  60. wayfinder_paths/templates/strategy/test_strategy.py +3 -2
  61. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/METADATA +21 -59
  62. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/RECORD +64 -65
  63. wayfinder_paths/core/clients/AuthClient.py +0 -83
  64. wayfinder_paths/core/settings.py +0 -61
  65. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/LICENSE +0 -0
  66. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/WHEEL +0 -0
@@ -12,16 +12,15 @@ import math
12
12
  import random
13
13
  import time
14
14
  from datetime import UTC, datetime, timedelta
15
- from decimal import ROUND_UP, Decimal, getcontext
15
+ from decimal import ROUND_DOWN, ROUND_UP, Decimal, getcontext
16
16
  from pathlib import Path
17
- from statistics import fmean, mean, pstdev
17
+ from statistics import fmean
18
18
  from typing import Any
19
19
 
20
20
  from wayfinder_paths.adapters.balance_adapter.adapter import BalanceAdapter
21
21
  from wayfinder_paths.adapters.hyperliquid_adapter.adapter import (
22
22
  HYPERLIQUID_BRIDGE_ADDRESS,
23
23
  HyperliquidAdapter,
24
- SimpleCache,
25
24
  )
26
25
  from wayfinder_paths.adapters.hyperliquid_adapter.executor import (
27
26
  HyperliquidExecutor,
@@ -44,9 +43,6 @@ from wayfinder_paths.adapters.hyperliquid_adapter.utils import (
44
43
  from wayfinder_paths.adapters.hyperliquid_adapter.utils import (
45
44
  spot_index_from_asset_id as hl_spot_index_from_asset_id,
46
45
  )
47
- from wayfinder_paths.adapters.hyperliquid_adapter.utils import (
48
- sz_decimals_for_asset as hl_sz_decimals_for_asset,
49
- )
50
46
  from wayfinder_paths.adapters.hyperliquid_adapter.utils import (
51
47
  usd_depth_in_band as hl_usd_depth_in_band,
52
48
  )
@@ -91,10 +87,6 @@ from wayfinder_paths.strategies.basis_trading_strategy.types import (
91
87
  # Set decimal precision for precise price/size calculations
92
88
  getcontext().prec = 28
93
89
 
94
- # Hyperliquid price decimal limits
95
- MAX_DECIMALS_PERP = 6
96
- MAX_DECIMALS_SPOT = 8
97
-
98
90
 
99
91
  def _d(x: float | Decimal | str) -> Decimal:
100
92
  """Convert to Decimal for precise calculations."""
@@ -115,21 +107,20 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
115
107
  # Strategy parameters
116
108
  MIN_DEPOSIT_USDC = 25
117
109
  DEFAULT_LOOKBACK_DAYS = 30 # Supports up to ~208 days via chunked API calls
118
- DEFAULT_CONFIDENCE = 0.975
119
110
  DEFAULT_FEE_EPS = 0.003 # 0.3% fee buffer
120
- DEFAULT_OI_FLOOR = 100_000.0 # Min OI in USD (matches Django)
111
+ DEFAULT_OI_FLOOR = 100_000.0 # Min OI in USD
121
112
  DEFAULT_DAY_VLM_FLOOR = 100_000 # Min daily volume
122
113
  DEFAULT_MAX_LEVERAGE = 2
123
114
  GAS_MAXIMUM = 0.01 # ETH
124
115
  DEFAULT_BOOTSTRAP_SIMS = 50
125
116
  DEFAULT_BOOTSTRAP_BLOCK_HOURS = 48
126
117
 
127
- # Liquidation and rebalance thresholds (from Django funding_rate_strategy.py)
118
+ # Liquidation and rebalance thresholds
128
119
  LIQUIDATION_REBALANCE_THRESHOLD = 0.75 # Trigger rebalance at 75% to liquidation
129
120
  LIQUIDATION_STOP_LOSS_THRESHOLD = 0.90 # Stop-loss at 90% to liquidation (closer)
130
121
  FUNDING_REBALANCE_THRESHOLD = 0.02 # Rebalance when funding hits 2% gains
131
122
 
132
- # Position tolerances (from Django hyperliquid_adapter.py)
123
+ # Position tolerances
133
124
  SPOT_POSITION_DUST_TOLERANCE = 0.04 # ±4% size drift allowed
134
125
  MIN_UNUSED_USD = 5.0 # Minimum idle USD threshold
135
126
  UNUSED_REL_EPS = 0.01 # 1% of bankroll idle threshold
@@ -157,6 +148,8 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
157
148
  "Automated delta-neutral basis trading on Hyperliquid, capturing funding rate payments "
158
149
  "through matched spot long / perp short positions with intelligent leverage sizing."
159
150
  ),
151
+ risk_description="Protocol risk is always present when engaging with DeFi strategies, this includes underlying DeFi protocols and Wayfinder itself. Additional risks include funding rate reversals, liquidity constraints on Hyperliquid, smart contract risk, and temporary capital lock-up during volatile market conditions. During extreme price movements, high volatility can stop out the short side of positions, breaking delta-neutrality and leaving unhedged long exposure that suffers losses when prices revert downward. This can cause significant damage beyond normal funding rate fluctuations.",
152
+ fee_description="Wayfinder takes a 2 basis point (0.02%) builder fee on all orders placed on Hyperliquid through this strategy. If fees remain unpaid, Wayfinder may pause automated management of this vault.",
160
153
  gas_token_symbol="ETH",
161
154
  gas_token_id="ethereum-arbitrum",
162
155
  deposit_token_id="usd-coin-arbitrum",
@@ -210,9 +203,8 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
210
203
  strategy_wallet: dict[str, Any] | None = None,
211
204
  web3_service: Web3Service | None = None,
212
205
  hyperliquid_executor: HyperliquidExecutor | None = None,
213
- api_key: str | None = None,
214
206
  ) -> None:
215
- super().__init__(api_key=api_key)
207
+ super().__init__()
216
208
 
217
209
  merged_config = dict(config or {})
218
210
  if main_wallet:
@@ -231,8 +223,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
231
223
  "builder_fee", self.DEFAULT_BUILDER_FEE
232
224
  )
233
225
 
234
- # Initialize cache
235
- self._cache = SimpleCache()
236
226
  self._margin_table_cache: dict[int, list[dict[str, float]]] = {}
237
227
 
238
228
  # Adapters (some are optional for analysis-only usage).
@@ -315,8 +305,8 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
315
305
  success, deposit_data = await self.ledger_adapter.get_strategy_net_deposit(
316
306
  wallet_address=self._get_strategy_wallet_address()
317
307
  )
318
- if success and deposit_data:
319
- self.deposit_amount = float(deposit_data.get("net_deposit", 0) or 0)
308
+ if success and deposit_data is not None:
309
+ self.deposit_amount = float(deposit_data)
320
310
  except Exception as e:
321
311
  self.logger.warning(f"Could not fetch deposit data: {e}")
322
312
 
@@ -488,7 +478,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
488
478
  gas_ok,
489
479
  gas_res,
490
480
  ) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
491
- token_id="ethereum-arbitrum", # Native ETH on Arbitrum
481
+ query="ethereum-arbitrum", # Native ETH on Arbitrum
492
482
  amount=gas_token_amount,
493
483
  strategy_name=self.name or "basis_trading_strategy",
494
484
  skip_ledger=True,
@@ -509,7 +499,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
509
499
  strategy_balance_ok,
510
500
  strategy_balance,
511
501
  ) = await self.balance_adapter.get_balance(
512
- token_id=USDC_ARBITRUM_TOKEN_ID,
502
+ query=USDC_ARBITRUM_TOKEN_ID,
513
503
  wallet_address=strategy_address,
514
504
  )
515
505
  strategy_usdc = 0.0
@@ -527,7 +517,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
527
517
  move_ok,
528
518
  move_res,
529
519
  ) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
530
- token_id=USDC_ARBITRUM_TOKEN_ID,
520
+ query=USDC_ARBITRUM_TOKEN_ID,
531
521
  amount=need_to_move,
532
522
  strategy_name=self.name or "basis_trading_strategy",
533
523
  skip_ledger=True,
@@ -552,7 +542,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
552
542
 
553
543
  # Send USDC to bridge address (deposit credits the sender address on Hyperliquid)
554
544
  success, result = await self.balance_adapter.send_to_address(
555
- token_id=USDC_ARBITRUM_TOKEN_ID,
545
+ query=USDC_ARBITRUM_TOKEN_ID,
556
546
  amount=main_token_amount,
557
547
  from_wallet=strategy_wallet,
558
548
  to_address=HYPERLIQUID_BRIDGE_ADDRESS,
@@ -632,18 +622,15 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
632
622
  Returns:
633
623
  StatusTuple (success, message)
634
624
  """
635
- # If deposit_amount not set, try to detect from Hyperliquid balance
625
+ # If deposit_amount not set, try to detect from Hyperliquid USDC (spot + perp)
636
626
  if self.deposit_amount <= 0:
637
- address = self._get_strategy_wallet_address()
638
- success, user_state = await self.hyperliquid_adapter.get_user_state(address)
639
- if success:
640
- margin_summary = user_state.get("marginSummary", {})
641
- account_value = float(margin_summary.get("accountValue", 0))
642
- if account_value > 1.0:
643
- self.logger.info(
644
- f"Detected ${account_value:.2f} on Hyperliquid, using as deposit amount"
645
- )
646
- self.deposit_amount = account_value
627
+ perp_margin, spot_usdc = await self._get_undeployed_capital()
628
+ total_usdc = perp_margin + spot_usdc
629
+ if total_usdc > 1.0:
630
+ self.logger.info(
631
+ f"Detected ${total_usdc:.2f} USDC on Hyperliquid (spot+perp), using as deposit amount"
632
+ )
633
+ self.deposit_amount = total_usdc
647
634
 
648
635
  if self.deposit_amount <= 0:
649
636
  return (False, "No deposit to manage. Call deposit() first.")
@@ -662,7 +649,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
662
649
  Analyze basis trading opportunities without executing.
663
650
 
664
651
  Uses the Net-APY + stop-churn backtest solver with block-bootstrap
665
- resampling (ported from Django's NetApyBasisTradingService).
652
+ resampling.
666
653
 
667
654
  Args:
668
655
  deposit_usdc: Hypothetical deposit amount for sizing calculations (default $1000)
@@ -833,7 +820,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
833
820
  strategy_usdc = 0.0
834
821
  try:
835
822
  success, balance_data = await self.balance_adapter.get_balance(
836
- token_id=usdc_token_id,
823
+ query=usdc_token_id,
837
824
  wallet_address=address,
838
825
  )
839
826
  if success:
@@ -878,7 +865,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
878
865
  send_success,
879
866
  send_result,
880
867
  ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
881
- token_id=usdc_token_id,
868
+ query=usdc_token_id,
882
869
  amount=amount_to_send,
883
870
  strategy_name=self.name,
884
871
  skip_ledger=False,
@@ -1023,7 +1010,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1023
1010
  final_balance = 0.0
1024
1011
  try:
1025
1012
  success, balance_data = await self.balance_adapter.get_balance(
1026
- token_id=usdc_token_id,
1013
+ query=usdc_token_id,
1027
1014
  wallet_address=address,
1028
1015
  )
1029
1016
  if success:
@@ -1044,7 +1031,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1044
1031
  send_success,
1045
1032
  send_result,
1046
1033
  ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
1047
- token_id=usdc_token_id,
1034
+ query=usdc_token_id,
1048
1035
  amount=amount_to_send,
1049
1036
  strategy_name=self.name,
1050
1037
  skip_ledger=False, # Record in ledger
@@ -1113,8 +1100,8 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1113
1100
  wallet_address=self._get_strategy_wallet_address()
1114
1101
  )
1115
1102
  net_deposit = (
1116
- float(deposit_data.get("net_deposit", 0) or 0)
1117
- if success
1103
+ float(deposit_data)
1104
+ if success and deposit_data is not None
1118
1105
  else self.deposit_amount
1119
1106
  )
1120
1107
  except Exception:
@@ -1207,6 +1194,13 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1207
1194
  self.logger.info("Analyzing basis trading opportunities...")
1208
1195
 
1209
1196
  try:
1197
+ # Use actual on-exchange USDC (spot + perp) for sizing when opening a fresh position.
1198
+ # This handles liquidation scenarios where most USDC ends up in spot.
1199
+ perp_margin, spot_usdc = await self._get_undeployed_capital()
1200
+ total_usdc = perp_margin + spot_usdc
1201
+ if total_usdc > 1.0:
1202
+ self.deposit_amount = total_usdc
1203
+
1210
1204
  best: dict[str, Any] | None = None
1211
1205
 
1212
1206
  snapshot = self._snapshot_from_config()
@@ -1350,21 +1344,14 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1350
1344
  self.logger.warning(f"Failed to set leverage: {lev_result}")
1351
1345
  # Continue anyway - leverage might already be set
1352
1346
 
1353
- # Step 3: Transfer USDC from perp to spot for spot purchase
1354
- # We need approximately order_usd in spot to buy the asset
1355
- self.logger.info(
1356
- f"Transferring ${order_usd:.2f} from perp to spot for {coin}"
1357
- )
1358
- (
1359
- success,
1360
- transfer_result,
1361
- ) = await self.hyperliquid_adapter.transfer_perp_to_spot(
1362
- amount=order_usd,
1347
+ # Step 3: Ensure USDC is split correctly between spot and perp.
1348
+ # Target: spot has order_usd for the spot buy, perp holds the remainder as margin.
1349
+ split_ok, split_msg = await self._rebalance_usdc_between_perp_and_spot(
1350
+ target_spot_usdc=order_usd,
1363
1351
  address=address,
1364
1352
  )
1365
- if not success:
1366
- self.logger.warning(f"Perp to spot transfer failed: {transfer_result}")
1367
- # May fail if already in spot, continue
1353
+ if not split_ok:
1354
+ self.logger.warning(f"USDC rebalance failed: {split_msg}")
1368
1355
 
1369
1356
  # Step 4: Execute paired fill
1370
1357
  filler = PairedFiller(
@@ -1378,8 +1365,8 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1378
1365
  perp_filled,
1379
1366
  spot_notional,
1380
1367
  perp_notional,
1381
- spot_pointers,
1382
- perp_pointers,
1368
+ _spot_pointers,
1369
+ _perp_pointers,
1383
1370
  ) = await filler.fill_pair_units(
1384
1371
  coin=coin,
1385
1372
  spot_asset_id=spot_asset_id,
@@ -1488,6 +1475,76 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1488
1475
 
1489
1476
  return withdrawable, spot_usdc
1490
1477
 
1478
+ async def _rebalance_usdc_between_perp_and_spot(
1479
+ self,
1480
+ *,
1481
+ target_spot_usdc: float,
1482
+ address: str,
1483
+ ) -> tuple[bool, str]:
1484
+ """
1485
+ Rebalance Hyperliquid USDC between spot and perp to hit a target spot USDC balance.
1486
+
1487
+ Used before opening/scaling a basis position so we can:
1488
+ - fund the spot buy (spot USDC ~= target_spot_usdc)
1489
+ - keep the remainder in perp as margin (perp USDC ~= total - target_spot_usdc)
1490
+ """
1491
+ if target_spot_usdc <= 0:
1492
+ return False, "Target spot USDC must be positive"
1493
+
1494
+ perp_margin, spot_usdc = await self._get_undeployed_capital()
1495
+ total_usdc = perp_margin + spot_usdc
1496
+ if total_usdc <= 0:
1497
+ return False, "No deployable USDC on Hyperliquid"
1498
+
1499
+ # Operate at cent precision to avoid dust churn.
1500
+ eps = 0.01
1501
+ target = float(
1502
+ Decimal(str(target_spot_usdc)).quantize(Decimal("0.01"), rounding=ROUND_UP)
1503
+ )
1504
+
1505
+ if target > total_usdc + eps:
1506
+ return (
1507
+ False,
1508
+ f"Target spot ${target:.2f} exceeds total deployable ${total_usdc:.2f}",
1509
+ )
1510
+
1511
+ delta = target - spot_usdc
1512
+ if abs(delta) < eps:
1513
+ return True, "Spot/perp USDC already balanced"
1514
+
1515
+ if delta > 0:
1516
+ # Need more spot USDC: move from perp to spot.
1517
+ amount = float(
1518
+ Decimal(str(min(delta, perp_margin))).quantize(
1519
+ Decimal("0.01"), rounding=ROUND_UP
1520
+ )
1521
+ )
1522
+ if amount < eps:
1523
+ return True, "No meaningful perp->spot transfer needed"
1524
+ success, result = await self.hyperliquid_adapter.transfer_perp_to_spot(
1525
+ amount=amount,
1526
+ address=address,
1527
+ )
1528
+ if not success:
1529
+ return False, f"Perp->spot transfer failed: {result}"
1530
+ return True, f"Transferred ${amount:.2f} perp->spot"
1531
+
1532
+ # Need more perp USDC: move from spot to perp.
1533
+ amount = float(
1534
+ Decimal(str(min(-delta, spot_usdc))).quantize(
1535
+ Decimal("0.01"), rounding=ROUND_DOWN
1536
+ )
1537
+ )
1538
+ if amount < eps:
1539
+ return True, "No meaningful spot->perp transfer needed"
1540
+ success, result = await self.hyperliquid_adapter.transfer_spot_to_perp(
1541
+ amount=amount,
1542
+ address=address,
1543
+ )
1544
+ if not success:
1545
+ return False, f"Spot->perp transfer failed: {result}"
1546
+ return True, f"Transferred ${amount:.2f} spot->perp"
1547
+
1491
1548
  async def _scale_up_position(self, additional_capital: float) -> StatusTuple:
1492
1549
  """
1493
1550
  Add capital to existing position without breaking it.
@@ -1553,18 +1610,14 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1553
1610
  f"(${order_usd:.2f}) at {leverage}x leverage"
1554
1611
  )
1555
1612
 
1556
- # Transfer USDC from perp to spot for the spot purchase
1557
- perp_margin, spot_usdc = await self._get_undeployed_capital()
1558
-
1559
- if perp_margin > 1.0 and spot_usdc < order_usd:
1560
- # Need to move some from perp margin to spot
1561
- transfer_amount = min(perp_margin, order_usd - spot_usdc)
1562
- success, result = await self.hyperliquid_adapter.transfer_perp_to_spot(
1563
- amount=transfer_amount,
1564
- address=address,
1565
- )
1566
- if not success:
1567
- self.logger.warning(f"Perp to spot transfer failed: {result}")
1613
+ # Ensure idle USDC is split correctly between spot and perp for this scale-up.
1614
+ # Target: spot has order_usd for the spot buy, perp holds the remainder as margin.
1615
+ split_ok, split_msg = await self._rebalance_usdc_between_perp_and_spot(
1616
+ target_spot_usdc=order_usd,
1617
+ address=address,
1618
+ )
1619
+ if not split_ok:
1620
+ self.logger.warning(f"USDC rebalance failed: {split_msg}")
1568
1621
 
1569
1622
  # Execute paired fill to add to both legs
1570
1623
  filler = PairedFiller(
@@ -1645,6 +1698,22 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1645
1698
  # Calculate deposited amount from current on-exchange value
1646
1699
  total_value, hl_value, _ = await self._get_total_portfolio_value()
1647
1700
 
1701
+ # ------------------------------------------------------------------ #
1702
+ # Emergency: Near-liquidation risk management #
1703
+ # ------------------------------------------------------------------ #
1704
+ near_liq, near_msg = await self._is_near_liquidation(state)
1705
+ if near_liq:
1706
+ self.logger.warning(f"Near liquidation on {coin}: {near_msg}")
1707
+ # Close both legs (sell spot, buy perp) and redeploy into a fresh position.
1708
+ # This bypasses rotation cooldown because it's an emergency safety action.
1709
+ close_success, close_msg = await self._close_position()
1710
+ if not close_success:
1711
+ return (
1712
+ False,
1713
+ f"Emergency rebalance failed - could not close: {close_msg}",
1714
+ )
1715
+ return await self._find_and_open_position()
1716
+
1648
1717
  # ------------------------------------------------------------------ #
1649
1718
  # Check 1: Rebalance needed? #
1650
1719
  # ------------------------------------------------------------------ #
@@ -1728,6 +1797,70 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1728
1797
  f"Position on {coin} healthy, age: {position_age_hours:.1f}h",
1729
1798
  )
1730
1799
 
1800
+ async def _is_near_liquidation(self, state: dict[str, Any]) -> tuple[bool, str]:
1801
+ """
1802
+ Check whether the perp leg is too close to liquidation.
1803
+
1804
+ For a short perp, liquidation is ABOVE entry. We measure progress from entry -> liquidation:
1805
+ frac = (mid - entry) / (liq - entry)
1806
+ """
1807
+ if self.current_position is None:
1808
+ return False, "No position"
1809
+
1810
+ coin = self.current_position.coin
1811
+
1812
+ perp_pos = None
1813
+ for pos_wrapper in state.get("assetPositions", []):
1814
+ pos = pos_wrapper.get("position", {})
1815
+ if pos.get("coin") == coin:
1816
+ perp_pos = pos
1817
+ break
1818
+
1819
+ if not perp_pos:
1820
+ return False, "No perp position found"
1821
+
1822
+ try:
1823
+ szi = float(perp_pos.get("szi", 0) or 0)
1824
+ except (TypeError, ValueError):
1825
+ szi = 0.0
1826
+
1827
+ # Only applies to short perps (basis trade is short perp).
1828
+ if szi >= 0:
1829
+ return False, "Perp is not short"
1830
+
1831
+ entry_px_raw = perp_pos.get("entryPx") or self.current_position.entry_price
1832
+ liq_px_raw = perp_pos.get("liquidationPx")
1833
+ try:
1834
+ entry_px = float(entry_px_raw or 0)
1835
+ liq_px = float(liq_px_raw or 0)
1836
+ except (TypeError, ValueError):
1837
+ return False, "Missing entry/liquidation price"
1838
+
1839
+ if entry_px <= 0 or liq_px <= 0 or liq_px <= entry_px:
1840
+ return False, "Invalid entry/liquidation prices"
1841
+
1842
+ success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
1843
+ if not success:
1844
+ return False, "Failed to fetch mid prices"
1845
+
1846
+ mid_px = float(mids.get(coin, 0.0) or 0.0)
1847
+ if mid_px <= 0:
1848
+ return False, "Missing mid price"
1849
+
1850
+ denom = liq_px - entry_px
1851
+ frac = (mid_px - entry_px) / denom if denom != 0 else 0.0
1852
+
1853
+ if frac >= self.LIQUIDATION_REBALANCE_THRESHOLD:
1854
+ return (
1855
+ True,
1856
+ f"mid=${mid_px:.4f} is {frac:.2%} of the way from entry=${entry_px:.4f} to liq=${liq_px:.4f}",
1857
+ )
1858
+
1859
+ return (
1860
+ False,
1861
+ f"mid=${mid_px:.4f} is {frac:.2%} of the way from entry=${entry_px:.4f} to liq=${liq_px:.4f}",
1862
+ )
1863
+
1731
1864
  async def _verify_leg_balance(self, state: dict[str, Any]) -> tuple[bool, str]:
1732
1865
  """
1733
1866
  Verify that spot and perp legs are balanced (delta neutral).
@@ -2040,7 +2173,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2040
2173
  """
2041
2174
  Check if current delta-neutral position needs rebalancing.
2042
2175
 
2043
- Implements 7 health checks from Django hyperliquid_adapter.py:
2176
+ Implements the following health checks:
2044
2177
  1. Missing positions
2045
2178
  2. Asset mismatch (if best specified)
2046
2179
  3. Funding accumulation threshold
@@ -2146,74 +2279,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2146
2279
 
2147
2280
  return 0.0
2148
2281
 
2149
- def _calculate_unused_usd(
2150
- self, state: dict[str, Any], deposited_amount: float
2151
- ) -> float:
2152
- """Calculate unused USD not deployed in positions."""
2153
- # Get account value
2154
- margin_summary = state.get("marginSummary", {})
2155
- account_value = float(margin_summary.get("accountValue", 0))
2156
-
2157
- # Get total position value
2158
- total_ntl = float(margin_summary.get("totalNtlPos", 0))
2159
-
2160
- # Unused = account value - position value
2161
- # For basis trading, we want most capital deployed
2162
- unused = account_value - abs(total_ntl)
2163
- return max(0.0, unused)
2164
-
2165
- async def _validate_stop_loss_orders(
2166
- self,
2167
- state: dict[str, Any],
2168
- perp_position: dict[str, Any],
2169
- ) -> tuple[bool, str]:
2170
- """Validate stop-loss orders exist and are below liquidation price."""
2171
- address = self._get_strategy_wallet_address()
2172
-
2173
- # Get liquidation price from perp position
2174
- liquidation_price = perp_position.get("liquidationPx")
2175
- if liquidation_price is None:
2176
- return False, "No liquidation price found"
2177
- liquidation_price = float(liquidation_price)
2178
-
2179
- # Get open orders
2180
- success, open_orders = await self.hyperliquid_adapter.get_open_orders(address)
2181
- if not success:
2182
- return False, "Failed to fetch open orders"
2183
-
2184
- perp_asset_id = perp_position.get("asset_id")
2185
-
2186
- # Find stop-loss orders for perp
2187
- perp_sl_order = None
2188
-
2189
- for order in open_orders:
2190
- order_type = order.get("orderType", "")
2191
- if "trigger" not in str(order_type).lower():
2192
- continue
2193
-
2194
- asset = order.get("coin") or order.get("asset")
2195
- coin_match = (
2196
- asset == self.current_position.coin if self.current_position else False
2197
- )
2198
-
2199
- if coin_match or order.get("asset_id") == perp_asset_id:
2200
- perp_sl_order = order
2201
- break
2202
-
2203
- # Validate perp stop-loss exists
2204
- if perp_sl_order is None:
2205
- return False, "Missing perp stop-loss order"
2206
-
2207
- # Validate price is below liquidation (for short, SL triggers on price RISE)
2208
- perp_sl_price = float(perp_sl_order.get("triggerPx", 0))
2209
- if perp_sl_price >= liquidation_price:
2210
- return (
2211
- False,
2212
- f"Perp stop-loss {perp_sl_price} >= liquidation {liquidation_price}",
2213
- )
2214
-
2215
- return True, "Stop-loss orders valid"
2216
-
2217
2282
  async def _place_stop_loss_orders(
2218
2283
  self,
2219
2284
  coin: str,
@@ -2503,7 +2568,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2503
2568
  try:
2504
2569
  strategy_address = self._get_strategy_wallet_address()
2505
2570
  success, balance = await self.balance_adapter.get_balance(
2506
- token_id=USDC_ARBITRUM_TOKEN_ID,
2571
+ query=USDC_ARBITRUM_TOKEN_ID,
2507
2572
  wallet_address=strategy_address,
2508
2573
  )
2509
2574
  if success and balance:
@@ -2515,132 +2580,9 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2515
2580
  return total_value, hl_value, strategy_wallet_value
2516
2581
 
2517
2582
  # ------------------------------------------------------------------ #
2518
- # Analysis Methods (ported from BasisTradingService) #
2583
+ # Analysis Methods #
2519
2584
  # ------------------------------------------------------------------ #
2520
2585
 
2521
- async def find_best_basis_trades(
2522
- self,
2523
- deposit_usdc: float,
2524
- lookback_days: int = 180,
2525
- confidence: float = 0.975,
2526
- fee_eps: float = 0.003,
2527
- oi_floor: float = 50.0,
2528
- day_vlm_floor: float = 1e5,
2529
- horizons_days: list[int] | None = None,
2530
- max_leverage: int = 3,
2531
- ) -> list[dict[str, Any]]:
2532
- """
2533
- Find optimal basis trading opportunities.
2534
-
2535
- Args:
2536
- deposit_usdc: Total deposit amount in USDC
2537
- lookback_days: Days of historical data to analyze
2538
- confidence: VaR confidence level (default 97.5%)
2539
- fee_eps: Fee buffer as fraction of notional
2540
- oi_floor: Minimum open interest threshold in USD
2541
- day_vlm_floor: Minimum daily volume threshold in USD
2542
- horizons_days: Time horizons for risk calculation
2543
- max_leverage: Maximum leverage allowed
2544
-
2545
- Returns:
2546
- List of basis trade opportunities sorted by expected APY
2547
- """
2548
- if horizons_days is None:
2549
- horizons_days = [1, 7]
2550
-
2551
- # Validate lookback doesn't exceed HL's 5000 candle limit
2552
- max_hours = 5000
2553
- max_days = max_hours // 24
2554
- if lookback_days > max_days:
2555
- self.logger.warning(
2556
- f"Lookback {lookback_days}d exceeds limit. Capping at {max_days}d"
2557
- )
2558
- lookback_days = max_days
2559
-
2560
- try:
2561
- # Get perpetual market data
2562
- (
2563
- success,
2564
- perps_ctx_pack,
2565
- ) = await self.hyperliquid_adapter.get_meta_and_asset_ctxs()
2566
- if not success:
2567
- raise ValueError(f"Failed to fetch perp metadata: {perps_ctx_pack}")
2568
-
2569
- perps_meta_list = perps_ctx_pack[0]["universe"]
2570
- perps_ctxs = perps_ctx_pack[1]
2571
-
2572
- coin_to_ctx: dict[str, Any] = {}
2573
- coin_to_maxlev: dict[str, int] = {}
2574
- coin_to_margin_table: dict[str, int | None] = {}
2575
- coins: list[str] = []
2576
-
2577
- for meta, ctx in zip(perps_meta_list, perps_ctxs, strict=False):
2578
- coin = meta["name"]
2579
- coin_to_ctx[coin] = ctx
2580
- coin_to_maxlev[coin] = int(meta.get("maxLeverage", 10))
2581
- coin_to_margin_table[coin] = meta.get("marginTableId")
2582
- coins.append(coin)
2583
-
2584
- perps_set = set(coins)
2585
-
2586
- # Get spot market data
2587
- success, spot_meta = await self.hyperliquid_adapter.get_spot_meta()
2588
- if not success:
2589
- raise ValueError(f"Failed to fetch spot metadata: {spot_meta}")
2590
-
2591
- tokens = spot_meta.get("tokens", [])
2592
- spot_pairs = spot_meta.get("universe", [])
2593
- idx_to_token = {t["index"]: t["name"] for t in tokens}
2594
-
2595
- # Find candidate basis pairs
2596
- candidates = self._find_basis_candidates(
2597
- spot_pairs, idx_to_token, perps_set
2598
- )
2599
- self.logger.info(f"Found {len(candidates)} spot-perp candidate pairs")
2600
-
2601
- # Get perp asset ID mapping
2602
- perp_coin_to_asset_id = {
2603
- k: v
2604
- for k, v in self.hyperliquid_adapter.coin_to_asset.items()
2605
- if v < 10000
2606
- }
2607
-
2608
- # Filter by liquidity
2609
- liquid = await self._filter_by_liquidity(
2610
- candidates=candidates,
2611
- coin_to_ctx=coin_to_ctx,
2612
- coin_to_maxlev=coin_to_maxlev,
2613
- coin_to_margin_table=coin_to_margin_table,
2614
- deposit_usdc=deposit_usdc,
2615
- max_leverage=max_leverage,
2616
- oi_floor=oi_floor,
2617
- day_vlm_floor=day_vlm_floor,
2618
- perp_coin_to_asset_id=perp_coin_to_asset_id,
2619
- )
2620
- self.logger.info(
2621
- f"After liquidity filter: {len(liquid)} candidates "
2622
- f"(OI >= ${oi_floor}, volume >= ${day_vlm_floor:,.0f})"
2623
- )
2624
-
2625
- # Analyze each candidate
2626
- results = await self._analyze_candidates(
2627
- liquid,
2628
- deposit_usdc,
2629
- lookback_days,
2630
- confidence,
2631
- fee_eps,
2632
- horizons_days,
2633
- )
2634
- self.logger.info(f"After historical analysis: {len(results)} opportunities")
2635
-
2636
- # Sort by expected APY
2637
- results.sort(key=self._get_safe_apy_key, reverse=True)
2638
- return results
2639
-
2640
- except Exception as e:
2641
- self.logger.error(f"Error finding basis trades: {e}")
2642
- raise
2643
-
2644
2586
  def _find_basis_candidates(
2645
2587
  self,
2646
2588
  spot_pairs: list[dict],
@@ -2775,249 +2717,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2775
2717
 
2776
2718
  return liquid
2777
2719
 
2778
- async def _analyze_candidates(
2779
- self,
2780
- candidates: list[BasisCandidate],
2781
- deposit_usdc: float,
2782
- lookback_days: int,
2783
- confidence: float,
2784
- fee_eps: float,
2785
- horizons_days: list[int],
2786
- ) -> list[dict[str, Any]]:
2787
- """Analyze each liquid candidate for basis trading metrics."""
2788
- ms_now = int(time.time() * 1000)
2789
- start_ms = ms_now - int(lookback_days * 24 * 3600 * 1000)
2790
- z = self._z_from_conf(confidence)
2791
-
2792
- results: list[dict[str, Any]] = []
2793
-
2794
- required_hours = lookback_days * 24
2795
- skipped_reasons: dict[str, list[str]] = {
2796
- "no_funding": [],
2797
- "no_candles": [],
2798
- "insufficient_funding": [],
2799
- "insufficient_candles": [],
2800
- }
2801
-
2802
- for candidate in candidates:
2803
- coin = candidate.coin
2804
- spot_sym = candidate.spot_pair
2805
-
2806
- # Fetch funding history with chunking for longer lookbacks
2807
- success, funding_data = await self._fetch_funding_history_chunked(
2808
- coin, start_ms, ms_now
2809
- )
2810
- if not success or not funding_data:
2811
- skipped_reasons["no_funding"].append(coin)
2812
- continue
2813
-
2814
- hourly_funding = [float(x.get("fundingRate", 0.0)) for x in funding_data]
2815
-
2816
- # Fetch candle data with chunking for longer lookbacks
2817
- success, candle_data = await self._fetch_candles_chunked(
2818
- coin, "1h", start_ms, ms_now
2819
- )
2820
- if not success or not candle_data:
2821
- skipped_reasons["no_candles"].append(coin)
2822
- continue
2823
-
2824
- closes = [float(c.get("c", 0)) for c in candle_data if c.get("c")]
2825
- highs = [float(c.get("h", 0)) for c in candle_data if c.get("h")]
2826
-
2827
- # Require at least 7 days of data minimum, or 50% of lookback for longer periods
2828
- min_required = max(7 * 24, required_hours // 2)
2829
- if len(hourly_funding) < min_required:
2830
- skipped_reasons["insufficient_funding"].append(
2831
- f"{coin}({len(hourly_funding)}/{min_required})"
2832
- )
2833
- continue
2834
- if len(closes) < min_required or len(highs) < min_required:
2835
- skipped_reasons["insufficient_candles"].append(
2836
- f"{coin}(closes={len(closes)},highs={len(highs)}/{min_required})"
2837
- )
2838
- continue
2839
-
2840
- # Calculate price volatility
2841
- sigma_hourly = (
2842
- pstdev(
2843
- [(closes[i] / closes[i - 1] - 1.0) for i in range(1, len(closes))]
2844
- )
2845
- if len(closes) > 1
2846
- else 0.005
2847
- )
2848
-
2849
- # Calculate funding statistics
2850
- funding_stats = self._calculate_funding_stats(hourly_funding)
2851
-
2852
- # Calculate safe leverages
2853
- max_lev = candidate.target_leverage
2854
- m_maint = self.maintenance_rate_from_max_leverage(max_lev)
2855
-
2856
- safe = self._calculate_safe_leverages(
2857
- hourly_funding=hourly_funding,
2858
- closes=closes,
2859
- highs=highs,
2860
- z=z,
2861
- m_maint=m_maint,
2862
- fee_eps=fee_eps,
2863
- max_lev=max_lev,
2864
- deposit_usdc=deposit_usdc,
2865
- horizons_days=horizons_days,
2866
- )
2867
-
2868
- result = {
2869
- "coin": coin,
2870
- "spot_pair": spot_sym,
2871
- "spot_asset_id": candidate.spot_asset_id,
2872
- "perp_asset_id": candidate.perp_asset_id,
2873
- "maxLeverage": max_lev,
2874
- "maintenance_rate_est": m_maint,
2875
- "openInterest": candidate.open_interest_base,
2876
- "day_notional_volume": candidate.day_notional_usd,
2877
- "funding_stats": funding_stats,
2878
- "price_stats": {
2879
- "sigma_hourly": sigma_hourly,
2880
- "z_for_confidence": z,
2881
- "confidence": confidence,
2882
- },
2883
- "safe": safe,
2884
- "depth_checks": candidate.depth_checks,
2885
- }
2886
-
2887
- results.append(result)
2888
-
2889
- # Log skip reasons
2890
- for reason, coins in skipped_reasons.items():
2891
- if coins:
2892
- self.logger.debug(
2893
- f"Skipped ({reason}): {', '.join(coins[:5])}{'...' if len(coins) > 5 else ''}"
2894
- )
2895
-
2896
- return results
2897
-
2898
- def _calculate_funding_stats(self, hourly_funding: list[float]) -> dict[str, Any]:
2899
- """Calculate comprehensive funding rate statistics."""
2900
- if not hourly_funding:
2901
- return {
2902
- "mean_hourly": 0.0,
2903
- "neg_hour_fraction": 0.0,
2904
- "hourly_vol": 0.0,
2905
- "worst_24h_sum": 0.0,
2906
- "worst_7d_sum": 0.0,
2907
- "points": 0,
2908
- }
2909
-
2910
- mean_hourly = mean(hourly_funding)
2911
- neg_hour_frac = sum(1 for r in hourly_funding if r < 0.0) / len(hourly_funding)
2912
- hourly_vol = pstdev(hourly_funding) if len(hourly_funding) > 1 else 0.0
2913
- worst_24h = self._rolling_min_sum(hourly_funding, 24)
2914
- worst_7d = self._rolling_min_sum(hourly_funding, 24 * 7)
2915
-
2916
- return {
2917
- "mean_hourly": mean_hourly,
2918
- "neg_hour_fraction": neg_hour_frac,
2919
- "hourly_vol": hourly_vol,
2920
- "worst_24h_sum": worst_24h,
2921
- "worst_7d_sum": worst_7d,
2922
- "points": len(hourly_funding),
2923
- }
2924
-
2925
- def _calculate_safe_leverages(
2926
- self,
2927
- hourly_funding: list[float],
2928
- closes: list[float],
2929
- highs: list[float],
2930
- z: float,
2931
- m_maint: float,
2932
- fee_eps: float,
2933
- max_lev: int,
2934
- deposit_usdc: float,
2935
- horizons_days: list[int],
2936
- ) -> dict[str, Any]:
2937
- """Calculate safe leverage for each time horizon."""
2938
- results: dict[str, Any] = {}
2939
-
2940
- for horizon in horizons_days:
2941
- window = horizon * 24
2942
- b_star = self._worst_buffer_requirement(
2943
- closes, highs, hourly_funding, window, m_maint, fee_eps
2944
- )
2945
-
2946
- if b_star >= 1.0:
2947
- results[f"{horizon}d"] = {
2948
- "pass": False,
2949
- "leverage": 0,
2950
- "reason": "Buffer requirement exceeds 100%",
2951
- }
2952
- continue
2953
-
2954
- safe_lev = min(max_lev, int(1.0 / b_star)) if b_star > 0 else max_lev
2955
-
2956
- # Calculate expected APY
2957
- mean_funding = mean(hourly_funding) if hourly_funding else 0
2958
- expected_apy = mean_funding * 24 * 365 * safe_lev
2959
-
2960
- # Estimate quantities
2961
- order_usd = deposit_usdc * (safe_lev / (safe_lev + 1))
2962
- avg_price = mean(closes) if closes else 1.0
2963
- qty = order_usd / avg_price if avg_price > 0 else 0
2964
-
2965
- results[f"{horizon}d"] = {
2966
- "pass": True,
2967
- "leverage": safe_lev,
2968
- "buffer_requirement": b_star,
2969
- "expected_apy_pct": expected_apy,
2970
- "spot_qty": qty,
2971
- "perp_qty": qty,
2972
- "order_usd": order_usd,
2973
- }
2974
-
2975
- return results
2976
-
2977
- def _worst_buffer_requirement(
2978
- self,
2979
- closes: list[float],
2980
- highs: list[float],
2981
- hourly_funding: list[float],
2982
- window: int,
2983
- mmr: float,
2984
- fee_eps: float,
2985
- ) -> float:
2986
- """
2987
- Calculate worst-case buffer requirement over rolling windows.
2988
-
2989
- Uses deterministic historical "stress test" approach.
2990
- """
2991
- n = min(len(closes), len(highs), len(hourly_funding))
2992
- if n < window or n == 0:
2993
- return 1.0
2994
-
2995
- worst_req = 0.0
2996
-
2997
- for start in range(n - window + 1):
2998
- end = start + window
2999
- entry_price = closes[start]
3000
- if entry_price <= 0:
3001
- continue
3002
-
3003
- cum_f = 0.0
3004
- runup = 0.0
3005
-
3006
- for i in range(start, end):
3007
- peak = highs[i]
3008
- step_runup = (peak / entry_price - 1.0) if entry_price > 0 else 0.0
3009
- runup = max(runup, step_runup)
3010
-
3011
- r = hourly_funding[i] if i < len(hourly_funding) else 0.0
3012
- if r < 0.0:
3013
- cum_f += (-r) * (1.0 + runup)
3014
-
3015
- req = mmr * (1.0 + runup) + runup + cum_f + fee_eps
3016
- if req > worst_req:
3017
- worst_req = req
3018
-
3019
- return worst_req
3020
-
3021
2720
  # ------------------------------------------------------------------ #
3022
2721
  # Chunked Data Fetching #
3023
2722
  # ------------------------------------------------------------------ #
@@ -3177,7 +2876,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3177
2876
  return True, all_candles
3178
2877
 
3179
2878
  # ------------------------------------------------------------------ #
3180
- # Net APY Solver + Bootstrap (ported from Django NetApyBasisTradingService)
2879
+ # Net APY Solver + Bootstrap #
3181
2880
  # ------------------------------------------------------------------ #
3182
2881
 
3183
2882
  def _spot_index_from_asset_id(self, spot_asset_id: int) -> int:
@@ -3904,16 +3603,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3904
3603
 
3905
3604
  return worst_req if worst_req > 0 else float(fallback_mmr + fee_eps)
3906
3605
 
3907
- def get_sz_decimals_for_hypecore_asset(self, asset_id: int) -> int:
3908
- try:
3909
- mapping = self.hyperliquid_adapter.asset_to_sz_decimals
3910
- except Exception as exc: # noqa: BLE001
3911
- raise ValueError("Hyperliquid asset_to_sz_decimals not available") from exc
3912
-
3913
- if not isinstance(mapping, dict):
3914
- raise ValueError(f"Unknown asset_id {asset_id}: missing szDecimals")
3915
- return hl_sz_decimals_for_asset(mapping, asset_id)
3916
-
3917
3606
  def _size_step(self, asset_id: int) -> Decimal:
3918
3607
  try:
3919
3608
  mapping = self.hyperliquid_adapter.asset_to_sz_decimals
@@ -4441,15 +4130,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
4441
4130
  return 0.5
4442
4131
  return 0.5 / max_lev
4443
4132
 
4444
- @staticmethod
4445
- def _get_safe_apy_key(result: dict[str, Any]) -> float:
4446
- """Sort key for results by 7d expected APY."""
4447
- safe = result.get("safe", {})
4448
- safe_7d = safe.get("7d", {})
4449
- if not safe_7d.get("pass", False):
4450
- return -999.0
4451
- return safe_7d.get("expected_apy_pct", 0.0)
4452
-
4453
4133
  def _get_strategy_wallet_address(self) -> str:
4454
4134
  """Get strategy wallet address from config."""
4455
4135
  strategy_wallet = self.config.get("strategy_wallet")