mexc-websocket added
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 1m12s

This commit is contained in:
dongho
2024-12-20 20:48:02 +09:00
parent 1b7cd7960e
commit 9e9cbf3547
6 changed files with 4419 additions and 0 deletions

25
mexc-socket.py Normal file
View File

@ -0,0 +1,25 @@
from pymexc import spot, futures
from dotenv import load_dotenv
import os
load_dotenv()
api_key = os.getenv("API_KEY")
api_secret = os.getenv("API_SECRET")
def handle_message(message):
print(message)
# initialize WebSocket client
ws_spot_client = spot.WebSocket(api_key = api_key, api_secret = api_secret)
# make http request to api
# create websocket connection to public channel (spot@public.deals.v3.api@BTCUSDT)
# all messages will be handled by function `handle_message`
ws_spot_client.deals_stream(handle_message, "BTCUSDT")
while True:
...

60
pymexc/__init__.py Normal file
View File

@ -0,0 +1,60 @@
"""
### Usage
```python
from pymexc import spot, futures
api_key = "YOUR API KEY"
api_secret = "YOUR API SECRET KEY"
def handle_message(message):
# handle websocket message
print(message)
# SPOT V3
# initialize HTTP client
spot_client = spot.HTTP(api_key = api_key, api_secret = api_secret)
# initialize WebSocket client
ws_spot_client = spot.WebSocket(api_key = api_key, api_secret = api_secret)
# make http request to api
print(spot_client.exchange_info())
# create websocket connection to public channel (spot@public.deals.v3.api@BTCUSDT)
# all messages will be handled by function `handle_message`
ws_spot_client.deals_stream(handle_message, "BTCUSDT")
# FUTURES V1
# initialize HTTP client
futures_client = futures.HTTP(api_key = api_key, api_secret = api_secret)
# initialize WebSocket client
ws_futures_client = futures.WebSocket(api_key = api_key, api_secret = api_secret)
# make http request to api
print(futures_client.index_price("MX_USDT"))
# create websocket connection to public channel (sub.tickers)
# all messages will be handled by function `handle_message`
ws_futures_client.tickers_stream(handle_message)
# loop forever for save websocket connection
while True:
...
"""
try:
from . import futures
from . import spot
except ImportError:
import futures
import spot
__all__ = [
"futures",
"spot"
]

164
pymexc/base.py Normal file
View File

