2
0
forked from kodorvan/stcs

64 Commits

Author SHA1 Message Date
algizn97
42f0f8ddc0 The text has been corrected 2025-10-23 11:31:15 +05:00
algizn97
3df88d07ab The stop trading button has been added, and the switch mode has been fixed 2025-10-23 11:28:44 +05:00
algizn97
ddfa3a7360 Fixed the switch trading mode, adjusted the take profit, added a trading cycle 2025-10-22 17:15:25 +05:00
algizn97
e61b7334a4 The formula for calculating the number of contracts by price has been changed 2025-10-21 13:59:09 +05:00
algizn97
5ad69f3f6d Fixed take profit calculation. Added a position check for the current pair when trying to change the margin. 2025-10-18 13:52:20 +05:00
algizn97
abad01352a Fixed the output 2025-10-17 11:28:57 +05:00
algizn97
720b30d681 Redundant call removed 2025-10-17 11:13:31 +05:00
algizn97
3616e2cbd3 Added verification for open orders. Adjusted responses for the user 2025-10-17 11:12:50 +05:00
algizn97
7d108337fa Fixed receiving the commission and calculating the total commission 2025-10-17 11:10:35 +05:00
algizn97
0f6e6a2168 Added position mode setting, fixed stop loss calculation 2025-10-17 11:09:21 +05:00
algizn97
258ed970f1 Fixed database creation 2025-10-12 15:08:27 +05:00
algizn97
a3a6509933 Fixed database creation 2025-10-12 15:05:50 +05:00
algizn97
8251938b2f The database has been converted to SQLite 2025-10-12 12:34:32 +05:00
algizn97
458b34fcec The instruction has been corrected 2025-10-11 14:14:27 +05:00
algizn97
4a7577b977 Added migrations for the database 2025-10-11 13:36:38 +05:00
algizn97
6e0a170f4b Merge branch 'devel' of https://git.svoboda.works/Alex/stcs into devel 2025-10-10 15:21:09 +05:00
algizn97
c7b4a08a6a Merge branch 'stable' of https://git.svoboda.works/Alex/stcs into devel 2025-10-10 15:19:31 +05:00
algizn97
d0971f59b4 Added environments 2025-10-10 14:42:47 +05:00
b92376d2da merge upstream 2025-10-10 16:35:16 +07:00
630f2002d3 Удалить alembic.ini 2025-10-10 16:24:01 +07:00
0784cbb54a Удалить alembic/versions/fd8581c0cc87_updated_leverage.py 2025-10-10 16:23:51 +07:00
eeb7f81440 Удалить alembic/versions/f00a94ccdf01_updated_deals.py 2025-10-10 16:23:47 +07:00
b03d05bb75 Удалить alembic/versions/ef38c90eed55_added_last_side_the_conditional_data.py 2025-10-10 16:23:43 +07:00
e0e4ad5d4b Удалить alembic/versions/ef342b38e17b_added_fee_user_deals.py 2025-10-10 16:23:39 +07:00
fab8ff5040 Удалить alembic/versions/dbffe818030c_added_last_side_and_auto_trading_for_.py 2025-10-10 16:23:35 +07:00
8071f8c896 Удалить alembic/versions/d3c85bad8c98_added_limit_and_trigger_price_for_user_.py 2025-10-10 16:23:30 +07:00
3db001bd19 Удалить alembic/versions/ccdc5764eb4f_added_userdeals.py 2025-10-10 16:23:26 +07:00
99c59be9ed Удалить alembic/versions/acbcc95de48d_updated_userdeals.py 2025-10-10 16:23:21 +07:00
37b7b6effd Удалить alembic/versions/c98b9dc36d15_fixed_auto_trade.py 2025-10-10 16:23:17 +07:00
ee285523f2 Удалить alembic/versions/8f1476c68efa_added_position_idx_for_user_deals_table.py 2025-10-10 16:23:12 +07:00
b426eb2136 Удалить alembic/versions/968f8121104f_updated_user_deals_and_user_conditional_.py 2025-10-10 16:23:07 +07:00
2df3b8b40d Удалить alembic/versions/c710f4e2259c_unnecessary_data_has_been_deleted.py 2025-10-10 16:23:03 +07:00
8c08451d82 Удалить alembic/versions/863d6215e1eb_updated_deals.py 2025-10-10 16:22:52 +07:00
d81a47b669 Удалить alembic/versions/77197715747c_deleted_position_idx_for_user_deals_.py 2025-10-10 16:22:48 +07:00
2cdfba3537 Удалить alembic/versions/73a00faa4f7f_added_user_auto_trading_table.py 2025-10-10 16:22:43 +07:00
c89c2ad803 Удалить alembic/versions/70094ba27e80_create_user_conditional_setting.py 2025-10-10 16:22:39 +07:00
3986989dbd Удалить alembic/versions/42c66cfe8d4e_updated_martingale_factor.py 2025-10-10 16:22:35 +07:00
c0e40dc205 Удалить alembic/versions/3534adf891fc_update_last_side_the_conditional_data.py 2025-10-10 16:22:30 +07:00
6c6f0dbb7b Удалить alembic/versions/10bf073c71f9_added_fee_for_user_auto_trading.py 2025-10-10 16:22:26 +07:00
44c4fde036 Удалить alembic/versions/45977e9d8558_updated_order_quantity.py 2025-10-10 16:22:21 +07:00
21a93d47d4 Удалить alembic/versions/2b9572b49ecd_added_side_for_user_auto_trading.py 2025-10-10 16:22:18 +07:00
3f43d42651 Удалить alembic/versions/0eed68eddcdb_added_conditional_order_type.py 2025-10-10 16:22:13 +07:00
aab05994ce Удалить alembic/versions/09db71875980_updated_user_deals_table.py 2025-10-10 16:22:08 +07:00
a58ebe6a46 Удалить alembic/README 2025-10-10 16:21:38 +07:00
1ec1f1784d Удалить alembic/script.py.mako 2025-10-10 16:21:33 +07:00
7901af86af Удалить alembic/env.py 2025-10-10 16:21:24 +07:00
fedfa00c10 Merge pull request 'devel' (#3) from devel into stable
Reviewed-on: #3
2025-10-10 16:18:23 +07:00
algizn97
fc8ab19ae9 Fixed the budget calculation function 2025-10-10 14:14:46 +05:00
algizn97
42c4660fe3 The price of the trading pair has been removed, and the trade cancellation button has been removed. The text has been corrected 2025-10-10 13:31:52 +05:00
algizn97
fe030baef5 When choosing a coin, the leverage is set to the maximum possible for this coin, also SL in accordance with the leverage. The verification range from 1 to 100 has been removed, now the verification is within the acceptable values from the exchange 2025-10-10 13:30:32 +05:00
algizn97
9d06412605 Added the ability to summarize all commissions within a series.Minor bugs have been fixed 2025-10-10 13:28:12 +05:00
algizn97
9c1f289870 The currency of the coin is treason on USDT, unnecessary parameters are removed 2025-10-10 13:26:56 +05:00
algizn97
3533e7e99a Unnecessary buttons have been removed, the buttons of the trading mode and the direction of the first transaction of the series have been moved. 2025-10-10 13:25:30 +05:00
algizn97
8114533475 When adjusting the leverage, the SL changes according to the criteria. In place of the position mode, there is now a trading mode. All unnecessary functions are also removed. 2025-10-10 13:24:32 +05:00
algizn97
fcdc9d7483 The function allows you to write the number 0 2025-10-10 13:22:13 +05:00
algizn97
aa9f04c27e The stop trading button has been removed. 2025-10-10 13:21:15 +05:00
algizn97
89ab106992 Fixed percentages of TP and SL from integers to floats 2025-10-10 13:20:24 +05:00
algizn97
ebe2d58975 The trading mode has been moved to the main settings, Position mode, limit order and conditional order have been removed. The number of bids has been renamed to the base rate. The choice of the direction of the first transaction has been moved to the main settings 2025-10-10 13:18:43 +05:00
algizn97
09606a057b Added the addition of a common commission 2025-10-10 13:16:00 +05:00
algizn97
a0a2fd30f0 TP AND SL have been converted to float. Switch control has been moved to the main settings, Removed unnecessary parameters 2025-10-10 13:14:59 +05:00
algizn97
2136de5d69 Added alembic migrations 2025-10-09 14:21:54 +05:00
algizn97
dbbea16c19 Added alembic 2025-10-09 14:21:21 +05:00
898ff91392 Merge pull request 'added the exc_info flag' (#10) from Alex/stcs:dev into stable
Reviewed-on: kodorvan/stcs#10
2025-10-07 12:10:58 +07:00
algizn97
f5677e6e7e added the exc_info flag 2025-10-07 09:44:02 +05:00
29 changed files with 1095 additions and 2247 deletions

View File

@@ -1,6 +1 @@
BOT_TOKEN=YOUR_BOT_TOKEN
DB_USER=your_username
DB_PASS=your_password
DB_HOST=your_host
DB_PORT=your_port
DB_NAME=your_database
BOT_TOKEN=YOUR_BOT_TOKEN

6
.gitignore vendored
View File

@@ -146,9 +146,9 @@ myenv
ENV/
env.bak/
venv.bak/
/alembic/versions
/alembic
alembic.ini
/logger_helper/loggers
/app/bybit/logger_bybit/loggers
*.db
# Spyder project settings
.spyderproject
.spyproject

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. Запустите бота:

147
alembic.ini Normal file
View File

@@ -0,0 +1,147 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# 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/db/stcs.db
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

1
alembic/README Normal file
View File

@@ -0,0 +1 @@
Generic single-database configuration.

53
alembic/env.py Normal file
View File

@@ -0,0 +1,53 @@
import asyncio
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
from database.models import Base
target_metadata = Base.metadata
def do_run_migrations(connection):
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations():
connectable = async_engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

28
alembic/script.py.mako Normal file
View File

@@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}

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

@@ -7,25 +7,25 @@ logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("close_positions")
async def close_position(
tg_id: int, symbol: str, side: str, position_idx: int, qty: float
async def close_position_by_symbol(
tg_id: int, symbol: str
) -> bool:
"""
Closes all positions
:param tg_id: Telegram user ID
:param symbol: symbol
:param side: side
:param position_idx: position index
:param qty: quantity
:return: bool
"""
try:
client = await get_bybit_client(tg_id)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
response = client.get_positions(
category="linear", symbol=symbol
)
positions = response.get("result", {}).get("list", [])
r_side = "Sell" if positions[0].get("side") == "Buy" else "Buy"
qty = positions[0].get("size")
position_idx = positions[0].get("positionIdx")
response = client.place_order(
category="linear",
@@ -37,16 +37,16 @@ async def close_position(
positionIdx=position_idx,
)
if response["retCode"] == 0:
logger.info("All positions closed for %s for user %s", symbol, tg_id)
logger.info("Positions closed for %s for user %s", symbol, tg_id)
return True
else:
logger.error(
"Error closing all positions for %s for user %s", symbol, tg_id
"Error closing position for %s for user %s", symbol, tg_id
)
return False
except Exception as e:
logger.error(
"Error closing all positions for %s for user %s: %s", symbol, tg_id, e
"Error closing positions for %s for user %s: %s", symbol, tg_id, e
)
return False

View File

@@ -1,130 +1,125 @@
import logging.config
import math
from pybit.exceptions import InvalidRequestError
import database.request as rq
from app.bybit import get_bybit_client
from app.bybit.get_functions.get_balance import get_balance
from app.bybit.get_functions.get_instruments_info import get_instruments_info
from app.bybit.get_functions.get_tickers import get_tickers
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
from app.bybit.set_functions.set_leverage import (
set_leverage,
set_leverage_to_buy_and_sell,
)
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 check_limit_price, 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, side: str, switch_side_mode: bool
tg_id: int
) -> str | None:
"""
Start trading cycle
:param tg_id: Telegram user ID
:param side: Buy or Sell
:param switch_side_mode: switch_side_mode
"""
try:
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)
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
)
trade_mode = additional_data.trade_mode
switch_side = additional_data.switch_side
margin_type = additional_data.margin_type
leverage = additional_data.leverage
leverage_to_buy = additional_data.leverage_to_buy
leverage_to_sell = additional_data.leverage_to_sell
order_type = additional_data.order_type
conditional_order_type = additional_data.conditional_order_type
order_quantity = additional_data.order_quantity
limit_price = additional_data.limit_price
trigger_price = additional_data.trigger_price
martingale_factor = additional_data.martingale_factor
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
max_risk_percent = risk_management_data.max_risk_percent
total_commission = 0
mode = 3 if trade_mode == "Both_Sides" else 0
await set_switch_position_mode(tg_id=tg_id, symbol=symbol, mode=mode)
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
if trade_mode == "Both_Sides" and margin_type == "ISOLATED_MARGIN":
await set_leverage_to_buy_and_sell(
tg_id=tg_id,
symbol=symbol,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
)
get_side = "Buy"
if user_deals_data:
get_side = user_deals_data.last_side or "Buy"
if trade_mode == "Switch":
if switch_side == "По направлению":
side = get_side
else:
if get_side == "Buy":
side = "Sell"
else:
side = "Buy"
else:
await set_leverage(
tg_id=tg_id,
symbol=symbol,
leverage=leverage,
)
if trade_mode == "Long":
side = "Buy"
else:
side = "Sell"
await set_switch_position_mode(
tg_id=tg_id,
symbol=symbol,
mode=0)
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,
side=side,
order_type=order_type,
conditional_order_type=conditional_order_type,
order_quantity=order_quantity,
limit_price=limit_price,
trigger_price=trigger_price,
trade_mode=trade_mode,
margin_type=margin_type,
leverage=leverage,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
max_risk_percent=max_risk_percent,
commission_fee_percent=total_commission
)
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=switch_side,
margin_type=margin_type,
leverage=leverage,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
order_type="Market",
conditional_order_type=conditional_order_type,
order_quantity=order_quantity,
limit_price=limit_price,
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,
max_risk_percent=max_risk_percent,
switch_side_mode=switch_side_mode,
base_quantity=order_quantity
)
return "OK"
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",
}
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
)
@@ -133,116 +128,171 @@ async def start_trading_cycle(
return None
async def trading_cycle(
tg_id: int, symbol: str, reverse_side: str, size: str
) -> str | 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
order_type = user_deals_data.order_type
conditional_order_type = user_deals_data.conditional_order_type
margin_type = user_deals_data.margin_type
leverage = user_deals_data.leverage
leverage_to_buy = user_deals_data.leverage_to_buy
leverage_to_sell = user_deals_data.leverage_to_sell
limit_price = user_deals_data.limit_price
trigger_price = user_deals_data.trigger_price
trigger_price = 0
take_profit_percent = user_deals_data.take_profit_percent
stop_loss_percent = user_deals_data.stop_loss_percent
max_risk_percent = user_deals_data.max_risk_percent
max_bets_in_series = user_deals_data.max_bets_in_series
martingale_factor = user_deals_data.martingale_factor
current_step = user_deals_data.current_step
switch_side_mode = user_deals_data.switch_side_mode
side_mode = user_deals_data.side_mode
base_quantity = user_deals_data.base_quantity
mode = 3 if trade_mode == "Both_Sides" else 0
await set_switch_position_mode(tg_id=tg_id, symbol=symbol, mode=mode)
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
if trade_mode == "Both_Sides" and margin_type == "ISOLATED_MARGIN":
await set_leverage_to_buy_and_sell(
tg_id=tg_id,
symbol=symbol,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
)
else:
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 switch_side_mode:
side = "Sell" if real_side == "Buy" else "Buy"
next_quantity = safe_float(size) * (
safe_float(martingale_factor) ** current_step
await set_leverage(
tg_id=tg_id,
symbol=symbol,
leverage=leverage,
)
current_step += 1
if max_bets_in_series < current_step:
return "Max bets in series"
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=side,
order_type="Market",
conditional_order_type=conditional_order_type,
order_quantity=next_quantity,
limit_price=limit_price,
side=s_side,
order_quantity=base_quantity,
trigger_price=trigger_price,
trade_mode=trade_mode,
margin_type=margin_type,
leverage=leverage,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
max_risk_percent=max_risk_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=current_step,
current_step=1,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
leverage=leverage,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
order_type=order_type,
conditional_order_type=conditional_order_type,
order_quantity=next_quantity,
limit_price=limit_price,
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,
max_risk_percent=max_risk_percent,
switch_side_mode=switch_side_mode,
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",
}
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, 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
current_step = user_deals_data.current_step
order_quantity = user_deals_data.order_quantity
base_quantity = user_deals_data.base_quantity
side_mode = user_deals_data.side_mode
next_quantity = safe_float(order_quantity) * (
safe_float(martingale_factor)
)
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,
symbol=symbol,
leverage=leverage,
)
if trade_mode == "Switch":
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
else:
r_side = side
res = await open_positions(
tg_id=tg_id,
symbol=symbol,
side=r_side,
order_quantity=next_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,
current_step=current_step,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
leverage=leverage,
order_quantity=next_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
)
@@ -252,140 +302,59 @@ async def trading_cycle(
async def open_positions(
tg_id: int,
side: str,
symbol: str,
order_type: str,
conditional_order_type: str,
order_quantity: float,
limit_price: float,
trigger_price: float,
trade_mode: str,
margin_type: str,
leverage: str,
leverage_to_buy: str,
leverage_to_sell: str,
take_profit_percent: float,
stop_loss_percent: float,
max_risk_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)
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
commission_fee = risk_management_data.commission_fee
wallet = await get_balance(tg_id=tg_id)
user_balance = wallet.get("totalWalletBalance", 0)
instruments_resp = await get_instruments_info(tg_id=tg_id, symbol=symbol)
get_order_prices = instruments_resp.get("priceFilter")
min_price = safe_float(get_order_prices.get("minPrice"))
max_price = safe_float(get_order_prices.get("maxPrice"))
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(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)
if order_type == "Conditional":
if trigger_price > 0:
po_trigger_price = str(trigger_price)
trigger_direction = 1 if trigger_price > price_symbol else 2
if conditional_order_type == "Limit":
error = check_limit_price(limit_price, min_price, max_price)
if error in {
"Limit price is out min price",
"Limit price is out max price",
}:
return error
order_type = "Limit"
price_for_calc = limit_price
tpsl_mode = "Partial"
else:
order_type = "Market"
price_for_calc = trigger_price
tpsl_mode = "Full"
else:
if order_type == "Limit":
error = check_limit_price(limit_price, min_price, max_price)
if error in {
"Limit price is out min price",
"Limit price is out max price",
}:
return error
price_for_calc = limit_price
tpsl_mode = "Partial"
else:
order_type = "Market"
price_for_calc = price_symbol
tpsl_mode = "Full"
po_trigger_price = None
trigger_direction = None
if trade_mode == "Both_Sides":
po_position_idx = 1 if side == "Buy" else 2
if margin_type == "ISOLATED_MARGIN":
get_leverage = safe_float(
leverage_to_buy if side == "Buy" else leverage_to_sell
)
else:
get_leverage = safe_float(leverage)
else:
po_position_idx = 0
get_leverage = safe_float(leverage)
price_for_cals = trigger_price if po_trigger_price is not None else price_symbol
potential_loss = (
safe_float(order_quantity)
* safe_float(price_for_calc)
* (stop_loss_percent / 100)
)
adjusted_loss = potential_loss / get_leverage
allowed_loss = safe_float(user_balance) * (max_risk_percent / 100)
if adjusted_loss > allowed_loss:
return "Risk is too high for this trade"
# 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"]
)
total_commission = price_for_calc * order_quantity * commission_fee_percent
tp_multiplier = 1 + (take_profit_percent / 100)
if total_commission > 0:
tp_multiplier += total_commission
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_calc,
symbol=symbol,
leverage=get_leverage,
)
if (liq_long > 0 or liq_short > 0) and price_for_calc > 0:
if side == "Buy":
base_tp = price_for_calc + (price_for_calc - liq_long)
take_profit_price = base_tp + total_commission
else:
base_tp = price_for_calc - (liq_short - price_for_calc)
take_profit_price = base_tp - total_commission
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_calc * tp_multiplier
stop_loss_price = price_for_calc * (1 - stop_loss_percent / 100)
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_calc * (
1 - (take_profit_percent / 100) - total_commission
)
stop_loss_price = price_for_calc * (1 + stop_loss_percent / 100)
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)
stop_loss_price = max(stop_loss_price, 0)
@@ -395,24 +364,18 @@ async def open_positions(
"category": "linear",
"symbol": symbol,
"side": side,
"orderType": order_type,
"qty": str(order_quantity),
"orderType": "Market",
"qty": str(qty_formatted),
"triggerDirection": trigger_direction,
"triggerPrice": po_trigger_price,
"triggerBy": "LastPrice",
"timeInForce": "GTC",
"positionIdx": po_position_idx,
"tpslMode": tpsl_mode,
"positionIdx": 0,
"tpslMode": "Full",
"takeProfit": str(take_profit_price) if take_profit_price else None,
"stopLoss": str(stop_loss_price) if stop_loss_price else None,
}
if order_type == "Conditional":
if conditional_order_type == "Limit":
order_params["price"] = str(limit_price)
if order_type == "Limit":
order_params["price"] = str(limit_price)
response = client.place_order(**order_params)
if response["retCode"] == 0:
@@ -430,6 +393,8 @@ async def open_positions(
"ab not enough for new order": "ab not enough for new order",
"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",
}
for key, msg in known_errors.items():
if key in error_text:
@@ -439,5 +404,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

