Skip to content

Commit

Permalink
Merge pull request hummingbot#6832 from yancong001/feat/hyperliquid_v…
Browse files Browse the repository at this point in the history
…ault

Feat/hyperliquid vault
  • Loading branch information
rapcmia authored Feb 6, 2024
2 parents de3097d + 6ecae54 commit 418b9dd
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ class HyperliquidPerpetualAuth(AuthBase):
Auth class required by Hyperliquid Perpetual API
"""

def __init__(self, api_key: str, api_secret: str):
def __init__(self, api_key: str, api_secret: str, use_vault: bool):
self._api_key: str = api_key
self._api_secret: str = api_secret
self._use_vault: bool = use_vault
self.wallet = eth_account.Account.from_key(api_secret)

def sign_inner(self, wallet, data):
Expand Down Expand Up @@ -95,15 +96,15 @@ def _sign_update_leverage_params(self, params, base_url, timestamp):
self.wallet,
signature_types,
res,
ZERO_ADDRESS,
ZERO_ADDRESS if not self._use_vault else self._api_key,
timestamp,
CONSTANTS.PERPETUAL_BASE_URL in base_url,
)
payload = {
"action": params,
"nonce": timestamp,
"signature": signature,
"vaultAddress": None,
"vaultAddress": self._api_key if self._use_vault else None,
}
return payload

Expand All @@ -119,7 +120,7 @@ def _sign_cancel_params(self, params, base_url, timestamp):
self.wallet,
signature_types,
[[res]],
ZERO_ADDRESS,
ZERO_ADDRESS if not self._use_vault else self._api_key,
timestamp,
CONSTANTS.PERPETUAL_BASE_URL in base_url,
)
Expand All @@ -130,7 +131,8 @@ def _sign_cancel_params(self, params, base_url, timestamp):
},
"nonce": timestamp,
"signature": signature,
"vaultAddress": None,
"vaultAddress": self._api_key if self._use_vault else None,

}
return payload

Expand All @@ -155,7 +157,7 @@ def _sign_order_params(self, params, base_url, timestamp):
self.wallet,
signature_types,
[[res], order_grouping_to_number(grouping)],
ZERO_ADDRESS,
ZERO_ADDRESS if not self._use_vault else self._api_key,
timestamp,
CONSTANTS.PERPETUAL_BASE_URL in base_url,
)
Expand All @@ -168,7 +170,8 @@ def _sign_order_params(self, params, base_url, timestamp):
},
"nonce": timestamp,
"signature": signature,
"vaultAddress": None,
"vaultAddress": self._api_key if self._use_vault else None,

}
return payload

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,23 @@

class HyperliquidPerpetualDerivative(PerpetualDerivativePyBase):
web_utils = web_utils

SHORT_POLL_INTERVAL = 5.0
LONG_POLL_INTERVAL = 12.0

def __init__(
self,
client_config_map: "ClientConfigAdapter",
hyperliquid_perpetual_api_key: str = None,
hyperliquid_perpetual_api_secret: str = None,
use_vault: bool = False,
hyperliquid_perpetual_api_key: str = None,
trading_pairs: Optional[List[str]] = None,
trading_required: bool = True,
domain: str = CONSTANTS.DOMAIN,
):
self.hyperliquid_perpetual_api_key = hyperliquid_perpetual_api_key
self.hyperliquid_perpetual_secret_key = hyperliquid_perpetual_api_secret
self._use_vault = use_vault
self._trading_required = trading_required
self._trading_pairs = trading_pairs
self._domain = domain
Expand All @@ -62,14 +65,19 @@ def __init__(
self.coin_to_asset: Dict[str, int] = {}
super().__init__(client_config_map)

SHORT_POLL_INTERVAL = 5.0

LONG_POLL_INTERVAL = 12.0

@property
def name(self) -> str:
# Note: domain here refers to the entire exchange name. i.e. hyperliquid_perpetual or hyperliquid_perpetual_testnet
return self._domain

@property
def authenticator(self) -> HyperliquidPerpetualAuth:
return HyperliquidPerpetualAuth(self.hyperliquid_perpetual_api_key, self.hyperliquid_perpetual_secret_key)
return HyperliquidPerpetualAuth(self.hyperliquid_perpetual_api_key, self.hyperliquid_perpetual_secret_key,
self._use_vault)

@property
def rate_limits_rules(self) -> List[RateLimit]:
Expand Down Expand Up @@ -464,7 +472,8 @@ async def _handle_update_error_for_active_order(self, order: InFlightOrder, erro
self.logger().warning(
f"Error fetching status update for the active order {order.client_order_id}: {request_error}.",
)
self.logger().debug(f"Order {order.client_order_id} not found counter: {self._order_tracker._order_not_found_records.get(order.client_order_id, 0)}")
self.logger().debug(
f"Order {order.client_order_id} not found counter: {self._order_tracker._order_not_found_records.get(order.client_order_id, 0)}")
await self._order_tracker.process_order_not_found(order.client_order_id)

async def _request_order_status(self, tracked_order: InFlightOrder) -> OrderUpdate:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from decimal import Decimal
from typing import Optional

from pydantic import Field, SecretStr
from pydantic.class_validators import validator

from hummingbot.client.config.config_data_types import BaseConnectorConfigMap, ClientFieldData
from hummingbot.core.data_type.trade_fee import TradeFeeSchema
Expand All @@ -19,27 +21,54 @@
BROKER_ID = "HBOT"


def validate_bool(value: str) -> Optional[str]:
"""
Permissively interpret a string as a boolean
"""
valid_values = ('true', 'yes', 'y', 'false', 'no', 'n')
if value.lower() not in valid_values:
return f"Invalid value, please choose value from {valid_values}"


class HyperliquidPerpetualConfigMap(BaseConnectorConfigMap):
connector: str = Field(default="hyperliquid_perpetual", client_data=None)
hyperliquid_perpetual_api_key: SecretStr = Field(
hyperliquid_perpetual_api_secret: SecretStr = Field(
default=...,
client_data=ClientFieldData(
prompt=lambda cm: "Enter your Arbitrum wallet public key",
prompt=lambda cm: "Enter your Arbitrum wallet private key",
is_secure=True,
is_connect_key=True,
prompt_on_new=True,
)
)
hyperliquid_perpetual_api_secret: SecretStr = Field(
use_vault: bool = Field(
default="no",
client_data=ClientFieldData(
prompt=lambda cm: "Do you want to use the vault address?(Yes/No)",
is_secure=False,
is_connect_key=True,
prompt_on_new=True,
),
)
hyperliquid_perpetual_api_key: SecretStr = Field(
default=...,
client_data=ClientFieldData(
prompt=lambda cm: "Enter your Arbitrum wallet private key",
prompt=lambda cm: "Enter your Arbitrum or vault address",
is_secure=True,
is_connect_key=True,
prompt_on_new=True,
)
)

@validator("use_vault", pre=True)
def validate_bool(cls, v: str):
"""Used for client-friendly error output."""
if isinstance(v, str):
ret = validate_bool(v)
if ret is not None:
raise ValueError(ret)
return v


KEYS = HyperliquidPerpetualConfigMap.construct()

Expand All @@ -51,19 +80,28 @@ class HyperliquidPerpetualConfigMap(BaseConnectorConfigMap):

class HyperliquidPerpetualTestnetConfigMap(BaseConnectorConfigMap):
connector: str = Field(default="hyperliquid_perpetual_testnet", client_data=None)
hyperliquid_perpetual_testnet_api_key: SecretStr = Field(
hyperliquid_perpetual_testnet_api_secret: SecretStr = Field(
default=...,
client_data=ClientFieldData(
prompt=lambda cm: "Enter your Arbitrum wallet address",
prompt=lambda cm: "Enter your Arbitrum wallet private key",
is_secure=True,
is_connect_key=True,
prompt_on_new=True,
)
)
hyperliquid_perpetual_testnet_api_secret: SecretStr = Field(
use_vault: bool = Field(
default="no",
client_data=ClientFieldData(
prompt=lambda cm: "Do you want to use the vault address?(Yes/No)",
is_secure=False,
is_connect_key=True,
prompt_on_new=True,
),
)
hyperliquid_perpetual_testnet_api_key: SecretStr = Field(
default=...,
client_data=ClientFieldData(
prompt=lambda cm: "Enter your Arbitrum wallet private key",
prompt=lambda cm: "Enter your Arbitrum or vault address",
is_secure=True,
is_connect_key=True,
prompt_on_new=True,
Expand All @@ -73,5 +111,14 @@ class HyperliquidPerpetualTestnetConfigMap(BaseConnectorConfigMap):
class Config:
title = "hyperliquid_perpetual"

@validator("use_vault", pre=True)
def validate_bool(cls, v: str):
"""Used for client-friendly error output."""
if isinstance(v, str):
ret = validate_bool(v)
if ret is not None:
raise ValueError(ret)
return v


OTHER_DOMAINS_KEYS = {"hyperliquid_perpetual_testnet": HyperliquidPerpetualTestnetConfigMap.construct()}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def setUp(self) -> None:
client_config_map,
hyperliquid_perpetual_api_key="testkey",
hyperliquid_perpetual_api_secret="13e56ca9cceebf1f33065c2c5376ab38570a114bc1b003b60d838f92be9d7930", # noqa: mock
use_vault=False,
trading_pairs=[self.trading_pair],
)
self.data_source = HyperliquidPerpetualAPIOrderBookDataSource(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ def setUp(self) -> None:
super().setUp()
self.api_key = "testApiKey"
self.secret_key = "13e56ca9cceebf1f33065c2c5376ab38570a114bc1b003b60d838f92be9d7930" # noqa: mock

self.auth = HyperliquidPerpetualAuth(api_key=self.api_key, api_secret=self.secret_key)
self.use_vault = False # noqa: mock
self.auth = HyperliquidPerpetualAuth(api_key=self.api_key, api_secret=self.secret_key, use_vault=self.use_vault)

def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
ret = asyncio.get_event_loop().run_until_complete(asyncio.wait_for(coroutine, timeout))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def setUpClass(cls) -> None:
super().setUpClass()
cls.api_key = "someKey"
cls.api_secret = "13e56ca9cceebf1f33065c2c5376ab38570a114bc1b003b60d838f92be9d7930" # noqa: mock
cls.use_vault = False # noqa: mock
cls.user_id = "someUserId"
cls.base_asset = "BTC"
cls.quote_asset = "USD" # linear
Expand Down Expand Up @@ -385,8 +386,9 @@ def create_exchange_instance(self):
client_config_map = ClientConfigAdapter(ClientConfigMap())
exchange = HyperliquidPerpetualDerivative(
client_config_map,
self.api_key,
self.api_secret,
self.use_vault,
self.api_key,
trading_pairs=[self.trading_pair],
)
# exchange._last_trade_history_timestamp = self.latest_trade_hist_timestamp
Expand Down Expand Up @@ -777,6 +779,7 @@ def test_supported_position_modes(self):
client_config_map=client_config_map,
hyperliquid_perpetual_api_key=self.api_key,
hyperliquid_perpetual_api_secret=self.api_secret,
use_vault=self.use_vault,
trading_pairs=[self.trading_pair],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def setUpClass(cls) -> None:
cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
cls.ex_trading_pair = f"{cls.base_asset}_{cls.quote_asset}"
cls.api_key = "someKey"
cls.use_vault = False
cls.api_secret_key = "13e56ca9cceebf1f33065c2c5376ab38570a114bc1b003b60d838f92be9d7930" # noqa: mock"

def setUp(self) -> None:
Expand All @@ -47,7 +48,8 @@ def setUp(self) -> None:
self.mock_time_provider.time.return_value = 1000
self.auth = HyperliquidPerpetualAuth(
api_key=self.api_key,
api_secret=self.api_secret_key)
api_secret=self.api_secret_key,
use_vault=self.use_vault)
self.time_synchronizer = TimeSynchronizer()
self.time_synchronizer.add_time_offset_ms_sample(0)

Expand All @@ -56,6 +58,7 @@ def setUp(self) -> None:
client_config_map=client_config_map,
hyperliquid_perpetual_api_key=self.api_key,
hyperliquid_perpetual_api_secret=self.api_secret_key,
use_vault=self.use_vault,
trading_pairs=[])
self.connector._web_assistants_factory._auth = self.auth

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
from unittest import TestCase

from hummingbot.connector.derivative.hyperliquid_perpetual.hyperliquid_perpetual_utils import (
HyperliquidPerpetualConfigMap,
HyperliquidPerpetualTestnetConfigMap,
validate_bool,
)


class HyperliquidPerpetualUtilsTests(TestCase):
pass

def test_validate_bool_succeed(self):
valid_values = ['true', 'yes', 'y', 'false', 'no', 'n']

validations = [validate_bool(value) for value in valid_values]
for validation in validations:
self.assertIsNone(validation)

def test_validate_bool_fails(self):
wrong_value = "ye"
valid_values = ('true', 'yes', 'y', 'false', 'no', 'n')

validation_error = validate_bool(wrong_value)
self.assertEqual(validation_error, f"Invalid value, please choose value from {valid_values}")

def test_cls_validate_bool_succeed(self):
valid_values = ['true', 'yes', 'y', 'false', 'no', 'n']

validations = [HyperliquidPerpetualConfigMap.validate_bool(value) for value in valid_values]
for validation in validations:
self.assertTrue(validation)

def test_cls_validate_bool_fails(self):
wrong_value = "ye"
valid_values = ('true', 'yes', 'y', 'false', 'no', 'n')
with self.assertRaises(ValueError) as exception_context:
HyperliquidPerpetualConfigMap.validate_bool(wrong_value)
self.assertEqual(str(exception_context.exception), f"Invalid value, please choose value from {valid_values}")

def test_cls_testnet_validate_bool_succeed(self):
valid_values = ['true', 'yes', 'y', 'false', 'no', 'n']

validations = [HyperliquidPerpetualTestnetConfigMap.validate_bool(value) for value in valid_values]
for validation in validations:
self.assertTrue(validation)

def test_cls_testnet_validate_bool_fails(self):
wrong_value = "ye"
valid_values = ('true', 'yes', 'y', 'false', 'no', 'n')
with self.assertRaises(ValueError) as exception_context:
HyperliquidPerpetualTestnetConfigMap.validate_bool(wrong_value)
self.assertEqual(str(exception_context.exception), f"Invalid value, please choose value from {valid_values}")

0 comments on commit 418b9dd

Please sign in to comment.