documentation WIP

This commit is contained in:
2025-05-18 17:03:59 +03:00
parent f70126af46
commit 87d56de922
15 changed files with 531 additions and 160 deletions

View File

@ -1,6 +1,7 @@
import options
from random import randint
async def getLevels(amount, highPrice, lowPrice, roundDecimals):
# Returns array of prices from low to high for each level
levels = []
@ -9,10 +10,20 @@ async def getLevels(amount, highPrice, lowPrice, roundDecimals):
levelPrice = lowPrice
for i in range(amount - 1):
levels.append(levelPrice)
levelPrice = round(levelPrice + delta, 2)
levelPrice = round(levelPrice + delta, roundDecimals)
levels.append(highPrice)
return levels
def floor(value, decimals):
# Rounds float to the lower side with the decimals given
factor = 1/(10**decimals)
return (value//factor)*factor
def countDecimals(value):
# Counts decimals in a float
decimals = len(str(value).split('.')[-1])
return decimals
def getArbus():
# Returnes n arbus (n is random 1 byte positive number)

View File

@ -1,4 +1,5 @@
import time
import traceback
import asyncio
from pybit.unified_trading import HTTP
@ -8,74 +9,268 @@ import options
import credentials
import jsonProcessing
import arbus
from logger import generalLogger
from logger import tradingLogger
async def getClient(apiKey, apiSecret, testnet):
if testnet:
print('Using testnet API.')
else:
print('Using real API.')
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()
print('Auth succesful!')
print('Account info:', response.get('retMsg'))
generalLogger.info("Got client from getClient")
generalLogger.info(f"Account info: {response.get('retMsg')}")
return client
except Exception as e:
print('Auth failed! Check API key!')
print('Error:', e)
generalLogger.warning("Auth failed! Check API key or internet connection!")
return -1
def handlePrice(message):
if message['price'] == '':
print('meow')
print(message)
def handleMessage(message):
print(message)
async def socketStrategy(pair: str, params):
print('Starting strategy with ', pair)
# 'highEnd',
# 'lowEnd',
# 'highBreak',
# 'lowBreak',
# 'netLevelsAmount',
# 'takeDelta',
# 'stopDelta',
# 'orderSize'
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 = options.category,
# callback = handleMessage
channel_type = 'linear',
)
ws_private = WebSocket(
wsPrivate = WebSocket(
testnet = options.testnet,
demo = options.demoTrading,
channel_type = "private",
api_key = credentials.api_key,
api_secret = credentials.api_secret,
# callback = handleMessage
# trace_logging = True
)
print(ws.is_connected())
generalLogger.info(f"Websocket connection state: {ws.is_connected()} (for {pair})")
ws.ticker_stream(
symbol = pair,
callback = handlePrice
callback = td.handlePrice
)
wsPrivate.order_stream(
callback = td.handlePositionInfo
)
i = 0
@ -83,53 +278,12 @@ async def socketStrategy(pair: str, params):
while t:
t = await jsonProcessing.checkPair(pair)
if t != 1:
# ws.exit()
# ws_private.exit()
generalLogger.info("Closing websockets for {pair}")
ws.exit()
wsPrivate.exit()
break
await asyncio.sleep(options.loopSleepTime)
i += 1
print('Ending strategy with ', pair)
print('Ended on the iteration number ', i)
return i
async def strategy(client: HTTP, pair: str, params):
startTime = time.time()
print('Starting strategy with ', pair)
paramsDict = await jsonProcessing.parseParams(params)
i = 0
t = await jsonProcessing.checkPair(pair)
while t:
t = await jsonProcessing.checkPair(pair)
if t != 1:
break
# client = getClient(credentials.api_key, credentials.api_secret, options.testnet)
r1 = client.get_order_history(
category=options.category,
symbol=pair
)
print(r1, '\n')
r2 = client.place_order(
category = options.category,
symbol = pair,
side = 'BUY',
orderType = 'Market',
qty = paramsDict['orderSize'],
marketUnit = 'quoteCoin'
)
print(r2, '\n')
if r2['retMsg'] == 'OK':
print('Order placed succesfully!')
await asyncio.sleep(20)
i += 1
print('Ending strategy with ', pair)
print('Ended on the iteration number ', i)
generalLogger.info(f"Ending strategy with {pair}; Ended on the iteration number {i}")
return i

11
src/credentials.py Normal file
View File

@ -0,0 +1,11 @@
from decouple import config
# Bybit
api_key = config('API_KEY', default='')
api_secret = config('API_SECRET', default='')
# Telegram
bot_token = config('BOT_TOKEN', default='')

View File

@ -1,9 +0,0 @@
# Bybit
api_key = "..."
api_secret = "..."
# Telegram
bot_token = "..."

View File

@ -1,10 +1,29 @@
import json
import os
import shutil
from datetime import datetime
from logger import generalLogger
import options
def startUp():
filePath = 'data.json'
if os.path.exists(filePath):
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backupPath = (f'data_backup_{timestamp}.json')
shutil.copy(filePath, backupPath)
generalLogger.info(f'JSON backup was created: {backupPath}')
with open(filePath, 'w') as f:
json.dump({}, f, ensure_ascii = False, indent=4)
generalLogger.info(f'New {filePath} created with empty JSON.')
async def parseParams(params):
# Returnes dictionary of params as paramsLines in options
# Returnes dictionary of string params as paramsLines in options
paramsList = params.split()
paramsDict = {}
for i in range(len(options.paramsLines)):
@ -23,6 +42,7 @@ async def toDictPairParams(pair: str, params):
paramsDict[pair][options.paramsLines[i]] = paramsList[i]
return paramsDict
async def checkPair(pair: str):
# Returnes 1 if pair exists and 0 if not
currentData = {}
@ -30,13 +50,11 @@ async def checkPair(pair: str):
with open('data.json', 'r') as f:
currentData = json.load(f)
except json.decoder.JSONDecodeError as e:
print('WARNING: JSON file is empty! Ignore if your installation is fresh.')
generalLogger.info('WARNING: JSON file is empty! Ignore if your installation is fresh.')
if pair in currentData:
# print(pair, ' exists in data file.')
return 1
else:
# print(pair, ' not found in data file.')
return 0
async def deletePair(pair: str):
@ -46,21 +64,20 @@ async def deletePair(pair: str):
with open('data.json', 'r') as f:
currentData = json.load(f)
except json.decoder.JSONDecodeError as e:
print('WARNING: JSON file is empty! Ignore if your installation is fresh.')
generalLogger.info('WARNING: JSON file is empty! Ignore if your installation is fresh.')
if pair in currentData:
# print(pair, ' exists in data file.')
del currentData[pair]
with open('data.json', 'w', encoding = 'utf-8') as f:
json.dump(currentData, f, ensure_ascii = False, indent = 4)
generalLogger.info(f'Pair {pair} was deleted successfully!')
return 0
else:
# print(pair, ' not found in data file.')
generalLogger.info(f'Pair {pair} was not found in the data file when trying to delete it.')
return -1
async def savePairParams(pair: str, params):
# Saves or updates data in JSON
# Fix no file or empty file
newData = await toDictPairParams(pair, params)
if newData == -1:
@ -71,14 +88,25 @@ async def savePairParams(pair: str, params):
with open('data.json', 'r') as f:
currentData = json.load(f)
except json.decoder.JSONDecodeError as e:
print('WARNING: JSON file is empty! Ignore if your installation is fresh.')
generalLogger.info('WARNING: JSON file is empty! Ignore if your installation is fresh.')
if pair in currentData:
print(pair, ' already exists.')
generalLogger.info(f"Pair {pair} already exists.")
return -2
else:
with open('data.json', 'w', encoding = 'utf-8') as f:
currentData.update(newData)
json.dump(currentData, f, ensure_ascii = False, indent = 4)
print(pair, ' was added!')
generalLogger.info(f"Pair {pair} was added!")
return 0
async def loadJson():
# Returnes the contents of the JSON file as a dictionary
data = {}
try:
with open('data.json', 'r') as f:
data = json.load(f)
except json.decoder.JSONDecodeError as e:
generalLogger.info('WARNING: JSON file is empty! Ignore if your installation is fresh.')
return data

34
src/logger.py Normal file
View File

@ -0,0 +1,34 @@
import logging
import sys
generalLogPath = "./generalLog.log"
tradingLogPath = "./tradingLog.log"
def setupLogger(name, level, logPath, formatter):
logger = logging.getLogger(name)
logger.setLevel(level)
streamHandler = logging.StreamHandler(sys.stdout)
fileHandler = logging.FileHandler(logPath)
streamHandler.setFormatter(formatter)
fileHandler.setFormatter(formatter)
logger.addHandler(streamHandler)
logger.addHandler(fileHandler)
return logger
# Основной лог
generalFormatter = logging.Formatter('%(asctime)s - %(module)s - %(levelname)s - %(message)s')
generalLogger = setupLogger('general', logging.INFO, generalLogPath, generalFormatter)
# Торговый лог (ордера)
tradingFormatter = logging.Formatter('%(asctime)s - %(message)s')
tradingLogger = setupLogger('trade', logging.NOTSET, tradingLogPath, tradingFormatter)
# Общий лог ну совсем
logging.basicConfig(level=logging.DEBUG)
superGeneralLogger = logging.getLogger('superGeneral')

View File

@ -12,28 +12,22 @@ from aiogram import Router, F
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage
from logger import generalLogger
from logger import tradingLogger
import bybit
import jsonProcessing
import credentials
import whitelist
import strings
import options
# Входные поля для трейдинг-бота:
# Верхняя граница ордеров
# Нижняя граница ордеров
# Верхняя граница для брейка
# Нижняя граница для брейка
# Количество уровней сетки
# Дельта для тейка
# Дельта для стопа
# Размер позиции на каждом уровне
async def set_commands():
commands = [BotCommand(command='start', description='Старт'),
BotCommand(command='help', description='Инструкция'),
BotCommand(command='info', description='Статус'),
BotCommand(command='strategy', description='Запустить стратегию'),
BotCommand(command='stop', description='Остановить стратегию')
]
@ -60,88 +54,101 @@ stop_router = Router()
@dp.message(Command("start"))
async def commandStart(message: Message) -> None:
print("Called function commandStart")
# print(whitelist.chatIDs)
# id = message.from_user.id
# print('Got message from', id, ' with type ', type(id))
await message.answer(strings.startCommand)
@dp.message(Command("help"))
@dp.message(Command("help"), F.chat.id.in_(whitelist.chatIDs))
async def commandHelp(message: Message) -> None:
print("Called function commandHelp")
await message.answer(strings.helpCommand)
@dp.message(Command("info"), F.chat.id.in_(whitelist.chatIDs))
async def commandInfo(message: Message) -> None:
data = await jsonProcessing.loadJson()
msgText = strings.foundData
if data == {}:
msgText = strings.noData
else:
msgText = strings.foundData
for i in data:
msgText += (f"<b>{str(i)}</b>: P&L - x%\n")
await message.answer(msgText)
@strategy_router.message(Command("strategy"))
@strategy_router.message(Command("strategy"), F.chat.id.in_(whitelist.chatIDs))
async def commandStrategy(message: Message, state: FSMContext):
print("Called function commandStrategy")
await message.answer(strings.strategyCommand + '\n' + strings.askPair)
await state.set_state(startForm.pair)
@strategy_router.message(F.text, startForm.pair)
async def capture_start_pair(message: Message, state: FSMContext):
print("Called function capture_start_pair")
await state.update_data(pair=message.text)
data = await state.get_data()
t = 0
if await jsonProcessing.checkPair(data.get("pair")) == 1:
msg_text = (f'Стратегия на паре <b>{data.get("pair")}</b> уже запущена.\nПожалуйста остановите стратегию либо введите другую пару.')
msgText = (f'Стратегия на паре <b>{data.get("pair")}</b> уже запущена.\nПожалуйста остановите стратегию либо введите другую пару.')
t = 1
else:
msg_text = strings.askParams
msgText = strings.askParams
await message.answer(msg_text)
await message.answer(msgText)
if t == 1:
print('Clearing state!')
await state.clear()
else:
await state.set_state(startForm.params)
@strategy_router.message(F.text, startForm.params)
async def capture_params(message: Message, state: FSMContext):
print("Called function capture_params")
await state.update_data(params=message.text)
data = await state.get_data()
t = await jsonProcessing.savePairParams(pair=data.get("pair"), params=data.get("params"))
if t == 0:
client = await bybit.getClient(credentials.api_key, credentials.api_secret, options.testnet)
client = await bybit.getClient(
credentials.api_key,
credentials.api_secret,
options.testnet,
options.demoTrading
)
if client == -1:
msg_text = (f'Аутентификация не удалась, сообщите администратору если увидете данное сообщение.')
msgText = strings.authFailed
await jsonProcessing.deletePair(pair=data.get("pair"))
else:
asyncio.create_task(bybit.socketStrategy(data.get("pair"), data.get("params")))
msg_text = (f'Вы запустили стратегию на паре <b>{data.get("pair")}</b> с данными параметрами:\n<b>{data.get("params")}</b>\n')
try:
asyncio.create_task(bybit.strategy(data.get("pair"), data.get("params")))
msgText = (f'Вы запустили стратегию на паре <b>{data.get("pair")}</b> с данными параметрами:\n<b>{data.get("params")}</b>\n')
except:
await jsonProcessing.deletePair(pair=data.get("pair"))
msgText = (f'Возникла ошибка в работе стратегии =( Пожалуйста сообщите об этом администратору.')
elif t == -1:
msg_text = (f'Параметры введены в неверном формате, пожалуйста начните заново.')
msgText = (f'Параметры введены в неверном формате, пожалуйста начните заново.')
elif t == -2:
msg_text = (f'Стратегия на паре <b>{data.get("pair")}</b> уже запущена.')
msgText = (f'Стратегия на паре <b>{data.get("pair")}</b> уже запущена.')
else:
msg_text = (f'Возникла непредвиденная ошибка. =(')
await message.answer(msg_text)
msgText = (f'Возникла непредвиденная ошибка. =(')
await message.answer(msgText)
await state.clear()
@stop_router.message(Command("stop"))
@stop_router.message(Command("stop"), F.chat.id.in_(whitelist.chatIDs))
async def commandStop(message: Message, state: FSMContext):
print("Called function commandStop")
await message.answer(strings.stopCommand + '\n' + strings.askPair)
await state.set_state(stopForm.pair)
@stop_router.message(F.text, stopForm.pair)
async def capture_stop_pair(message: Message, state: FSMContext):
print("Called function capture_stop_pair")
await state.update_data(pair=message.text)
data = await state.get_data()
if await jsonProcessing.checkPair(data.get("pair")) == 1:
t = await jsonProcessing.deletePair(data.get("pair"))
if t == 0:
print('Deleted pair succesfuly')
else:
print('Error with deleting pair')
msg_text = strings.stopStrategy
t = await jsonProcessing.deletePair(data.get("pair"))
if t == 0:
msgText = strings.stopStrategy
else:
msg_text = strings.pairNotFound
msgText = strings.pairNotFound
await message.answer(msg_text)
await message.answer(msgText)
await state.clear()
@ -158,5 +165,7 @@ async def main() -> None:
# Main
if __name__ == "__main__":
print('Started bot!')
generalLogger.info("Started bot!")
tradingLogger.info("Started bot!")
jsonProcessing.startUp()
asyncio.run(main())

View File

@ -1,18 +1,13 @@
url = 'https://testnet.binance.vision/api' # API url
testnet = True # Use testnet or not
from decouple import config
testnet = config('TESTNET', default='False').lower() != 'false' # Use testnet or not
demoTrading = config('DEMOTRADING', default='False').lower() != 'false' # Use demo trading or not
# Please do not combine testnet and demo trading
leverage = int(config('LEVERAGE', default='1')) # Leverage
# notification = 1 # Telegram notifications (not currently supported)
pairSymbol = 'ETHUSDT' # Trading pair
mainSymbol = 'USDT' # Balance asset
timeScape = '15m' # Candle length
category = 'linear'
leverage = 1 # Leverage
notification = 1 # Telegram notifications (not currently supported)
marketBuyRange = 0.5
loopSleepTime = 1 # Time passing between loops/checks
loopSleepTime = int(config('LOOPSLEEPTIME', default='1')) # Time passing between checks for stopping strategy
paramsLines = ['highEnd',
'lowEnd',

View File

@ -18,7 +18,7 @@ stopBot = "Бот остановлен!"
# Commands
startCommand = "Привет! Это приватный бот для полуавтоматической торговли криптовалютой. Хороших позиций!"
startCommand = "Привет! Это приватный бот для полуавтоматической торговли криптовалютой. В данный момент он работает по вайтлисту. Хороших позиций!"
stopCommand = "Вы собираетесь остановить стратегию."
@ -44,3 +44,12 @@ askParams = "Введите параметры:"
gotParams = "Параметры заданы!"
pairNotFound = "Стратегия на данную монетную пару не найдена."
authFailed = (f'Аутентификация не удалась, пожалуйста сообщите администратору если увидете данное сообщение.')
# Data status
noData = "Нет запущенных стратегий!"
foundData = "В данный момент стратегия запущена на следующих монетных парах:\n"

9
src/whitelist.py Normal file
View File

@ -0,0 +1,9 @@
from decouple import config
import re
def checkWhiteList(id):
# Checks if id exists in the whitelist. Returnes 1 if exists and 0 if not
return id in chatIDs
chatIDsstring = config('WHITELIST', default='')
chatIDs = [int(x) for x in re.split(r',\s*', chatIDsstring)]