wayfinder-paths 0.1.19__py3-none-any.whl → 0.1.20__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 (98) hide show
  1. wayfinder_paths/__init__.py +0 -2
  2. wayfinder_paths/adapters/balance_adapter/README.md +59 -45
  3. wayfinder_paths/adapters/balance_adapter/adapter.py +0 -21
  4. wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -14
  5. wayfinder_paths/adapters/brap_adapter/README.md +61 -184
  6. wayfinder_paths/adapters/brap_adapter/__init__.py +0 -4
  7. wayfinder_paths/adapters/brap_adapter/adapter.py +0 -147
  8. wayfinder_paths/adapters/brap_adapter/test_adapter.py +0 -15
  9. wayfinder_paths/adapters/hyperlend_adapter/__init__.py +0 -4
  10. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +0 -9
  11. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +0 -17
  12. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +3 -312
  13. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +1 -71
  14. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +0 -57
  15. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +0 -17
  16. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +2 -42
  17. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +1 -9
  18. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +15 -47
  19. wayfinder_paths/adapters/hyperliquid_adapter/utils.py +0 -7
  20. wayfinder_paths/adapters/ledger_adapter/README.md +54 -74
  21. wayfinder_paths/adapters/ledger_adapter/__init__.py +0 -4
  22. wayfinder_paths/adapters/ledger_adapter/adapter.py +0 -106
  23. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +0 -12
  24. wayfinder_paths/adapters/moonwell_adapter/README.md +67 -106
  25. wayfinder_paths/adapters/moonwell_adapter/__init__.py +0 -4
  26. wayfinder_paths/adapters/moonwell_adapter/adapter.py +9 -121
  27. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +84 -83
  28. wayfinder_paths/adapters/pool_adapter/README.md +30 -51
  29. wayfinder_paths/adapters/pool_adapter/__init__.py +0 -4
  30. wayfinder_paths/adapters/pool_adapter/adapter.py +0 -19
  31. wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -8
  32. wayfinder_paths/adapters/token_adapter/README.md +41 -49
  33. wayfinder_paths/adapters/token_adapter/adapter.py +0 -32
  34. wayfinder_paths/adapters/token_adapter/test_adapter.py +1 -12
  35. wayfinder_paths/conftest.py +0 -8
  36. wayfinder_paths/core/__init__.py +0 -2
  37. wayfinder_paths/core/adapters/BaseAdapter.py +0 -22
  38. wayfinder_paths/core/adapters/__init__.py +0 -5
  39. wayfinder_paths/core/adapters/models.py +0 -5
  40. wayfinder_paths/core/analytics/__init__.py +0 -2
  41. wayfinder_paths/core/analytics/bootstrap.py +0 -16
  42. wayfinder_paths/core/analytics/stats.py +0 -7
  43. wayfinder_paths/core/analytics/test_analytics.py +5 -34
  44. wayfinder_paths/core/clients/BRAPClient.py +0 -35
  45. wayfinder_paths/core/clients/ClientManager.py +0 -51
  46. wayfinder_paths/core/clients/HyperlendClient.py +0 -77
  47. wayfinder_paths/core/clients/LedgerClient.py +2 -122
  48. wayfinder_paths/core/clients/PoolClient.py +0 -2
  49. wayfinder_paths/core/clients/TokenClient.py +0 -39
  50. wayfinder_paths/core/clients/WalletClient.py +0 -15
  51. wayfinder_paths/core/clients/WayfinderClient.py +0 -24
  52. wayfinder_paths/core/clients/__init__.py +0 -4
  53. wayfinder_paths/core/clients/protocols.py +25 -98
  54. wayfinder_paths/core/config.py +0 -24
  55. wayfinder_paths/core/constants/__init__.py +0 -7
  56. wayfinder_paths/core/constants/base.py +2 -9
  57. wayfinder_paths/core/constants/erc20_abi.py +0 -5
  58. wayfinder_paths/core/constants/hyperlend_abi.py +0 -7
  59. wayfinder_paths/core/constants/moonwell_abi.py +0 -35
  60. wayfinder_paths/core/engine/StrategyJob.py +0 -32
  61. wayfinder_paths/core/strategies/Strategy.py +0 -99
  62. wayfinder_paths/core/strategies/__init__.py +0 -2
  63. wayfinder_paths/core/utils/__init__.py +0 -1
  64. wayfinder_paths/core/utils/erc20_service.py +0 -1
  65. wayfinder_paths/core/utils/evm_helpers.py +0 -50
  66. wayfinder_paths/core/utils/transaction.py +0 -1
  67. wayfinder_paths/run_strategy.py +0 -46
  68. wayfinder_paths/scripts/create_strategy.py +0 -17
  69. wayfinder_paths/scripts/make_wallets.py +1 -4
  70. wayfinder_paths/strategies/basis_trading_strategy/README.md +71 -163
  71. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +0 -24
  72. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +36 -400
  73. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +15 -64
  74. wayfinder_paths/strategies/basis_trading_strategy/types.py +0 -4
  75. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +65 -56
  76. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +4 -27
  77. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -10
  78. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +71 -72
  79. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +23 -227
  80. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +120 -113
  81. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +64 -59
  82. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +4 -44
  83. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +2 -35
  84. wayfinder_paths/templates/adapter/README.md +107 -46
  85. wayfinder_paths/templates/adapter/adapter.py +0 -9
  86. wayfinder_paths/templates/adapter/test_adapter.py +0 -19
  87. wayfinder_paths/templates/strategy/README.md +113 -59
  88. wayfinder_paths/templates/strategy/strategy.py +0 -22
  89. wayfinder_paths/templates/strategy/test_strategy.py +0 -28
  90. wayfinder_paths/tests/test_test_coverage.py +2 -12
  91. wayfinder_paths/tests/test_utils.py +1 -31
  92. wayfinder_paths-0.1.20.dist-info/METADATA +355 -0
  93. wayfinder_paths-0.1.20.dist-info/RECORD +129 -0
  94. wayfinder_paths/core/adapters/base.py +0 -5
  95. wayfinder_paths-0.1.19.dist-info/METADATA +0 -592
  96. wayfinder_paths-0.1.19.dist-info/RECORD +0 -130
  97. {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.20.dist-info}/LICENSE +0 -0
  98. {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.20.dist-info}/WHEEL +0 -0
@@ -1,10 +1,3 @@
1
- """
2
- BasisTradingStrategy - Delta-neutral basis trading on Hyperliquid.
3
-
4
- Identifies and executes basis trading opportunities by pairing spot long
5
- positions with perpetual short positions to capture funding rate payments.
6
- """
7
-
8
1
  from __future__ import annotations
9
2
 
10
3
  import asyncio
@@ -81,34 +74,24 @@ from wayfinder_paths.strategies.basis_trading_strategy.types import (
81
74
  BasisPosition,
82
75
  )
83
76
 
84
- # Set decimal precision for precise price/size calculations
85
77
  getcontext().prec = 28
86
78
 
87
79
 
88
80
  def _d(x: float | Decimal | str) -> Decimal:
89
- """Convert to Decimal for precise calculations."""
90
81
  return x if isinstance(x, Decimal) else Decimal(str(x))
91
82
 
92
83
 
93
84
  class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
94
- """
95
- Delta-neutral basis trading strategy on Hyperliquid.
96
-
97
- Captures funding rate payments by maintaining offsetting spot long and
98
- perpetual short positions. Uses historical funding rate and volatility
99
- analysis to select optimal opportunities.
100
- """
101
-
102
85
  name = "Basis Trading Strategy"
103
86
 
104
87
  # Strategy parameters
105
88
  MIN_DEPOSIT_USDC = 25
106
- DEFAULT_LOOKBACK_DAYS = 30 # Supports up to ~208 days via chunked API calls
107
- DEFAULT_FEE_EPS = 0.003 # 0.3% fee buffer
108
- DEFAULT_OI_FLOOR = 100_000.0 # Min OI in USD
109
- DEFAULT_DAY_VLM_FLOOR = 100_000 # Min daily volume
89
+ DEFAULT_LOOKBACK_DAYS = 30
90
+ DEFAULT_FEE_EPS = 0.003
91
+ DEFAULT_OI_FLOOR = 100_000.0
92
+ DEFAULT_DAY_VLM_FLOOR = 100_000
110
93
  DEFAULT_MAX_LEVERAGE = 2
111
- GAS_MAXIMUM = 0.01 # ETH
94
+ GAS_MAXIMUM = 0.01
112
95
  DEFAULT_BOOTSTRAP_SIMS = 50
113
96
  DEFAULT_BOOTSTRAP_BLOCK_HOURS = 48
114
97
 
@@ -117,19 +100,18 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
117
100
  LIQUIDATION_REBALANCE_THRESHOLD = 0.75
118
101
  # Stop-loss at 90% to liquidation (closer)
