Skip to content

Commit cb63a3c

Browse files
author
Louis Tao
committed
Version 0.1.14
1 parent 62f2dfd commit cb63a3c

File tree

10 files changed

+286
-38
lines changed

10 files changed

+286
-38
lines changed

assets/images/coverage.svg

Lines changed: 2 additions & 2 deletions
Loading

examples/basic_order.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,16 @@ def main():
3030

3131
# Place an order that should rest by setting the price very low
3232
exchange = Exchange(account, constants.TESTNET_API_URL)
33-
order_result = exchange.order("ETH", True, 0.2, 100, {"limit": {"tif": "Gtc"}})
33+
order_result = exchange.order("ETH", True, 0.2, 1100, {"limit": {"tif": "Gtc"}})
3434
print(order_result)
3535

36+
# Query the order status by oid
37+
if order_result["status"] == "ok":
38+
status = order_result["response"]["data"]["statuses"][0]
39+
if "resting" in status:
40+
order_status = info.query_order_by_oid(account.address, status["resting"]["oid"])
41+
print("Order status by oid:", order_status)
42+
3643
# Cancel the order
3744
if order_result["status"] == "ok":
3845
status = order_result["response"]["data"]["statuses"][0]

examples/basic_order_with_cloid.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import json
2+
3+
import eth_account
4+
import utils
5+
from eth_account.signers.local import LocalAccount
6+
7+
from hyperliquid.exchange import Exchange
8+
from hyperliquid.info import Info
9+
from hyperliquid.utils import constants
10+
from hyperliquid.utils.types import Cloid
11+
12+
13+
def main():
14+
config = utils.get_config()
15+
account: LocalAccount = eth_account.Account.from_key(config["secret_key"])
16+
print("Running with account address:", account.address)
17+
info = Info(constants.TESTNET_API_URL, skip_ws=True)
18+
19+
# Get the user state and print out position information
20+
user_state = info.user_state(account.address)
21+
positions = []
22+
for position in user_state["assetPositions"]:
23+
if float(position["position"]["szi"]) != 0:
24+
positions.append(position["position"])
25+
if len(positions) > 0:
26+
print("positions:")
27+
for position in positions:
28+
print(json.dumps(position, indent=2))
29+
else:
30+
print("no open positions")
31+
32+
cloid = Cloid.from_str("0x00000000000000000000000000000001")
33+
# Users can also generate a cloid from an int
34+
# cloid = Cloid.from_int(1)
35+
# Place an order that should rest by setting the price very low
36+
exchange = Exchange(account, constants.TESTNET_API_URL)
37+
order_result = exchange.order("ETH", True, 0.2, 1100, {"limit": {"tif": "Gtc"}}, cloid=cloid)
38+
print(order_result)
39+
40+
# Query the order status by cloid
41+
order_status = info.query_order_by_cloid(account.address, cloid)
42+
print("Order status by cloid:", order_status)
43+
44+
# Non-existent cloid example
45+
invalid_cloid = Cloid.from_int(2)
46+
order_status = info.query_order_by_cloid(account.address, invalid_cloid)
47+
print("Order status by cloid:", order_status)
48+
49+
# Cancel the order by cloid
50+
if order_result["status"] == "ok":
51+
status = order_result["response"]["data"]["statuses"][0]
52+
if "resting" in status:
53+
cancel_result = exchange.cancel_by_cloid("ETH", cloid)
54+
print(cancel_result)
55+
56+
57+
if __name__ == "__main__":
58+
main()

hyperliquid/exchange.py

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,22 @@
1212
from hyperliquid.utils.signing import (
1313
ZERO_ADDRESS,
1414
CancelRequest,
15+
CancelByCloidRequest,
1516
OrderRequest,
1617
OrderSpec,
1718
OrderType,
1819
float_to_usd_int,
1920
get_timestamp_ms,
2021
order_grouping_to_number,
22+
order_request_to_order_spec,
2123
order_spec_preprocessing,
2224
order_spec_to_order_wire,
2325
sign_l1_action,
2426
sign_usd_transfer_action,
2527
sign_agent,
28+
str_to_bytes16,
2629
)
27-
from hyperliquid.utils.types import Any, List, Literal, Meta, Optional, Tuple
30+
from hyperliquid.utils.types import Any, List, Literal, Meta, Optional, Tuple, Cloid
2831

