mexc_spot_rest.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. import model
  2. import sys
  3. import time
  4. import utils
  5. import logging
  6. import aiohttp
  7. import asyncio
  8. import traceback
  9. from urllib.parse import urlparse
  10. from urllib.parse import urljoin
  11. from decimal import Decimal
  12. from hashlib import sha256
  13. import hmac, base64
  14. import json
  15. def empty_call(msg):
  16. print(f'空的回调函数 {msg}')
  17. class MexcSpotRest:
  18. def __init__(self, params:model.ClientParams, colo=0):
  19. if colo:
  20. print('不支持colo高速线路')
  21. self.HOST = 'https://api.mexc.com'
  22. else:
  23. self.HOST = 'https://api.mexc.com'
  24. self.params = params
  25. self.name = self.params.name
  26. self.base = self.params.pair.split('_')[0].upper()
  27. self.quote = self.params.pair.split('_')[1].upper()
  28. self.symbol = self.base + self.quote
  29. self.data = {}
  30. self._SESSIONS = dict()
  31. self.logger = self.get_logger()
  32. self.data['account'] = {}
  33. self.callback = {
  34. "onMarket": empty_call,
  35. "onPosition": empty_call,
  36. "onOrder": empty_call,
  37. "onEquity": empty_call,
  38. "onTicker": empty_call,
  39. "onExit": empty_call,
  40. }
  41. self.exchange_info = dict()
  42. self.tickSize = None
  43. self.stepSize = None
  44. self.delays = []
  45. self.max_delay = 0
  46. self.avg_delay = 0
  47. self.proxy = None
  48. if 'win' in sys.platform:
  49. self.proxy = self.params.proxy
  50. self.logger = self.get_logger()
  51. self.stop_flag = 0
  52. self.coin_value = 0.0
  53. self.cash_value = 0.0
  54. #### 指定发包ip
  55. iplist = utils.get_local_ip_list()
  56. self.ip = iplist[int(self.params.ip)]
  57. def get_logger(self):
  58. logger = logging.getLogger(__name__)
  59. logger.setLevel(logging.DEBUG)
  60. # log to txt
  61. formatter = logging.Formatter(
  62. '[%(asctime)s] - %(levelname)s - %(message)s')
  63. handler = logging.handlers.RotatingFileHandler(
  64. "log.log", maxBytes=1024*1024, encoding='utf-8')
  65. handler.setLevel(logging.DEBUG)
  66. handler.setFormatter(formatter)
  67. # log to console
  68. console = logging.StreamHandler()
  69. console.setLevel(logging.WARNING)
  70. logger.addHandler(handler)
  71. logger.addHandler(console)
  72. return logger
  73. def get_sign(self, params, secret_key):
  74. key = secret_key.encode('utf-8')
  75. params = params.encode('utf-8')
  76. sign = base64.b64encode(hmac.new(key, params, digestmod=sha256).digest()).decode()
  77. return sign
  78. def _get_session(self, url):
  79. parsed_url = urlparse(url)
  80. key = parsed_url.netloc or parsed_url.hostname
  81. if key not in self._SESSIONS:
  82. tcp = aiohttp.TCPConnector(limit=50,keepalive_timeout=120,verify_ssl=False,local_addr=(self.ip,0))
  83. session = aiohttp.ClientSession(connector=tcp)
  84. self._SESSIONS[key] = session
  85. return self._SESSIONS[key]
  86. async def _request(self, method, uri, body=None, params=None, HOST=None, auth=False):
  87. url = urljoin(HOST, uri)
  88. headers = {'X-MEXC-APIKEY': self.access_key, 'Content-Type': 'application/json'}
  89. if params != None:
  90. params['timestamp'] = int(time.time())*1000
  91. query_string = "&".join(["{}={}".format(k, params[k]) for k in params.keys()])
  92. params['signature'] = self.get_sign(query_string, self.secret_key)
  93. if auth:
  94. if method == 'GET':
  95. headers = {
  96. 'X-MEXC-APIKEY': self.access_key,
  97. 'Content-Type': 'application/json'
  98. }
  99. else:
  100. headers = {
  101. 'X-MEXC-APIKEY': self.access_key,
  102. 'Content-Type': 'application/x-www-form-urlencoded'
  103. }
  104. # 发起请求
  105. session = self._get_session(url)
  106. timeout = aiohttp.ClientTimeout(10)
  107. msg = "rest请求记录" + str(method) + str(url) + str(params) + str(body)
  108. self.logger.debug(msg)
  109. try:
  110. start_time = time.time()
  111. if method == "GET":
  112. response = await session.get(url, params=params, headers=headers, timeout=timeout, proxy=self.proxy)
  113. elif method == "POST":
  114. response = await session.post(url, params=None, data=json.dumps(body), headers=headers, timeout=timeout, proxy=self.proxy)
  115. elif method == "DELETE":
  116. response = await session.delete(url, params=params, data=body, headers=headers, timeout=timeout, proxy=self.proxy)
  117. code = response.status
  118. res = await response.json()
  119. delay = int(1000*(time.time() - start_time))
  120. self.delays.append(delay)
  121. res_msg = msg + f' 回报 {res}'
  122. self.logger.debug(res_msg)
  123. if code not in (200, 201, 202, 203, 204, 205, 206) or res['code'] not in (0, 200):
  124. print(f'URL:{url} PARAMS:{params} body:{body} ERROR:{res}')
  125. return None, res
  126. return res, None
  127. except Exception as e:
  128. print(f'{self.name} rest 请求出错', str(e))
  129. self.logger.error('请求错误'+str(e))
  130. self.logger.error(traceback.format_exc())
  131. return None, e
  132. def get_delay_info(self):
  133. if len(self.delays) > 100:
  134. self.delays = self.delays[-100:]
  135. if max(self.delays) > self.max_delay:self.max_delay = max(self.delays)
  136. self.avg_delay = round(sum(self.delays)/len(self.delays),1)
  137. async def take_order(self, symbol, amount, origin_side, price, cid="", order_type='LIMIT'):
  138. if origin_side == 'kd':
  139. side = 'buy'
  140. elif origin_side == 'pd':
  141. side = 'sell'
  142. elif origin_side == 'kk':
  143. side = 'sell'
  144. elif origin_side == 'pk':
  145. side = 'buy'
  146. else:
  147. print("现货不允许此交易方向")
  148. return None
  149. if symbol not in self.exchange_info:
  150. await self.before_trade()
  151. amount = float(Decimal(str(amount//self.exchange_info[symbol].stepSize))
  152. * Decimal(str(self.exchange_info[symbol].stepSize)))
  153. price = float(Decimal(str(price//self.exchange_info[symbol].tickSize))
  154. * Decimal(str(self.exchange_info[symbol].tickSize)))
  155. if amount <= 0:
  156. self.logger.error(f'下单参数错误 amount:{amount}')
  157. order_event = dict()
  158. order_event['status'] = "REMOVE"
  159. order_event['filled_price'] = 0.0
  160. order_event['fee'] = 0.0
  161. order_event['filled'] = 0.0
  162. order_event['client_id'] = cid
  163. self.callback["onOrder"](order_event)
  164. return None
  165. if price <= 0:
  166. self.logger.error(f'下单参数错误 price:{price}')
  167. order_event = dict()
  168. order_event['status'] = "REMOVE"
  169. order_event['filled_price'] = 0.0
  170. order_event['fee'] = 0.0
  171. order_event['filled'] = 0.0
  172. order_event['client_id'] = cid
  173. self.callback["onOrder"](order_event)
  174. return None
  175. # Mexc 参数可能有变 Galois
  176. params = {
  177. 'access_id': self.params.access_key,
  178. 'client_id': cid,
  179. 'symbol': symbol,
  180. 'amount': utils.num_to_str(amount, self.exchange_info[symbol].stepSize),
  181. 'type': side,
  182. 'price': utils.num_to_str(price, self.exchange_info[symbol].tickSize),
  183. }
  184. # logger.info(f'下单指令 {params}')
  185. if self.params.debug == 'True':
  186. return await asyncio.sleep(0.1)
  187. else:
  188. # 发单
  189. response, error = await self._request('POST', '/api/v3/order', body=params, auth=1)
  190. # 再更新
  191. if response:
  192. # logger.info(f'下单回报 {response}')
  193. # 增加新的
  194. if 'data' in response:
  195. order_event = dict()
  196. order_event['status'] = "NEW"
  197. order_event['client_id'] = params["client_id"]
  198. order_event['order_id'] = response['data']["id"]
  199. self.callback["onOrder"](order_event)
  200. if error:
  201. order_event = dict()
  202. order_event['status'] = "REMOVE"
  203. order_event['filled_price'] = 0.0
  204. order_event['fee'] = 0.0
  205. order_event['filled'] = 0.0
  206. order_event['client_id'] = params["client_id"]
  207. self.callback["onOrder"](order_event)
  208. return error
  209. return response
  210. async def cancel_order(self, order_id=None, client_id=None, symbol=None):
  211. if order_id:
  212. response, error = await self._request('DELETE', f'/api/v3/margin/order', params={'symbol': self.symbol, 'orderId': order_id}, auth=1)
  213. elif client_id:
  214. response, error = await self._request('DELETE', f'/api/v3/margin/order', params={'symbol': self.symbol, 'orderId': client_id}, auth=1)
  215. else:
  216. raise Exception("撤单出错 没指定订单号")
  217. if response:
  218. self.logger.debug(f'撤单回报 {response}')
  219. if error:
  220. print("撤单失败", error)
  221. self.logger.error(error)
  222. return response
  223. async def check_order(self, order_id=None, client_id=None):
  224. if order_id:
  225. response, error = await self._request('GET', f'/api/v3/order', params={'symbol': self.symbol, 'orderId': order_id}, auth=1)
  226. elif client_id:
  227. response, error = await self._request('GET', f'/api/v3/order', params={'symbol': self.symbol, 'orderId': client_id}, auth=1)
  228. else:
  229. return
  230. if response:
  231. self.logger.debug(f'查单回报 {response}')
  232. order_event = dict()
  233. if response["data"]['status'] in ['not_deal', 'part_deal']:
  234. order_event['status'] = "NEW"
  235. elif response["data"]['status'] in ['cancel', 'done']:
  236. order_event['status'] = "REMOVE"
  237. else:
  238. self.logger.error("错误的订单状态")
  239. order_event['price'] = float(response["data"]["price"])
  240. order_event['amount'] = float(response["data"]["amount"])
  241. order_event['filled'] = float(
  242. response["data"]["amount"])-float(response["data"]["left"])
  243. order_event['filled_price'] = float(response["data"]["avg_price"])
  244. order_event['client_id'] = response["data"]["client_id"]
  245. order_event['order_id'] = response["data"]['id']
  246. asset_fee = float(response['data']["asset_fee"])
  247. money_fee = float(response['data']["money_fee"])
  248. stock_fee = float(response['data']["stock_fee"])
  249. # 非amm品种 优先扣cet 其次u 再次b
  250. # amm品种 买入收b 卖出收u
  251. if response['data']['type'] == "sell":
  252. # 卖出
  253. order_event['fee'] = money_fee
  254. elif response['data']['type'] == "buy":
  255. # 买入
  256. order_event['fee'] = stock_fee
  257. self.callback["onOrder"](order_event)
  258. if error:
  259. print("查单失败", error)
  260. self.logger.error(error)
  261. return response
  262. async def get_order_list(self):
  263. params = {
  264. 'symbol': self.symbol,
  265. 'limit': 100,
  266. }
  267. response, error = await self._request('GET', '/api/v3/allOrders', params=params, auth=1)
  268. orders = [] # 重置本地订单列表
  269. if response is not None:
  270. for i in response['data']['data']:
  271. order_event = dict()
  272. order_event['symbol'] = self.symbol
  273. order_event['price'] = float(i["price"])
  274. order_event['amount'] = float(i["amount"])
  275. order_event['filled'] = float(i["amount"])-float(i["left"])
  276. order_event['filled_price'] = float(i["avg_price"])
  277. order_event['client_id'] = i["client_id"] if 'client_id' in i else ""
  278. order_event['order_id'] = i['id']
  279. asset_fee = float(i["asset_fee"])
  280. money_fee = float(i["money_fee"])
  281. stock_fee = float(i["stock_fee"])
  282. # 非amm品种 优先扣cet 其次u 再次b
  283. # amm品种 买入收b 卖出收u
  284. if i['type'] == "sell":
  285. # 卖出
  286. order_event['fee'] = money_fee
  287. elif i['type'] == "buy":
  288. # 买入
  289. order_event['fee'] = stock_fee
  290. if i['status'] in ['not_deal', 'part_deal']:
  291. order_event['status'] = "NEW"
  292. elif i['status'] in ['cancel', 'done']:
  293. order_event['status'] = "REMOVE"
  294. else:
  295. self.logger.error("错误的订单状态")
  296. self.callback["onOrder"](order_event)
  297. if error:
  298. print(error)
  299. return response
  300. async def get_history_order(self):
  301. pass
  302. async def get_server_time(self):
  303. params = {}
  304. response = await self._request('GET', '/api/v3/time', params=params)
  305. return response
  306. async def before_trade(self):
  307. # 获取市场基本情况
  308. res, error = await self.get_market_details()
  309. if error:
  310. pass
  311. else:
  312. for i in res['data']:
  313. if res['data'][i]['name'] == self.symbol:
  314. self.stepSize = float(Decimal("0.1")**Decimal(res['data'][i]["trading_decimal"]))
  315. self.tickSize = float(Decimal("0.1")**Decimal(res['data'][i]["pricing_decimal"]))
  316. #### 保存交易规则信息
  317. exchange_info = model.ExchangeInfo()
  318. exchange_info.symbol = i
  319. exchange_info.multiplier = 1
  320. exchange_info.stepSize = float(Decimal("0.1")**Decimal(res['data'][i]["trading_decimal"]))
  321. exchange_info.tickSize = float(Decimal("0.1")**Decimal(res['data'][i]["pricing_decimal"]))
  322. self.exchange_info[exchange_info.symbol] = exchange_info
  323. async def get_equity(self):
  324. # 更新账户
  325. res, err = await self.get_account()
  326. if err:
  327. print(err)
  328. if res:
  329. for i in res["data"]:
  330. if self.quote == i:
  331. self.data['equity'] = float(
  332. res['data'][i]['available'])+float(res['data'][i]['frozen'])
  333. self.callback['onEquity']({
  334. self.quote: self.data['equity']
  335. })
  336. self.cash_value = self.data['equity']
  337. elif self.base == i:
  338. coin = float(res['data'][i]['available']) + \
  339. float(res['data'][i]['frozen'])
  340. self.callback['onEquity']({
  341. self.base: coin
  342. })
  343. self.coin_value = coin
  344. async def universalTransfer(self, _type='UMFUTURE_MAIN', asset='USDT', amount=0):
  345. pass
  346. async def futuresTransfer(self, _type='2', asset='USDT', amount=0):
  347. pass
  348. async def get_account(self):
  349. return await self._request('GET', '/api/v3/account', params={"access_id": self.params.access_key}, auth=1)
  350. async def get_market_details(self):
  351. return await self._request('GET', f'/api/v3/exchangeInfo', params={}, auth=0)
  352. async def get_ticker(self):
  353. ## 'merge' 参数可能需要去掉 Galois
  354. res, err = await self._request('GET', f'/api/v3/depth', params={"symbol": self.symbol, 'merge': '0.00000001'}, auth=0)
  355. if res:
  356. ap = float(res["data"]['asks'][0][0])
  357. bp = float(res["data"]['bids'][0][0])
  358. mp = (ap+bp)*0.5
  359. d = {"name": self.name, 'mp': mp, 'bp': bp, 'ap': ap}
  360. self.callback['onTicker'](d)
  361. return d
  362. if err:
  363. self.logger.error(err)
  364. return None
  365. async def buy_token(self):
  366. pass
  367. async def go(self):
  368. await self.before_trade()
  369. await asyncio.sleep(1)
  370. ### Mexc无法检查是否为AMMM品种
  371. # try:
  372. # async with aiohttp.ClientSession(connector = aiohttp.TCPConnector(
  373. # limit=50,
  374. # keepalive_timeout=120,
  375. # verify_ssl=False,
  376. # local_addr=(self.ip,0)
  377. # )) as session:
  378. # response = await session.get(
  379. # "https://api.coinex.com/v1/amm/market",
  380. # proxy=self.proxy
  381. # )
  382. # res = await response.json()
  383. # amm_list = res['data']
  384. # print(f'AMM列表{amm_list}')
  385. # if self.symbol in amm_list:
  386. # self.callback['onExit'](f"{self.name} coinex spot 禁止跑AMM品种")
  387. # else:
  388. # print(f'不是AMM品种 正常运行')
  389. # except:
  390. # self.logger.error(traceback.format_exc())
  391. # self.callback['onExit'](f"{self.name} coinex spot AMM列表获取失败")
  392. while 1:
  393. try:
  394. # 停机信号
  395. if self.stop_flag:
  396. return
  397. # 更新账户
  398. res, err = await self.get_account()
  399. if err:
  400. print(err)
  401. if res:
  402. for i in res["data"]:
  403. if self.quote == i:
  404. self.data['equity'] = float(
  405. res['data'][i]['available']) + float(res['data'][i]['frozen'])
  406. self.callback['onEquity']({
  407. self.quote: self.data['equity']
  408. })
  409. elif self.base == i:
  410. coin = float(res['data'][i]['available']) + \
  411. float(res['data'][i]['frozen'])
  412. self.callback['onEquity']({
  413. self.base: coin
  414. })
  415. # 更新订单
  416. # res = await self.get_order_list()
  417. await asyncio.sleep(60)
  418. # 打印延迟
  419. self.get_delay_info()
  420. self.logger.debug(f'报单延迟 平均{self.avg_delay}ms 最大{self.max_delay}ms')
  421. except:
  422. traceback.print_exc()
  423. await asyncio.sleep(10)
  424. def get_data(self):
  425. return self.data
  426. async def handle_signals(self, orders):
  427. '''执行策略指令'''
  428. try:
  429. for order_name in orders:
  430. if 'Cancel' in order_name:
  431. cid = orders[order_name][0]
  432. oid = orders[order_name][1]
  433. # 只能用oid撤单
  434. if oid:
  435. asyncio.get_event_loop().create_task(self.cancel_order(order_id=oid))
  436. for order_name in orders:
  437. if 'Limits' in order_name:
  438. for i in orders[order_name]:
  439. asyncio.get_event_loop().create_task(self.take_order(
  440. self.symbol,
  441. i[0],
  442. i[1],
  443. i[2],
  444. i[3]
  445. ))
  446. for order_name in orders:
  447. if 'Check' in order_name:
  448. # cid = orders[order_name][0]
  449. oid = orders[order_name][1]
  450. asyncio.get_event_loop().create_task(self.check_order(order_id=oid))
  451. except Exception as e:
  452. traceback.print_exc()
  453. self.logger.error("执行信号出错"+str(e))
  454. await asyncio.sleep(0.1)