5 Commits
0.0.5 ... 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
10 changed files with 104 additions and 45 deletions

View File

@ -27,6 +27,8 @@ services:
DEMOTRADING: "False" DEMOTRADING: "False"
LOOPSLEEPTIME: "1" LOOPSLEEPTIME: "1"
SHOWEXTRADEBUGLOGS: "False" SHOWEXTRADEBUGLOGS: "False"
volumes:
- ./data:/app/data
restart: unless-stopped restart: unless-stopped
``` ```
### After the basic pre-setup: ### After the basic pre-setup:

View File

@ -27,6 +27,8 @@ services:
DEMOTRADING: "False" DEMOTRADING: "False"
LOOPSLEEPTIME: "1" LOOPSLEEPTIME: "1"
SHOWEXTRADEBUGLOGS: "False" SHOWEXTRADEBUGLOGS: "False"
volumes:
- ./data:/app/data
restart: unless-stopped restart: unless-stopped
``` ```
### После базовой развёртки выполните следующие шаги: ### После базовой развёртки выполните следующие шаги:

View File

@ -12,4 +12,6 @@ services:
DEMOTRADING: "False" DEMOTRADING: "False"
LOOPSLEEPTIME: "1" LOOPSLEEPTIME: "1"
SHOWEXTRADEBUGLOGS: "False" SHOWEXTRADEBUGLOGS: "False"
volumes:
- ./data:/app/data
restart: unless-stopped restart: unless-stopped

View File

@ -11,7 +11,7 @@ def setStartTime():
startTime = time.time() startTime = time.time()
def getPnL(pair): def getPnL(pair):
with open("tradingLog.log", "r") as f: with open("./data/tradingLog.log", "r") as f:
lines = f.readlines() lines = f.readlines()
logEntries = [] logEntries = []
@ -25,7 +25,7 @@ def getPnL(pair):
strategyStartTime = None strategyStartTime = None
for timestamp, message in logEntries: for timestamp, message in logEntries:
if message == f"Starting strategy with {pair}": if message == (f"Starting strategy with {pair}"):
strategyStartTime = timestamp strategyStartTime = timestamp
if not strategyStartTime: if not strategyStartTime:

View File

@ -12,6 +12,7 @@ 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): def getPrice(client, pair):
@ -59,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):
@ -84,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 not (self.lowBreak < markPrice < self.highBreak): if self.closed == 0:
self.close() if not (self.lowBreak < markPrice < self.highBreak):
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):
@ -147,6 +154,7 @@ 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}")
@ -172,7 +180,9 @@ 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 = []
@ -209,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']
@ -221,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']}")
@ -244,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)
@ -302,9 +318,9 @@ 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(f"Closing websockets for {pair}") generalLogger.info(f"Closing websockets for {pair}")
ws.exit() ws.exit()

View File

@ -16,10 +16,12 @@ def startUp():
if not(os.path.exists('data')): if not(os.path.exists('data')):
os.mkdir('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/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}')
@ -28,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()
@ -49,7 +54,7 @@ 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:
@ -63,7 +68,7 @@ async def checkPair(pair: str):
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:
@ -82,9 +87,9 @@ 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
@ -107,7 +112,7 @@ async def savePairParams(pair: str, params):
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:

View File

@ -7,6 +7,7 @@ import options
generalLogPath = "./data/generalLog.log" generalLogPath = "./data/generalLog.log"
tradingLogPath = "./data/tradingLog.log" tradingLogPath = "./data/tradingLog.log"
debugLogPath = "./data/debugLog.log"
def setupLogger(name, level, logPath, formatter): def setupLogger(name, level, logPath, formatter):
@ -35,6 +36,10 @@ generalLogger = setupLogger('general', logging.INFO, generalLogPath, generalForm
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, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

View File

@ -16,6 +16,7 @@ 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 arbus
@ -27,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='Остановить стратегию')
] ]
@ -51,8 +53,8 @@ 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"))
@ -74,7 +76,7 @@ async def commandStatus(message: Message) -> None:
@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 = '' msgText = ''
if data == {}: if data == {}:
msgText = strings.noData msgText = strings.noData
@ -86,18 +88,18 @@ async def commandInfo(message: Message) -> None:
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 = strings.strategyAlreadyRunning msgText = strings.strategyAlreadyRunning
t = 1 t = 1
else: else:
@ -109,14 +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"))
params = await jsonProcessing.parseParams(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,
@ -126,7 +128,7 @@ 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") generalLogger.info("Auth failed. Strategy not started")
await jsonProcessing.deletePair(pair=data.get("pair")) jsonProcessing.deletePair(pair=data.get("pair"))
else: else:
orderSize = float(params.get('orderSize')) orderSize = float(params.get('orderSize'))
minqty = bybit.getStartFilters(client, data.get("pair")) minqty = bybit.getStartFilters(client, data.get("pair"))
@ -138,17 +140,17 @@ async def capture_params(message: Message, state: FSMContext):
if qty <= minqty: if qty <= minqty:
generalLogger.info("Qty < minqty. Strategy not started") generalLogger.info("Qty < minqty. Strategy not started")
msgText = strings.orderSizeLowerThanQty msgText = strings.orderSizeLowerThanQty
await jsonProcessing.deletePair(pair=data.get("pair")) jsonProcessing.deletePair(pair=data.get("pair"))
elif balance <= orderSize: elif balance <= orderSize:
generalLogger.info("Balance < order size. Strategy not started") generalLogger.info("Balance < order size. Strategy not started")
msgText = strings.notEnoughBalance msgText = strings.notEnoughBalance
await jsonProcessing.deletePair(pair=data.get("pair")) 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 = (strings.strategyError) msgText = (strings.strategyError)
elif t == -1: elif t == -1:
msgText = strings.wrongFormat msgText = strings.wrongFormat
@ -160,17 +162,17 @@ async def capture_params(message: Message, state: FSMContext):
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:
@ -180,13 +182,26 @@ 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)

View File

@ -37,7 +37,9 @@ helpCommand = (f"При старте стратегии требуется за
"Количество уровней сетки\n" \ "Количество уровней сетки\n" \
"Дельта для тейка\n" \ "Дельта для тейка\n" \
"Дельта для стопа\n" \ "Дельта для стопа\n" \
"Размер позиции на каждом уровне</b>") "Размер позиции на каждом уровне</b>\n" \
"\n"\
"Чтобы остановаить запуск стратегии просто при запросе параметров введите не 8 строк, можно одну любую букву.")
strategyCommand = "Вы собираетесь запустить стратегию." strategyCommand = "Вы собираетесь запустить стратегию."

10
todo2.md Normal file
View File

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