wayfinder-paths 0.1.6__py3-none-any.whl → 0.1.8__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 (50) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +0 -10
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +0 -20
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -30
  4. wayfinder_paths/adapters/brap_adapter/adapter.py +3 -2
  5. wayfinder_paths/adapters/brap_adapter/test_adapter.py +9 -13
  6. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +14 -7
  7. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
  8. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
  9. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
  10. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
  11. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
  12. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
  13. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
  14. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
  15. wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
  16. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +7 -6
  17. wayfinder_paths/adapters/pool_adapter/README.md +3 -28
  18. wayfinder_paths/adapters/pool_adapter/adapter.py +0 -72
  19. wayfinder_paths/adapters/pool_adapter/examples.json +0 -43
  20. wayfinder_paths/adapters/pool_adapter/test_adapter.py +4 -54
  21. wayfinder_paths/adapters/token_adapter/test_adapter.py +4 -14
  22. wayfinder_paths/core/adapters/models.py +9 -4
  23. wayfinder_paths/core/analytics/__init__.py +11 -0
  24. wayfinder_paths/core/analytics/bootstrap.py +57 -0
  25. wayfinder_paths/core/analytics/stats.py +48 -0
  26. wayfinder_paths/core/analytics/test_analytics.py +170 -0
  27. wayfinder_paths/core/clients/BRAPClient.py +1 -0
  28. wayfinder_paths/core/clients/LedgerClient.py +2 -7
  29. wayfinder_paths/core/clients/PoolClient.py +0 -16
  30. wayfinder_paths/core/clients/WalletClient.py +0 -27
  31. wayfinder_paths/core/clients/protocols.py +104 -18
  32. wayfinder_paths/scripts/make_wallets.py +9 -0
  33. wayfinder_paths/scripts/run_strategy.py +124 -0
  34. wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
  35. wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
  36. wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
  37. wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
  38. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
  39. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
  40. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
  41. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
  42. wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
  43. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +1 -9
  44. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +36 -5
  45. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +367 -278
  46. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +204 -7
  47. {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/METADATA +32 -3
  48. {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/RECORD +50 -27
  49. {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/LICENSE +0 -0
  50. {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/WHEEL +0 -0
@@ -0,0 +1,549 @@
1
+ """
2
+ Hyperliquid Executor Protocol and Implementations.
3
+
4
+ Defines the interface for Hyperliquid order execution and a local-signing implementation.
5
+
6
+ Other execution environments can provide their own `HyperliquidExecutor` that satisfies
7
+ the protocol (for example, by delegating signing to a hosted signer).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import uuid
13
+ from typing import Any
14
+
15
+ from loguru import logger
16
+
17
+ from wayfinder_paths.core.clients.protocols import HyperliquidExecutorProtocol
18
+
19
+ # Re-export for backwards compatibility with existing imports.
20
+ HyperliquidExecutor = HyperliquidExecutorProtocol
21
+
22
+ try:
23
+ from eth_account import Account
24
+ from hyperliquid.exchange import Exchange
25
+ from hyperliquid.info import Info
26
+ from hyperliquid.utils import constants
27
+ from hyperliquid.utils.types import BuilderInfo, Cloid
28
+
29
+ HYPERLIQUID_AVAILABLE = True
30
+ except ImportError:
31
+ HYPERLIQUID_AVAILABLE = False
32
+ Account = None
33
+ Exchange = None
34
+ Info = None
35
+ constants = None
36
+ Cloid = None
37
+ BuilderInfo = None
38
+
39
+
40
+ def _new_client_id() -> Cloid:
41
+ """Generate a new client order ID as a Cloid object."""
42
+ cloid_str = "0x" + uuid.uuid4().hex
43
+ return Cloid(cloid_str)
44
+
45
+
46
+ class LocalHyperliquidExecutor:
47
+ """
48
+ Local Hyperliquid executor using SDK with private key signing.
49
+
50
+ Uses the hyperliquid SDK's Exchange class which handles EIP-712 signing internally.
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ *,
56
+ config: dict[str, Any],
57
+ network: str = "mainnet",
58
+ ) -> None:
59
+ if not HYPERLIQUID_AVAILABLE:
60
+ raise ImportError(
61
+ "hyperliquid package not installed. Install with: poetry add hyperliquid"
62
+ )
63
+
64
+ self.config = config
65
+ self.network = network
66
+
67
+ # Resolve private key from config
68
+ self._private_key = self._resolve_private_key(config)
69
+ if not self._private_key:
70
+ raise ValueError(
71
+ "No private key found in config. "
72
+ "Provide strategy_wallet.private_key_hex or strategy_wallet.private_key"
73
+ )
74
+
75
+ # Create wallet account
76
+ pk = self._private_key
77
+ if not pk.startswith("0x"):
78
+ pk = "0x" + pk
79
+ self._wallet = Account.from_key(pk)
80
+
81
+ # Initialize SDK clients
82
+ base_url = (
83
+ constants.MAINNET_API_URL
84
+ if network == "mainnet"
85
+ else constants.TESTNET_API_URL
86
+ )
87
+ self.info = Info(base_url, skip_ws=True)
88
+ self.exchange = Exchange(self._wallet, base_url)
89
+
90
+ logger.info(
91
+ f"LocalHyperliquidExecutor initialized for address: {self._wallet.address}"
92
+ )
93
+
94
+ def _resolve_private_key(self, config: dict[str, Any]) -> str | None:
95
+ """Extract private key from config."""
96
+ # Try strategy_wallet first
97
+ strategy_wallet = config.get("strategy_wallet", {})
98
+ if isinstance(strategy_wallet, dict):
99
+ pk = strategy_wallet.get("private_key_hex") or strategy_wallet.get(
100
+ "private_key"
101
+ )
102
+ if pk:
103
+ return pk
104
+
105
+ # Try main_wallet as fallback (for single-wallet setups)
106
+ main_wallet = config.get("main_wallet", {})
107
+ if isinstance(main_wallet, dict):
108
+ pk = main_wallet.get("private_key_hex") or main_wallet.get("private_key")
109
+ if pk:
110
+ return pk
111
+
112
+ return None
113
+
114
+ @property
115
+ def address(self) -> str:
116
+ """Get the wallet address."""
117
+ return self._wallet.address
118
+
119
+ async def place_market_order(
120
+ self,
121
+ *,
122
+ asset_id: int,
123
+ is_buy: bool,
124
+ slippage: float,
125
+ size: float,
126
+ address: str,
127
+ reduce_only: bool = False,
128
+ cloid: Any = None,
129
+ builder: dict[str, Any] | None = None,
130
+ ) -> dict[str, Any]:
131
+ """Place a market order using the SDK.
132
+
133
+ Args:
134
+ builder: Optional builder fee config with keys 'b' (address) and 'f' (fee bps)
135
+ """
136
+ if cloid is None:
137
+ cloid = _new_client_id()
138
+ elif isinstance(cloid, str):
139
+ cloid = Cloid(cloid)
140
+
141
+ # Convert builder dict to BuilderInfo if provided
142
+ builder_info = None
143
+ if builder:
144
+ builder_info = BuilderInfo(b=builder.get("b", ""), f=builder.get("f", 0))
145
+
146
+ # Validate address matches our wallet
147
+ if address.lower() != self._wallet.address.lower():
148
+ return {
149
+ "status": "err",
150
+ "response": {
151
+ "type": "error",
152
+ "data": f"Address mismatch: expected {self._wallet.address}, got {address}",
153
+ },
154
+ }
155
+
156
+ try:
157
+ # The SDK's market_open handles slippage internally
158
+ # For spot (asset_id >= 10000), use different method
159
+ is_spot = asset_id >= 10000
160
+
161
+ if is_spot:
162
+ # Spot market order. Hyperliquid spot uses `@{spot_index}` where
163
+ # spot_index == spot_asset_id - 10000.
164
+ spot_index = asset_id - 10000
165
+ result = self.exchange.market_open(
166
+ name=f"@{spot_index}",
167
+ is_buy=is_buy,
168
+ sz=size,
169
+ slippage=slippage,
170
+ cloid=cloid,
171
+ builder=builder_info,
172
+ )
173
+ else:
174
+ # Perp market order
175
+ coin = self.info.asset_to_coin.get(asset_id)
176
+ if not coin:
177
+ return {
178
+ "status": "err",
179
+ "response": {
180
+ "type": "error",
181
+ "data": f"Unknown asset_id: {asset_id}",
182
+ },
183
+ }
184
+
185
+ if reduce_only:
186
+ result = self.exchange.market_close(
187
+ coin=coin,
188
+ sz=size,
189
+ slippage=slippage,
190
+ cloid=cloid,
191
+ builder=builder_info,
192
+ )
193
+ else:
194
+ result = self.exchange.market_open(
195
+ name=coin,
196
+ is_buy=is_buy,
197
+ sz=size,
198
+ slippage=slippage,
199
+ cloid=cloid,
200
+ builder=builder_info,
201
+ )
202
+
203
+ logger.debug(f"Market order result: {result}")
204
+ return result
205
+
206
+ except Exception as exc:
207
+ logger.error(f"Market order failed: {exc}")
208
+ return {
209
+ "status": "err",
210
+ "response": {"type": "error", "data": str(exc)},
211
+ }
212
+
213
+ async def cancel_order(
214
+ self,
215
+ *,
216
+ asset_id: int,
217
+ order_id: int,
218
+ address: str,
219
+ ) -> dict[str, Any]:
220
+ """Cancel an open order."""
221
+ if address.lower() != self._wallet.address.lower():
222
+ return {
223
+ "status": "err",
224
+ "response": {"type": "error", "data": "Address mismatch"},
225
+ }
226
+
227
+ try:
228
+ # Resolve coin name
229
+ is_spot = asset_id >= 10000
230
+ if is_spot:
231
+ spot_index = asset_id - 10000
232
+ coin = f"@{spot_index}"
233
+ else:
234
+ coin = self.info.asset_to_coin.get(asset_id)
235
+ if not coin:
236
+ return {
237
+ "status": "err",
238
+ "response": {
239
+ "type": "error",
240
+ "data": f"Unknown asset_id: {asset_id}",
241
+ },
242
+ }
243
+
244
+ result = self.exchange.cancel(name=coin, oid=order_id)
245
+ logger.debug(f"Cancel order result: {result}")
246
+ return result
247
+
248
+ except Exception as exc:
249
+ logger.error(f"Cancel order failed: {exc}")
250
+ return {
251
+ "status": "err",
252
+ "response": {"type": "error", "data": str(exc)},
253
+ }
254
+
255
+ async def update_leverage(
256
+ self,
257
+ *,
258
+ asset_id: int,
259
+ leverage: int,
260
+ is_cross: bool,
261
+ address: str,
262
+ ) -> dict[str, Any]:
263
+ """Update leverage for an asset."""
264
+ if address.lower() != self._wallet.address.lower():
265
+ return {
266
+ "status": "err",
267
+ "response": {"type": "error", "data": "Address mismatch"},
268
+ }
269
+
270
+ try:
271
+ coin = self.info.asset_to_coin.get(asset_id)
272
+ if not coin:
273
+ return {
274
+ "status": "err",
275
+ "response": {
276
+ "type": "error",
277
+ "data": f"Unknown asset_id: {asset_id}",
278
+ },
279
+ }
280
+
281
+ result = self.exchange.update_leverage(
282
+ leverage=leverage,
283
+ name=coin,
284
+ is_cross=is_cross,
285
+ )
286
+ logger.debug(f"Update leverage result: {result}")
287
+ return result
288
+
289
+ except Exception as exc:
290
+ logger.error(f"Update leverage failed: {exc}")
291
+ return {
292
+ "status": "err",
293
+ "response": {"type": "error", "data": str(exc)},
294
+ }
295
+
296
+ async def transfer_spot_to_perp(
297
+ self,
298
+ *,
299
+ amount: float,
300
+ address: str,
301
+ ) -> dict[str, Any]:
302
+ """Transfer USDC from spot to perp balance."""
303
+ if address.lower() != self._wallet.address.lower():
304
+ return {
305
+ "status": "err",
306
+ "response": {"type": "error", "data": "Address mismatch"},
307
+ }
308
+
309
+ try:
310
+ result = self.exchange.usd_class_transfer(
311
+ amount=amount,
312
+ to_perp=True,
313
+ )
314
+ logger.debug(f"Spot to perp transfer result: {result}")
315
+ return result
316
+
317
+ except Exception as exc:
318
+ logger.error(f"Spot to perp transfer failed: {exc}")
319
+ return {
320
+ "status": "err",
321
+ "response": {"type": "error", "data": str(exc)},
322
+ }
323
+
324
+ async def transfer_perp_to_spot(
325
+ self,
326
+ *,
327
+ amount: float,
328
+ address: str,
329
+ ) -> dict[str, Any]:
330
+ """Transfer USDC from perp to spot balance."""
331
+ if address.lower() != self._wallet.address.lower():
332
+ return {
333
+ "status": "err",
334
+ "response": {"type": "error", "data": "Address mismatch"},
335
+ }
336
+
337
+ try:
338
+ result = self.exchange.usd_class_transfer(
339
+ amount=amount,
340
+ to_perp=False,
341
+ )
342
+ logger.debug(f"Perp to spot transfer result: {result}")
343
+ return result
344
+
345
+ except Exception as exc:
346
+ logger.error(f"Perp to spot transfer failed: {exc}")
347
+ return {
348
+ "status": "err",
349
+ "response": {"type": "error", "data": str(exc)},
350
+ }
351
+
352
+ async def place_stop_loss(
353
+ self,
354
+ *,
355
+ asset_id: int,
356
+ is_buy: bool,
357
+ trigger_price: float,
358
+ size: float,
359
+ address: str,
360
+ ) -> dict[str, Any]:
361
+ """Place a stop-loss order."""
362
+ if address.lower() != self._wallet.address.lower():
363
+ return {
364
+ "status": "err",
365
+ "response": {"type": "error", "data": "Address mismatch"},
366
+ }
367
+
368
+ try:
369
+ coin = self.info.asset_to_coin.get(asset_id)
370
+ if not coin:
371
+ return {
372
+ "status": "err",
373
+ "response": {
374
+ "type": "error",
375
+ "data": f"Unknown asset_id: {asset_id}",
376
+ },
377
+ }
378
+
379
+ # Use the SDK's order method with trigger order type
380
+ result = self.exchange.order(
381
+ name=coin,
382
+ is_buy=is_buy,
383
+ sz=size,
384
+ limit_px=trigger_price,
385
+ order_type={
386
+ "trigger": {
387
+ "triggerPx": trigger_price,
388
+ "isMarket": True,
389
+ "tpsl": "sl",
390
+ }
391
+ },
392
+ reduce_only=True,
393
+ )
394
+ logger.debug(f"Stop loss result: {result}")
395
+ return result
396
+
397
+ except Exception as exc:
398
+ logger.error(f"Place stop loss failed: {exc}")
399
+ return {
400
+ "status": "err",
401
+ "response": {"type": "error", "data": str(exc)},
402
+ }
403
+
404
+ async def place_limit_order(
405
+ self,
406
+ *,
407
+ asset_id: int,
408
+ is_buy: bool,
409
+ price: float,
410
+ size: float,
411
+ address: str,
412
+ reduce_only: bool = False,
413
+ builder: dict[str, Any] | None = None,
414
+ ) -> dict[str, Any]:
415
+ """
416
+ Place a limit order (GTC - Good Till Cancelled).
417
+
418
+ Used for spot stop-loss orders in basis trading.
419
+
420
+ Args:
421
+ asset_id: Asset ID (perp or spot)
422
+ is_buy: True for buy, False for sell
423
+ price: Limit price
424
+ size: Order size
425
+ address: Wallet address
426
+ reduce_only: If True, only reduces existing position
427
+ builder: Optional builder fee config
428
+ """
429
+ if address.lower() != self._wallet.address.lower():
430
+ return {
431
+ "status": "err",
432
+ "response": {"type": "error", "data": "Address mismatch"},
433
+ }
434
+
435
+ try:
436
+ # Resolve coin name
437
+ is_spot = asset_id >= 10000
438
+ if is_spot:
439
+ spot_index = asset_id - 10000
440
+ coin = f"@{spot_index}"
441
+ else:
442
+ coin = self.info.asset_to_coin.get(asset_id)
443
+ if not coin:
444
+ return {
445
+ "status": "err",
446
+ "response": {
447
+ "type": "error",
448
+ "data": f"Unknown asset_id: {asset_id}",
449
+ },
450
+ }
451
+
452
+ # Convert builder dict to BuilderInfo if provided
453
+ builder_info = None
454
+ if builder:
455
+ builder_info = BuilderInfo(
456
+ b=builder.get("b", ""), f=builder.get("f", 0)
457
+ )
458
+
459
+ # Place limit order using SDK
460
+ result = self.exchange.order(
461
+ name=coin,
462
+ is_buy=is_buy,
463
+ sz=size,
464
+ limit_px=price,
465
+ order_type={"limit": {"tif": "Gtc"}},
466
+ reduce_only=reduce_only,
467
+ builder=builder_info,
468
+ )
469
+ logger.debug(f"Limit order result: {result}")
470
+ return result
471
+
472
+ except Exception as exc:
473
+ logger.error(f"Place limit order failed: {exc}")
474
+ return {
475
+ "status": "err",
476
+ "response": {"type": "error", "data": str(exc)},
477
+ }
478
+
479
+ async def withdraw(
480
+ self,
481
+ *,
482
+ amount: float,
483
+ address: str,
484
+ ) -> dict[str, Any]:
485
+ """Withdraw USDC from Hyperliquid to Arbitrum."""
486
+ if address.lower() != self._wallet.address.lower():
487
+ return {
488
+ "status": "err",
489
+ "response": {"type": "error", "data": "Address mismatch"},
490
+ }
491
+
492
+ try:
493
+ # Use withdraw_from_bridge to withdraw to the wallet's own address on Arbitrum
494
+ result = self.exchange.withdraw_from_bridge(
495
+ amount=amount,
496
+ destination=address, # Withdraw to same address on Arbitrum
497
+ )
498
+ logger.debug(f"Withdraw result: {result}")
499
+ return result
500
+
501
+ except Exception as exc:
502
+ logger.error(f"Withdraw failed: {exc}")
503
+ return {
504
+ "status": "err",
505
+ "response": {"type": "error", "data": str(exc)},
506
+ }
507
+
508
+ async def approve_builder_fee(
509
+ self,
510
+ *,
511
+ builder: str,
512
+ max_fee_rate: str,
513
+ address: str,
514
+ ) -> dict[str, Any]:
515
+ """
516
+ Approve a builder fee for the user.
517
+
518
+ This signs and broadcasts an approveBuilderFee action that allows
519
+ the specified builder to charge up to max_fee_rate on trades.
520
+
521
+ Args:
522
+ builder: Builder wallet address
523
+ max_fee_rate: Fee rate as percentage string (e.g., "0.030%" for 30 tenths bp)
524
+ address: User wallet address (must match executor wallet)
525
+
526
+ Returns:
527
+ Dict with status "ok" or "err" and response data
528
+ """
529
+ if address.lower() != self._wallet.address.lower():
530
+ return {
531
+ "status": "err",
532
+ "response": {"type": "error", "data": "Address mismatch"},
533
+ }
534
+
535
+ try:
536
+ # The SDK's approve_builder_fee method handles the signing internally
537
+ result = self.exchange.approve_builder_fee(
538
+ builder=builder,
539
+ max_fee_rate=max_fee_rate,
540
+ )
541
+ logger.debug(f"Approve builder fee result: {result}")
542
+ return result
543
+
544
+ except Exception as exc:
545
+ logger.error(f"Approve builder fee failed: {exc}")
546
+ return {
547
+ "status": "err",
548
+ "response": {"type": "error", "data": str(exc)},
549
+ }
@@ -0,0 +1,8 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "adapters.hyperliquid_adapter.adapter.HyperliquidAdapter"
3
+ capabilities:
4
+ - "market.meta"
5
+ - "market.funding"
6
+ - "market.candles"
7
+ - "market.orderbook"
8
+ - "user.state"