119
102
  LIQUIDATION_STOP_LOSS_THRESHOLD = 0.90
120
- FUNDING_REBALANCE_THRESHOLD = 0.02 # Rebalance when funding hits 2% gains
103
+ FUNDING_REBALANCE_THRESHOLD = 0.02
121
104
 
122
105
  # Position tolerances
123
- SPOT_POSITION_DUST_TOLERANCE = 0.04 # ±4% size drift allowed
124
- MIN_UNUSED_USD = 5.0 # Minimum idle USD threshold
125
- UNUSED_REL_EPS = 0.01 # 1% of bankroll idle threshold
106
+ SPOT_POSITION_DUST_TOLERANCE = 0.04
107
+ MIN_UNUSED_USD = 5.0
108
+ UNUSED_REL_EPS = 0.01
126
109
 
127
110
  # Rotation cooldown
128
- ROTATION_MIN_INTERVAL_DAYS = 14 # 14 days between rotations
111
+ ROTATION_MIN_INTERVAL_DAYS = 14
129
112
 
130
- # Builder fee for Hyperliquid trades
131
113
  HYPE_FEE_WALLET: str = "0xaA1D89f333857eD78F8434CC4f896A9293EFE65c"
132
- HYPE_PRO_FEE: int = 30 # in tenths of basis points (0.03% = 3 bps)
114
+ HYPE_PRO_FEE: int = 30
133
115
  DEFAULT_BUILDER_FEE: dict[str, Any] = {"b": HYPE_FEE_WALLET, "f": HYPE_PRO_FEE}
134
116
 
135
117
  INFO = StratDescriptor(
@@ -223,8 +205,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
223
205
  self.current_position: BasisPosition | None = None
224
206
  self.deposit_amount: float = 0.0
225
207
 
226
- # Builder fee for Hyperliquid trades (from config or default)
227
- # Format: {"b": "0x...", "f": 10} where 'b' is address, 'f' is fee in bps
228
208
  self.builder_fee: dict[str, Any] | None = self.config.get(
229
209
  "builder_fee", self.DEFAULT_BUILDER_FEE
230
210
  )
@@ -243,7 +223,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
243
223
  "strategy": self.config,
244
224
  }
245
225
 
246
- # Create Hyperliquid executor if not provided.
247
226
  # This is only required for placing/canceling orders (not market reads).
248
227
  hl_executor = hyperliquid_executor
249
228
  if hl_executor is None:
@@ -291,13 +270,11 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
291
270
  self.register_adapters(adapters)
292
271
 
293
272
  async def setup(self) -> None:
294
- """Initialize strategy state from chain/ledger and discover existing positions."""
295
273
  self.logger.info("Starting BasisTradingStrategy setup")
296
274
  start_time = time.time()
297
275
 
298
276
  await super().setup()
299
277
 
300
- # Get net deposit from ledger
301
278
  try:
302
279
  success, deposit_data = await self.ledger_adapter.get_strategy_net_deposit(
303
280
  wallet_address=self._get_strategy_wallet_address()
@@ -317,15 +294,8 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
317
294
  self.logger.info(f"BasisTradingStrategy setup completed in {elapsed:.2f}s")
318
295
 
319
296
  async def _discover_existing_position(self) -> None:
320
- """
321
- Discover existing delta-neutral position from Hyperliquid state.
322
-
323
- This is critical for restart recovery - we must not open new positions
324
- if one already exists on-chain.
325
- """
326
297
  address = self._get_strategy_wallet_address()
327
298
 
328
- # Get perp positions
329
299
  success, user_state = await self.hyperliquid_adapter.get_user_state(address)
330
300
  if not success:
331
301
  self.logger.warning("Could not fetch user state for position discovery")
@@ -341,7 +311,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
341
311
  for pos_wrapper in asset_positions:
342
312
  pos = pos_wrapper.get("position", {})
343
313
  szi = float(pos.get("szi", 0))
344
- if szi < 0: # Short position
314
+ if szi < 0:
345
315
  perp_position = pos
346
316
  break
347
317
 
@@ -353,7 +323,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
353
323
  perp_size = abs(float(perp_position.get("szi", 0)))
354
324
  entry_px = float(perp_position.get("entryPx", 0))
355
325
 
356
- # Get spot positions to find matching spot leg
357
326
  success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
358
327
  address
359
328
  )
@@ -389,7 +358,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
389
358
  else:
390
359
  spot_size = float(spot_position.get("total", 0))
391
360
 
392
- # Get asset IDs
393
361
  perp_asset_id = self.hyperliquid_adapter.coin_to_asset.get(coin)
394
362
  # Spot asset ID: look up from spot meta or estimate
395
363
  spot_asset_id = None
@@ -401,7 +369,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
401
369
  base_idx = pair["tokens"][0]
402
370
  for t in tokens:
403
371
  if t["index"] == base_idx:
404
- # Check if this token matches our coin
405
372
  if (
406
373
  t["name"] == coin
407
374
  or t["name"] == f"U{coin}"
@@ -420,14 +387,13 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
420
387
  spot_amount=spot_size,
421
388
  perp_amount=perp_size,
422
389
  entry_price=entry_px,
423
- leverage=2, # Default, actual leverage can be inferred
424
- entry_timestamp=int(time.time() * 1000), # Approximate
390
+ leverage=2,
391
+ entry_timestamp=int(time.time() * 1000),
425
392
  funding_collected=abs(
426
393
  float(perp_position.get("cumFunding", {}).get("sinceOpen", 0))
427
394
  ),
428
395
  )
429
396
 
430
- # Update deposit amount from actual account value if not set
431
397
  if self.deposit_amount <= 0:
432
398
  margin_summary = user_state.get("marginSummary", {})
433
399
  self.deposit_amount = float(margin_summary.get("accountValue", 0))
@@ -442,19 +408,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
442
408
  main_token_amount: float = 0.0,
443
409
  gas_token_amount: float = 0.0,
444
410
  ) -> StatusTuple:
445
- """
446
- Deposit USDC to Hyperliquid L1 for basis trading.
447
-
448
- Sends USDC from the strategy wallet to the Hyperliquid bridge address on Arbitrum,
449
- then waits for it to be credited on Hyperliquid L1.
450
-
451
- Args:
452
- main_token_amount: Amount of USDC to deposit
453
- gas_token_amount: Amount of ETH for gas (unused, kept for interface compatibility)
454
-
455
- Returns:
456
- StatusTuple (success, message)
457
- """
458
411
  if main_token_amount < self.MIN_DEPOSIT_USDC:
459
412
  return (False, f"Minimum deposit is {self.MIN_DEPOSIT_USDC} USDC")
460
413
 
@@ -475,7 +428,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
475
428
  gas_ok,
476
429
  gas_res,
477
430
  ) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
478
- token_id="ethereum-arbitrum", # Native ETH on Arbitrum
431
+ token_id="ethereum-arbitrum",
479
432
  amount=gas_token_amount,
480
433
  strategy_name=self.name or "basis_trading_strategy",
481
434
  )
@@ -489,7 +442,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
489
442
  main_address = self._get_main_wallet_address()
490
443
  strategy_address = self._get_strategy_wallet_address()
491
444
 
