Skip to content

Commit 374c86e

Browse files
author
Louis Tao
committed
Version 0.1.16
1 parent 25c42a1 commit 374c86e

File tree

9 files changed

+224
-16
lines changed

9 files changed

+224
-16
lines changed

assets/images/coverage.svg

Lines changed: 2 additions & 2 deletions
Loading

examples/basic_market_order.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import time
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.utils import constants
9+
10+
11+
def main():
12+
13+
config = utils.get_config()
14+
account: LocalAccount = eth_account.Account.from_key(config["secret_key"])
15+
print("Running with account address:", account.address)
16+
17+
exchange = Exchange(account, constants.TESTNET_API_URL)
18+
19+
coin = "ETH"
20+
is_buy = True
21+
sz = 0.05
22+
23+
print(f"We try to Market {'Buy' if is_buy else 'Sell'} {sz} {coin}.")
24+
25+
order_result = exchange.market_open("ETH", is_buy, sz, 2040, 0.01)
26+
if order_result["status"] == "ok":
27+
for status in order_result["response"]["data"]["statuses"]:
28+
try:
29+
filled = status["filled"]
30+
print(f'Order #{filled["oid"]} filled {filled["totalSz"]} @{filled["avgPx"]}')
31+
except KeyError:
32+
print(f'Error: {status["error"]}')
33+
34+
print("We wait for 2s before closing")
35+
time.sleep(2)
36+
37+
print(f"We try to Market Close all {coin}.")
38+
order_result = exchange.market_close(coin)
39+
if order_result["status"] == "ok":
40+
for status in order_result["response"]["data"]["statuses"]:
41+
try:
42+
filled = status["filled"]
43+
print(f'Order #{filled["oid"]} filled {filled["totalSz"]} @{filled["avgPx"]}')
44+
except KeyError:
45+
print(f'Error: {status["error"]}')
46+
47+
48+
if __name__ == "__main__":
49+
main()

examples/basic_order_modify.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
11+
12+
def main():
13+
config = utils.get_config()
14+
account: LocalAccount = eth_account.Account.from_key(config["secret_key"])
15+
print("Running with account address:", account.address)
16+
info = Info(constants.TESTNET_API_URL, skip_ws=True)
17+
18+
# Get the user state and print out position information
19+
user_state = info.user_state(account.address)
20+
positions = []
21+
for position in user_state["assetPositions"]:
22+
if float(position["position"]["szi"]) != 0:
23+
positions.append(position["position"])
24+
if len(positions) > 0:
25+
print("positions:")
26+
for position in positions:
27+
print(json.dumps(position, indent=2))
28+
else:
29+
print("no open positions")
30+
31+
# Place an order that should rest by setting the price very low
32+
exchange = Exchange(account, constants.TESTNET_API_URL)
33+
order_result = exchange.order("ETH", True, 0.2, 1100, {"limit": {"tif": "Gtc"}})
34+
print(order_result)
35+
36+
# Modify the order by oid
37+
if order_result["status"] == "ok":
38+
status = order_result["response"]["data"]["statuses"][0]
39+
if "resting" in status:
40+
oid = status["resting"]["oid"]
41+
order_status = info.query_order_by_oid(account.address, oid)
42+
print("Order status by oid:", order_status)
43+
44+
modify_result = exchange.modify_order(oid, "ETH", True, 0.1, 1105, {"limit": {"tif": "Gtc"}})
45+
print(modify_result)
46+
47+
48+
if __name__ == "__main__":
49+
main()

hyperliquid/exchange.py

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131

3232