@@ -6,7 +6,6 @@ from aiogram.types import Message
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.bybit.get_functions.get_balance import get_balance
from app.bybit.get_functions.get_tickers import get_tickers
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
@@ -22,19 +21,20 @@ async def user_profile_bybit(tg_id: int, message: Message, state: FSMContext) ->
if wallet:
balance = wallet.get("totalWalletBalance", "0")
symbol = await rq.get_user_symbol(tg_id=tg_id)
get_tickers_info = await get_tickers(tg_id=tg_id, symbol=symbol)
price_symbol = get_tickers_info.get("lastPrice") or 0
await message.answer(
text=f"💎Ваш профиль Bybit:\n\n"
f"⚖️ Баланс: {float(balance):,.2f} USD\n"
f"📊Торговая пара: {symbol}\n"
f"$$$ Цена: {float(price_symbol):,.4f}\n\n"
f"Краткая инструкция:\n"
f"1. Укажите торговую пару (например: BTCUSDT).\n"
f"2. В настройках выставьте все необходимые параметры.\n"
f"3. Нажмите кнопку 'Начать торговлю' и выберите режим торговли.\n",
reply_markup=kbi.main_menu,
)
if symbol is None:
await rq.set_user_symbol(tg_id=tg_id, symbol="BTCUSDT")
await user_profile_bybit(tg_id=tg_id, message=message, state=state)
else:
await message.answer(
text=f"💎Ваш профиль:\n\n"
f"⚖️ Баланс: {float(balance):,.2f} USD\n"
f"📊Торговая пара: {symbol}\n\n"
f"Краткая инструкция:\n"
f"1. Укажите торговую пару (например: BTCUSDT).\n"
f"2. В настройках выставьте все необходимые параметры.\n"
f"3. Нажмите кнопку 'Начать торговлю'.\n",
reply_markup=kbi.main_menu,
)
else:
await message.answer(
text="Ошибка при подключении, повторите попытку",

View File

@@ -3,8 +3,8 @@ 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.helper_functions import format_value, safe_float, safe_int
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)
logger = logging.getLogger("telegram_message_handler")
@@ -22,12 +22,6 @@ class TelegramMessageHandler:
order_data = message.get("data", [{}])[0]
symbol = format_value(order_data.get("symbol"))
qty = format_value(order_data.get("qty"))
order_type = format_value(order_data.get("orderType"))
order_type_rus = (
"Рыночный"
if order_type == "Market"
else "Лимитный" if order_type == "Limit" else "Нет данных"
)
side = format_value(order_data.get("side"))
side_rus = (
"Покупка"
@@ -40,41 +34,33 @@ class TelegramMessageHandler:
take_profit = format_value(order_data.get("takeProfit"))
stop_loss = format_value(order_data.get("stopLoss"))
position_idx = safe_int(order_data.get("positionIdx"))
position_idx_rus = (
"Односторонний"
if position_idx == 0
else (
"Покупка в режиме хеджирования"
if position_idx == 1
else (
"Продажа в режиме хеджирования"
if position_idx == 2
else "Нет данных"
)
)
)
status_map = {
"New": "Ордер создан",
"Cancelled": "Ордер отменен",
"Deactivated": "Ордер деактивирован",
"Untriggered": "Условный ордер выставлен",
}
if order_status == "Filled" or order_status not in status_map:
return None
status_text = status_map[order_status]
user_auto_trading = await rq.get_user_auto_trading(
tg_id=tg_id, symbol=symbol
)
auto_trading = (
user_auto_trading.auto_trading if user_auto_trading else False
)
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
)
text = (
f"{status_text}:\n"
f"Торговая пара: {symbol}\n"
f"Режим позиции: {position_idx_rus}\n"
f"Количество: {qty}\n"
f"Тип ордера: {order_type_rus}\n"
f"Движение: {side_rus}\n"
)
if user_deals_data is not None and auto_trading:
text += f"Текущая ставка: {user_deals_data.order_quantity} USDT\n"
else:
text += f"Количество: {qty}\n"
if price and price != "0":
text += f"Цена: {price}\n"
if take_profit and take_profit != "Нет данных":
@@ -96,33 +82,35 @@ class TelegramMessageHandler:
closed_size = format_value(execution.get("closedSize"))
symbol = format_value(execution.get("symbol"))
exec_price = format_value(execution.get("execPrice"))
exec_fee = format_value(execution.get("execFee"))
exec_qty = format_value(execution.get("execQty"))
order_type = format_value(execution.get("orderType"))
order_type_rus = (
"Рыночный"
if order_type == "Market"
else "Лимитный" if order_type == "Limit" else "Нет данных"
)
exec_fees = format_value(execution.get("execFee"))
fee_rate = format_value(execution.get("feeRate"))
side = format_value(execution.get("side"))
side_rus = (
"Покупка"
if side == "Buy"
else "Продажа" if side == "Sell" else "Нет данных"
)
if safe_float(exec_fees) == 0:
exec_fee = safe_float(exec_price) * safe_float(exec_qty) * safe_float(
fee_rate
)
else:
exec_fee = safe_float(exec_fees)
if safe_float(closed_size) == 0:
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, side=side, fee=safe_float(exec_fee)
tg_id=tg_id, symbol=symbol, fee=safe_float(exec_fee)
)
if side == "Buy":
res_side = "Sell"
else:
res_side = "Buy"
user_auto_trading = await rq.get_user_auto_trading(
tg_id=tg_id, symbol=symbol, side=res_side
tg_id=tg_id, symbol=symbol
)
get_total_fee = user_auto_trading.total_fee
total_fee = safe_float(exec_fee) + safe_float(get_total_fee)
if user_auto_trading is not None and user_auto_trading.fee is not None:
fee = user_auto_trading.fee
else:
@@ -142,27 +130,32 @@ class TelegramMessageHandler:
)
text = f"{header}\n" f"Торговая пара: {symbol}\n"
if safe_float(closed_size) > 0:
text += f"Количество закрытых сделок: {closed_size}\n"
auto_trading = (
user_auto_trading.auto_trading if user_auto_trading else False
)
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:
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"Цена исполнения: {exec_price}\n"
f"Количество исполненных сделок: {exec_qty}\n"
f"Тип ордера: {order_type_rus}\n"
f"Движение: {side_rus}\n"
f"Комиссия за сделку: {exec_fee}\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(
chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
)
auto_trading = (
user_auto_trading.auto_trading if user_auto_trading else False
)
user_symbols = user_auto_trading.symbol if user_auto_trading else None
if (
@@ -171,31 +164,27 @@ 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
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False, side=r_side
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
)
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
if user_deals_data and user_deals_data.switch_side_mode:
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False, side=side
)
else:
open_order_text = "\n❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n"
await self.telegram_bot.send_message(
chat_id=tg_id, text=open_order_text
)
res = await trading_cycle(
tg_id=tg_id, symbol=symbol, reverse_side=side, size=closed_size
res = await trading_cycle_profit(
tg_id=tg_id, symbol=symbol, side=r_side
)
if res == "OK":
@@ -206,32 +195,69 @@ class TelegramMessageHandler:
"Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
"ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
"InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
"The number of contracts exceeds maximum limit allowed": "❗️ Количество контрактов превышает допустимое максимальное количество контрактов",
"The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки",
}
error_text = errors.get(
res, "❗️ Не удалось открыть новую сделку"
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False, side=r_side
tg_id=tg_id, symbol=symbol, auto_trading=False
)
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
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
)
if user_deals_data and user_deals_data.switch_side_mode:
await rq.set_auto_trading(
tg_id=tg_id,
symbol=symbol,
auto_trading=False,
side=side,
)
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 = {
"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,
)
except Exception as e:
logger.error("Error in telegram_message_handler: %s", e)