492
- # Check if strategy wallet already has sufficient USDC
493
445
  (
494
446
  strategy_balance_ok,
495
447
  strategy_balance,
@@ -544,24 +496,9 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
544
496
  return (False, f"Deposit failed: {e}")
545
497
 
546
498
  async def update(self) -> StatusTuple:
547
- """
548
- Analyze markets and manage positions.
549
-
550
- - If no position exists, analyzes opportunities and opens the best one.
551
- - If position exists, monitors and maintains it:
552
- - Checks for rebalance conditions
553
- - Verifies leg balance (spot == perp)
554
- - Deploys any idle capital
555
- - Ensures stop-loss orders are valid
556
-
557
- Returns:
558
- StatusTuple (success, message)
559
- """
560
- # Check actual balances instead of relying on in-memory deposit_amount
561
499
  strategy_address = self._get_strategy_wallet_address()
562
500
  strategy_wallet = self.config.get("strategy_wallet")
563
501
 
564
- # Check strategy wallet USDC balance on Arbitrum
565
502
  strategy_usdc = 0.0
566
503
  try:
567
504
  (
@@ -576,7 +513,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
576
513
  except Exception as e:
577
514
  self.logger.warning(f"Could not check strategy wallet balance: {e}")
578
515
 
579
- # Check Hyperliquid USDC balance (spot + perp)
580
516
  hl_usdc = 0.0
581
517
  try:
582
518
  perp_margin, spot_usdc = await self._get_undeployed_capital()
@@ -584,7 +520,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
584
520
  except Exception as e:
585
521
  self.logger.warning(f"Could not check Hyperliquid balance: {e}")
586
522
 
587
- # Update deposit_amount from actual balances
588
523
  total_available = strategy_usdc + hl_usdc
589
524
  if total_available > 1.0:
590
525
  self.deposit_amount = max(self.deposit_amount, total_available)
@@ -599,7 +534,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
599
534
  f"Found ${strategy_usdc:.2f} USDC in strategy wallet, bridging to Hyperliquid"
600
535
  )
601
536
 
602
- # Send USDC to bridge address (internal operation, not a deposit event)
603
537
  success, result = await self.balance_adapter.send_to_address(
604
538
  token_id=USDC_ARBITRUM_TOKEN_ID,
605
539
  amount=strategy_usdc,
@@ -622,7 +556,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
622
556
  ) = await self.hyperliquid_adapter.wait_for_deposit(
623
557
  address=strategy_address,
624
558
  expected_increase=strategy_usdc,
625
- timeout_s=180, # 3 minutes
559
+ timeout_s=180,
626
560
  poll_interval_s=10,
627
561
  )
628
562
 
@@ -654,19 +588,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
654
588
  async def analyze(
655
589
  self, deposit_usdc: float = 1000.0, verbose: bool = True
656
590
  ) -> dict[str, Any]:
657
- """
658
- Analyze basis trading opportunities without executing.
659
-
660
- Uses the Net-APY + stop-churn backtest solver with block-bootstrap
661
- resampling.
662
-
663
- Args:
664
- deposit_usdc: Hypothetical deposit amount for sizing calculations (default $1000)
665
- verbose: Include debug info about filtering
666
-
667
- Returns:
668
- Dict with opportunities sorted by net APY (includes bootstrap metrics)
669
- """
670
591
  self.logger.info(
671
592
  f"Analyzing basis opportunities for ${deposit_usdc} deposit..."
672
593
  )
@@ -718,7 +639,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
718
639
  "Falling back to live analysis."
719
640
  )
720
641
 
721
- # Get market data for debug info
722
642
  (
723
643
  success,
724
644
  perps_ctx_pack,
@@ -793,12 +713,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
793
713
  }
794
714
 
795
715
  def _cfg_get(self, key: str, default: Any | None = None) -> Any:
796
- """
797
- Read a strategy config value.
798
-
799
- Supports both flat configs (common in this repo) and nested configs
800
- where strategy settings live under a "strategy" key.
801
- """
802
716
  if key in self.config:
803
717
  return self.config.get(key, default)
804
718
  nested = self.config.get("strategy")
@@ -807,11 +721,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
807
721
  return default
808
722
 
809
723
  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
724
  # Direct match
816
725
  if coin in mid_prices:
817
726
  return mid_prices[coin]
@@ -828,7 +737,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
828
737
  if key in mid_prices:
829
738
  return mid_prices[key]
830
739
 
831
- # Add U-prefix (XPL -> UXPL)
832
740
  prefixed = f"U{coin}"
833
741
  for key in [prefixed, prefixed.upper(), prefixed.lower()]:
834
742
  if key in mid_prices:
@@ -837,7 +745,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
837
745
  return 0.0
838
746
 
839
747
  def _coins_match(self, coin1: str, coin2: str) -> bool:
840
- """Check if two coin names match, handling U-prefix (UXPL == XPL)."""
841
748
  if coin1 == coin2:
842
749
  return True
843
750
  # Strip U-prefix from either and compare
@@ -846,25 +753,9 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
846
753
  return c1 == c2
847
754
 
848
755
  async def withdraw(self, amount: float | None = None) -> StatusTuple:
849
- """
850
- Close all positions and liquidate to strategy wallet.
851
-
852
- Handles funds in:
853
- 1. Strategy wallet on Arbitrum (USDC)
854
- 2. Hyperliquid L1 (positions + margin)
855
-
856
- Does NOT transfer to main wallet - call exit() for that.
857
-
858
- Args:
859
- amount: Amount to withdraw (None = all)
860
-
861
- Returns:
862
- StatusTuple (success, message)
863
- """
864
756
  address = self._get_strategy_wallet_address()
865
757
  usdc_token_id = "usd-coin-arbitrum"
866
758
 
867
- # Check for USDC already in strategy wallet on Arbitrum
868
759
  strategy_usdc = 0.0
869
760
  try:
870
761
  success, balance_data = await self.balance_adapter.get_balance(
@@ -872,11 +763,10 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
872
763
  wallet_address=address,
873
764
  )
874
765
  if success:
875
- strategy_usdc = float(balance_data) / 1e6 # USDC has 6 decimals
766
+ strategy_usdc = float(balance_data) / 1e6
876
767
  except Exception as e:
877
768
  self.logger.warning(f"Could not get strategy wallet balance: {e}")
878
769
 
879
- # Get current Hyperliquid value (perp + spot)
880
770
  hl_perp_value = 0.0
881
771
  hl_spot_usdc = 0.0
882
772
  success, user_state = await self.hyperliquid_adapter.get_user_state(address)
@@ -896,7 +786,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
896
786
 
897
787
  hl_value = hl_perp_value + hl_spot_usdc
898
788
 
899
- # Check if there's anything to withdraw
900
789
  if strategy_usdc < 1.0 and hl_value < 1.0 and self.current_position is None:
901
790
  return (False, "Nothing to withdraw")
902
791
 
@@ -937,7 +826,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
937
826
  # Floor to 2 decimal places to avoid precision issues
938
827
  spot_usdc = math.floor(available * 100) / 100
939
828
 
940
- if spot_usdc > 1.0: # Only transfer if meaningful amount
829
+ if spot_usdc > 1.0:
941
830
  self.logger.info(
942
831
  f"Transferring ${spot_usdc:.2f} from spot to perp "
943
832
  f"(available={available:.8f}, floored={spot_usdc:.2f})"
@@ -1009,7 +898,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1009
898
  ) = await self.hyperliquid_adapter.wait_for_withdrawal(
1010
899
  address=address,
1011
900
  lookback_s=5,
1012
- max_poll_time_s=20 * 60, # 20 minutes max
901
+ max_poll_time_s=20 * 60,
1013
902
  poll_interval_s=10,
1014
903
  )
1015
904
 
@@ -1020,7 +909,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1020
909
  "Check Hyperliquid explorer for status.",
1021
910
  )
1022
911
 
1023
- # Get the withdrawal amount from the most recent tx
1024
912
  tx_hash = list(withdrawals.keys())[-1]
1025
913
  withdrawn_amount = withdrawals[tx_hash]
1026
914
  self.logger.info(
@@ -1030,7 +918,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1030
918
  # Step 5: Wait a bit for the USDC to be credited on Arbitrum
1031
919
  await asyncio.sleep(10)
1032
920
 
1033
- # Get final USDC balance in strategy wallet
1034
921
  final_balance = 0.0
1035
922
  try:
1036
923
  success, balance_data = await self.balance_adapter.get_balance(
@@ -1038,7 +925,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1038
925
  wallet_address=address,
1039
926
  )
1040
927
  if success:
1041
- final_balance = float(balance_data) / 1e6 # USDC has 6 decimals
928
+ final_balance = float(balance_data) / 1e6
1042
929
  except Exception as e:
1043
930
  self.logger.warning(f"Could not get final balance: {e}")
1044
931
 
@@ -1052,7 +939,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1052
939
  )
1053
940
 
1054
941
  async def exit(self, **kwargs) -> StatusTuple:
1055
- """Transfer funds from strategy wallet to main wallet."""
1056
942
  self.logger.info("EXIT: Transferring remaining funds to main wallet")
1057
943
 
1058
944
  strategy_address = self._get_strategy_wallet_address()
@@ -1119,7 +1005,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1119
1005
  return (True, f"Transferred to main wallet: {', '.join(transferred_items)}")
1120
1006
 
1121
1007
  async def _status(self) -> StatusDict:
1122
- """Return portfolio value and strategy status with live data."""
1123
1008
  total_value, hl_value, vault_value = await self._get_total_portfolio_value()
1124
1009
 
1125
1010
  status_payload: dict[str, Any] = {
@@ -1140,7 +1025,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1140
1025
  }
1141
1026
  )
1142
1027
 
1143
- # Get net deposit from ledger
1144
1028
  try:
