7 Commits
0.0.3 ... 0.0.8

Author SHA1 Message Date
98d770efae Auto strategy close fix
All checks were successful
Build and Push Docker Image / build-and-push (release) Successful in 1m27s
2025-06-03 17:41:28 +03:00
c1835ca4eb Info and log fixes
All checks were successful
Build and Push Docker Image / build-and-push (release) Successful in 1m16s
2025-05-26 11:20:39 +03:00
f27df7b1b8 Bot start/stop
All checks were successful
Build and Push Docker Image / build-and-push (release) Successful in 1m27s
2025-05-26 10:59:37 +03:00
7fbd2887b7 Small fixes 2025-05-25 23:26:35 +03:00
262cef3f2f Updated docs 2025-05-22 14:42:28 +03:00
a94f90dd64 Saving logs+data to dir. fixes
All checks were successful
Build and Push Docker Image / build-and-push (release) Successful in 1m20s
2025-05-22 14:27:04 +03:00
808f7112f7 Added status and PnL
All checks were successful
Build and Push Docker Image / build-and-push (release) Successful in 1m13s
2025-05-19 15:45:30 +03:00
13 changed files with 380 additions and 115 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ src/test2.py
tradingLog.log tradingLog.log
generalLog.log generalLog.log
src/.env src/.env
data/

47
README-EN.md Normal file
View File

@ -0,0 +1,47 @@
# [ENG] Side Strategy Bybit Bot
This is a simple semi-automatic trading bot working with Bybit API and written in Python.
The strategy is based on the side moving of the token which crosses user input levels. On the level cross long and short orders are opened.
# Install guide
Bot is installed via **docker compose** file and **docker pull** command.
This "guide" is made for **linux** and **docker**, but you should be able to run this bot on any system with *Python 3*.
### Installing via `docker compose`:
1) Create bot directory
2) Pull image from git:
```sh
docker pull git.frik.su/eugenebee/tradingbot-with-bybitapi:latest
```
3) Create **compose.yml** file:
```yaml
services:
bybit-bot:
image: git.frik.su/eugenebee/tradingbot-with-bybitapi:latest
container_name: bybit-bot
environment:
API_KEY: "bybit-API-key"
API_SECRET: "bybit-secret-API-key"
BOT_TOKEN: "telegram-bot-token"
WHITELIST: "chat-id-1, chat-id-2"
LEVERAGE: "1" # Currently not supported. Set leverage in your Bybit account
TESTNET: "False"
DEMOTRADING: "False"
LOOPSLEEPTIME: "1"
SHOWEXTRADEBUGLOGS: "False"
volumes:
- ./data:/app/data
restart: unless-stopped
```
### After the basic pre-setup:
*It is recommended to use Bybit sub accaunt forn the bot.*
1) Get your **API** keys for the **Bybit** and **Telegram**
2) Enter those in the **compose.yml** variables and your **Telegram id** into the `WHITELIST`
2) At the **Bybit** platform turn on hedge mode foor all tokens you want the bot to trade
3) Read the milk leaves and then make ***4*** circles around the server with the bee hive in your hands
4) Everything is ready for the first start, enjoy!
### To start up the bot enter the next command:
```sh
docker compose up -d
```
*If you need use the `sudo` before the command*
Thank you for reafing, hope everything will work fine!

View File

@ -1,4 +1,47 @@
# Side Strategy Bybit Bot # [RU] Side Strategy Bybit Bot
This is a simple semi-automatic trading bot working with Bybit API and written in Python. Это достаточно простой полуавтоматический торговый бот, работающий с Bybit API, написанный на Python.
The strategy is based on the side moving of the token which crosses user input levels. On the level cross long and short orders are opened. Стратегия основана на боковом движении цены токена и пересечении заданных пользователей уровней. При пересечении уровня открываются long и short позиции.
### For the install and setup guide look at the `setup.md`
## Установка
Бот устанавливается с помощью **docker compose** файла и **docker pull**.
Данный "гайд" расчитан на **linux** и **docker**, однако вы можете развернуть данное приложение/бота на любой системе с *Python 3*.
### Для установки через `docker compose`:
1) Создайте директорию для бота
2) Введите команду:
```sh
docker pull git.frik.su/eugenebee/tradingbot-with-bybitapi:latest
```
3) Создайте **compose.yml** файл с следующим содержимым:
```yaml
services:
bybit-bot:
image: git.frik.su/eugenebee/tradingbot-with-bybitapi:latest
container_name: bybit-bot
environment:
API_KEY: "bybit-API-key"
API_SECRET: "bybit-secret-API-key"
BOT_TOKEN: "telegram-bot-token"
WHITELIST: "chat-id-1, chat-id-2"
LEVERAGE: "1" # Временно не поддерживается, устанавливайте плечо в вашем Bybit аккаунте
TESTNET: "False"
DEMOTRADING: "False"
LOOPSLEEPTIME: "1"
SHOWEXTRADEBUGLOGS: "False"
volumes:
- ./data:/app/data
restart: unless-stopped
```
### После базовой развёртки выполните следующие шаги:
*Рекомендуется использовать суб аккаунт Bybit для бота.*
1) Получите **API** ключи на **Bybit** и **Telegram**. Рекомендуем использовать суб аккаунт Bybit для бота
2) Введите в **compose.yml** свои ключи в соответствующие поля и ваш **Telegram id** в `WHITELIST`
2) На платформе **Bybit** включите режим хеджирования на все пары, которыми планируете торговать
3) Погадайте на молочной гуще и сделайте ***4*** круга с пчелиным ульем вокруг сервера
4) Всё готово к запуску, наслаждайтесь!
### Для запуска бота введите следующую команду:
```sh
docker compose up -d
```
*По надобности используйте `sudo` перед командой*
Спасибо что заглянули, желаем удачной настройки и стабильной работы!

