kucoin_usdt_swap_rest.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import random
  2. import aiohttp
  3. import time
  4. import asyncio
  5. import zlib
  6. import json
  7. import hmac
  8. import base64
  9. import hashlib
  10. import traceback
  11. import urllib
  12. from urllib import parse
  13. from urllib.parse import urljoin
  14. import datetime, sys
  15. from urllib.parse import urlparse
  16. import logging, logging.handlers
  17. import utils
  18. import logging, logging.handlers
  19. import model
  20. from loguru import logger
  21. logger.add("kucoin_limits.log", format="{time} {level} {message}", level="INFO")
  22. def empty_call(msg):
  23. print(f'空的回调函数 {msg}')
  24. def decimal_amount(amount, d):
  25. if int(d) == 0:
  26. return str(int(amount))
  27. elif int(d) > 0:
  28. return str(round(int(amount*10**int(d))*0.1**int(d), int(d)))
  29. def decimal_price(price, d):
  30. if int(d) == 0:
  31. return str(int(price))
  32. elif int(d) > 0:
  33. return str(round(float(price), int(d)))
  34. class KucoinUsdtSwapRest:
  35. def __init__(self, params:model.ClientParams, colo=0):
  36. if colo:
  37. print('不支持colo高速线路')
  38. self.HOST = "https://api-futures.kucoin.com"
  39. else:
  40. self.HOST = "https://api-futures.kucoin.com"
  41. self.params = params
  42. self.name = self.params.name
  43. self.base = self.params.pair.split('_')[0].upper()
  44. # 处理特殊情况
  45. if self.base == "BTC":self.base = "XBT"
  46. self.quote = self.params.pair.split('_')[1].upper()
  47. self.symbol = self.base + self.quote + "M"
  48. self._SESSIONS = dict()
  49. self.logger = self.get_logger()
  50. self.callback = {
  51. "onMarket":empty_call,
  52. "onPosition":empty_call,
  53. "onOrder":empty_call,
  54. "onEquity":empty_call,
  55. "onTicker":empty_call,
  56. "onDepth":empty_call,
  57. "onExit":empty_call,
  58. }
  59. self.exchange_info = dict()
  60. self.decimal_amount = 10
  61. self.decimal_price = 10
  62. self.delays = []
  63. self.max_delay = 0
  64. self.avg_delay = 0
  65. self.multiplier = None
  66. self.proxy = None
  67. if 'win' in sys.platform:
  68. self.proxy = self.params.proxy
  69. self.logger = self.get_logger()
  70. self.coin_value = 0.0
  71. self.cash_value = 0.0
  72. self.last_cash = 0.0
  73. #### 指定发包ip
  74. iplist = utils.get_local_ip_list()
  75. self.ip = iplist[int(self.params.ip)]
  76. def get_logger(self):
  77. logger = logging.getLogger(__name__)
  78. logger.setLevel(logging.DEBUG)
  79. # log to txt
  80. formatter = logging.Formatter('[%(asctime)s] - %(levelname)s - %(message)s')
  81. handler = logging.handlers.RotatingFileHandler(f"log.log",maxBytes=1024*1024)
  82. handler.setLevel(logging.DEBUG)
  83. handler.setFormatter(formatter)
  84. logger.addHandler(handler)
  85. return logger
  86. def get_logger(self):
  87. logger = logging.getLogger(__name__)
  88. logger.setLevel(logging.DEBUG)
  89. # log to txt
  90. formatter = logging.Formatter('[%(asctime)s] - %(levelname)s - %(message)s')
  91. handler = logging.handlers.RotatingFileHandler("log.log",maxBytes=1024*1024,encoding='utf-8')
  92. handler.setLevel(logging.DEBUG)
  93. handler.setFormatter(formatter)
  94. # log to console
  95. console = logging.StreamHandler()
  96. console.setLevel(logging.WARNING)
  97. logger.addHandler(handler)
  98. logger.addHandler(console)
  99. return logger
  100. def _get_session(self, url):
  101. parsed_url = urlparse(url)
  102. key = parsed_url.netloc or parsed_url.hostname
  103. if key not in self._SESSIONS:
  104. tcp = aiohttp.TCPConnector(limit=50,keepalive_timeout=120,verify_ssl=False,local_addr=(self.ip,0))
  105. session = aiohttp.ClientSession(connector=tcp)
  106. self._SESSIONS[key] = session
  107. return self._SESSIONS[key]
  108. async def _request(self, method, uri, body=None, params=None, auth=False):
  109. url = urljoin(self.HOST, uri)
  110. headers = {}
  111. if auth:
  112. now_time = int(time.time()) * 1000
  113. str_to_sign = str(now_time) + method + uri
  114. if method in ['GET', 'DELETE']:
  115. data_json = ''
  116. if params:
  117. strl = []
  118. for key in params:
  119. strl.append("{}={}".format(key, params[key]))
  120. data_json += '&'.join(strl)
  121. str_to_sign += '?' + data_json
  122. else:
  123. if body:str_to_sign += body
  124. sign = base64.b64encode(hmac.new(self.params.secret_key.encode('utf-8'), str_to_sign.encode('utf-8'), hashlib.sha256).digest())
  125. passphrase = base64.b64encode(hmac.new(self.params.secret_key.encode('utf-8'), self.params.pass_key.encode('utf-8'), hashlib.sha256).digest())
  126. headers = {
  127. "KC-API-SIGN": sign.decode(),
  128. "KC-API-TIMESTAMP": str(now_time),
  129. "KC-API-KEY": self.params.access_key,
  130. "KC-API-PASSPHRASE": passphrase.decode(),
  131. "Content-Type": "application/json",
  132. "KC-API-KEY-VERSION": "2"
  133. }
  134. headers["User-Agent"] = "kucoin-python-sdk/v1.0"
  135. # 发起请求
  136. session = self._get_session(url)
  137. timeout = aiohttp.ClientTimeout(10)
  138. msg = "rest请求记录" + str(method) + str(url) + str(params) + str(body)
  139. self.logger.debug(msg)
  140. try:
  141. start_time = time.time()
  142. if method == "GET":
  143. response = await session.get(url, params=params, headers=headers, timeout=timeout, proxy=self.proxy)
  144. elif method == "POST":
  145. response = await session.post(url, params=None, data=body, headers=headers, timeout=timeout, proxy=self.proxy)
  146. elif method == "DELETE":
  147. response = await session.delete(url, params=params, data=body, headers=headers, timeout=timeout, proxy=self.proxy)
  148. code = response.status
  149. res = await response.json()
  150. delay = int(1000*(time.time() - start_time))
  151. res_msg = msg + f' 回报 {res}'
  152. self.logger.debug(res_msg)
  153. self.delays.append(delay)
  154. if code not in (200, 201, 202, 203, 204, 205, 206) or \
  155. int(res['code']) not in (200, 201, 202, 203, 204, 205, 206, 200000):
  156. print(f'URL:{url} METHOD:{method} PARAMS:{params} body:{body} ERROR:{res}')
  157. self.logger.error('请求错误'+str(res))
  158. self.logger.error(res)
  159. return None, res
  160. return res, None
  161. except Exception as e:
  162. print(f'{self.name} rest 请求出错', str(e))
  163. self.logger.error('请求错误'+str(e))
  164. self.logger.error(traceback.format_exc())
  165. return None, e
  166. async def buy_token(self):
  167. '''买入平台币'''
  168. pass
  169. async def check_position(self, hold_coin=0.0):
  170. '''
  171. 两次执行check_position之间必须停留足够时间 避免position没及时更新 导致重复下单 已支持全品种
  172. '''
  173. try:
  174. ##############################
  175. self.logger.info("清空挂单")
  176. params = {
  177. 'status':"active",
  178. 'tradeType':'TRADE',
  179. }
  180. response, error = await self._request('GET', '/api/v1/orders', params=params, auth=1)
  181. if response is not None and response['data']['items'] is not None:
  182. for i in response['data']['items']:
  183. res = await self.cancel_order(order_id=i["id"])
  184. self.logger.info(res)
  185. self.logger.info(f"{self.name} 全平仓位")
  186. ##############################
  187. # 更新仓位
  188. print("获取仓位")
  189. res, err = await self._request('GET','/api/v1/positions', params={}, auth=1)
  190. if err:self.logger.info(err)
  191. if res:
  192. for i in res['data']:
  193. symbol = i['symbol']
  194. amt = float(i['currentQty'])
  195. # 转换为单位:张
  196. if self.exchange_info == dict():
  197. await self.before_trade()
  198. if amt > 0:
  199. self.logger.info( await self.take_order(
  200. symbol,
  201. amt*self.exchange_info[symbol].multiplier,
  202. "pd",
  203. 1,
  204. utils.get_cid(),
  205. "market"
  206. ))
  207. elif amt < 0:
  208. self.logger.info( await self.take_order(
  209. symbol,
  210. -amt*self.exchange_info[symbol].multiplier,
  211. "pk",
  212. 1,
  213. utils.get_cid(),
  214. "market"
  215. ))
  216. ##############################
  217. except:
  218. self.logger.error("清仓程序执行出错")
  219. self.logger.error(traceback.format_exc())
  220. return
  221. async def take_order(self, symbol, amount, origin_side, price, cid="", order_type='limit'):
  222. '''
  223. kumex会合并多空方向的仓位 不支持双向交易 所以reduce_only要小心使用
  224. '''
  225. reduceOnly = False
  226. if origin_side =='kd':
  227. side = 'buy'
  228. elif origin_side =='pd':
  229. side = 'sell'
  230. elif origin_side =='kk':
  231. side = 'sell'
  232. elif origin_side =='pk':
  233. side = 'buy'
  234. else:
  235. return None
  236. # 转换为单位:张
  237. if symbol not in self.exchange_info:
  238. await self.before_trade()
  239. amount_paper = int(amount/self.exchange_info[symbol].multiplier)
  240. if amount_paper == 0:
  241. order_event = dict()
  242. order_event['status'] = "REMOVE"
  243. order_event['client_id'] = cid
  244. order_event['filled'] = 0
  245. order_event['filled_price'] = 0
  246. self.callback["onOrder"](order_event)
  247. return
  248. params = {
  249. 'clientOid':cid,
  250. 'symbol': symbol,
  251. 'size': amount_paper,
  252. 'side': side,
  253. 'leverage': 10, #10, 没kyc老报错,##############################################
  254. 'reduceOnly':reduceOnly,
  255. 'price':utils.num_to_str(price, self.exchange_info[symbol].tickSize),
  256. 'type':order_type,
  257. }
  258. # logger.info(f'下单指令 {params}')
  259. if self.params.debug == 'True':
  260. return await asyncio.sleep(0.1)
  261. else:
  262. # 发单
  263. response, error = await self._request('POST', '/api/v1/orders', body=json.dumps(params), auth=1)
  264. # 更新
  265. if response:
  266. # 增加新的
  267. if 'data' in response:
  268. order_event = dict()
  269. order_event['status'] = "NEW"
  270. order_event['client_id'] = cid
  271. order_event['order_id'] = response['data']["orderId"]
  272. self.callback["onOrder"](order_event)
  273. if error:
  274. order_event = dict()
  275. order_event['status'] = "REMOVE"
  276. order_event['client_id'] = cid
  277. order_event['filled_price'] = 0
  278. order_event['filled'] = 0
  279. self.callback["onOrder"](order_event)
  280. return response
  281. async def cancel_order(self, order_id=None, client_id=None):
  282. if order_id:
  283. response, error = await self._request('DELETE', f'/api/v1/orders/{order_id}', auth=1)
  284. elif client_id:
  285. response, error = await self._request('DELETE', f'/api/v1/orders/byClientOid?clientOid={client_id}', auth=1)
  286. else:
  287. raise Exception("撤单出错 没指定订单号")
  288. if response:
  289. self.logger.debug(f'撤单回报 {response}')
  290. # 撤单成功不会返回成交信息 所以不触发回调
  291. if error:
  292. print("撤单失败",error)
  293. return error
  294. return response
  295. async def check_order(self, order_id=None, client_id=None):
  296. if order_id:
  297. response, error = await self._request('GET', f'/api/v1/orders/{order_id}', auth=1)
  298. elif client_id:
  299. response, error = await self._request('GET', f'/api/v1/orders/byClientOid?clientOid={client_id}', auth=1)
  300. else:
  301. return
  302. if response:
  303. self.logger.debug(f'查单回报 {response}')
  304. order_event = dict()
  305. if response["data"]['isActive'] == True:
  306. order_event['status'] = "NEW"
  307. elif response["data"]['isActive'] == False:
  308. order_event['status'] = "REMOVE"
  309. else:
  310. self.logger.error("错误的订单状态")
  311. if self.multiplier == None:
  312. await self.before_trade()
  313. order_event['filled'] = float(response["data"]["filledSize"])*self.multiplier
  314. order_event['filled_price'] = float(response["data"]["filledValue"])/float(response["data"]["filledSize"])/self.multiplier if float(response["data"]["filledSize"]) > 0 else 0
  315. order_event['client_id'] = response["data"]["clientOid"]
  316. order_event['order_id'] = response["data"]['id']
  317. self.callback["onOrder"](order_event)
  318. if error:
  319. print("查单失败",error)
  320. self.logger.error(error)
  321. return response
  322. async def get_order_list(self):
  323. params = {
  324. 'symbol':self.symbol,
  325. 'status':"active",
  326. 'tradeType':'TRADE',
  327. }
  328. response, error = await self._request('GET', '/api/v1/orders', params=params, auth=1)
  329. orders = [] # 重置本地订单列表
  330. if response is not None:
  331. for i in response['data']['items']:
  332. order_event = dict()
  333. if i['isActive'] == True:
  334. order_event['status'] = "NEW"
  335. elif i['isActive'] == False:
  336. order_event['status'] = "REMOVE"
  337. else:
  338. self.logger.error("错误的订单状态")
  339. order_event['filled'] = float(i["dealSize"])
  340. order_event['filled_price'] = float(i["dealFunds"])/float(i["dealSize"]) if float(i["dealSize"]) > 0 else 0
  341. order_event['client_id'] = i["clientOid"]
  342. order_event['order_id'] = i['id']
  343. self.callback["onOrder"](order_event)
  344. if error:
  345. print(error)
  346. return response
  347. async def get_server_time(self):
  348. params = {}
  349. response = await self._request('GET', '/api/v1/timestamp', params=params)
  350. return response
  351. async def get_account(self):
  352. return await self._request('GET','/api/v1/account-overview', params={"currency":self.quote}, auth=1)
  353. async def get_position(self):
  354. return await self._request('GET','/api/v1/position', params={"symbol":self.symbol}, auth=1)
  355. async def get_market_details(self):
  356. return await self._request('GET',f'api/v1/contracts/active', params={}, auth=1)
  357. async def get_ticker(self):
  358. res, err = await self._request('GET',f'api/v1/ticker', params={"symbol":self.symbol}, auth=0)
  359. if res:
  360. ap = float(res['data']['bestAskPrice'])
  361. bp = float(res['data']['bestBidPrice'])
  362. mp = (ap+bp)/2
  363. ticker = {"name":self.name,'mp': mp, 'bp':bp, 'ap':ap}
  364. return ticker
  365. if err:
  366. self.logger.error(err)
  367. return
  368. async def before_trade(self):
  369. # 获取市场
  370. res, error = await self.get_market_details()
  371. if error:
  372. pass
  373. if res:
  374. for i in res['data']:
  375. if i['symbol'] == self.symbol:
  376. # 1张多少个币
  377. self.multiplier = float(i["multiplier"])
  378. # 1张 币的数量精度 张 转换成 币 需要乘以乘数
  379. self.stepSize = i["lotSize"]*self.multiplier
  380. self.tickSize = i['tickSize']
  381. #### 保存交易规则信息
  382. exchange_info = model.ExchangeInfo()
  383. exchange_info.symbol = i['symbol']
  384. exchange_info.multiplier = float(i["multiplier"])
  385. exchange_info.tickSize = i['tickSize']
  386. exchange_info.stepSize = i["lotSize"]*float(i["multiplier"])
  387. self.exchange_info[exchange_info.symbol] = exchange_info
  388. # 设置杠杆
  389. await self._request('GET',f'api/v1/contracts/active', params={}, auth=1)
  390. # 更新账户
  391. res, err = await self.get_position()
  392. if err:print(err)
  393. if res:
  394. amt = float(res['data']['currentQty']) * self.multiplier
  395. ep = float(res['data']['avgEntryPrice'])
  396. pos = model.Position()
  397. if amt == 0.0:
  398. pos.longPos = 0
  399. pos.longAvg = 0
  400. pos.shortPos = 0
  401. pos.shortAvg = 0
  402. elif amt > 0.0:
  403. pos.longPos = amt
  404. pos.longAvg = ep
  405. pos.shortPos = 0
  406. pos.shortAvg = 0
  407. elif amt < 0.0:
  408. pos.longPos = 0
  409. pos.longAvg = 0
  410. pos.shortPos = -amt
  411. pos.shortAvg = ep
  412. self.callback['onPosition'](pos)
  413. async def get_equity(self):
  414. # 更新账户
  415. res, err = await self.get_account()
  416. if err:print(err)
  417. if res:
  418. cash = float(res['data']['accountEquity'])
  419. if self.last_cash == 0:
  420. self.last_cash = cash
  421. self.callback['onEquity']({self.quote:cash})
  422. self.cash_value = cash
  423. async def go(self):
  424. interval = 60
  425. await self.before_trade()
  426. await asyncio.sleep(1)
  427. while 1:
  428. try:
  429. # 更新账户
  430. res, err = await self.get_account()
  431. if err:print(err)
  432. if res:
  433. if res['data']['currency'] == "USDT":
  434. cash = float(res['data']['accountEquity'])
  435. # rest有可能获取到中间态 为了避免得到错误的账户信息 需进行判断
  436. if self.last_cash == 0:
  437. self.last_cash = cash
  438. self.callback['onEquity']({self.quote:cash})
  439. else:
  440. # 判断净值是否出现大幅度偏离 两次更新之间的差值不应超过5%
  441. if abs(self.last_cash - cash)/cash < 0.05:
  442. self.last_cash = cash
  443. self.callback['onEquity']({self.quote:cash})
  444. else:
  445. # 否则舍弃本次更新
  446. pass
  447. # 更新账户
  448. res, err = await self.get_position()
  449. if err:print(err)
  450. if res:
  451. amt = float(res['data']['currentQty']) * self.multiplier
  452. ep = float(res['data']['avgEntryPrice'])
  453. pos = model.Position()
  454. if amt == 0.0:
  455. pos.longPos = 0
  456. pos.longAvg = 0
  457. pos.shortPos = 0
  458. pos.shortAvg = 0
  459. elif amt > 0.0:
  460. pos.longPos = amt
  461. pos.longAvg = ep
  462. pos.shortPos = 0
  463. pos.shortAvg = 0
  464. elif amt < 0.0:
  465. pos.longPos = 0
  466. pos.longAvg = 0
  467. pos.shortPos = -amt
  468. pos.shortAvg = ep
  469. self.callback['onPosition'](pos)
  470. await asyncio.sleep(interval)
  471. # 打印延迟
  472. self.get_delay_info()
  473. self.logger.debug(f'报单延迟 平均{self.avg_delay}ms 最大{self.max_delay}ms')
  474. except:
  475. traceback.print_exc()
  476. await asyncio.sleep(10)
  477. def get_delay_info(self):
  478. if len(self.delays) > 100:
  479. self.delays = self.delays[-100:]
  480. if max(self.delays) > self.max_delay:self.max_delay = max(self.delays)
  481. self.avg_delay = round(sum(self.delays)/len(self.delays),1)
  482. async def handle_signals(self, orders):
  483. '''执行策略指令'''
  484. try:
  485. for order_name in orders:
  486. if 'Cancel' in order_name:
  487. # cid = orders[order_name][0]
  488. oid = orders[order_name][1]
  489. # kumex 只能按oid撤单
  490. if oid:
  491. asyncio.get_event_loop().create_task(self.cancel_order(order_id=oid))
  492. for order_name in orders:
  493. if 'Limits' in order_name:
  494. for i in orders[order_name]:
  495. logger.info(i)
  496. asyncio.get_event_loop().create_task(self.take_order(
  497. self.symbol,
  498. i[0],
  499. i[1],
  500. i[2],
  501. i[3]
  502. ))
  503. if len(orders[order_name]) > 0:
  504. logger.info("")
  505. for order_name in orders:
  506. if 'Check' in order_name:
  507. # cid = orders[order_name][0]
  508. oid = orders[order_name][1]
  509. if oid:
  510. asyncio.get_event_loop().create_task(self.check_order(order_id=oid))
  511. except:
  512. # traceback.print_exc()
  513. await asyncio.sleep(0.1)