2
0
forked from kodorvan/stcs

Merge pull request 'devel' (#34) from Alex/stcs:devel into stable

Reviewed-on: kodorvan/stcs#34
This commit is contained in:
2025-11-20 14:33:56 +07:00
9 changed files with 195 additions and 91 deletions

View File

@@ -84,7 +84,7 @@ path_separator = os
# database URL. This is consumed by the user-maintained env.py script only. # 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 # other means of configuring database URLs may be customized within the env.py
# file. # file.
sqlalchemy.url = sqlite+aiosqlite:///./database/dbs/stcs.db sqlalchemy.url = sqlite+aiosqlite:///./database/stcs.db
[post_write_hooks] [post_write_hooks]

View File

@@ -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 ###

View File

@@ -38,7 +38,7 @@ async def get_active_positions(tg_id: int) -> list | None:
return 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 Get active positions for a user by symbol
""" """
@@ -62,6 +62,10 @@ async def get_active_positions_by_symbol(tg_id: int, symbol: str) -> dict | None
) )
return None return None
except Exception as e: except Exception as e:
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) logger.error("Error getting active positions for user %s: %s", tg_id, e)
return None return None

View File

@@ -109,6 +109,7 @@ async def start_trading_cycle(
"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"
} }
else None else None
) )
@@ -232,9 +233,6 @@ async def trading_cycle(
) )
current_step += 1 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_margin_mode(tg_id=tg_id, margin_mode=margin_type)
await set_leverage( await set_leverage(
tg_id=tg_id, 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 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", "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", "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(): for key, msg in known_errors.items():
if key in error_text: if key in error_text:

View File

@@ -37,7 +37,7 @@ async def user_profile_bybit(tg_id: int, message: Message, state: FSMContext) ->
) )
else: else:
await message.answer( await message.answer(
text="Ошибка при подключении к платформе. Проверьте ключи и повторите попытку.", text="Ошибка при подключении к платформе. Проверьте корректность и разрешения API ключа и добавьте повторно.",
reply_markup=kbi.connect_the_platform, reply_markup=kbi.connect_the_platform,
) )
logger.error("Error processing user profile for user %s", tg_id) logger.error("Error processing user profile for user %s", tg_id)

View File

