mexc-websocket added
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 1m12s
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 1m12s
This commit is contained in:
25
mexc-socket.py
Normal file
25
mexc-socket.py
Normal 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
60
pymexc/__init__.py
Normal 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
164
pymexc/base.py
Normal 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
509
pymexc/base_websocket.py
Normal 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
1694
pymexc/futures.py
Normal file
File diff suppressed because it is too large
Load Diff
1967
pymexc/spot.py
Normal file
1967
pymexc/spot.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user