Skip to content

Commit

Permalink
Version 0.1.14
Browse files Browse the repository at this point in the history
  • Loading branch information
Louis Tao committed Nov 17, 2023
1 parent 62f2dfd commit cb63a3c
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 38 deletions.
4 changes: 2 additions & 2 deletions assets/images/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion examples/basic_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,16 @@ def main():

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

# Query the order status by oid
if order_result["status"] == "ok":
status = order_result["response"]["data"]["statuses"][0]
if "resting" in status:
order_status = info.query_order_by_oid(account.address, status["resting"]["oid"])
print("Order status by oid:", order_status)

# Cancel the order
if order_result["status"] == "ok":
status = order_result["response"]["data"]["statuses"][0]
Expand Down
58 changes: 58 additions & 0 deletions examples/basic_order_with_cloid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import json

import eth_account
import utils
from eth_account.signers.local import LocalAccount

from hyperliquid.exchange import Exchange
from hyperliquid.info import Info
from hyperliquid.utils import constants
from hyperliquid.utils.types import Cloid


def main():
config = utils.get_config()
account: LocalAccount = eth_account.Account.from_key(config["secret_key"])
print("Running with account address:", account.address)
info = Info(constants.TESTNET_API_URL, skip_ws=True)

# Get the user state and print out position information
user_state = info.user_state(account.address)
positions = []
for position in user_state["assetPositions"]:
if float(position["position"]["szi"]) != 0:
positions.append(position["position"])
if len(positions) > 0:
print("positions:")
for position in positions:
print(json.dumps(position, indent=2))
else:
print("no open positions")

cloid = Cloid.from_str("0x00000000000000000000000000000001")
# Users can also generate a cloid from an int
# cloid = Cloid.from_int(1)
# Place an order that should rest by setting the price very low
exchange = Exchange(account, constants.TESTNET_API_URL)
order_result = exchange.order("ETH", True, 0.2, 1100, {"limit": {"tif": "Gtc"}}, cloid=cloid)
print(order_result)

# Query the order status by cloid
order_status = info.query_order_by_cloid(account.address, cloid)
print("Order status by cloid:", order_status)

# Non-existent cloid example
invalid_cloid = Cloid.from_int(2)
order_status = info.query_order_by_cloid(account.address, invalid_cloid)
print("Order status by cloid:", order_status)

# Cancel the order by cloid
if order_result["status"] == "ok":
status = order_result["response"]["data"]["statuses"][0]
if "resting" in status:
cancel_result = exchange.cancel_by_cloid("ETH", cloid)
print(cancel_result)


if __name__ == "__main__":
main()
99 changes: 73 additions & 26 deletions hyperliquid/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@
from hyperliquid.utils.signing import (
ZERO_ADDRESS,
CancelRequest,
CancelByCloidRequest,
OrderRequest,
OrderSpec,
OrderType,
float_to_usd_int,
get_timestamp_ms,
order_grouping_to_number,
order_request_to_order_spec,
order_spec_preprocessing,
order_spec_to_order_wire,
sign_l1_action,
sign_usd_transfer_action,
sign_agent,
str_to_bytes16,
)
from hyperliquid.utils.types import Any, List, Literal, Meta, Optional, Tuple
from hyperliquid.utils.types import Any, List, Literal, Meta, Optional, Tuple, Cloid


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

def order(
self, coin: str, is_buy: bool, sz: float, limit_px: float, order_type: OrderType, reduce_only: bool = False
self,
coin: str,
is_buy: bool,
sz: float,
limit_px: float,
order_type: OrderType,
reduce_only: bool = False,
cloid: Optional[Cloid] = None,
) -> Any:
return self.bulk_orders(
[
{
"coin": coin,
"is_buy": is_buy,
"sz": sz,
"limit_px": limit_px,
"order_type": order_type,
"reduce_only": reduce_only,
}
]
)
order = {
"coin": coin,
"is_buy": is_buy,
"sz": sz,
"limit_px": limit_px,
"order_type": order_type,
"reduce_only": reduce_only,
}
if cloid:
order["cloid"] = cloid
return self.bulk_orders([order])

