async-hyperliquid 0.1.0__tar.gz
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.
- async_hyperliquid-0.1.0/PKG-INFO +140 -0
- async_hyperliquid-0.1.0/README.md +117 -0
- async_hyperliquid-0.1.0/async_hyperliquid/__init__.py +0 -0
- async_hyperliquid-0.1.0/async_hyperliquid/async_api.py +46 -0
- async_hyperliquid-0.1.0/async_hyperliquid/async_hyper.py +253 -0
- async_hyperliquid-0.1.0/async_hyperliquid/exchange_endpoint.py +43 -0
- async_hyperliquid-0.1.0/async_hyperliquid/info_endpoint.py +207 -0
- async_hyperliquid-0.1.0/async_hyperliquid/utils/__init__.py +0 -0
- async_hyperliquid-0.1.0/async_hyperliquid/utils/constants.py +2 -0
- async_hyperliquid-0.1.0/async_hyperliquid/utils/miscs.py +28 -0
- async_hyperliquid-0.1.0/async_hyperliquid/utils/signing.py +127 -0
- async_hyperliquid-0.1.0/async_hyperliquid/utils/types.py +336 -0
- async_hyperliquid-0.1.0/pyproject.toml +41 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: async-hyperliquid
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async Hyperliquid client using aiohttp
|
|
5
|
+
Author: oneforalone
|
|
6
|
+
Author-email: oneforalone@proton.me
|
|
7
|
+
Requires-Python: >=3.10,<4
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Dist: aiohttp (>=3.11.12,<4.0.0)
|
|
14
|
+
Requires-Dist: eth-account (>=0.13.5,<0.14.0)
|
|
15
|
+
Requires-Dist: eth-utils (>=5.2.0,<6.0.0)
|
|
16
|
+
Requires-Dist: msgpack (>=1.1.0,<2.0.0)
|
|
17
|
+
Project-URL: Documentation, https://github.com/oneforalone/async-hyperliquid
|
|
18
|
+
Project-URL: Homepage, https://github.com/oneforalone/async-hyperliquid
|
|
19
|
+
Project-URL: Issues, https://github.com/oneforalone/async-hyperliquid/issues
|
|
20
|
+
Project-URL: Repository, https://github.com/oneforalone/async-hyperliquid
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Async Hyperliquid
|
|
24
|
+
|
|
25
|
+
An asynchronous Python client for interacting with the Hyperliquid API using `aiohttp`.
|
|
26
|
+
|
|
27
|
+
## Overview
|
|
28
|
+
|
|
29
|
+
This library provides an easy-to-use asynchronous interface for the Hyperliquid cryptocurrency exchange, supporting both mainnet and testnet environments. It handles API interactions, request signing, and data processing for both perpetual futures and spot trading.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- Asynchronous API communication using `aiohttp`
|
|
34
|
+
- Support for both mainnet and testnet environments
|
|
35
|
+
- Message signing for authenticated endpoints
|
|
36
|
+
- Trading operations for both perpetual futures and spot markets
|
|
37
|
+
- Comprehensive type hints for better IDE integration
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Using pip
|
|
43
|
+
pip install async-hyperliquid
|
|
44
|
+
|
|
45
|
+
# Using Poetry
|
|
46
|
+
poetry add async-hyperliquid
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import asyncio
|
|
53
|
+
import os
|
|
54
|
+
from async_hyperliquid import AsyncHyper
|
|
55
|
+
|
|
56
|
+
async def main():
|
|
57
|
+
# Initialize the client
|
|
58
|
+
address = os.getenv("HYPER_ADDRESS")
|
|
59
|
+
api_key = os.getenv("HYPER_API_KEY")
|
|
60
|
+
client = AsyncHyper(address, api_key, is_mainnet=True)
|
|
61
|
+
|
|
62
|
+
# Initialize metadata (required before making other calls)
|
|
63
|
+
await client.init_metas()
|
|
64
|
+
|
|
65
|
+
# Place a market order
|
|
66
|
+
response = await client.place_order(
|
|
67
|
+
coin="BTC",
|
|
68
|
+
is_buy=True,
|
|
69
|
+
sz=0.001,
|
|
70
|
+
px=0, # For market orders, price is ignored
|
|
71
|
+
is_market=True
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
print(response)
|
|
75
|
+
|
|
76
|
+
# Clean up
|
|
77
|
+
await client.close()
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
asyncio.run(main())
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Environment Variables
|
|
84
|
+
|
|
85
|
+
Create a `.env.local` file with the following variables:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
HYPER_ADDRESS=your_ethereum_address
|
|
89
|
+
HYPER_API_KEY=your_ethereum_private_key
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API Reference
|
|
93
|
+
|
|
94
|
+
### AsyncHyper
|
|
95
|
+
|
|
96
|
+
The main client class that provides methods for interacting with the Hyperliquid API.
|
|
97
|
+
|
|
98
|
+
#### Initialization
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
client = AsyncHyper(address, api_key, is_mainnet=True)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### Methods
|
|
105
|
+
|
|
106
|
+
- `init_metas()`: Initialize metadata (required before using other methods)
|
|
107
|
+
- `update_leverage(leverage, coin, is_cross=True)`: Update leverage for a specific coin
|
|
108
|
+
- `place_order(coin, is_buy, sz, px, is_market=True, **kwargs)`: Place a single order
|
|
109
|
+
- `place_orders(orders, builder=None)`: Place multiple orders at once
|
|
110
|
+
|
|
111
|
+
### Additional Modules
|
|
112
|
+
|
|
113
|
+
- `InfoAPI`: Access market information endpoints
|
|
114
|
+
- `ExchangeAPI`: Access exchange operation endpoints
|
|
115
|
+
- `utils`: Various utility functions and constants
|
|
116
|
+
|
|
117
|
+
## Testing
|
|
118
|
+
|
|
119
|
+
Tests use pytest and pytest-asyncio. To run tests:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Run all tests
|
|
123
|
+
pytest
|
|
124
|
+
|
|
125
|
+
# Run with coverage
|
|
126
|
+
pytest --cov=async_hyperliquid
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
132
|
+
|
|
133
|
+
## Acknowledgements
|
|
134
|
+
|
|
135
|
+
This library is a community-developed project and is not officially affiliated with Hyperliquid.
|
|
136
|
+
|
|
137
|
+
## TODO
|
|
138
|
+
|
|
139
|
+
See the TODO.md file for upcoming features and improvements.
|
|
140
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Async Hyperliquid
|
|
2
|
+
|
|
3
|
+
An asynchronous Python client for interacting with the Hyperliquid API using `aiohttp`.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This library provides an easy-to-use asynchronous interface for the Hyperliquid cryptocurrency exchange, supporting both mainnet and testnet environments. It handles API interactions, request signing, and data processing for both perpetual futures and spot trading.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Asynchronous API communication using `aiohttp`
|
|
12
|
+
- Support for both mainnet and testnet environments
|
|
13
|
+
- Message signing for authenticated endpoints
|
|
14
|
+
- Trading operations for both perpetual futures and spot markets
|
|
15
|
+
- Comprehensive type hints for better IDE integration
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Using pip
|
|
21
|
+
pip install async-hyperliquid
|
|
22
|
+
|
|
23
|
+
# Using Poetry
|
|
24
|
+
poetry add async-hyperliquid
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import asyncio
|
|
31
|
+
import os
|
|
32
|
+
from async_hyperliquid import AsyncHyper
|
|
33
|
+
|
|
34
|
+
async def main():
|
|
35
|
+
# Initialize the client
|
|
36
|
+
address = os.getenv("HYPER_ADDRESS")
|
|
37
|
+
api_key = os.getenv("HYPER_API_KEY")
|
|
38
|
+
client = AsyncHyper(address, api_key, is_mainnet=True)
|
|
39
|
+
|
|
40
|
+
# Initialize metadata (required before making other calls)
|
|
41
|
+
await client.init_metas()
|
|
42
|
+
|
|
43
|
+
# Place a market order
|
|
44
|
+
response = await client.place_order(
|
|
45
|
+
coin="BTC",
|
|
46
|
+
is_buy=True,
|
|
47
|
+
sz=0.001,
|
|
48
|
+
px=0, # For market orders, price is ignored
|
|
49
|
+
is_market=True
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
print(response)
|
|
53
|
+
|
|
54
|
+
# Clean up
|
|
55
|
+
await client.close()
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
asyncio.run(main())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Environment Variables
|
|
62
|
+
|
|
63
|
+
Create a `.env.local` file with the following variables:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
HYPER_ADDRESS=your_ethereum_address
|
|
67
|
+
HYPER_API_KEY=your_ethereum_private_key
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API Reference
|
|
71
|
+
|
|
72
|
+
### AsyncHyper
|
|
73
|
+
|
|
74
|
+
The main client class that provides methods for interacting with the Hyperliquid API.
|
|
75
|
+
|
|
76
|
+
#### Initialization
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
client = AsyncHyper(address, api_key, is_mainnet=True)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Methods
|
|
83
|
+
|
|
84
|
+
- `init_metas()`: Initialize metadata (required before using other methods)
|
|
85
|
+
- `update_leverage(leverage, coin, is_cross=True)`: Update leverage for a specific coin
|
|
86
|
+
- `place_order(coin, is_buy, sz, px, is_market=True, **kwargs)`: Place a single order
|
|
87
|
+
- `place_orders(orders, builder=None)`: Place multiple orders at once
|
|
88
|
+
|
|
89
|
+
### Additional Modules
|
|
90
|
+
|
|
91
|
+
- `InfoAPI`: Access market information endpoints
|
|
92
|
+
- `ExchangeAPI`: Access exchange operation endpoints
|
|
93
|
+
- `utils`: Various utility functions and constants
|
|
94
|
+
|
|
95
|
+
## Testing
|
|
96
|
+
|
|
97
|
+
Tests use pytest and pytest-asyncio. To run tests:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Run all tests
|
|
101
|
+
pytest
|
|
102
|
+
|
|
103
|
+
# Run with coverage
|
|
104
|
+
pytest --cov=async_hyperliquid
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
|
110
|
+
|
|
111
|
+
## Acknowledgements
|
|
112
|
+
|
|
113
|
+
This library is a community-developed project and is not officially affiliated with Hyperliquid.
|
|
114
|
+
|
|
115
|
+
## TODO
|
|
116
|
+
|
|
117
|
+
See the TODO.md file for upcoming features and improvements.
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from types import TracebackType
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from traceback import TracebackException
|
|
5
|
+
|
|
6
|
+
from aiohttp import ClientSession
|
|
7
|
+
|
|
8
|
+
from async_hyperliquid.utils.constants import MAINNET_API_URL
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncAPI:
|
|
12
|
+
def __init__(
|
|
13
|
+
self, endpoint: str, base_url: str = None, session: ClientSession = None
|
|
14
|
+
):
|
|
15
|
+
self.endpoint = endpoint
|
|
16
|
+
self.base_url = base_url or MAINNET_API_URL
|
|
17
|
+
self.session = session
|
|
18
|
+
self.logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# for async with AsyncAPI() as api usage
|
|
21
|
+
async def __aenter__(self) -> "AsyncAPI":
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
async def __aexit__(
|
|
25
|
+
self,
|
|
26
|
+
exc_type: Exception,
|
|
27
|
+
exc_val: TracebackException,
|
|
28
|
+
traceback: TracebackType,
|
|
29
|
+
) -> None:
|
|
30
|
+
await self.close()
|
|
31
|
+
|
|
32
|
+
async def close(self) -> None:
|
|
33
|
+
if self.session and not self.session.closed:
|
|
34
|
+
await self.session.close()
|
|
35
|
+
|
|
36
|
+
async def post(self, payloads: Optional[dict] = None) -> dict:
|
|
37
|
+
payloads = payloads or {}
|
|
38
|
+
req_path = f"{self.base_url}/{self.endpoint.value}"
|
|
39
|
+
self.logger.info(f"POST {req_path} {payloads}")
|
|
40
|
+
async with self.session.post(req_path, json=payloads) as resp:
|
|
41
|
+
resp.raise_for_status()
|
|
42
|
+
try:
|
|
43
|
+
return await resp.json()
|
|
44
|
+
except Exception as e:
|
|
45
|
+
self.logger.error(f"Error parsing JSON response: {e}")
|
|
46
|
+
return await resp.text()
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from aiohttp import ClientSession, ClientTimeout
|
|
4
|
+
from eth_account import Account
|
|
5
|
+
|
|
6
|
+
from async_hyperliquid.async_api import AsyncAPI
|
|
7
|
+
from async_hyperliquid.utils.types import (
|
|
8
|
+
Cloid,
|
|
9
|
+
LimitOrder,
|
|
10
|
+
EncodedOrder,
|
|
11
|
+
OrderBuilder,
|
|
12
|
+
PlaceOrderRequest,
|
|
13
|
+
CancelOrderRequest,
|
|
14
|
+
)
|
|
15
|
+
from async_hyperliquid.info_endpoint import InfoAPI
|
|
16
|
+
from async_hyperliquid.utils.signing import encode_order, orders_to_action
|
|
17
|
+
from async_hyperliquid.utils.constants import MAINNET_API_URL, TESTNET_API_URL
|
|
18
|
+
from async_hyperliquid.exchange_endpoint import ExchangeAPI
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AsyncHyper(AsyncAPI):
|
|
22
|
+
def __init__(self, address: str, api_key: str, is_mainnet: bool = True):
|
|
23
|
+
self.address = address
|
|
24
|
+
self.is_mainnet = is_mainnet
|
|
25
|
+
self.account = Account.from_key(api_key)
|
|
26
|
+
self.session = ClientSession(timeout=ClientTimeout(connect=3))
|
|
27
|
+
self.base_url = MAINNET_API_URL if is_mainnet else TESTNET_API_URL
|
|
28
|
+
self._info = InfoAPI(self.base_url, self.session)
|
|
29
|
+
self._exchange = ExchangeAPI(
|
|
30
|
+
self.account, self.session, self.base_url, address=self.address
|
|
31
|
+
)
|
|
32
|
+
self.metas: Optional[Dict[str, Any]] = None
|
|
33
|
+
# TODO: figure out the vault address
|
|
34
|
+
self.vault: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
def _init_coin_assets(self):
|
|
37
|
+
self.coin_assets = {}
|
|
38
|
+
for asset, asset_info in enumerate(self.metas["perps"]["universe"]):
|
|
39
|
+
self.coin_assets[asset_info["name"]] = asset
|
|
40
|
+
|
|
41
|
+
for asset_info in self.metas["spots"]["universe"]:
|
|
42
|
+
asset = asset_info["index"] + 10_000
|
|
43
|
+
self.coin_assets[asset_info["name"]] = asset
|
|
44
|
+
|
|
45
|
+
def _init_coin_names(self):
|
|
46
|
+
self.coin_names = {}
|
|
47
|
+
for asset_info in self.metas["perps"]["universe"]:
|
|
48
|
+
self.coin_names[asset_info["name"]] = asset_info["name"]
|
|
49
|
+
|
|
50
|
+
for asset_info in self.metas["spots"]["universe"]:
|
|
51
|
+
self.coin_names[asset_info["name"]] = asset_info["name"]
|
|
52
|
+
# For token pairs
|
|
53
|
+
base, quote = asset_info["tokens"]
|
|
54
|
+
base_info = self.metas["spots"]["tokens"][base]
|
|
55
|
+
quote_info = self.metas["spots"]["tokens"][quote]
|
|
56
|
+
base_name = (
|
|
57
|
+
base_info["name"] if base_info["name"] != "UBTC" else "BTC"
|
|
58
|
+
)
|
|
59
|
+
quote_name = quote_info["name"]
|
|
60
|
+
name = f"{base_name}/{quote_name}"
|
|
61
|
+
if name not in self.coin_names:
|
|
62
|
+
self.coin_names[name] = asset_info["name"]
|
|
63
|
+
|
|
64
|
+
def _init_asset_sz_decimals(self):
|
|
65
|
+
self.asset_sz_decimals = {}
|
|
66
|
+
for asset, asset_info in enumerate(self.metas["perps"]["universe"]):
|
|
67
|
+
self.asset_sz_decimals[asset] = asset_info["szDecimals"]
|
|
68
|
+
|
|
69
|
+
for asset_info in self.metas["spots"]["universe"]:
|
|
70
|
+
asset = asset_info["index"] + 10_000
|
|
71
|
+
base, _quote = asset_info["tokens"]
|
|
72
|
+
base_info = self.metas["spots"]["tokens"][base]
|
|
73
|
+
self.asset_sz_decimals[asset] = base_info["szDecimals"]
|
|
74
|
+
|
|
75
|
+
async def get_metas(self, perp_only: bool = False) -> dict:
|
|
76
|
+
perp_meta = await self._info.get_perp_meta()
|
|
77
|
+
if perp_only:
|
|
78
|
+
return {"perps": perp_meta, "spots": []}
|
|
79
|
+
|
|
80
|
+
spot_meta = await self._info.get_spot_meta()
|
|
81
|
+
|
|
82
|
+
return {"perps": perp_meta, "spots": spot_meta}
|
|
83
|
+
|
|
84
|
+
async def init_metas(self):
|
|
85
|
+
if not hasattr(self, "metas") or not self.metas:
|
|
86
|
+
self.metas = await self.get_metas()
|
|
87
|
+
|
|
88
|
+
if not self.metas.get("perps") or not self.metas["perps"]:
|
|
89
|
+
self.metas["perps"] = await self._info.get_perp_meta()
|
|
90
|
+
|
|
91
|
+
if not self.metas.get("spots") or not self.metas["spots"]:
|
|
92
|
+
self.metas["spots"] = await self._info.get_spot_meta()
|
|
93
|
+
|
|
94
|
+
if not hasattr(self, "coin_assets") or not self.coin_assets:
|
|
95
|
+
self._init_coin_assets()
|
|
96
|
+
|
|
97
|
+
if not hasattr(self, "coin_names") or not self.coin_names:
|
|
98
|
+
self._init_coin_names()
|
|
99
|
+
|
|
100
|
+
if not hasattr(self, "asset_sz_decimals") or not self.asset_sz_decimals:
|
|
101
|
+
self._init_asset_sz_decimals()
|
|
102
|
+
|
|
103
|
+
async def get_coin_name(self, coin: str) -> str:
|
|
104
|
+
await self.init_metas()
|
|
105
|
+
|
|
106
|
+
if coin not in self.coin_names:
|
|
107
|
+
raise ValueError(f"Coin {coin} not found")
|
|
108
|
+
|
|
109
|
+
return self.coin_names[coin]
|
|
110
|
+
|
|
111
|
+
async def get_coin_asset(self, coin: str) -> int:
|
|
112
|
+
await self.init_metas()
|
|
113
|
+
|
|
114
|
+
if coin not in self.coin_assets:
|
|
115
|
+
raise ValueError(f"Coin {coin} not found")
|
|
116
|
+
|
|
117
|
+
return self.coin_assets[coin]
|
|
118
|
+
|
|
119
|
+
async def get_coin_sz_decimals(self, coin: str) -> int:
|
|
120
|
+
await self.init_metas()
|
|
121
|
+
|
|
122
|
+
asset = await self.get_coin_asset(coin)
|
|
123
|
+
|
|
124
|
+
return self.asset_sz_decimals[asset]
|
|
125
|
+
|
|
126
|
+
async def get_token_id(self, coin: str) -> str:
|
|
127
|
+
coin_name = await self.get_coin_name(coin)
|
|
128
|
+
spot_metas: Dict[str, Any] = self.metas["spots"]
|
|
129
|
+
for coin_info in spot_metas["universe"]:
|
|
130
|
+
if coin_name == coin_info["name"]:
|
|
131
|
+
base, _quote = coin_info["tokens"]
|
|
132
|
+
return spot_metas["tokens"][base]["tokenId"]
|
|
133
|
+
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
async def update_leverage(
|
|
137
|
+
self, leverage: int, coin: str, is_cross: bool = True
|
|
138
|
+
):
|
|
139
|
+
action = {
|
|
140
|
+
"type": "updateLeverage",
|
|
141
|
+
"asset": await self.get_coin_asset(coin),
|
|
142
|
+
"isCross": is_cross,
|
|
143
|
+
"leverage": leverage,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return await self._exchange.post_action(action)
|
|
147
|
+
|
|
148
|
+
async def place_orders(
|
|
149
|
+
self,
|
|
150
|
+
orders: List[PlaceOrderRequest],
|
|
151
|
+
builder: Optional[OrderBuilder] = None,
|
|
152
|
+
):
|
|
153
|
+
encoded_orders: List[EncodedOrder] = []
|
|
154
|
+
for order in orders:
|
|
155
|
+
asset = await self.get_coin_asset(order["coin"])
|
|
156
|
+
encoded_orders.append(encode_order(order, asset))
|
|
157
|
+
|
|
158
|
+
if builder:
|
|
159
|
+
builder["b"] = builder["b"].lower()
|
|
160
|
+
action = orders_to_action(encoded_orders, builder)
|
|
161
|
+
|
|
162
|
+
return await self._exchange.post_action(action)
|
|
163
|
+
|
|
164
|
+
async def _slippage_price(
|
|
165
|
+
self, coin: str, is_buy: bool, slippage: float, px: float
|
|
166
|
+
) -> float:
|
|
167
|
+
coin_name = await self.get_coin_name(coin)
|
|
168
|
+
if not px:
|
|
169
|
+
all_mids = await self._info.get_all_mids()
|
|
170
|
+
px = float(all_mids[coin_name])
|
|
171
|
+
|
|
172
|
+
asset = await self.get_coin_asset(coin)
|
|
173
|
+
is_spot = asset >= 10_000
|
|
174
|
+
sz_decimals = await self.get_coin_sz_decimals(coin)
|
|
175
|
+
px *= (1 + slippage) if is_buy else (1 - slippage)
|
|
176
|
+
return round(
|
|
177
|
+
float(f"{px:.5g}"), (6 if not is_spot else 8) - sz_decimals
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async def place_order(
|
|
181
|
+
self,
|
|
182
|
+
coin: str,
|
|
183
|
+
is_buy: bool,
|
|
184
|
+
sz: float,
|
|
185
|
+
px: float,
|
|
186
|
+
is_market: bool = True,
|
|
187
|
+
*,
|
|
188
|
+
order_type: dict = LimitOrder.IOC.value,
|
|
189
|
+
reduce_only: bool = False,
|
|
190
|
+
cloid: Optional[Cloid] = None,
|
|
191
|
+
slippage: float = 0.01, # Default slippage is 1%
|
|
192
|
+
builder: Optional[OrderBuilder] = None,
|
|
193
|
+
):
|
|
194
|
+
if is_market:
|
|
195
|
+
px = self._slippage_price(coin, is_buy, slippage, px)
|
|
196
|
+
# Market order is an aggressive Limit Order IoC
|
|
197
|
+
order_type = LimitOrder.IOC
|
|
198
|
+
reduce_only = False
|
|
199
|
+
|
|
200
|
+
name = await self.get_coin_name(coin)
|
|
201
|
+
|
|
202
|
+
order_req = {
|
|
203
|
+
"coin": name,
|
|
204
|
+
"is_buy": is_buy,
|
|
205
|
+
"sz": sz,
|
|
206
|
+
"limit_px": px,
|
|
207
|
+
"order_type": order_type,
|
|
208
|
+
"reduce_only": reduce_only,
|
|
209
|
+
"cloid": cloid,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if cloid:
|
|
213
|
+
order_req["cloid"] = cloid
|
|
214
|
+
|
|
215
|
+
return await self.place_orders([order_req], builder=builder)
|
|
216
|
+
|
|
217
|
+
async def cancel_order(self, coin: str, oid: int | str):
|
|
218
|
+
name = await self.get_coin_name(coin)
|
|
219
|
+
if not isinstance(oid, int):
|
|
220
|
+
oid = int(oid)
|
|
221
|
+
cancel_req = {"coin": name, "oid": oid}
|
|
222
|
+
return await self.cancel_orders([cancel_req])
|
|
223
|
+
|
|
224
|
+
async def cancel_orders(self, orders: List[CancelOrderRequest]):
|
|
225
|
+
# TODO: support cloid
|
|
226
|
+
action = {
|
|
227
|
+
"type": "cancel",
|
|
228
|
+
"cancels": [
|
|
229
|
+
{
|
|
230
|
+
"a": await self.get_coin_asset(order["coin"]),
|
|
231
|
+
"o": order["oid"],
|
|
232
|
+
}
|
|
233
|
+
for order in orders
|
|
234
|
+
],
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return await self._exchange.post_action(action, self.vault)
|
|
238
|
+
|
|
239
|
+
async def modify_order(self):
|
|
240
|
+
# TODO: implement modify order
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
async def set_referrer_code(self, code: str):
|
|
244
|
+
action = {"type": "setReferrer", "code": code}
|
|
245
|
+
return await self._exchange.post_action(action)
|
|
246
|
+
|
|
247
|
+
async def close_all_positions(self):
|
|
248
|
+
# TODO: implement close all positions
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
async def close_position(self, coin: str):
|
|
252
|
+
# TODO: implement close position
|
|
253
|
+
pass
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from aiohttp import ClientSession
|
|
4
|
+
from eth_account import Account
|
|
5
|
+
|
|
6
|
+
from async_hyperliquid.async_api import AsyncAPI
|
|
7
|
+
from async_hyperliquid.utils.miscs import get_timestamp_ms
|
|
8
|
+
from async_hyperliquid.utils.types import Endpoint
|
|
9
|
+
from async_hyperliquid.utils.signing import sign_action
|
|
10
|
+
from async_hyperliquid.utils.constants import MAINNET_API_URL
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExchangeAPI(AsyncAPI):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
account: Account,
|
|
17
|
+
session: ClientSession,
|
|
18
|
+
base_url: Optional[str] = None,
|
|
19
|
+
address: str = None,
|
|
20
|
+
):
|
|
21
|
+
self.account = account
|
|
22
|
+
self.address = address or account.address
|
|
23
|
+
super().__init__(Endpoint.EXCHANGE, base_url, session)
|
|
24
|
+
|
|
25
|
+
async def post_action(
|
|
26
|
+
self, action: dict, vault: Optional[str] = None
|
|
27
|
+
) -> dict:
|
|
28
|
+
assert self.endpoint == Endpoint.EXCHANGE, (
|
|
29
|
+
"only exchange endpoint supports action"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
nonce = get_timestamp_ms()
|
|
33
|
+
signature = sign_action(
|
|
34
|
+
self.account, action, None, nonce, self.base_url == MAINNET_API_URL
|
|
35
|
+
)
|
|
36
|
+
vault_address = vault if action["type"] != "usdClassTransfer" else None
|
|
37
|
+
payloads = {
|
|
38
|
+
"action": action,
|
|
39
|
+
"nonce": nonce,
|
|
40
|
+
"signature": signature,
|
|
41
|
+
"vaultAddress": vault_address,
|
|
42
|
+
}
|
|
43
|
+
return await self.post(payloads)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
|
|
3
|
+
from aiohttp import ClientSession
|
|
4
|
+
|
|
5
|
+
from async_hyperliquid.async_api import AsyncAPI
|
|
6
|
+
from async_hyperliquid.utils.types import (
|
|
7
|
+
Depth,
|
|
8
|
+
Order,
|
|
9
|
+
Endpoint,
|
|
10
|
+
RateLimit,
|
|
11
|
+
FilledOrder,
|
|
12
|
+
FundingRate,
|
|
13
|
+
OrderStatus,
|
|
14
|
+
UserFunding,
|
|
15
|
+
TokenDetails,
|
|
16
|
+
SpotDeployState,
|
|
17
|
+
PerpMetaResponse,
|
|
18
|
+
SpotMetaResponse,
|
|
19
|
+
ClearinghouseState,
|
|
20
|
+
PerpMetaCtxResponse,
|
|
21
|
+
SpotMetaCtxResponse,
|
|
22
|
+
SpotClearinghouseState,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InfoAPI(AsyncAPI):
|
|
27
|
+
def __init__(self, base_url: str, session: ClientSession):
|
|
28
|
+
super().__init__(Endpoint.INFO, base_url, session)
|
|
29
|
+
|
|
30
|
+
async def get_all_mids(self) -> List[Dict[str, int]]:
|
|
31
|
+
payloads = {"type": "allMids"}
|
|
32
|
+
return await self.post(payloads)
|
|
33
|
+
|
|
34
|
+
async def get_user_open_orders(
|
|
35
|
+
self, address: str, is_frontend: bool = False
|
|
36
|
+
) -> List[Order]:
|
|
37
|
+
payloads = {
|
|
38
|
+
"type": "frontendOpenOrders" if is_frontend else "openOrders",
|
|
39
|
+
"user": address,
|
|
40
|
+
}
|
|
41
|
+
return await self.post(payloads)
|
|
42
|
+
|
|
43
|
+
async def get_user_fills(
|
|
44
|
+
self,
|
|
45
|
+
address: str,
|
|
46
|
+
start_time: int = None,
|
|
47
|
+
end_time: int = None,
|
|
48
|
+
aggregated: bool = False,
|
|
49
|
+
) -> List[FilledOrder]:
|
|
50
|
+
payloads = {
|
|
51
|
+
"type": "userFillsByTime" if start_time else "userFills",
|
|
52
|
+
"user": address,
|
|
53
|
+
"aggregateByTime": aggregated,
|
|
54
|
+
}
|
|
55
|
+
if start_time:
|
|
56
|
+
payloads["startTime"] = start_time
|
|
57
|
+
payloads["endTime"] = end_time
|
|
58
|
+
return await self.post(payloads)
|
|
59
|
+
|
|
60
|
+
async def get_user_rate_limit(self, address: str) -> RateLimit:
|
|
61
|
+
payloads = {"type": "userRateLimit", "user": address}
|
|
62
|
+
return await self.post(payloads)
|
|
63
|
+
|
|
64
|
+
async def get_order_status(
|
|
65
|
+
self, order_id: str, address: str
|
|
66
|
+
) -> OrderStatus:
|
|
67
|
+
payloads = {"type": "orderStatus", "user": address, "oid": order_id}
|
|
68
|
+
return await self.post(payloads)
|
|
69
|
+
|
|
70
|
+
async def get_depth(
|
|
71
|
+
self, coin: str, level: int = None, mantissa: int = None
|
|
72
|
+
) -> List[Depth]:
|
|
73
|
+
payloads = {"type": "l2Book", "coin": coin}
|
|
74
|
+
if level:
|
|
75
|
+
payloads["nSigFigs"] = level
|
|
76
|
+
if level and level == 5 and mantissa:
|
|
77
|
+
payloads["mantissa"] = mantissa
|
|
78
|
+
return await self.post(payloads)
|
|
79
|
+
|
|
80
|
+
async def get_candles(self, **data):
|
|
81
|
+
# TODO: to implement
|
|
82
|
+
# https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#candle-snapshot
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
async def check_user_builder_fee(self, user: str, builder: str) -> None:
|
|
86
|
+
payloads = {"type": "maxBuilderFee", "user": user, "builder": builder}
|
|
87
|
+
return await self.post(payloads)
|
|
88
|
+
|
|
89
|
+
async def get_user_order_history(
|
|
90
|
+
self, address: str
|
|
91
|
+
) -> List[Dict[str, Any]]:
|
|
92
|
+
payloads = {"type": "historicalOrders", "user": address}
|
|
93
|
+
return await self.post(payloads)
|
|
94
|
+
|
|
95
|
+
async def get_user_twap_fills(self, address: str) -> List[Dict[str, Any]]:
|
|
96
|
+
payloads = {"type": "userTwapSliceFills", "user": address}
|
|
97
|
+
return await self.post(payloads)
|
|
98
|
+
|
|
99
|
+
async def get_user_subaccounts(self, address: str) -> List[Dict[str, Any]]:
|
|
100
|
+
payloads = {"type": "subAccounts", "user": address}
|
|
101
|
+
return await self.post(payloads)
|
|
102
|
+
|
|
103
|
+
async def get_vault_info(
|
|
104
|
+
self, address: str, user: str = None
|
|
105
|
+
) -> Dict[str, Any]:
|
|
106
|
+
payloads = {"type": "vaultDetails", "vaultAddress": address}
|
|
107
|
+
if user:
|
|
108
|
+
payloads["user"] = user
|
|
109
|
+
return await self.post(payloads)
|
|
110
|
+
|
|
111
|
+
async def get_user_vault_deposits(
|
|
112
|
+
self, address: str
|
|
113
|
+
) -> List[Dict[str, Any]]:
|
|
114
|
+
payloads = {"type": "userVaultEquities", "user": address}
|
|
115
|
+
return await self.post(payloads)
|
|
116
|
+
|
|
117
|
+
async def get_user_role(self, address: str) -> Dict[str, str]:
|
|
118
|
+
payloads = {"type": "userRole", "user": address}
|
|
119
|
+
return await self.post(payloads)
|
|
120
|
+
|
|
121
|
+
async def get_user_staking(self, address: str) -> List[Dict[str, Any]]:
|
|
122
|
+
payloads = {"type": "delegations", "user": address}
|
|
123
|
+
return await self.post(payloads)
|
|
124
|
+
|
|
125
|
+
async def get_user_staking_summary(self, address: str) -> Dict[str, Any]:
|
|
126
|
+
payloads = {"type": "delegatorSummary", "user": address}
|
|
127
|
+
return await self.post(payloads)
|
|
128
|
+
|
|
129
|
+
async def get_user_staking_history(
|
|
130
|
+
self, address: str
|
|
131
|
+
) -> List[Dict[str, Any]]:
|
|
132
|
+
payloads = {"type": "delegatorHistory", "user": address}
|
|
133
|
+
return await self.post(payloads)
|
|
134
|
+
|
|
135
|
+
async def get_user_staking_rewards(
|
|
136
|
+
self, address: str
|
|
137
|
+
) -> List[Dict[str, Any]]:
|
|
138
|
+
payloads = {"type": "delegatorRewards", "user": address}
|
|
139
|
+
return await self.post(payloads)
|
|
140
|
+
|
|
141
|
+
async def get_perp_meta(self) -> PerpMetaResponse:
|
|
142
|
+
payloads = {"type": "meta"}
|
|
143
|
+
return await self.post(payloads)
|
|
144
|
+
|
|
145
|
+
async def get_perp_meta_ctx(self) -> PerpMetaCtxResponse:
|
|
146
|
+
payloads = {"type": "metaAndAssetCtxs"}
|
|
147
|
+
return await self.post(payloads)
|
|
148
|
+
|
|
149
|
+
async def get_positions(self, address: str) -> ClearinghouseState:
|
|
150
|
+
payloads = {"type": "clearinghouseState", "user": address}
|
|
151
|
+
return await self.post(payloads)
|
|
152
|
+
|
|
153
|
+
async def get_user_funding(
|
|
154
|
+
self,
|
|
155
|
+
address: str,
|
|
156
|
+
start_time: int,
|
|
157
|
+
end_time: int = None,
|
|
158
|
+
is_funding: bool = True,
|
|
159
|
+
) -> List[UserFunding]:
|
|
160
|
+
payloads = {
|
|
161
|
+
"type": "userFunding"
|
|
162
|
+
if is_funding
|
|
163
|
+
else "userNonFundingLedgerUpdates",
|
|
164
|
+
"user": address,
|
|
165
|
+
"startTime": start_time,
|
|
166
|
+
"endTime": end_time,
|
|
167
|
+
}
|
|
168
|
+
return await self.post(payloads)
|
|
169
|
+
|
|
170
|
+
async def get_funding_rate(
|
|
171
|
+
self, coin: str, start_time: int, end_time: int = None
|
|
172
|
+
) -> List[FundingRate]:
|
|
173
|
+
payloads = {
|
|
174
|
+
"type": "fundingHistory",
|
|
175
|
+
"coin": coin,
|
|
176
|
+
"startTime": start_time,
|
|
177
|
+
"endTime": end_time,
|
|
178
|
+
}
|
|
179
|
+
return await self.post(payloads)
|
|
180
|
+
|
|
181
|
+
async def get_predicted_funding(self) -> List[Any]:
|
|
182
|
+
payloads = {"type": "predictedFundings"}
|
|
183
|
+
return await self.post(payloads)
|
|
184
|
+
|
|
185
|
+
async def get_perps_at_open_interest_cap(self) -> List[str]:
|
|
186
|
+
payloads = {"type": "perpsAtOpenInterestCap"}
|
|
187
|
+
return await self.post(payloads)
|
|
188
|
+
|
|
189
|
+
async def get_spot_meta(self) -> SpotMetaResponse:
|
|
190
|
+
payloads = {"type": "spotMeta"}
|
|
191
|
+
return await self.post(payloads)
|
|
192
|
+
|
|
193
|
+
async def get_spot_meta_ctx(self) -> SpotMetaCtxResponse:
|
|
194
|
+
payloads = {"type": "spotMetaAndAssetCtxs"}
|
|
195
|
+
return await self.post(payloads)
|
|
196
|
+
|
|
197
|
+
async def get_user_balances(self, address: str) -> SpotClearinghouseState:
|
|
198
|
+
payloads = {"type": "spotClearinghouseState", "user": address}
|
|
199
|
+
return await self.post(payloads)
|
|
200
|
+
|
|
201
|
+
async def get_spot_deploy_state(self, address: str) -> SpotDeployState:
|
|
202
|
+
payloads = {"type": "spotDeployState", "user": address}
|
|
203
|
+
return await self.post(payloads)
|
|
204
|
+
|
|
205
|
+
async def get_token_info(self, token_id: str) -> TokenDetails:
|
|
206
|
+
payloads = {"type": "tokenDetails", "tokenId": token_id}
|
|
207
|
+
return await self.post(payloads)
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_timestamp_ms() -> int:
|
|
7
|
+
return int(time.time() * 1000)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_numeric(n: Any) -> bool:
|
|
11
|
+
try:
|
|
12
|
+
Decimal(n)
|
|
13
|
+
return True
|
|
14
|
+
except ValueError:
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def convert_to_numeric(data: Any) -> Any:
|
|
19
|
+
if isinstance(data, dict):
|
|
20
|
+
for k, v in data.items():
|
|
21
|
+
if isinstance(v, str) and is_numeric(v):
|
|
22
|
+
data[k] = Decimal(v)
|
|
23
|
+
else:
|
|
24
|
+
data[k] = convert_to_numeric(v)
|
|
25
|
+
elif isinstance(data, list):
|
|
26
|
+
for i in data:
|
|
27
|
+
convert_to_numeric(i)
|
|
28
|
+
return data
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
import msgpack
|
|
5
|
+
from eth_utils import keccak, to_hex
|
|
6
|
+
from eth_account import Account
|
|
7
|
+
from eth_account.messages import encode_typed_data
|
|
8
|
+
|
|
9
|
+
from async_hyperliquid.utils.types import (
|
|
10
|
+
OrderType,
|
|
11
|
+
OrderAction,
|
|
12
|
+
EncodedOrder,
|
|
13
|
+
OrderBuilder,
|
|
14
|
+
PlaceOrderRequest,
|
|
15
|
+
SignedAction,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def address_to_bytes(address: str) -> bytes:
|
|
20
|
+
return bytes.fromhex(address[2:] if address.startswith("0x") else address)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def hash_action(action, vault, nonce) -> bytes:
|
|
24
|
+
data = msgpack.packb(action)
|
|
25
|
+
data += nonce.to_bytes(8, "big")
|
|
26
|
+
|
|
27
|
+
if vault is None:
|
|
28
|
+
data += b"\x00"
|
|
29
|
+
else:
|
|
30
|
+
data += b"\x01"
|
|
31
|
+
data += bytes.fromhex(vault.removeprefix("0x"))
|
|
32
|
+
return keccak(data)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def sign_action(
|
|
36
|
+
wallet: Account,
|
|
37
|
+
action: dict,
|
|
38
|
+
active_pool: str,
|
|
39
|
+
nonce: int,
|
|
40
|
+
is_mainnet: bool,
|
|
41
|
+
) -> SignedAction:
|
|
42
|
+
h = hash_action(action, active_pool, nonce)
|
|
43
|
+
msg = {"source": "a" if is_mainnet else "b", "connectionId": h}
|
|
44
|
+
data = {
|
|
45
|
+
"domain": {
|
|
46
|
+
"chainId": 1337,
|
|
47
|
+
"name": "Exchange",
|
|
48
|
+
"verifyingContract": "0x0000000000000000000000000000000000000000",
|
|
49
|
+
"version": "1",
|
|
50
|
+
},
|
|
51
|
+
"types": {
|
|
52
|
+
"Agent": [
|
|
53
|
+
{"name": "source", "type": "string"},
|
|
54
|
+
{"name": "connectionId", "type": "bytes32"},
|
|
55
|
+
],
|
|
56
|
+
"EIP712Domain": [
|
|
57
|
+
{"name": "name", "type": "string"},
|
|
58
|
+
{"name": "version", "type": "string"},
|
|
59
|
+
{"name": "chainId", "type": "uint256"},
|
|
60
|
+
{"name": "verifyingContract", "type": "address"},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
"primaryType": "Agent",
|
|
64
|
+
"message": msg,
|
|
65
|
+
}
|
|
66
|
+
encodes = encode_typed_data(full_message=data)
|
|
67
|
+
signed = wallet.sign_message(encodes)
|
|
68
|
+
return {
|
|
69
|
+
"r": to_hex(signed["r"]),
|
|
70
|
+
"s": to_hex(signed["s"]),
|
|
71
|
+
"v": signed["v"],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def round_float(x: float) -> str:
|
|
76
|
+
rounded = f"{x:.8f}"
|
|
77
|
+
if abs(float(rounded) - x) >= 1e-12:
|
|
78
|
+
raise ValueError("round_float causes rounding", x)
|
|
79
|
+
|
|
80
|
+
if rounded == "-0":
|
|
81
|
+
rounded = "0"
|
|
82
|
+
normalized = Decimal(rounded).normalize()
|
|
83
|
+
return f"{normalized:f}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def ensure_order_type(order_type: OrderType) -> OrderType:
|
|
87
|
+
if "limit" in order_type:
|
|
88
|
+
return {"limit": order_type["limit"]}
|
|
89
|
+
elif "trigger" in order_type:
|
|
90
|
+
return {
|
|
91
|
+
"trigger": {
|
|
92
|
+
"isMarket": order_type["trigger"]["isMarket"],
|
|
93
|
+
"triggerPx": round_float(order_type["trigger"]["triggerPx"]),
|
|
94
|
+
"tpsl": order_type["trigger"]["tpsl"],
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
raise ValueError("Invalid order type", order_type)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def encode_order(order: PlaceOrderRequest, asset: int) -> EncodedOrder:
|
|
102
|
+
encoded_order: EncodedOrder = {
|
|
103
|
+
"a": asset,
|
|
104
|
+
"b": order["is_buy"],
|
|
105
|
+
"p": round_float(order["limit_px"]),
|
|
106
|
+
"s": round_float(order["sz"]),
|
|
107
|
+
"r": order["reduce_only"],
|
|
108
|
+
"t": ensure_order_type(order["order_type"]),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if order["cloid"] is not None:
|
|
112
|
+
encoded_order["c"] = order["cloid"].to_raw()
|
|
113
|
+
|
|
114
|
+
return encoded_order
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def orders_to_action(
|
|
118
|
+
encoded_orders: List[EncodedOrder], builder: OrderBuilder = None
|
|
119
|
+
) -> OrderAction:
|
|
120
|
+
action: OrderAction = {
|
|
121
|
+
"type": "order",
|
|
122
|
+
"orders": encoded_orders,
|
|
123
|
+
"grouping": "na",
|
|
124
|
+
}
|
|
125
|
+
if builder:
|
|
126
|
+
action["builder"] = builder
|
|
127
|
+
return action
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any, List, Tuple, Union, Literal, Optional, TypedDict
|
|
3
|
+
|
|
4
|
+
from typing_extensions import NotRequired
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Cloid:
|
|
8
|
+
def __init__(self, raw_cloid: str):
|
|
9
|
+
self._raw_cloid: str = raw_cloid
|
|
10
|
+
self._validate()
|
|
11
|
+
|
|
12
|
+
def _validate(self) -> None:
|
|
13
|
+
if not self._raw_cloid[:2] == "0x":
|
|
14
|
+
raise TypeError("cloid is not a hex string")
|
|
15
|
+
if not len(self._raw_cloid[2:]) == 32:
|
|
16
|
+
raise TypeError("cloid is not 16 bytes")
|
|
17
|
+
|
|
18
|
+
def __str__(self) -> str:
|
|
19
|
+
return str(self._raw_cloid)
|
|
20
|
+
|
|
21
|
+
def __repr__(self) -> str:
|
|
22
|
+
return str(self._raw_cloid)
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def from_int(cloid: int) -> "Cloid":
|
|
26
|
+
return Cloid(f"{cloid:#034x}")
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def from_str(cloid: str) -> "Cloid":
|
|
30
|
+
return Cloid(cloid)
|
|
31
|
+
|
|
32
|
+
def to_raw(self) -> str:
|
|
33
|
+
return self._raw_cloid
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SignedAction(TypedDict):
|
|
37
|
+
r: str
|
|
38
|
+
s: str
|
|
39
|
+
v: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LimitOrderOptions(TypedDict):
|
|
43
|
+
tif: Literal["Alo", "Ioc", "Gtc"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LimitOrderType(TypedDict):
|
|
47
|
+
limit: LimitOrderOptions
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LimitOrder(Enum):
|
|
51
|
+
ALO = {"limit": {"tif": "Alo"}}
|
|
52
|
+
IOC = {"limit": {"tif": "Ioc"}}
|
|
53
|
+
GTC = {"limit": {"tif": "Gtc"}}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TriggerOrderOptions(TypedDict):
|
|
57
|
+
isMarket: bool
|
|
58
|
+
triggerPx: str
|
|
59
|
+
tpsl: Literal["tp", "sl"]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TriggerOrderType(TypedDict):
|
|
63
|
+
trigger: TriggerOrderOptions
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
OrderType = Union[LimitOrderType, TriggerOrderType]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PlaceOrderRequest(TypedDict):
|
|
70
|
+
coin: str
|
|
71
|
+
is_buy: bool
|
|
72
|
+
sz: float
|
|
73
|
+
limit_px: float
|
|
74
|
+
reduce_only: bool
|
|
75
|
+
order_type: OrderType
|
|
76
|
+
cloid: NotRequired[Cloid]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CancelOrderRequest(TypedDict):
|
|
80
|
+
coin: str
|
|
81
|
+
oid: str
|
|
82
|
+
cloid: NotRequired[Cloid]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class EncodedOrder(TypedDict):
|
|
86
|
+
a: int # asset universe index
|
|
87
|
+
b: bool # is_buy
|
|
88
|
+
p: str # limit_px
|
|
89
|
+
s: str # size
|
|
90
|
+
r: bool # reduce_only
|
|
91
|
+
t: OrderType # order type
|
|
92
|
+
c: NotRequired[Cloid] # cloid
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class OrderBuilder(TypedDict):
|
|
96
|
+
b: str # builder address
|
|
97
|
+
f: float
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class OrderAction(TypedDict):
|
|
101
|
+
type: Literal["order"]
|
|
102
|
+
orders: List[EncodedOrder]
|
|
103
|
+
grouping: Literal["na", "normalTpsl", "positionTpsl"]
|
|
104
|
+
builder: NotRequired[OrderBuilder]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Endpoint(str, Enum):
|
|
108
|
+
INFO = "info"
|
|
109
|
+
EXCHANGE = "exchange"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Order(TypedDict):
|
|
113
|
+
# TODO: add order type
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class FilledOrder(Order):
|
|
118
|
+
# TODO: add filled order type
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class RateLimit(TypedDict):
|
|
123
|
+
cumVlm: str
|
|
124
|
+
nRequestsUsed: int
|
|
125
|
+
nRequestsCap: int
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class OrderStatus(TypedDict):
|
|
129
|
+
# TODO: add order status
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class L2Book(TypedDict):
|
|
134
|
+
px: str
|
|
135
|
+
sz: str
|
|
136
|
+
n: int
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Depth(TypedDict):
|
|
140
|
+
bids: List[L2Book]
|
|
141
|
+
asks: List[L2Book]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class PerpMeta(TypedDict):
|
|
145
|
+
name: str
|
|
146
|
+
szDecimals: int
|
|
147
|
+
maxLeverage: int
|
|
148
|
+
onlyIsolated: NotRequired[bool]
|
|
149
|
+
isDelisted: NotRequired[bool]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class PerpMetaResponse(TypedDict):
|
|
153
|
+
universe: List[PerpMeta]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class PerpMetaCtx(TypedDict):
|
|
157
|
+
dayNtlVlm: str
|
|
158
|
+
funding: str
|
|
159
|
+
impactPxs: List[str]
|
|
160
|
+
markPx: str
|
|
161
|
+
midPx: str
|
|
162
|
+
openInterest: str
|
|
163
|
+
oraclePx: str
|
|
164
|
+
premium: str
|
|
165
|
+
prevDayPx: str
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
PerpMetaCtxResponse = List[Union[PerpMetaResponse, List[PerpMetaCtx]]]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class AssetFunding(TypedDict):
|
|
172
|
+
allTime: str
|
|
173
|
+
sinceChange: str
|
|
174
|
+
sinceOpen: str
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class AssetLeverage(TypedDict):
|
|
178
|
+
rawUsd: str
|
|
179
|
+
type: str
|
|
180
|
+
value: int
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class Position(TypedDict):
|
|
184
|
+
coin: str
|
|
185
|
+
cumFunding: AssetFunding
|
|
186
|
+
entryPx: str
|
|
187
|
+
leverage: AssetLeverage
|
|
188
|
+
liquidationPx: str
|
|
189
|
+
marginUsed: str
|
|
190
|
+
maxLeverage: int
|
|
191
|
+
positionValue: str
|
|
192
|
+
returnOnEquity: str
|
|
193
|
+
szi: str
|
|
194
|
+
unrealizedPnl: str
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class AssetPosition(TypedDict):
|
|
198
|
+
type: str
|
|
199
|
+
position: Position
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class MarginSummary(TypedDict):
|
|
203
|
+
accountValue: str
|
|
204
|
+
totalMarginUsed: str
|
|
205
|
+
totalNtlPos: str
|
|
206
|
+
totalRawUsd: str
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class ClearinghouseState(TypedDict):
|
|
210
|
+
assetPositions: List[AssetPosition]
|
|
211
|
+
crosssMaintenanceMarginUsed: str
|
|
212
|
+
crossMarginSummary: MarginSummary
|
|
213
|
+
marginSummary: MarginSummary
|
|
214
|
+
time: int
|
|
215
|
+
withdrawable: str
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class UserFundingDelta(TypedDict):
|
|
219
|
+
coin: str
|
|
220
|
+
fundingRate: str
|
|
221
|
+
szi: str
|
|
222
|
+
type: str
|
|
223
|
+
usdc: str
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class UserFunding(TypedDict):
|
|
227
|
+
delta: UserFundingDelta
|
|
228
|
+
hash: str
|
|
229
|
+
time: int
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class FundingRate(TypedDict):
|
|
233
|
+
coin: str
|
|
234
|
+
fundingRate: str
|
|
235
|
+
premium: str
|
|
236
|
+
time: int
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class Token(TypedDict):
|
|
240
|
+
name: str
|
|
241
|
+
index: int
|
|
242
|
+
isCanonical: bool
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class TokenPairs(Token):
|
|
246
|
+
tokens: Tuple[int, int]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class SpotMeta(Token):
|
|
250
|
+
szDecimals: int
|
|
251
|
+
weiDecimals: int
|
|
252
|
+
tokenId: str
|
|
253
|
+
evmContract: Optional[str]
|
|
254
|
+
fullName: Optional[str]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class SpotMetaResponse(TypedDict):
|
|
258
|
+
tokens: List[SpotMeta]
|
|
259
|
+
universe: List[TokenPairs]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class SpotMetaCtx(TypedDict):
|
|
263
|
+
dayNtlVlm: str
|
|
264
|
+
markPx: str
|
|
265
|
+
midPx: str
|
|
266
|
+
prevDayPx: str
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
SpotMetaCtxResponse = List[Union[SpotMetaResponse, List[SpotMetaCtx]]]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class TokenBalance(TypedDict):
|
|
273
|
+
coin: str
|
|
274
|
+
token: int
|
|
275
|
+
hold: str
|
|
276
|
+
total: str
|
|
277
|
+
entryNtl: str
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class SpotClearinghouseState(TypedDict):
|
|
281
|
+
balances: List[TokenBalance]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class GasAuction(TypedDict):
|
|
285
|
+
startTimeSeconds: int
|
|
286
|
+
durationSeconds: int
|
|
287
|
+
startGas: str
|
|
288
|
+
currentGas: Optional[str]
|
|
289
|
+
endGas: str
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class DeployStateSpec(TypedDict):
|
|
293
|
+
name: str
|
|
294
|
+
szDecimals: int
|
|
295
|
+
weiDecimals: int
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class DeployState(TypedDict):
|
|
299
|
+
token: int
|
|
300
|
+
spec: DeployStateSpec
|
|
301
|
+
fullName: str
|
|
302
|
+
spots: List[int]
|
|
303
|
+
maxSupply: int
|
|
304
|
+
hyperliquidityGenesisBalance: str
|
|
305
|
+
totalGenesisBalanceWei: str
|
|
306
|
+
userGenesisBalances: List[Tuple[str, str]]
|
|
307
|
+
existingTokenGenesisBalances: List[Tuple[int, str]]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class SpotDeployState(TypedDict):
|
|
311
|
+
states: List[DeployState]
|
|
312
|
+
gasAuction: GasAuction
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class TokenGenesis(TypedDict):
|
|
316
|
+
userBalances: List[Tuple[str, str]]
|
|
317
|
+
existingTokenBalances: List[Any]
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class TokenDetails(TypedDict):
|
|
321
|
+
name: str
|
|
322
|
+
maxSupply: str
|
|
323
|
+
totalSupply: str
|
|
324
|
+
circulatingSupply: str
|
|
325
|
+
szDecimals: int
|
|
326
|
+
weiDecimals: int
|
|
327
|
+
midPx: str
|
|
328
|
+
markPx: str
|
|
329
|
+
prevDayPx: str
|
|
330
|
+
genesis: List[TokenGenesis]
|
|
331
|
+
deployer: str
|
|
332
|
+
deployGas: str
|
|
333
|
+
deployTime: str
|
|
334
|
+
seededUsdc: str
|
|
335
|
+
nonCirculatingUserBalances: List[Any]
|
|
336
|
+
futureEmissions: str
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "async-hyperliquid"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Async Hyperliquid client using aiohttp"
|
|
5
|
+
authors = [{ name = "oneforalone", email = "oneforalone@proton.me" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.10,<4"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"aiohttp (>=3.11.12,<4.0.0)",
|
|
10
|
+
"msgpack (>=1.1.0,<2.0.0)",
|
|
11
|
+
"eth-account (>=0.13.5,<0.14.0)",
|
|
12
|
+
"eth-utils (>=5.2.0,<6.0.0)",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/oneforalone/async-hyperliquid"
|
|
17
|
+
Documentation = "https://github.com/oneforalone/async-hyperliquid"
|
|
18
|
+
Repository = "https://github.com/oneforalone/async-hyperliquid"
|
|
19
|
+
Issues = "https://github.com/oneforalone/async-hyperliquid/issues"
|
|
20
|
+
# Changelog = "https://github.com/oneforalone/async-hyperliquid/blob/master/CHANGELOG.md"
|
|
21
|
+
|
|
22
|
+
[tool.poetry]
|
|
23
|
+
packages = [{ include = "async_hyperliquid" }]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
[tool.poetry.group.dev.dependencies]
|
|
27
|
+
pytest = "^8.3.5"
|
|
28
|
+
pytest-asyncio = "^0.25.3"
|
|
29
|
+
pytest-ruff = "^0.4.1"
|
|
30
|
+
pytest-cov = "^6.0.0"
|
|
31
|
+
pre-commit = "^4.1.0"
|
|
32
|
+
python-dotenv = "^1.0.1"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
36
|
+
build-backend = "poetry.core.masonry.api"
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
asyncio_mode = "auto"
|
|
40
|
+
addopts = "--verbose --capture=no"
|
|
41
|
+
asyncio_default_fixture_loop_scope = "session"
|