View File

@@ -46,7 +46,9 @@ class WebSocketBot:
self.user_sockets.clear()
self.user_messages.clear()
self.user_keys.clear()
logger.info("Closed old websocket for user %s due to key change", tg_id)
logger.info(
"Closed old websocket for user %s due to key change", tg_id
)
success = await self.try_connect_user(api_key, api_secret, tg_id)
if success:

View File

@@ -34,7 +34,7 @@ def is_number(value: str) -> bool:
# Convert the string to a float
num = float(value)
# Check if the number is positive
if num <= 0:
if num < 0:
return False
# Check if the string contains "+" or "-"
if "+" in value or "-" in value:
@@ -158,7 +158,7 @@ async def get_liquidation_price(
async def calculate_total_budget(
quantity, martingale_factor, max_steps, commission_fee_percent
quantity, martingale_factor, max_steps
) -> float:
"""
Calculate the total budget for a series of trading steps.
@@ -167,7 +167,6 @@ async def calculate_total_budget(
quantity (float): The initial quantity of the asset.
martingale_factor (float): The factor by which the quantity is multiplied for each step.
max_steps (int): The maximum number of trading steps.
commission_fee_percent (float): The commission fee percentage.
Returns:
float: The total budget for the series of trading steps.
@@ -175,12 +174,8 @@ async def calculate_total_budget(
total = 0
for step in range(max_steps):
set_quantity = quantity * (martingale_factor**step)
if commission_fee_percent == 0:
# Commission fee is not added to the position size
r_quantity = set_quantity
else:
# Commission fee is added to the position size
r_quantity = set_quantity * (1 + 2 * commission_fee_percent)
r_quantity = set_quantity
total += r_quantity
return total

View File

@@ -7,13 +7,12 @@ from aiogram.types import CallbackQuery, Message
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.bybit.get_functions.get_tickers import get_tickers
from app.bybit.get_functions.get_instruments_info import get_instruments_info
from app.bybit.profile_bybit import user_profile_bybit
from app.bybit.set_functions.set_leverage import (
set_leverage,
set_leverage_to_buy_and_sell,
)
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 safe_float
from app.telegram.states.states import ChangingTheSymbolState
from logger_helper.logger_helper import LOGGING_CONFIG
@@ -91,12 +90,7 @@ async def set_symbol(message: Message, state: FSMContext) -> None:
await rq.create_user_additional_settings(tg_id=message.from_user.id)
return
trade_mode = additional_settings.trade_mode or "Merged_Single"
mode = 0 if trade_mode == "Merged_Single" else 3
margin_type = additional_settings.margin_type or "ISOLATED_MARGIN"
leverage = "10"
leverage_to_buy = "10"
leverage_to_sell = "10"
ticker = await get_tickers(tg_id=message.from_user.id, symbol=symbol)
if ticker is None:
@@ -105,6 +99,8 @@ async def set_symbol(message: Message, state: FSMContext) -> None:
)
return
instruments_info = await get_instruments_info(tg_id=message.from_user.id, symbol=symbol)
max_leverage = instruments_info.get("leverageFilter").get("maxLeverage")
req = await rq.set_user_symbol(tg_id=message.from_user.id, symbol=symbol)
if not req:
@@ -118,45 +114,20 @@ async def set_symbol(message: Message, state: FSMContext) -> None:
tg_id=message.from_user.id, message=message, state=state
)
res = await set_switch_position_mode(
tg_id=message.from_user.id, symbol=symbol, mode=mode
)
if res == "You have an existing position, so position mode cannot be switched":
if mode == 0:
mode = 3
else:
mode = 0
await set_switch_position_mode(
tg_id=message.from_user.id, symbol=symbol, mode=mode
)
if trade_mode == "Merged_Single":
trade_mode = "Both_Sides"
else:
trade_mode = "Merged_Single"
await rq.set_trade_mode(tg_id=message.from_user.id, trade_mode=trade_mode)
await set_margin_mode(tg_id=message.from_user.id, margin_mode=margin_type)
if margin_type == "ISOLATED_MARGIN":
await set_leverage_to_buy_and_sell(
tg_id=message.from_user.id,
symbol=symbol,
leverage_to_buy=str(leverage_to_buy),
leverage_to_sell=str(leverage_to_sell),
)
else:
await set_leverage(
tg_id=message.from_user.id, symbol=symbol, leverage=str(leverage)
)
await rq.set_leverage(tg_id=message.from_user.id, leverage=str(leverage))
await rq.set_leverage_to_buy_and_sell(
tg_id=message.from_user.id,
leverage_to_buy=str(leverage_to_buy),
leverage_to_sell=str(leverage_to_sell),
await set_leverage(
tg_id=message.from_user.id, symbol=symbol, leverage=str(max_leverage)
)
await rq.set_limit_price(tg_id=message.from_user.id, limit_price=0)
await rq.set_leverage(tg_id=message.from_user.id, leverage=str(max_leverage))
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)
await state.clear()
except Exception as e:

View File

@@ -4,9 +4,6 @@ from aiogram import Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
import database.request as rq
from app.bybit.close_positions import cancel_order, close_position
from app.helper_functions import safe_float
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
@@ -28,31 +25,6 @@ async def close_position_handler(
:return: None
"""
try:
data = callback_query.data
parts = data.split("_")
symbol = parts[2]
side = parts[3]
position_idx = int(parts[4])
qty = safe_float(parts[5])
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
side=side,
)
res = await close_position(
tg_id=callback_query.from_user.id,
symbol=symbol,
side=side,
position_idx=position_idx,
qty=qty,
)
if not res:
await callback_query.answer(text="Произошла ошибка при закрытии позиции.")
return
await callback_query.answer(text="Позиция успешно закрыта.")
logger.debug(
"Command close_position processed successfully for user: %s",
callback_query.from_user.id,
@@ -81,19 +53,6 @@ async def cancel_order_handler(
:return: None
"""
try:
data = callback_query.data
parts = data.split("_")
symbol = parts[2]
order_id = parts[3]
res = await cancel_order(
tg_id=callback_query.from_user.id, symbol=symbol, order_id=order_id
)
if not res:
await callback_query.answer(text="Произошла ошибка при закрытии ордера.")
return
await callback_query.answer(text="Ордер успешно закрыт.")
logger.debug(
"Command close_order processed successfully for user: %s",
callback_query.from_user.id,

View File

@@ -53,7 +53,7 @@ async def cmd_start(message: Message, state: FSMContext) -> None:
await rq.create_user_conditional_settings(tg_id=tg_id)
await message.answer(
text=f"Добро пожаловать, {full_name}!\n\n"
"PHANTOM TRADING - ваш надежный помощник для автоматизации трейдинга😉",
"Чат-робот для трейдинга - ваш надежный помощник для анализа рынка и принятия взвешенных решений.😉",
reply_markup=kbi.connect_the_platform,
)
logger.debug(

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ from aiogram.types import CallbackQuery, Message
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.helper_functions import is_int
from app.helper_functions import is_number, safe_float
from app.telegram.states.states import RiskManagementState
from logger_helper.logger_helper import LOGGING_CONFIG
@@ -86,7 +86,7 @@ async def set_take_profit_percent(message: Message, state: FSMContext) -> None:
take_profit_percent_value = message.text
if not is_int(take_profit_percent_value):
if not is_number(take_profit_percent_value):
await message.answer(
text="Ошибка: введите валидное число.",
reply_markup=kbi.back_to_risk_management,
@@ -98,7 +98,7 @@ async def set_take_profit_percent(message: Message, state: FSMContext) -> None:
)
return
if int(take_profit_percent_value) < 1 or int(take_profit_percent_value) > 100:
if safe_float(take_profit_percent_value) < 1 or safe_float(take_profit_percent_value) > 100:
await message.answer(
text="Ошибка: введите число от 1 до 100.",
reply_markup=kbi.back_to_risk_management,
@@ -112,7 +112,7 @@ async def set_take_profit_percent(message: Message, state: FSMContext) -> None:
req = await rq.set_take_profit_percent(
tg_id=message.from_user.id,
take_profit_percent=int(take_profit_percent_value),
take_profit_percent=safe_float(take_profit_percent_value),
)
if req:
@@ -207,7 +207,7 @@ async def set_stop_loss_percent(message: Message, state: FSMContext) -> None:
stop_loss_percent_value = message.text
if not is_int(stop_loss_percent_value):
if not is_number(stop_loss_percent_value):
await message.answer(
text="Ошибка: введите валидное число.",
reply_markup=kbi.back_to_risk_management,
@@ -219,7 +219,7 @@ async def set_stop_loss_percent(message: Message, state: FSMContext) -> None:
)
return
if int(stop_loss_percent_value) < 1 or int(stop_loss_percent_value) > 100:
if safe_float(stop_loss_percent_value) < 1 or safe_float(stop_loss_percent_value) > 100:
await message.answer(
text="Ошибка: введите число от 1 до 100.",
reply_markup=kbi.back_to_risk_management,
@@ -232,7 +232,7 @@ async def set_stop_loss_percent(message: Message, state: FSMContext) -> None:
return
req = await rq.set_stop_loss_percent(
tg_id=message.from_user.id, stop_loss_percent=int(stop_loss_percent_value)
tg_id=message.from_user.id, stop_loss_percent=safe_float(stop_loss_percent_value)
)
if req:
@@ -262,130 +262,6 @@ async def set_stop_loss_percent(message: Message, state: FSMContext) -> None:
)
@router_risk_management.callback_query(F.data == "max_risk_percent")
async def max_risk_percent(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the 'max_risk_percent' callback query.
Clears the current FSM state, edits the message text to display the maximum risk percentage 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()
await state.set_state(RiskManagementState.max_risk_percent_state)
msg = await callback_query.message.edit_text(
text="Введите максимальный процент риска: ",
reply_markup=kbi.back_to_risk_management,
)
await state.update_data(prompt_message_id=msg.message_id)
logger.debug(
"Command max_risk_percent processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
logger.error(
"Error processing command max_risk_percent for user %s: %s",
callback_query.from_user.id,
e,
)
@router_risk_management.message(RiskManagementState.max_risk_percent_state)
async def set_max_risk_percent(message: Message, state: FSMContext) -> None:
"""
Handles user input for setting the maximum risk percentage.
Updates FSM context with the selected percentage and persists the choice in database.
Sends an acknowledgement to user and clears FSM state afterward.
Args:
message (Message): Incoming message from user containing the maximum risk percentage.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
try:
data = await state.get_data()
if "prompt_message_id" in data:
prompt_message_id = data["prompt_message_id"]
await message.bot.delete_message(
chat_id=message.chat.id, message_id=prompt_message_id
)
await message.delete()
except Exception as e:
if "message to delete not found" in str(e).lower():
pass # Ignore this error
else:
raise e
max_risk_percent_value = message.text
if not is_int(max_risk_percent_value):
await message.answer(
text="Ошибка: введите валидное число.",
reply_markup=kbi.back_to_risk_management,
)
logger.debug(
"User %s input invalid (not an valid number): %s",
message.from_user.id,
max_risk_percent_value,
)
return
if int(max_risk_percent_value) < 1 or int(max_risk_percent_value) > 100:
await message.answer(
text="Ошибка: введите число от 1 до 100.",
reply_markup=kbi.back_to_risk_management,
)
logger.debug(
"User %s input invalid (not an valid number): %s",
message.from_user.id,
max_risk_percent_value,
)
return
req = await rq.set_max_risk_percent(
tg_id=message.from_user.id, max_risk_percent=int(max_risk_percent_value)
)
if req:
await message.answer(
text=f"Максимальный процент риска установлен на {max_risk_percent_value}%.",
reply_markup=kbi.back_to_risk_management,
)
else:
await message.answer(
text="Произошла ошибка при установке максимального процента риска. "
"Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_risk_management,
)
await state.clear()
except Exception as e:
await message.answer(
text="Произошла ошибка при установке максимального процента риска. "
"Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_risk_management,
)
logger.error(
"Error processing command max_risk_percent for user %s: %s",
message.from_user.id,
e,
)
@router_risk_management.callback_query(F.data == "commission_fee")
async def commission_fee(callback_query: CallbackQuery, state: FSMContext) -> None:
"""

View File

@@ -6,9 +6,8 @@ from aiogram.types import CallbackQuery
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.bybit import get_bybit_client
from app.bybit.get_functions.get_tickers import get_tickers
from app.helper_functions import calculate_total_budget, get_base_currency, safe_float
from app.helper_functions import calculate_total_budget, safe_float
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
@@ -26,7 +25,6 @@ async def additional_settings(callback_query: CallbackQuery, state: FSMContext)
try:
await state.clear()
tg_id = callback_query.from_user.id
symbol = await rq.get_user_symbol(tg_id=tg_id)
additional_data = await rq.get_user_additional_settings(tg_id=tg_id)
if not additional_data:
@@ -40,138 +38,54 @@ async def additional_settings(callback_query: CallbackQuery, state: FSMContext)
return
trade_mode_map = {
"Merged_Single": "Односторонний режим",
"Both_Sides": "Хеджирование",
"Long": "Лонг",
"Short": "Шорт",
"Switch": "Свитч",
}
margin_type_map = {
"ISOLATED_MARGIN": "Изолированная",
"REGULAR_MARGIN": "Кросс",
}
order_type_map = {"Market": "Рыночный", "Limit": "Лимитный"}
trade_mode = additional_data.trade_mode or ""
margin_type = additional_data.margin_type or ""
order_type = additional_data.order_type or ""
trade_mode_rus = trade_mode_map.get(trade_mode, trade_mode)
margin_type_rus = margin_type_map.get(margin_type, margin_type)
order_type_rus = order_type_map.get(order_type, "Условный")
switch_side = additional_data.switch_side
def f(x):
return safe_float(x)
leverage = f(additional_data.leverage)
leverage_to_buy = f(additional_data.leverage_to_buy)
leverage_to_sell = f(additional_data.leverage_to_sell)
martingale = f(additional_data.martingale_factor)
max_bets = additional_data.max_bets_in_series
quantity = f(additional_data.order_quantity)
limit_price = f(additional_data.limit_price)
trigger_price = f(additional_data.trigger_price) or 0
tickers = await get_tickers(tg_id=tg_id, symbol=symbol)
price_symbol = safe_float(tickers.get("lastPrice")) or 0
bid = f(tickers.get("bid1Price")) or 0
ask = f(tickers.get("ask1Price")) or 0
switch_side_mode = ""
if trade_mode == "Switch":
switch_side_mode = f"- Направление первой сделки: {switch_side}\n"
sym = get_base_currency(symbol)
if trade_mode == "Merged_Single":
leverage_str = f"{leverage:.2f}x"
else:
if margin_type == "ISOLATED_MARGIN":
leverage_str = f"{leverage_to_buy:.2f}x:{leverage_to_sell:.2f}x"
else:
leverage_str = f"{leverage:.2f}x"
conditional_order_type = additional_data.conditional_order_type or ""
conditional_order_type_rus = (
"Лимитный"
if conditional_order_type == "Limit"
else (
"Рыночный"
if conditional_order_type == "Market"
else conditional_order_type
)
)
conditional_order_type_text = (
f"- Тип условного ордера: {conditional_order_type_rus}\n"
if order_type == "Conditional"
else ""
)
limit_price_text = ""
trigger_price_text = ""
if order_type == "Limit":
limit_price_text = f"- Цена лимитного ордера: {limit_price:.4f} USDT\n"
elif order_type == "Conditional":
if conditional_order_type == "Limit":
limit_price_text = f"- Цена лимитного ордера: {limit_price:.4f} USDT\n"
trigger_price_text = f"- Триггер цена: {trigger_price:.4f} USDT\n"
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
commission_fee = risk_management_data.commission_fee
client = await get_bybit_client(tg_id=tg_id)
fee_info = client.get_fee_rates(category="linear", symbol=symbol)
if commission_fee == "Yes_commission_fee":
commission_fee_percent = safe_float(
fee_info["result"]["list"][0]["takerFeeRate"]
)
else:
commission_fee_percent = 0.0
if order_type == "Conditional":
if conditional_order_type == "Limit":
entry_price = limit_price
ask_price = limit_price
bid_price = limit_price
else:
ask_price = trigger_price
bid_price = trigger_price
entry_price = trigger_price
else:
if order_type == "Limit":
entry_price = limit_price
ask_price = limit_price
bid_price = limit_price
else:
entry_price = price_symbol
ask_price = ask
bid_price = bid
durability_buy = quantity * bid_price
durability_sell = quantity * ask_price
quantity_price = quantity * entry_price
total_commission = quantity_price * commission_fee_percent
total_budget = await calculate_total_budget(
quantity=durability_buy,
quantity=quantity,
martingale_factor=martingale,
max_steps=max_bets,
commission_fee_percent=total_commission,
)
text = (
f"Основные настройки:\n\n"
f"- Режим позиции: {trade_mode_rus}\n"
f"- Режим торговли: {trade_mode_rus}\n"
f"{switch_side_mode}"
f"- Тип маржи: {margin_type_rus}\n"
f"- Размер кредитного плеча: {leverage_str}\n"
f"- Тип ордера: {order_type_rus}\n"
f"- Количество ордера: {quantity} {sym}\n"
f"- Размер кредитного плеча: {leverage:.2f}\n"
f"- Базовая ставка: {quantity} USDT\n"
f"- Коэффициент мартингейла: {martingale:.2f}\n"
f"{conditional_order_type_text}"
f"{trigger_price_text}"
f"{limit_price_text}"
f"- Триггер цена: {trigger_price:.4f} USDT\n"
f"- Максимальное кол-во ставок в серии: {max_bets}\n\n"
f"- Стоимость: {durability_buy:.2f}/{durability_sell:.2f} USDT\n"
f"- Рекомендуемый бюджет: {total_budget:.4f} USDT\n"
f"- Бюджет серии: {total_budget:.2f} USDT\n"
)
keyboard = kbi.get_additional_settings_keyboard(
current_order_type=order_type, conditional_order=conditional_order_type
)
keyboard = kbi.get_additional_settings_keyboard(mode=trade_mode)
await callback_query.message.edit_text(text=text, reply_markup=keyboard)
logger.debug(
"Command additional_settings processed successfully for user: %s", tg_id
@@ -202,7 +116,6 @@ async def risk_management(callback_query: CallbackQuery, state: FSMContext) -> N
if risk_management_data:
take_profit_percent = risk_management_data.take_profit_percent or ""
stop_loss_percent = risk_management_data.stop_loss_percent or ""
max_risk_percent = risk_management_data.max_risk_percent or ""
commission_fee = risk_management_data.commission_fee or ""
commission_fee_rus = (
"Да" if commission_fee == "Yes_commission_fee" else "Нет"
@@ -210,9 +123,8 @@ async def risk_management(callback_query: CallbackQuery, state: FSMContext) -> N
await callback_query.message.edit_text(
text=f"Риск-менеджмент:\n\n"
f"- Процент изменения цены для фиксации прибыли: {take_profit_percent}%\n"
f"- Процент изменения цены для фиксации убытка: {stop_loss_percent}%\n\n"
f"- Максимальный риск на сделку (в % от баланса): {max_risk_percent}%\n\n"
f"- Процент изменения цены для фиксации прибыли: {take_profit_percent:.2f}%\n"
f"- Процент изменения цены для фиксации убытка: {stop_loss_percent:.2f}%\n\n"
f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n",
reply_markup=kbi.risk_management,
)
@@ -250,11 +162,9 @@ async def conditions(callback_query: CallbackQuery, state: FSMContext) -> None:
)
if conditional_settings_data:
start_timer = conditional_settings_data.timer_start or 0
stop_timer = conditional_settings_data.timer_end or 0
await callback_query.message.edit_text(
text="Условия торговли:\n\n"
f"- Таймер для старта: {start_timer} мин.\n"
f"- Таймер для остановки: {stop_timer} мин.\n",
f"- Таймер для старта: {start_timer} мин.\n",
reply_markup=kbi.conditions,
)
logger.debug(

View File

@@ -7,14 +7,12 @@ from aiogram.types import CallbackQuery
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.bybit.get_functions.get_positions import get_active_positions_by_symbol
from app.bybit.get_functions.get_positions import get_active_positions_by_symbol, get_active_orders_by_symbol
from app.bybit.open_positions import start_trading_cycle
from app.helper_functions import safe_float
from app.telegram.tasks.tasks import (
add_start_task_merged,
add_start_task_switch,
cancel_start_task_merged,
cancel_start_task_switch,
cancel_start_task_merged
)
from logger_helper.logger_helper import LOGGING_CONFIG
@@ -35,97 +33,30 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
"""
try:
await state.clear()
additional_data = await rq.get_user_additional_settings(
tg_id=callback_query.from_user.id
)
trade_mode = additional_data.trade_mode
tg_id = callback_query.from_user.id
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
position = next((d for d in deals if d.get("symbol") == symbol), None)
if position:
size = position.get("size", 0)
position_idx = position.get("positionIdx")
else:
size = 0
position_idx = None
if position_idx != 0 and safe_float(size) > 0 and trade_mode == "Merged_Single":
if safe_float(size) > 0:
await callback_query.answer(
text="У вас есть активная позиция в режиме хеджирования. "
"Открытие сделки в одностороннем режиме невозможно.",
text="У вас есть активная позиция по текущей паре",
)
return
if position_idx == 0 and safe_float(size) > 0 and trade_mode == "Both_Sides":
orders = await get_active_orders_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol)
if orders is not None:
await callback_query.answer(
text="У вас есть активная позиция в одностороннем режиме. "
"Открытие сделки в режиме хеджирования невозможно.",
)
return
if trade_mode == "Merged_Single":
await callback_query.message.edit_text(
text="Выберите режим торговли:\n\n"
"Лонг - все сделки серии открываются на покупку.\n"
"Шорт - все сделки серии открываются на продажу.\n"
"Свитч - направление каждой сделки серии меняется по переменно.\n",
reply_markup=kbi.merged_start_trading,
)
else: # trade_mode == "Both_Sides":
await callback_query.message.edit_text(
text="Выберите режим торговли:\n\n"
"Лонг - все сделки открываются на покупку.\n"
"Шорт - все сделки открываются на продажу.\n",
reply_markup=kbi.both_start_trading,
)
logger.debug(
"Command start_trading processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error(
"Error processing command start_trading for user %s: %s",
callback_query.from_user.id,
e,
)
@router_start_trading.callback_query(lambda c: c.data == "long" or c.data == "short")
async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the "long" or "short" callback query.
Clears the FSM state and starts the trading cycle.
:param callback_query: Message
:param state: FSMContext
:return: None
"""
try:
if callback_query.data == "long":
side = "Buy"
elif callback_query.data == "short":
side = "Sell"
else:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
return
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
position = next((d for d in deals if d.get("symbol") == symbol), None)
if position:
size = position.get("size", 0)
position_idx = position.get("positionIdx")
else:
size = 0
position_idx = None
if position_idx == 0 and safe_float(size) > 0:
await callback_query.answer(
text="Торговля уже запущена в одностороннем режиме для данного инструмента"
text="У вас есть активный ордер по текущей паре",
)
return
@@ -151,25 +82,30 @@ async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=True,
side=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
)
res = await start_trading_cycle(
tg_id=callback_query.from_user.id,
side=side,
switch_side_mode=False,
)
error_messages = {
"Limit price is out min price": "Цена лимитного ордера меньше минимального",
"Limit price is out max price": "Цена лимитного ордера больше максимального",
"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": "️️Количество контрактов превышает допустимое максимальное количество контрактов",
"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": "️️Лимит ставки меньше минимально допустимого",
}
if res == "OK":
@@ -180,7 +116,6 @@ async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
side=side,
)
text = error_messages.get(res, "Произошла ошибка при запуске торговли")
await callback_query.message.edit_text(
@@ -194,7 +129,7 @@ async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -
except Exception as e:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error(
"Error processing command long for user %s: %s",
"Error processing command start_trading for user %s: %s",
callback_query.from_user.id,
e,
)
@@ -202,189 +137,8 @@ async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -
logger.error("Cancelled timer for user %s", callback_query.from_user.id)
@router_start_trading.callback_query(lambda c: c.data == "switch")
async def start_trading_switch(
callback_query: CallbackQuery, state: FSMContext
) -> None:
"""
Handles the "switch" callback query.
Clears the FSM state and sends a message to the user to select the switch side.
:param callback_query: Message
:param state: FSMContext
:return: None
"""
try:
await state.clear()
await callback_query.message.edit_text(
text="Выберите направление первой сделки серии:\n\n"
"Лонг - открывается первая сделка на покупку.\n"
"Шорт - открывается первая сделка на продажу.\n"
"По направлению - сделка открывается в направлении последней сделки предыдущей серии.\n"
"Противоположно - сделка открывается в противоположном направлении последней сделки предыдущей серии.\n",
reply_markup=kbi.switch_side,
)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error(
"Error processing command start trading switch for user %s: %s",
callback_query.from_user.id,
e,
)
@router_start_trading.callback_query(
lambda c: c.data
in {"switch_long", "switch_short", "switch_direction", "switch_opposite"}
)
async def start_switch(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Starts the trading cycle with the selected side.
:param callback_query:
:param state:
:return:
"""
try:
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
get_side = "Buy"
if user_deals_data:
get_side = user_deals_data.last_side or "Buy"
if callback_query.data == "switch_long":
side = "Buy"
elif callback_query.data == "switch_short":
side = "Sell"
elif callback_query.data == "switch_direction":
side = get_side
elif callback_query.data == "switch_opposite":
if get_side == "Buy":
side = "Sell"
else:
side = "Buy"
else:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
return
deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
position = next((d for d in deals if d.get("symbol") == symbol), None)
if position:
size = position.get("size", 0)
position_idx = position.get("positionIdx")
else:
size = 0
position_idx = None
if position_idx == 1 and safe_float(size) > 0 and side == "Buy":
await callback_query.answer(
text="Торговля уже запущена в режиме хеджирования на покупку для данного инструмента"
)
return
if position_idx == 2 and safe_float(size) > 0 and side == "Sell":
await callback_query.answer(
text="Торговля уже запущена в режиме хеджирования на продажу для данного инструмента"
)
return
conditional_data = await rq.get_user_conditional_settings(
tg_id=callback_query.from_user.id
)
timer_start = conditional_data.timer_start
cancel_start_task_switch(user_id=callback_query.from_user.id)
async def delay_start():
if timer_start > 0:
await callback_query.message.edit_text(
text=f"Торговля будет запущена с задержкой {timer_start} мин.",
reply_markup=kbi.cancel_timer_switch,
)
await rq.set_start_timer(
tg_id=callback_query.from_user.id, timer_start=0
)
await asyncio.sleep(timer_start * 60)
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=True,
side=side,
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=True,
side=r_side,
)
res = await start_trading_cycle(
tg_id=callback_query.from_user.id,
side=side,
switch_side_mode=True,
)
error_messages = {
"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": " ️️Количество контрактов превышает допустимое максимальное количество контрактов",
}
if res == "OK":
await callback_query.message.edit_text(text="Торговля запущена")
await state.clear()
else:
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
side=side,
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
side=r_side,
)
text = error_messages.get(res, "Произошла ошибка при запуске торговли")
await callback_query.message.edit_text(
text=text, reply_markup=kbi.profile_bybit
)
await callback_query.message.edit_text("Запуск торговли...")
task = asyncio.create_task(delay_start())
await add_start_task_switch(user_id=callback_query.from_user.id, task=task)
except asyncio.CancelledError:
logger.error("Cancelled timer for user %s", callback_query.from_user.id)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error(
"Error processing command start switch for user %s: %s",
callback_query.from_user.id,
e,
)
@router_start_trading.callback_query(
lambda c: c.data == "cancel_timer_merged" or c.data == "cancel_timer_switch"
lambda c: c.data == "cancel_timer_merged"
)
async def cancel_start_trading(
callback_query: CallbackQuery, state: FSMContext
@@ -400,8 +154,6 @@ async def cancel_start_trading(
await state.clear()
if callback_query.data == "cancel_timer_merged":
cancel_start_task_merged(user_id=callback_query.from_user.id)
elif callback_query.data == "cancel_timer_switch":
cancel_start_task_switch(user_id=callback_query.from_user.id)
await callback_query.message.edit_text(
text="Запуск торговли отменен", reply_markup=kbi.profile_bybit
)

View File

@@ -5,6 +5,7 @@ from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
from app.bybit.close_positions import close_position_by_symbol
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.telegram.tasks.tasks import add_stop_task, cancel_stop_task
@@ -27,6 +28,7 @@ async def stop_all_trading(callback_query: CallbackQuery, state: FSMContext):
tg_id=callback_query.from_user.id
)
timer_end = conditional_data.timer_end
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
async def delay_start():
if timer_end > 0:
@@ -37,32 +39,21 @@ async def stop_all_trading(callback_query: CallbackQuery, state: FSMContext):
await rq.set_stop_timer(tg_id=callback_query.from_user.id, timer_end=0)
await asyncio.sleep(timer_end * 60)
user_auto_trading_list = await rq.get_all_user_auto_trading(
tg_id=callback_query.from_user.id
user_auto_trading = await rq.get_user_auto_trading(
tg_id=callback_query.from_user.id, symbol=symbol
)
if any(item.auto_trading for item in user_auto_trading_list):
for active_auto_trading in user_auto_trading_list:
if active_auto_trading.auto_trading:
symbol = active_auto_trading.symbol
get_side = active_auto_trading.side
req = await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
side=get_side,
)
if not req:
await callback_query.message.edit_text(
text="Произошла ошибка при остановке торговли",
reply_markup=kbi.profile_bybit,
)
return
await callback_query.message.edit_text(
text="Торговля остановлена", reply_markup=kbi.profile_bybit
if user_auto_trading and user_auto_trading.auto_trading:
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
)
await close_position_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol)
await callback_query.message.edit_text(text=f"Торговля для {symbol} остановлена", reply_markup=kbi.profile_bybit)
else:
await callback_query.message.edit_text(text="Нет активной торговли")
await callback_query.message.edit_text(text=f"Нет активной торговли для {symbol}", reply_markup=kbi.profile_bybit)
task = asyncio.create_task(delay_start())
await add_stop_task(user_id=callback_query.from_user.id, task=task)

