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.
- wayfinder_paths/__init__.py +0 -2
- wayfinder_paths/adapters/balance_adapter/README.md +59 -45
- wayfinder_paths/adapters/balance_adapter/adapter.py +0 -21
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -14
- wayfinder_paths/adapters/brap_adapter/README.md +61 -184
- wayfinder_paths/adapters/brap_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/brap_adapter/adapter.py +0 -147
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +0 -15
- wayfinder_paths/adapters/hyperlend_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +0 -9
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +0 -17
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +3 -312
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +1 -71
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +0 -57
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +0 -17
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +2 -42
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +1 -9
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +15 -47
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +0 -7
- wayfinder_paths/adapters/ledger_adapter/README.md +54 -74
- wayfinder_paths/adapters/ledger_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/ledger_adapter/adapter.py +0 -106
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +0 -12
- wayfinder_paths/adapters/moonwell_adapter/README.md +67 -106
- wayfinder_paths/adapters/moonwell_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +9 -121
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +84 -83
- wayfinder_paths/adapters/pool_adapter/README.md +30 -51
- wayfinder_paths/adapters/pool_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/pool_adapter/adapter.py +0 -19
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -8
- wayfinder_paths/adapters/token_adapter/README.md +41 -49
- wayfinder_paths/adapters/token_adapter/adapter.py +0 -32
- wayfinder_paths/adapters/token_adapter/test_adapter.py +1 -12
- wayfinder_paths/conftest.py +0 -8
- wayfinder_paths/core/__init__.py +0 -2
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -22
- wayfinder_paths/core/adapters/__init__.py +0 -5
- wayfinder_paths/core/adapters/models.py +0 -5
- wayfinder_paths/core/analytics/__init__.py +0 -2
- wayfinder_paths/core/analytics/bootstrap.py +0 -16
- wayfinder_paths/core/analytics/stats.py +0 -7
- wayfinder_paths/core/analytics/test_analytics.py +5 -34
- wayfinder_paths/core/clients/BRAPClient.py +0 -35
- wayfinder_paths/core/clients/ClientManager.py +0 -51
- wayfinder_paths/core/clients/HyperlendClient.py +0 -77
- wayfinder_paths/core/clients/LedgerClient.py +2 -122
- wayfinder_paths/core/clients/PoolClient.py +0 -2
- wayfinder_paths/core/clients/TokenClient.py +0 -39
- wayfinder_paths/core/clients/WalletClient.py +0 -15
- wayfinder_paths/core/clients/WayfinderClient.py +0 -24
- wayfinder_paths/core/clients/__init__.py +0 -4
- wayfinder_paths/core/clients/protocols.py +25 -98
- wayfinder_paths/core/config.py +0 -24
- wayfinder_paths/core/constants/__init__.py +0 -7
- wayfinder_paths/core/constants/base.py +2 -9
- wayfinder_paths/core/constants/erc20_abi.py +0 -5
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -7
- wayfinder_paths/core/constants/moonwell_abi.py +0 -35
- wayfinder_paths/core/engine/StrategyJob.py +0 -32
- wayfinder_paths/core/strategies/Strategy.py +0 -99
- wayfinder_paths/core/strategies/__init__.py +0 -2
- wayfinder_paths/core/utils/__init__.py +0 -1
- wayfinder_paths/core/utils/erc20_service.py +0 -1
- wayfinder_paths/core/utils/evm_helpers.py +0 -50
- wayfinder_paths/core/utils/transaction.py +0 -1
- wayfinder_paths/run_strategy.py +0 -46
- wayfinder_paths/scripts/create_strategy.py +0 -17
- wayfinder_paths/scripts/make_wallets.py +1 -4
- wayfinder_paths/strategies/basis_trading_strategy/README.md +71 -163
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +0 -24
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +36 -400
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +15 -64
- wayfinder_paths/strategies/basis_trading_strategy/types.py +0 -4
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +65 -56
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +4 -27
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -10
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +71 -72
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +23 -227
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +120 -113
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +64 -59
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +4 -44
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +2 -35
- wayfinder_paths/templates/adapter/README.md +107 -46
- wayfinder_paths/templates/adapter/adapter.py +0 -9
- wayfinder_paths/templates/adapter/test_adapter.py +0 -19
- wayfinder_paths/templates/strategy/README.md +113 -59
- wayfinder_paths/templates/strategy/strategy.py +0 -22
- wayfinder_paths/templates/strategy/test_strategy.py +0 -28
- wayfinder_paths/tests/test_test_coverage.py +2 -12
- wayfinder_paths/tests/test_utils.py +1 -31
- wayfinder_paths-0.1.20.dist-info/METADATA +355 -0
- wayfinder_paths-0.1.20.dist-info/RECORD +129 -0
- wayfinder_paths/core/adapters/base.py +0 -5
- wayfinder_paths-0.1.19.dist-info/METADATA +0 -592
- wayfinder_paths-0.1.19.dist-info/RECORD +0 -130
- {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.20.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.20.dist-info}/WHEEL +0 -0
|
@@ -1,12 +1,3 @@
|
|
|
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
1
|
from __future__ import annotations
|
|
11
2
|
|
|
12
3
|
import uuid
|
|
@@ -39,18 +30,11 @@ except ImportError:
|
|
|
39
30
|
|
|
40
31
|
|
|
41
32
|
def _new_client_id() -> Cloid:
|
|
42
|
-
"""Generate a new client order ID as a Cloid object."""
|
|
43
33
|
cloid_str = "0x" + uuid.uuid4().hex
|
|
44
34
|
return Cloid(cloid_str)
|
|
45
35
|
|
|
46
36
|
|
|
47
37
|
class LocalHyperliquidExecutor:
|
|
48
|
-
"""
|
|
49
|
-
Local Hyperliquid executor using SDK with private key signing.
|
|
50
|
-
|
|
51
|
-
Uses the hyperliquid SDK's Exchange class which handles EIP-712 signing internally.
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
38
|
def __init__(
|
|
55
39
|
self,
|
|
56
40
|
*,
|
|
@@ -73,13 +57,11 @@ class LocalHyperliquidExecutor:
|
|
|
73
57
|
"Provide strategy_wallet.private_key_hex or strategy_wallet.private_key"
|
|
74
58
|
)
|
|
75
59
|
|
|
76
|
-
# Create wallet account
|
|
77
60
|
pk = self._private_key
|
|
78
61
|
if not pk.startswith("0x"):
|
|
79
62
|
pk = "0x" + pk
|
|
80
63
|
self._wallet = Account.from_key(pk)
|
|
81
64
|
|
|
82
|
-
# Initialize SDK clients
|
|
83
65
|
base_url = (
|
|
84
66
|
constants.MAINNET_API_URL
|
|
85
67
|
if network == "mainnet"
|
|
@@ -94,12 +76,6 @@ class LocalHyperliquidExecutor:
|
|
|
94
76
|
)
|
|
95
77
|
|
|
96
78
|
def _get_perp_coin(self, asset_id: int) -> str | None:
|
|
97
|
-
"""
|
|
98
|
-
Resolve a perp coin name from a perp asset_id.
|
|
99
|
-
|
|
100
|
-
Note: newer versions of the hyperliquid SDK expose `coin_to_asset` but not
|
|
101
|
-
`asset_to_coin`, so we build the reverse mapping when needed.
|
|
102
|
-
"""
|
|
103
79
|
if self._asset_id_to_coin is None:
|
|
104
80
|
mapping: dict[int, str] = {}
|
|
105
81
|
|
|
@@ -131,7 +107,6 @@ class LocalHyperliquidExecutor:
|
|
|
131
107
|
return self._asset_id_to_coin.get(asset_id) if self._asset_id_to_coin else None
|
|
132
108
|
|
|
133
109
|
def _resolve_private_key(self, config: dict[str, Any]) -> str | None:
|
|
134
|
-
"""Extract private key from config."""
|
|
135
110
|
# Try strategy_wallet first
|
|
136
111
|
strategy_wallet = config.get("strategy_wallet", {})
|
|
137
112
|
if isinstance(strategy_wallet, dict):
|
|
@@ -152,7 +127,6 @@ class LocalHyperliquidExecutor:
|
|
|
152
127
|
|
|
153
128
|
@property
|
|
154
129
|
def address(self) -> str:
|
|
155
|
-
"""Get the wallet address."""
|
|
156
130
|
return self._wallet.address
|
|
157
131
|
|
|
158
132
|
async def place_market_order(
|
|
@@ -167,22 +141,15 @@ class LocalHyperliquidExecutor:
|
|
|
167
141
|
cloid: Any = None,
|
|
168
142
|
builder: dict[str, Any] | None = None,
|
|
169
143
|
) -> dict[str, Any]:
|
|
170
|
-
"""Place a market order using the SDK.
|
|
171
|
-
|
|
172
|
-
Args:
|
|
173
|
-
builder: Optional builder fee config with keys 'b' (address) and 'f' (fee bps)
|
|
174
|
-
"""
|
|
175
144
|
if cloid is None:
|
|
176
145
|
cloid = _new_client_id()
|
|
177
146
|
elif isinstance(cloid, str):
|
|
178
147
|
cloid = Cloid(cloid)
|
|
179
148
|
|
|
180
|
-
# Convert builder dict to BuilderInfo if provided
|
|
181
149
|
builder_info = None
|
|
182
150
|
if builder:
|
|
183
151
|
builder_info = BuilderInfo(b=builder.get("b", ""), f=builder.get("f", 0))
|
|
184
152
|
|
|
185
|
-
# Validate address matches our wallet
|
|
186
153
|
if address.lower() != self._wallet.address.lower():
|
|
187
154
|
return {
|
|
188
155
|
"status": "err",
|
|
@@ -193,7 +160,6 @@ class LocalHyperliquidExecutor:
|
|
|
193
160
|
}
|
|
194
161
|
|
|
195
162
|
try:
|
|
196
|
-
# The SDK's market_open handles slippage internally
|
|
197
163
|
# For spot (asset_id >= 10000), use different method
|
|
198
164
|
is_spot = asset_id >= 10000
|
|
199
165
|
|
|
@@ -256,7 +222,6 @@ class LocalHyperliquidExecutor:
|
|
|
256
222
|
order_id: int,
|
|
257
223
|
address: str,
|
|
258
224
|
) -> dict[str, Any]:
|
|
259
|
-
"""Cancel an open order."""
|
|
260
225
|
if address.lower() != self._wallet.address.lower():
|
|
261
226
|
return {
|
|
262
227
|
"status": "err",
|
|
@@ -299,7 +264,6 @@ class LocalHyperliquidExecutor:
|
|
|
299
264
|
is_cross: bool,
|
|
300
265
|
address: str,
|
|
301
266
|
) -> dict[str, Any]:
|
|
302
|
-
"""Update leverage for an asset."""
|
|
303
267
|
if address.lower() != self._wallet.address.lower():
|
|
304
268
|
return {
|
|
305
269
|
"status": "err",
|
|
@@ -338,7 +302,6 @@ class LocalHyperliquidExecutor:
|
|
|
338
302
|
amount: float,
|
|
339
303
|
address: str,
|
|
340
304
|
) -> dict[str, Any]:
|
|
341
|
-
"""Transfer USDC from spot to perp balance."""
|
|
342
305
|
if address.lower() != self._wallet.address.lower():
|
|
343
306
|
return {
|
|
344
307
|
"status": "err",
|
|
@@ -366,7 +329,6 @@ class LocalHyperliquidExecutor:
|
|
|
366
329
|
amount: float,
|
|
367
330
|
address: str,
|
|
368
331
|
) -> dict[str, Any]:
|
|
369
|
-
"""Transfer USDC from perp to spot balance."""
|
|
370
332
|
if address.lower() != self._wallet.address.lower():
|
|
371
333
|
return {
|
|
372
334
|
"status": "err",
|
|
@@ -397,7 +359,6 @@ class LocalHyperliquidExecutor:
|
|
|
397
359
|
size: float,
|
|
398
360
|
address: str,
|
|
399
361
|
) -> dict[str, Any]:
|
|
400
|
-
"""Place a stop-loss order."""
|
|
401
362
|
if address.lower() != self._wallet.address.lower():
|
|
402
363
|
return {
|
|
403
364
|
"status": "err",
|
|
@@ -451,20 +412,6 @@ class LocalHyperliquidExecutor:
|
|
|
451
412
|
reduce_only: bool = False,
|
|
452
413
|
builder: dict[str, Any] | None = None,
|
|
453
414
|
) -> dict[str, Any]:
|
|
454
|
-
"""
|
|
455
|
-
Place a limit order (GTC - Good Till Cancelled).
|
|
456
|
-
|
|
457
|
-
Used for spot stop-loss orders in basis trading.
|
|
458
|
-
|
|
459
|
-
Args:
|
|
460
|
-
asset_id: Asset ID (perp or spot)
|
|
461
|
-
is_buy: True for buy, False for sell
|
|
462
|
-
price: Limit price
|
|
463
|
-
size: Order size
|
|
464
|
-
address: Wallet address
|
|
465
|
-
reduce_only: If True, only reduces existing position
|
|
466
|
-
builder: Optional builder fee config
|
|
467
|
-
"""
|
|
468
415
|
if address.lower() != self._wallet.address.lower():
|
|
469
416
|
return {
|
|
470
417
|
"status": "err",
|
|
@@ -488,7 +435,6 @@ class LocalHyperliquidExecutor:
|
|
|
488
435
|
},
|
|
489
436
|
}
|
|
490
437
|
|
|
491
|
-
# Convert builder dict to BuilderInfo if provided
|
|
492
438
|
builder_info = None
|
|
493
439
|
if builder:
|
|
494
440
|
builder_info = BuilderInfo(
|
|
@@ -521,7 +467,6 @@ class LocalHyperliquidExecutor:
|
|
|
521
467
|
amount: float,
|
|
522
468
|
address: str,
|
|
523
469
|
) -> dict[str, Any]:
|
|
524
|
-
"""Withdraw USDC from Hyperliquid to Arbitrum."""
|
|
525
470
|
if address.lower() != self._wallet.address.lower():
|
|
526
471
|
return {
|
|
527
472
|
"status": "err",
|
|
@@ -532,7 +477,7 @@ class LocalHyperliquidExecutor:
|
|
|
532
477
|
# Use withdraw_from_bridge to withdraw to the wallet's own address on Arbitrum
|
|
533
478
|
result = self.exchange.withdraw_from_bridge(
|
|
534
479
|
amount=amount,
|
|
535
|
-
destination=address,
|
|
480
|
+
destination=address,
|
|
536
481
|
)
|
|
537
482
|
logger.debug(f"Withdraw result: {result}")
|
|
538
483
|
return result
|
|
@@ -551,20 +496,6 @@ class LocalHyperliquidExecutor:
|
|
|
551
496
|
max_fee_rate: str,
|
|
552
497
|
address: str,
|
|
553
498
|
) -> dict[str, Any]:
|
|
554
|
-
"""
|
|
555
|
-
Approve a builder fee for the user.
|
|
556
|
-
|
|
557
|
-
This signs and broadcasts an approveBuilderFee action that allows
|
|
558
|
-
the specified builder to charge up to max_fee_rate on trades.
|
|
559
|
-
|
|
560
|
-
Args:
|
|
561
|
-
builder: Builder wallet address
|
|
562
|
-
max_fee_rate: Fee rate as percentage string (e.g., "0.030%" for 30 tenths bp)
|
|
563
|
-
address: User wallet address (must match executor wallet)
|
|
564
|
-
|
|
565
|
-
Returns:
|
|
566
|
-
Dict with status "ok" or "err" and response data
|
|
567
|
-
"""
|
|
568
499
|
if address.lower() != self._wallet.address.lower():
|
|
569
500
|
return {
|
|
570
501
|
"status": "err",
|
|
@@ -572,7 +503,6 @@ class LocalHyperliquidExecutor:
|
|
|
572
503
|
}
|
|
573
504
|
|
|
574
505
|
try:
|
|
575
|
-
# The SDK's approve_builder_fee method handles the signing internally
|
|
576
506
|
result = self.exchange.approve_builder_fee(
|
|
577
507
|
builder=builder,
|
|
578
508
|
max_fee_rate=max_fee_rate,
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""PairedFiller - atomic paired spot+perp order execution with imbalance repair."""
|
|
2
|
-
|
|
3
1
|
from __future__ import annotations
|
|
4
2
|
|
|
5
3
|
import asyncio
|
|
@@ -47,7 +45,6 @@ def _round_up_units(units: float, step: Decimal) -> float:
|
|
|
47
45
|
def _parse_oids_and_immediate_fill(
|
|
48
46
|
resp: dict[str, Any],
|
|
49
47
|
) -> tuple[list[int], float, float]:
|
|
50
|
-
"""Extract order IDs and immediate fill info from API response."""
|
|
51
48
|
oids: list[int] = []
|
|
52
49
|
filled_units = 0.0
|
|
53
50
|
filled_notional = 0.0
|
|
@@ -112,8 +109,6 @@ def _parse_oids_and_immediate_fill(
|
|
|
112
109
|
|
|
113
110
|
@dataclass
|
|
114
111
|
class FillConfig:
|
|
115
|
-
"""Configuration for paired order filling."""
|
|
116
|
-
|
|
117
112
|
max_slip_bps: int = 35
|
|
118
113
|
max_chunk_usd: float = 7_500.0
|
|
119
114
|
max_loops: int = 40
|
|
@@ -122,8 +117,6 @@ class FillConfig:
|
|
|
122
117
|
|
|
123
118
|
@dataclass
|
|
124
119
|
class FillConfirmCfg:
|
|
125
|
-
"""Configuration for fill confirmation polling."""
|
|
126
|
-
|
|
127
120
|
max_status_polls: int = 4
|
|
128
121
|
poll_sleep_s: float = 0.20
|
|
129
122
|
fills_time_early_ms: int = 3_000
|
|
@@ -132,16 +125,12 @@ class FillConfirmCfg:
|
|
|
132
125
|
|
|
133
126
|
@dataclass
|
|
134
127
|
class LegFillResult:
|
|
135
|
-
"""Result of filling a single leg (spot or perp)."""
|
|
136
|
-
|
|
137
128
|
units: float = 0.0
|
|
138
129
|
notional: float = 0.0
|
|
139
130
|
|
|
140
131
|
|
|
141
132
|
@dataclass
|
|
142
133
|
class LegSubmitResult:
|
|
143
|
-
"""Result of submitting a leg order."""
|
|
144
|
-
|
|
145
134
|
oids: list[int]
|
|
146
135
|
start_ms: int
|
|
147
136
|
coin_label: str
|
|
@@ -151,8 +140,6 @@ class LegSubmitResult:
|
|
|
151
140
|
|
|
152
141
|
|
|
153
142
|
class LegConfirmer:
|
|
154
|
-
"""Confirms fill completion for individual legs via polling."""
|
|
155
|
-
|
|
156
143
|
def __init__(self, adapter: HyperliquidAdapter, cfg: FillConfirmCfg):
|
|
157
144
|
self.adapter = adapter
|
|
158
145
|
self.cfg = cfg
|
|
@@ -168,7 +155,6 @@ class LegConfirmer:
|
|
|
168
155
|
fallback_units: float = 0.0,
|
|
169
156
|
fallback_notional: float = 0.0,
|
|
170
157
|
) -> LegFillResult:
|
|
171
|
-
"""Confirm that a leg order has been filled."""
|
|
172
158
|
oids: list[int] = list(initial_oids)
|
|
173
159
|
|
|
174
160
|
if not oids and cloid:
|
|
@@ -208,7 +194,6 @@ class LegConfirmer:
|
|
|
208
194
|
return result
|
|
209
195
|
|
|
210
196
|
async def _oid_from_cloid(self, cloid: str, address: str) -> int | None:
|
|
211
|
-
"""Get numeric order ID from client order ID."""
|
|
212
197
|
try:
|
|
213
198
|
success, status = await self.adapter.get_order_status(address, cloid)
|
|
214
199
|
if not success:
|
|
@@ -227,7 +212,6 @@ class LegConfirmer:
|
|
|
227
212
|
return None
|
|
228
213
|
|
|
229
214
|
async def _ensure_not_open(self, address: str, oids: list[int]) -> None:
|
|
230
|
-
"""Cancel any orders that are still open."""
|
|
231
215
|
if not oids:
|
|
232
216
|
return
|
|
233
217
|
try:
|
|
@@ -266,7 +250,6 @@ class LegConfirmer:
|
|
|
266
250
|
logger.info(f"Cancel failed for oid {oid_int}: {exc}")
|
|
267
251
|
|
|
268
252
|
def _resolve_asset_id(self, order: dict[str, Any]) -> int | None:
|
|
269
|
-
"""Resolve asset ID from order info."""
|
|
270
253
|
coin = order.get("coin")
|
|
271
254
|
if not isinstance(coin, str):
|
|
272
255
|
return None
|
|
@@ -281,7 +264,6 @@ class LegConfirmer:
|
|
|
281
264
|
return None
|
|
282
265
|
|
|
283
266
|
def _flatten_open_orders(self, obj: Any) -> list[dict[str, Any]]:
|
|
284
|
-
"""Flatten nested order structures into a flat list."""
|
|
285
267
|
results: list[dict[str, Any]] = []
|
|
286
268
|
|
|
287
269
|
def walk(node: Any, coin_ctx: str | None = None) -> None:
|
|
@@ -315,7 +297,6 @@ class LegConfirmer:
|
|
|
315
297
|
start_ms: int,
|
|
316
298
|
oids: list[int],
|
|
317
299
|
) -> LegFillResult:
|
|
318
|
-
"""Sum fills matching order IDs within time window."""
|
|
319
300
|
if not oids:
|
|
320
301
|
return LegFillResult()
|
|
321
302
|
|
|
@@ -393,7 +374,6 @@ class LegConfirmer:
|
|
|
393
374
|
|
|
394
375
|
@staticmethod
|
|
395
376
|
def _to_records(data: Any) -> list[dict[str, Any]]:
|
|
396
|
-
"""Convert various data formats to list of dicts."""
|
|
397
377
|
if data is None:
|
|
398
378
|
return []
|
|
399
379
|
if hasattr(data, "to_dict"):
|
|
@@ -408,17 +388,6 @@ class LegConfirmer:
|
|
|
408
388
|
|
|
409
389
|
|
|
410
390
|
class PairedFiller:
|
|
411
|
-
"""
|
|
412
|
-
Executes atomic paired spot+perp orders with imbalance repair.
|
|
413
|
-
|
|
414
|
-
Handles:
|
|
415
|
-
- Chunking large orders into $7,500 pieces
|
|
416
|
-
- Parallel execution of spot and perp legs
|
|
417
|
-
- Imbalance repair if one leg fills more than the other
|
|
418
|
-
- Rollback on failure
|
|
419
|
-
- Fill confirmation via polling
|
|
420
|
-
"""
|
|
421
|
-
|
|
422
391
|
def __init__(
|
|
423
392
|
self,
|
|
424
393
|
adapter: HyperliquidAdapter,
|
|
@@ -450,21 +419,6 @@ class PairedFiller:
|
|
|
450
419
|
list[dict[str, Any]],
|
|
451
420
|
list[dict[str, Any]],
|
|
452
421
|
]:
|
|
453
|
-
"""
|
|
454
|
-
Fill paired spot+perp positions atomically.
|
|
455
|
-
|
|
456
|
-
Args:
|
|
457
|
-
coin: Asset symbol (e.g., "ETH")
|
|
458
|
-
spot_asset_id: Spot asset ID (>= 10000)
|
|
459
|
-
perp_asset_id: Perpetual asset ID (< 10000)
|
|
460
|
-
total_units: Total units to fill
|
|
461
|
-
direction: "long_spot_short_perp" or "short_spot_long_perp"
|
|
462
|
-
builder_fee: Optional builder fee configuration
|
|
463
|
-
|
|
464
|
-
Returns:
|
|
465
|
-
Tuple of (spot_units, perp_units, spot_notional, perp_notional,
|
|
466
|
-
spot_pointers, perp_pointers)
|
|
467
|
-
"""
|
|
468
422
|
step = self._common_step(spot_asset_id, perp_asset_id)
|
|
469
423
|
remaining = _round_down_units(total_units, step)
|
|
470
424
|
if remaining <= 0:
|
|
@@ -502,7 +456,6 @@ class PairedFiller:
|
|
|
502
456
|
while loops < self.cfg.max_loops and (
|
|
503
457
|
remaining > 0 or abs(delta_units) >= step_float
|
|
504
458
|
):
|
|
505
|
-
# Handle imbalance repair
|
|
506
459
|
if abs(delta_units) >= step_float:
|
|
507
460
|
loops += 1
|
|
508
461
|
fix_units = _round_down_units(abs(delta_units), step)
|
|
@@ -751,7 +704,6 @@ class PairedFiller:
|
|
|
751
704
|
logger.warning("Paired filler made no progress for {}; aborting.", coin)
|
|
752
705
|
break
|
|
753
706
|
|
|
754
|
-
# Handle margin rejection - rollback spot if perp rejected
|
|
755
707
|
if (
|
|
756
708
|
spot_fill.units > 0.0
|
|
757
709
|
and perp_fill.units <= 0.0
|
|
@@ -794,7 +746,6 @@ class PairedFiller:
|
|
|
794
746
|
|
|
795
747
|
break
|
|
796
748
|
|
|
797
|
-
# Handle spot failure - rollback perp if spot failed
|
|
798
749
|
if (
|
|
799
750
|
perp_fill.units > 0.0
|
|
800
751
|
and spot_fill.units <= 0.0
|
|
@@ -894,7 +845,6 @@ class PairedFiller:
|
|
|
894
845
|
|
|
895
846
|
@staticmethod
|
|
896
847
|
def _is_margin_rejected(resp: dict[str, Any]) -> bool:
|
|
897
|
-
"""Detect Hyperliquid margin rejection responses."""
|
|
898
848
|
if not isinstance(resp, dict):
|
|
899
849
|
return False
|
|
900
850
|
|
|
@@ -912,7 +862,6 @@ class PairedFiller:
|
|
|
912
862
|
|
|
913
863
|
@staticmethod
|
|
914
864
|
def _is_errorish(resp: dict[str, Any]) -> bool:
|
|
915
|
-
"""Best-effort detection for failed API responses."""
|
|
916
865
|
if not isinstance(resp, dict):
|
|
917
866
|
return True
|
|
918
867
|
|
|
@@ -941,7 +890,6 @@ class PairedFiller:
|
|
|
941
890
|
cloid: str,
|
|
942
891
|
builder_fee: dict[str, Any] | None = None,
|
|
943
892
|
) -> LegSubmitResult:
|
|
944
|
-
"""Submit an IOC order for one leg."""
|
|
945
893
|
start_ms = _now_ms()
|
|
946
894
|
rounded_units = self.adapter.get_valid_order_size(asset_id, units)
|
|
947
895
|
if rounded_units <= 0:
|
|
@@ -975,7 +923,6 @@ class PairedFiller:
|
|
|
975
923
|
)
|
|
976
924
|
|
|
977
925
|
async def _spot_usdc_available(self) -> float:
|
|
978
|
-
"""Get available USDC balance for spot trades."""
|
|
979
926
|
try:
|
|
980
927
|
success, state = await self.adapter.get_spot_user_state(self.address)
|
|
981
928
|
if not success:
|
|
@@ -1003,7 +950,6 @@ class PairedFiller:
|
|
|
1003
950
|
step: Decimal,
|
|
1004
951
|
min_units: float,
|
|
1005
952
|
) -> float:
|
|
1006
|
-
"""Calculate maximum spot units based on available USDC."""
|
|
1007
953
|
if desired_units <= 0 or mid_price <= 0:
|
|
1008
954
|
return 0.0
|
|
1009
955
|
available = await self._spot_usdc_available()
|
|
@@ -1019,7 +965,6 @@ class PairedFiller:
|
|
|
1019
965
|
return min(desired_units, max_units)
|
|
1020
966
|
|
|
1021
967
|
def _min_units_for_notional(self, mid_price: float, step: Decimal) -> float:
|
|
1022
|
-
"""Calculate minimum units to meet notional threshold."""
|
|
1023
968
|
if mid_price <= 0:
|
|
1024
969
|
return max(float(step), 0.0)
|
|
1025
970
|
raw_units = MIN_NOTIONAL_USD / mid_price
|
|
@@ -1029,7 +974,6 @@ class PairedFiller:
|
|
|
1029
974
|
return max(float(step), quantized)
|
|
1030
975
|
|
|
1031
976
|
def _common_step(self, spot_asset_id: int, perp_asset_id: int) -> Decimal:
|
|
1032
|
-
"""Get common step size for both assets."""
|
|
1033
977
|
spot_decimals = self.adapter.get_sz_decimals(spot_asset_id)
|
|
1034
978
|
perp_decimals = self.adapter.get_sz_decimals(perp_asset_id)
|
|
1035
979
|
return Decimal(10) ** -min(spot_decimals, perp_decimals)
|
|
@@ -1040,7 +984,6 @@ class PairedFiller:
|
|
|
1040
984
|
reason: str,
|
|
1041
985
|
metadata: dict[str, Any],
|
|
1042
986
|
) -> dict[str, Any] | None:
|
|
1043
|
-
"""Build order pointer for tracking."""
|
|
1044
987
|
if not response or response.get("status") != "ok":
|
|
1045
988
|
return None
|
|
1046
989
|
return {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Tests for HyperliquidAdapter."""
|
|
2
|
-
|
|
3
1
|
from types import SimpleNamespace
|
|
4
2
|
from unittest.mock import MagicMock, patch
|
|
5
3
|
|
|
@@ -7,11 +5,8 @@ import pytest
|
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
class TestHyperliquidAdapter:
|
|
10
|
-
"""Tests for HyperliquidAdapter functionality."""
|
|
11
|
-
|
|
12
8
|
@pytest.fixture
|
|
13
9
|
def mock_info(self):
|
|
14
|
-
"""Create a mock Info client."""
|
|
15
10
|
mock = MagicMock()
|
|
16
11
|
mock.meta_and_asset_ctxs.return_value = [
|
|
17
12
|
{"universe": [{"name": "BTC"}, {"name": "ETH"}]},
|
|
@@ -36,12 +31,10 @@ class TestHyperliquidAdapter:
|
|
|
36
31
|
|
|
37
32
|
@pytest.fixture
|
|
38
33
|
def mock_constants(self):
|
|
39
|
-
"""Create mock constants module."""
|
|
40
34
|
return SimpleNamespace(MAINNET_API_URL="https://api.hyperliquid.xyz")
|
|
41
35
|
|
|
42
36
|
@pytest.fixture
|
|
43
37
|
def adapter(self, mock_info, mock_constants):
|
|
44
|
-
"""Create adapter with mocked Info client."""
|
|
45
38
|
with patch(
|
|
46
39
|
"wayfinder_paths.adapters.hyperliquid_adapter.adapter.Info",
|
|
47
40
|
return_value=mock_info,
|
|
@@ -64,63 +57,53 @@ class TestHyperliquidAdapter:
|
|
|
64
57
|
|
|
65
58
|
@pytest.mark.asyncio
|
|
66
59
|
async def test_connect(self, adapter):
|
|
67
|
-
"""Test connection verification."""
|
|
68
60
|
result = await adapter.connect()
|
|
69
61
|
assert result is True
|
|
70
62
|
|
|
71
63
|
@pytest.mark.asyncio
|
|
72
64
|
async def test_get_meta_and_asset_ctxs(self, adapter):
|
|
73
|
-
"""Test fetching market metadata."""
|
|
74
65
|
success, data = await adapter.get_meta_and_asset_ctxs()
|
|
75
66
|
assert success
|
|
76
67
|
assert "universe" in data[0]
|
|
77
68
|
|
|
78
69
|
@pytest.mark.asyncio
|
|
79
70
|
async def test_get_spot_meta(self, adapter):
|
|
80
|
-
"""Test fetching spot metadata."""
|
|
81
71
|
success, data = await adapter.get_spot_meta()
|
|
82
72
|
assert success
|
|
83
73
|
|
|
84
74
|
@pytest.mark.asyncio
|
|
85
75
|
async def test_get_funding_history(self, adapter):
|
|
86
|
-
"""Test fetching funding history."""
|
|
87
76
|
success, data = await adapter.get_funding_history("ETH", 1700000000000)
|
|
88
77
|
assert success
|
|
89
78
|
assert isinstance(data, list)
|
|
90
79
|
|
|
91
80
|
@pytest.mark.asyncio
|
|
92
81
|
async def test_get_candles(self, adapter):
|
|
93
|
-
"""Test fetching candle data."""
|
|
94
82
|
success, data = await adapter.get_candles("ETH", "1h", 1700000000000)
|
|
95
83
|
assert success
|
|
96
84
|
assert isinstance(data, list)
|
|
97
85
|
|
|
98
86
|
@pytest.mark.asyncio
|
|
99
87
|
async def test_get_l2_book(self, adapter):
|
|
100
|
-
"""Test fetching order book."""
|
|
101
88
|
success, data = await adapter.get_l2_book("ETH")
|
|
102
89
|
assert success
|
|
103
90
|
assert "levels" in data
|
|
104
91
|
|
|
105
92
|
@pytest.mark.asyncio
|
|
106
93
|
async def test_get_user_state(self, adapter):
|
|
107
|
-
"""Test fetching user state."""
|
|
108
94
|
success, data = await adapter.get_user_state("0x1234")
|
|
109
95
|
assert success
|
|
110
96
|
assert "assetPositions" in data
|
|
111
97
|
|
|
112
98
|
@pytest.mark.asyncio
|
|
113
99
|
async def test_health_check(self, adapter):
|
|
114
|
-
"""Test health check."""
|
|
115
100
|
result = await adapter.health_check()
|
|
116
101
|
assert result["status"] == "healthy"
|
|
117
102
|
|
|
118
103
|
def test_get_sz_decimals(self, adapter):
|
|
119
|
-
"""Test getting size decimals."""
|
|
120
104
|
decimals = adapter.get_sz_decimals(0)
|
|
121
105
|
assert decimals == 4
|
|
122
106
|
|
|
123
107
|
def test_get_sz_decimals_unknown_asset(self, adapter):
|
|
124
|
-
"""Test error on unknown asset."""
|
|
125
108
|
with pytest.raises(ValueError, match="Unknown asset_id"):
|
|
126
109
|
adapter.get_sz_decimals(99999)
|