wayfinder-paths 0.1.7__py3-none-any.whl → 0.1.9__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.
Files changed (51) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +5 -14
  2. wayfinder_paths/adapters/brap_adapter/README.md +1 -1
  3. wayfinder_paths/adapters/brap_adapter/adapter.py +1 -53
  4. wayfinder_paths/adapters/brap_adapter/test_adapter.py +5 -7
  5. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +0 -7
  6. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +0 -54
  7. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +1 -1
  8. wayfinder_paths/adapters/ledger_adapter/README.md +1 -1
  9. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +3 -0
  10. wayfinder_paths/adapters/pool_adapter/README.md +3 -104
  11. wayfinder_paths/adapters/pool_adapter/adapter.py +0 -194
  12. wayfinder_paths/adapters/pool_adapter/examples.json +0 -100
  13. wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -134
  14. wayfinder_paths/adapters/token_adapter/README.md +1 -1
  15. wayfinder_paths/core/clients/AuthClient.py +0 -3
  16. wayfinder_paths/core/clients/BRAPClient.py +1 -0
  17. wayfinder_paths/core/clients/ClientManager.py +1 -22
  18. wayfinder_paths/core/clients/PoolClient.py +0 -16
  19. wayfinder_paths/core/clients/WalletClient.py +0 -8
  20. wayfinder_paths/core/clients/WayfinderClient.py +9 -14
  21. wayfinder_paths/core/clients/__init__.py +0 -8
  22. wayfinder_paths/core/clients/protocols.py +0 -64
  23. wayfinder_paths/core/config.py +5 -45
  24. wayfinder_paths/core/engine/StrategyJob.py +0 -3
  25. wayfinder_paths/core/services/base.py +0 -49
  26. wayfinder_paths/core/services/local_evm_txn.py +3 -82
  27. wayfinder_paths/core/services/local_token_txn.py +61 -70
  28. wayfinder_paths/core/services/web3_service.py +0 -2
  29. wayfinder_paths/core/settings.py +8 -8
  30. wayfinder_paths/core/strategies/Strategy.py +1 -5
  31. wayfinder_paths/core/utils/evm_helpers.py +7 -12
  32. wayfinder_paths/core/wallets/README.md +3 -6
  33. wayfinder_paths/run_strategy.py +29 -32
  34. wayfinder_paths/scripts/make_wallets.py +1 -25
  35. wayfinder_paths/scripts/run_strategy.py +0 -2
  36. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -3
  37. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +86 -137
  38. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +96 -58
  39. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
  40. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +4 -1
  41. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +106 -28
  42. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +53 -14
  43. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1 -6
  44. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +40 -17
  45. wayfinder_paths/templates/strategy/test_strategy.py +0 -4
  46. {wayfinder_paths-0.1.7.dist-info → wayfinder_paths-0.1.9.dist-info}/METADATA +33 -15
  47. {wayfinder_paths-0.1.7.dist-info → wayfinder_paths-0.1.9.dist-info}/RECORD +49 -51
  48. wayfinder_paths/core/clients/SimulationClient.py +0 -192
  49. wayfinder_paths/core/clients/TransactionClient.py +0 -63
  50. {wayfinder_paths-0.1.7.dist-info → wayfinder_paths-0.1.9.dist-info}/LICENSE +0 -0
  51. {wayfinder_paths-0.1.7.dist-info → wayfinder_paths-0.1.9.dist-info}/WHEEL +0 -0
@@ -59,7 +59,7 @@ def resolve_rpc_url(
59
59
  explicit_rpc_url: str | None = None,
60
60
  ) -> str:
61
61
  """
62
- Resolve RPC URL from config or environment variables.
62
+ Resolve RPC URL from config.
63
63
 
64
64
  Args:
65
65
  chain_id: Chain ID to look up RPC URL for
@@ -83,10 +83,7 @@ def resolve_rpc_url(
83
83
  by_str = mapping.get(str(chain_id))
84
84
  if by_str:
85
85
  return str(by_str)
86
- env_rpc = os.getenv("RPC_URL")
87
- if env_rpc:
88
- return env_rpc
89
- raise ValueError("RPC URL not provided. Set strategy.rpc_urls or env RPC_URL.")
86
+ raise ValueError("RPC URL not provided. Set strategy.rpc_urls in config.json.")
90
87
 
91
88
 
92
89
  async def get_next_nonce(
@@ -119,7 +116,7 @@ def resolve_private_key_for_from_address(
119
116
  from_address: str, config: dict[str, Any]
120
117
  ) -> str | None:
121
118
  """