2932

3033
class Exchange(API):
@@ -56,42 +59,53 @@ def _post_action(self, action, signature, nonce):
5659
return self.post("/exchange", payload)
5760

5861
def order(
59-
self, coin: str, is_buy: bool, sz: float, limit_px: float, order_type: OrderType, reduce_only: bool = False
62+
self,
63+
coin: str,
64+
is_buy: bool,
65+
sz: float,
66+
limit_px: float,
67+
order_type: OrderType,
68+
reduce_only: bool = False,
69+
cloid: Optional[Cloid] = None,
6070
) -> Any:
61-
return self.bulk_orders(
62-
[
63-
{
64-
"coin": coin,
65-
"is_buy": is_buy,
66-
"sz": sz,
67-
"limit_px": limit_px,
68-
"order_type": order_type,
69-
"reduce_only": reduce_only,
70-
}
71-
]
72-
)
71+
order = {
72+
"coin": coin,
73+
"is_buy": is_buy,
74+
"sz": sz,
75+
"limit_px": limit_px,
76+
"order_type": order_type,
77+
"reduce_only": reduce_only,
78+
}
79+
if cloid:
80+
order["cloid"] = cloid
81+
return self.bulk_orders([order])
7382

7483
def bulk_orders(self, order_requests: List[OrderRequest]) -> Any:
7584
order_specs: List[OrderSpec] = [
76-
{
77-
"order": {
78-
"asset": self.coin_to_asset[order["coin"]],
79-
"isBuy": order["is_buy"],
80-
"reduceOnly": order["reduce_only"],
81-
"limitPx": order["limit_px"],
82-
"sz": order["sz"],
83-
},
84-
"orderType": order["order_type"],
85-
}
86-
for order in order_requests
85+
order_request_to_order_spec(order, self.coin_to_asset[order["coin"]]) for order in order_requests
8786
]
8887

8988
timestamp = get_timestamp_ms()
9089
grouping: Literal["na"] = "na"
9190

