Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Genius yield spot connector #1

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
Empty file.
2 changes: 2 additions & 0 deletions hummingbot/connector/exchange/genius_yield/dummy.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cdef class dummy():
pass
2 changes: 2 additions & 0 deletions hummingbot/connector/exchange/genius_yield/dummy.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cdef class dummy():
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import asyncio
import time
from typing import TYPE_CHECKING, Any, Dict, List, Optional

from hummingbot.connector.exchange.genius_yield import genius_yield_constants as CONSTANTS, genius_yield_web_utils as web_utils
from hummingbot.connector.exchange.genius_yield.genius_yield_order_book import GeniusYieldOrderBook
from hummingbot.core.data_type.order_book_message import OrderBookMessage
from hummingbot.core.data_type.order_book_tracker_data_source import OrderBookTrackerDataSource
from hummingbot.core.web_assistant.connections.data_types import RESTMethod
from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory
from hummingbot.logger import HummingbotLogger

if TYPE_CHECKING:
from hummingbot.connector.exchange.genius_yield.genius_yield_exchange import GeniusYieldExchange


class GeniusYieldAPIOrderBookDataSource(OrderBookTrackerDataSource):
HEARTBEAT_TIME_INTERVAL = 30.0

_logger: Optional[HummingbotLogger] = None

def __init__(self,
trading_pairs: List[str],
connector: 'GeniusYieldExchange',
api_factory: WebAssistantsFactory,
domain: str = CONSTANTS.DEFAULT_DOMAIN):
super().__init__(trading_pairs)
self._connector = connector
self._domain = domain
self._api_factory = api_factory

async def get_last_traded_prices(self,
trading_pairs: List[str],
domain: Optional[str] = None) -> Dict[str, float]:
return await self._connector.get_last_traded_prices(trading_pairs=trading_pairs)

async def _request_order_book_snapshot(self, trading_pair: str) -> Dict[str, Any]:
"""
Retrieves a copy of the full order book from the exchange, for a particular trading pair.

:param trading_pair: the trading pair for which the order book will be retrieved

:return: the response from the exchange (JSON dictionary)
"""
params = {
"symbol": await self._connector.exchange_symbol_associated_to_pair(trading_pair=trading_pair),
"limit": "1000"
}

rest_assistant = await self._api_factory.get_rest_assistant()
data = await rest_assistant.execute_request(
url=web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self._domain),
params=params,
method=RESTMethod.GET,
throttler_limit_id=CONSTANTS.SNAPSHOT_PATH_URL,
)

return data

async def _order_book_snapshot(self, trading_pair: str) -> OrderBookMessage:
snapshot: Dict[str, Any] = await self._request_order_book_snapshot(trading_pair)
snapshot_timestamp: float = time.time()
snapshot_msg: OrderBookMessage = GeniusYieldOrderBook.snapshot_message_from_exchange(
snapshot,
snapshot_timestamp,
metadata={"trading_pair": trading_pair}
)
return snapshot_msg

async def _parse_trade_message(self, raw_message: Dict[str, Any], message_queue: asyncio.Queue):
if "result" not in raw_message:
trading_pair = await self._connector.trading_pair_associated_to_exchange_symbol(symbol=raw_message["s"])
trade_message = GeniusYieldOrderBook.trade_message_from_exchange(
raw_message, {"trading_pair": trading_pair})
message_queue.put_nowait(trade_message)

async def _parse_order_book_diff_message(self, raw_message: Dict[str, Any], message_queue: asyncio.Queue):
if "result" not in raw_message:
trading_pair = await self._connector.trading_pair_associated_to_exchange_symbol(symbol=raw_message["s"])
order_book_message: OrderBookMessage = GeniusYieldOrderBook.diff_message_from_exchange(
raw_message, time.time(), {"trading_pair": trading_pair})
message_queue.put_nowait(order_book_message)

def _channel_originating_message(self, event_message: Dict[str, Any]) -> str:
channel = ""
if "result" not in event_message:
event_type = event_message.get("e")
channel = (self._diff_messages_queue_key if event_type == CONSTANTS.DIFF_EVENT_TYPE
else self._trade_messages_queue_key)
return channel
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import asyncio
from typing import TYPE_CHECKING, List, Optional

from hummingbot.connector.exchange.genius_yield import genius_yield_constants as CONSTANTS, genius_yield_web_utils as web_utils
from hummingbot.connector.exchange.genius_yield.genius_yield_auth import GeniusYieldAuth
from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource
from hummingbot.core.web_assistant.connections.data_types import RESTMethod
from hummingbot.core.web_assistant.web_assistants_factory import WebAssistantsFactory
from hummingbot.logger import HummingbotLogger

if TYPE_CHECKING:
from hummingbot.connector.exchange.genius_yield.genius_yield_exchange import GeniusYieldExchange