122
- Resolve private key for the given address from config or environment.
119
+ Resolve private key for the given address from config.
123
120
 
124
121
  Args:
125
122
  from_address: Address to resolve private key for
@@ -159,14 +156,12 @@ def resolve_private_key_for_from_address(
159
156
  logger.debug("Error resolving addresses from wallet config: %s", e)
160
157
 
161
158
  if main_addr and from_addr_norm == (main_addr or "").lower():
162
- return main_pk or os.getenv("PRIVATE_KEY")
159
+ return main_pk
163
160
  if strategy_addr and from_addr_norm == (strategy_addr or "").lower():
164
- return (
165
- strategy_pk or os.getenv("PRIVATE_KEY_STRATEGY") or os.getenv("PRIVATE_KEY")
166
- )
161
+ return strategy_pk
167
162
 
168
- # Fallback to environment variables
169
- return os.getenv("PRIVATE_KEY_STRATEGY") or os.getenv("PRIVATE_KEY")
163
+ # No fallback - private keys must be in config or wallets.json
164
+ return None
170
165
 
171
166
 
172
167
  async def _get_abi(chain_id: int, address: str) -> str | None:
@@ -1,6 +1,6 @@
1
1
  # Wallet Abstraction Layer
2
2
 
3
- Wayfinder strategies interact with blockchains through a single abstraction: the `EvmTxn` interface defined in `wayfinder_paths/core/services/base.py`. The default implementation (`LocalEvmTxn`) signs transactions with private keys pulled from config/environment variables, while `WalletManager` resolves which provider to use at runtime.
3
+ Wayfinder strategies interact with blockchains through a single abstraction: the `EvmTxn` interface defined in `wayfinder_paths/core/services/base.py`. The default implementation (`LocalEvmTxn`) signs transactions with private keys pulled from config.json or wallets.json, while `WalletManager` resolves which provider to use at runtime.
4
4
 
5
5
  ## Pieces
6
6
 
@@ -46,9 +46,6 @@ class PrivyWallet(EvmTxn):
46
46
  async def read_erc20_allowance(self, chain_id: int, token_address: str, owner_address: str, spender_address: str) -> tuple[bool, Any]:
47
47
  ...
48
48
 
49
- async def approve_token(...):
50
- ...
51
-
52
49
  async def broadcast_transaction(...):
53
50
  ...
54
51
 
@@ -76,8 +73,8 @@ web3_service = DefaultWeb3Service(config, wallet_provider=custom_wallet)
76
73
  {
77
74
  "strategy": {
78
75
  "wallet_type": "local",
79
- "main_wallet": {"address": "0x...", "wallet_type": "local"},
80
- "strategy_wallet": {"address": "0x..."}
76
+ "main_wallet": { "address": "0x...", "wallet_type": "local" },
77
+ "strategy_wallet": { "address": "0x..." }
81
78
  }
82
79
  }
83
80
  ```
@@ -12,7 +12,7 @@ from pathlib import Path
12
12
 
13
13
  from loguru import logger
14
14
 
15
- from wayfinder_paths.core.config import StrategyJobConfig, load_config_from_env
15
+ from wayfinder_paths.core.config import StrategyJobConfig
16
16
  from wayfinder_paths.core.engine.manifest import load_manifest, validate_manifest
17
17
  from wayfinder_paths.core.engine.StrategyJob import StrategyJob
18
18
 
@@ -21,7 +21,6 @@ def load_strategy(
21
21
  strategy_name: str,
22
22
  *,
23
23
  strategy_config: dict | None = None,
24
- simulation: bool = False,
25
24
  api_key: str | None = None,
26
25
  ):
27
26
  """
@@ -30,7 +29,6 @@ def load_strategy(
30
29
  Args:
31
30
  strategy_name: Name of the strategy to load (directory name in strategies/)
32
31
  strategy_config: Configuration dict for the strategy
33
- simulation: Enable simulation mode for testing
34
32
  api_key: Optional API key for service account authentication
35
33
 
36
34
  Returns:
@@ -59,36 +57,45 @@ def load_strategy(
59
57
  module = __import__(module_path, fromlist=[class_name])
60
58
  strategy_class = getattr(module, class_name)
61
59
 
62
- return strategy_class(
63
- config=strategy_config, simulation=simulation, api_key=api_key
64
- )
60
+ return strategy_class(config=strategy_config, api_key=api_key)
65
61
 
66
62
 
67
63
  def load_config(
68
64
  config_path: str | None = None, strategy_name: str | None = None
69
65
  ) -> StrategyJobConfig:
70
66
  """
71
- Load configuration from file or environment
67
+ Load configuration from config.json file
72
68
 
73
69
  Args:
74
- config_path: Optional path to config file
70
+ config_path: Path to config file (defaults to "config.json")
75
71
  strategy_name: Optional strategy name for per-strategy wallet lookup
76
72
 
77
73
  Returns:
78
74
  StrategyJobConfig instance
75
+
76
+ Raises:
77
+ FileNotFoundError: If config file does not exist
79
78
  """
80
- if config_path and Path(config_path).exists():
81
- logger.info(f"Loading config from {config_path}")
82
- with open(config_path) as f:
83
- config_data = json.load(f)
84
- return StrategyJobConfig.from_dict(config_data, strategy_name=strategy_name)
85
- else:
86
- logger.info("Loading config from environment variables")
87
- config = load_config_from_env()
88
- if strategy_name:
89
- config.strategy_config["_strategy_name"] = strategy_name
90
- config.__post_init__()
91
- return config
79
+ # Default to config.json if not provided
80
+ if not config_path:
81
+ config_path = "config.json"
82
+
83
+ config_file = Path(config_path)
84
+ if not config_file.exists():
85
+ raise FileNotFoundError(
86
+ f"Config file not found: {config_path}. "
87
+ "Please create config.json (see wayfinder_paths/config.example.json for template)"
88
+ )
89
+
90
+ logger.info(f"Loading config from {config_path}")
91
+ with open(config_file) as f:
92
+ config_data = json.load(f)
93
+
94
+ config = StrategyJobConfig.from_dict(config_data, strategy_name=strategy_name)
95
+ if strategy_name:
96
+ config.strategy_config["_strategy_name"] = strategy_name
97
+ config.__post_init__()
98
+ return config
92
99
 
93
100
 
94
101
  async def run_strategy(
@@ -96,7 +103,6 @@ async def run_strategy(
96
103
  config_path: str | None = None,
97
104
  action: str = "run",
98
105
  manifest_path: str | None = None,
99
- simulation: bool = False,
100
106
  **kwargs,
101
107
  ):
102
108
  """
@@ -159,9 +165,7 @@ async def run_strategy(
159
165
  module_path, class_name = manifest.entrypoint.rsplit(".", 1)
160
166
  module = __import__(module_path, fromlist=[class_name])
161
167
  strategy_class = getattr(module, class_name)
162
- strategy = strategy_class(
163
- config=config.strategy_config, simulation=simulation
164
- )
168
+ strategy = strategy_class(config=config.strategy_config)
165
169
  logger.info(
166
170
  f"Loaded strategy from manifest: {strategy_name_for_wallet or 'unnamed'}"
167
171
  )
@@ -169,7 +173,6 @@ async def run_strategy(
169
173
  strategy = load_strategy(
170
174
  strategy_name,
171
175
  strategy_config=config.strategy_config,
172
- simulation=simulation,
173
176
  )
174
177
  logger.info(f"Loaded strategy: {strategy.name}")
175
178
 
@@ -326,7 +329,7 @@ def main():
326
329
  help="Path to strategy manifest YAML (alternative to strategy name)",
327
330
  )
328
331
  parser.add_argument(
329
- "--config", help="Path to config file (defaults to environment variables)"
332
+ "--config", help="Path to config file (defaults to config.json)"
330
333
  )
331
334
  parser.add_argument(
332
335
  "--action",
@@ -372,11 +375,6 @@ def main():
372
375
  "--duration", type=int, help="Duration in seconds for script action"
373
376
  )
374
377
  parser.add_argument("--debug", action="store_true", help="Enable debug logging")
375
- parser.add_argument(
376
- "--simulation",
377
- action="store_true",
378
- help="Run in simulation mode (no real transactions)",
379
- )
380
378
  parser.add_argument(
381
379
  "--wallet-id",
382
380
  help="Wallet ID for policy rendering (replaces FORMAT_WALLET_ID in policies)",
@@ -401,7 +399,6 @@ def main():
401
399
  gas_token_amount=args.gas_token_amount,
402
400
  interval=args.interval,
403
401
  duration=args.duration,
404
- simulation=args.simulation,
405
402
  wallet_id=getattr(args, "wallet_id", None),
406
403
  )
407
404
  )
@@ -15,27 +15,6 @@ def to_keystore_json(private_key_hex: str, password: str):
15
15
  return Account.encrypt(private_key_hex, password)
16
16
 
17
17
 
18
- def write_env(rows: list[dict[str, str]], out_dir: Path) -> None:
19
- with open(out_dir / ".env.example", "w") as f:
20
- if rows:
21
- label_to_wallet = {r.get("label"): r for r in rows if r.get("label")}
22
- main_w = (
23
- label_to_wallet.get("main") or label_to_wallet.get("default") or rows[0]
24
- )
25
- strategy_w = label_to_wallet.get("strategy")
26
-
27
- f.write("RPC_URL=https://rpc.ankr.com/eth\n")
28
- # Back-compat defaults
29
- f.write(f"PRIVATE_KEY={main_w['private_key_hex']}\n")
30
- f.write(f"FROM_ADDRESS={main_w['address']}\n")
31
- # Explicit main/strategy variables
32
- f.write(f"MAIN_WALLET_ADDRESS={main_w['address']}\n")
33
- if strategy_w:
34
- f.write(f"STRATEGY_WALLET_ADDRESS={strategy_w['address']}\n")
35
- # Optional: expose strategy private key for local dev only
36
- f.write(f"PRIVATE_KEY_STRATEGY={strategy_w['private_key_hex']}\n")
37
-
38
-
39
18
  def main():
40
19
  parser = argparse.ArgumentParser(description="Generate local dev wallets")
41
20
  parser.add_argument(
@@ -48,7 +27,7 @@ def main():
48
27
  "--out-dir",
49
28
  type=Path,
50
29
  default=Path("."),
51
- help="Output directory for wallets.json (and .env/keystore)",
30
+ help="Output directory for wallets.json (and keystore files)",
52
31
  )
53
32
  parser.add_argument(
54
33
  "--keystore-password",
@@ -161,9 +140,6 @@ def main():
161
140
  ks_path.write_text(json.dumps(ks))
162
141
  index += 1
163
142
 
164
- # Convenience outputs
165
- write_env(rows, args.out_dir)
166
-
167
143
 
168
144
  if __name__ == "__main__":
169
145
  main()
@@ -55,7 +55,6 @@ async def _run(args: argparse.Namespace) -> int:
55
55
  "main_wallet": main_wallet,
56
56
  "strategy_wallet": strategy_wallet,
57
57
  },
58
- simulation=args.simulation,
59
58
  )
60
59
 
61
60
  await s.setup()
@@ -101,7 +100,6 @@ def main() -> int:
101
100
  )
102
101
  p.add_argument("--main-wallet-label", default="main")
103
102
  p.add_argument("--strategy-wallet-label", default="basis_trading_strategy")
104
- p.add_argument("--simulation", action="store_true")
105
103
 
106
104
  sub = p.add_subparsers(dest="command", required=True)
107
105
 
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  import asyncio
10
10
  import json
11
- import os
12
11
  import time
13
12
  from datetime import UTC, datetime
14
13
  from decimal import ROUND_DOWN, Decimal
@@ -1007,5 +1006,4 @@ class BasisSnapshotMixin:
1007
1006
  )
1008
1007
  if isinstance(val, str) and val.strip():
1009
1008
  return val.strip()
1010
- env = os.getenv("BASIS_SNAPSHOT_PATH")
1011
- return env.strip() if isinstance(env, str) and env.strip() else None
1009
+ return None
@@ -208,7 +208,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
208
208
  *,
209
209
  main_wallet: dict[str, Any] | None = None,
210
210
  strategy_wallet: dict[str, Any] | None = None,
211
- simulation: bool = False,
212
211
  web3_service: Web3Service | None = None,
213
212
  hyperliquid_executor: HyperliquidExecutor | None = None,
214
213
  api_key: str | None = None,
@@ -221,7 +220,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
221
220
  if strategy_wallet:
222
221
  merged_config["strategy_wallet"] = strategy_wallet
223
222
  self.config = merged_config
224
- self.simulation = simulation
225
223
 
226
224
  # Position tracking
227
225
  self.current_position: BasisPosition | None = None
@@ -249,10 +247,10 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
249
247
  "strategy": self.config,
250
248
  }
251
249
 
252
- # Create Hyperliquid executor if not provided and not in simulation.
250
+ # Create Hyperliquid executor if not provided.
253
251
  # This is only required for placing/canceling orders (not market reads).
254
252
  hl_executor = hyperliquid_executor
255
- if hl_executor is None and not self.simulation:
253
+ if hl_executor is None:
256
254
  try:
257
255
  hl_executor = LocalHyperliquidExecutor(config=adapter_config)
258
256
  self.logger.info("Created LocalHyperliquidExecutor for real execution")
@@ -267,7 +265,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
267
265
  try:
268
266
  self.hyperliquid_adapter = HyperliquidAdapter(
269
267
  config=adapter_config,
270
- simulation=self.simulation,
271
268
  executor=hl_executor,
272
269
  )
273
270
  except Exception as e:
@@ -280,7 +277,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
280
277
  tx_adapter = LocalTokenTxnService(
281
278
  adapter_config,
282
279
  wallet_provider=wallet_provider,
283
- simulation=self.simulation,
284
280
  )
285
281
  web3_service = DefaultWeb3Service(
286
282
  wallet_provider=wallet_provider, evm_transactions=tx_adapter
@@ -502,18 +498,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
502
498
  return (False, f"Failed to transfer ETH for gas: {gas_res}")
503
499
  self.logger.info(f"Gas transfer successful: {gas_res}")
504
500
 
505
- # Simulation mode - just track the deposit
506
- if self.simulation:
507
- self.logger.info(
508
- f"[SIMULATION] Would send {main_token_amount} USDC to Hyperliquid bridge"
509
- )
510
- self.deposit_amount = main_token_amount
511
- return (
512
- True,
513
- f"[SIMULATION] Deposited {main_token_amount} USDC. "
514
- f"Call update() to analyze and open positions.",
515
- )
516
-
517
501
  # Real deposit: ensure funds are in the strategy wallet, then send USDC to bridge.
518
502
  try:
519
503
  main_address = self._get_main_wallet_address()
@@ -926,11 +910,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
926
910
  if not close_success:
927
911
  return (False, f"Failed to close position: {close_msg}")
928
912
 
929
- if self.simulation:
930
- withdrawn = self.deposit_amount
931
- self.deposit_amount = 0
932
- return (True, f"[SIMULATION] Withdrew {withdrawn} USDC to main wallet")
933
-
934
913
  # Step 1: Transfer any spot USDC to perp for withdrawal
935
914
  success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
936
915
  address
@@ -1168,9 +1147,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1168
1147
  if not self.builder_fee:
1169
1148
  return True, "No builder fee configured"
1170
1149
 
1171
- if self.simulation:
1172
- return True, "[SIMULATION] Builder fee approval skipped"
1173
-
1174
1150
  address = self._get_strategy_wallet_address()
1175
1151
  builder = self.builder_fee.get("b", "")
1176
1152
  required_fee = self.builder_fee.get("f", 0)
@@ -1357,112 +1333,98 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1357
1333
  Decimal(str(order_usd)).quantize(Decimal("0.01"), rounding=ROUND_UP)
1358
1334
  )
1359
1335
 
1360
- if self.simulation:
1361
- self.logger.info(
1362
- f"[SIMULATION] Would open {target_qty} {coin} basis position"
1363
- )
1364
- spot_filled = target_qty
1365
- perp_filled = target_qty
1366
- spot_notional = order_usd
1367
- perp_notional = order_usd
1368
- entry_price = float(best.get("mark_price", 0.0) or 100.0)
1369
- else:
1370
- # Step 1: Ensure builder fee is approved
1371
- fee_success, fee_msg = await self.ensure_builder_fee_approved()
1372
- if not fee_success:
1373
- return (False, f"Builder fee approval failed: {fee_msg}")
1374
-
1375
- # Step 2: Update leverage for the perp asset
1376
- self.logger.info(f"Setting leverage to {leverage}x for {coin}")
1377
- success, lev_result = await self.hyperliquid_adapter.update_leverage(
1378
- asset_id=perp_asset_id,
1379
- leverage=leverage,
1380
- is_cross=True,
1381
- address=address,
1382
- )
1383
- if not success:
1384
- self.logger.warning(f"Failed to set leverage: {lev_result}")
1385
- # Continue anyway - leverage might already be set
1336
+ # Step 1: Ensure builder fee is approved
1337
+ fee_success, fee_msg = await self.ensure_builder_fee_approved()
1338
+ if not fee_success:
1339
+ return (False, f"Builder fee approval failed: {fee_msg}")
1386
1340
 
1387
- # Step 3: Transfer USDC from perp to spot for spot purchase
1388
- # We need approximately order_usd in spot to buy the asset
1389
- self.logger.info(
1390
- f"Transferring ${order_usd:.2f} from perp to spot for {coin}"
1391
- )
1392
- (
1393
- success,
1394
- transfer_result,
1395
- ) = await self.hyperliquid_adapter.transfer_perp_to_spot(
1396
- amount=order_usd,
1397
- address=address,
1398
- )
1399
- if not success:
1400
- self.logger.warning(
1401
- f"Perp to spot transfer failed: {transfer_result}"
1402
- )
1403
- # May fail if already in spot, continue
1341
+ # Step 2: Update leverage for the perp asset
1342
+ self.logger.info(f"Setting leverage to {leverage}x for {coin}")
1343
+ success, lev_result = await self.hyperliquid_adapter.update_leverage(
1344
+ asset_id=perp_asset_id,
1345
+ leverage=leverage,
1346
+ is_cross=True,
1347
+ address=address,
1348
+ )
1349
+ if not success:
1350
+ self.logger.warning(f"Failed to set leverage: {lev_result}")
1351
+ # Continue anyway - leverage might already be set
1404
1352
 
1405
- # Step 4: Execute paired fill
1406
- filler = PairedFiller(
1407
- adapter=self.hyperliquid_adapter,
1408
- address=address,
1409
- cfg=FillConfig(max_slip_bps=35, max_chunk_usd=7500.0),
1410
- )
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,
1363
+ address=address,
1364
+ )
1365
+ if not success:
1366
+ self.logger.warning(f"Perp to spot transfer failed: {transfer_result}")
1367
+ # May fail if already in spot, continue
1411
1368
 
1412
- (
1413
- spot_filled,
1414
- perp_filled,
1415
- spot_notional,
1416
- perp_notional,
1417
- spot_pointers,
1418
- perp_pointers,
1419
- ) = await filler.fill_pair_units(
1420
- coin=coin,
1421
- spot_asset_id=spot_asset_id,
1422
- perp_asset_id=perp_asset_id,
1423
- total_units=target_qty,
1424
- direction="long_spot_short_perp",
1425
- builder_fee=self.builder_fee,
1426
- )
1369
+ # Step 4: Execute paired fill
1370
+ filler = PairedFiller(
1371
+ adapter=self.hyperliquid_adapter,
1372
+ address=address,
1373
+ cfg=FillConfig(max_slip_bps=35, max_chunk_usd=7500.0),
1374
+ )
1427
1375
 
1428
- if spot_filled <= 0 or perp_filled <= 0:
1429
- return (False, f"Failed to fill basis position on {coin}")
1376
+ (
1377
+ spot_filled,
1378
+ perp_filled,
1379
+ spot_notional,
1380
+ perp_notional,
1381
+ spot_pointers,
1382
+ perp_pointers,
1383
+ ) = await filler.fill_pair_units(
1384
+ coin=coin,
1385
+ spot_asset_id=spot_asset_id,
1386
+ perp_asset_id=perp_asset_id,
1387
+ total_units=target_qty,
1388
+ direction="long_spot_short_perp",
1389
+ builder_fee=self.builder_fee,
1390
+ )
1430
1391
 
1431
- self.logger.info(
1432
- f"Filled basis position: spot={spot_filled:.6f}, perp={perp_filled:.6f}, "
1433
- f"notional=${spot_notional:.2f}/${perp_notional:.2f}"
1434
- )
1392
+ if spot_filled <= 0 or perp_filled <= 0:
1393
+ return (False, f"Failed to fill basis position on {coin}")
1435
1394
 
1436
- # Get entry price from current mid
1437
- success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
1438
- entry_price = mids.get(coin, 0.0) if success else 0.0
1395
+ self.logger.info(
1396
+ f"Filled basis position: spot={spot_filled:.6f}, perp={perp_filled:.6f}, "
1397
+ f"notional=${spot_notional:.2f}/${perp_notional:.2f}"
1398
+ )
1439
1399
 
1440
- # Step 5: Get liquidation price and place stop-loss
1441
- success, user_state = await self.hyperliquid_adapter.get_user_state(
1442
- address
1443
- )
1444
- liquidation_price = None
1445
- if success:
1446
- for pos_wrapper in user_state.get("assetPositions", []):
1447
- pos = pos_wrapper.get("position", {})
1448
- if pos.get("coin") == coin:
1449
- liquidation_price = float(pos.get("liquidationPx", 0))
1450
- break
1400
+ # Get entry price from current mid
1401
+ success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
1402
+ entry_price = mids.get(coin, 0.0) if success else 0.0
1451
1403
 
1452
- if liquidation_price and liquidation_price > 0:
1453
- sl_success, sl_msg = await self._place_stop_loss_orders(
1454
- coin=coin,
1455
- perp_asset_id=perp_asset_id,
1456
- position_size=perp_filled,
1457
- entry_price=entry_price,
1458
- liquidation_price=liquidation_price,
1459
- spot_asset_id=spot_asset_id,
1460
- spot_position_size=spot_filled,
1461
- )
1462
- if not sl_success:
1463
- self.logger.warning(f"Stop-loss placement failed: {sl_msg}")
1464
- else:
1465
- self.logger.warning("Could not get liquidation price for stop-loss")
1404
+ # Step 5: Get liquidation price and place stop-loss
1405
+ success, user_state = await self.hyperliquid_adapter.get_user_state(address)
1406
+ liquidation_price = None
1407
+ if success:
1408
+ for pos_wrapper in user_state.get("assetPositions", []):
1409
+ pos = pos_wrapper.get("position", {})
1410
+ if pos.get("coin") == coin:
1411
+ liquidation_price = float(pos.get("liquidationPx", 0))
1412
+ break
1413
+
1414
+ if liquidation_price and liquidation_price > 0:
1415
+ sl_success, sl_msg = await self._place_stop_loss_orders(
1416
+ coin=coin,
1417
+ perp_asset_id=perp_asset_id,
1418
+ position_size=perp_filled,
1419
+ entry_price=entry_price,
1420
+ liquidation_price=liquidation_price,
1421
+ spot_asset_id=spot_asset_id,
1422
+ spot_position_size=spot_filled,
1423
+ )
1424
+ if not sl_success:
1425
+ self.logger.warning(f"Stop-loss placement failed: {sl_msg}")
1426
+ else:
1427
+ self.logger.warning("Could not get liquidation price for stop-loss")
1466
1428
 
1467
1429
  # Create position record
1468
1430
  self.current_position = BasisPosition(
@@ -1836,9 +1798,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1836
1798
  if self.current_position is None:
1837
1799
  return True, "No position"
1838
1800
 
1839
- if self.simulation:
1840
- return True, "[SIMULATION] Would repair leg imbalance"
1841
-
1842
1801
  pos = self.current_position
1843
1802
  coin = pos.coin
1844
1803
  address = self._get_strategy_wallet_address()
@@ -1930,9 +1889,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1930
1889
  if self.current_position is None:
1931
1890
  return True, "No position"
1932
1891
 
1933
- if self.simulation:
1934
- return True, "[SIMULATION] Stop-loss check skipped"
1935
-
1936
1892
  pos = self.current_position
1937
1893
  coin = pos.coin
1938
1894
 
@@ -2026,13 +1982,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2026
1982
  pos = self.current_position
2027
1983
  self.logger.info(f"Closing position on {pos.coin}")
2028
1984
 
2029
- if self.simulation:
2030
- self.logger.info(
2031
- f"[SIMULATION] Would close {pos.spot_amount} {pos.coin} basis position"
2032
- )
2033
- self.current_position = None
2034
- return (True, "Position closed (simulation)")
2035
-
2036
1985
  # Cancel all stop-loss and limit orders first
2037
1986
  await self._cancel_all_position_orders()
2038
1987