@ -0,0 +1,164 @@
from abc import ABC, abstractclassmethod
from typing import Union, Literal
import hmac
import hashlib
import requests
from urllib.parse import urlencode
import logging
import time
logger = logging.getLogger(__name__)
class MexcAPIError(Exception):
pass
class MexcSDK(ABC):
"""
Initializes a new instance of the class with the given `api_key` and `api_secret` parameters.
:param api_key: A string representing the API key.
:param api_secret: A string representing the API secret.
:param base_url: A string representing the base URL of the API.
"""
def __init__(self, api_key: str, api_secret: str, base_url: str, proxies: dict = None):
self.api_key = api_key
self.api_secret = api_secret
self.recvWindow = 5000
self.base_url = base_url
self.session = requests.Session()
self.session.headers.update({
"Content-Type": "application/json",
})
if proxies:
self.session.proxies.update(proxies)
@abstractclassmethod
def sign(self, **kwargs) -> str:
...
@abstractclassmethod
def call(self, method: Union[Literal["GET"], Literal["POST"], Literal["PUT"], Literal["DELETE"]], router: str, *args, **kwargs) -> dict:
...
class _SpotHTTP(MexcSDK):
def __init__(self, api_key: str = None, api_secret: str = None, proxies: dict = None):
super().__init__(api_key, api_secret, "https://api.mexc.com", proxies = proxies)
self.session.headers.update({
"X-MEXC-APIKEY": self.api_key
})
def sign(self, query_string: str) -> str:
"""
Generates a signature for an API request using HMAC SHA256 encryption.
Args:
**kwargs: Arbitrary keyword arguments representing request parameters.
Returns:
A hexadecimal string representing the signature of the request.
"""
# Generate signature
signature = hmac.new(self.api_secret.encode('utf-8'), query_string.encode('utf-8'), hashlib.sha256).hexdigest()
return signature
def call(self, method: Union[Literal["GET"], Literal["POST"], Literal["PUT"], Literal["DELETE"]], router: str, auth: bool = True, *args, **kwargs) -> dict:
if not router.startswith("/"):
router = f"/{router}"
# clear None values
kwargs = {k: v for k, v in kwargs.items() if v is not None}
if kwargs.get('params'):
kwargs['params'] = {k: v for k, v in kwargs['params'].items() if v is not None}
else:
kwargs['params'] = {}
timestamp = str(int(time.time() * 1000))
kwargs['params']['timestamp'] = timestamp
kwargs['params']['recvWindow'] = self.recvWindow
kwargs['params'] = {k: v for k, v in sorted(kwargs['params'].items())}
params = urlencode(kwargs.pop('params'), doseq=True).replace('+', '%20')
if self.api_key and self.api_secret and auth:
params += "&signature=" + self.sign(params)
response = self.session.request(method, f"{self.base_url}{router}", params = params, *args, **kwargs)
if not response.ok:
raise MexcAPIError(f'(code={response.json()["code"]}): {response.json()["msg"]}')
return response.json()
class _FuturesHTTP(MexcSDK):
def __init__(self, api_key: str = None, api_secret: str = None, proxies: dict = None):
super().__init__(api_key, api_secret, "https://contract.mexc.com", proxies = proxies)
self.session.headers.update({
"Content-Type": "application/json",
"ApiKey": self.api_key
})
def sign(self, timestamp: str, **kwargs) -> str:
"""
Generates a signature for an API request using HMAC SHA256 encryption.
:param timestamp: A string representing the timestamp of the request.
:type timestamp: str
:param kwargs: Arbitrary keyword arguments representing request parameters.
:type kwargs: dict
:return: A hexadecimal string representing the signature of the request.
:rtype: str
"""
# Generate signature
query_string = "&".join([f"{k}={v}" for k, v in sorted(kwargs.items())])
query_string = self.api_key + timestamp + query_string
signature = hmac.new(self.api_secret.encode('utf-8'), query_string.encode('utf-8'), hashlib.sha256).hexdigest()
return signature
def call(self, method: Union[Literal["GET"], Literal["POST"], Literal["PUT"], Literal["DELETE"]], router: str, *args, **kwargs) -> dict:
"""
Makes a request to the specified HTTP method and router using the provided arguments.
:param method: A string that represents the HTTP method(GET, POST, PUT, or DELETE) to be used.
:type method: str
:param router: A string that represents the API endpoint to be called.
:type router: str
:param *args: Variable length argument list.
:type *args: list
:param **kwargs: Arbitrary keyword arguments.
:type **kwargs: dict
:return: A dictionary containing the JSON response of the request.
"""
if not router.startswith("/"):
router = f"/{router}"
# clear None values
kwargs = {k: v for k, v in kwargs.items() if v is not None}
for variant in ('params', 'json'):
if kwargs.get(variant):
kwargs[variant] = {k: v for k, v in kwargs[variant].items() if v is not None}
if self.api_key and self.api_secret:
# add signature
timestamp = str(int(time.time() * 1000))
kwargs['headers'] = {
"Request-Time": timestamp,
"Signature": self.sign(timestamp, **kwargs[variant])
}
response = self.session.request(method, f"{self.base_url}{router}", *args, **kwargs)
return response.json()

509
pymexc/base_websocket.py Normal file
View File