1145
1029
  success, deposit_data = await self.ledger_adapter.get_strategy_net_deposit(
1146
1030
  wallet_address=self._get_strategy_wallet_address()
@@ -1163,20 +1047,10 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1163
1047
 
1164
1048
  @staticmethod
1165
1049
  async def policies() -> list[str]:
1166
- """Return wallet permission policies."""
1167
1050
  # Placeholder - would include Hyperliquid-specific policies
1168
1051
  return []
1169
1052
 
1170
1053
  async def ensure_builder_fee_approved(self) -> StatusTuple:
1171
- """
1172
- Ensure the builder fee is approved before trading.
1173
-
1174
- Checks the current max builder fee approval for the user/builder pair.
1175
- If the current approval is less than required, submits an approval transaction.
1176
-
1177
- Returns:
1178
- StatusTuple (success, message)
1179
- """
1180
1054
  if not self.builder_fee:
1181
1055
  return True, "No builder fee configured"
1182
1056
 
@@ -1188,7 +1062,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1188
1062
  return True, "Builder fee not required"
1189
1063
 
1190
1064
  try:
1191
- # Check current approval
1192
1065
  success, current_fee = await self.hyperliquid_adapter.get_max_builder_fee(
1193
1066
  user=address,
1194
1067
  builder=builder,
@@ -1208,7 +1081,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1208
1081
  return True, f"Builder fee already approved: {current_fee}"
1209
1082
 
1210
1083
  # Need to approve
1211
- # Convert fee to percentage string (e.g., 30 tenths bp = 0.030%)
1212
1084
  max_fee_rate = f"{required_fee / 1000:.3f}%"
1213
1085
  self.logger.info(
1214
1086
  f"Approving builder fee: builder={builder}, rate={max_fee_rate}"
@@ -1236,7 +1108,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1236
1108
  # ------------------------------------------------------------------ #
1237
1109
 
1238
1110
  async def _find_and_open_position(self) -> StatusTuple:
1239
- """Analyze markets and open the best basis position."""
1240
1111
  self.logger.info("Analyzing basis trading opportunities...")
1241
1112
 
1242
1113
  try:
@@ -1430,7 +1301,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1430
1301
  f"notional=${spot_notional:.2f}/${perp_notional:.2f}"
1431
1302
  )
1432
1303
 
1433
- # Get entry price from current mid
1434
1304
  success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
1435
1305
  entry_price = self._resolve_mid_price(coin, mids) if success else 0.0
1436
1306
 
@@ -1459,7 +1329,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1459
1329
  else:
1460
1330
  self.logger.warning("Could not get liquidation price for stop-loss")
1461
1331
 
1462
- # Create position record
1463
1332
  self.current_position = BasisPosition(
1464
1333
  coin=coin,
1465
1334
  spot_asset_id=spot_asset_id,
@@ -1485,15 +1354,8 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1485
1354
  # ------------------------------------------------------------------ #
1486
1355
 
1487
1356
  async def _get_undeployed_capital(self) -> tuple[float, float]:
1488
- """
1489
- Calculate undeployed capital that can be added to the position.
1490
-
1491
- Returns:
1492
- (perp_margin_available, spot_usdc_available)
1493
- """
1494
1357
  address = self._get_strategy_wallet_address()
1495
1358
 
1496
- # Get perp state
1497
1359
  success, user_state = await self.hyperliquid_adapter.get_user_state(address)
1498
1360
  if not success:
1499
1361
  return 0.0, 0.0
@@ -1508,7 +1370,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1508
1370
 
1509
1371
  withdrawable = float(withdrawable_val or 0.0)
1510
1372
 
1511
- # Get spot USDC balance
1512
1373
  success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
1513
1374
  address
1514
1375
  )
@@ -1527,13 +1388,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1527
1388
  target_spot_usdc: float,
1528
1389
  address: str,
1529
1390
  ) -> tuple[bool, str]:
1530
- """
1531
- Rebalance Hyperliquid USDC between spot and perp to hit a target spot USDC balance.
1532
-
1533
- Used before opening/scaling a basis position so we can:
1534
- - fund the spot buy (spot USDC ~= target_spot_usdc)
1535
- - keep the remainder in perp as margin (perp USDC ~= total - target_spot_usdc)
1536
- """
1537
1391
  if target_spot_usdc <= 0:
1538
1392
  return False, "Target spot USDC must be positive"
1539
1393
 
@@ -1592,32 +1446,17 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1592
1446
  return True, f"Transferred ${amount:.2f} spot->perp"
1593
1447
 
1594
1448
  async def _scale_up_position(self, additional_capital: float) -> StatusTuple:
1595
- """
1596
- Add capital to existing position without breaking it.
1597
-
1598
- Uses PairedFiller to atomically add to both spot and perp legs,
1599
- maintaining delta neutrality.
1600
-
1601
- Args:
1602
- additional_capital: USD amount of new capital to deploy
1603
-
1604
- Returns:
1605
- StatusTuple (success, message)
1606
- """
1607
1449
  if self.current_position is None:
1608
1450
  return False, "No position to scale up"
1609
1451
 
1610
1452
  pos = self.current_position
1611
1453
  address = self._get_strategy_wallet_address()
1612
1454
 
1613
- # Get current leverage from position
1614
1455
  leverage = pos.leverage or 2
1615
1456
 
1616
- # Calculate how much to add to each leg
1617
1457
  # order_usd = capital * (L / (L + 1)) for leveraged position
1618
1458
  order_usd = additional_capital * (leverage / (leverage + 1))
1619
1459
 
1620
- # Get current price
1621
1460
  success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
1622
1461
  if not success:
1623
1462
  return False, "Failed to get mid prices"
@@ -1626,14 +1465,12 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1626
1465
  if price <= 0:
1627
1466
  return False, f"Invalid price for {pos.coin}"
1628
1467
 
1629
- # Check minimum notional ($10 USD per side)
1630
1468
  if order_usd < MIN_NOTIONAL_USD:
1631
1469
  return (
1632
1470
  True,
1633
1471
  f"Additional capital ${order_usd:.2f} below minimum notional ${MIN_NOTIONAL_USD}",
1634
1472
  )
1635
1473
 
1636
- # Calculate units to add
1637
1474
  units_to_add = order_usd / price
1638
1475
 
1639
1476
  # Round to valid decimals for the assets
@@ -1685,7 +1522,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1685
1522
  spot_asset_id=pos.spot_asset_id,
1686
1523
  perp_asset_id=pos.perp_asset_id,
1687
1524
  total_units=units_to_add,
1688
- direction="long_spot_short_perp", # Buy spot, sell perp
1525
+ direction="long_spot_short_perp",
1689
1526
  builder_fee=self.builder_fee,
1690
1527
  )
1691
1528
  except Exception as e:
@@ -1695,16 +1532,15 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1695
1532
  if spot_filled <= 0 or perp_filled <= 0:
1696
1533
  return False, f"Failed to add to position on {pos.coin}"
1697
1534
 
1698
- # Update position tracking
1699
1535
  self.current_position = BasisPosition(
1700
1536
  coin=pos.coin,
1701
1537
  spot_asset_id=pos.spot_asset_id,
1702
1538
  perp_asset_id=pos.perp_asset_id,
1703
1539
  spot_amount=pos.spot_amount + spot_filled,
1704
1540
  perp_amount=pos.perp_amount + perp_filled,
1705
- entry_price=price, # Use new price as weighted entry
1541
+ entry_price=price,
1706
1542
  leverage=leverage,
1707
- entry_timestamp=pos.entry_timestamp, # Keep original
1543
+ entry_timestamp=pos.entry_timestamp,
1708
1544
  funding_collected=pos.funding_collected,
1709
1545
  )
1710
1546
 
@@ -1719,15 +1555,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1719
1555
  )
1720
1556
 
1721
1557
  async def _monitor_position(self) -> StatusTuple:
1722
- """
1723
- Monitor existing position for exit/rebalance conditions.
1724
-
1725
- Checks:
1726
- 1. Whether rebalance is needed (funding, liquidity, etc.)
1727
- 2. Both legs are balanced (spot and perp amounts match)
1728
- 3. No significant idle capital (deploy if found)
1729
- 4. Stop-loss orders are in place and valid
1730
- """
1731
1558
  if self.current_position is None:
1732
1559
  return (True, "No position to monitor")
1733
1560
 
@@ -1736,12 +1563,10 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1736
1563
  address = self._get_strategy_wallet_address()
1737
1564
  actions_taken: list[str] = []
1738
1565
 
1739
- # Get current state
1740
1566
  success, state = await self.hyperliquid_adapter.get_user_state(address)
1741
1567
  if not success:
1742
1568
  return (False, f"Failed to fetch user state: {state}")
1743
1569
 
1744
- # Calculate deposited amount from current on-exchange value
1745
1570
  total_value, hl_value, _ = await self._get_total_portfolio_value()
1746
1571
 
1747
1572
  # ------------------------------------------------------------------ #
@@ -1761,12 +1586,10 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1761
1586
  return await self._find_and_open_position()
1762
1587
 
1763
1588
  # ------------------------------------------------------------------ #
1764
- # Check 1: Rebalance needed? #
1765
1589
  # ------------------------------------------------------------------ #
1766
1590
  needs_rebalance, reason = await self._needs_new_position(state, hl_value)
1767
1591
 
1768
1592
  if needs_rebalance:
1769
- # Check rotation cooldown
1770
1593
  rotation_allowed, cooldown_reason = await self._is_rotation_allowed()
1771
1594
 
1772
1595
  if not rotation_allowed:
@@ -1788,7 +1611,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1788
1611
  return await self._find_and_open_position()
1789
1612
 
1790
1613
  # ------------------------------------------------------------------ #
1791
- # Check 2: Verify both legs are balanced #
1792
1614
  # ------------------------------------------------------------------ #
1793
1615
  leg_ok, leg_msg = await self._verify_leg_balance(state)
1794
1616
  if not leg_ok:
@@ -1801,7 +1623,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1801
1623
  actions_taken.append(f"Leg imbalance repair failed: {repair_msg}")
1802
1624
 
1803
1625
  # ------------------------------------------------------------------ #
1804
- # Check 3: Deploy any idle capital #
1805
1626
  # ------------------------------------------------------------------ #
1806
1627
  perp_margin, spot_usdc = await self._get_undeployed_capital()
1807
1628
  total_idle = perp_margin + spot_usdc
@@ -1822,7 +1643,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1822
1643
  actions_taken.append(f"Scale-up failed: {scale_msg}")
1823
1644
 
1824
1645
  # ------------------------------------------------------------------ #
1825
- # Check 4: Verify stop-loss orders #
1826
1646
  # ------------------------------------------------------------------ #
1827
1647
  sl_ok, sl_msg = await self._ensure_stop_loss_valid(state)
1828
1648
  if not sl_ok:
@@ -1844,12 +1664,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1844
1664
  )
1845
1665
 
1846
1666
  async def _is_near_liquidation(self, state: dict[str, Any]) -> tuple[bool, str]:
1847
- """
1848
- Check whether the perp leg is too close to liquidation.
1849
-
1850
- For a short perp, liquidation is ABOVE entry. We measure progress from entry -> liquidation:
1851
- frac = (mid - entry) / (liq - entry)
1852
- """
1853
1667
  if self.current_position is None:
1854
1668
  return False, "No position"
1855
1669
 
@@ -1908,19 +1722,12 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1908
1722
  )
1909
1723
 
1910
1724
  async def _verify_leg_balance(self, state: dict[str, Any]) -> tuple[bool, str]:
1911
- """
1912
- Verify that spot and perp legs are balanced (delta neutral).
1913
-
1914
- Returns:
1915
- (is_balanced, message)
1916
- """
1917
1725
  if self.current_position is None:
1918
1726
  return True, "No position"
1919
1727
 
1920
1728
  pos = self.current_position
1921
1729
  coin = pos.coin
1922
1730
 
1923
- # Get actual perp position size from state
1924
1731
  perp_size = 0.0
1925
1732
  for pos_wrapper in state.get("assetPositions", []):
1926
1733
  position = pos_wrapper.get("position", {})
@@ -1928,7 +1735,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1928
1735
  perp_size = abs(float(position.get("szi", 0)))
1929
1736
  break
1930
1737
 
1931
- # Get actual spot balance
1932
1738
  address = self._get_strategy_wallet_address()
1933
1739
  success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
1934
1740
  address
@@ -1940,20 +1746,18 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1940
1746
  spot_size = float(bal.get("total", 0))
1941
1747
  break
1942
1748
 
1943
- # Check balance - allow 2% tolerance
1944
1749
  if spot_size <= 0 and perp_size <= 0:
1945
1750
  return False, "Both legs are zero"
1946
1751
 
1947
1752
  max_size = max(spot_size, perp_size)
1948
1753
  if max_size > 0:
1949
1754
  imbalance_pct = abs(spot_size - perp_size) / max_size
1950
- if imbalance_pct > 0.02: # 2% tolerance
1755
+ if imbalance_pct > 0.02:
1951
1756
  return (
1952
1757
  False,
1953
1758
  f"Imbalance: spot={spot_size:.6f}, perp={perp_size:.6f} ({imbalance_pct * 100:.1f}%)",
1954
1759
  )
1955
1760
 
1956
- # Update tracked position with actual values
1957
1761
  self.current_position = BasisPosition(
1958
1762
  coin=pos.coin,
1959
1763
  spot_asset_id=pos.spot_asset_id,
@@ -1969,11 +1773,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1969
1773
  return True, f"Balanced: spot={spot_size:.6f}, perp={perp_size:.6f}"
1970
1774
 
1971
1775
  async def _repair_leg_imbalance(self, state: dict[str, Any]) -> tuple[bool, str]:
1972
- """
1973
- Attempt to repair an imbalance between spot and perp legs.
1974
-
1975
- If one leg is larger, adds to the smaller leg to match.
1976
- """
1977
1776
  if self.current_position is None:
1978
1777
  return True, "No position"
1979
1778
 
@@ -1981,7 +1780,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1981
1780
  coin = pos.coin
1982
1781
  address = self._get_strategy_wallet_address()
1983
1782
 
1984
- # Get actual sizes
1985
1783
  perp_size = 0.0
1986
1784
  for pos_wrapper in state.get("assetPositions", []):
1987
1785
  position = pos_wrapper.get("position", {})
@@ -2003,7 +1801,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2003
1801
  if diff < 0.001:
2004
1802
  return True, "Legs already balanced"
2005
1803
 
2006
- # Get current price
2007
1804
  success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
2008
1805
  if not success:
2009
1806
  return False, "Failed to get mid prices"
@@ -2012,7 +1809,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2012
1809
  return False, f"Invalid price for {coin}"
2013
1810
 
2014
1811
  diff_usd = diff * price
2015
- if diff_usd < 10: # Below minimum notional
1812
+ if diff_usd < 10:
2016
1813
  return True, f"Imbalance ${diff_usd:.2f} below minimum notional"
2017
1814
 
2018
1815
  try:
@@ -2054,24 +1851,12 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2054
1851
  return False, f"Repair failed: {e}"
2055
1852
 
2056
1853
  async def _ensure_stop_loss_valid(self, state: dict[str, Any]) -> tuple[bool, str]:
2057
- """
2058
- Ensure stop-loss orders are in place and valid for current position.
2059
-
2060
- Checks:
2061
- - Stop-loss exists for the perp leg
2062
- - Trigger price is valid (below liquidation price)
2063
- - Size matches position size
2064
-
2065
- Returns:
2066
- (success, message)
2067
- """
2068
1854
  if self.current_position is None:
2069
1855
  return True, "No position"
2070
1856
 
2071
1857
  pos = self.current_position
2072
1858
  coin = pos.coin
2073
1859
 
2074
- # Get current perp position and liquidation price
2075
1860
  perp_size = 0.0
2076
1861
  liquidation_price = None
2077
1862
  entry_price = pos.entry_price
@@ -2081,7 +1866,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2081
1866
  if position.get("coin") == coin:
2082
1867
  perp_size = abs(float(position.get("szi", 0)))
2083
1868
  liquidation_price = float(position.get("liquidationPx", 0))
2084
- # Update entry price from position if available
2085
1869
  entry_px = position.get("entryPx")
2086
1870
  if entry_px:
2087
1871
  entry_price = float(entry_px)
@@ -2093,7 +1877,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2093
1877
  if not liquidation_price or liquidation_price <= 0:
2094
1878
  return False, "Could not determine liquidation price"
2095
1879
 
2096
- # Get spot position size from LIVE balance (not stored position)
2097
1880
  # to ensure stop-loss covers the actual spot holdings
2098
1881
  spot_position = await self._get_spot_position()
2099
1882
  if spot_position:
@@ -2101,7 +1884,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2101
1884
  else:
2102
1885
  spot_size = pos.spot_amount
2103
1886
 
2104
- # Call existing method which checks and places/updates if needed
2105
1887
  return await self._place_stop_loss_orders(
2106
1888
  coin=coin,
2107
1889
  perp_asset_id=pos.perp_asset_id,
@@ -2113,7 +1895,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2113
1895
  )
2114
1896
 
2115
1897
  async def _cancel_all_position_orders(self) -> None:
2116
- """Cancel all open orders (stop-loss, limit) for the current position."""
2117
1898
  if self.current_position is None:
2118
1899
  return
2119
1900
 
@@ -2123,7 +1904,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2123
1904
  f"@{pos.spot_asset_id - 10000}" if pos.spot_asset_id >= 10000 else None
2124
1905
  )
2125
1906
 
2126
- # Get all open orders including triggers
2127
1907
  success, open_orders = await self.hyperliquid_adapter.get_frontend_open_orders(
2128
1908
  address
2129
1909
  )
@@ -2154,7 +1934,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2154
1934
  )
2155
1935
 
2156
1936
  async def _close_position(self) -> StatusTuple:
2157
- """Close the current position."""
2158
1937
  if self.current_position is None:
2159
1938
  return (True, "No position to close")
2160
1939
 
@@ -2187,7 +1966,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2187
1966
  spot_asset_id=pos.spot_asset_id,
2188
1967
  perp_asset_id=pos.perp_asset_id,
2189
1968
  total_units=close_units,