91+
has_cloid = False
92+
for order_spec in order_specs:
93+
if "cloid" in order_spec["order"] and order_spec["order"]["cloid"]:
94+
has_cloid = True
95+
96+
if has_cloid:
97+
for order_spec in order_specs:
98+
if "cloid" not in order_spec["order"] or not order_spec["order"]["cloid"]:
99+
raise ValueError("all orders must have cloids if at least one has a cloid")
100+
101+
if has_cloid:
102+
signature_types = ["(uint32,bool,uint64,uint64,bool,uint8,uint64,bytes16)[]", "uint8"]
103+
else:
104+
signature_types = ["(uint32,bool,uint64,uint64,bool,uint8,uint64)[]", "uint8"]
105+
92106
signature = sign_l1_action(
93107
self.wallet,
94-
["(uint32,bool,uint64,uint64,bool,uint8,uint64)[]", "uint8"],
108+
signature_types,
95109
[[order_spec_preprocessing(order_spec) for order_spec in order_specs], order_grouping_to_number(grouping)],
96110
ZERO_ADDRESS if self.vault_address is None else self.vault_address,
97111
timestamp,
@@ -111,6 +125,9 @@ def bulk_orders(self, order_requests: List[OrderRequest]) -> Any:
111125
def cancel(self, coin: str, oid: int) -> Any:
112126
return self.bulk_cancel([{"coin": coin, "oid": oid}])
113127

128+
def cancel_by_cloid(self, coin: str, cloid: Cloid) -> Any:
129+
return self.bulk_cancel_by_cloid([{"coin": coin, "cloid": cloid}])
130+
114131
def bulk_cancel(self, cancel_requests: List[CancelRequest]) -> Any:
115132
timestamp = get_timestamp_ms()
116133
signature = sign_l1_action(
@@ -136,6 +153,36 @@ def bulk_cancel(self, cancel_requests: List[CancelRequest]) -> Any:
136153
timestamp,
137154
)
138155

156+
def bulk_cancel_by_cloid(self, cancel_requests: List[CancelByCloidRequest]) -> Any:
157+
timestamp = get_timestamp_ms()
158+
signature = sign_l1_action(
159+
self.wallet,
160+
["(uint32,bytes16)[]"],
161+
[
162+
[
163+
(self.coin_to_asset[cancel["coin"]], str_to_bytes16(cancel["cloid"].to_raw()))
164+
for cancel in cancel_requests
165+
]
166+
],
167+
ZERO_ADDRESS if self.vault_address is None else self.vault_address,
168+
timestamp,
169+
self.base_url == MAINNET_API_URL,
170+
)
171+
return self._post_action(
172+
{
173+
"type": "cancelByCloid",
174+
"cancels": [
175+
{
176+
"asset": self.coin_to_asset[cancel["coin"]],
177+
"cloid": cancel["cloid"],
178+
}
179+
for cancel in cancel_requests
180+
],
181+
},
182+
signature,
183+
timestamp,
184+
)
185+
139186
def update_leverage(self, leverage: int, coin: str, is_cross: bool = True) -> Any:
140187
timestamp = get_timestamp_ms()
141188
asset = self.coin_to_asset[coin]

hyperliquid/info.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from hyperliquid.api import API
2-
from hyperliquid.utils.types import Any, Callable, Meta, Optional, Subscription, cast
2+
from hyperliquid.utils.types import Any, Callable, Meta, Optional, Subscription, cast, Cloid
33
from hyperliquid.websocket_manager import WebsocketManager
44

55

@@ -221,6 +221,12 @@ def candles_snapshot(self, coin: str, interval: str, startTime: int, endTime: in
221221
req = {"coin": coin, "interval": interval, "startTime": startTime, "endTime": endTime}
222222
return self.post("/info", {"type": "candleSnapshot", "req": req})
223223

224+
def query_order_by_oid(self, user: str, oid: int) -> Any:
225+
return self.post("/info", {"type": "orderStatus", "user": user, "oid": oid})
226+
227+
def query_order_by_cloid(self, user: str, cloid: Cloid) -> Any:
228+
return self.post("/info", {"type": "orderStatus", "user": user, "oid": cloid.to_raw()})
229+
224230
def subscribe(self, subscription: Subscription, callback: Callable[[Any], None]) -> int:
225231
if self.ws_manager is None:
226232
raise RuntimeError("Cannot call subscribe since skip_ws was used")

hyperliquid/utils/signing.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from eth_account.messages import encode_structured_data
55
from eth_utils import keccak, to_hex
66

7-
from hyperliquid.utils.types import Any, Literal, Tuple, TypedDict, Union
7+
from hyperliquid.utils.types import Any, Literal, Optional, Tuple, TypedDict, Union, Cloid
88

99
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
1010

@@ -17,9 +17,19 @@
1717
OrderTypeWire = TypedDict("OrderTypeWire", {"limit": LimitOrderType, "trigger": TriggerOrderTypeWire}, total=False)
1818
OrderRequest = TypedDict(
1919
"OrderRequest",
20-
{"coin": str, "is_buy": bool, "sz": float, "limit_px": float, "order_type": OrderType, "reduce_only": bool},
20+
{
21+
"coin": str,
22+
"is_buy": bool,
23+
"sz": float,
24+
"limit_px": float,
25+
"order_type": OrderType,
26+
"reduce_only": bool,
27+
"cloid": Optional[Cloid],
28+
},
29+
total=False,
2130
)
2231
CancelRequest = TypedDict("CancelRequest", {"coin": str, "oid": int})
32+
CancelByCloidRequest = TypedDict("CancelByCloidRequest", {"coin": str, "cloid": Cloid})
2333

2434

2535
def order_type_to_tuple(order_type: OrderType) -> Tuple[int, float]:
@@ -57,14 +67,16 @@ def order_grouping_to_number(grouping: Grouping) -> int:
5767
return 2
5868

5969

60-
Order = TypedDict("Order", {"asset": int, "isBuy": bool, "limitPx": float, "sz": float, "reduceOnly": bool})
70+
Order = TypedDict(
71+
"Order", {"asset": int, "isBuy": bool, "limitPx": float, "sz": float, "reduceOnly": bool, "cloid": Optional[Cloid]}
72+
)
6173
OrderSpec = TypedDict("OrderSpec", {"order": Order, "orderType": OrderType})
6274

6375

6476
def order_spec_preprocessing(order_spec: OrderSpec) -> Any:
6577
order = order_spec["order"]
6678
order_type_array = order_type_to_tuple(order_spec["orderType"])
67-
return (
79+
res = (
6880
order["asset"],
6981
order["isBuy"],
7082
float_to_int_for_hashing(order["limitPx"]),
@@ -73,11 +85,22 @@ def order_spec_preprocessing(order_spec: OrderSpec) -> Any:
7385
order_type_array[0],
7486
float_to_int_for_hashing(order_type_array[1]),
7587
)
88+
if "cloid" in order and order["cloid"]:
89+
res += (str_to_bytes16(order["cloid"].to_raw()),)
90+
return res
7691

7792

7893
OrderWire = TypedDict(
7994
"OrderWire",
80-
{"asset": int, "isBuy": bool, "limitPx": str, "sz": str, "reduceOnly": bool, "orderType": OrderTypeWire},
95+
{
96+
"asset": int,
97+
"isBuy": bool,
98+
"limitPx": str,
99+
"sz": str,
100+
"reduceOnly": bool,
101+
"orderType": OrderTypeWire,
102+
"cloid": Optional[Cloid],
103+
},
81104
)
82105

83106

@@ -97,13 +120,17 @@ def order_type_to_wire(order_type: OrderType) -> OrderTypeWire:
97120

98121
def order_spec_to_order_wire(order_spec: OrderSpec) -> OrderWire:
99122
order = order_spec["order"]
123+
cloid = None
124+
if "cloid" in order and order["cloid"]:
125+
cloid = order["cloid"].to_raw()
100126
return {
101127
"asset": order["asset"],
102128
"isBuy": order["isBuy"],
103129
"limitPx": float_to_wire(order["limitPx"]),
104130
"sz": float_to_wire(order["sz"]),
105131
"reduceOnly": order["reduceOnly"],
106132
"orderType": order_type_to_wire(order_spec["orderType"]),
133+
"cloid": cloid,
107134
}
108135

109136

@@ -230,5 +257,27 @@ def float_to_int(x: float, power: int) -> int:
230257
return round(with_decimals)
231258

232259

260+
def str_to_bytes16(x: str) -> bytearray:
261+
assert x.startswith("0x")
262+
return bytearray.fromhex(x[2:])
263+
264+
233265
def get_timestamp_ms() -> int:
234266
return int(time.time() * 1000)
267+
268+
269+
def order_request_to_order_spec(order: OrderRequest, asset: int) -> OrderSpec:
270+
cloid = None
271+
if "cloid" in order:
272+
cloid = order["cloid"]
273+
return {
274+
"order": {
275+
"asset": asset,
276+
"isBuy": order["is_buy"],
277+
"reduceOnly": order["reduce_only"],
278+
"limitPx": order["limit_px"],
279+
"sz": order["sz"],
280+
"cloid": cloid,
281+
},
282+
"orderType": order["order_type"],
283+
}

0 commit comments

Comments
 (0)