@ -0,0 +1,509 @@
import websocket
import threading
import logging
import time
import json
import hmac
logger = logging.getLogger(__name__)
SPOT = "wss://wbs.mexc.com/ws"
FUTURES = "wss://contract.mexc.com/ws"
class _WebSocketManager:
def __init__(self, callback_function, ws_name, api_key=None, api_secret=None,
ping_interval=20, ping_timeout=10, retries=10,
restart_on_error=True, trace_logging=False,
http_proxy_host = None,
http_proxy_port = None,
http_no_proxy = None,
http_proxy_auth = None,
http_proxy_timeout = None):
self.proxy_settings = dict(
http_proxy_host = http_proxy_host,
http_proxy_port = http_proxy_port,
http_no_proxy = http_no_proxy,
http_proxy_auth = http_proxy_auth,
http_proxy_timeout = http_proxy_timeout
)
# Set API keys.
self.api_key = api_key
self.api_secret = api_secret
self.callback = callback_function
self.ws_name = ws_name
if api_key:
self.ws_name += " (Auth)"
# Setup the callback directory following the format:
# {
# "topic_name": function
# }
self.callback_directory = {}
# Record the subscriptions made so that we can resubscribe if the WSS
# connection is broken.
self.subscriptions = []
# Set ping settings.
self.ping_interval = ping_interval
self.ping_timeout = ping_timeout
self.retries = retries
# Other optional data handling settings.
self.handle_error = restart_on_error
# Enable websocket-client's trace logging for extra debug information
# on the websocket connection, including the raw sent & recv messages
websocket.enableTrace(trace_logging)
# Set initial state, initialize dictionary and connect.
self._reset()
self.attempting_connection = False
def _on_open(self):
"""
Log WS open.
"""
logger.debug(f"WebSocket {self.ws_name} opened.")
def _on_message(self, message):
"""
Parse incoming messages.
"""
self.callback(json.loads(message))
def is_connected(self):
try:
if self.ws.sock or not self.ws.sock.is_connected:
return True
else:
return False
except AttributeError:
return False
@staticmethod
def _are_connections_connected(active_connections):
for connection in active_connections:
if not connection.is_connected():
return False
return True
def _ping_loop(self, ping_payload: str, ping_interval: int, ping_timeout: int):
"""
Ping the websocket.
"""
time.sleep(ping_timeout)
while True:
logger.info(f"WebSocket {self.ws_name} send ping...")
self.ws.send(ping_payload)
time.sleep(ping_interval)
def _connect(self, url):
"""
Open websocket in a thread.
"""
def resubscribe_to_topics():
if not self.subscriptions:
# There are no subscriptions to resubscribe to, probably
# because this is a brand new WSS initialisation so there was
# no previous WSS connection.
return
for subscription_message in self.subscriptions:
self.ws.send(subscription_message)
self.attempting_connection = True
# Set endpoint.
self.endpoint = url
# Attempt to connect for X seconds.
retries = self.retries
if retries == 0:
infinitely_reconnect = True
else:
infinitely_reconnect = False
while (infinitely_reconnect or retries > 0) and not self.is_connected():
logger.info(f"WebSocket {self.ws_name} attempting connection...")
self.ws = websocket.WebSocketApp(
url=url,
on_message=lambda ws, msg: self._on_message(msg),
on_close=self._on_close(),
on_open=self._on_open(),
on_error=lambda ws, err: self._on_error(err)
)
# Setup the thread running WebSocketApp.
self.wst = threading.Thread(target=lambda: self.ws.run_forever(
ping_interval=self.ping_interval,
ping_timeout=self.ping_timeout,
**self.proxy_settings
))
# Configure as daemon; start.
self.wst.daemon = True
self.wst.start()
# setup ping loop
self.wsl = threading.Thread(target=lambda: self._ping_loop(
ping_payload='{"method":"ping"}',
ping_interval=self.ping_interval,
ping_timeout=self.ping_timeout
))
self.wsl.daemon = True
self.wsl.start()
retries -= 1
time.sleep(1)
# If connection was not successful, raise error.
if not infinitely_reconnect and retries <= 0:
self.exit()
raise websocket.WebSocketTimeoutException(
f"WebSocket {self.ws_name} connection failed. Too many "
f"connection attempts. pybit will "
f"no longer try to reconnect.")
logger.info(f"WebSocket {self.ws_name} connected")
# If given an api_key, authenticate.
if self.api_key and self.api_secret:
self._auth()
resubscribe_to_topics()
self.attempting_connection = False
def _auth(self):
# Generate signature
# make auth if futures. spot has a different auth system.
isspot = self.endpoint.startswith(SPOT)
if isspot:
return
timestamp = str(int(time.time() * 1000))
_val = self.api_key + timestamp
signature = str(hmac.new(
bytes(self.api_secret, "utf-8"),
bytes(_val, "utf-8"), digestmod="sha256"
).hexdigest())
# Authenticate with API.
self.ws.send(
json.dumps({
"subscribe": False,
"method": "login",
"param": {
"apiKey": self.api_key,
"reqTime": timestamp,
"signature": signature
}
})
)
def _on_error(self, error):
"""
Exit on errors and raise exception, or attempt reconnect.
"""
if type(error).__name__ not in ["WebSocketConnectionClosedException",
"ConnectionResetError",
"WebSocketTimeoutException"]:
# Raises errors not related to websocket disconnection.
self.exit()
raise error
if not self.exited:
logger.error(f"WebSocket {self.ws_name} encountered error: {error}.")
self.exit()
# Reconnect.
if self.handle_error and not self.attempting_connection:
self._reset()
self._connect(self.endpoint)
def _on_close(self):
"""
Log WS close.
"""
logger.debug(f"WebSocket {self.ws_name} closed.")
def _reset(self):
"""
Set state booleans and initialize dictionary.
"""
self.exited = False
self.auth = False
self.data = {}
def exit(self):
"""
Closes the websocket connection.
"""
self.ws.close()
while self.ws.sock:
continue
self.exited = True
class _FuturesWebSocketManager(_WebSocketManager):
def __init__(self, ws_name, **kwargs):
callback_function = kwargs.pop("callback_function") if \
kwargs.get("callback_function") else self._handle_incoming_message
super().__init__(callback_function, ws_name, **kwargs)
self.private_topics = ["personal.order", "personal.asset",
"personal.position", "personal.risk.limit",
"personal.adl.level", "personal.position.mode"]
self.symbol_wildcard = "*"
self.symbol_separator = "|"
self.last_subsctiption = None
def subscribe(self, topic, callback, params: dict = {}):
subscription_args = {
"method": topic,
"param": params
}
self._check_callback_directory(subscription_args)
while not self.is_connected():
# Wait until the connection is open before subscribing.
time.sleep(0.1)
subscription_message = json.dumps(subscription_args)
self.ws.send(subscription_message)
self.subscriptions.append(subscription_message)
self._set_callback(topic.replace("sub.", ""), callback)
self.last_subsctiption = topic.replace("sub.", "")
def _initialise_local_data(self, topic):
# Create self.data
try:
self.data[topic]
except KeyError:
self.data[topic] = []
def _process_auth_message(self, message):
# If we get successful futures auth, notify user
if message.get("data") == "success":
logger.debug(f"Authorization for {self.ws_name} successful.")
self.auth = True
# If we get unsuccessful auth, notify user.
elif message.get("data") != "success": # !!!!
logger.debug(f"Authorization for {self.ws_name} failed. Please "
f"check your API keys and restart.")
def _process_subscription_message(self, message):
#try:
sub = message["channel"]
#except KeyError:
#sub = message["c"] # SPOT PUBLIC & PRIVATE format
# If we get successful futures subscription, notify user
if (
message.get("channel", "").startswith("rs.") or
message.get("channel", "").startswith("push.")
) and message.get("channel", "") != "rs.error":
logger.debug(f"Subscription to {sub} successful.")
# Futures subscription fail
else:
response = message["data"]
logger.error("Couldn't subscribe to topic. "
f"Error: {response}.")
if self.last_subsctiption:
self._pop_callback(self.last_subsctiption)
def _process_normal_message(self, message):
topic = message["channel"].replace("push.", "").replace("rs.sub.", "")
callback_data = message
callback_function = self._get_callback(topic)
callback_function(callback_data)
def _handle_incoming_message(self, message):
def is_auth_message():
if message.get("channel", "") == "rs.login":
return True
else:
return False
def is_subscription_message():
if str(message).startswith("{'channel': 'push."):
return True
else:
return False
def is_pong_message():
if message.get("channel", "") in ("pong", "clientId"):
return True
else:
return False
if is_auth_message():
self._process_auth_message(message)
elif is_subscription_message():
self._process_subscription_message(message)
elif is_pong_message():
pass
else:
self._process_normal_message(message)
def custom_topic_stream(self, topic, callback):
return self.subscribe(topic=topic, callback=callback)
def _check_callback_directory(self, topics):
for topic in topics:
if topic in self.callback_directory:
raise Exception(f"You have already subscribed to this topic: "
f"{topic}")
def _set_callback(self, topic, callback_function):
self.callback_directory[topic] = callback_function
def _get_callback(self, topic):
return self.callback_directory[topic]
def _pop_callback(self, topic):
self.callback_directory.pop(topic)
class _FuturesWebSocket(_FuturesWebSocketManager):
def __init__(self, **kwargs):
self.ws_name = "FuturesV1"
self.endpoint = "wss://contract.mexc.com/ws"
super().__init__(self.ws_name, **kwargs)
self.ws = None
self.active_connections = []
self.kwargs = kwargs
def is_connected(self):
return self._are_connections_connected(self.active_connections)
def _ws_subscribe(self, topic, callback, params: list = []):
if not self.ws:
self.ws = _FuturesWebSocketManager(
self.ws_name, **self.kwargs)
self.ws._connect(self.endpoint)
self.active_connections.append(self.ws)
self.ws.subscribe(topic, callback, params)
class _SpotWebSocketManager(_WebSocketManager):
def __init__(self, ws_name, **kwargs):
callback_function = kwargs.pop("callback_function") if \
kwargs.get("callback_function") else self._handle_incoming_message
super().__init__(callback_function, ws_name, **kwargs)
self.private_topics = ["account", "deals", "orders"]
self.last_subsctiption = None
def subscribe(self, topic: str, callback, params_list: list):
subscription_args = {
"method": "SUBSCRIPTION",
"params": [
'@'.join([f"spot@{topic}.v3.api"] + list(map(str, params.values())))
for params in params_list
]
}
self._check_callback_directory(subscription_args)
while not self.is_connected():
# Wait until the connection is open before subscribing.
time.sleep(0.1)
subscription_message = json.dumps(subscription_args)
self.ws.send(subscription_message)
self.subscriptions.append(subscription_message)
self._set_callback(topic, callback)
self.last_subsctiption = topic
def _initialise_local_data(self, topic):
# Create self.data
try:
self.data[topic]
except KeyError:
self.data[topic] = []
def _process_subscription_message(self, message):
sub = message["msg"].replace("spot@", "").split(".v3.api")[0]
# If we get successful futures subscription, notify user
if message.get("id") == 0 and message.get("code") == 0:
logger.debug(f"Subscription to {sub} successful.")
# Futures subscription fail
else:
response = message["msg"]
logger.error("Couldn't subscribe to topic. "
f"Error: {response}.")
if self.last_subsctiption:
self._pop_callback(self.last_subsctiption)
def _process_normal_message(self, message):
topic = message["c"].replace("spot@", "").split(".v3.api")[0]
callback_data = message
callback_function = self._get_callback(topic)
callback_function(callback_data)
def _handle_incoming_message(self, message):
def is_subscription_message():
if (message.get("id") == 0 and
message.get("code") == 0 and
message.get("msg")):
return True
else:
return False
if is_subscription_message():
self._process_subscription_message(message)
else:
self._process_normal_message(message)
def custom_topic_stream(self, topic, callback):
return self.subscribe(topic=topic, callback=callback)
def _check_callback_directory(self, topics):
for topic in topics:
if topic in self.callback_directory:
raise Exception(f"You have already subscribed to this topic: "
f"{topic}")
def _set_callback(self, topic, callback_function):
self.callback_directory[topic] = callback_function
def _get_callback(self, topic):
return self.callback_directory[topic]
def _pop_callback(self, topic):
self.callback_directory.pop(topic)
class _SpotWebSocket(_SpotWebSocketManager):
def __init__(self, endpoint: str = "wss://wbs.mexc.com/ws", **kwargs):
self.ws_name = "SpotV3"
self.endpoint = endpoint
super().__init__(self.ws_name, **kwargs)
self.ws = None
self.active_connections = []
self.kwargs = kwargs
def is_connected(self):
return self._are_connections_connected(self.active_connections)
def _ws_subscribe(self, topic, callback, params: list = []):
if not self.ws:
self.ws = _SpotWebSocketManager(
self.ws_name, **self.kwargs)
self.ws._connect(self.endpoint)
self.active_connections.append(self.ws)
self.ws.subscribe(topic, callback, params)

1694
pymexc/futures.py Normal file

File diff suppressed because it is too large Load Diff

1967
pymexc/spot.py Normal file

File diff suppressed because it is too large Load Diff