322 lines
11 KiB
Python
322 lines
11 KiB
Python
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
|
|
|
|
|
|
def getPrice(client, pair):
|
|
ticker = client.get_tickers(
|
|
category = "linear",
|
|
symbol = pair
|
|
)
|
|
price = float(ticker.get('result').get('list')[0].get('ask1Price'))
|
|
return price
|
|
|
|
def getStartBalance(client, pair):
|
|
coin = pair[:-4]
|
|
response = client.get_wallet_balance(
|
|
accountType = "UNIFIED",
|
|
coin = coin
|
|
)
|
|
balance = float(response['result']['list'][0]['totalAvailableBalance'])
|
|
return balance
|
|
|
|
def getStartFilters(client, pair):
|
|
instrumentInfo = client.get_instruments_info(
|
|
symbol = pair,
|
|
category = "linear"
|
|
)
|
|
infoContents = instrumentInfo.get('result').get('list')[0]
|
|
minimumQty = float(infoContents.get('lotSizeFilter').get('minOrderQty'))
|
|
return minimumQty
|
|
|
|
|
|
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.orderCounter = 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"
|
|
)
|
|
self.orderCounter += 1
|
|
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!")
|
|
tradingLogger.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")
|
|
self.orderCounter -= 1
|
|
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")
|
|
self.orderCounter -= 1
|
|
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 first is main order (market opening), second is SL and third is TP
|
|
levelsRaw = 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(f"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}")
|
|
tradingLogger.info(f"Ending strategy with {pair}")
|
|
return i
|