import time import traceback import asyncio from pybit.unified_trading import HTTP from pybit.unified_trading import WebSocket import options import credentials import jsonProcessing import arbus from logger import generalLogger from logger import tradingLogger class tradingData: def __init__(self, pair, levels, highBreak, lowBreak, takeDelta, stopDelta, orderSize): self.client = HTTP( testnet = options.testnet, demo = options.demoTrading, api_key = credentials.api_key, api_secret = credentials.api_secret ) self.pair = pair self.balance = self.getBalance(pair) self.levels = levels self.highBreak = highBreak self.lowBreak = lowBreak self.takeDelta = takeDelta self.stopDelta = stopDelta self.orderSize = orderSize self.priceDecimals, self.qtyDecimals, self.minimumQty = self.getFilters(pair) self.previousPrice = -1 self.counter = 0 def getBalance(self, pair): coin = pair[:-4] response = self.client.get_wallet_balance( accountType = 'UNIFIED', coin = coin ) balance = float(response['result']['list'][0]['totalAvailableBalance']) return balance def getFilters(self, pair): instrumentInfo = self.client.get_instruments_info( symbol = pair, category = "linear" ) infoContents = instrumentInfo.get('result').get('list')[0] minimumQty = float(infoContents.get('lotSizeFilter').get('minOrderQty')) qtyDecimals = arbus.countDecimals(minimumQty) priceDecimals = int(infoContents.get('priceScale')) return priceDecimals, qtyDecimals, minimumQty def close(self): jsonProcessing.deletePair(self.pair) generalLogger.info(f"Closing strategy on {self.pair}") def checkCloseConditions(self, markPrice): # If the price is outside of the (lowBreak; highBreak) interval then stop strategy if not (self.lowBreak < markPrice < self.highBreak): self.close() def checkOrderConditions(self, markPrice): for i in self.levels: levelPrice = i.get('price') # If level gets crossed from below or from above then go on # Can be split for different sides (long or short) if self.previousPrice <= levelPrice <= markPrice or \ self.previousPrice >= levelPrice >= markPrice: if i.get('long') == False: id = self.placeCombiOrder( True, markPrice, 'Buy', levelPrice+self.takeDelta, levelPrice-self.stopDelta, ) i['long'] = True i['longIDs'][0] = id if i.get('short') == False: id = self.placeCombiOrder( True, markPrice, 'Sell', levelPrice-self.takeDelta, levelPrice+self.stopDelta, ) i['short'] = True i['shortIDs'][0] = id def placeCombiOrder(self, useQuoteSymbol, markPrice, side, tp, sl): positionIdx = 1 if side == 'Buy': positionIdx = 1 if side == 'Sell': positionIdx = 2 qty = self.orderSize if useQuoteSymbol: qty = arbus.floor(self.orderSize/markPrice, self.qtyDecimals) orderID = '-1' if qty >= self.minimumQty and qty < self.balance: response = self.client.place_order( category = "linear", symbol = self.pair, side = side, orderType = "Market", qty = str(qty), isLeverage = 1, positionIdx = positionIdx, takeProfit = str(tp), stopLoss = str(sl), tpslMode = "Full" ) orderID = response.get('result').get('orderId') generalLogger.info(f"Placed oder on {self.pair} with TP {tp}; SL {sl}") tradingLogger.info(f"Placed oder on {self.pair} with TP {tp}; SL {sl}") else: generalLogger.warning(f"Failed to place order on {self.pair}; qty is too small!") return orderID def handlePrice(self, message): try: markPrice = float(message.get('data')['markPrice']) self.checkCloseConditions(markPrice) if self.previousPrice != -1: self.checkOrderConditions(markPrice) self.previousPrice = markPrice except Exception as e: generalLogger.error(e) for line in traceback.format_exception(e): generalLogger.error(line) def handlePositionInfo(self, message): data = message.get('data') # Usually the 3-order response means SL + market + TP orders were placed. if len(data) == 3: orderType = [] orderIDs = [] mainOrderID = '' f = 0 # Fill order types accordingly to the ids for i in data: orderType.append(i['stopOrderType']) orderIDs.append(i['orderId']) author = i['createType'] # Verifying it was the TP/SL order placement if i['stopOrderType'] == '' and author == 'CreateByUser': mainOrderID = i['orderID'] f = 1 if f: for i in self.levels: if mainOrderID == i.get('longIDs')[0]: for j in range(3): if orderType[j] == 'StopLoss': i['longIDs'][1] = orderIDs[j] if orderType[j] == 'TakeProfit': i['longIDs'][2] = orderIDs[j] else: for i in data: orderID = i['orderId'] orderStatus = i['orderStatus'] if orderStatus == 'Triggered': for j in self.levels: longIDs = j['longIDs'] shortIDs = j['shortIDs'] if orderID in longIDs: j['long'] = False j['longIDs'] = ['-1', '-1', '-1'] generalLogger.info(f"Long order on {self.pair} level {j['price']} triggered TP/SL") if orderID in shortIDs: j['short'] = False j['shortIDs'] = ['-1', '-1', '-1'] generalLogger.info(f"Short order on {self.pair} level {j['price']} triggered TP/SL") if orderStatus == 'Filled': for j in self.levels: longIDs = j['longIDs'] shortIDs = j['shortIDs'] if orderID in longIDs: generalLogger.info(f"Long order on {self.pair} level {j['price']} filled with P&L {i['closedPnl']} and qty {i['qty']}") tradingLogger.info(f"Long order on {self.pair} level {j['price']} filled with P&L {i['closedPnl']} and qty {i['qty']}") if orderID in shortIDs: tradingLogger.info(f"Short order on {self.pair} level {j['price']} filled with P&L {i['closedPnl']} and qty {i['qty']}") async def getClient(apiKey, apiSecret, testnet, demoTrading): client = HTTP( testnet = testnet, demo = demoTrading, api_key = apiKey, api_secret = apiSecret, ) try: response = client.get_account_info() generalLogger.info("Got client from getClient") generalLogger.info(f"Account info: {response.get('retMsg')}") return client except Exception as e: generalLogger.warning("Auth failed! Check API key or internet connection!") return -1 async def strategy(pair: str, params): generalLogger.info('Starting strategy with ' + pair) paramsDict = await jsonProcessing.parseParams(params) client = HTTP(testnet=True) levelsAmount = int(paramsDict['netLevelsAmount']) highEnd = float(paramsDict['highEnd']) lowEnd = float(paramsDict['lowEnd']) instrumentInfo = client.get_instruments_info( symbol = pair, category = "linear" ) infoContents = instrumentInfo.get('result').get('list')[0] priceDecimals = int(infoContents.get('priceScale')) # Levels have 3 IDs for both long and short position. The 1 is main order (market opening), 2 is SL and 3 is TP. levelsRaw = await arbus.getLevels(levelsAmount, highEnd, lowEnd, priceDecimals) levels = [] for i in range(levelsAmount): levels.append({'price':levelsRaw[i], 'long':False, 'longIDs':['-1', '-1', '-1'], 'short':False, 'shortIDs':['-1', '-1', '-1']}) td = tradingData( pair, levels, float(paramsDict['highBreak']), float(paramsDict['lowBreak']), float(paramsDict['takeDelta']), float(paramsDict['stopDelta']), float(paramsDict['orderSize']) ) ws = WebSocket( testnet = options.testnet, channel_type = 'linear', ) wsPrivate = WebSocket( testnet = options.testnet, demo = options.demoTrading, channel_type = "private", api_key = credentials.api_key, api_secret = credentials.api_secret, ) generalLogger.info(f"Websocket connection state: {ws.is_connected()} (for {pair})") ws.ticker_stream( symbol = pair, callback = td.handlePrice ) wsPrivate.order_stream( callback = td.handlePositionInfo ) i = 0 t = await jsonProcessing.checkPair(pair) while t: t = await jsonProcessing.checkPair(pair) if t != 1: generalLogger.info("Closing websockets for {pair}") ws.exit() wsPrivate.exit() break await asyncio.sleep(options.loopSleepTime) i += 1 generalLogger.info(f"Ending strategy with {pair}; Ended on the iteration number {i}") return i