View File

@@ -35,13 +35,8 @@ main_menu = InlineKeyboardMarkup(
text="Сменить торговую пару", callback_data="change_symbol"
)
],
[InlineKeyboardButton(text="Мои сделки", callback_data="my_deals")],
[InlineKeyboardButton(text="Начать торговлю", callback_data="start_trading")],
[
InlineKeyboardButton(
text="Остановить торговлю", callback_data="stop_trading"
)
],
[InlineKeyboardButton(text="Остановить торговлю", callback_data="stop_trading")],
]
)
@@ -67,62 +62,39 @@ main_settings = InlineKeyboardMarkup(
# additional_settings
def get_additional_settings_keyboard(
current_order_type: str, conditional_order: str
def get_additional_settings_keyboard(mode: str
) -> InlineKeyboardMarkup:
"""
Create keyboard for additional settings
:param current_order_type: Market, Limit or Conditional
:param conditional_order: Market or Limit
:param mode: Trade mode
:return: InlineKeyboardMarkup
"""
buttons = [
[
InlineKeyboardButton(text="Режим позиции", callback_data="trade_mode"),
InlineKeyboardButton(text="Режим торговли", callback_data="trade_mode"),
InlineKeyboardButton(text="Тип маржи", callback_data="margin_type"),
],
[
InlineKeyboardButton(
text="Размер кредитного плеча", callback_data="leverage"
),
InlineKeyboardButton(text="Тип ордера", callback_data="order_type"),
InlineKeyboardButton(
text="Базовая ставка", callback_data="order_quantity"),
],
[
InlineKeyboardButton(
text="Количество ордера", callback_data="order_quantity"
),
InlineKeyboardButton(
text="Коэффициент мартингейла", callback_data="martingale_factor"
),
InlineKeyboardButton(text="Триггер цена", callback_data="trigger_price"
),
],
]
if current_order_type == "Conditional":
if mode == "Switch":
buttons.append(
[
InlineKeyboardButton(
text="Тип условного ордера", callback_data="conditional_order_type"
)
]
)
buttons.append(
[InlineKeyboardButton(text="Триггер цена", callback_data="trigger_price")]
)
if conditional_order == "Limit":
buttons.append(
[
InlineKeyboardButton(
text="Цена лимитного ордера", callback_data="limit_price"
)
]
)
elif current_order_type == "Limit":
buttons.append(
[
InlineKeyboardButton(
text="Цена лимитного ордера", callback_data="limit_price"
)
]
[InlineKeyboardButton(text="Направление первой сделки", callback_data="switch_side_start")]
)
buttons.append(
@@ -143,40 +115,31 @@ def get_additional_settings_keyboard(
return InlineKeyboardMarkup(inline_keyboard=buttons)
order_type = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Рыночный", callback_data="Market"),
InlineKeyboardButton(text="Лимитный", callback_data="Limit"),
],
[InlineKeyboardButton(text="Условный", callback_data="Conditional")],
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
conditional_order_type = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Рыночный", callback_data="set_market"),
InlineKeyboardButton(text="Лимитный", callback_data="set_limit"),
],
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
trade_mode = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Односторонний режим", callback_data="Merged_Single"
text="Лонг", callback_data="Long"
),
InlineKeyboardButton(text="Шорт", callback_data="Short"),
InlineKeyboardButton(text="Свитч", callback_data="Switch"),
],
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
switch_side = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="По направлению", callback_data="switch_direction"
),
InlineKeyboardButton(
text="Противоположно", callback_data="switch_opposite"
),
InlineKeyboardButton(text="Хеджирование", callback_data="Both_Sides"),
],
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
@@ -207,21 +170,6 @@ back_to_additional_settings = InlineKeyboardMarkup(
]
)
change_limit_price = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Установить цену", callback_data="set_limit_price"
),
InlineKeyboardButton(text="Последняя цена", callback_data="last_price"),
],
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
back_to_change_limit_price = InlineKeyboardMarkup(
inline_keyboard=[
[
@@ -239,17 +187,12 @@ risk_management = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Изм. цены прибыли", callback_data="take_profit_percent"
text="Тейк-профит", callback_data="take_profit_percent"
),
InlineKeyboardButton(
text="Изм. цены убытка", callback_data="stop_loss_percent"
text="Стоп-лосс", callback_data="stop_loss_percent"
),
],
[
InlineKeyboardButton(
text="Максимальный риск", callback_data="max_risk_percent"
)
],
[InlineKeyboardButton(text="Комиссия биржи", callback_data="commission_fee")],
[
InlineKeyboardButton(text="Назад", callback_data="main_settings"),
@@ -285,9 +228,6 @@ conditions = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Таймер для старта", callback_data="start_timer"),
InlineKeyboardButton(
text="Таймер для остановки", callback_data="stop_timer"
),
],
[
InlineKeyboardButton(text="Назад", callback_data="main_settings"),
@@ -391,48 +331,6 @@ def make_close_orders_keyboard(symbol_order: str, order_id: str):
# START TRADING
merged_start_trading = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Лонг", callback_data="long"),
InlineKeyboardButton(text="Шорт", callback_data="short"),
],
[InlineKeyboardButton(text="Свитч", callback_data="switch")],
[InlineKeyboardButton(text="Назад", callback_data="profile_bybit")],
]
)
both_start_trading = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Лонг", callback_data="long"),
InlineKeyboardButton(text="Шорт", callback_data="short"),
],
[InlineKeyboardButton(text="Назад", callback_data="profile_bybit")],
]
)
switch_side = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Лонг", callback_data="switch_long"),
InlineKeyboardButton(text="Шорт", callback_data="switch_short"),
],
[
InlineKeyboardButton(
text="По направлению", callback_data="switch_direction"
),
InlineKeyboardButton(
text="Противоположно", callback_data="switch_opposite"
),
],
[
InlineKeyboardButton(text="Назад", callback_data="start_trading"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
back_to_start_trading = InlineKeyboardMarkup(
inline_keyboard=[
[

View File

@@ -1,8 +1,10 @@
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup
profile = ReplyKeyboardMarkup(
keyboard=[[KeyboardButton(text="Панель Bybit"), KeyboardButton(text="Профиль")],
[KeyboardButton(text="Подключить платформу Bybit")]],
keyboard=[
[KeyboardButton(text="Панель Bybit"), KeyboardButton(text="Профиль")],
[KeyboardButton(text="Подключить платформу Bybit")],
],
resize_keyboard=True,
one_time_keyboard=True,
input_field_placeholder="Выберите пункт меню...",

View File

@@ -1,31 +1,8 @@
import os
from dotenv import load_dotenv, find_dotenv
import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("config")
env_path = find_dotenv()
if env_path:
load_dotenv(env_path)
logging.info(f"Loaded env from {env_path}")
else:
logging.warning(".env file not found, environment variables won't be loaded")
BOT_TOKEN = os.getenv('BOT_TOKEN')
if not BOT_TOKEN:
logging.error("BOT_TOKEN is not set in environment variables")
DB_USER = os.getenv('DB_USER')
DB_PASS = os.getenv('DB_PASS')
DB_HOST = os.getenv('DB_HOST')
DB_PORT = os.getenv('DB_PORT')
DB_NAME = os.getenv('DB_NAME')
if not all([DB_USER, DB_PASS, DB_HOST, DB_PORT, DB_NAME]):
logger.error("One or more database environment variables are not set")
DATABASE_URL = f"postgresql+asyncpg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
BOT_TOKEN = os.getenv("BOT_TOKEN")

View File

@@ -1,18 +1,39 @@
from database.models import Base, User, UserAdditionalSettings, UserApi, UserConditionalSettings, UserDeals, \
UserRiskManagement, UserSymbol
import logging.config
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine, AsyncSession
from database.models import Base
from config import DATABASE_URL
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy import event
from pathlib import Path
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("database")
async_engine = create_async_engine(DATABASE_URL, echo=False)
BASE_DIR = Path(__file__).parent.resolve()
DATA_DIR = BASE_DIR / "db"
DATA_DIR.mkdir(parents=True, exist_ok=True)
async_session = async_sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False)
DATABASE_URL = f"sqlite+aiosqlite:///{DATA_DIR / 'stcs.db'}"
async_engine = create_async_engine(
DATABASE_URL,
echo=False,
connect_args={"check_same_thread": False}
)
@event.listens_for(async_engine.sync_engine, "connect")
def _enable_foreign_keys(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
async_session = async_sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False
)
async def init_db():

View File

@@ -1,6 +1,6 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy import Column, ForeignKey, Integer, String, BigInteger, Float, Boolean, UniqueConstraint
from sqlalchemy import Column, ForeignKey, Integer, String, Float, Boolean, UniqueConstraint
from sqlalchemy.orm import relationship
Base = declarative_base(cls=AsyncAttrs)
@@ -11,7 +11,7 @@ class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
tg_id = Column(BigInteger, nullable=False, unique=True)
tg_id = Column(Integer, nullable=False, unique=True)
username = Column(String, nullable=False)
user_api = relationship("UserApi",
@@ -91,14 +91,10 @@ class UserAdditionalSettings(Base):
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False, unique=True)
trade_mode = Column(String, nullable=False, default="Merged_Single")
order_type = Column(String, nullable=False, default="Market")
conditional_order_type = Column(String, nullable=False, default="Market")
limit_price = Column(Float, nullable=False, default=0.0)
switch_side = Column(String, nullable=False, default="По направлению")
trigger_price = Column(Float, nullable=False, default=0.0)
margin_type = Column(String, nullable=False, default="ISOLATED_MARGIN")
leverage = Column(String, nullable=False, default="10")
leverage_to_buy = Column(String, nullable=False, default="10")
leverage_to_sell = Column(String, nullable=False, default="10")
order_quantity = Column(Float, nullable=False, default=5.0)
martingale_factor = Column(Float, nullable=False, default=1.0)
max_bets_in_series = Column(Integer, nullable=False, default=1)
@@ -114,9 +110,8 @@ class UserRiskManagement(Base):
user_id = Column(Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False, unique=True)
take_profit_percent = Column(Integer, nullable=False, default=1)
stop_loss_percent = Column(Integer, nullable=False, default=1)
max_risk_percent = Column(Integer, nullable=False, default=100)
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")
user = relationship("User", back_populates="user_risk_management")
@@ -148,13 +143,10 @@ class UserDeals(Base):
current_step = Column(Integer, nullable=True)
symbol = Column(String, nullable=True)
trade_mode = Column(String, nullable=True)
trading_type = Column(String, nullable=True)
side_mode = Column(String, nullable=True)
base_quantity = Column(Float, nullable=True)
margin_type = Column(String, nullable=True)
order_type = Column(String, nullable=True)
conditional_order_type = Column(String, nullable=True)
leverage = Column(String, nullable=True)
leverage_to_buy = Column(String, nullable=True)
leverage_to_sell = Column(String, nullable=True)
last_side = Column(String, nullable=True)
closed_side = Column(String, nullable=True)
order_quantity = Column(Float, nullable=True)
@@ -162,9 +154,6 @@ class UserDeals(Base):
max_bets_in_series = Column(Integer, nullable=True)
take_profit_percent = Column(Integer, nullable=True)
stop_loss_percent = Column(Integer, nullable=True)
max_risk_percent = Column(Integer, nullable=True)
switch_side_mode = Column(Boolean, nullable=True)
limit_price = Column(Float, nullable=True)
trigger_price = Column(Float, nullable=True)
user = relationship("User", back_populates="user_deals")
@@ -184,7 +173,7 @@ class UserAutoTrading(Base):
nullable=False)
symbol = Column(String, nullable=True)
auto_trading = Column(Boolean, nullable=True)
side = Column(String, nullable=True)
fee = Column(Float, nullable=True)
total_fee = Column(Float, nullable=True)
user = relationship("User", back_populates="user_auto_trading")
user = relationship("User", back_populates="user_auto_trading")

View File

@@ -198,15 +198,12 @@ async def create_user_additional_settings(tg_id: int) -> None:
# Create the user additional settings
user_additional_settings = UserAdditionalSettings(
user=user,
trade_mode="Merged_Single", # Default value
order_type="Market",
conditional_order_type="Market",
trade_mode="Long", # Default value
switch_side="По направлению",
margin_type="ISOLATED_MARGIN",
leverage="10",
leverage_to_buy="10",
leverage_to_sell="10",
order_quantity=5.0,
martingale_factor=1.0,
order_quantity=1.0,
martingale_factor=2.0,
max_bets_in_series=10,
)
session.add(user_additional_settings)
@@ -283,90 +280,6 @@ async def set_trade_mode(tg_id: int, trade_mode: str) -> bool:
return False
async def set_order_type(tg_id: int, order_type: str) -> bool:
"""
Set order type for a user in the database.
:param tg_id: Telegram user ID
:param order_type: "Market" or "Limit"
:return: True if successful, False otherwise
"""
try:
async with async_session() as session:
result = await session.execute(
select(User)
.options(joinedload(User.user_additional_settings))
.filter_by(tg_id=tg_id)
)
user = result.scalars().first()
if user:
if user.user_additional_settings:
# Updating existing record
user.user_additional_settings.order_type = order_type
else:
# Creating new record
user_additional_settings = UserAdditionalSettings(
order_type=order_type,
user=user,
)
session.add(user_additional_settings)
await session.commit()
logger.info("User order type 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 order type for user %s: %s", tg_id, e)
return False
async def set_conditional_order_type(tg_id: int, conditional_order_type: str) -> bool:
"""
Set conditional order type for a user in the database.
:param tg_id: Telegram user ID
:param conditional_order_type: "Market" or "Limit"
:return: True if successful, False otherwise
"""
try:
async with async_session() as session:
result = await session.execute(
select(User)
.options(joinedload(User.user_additional_settings))
.filter_by(tg_id=tg_id)
)
user = result.scalars().first()
if user:
if user.user_additional_settings:
# Updating existing record
user.user_additional_settings.conditional_order_type = (
conditional_order_type
)
else:
# Creating new record
user_additional_settings = UserAdditionalSettings(
conditional_order_type=conditional_order_type,
user=user,
)
session.add(user_additional_settings)
await session.commit()
logger.info("User conditional order type 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 conditional order type for user %s: %s",
tg_id,
e,
)
return False
async def set_margin_type(tg_id: int, margin_type: str) -> bool:
"""
Set margin type for a user in the database.
@@ -406,6 +319,45 @@ async def set_margin_type(tg_id: int, margin_type: str) -> bool:
return False
async def set_switch_side(tg_id: int, switch_side: str) -> bool:
"""
Set switch side for a user in the database.
:param tg_id: Telegram user ID
:param switch_side: "По направлению" or "По цене"
:return: True if successful, False otherwise
"""
try:
async with async_session() as session:
result = await session.execute(
select(User)
.options(joinedload(User.user_additional_settings))
.filter_by(tg_id=tg_id)
)
user = result.scalars().first()
if user:
if user.user_additional_settings:
# Updating existing record
user.user_additional_settings.switch_side = switch_side
else:
# Creating new record
user_additional_settings = UserAdditionalSettings(
switch_side=switch_side,
user=user,
)
session.add(user_additional_settings)
await session.commit()
logger.info("User switch side 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 switch side for user %s: %s", tg_id, e)
return False
async def set_leverage(tg_id: int, leverage: str) -> bool:
"""
Set leverage for a user in the database.
@@ -445,50 +397,6 @@ async def set_leverage(tg_id: int, leverage: str) -> bool:
return False
async def set_leverage_to_buy_and_sell(
tg_id: int, leverage_to_buy: str, leverage_to_sell: str
) -> bool:
"""
Set leverage for a user in the database.
:param tg_id: Telegram user ID
:param leverage_to_buy: Leverage to buy
:param leverage_to_sell: Leverage to sell
:return: True if successful, False otherwise
"""
try:
async with async_session() as session:
result = await session.execute(
select(User)
.options(joinedload(User.user_additional_settings))
.filter_by(tg_id=tg_id)
)
user = result.scalars().first()
if user:
if user.user_additional_settings:
# Updating existing record
user.user_additional_settings.leverage_to_buy = leverage_to_buy
user.user_additional_settings.leverage_to_sell = leverage_to_sell
else:
# Creating new record
user_additional_settings = UserAdditionalSettings(
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
user=user,
)
session.add(user_additional_settings)
await session.commit()
logger.info("User leverage 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 leverage for user %s: %s", tg_id, e)
return False
async def set_order_quantity(tg_id: int, order_quantity: float) -> bool:
"""
Set order quantity for a user in the database.
@@ -614,45 +522,6 @@ async def set_max_bets_in_series(tg_id: int, max_bets_in_series: int) -> bool:
return False
async def set_limit_price(tg_id: int, limit_price: float) -> bool:
"""
Set limit price for a user in the database.
:param tg_id:
:param limit_price:
:return: bool
"""
try:
async with async_session() as session:
result = await session.execute(
select(User)
.options(joinedload(User.user_additional_settings))
.filter_by(tg_id=tg_id)
)
user = result.scalars().first()
if user:
if user.user_additional_settings:
# Updating existing record
user.user_additional_settings.limit_price = limit_price
else:
# Creating new record
user_additional_settings = UserAdditionalSettings(
limit_price=limit_price,
user=user,
)
session.add(user_additional_settings)
await session.commit()
logger.info("User limit price 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 limit price for user %s: %s", tg_id, e)
return False
async def set_trigger_price(tg_id: int, trigger_price: float) -> bool:
"""
Set trigger price for a user in the database.
@@ -718,9 +587,8 @@ async def create_user_risk_management(tg_id: int) -> None:
# Create the user risk management
user_risk_management = UserRiskManagement(
user=user,
take_profit_percent=1,
stop_loss_percent=1,
max_risk_percent=100,
take_profit_percent=1.0,
stop_loss_percent=1.0,
commission_fee="Yes_commission_fee",
)
session.add(user_risk_management)
@@ -758,7 +626,7 @@ async def get_user_risk_management(tg_id: int):
return None
async def set_take_profit_percent(tg_id: int, take_profit_percent: int) -> bool:
async def set_take_profit_percent(tg_id: int, take_profit_percent: float) -> bool:
"""
Set take profit percent for a user in the database.
:param tg_id: Telegram user ID
@@ -799,7 +667,7 @@ async def set_take_profit_percent(tg_id: int, take_profit_percent: int) -> bool:
return False
async def set_stop_loss_percent(tg_id: int, stop_loss_percent: int) -> bool:
async def set_stop_loss_percent(tg_id: int, stop_loss_percent: float) -> bool:
"""
Set stop loss percent for a user in the database.
:param tg_id: Telegram user ID
@@ -840,47 +708,6 @@ async def set_stop_loss_percent(tg_id: int, stop_loss_percent: int) -> bool:
return False
async def set_max_risk_percent(tg_id: int, max_risk_percent: int) -> bool:
"""
Set max risk percent for a user in the database.
:param tg_id: Telegram user ID
:param max_risk_percent: Max risk percent
: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.max_risk_percent = max_risk_percent
else:
# Creating new record
user_risk_management = UserRiskManagement(
max_risk_percent=max_risk_percent,
user=user,
)
session.add(user_risk_management)
await session.commit()
logger.info("User max risk percent 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 max risk percent for user %s: %s", tg_id, e
)
return False
async def set_commission_fee(tg_id: int, commission_fee: str) -> bool:
"""
Set commission fee for a user in the database.
@@ -1068,47 +895,35 @@ async def set_stop_timer(tg_id: int, timer_end: int) -> bool:
async def set_user_deal(
tg_id: int,
symbol: str,
last_side: str,
current_step: int,
trade_mode: str,
side_mode: str,
margin_type: str,
leverage: str,
leverage_to_buy: str,
leverage_to_sell: str,
order_type: str,
conditional_order_type: str,
order_quantity: float,
limit_price: float,
trigger_price: float,
martingale_factor: float,
max_bets_in_series: int,
take_profit_percent: int,
stop_loss_percent: int,
max_risk_percent: int,
switch_side_mode: bool,
base_quantity: float
):
"""
Set the user deal in the database.
:param tg_id: Telegram user ID
:param symbol: Symbol
: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 leverage_to_buy: Leverage to buy
:param leverage_to_sell: Leverage to sell
:param order_type: Order type
:param conditional_order_type: Conditional order type
:param order_quantity: Order quantity
:param limit_price: Limit price
:param trigger_price: Trigger price
:param martingale_factor: Martingale factor
:param max_bets_in_series: Max bets in series
:param take_profit_percent: Take profit percent
:param stop_loss_percent: Stop loss percent
:param max_risk_percent: Max risk percent
:param switch_side_mode: Switch side mode
:param base_quantity: Base quantity
:return: bool
"""
try:
@@ -1126,47 +941,35 @@ async def set_user_deal(
if deal:
# Updating existing record
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.leverage_to_buy = leverage_to_buy
deal.leverage_to_sell = leverage_to_sell
deal.order_type = order_type
deal.conditional_order_type = conditional_order_type
deal.order_quantity = order_quantity
deal.limit_price = limit_price
deal.trigger_price = trigger_price
deal.martingale_factor = martingale_factor
deal.max_bets_in_series = max_bets_in_series
deal.take_profit_percent = take_profit_percent
deal.stop_loss_percent = stop_loss_percent
deal.max_risk_percent = max_risk_percent
deal.switch_side_mode = switch_side_mode
deal.base_quantity = base_quantity
else:
# Creating new record
new_deal = UserDeals(
user=user,
symbol=symbol,
last_side=last_side,
current_step=current_step,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
leverage=leverage,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
order_type=order_type,
conditional_order_type=conditional_order_type,
order_quantity=order_quantity,
limit_price=limit_price,
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,
max_risk_percent=max_risk_percent,
switch_side_mode=switch_side_mode,
base_quantity=base_quantity
)
session.add(new_deal)
@@ -1247,6 +1050,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):
@@ -1268,7 +1099,7 @@ async def get_all_user_auto_trading(tg_id: int):
return []
async def get_user_auto_trading(tg_id: int, symbol: str, side: str):
async def get_user_auto_trading(tg_id: int, symbol: str):
"""Get user auto trading from the database asynchronously."""
try:
async with async_session() as session:
@@ -1278,7 +1109,7 @@ async def get_user_auto_trading(tg_id: int, symbol: str, side: str):
return None
result_auto_trading = await session.execute(
select(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol, side=side)
select(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol)
)
auto_trading = result_auto_trading.scalars().first()
return auto_trading
@@ -1287,13 +1118,12 @@ async def get_user_auto_trading(tg_id: int, symbol: str, side: str):
return None
async def set_auto_trading(tg_id: int, symbol: str, auto_trading: bool, side: str) -> bool:
async def set_auto_trading(tg_id: int, symbol: str, auto_trading: bool) -> bool:
"""
Set the auto trading for a user in the database.
:param tg_id: Telegram user ID
:param symbol: Symbol
:param auto_trading: Auto trading
:param side: Side
:return: bool
"""
try:
@@ -1305,7 +1135,7 @@ async def set_auto_trading(tg_id: int, symbol: str, auto_trading: bool, side: st
return False
result = await session.execute(
select(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol, side=side)
select(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol)
)
record = result.scalars().first()
if record:
@@ -1315,7 +1145,6 @@ async def set_auto_trading(tg_id: int, symbol: str, auto_trading: bool, side: st
user_id=user.id,
symbol=symbol,
auto_trading=auto_trading,
side=side
)
session.add(new_record)
await session.commit()
@@ -1326,12 +1155,11 @@ async def set_auto_trading(tg_id: int, symbol: str, auto_trading: bool, side: st
return False
async def set_fee_user_auto_trading(tg_id: int, symbol: str, side: str, fee: float) -> bool:
async def set_fee_user_auto_trading(tg_id: int, symbol: str, fee: float) -> bool:
"""
Set the fee for a user auto trading in the database.
:param tg_id:
:param symbol:
:param side:
:param fee:
:return:
"""
@@ -1344,7 +1172,7 @@ async def set_fee_user_auto_trading(tg_id: int, symbol: str, side: str, fee: flo
return False
result = await session.execute(
select(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol, side=side)
select(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol)
)
record = result.scalars().first()
@@ -1355,7 +1183,6 @@ async def set_fee_user_auto_trading(tg_id: int, symbol: str, side: str, fee: flo
user_id=user.id,
symbol=symbol,
fee=fee,
side=side
)
session.add(user_fee)
await session.commit()
@@ -1364,3 +1191,41 @@ async def set_fee_user_auto_trading(tg_id: int, symbol: str, side: str, fee: flo
except Exception as e:
logger.error("Error setting user auto trading fee for user %s and symbol %s: %s", tg_id, symbol, e)
return False
async def set_total_fee_user_auto_trading(tg_id: int, symbol: str, total_fee: float) -> bool:
"""
Set the total fee for a user auto trading in the database.
:param tg_id: Telegram user ID
:param symbol: Symbol
:param total_fee: Total fee
:return: bool
"""
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(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol)
)
record = result.scalars().first()
if record:
record.total_fee = total_fee
else:
user_total_fee = UserAutoTrading(
user_id=user.id,
symbol=symbol,
total_fee=total_fee,
)
session.add(user_total_fee)
await session.commit()
logger.info("Set total fee for user %s and symbol %s", tg_id, symbol)
return True
except Exception as e:
logger.error("Error setting user auto trading total fee for user %s and symbol %s: %s", tg_id, symbol, e)
return False

3
run.py
View File

@@ -5,10 +5,10 @@ import logging.config
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.redis import RedisStorage
from database import init_db
from app.bybit.web_socket import WebSocketBot
from app.telegram.handlers import router
from config import BOT_TOKEN
from database import init_db
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
@@ -46,7 +46,6 @@ async def main():
with contextlib.suppress(asyncio.CancelledError):
await ws_task
await tg_task
await web_socket.clear_user_sockets()
except Exception as e:
logger.error("Bot stopped with error: %s", e)