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

Reviewed-on: #23
This commit is contained in:
2025-10-26 16:39:41 +07:00
9 changed files with 302 additions and 32 deletions

View File

@@ -0,0 +1,32 @@
"""Added column current_series
Revision ID: 0ee52ab23e66
Revises: e5d612e44563
Create Date: 2025-10-26 11:48:48.055031
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '0ee52ab23e66'
down_revision: Union[str, Sequence[str], None] = 'e5d612e44563'
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('current_series', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user_deals', 'current_series')
# ### end Alembic commands ###

View File

@@ -0,0 +1,45 @@
"""Added column commission_place
Revision ID: adf3d2991896
Revises: 0ee52ab23e66
Create Date: 2025-10-26 13:37:33.662318
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy import inspect
# revision identifiers, used by Alembic.
revision: str = 'adf3d2991896'
down_revision: Union[str, Sequence[str], None] = '0ee52ab23e66'
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! ###
bind = op.get_bind()
inspector = inspect(bind)
columns_user_deals = [col['name'] for col in inspector.get_columns('user_deals')]
if 'commission_fee' not in columns_user_deals:
op.add_column('user_deals', sa.Column('commission_fee', sa.String(), server_default='', nullable=True))
if 'commission_place' not in columns_user_deals:
op.add_column('user_deals', sa.Column('commission_place', sa.String(), server_default='', nullable=True))
columns_user_risk_mgmt = [col['name'] for col in inspector.get_columns('user_risk_management')]
if 'commission_place' not in columns_user_risk_mgmt:
op.add_column('user_risk_management',
sa.Column('commission_place', sa.String(), server_default='', nullable=False))
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user_risk_management', 'commission_place')
op.drop_column('user_deals', 'commission_place')
op.drop_column('user_deals', 'commission_fee')
# ### end Alembic commands ###

View File

