2
0
forked from kodorvan/stcs

10 Commits

Author SHA1 Message Date
7b1a803db4 Merge pull request 'Fixed the switch trading mode, adjusted the take profit, added a trading cycle' (#18) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#18
2025-10-22 22:20:21 +07:00
algizn97
ddfa3a7360 Fixed the switch trading mode, adjusted the take profit, added a trading cycle 2025-10-22 17:15:25 +05:00
9fcd92cc72 Merge pull request 'The formula for calculating the number of contracts by price has been changed' (#17) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#17
2025-10-21 20:33:42 +07:00
algizn97
e61b7334a4 The formula for calculating the number of contracts by price has been changed 2025-10-21 13:59:09 +05:00
97a199f31e Merge pull request 'devel' (#16) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#16
2025-10-18 18:09:23 +07:00
951bc15957 Merge pull request 'devel' (#15) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#15
2025-10-12 17:26:11 +07:00
5937058899 Merge pull request 'The database has been converted to SQLite' (#14) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#14
2025-10-12 14:35:54 +07:00
f0732607e2 Merge pull request 'The instruction has been corrected' (#13) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#13
2025-10-11 16:36:48 +07:00
56af1d8f3b Merge pull request 'Added migrations for the database' (#12) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#12
2025-10-11 15:49:40 +07:00
9f069df68a Merge pull request 'devel' (#11) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#11
2025-10-11 11:58:36 +07:00
8 changed files with 268 additions and 117 deletions

View File

@@ -54,6 +54,10 @@ sudo -u www-data /usr/bin/pip install -r requirements.txt
cp .env.sample .env
nvim .env
```
5. Выполните миграции:
```bash
alembic upgrade head
```
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,25 +11,23 @@ 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_margin_mode import set_margin_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)
logger = logging.getLogger("open_positions")
async def start_trading_cycle(
tg_id: int
tg_id: int
) -> str | None:
"""
Start trading cycle
:param tg_id: Telegram user ID
"""
try:
client = await get_bybit_client(tg_id=tg_id)
symbol = await rq.get_user_symbol(tg_id=tg_id)
additional_data = await rq.get_user_additional_settings(tg_id=tg_id)
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
commission_fee = risk_management_data.commission_fee
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
)
@@ -43,6 +41,7 @@ async def start_trading_cycle(
max_bets_in_series = additional_data.max_bets_in_series
take_profit_percent = risk_management_data.take_profit_percent
stop_loss_percent = risk_management_data.stop_loss_percent
total_commission = 0
get_side = "Buy"
@@ -63,34 +62,6 @@ async def start_trading_cycle(
else:
side = "Sell"
# Get fee rates
fee_info = client.get_fee_rates(category="linear", symbol=symbol)
# Check if commission fee is enabled
commission_fee_percent = 0.0
if commission_fee == "Yes_commission_fee":
commission_fee_percent = safe_float(
fee_info["result"]["list"][0]["takerFeeRate"]
)
get_ticker = await get_tickers(tg_id, symbol=symbol)
price_symbol = safe_float(get_ticker.get("lastPrice")) or 0
instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol)
qty_step_str = instruments_info.get("lotSizeFilter").get("qtyStep")
qty_step = safe_float(qty_step_str)
qty = safe_float(order_quantity) / safe_float(price_symbol)
decimals = abs(int(round(math.log10(qty_step))))
qty_formatted = math.floor(qty / qty_step) * qty_step
qty_formatted = round(qty_formatted, decimals)
if trigger_price > 0:
po_trigger_price = str(trigger_price)
else:
po_trigger_price = None
price_for_cals = trigger_price if po_trigger_price is not None else price_symbol
total_commission = price_for_cals * qty_formatted * commission_fee_percent
await set_switch_position_mode(
tg_id=tg_id,
symbol=symbol,
@@ -122,6 +93,7 @@ async def start_trading_cycle(
last_side=side,
current_step=1,
trade_mode=trade_mode,
side_mode=switch_side,
margin_type=margin_type,
leverage=leverage,
order_quantity=order_quantity,
@@ -136,19 +108,19 @@ async def start_trading_cycle(
return (
res
if res
in {
"Limit price is out min price",
"Limit price is out max price",
"Risk is too high for this trade",
"estimated will trigger liq",
"ab not enough for new order",
"InvalidRequestError",
"Order does not meet minimum order value",
"position idx not match position mode",
"Qty invalid",
"The number of contracts exceeds maximum limit allowed",
"The number of contracts exceeds minimum limit allowed"
}
in {
"Limit price is out min price",
"Limit price is out max price",
"Risk is too high for this trade",
"estimated will trigger liq",
"ab not enough for new order",
"InvalidRequestError",
"Order does not meet minimum order value",
"position idx not match position mode",
"Qty invalid",
"The number of contracts exceeds maximum limit allowed",
"The number of contracts exceeds minimum limit allowed"
}
else None
)
@@ -157,8 +129,90 @@ async def start_trading_cycle(
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(
tg_id: int, symbol: str, reverse_side: str
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)
@@ -175,23 +229,7 @@ async def trading_cycle(
current_step = user_deals_data.current_step
order_quantity = user_deals_data.order_quantity
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 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"
side_mode = user_deals_data.side_mode
next_quantity = safe_float(order_quantity) * (
safe_float(martingale_factor)
@@ -201,6 +239,13 @@ async def trading_cycle(
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,
symbol=symbol,
leverage=leverage,
)
res = await open_positions(
tg_id=tg_id,
symbol=symbol,
@@ -221,6 +266,7 @@ async def trading_cycle(
last_side=side,
current_step=current_step,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
leverage=leverage,
order_quantity=next_quantity,
@@ -236,12 +282,12 @@ async def trading_cycle(
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",
}
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
)
@@ -251,16 +297,16 @@ async def trading_cycle(
async def open_positions(
tg_id: int,
side: str,
symbol: str,
order_quantity: float,
trigger_price: float,
margin_type: str,
leverage: str,
take_profit_percent: float,
stop_loss_percent: float,
commission_fee_percent: float
tg_id: int,
side: str,
symbol: str,
order_quantity: float,
trigger_price: float,
margin_type: str,
leverage: str,
take_profit_percent: float,
stop_loss_percent: float,
commission_fee_percent: float
) -> str | None:
try:
client = await get_bybit_client(tg_id=tg_id)
@@ -269,7 +315,7 @@ async def open_positions(
instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol)
qty_step_str = instruments_info.get("lotSizeFilter").get("qtyStep")
qty_step = safe_float(qty_step_str)
qty = safe_float(order_quantity) / safe_float(price_symbol)
qty = (safe_float(order_quantity) * safe_float(leverage)) / safe_float(price_symbol)
decimals = abs(int(round(math.log10(qty_step))))
qty_formatted = math.floor(qty / qty_step) * qty_step
qty_formatted = round(qty_formatted, decimals)
@@ -281,36 +327,28 @@ async def open_positions(
po_trigger_price = None
trigger_direction = None
get_leverage = safe_float(leverage)
price_for_cals = trigger_price if po_trigger_price is not None else price_symbol
if qty_formatted <= 0:
return "Order does not meet minimum order value"
if margin_type == "ISOLATED_MARGIN":
liq_long, liq_short = await get_liquidation_price(
tg_id=tg_id,
entry_price=price_for_cals,
symbol=symbol,
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)
if side == "Buy":
take_profit_price = price_for_cals * (
1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted
stop_loss_price = None
else:
take_profit_price = None
stop_loss_price = None
take_profit_price = price_for_cals * (
1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted
stop_loss_price = None
else:
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)
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)
take_profit_price = max(take_profit_price, 0)
@@ -361,5 +399,5 @@ async def open_positions(
return "InvalidRequestError"
except Exception as e:
logger.error("Error opening position for user %s: %s", tg_id, e)
logger.error("Error opening position for user %s: %s", tg_id, e, exc_info=True)
return None

View File

@@ -3,7 +3,7 @@ import logging.config
import app.telegram.keyboards.inline as kbi
import database.request as rq
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
logging.config.dictConfig(LOGGING_CONFIG)
@@ -57,7 +57,7 @@ class TelegramMessageHandler:
)
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:
text += f"Количество: {qty}\n"
@@ -140,17 +140,18 @@ class TelegramMessageHandler:
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=total_fee
)
text += f"Текущая ставка: {user_deals_data.order_quantity}\n"
text += f"Текущая ставка: {user_deals_data.order_quantity} USDT\n"
else:
text += f"Количество: {exec_qty}\n"
text += (
f"Цена исполнения: {exec_price}\n"
f"Движение: {side_rus}\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"
await self.telegram_bot.send_message(
@@ -165,31 +166,70 @@ class TelegramMessageHandler:
and user_symbols is not None
):
if safe_float(total_pnl) > 0:
profit_text = "📈 Прибыль достигнута\n"
profit_text = "📈 Прибыль достигнута. Начинаем новую серию с базовой ставки\n"
await self.telegram_bot.send_message(
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(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
base_quantity = user_deals_data.base_quantity
await rq.set_order_quantity(
tg_id=tg_id, order_quantity=base_quantity
res = await trading_cycle_profit(
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:
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, reverse_side=side
tg_id=tg_id, symbol=symbol, side=r_side
)
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)
await rq.set_stop_loss_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_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"
"Лонг - все сделки серии открываются на покупку.\n"
"Шорт - все сделки серии открываются на продажу.\n"
"Свитч - направление каждой сделки серии меняется по переменно.\n",
"Свитч - направление первой сделки серии меняется по переменно.\n",
reply_markup=kbi.trade_mode,
)
logger.debug(
@@ -580,6 +580,8 @@ async def set_leverage_handler(message: Message, state: FSMContext) -> None:
risk_percent = 100 / safe_float(leverage_float)
await rq.set_stop_loss_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(
"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)
symbol = Column(String, nullable=True)
trade_mode = Column(String, nullable=True)
side_mode = Column(String, nullable=True)
base_quantity = Column(Float, nullable=True)
margin_type = Column(String, nullable=True)
leverage = Column(String, nullable=True)

View File

@@ -898,6 +898,7 @@ async def set_user_deal(
last_side: str,
current_step: int,
trade_mode: str,
side_mode: str,
margin_type: str,
leverage: str,
order_quantity: float,
@@ -915,6 +916,7 @@ async def set_user_deal(
:param last_side: Last side
:param current_step: Current step
:param trade_mode: Trade mode
:param side_mode: Side mode
:param margin_type: Margin type
:param leverage: Leverage
:param order_quantity: Order quantity
@@ -944,6 +946,7 @@ async def set_user_deal(
deal.last_side = last_side
deal.current_step = current_step
deal.trade_mode = trade_mode
deal.side_mode = side_mode
deal.margin_type = margin_type
deal.leverage = leverage
deal.order_quantity = order_quantity
@@ -961,6 +964,7 @@ async def set_user_deal(
last_side=last_side,
current_step=current_step,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
leverage=leverage,
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
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
async def get_all_user_auto_trading(tg_id: int):