17
compose.yml Normal file
View File

@ -0,0 +1,17 @@
services:
bybit-bot:
image: git.frik.su/eugenebee/tradingbot-with-bybitapi:latest
container_name: bybit-bot
environment:
API_KEY: "bybit-API-key"
API_SECRET: "bybit-secret-API-key"
BOT_TOKEN: "telegram-bot-token"
WHITELIST: "chat-id-1, chat-id-2"
LEVERAGE: "1" # Not supported. Set leverage in bybit account
TESTNET: "False"
DEMOTRADING: "False"
LOOPSLEEPTIME: "1"
SHOWEXTRADEBUGLOGS: "False"
volumes:
- ./data:/app/data
restart: unless-stopped

View File

@ -1,25 +0,0 @@
# Краткий гайд по установке и настройке бота.
Бот устанавливается с помощью **docker compose** файла и **git pull** (предварительно) и последующей лёгкой настройки через файлы.
Данный "гайд" расчитан на **linux** и **docker**, однако вы можете развернуть данное приложение/бота на любой системе с *Python 3*.
### Для установки через `docker compose`:
1) Создайте директорию для бота
2) Введите команду:
```
git clone
```
### После базовой развёртки выполните следующие шаги:
1) Создайте data.json файл и заполните его данным содержимым:
```> {}```
2) Создайте файл `credentials.py` в `src/` и заполните его по аналогии с `cretentialsExample.py`
*Рекомендуется использовать суб аккаунт Bybit для бота*
3) Настройте options.py файл на ваше усмотрение (обратите особо внимание на параметр `testnet`)
4) Погадайте на молочной гуще и сделайте ***4*** круга с пчелиным ульем вокруг сервера
5) Всё готово к запуску, наслаждайтесь!
### Для запуска бота введите следующую команду:
```
docker compose up -d
```
*По надобности используйте `sudo` перед командой*
6) На платформе **Bybit** включите режим хеджирования на все пары, которыми планируете торговать
Спасибо что заглянули, желаем удачной настройки и стабильной работы!

View File

@ -1,8 +1,52 @@
import options import time
from datetime import datetime
import re
from random import randint from random import randint
async def getLevels(amount, highPrice, lowPrice, roundDecimals): startTime = None
def setStartTime():
global startTime
startTime = time.time()
def getPnL(pair):
with open("./data/tradingLog.log", "r") as f:
lines = f.readlines()
logEntries = []
for i in lines:
# Get timestamp + message tuples
t = re.match(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) - (.+)", i)
if t:
timestamp_str, message = t.groups()
timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S,%f")
logEntries.append((timestamp, message.strip()))
strategyStartTime = None
for timestamp, message in logEntries:
if message == (f"Starting strategy with {pair}"):
strategyStartTime = timestamp
if not strategyStartTime:
print(f"No 'Starting strategy' found for pair {pair}")
return 0.0
totalPnL = 0.0
pattern = re.compile(
rf"(Long|Short) order on {re.escape(pair)} level .*? filled with P&L (-?\d+\.?\d*) and qty .*"
)
for timestamp, message in logEntries:
if timestamp >= strategyStartTime:
match = pattern.search(message)
if match:
t = float(match.group(2))
totalPnL += t
return totalPnL
def getLevels(amount, highPrice, lowPrice, roundDecimals):
# Returns array of prices from low to high for each level # Returns array of prices from low to high for each level
levels = [] levels = []
delta = (highPrice - lowPrice)/amount delta = (highPrice - lowPrice)/amount

View File