2190
- direction="short_spot_long_perp", # Reverse to close
1969
+ direction="short_spot_long_perp",
2191
1970
  builder_fee=self.builder_fee,
2192
1971
  )
2193
1972
 
@@ -2216,36 +1995,18 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2216
1995
  deposited_amount: float,
2217
1996
  best: dict[str, Any] | None = None,
2218
1997
  ) -> tuple[bool, str]:
2219
- """
2220
- Check if current delta-neutral position needs rebalancing.
2221
-
2222
- Implements the following health checks:
2223
- 1. Missing positions
2224
- 2. Asset mismatch (if best specified)
2225
- 3. Funding accumulation threshold
2226
- 4. Perp must be SHORT
2227
- 5. Position imbalance (±4% dust tolerance)
2228
- 6. Unused bankroll
2229
- 7. Stop-loss orders exist and are valid
2230
-
2231
- Returns:
2232
- (needs_rebalance, reason) - True if rebalance needed
2233
- """
2234
1998
  perp_position = self._get_perp_position(state)
2235
1999
  spot_position = await self._get_spot_position()
2236
2000
 
2237
- # Check 1: Missing positions
2238
2001
  if perp_position is None or spot_position is None:
2239
2002
  return True, "Missing perp or spot position"
2240
2003
 
2241
- # Check 2: Asset mismatch (if best specified)
2242
2004
  if best:
2243
2005
  if perp_position.get("asset_id") != best.get("perp_asset_id"):
2244
2006
  return True, "Perp asset mismatch"
2245
2007
  if spot_position.get("asset_id") != best.get("spot_asset_id"):
2246
2008
  return True, "Spot asset mismatch"
2247
2009
 
2248
- # Check 3: Funding accumulation threshold
2249
2010
  funding_earned = self._get_funding_earned(state)
2250
2011
  if funding_earned > deposited_amount * self.FUNDING_REBALANCE_THRESHOLD:
2251
2012
  return True, f"Funding earned {funding_earned:.2f} exceeds threshold"
@@ -2255,7 +2016,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2255
2016
  if perp_size >= 0:
2256
2017
  return True, "Perp position is not short"
2257
2018
 
2258
- # Check 5: Position imbalance (±4% dust tolerance)
2259
2019
  spot_size = abs(float(spot_position.get("total", 0)))
2260
2020
  perp_size_abs = abs(perp_size)
2261
2021
  lower = spot_size * (1 - self.SPOT_POSITION_DUST_TOLERANCE)
@@ -2268,13 +2028,11 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2268
2028
  # there's idle capital - that should be added to the existing position.
2269
2029
 
2270
2030
  # Note: Stop-loss validation is handled separately in _monitor_position's
2271
- # Check 4 (_ensure_stop_loss_valid) which will place/update orders as needed
2272
2031
  # without triggering a full rebalance.
2273
2032
 
2274
2033
  return False, "Position healthy"
2275
2034
 
2276
2035
  def _get_perp_position(self, state: dict[str, Any]) -> dict[str, Any] | None:
2277
- """Extract perp position matching current position from user state."""
2278
2036
  if self.current_position is None:
2279
2037
  return None
2280
2038
 
@@ -2289,7 +2047,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2289
2047
  return None
2290
2048
 
2291
2049
  async def _get_spot_position(self) -> dict[str, Any] | None:
2292
- """Get spot position from spot user state."""
2293
2050
  if self.current_position is None:
2294
2051
  return None
2295
2052
 
@@ -2310,7 +2067,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2310
2067
  return None
2311
2068
 
2312
2069
  def _get_funding_earned(self, state: dict[str, Any]) -> float:
2313
- """Extract cumulative funding earned from user state."""
2314
2070
  if self.current_position is None:
2315
2071
  return 0.0
2316
2072
 
@@ -2332,18 +2088,8 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2332
2088
  spot_asset_id: int | None = None,
2333
2089
  spot_position_size: float | None = None,
2334
2090
  ) -> tuple[bool, str]:
2335
- """
2336
- Place stop-loss orders for both perp and spot legs.
2337
-
2338
- For basis trading:
2339
- - Perp leg: Stop-market trigger order (buy to close short when price rises)
2340
- - Spot leg: Limit sell order (sell spot at stop-loss price)
2341
-
2342
- Both orders together maintain delta neutrality when the stop-loss is hit.
2343
- """
2344
2091
  address = self._get_strategy_wallet_address()
2345
2092
 
2346
- # Get spot info from current position if not provided
2347
2093
  if spot_asset_id is None or spot_position_size is None:
2348
2094
  if self.current_position:
2349
2095
  spot_asset_id = self.current_position.spot_asset_id
@@ -2352,7 +2098,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2352
2098
  spot_asset_id = None
2353
2099
  spot_position_size = 0.0
2354
2100
 
2355
- # Calculate stop-loss trigger price (90% of distance to liquidation)
2356
2101
  # For short perp, liquidation is ABOVE entry price
2357
2102
  stop_loss_price = (
2358
2103
  entry_price
@@ -2361,7 +2106,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2361
2106
  # Round to 5 significant figures to avoid SDK float_to_wire precision errors
2362
2107
  stop_loss_price = float(f"{stop_loss_price:.5g}")
2363
2108
 
2364
- # Get all open orders (frontend_open_orders includes trigger orders)
2365
2109
  success, open_orders = await self.hyperliquid_adapter.get_frontend_open_orders(
2366
2110
  address
2367
2111
  )
@@ -2384,9 +2128,8 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2384
2128
  order_id = order.get("oid")
2385
2129
  is_trigger = order.get("isTrigger", False)
2386
2130
  order_type = str(order.get("orderType", "")).lower()
2387
- is_sell = order.get("side", "").upper() == "A" # "A" = Ask/Sell
2131
+ is_sell = order.get("side", "").upper() == "A"
2388
2132
 
2389
- # Check PERP trigger orders (stop-loss)
2390
2133
  if order_coin == coin:
2391
2134
  is_trigger_order = (
2392
2135
  is_trigger or "stop" in order_type or "trigger" in order_type
@@ -2414,13 +2157,11 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2414
2157
  (perp_asset_id, order_id, "perp stop-loss")
2415
2158
  )
2416
2159
 
2417
- # Check SPOT limit sell orders
2418
2160
  if spot_coin and order_coin == spot_coin and is_sell:
2419
2161
  # This is a spot sell order (could be our stop-loss limit)
2420
2162
  existing_price = float(order.get("limitPx", 0))
2421
2163
  existing_size = float(order.get("sz", 0))
2422
2164
 
2423
- # Check if it's around our stop-loss price (within 5%)
2424
2165
  price_match = (
2425
2166
  abs(existing_price - stop_loss_price) / stop_loss_price < 0.05
2426
2167
  )
@@ -2455,7 +2196,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2455
2196
  if not has_valid_perp_stop:
2456
2197
  success, result = await self.hyperliquid_adapter.place_stop_loss(
2457
2198
  asset_id=perp_asset_id,
2458
- is_buy=True, # Buy to close short
2199
+ is_buy=True,
2459
2200
  trigger_price=stop_loss_price,
2460
2201
  size=position_size,
2461
2202
  address=address,
@@ -2471,18 +2212,17 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2471
2212
  and spot_position_size > 0
2472
2213
  and not has_valid_spot_limit
2473
2214
  ):
2474
- # Get valid order size for spot
2475
2215
  spot_sell_size = self.hyperliquid_adapter.get_valid_order_size(
2476
2216
  spot_asset_id, spot_position_size
2477
2217
  )
2478
2218
  if spot_sell_size > 0:
2479
2219
  success, result = await self.hyperliquid_adapter.place_limit_order(
2480
2220
  asset_id=spot_asset_id,
2481
- is_buy=False, # Sell
2221
+ is_buy=False,
2482
2222
  price=stop_loss_price,
2483
2223
  size=spot_sell_size,
2484
2224
  address=address,
2485
- reduce_only=False, # Spot doesn't have reduce_only
2225
+ reduce_only=False,
2486
2226
  )
2487
2227
  if not success:
2488
2228
  self.logger.warning(f"Failed to place spot limit sell: {result}")
@@ -2499,7 +2239,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2499
2239
  # ------------------------------------------------------------------ #
2500
2240
 
2501
2241
  async def _get_last_rotation_time(self) -> datetime | None:
2502
- """Get timestamp of last position rotation from ledger."""
2503
2242
  wallet_address = self._get_strategy_wallet_address()
2504
2243
 
2505
2244
  try:
@@ -2528,7 +2267,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2528
2267
  return None
2529
2268
 
2530
2269
  async def _is_rotation_allowed(self) -> tuple[bool, str]:
2531
- """Check if rotation cooldown has passed."""
2532
2270
  if self.current_position is None:
2533
2271
  return True, "No existing position"
2534
2272
 
@@ -2555,29 +2293,20 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2555
2293
  # ------------------------------------------------------------------ #
2556
2294
 
2557
2295
  async def _get_total_portfolio_value(self) -> tuple[float, float, float]:
2558
- """
2559
- Get total portfolio value including Hyperliquid and vault balances.
2560
-
2561
- Returns:
2562
- (total_value, hyperliquid_value, vault_wallet_value)
2563
- """
2564
2296
  address = self._get_strategy_wallet_address()
2565
2297
 
2566
- # Get Hyperliquid account value
2567
2298
  hl_value = 0.0
2568
2299
  success, user_state = await self.hyperliquid_adapter.get_user_state(address)
2569
2300
  if success:
2570
2301
  margin_summary = user_state.get("marginSummary", {})
2571
2302
  hl_value = float(margin_summary.get("accountValue", 0))
2572
2303
 
2573
- # Add spot value (all spot holdings, not just USDC)
2574
2304
  (
2575
2305
  success_spot,
2576
2306
  spot_state,
2577
2307
  ) = await self.hyperliquid_adapter.get_spot_user_state(address)
2578
2308
  if success_spot:
2579
2309
  spot_balances = spot_state.get("balances", [])
2580
- # Get mid prices for non-USDC assets
2581
2310
  mid_prices: dict[str, float] = {}
2582
2311
  if any(bal.get("coin") != "USDC" for bal in spot_balances):
2583
2312
  (
@@ -2606,7 +2335,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2606
2335
  f"No mid price found for spot {coin}, skipping"
2607
2336
  )
2608
2337
 
2609
- # Get strategy wallet USDC balance (on Arbitrum)
2610
2338
  strategy_wallet_value = 0.0
2611
2339
  try:
2612
2340
  strategy_address = self._get_strategy_wallet_address()
@@ -2615,7 +2343,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2615
2343
  wallet_address=strategy_address,
2616
2344
  )