class GeniusYieldAPIUserStreamDataSource(UserStreamTrackerDataSource):
_logger: Optional[HummingbotLogger] = None

def __init__(self,
auth: GeniusYieldAuth,
trading_pairs: List[str],
connector: 'GeniusYieldExchange',
api_factory: WebAssistantsFactory,
domain: str = CONSTANTS.DEFAULT_DOMAIN):
super().__init__()
self._auth: GeniusYieldAuth = auth
self._domain = domain
self._api_factory = api_factory

async def _request_user_stream(self):
rest_assistant = await self._api_factory.get_rest_assistant()
try:
data = await rest_assistant.execute_request(
url=web_utils.private_rest_url(path_url=CONSTANTS.GENIUS_YIELD_USER_STREAM_PATH_URL, domain=self._domain),
method=RESTMethod.POST,
throttler_limit_id=CONSTANTS.GENIUS_YIELD_USER_STREAM_PATH_URL,
headers=self._auth.header_for_authentication()
)
except asyncio.CancelledError:
raise
except Exception as exception:
raise IOError(f"Error fetching user stream. Error: {exception}")

return data

async def _listen_for_user_stream(self):
"""
This method listens to the user stream.
"""
while True:
try:
data = await self._request_user_stream()
# Process the user stream data
except asyncio.CancelledError:
raise
except Exception as e:
self.logger().error(f"Unexpected error while listening to user stream. Error: {str(e)}")
await self._sleep(5.0)
55 changes: 55 additions & 0 deletions hummingbot/connector/exchange/genius_yield/genius_yield_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import hashlib
import hmac
import json
from collections import OrderedDict
from typing import Any, Dict
from urllib.parse import urlencode

from hummingbot.connector.time_synchronizer import TimeSynchronizer
from hummingbot.core.web_assistant.auth import AuthBase
from hummingbot.core.web_assistant.connections.data_types import RESTMethod, RESTRequest


class GeniusYieldAuth(AuthBase):
def __init__(self, api_key: str, secret_key: str, time_provider: TimeSynchronizer):
self.api_key = api_key
self.secret_key = secret_key
self.time_provider = time_provider

async def rest_authenticate(self, request: RESTRequest) -> RESTRequest:
"""
Adds the server time and the signature to the request, required for authenticated interactions. It also adds
the required parameter in the request header.
:param request: the request to be configured for authenticated interaction
"""
if request.method == RESTMethod.POST:
request.data = self.add_auth_to_params(params=json.loads(request.data))
else:
request.params = self.add_auth_to_params(params=request.params)

headers = {}
if request.headers is not None:
headers.update(request.headers)
headers.update(self.header_for_authentication())
request.headers = headers

return request

def add_auth_to_params(self, params: Dict[str, Any]) -> Dict[str, Any]:
timestamp = int(self.time_provider.time() * 1e3)

request_params = OrderedDict(params or {})
request_params["timestamp"] = timestamp

signature = self._generate_signature(params=request_params)
request_params["signature"] = signature

return request_params

def header_for_authentication(self) -> Dict[str, str]:
return {"X-MBX-APIKEY": self.api_key}

def _generate_signature(self, params: Dict[str, Any]) -> str:
encoded_params_str = urlencode(params)
digest = hmac.new(self.secret_key.encode("utf8"), encoded_params_str.encode("utf8"), hashlib.sha256).hexdigest()
return digest
107 changes: 107 additions & 0 deletions hummingbot/connector/exchange/genius_yield/genius_yield_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from hummingbot.core.api_throttler.data_types import LinkedLimitWeightPair, RateLimit
from hummingbot.core.data_type.in_flight_order import OrderState

DEFAULT_DOMAIN = "com"

HBOT_ORDER_ID_PREFIX = "x-XEKWYICX"
MAX_ORDER_ID_LEN = 32

# Base URL
REST_URL = "https://api.genius_yield.{}/api/"
WSS_URL = "wss://stream.genius_yield.{}:9443/ws"

PUBLIC_API_VERSION = "v0"
PRIVATE_API_VERSION = "v0"

# Public API endpoints
TICKER_PRICE_CHANGE_PATH_URL = "/historical-prices/tap-tools/{asset}"
TICKER_BOOK_PATH_URL = "/order-books/{market-id}"
EXCHANGE_INFO_PATH_URL = "/markets"
PING_PATH_URL = "/settings"
SNAPSHOT_PATH_URL = "/order-books/{market-id}"
SERVER_TIME_PATH_URL = "/settings"

# Private API endpoints
ACCOUNTS_PATH_URL = "/balances/{address}"
MY_TRADES_PATH_URL = "/orders/details/{nft-token}"
ORDER_PATH_URL = "/orders"
GENIUS_YIELD_USER_STREAM_PATH_URL = "/userDataStream"

WS_HEARTBEAT_TIME_INTERVAL = 30

# Genius Yield params
SIDE_BUY = "BUY"
SIDE_SELL = "SELL"