@ -12,6 +12,34 @@ import arbus
from logger import generalLogger from logger import generalLogger
from logger import tradingLogger from logger import tradingLogger
from logger import debugLogger
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: class tradingData:
@ -32,7 +60,8 @@ class tradingData:
self.orderSize = orderSize self.orderSize = orderSize
self.priceDecimals, self.qtyDecimals, self.minimumQty = self.getFilters(pair) self.priceDecimals, self.qtyDecimals, self.minimumQty = self.getFilters(pair)
self.previousPrice = -1 self.previousPrice = -1
self.counter = 0 self.orderCounter = 0
self.closed = 0
def getBalance(self, pair): def getBalance(self, pair):
@ -57,13 +86,18 @@ class tradingData:
return priceDecimals, qtyDecimals, minimumQty return priceDecimals, qtyDecimals, minimumQty
def close(self): def close(self):
jsonProcessing.deletePair(self.pair) self.closed = 1
t = jsonProcessing.deletePair(self.pair)
generalLogger.info(f"Closing strategy on {self.pair}") generalLogger.info(f"Closing strategy on {self.pair}")
def checkCloseConditions(self, markPrice): def checkCloseConditions(self, markPrice):
# If the price is outside of the (lowBreak; highBreak) interval then stop strategy # If the price is outside of the (lowBreak; highBreak) interval then stop strategy
if self.closed == 0:
if not (self.lowBreak < markPrice < self.highBreak): if not (self.lowBreak < markPrice < self.highBreak):
self.close() self.close()
elif self.closed == 1:
generalLogger.error(f"Called checkCloseConditions with selfClose = 1 on {self.pair}")
self.closed = 2
def checkOrderConditions(self, markPrice): def checkOrderConditions(self, markPrice):
@ -120,12 +154,14 @@ class tradingData:
stopLoss = str(sl), stopLoss = str(sl),
tpslMode = "Full" tpslMode = "Full"
) )
self.orderCounter += 1
orderID = response.get('result').get('orderId') orderID = response.get('result').get('orderId')
generalLogger.info(f"Placed oder on {self.pair} with TP {tp}; SL {sl}") 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}") tradingLogger.info(f"Placed oder on {self.pair} with TP {tp}; SL {sl}")
else: else:
generalLogger.warning(f"Failed to place order on {self.pair}; qty is too small!") 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 return orderID
@ -145,6 +181,8 @@ class tradingData:
def handlePositionInfo(self, message): def handlePositionInfo(self, message):
data = message.get('data') data = message.get('data')
debugLogger.debug(data)
# Usually the 3-order response means SL + market + TP orders were placed. # Usually the 3-order response means SL + market + TP orders were placed.
if len(data) == 3: if len(data) == 3:
orderType = [] orderType = []
@ -181,10 +219,14 @@ class tradingData:
j['long'] = False j['long'] = False
j['longIDs'] = ['-1', '-1', '-1'] j['longIDs'] = ['-1', '-1', '-1']
generalLogger.info(f"Long order on {self.pair} level {j['price']} triggered TP/SL") generalLogger.info(f"Long order on {self.pair} level {j['price']} triggered TP/SL")
tradingLogger.info(f"Long order on {self.pair} level {j['price']} triggered TP/SL")
self.orderCounter -= 1
if orderID in shortIDs: if orderID in shortIDs:
j['short'] = False j['short'] = False
j['shortIDs'] = ['-1', '-1', '-1'] j['shortIDs'] = ['-1', '-1', '-1']
generalLogger.info(f"Short order on {self.pair} level {j['price']} triggered TP/SL") generalLogger.info(f"Short order on {self.pair} level {j['price']} triggered TP/SL")
tradingLogger.info(f"Short order on {self.pair} level {j['price']} triggered TP/SL")
self.orderCounter -= 1
if orderStatus == 'Filled': if orderStatus == 'Filled':
for j in self.levels: for j in self.levels:
longIDs = j['longIDs'] longIDs = j['longIDs']
@ -193,6 +235,7 @@ class tradingData:
generalLogger.info(f"Long order on {self.pair} level {j['price']} filled with P&L {i['closedPnl']} and qty {i['qty']}") 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']}") 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: if orderID in shortIDs:
generalLogger.info(f"Short order on {self.pair} level {j['price']} filled with P&L {i['closedPnl']} and qty {i['qty']}")
tradingLogger.info(f"Short order on {self.pair} level {j['price']} filled with P&L {i['closedPnl']} and qty {i['qty']}") tradingLogger.info(f"Short order on {self.pair} level {j['price']} filled with P&L {i['closedPnl']} and qty {i['qty']}")
@ -216,8 +259,9 @@ async def getClient(apiKey, apiSecret, testnet, demoTrading):
async def strategy(pair: str, params): async def strategy(pair: str, params):
generalLogger.info('Starting strategy with ' + pair) generalLogger.info('Starting strategy with ' + pair)
tradingLogger.info('Starting strategy with ' + pair)
paramsDict = await jsonProcessing.parseParams(params) paramsDict = jsonProcessing.parseParams(params)
client = HTTP(testnet=True) client = HTTP(testnet=True)
@ -233,8 +277,9 @@ async def strategy(pair: str, params):
priceDecimals = int(infoContents.get('priceScale')) 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. # Levels have 3 IDs for both long and short position
levelsRaw = await arbus.getLevels(levelsAmount, highEnd, lowEnd, priceDecimals) # The first is main order (market opening), second is SL and third is TP
levelsRaw = arbus.getLevels(levelsAmount, highEnd, lowEnd, priceDecimals)
levels = [] levels = []
for i in range(levelsAmount): for i in range(levelsAmount):
levels.append({'price':levelsRaw[i], 'long':False, 'longIDs':['-1', '-1', '-1'], 'short':False, 'shortIDs':['-1', '-1', '-1']}) levels.append({'price':levelsRaw[i], 'long':False, 'longIDs':['-1', '-1', '-1'], 'short':False, 'shortIDs':['-1', '-1', '-1']})
@ -273,11 +318,11 @@ async def strategy(pair: str, params):
) )
i = 0 i = 0
t = await jsonProcessing.checkPair(pair) t = jsonProcessing.checkPair(pair)
while t: while t:
t = await jsonProcessing.checkPair(pair) t = jsonProcessing.checkPair(pair)
if t != 1: if t != 1:
generalLogger.info("Closing websockets for {pair}") generalLogger.info(f"Closing websockets for {pair}")
ws.exit() ws.exit()
wsPrivate.exit() wsPrivate.exit()
break break
@ -285,4 +330,5 @@ async def strategy(pair: str, params):
i += 1 i += 1
generalLogger.info(f"Ending strategy with {pair}; Ended on the iteration number {i}") generalLogger.info(f"Ending strategy with {pair}; Ended on the iteration number {i}")
tradingLogger.info(f"Ending strategy with {pair}")
return i return i