@@ -1,10 +1,9 @@
import logging.config import logging.config
import math import math
# import json import json
import app.telegram.keyboards.inline as kbi import app.telegram.keyboards.inline as kbi
import database.request as rq import database.request as rq
from app.bybit.get_functions.get_instruments_info import get_instruments_info 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.logger_bybit.logger_bybit import LOGGING_CONFIG
from app.bybit.open_positions import trading_cycle, trading_cycle_profit 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 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 current_step = user_deals_data.current_step
order_quantity = user_deals_data.order_quantity order_quantity = user_deals_data.order_quantity
pnl_series = user_deals_data.pnl_series pnl_series = user_deals_data.pnl_series
margin_type = user_deals_data.margin_type
take_profit_percent = user_deals_data.take_profit_percent take_profit_percent = user_deals_data.take_profit_percent
stop_loss_percent = user_deals_data.stop_loss_percent stop_loss_percent = user_deals_data.stop_loss_percent
leverage = safe_float(user_deals_data.leverage) leverage = safe_float(user_deals_data.leverage)
@@ -167,16 +165,6 @@ class TelegramMessageHandler:
if commission_place == "Commission_for_tp": if commission_place == "Commission_for_tp":
total_commission = safe_float(total_fee) / qty_formatted 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
else:
if side == "Buy": if side == "Buy":
take_profit_price = safe_float(exec_price) * ( take_profit_price = safe_float(exec_price) * (
1 + take_profit_percent / 100) + total_commission 1 + take_profit_percent / 100) + total_commission
@@ -193,27 +181,11 @@ class TelegramMessageHandler:
position_idx=0) position_idx=0)
if ress or ress == "not modified": if ress or ress == "not modified":
take_profit_truncated = await truncate_float(take_profit_price, 6) 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" text += (f"Движение: {side_rus}\n"
f"Тейк-профит: {take_profit_truncated}\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: else:
text += (f"Движение: {side_rus}\n" text += (f"Движение: {side_rus}\n"
"Не удалось установить ТП и СЛ\n") "Не удалось установить ТП и СЛ\n")
@@ -258,7 +230,6 @@ class TelegramMessageHandler:
pass pass
else: else:
errors = { errors = {
"Max bets in series": "❗️ Максимальное количество сделок в серии достигнуто",
"Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения", "Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
"ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли", "ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
"InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.", "InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
@@ -285,6 +256,63 @@ class TelegramMessageHandler:
) )
elif stop_order_type == "StopLoss" or exec_type == "BustTrade": elif stop_order_type == "StopLoss" or exec_type == "BustTrade":
current_step = user_deals_data.current_step
max_bets_in_series = user_deals_data.max_bets_in_series
current_step += 1
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 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" open_order_text = "\n❗️ Открываю новую сделку с увеличенной ставкой.\n"
await self.telegram_bot.send_message( await self.telegram_bot.send_message(
chat_id=tg_id, text=open_order_text chat_id=tg_id, text=open_order_text
@@ -303,7 +331,6 @@ class TelegramMessageHandler:
pass pass
else: else:
errors = { errors = {
"Max bets in series": "❗️ Максимальное количество сделок в серии достигнуто",
"Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения", "Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
"ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли", "ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
"InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.", "InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
@@ -346,5 +373,7 @@ class TelegramMessageHandler:
) )
logger.info("Stop trading for symbol: %s, create_type: %s, stop_order_type: %s: %s", logger.info("Stop trading for symbol: %s, create_type: %s, stop_order_type: %s: %s",
symbol, create_type, stop_order_type, tg_id) symbol, create_type, stop_order_type, tg_id)
else:
logger.info("Execution update: %s", json.dumps(message))
except Exception as e: except Exception as e:
logger.error("Error in telegram_message_handler: %s", e, exc_info=True) logger.error("Error in telegram_message_handler: %s", e, exc_info=True)

View File

@@ -11,6 +11,13 @@ logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("web_socket") 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 WebSocketBot:
""" """
Class to handle WebSocket connections and messages. Class to handle WebSocket connections and messages.
@@ -43,9 +50,9 @@ class WebSocketBot:
continue continue
if tg_id in self.user_sockets: if tg_id in self.user_sockets:
self.user_sockets.clear() self.user_sockets.pop(tg_id, None)
self.user_messages.clear() self.user_messages.pop(tg_id, None)
self.user_keys.clear() self.user_keys.pop(tg_id, None)
logger.info( logger.info(
"Closed old websocket for user %s due to key change", tg_id "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): async def try_connect_user(self, api_key, api_secret, tg_id):
"""Try to connect a user to the WebSocket.""" """Try to connect a user to the WebSocket."""
try: try:
self.ws_private = WebSocket( self.ws_private = CustomWebSocket(
demo=True, demo=True,
testnet=False, testnet=False,
channel_type="private", channel_type="private",
api_key=api_key, api_key=api_key,
api_secret=api_secret, api_secret=api_secret,
ping_interval=20,
) )
self.user_sockets[tg_id] = self.ws_private self.user_sockets[tg_id] = self.ws_private
@@ -98,7 +106,7 @@ class WebSocketBot:
return True return True
except Exception as e: except Exception as e:
logger.error("Error connecting user %s: %s", tg_id, 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): async def handle_order_update(self, message, tg_id):
"""Handle order updates.""" """Handle order updates."""

View File

@@ -299,6 +299,12 @@ async def settings_for_margin_type(
deals = await get_active_positions_by_symbol( deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=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) position = next((d for d in deals if d.get("symbol") == symbol), None)
if position: if position:
@@ -676,6 +682,15 @@ async def set_leverage_handler(message: Message, state: FSMContext) -> None:
await state.clear() await state.clear()
except Exception as e: except Exception as e:
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( await message.answer(
text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.", text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_additional_settings, reply_markup=kbi.back_to_additional_settings,

View File

@@ -38,6 +38,12 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
deals = await get_active_positions_by_symbol( deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=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) position = next((d for d in deals if d.get("symbol") == symbol), None)
if position: if position:
@@ -109,7 +115,9 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
"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": "API ключ не имеет достаточных прав для запуска торговли"
} }
if res == "OK": if res == "OK":
@@ -131,6 +139,15 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
await add_start_task_merged(user_id=callback_query.from_user.id, task=task) await add_start_task_merged(user_id=callback_query.from_user.id, task=task)
except Exception as e: except Exception as e:
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="Произошла ошибка при запуске торговли") await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error( logger.error(
"Error processing command start_trading for user %s: %s", "Error processing command start_trading for user %s: %s",