diff --git a/alembic.ini b/alembic.ini index 5d92823..4af3967 100644 --- a/alembic.ini +++ b/alembic.ini @@ -84,7 +84,7 @@ path_separator = os # database URL. This is consumed by the user-maintained env.py script only. # other means of configuring database URLs may be customized within the env.py # file. -sqlalchemy.url = sqlite+aiosqlite:///./database/dbs/stcs.db +sqlalchemy.url = sqlite+aiosqlite:///./database/stcs.db [post_write_hooks] diff --git a/alembic/versions/f6e7eb3f25c0_initial.py b/alembic/versions/f6e7eb3f25c0_initial.py new file mode 100644 index 0000000..da929ca --- /dev/null +++ b/alembic/versions/f6e7eb3f25c0_initial.py @@ -0,0 +1,32 @@ +"""initial + +Revision ID: f6e7eb3f25c0 +Revises: +Create Date: 2025-11-12 22:53:02.189445 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f6e7eb3f25c0' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/app/bybit/get_functions/get_positions.py b/app/bybit/get_functions/get_positions.py index 1f6606a..986f3a7 100644 --- a/app/bybit/get_functions/get_positions.py +++ b/app/bybit/get_functions/get_positions.py @@ -38,7 +38,7 @@ async def get_active_positions(tg_id: int) -> list | None: return None -async def get_active_positions_by_symbol(tg_id: int, symbol: str) -> dict | None: +async def get_active_positions_by_symbol(tg_id: int, symbol: str): """ Get active positions for a user by symbol """ @@ -62,8 +62,12 @@ async def get_active_positions_by_symbol(tg_id: int, symbol: str) -> dict | None ) return None except Exception as e: - logger.error("Error getting active positions for user %s: %s", tg_id, e) - return None + errors = str(e) + if errors.startswith("Permission denied, please check your API key permissions"): + return "Invalid API key permissions" + else: + logger.error("Error getting active positions for user %s: %s", tg_id, e) + return None async def get_active_orders(tg_id: int) -> list | None: diff --git a/app/bybit/open_positions.py b/app/bybit/open_positions.py index 680a76f..00d3fc8 100644 --- a/app/bybit/open_positions.py +++ b/app/bybit/open_positions.py @@ -109,6 +109,7 @@ async def start_trading_cycle( "The number of contracts exceeds maximum limit allowed", "The number of contracts exceeds minimum limit allowed", "Order placement failed as your position may exceed the max", + "Permission denied, please check your API key permissions" } else None ) @@ -232,9 +233,6 @@ async def trading_cycle( ) current_step += 1 - if max_bets_in_series < current_step: - return "Max bets in series" - await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) await set_leverage( tg_id=tg_id, @@ -371,6 +369,7 @@ async def open_positions( "The number of contracts exceeds maximum limit allowed": "The number of contracts exceeds maximum limit allowed", "The number of contracts exceeds minimum limit allowed": "The number of contracts exceeds minimum limit allowed", "Order placement failed as your position may exceed the max": "Order placement failed as your position may exceed the max", + "Permission denied, please check your API key permissions": "Permission denied, please check your API key permissions" } for key, msg in known_errors.items(): if key in error_text: diff --git a/app/bybit/profile_bybit.py b/app/bybit/profile_bybit.py index b92f671..ac4f9f1 100644 --- a/app/bybit/profile_bybit.py +++ b/app/bybit/profile_bybit.py @@ -37,7 +37,7 @@ async def user_profile_bybit(tg_id: int, message: Message, state: FSMContext) -> ) else: await message.answer( - text="Ошибка при подключении к платформе. Проверьте ключи и повторите попытку.", + text="Ошибка при подключении к платформе. Проверьте корректность и разрешения API ключа и добавьте повторно.", reply_markup=kbi.connect_the_platform, ) logger.error("Error processing user profile for user %s", tg_id) diff --git a/app/bybit/telegram_message_handler.py b/app/bybit/telegram_message_handler.py index f2a3407..e6251df 100644 --- a/app/bybit/telegram_message_handler.py +++ b/app/bybit/telegram_message_handler.py @@ -1,10 +1,9 @@ import logging.config import math -# import json +import json import app.telegram.keyboards.inline as kbi import database.request as rq from app.bybit.get_functions.get_instruments_info import get_instruments_info -from app.bybit.get_functions.get_positions import get_active_positions_by_symbol from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG from app.bybit.open_positions import trading_cycle, trading_cycle_profit from app.bybit.set_functions.set_tp_sl import set_tp_sl_for_position @@ -123,7 +122,6 @@ class TelegramMessageHandler: current_step = user_deals_data.current_step order_quantity = user_deals_data.order_quantity pnl_series = user_deals_data.pnl_series - margin_type = user_deals_data.margin_type take_profit_percent = user_deals_data.take_profit_percent stop_loss_percent = user_deals_data.stop_loss_percent leverage = safe_float(user_deals_data.leverage) @@ -167,24 +165,14 @@ class TelegramMessageHandler: if commission_place == "Commission_for_tp": total_commission = safe_float(total_fee) / qty_formatted - if margin_type == "ISOLATED_MARGIN": - if side == "Buy": - take_profit_price = safe_float(exec_price) * ( - 1 + take_profit_percent / 100) + total_commission - stop_loss_price = None - else: - take_profit_price = safe_float(exec_price) * ( - 1 - take_profit_percent / 100) - total_commission - stop_loss_price = None + if side == "Buy": + take_profit_price = safe_float(exec_price) * ( + 1 + take_profit_percent / 100) + total_commission + stop_loss_price = safe_float(exec_price) * (1 - stop_loss_percent / 100) else: - if side == "Buy": - take_profit_price = safe_float(exec_price) * ( - 1 + take_profit_percent / 100) + total_commission - stop_loss_price = safe_float(exec_price) * (1 - stop_loss_percent / 100) - else: - take_profit_price = safe_float(exec_price) * ( - 1 - take_profit_percent / 100) - total_commission - stop_loss_price = safe_float(exec_price) * (1 + stop_loss_percent / 100) + take_profit_price = safe_float(exec_price) * ( + 1 - take_profit_percent / 100) - total_commission + stop_loss_price = safe_float(exec_price) * (1 + stop_loss_percent / 100) ress = await set_tp_sl_for_position(tg_id=tg_id, symbol=symbol, @@ -193,27 +181,11 @@ class TelegramMessageHandler: position_idx=0) if ress or ress == "not modified": take_profit_truncated = await truncate_float(take_profit_price, 6) + stop_loss_truncated = await truncate_float(stop_loss_price, 6) text += (f"Движение: {side_rus}\n" f"Тейк-профит: {take_profit_truncated}\n" + f"Стоп-лосс: {stop_loss_truncated}\n" ) - if stop_loss_price is not None: - stop_loss_truncated = await truncate_float(stop_loss_price, 6) - else: - stop_loss_truncated = None - - if stop_loss_truncated is not None: - text += f"Стоп-лосс: {stop_loss_truncated}\n" - else: - deals = await get_active_positions_by_symbol( - tg_id=tg_id, symbol=symbol - ) - position = next((d for d in deals if d.get("symbol") == symbol), None) - - if position: - liq_price = position.get("liqPrice", 0) - text += f"Цена ликвидации: {liq_price}\n" - - else: text += (f"Движение: {side_rus}\n" "Не удалось установить ТП и СЛ\n") @@ -258,7 +230,6 @@ class TelegramMessageHandler: pass else: errors = { - "Max bets in series": "❗️ Максимальное количество сделок в серии достигнуто", "Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения", "ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли", "InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.", @@ -285,49 +256,105 @@ class TelegramMessageHandler: ) elif stop_order_type == "StopLoss" or exec_type == "BustTrade": - open_order_text = "\n❗️ Открываю новую сделку с увеличенной ставкой.\n" - await self.telegram_bot.send_message( - chat_id=tg_id, text=open_order_text - ) + current_step = user_deals_data.current_step + max_bets_in_series = user_deals_data.max_bets_in_series + current_step += 1 - if side == "Buy": - r_side = "Sell" - else: - r_side = "Buy" - - res = await trading_cycle( - tg_id=tg_id, symbol=symbol, side=r_side - ) - - if res == "OK": - pass - else: - errors = { - "Max bets in series": "❗️ Максимальное количество сделок в серии достигнуто", - "Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения", - "ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли", - "InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.", - "The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки", - "Order placement failed as your position may exceed the max": "❗️ Превышен максимальный лимит ставки с текущим плечом", - } - error_text = errors.get( - res, "❗️ Не удалось открыть новую сделку" - ) - await rq.set_auto_trading( - tg_id=tg_id, symbol=symbol, auto_trading=False + if max_bets_in_series < current_step: + text_series = ("\n❗️ Максимальное количество сделок в серии достигнуто.\n" + "📈 Начинаю новую серию с базовой ставки\n") + await self.telegram_bot.send_message( + chat_id=tg_id, text=text_series ) + if side == "Buy": + r_side = "Sell" + else: + r_side = "Buy" + await rq.set_last_side_by_symbol( + tg_id=tg_id, symbol=symbol, last_side=r_side) await rq.set_total_fee_user_auto_trading( tg_id=tg_id, symbol=symbol, total_fee=0 ) await rq.set_fee_user_auto_trading( tg_id=tg_id, symbol=symbol, fee=0 ) - await self.telegram_bot.send_message( - chat_id=tg_id, - text=error_text, - reply_markup=kbi.profile_bybit, + await rq.set_pnl_series_by_symbol(tg_id=tg_id, symbol=symbol, pnl_series=0) + + res = await trading_cycle_profit( + tg_id=tg_id, symbol=symbol, side=r_side ) + if res == "OK": + pass + else: + errors = { + "Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения", + "ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли", + "InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.", + "The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки", + "Order placement failed as your position may exceed the max": "❗️ Превышен максимальный лимит ставки", + } + error_text = errors.get( + res, "❗️ Не удалось открыть новую сделку" + ) + await rq.set_auto_trading( + tg_id=tg_id, symbol=symbol, auto_trading=False + ) + + await rq.set_total_fee_user_auto_trading( + tg_id=tg_id, symbol=symbol, total_fee=0 + ) + await rq.set_fee_user_auto_trading( + tg_id=tg_id, symbol=symbol, fee=0 + ) + await self.telegram_bot.send_message( + chat_id=tg_id, + text=error_text, + reply_markup=kbi.profile_bybit, + ) + else: + open_order_text = "\n❗️ Открываю новую сделку с увеличенной ставкой.\n" + await self.telegram_bot.send_message( + chat_id=tg_id, text=open_order_text + ) + + if side == "Buy": + r_side = "Sell" + else: + r_side = "Buy" + + res = await trading_cycle( + tg_id=tg_id, symbol=symbol, side=r_side + ) + + if res == "OK": + pass + else: + errors = { + "Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения", + "ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли", + "InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.", + "The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки", + "Order placement failed as your position may exceed the max": "❗️ Превышен максимальный лимит ставки с текущим плечом", + } + error_text = errors.get( + res, "❗️ Не удалось открыть новую сделку" + ) + await rq.set_auto_trading( + tg_id=tg_id, symbol=symbol, auto_trading=False + ) + + await rq.set_total_fee_user_auto_trading( + tg_id=tg_id, symbol=symbol, total_fee=0 + ) + await rq.set_fee_user_auto_trading( + tg_id=tg_id, symbol=symbol, fee=0 + ) + await self.telegram_bot.send_message( + chat_id=tg_id, + text=error_text, + reply_markup=kbi.profile_bybit, + ) elif create_type == "CreateByClosing": await self.telegram_bot.send_message( chat_id=tg_id, @@ -346,5 +373,7 @@ class TelegramMessageHandler: ) logger.info("Stop trading for symbol: %s, create_type: %s, stop_order_type: %s: %s", symbol, create_type, stop_order_type, tg_id) + else: + logger.info("Execution update: %s", json.dumps(message)) except Exception as e: logger.error("Error in telegram_message_handler: %s", e, exc_info=True) diff --git a/app/bybit/web_socket.py b/app/bybit/web_socket.py index f69e85a..225cae5 100644 --- a/app/bybit/web_socket.py +++ b/app/bybit/web_socket.py @@ -11,6 +11,13 @@ logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("web_socket") +class CustomWebSocket(WebSocket): + def _on_error(self, error): + logger.error(f"WebSocket error: {error}") + # Здесь можно добавить собственную логику, например уведомления, метрики и т.д. + return super()._on_error(error) + + class WebSocketBot: """ Class to handle WebSocket connections and messages. @@ -43,9 +50,9 @@ class WebSocketBot: continue if tg_id in self.user_sockets: - self.user_sockets.clear() - self.user_messages.clear() - self.user_keys.clear() + self.user_sockets.pop(tg_id, None) + self.user_messages.pop(tg_id, None) + self.user_keys.pop(tg_id, None) logger.info( "Closed old websocket for user %s due to key change", tg_id ) @@ -73,12 +80,13 @@ class WebSocketBot: async def try_connect_user(self, api_key, api_secret, tg_id): """Try to connect a user to the WebSocket.""" try: - self.ws_private = WebSocket( + self.ws_private = CustomWebSocket( demo=True, testnet=False, channel_type="private", api_key=api_key, api_secret=api_secret, + ping_interval=20, ) self.user_sockets[tg_id] = self.ws_private @@ -98,7 +106,7 @@ class WebSocketBot: return True except Exception as e: logger.error("Error connecting user %s: %s", tg_id, e) - return False + await asyncio.sleep(5) async def handle_order_update(self, message, tg_id): """Handle order updates.""" diff --git a/app/telegram/handlers/main_settings/additional_settings.py b/app/telegram/handlers/main_settings/additional_settings.py index 262ed85..425029e 100644 --- a/app/telegram/handlers/main_settings/additional_settings.py +++ b/app/telegram/handlers/main_settings/additional_settings.py @@ -299,6 +299,12 @@ async def settings_for_margin_type( deals = await get_active_positions_by_symbol( tg_id=callback_query.from_user.id, symbol=symbol ) + if deals == "Invalid API key permissions": + await callback_query.answer( + text="API ключ не имеет достаточных прав для смены маржи", + ) + return + position = next((d for d in deals if d.get("symbol") == symbol), None) if position: @@ -676,10 +682,19 @@ async def set_leverage_handler(message: Message, state: FSMContext) -> None: await state.clear() except Exception as e: - await message.answer( - text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.", - reply_markup=kbi.back_to_additional_settings, - ) + errors_text = str(e) + known_errors = { + "Permission denied, please check your API key permissions": "API ключ не имеет достаточных прав для установки кредитного плеча" + + } + for key, msg in known_errors.items(): + if key in errors_text: + await message.answer(msg, reply_markup=kbi.back_to_additional_settings) + else: + await message.answer( + text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) logger.error( "Error processing command leverage for user %s: %s", message.from_user.id, e ) diff --git a/app/telegram/handlers/start_trading.py b/app/telegram/handlers/start_trading.py index e410d61..9368e1a 100644 --- a/app/telegram/handlers/start_trading.py +++ b/app/telegram/handlers/start_trading.py @@ -38,6 +38,12 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non deals = await get_active_positions_by_symbol( tg_id=callback_query.from_user.id, symbol=symbol ) + if deals == "Invalid API key permissions": + await callback_query.answer( + text="API ключ не имеет достаточных прав для запуска торговли", + ) + return + position = next((d for d in deals if d.get("symbol") == symbol), None) if position: @@ -109,7 +115,9 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non "The number of contracts exceeds minimum limit allowed": "️️Лимит ставки меньше минимально допустимого", "Order placement failed as your position may exceed the max": "Не удалось разместить ордер, так как ваша позиция может превышать максимальный лимит." - "Пожалуйста, уменьшите кредитное плечо, чтобы увеличить максимальное значение" + "Пожалуйста, уменьшите кредитное плечо, чтобы увеличить максимальное значение", + "Permission denied, please check your API key permissions": "API ключ не имеет достаточных прав для запуска торговли" + } if res == "OK": @@ -131,7 +139,16 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non await add_start_task_merged(user_id=callback_query.from_user.id, task=task) except Exception as e: - await callback_query.answer(text="Произошла ошибка при запуске торговли") + error_text = str(e) + known_errors = { + "Permission denied, please check your API key permissions": "API ключ не имеет достаточных прав для запуска торговли" + + } + for key, msg in known_errors.items(): + if key in error_text: + await callback_query.answer(msg) + else: + await callback_query.answer(text="Произошла ошибка при запуске торговли") logger.error( "Error processing command start_trading for user %s: %s", callback_query.from_user.id,