diff --git a/.gitea/workflows/docker-build-push.yaml b/.gitea/workflows/docker-build-push.yaml
new file mode 100644
index 0000000..4e6fc26
--- /dev/null
+++ b/.gitea/workflows/docker-build-push.yaml
@@ -0,0 +1,48 @@
+name: Build and Push Docker Image
+
+on:
+ release:
+ types: [published]
+
+env:
+ REGISTRY: git.frik.su
+ IMAGE_NAME: ${{ gitea.repository }}
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Install Docker
+ run: curl -fsSL https://get.docker.com | sh
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Login to registry
+ uses: docker/login-action@v2
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+
+ - name: Extract Docker tags from release
+ id: meta
+ uses: docker/metadata-action@v4
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=ref,event=tag
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=semver,pattern={{major}}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v4
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/.gitignore b/.gitignore
index e4c5ae6..00c1212 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,11 @@
.venv
src/__pycache__
src/test.py
-src/credentials.py
exampleData.py
data.json
+data_backup_*.json
+botlog.log
+src/test2.py
+tradingLog.log
+generalLog.log
+src/.env
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0656334
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+# 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.
+### For the install and setup guide look at the `setup.md`
\ No newline at end of file
diff --git a/setup.md b/setup.md
new file mode 100644
index 0000000..6066042
--- /dev/null
+++ b/setup.md
@@ -0,0 +1,25 @@
+# Краткий гайд по установке и настройке бота.
+Бот устанавливается с помощью **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** включите режим хеджирования на все пары, которыми планируете торговать
+
+Спасибо что заглянули, желаем удачной настройки и стабильной работы!
diff --git a/src/arbus.py b/src/arbus.py
index e45247e..09aa2a2 100644
--- a/src/arbus.py
+++ b/src/arbus.py
@@ -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)
diff --git a/src/bybit.py b/src/bybit.py
index 8a86b7d..2a67ba9 100644
--- a/src/bybit.py
+++ b/src/bybit.py
@@ -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
diff --git a/src/credentials.py b/src/credentials.py
new file mode 100644
index 0000000..b5f94ac
--- /dev/null
+++ b/src/credentials.py
@@ -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='')
diff --git a/src/credentialsExample.py b/src/credentialsExample.py
deleted file mode 100644
index 355f66d..0000000
--- a/src/credentialsExample.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Bybit
-
-api_key = "..."
-api_secret = "..."
-
-
-# Telegram
-
-bot_token = "..."
diff --git a/src/jsonProcessing.py b/src/jsonProcessing.py
index 1dc848c..78e4fd7 100644
--- a/src/jsonProcessing.py
+++ b/src/jsonProcessing.py
@@ -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
diff --git a/src/logger.py b/src/logger.py
new file mode 100644
index 0000000..725be71
--- /dev/null
+++ b/src/logger.py
@@ -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')
diff --git a/src/main.py b/src/main.py
index 81b129d..0838639 100644
--- a/src/main.py
+++ b/src/main.py
@@ -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"{str(i)}: 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'Стратегия на паре {data.get("pair")} уже запущена.\nПожалуйста остановите стратегию либо введите другую пару.')
+ msgText = (f'Стратегия на паре {data.get("pair")} уже запущена.\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'Вы запустили стратегию на паре {data.get("pair")} с данными параметрами:\n{data.get("params")}\n')
+ try:
+ asyncio.create_task(bybit.strategy(data.get("pair"), data.get("params")))
+ msgText = (f'Вы запустили стратегию на паре {data.get("pair")} с данными параметрами:\n{data.get("params")}\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'Стратегия на паре {data.get("pair")} уже запущена.')
+ msgText = (f'Стратегия на паре {data.get("pair")} уже запущена.')
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())
diff --git a/src/options.py b/src/options.py
index 9c4e7bf..572fc98 100644
--- a/src/options.py
+++ b/src/options.py
@@ -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',
diff --git a/src/strings.py b/src/strings.py
index ea6bd20..a68f74a 100644
--- a/src/strings.py
+++ b/src/strings.py
@@ -18,7 +18,7 @@ stopBot = "Бот остановлен!"
# Commands
-startCommand = "Привет! Это приватный бот для полуавтоматической торговли криптовалютой. Хороших позиций!"
+startCommand = "Привет! Это приватный бот для полуавтоматической торговли криптовалютой. В данный момент он работает по вайтлисту. Хороших позиций!"
stopCommand = "Вы собираетесь остановить стратегию."
@@ -44,3 +44,12 @@ askParams = "Введите параметры:"
gotParams = "Параметры заданы!"
pairNotFound = "Стратегия на данную монетную пару не найдена."
+
+authFailed = (f'Аутентификация не удалась, пожалуйста сообщите администратору если увидете данное сообщение.')
+
+
+# Data status
+
+noData = "Нет запущенных стратегий!"
+
+foundData = "В данный момент стратегия запущена на следующих монетных парах:\n"
diff --git a/src/whitelist.py b/src/whitelist.py
new file mode 100644
index 0000000..17c2ec9
--- /dev/null
+++ b/src/whitelist.py
@@ -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)]
diff --git a/todo.md b/todo.md
new file mode 100644
index 0000000..fad1636
--- /dev/null
+++ b/todo.md
@@ -0,0 +1,38 @@
+Версия 0.x.x
+### ToFix
+ - [x] ID бота (создать нового)
+ - [x] Замена print на логирование
+
+### Новые функции
+ - [x] Реализация базы программы
+ - - [x] Общее взаимодействие файлов и функций
+ - - [x] Обработка ошибок
+ - [x] Реализация JSON хранилища
+ - - [x] Функция записи данных в JSON
+ - - [x] Функция удаления данных из JSON
+ - - [x] Функция получения данных из JSON
+ - - [x] Функция проверки наличия данных в JSON
+ - - [x] Создание JSON файла при первом запуске
+ - - [x] Сохранение предыдущего JSON файла в архив и создание нового при перезапуске
+ - [x] Реализация взаимодействия с ботом через телеграмм
+ - - [x] Реализация команд
+ - - - [x] Start (общая информация про бота)
+ - - - [x] Help (шпаргалка по другим командам и порядку заполнения параметров)
+ - - - [x] Strategy (Запуск стратегии)
+ - - - [x] Stop (Остановка стратегии)
+ - - - [x] Info (Информация о запущеных стратегиях)
+ - - [x] Обеспечение безопасности и приватности бота (через chat id или пароль)
+ - [x] Реализация стратегии
+ - - [x] Основная функция для запуска стратегии
+ - - [x] Класс работы по параметрам
+ - - [x] Реализация уровней
+ - - [x] Установка позиций
+ - [ ] Рализация развёртывания программы
+ - - [ ] Написать compose.yml
+ - - [ ] Добавить requirements.txt
+ - - [ ] Сделать подсасывание контейнера с гита
+ - - [x] Составить список и реализовать получение переменных окружения
+ - [ ] QOL
+ - - [x] Написать todo.md
+ - - [ ] Написать README.md
+ - - [ ] Написать setup.md