TIME_IN_FORCE_GTC = "GTC" # Good till cancelled
TIME_IN_FORCE_IOC = "IOC" # Immediate or cancel
TIME_IN_FORCE_FOK = "FOK" # Fill or kill

# Rate Limit Type
REQUEST_WEIGHT = "REQUEST_WEIGHT"
ORDERS = "ORDERS"
ORDERS_24HR = "ORDERS_24HR"
RAW_REQUESTS = "RAW_REQUESTS"

# Rate Limit time intervals
ONE_MINUTE = 60
ONE_SECOND = 1
ONE_DAY = 86400

MAX_REQUEST = 5000

# Order States
ORDER_STATE = {
"PENDING": OrderState.PENDING_CREATE,
"NEW": OrderState.OPEN,
"FILLED": OrderState.FILLED,
"PARTIALLY_FILLED": OrderState.PARTIALLY_FILLED,
"PENDING_CANCEL": OrderState.OPEN,
"CANCELED": OrderState.CANCELED,
"REJECTED": OrderState.FAILED,
"EXPIRED": OrderState.FAILED,
}

# Rate limits
RATE_LIMITS = [
# Pools
RateLimit(limit_id=REQUEST_WEIGHT, limit=6000, time_interval=ONE_MINUTE),
RateLimit(limit_id=ORDERS, limit=50, time_interval=10 * ONE_SECOND),
RateLimit(limit_id=ORDERS_24HR, limit=160000, time_interval=ONE_DAY),
RateLimit(limit_id=RAW_REQUESTS, limit=61000, time_interval=5 * ONE_MINUTE),
# Weighted Limits
RateLimit(limit_id=TICKER_PRICE_CHANGE_PATH_URL, limit=MAX_REQUEST, time_interval=ONE_MINUTE,
linked_limits=[LinkedLimitWeightPair(REQUEST_WEIGHT, 2),
LinkedLimitWeightPair(RAW_REQUESTS, 1)]),
RateLimit(limit_id=TICKER_BOOK_PATH_URL, limit=MAX_REQUEST, time_interval=ONE_MINUTE,
linked_limits=[LinkedLimitWeightPair(REQUEST_WEIGHT, 4),
LinkedLimitWeightPair(RAW_REQUESTS, 1)]),
RateLimit(limit_id=EXCHANGE_INFO_PATH_URL, limit=MAX_REQUEST, time_interval=ONE_MINUTE,
linked_limits=[LinkedLimitWeightPair(REQUEST_WEIGHT, 20),
LinkedLimitWeightPair(RAW_REQUESTS, 1)]),
RateLimit(limit_id=SNAPSHOT_PATH_URL, limit=MAX_REQUEST, time_interval=ONE_MINUTE,
linked_limits=[LinkedLimitWeightPair(REQUEST_WEIGHT, 100),
LinkedLimitWeightPair(RAW_REQUESTS, 1)]),
RateLimit(limit_id=SERVER_TIME_PATH_URL, limit=MAX_REQUEST, time_interval=ONE_MINUTE,
linked_limits=[LinkedLimitWeightPair(REQUEST_WEIGHT, 1),
LinkedLimitWeightPair(RAW_REQUESTS, 1)]),
RateLimit(limit_id=PING_PATH_URL, limit=MAX_REQUEST, time_interval=ONE_MINUTE,
linked_limits=[LinkedLimitWeightPair(REQUEST_WEIGHT, 1),
LinkedLimitWeightPair(RAW_REQUESTS, 1)]),
RateLimit(limit_id=ACCOUNTS_PATH_URL, limit=MAX_REQUEST, time_interval=ONE_MINUTE,
linked_limits=[LinkedLimitWeightPair(REQUEST_WEIGHT, 20),
LinkedLimitWeightPair(RAW_REQUESTS, 1)]),
RateLimit(limit_id=MY_TRADES_PATH_URL, limit=MAX_REQUEST, time_interval=ONE_MINUTE,
linked_limits=[LinkedLimitWeightPair(REQUEST_WEIGHT, 20),
LinkedLimitWeightPair(RAW_REQUESTS, 1)]),
RateLimit(limit_id=ORDER_PATH_URL, limit=MAX_REQUEST, time_interval=ONE_MINUTE,
linked_limits=[LinkedLimitWeightPair(REQUEST_WEIGHT, 4),
LinkedLimitWeightPair(ORDERS, 1),
LinkedLimitWeightPair(ORDERS_24HR, 1),
LinkedLimitWeightPair(RAW_REQUESTS, 1)])
]

ORDER_NOT_EXIST_ERROR_CODE = -2013
ORDER_NOT_EXIST_MESSAGE = "Order does not exist"
UNKNOWN_ORDER_ERROR_CODE = -2011
UNKNOWN_ORDER_MESSAGE = "Unknown order sent"
Loading