View File

@ -8,12 +8,20 @@ from logger import generalLogger
import options import options
jsonPath = 'data/data.json'
def startUp(): def startUp():
filePath = 'data.json' filePath = jsonPath
if not(os.path.exists('data')):
os.mkdir('data')
if not(os.path.exists('data/backup')):
os.mkdir('data/backup')
if os.path.exists(filePath): if os.path.exists(filePath):
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
backupPath = (f'data_backup_{timestamp}.json') backupPath = (f'data/backup/data_backup_{timestamp}.json')
shutil.copy(filePath, backupPath) shutil.copy(filePath, backupPath)
generalLogger.info(f'JSON backup was created: {backupPath}') generalLogger.info(f'JSON backup was created: {backupPath}')
@ -22,15 +30,18 @@ def startUp():
generalLogger.info(f'New {filePath} created with empty JSON.') generalLogger.info(f'New {filePath} created with empty JSON.')
async def parseParams(params): def parseParams(params):
# Returnes dictionary of string params as paramsLines in options # Returnes dictionary of string params as paramsLines in options
paramsList = params.split() paramsList = params.split()
paramsDict = {} paramsDict = {}
if len(paramsList) != len(options.paramsLines):
return -1
for i in range(len(options.paramsLines)): for i in range(len(options.paramsLines)):
paramsDict[options.paramsLines[i]] = paramsList[i] paramsDict[options.paramsLines[i]] = paramsList[i]
return paramsDict return paramsDict
async def toDictPairParams(pair: str, params): def toDictPairParams(pair: str, params):
# Returnes dictionary as pair:internal(params) # Returnes dictionary as pair:internal(params)
paramsList = params.split() paramsList = params.split()
@ -43,32 +54,32 @@ async def toDictPairParams(pair: str, params):
return paramsDict return paramsDict
async def checkPair(pair: str): def checkPair(pair: str):
# Returnes 1 if pair exists and 0 if not # Returnes 1 if pair exists and 0 if not
currentData = {} currentData = {}
try: try:
with open('data.json', 'r') as f: with open(jsonPath, 'r') as f:
currentData = json.load(f) currentData = json.load(f)
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:
generalLogger.info('WARNING: JSON file is empty! Ignore if your installation is fresh.') generalLogger.warning('JSON file is empty!')
if pair in currentData: if pair in currentData:
return 1 return 1
else: else:
return 0 return 0
async def deletePair(pair: str): def deletePair(pair: str):
# Returnes 0 if deleted successfully and -1 if not # Returnes 0 if deleted successfully and -1 if not
currentData = {} currentData = {}
try: try:
with open('data.json', 'r') as f: with open(jsonPath, 'r') as f:
currentData = json.load(f) currentData = json.load(f)
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:
generalLogger.info('WARNING: JSON file is empty! Ignore if your installation is fresh.') generalLogger.warning('JSON file is empty!')
if pair in currentData: if pair in currentData:
del currentData[pair] del currentData[pair]
with open('data.json', 'w', encoding = 'utf-8') as f: with open(jsonPath, 'w', encoding = 'utf-8') as f:
json.dump(currentData, f, ensure_ascii = False, indent = 4) json.dump(currentData, f, ensure_ascii = False, indent = 4)
generalLogger.info(f'Pair {pair} was deleted successfully!') generalLogger.info(f'Pair {pair} was deleted successfully!')
return 0 return 0
@ -76,37 +87,37 @@ async def deletePair(pair: str):
generalLogger.info(f'Pair {pair} was not found in the data file when trying to delete it.') generalLogger.info(f'Pair {pair} was not found in the data file when trying to delete it.')
return -1 return -1
async def savePairParams(pair: str, params): def savePairParams(pair: str, params):
# Saves or updates data in JSON # Saves or updates data in JSON
newData = await toDictPairParams(pair, params) newData = toDictPairParams(pair, params)
if newData == -1: if newData == -1:
return -1 return -1
currentData = {} currentData = {}
try: try:
with open('data.json', 'r') as f: with open(jsonPath, 'r') as f:
currentData = json.load(f) currentData = json.load(f)
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:
generalLogger.info('WARNING: JSON file is empty! Ignore if your installation is fresh.') generalLogger.warning('JSON file is empty!')
if pair in currentData: if pair in currentData:
generalLogger.info(f"Pair {pair} already exists.") generalLogger.info(f"Pair {pair} already exists.")
return -2 return -2
else: else:
with open('data.json', 'w', encoding = 'utf-8') as f: with open(jsonPath, 'w', encoding = 'utf-8') as f:
currentData.update(newData) currentData.update(newData)
json.dump(currentData, f, ensure_ascii = False, indent = 4) json.dump(currentData, f, ensure_ascii = False, indent = 4)
generalLogger.info(f"Pair {pair} was added!") generalLogger.info(f"Pair {pair} was added!")
return 0 return 0
async def loadJson(): def loadJson():
# Returnes the contents of the JSON file as a dictionary # Returnes the contents of the JSON file as a dictionary
data = {} data = {}
try: try:
with open('data.json', 'r') as f: with open(jsonPath, 'r') as f:
data = json.load(f) data = json.load(f)
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:
generalLogger.info('WARNING: JSON file is empty! Ignore if your installation is fresh.') generalLogger.warning('JSON file is empty!')
return data return data