def bulk_orders(self, order_requests: List[OrderRequest]) -> Any:
order_specs: List[OrderSpec] = [
{
"order": {
"asset": self.coin_to_asset[order["coin"]],
"isBuy": order["is_buy"],
"reduceOnly": order["reduce_only"],
"limitPx": order["limit_px"],
"sz": order["sz"],
},
"orderType": order["order_type"],
}
for order in order_requests
order_request_to_order_spec(order, self.coin_to_asset[order["coin"]]) for order in order_requests
]

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

has_cloid = False
for order_spec in order_specs:
if "cloid" in order_spec["order"] and order_spec["order"]["cloid"]:
has_cloid = True

if has_cloid:
for order_spec in order_specs:
if "cloid" not in order_spec["order"] or not order_spec["order"]["cloid"]:
raise ValueError("all orders must have cloids if at least one has a cloid")

if has_cloid:
signature_types = ["(uint32,bool,uint64,uint64,bool,uint8,uint64,bytes16)[]", "uint8"]
else:
signature_types = ["(uint32,bool,uint64,uint64,bool,uint8,uint64)[]", "uint8"]

signature = sign_l1_action(
self.wallet,
["(uint32,bool,uint64,uint64,bool,uint8,uint64)[]", "uint8"],
signature_types,
[[order_spec_preprocessing(order_spec) for order_spec in order_specs], order_grouping_to_number(grouping)],
ZERO_ADDRESS if self.vault_address is None else self.vault_address,
timestamp,
Expand All @@ -111,6 +125,9 @@ def bulk_orders(self, order_requests: List[OrderRequest]) -> Any:
def cancel(self, coin: str, oid: int) -> Any:
return self.bulk_cancel([{"coin": coin, "oid": oid}])

def cancel_by_cloid(self, coin: str, cloid: Cloid) -> Any:
return self.bulk_cancel_by_cloid([{"coin": coin, "cloid": cloid}])

def bulk_cancel(self, cancel_requests: List[CancelRequest]) -> Any:
timestamp = get_timestamp_ms()
signature = sign_l1_action(
Expand All @@ -136,6 +153,36 @@ def bulk_cancel(self, cancel_requests: List[CancelRequest]) -> Any:
timestamp,
)

def bulk_cancel_by_cloid(self, cancel_requests: List[CancelByCloidRequest]) -> Any:
timestamp = get_timestamp_ms()
signature = sign_l1_action(
self.wallet,
["(uint32,bytes16)[]"],
[
[
(self.coin_to_asset[cancel["coin"]], str_to_bytes16(cancel["cloid"].to_raw()))
for cancel in cancel_requests
]
],
ZERO_ADDRESS if self.vault_address is None else self.vault_address,
timestamp,
self.base_url == MAINNET_API_URL,
)
return self._post_action(
{
"type": "cancelByCloid",
"cancels": [
{
"asset": self.coin_to_asset[cancel["coin"]],
"cloid": cancel["cloid"],
}
for cancel in cancel_requests
],
},
signature,
timestamp,
)

def update_leverage(self, leverage: int, coin: str, is_cross: bool = True) -> Any:
timestamp = get_timestamp_ms()
asset = self.coin_to_asset[coin]
Expand Down
8 changes: 7 additions & 1 deletion hyperliquid/info.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from hyperliquid.api import API
from hyperliquid.utils.types import Any, Callable, Meta, Optional, Subscription, cast
from hyperliquid.utils.types import Any, Callable, Meta, Optional, Subscription, cast, Cloid
from hyperliquid.websocket_manager import WebsocketManager


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

def query_order_by_oid(self, user: str, oid: int) -> Any:
return self.post("/info", {"type": "orderStatus", "user": user, "oid": oid})

def query_order_by_cloid(self, user: str, cloid: Cloid) -> Any:
return self.post("/info", {"type": "orderStatus", "user": user, "oid": cloid.to_raw()})

def subscribe(self, subscription: Subscription, callback: Callable[[Any], None]) -> int:
if self.ws_manager is None:
raise RuntimeError("Cannot call subscribe since skip_ws was used")
Expand Down
59 changes: 54 additions & 5 deletions hyperliquid/utils/signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from eth_account.messages import encode_structured_data
from eth_utils import keccak, to_hex

from hyperliquid.utils.types import Any, Literal, Tuple, TypedDict, Union
from hyperliquid.utils.types import Any, Literal, Optional, Tuple, TypedDict, Union, Cloid

ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"

Expand All @@ -17,9 +17,19 @@
OrderTypeWire = TypedDict("OrderTypeWire", {"limit": LimitOrderType, "trigger": TriggerOrderTypeWire}, total=False)
OrderRequest = TypedDict(
"OrderRequest",
{"coin": str, "is_buy": bool, "sz": float, "limit_px": float, "order_type": OrderType, "reduce_only": bool},
{
"coin": str,
"is_buy": bool,
"sz": float,
"limit_px": float,
"order_type": OrderType,
"reduce_only": bool,
"cloid": Optional[Cloid],
},
total=False,
)
CancelRequest = TypedDict("CancelRequest", {"coin": str, "oid": int})
CancelByCloidRequest = TypedDict("CancelByCloidRequest", {"coin": str, "cloid": Cloid})


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


Order = TypedDict("Order", {"asset": int, "isBuy": bool, "limitPx": float, "sz": float, "reduceOnly": bool})
Order = TypedDict(
"Order", {"asset": int, "isBuy": bool, "limitPx": float, "sz": float, "reduceOnly": bool, "cloid": Optional[Cloid]}
)
OrderSpec = TypedDict("OrderSpec", {"order": Order, "orderType": OrderType})


def order_spec_preprocessing(order_spec: OrderSpec) -> Any:
order = order_spec["order"]
order_type_array = order_type_to_tuple(order_spec["orderType"])
return (
res = (
order["asset"],
order["isBuy"],
float_to_int_for_hashing(order["limitPx"]),
Expand All @@ -73,11 +85,22 @@ def order_spec_preprocessing(order_spec: OrderSpec) -> Any:
order_type_array[0],
float_to_int_for_hashing(order_type_array[1]),
)
if "cloid" in order and order["cloid"]:
res += (str_to_bytes16(order["cloid"].to_raw()),)
return res


OrderWire = TypedDict(
"OrderWire",
{"asset": int, "isBuy": bool, "limitPx": str, "sz": str, "reduceOnly": bool, "orderType": OrderTypeWire},
{
"asset": int,
"isBuy": bool,
"limitPx": str,
"sz": str,
"reduceOnly": bool,
"orderType": OrderTypeWire,
"cloid": Optional[Cloid],
},
)


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

def order_spec_to_order_wire(order_spec: OrderSpec) -> OrderWire:
order = order_spec["order"]
cloid = None
if "cloid" in order and order["cloid"]:
cloid = order["cloid"].to_raw()
return {
"asset": order["asset"],
"isBuy": order["isBuy"],
"limitPx": float_to_wire(order["limitPx"]),
"sz": float_to_wire(order["sz"]),
"reduceOnly": order["reduceOnly"],
"orderType": order_type_to_wire(order_spec["orderType"]),
"cloid": cloid,
}


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


def str_to_bytes16(x: str) -> bytearray:
assert x.startswith("0x")
return bytearray.fromhex(x[2:])


def get_timestamp_ms() -> int:
return int(time.time() * 1000)


def order_request_to_order_spec(order: OrderRequest, asset: int) -> OrderSpec:
cloid = None
if "cloid" in order:
cloid = order["cloid"]
return {
"order": {
"asset": asset,
"isBuy": order["is_buy"],
"reduceOnly": order["reduce_only"],
"limitPx": order["limit_px"],
"sz": order["sz"],
"cloid": cloid,
},
"orderType": order["order_type"],
}
Loading

0 comments on commit cb63a3c

Please sign in to comment.