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.
- wayfinder_paths/adapters/balance_adapter/README.md +0 -10
- wayfinder_paths/adapters/balance_adapter/adapter.py +0 -20
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -30
- wayfinder_paths/adapters/brap_adapter/adapter.py +3 -2
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +9 -13
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +14 -7
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +7 -6
- wayfinder_paths/adapters/pool_adapter/README.md +3 -28
- wayfinder_paths/adapters/pool_adapter/adapter.py +0 -72
- wayfinder_paths/adapters/pool_adapter/examples.json +0 -43
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +4 -54
- wayfinder_paths/adapters/token_adapter/test_adapter.py +4 -14
- wayfinder_paths/core/adapters/models.py +9 -4
- wayfinder_paths/core/analytics/__init__.py +11 -0
- wayfinder_paths/core/analytics/bootstrap.py +57 -0
- wayfinder_paths/core/analytics/stats.py +48 -0
- wayfinder_paths/core/analytics/test_analytics.py +170 -0
- wayfinder_paths/core/clients/BRAPClient.py +1 -0
- wayfinder_paths/core/clients/LedgerClient.py +2 -7
- wayfinder_paths/core/clients/PoolClient.py +0 -16
- wayfinder_paths/core/clients/WalletClient.py +0 -27
- wayfinder_paths/core/clients/protocols.py +104 -18
- wayfinder_paths/scripts/make_wallets.py +9 -0
- wayfinder_paths/scripts/run_strategy.py +124 -0
- wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
- wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
- wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
- wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +1 -9
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +36 -5
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +367 -278
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +204 -7
- {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/METADATA +32 -3
- {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/RECORD +50 -27
- {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/LICENSE +0 -0
- {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
|
+
}
|