Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions examples/basic_twap_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import json
import time

import example_utils

from hyperliquid.utils import constants

PURR = "PURR/USDC"


def main():
address, info, exchange = example_utils.setup(base_url=constants.TESTNET_API_URL, skip_ws=True)

# Get the user state and print out spot balance information
spot_user_state = info.spot_user_state(address)
if len(spot_user_state["balances"]) > 0:
print("spot balances:")
for balance in spot_user_state["balances"]:
print(json.dumps(balance, indent=2))
else:
print("no available token balances")

# Place a TWAP order
# TWAP orders spread the total size over the specified duration in minutes
# is_buy=True for buy, False for sell
# sz=total size to execute over the duration
# minutes=duration in minutes to spread the order
# reduce_only=False (can only reduce position if True)
# randomize=False (randomize execution timing if True)
twap_result = exchange.twap_order(PURR, True, 20, 5, reduce_only=False, randomize=False)
print("TWAP order result:")
print(json.dumps(twap_result, indent=2))

# Extract TWAP ID from the response
twap_id = None
if twap_result["status"] == "ok":
response_data = twap_result.get("response", {}).get("data", {})
status_info = response_data.get("status", {})
if "running" in status_info:
twap_id = status_info["running"]["twapId"]
print(f"\n✅ TWAP order placed successfully! TWAP ID: {twap_id}")
print(f"Order will execute 20 {PURR} over 5 minutes")
else:
print(f"\n⚠️ Unexpected TWAP status: {status_info}")
else:
print(f"\n❌ TWAP order failed: {twap_result}")
return

# Wait a moment to let the TWAP start executing
print("\nWaiting 15 seconds before cancelling...")
time.sleep(15)

# Cancel the TWAP order
if twap_id is not None:
print(f"\nCancelling TWAP order ID: {twap_id}")
cancel_result = exchange.cancel_twap(PURR, twap_id)
print("TWAP cancel result:")
print(json.dumps(cancel_result, indent=2))

if cancel_result["status"] == "ok":
response_data = cancel_result.get("response", {}).get("data", {})
if response_data.get("status") == "success":
print(f"\n✅ TWAP order {twap_id} cancelled successfully!")
else:
print(f"\n⚠️ TWAP cancel response: {response_data}")
else:
print(f"\n❌ TWAP cancel failed: {cancel_result}")


if __name__ == "__main__":
main()
90 changes: 90 additions & 0 deletions hyperliquid/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
sign_usd_class_transfer_action,
sign_usd_transfer_action,
sign_withdraw_from_bridge_action,
twap_request_to_twap_wire,
)
from hyperliquid.utils.types import (
Any,
Expand Down Expand Up @@ -353,6 +354,95 @@ def schedule_cancel(self, time: Optional[int]) -> Any:
timestamp,
)

def twap_order(
self,
name: str,
is_buy: bool,
sz: float,
minutes: int,
reduce_only: bool = False,
randomize: bool = False,
) -> Any:
"""Place a TWAP (Time-Weighted Average Price) order.

Args:
name: Asset name (e.g., "BTC", "ETH", "PURR/USDC")
is_buy: True for buy, False for sell
sz: Total size to execute over the duration
minutes: Duration in minutes to spread the order
reduce_only: If True, order will only reduce position
randomize: If True, randomize execution timing

Returns:
Response containing twapId in response.data.status.running.twapId
"""
timestamp = get_timestamp_ms()

twap_wire = twap_request_to_twap_wire(
{
"coin": name,
"is_buy": is_buy,
"sz": sz,
"reduce_only": reduce_only,
"minutes": minutes,
"randomize": randomize,
},
self.info.name_to_asset(name),
)

twap_action = {
"type": "twapOrder",
"twap": twap_wire,
}

signature = sign_l1_action(
self.wallet,
twap_action,
self.vault_address,
timestamp,
self.expires_after,
self.base_url == MAINNET_API_URL,
)

return self._post_action(
twap_action,
signature,
timestamp,
)

def cancel_twap(self, name: str, twap_id: int) -> Any:
"""Cancel a TWAP order.

Args:
name: Asset name (e.g., "BTC", "ETH", "PURR/USDC")
twap_id: The TWAP order ID to cancel

Returns:
Response with success/error status
"""
timestamp = get_timestamp_ms()

twap_cancel_action = {
"type": "twapCancel",
"a": self.info.name_to_asset(name),
"t": twap_id,
}

signature = sign_l1_action(
self.wallet,
twap_cancel_action,
self.vault_address,
timestamp,
self.expires_after,
self.base_url == MAINNET_API_URL,
)

return self._post_action(
twap_cancel_action,
signature,
timestamp,
)

def update_leverage(self, leverage: int, name: str, is_cross: bool = True) -> Any:
timestamp = get_timestamp_ms()
update_leverage_action = {
Expand Down
35 changes: 35 additions & 0 deletions hyperliquid/utils/signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@
)
CancelRequest = TypedDict("CancelRequest", {"coin": str, "oid": int})
CancelByCloidRequest = TypedDict("CancelByCloidRequest", {"coin": str, "cloid": Cloid})
TwapRequest = TypedDict(
"TwapRequest",
{
"coin": str,
"is_buy": bool,
"sz": float,
"reduce_only": bool,
"minutes": int,
"randomize": bool,
},
)
TwapCancelRequest = TypedDict("TwapCancelRequest", {"coin": str, "twap_id": int})