View File

@ -1,11 +1,13 @@
import logging import logging
import sys import sys
import os
import options import options
generalLogPath = "./generalLog.log" generalLogPath = "./data/generalLog.log"
tradingLogPath = "./tradingLog.log" tradingLogPath = "./data/tradingLog.log"
debugLogPath = "./data/debugLog.log"
def setupLogger(name, level, logPath, formatter): def setupLogger(name, level, logPath, formatter):
@ -23,15 +25,22 @@ def setupLogger(name, level, logPath, formatter):
return logger return logger
if not(os.path.exists('data')):
os.mkdir('data')
# Основной лог # Основной лог
generalFormatter = logging.Formatter('%(asctime)s - %(module)s - %(levelname)s - %(message)s') generalFormatter = logging.Formatter('%(asctime)s - %(module)s - %(levelname)s - %(message)s')
generalLogger = setupLogger('general', logging.INFO, generalLogPath, generalFormatter) generalLogger = setupLogger('general', logging.INFO, generalLogPath, generalFormatter)
# Торговый лог (ордера) # Торговый лог (открытие, закрытие ордеров, запуск стратегии)
tradingFormatter = logging.Formatter('%(asctime)s - %(message)s') tradingFormatter = logging.Formatter('%(asctime)s - %(message)s')
tradingLogger = setupLogger('trade', logging.NOTSET, tradingLogPath, tradingFormatter) tradingLogger = setupLogger('trade', logging.NOTSET, tradingLogPath, tradingFormatter)
# Максимально подробный общий лог # Дебаг лог (содержимое ответов от API, всякие мелочи)
debugFormatter = logging.Formatter('%(asctime)s - %(message)s')
debugLogger = setupLogger('debug', logging.DEBUG, debugLogPath, debugFormatter)
# Максимально подробный дебаг лог, вызывает повторы в логировании. Его наличие настраивается
if options.showExtraDebugLogs: if options.showExtraDebugLogs:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
superGeneralLogger = logging.getLogger('superGeneral') superGeneralLogger = logging.getLogger('superGeneral')

View File

