Files
TradingBot-with-BybitAPI/src/bybit.py
2025-05-18 17:03:59 +03:00

290 lines
10 KiB
Python

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