2617
2345
  if success and balance:
2618
- strategy_wallet_value = float(balance) / 1e6 # Convert from raw to USDC
2346
+ strategy_wallet_value = float(balance) / 1e6
2619
2347
  except Exception as e:
2620
2348
  self.logger.debug(f"Could not fetch strategy wallet balance: {e}")
2621
2349
 
@@ -2632,7 +2360,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2632
2360
  idx_to_token: dict[int, str],
2633
2361
  perps_set: set[str],
2634
2362
  ) -> list[tuple[str, str, int]]:
2635
- """Find spot-perp pairs that can form basis trades."""
2636
2363
  candidates: list[tuple[str, str, int]] = []
2637
2364
 
2638
2365
  for pe in spot_pairs:
@@ -2650,7 +2377,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2650
2377
  spot_pair_name = f"{base}/{quote}"
2651
2378
  spot_asset_id = pe["index"] + 10000
2652
2379
 
2653
- # Handle USDT prefixed tokens (UPUMP -> PUMP)
2654
2380
  base_norm = (
2655
2381
  base[1:] if (base.startswith("U") and base[1:] in perps_set) else base
2656
2382
  )
@@ -2672,7 +2398,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2672
2398
  perp_coin_to_asset_id: dict[str, int],
2673
2399
  depth_params: dict[str, Any] | None = None,
2674
2400
  ) -> list[BasisCandidate]:
2675
- """Filter candidates by liquidity and venue depth, returning structured data."""
2676
2401
  liquid: list[BasisCandidate] = []
2677
2402
 
2678
2403
  if deposit_usdc <= 0:
@@ -2706,7 +2431,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2706
2431
  if order_usd <= 0:
2707
2432
  continue
2708
2433
 
2709
- # Get spot order book
2710
2434
  try:
2711
2435
  book_snapshot = await self._l2_book_spot(
2712
2436
  spot_asset_id,
@@ -2764,29 +2488,14 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2764
2488
  # Chunked Data Fetching #
2765
2489
  # ------------------------------------------------------------------ #
2766
2490
 
2767
- HOURS_PER_CHUNK = 500 # Hyperliquid API returns max 500 data points per call
2768
- CHUNK_DELAY_SECONDS = 0.2 # Delay between API chunks to avoid rate limiting
2491
+ HOURS_PER_CHUNK = 500
2492
+ CHUNK_DELAY_SECONDS = 0.2
2769
2493
 
2770
2494
  def _hour_chunks(
2771
2495
  self, start_ms: int, end_ms: int, step_hours: int = 500
2772
2496
  ) -> list[tuple[int, int]]:
2773
- """
2774
- Generate time chunks for API calls.
2775
-
2776
- Each chunk is (start_ms, end_ms) tuple representing a time window
2777
- of up to `step_hours` hours. This allows fetching >500 data points
2778
- by making multiple API calls.
2779
-
2780
- Args:
2781
- start_ms: Start time in milliseconds
2782
- end_ms: End time in milliseconds
2783
- step_hours: Hours per chunk (default 500, Hyperliquid API limit)
2784
-
2785
- Returns:
2786
- List of (chunk_start_ms, chunk_end_ms) tuples
2787
- """
2788
2497
  chunks = []
2789
- step_ms = step_hours * 3600 * 1000 # Convert hours to milliseconds
2498
+ step_ms = step_hours * 3600 * 1000
2790
2499
  t0 = start_ms
2791
2500
 
2792
2501
  while t0 < end_ms:
@@ -2802,30 +2511,14 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2802
2511
  start_ms: int,
2803
2512
  end_ms: int | None = None,
2804
2513
  ) -> tuple[bool, list[dict[str, Any]]]:
2805
- """
2806
- Fetch funding history with automatic chunking for long time ranges.
2807
-
2808
- Hyperliquid API returns max ~500 data points per call. This method
2809
- automatically splits long requests into multiple chunks and merges
2810
- the results.
2811
-
2812
- Args:
2813
- coin: Coin symbol (e.g., "ETH", "BTC")
2814
- start_ms: Start time in milliseconds
2815
- end_ms: End time in milliseconds (defaults to now)
2816
-
2817
- Returns:
2818
- (success, combined_funding_data)
2819
- """
2820
2514
  if end_ms is None:
2821
2515
  end_ms = int(time.time() * 1000)
2822
2516
 
2823
2517
  chunks = self._hour_chunks(start_ms, end_ms, self.HOURS_PER_CHUNK)
2824
2518
  all_funding: list[dict[str, Any]] = []
2825
- seen_times: set[int] = set() # Dedupe by timestamp
2519
+ seen_times: set[int] = set()
2826
2520
 
2827
2521
  for i, (chunk_start, chunk_end) in enumerate(chunks):
2828
- # Add delay between chunks to avoid rate limiting
2829
2522
  if i > 0:
2830
2523
  await asyncio.sleep(self.CHUNK_DELAY_SECONDS)
2831
2524
 
@@ -2866,27 +2559,14 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2866
2559
  start_ms: int,
2867
2560
  end_ms: int | None = None,
2868
2561
  ) -> tuple[bool, list[dict[str, Any]]]:
2869
- """
2870
- Fetch candle data with automatic chunking for long time ranges.
2871
-
2872
- Args:
2873
- coin: Coin symbol (e.g., "ETH", "BTC")
2874
- interval: Candle interval (e.g., "1h")
2875
- start_ms: Start time in milliseconds
2876
- end_ms: End time in milliseconds (defaults to now)
2877
-
2878
- Returns:
2879
- (success, combined_candle_data)
2880
- """
2881
2562
  if end_ms is None:
2882
2563
  end_ms = int(time.time() * 1000)
2883
2564
 
2884
2565
  chunks = self._hour_chunks(start_ms, end_ms, self.HOURS_PER_CHUNK)
2885
2566
  all_candles: list[dict[str, Any]] = []
2886
- seen_times: set[int] = set() # Dedupe by open timestamp
2567
+ seen_times: set[int] = set()
2887
2568
 
2888
2569
  for i, (chunk_start, chunk_end) in enumerate(chunks):
2889
- # Add delay between chunks to avoid rate limiting
2890
2570
  if i > 0:
2891
2571
  await asyncio.sleep(self.CHUNK_DELAY_SECONDS)
2892
2572
 
@@ -2931,7 +2611,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2931
2611
  *,
2932
2612
  fallback_mid: float | None = None,
2933
2613
  ) -> dict[str, Any]:
2934
- """Normalize Hyperliquid L2 into bids/asks lists with floats."""
2935
2614
  return hl_normalize_l2_book(raw, fallback_mid=fallback_mid)
2936
2615
 
2937
2616
  async def _l2_book_spot(
@@ -2941,7 +2620,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2941
2620
  fallback_mid: float | None = None,
2942
2621
  spot_symbol: str | None = None,
2943
2622
  ) -> dict[str, Any]:
2944
- """Fetch and normalize Level-2 order book snapshot for a spot asset."""
2945
2623
  last_exc: Exception | None = None