@ -1,3 +1,5 @@
import time
from datetime import datetime
import asyncio import asyncio
from aiogram import Bot, Dispatcher from aiogram import Bot, Dispatcher
@ -14,8 +16,10 @@ from aiogram.fsm.storage.memory import MemoryStorage
from logger import generalLogger from logger import generalLogger
from logger import tradingLogger from logger import tradingLogger
from logger import debugLogger
import bybit import bybit
import arbus
import jsonProcessing import jsonProcessing
import credentials import credentials
@ -24,10 +28,11 @@ import strings
import options import options
async def set_commands(): async def setCommands():
commands = [BotCommand(command='start', description='Старт'), commands = [BotCommand(command='start', description='Старт'),
BotCommand(command='help', description='Инструкция'), BotCommand(command='help', description='Инструкция'),
BotCommand(command='info', description='Статус'), BotCommand(command='info', description='Информация о стратегиях'),
BotCommand(command='status', description='Статус'),
BotCommand(command='strategy', description='Запустить стратегию'), BotCommand(command='strategy', description='Запустить стратегию'),
BotCommand(command='stop', description='Остановить стратегию') BotCommand(command='stop', description='Остановить стратегию')
] ]
@ -48,47 +53,54 @@ bot = Bot(
default=DefaultBotProperties(parse_mode=ParseMode.HTML), default=DefaultBotProperties(parse_mode=ParseMode.HTML),
) )
strategy_router = Router() strategyRouter = Router()
stop_router = Router() stopRouter = Router()
@dp.message(Command("start")) @dp.message(Command("start"))
async def commandStart(message: Message) -> None: async def commandStart(message: Message) -> None:
# print(whitelist.chatIDs)
# id = message.from_user.id
# print('Got message from', id, ' with type ', type(id))
await message.answer(strings.startCommand) await message.answer(strings.startCommand)
@dp.message(Command("help"), F.chat.id.in_(whitelist.chatIDs)) @dp.message(Command("help"), F.chat.id.in_(whitelist.chatIDs))
async def commandHelp(message: Message) -> None: async def commandHelp(message: Message) -> None:
await message.answer(strings.helpCommand) await message.answer(strings.helpCommand)
@dp.message(Command("status"), F.chat.id.in_(whitelist.chatIDs))
async def commandStatus(message: Message) -> None:
currentTime = time.time()
timeDiff = round(currentTime - arbus.startTime) # Время работы в секундах
timeDiffH = round(timeDiff/60/60, 3) # Время работы в часах
await message.answer(strings.status + str(timeDiff) + ' секунд' \
+ ' (' + str(timeDiffH) + ' часов)')
@dp.message(Command("info"), F.chat.id.in_(whitelist.chatIDs)) @dp.message(Command("info"), F.chat.id.in_(whitelist.chatIDs))
async def commandInfo(message: Message) -> None: async def commandInfo(message: Message) -> None:
data = await jsonProcessing.loadJson() data = jsonProcessing.loadJson()
msgText = strings.foundData msgText = ''
if data == {}: if data == {}:
msgText = strings.noData msgText = strings.noData
else: else:
msgText = strings.foundData msgText = strings.foundData
for i in data: for i in data:
msgText += (f"<b>{str(i)}</b>: P&L - x%\n") pnl = arbus.getPnL(str(i))
msgText += (f"<b>{str(i)}</b>: P&L - {pnl}%\n")
await message.answer(msgText) await message.answer(msgText)
@strategy_router.message(Command("strategy"), F.chat.id.in_(whitelist.chatIDs)) @strategyRouter.message(Command("strategy"), F.chat.id.in_(whitelist.chatIDs))
async def commandStrategy(message: Message, state: FSMContext): async def commandStrategy(message: Message, state: FSMContext):
await message.answer(strings.strategyCommand + '\n' + strings.askPair) await message.answer(strings.strategyCommand + '\n' + strings.askPair)
await state.set_state(startForm.pair) await state.set_state(startForm.pair)
@strategy_router.message(F.text, startForm.pair) @strategyRouter.message(F.text, startForm.pair)
async def capture_start_pair(message: Message, state: FSMContext): async def captureStartPair(message: Message, state: FSMContext):
await state.update_data(pair=message.text) await state.update_data(pair=message.text)
data = await state.get_data() data = await state.get_data()
t = 0 t = 0
if await jsonProcessing.checkPair(data.get("pair")) == 1: if jsonProcessing.checkPair(data.get("pair")) == 1:
msgText = (f'Стратегия на паре <b>{data.get("pair")}</b> уже запущена.\nПожалуйста остановите стратегию либо введите другую пару.') msgText = strings.strategyAlreadyRunning
t = 1 t = 1
else: else:
msgText = strings.askParams msgText = strings.askParams
@ -99,13 +111,14 @@ async def capture_start_pair(message: Message, state: FSMContext):
else: else:
await state.set_state(startForm.params) await state.set_state(startForm.params)
@strategy_router.message(F.text, startForm.params) @strategyRouter.message(F.text, startForm.params)
async def capture_params(message: Message, state: FSMContext): async def captureParams(message: Message, state: FSMContext):
await state.update_data(params=message.text) await state.update_data(params=message.text)
data = await state.get_data() data = await state.get_data()
t = await jsonProcessing.savePairParams(pair=data.get("pair"), params=data.get("params")) t = jsonProcessing.savePairParams(pair=data.get("pair"), params=data.get("params"))
if t == 0: if t == 0:
params = jsonProcessing.parseParams(params=data.get("params"))
client = await bybit.getClient( client = await bybit.getClient(
credentials.api_key, credentials.api_key,
credentials.api_secret, credentials.api_secret,
@ -114,35 +127,52 @@ async def capture_params(message: Message, state: FSMContext):
) )
if client == -1: if client == -1:
msgText = strings.authFailed msgText = strings.authFailed
await jsonProcessing.deletePair(pair=data.get("pair")) generalLogger.info("Auth failed. Strategy not started")
jsonProcessing.deletePair(pair=data.get("pair"))
else:
orderSize = float(params.get('orderSize'))
minqty = bybit.getStartFilters(client, data.get("pair"))
qtyDecimals = arbus.countDecimals(minqty)
balance = bybit.getStartBalance(client, data.get("pair"))
price = bybit.getPrice(client, data.get("pair"))
qty = arbus.floor(orderSize/price, qtyDecimals)
if qty <= minqty:
generalLogger.info("Qty < minqty. Strategy not started")
msgText = strings.orderSizeLowerThanQty
jsonProcessing.deletePair(pair=data.get("pair"))
elif balance <= orderSize:
generalLogger.info("Balance < order size. Strategy not started")
msgText = strings.notEnoughBalance
jsonProcessing.deletePair(pair=data.get("pair"))
else: else:
try: try:
asyncio.create_task(bybit.strategy(data.get("pair"), data.get("params"))) 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') msgText = (f'Вы запустили стратегию на паре <b>{data.get("pair")}</b> с данными параметрами:\n<b>{data.get("params")}</b>\n')
except: except:
await jsonProcessing.deletePair(pair=data.get("pair")) jsonProcessing.deletePair(pair=data.get("pair"))
msgText = (f'Возникла ошибка в работе стратегии =( Пожалуйста сообщите об этом администратору.') msgText = (strings.strategyError)
elif t == -1: elif t == -1:
msgText = (f'Параметры введены в неверном формате, пожалуйста начните заново.') msgText = strings.wrongFormat
elif t == -2: elif t == -2:
msgText = (f'Стратегия на паре <b>{data.get("pair")}</b> уже запущена.') msgText = (f"Стратегия на паре <b>{data.get("pair")}</b> уже запущена.")
else: else:
msgText = (f'Возникла непредвиденная ошибка. =(') msgText = strings.unexpectedError
await message.answer(msgText) await message.answer(msgText)
await state.clear() await state.clear()
@stop_router.message(Command("stop"), F.chat.id.in_(whitelist.chatIDs)) @stopRouter.message(Command("stop"), F.chat.id.in_(whitelist.chatIDs))
async def commandStop(message: Message, state: FSMContext): async def commandStop(message: Message, state: FSMContext):
await message.answer(strings.stopCommand + '\n' + strings.askPair) await message.answer(strings.stopCommand + '\n' + strings.askPair)
await state.set_state(stopForm.pair) await state.set_state(stopForm.pair)
@stop_router.message(F.text, stopForm.pair) @stopRouter.message(F.text, stopForm.pair)
async def capture_stop_pair(message: Message, state: FSMContext): async def captureStopPair(message: Message, state: FSMContext):
await state.update_data(pair=message.text) await state.update_data(pair=message.text)
data = await state.get_data() data = await state.get_data()
t = await jsonProcessing.deletePair(data.get("pair")) t = jsonProcessing.deletePair(data.get("pair"))
if t == 0: if t == 0:
msgText = strings.stopStrategy msgText = strings.stopStrategy
else: else:
@ -152,20 +182,35 @@ async def capture_stop_pair(message: Message, state: FSMContext):
await state.clear() await state.clear()
async def start_bot(): async def startBot():
await set_commands() await setCommands()
try:
for i in whitelist.chatIDs:
await bot.send_message(chat_id=i, text=strings.startBot)
except Exception as e:
generalLogger.error(e)
async def stopBot():
try:
for i in whitelist.chatIDs:
await bot.send_message(chat_id=i, text=strings.stopBot)
except Exception as e:
generalLogger.error(e)
async def main() -> None: async def main() -> None:
dp.include_router(strategy_router) dp.include_router(strategyRouter)
dp.include_router(stop_router) dp.include_router(stopRouter)
dp.startup.register(start_bot) dp.startup.register(startBot)
dp.shutdown.register(stopBot)
await dp.start_polling(bot) await dp.start_polling(bot)
# Main # Main
if __name__ == "__main__": if __name__ == "__main__":
generalLogger.info("Started bot!") arbus.setStartTime()
tradingLogger.info("Started bot!")
jsonProcessing.startUp() jsonProcessing.startUp()
currentTime = datetime.now().strftime("%H:%M:%S")
generalLogger.info(f"Started bot!")
tradingLogger.info(f"Started bot!")
asyncio.run(main()) asyncio.run(main())

