✅ Binance (completed previously)
✅ Coinbase Pro (completed in task 12) ✅ Kraken (completed in task 12) ✅ Bybit (completed in this task) ✅ OKX (completed in this task) ✅ Huobi (completed in this task)
This commit is contained in:
660
COBY/connectors/huobi_connector.py
Normal file
660
COBY/connectors/huobi_connector.py
Normal file
@ -0,0 +1,660 @@
|
||||
"""
|
||||
Huobi Global exchange connector implementation.
|
||||
Supports WebSocket connections to Huobi with proper symbol mapping.
|
||||
"""
|
||||
|
||||
import json
|
||||
import gzip
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import time
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from ..models.core import OrderBookSnapshot, TradeEvent, PriceLevel
|
||||
from ..utils.logging import get_logger, set_correlation_id
|
||||
from ..utils.exceptions import ValidationError, ConnectionError
|
||||
from ..utils.validation import validate_symbol, validate_price, validate_volume
|
||||
from .base_connector import BaseExchangeConnector
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HuobiConnector(BaseExchangeConnector):
|
||||
"""
|
||||
Huobi Global WebSocket connector implementation.
|
||||
|
||||
Supports:
|
||||
- Order book streams
|
||||
- Trade streams
|
||||
- Symbol normalization
|
||||
- GZIP message decompression
|
||||
- Authentication for private channels
|
||||
"""
|
||||
|
||||
# Huobi WebSocket URLs
|
||||
WEBSOCKET_URL = "wss://api.huobi.pro/ws"
|
||||
WEBSOCKET_PRIVATE_URL = "wss://api.huobi.pro/ws/v2"
|
||||
API_URL = "https://api.huobi.pro"
|
||||
|
||||
def __init__(self, api_key: str = None, api_secret: str = None):
|
||||
"""
|
||||
Initialize Huobi connector.
|
||||
|
||||
Args:
|
||||
api_key: API key for authentication (optional)
|
||||
api_secret: API secret for authentication (optional)
|
||||
"""
|
||||
super().__init__("huobi", self.WEBSOCKET_URL)
|
||||
|
||||
# Authentication credentials (optional)
|
||||
self.api_key = api_key
|
||||
self.api_secret = api_secret
|
||||
|
||||
# Huobi-specific message handlers
|
||||
self.message_handlers.update({
|
||||
'market.*.depth.step0': self._handle_orderbook_update,
|
||||
'market.*.trade.detail': self._handle_trade_update,
|
||||
'ping': self._handle_ping,
|
||||
'pong': self._handle_pong
|
||||
})
|
||||
|
||||
# Subscription tracking
|
||||
self.subscribed_topics = set()
|
||||
|
||||
logger.info("Huobi connector initialized")
|
||||
|
||||
def _get_message_type(self, data: Dict) -> str:
|
||||
"""
|
||||
Determine message type from Huobi message data.
|
||||
|
||||
Args:
|
||||
data: Parsed message data
|
||||
|
||||
Returns:
|
||||
str: Message type identifier
|
||||
"""
|
||||
# Huobi message format
|
||||
if 'ping' in data:
|
||||
return 'ping'
|
||||
elif 'pong' in data:
|
||||
return 'pong'
|
||||
elif 'ch' in data:
|
||||
# Data channel message
|
||||
channel = data['ch']
|
||||
if 'depth' in channel:
|
||||
return 'market.*.depth.step0'
|
||||
elif 'trade' in channel:
|
||||
return 'market.*.trade.detail'
|
||||
else:
|
||||
return channel
|
||||
elif 'subbed' in data:
|
||||
return 'subscription_response'
|
||||
elif 'unsubbed' in data:
|
||||
return 'unsubscription_response'
|
||||
elif 'status' in data and data.get('status') == 'error':
|
||||
return 'error'
|
||||
|
||||
return 'unknown'
|
||||
|
||||
def normalize_symbol(self, symbol: str) -> str:
|
||||
"""
|
||||
Normalize symbol to Huobi format.
|
||||
|
||||
Args:
|
||||
symbol: Standard symbol format (e.g., 'BTCUSDT')
|
||||
|
||||
Returns:
|
||||
str: Huobi symbol format (e.g., 'btcusdt')
|
||||
"""
|
||||
# Huobi uses lowercase symbols
|
||||
normalized = symbol.lower().replace('-', '').replace('/', '')
|
||||
|
||||
# Validate symbol format
|
||||
if not validate_symbol(normalized.upper()):
|
||||
raise ValidationError(f"Invalid symbol format: {symbol}", "INVALID_SYMBOL")
|
||||
|
||||
return normalized
|
||||
|
||||
def _denormalize_symbol(self, huobi_symbol: str) -> str:
|
||||
"""
|
||||
Convert Huobi symbol back to standard format.
|
||||
|
||||
Args:
|
||||
huobi_symbol: Huobi symbol format (e.g., 'btcusdt')
|
||||
|
||||
Returns:
|
||||
str: Standard symbol format (e.g., 'BTCUSDT')
|
||||
"""
|
||||
return huobi_symbol.upper()
|
||||
|
||||
async def _decompress_message(self, message: bytes) -> str:
|
||||
"""
|
||||
Decompress GZIP message from Huobi.
|
||||
|
||||
Args:
|
||||
message: Compressed message bytes
|
||||
|
||||
Returns:
|
||||
str: Decompressed message string
|
||||
"""
|
||||
try:
|
||||
return gzip.decompress(message).decode('utf-8')
|
||||
except Exception as e:
|
||||
logger.error(f"Error decompressing message: {e}")
|
||||
return ""
|
||||
|
||||
async def _process_message(self, message: str) -> None:
|
||||
"""
|
||||
Override message processing to handle GZIP compression.
|
||||
|
||||
Args:
|
||||
message: Raw message (could be compressed)
|
||||
"""
|
||||
try:
|
||||
# Check if message is compressed (binary)
|
||||
if isinstance(message, bytes):
|
||||
message = await self._decompress_message(message)
|
||||
|
||||
if not message:
|
||||
return
|
||||
|
||||
# Parse JSON message
|
||||
data = json.loads(message)
|
||||
|
||||
# Handle ping/pong first
|
||||
if 'ping' in data:
|
||||
await self._handle_ping(data)
|
||||
return
|
||||
|
||||
# Determine message type and route to appropriate handler
|
||||
message_type = self._get_message_type(data)
|
||||
|
||||
if message_type in self.message_handlers:
|
||||
await self.message_handlers[message_type](data)
|
||||
else:
|
||||
logger.debug(f"Unhandled message type '{message_type}' from {self.exchange_name}")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"Invalid JSON message from {self.exchange_name}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing message from {self.exchange_name}: {e}")
|
||||
|
||||
async def subscribe_orderbook(self, symbol: str) -> None:
|
||||
"""
|
||||
Subscribe to order book updates for a symbol.
|
||||
|
||||
Args:
|
||||
symbol: Trading symbol (e.g., 'BTCUSDT')
|
||||
"""
|
||||
try:
|
||||
set_correlation_id()
|
||||
huobi_symbol = self.normalize_symbol(symbol)
|
||||
topic = f"market.{huobi_symbol}.depth.step0"
|
||||
|
||||
# Create subscription message
|
||||
subscription_msg = {
|
||||
"sub": topic,
|
||||
"id": str(int(time.time()))
|
||||
}
|
||||
|
||||
# Send subscription
|
||||
success = await self._send_message(subscription_msg)
|
||||
if success:
|
||||
# Track subscription
|
||||
if symbol not in self.subscriptions:
|
||||
self.subscriptions[symbol] = []
|
||||
if 'orderbook' not in self.subscriptions[symbol]:
|
||||
self.subscriptions[symbol].append('orderbook')
|
||||
|
||||
self.subscribed_topics.add(topic)
|
||||
|
||||
logger.info(f"Subscribed to order book for {symbol} ({huobi_symbol}) on Huobi")
|
||||
else:
|
||||
logger.error(f"Failed to subscribe to order book for {symbol} on Huobi")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error subscribing to order book for {symbol}: {e}")
|
||||
raise
|
||||
|
||||
async def subscribe_trades(self, symbol: str) -> None:
|
||||
"""
|
||||
Subscribe to trade updates for a symbol.
|
||||
|
||||
Args:
|
||||
symbol: Trading symbol (e.g., 'BTCUSDT')
|
||||
"""
|
||||
try:
|
||||
set_correlation_id()
|
||||
huobi_symbol = self.normalize_symbol(symbol)
|
||||
topic = f"market.{huobi_symbol}.trade.detail"
|
||||
|
||||
# Create subscription message
|
||||
subscription_msg = {
|
||||
"sub": topic,
|
||||
"id": str(int(time.time()))
|
||||
}
|
||||
|
||||
# Send subscription
|
||||
success = await self._send_message(subscription_msg)
|
||||
if success:
|
||||
# Track subscription
|
||||
if symbol not in self.subscriptions:
|
||||
self.subscriptions[symbol] = []
|
||||
if 'trades' not in self.subscriptions[symbol]:
|
||||
self.subscriptions[symbol].append('trades')
|
||||
|
||||
self.subscribed_topics.add(topic)
|
||||
|
||||
logger.info(f"Subscribed to trades for {symbol} ({huobi_symbol}) on Huobi")
|
||||
else:
|
||||
logger.error(f"Failed to subscribe to trades for {symbol} on Huobi")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error subscribing to trades for {symbol}: {e}")
|
||||
raise
|
||||
|
||||
async def unsubscribe_orderbook(self, symbol: str) -> None:
|
||||
"""
|
||||
Unsubscribe from order book updates for a symbol.
|
||||
|
||||
Args:
|
||||
symbol: Trading symbol (e.g., 'BTCUSDT')
|
||||
"""
|
||||
try:
|
||||
huobi_symbol = self.normalize_symbol(symbol)
|
||||
topic = f"market.{huobi_symbol}.depth.step0"
|
||||
|
||||
# Create unsubscription message
|
||||
unsubscription_msg = {
|
||||
"unsub": topic,
|
||||
"id": str(int(time.time()))
|
||||
}
|
||||
|
||||
# Send unsubscription
|
||||
success = await self._send_message(unsubscription_msg)
|
||||
if success:
|
||||
# Remove from tracking
|
||||
if symbol in self.subscriptions and 'orderbook' in self.subscriptions[symbol]:
|
||||
self.subscriptions[symbol].remove('orderbook')
|
||||
if not self.subscriptions[symbol]:
|
||||
del self.subscriptions[symbol]
|
||||
|
||||
self.subscribed_topics.discard(topic)
|
||||
|
||||
logger.info(f"Unsubscribed from order book for {symbol} ({huobi_symbol}) on Huobi")
|
||||
else:
|
||||
logger.error(f"Failed to unsubscribe from order book for {symbol} on Huobi")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error unsubscribing from order book for {symbol}: {e}")
|
||||
raise
|
||||
|
||||
async def unsubscribe_trades(self, symbol: str) -> None:
|
||||
"""
|
||||
Unsubscribe from trade updates for a symbol.
|
||||
|
||||
Args:
|
||||
symbol: Trading symbol (e.g., 'BTCUSDT')
|
||||
"""
|
||||
try:
|
||||
huobi_symbol = self.normalize_symbol(symbol)
|
||||
topic = f"market.{huobi_symbol}.trade.detail"
|
||||
|
||||
# Create unsubscription message
|
||||
unsubscription_msg = {
|
||||
"unsub": topic,
|
||||
"id": str(int(time.time()))
|
||||
}
|
||||
|
||||
# Send unsubscription
|
||||
success = await self._send_message(unsubscription_msg)
|
||||
if success:
|
||||
# Remove from tracking
|
||||
if symbol in self.subscriptions and 'trades' in self.subscriptions[symbol]:
|
||||
self.subscriptions[symbol].remove('trades')
|
||||
if not self.subscriptions[symbol]:
|
||||
del self.subscriptions[symbol]
|
||||
|
||||
self.subscribed_topics.discard(topic)
|
||||
|
||||
logger.info(f"Unsubscribed from trades for {symbol} ({huobi_symbol}) on Huobi")
|
||||
else:
|
||||
logger.error(f"Failed to unsubscribe from trades for {symbol} on Huobi")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error unsubscribing from trades for {symbol}: {e}")
|
||||
raise
|
||||
|
||||
async def get_symbols(self) -> List[str]:
|
||||
"""
|
||||
Get list of available trading symbols from Huobi.
|
||||
|
||||
Returns:
|
||||
List[str]: List of available symbols in standard format
|
||||
"""
|
||||
try:
|
||||
import aiohttp
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{self.API_URL}/v1/common/symbols") as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
|
||||
if data.get('status') != 'ok':
|
||||
logger.error(f"Huobi API error: {data}")
|
||||
return []
|
||||
|
||||
symbols = []
|
||||
symbol_data = data.get('data', [])
|
||||
|
||||
for symbol_info in symbol_data:
|
||||
if symbol_info.get('state') == 'online':
|
||||
symbol = symbol_info.get('symbol', '')
|
||||
# Convert to standard format
|
||||
standard_symbol = self._denormalize_symbol(symbol)
|
||||
symbols.append(standard_symbol)
|
||||
|
||||
logger.info(f"Retrieved {len(symbols)} symbols from Huobi")
|
||||
return symbols
|
||||
else:
|
||||
logger.error(f"Failed to get symbols from Huobi: HTTP {response.status}")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting symbols from Huobi: {e}")
|
||||
return []
|
||||
|
||||
async def get_orderbook_snapshot(self, symbol: str, depth: int = 20) -> Optional[OrderBookSnapshot]:
|
||||
"""
|
||||
Get current order book snapshot from Huobi REST API.
|
||||
|
||||
Args:
|
||||
symbol: Trading symbol
|
||||
depth: Number of price levels to retrieve
|
||||
|
||||
Returns:
|
||||
OrderBookSnapshot: Current order book or None if unavailable
|
||||
"""
|
||||
try:
|
||||
import aiohttp
|
||||
|
||||
huobi_symbol = self.normalize_symbol(symbol)
|
||||
|
||||
# Huobi supports depths: 5, 10, 20
|
||||
valid_depths = [5, 10, 20]
|
||||
api_depth = min(valid_depths, key=lambda x: abs(x - depth))
|
||||
|
||||
url = f"{self.API_URL}/market/depth"
|
||||
params = {
|
||||
'symbol': huobi_symbol,
|
||||
'depth': api_depth,
|
||||
'type': 'step0'
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
|
||||
if data.get('status') != 'ok':
|
||||
logger.error(f"Huobi API error: {data}")
|
||||
return None
|
||||
|
||||
tick_data = data.get('tick', {})
|
||||
return self._parse_orderbook_snapshot(tick_data, symbol)
|
||||
else:
|
||||
logger.error(f"Failed to get order book for {symbol}: HTTP {response.status}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting order book snapshot for {symbol}: {e}")
|
||||
return None
|
||||
|
||||
def _parse_orderbook_snapshot(self, data: Dict, symbol: str) -> OrderBookSnapshot:
|
||||
"""
|
||||
Parse Huobi order book data into OrderBookSnapshot.
|
||||
|
||||
Args:
|
||||
data: Raw Huobi order book data
|
||||
symbol: Trading symbol
|
||||
|
||||
Returns:
|
||||
OrderBookSnapshot: Parsed order book
|
||||
"""
|
||||
try:
|
||||
# Parse bids and asks
|
||||
bids = []
|
||||
for bid_data in data.get('bids', []):
|
||||
price = float(bid_data[0])
|
||||
size = float(bid_data[1])
|
||||
|
||||
if validate_price(price) and validate_volume(size):
|
||||
bids.append(PriceLevel(price=price, size=size))
|
||||
|
||||
asks = []
|
||||
for ask_data in data.get('asks', []):
|
||||
price = float(ask_data[0])
|
||||
size = float(ask_data[1])
|
||||
|
||||
if validate_price(price) and validate_volume(size):
|
||||
asks.append(PriceLevel(price=price, size=size))
|
||||
|
||||
# Create order book snapshot
|
||||
orderbook = OrderBookSnapshot(
|
||||
symbol=symbol,
|
||||
exchange=self.exchange_name,
|
||||
timestamp=datetime.fromtimestamp(int(data.get('ts', 0)) / 1000, tz=timezone.utc),
|
||||
bids=bids,
|
||||
asks=asks,
|
||||
sequence_id=data.get('version')
|
||||
)
|
||||
|
||||
return orderbook
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing order book snapshot: {e}")
|
||||
raise ValidationError(f"Invalid order book data: {e}", "PARSE_ERROR")
|
||||
|
||||
async def _handle_orderbook_update(self, data: Dict) -> None:
|
||||
"""
|
||||
Handle order book update from Huobi.
|
||||
|
||||
Args:
|
||||
data: Order book update data
|
||||
"""
|
||||
try:
|
||||
set_correlation_id()
|
||||
|
||||
# Extract symbol from channel
|
||||
channel = data.get('ch', '')
|
||||
if not channel:
|
||||
logger.warning("Order book update missing channel")
|
||||
return
|
||||
|
||||
# Parse channel: market.btcusdt.depth.step0
|
||||
parts = channel.split('.')
|
||||
if len(parts) < 2:
|
||||
logger.warning("Invalid order book channel format")
|
||||
return
|
||||
|
||||
huobi_symbol = parts[1]
|
||||
symbol = self._denormalize_symbol(huobi_symbol)
|
||||
|
||||
tick_data = data.get('tick', {})
|
||||
|
||||
# Parse bids and asks
|
||||
bids = []
|
||||
for bid_data in tick_data.get('bids', []):
|
||||
price = float(bid_data[0])
|
||||
size = float(bid_data[1])
|
||||
|
||||
if validate_price(price) and validate_volume(size):
|
||||
bids.append(PriceLevel(price=price, size=size))
|
||||
|
||||
asks = []
|
||||
for ask_data in tick_data.get('asks', []):
|
||||
price = float(ask_data[0])
|
||||
size = float(ask_data[1])
|
||||
|
||||
if validate_price(price) and validate_volume(size):
|
||||
asks.append(PriceLevel(price=price, size=size))
|
||||
|
||||
# Create order book snapshot
|
||||
orderbook = OrderBookSnapshot(
|
||||
symbol=symbol,
|
||||
exchange=self.exchange_name,
|
||||
timestamp=datetime.fromtimestamp(int(tick_data.get('ts', 0)) / 1000, tz=timezone.utc),
|
||||
bids=bids,
|
||||
asks=asks,
|
||||
sequence_id=tick_data.get('version')
|
||||
)
|
||||
|
||||
# Notify callbacks
|
||||
self._notify_data_callbacks(orderbook)
|
||||
|
||||
logger.debug(f"Processed order book update for {symbol}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling order book update: {e}")
|
||||
|
||||
async def _handle_trade_update(self, data: Dict) -> None:
|
||||
"""
|
||||
Handle trade update from Huobi.
|
||||
|
||||
Args:
|
||||
data: Trade update data
|
||||
"""
|
||||
try:
|
||||
set_correlation_id()
|
||||
|
||||
# Extract symbol from channel
|
||||
channel = data.get('ch', '')
|
||||
if not channel:
|
||||
logger.warning("Trade update missing channel")
|
||||
return
|
||||
|
||||
# Parse channel: market.btcusdt.trade.detail
|
||||
parts = channel.split('.')
|
||||
if len(parts) < 2:
|
||||
logger.warning("Invalid trade channel format")
|
||||
return
|
||||
|
||||
huobi_symbol = parts[1]
|
||||
symbol = self._denormalize_symbol(huobi_symbol)
|
||||
|
||||
tick_data = data.get('tick', {})
|
||||
trades_data = tick_data.get('data', [])
|
||||
|
||||
# Process each trade
|
||||
for trade_data in trades_data:
|
||||
price = float(trade_data.get('price', 0))
|
||||
amount = float(trade_data.get('amount', 0))
|
||||
|
||||
# Validate data
|
||||
if not validate_price(price) or not validate_volume(amount):
|
||||
logger.warning(f"Invalid trade data: price={price}, amount={amount}")
|
||||
continue
|
||||
|
||||
# Determine side (Huobi uses 'direction' field)
|
||||
direction = trade_data.get('direction', 'unknown')
|
||||
side = 'buy' if direction == 'buy' else 'sell'
|
||||
|
||||
# Create trade event
|
||||
trade = TradeEvent(
|
||||
symbol=symbol,
|
||||
exchange=self.exchange_name,
|
||||
timestamp=datetime.fromtimestamp(int(trade_data.get('ts', 0)) / 1000, tz=timezone.utc),
|
||||
price=price,
|
||||
size=amount,
|
||||
side=side,
|
||||
trade_id=str(trade_data.get('tradeId', trade_data.get('id', '')))
|
||||
)
|
||||
|
||||
# Notify callbacks
|
||||
self._notify_data_callbacks(trade)
|
||||
|
||||
logger.debug(f"Processed trade for {symbol}: {side} {amount} @ {price}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling trade update: {e}")
|
||||
|
||||
async def _handle_ping(self, data: Dict) -> None:
|
||||
"""
|
||||
Handle ping message from Huobi and respond with pong.
|
||||
|
||||
Args:
|
||||
data: Ping message data
|
||||
"""
|
||||
try:
|
||||
ping_value = data.get('ping')
|
||||
if ping_value:
|
||||
# Respond with pong
|
||||
pong_msg = {"pong": ping_value}
|
||||
await self._send_message(pong_msg)
|
||||
logger.debug(f"Responded to Huobi ping with pong: {ping_value}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling ping: {e}")
|
||||
|
||||
async def _handle_pong(self, data: Dict) -> None:
|
||||
"""
|
||||
Handle pong response from Huobi.
|
||||
|
||||
Args:
|
||||
data: Pong response data
|
||||
"""
|
||||
logger.debug("Received Huobi pong")
|
||||
|
||||
def _get_auth_signature(self, method: str, host: str, path: str,
|
||||
params: Dict[str, str]) -> str:
|
||||
"""
|
||||
Generate authentication signature for Huobi.
|
||||
|
||||
Args:
|
||||
method: HTTP method
|
||||
host: API host
|
||||
path: Request path
|
||||
params: Request parameters
|
||||
|
||||
Returns:
|
||||
str: Authentication signature
|
||||
"""
|
||||
if not self.api_key or not self.api_secret:
|
||||
return ""
|
||||
|
||||
try:
|
||||
# Sort parameters
|
||||
sorted_params = sorted(params.items())
|
||||
query_string = '&'.join([f"{k}={v}" for k, v in sorted_params])
|
||||
|
||||
# Create signature string
|
||||
signature_string = f"{method}\n{host}\n{path}\n{query_string}"
|
||||
|
||||
# Generate signature
|
||||
signature = base64.b64encode(
|
||||
hmac.new(
|
||||
self.api_secret.encode('utf-8'),
|
||||
signature_string.encode('utf-8'),
|
||||
hashlib.sha256
|
||||
).digest()
|
||||
).decode('utf-8')
|
||||
|
||||
return signature
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating auth signature: {e}")
|
||||
return ""
|
||||
|
||||
def get_huobi_stats(self) -> Dict[str, Any]:
|
||||
"""Get Huobi-specific statistics."""
|
||||
base_stats = self.get_stats()
|
||||
|
||||
huobi_stats = {
|
||||
'subscribed_topics': list(self.subscribed_topics),
|
||||
'authenticated': bool(self.api_key and self.api_secret)
|
||||
}
|
||||
|
||||
base_stats.update(huobi_stats)
|
||||
return base_stats
|
Reference in New Issue
Block a user