@@ -39,7 +39,8 @@ 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
commission_fee = risk_management_data.commission_fee
commission_place = risk_management_data.commission_place
if trade_mode == "Switch":
side = side
@@ -70,7 +71,6 @@ async def start_trading_cycle(
leverage=leverage,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
commission_fee_percent=total_commission
)
if res == "OK":
@@ -78,6 +78,7 @@ async def start_trading_cycle(
tg_id=tg_id,
symbol=symbol,
current_step=1,
current_series=1,
trade_mode=trade_mode,
side_mode=switch_side,
margin_type=margin_type,
@@ -88,7 +89,9 @@ async def start_trading_cycle(
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=order_quantity
base_quantity=order_quantity,
commission_fee=commission_fee,
commission_place=commission_place
)
return "OK"
return (
@@ -119,8 +122,6 @@ 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
@@ -131,6 +132,9 @@ async def trading_cycle_profit(
martingale_factor = user_deals_data.martingale_factor
side_mode = user_deals_data.side_mode
base_quantity = user_deals_data.base_quantity
current_series = user_deals_data.current_series
commission_fee = user_deals_data.commission_fee
commission_place = user_deals_data.commission_place
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
await set_leverage(
@@ -139,7 +143,6 @@ async def trading_cycle_profit(
leverage=leverage,
)
if trade_mode == "Switch":
if side_mode == "Противоположно":
s_side = "Sell" if side == "Buy" else "Buy"
@@ -148,6 +151,8 @@ async def trading_cycle_profit(
else:
s_side = side
next_series = current_series + 1
res = await open_positions(
tg_id=tg_id,
symbol=symbol,
@@ -158,7 +163,6 @@ async def trading_cycle_profit(
leverage=leverage,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
commission_fee_percent=total_fee
)
if res == "OK":
@@ -166,6 +170,7 @@ async def trading_cycle_profit(
tg_id=tg_id,
symbol=symbol,
current_step=1,
current_series=next_series,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
@@ -176,7 +181,9 @@ async def trading_cycle_profit(
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity
base_quantity=base_quantity,
commission_fee=commission_fee,
commission_place=commission_place
)
return "OK"
@@ -202,8 +209,8 @@ async def trading_cycle(
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)
user_risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
commission_fee = user_risk_management_data.commission_fee
commission_fee = user_deals_data.commission_fee
commission_place = user_deals_data.commission_place
total_fee = user_auto_trading_data.total_fee
trade_mode = user_deals_data.trade_mode
margin_type = user_deals_data.margin_type
@@ -217,6 +224,7 @@ async def trading_cycle(
order_quantity = user_deals_data.order_quantity
base_quantity = user_deals_data.base_quantity
side_mode = user_deals_data.side_mode
current_series = user_deals_data.current_series
next_quantity = safe_float(order_quantity) * (
safe_float(martingale_factor)
@@ -232,10 +240,10 @@ async def trading_cycle(
symbol=symbol,
leverage=leverage,
)
total_quantity = next_quantity
if commission_fee == "Yes_commission_fee":
total_fee = total_fee
else:
total_fee = 0
if commission_place == "Commission_for_qty":
total_quantity = next_quantity + total_fee
if trade_mode == "Switch":
if side == "Buy":
@@ -249,13 +257,12 @@ async def trading_cycle(
tg_id=tg_id,
symbol=symbol,
side=r_side,
order_quantity=next_quantity,
order_quantity=total_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":
@@ -263,6 +270,7 @@ async def trading_cycle(
tg_id=tg_id,
symbol=symbol,
current_step=current_step,
current_series=current_series,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
@@ -273,7 +281,9 @@ async def trading_cycle(
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity
base_quantity=base_quantity,
commission_fee=commission_fee,
commission_place=commission_place
)
return "OK"
@@ -303,11 +313,15 @@ async def open_positions(
margin_type: str,
leverage: str,
take_profit_percent: float,
stop_loss_percent: float,
commission_fee_percent: float
stop_loss_percent: float
) -> str | None:
try:
client = await get_bybit_client(tg_id=tg_id)
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
commission_fee = user_deals_data.commission_fee
commission_place = user_deals_data.commission_place
user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol)
total_fee = user_auto_trading_data.total_fee
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)
@@ -315,8 +329,8 @@ async def open_positions(
qty_step = safe_float(qty_step_str)
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)
qty_format = math.floor(qty / qty_step) * qty_step
qty_formatted = round(qty_format, decimals)
if trigger_price > 0:
po_trigger_price = str(trigger_price)
@@ -330,23 +344,28 @@ async def open_positions(
if qty_formatted <= 0:
return "Order does not meet minimum order value"
total_commission = 0
if commission_fee == "Yes_commission_fee":
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 = price_for_cals * (
1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted
1 + take_profit_percent / 100) + total_commission
stop_loss_price = None
else:
take_profit_price = price_for_cals * (
1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted
1 - take_profit_percent / 100) - total_commission
stop_loss_price = None
else:
if side == "Buy":
take_profit_price = price_for_cals * (
1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted
1 + take_profit_percent / 100) + total_commission
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
1 - take_profit_percent / 100) - total_commission
stop_loss_price = price_for_cals * (1 + stop_loss_percent / 100)
take_profit_price = max(take_profit_price, 0)

View File

@@ -118,6 +118,7 @@ class TelegramMessageHandler:
exec_pnl = format_value(execution.get("execPnl"))
total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee
header = (
"Сделка закрыта:" if safe_float(closed_size) > 0 else "Сделка открыта:"
)
@@ -129,11 +130,26 @@ class TelegramMessageHandler:
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
)
if user_deals_data is not None and auto_trading:
commission_fee = user_deals_data.commission_fee
commission_place = user_deals_data.commission_place
if commission_fee == "Yes_commission_fee":
if commission_place == "Commission_for_qty":
total_quantity = safe_float(user_deals_data.order_quantity) + safe_float(
total_fee
)
else:
total_quantity = safe_float(user_deals_data.order_quantity)
else:
total_quantity = safe_float(user_deals_data.order_quantity)
if user_deals_data is not None and auto_trading and safe_float(closed_size) == 0:
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} USDT\n"
text += f"Текущая ставка: {total_quantity:.2f} USDT\n"
text += f"Серия №: {user_deals_data.current_series}\n"
text += f"Сделка №: {user_deals_data.current_step}\n"
text += (
f"Цена исполнения: {exec_price}\n"

View File

@@ -341,3 +341,83 @@ async def set_commission_fee(callback_query: CallbackQuery, state: FSMContext) -
)
finally:
await state.clear()
@router_risk_management.callback_query(F.data == "compensation_commission")
async def compensation_commission(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the 'compensation_commission' callback query.
Clears the current FSM state, edits the message text to display the compensation commission options,
and shows an inline keyboard for selection.
Args:
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
await state.clear()
msg = await callback_query.message.edit_text(
text="Выберите за счет чего будет происходить компенсация комиссии: ",
reply_markup=kbi.commission_place,
)
await state.update_data(prompt_message_id=msg.message_id)
logger.debug(
"Command compensation_commission processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
logger.error(
"Error processing command compensation_commission for user %s: %s",
callback_query.from_user.id,
e,
)
@router_risk_management.callback_query(
lambda c: c.data in ["Commission_for_qty", "Commission_for_tp"]
)
async def set_compensation_commission(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles user input for setting the compensation commission.
Updates FSM context with the selected option and persists the choice in database.
Sends an acknowledgement to user and clears FSM state afterward.
Args:
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
req = await rq.set_commission_place(
tg_id=callback_query.from_user.id, commission_place=callback_query.data
)
if not req:
await callback_query.answer(
text="Произошла ошибка при установке компенсации комиссии. Пожалуйста, попробуйте позже."
)
return
if callback_query.data == "Commission_for_qty":
await callback_query.answer(text="Комиссия компенсируется по ставке.")
else:
await callback_query.answer(text="Комиссия компенсируется по тейк-профиту.")
except Exception as e:
logger.error(
"Error processing command compensation_commission for user %s: %s",
callback_query.from_user.id,
e,
)
finally:
await state.clear()

View File

@@ -130,12 +130,17 @@ async def risk_management(callback_query: CallbackQuery, state: FSMContext) -> N
commission_fee_rus = (
"Да" if commission_fee == "Yes_commission_fee" else "Нет"
)
commission_place = risk_management_data.commission_place
commission_place_rus = (
"Ставке" if commission_place == "Commission_for_qty" else "Тейк-профиту"
)
await callback_query.message.edit_text(
text=f"Риск-менеджмент:\n\n"
f"- Процент изменения цены для фиксации прибыли: {take_profit_percent:.2f}%\n"
f"- Процент изменения цены для фиксации убытка: {stop_loss_percent:.2f}%\n\n"
f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n",
f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n"
f"- Компенсация комиссии по: {commission_place_rus}",
reply_markup=kbi.risk_management,
)
logger.debug(

View File

@@ -210,6 +210,7 @@ risk_management = InlineKeyboardMarkup(
),
],
[InlineKeyboardButton(text="Комиссия биржи", callback_data="commission_fee")],
[InlineKeyboardButton(text="Компенсация комиссии", callback_data="compensation_commission")],
[
InlineKeyboardButton(text="Назад", callback_data="main_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
@@ -239,6 +240,20 @@ commission_fee = InlineKeyboardMarkup(
]
)
commission_place = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="По ставке", callback_data="Commission_for_qty"),
InlineKeyboardButton(text="По тейк-профиту", callback_data="Commission_for_tp"),
],
[
InlineKeyboardButton(text="Назад", callback_data="risk_management"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
# conditions
conditions = InlineKeyboardMarkup(
inline_keyboard=[

View File

@@ -114,6 +114,7 @@ class UserRiskManagement(Base):
take_profit_percent = Column(Float, nullable=False, default=1)
stop_loss_percent = Column(Float, nullable=False, default=1)
commission_fee = Column(String, nullable=False, default="Yes_commission_fee")
commission_place = Column(String, nullable=False, default="Commission_for_qty")
user = relationship("User", back_populates="user_risk_management")
@@ -156,6 +157,9 @@ class UserDeals(Base):
take_profit_percent = Column(Integer, nullable=True)
stop_loss_percent = Column(Integer, nullable=True)
trigger_price = Column(Float, nullable=True)
current_series = Column(Integer, nullable=True)
commission_fee = Column(String, nullable=True)
commission_place = Column(String, nullable=True)
user = relationship("User", back_populates="user_deals")

View File

@@ -629,6 +629,7 @@ async def create_user_risk_management(tg_id: int) -> None:
take_profit_percent=1.0,
stop_loss_percent=1.0,
commission_fee="Yes_commission_fee",
commission_place="Commission_for_qty"
)
session.add(user_risk_management)
await session.commit()
@@ -788,6 +789,47 @@ async def set_commission_fee(tg_id: int, commission_fee: str) -> bool:
return False
async def set_commission_place(tg_id: int, commission_place: str) -> bool:
"""
Set commission place for a user in the database.
:param tg_id: Telegram user ID
:param commission_place: Commission place
:return: True if successful, False otherwise
"""
try:
async with async_session() as session:
result = await session.execute(
select(User)
.options(joinedload(User.user_risk_management))
.filter_by(tg_id=tg_id)
)
user = result.scalars().first()
if user:
if user.user_risk_management:
# Updating existing record
user.user_risk_management.commission_place = commission_place
else:
# Creating new record
user_risk_management = UserRiskManagement(
commission_place=commission_place,
user=user,
)
session.add(user_risk_management)
await session.commit()
logger.info("User commission place updated for user: %s", tg_id)
return True
else:
logger.error("User not found with tg_id: %s", tg_id)
return False
except Exception as e:
logger.error(
"Error adding/updating user commission place for user %s: %s", tg_id, e
)
return False
# USER CONDITIONAL SETTINGS
@@ -935,6 +977,7 @@ async def set_user_deal(
tg_id: int,
symbol: str,
current_step: int,
current_series: int,
trade_mode: str,
side_mode: str,
margin_type: str,
@@ -945,13 +988,16 @@ async def set_user_deal(
max_bets_in_series: int,
take_profit_percent: int,
stop_loss_percent: int,
base_quantity: float
base_quantity: float,
commission_fee: str,
commission_place: str
):
"""
Set the user deal in the database.
:param tg_id: Telegram user ID
:param symbol: Symbol
:param current_step: Current step
:param current_series: Current series
:param trade_mode: Trade mode
:param side_mode: Side mode
:param margin_type: Margin type
@@ -963,6 +1009,8 @@ async def set_user_deal(
:param take_profit_percent: Take profit percent
:param stop_loss_percent: Stop loss percent
:param base_quantity: Base quantity
:param commission_fee: Commission fee
:param commission_place: Commission place
:return: bool
"""
try:
@@ -981,6 +1029,7 @@ async def set_user_deal(
if deal:
# Updating existing record
deal.current_step = current_step
deal.current_series = current_series
deal.trade_mode = trade_mode
deal.side_mode = side_mode
deal.margin_type = margin_type
@@ -992,12 +1041,15 @@ async def set_user_deal(
deal.take_profit_percent = take_profit_percent
deal.stop_loss_percent = stop_loss_percent
deal.base_quantity = base_quantity
deal.commission_fee = commission_fee
deal.commission_place = commission_place
else:
# Creating new record
new_deal = UserDeals(
user=user,
symbol=symbol,
current_step=current_step,
current_series=current_series,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
@@ -1008,7 +1060,9 @@ async def set_user_deal(
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity
base_quantity=base_quantity,
commission_fee=commission_fee,
commission_place=commission_place
)
session.add(new_deal)