Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f27df7b1b8 | |||
| 7fbd2887b7 | |||
| 262cef3f2f | |||
| a94f90dd64 | |||
| 808f7112f7 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
47
README-EN.md
Normal 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!
|
||||||
51
README.md
51
README.md
@ -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
17
compose.yml
Normal 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
|
||||||
25
setup.md
25
setup.md
@ -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** включите режим хеджирования на все пары, которыми планируете торговать
|
|
||||||
|
|
||||||
Спасибо что заглянули, желаем удачной настройки и стабильной работы!
|
|
||||||
48
src/arbus.py
48
src/arbus.py
@ -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
|
||||||
|
|||||||
41
src/bybit.py
41
src/bybit.py
@ -14,6 +14,33 @@ from logger import generalLogger
|
|||||||
from logger import tradingLogger
|
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:
|
class tradingData:
|
||||||
def __init__(self, pair, levels, highBreak, lowBreak, takeDelta, stopDelta, orderSize):
|
def __init__(self, pair, levels, highBreak, lowBreak, takeDelta, stopDelta, orderSize):
|
||||||
self.client = HTTP(
|
self.client = HTTP(
|
||||||
@ -32,7 +59,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
def getBalance(self, pair):
|
def getBalance(self, pair):
|
||||||
@ -120,12 +147,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
|
||||||
|
|
||||||
|
|
||||||
@ -181,10 +210,12 @@ 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")
|
||||||
|
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")
|
||||||
|
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']
|
||||||
@ -233,8 +264,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']})
|
||||||
@ -277,7 +309,7 @@ async def strategy(pair: str, params):
|
|||||||
while t:
|
while t:
|
||||||
t = await jsonProcessing.checkPair(pair)
|
t = await 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 +317,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
|
||||||
|
|||||||
@ -8,12 +8,18 @@ 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 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/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}')
|
||||||
|
|
||||||
@ -26,6 +32,9 @@ async 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
|
||||||
@ -47,10 +56,10 @@ async 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
|
||||||
@ -61,14 +70,14 @@ async 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
|
||||||
@ -85,16 +94,16 @@ async def savePairParams(pair: str, params):
|
|||||||
|
|
||||||
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!")
|
||||||
@ -105,8 +114,8 @@ async 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
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
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"
|
||||||
|
|
||||||
|
|
||||||
def setupLogger(name, level, logPath, formatter):
|
def setupLogger(name, level, logPath, formatter):
|
||||||
@ -23,15 +24,18 @@ 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)
|
||||||
|
|
||||||
# Максимально подробный общий лог
|
# Максимально подробный дебаг лог, вызывает повторы в логировании. Его наличие настраивается
|
||||||
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')
|
||||||
|
|||||||
110
src/main.py
110
src/main.py
@ -1,3 +1,5 @@
|
|||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher
|
from aiogram import Bot, Dispatcher
|
||||||
@ -16,6 +18,7 @@ from logger import generalLogger
|
|||||||
from logger import tradingLogger
|
from logger import tradingLogger
|
||||||
|
|
||||||
import bybit
|
import bybit
|
||||||
|
import arbus
|
||||||
import jsonProcessing
|
import jsonProcessing
|
||||||
|
|
||||||
import credentials
|
import credentials
|
||||||
@ -24,10 +27,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 +52,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 = await 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 await 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 +110,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 = await jsonProcessing.savePairParams(pair=data.get("pair"), params=data.get("params"))
|
||||||
if t == 0:
|
if t == 0:
|
||||||
|
params = await 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,31 +126,48 @@ async def capture_params(message: Message, state: FSMContext):
|
|||||||
)
|
)
|
||||||
if client == -1:
|
if client == -1:
|
||||||
msgText = strings.authFailed
|
msgText = strings.authFailed
|
||||||
|
generalLogger.info("Auth failed. Strategy not started")
|
||||||
await jsonProcessing.deletePair(pair=data.get("pair"))
|
await jsonProcessing.deletePair(pair=data.get("pair"))
|
||||||
else:
|
else:
|
||||||
try:
|
orderSize = float(params.get('orderSize'))
|
||||||
asyncio.create_task(bybit.strategy(data.get("pair"), data.get("params")))
|
minqty = bybit.getStartFilters(client, data.get("pair"))
|
||||||
msgText = (f'Вы запустили стратегию на паре <b>{data.get("pair")}</b> с данными параметрами:\n<b>{data.get("params")}</b>\n')
|
qtyDecimals = arbus.countDecimals(minqty)
|
||||||
except:
|
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
|
||||||
await jsonProcessing.deletePair(pair=data.get("pair"))
|
await jsonProcessing.deletePair(pair=data.get("pair"))
|
||||||
msgText = (f'Возникла ошибка в работе стратегии =( Пожалуйста сообщите об этом администратору.')
|
elif balance <= orderSize:
|
||||||
|
generalLogger.info("Balance < order size. Strategy not started")
|
||||||
|
msgText = strings.notEnoughBalance
|
||||||
|
await jsonProcessing.deletePair(pair=data.get("pair"))
|
||||||
|
else:
|
||||||
|
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 = (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()
|
||||||
|
|
||||||
@ -152,20 +181,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())
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
@ -45,7 +52,13 @@ gotParams = "Параметры заданы!"
|
|||||||
|
|
||||||
pairNotFound = "Стратегия на данную монетную пару не найдена."
|
pairNotFound = "Стратегия на данную монетную пару не найдена."
|
||||||
|
|
||||||
authFailed = (f'Аутентификация не удалась, пожалуйста сообщите администратору если увидете данное сообщение.')
|
authFailed = (f"Аутентификация не удалась, пожалуйста сообщите администратору если увидете данное сообщение.")
|
||||||
|
|
||||||
|
orderSizeLowerThanQty = "Введённая сумма меньше минимального размера ордера на Bybit."
|
||||||
|
|
||||||
|
notEnoughBalance = "На балансе недостаточно средств для зпуска стратегии."
|
||||||
|
|
||||||
|
wrongFormat = "Параметры введены в неверном формате, пожалуйста начните заново."
|
||||||
|
|
||||||
|
|
||||||
# Data status
|
# Data status
|
||||||
|
|||||||
16
todo.md
16
todo.md
@ -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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user