3333
class Exchange(API):
34+
35+
# Default Max Slippage for Market Orders 5%
36+
DEFAULT_SLIPPAGE = 0.05
37+
3438
def __init__(
3539
self,
3640
wallet: LocalAccount,
@@ -41,9 +45,9 @@ def __init__(
4145
super().__init__(base_url)
4246
self.wallet = wallet
4347
self.vault_address = vault_address
48+
self.info = Info(base_url, skip_ws=True)
4449
if meta is None:
45-
info = Info(base_url, skip_ws=True)
46-
self.meta = info.meta()
50+
self.meta = self.info.meta()
4751
else:
4852
self.meta = meta
4953
self.coin_to_asset = {asset_info["name"]: asset for (asset, asset_info) in enumerate(self.meta["universe"])}
@@ -58,6 +62,22 @@ def _post_action(self, action, signature, nonce):
5862
logging.debug(payload)
5963
return self.post("/exchange", payload)
6064

65+
def _slippage_price(
66+
self,
67+
coin: str,
68+
is_buy: bool,
69+
slippage: float,
70+
px: Optional[float] = None,
71+
) -> float:
72+
73+
if not px:
74+
# Get midprice
75+
px = float(self.info.all_mids()[coin])
76+
# Calculate Slippage
77+
px *= (1 + slippage) if is_buy else (1 - slippage)
78+
# We round px to 5 significant figures and 6 decimals
79+
return round(float(f"{px:.5g}"), 6)
80+
6181
def order(
6282
self,
6383
coin: str,
@@ -68,7 +88,7 @@ def order(
6888
reduce_only: bool = False,
6989
cloid: Optional[Cloid] = None,
7090
) -> Any:
71-
order = {
91+
order: OrderRequest = {
7292
"coin": coin,
7393
"is_buy": is_buy,
7494
"sz": sz,
@@ -122,6 +142,93 @@ def bulk_orders(self, order_requests: List[OrderRequest]) -> Any:
122142
timestamp,
123143
)
124144

145+
def modify_order(
146+
self,
147+
oid: int,
148+
coin: str,
149+
is_buy: bool,
150+
sz: float,
151+
limit_px: float,
152+
order_type: OrderType,
153+
reduce_only: bool = False,
154+
cloid: Optional[Cloid] = None,
155+
) -> Any:
156+
order: OrderRequest = {
157+
"coin": coin,
158+
"is_buy": is_buy,
159+
"sz": sz,
160+
"limit_px": limit_px,
161+
"order_type": order_type,
162+
"reduce_only": reduce_only,
163+
}
164+
if cloid:
165+
order["cloid"] = cloid
166+
167+
order_spec = order_request_to_order_spec(order, self.coin_to_asset[order["coin"]])
168+
169+
timestamp = get_timestamp_ms()
170+
171+
if cloid:
172+
signature_types = ["uint64", "(uint32,bool,uint64,uint64,bool,uint8,uint64,bytes16)"]
173+
else:
174+
signature_types = ["uint64", "(uint32,bool,uint64,uint64,bool,uint8,uint64)"]
175+
176+
signature = sign_l1_action(
177+
self.wallet,
178+
signature_types,
179+
[oid, order_spec_preprocessing(order_spec)],
180+
ZERO_ADDRESS if self.vault_address is None else self.vault_address,
181+
timestamp,
182+
self.base_url == MAINNET_API_URL,
183+
)
184+
185+
return self._post_action(
186+
{
187+
"type": "modify",
188+
"oid": oid,
189+
"order": order_spec_to_order_wire(order_spec),
190+
},
191+
signature,
192+
timestamp,
193+
)
194+
195+
def market_open(
196+
self,
197+
coin: str,
198+
is_buy: bool,
199+
sz: float,
200+
px: Optional[float] = None,
201+
slippage: float = DEFAULT_SLIPPAGE,
202+
cloid: Optional[Cloid] = None,
203+
) -> Any:
204+
205+
# Get aggressive Market Price
206+
px = self._slippage_price(coin, is_buy, slippage, px)
207+
# Market Order is an aggressive Limit Order IoC
208+
return self.order(coin, is_buy, sz, px, order_type={"limit": {"tif": "Ioc"}}, reduce_only=False, cloid=cloid)
209+
210+
def market_close(
211+
self,
212+
coin: str,
213+
sz: Optional[float] = None,
214+
px: Optional[float] = None,
215+
slippage: float = DEFAULT_SLIPPAGE,
216+
cloid: Optional[Cloid] = None,
217+
) -> Any:
218+
positions = self.info.user_state(self.wallet.address)["assetPositions"]
219+
for position in positions:
220+
item = position["position"]
221+
if coin != item["coin"]:
222+
continue
223+
szi = float(item["szi"])
224+
if not sz:
225+
sz = szi
226+
is_buy = True if szi < 0 else False
227+
# Get aggressive Market Price
228+
px = self._slippage_price(coin, is_buy, slippage, px)
229+
# Market Order is an aggressive Limit Order IoC
230+
return self.order(coin, is_buy, sz, px, order_type={"limit": {"tif": "Ioc"}}, reduce_only=True, cloid=cloid)
231+
125232
def cancel(self, coin: str, oid: int) -> Any:
126233
return self.bulk_cancel([{"coin": coin, "oid": oid}])
127234

hyperliquid/info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def frontend_open_orders(self, address: str) -> Any:
9090
dict of frontend orders
9191
]
9292
coin: str,
93-
isPositoinTpsl: bool,
93+
isPositionTpsl: bool,
9494
isTrigger: bool,
9595
limitPx: float string,
9696
oid: int,

hyperliquid/utils/signing.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def order_grouping_to_number(grouping: Grouping) -> int:
7676
def order_spec_preprocessing(order_spec: OrderSpec) -> Any:
7777
order = order_spec["order"]
7878
order_type_array = order_type_to_tuple(order_spec["orderType"])
79-
res = (
79+
res: Any = (
8080
order["asset"],
8181
order["isBuy"],
8282
float_to_int_for_hashing(order["limitPx"]),
@@ -85,7 +85,7 @@ def order_spec_preprocessing(order_spec: OrderSpec) -> Any:
8585
order_type_array[0],
8686
float_to_int_for_hashing(order_type_array[1]),
8787
)
88-
if "cloid" in order and order["cloid"]:
88+
if "cloid" in order and order["cloid"] is not None:
8989
res += (str_to_bytes16(order["cloid"].to_raw()),)
9090
return res
9191

@@ -121,7 +121,7 @@ def order_type_to_wire(order_type: OrderType) -> OrderTypeWire:
121121
def order_spec_to_order_wire(order_spec: OrderSpec) -> OrderWire:
122122
order = order_spec["order"]
123123
cloid = None
124-
if "cloid" in order and order["cloid"]:
124+
if "cloid" in order and order["cloid"] is not None:
125125
cloid = order["cloid"].to_raw()
126126
return {
127127
"asset": order["asset"],
@@ -254,7 +254,8 @@ def float_to_int(x: float, power: int) -> int:
254254
with_decimals = x * 10**power
255255
if abs(round(with_decimals) - with_decimals) >= 1e-3:
256256
raise ValueError("float_to_int causes rounding", x)
257-
return round(with_decimals)
257+
res: int = round(with_decimals)
258+
return res
258259

259260

260261
def str_to_bytes16(x: str) -> bytearray:

hyperliquid/utils/types.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import sys
24

35
if sys.version_info >= (3, 8):
@@ -64,11 +66,11 @@ def _validate(self):
6466
assert len(self._raw_cloid[2:]) == 32, "cloid is not 16 bytes"
6567

6668
@staticmethod
67-
def from_int(cloid: int):
69+
def from_int(cloid: int) -> Cloid:
6870
return Cloid(f"{cloid:#034x}")
6971

7072
@staticmethod
71-
def from_str(cloid: str):
73+
def from_str(cloid: str) -> Cloid:
7274
return Cloid(cloid)
7375

7476
def to_raw(self):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
55

66
[tool.poetry]
77
name = "hyperliquid-python-sdk"
8-
version = "0.1.15"
8+
version = "0.1.16"
99
description = "SDK for Hyperliquid API trading with Python."
1010
readme = "README.md"
1111
authors = ["Hyperliquid <[email protected]>"]

tests/signing_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,6 @@ def test_sign_usd_transfer_action():
207207
"time": 1687816341423,
208208
}
209209
signature = sign_usd_transfer_action(wallet, message, False)
210-
assert signature["r"] == "0x784b36718fa34328e9875712c3aae5c599e0ed07f2cf59add3102cfba1dc634e"
211-
assert signature["s"] == "0x1cf2ee3b0bc7dd0d7f3236e9c8923ef17bcfbfd524deda4c9427361f1a77af67"
212-
assert signature["v"] == 27
210+
assert signature["r"] == "0x283ca602ac69be536bd2272f050eddf8d250ed3eef083d1fc26989e57f891759"
211+
assert signature["s"] == "0x9bc743cf95042269236bc7f48c06ab8a6a9ee53e04f3336c6cfd1b22783aa74"
212+
assert signature["v"] == 28

0 commit comments

Comments
 (0)