View File

@ -8,6 +8,10 @@ startStrategy = "Стратегия запущена!"
stopStrategy = "Стратегия остановлена!" stopStrategy = "Стратегия остановлена!"
strategyAlreadyRunning = "Стратегия на данной паре уже запущена.\nПожалуйста остановите стратегию либо введите другую пару."
strategyError = "Возникла ошибка в работе стратегии =( Пожалуйста сообщите об этом администратору."
# Bot Statuses # Bot Statuses
@ -15,6 +19,9 @@ startBot = "Бот запущен!"
stopBot = "Бот остановлен!" stopBot = "Бот остановлен!"
status = "Бот работает в течение "
unexpectedError = "Возникла непредвиденная ошибка. =("
# Commands # Commands
@ -30,7 +37,9 @@ helpCommand = (f"При старте стратегии требуется за
"Количество уровней сетки\n" \ "Количество уровней сетки\n" \
"Дельта для тейка\n" \ "Дельта для тейка\n" \
"Дельта для стопа\n" \ "Дельта для стопа\n" \
"Размер позиции на каждом уровне</b>") "Размер позиции на каждом уровне</b>\n" \
"\n"\
"Чтобы остановаить запуск стратегии просто при запросе параметров введите не 8 строк, можно одну любую букву.")
strategyCommand = "Вы собираетесь запустить стратегию." strategyCommand = "Вы собираетесь запустить стратегию."
@ -45,7 +54,13 @@ gotParams = "Параметры заданы!"
pairNotFound = "Стратегия на данную монетную пару не найдена." pairNotFound = "Стратегия на данную монетную пару не найдена."
authFailed = (f'Аутентификация не удалась, пожалуйста сообщите администратору если увидете данное сообщение.') authFailed = (f"Аутентификация не удалась, пожалуйста сообщите администратору если увидете данное сообщение.")
orderSizeLowerThanQty = "Введённая сумма меньше минимального размера ордера на Bybit."
notEnoughBalance = "На балансе недостаточно средств для зпуска стратегии."
wrongFormat = "Параметры введены в неверном формате, пожалуйста начните заново."
# Data status # Data status

