2
0
forked from kodorvan/stcs

Fixed the switch trading mode, adjusted the take profit, added a trading cycle

This commit is contained in:
algizn97
2025-10-22 17:15:25 +05:00
parent e61b7334a4
commit ddfa3a7360
8 changed files with 261 additions and 84 deletions

View File

@@ -54,6 +54,10 @@ sudo -u www-data /usr/bin/pip install -r requirements.txt
cp .env.sample .env cp .env.sample .env
nvim .env nvim .env
``` ```
5. Выполните миграции:
```bash
alembic upgrade head
```
5. Запустите бота: 5. Запустите бота:

View File

@@ -0,0 +1,32 @@
"""Added side_mode column
Revision ID: fbf4e3658310
Revises:
Create Date: 2025-10-22 13:08:02.317419
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'fbf4e3658310'
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! ###
op.add_column('user_deals', sa.Column('side_mode', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user_deals', 'side_mode')
# ### end Alembic commands ###

View File

@@ -11,14 +11,14 @@ from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
from app.bybit.set_functions.set_leverage import set_leverage from app.bybit.set_functions.set_leverage import set_leverage
from app.bybit.set_functions.set_margin_mode import set_margin_mode from app.bybit.set_functions.set_margin_mode import set_margin_mode
from app.bybit.set_functions.set_switch_position_mode import set_switch_position_mode from app.bybit.set_functions.set_switch_position_mode import set_switch_position_mode
from app.helper_functions import get_liquidation_price, safe_float from app.helper_functions import safe_float
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("open_positions") logger = logging.getLogger("open_positions")
async def start_trading_cycle( async def start_trading_cycle(
tg_id: int tg_id: int
) -> str | None: ) -> str | None:
""" """
Start trading cycle Start trading cycle
@@ -93,6 +93,7 @@ async def start_trading_cycle(
last_side=side, last_side=side,
current_step=1, current_step=1,
trade_mode=trade_mode, trade_mode=trade_mode,
side_mode=switch_side,
margin_type=margin_type, margin_type=margin_type,
leverage=leverage, leverage=leverage,
order_quantity=order_quantity, order_quantity=order_quantity,
@@ -107,19 +108,19 @@ async def start_trading_cycle(
return ( return (
res res
if res if res
in { in {
"Limit price is out min price", "Limit price is out min price",
"Limit price is out max price", "Limit price is out max price",
"Risk is too high for this trade", "Risk is too high for this trade",
"estimated will trigger liq", "estimated will trigger liq",
"ab not enough for new order", "ab not enough for new order",
"InvalidRequestError", "InvalidRequestError",
"Order does not meet minimum order value", "Order does not meet minimum order value",
"position idx not match position mode", "position idx not match position mode",
"Qty invalid", "Qty invalid",
"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"
} }
else None else None
) )
@@ -128,8 +129,90 @@ async def start_trading_cycle(
return None return None
async def trading_cycle_profit(
tg_id: int, symbol: str, side: str) -> str | None:
try:
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol)
total_fee = user_auto_trading_data.total_fee
trade_mode = user_deals_data.trade_mode
margin_type = user_deals_data.margin_type
leverage = user_deals_data.leverage
trigger_price = 0
take_profit_percent = user_deals_data.take_profit_percent
stop_loss_percent = user_deals_data.stop_loss_percent
max_bets_in_series = user_deals_data.max_bets_in_series
martingale_factor = user_deals_data.martingale_factor
side_mode = user_deals_data.side_mode
base_quantity = user_deals_data.base_quantity
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
await set_leverage(
tg_id=tg_id,
symbol=symbol,
leverage=leverage,
)
if trade_mode == "Switch":
if side_mode == "Противоположно":
s_side = "Sell" if side == "Buy" else "Buy"
else:
s_side = side
else:
s_side = side
res = await open_positions(
tg_id=tg_id,
symbol=symbol,
side=s_side,
order_quantity=base_quantity,
trigger_price=trigger_price,
margin_type=margin_type,
leverage=leverage,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
commission_fee_percent=total_fee
)
if res == "OK":
await rq.set_user_deal(
tg_id=tg_id,
symbol=symbol,
last_side=side,
current_step=1,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
leverage=leverage,
order_quantity=base_quantity,
trigger_price=trigger_price,
martingale_factor=martingale_factor,
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity
)
return "OK"
return (
res
if res
in {
"Risk is too high for this trade",
"ab not enough for new order",
"InvalidRequestError",
"The number of contracts exceeds maximum limit allowed",
}
else None
)
except Exception as e:
logger.error("Error in trading_cycle_profit: %s", e)
return None
async def trading_cycle( async def trading_cycle(
tg_id: int, symbol: str, reverse_side: str tg_id: int, symbol: str, side: str,
) -> str | None: ) -> str | None:
try: try:
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol) user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
@@ -146,23 +229,7 @@ async def trading_cycle(
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
base_quantity = user_deals_data.base_quantity base_quantity = user_deals_data.base_quantity
side_mode = user_deals_data.side_mode
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
await set_leverage(
tg_id=tg_id,
symbol=symbol,
leverage=leverage,
)
if reverse_side == "Buy":
real_side = "Sell"
else:
real_side = "Buy"
side = real_side
if trade_mode == "Switch":
side = "Sell" if real_side == "Buy" else "Buy"
next_quantity = safe_float(order_quantity) * ( next_quantity = safe_float(order_quantity) * (
safe_float(martingale_factor) safe_float(martingale_factor)
@@ -172,6 +239,13 @@ async def trading_cycle(
if max_bets_in_series < current_step: if max_bets_in_series < current_step:
return "Max bets in series" return "Max bets in series"
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
await set_leverage(
tg_id=tg_id,
symbol=symbol,
leverage=leverage,
)
res = await open_positions( res = await open_positions(
tg_id=tg_id, tg_id=tg_id,
symbol=symbol, symbol=symbol,
@@ -192,6 +266,7 @@ async def trading_cycle(
last_side=side, last_side=side,
current_step=current_step, current_step=current_step,
trade_mode=trade_mode, trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type, margin_type=margin_type,
leverage=leverage, leverage=leverage,
order_quantity=next_quantity, order_quantity=next_quantity,
@@ -207,12 +282,12 @@ async def trading_cycle(
return ( return (
res res
if res if res
in { in {
"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",
"The number of contracts exceeds maximum limit allowed", "The number of contracts exceeds maximum limit allowed",
} }
else None else None
) )
@@ -222,16 +297,16 @@ async def trading_cycle(
async def open_positions( async def open_positions(
tg_id: int, tg_id: int,
side: str, side: str,
symbol: str, symbol: str,
order_quantity: float, order_quantity: float,
trigger_price: float, trigger_price: float,
margin_type: str, margin_type: str,
leverage: str, leverage: str,
take_profit_percent: float, take_profit_percent: float,
stop_loss_percent: float, stop_loss_percent: float,
commission_fee_percent: float commission_fee_percent: float
) -> str | None: ) -> str | None:
try: try:
client = await get_bybit_client(tg_id=tg_id) client = await get_bybit_client(tg_id=tg_id)
@@ -252,39 +327,28 @@ async def open_positions(
po_trigger_price = None po_trigger_price = None
trigger_direction = None trigger_direction = None
get_leverage = safe_float(leverage)
price_for_cals = trigger_price if po_trigger_price is not None else price_symbol price_for_cals = trigger_price if po_trigger_price is not None else price_symbol
if qty_formatted <= 0: if qty_formatted <= 0:
return "Order does not meet minimum order value" return "Order does not meet minimum order value"
if margin_type == "ISOLATED_MARGIN": if margin_type == "ISOLATED_MARGIN":
liq_long, liq_short = await get_liquidation_price( if side == "Buy":
tg_id=tg_id, take_profit_price = price_for_cals * (
entry_price=price_for_cals, 1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted
symbol=symbol, stop_loss_price = None
leverage=get_leverage,
)
if (liq_long > 0 or liq_short > 0) and price_for_cals > 0:
if side == "Buy":
base_tp = price_for_cals + (price_for_cals - liq_long)
take_profit_price = base_tp + commission_fee_percent / qty_formatted
else:
base_tp = price_for_cals - (liq_short - price_for_cals)
take_profit_price = base_tp - commission_fee_percent / qty_formatted
take_profit_price = max(take_profit_price, 0)
else: else:
take_profit_price = None take_profit_price = price_for_cals * (
1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted
stop_loss_price = None stop_loss_price = None
else: else:
if side == "Buy": if side == "Buy":
take_profit_price = price_for_cals * (1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted take_profit_price = price_for_cals * (
1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted
stop_loss_price = price_for_cals * (1 - stop_loss_percent / 100) stop_loss_price = price_for_cals * (1 - stop_loss_percent / 100)
else: else:
take_profit_price = price_for_cals * (1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted take_profit_price = price_for_cals * (
1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted
stop_loss_price = price_for_cals * (1 + stop_loss_percent / 100) stop_loss_price = price_for_cals * (1 + stop_loss_percent / 100)
take_profit_price = max(take_profit_price, 0) take_profit_price = max(take_profit_price, 0)

View File

@@ -3,7 +3,7 @@ import logging.config
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.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 from app.bybit.open_positions import trading_cycle, trading_cycle_profit
from app.helper_functions import format_value, safe_float from app.helper_functions import format_value, safe_float
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
@@ -57,7 +57,7 @@ class TelegramMessageHandler:
) )
if user_deals_data is not None and auto_trading: if user_deals_data is not None and auto_trading:
text += f"Текущая ставка: {user_deals_data.order_quantity}\n" text += f"Текущая ставка: {user_deals_data.order_quantity} USDT\n"
else: else:
text += f"Количество: {qty}\n" text += f"Количество: {qty}\n"
@@ -146,11 +146,12 @@ class TelegramMessageHandler:
text += ( text += (
f"Цена исполнения: {exec_price}\n" f"Цена исполнения: {exec_price}\n"
f"Движение: {side_rus}\n"
f"Комиссия: {exec_fee:.8f}\n" f"Комиссия: {exec_fee:.8f}\n"
) )
if safe_float(closed_size) > 0: if safe_float(closed_size) == 0:
text += f"Движение: {side_rus}\n"
else:
text += f"\nРеализованная прибыль: {total_pnl:.7f}\n" text += f"\nРеализованная прибыль: {total_pnl:.7f}\n"
await self.telegram_bot.send_message( await self.telegram_bot.send_message(
@@ -165,31 +166,70 @@ class TelegramMessageHandler:
and user_symbols is not None and user_symbols is not None
): ):
if safe_float(total_pnl) > 0: if safe_float(total_pnl) > 0:
profit_text = "📈 Прибыль достигнута\n" profit_text = "📈 Прибыль достигнута. Начинаем новую серию с базовой ставки\n"
await self.telegram_bot.send_message( await self.telegram_bot.send_message(
chat_id=tg_id, text=profit_text, reply_markup=kbi.profile_bybit chat_id=tg_id, text=profit_text, reply_markup=kbi.profile_bybit
) )
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False
)
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( await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0 tg_id=tg_id, symbol=symbol, total_fee=0
) )
await rq.set_fee_user_auto_trading( await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0 tg_id=tg_id, symbol=symbol, fee=0
) )
base_quantity = user_deals_data.base_quantity
await rq.set_order_quantity( res = await trading_cycle_profit(
tg_id=tg_id, order_quantity=base_quantity 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": "❗️ Превышен максимальный лимит ставки",
}
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: 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
) )
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
res = await trading_cycle( res = await trading_cycle(
tg_id=tg_id, symbol=symbol, reverse_side=side tg_id=tg_id, symbol=symbol, side=r_side
) )
if res == "OK": if res == "OK":

View File

@@ -124,6 +124,8 @@ async def set_symbol(message: Message, state: FSMContext) -> None:
risk_percent = 100 / safe_float(max_leverage) risk_percent = 100 / safe_float(max_leverage)
await rq.set_stop_loss_percent( await rq.set_stop_loss_percent(
tg_id=message.from_user.id, stop_loss_percent=risk_percent) tg_id=message.from_user.id, stop_loss_percent=risk_percent)
await rq.set_take_profit_percent(
tg_id=message.from_user.id, take_profit_percent=risk_percent)
await rq.set_trigger_price(tg_id=message.from_user.id, trigger_price=0) await rq.set_trigger_price(tg_id=message.from_user.id, trigger_price=0)
await rq.set_order_quantity(tg_id=message.from_user.id, order_quantity=1.0) await rq.set_order_quantity(tg_id=message.from_user.id, order_quantity=1.0)

View File

@@ -43,7 +43,7 @@ async def settings_for_trade_mode(
text="Выберите режим торговли:\n\n" text="Выберите режим торговли:\n\n"
"Лонг - все сделки серии открываются на покупку.\n" "Лонг - все сделки серии открываются на покупку.\n"
"Шорт - все сделки серии открываются на продажу.\n" "Шорт - все сделки серии открываются на продажу.\n"
"Свитч - направление каждой сделки серии меняется по переменно.\n", "Свитч - направление первой сделки серии меняется по переменно.\n",
reply_markup=kbi.trade_mode, reply_markup=kbi.trade_mode,
) )
logger.debug( logger.debug(
@@ -580,6 +580,8 @@ async def set_leverage_handler(message: Message, state: FSMContext) -> None:
risk_percent = 100 / safe_float(leverage_float) risk_percent = 100 / safe_float(leverage_float)
await rq.set_stop_loss_percent( await rq.set_stop_loss_percent(
tg_id=message.from_user.id, stop_loss_percent=risk_percent) tg_id=message.from_user.id, stop_loss_percent=risk_percent)
await rq.set_take_profit_percent(
tg_id=message.from_user.id, take_profit_percent=risk_percent)
logger.info( logger.info(
"User %s set leverage: %s", message.from_user.id, leverage_float "User %s set leverage: %s", message.from_user.id, leverage_float
) )

View File

@@ -143,6 +143,7 @@ class UserDeals(Base):
current_step = Column(Integer, nullable=True) current_step = Column(Integer, nullable=True)
symbol = Column(String, nullable=True) symbol = Column(String, nullable=True)
trade_mode = Column(String, nullable=True) trade_mode = Column(String, nullable=True)
side_mode = Column(String, nullable=True)
base_quantity = Column(Float, nullable=True) base_quantity = Column(Float, nullable=True)
margin_type = Column(String, nullable=True) margin_type = Column(String, nullable=True)
leverage = Column(String, nullable=True) leverage = Column(String, nullable=True)

View File

@@ -898,6 +898,7 @@ async def set_user_deal(
last_side: str, last_side: str,
current_step: int, current_step: int,
trade_mode: str, trade_mode: str,
side_mode: str,
margin_type: str, margin_type: str,
leverage: str, leverage: str,
order_quantity: float, order_quantity: float,
@@ -915,6 +916,7 @@ async def set_user_deal(
:param last_side: Last side :param last_side: Last side
:param current_step: Current step :param current_step: Current step
:param trade_mode: Trade mode :param trade_mode: Trade mode
:param side_mode: Side mode
:param margin_type: Margin type :param margin_type: Margin type
:param leverage: Leverage :param leverage: Leverage
:param order_quantity: Order quantity :param order_quantity: Order quantity
@@ -944,6 +946,7 @@ async def set_user_deal(
deal.last_side = last_side deal.last_side = last_side
deal.current_step = current_step deal.current_step = current_step
deal.trade_mode = trade_mode deal.trade_mode = trade_mode
deal.side_mode = side_mode
deal.margin_type = margin_type deal.margin_type = margin_type
deal.leverage = leverage deal.leverage = leverage
deal.order_quantity = order_quantity deal.order_quantity = order_quantity
@@ -961,6 +964,7 @@ async def set_user_deal(
last_side=last_side, last_side=last_side,
current_step=current_step, current_step=current_step,
trade_mode=trade_mode, trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type, margin_type=margin_type,
leverage=leverage, leverage=leverage,
order_quantity=order_quantity, order_quantity=order_quantity,
@@ -1050,6 +1054,34 @@ async def set_fee_user_deal_by_symbol(tg_id: int, symbol: str, fee: float):
return False return False
async def set_last_side_by_symbol(tg_id: int, symbol: str, last_side: str):
"""Set last side for a user deal by symbol in the database."""
try:
async with async_session() as session:
result = await session.execute(select(User).filter_by(tg_id=tg_id))
user = result.scalars().first()
if user is None:
logger.error(f"User with tg_id={tg_id} not found")
return False
result = await session.execute(
select(UserDeals).filter_by(user_id=user.id, symbol=symbol)
)
record = result.scalars().first()
if record:
record.last_side = last_side
else:
logger.error(f"User deal with user_id={user.id} and symbol={symbol} not found")
return False
await session.commit()
logger.info("Set last side for user %s and symbol %s", tg_id, symbol)
return True
except Exception as e:
logger.error("Error setting user deal last side for user %s and symbol %s: %s", tg_id, symbol, e)
return False
# USER AUTO TRADING # USER AUTO TRADING
async def get_all_user_auto_trading(tg_id: int): async def get_all_user_auto_trading(tg_id: int):