2946
2624
 
2947
2625
  try:
@@ -2997,7 +2675,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2997
2675
  max_bps: int = 100,
2998
2676
  gamma: int = 20,
2999
2677
  ) -> int:
3000
- """Widen the depth band slowly with order size."""
3001
2678
  if order_usd <= 0:
3002
2679
  return base_bps
3003
2680
 
@@ -3017,12 +2694,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3017
2694
  fallback_mid: float | None = None,
3018
2695
  spot_symbol: str | None = None,
3019
2696
  ) -> dict[str, Any]:
3020
- """
3021
- Heuristic spot book depth gate using USD notionals.
3022
-
3023
- Returns diagnostics including available depth, thresholds, and pass/fail flags.
3024
- """
3025
-
3026
2697
  config: dict[str, Any] = {
3027
2698
  "base_band_bps": 50,
3028
2699
  "max_band_bps": 100,
@@ -3148,8 +2819,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3148
2819
  day_ntl_usd: float | None = None,
3149
2820
  spot_symbol: str | None = None,
3150
2821
  ) -> tuple[float, float, dict[str, float], dict[str, dict[str, Any]]]:
3151
- """Estimate entry/exit execution costs for a full cycle on both legs."""
3152
-
3153
2822
  cfg_fees = {"spot_bps": 9.0, "perp_bps": 6.0}
3154
2823
  if fee_model:
3155
2824
  cfg_fees.update(fee_model)
@@ -3210,7 +2879,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3210
2879
  return entry_cost, exit_cost, breakdown, {"buy": buy_chk, "sell": sell_chk}
3211
2880
 
3212
2881
  async def _get_margin_table_tiers(self, table_id: int) -> list[dict[str, float]]:
3213
- """Fetch and cache margin table tiers with maintenance rates and deductions."""
3214
2882
  if table_id in self._margin_table_cache:
3215
2883
  return [dict(t) for t in self._margin_table_cache[table_id]]
3216
2884
 
@@ -3275,7 +2943,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3275
2943
  notional_usd: float,
3276
2944
  fallback_max_leverage: int,
3277
2945
  ) -> float:
3278
- """Return maintenance margin fraction for a given notional, honoring tiered tables."""
3279
2946
  fallback_mmr = self.maintenance_rate_from_max_leverage(
3280
2947
  max(1, int(fallback_max_leverage))
3281
2948
  )
@@ -3316,7 +2983,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3316
2983
  maintenance_fn,
3317
2984
  base_notional: float,
3318
2985
  ) -> int:
3319
- """Return the forward hours until the stop barrier is hit or data is exhausted."""
3320
2986
  n = min(len(closes), len(highs), len(hourly_funding)) - 1
3321
2987
  if start_idx >= n:
3322
2988
  return 0
@@ -3370,7 +3036,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3370
3036
  fallback_max_leverage: int,
3371
3037
  cooloff_hours: int = 0,
3372
3038
  ) -> dict[str, float]:
3373
- """Simulate repeated entries/exits under a stop barrier and accumulate PnL."""
3374
3039
  n = min(len(funding), len(closes), len(highs)) - 1
3375
3040
  if n <= 0:
3376
3041
  return {
@@ -3446,7 +3111,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3446
3111
 
3447
3112
  @staticmethod
3448
3113
  def _percentile(sorted_values: list[float], pct: float) -> float:
3449
- """Inclusive percentile on a pre-sorted list."""
3450
3114
  return analytics_percentile(sorted_values, pct)
3451
3115
 
3452
3116
  def _block_bootstrap_paths(
@@ -3459,7 +3123,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3459
3123
  sims: int,
3460
3124
  rng: random.Random,
3461
3125
  ) -> list[tuple[list[float], list[float], list[float]]]:
3462
- """Return block-bootstrap resampled series for funding/close/high paths."""
3463
3126
  paths = analytics_block_bootstrap_paths(
3464
3127
  funding,
3465
3128
  closes,
@@ -3490,7 +3153,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3490
3153
  block_hours: int,
3491
3154
  seed: int | None,
3492
3155
  ) -> dict[str, Any] | None:
3493
- """Run block-bootstrap replays and summarize churn metrics."""
3494
3156
  if sims <= 0 or deposit_usdc <= 0:
3495
3157
  return None
3496
3158
 
@@ -3597,8 +3259,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3597
3259
  fee_eps: float,
3598
3260
  require_full_window: bool = True,
3599
3261
  ) -> float:
3600
- """Worst-case buffer requirement accounting for tiered maintenance margin."""
3601
-
3602
3262
  fallback_mmr = self.maintenance_rate_from_max_leverage(
3603
3263
  max(1, int(fallback_max_leverage))
3604
3264
  )
@@ -3659,7 +3319,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3659
3319
  def round_size_for_hypecore_asset(
3660
3320
  self, asset_id: int, size: float | Decimal, *, ensure_min_step: bool = False
3661
3321
  ) -> float:
3662
- """Floor to step using Decimal to avoid float issues."""
3663
3322
  try:
3664
3323
  mapping = self.hyperliquid_adapter.asset_to_sz_decimals
3665
3324
  except Exception as exc: # noqa: BLE001
@@ -3688,11 +3347,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3688
3347
  spot_asset_id: int,
3689
3348
  perp_asset_id: int | None,
3690
3349
  ) -> float:
3691
- """
3692
- Minimum USDC deposit to place at least one lot on both legs at leverage L.
3693
-
3694
- D_min(L) = N * (1 + 1/L), with N = unit_step * mark_px.
3695
- """
3696
3350
  L = max(1, int(leverage))
3697
3351
  unit_step = self._common_unit_step(spot_asset_id, perp_asset_id)
3698
3352
  mark = _d(mark_price)
@@ -3708,11 +3362,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3708
3362
  day_ntl_usd: float | None,
3709
3363
  params: dict[str, Any] | None,
3710
3364
  ) -> float:
3711
- """
3712
- Conservative upper bound for order size that could ever pass depth checks.
3713
-
3714
- Uses depth at max band and turnover cap (if provided).
3715
- """
3716
3365
  config: dict[str, Any] = {
3717
3366
  "max_band_bps": 100,
3718
3367
  "max_fill_ratio": 0.10,
@@ -3769,12 +3418,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3769
3418
  params: dict[str, Any] | None = None,
3770
3419
  refine_iters: int = 12,
3771
3420
  ) -> dict[str, Any]:
3772
- """
3773
- Compute the maximum order_usd that passes spot depth checks on both sides.
3774
-
3775
- This is used for batch precompute so workers can quickly filter candidates
3776
- by a user's required order size.
3777
- """
3778
3421
  upper_buy = self._depth_upper_bound_usd(
3779
3422
  book=book, side="buy", day_ntl_usd=day_ntl_usd, params=params
3780
3423
  )
@@ -3933,8 +3576,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
3933
3576
  bootstrap_block_hours: int = DEFAULT_BOOTSTRAP_BLOCK_HOURS,
3934
3577
  bootstrap_seed: int | None = None,
3935
3578
  ) -> list[dict[str, Any]]:
3936
- """Rank spot/perp pairs by simulated net APY under stop-driven churn."""
3937
-
3938
3579
  if deposit_usdc <= 0:
3939
3580
  return []
3940
3581
 
@@ -4159,22 +3800,18 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
4159
3800
  # ------------------------------------------------------------------ #
4160
3801
 
4161
3802
  def _z_from_conf(self, confidence: float) -> float:
4162
- """Get z-score for given confidence level."""
4163
3803
  return analytics_z_from_conf(confidence)
4164
3804
 
4165
3805
  def _rolling_min_sum(self, arr: list[float], window: int) -> float:
4166
- """Calculate minimum rolling sum over window."""
4167
3806
  return analytics_rolling_min_sum(arr, window)
4168
3807
 
4169
3808
  @staticmethod
4170
3809
  def maintenance_rate_from_max_leverage(max_lev: int) -> float:
4171
- """Estimate maintenance margin rate from max leverage."""
4172
3810
  if max_lev <= 0:
4173
3811
  return 0.5
4174
3812
  return 0.5 / max_lev
4175
3813
 
4176
3814
  def _get_strategy_wallet_address(self) -> str:
4177
- """Get strategy wallet address from config."""
4178
3815
  strategy_wallet = self.config.get("strategy_wallet")
4179
3816
  if not strategy_wallet or not isinstance(strategy_wallet, dict):
4180
3817
  raise ValueError("strategy_wallet not configured")
@@ -4184,7 +3821,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
4184
3821
  return str(address)
4185
3822
 
4186
3823
  def _get_main_wallet_address(self) -> str:
4187
- """Get main wallet address from config."""
4188
3824
  main_wallet = self.config.get("main_wallet")
4189
3825
  if not main_wallet or not isinstance(main_wallet, dict):
4190
3826
  raise ValueError("main_wallet not configured")