Grouping = Union[Literal["na"], Literal["normalTpsl"], Literal["positionTpsl"]]
Order = TypedDict(
Expand Down Expand Up @@ -67,6 +79,18 @@
},
)

TwapWire = TypedDict(
"TwapWire",
{
"a": int,
"b": bool,
"s": str,
"r": bool,
"m": int,
"t": bool,
},
)

ScheduleCancelAction = TypedDict(
"ScheduleCancelAction",
{
Expand Down Expand Up @@ -479,6 +503,17 @@ def order_request_to_order_wire(order: OrderRequest, asset: int) -> OrderWire:
return order_wire


def twap_request_to_twap_wire(twap: TwapRequest, asset: int) -> TwapWire:
return {
"a": asset,
"b": twap["is_buy"],
"s": float_to_wire(twap["sz"]),
"r": twap["reduce_only"],
"m": twap["minutes"],
"t": twap["randomize"],
}


def order_wires_to_order_action(order_wires, builder=None):
action = {
"type": "order",
Expand Down
96 changes: 96 additions & 0 deletions tests/signing_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from hyperliquid.utils.signing import (
OrderRequest,
ScheduleCancelAction,
TwapRequest,
action_hash,
construct_phantom_agent,
float_to_int_for_hashing,
Expand All @@ -13,6 +14,7 @@
sign_l1_action,
sign_usd_transfer_action,
sign_withdraw_from_bridge_action,
twap_request_to_twap_wire,
)
from hyperliquid.utils.types import Cloid

Expand Down Expand Up @@ -271,3 +273,97 @@ def test_schedule_cancel_action():
assert signature_testnet["r"] == "0x4e4f2dbd4107c69783e251b7e1057d9f2b9d11cee213441ccfa2be63516dc5bc"
assert signature_testnet["s"] == "0x706c656b23428c8ba356d68db207e11139ede1670481a9e01ae2dfcdb0e1a678"
assert signature_testnet["v"] == 27


def test_twap_request_to_twap_wire():
twap_request: TwapRequest = {
"coin": "ETH",
"is_buy": True,
"sz": 100.5,
"reduce_only": False,
"minutes": 5,
"randomize": False,
}
twap_wire = twap_request_to_twap_wire(twap_request, 1)
assert twap_wire["a"] == 1
assert twap_wire["b"] is True
assert twap_wire["s"] == "100.5"
assert twap_wire["r"] is False
assert twap_wire["m"] == 5
assert twap_wire["t"] is False


def test_twap_order_action_signing():
wallet = eth_account.Account.from_key("0x0123456789012345678901234567890123456789012345678901234567890123")
twap_request: TwapRequest = {
"coin": "BTC",
"is_buy": True,
"sz": 1.5,
"reduce_only": False,
"minutes": 10,
"randomize": False,
}
twap_wire = twap_request_to_twap_wire(twap_request, 0)
twap_action = {
"type": "twapOrder",
"twap": twap_wire,
}
timestamp = 0

signature_mainnet = sign_l1_action(
wallet,
twap_action,
None,
timestamp,
None,
True,
)
assert signature_mainnet["r"] == "0x459e5dd4a30e60252536cf864a946be1a69da80d937f7841382a1bb471a6ccef"
assert signature_mainnet["s"] == "0x37229416621aaab0b74e4377e87a01e88cb5f946c0eeed4581b640aa859a9bfc"
assert signature_mainnet["v"] == 27

signature_testnet = sign_l1_action(
wallet,
twap_action,
None,
timestamp,
None,
False,
)
assert signature_testnet["r"] == "0x8c99939356b65ea42731d12d5dcecf9c750ab49c9f5112e5356f13d6b94077c2"
assert signature_testnet["s"] == "0x1523831a22a41779ae772cdba6206eaba1cef79081d965bf912a16fd9a6363ab"
assert signature_testnet["v"] == 28


def test_twap_cancel_action_signing():
wallet = eth_account.Account.from_key("0x0123456789012345678901234567890123456789012345678901234567890123")
twap_cancel_action = {
"type": "twapCancel",
"a": 10001, # Spot asset (10000 + index 1)
"t": 12345, # TWAP ID
}
timestamp = 0

signature_mainnet = sign_l1_action(
wallet,
twap_cancel_action,
None,
timestamp,
None,
True,
)
assert signature_mainnet["r"] == "0x459fdfa693f77f07defb7e4bf9302f5f887234d31129cdd0bb9894e0888af54d"
assert signature_mainnet["s"] == "0x46c4f00b90a53c9b51d8825e899188eab52671206a2cb39bcb1e3fade009a1da"
assert signature_mainnet["v"] == 27

signature_testnet = sign_l1_action(
wallet,
twap_cancel_action,
None,
timestamp,
None,
False,
)
assert signature_testnet["r"] == "0x638e0b4fa50bef42ce84ef765423670c6f25da43a9af1c9cfb9f367d628b3226"
assert signature_testnet["s"] == "0x7ffb43b336be19ada86138bd79cde5c6680e97bb479ab222f6e3055daa83bb9e"
assert signature_testnet["v"] == 28