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"
LOOPSLEEPTIME: "1"
SHOWEXTRADEBUGLOGS: "False"
volumes:
- ./data:/app/data
restart: unless-stopped
```
### After the basic pre-setup:

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import arbus
from logger import generalLogger
from logger import tradingLogger
from logger import debugLogger
def getPrice(client, pair):
@ -59,7 +60,8 @@ class tradingData:
self.orderSize = orderSize
self.priceDecimals, self.qtyDecimals, self.minimumQty = self.getFilters(pair)
self.previousPrice = -1
self.counter = 0
self.orderCounter = 0
self.closed = 0
def getBalance(self, pair):
@ -84,13 +86,18 @@ class tradingData:
return priceDecimals, qtyDecimals, minimumQty
def close(self):
jsonProcessing.deletePair(self.pair)
self.closed = 1
t = 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 self.closed == 0:
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):
@ -147,6 +154,7 @@ class tradingData:
stopLoss = str(sl),
tpslMode = "Full"
)
self.orderCounter += 1
orderID = response.get('result').get('orderId')
generalLogger.info(f"Placed oder on {self.pair} with TP {tp}; SL {sl}")
@ -173,6 +181,8 @@ class tradingData:
def handlePositionInfo(self, message):
data = message.get('data')
debugLogger.debug(data)
# Usually the 3-order response means SL + market + TP orders were placed.
if len(data) == 3:
orderType = []
@ -209,10 +219,14 @@ class tradingData:
j['long'] = False
j['longIDs'] = ['-1', '-1', '-1']
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:
j['short'] = False
j['shortIDs'] = ['-1', '-1', '-1']
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':
for j in self.levels:
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']}")
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:
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']}")
@ -244,8 +259,9 @@ async def getClient(apiKey, apiSecret, testnet, demoTrading):
async def strategy(pair: str, params):
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)
@ -302,9 +318,9 @@ async def strategy(pair: str, params):
)
i = 0
t = await jsonProcessing.checkPair(pair)
t = jsonProcessing.checkPair(pair)
while t:
t = await jsonProcessing.checkPair(pair)
t = jsonProcessing.checkPair(pair)
if t != 1:
generalLogger.info(f"Closing websockets for {pair}")
ws.exit()

View File

@ -16,10 +16,12 @@ def startUp():
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):
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)
generalLogger.info(f'JSON backup was created: {backupPath}')
@ -28,15 +30,18 @@ def startUp():
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
paramsList = params.split()
paramsDict = {}
if len(paramsList) != len(options.paramsLines):
return -1
for i in range(len(options.paramsLines)):
paramsDict[options.paramsLines[i]] = paramsList[i]
return paramsDict
async def toDictPairParams(pair: str, params):
def toDictPairParams(pair: str, params):
# Returnes dictionary as pair:internal(params)
paramsList = params.split()
@ -49,7 +54,7 @@ async def toDictPairParams(pair: str, params):
return paramsDict
async def checkPair(pair: str):
def checkPair(pair: str):
# Returnes 1 if pair exists and 0 if not
currentData = {}
try:
@ -63,7 +68,7 @@ async def checkPair(pair: str):
else:
return 0
async def deletePair(pair: str):
def deletePair(pair: str):
# Returnes 0 if deleted successfully and -1 if not
currentData = {}
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.')
return -1
async def savePairParams(pair: str, params):
def savePairParams(pair: str, params):
# Saves or updates data in JSON
newData = await toDictPairParams(pair, params)
newData = toDictPairParams(pair, params)
if newData == -1:
return -1
@ -107,7 +112,7 @@ async def savePairParams(pair: str, params):
return 0
async def loadJson():
def loadJson():
# Returnes the contents of the JSON file as a dictionary
data = {}
try:

View File

@ -7,6 +7,7 @@ import options
generalLogPath = "./data/generalLog.log"
tradingLogPath = "./data/tradingLog.log"
debugLogPath = "./data/debugLog.log"
def setupLogger(name, level, logPath, formatter):
@ -35,6 +36,10 @@ generalLogger = setupLogger('general', logging.INFO, generalLogPath, generalForm
tradingFormatter = logging.Formatter('%(asctime)s - %(message)s')
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:
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 tradingLogger
from logger import debugLogger
import bybit
import arbus
@ -27,10 +28,11 @@ import strings
import options
async def set_commands():
async def setCommands():
commands = [BotCommand(command='start', description='Старт'),
BotCommand(command='help', description='Инструкция'),
BotCommand(command='info', description='Статус'),
BotCommand(command='info', description='Информация о стратегиях'),
BotCommand(command='status', description='Статус'),
BotCommand(command='strategy', description='Запустить стратегию'),
BotCommand(command='stop', description='Остановить стратегию')
]
@ -51,8 +53,8 @@ bot = Bot(
default=DefaultBotProperties(parse_mode=ParseMode.HTML),
)
strategy_router = Router()
stop_router = Router()
strategyRouter = Router()
stopRouter = Router()
@dp.message(Command("start"))
@ -74,7 +76,7 @@ async def commandStatus(message: Message) -> None:
@dp.message(Command("info"), F.chat.id.in_(whitelist.chatIDs))
async def commandInfo(message: Message) -> None:
data = await jsonProcessing.loadJson()
data = jsonProcessing.loadJson()
msgText = ''
if data == {}:
msgText = strings.noData
@ -86,18 +88,18 @@ async def commandInfo(message: Message) -> None:
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):
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):
@strategyRouter.message(F.text, startForm.pair)
async def captureStartPair(message: Message, state: FSMContext):
await state.update_data(pair=message.text)
data = await state.get_data()
t = 0
if await jsonProcessing.checkPair(data.get("pair")) == 1:
if jsonProcessing.checkPair(data.get("pair")) == 1:
msgText = strings.strategyAlreadyRunning
t = 1
else:
@ -109,14 +111,14 @@ async def capture_start_pair(message: Message, state: FSMContext):
else:
await state.set_state(startForm.params)
@strategy_router.message(F.text, startForm.params)
async def capture_params(message: Message, state: FSMContext):
@strategyRouter.message(F.text, startForm.params)
async def captureParams(message: Message, state: FSMContext):
await state.update_data(params=message.text)
data = await state.get_data()
t = await jsonProcessing.savePairParams(pair=data.get("pair"), params=data.get("params"))
params = await jsonProcessing.parseParams(params=data.get("params"))
t = jsonProcessing.savePairParams(pair=data.get("pair"), params=data.get("params"))
if t == 0:
params = jsonProcessing.parseParams(params=data.get("params"))
client = await bybit.getClient(
credentials.api_key,
credentials.api_secret,
@ -126,7 +128,7 @@ async def capture_params(message: Message, state: FSMContext):
if client == -1:
msgText = strings.authFailed
generalLogger.info("Auth failed. Strategy not started")
await jsonProcessing.deletePair(pair=data.get("pair"))
jsonProcessing.deletePair(pair=data.get("pair"))
else:
orderSize = float(params.get('orderSize'))
minqty = bybit.getStartFilters(client, data.get("pair"))
@ -138,17 +140,17 @@ async def capture_params(message: Message, state: FSMContext):
if qty <= minqty:
generalLogger.info("Qty < minqty. Strategy not started")
msgText = strings.orderSizeLowerThanQty
await jsonProcessing.deletePair(pair=data.get("pair"))
jsonProcessing.deletePair(pair=data.get("pair"))
elif balance <= orderSize:
generalLogger.info("Balance < order size. Strategy not started")
msgText = strings.notEnoughBalance
await jsonProcessing.deletePair(pair=data.get("pair"))
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"))
jsonProcessing.deletePair(pair=data.get("pair"))
msgText = (strings.strategyError)
elif t == -1:
msgText = strings.wrongFormat
@ -160,17 +162,17 @@ async def capture_params(message: Message, state: FSMContext):
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):
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):
@stopRouter.message(F.text, stopForm.pair)
async def captureStopPair(message: Message, state: FSMContext):
await state.update_data(pair=message.text)
data = await state.get_data()
t = await jsonProcessing.deletePair(data.get("pair"))
t = jsonProcessing.deletePair(data.get("pair"))
if t == 0:
msgText = strings.stopStrategy
else:
@ -180,13 +182,26 @@ async def capture_stop_pair(message: Message, state: FSMContext):
await state.clear()
async def start_bot():
await set_commands()
async def startBot():
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:
dp.include_router(strategy_router)
dp.include_router(stop_router)
dp.startup.register(start_bot)
dp.include_router(strategyRouter)
dp.include_router(stopRouter)
dp.startup.register(startBot)
dp.shutdown.register(stopBot)
await dp.start_polling(bot)

View File

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

10
todo2.md Normal file
View File

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