Skip to content

Commit

Permalink
Split binance_us rate oracle from binance
Browse files Browse the repository at this point in the history
  • Loading branch information
mlguys committed Apr 25, 2024
1 parent 50be513 commit 1074ecd
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 19 deletions.
12 changes: 12 additions & 0 deletions hummingbot/client/config/client_config_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,17 @@ class Config:
title = "binance"


class BinanceUSRateSourceMode(ExchangeRateSourceModeBase):
name: str = Field(
default="binance_us",
const=True,
client_data=None,
)

class Config:
title = "binance_us"


class CubeRateSourceMode(ExchangeRateSourceModeBase):
name: str = Field(
default="cube",
Expand Down Expand Up @@ -935,6 +946,7 @@ class Config:
RATE_SOURCE_MODES = {
AscendExRateSourceMode.Config.title: AscendExRateSourceMode,
BinanceRateSourceMode.Config.title: BinanceRateSourceMode,
BinanceUSRateSourceMode.Config.title: BinanceUSRateSourceMode,
CoinGeckoRateSourceMode.Config.title: CoinGeckoRateSourceMode,
CoinCapRateSourceMode.Config.title: CoinCapRateSourceMode,
KuCoinRateSourceMode.Config.title: KuCoinRateSourceMode,
Expand Down
2 changes: 2 additions & 0 deletions hummingbot/core/rate_oracle/rate_oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from hummingbot.core.network_iterator import NetworkStatus
from hummingbot.core.rate_oracle.sources.ascend_ex_rate_source import AscendExRateSource
from hummingbot.core.rate_oracle.sources.binance_rate_source import BinanceRateSource
from hummingbot.core.rate_oracle.sources.binance_us_rate_source import BinanceUSRateSource
from hummingbot.core.rate_oracle.sources.coin_cap_rate_source import CoinCapRateSource
from hummingbot.core.rate_oracle.sources.coin_gecko_rate_source import CoinGeckoRateSource
from hummingbot.core.rate_oracle.sources.coinbase_advanced_trade_rate_source import CoinbaseAdvancedTradeRateSource
Expand All @@ -22,6 +23,7 @@

RATE_ORACLE_SOURCES = {
"binance": BinanceRateSource,
"binance_us": BinanceUSRateSource,
"coin_gecko": CoinGeckoRateSource,
"coin_cap": CoinCapRateSource,
"kucoin": KucoinRateSource,
Expand Down
87 changes: 87 additions & 0 deletions hummingbot/core/rate_oracle/sources/binance_us_rate_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from decimal import Decimal
from typing import TYPE_CHECKING, Dict, Optional

from hummingbot.connector.utils import split_hb_trading_pair
from hummingbot.core.rate_oracle.sources.rate_source_base import RateSourceBase
from hummingbot.core.utils import async_ttl_cache
from hummingbot.core.utils.async_utils import safe_gather

if TYPE_CHECKING:
from hummingbot.connector.exchange.binance.binance_exchange import BinanceExchange


class BinanceUSRateSource(RateSourceBase):
def __init__(self):
super().__init__()
self._binance_us_exchange: Optional[BinanceExchange] = None # delayed because of circular reference

@property
def name(self) -> str:
return "binance_us"

@async_ttl_cache(ttl=30, maxsize=1)
async def get_prices(self, quote_token: Optional[str] = None) -> Dict[str, Decimal]:
self._ensure_exchanges()
results = {}
tasks = [
self._get_binance_prices(exchange=self._binance_us_exchange, quote_token="USD"),
]
task_results = await safe_gather(*tasks, return_exceptions=True)
for task_result in task_results:
if isinstance(task_result, Exception):
self.logger().error(
msg="Unexpected error while retrieving rates from Binance. Check the log file for more info.",
exc_info=task_result,
)
break
else:
results.update(task_result)
return results

def _ensure_exchanges(self):
if self._binance_us_exchange is None:
self._binance_us_exchange = self._build_binance_connector_without_private_keys(domain="us")

@staticmethod
async def _get_binance_prices(exchange: 'BinanceExchange', quote_token: str = None) -> Dict[str, Decimal]:
"""
Fetches binance prices
:param exchange: The exchange instance from which to query prices.
:param quote_token: A quote symbol, if specified only pairs with the quote symbol are included for prices
:return: A dictionary of trading pairs and prices
"""
pairs_prices = await exchange.get_all_pairs_prices()
results = {}
for pair_price in pairs_prices:
try:
trading_pair = await exchange.trading_pair_associated_to_exchange_symbol(symbol=pair_price["symbol"])
except KeyError:
continue # skip pairs that we don't track
if quote_token is not None:
base, quote = split_hb_trading_pair(trading_pair=trading_pair)
if quote != quote_token:
continue
bid_price = pair_price.get("bidPrice")
ask_price = pair_price.get("askPrice")
if bid_price is not None and ask_price is not None and 0 < Decimal(bid_price) <= Decimal(ask_price):
results[trading_pair] = (Decimal(bid_price) + Decimal(ask_price)) / Decimal("2")

return results

@staticmethod
def _build_binance_connector_without_private_keys(domain: str) -> 'BinanceExchange':
from hummingbot.client.hummingbot_application import HummingbotApplication
from hummingbot.connector.exchange.binance.binance_exchange import BinanceExchange

app = HummingbotApplication.main_application()
client_config_map = app.client_config_map

return BinanceExchange(
client_config_map=client_config_map,
binance_api_key="",
binance_api_secret="",
trading_pairs=[],
trading_required=False,
domain=domain,
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ def setUpClass(cls):
cls.global_token = "HBOT"
cls.binance_pair = f"{cls.target_token}-{cls.global_token}"
cls.trading_pair = combine_to_hb_trading_pair(base=cls.target_token, quote=cls.global_token)
# cls.binance_us_pair = f"{cls.target_token}USD"
# cls.us_trading_pair = combine_to_hb_trading_pair(base=cls.target_token, quote="USD")
cls.binance_ignored_pair = "SOMEPAIR"
cls.ignored_trading_pair = combine_to_hb_trading_pair(base="SOME", quote="PAIR")

Expand Down Expand Up @@ -53,23 +51,7 @@ def setup_binance_responses(self, mock_api, expected_rate: Decimal):
},
]
}
# binance_prices_us_url = web_utils.public_rest_url(path_url=CONSTANTS.TICKER_BOOK_PATH_URL, domain="us")
# binance_prices_us_response = [
# {
# "symbol": self.binance_us_pair,
# "bidPrice": "20862.0000",
# "bidQty": "0.50000000",
# "askPrice": "20865.6100",
# "askQty": "0.14500000",
# },
# {
# "symbol": self.binance_ignored_pair,
# "bidPrice": "0",
# "bidQty": "0",
# "askPrice": "0",
# "askQty": "0",
# }
# ]

binance_prices_global_url = web_utils.public_rest_url(path_url=CONSTANTS.TICKER_BOOK_PATH_URL)
binance_prices_global_response = [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import asyncio
import json
import unittest
from decimal import Decimal
from typing import Awaitable

from aioresponses import aioresponses

from hummingbot.connector.exchange.binance import binance_constants as CONSTANTS, binance_web_utils as web_utils
from hummingbot.connector.utils import combine_to_hb_trading_pair
from hummingbot.core.rate_oracle.sources.binance_us_rate_source import BinanceUSRateSource


class BinanceUSRateSourceTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ev_loop = asyncio.get_event_loop()
cls.target_token = "COINALPHA"
cls.binance_us_pair = f"{cls.target_token}USD"
cls.us_trading_pair = combine_to_hb_trading_pair(base=cls.target_token, quote="USD")
cls.binance_ignored_pair = "SOMEPAIR"
cls.ignored_trading_pair = combine_to_hb_trading_pair(base="SOME", quote="PAIR")

def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
ret = asyncio.get_event_loop().run_until_complete(asyncio.wait_for(coroutine, timeout))
return ret

def setup_binance_responses(self, mock_api, expected_rate: Decimal):
pairs_us_url = web_utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain="us")
symbols_response = { # truncated
"symbols": [
{
"symbol": self.binance_us_pair,
"status": "TRADING",
"baseAsset": self.target_token,
"quoteAsset": "USD",
"permissionSets": [[
"SPOT",
]],
},
{
"symbol": self.binance_ignored_pair,
"status": "PAUSED",
"baseAsset": "SOME",
"quoteAsset": "PAIR",
"permissionSets": [[
"SPOT",
]],
},
]
}
binance_prices_us_url = web_utils.public_rest_url(path_url=CONSTANTS.TICKER_BOOK_PATH_URL, domain="us")
binance_prices_us_response = [
{
"symbol": self.binance_us_pair,
"bidPrice": str(expected_rate - Decimal("0.1")),
"bidQty": "0.50000000",
"askPrice": str(expected_rate + Decimal("0.1")),
"askQty": "0.14500000",
},
{
"symbol": self.binance_ignored_pair,
"bidPrice": "0",
"bidQty": "0",
"askPrice": "0",
"askQty": "0",
}
]
mock_api.get(pairs_us_url, body=json.dumps(symbols_response))
mock_api.get(binance_prices_us_url, body=json.dumps(binance_prices_us_response))

@aioresponses()
def test_get_binance_prices(self, mock_api):
expected_rate = Decimal("10")
self.setup_binance_responses(mock_api=mock_api, expected_rate=expected_rate)

rate_source = BinanceUSRateSource()
prices = self.async_run_with_timeout(rate_source.get_prices())

self.assertIn(self.us_trading_pair, prices)
self.assertEqual(expected_rate, prices[self.us_trading_pair])
# self.assertIn(self.us_trading_pair, prices)
self.assertNotIn(self.ignored_trading_pair, prices)

0 comments on commit 1074ecd

Please sign in to comment.