16
todo.md
View File

@ -2,6 +2,8 @@
### ToFix ### ToFix
- [x] ID бота (создать нового) - [x] ID бота (создать нового)
- [x] Замена print на логирование - [x] Замена print на логирование
- [x] EN README.md
- [ ] UA README.md
### Новые функции ### Новые функции
- [x] Реализация базы программы - [x] Реализация базы программы
@ -21,18 +23,18 @@
- - - [x] Strategy (Запуск стратегии) - - - [x] Strategy (Запуск стратегии)
- - - [x] Stop (Остановка стратегии) - - - [x] Stop (Остановка стратегии)
- - - [x] Info (Информация о запущеных стратегиях) - - - [x] Info (Информация о запущеных стратегиях)
- - - [x] Проверка на баланс и минимальное количество
- - [x] Обеспечение безопасности и приватности бота (через chat id или пароль) - - [x] Обеспечение безопасности и приватности бота (через chat id или пароль)
- [x] Реализация стратегии - [x] Реализация стратегии
- - [x] Основная функция для запуска стратегии - - [x] Основная функция для запуска стратегии
- - [x] Класс работы по параметрам - - [x] Класс работы по параметрам
- - [x] Реализация уровней - - [x] Реализация уровней
- - [x] Установка позиций - - [x] Установка позиций
- [ ] Рализация развёртывания программы - [x] Рализация развёртывания программы
- - [ ] Написать compose.yml - - [x] Написать compose.yml
- - [ ] Добавить requirements.txt - - [x] Добавить requirements.txt
- - [ ] Сделать подсасывание контейнера с гита - - [x] Сделать подсасывание контейнера с гита
- - [x] Составить список и реализовать получение переменных окружения - - [x] Составить список и реализовать получение переменных окружения
- [ ] QOL - [x] QOL
- - [x] Написать todo.md - - [x] Написать todo.md
- - [ ] Написать README.md - - [x] Написать README.md
- - [ ] Написать setup.md

10
todo2.md Normal file
View File

@ -0,0 +1,10 @@
Версия 1.x.x
### ToFix:
- [ ] Подсчёт P&L (активные ордера)
### Новые функции:
- [ ] Запрос P&L за период
- [ ] Поддержка параллельного пользователя
- [ ] Мультиюзер версия
- [ ] Команда отмены
- [ ] Команда myid