2
0
forked from kodorvan/stcs

67 Commits

Author SHA1 Message Date
8706449c78 redis 2025-12-24 11:45:59 +05:00
89a3c70e4b venv 2025-12-23 12:58:30 +05:00
98cc3c248c Merge pull request 'Fixed the work of the websocket' (#36) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#36
2025-12-18 22:19:34 +07:00
algizn97
cb6499e347 Fixed the work of the websocket 2025-12-18 18:46:21 +05:00
7494f85202 Merge pull request 'Fixed the websocket' (#35) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#35
2025-12-16 00:18:12 +07:00
algizn97
867802b2a7 Fixed the websocket 2025-12-15 21:57:13 +05:00
28c9614ecb Merge pull request 'devel' (#34) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#34
2025-11-20 14:33:56 +07:00
algizn97
1f123c77e7 Fixed the websocket 2025-11-19 16:50:18 +05:00
algizn97
56729c287b Step comparison postponed 2025-11-17 20:40:24 +05:00
algizn97
f268d3290b Added stop loss setting in isolated mode and start trading with the base rate when the maximum number of steps is reached. 2025-11-17 20:39:06 +05:00
algizn97
856169cba9 Added API key verification for permissions 2025-11-14 13:56:08 +05:00
d6b36799dc Merge pull request 'Fixed database initialization' (#33) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#33
2025-11-13 01:24:04 +07:00
algizn97
0bc74ed188 Fixed database initialization 2025-11-12 22:18:38 +05:00
0b3e9ff476 Merge pull request 'Creating a new database' (#32) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#32
2025-11-11 23:11:01 +07:00
algizn97
d7b558664b Creating a new database 2025-11-10 09:44:29 +05:00
ec7e10f7a1 Merge pull request 'Updated database, added new migrations' (#31) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#31
2025-11-09 22:28:32 +07:00
algizn97
34922e6998 Updated database, added new migrations 2025-11-09 15:18:18 +05:00
37257d2ec2 Merge pull request 'The logic of the trading cycle has been changed' (#30) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#30
2025-11-09 15:35:45 +07:00
algizn97
7c85c03d10 The logic of the trading cycle has been changed, fixed errors when setting TP and SL 2025-11-09 13:10:13 +05:00
algizn97
39bbe8d997 Added user verification 2025-11-09 13:07:11 +05:00
88c358b90e Merge pull request 'Fixed websocket' (#29) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#29
2025-11-02 19:55:17 +07:00
algizn97
3ba32660ea The log is hidden 2025-11-02 17:33:11 +05:00
algizn97
e0167ea406 Fixed the function of setting TP and SL 2025-11-02 17:14:22 +05:00
algizn97
dbf0a30d54 The message receipt has been sorted 2025-11-02 17:13:36 +05:00
a7a23a4662 Merge pull request 'Added error information output' (#28) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#28
2025-10-30 23:37:38 +07:00
algizn97
78f21e6718 Fixed message output for certain conditions 2025-10-30 13:47:29 +05:00
algizn97
6416bd6dc9 Added output in case of installation error TP and SL 2025-10-30 13:21:22 +05:00
algizn97
29c168e31d Added error information output 2025-10-30 12:45:22 +05:00
245cadf650 Merge pull request 'devel' (#27) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#27
2025-10-30 12:28:18 +07:00
algizn97
e043a2429f The logic of setting take profit and stop loss has been changed, added data for the end user 2025-10-29 20:58:04 +05:00
algizn97
a8119d2811 adjusted percentages of TP and SL 2025-10-29 20:55:51 +05:00
algizn97
7e4c936ef5 Added pnl, tp and sl column 2025-10-29 20:54:29 +05:00
algizn97
d8866af185 Added pnl, tp and sl column 2025-10-29 20:54:20 +05:00
8d32439a15 Merge pull request 'Switched to demo mode' (#26) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#26
2025-10-27 21:45:00 +07:00
algizn97
9497cca3e0 Switched to demo mode 2025-10-27 13:05:19 +05:00
690d793e8c Merge pull request 'Fixed the counting of the series, added a request to set the serial number' (#25) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#25
2025-10-26 22:09:25 +07:00
algizn97
a0ef48810a Fixed the counting of the series, added a request to set the serial number 2025-10-26 19:44:16 +05:00
ab752b5dd8 Merge pull request 'adjusted profit' (#24) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#24
2025-10-26 21:18:20 +07:00
algizn97
a2164853d9 adjusted profit 2025-10-26 19:17:38 +05:00
algizn97
92bb052151 adjusted profit 2025-10-26 19:11:37 +05:00
ca7bd5c795 Merge pull request 'Shtoto' (#23) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#23
2025-10-26 16:39:41 +07:00
algizn97
62e923cefa Fixed the place for commission compensation 2025-10-26 14:03:28 +05:00
algizn97
faae2475c1 Added a function to select the type of cost compensation. 2025-10-26 14:02:42 +05:00
algizn97
3ef8eae997 Added the output of the current series number and the transaction 2025-10-26 14:01:55 +05:00
algizn97
2bebada215 Added a new column and requests to them 2025-10-26 14:01:12 +05:00
algizn97
8be1636279 Added buttons to select commission compensation 2025-10-26 14:00:32 +05:00
algizn97
b756aadf26 Added columns for commission 2025-10-26 13:59:50 +05:00
46c890f7af Merge pull request 'The range of TP and SL settings has been changed' (#22) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#22
2025-10-25 23:43:24 +07:00
algizn97
f10500cc79 The range of TP and SL settings has been changed 2025-10-25 21:35:44 +05:00
2d7acb491e Merge pull request 'разъебаться по полной' (#21) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#21
2025-10-25 21:05:58 +07:00
algizn97
d767399988 Added a function to set the direction of the first transaction 2025-10-25 18:49:36 +05:00
algizn97
89603f0b62 Fixed the direction at the start of the series 2025-10-25 18:49:06 +05:00
algizn97
14f2a9e773 Added the display of the first transaction 2025-10-25 18:48:25 +05:00
algizn97
a43fc6a66b Added the side parameter and the request 2025-10-25 18:47:07 +05:00
algizn97
869458b2e1 Added a button to select the direction of the first transaction. 2025-10-25 18:46:26 +05:00
algizn97
07948d93cf Added a new migration for the database 2025-10-25 18:45:42 +05:00
12d1db16d3 вот бы ебануло нормально...
Reviewed-on: kodorvan/stcs#20
2025-10-25 19:54:27 +07:00
algizn97
7350c86927 The text has been corrected, fixed the commission check 2025-10-25 17:50:46 +05:00
0a369b10f2 Merge pull request 'devel' (#19) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#19
2025-10-23 14:35:32 +07:00
7b1a803db4 Merge pull request 'Fixed the switch trading mode, adjusted the take profit, added a trading cycle' (#18) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#18
2025-10-22 22:20:21 +07:00
9fcd92cc72 Merge pull request 'The formula for calculating the number of contracts by price has been changed' (#17) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#17
2025-10-21 20:33:42 +07:00
97a199f31e Merge pull request 'devel' (#16) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#16
2025-10-18 18:09:23 +07:00
951bc15957 Merge pull request 'devel' (#15) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#15
2025-10-12 17:26:11 +07:00
5937058899 Merge pull request 'The database has been converted to SQLite' (#14) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#14
2025-10-12 14:35:54 +07:00
f0732607e2 Merge pull request 'The instruction has been corrected' (#13) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#13
2025-10-11 16:36:48 +07:00
56af1d8f3b Merge pull request 'Added migrations for the database' (#12) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#12
2025-10-11 15:49:40 +07:00
9f069df68a Merge pull request 'devel' (#11) from Alex/stcs:devel into stable
Reviewed-on: kodorvan/stcs#11
2025-10-11 11:58:36 +07:00
25 changed files with 1164 additions and 502 deletions

3
.gitignore vendored
View File

@@ -210,3 +210,6 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/
stcs_venv
venv

View File

@@ -1,6 +1,11 @@
Crypto Trading Telegram Bot
# Crypto Trading Telegram Bot by [KODORVAN](https://git.svoboda.works/kodorvan)
Этот бот — автоматизированный торговый помощник для работы с криптовалютной биржей Bybit на основе стратегии мартингейла. Он позволяет торговать бессрочными контрактами с управлением рисками, тейк-профитами, стоп-лоссами и кредитным плечом.
Автоматизированный торговый помощник для работы с криптовалютной биржей Bybit на основе стратегии мартингейла.<br>
Он позволяет торговать бессрочными контрактами с управлением рисками, тейк-профитами, стоп-лоссами и кредитным плечом.
**Разработано командой [КОДОРВАНЬ](https://git.svoboda.works/kodorvan)**<br>
_Мы окажем полное содействие и поддержку, пишите в телеграм: https://t.me/kodorvan_
## Основные возможности
@@ -59,7 +64,12 @@ nvim .env
alembic upgrade head
```
5. Запустите бота:
6. Убедитесь в том, что установлен redis и открыт порт 6789
```bash
sudo ufw allow 6789
```
7. Запустите бота:
```bash
python run.py
@@ -114,4 +124,4 @@ sudo service stcs status
- Бот требует аккуратной настройки параметров риска.
- Храните API ключи в безопасности, избегайте публикации.
- Храните API ключи в безопасности, избегайте публикации.

View File

@@ -84,7 +84,7 @@ path_separator = os
# 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
sqlalchemy.url = sqlite+aiosqlite:///./database/stcs.db
[post_write_hooks]

View File

@@ -1,8 +1,8 @@
"""Added side_mode column
"""initial
Revision ID: fbf4e3658310
Revision ID: f6e7eb3f25c0
Revises:
Create Date: 2025-10-22 13:08:02.317419
Create Date: 2025-11-12 22:53:02.189445
"""
from typing import Sequence, Union
@@ -12,7 +12,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'fbf4e3658310'
revision: str = 'f6e7eb3f25c0'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -21,12 +21,12 @@ 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))
pass
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user_deals', 'side_mode')
pass
# ### end Alembic commands ###

View File

@@ -15,7 +15,7 @@ async def get_bybit_client(tg_id: int) -> HTTP | None:
"""
try:
api_key, api_secret = await rq.get_user_api(tg_id=tg_id)
return HTTP(api_key=api_key, api_secret=api_secret)
return HTTP(demo=True, api_key=api_key, api_secret=api_secret)
except Exception as e:
logger.error("Error getting bybit client for user %s: %s", tg_id, e)
return None

View File

@@ -38,7 +38,7 @@ async def get_active_positions(tg_id: int) -> list | None:
return None
async def get_active_positions_by_symbol(tg_id: int, symbol: str) -> dict | None:
async def get_active_positions_by_symbol(tg_id: int, symbol: str):
"""
Get active positions for a user by symbol
"""
@@ -62,8 +62,12 @@ async def get_active_positions_by_symbol(tg_id: int, symbol: str) -> dict | None
)
return None
except Exception as e:
logger.error("Error getting active positions for user %s: %s", tg_id, e)
return None
errors = str(e)
if errors.startswith("Permission denied, please check your API key permissions"):
return "Invalid API key permissions"
else:
logger.error("Error getting active positions for user %s: %s", tg_id, e)
return None
async def get_active_orders(tg_id: int) -> list | None:

View File

@@ -28,11 +28,9 @@ async def start_trading_cycle(
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
side = additional_data.side
margin_type = additional_data.margin_type
leverage = additional_data.leverage
order_quantity = additional_data.order_quantity
@@ -41,21 +39,11 @@ async def start_trading_cycle(
max_bets_in_series = additional_data.max_bets_in_series
take_profit_percent = risk_management_data.take_profit_percent
stop_loss_percent = risk_management_data.stop_loss_percent
total_commission = 0
get_side = "Buy"
if user_deals_data:
get_side = user_deals_data.last_side or "Buy"
commission_fee = risk_management_data.commission_fee
commission_place = risk_management_data.commission_place
if trade_mode == "Switch":
if switch_side == "По направлению":
side = get_side
else:
if get_side == "Buy":
side = "Sell"
else:
side = "Buy"
side = side
else:
if trade_mode == "Long":
side = "Buy"
@@ -66,11 +54,32 @@ async def start_trading_cycle(
tg_id=tg_id,
symbol=symbol,
mode=0)
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
await set_leverage(
await rq.set_user_deal(
tg_id=tg_id,
symbol=symbol,
current_step=1,
current_series=1,
trade_mode=trade_mode,
side_mode=switch_side,
margin_type=margin_type,
leverage=leverage,
order_quantity=order_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=order_quantity,
commission_fee=commission_fee,
commission_place=commission_place,
pnl_series=0
)
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 open_positions(
@@ -79,30 +88,10 @@ async def start_trading_cycle(
side=side,
order_quantity=order_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_commission
leverage=leverage
)
if res == "OK":
await rq.set_user_deal(
tg_id=tg_id,
symbol=symbol,
current_step=1,
trade_mode=trade_mode,
side_mode=switch_side,
margin_type=margin_type,
leverage=leverage,
order_quantity=order_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=order_quantity
)
return "OK"
return (
res
@@ -118,7 +107,9 @@ async def start_trading_cycle(
"position idx not match position mode",
"Qty invalid",
"The number of contracts exceeds maximum limit allowed",
"The number of contracts exceeds minimum limit allowed"
"The number of contracts exceeds minimum limit allowed",
"Order placement failed as your position may exceed the max",
"Permission denied, please check your API key permissions"
}
else None
)
@@ -132,8 +123,6 @@ async def trading_cycle_profit(
tg_id: int, symbol: str, side: str) -> str | None:
try:
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol)
total_fee = user_auto_trading_data.total_fee
trade_mode = user_deals_data.trade_mode
margin_type = user_deals_data.margin_type
leverage = user_deals_data.leverage
@@ -144,6 +133,9 @@ async def trading_cycle_profit(
martingale_factor = user_deals_data.martingale_factor
side_mode = user_deals_data.side_mode
base_quantity = user_deals_data.base_quantity
current_series = user_deals_data.current_series
commission_fee = user_deals_data.commission_fee
commission_place = user_deals_data.commission_place
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
await set_leverage(
@@ -152,7 +144,6 @@ async def trading_cycle_profit(
leverage=leverage,
)
if trade_mode == "Switch":
if side_mode == "Противоположно":
s_side = "Sell" if side == "Buy" else "Buy"
@@ -161,36 +152,39 @@ async def trading_cycle_profit(
else:
s_side = side
next_series = current_series + 1
await rq.set_user_deal(
tg_id=tg_id,
symbol=symbol,
current_step=1,
current_series=next_series,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
leverage=leverage,
order_quantity=base_quantity,
trigger_price=trigger_price,
martingale_factor=martingale_factor,
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity,
commission_fee=commission_fee,
commission_place=commission_place,
pnl_series=0
)
res = await open_positions(
tg_id=tg_id,
symbol=symbol,
side=s_side,
order_quantity=base_quantity,
trigger_price=trigger_price,
margin_type=margin_type,
leverage=leverage,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
commission_fee_percent=total_fee
leverage=leverage
)
if res == "OK":
await rq.set_user_deal(
tg_id=tg_id,
symbol=symbol,
current_step=1,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
leverage=leverage,
order_quantity=base_quantity,
trigger_price=trigger_price,
martingale_factor=martingale_factor,
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity
)
return "OK"
return (
@@ -201,6 +195,7 @@ async def trading_cycle_profit(
"ab not enough for new order",
"InvalidRequestError",
"The number of contracts exceeds maximum limit allowed",
"Order placement failed as your position may exceed the max",
}
else None
)
@@ -215,6 +210,8 @@ async def trading_cycle(
try:
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol)
commission_fee = user_deals_data.commission_fee
commission_place = user_deals_data.commission_place
total_fee = user_auto_trading_data.total_fee
trade_mode = user_deals_data.trade_mode
margin_type = user_deals_data.margin_type
@@ -228,21 +225,24 @@ async def trading_cycle(
order_quantity = user_deals_data.order_quantity
base_quantity = user_deals_data.base_quantity
side_mode = user_deals_data.side_mode
current_series = user_deals_data.current_series
pnl_series = user_deals_data.pnl_series
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,
)
total_quantity = next_quantity
if commission_fee == "Yes_commission_fee":
if commission_place == "Commission_for_qty":
total_quantity = next_quantity + total_fee
if trade_mode == "Switch":
if side == "Buy":
@@ -252,36 +252,37 @@ async def trading_cycle(
else:
r_side = side
await rq.set_user_deal(
tg_id=tg_id,
symbol=symbol,
current_step=current_step,
current_series=current_series,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
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,
commission_fee=commission_fee,
commission_place=commission_place,
pnl_series=pnl_series
)
res = await open_positions(
tg_id=tg_id,
symbol=symbol,
side=r_side,
order_quantity=next_quantity,
order_quantity=total_quantity,
trigger_price=trigger_price,
margin_type=margin_type,
leverage=leverage,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
commission_fee_percent=total_fee
leverage=leverage
)
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 (
@@ -292,6 +293,7 @@ async def trading_cycle(
"ab not enough for new order",
"InvalidRequestError",
"The number of contracts exceeds maximum limit allowed",
"Order placement failed as your position may exceed the max",
}
else None
)
@@ -307,23 +309,24 @@ async def open_positions(
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
leverage: str
) -> str | None:
try:
client = await get_bybit_client(tg_id=tg_id)
get_ticker = await get_tickers(tg_id, symbol=symbol)
price_symbol = safe_float(get_ticker.get("lastPrice")) or 0
if get_ticker is None:
price_symbol = 0
else:
price_symbol = safe_float(get_ticker.get("lastPrice"))
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)
qty_format = math.floor(qty / qty_step) * qty_step
qty_formatted = round(qty_format, decimals)
if trigger_price > 0:
po_trigger_price = str(trigger_price)
@@ -332,33 +335,6 @@ async def open_positions(
po_trigger_price = None
trigger_direction = None
price_for_cals = trigger_price if po_trigger_price is not None else price_symbol
if qty_formatted <= 0:
return "Order does not meet minimum order value"
if margin_type == "ISOLATED_MARGIN":
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 = price_for_cals * (
1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted
stop_loss_price = None
else:
if side == "Buy":
take_profit_price = price_for_cals * (
1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted
stop_loss_price = price_for_cals * (1 - stop_loss_percent / 100)
else:
take_profit_price = price_for_cals * (
1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted
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)
# Place order
order_params = {
"category": "linear",
@@ -371,9 +347,6 @@ async def open_positions(
"triggerBy": "LastPrice",
"timeInForce": "GTC",
"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,
}
response = client.place_order(**order_params)
@@ -395,6 +368,8 @@ async def open_positions(
"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",
"Order placement failed as your position may exceed the max": "Order placement failed as your position may exceed the max",
"Permission denied, please check your API key permissions": "Permission denied, please check your API key permissions"
}
for key, msg in known_errors.items():
if key in error_text:

View File

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

View File

@@ -13,7 +13,7 @@ async def set_tp_sl_for_position(
take_profit_price: float,
stop_loss_price: float,
position_idx: int,
) -> bool:
) -> bool | str:
"""
Set take profit and stop loss for a symbol.
:param tg_id: Telegram user ID
@@ -21,15 +21,17 @@ async def set_tp_sl_for_position(
:param take_profit_price: Take profit price
:param stop_loss_price: Stop loss price
:param position_idx: Position index
:return: bool
:return: bool or str
"""
try:
client = await get_bybit_client(tg_id)
take_profit = round(take_profit_price, 6) if take_profit_price is not None else None
stop_loss = round(stop_loss_price, 6) if stop_loss_price is not None else None
resp = client.set_trading_stop(
category="linear",
symbol=symbol,
takeProfit=str(round(take_profit_price, 5)),
stopLoss=str(round(stop_loss_price, 5)),
takeProfit=str(take_profit) if take_profit is not None else None,
stopLoss=str(stop_loss) if stop_loss is not None else None,
positionIdx=position_idx,
tpslMode="Full",
)
@@ -38,8 +40,18 @@ async def set_tp_sl_for_position(
logger.info("TP/SL for %s has been set", symbol)
return True
else:
logger.error("Error setting TP/SL for %s: %s", symbol, resp.get("retMsg"))
return False
error_msg = resp.get("retMsg")
if "not modified" in error_msg.lower():
logger.info("TP/SL for %s not modified: %s", symbol, error_msg)
return "not modified"
else:
logger.error("Error setting TP/SL for %s: %s", symbol, error_msg)
return False
except Exception as e:
logger.error("Error setting TP/SL for %s: %s", symbol, e)
return False
error_msg = str(e)
if "not modified" in error_msg.lower():
logger.info("TP/SL for %s not modified: %s", symbol, error_msg)
return "not modified"
else:
logger.error("Error set TP/SL for %s: %s", symbol, e)
return False

View File

@@ -1,10 +1,13 @@
import logging.config
import math
import json
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.bybit.get_functions.get_instruments_info import get_instruments_info
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
from app.bybit.open_positions import trading_cycle, trading_cycle_profit
from app.helper_functions import format_value, safe_float
from app.bybit.set_functions.set_tp_sl import set_tp_sl_for_position
from app.helper_functions import format_value, safe_float, truncate_float
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("telegram_message_handler")
@@ -12,193 +15,351 @@ logger = logging.getLogger("telegram_message_handler")
class TelegramMessageHandler:
def __init__(self, telegram_bot):
"""Initialize the TelegramMessageHandler class."""
self.telegram_bot = telegram_bot
async def format_position_update(self, message):
pass
async def format_order_update(self, message, tg_id):
"""Handle order updates."""
try:
order_data = message.get("data", [{}])[0]
symbol = format_value(order_data.get("symbol"))
qty = format_value(order_data.get("qty"))
side = format_value(order_data.get("side"))
side_rus = (
"Покупка"
if side == "Buy"
else "Продажа" if side == "Sell" else "Нет данных"
)
order_status = format_value(order_data.get("orderStatus"))
price = format_value(order_data.get("price"))
trigger_price = format_value(order_data.get("triggerPrice"))
take_profit = format_value(order_data.get("takeProfit"))
stop_loss = format_value(order_data.get("stopLoss"))
# logger.info("Order update: %s", json.dumps(message))
user_additional_data = await rq.get_user_additional_settings(tg_id=tg_id)
trigger_price = safe_float(user_additional_data.trigger_price)
if trigger_price > 0:
order_data = message.get("data", [{}])[0]
symbol = format_value(order_data.get("symbol"))
side = format_value(order_data.get("side"))
side_rus = (
"Покупка"
if side == "Buy"
else "Продажа" if side == "Sell" else "Нет данных"
)
order_status = format_value(order_data.get("orderStatus"))
tr_price = format_value(order_data.get("triggerPrice"))
status_map = {
"Untriggered": "Условный ордер выставлен",
}
status_map = {
"Untriggered": "Условный ордер выставлен",
}
if order_status == "Filled" or order_status not in status_map:
return None
if order_status == "Filled" or order_status not in status_map:
return None
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"Торговая пара: {symbol}\n"
f"Движение: {side_rus}\n"
)
if tr_price and tr_price != "Нет данных":
text += f"Триггер цена: {tr_price}\n"
text = (
f"Торговая пара: {symbol}\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 != "Нет данных":
text += f"Тейк-профит: {take_profit}\n"
if stop_loss and stop_loss != "Нет данных":
text += f"Стоп-лосс: {stop_loss}\n"
if trigger_price and trigger_price != "Нет данных":
text += f"Триггер цена: {trigger_price}\n"
await self.telegram_bot.send_message(
chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
)
await self.telegram_bot.send_message(
chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
)
await rq.set_trigger_price(tg_id=tg_id, trigger_price=0)
except Exception as e:
logger.error("Error in format_order_update: %s", e)
async def format_execution_update(self, message, tg_id):
"""Handle execution updates without duplicate processing."""
try:
# logger.info("Execution update: %s", json.dumps(message))
execution = message.get("data", [{}])[0]
closed_size = format_value(execution.get("closedSize"))
symbol = format_value(execution.get("symbol"))
exec_price = format_value(execution.get("execPrice"))
exec_qty = format_value(execution.get("execQty"))
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)
exec_type = format_value(execution.get("execType"))
if exec_type == "Trade" or exec_type == "BustTrade":
closed_size = format_value(execution.get("closedSize"))
symbol = format_value(execution.get("symbol"))
exec_price = format_value(execution.get("execPrice"))
exec_qty = format_value(execution.get("execQty"))
exec_fees = format_value(execution.get("execFee"))
fee_rate = format_value(execution.get("feeRate"))
side = format_value(execution.get("side"))
exec_pnl = format_value(execution.get("execPnl"))
stop_order_type = format_value(execution.get("stopOrderType"))
create_type = format_value(execution.get("createType"))
if safe_float(closed_size) == 0:
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=safe_float(exec_fee)
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_auto_trading = await rq.get_user_auto_trading(
tg_id=tg_id, symbol=symbol
)
if auto_trading:
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)
get_total_fee = user_auto_trading.total_fee
total_fee = safe_float(exec_fee) + safe_float(get_total_fee)
if safe_float(closed_size) == 0:
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=safe_float(exec_fee)
)
get_total_fee = 0
if user_auto_trading is not None and user_auto_trading.fee is not None:
fee = user_auto_trading.fee
else:
fee = 0
if user_auto_trading is not None:
get_total_fee = user_auto_trading.total_fee
exec_pnl = format_value(execution.get("execPnl"))
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
commission_fee = risk_management_data.commission_fee
total_fee = safe_float(exec_fee) + safe_float(get_total_fee)
ex_pnl = safe_float(exec_pnl)
if commission_fee == "Yes_commission_fee":
total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee
else:
total_pnl = safe_float(exec_pnl)
header = (
"Сделка закрыта:" if safe_float(closed_size) > 0 else "Сделка открыта:"
)
text = f"{header}\n" f"Торговая пара: {symbol}\n"
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
)
header = (
"Сделка закрыта:" if safe_float(closed_size) > 0 else "Сделка открыта:"
)
text = f"{header}\n" f"Торговая пара: {symbol}\n"
commission_fee = user_deals_data.commission_fee
commission_place = user_deals_data.commission_place
current_series = user_deals_data.current_series
current_step = user_deals_data.current_step
order_quantity = user_deals_data.order_quantity
pnl_series = user_deals_data.pnl_series
take_profit_percent = user_deals_data.take_profit_percent
stop_loss_percent = user_deals_data.stop_loss_percent
leverage = safe_float(user_deals_data.leverage)
fee = safe_float(user_auto_trading.fee)
total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee
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"
if commission_fee == "Yes_commission_fee":
if commission_place == "Commission_for_qty":
total_quantity = safe_float(order_quantity) + safe_float(
total_fee
) * 2
else:
total_quantity = safe_float(order_quantity)
else:
total_quantity = safe_float(order_quantity)
text += (
f"Цена исполнения: {exec_price}\n"
f"Комиссия: {exec_fee:.8f}\n"
)
if user_deals_data is not None and safe_float(closed_size) == 0:
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=total_fee
)
text += f"Текущая ставка: {total_quantity:.2f} USDT\n"
text += f"Серия №: {current_series}\n"
text += f"Сделка №: {current_step}\n"
if safe_float(closed_size) == 0:
text += f"Движение: {side_rus}\n"
else:
text += f"\nРеализованная прибыль: {total_pnl:.7f}\n"
text += (
f"Цена исполнения: {exec_price}\n"
f"Комиссия: {exec_fee:.8f}\n"
)
await self.telegram_bot.send_message(
chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
)
if safe_float(closed_size) == 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(exec_price)
decimals = abs(int(round(math.log10(qty_step))))
qty_format = math.floor(qty / qty_step) * qty_step
qty_formatted = round(qty_format, decimals)
total_commission = 0
user_symbols = user_auto_trading.symbol if user_auto_trading else None
if commission_fee == "Yes_commission_fee":
if commission_place == "Commission_for_tp":
total_commission = safe_float(total_fee) / qty_formatted
if side == "Buy":
take_profit_price = safe_float(exec_price) * (
1 + take_profit_percent / 100) + total_commission
stop_loss_price = safe_float(exec_price) * (1 - stop_loss_percent / 100)
else:
take_profit_price = safe_float(exec_price) * (
1 - take_profit_percent / 100) - total_commission
stop_loss_price = safe_float(exec_price) * (1 + stop_loss_percent / 100)
ress = await set_tp_sl_for_position(tg_id=tg_id,
symbol=symbol,
take_profit_price=take_profit_price,
stop_loss_price=stop_loss_price,
position_idx=0)
if ress or ress == "not modified":
take_profit_truncated = await truncate_float(take_profit_price, 6)
stop_loss_truncated = await truncate_float(stop_loss_price, 6)
text += (f"Движение: {side_rus}\n"
f"Тейк-профит: {take_profit_truncated}\n"
f"Стоп-лосс: {stop_loss_truncated}\n"
)
else:
text += (f"Движение: {side_rus}\n"
"Не удалось установить ТП и СЛ\n")
elif safe_float(closed_size) > 0 and auto_trading:
new_pnl = safe_float(pnl_series) + total_pnl
await rq.set_pnl_series_by_symbol(
tg_id=tg_id, symbol=symbol, pnl_series=new_pnl)
text += f"\nПрибыль без комиссии: {ex_pnl:.4f}\n"
text += f"Реализованная прибыль: {total_pnl:.4f}\n"
text += f"Прибыль серии: {safe_float(new_pnl):.4f}\n"
if (
auto_trading
and safe_float(closed_size) > 0
and user_symbols is not None
):
if safe_float(total_pnl) > 0:
profit_text = "📈 Прибыль достигнута. Начинаем новую серию с базовой ставки\n"
await self.telegram_bot.send_message(
chat_id=tg_id, text=profit_text, reply_markup=kbi.profile_bybit
chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
if stop_order_type == "TakeProfit":
profit_text = "📈 Начинаю новую серию с базовой ставки\n"
await self.telegram_bot.send_message(
chat_id=tg_id, text=profit_text, reply_markup=kbi.profile_bybit
)
await rq.set_last_side_by_symbol(
tg_id=tg_id, symbol=symbol, last_side=r_side)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
res = await trading_cycle_profit(
tg_id=tg_id, symbol=symbol, 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
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
await rq.set_pnl_series_by_symbol(tg_id=tg_id, symbol=symbol, pnl_series=0)
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, "❗️ Не удалось открыть новую сделку"
res = await trading_cycle_profit(
tg_id=tg_id, symbol=symbol, side=r_side
)
if res == "OK":
pass
else:
errors = {
"Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
"ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
"InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
"The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки",
"Order placement failed as your position may exceed the max": "❗️ Превышен максимальный лимит ставки",
}
error_text = errors.get(
res, "❗️ Не удалось открыть новую сделку"
)
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False
)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
await self.telegram_bot.send_message(
chat_id=tg_id,
text=error_text,
reply_markup=kbi.profile_bybit,
)
elif stop_order_type == "StopLoss" or exec_type == "BustTrade":
current_step = user_deals_data.current_step
max_bets_in_series = user_deals_data.max_bets_in_series
current_step += 1
if max_bets_in_series < current_step:
text_series = ("\n❗️ Максимальное количество сделок в серии достигнуто.\n"
"📈 Начинаю новую серию с базовой ставки\n")
await self.telegram_bot.send_message(
chat_id=tg_id, text=text_series
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
await rq.set_last_side_by_symbol(
tg_id=tg_id, symbol=symbol, last_side=r_side)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
await rq.set_pnl_series_by_symbol(tg_id=tg_id, symbol=symbol, pnl_series=0)
res = await trading_cycle_profit(
tg_id=tg_id, symbol=symbol, side=r_side
)
if res == "OK":
pass
else:
errors = {
"Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
"ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
"InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
"The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки",
"Order placement failed as your position may exceed the max": "❗️ Превышен максимальный лимит ставки",
}
error_text = errors.get(
res, "❗️ Не удалось открыть новую сделку"
)
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False
)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
await self.telegram_bot.send_message(
chat_id=tg_id,
text=error_text,
reply_markup=kbi.profile_bybit,
)
else:
open_order_text = "\n❗️ Открываю новую сделку с увеличенной ставкой.\n"
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 = {
"Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
"ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
"InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
"The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки",
"Order placement failed as your position may exceed the max": "❗️ Превышен максимальный лимит ставки с текущим плечом",
}
error_text = errors.get(
res, "❗️ Не удалось открыть новую сделку"
)
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False
)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
await self.telegram_bot.send_message(
chat_id=tg_id,
text=error_text,
reply_markup=kbi.profile_bybit,
)
elif create_type == "CreateByClosing":
await self.telegram_bot.send_message(
chat_id=tg_id,
text=f"❗️ Торговля для {symbol} остановлена",
reply_markup=kbi.profile_bybit,
)
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False
@@ -210,54 +371,9 @@ class TelegramMessageHandler:
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
await self.telegram_bot.send_message(
chat_id=tg_id,
text=error_text,
reply_markup=kbi.profile_bybit,
)
else:
open_order_text = "\n❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n"
await self.telegram_bot.send_message(
chat_id=tg_id, text=open_order_text
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
res = await trading_cycle(
tg_id=tg_id, symbol=symbol, 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,
)
logger.info("Stop trading for symbol: %s, create_type: %s, stop_order_type: %s: %s",
symbol, create_type, stop_order_type, tg_id)
else:
logger.info("Execution update: %s", json.dumps(message))
except Exception as e:
logger.error("Error in telegram_message_handler: %s", e)
logger.error("Error in telegram_message_handler: %s", e, exc_info=True)

View File

@@ -1,4 +1,5 @@
import asyncio
from collections import deque
import logging.config
from pybit.unified_trading import WebSocket
@@ -11,112 +12,178 @@ logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("web_socket")
class CustomWebSocket(WebSocket):
"""Custom WebSocket wrapper with enhanced error handling."""
def _on_error(self, error):
logger.error(f"WebSocket error: {error}")
return super()._on_error(error)
def _on_close(self):
logger.warning("WebSocket connection closed")
super()._on_close()
class WebSocketBot:
"""
Class to handle WebSocket connections and messages.
Manages multiple Bybit private WebSocket connections for Telegram users.
Uses queue-based message processing to handle thread-safe async calls.
"""
def __init__(self, telegram_bot):
"""Initialize the TradingBot class."""
"""
Initialize WebSocketBot.
Args:
telegram_bot: Telegram bot instance for message handling
"""
self.telegram_bot = telegram_bot
self.ws_private = None
self.user_messages = {}
self.user_sockets = {}
self.user_messages = {}
self.user_keys = {}
self.loop = None
self.message_handler = TelegramMessageHandler(telegram_bot)
self.order_queues = {} # {tg_id: deque}
self.execution_queues = {} # {tg_id: deque}
self.processing_tasks = {} # {tg_id: task}
async def run_user_check_loop(self):
"""Run a loop to check for users and connect them to the WebSocket."""
"""Main loop that continuously checks users and maintains connections."""
self.loop = asyncio.get_running_loop()
logger.info("Starting WebSocket user check loop")
while True:
users = await WebSocketBot.get_users_from_db()
for user in users:
tg_id = user.tg_id
api_key, api_secret = await rq.get_user_api(tg_id=tg_id)
try:
users = await WebSocketBot.get_users_from_db()
for user in users:
tg_id = user.tg_id
api_key, api_secret = await rq.get_user_api(tg_id=tg_id)
if not api_key or not api_secret:
continue
if not api_key or not api_secret:
continue
keys_stored = self.user_keys.get(tg_id)
if tg_id in self.user_sockets and keys_stored == (api_key, api_secret):
continue
keys_stored = self.user_keys.get(tg_id)
socket_exists = tg_id in self.user_sockets
if tg_id in self.user_sockets:
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
)
if socket_exists and keys_stored == (api_key, api_secret):
continue
success = await self.try_connect_user(api_key, api_secret, tg_id)
if success:
self.user_keys[tg_id] = (api_key, api_secret)
self.user_messages.setdefault(
tg_id, {"position": None, "order": None, "execution": None}
)
logger.info("User %s connected to WebSocket", tg_id)
else:
await asyncio.sleep(30)
if socket_exists:
await self.close_user_socket(tg_id)
success = await self.try_connect_user(api_key, api_secret, tg_id)
if success:
self.user_keys[tg_id] = (api_key, api_secret)
self.user_messages.setdefault(
tg_id, {"position": None, "order": None, "execution": None}
)
logger.info("User %s successfully connected", tg_id)
except Exception as e:
logger.error("Error in user check loop: %s", e)
await asyncio.sleep(10)
async def clear_user_sockets(self):
"""Clear the user_sockets and user_messages dictionaries."""
self.user_sockets.clear()
self.user_messages.clear()
self.user_keys.clear()
logger.info("Cleared user_sockets")
async def try_connect_user(self, api_key, api_secret, tg_id):
"""Try to connect a user to the WebSocket."""
"""
Create and setup WebSocket streams with thread-safe queues.
"""
try:
self.ws_private = WebSocket(
ws = CustomWebSocket(
demo=True,
testnet=False,
channel_type="private",
api_key=api_key,
api_secret=api_secret,
api_secret=api_secret
)
self.user_sockets[tg_id] = self.ws_private
# Connect to the WebSocket private channel
# Handle position updates
self.ws_private.position_stream(
lambda msg: self.loop.call_soon_threadsafe(
asyncio.create_task, self.handle_position_update(msg)
)
self.user_sockets[tg_id] = ws
self.order_queues[tg_id] = deque()
self.execution_queues[tg_id] = deque()
self.processing_tasks[tg_id] = asyncio.create_task(
self._process_order_queue(tg_id)
)
# Handle order updates
self.ws_private.order_stream(
lambda msg: self.loop.call_soon_threadsafe(
asyncio.create_task, self.handle_order_update(msg, tg_id)
)
)
# Handle execution updates
self.ws_private.execution_stream(
lambda msg: self.loop.call_soon_threadsafe(
asyncio.create_task, self.handle_execution_update(msg, tg_id)
)
self.processing_tasks[tg_id + 1] = asyncio.create_task(
self._process_execution_queue(tg_id)
)
def order_callback(msg):
self.order_queues[tg_id].append(msg)
def execution_callback(msg):
self.execution_queues[tg_id].append(msg)
ws.order_stream(order_callback)
ws.execution_stream(execution_callback)
logger.info("WebSocket streams configured for user %s", tg_id)
return True
except Exception as e:
logger.error("Error connecting user %s: %s", tg_id, e)
self.user_sockets.pop(tg_id, None)
return False
async def handle_position_update(self, message):
"""Handle position updates."""
await self.message_handler.format_position_update(message)
async def _process_order_queue(self, tg_id):
"""Continuously process order queue for user."""
while tg_id in self.user_sockets:
try:
if self.order_queues[tg_id]:
msg = self.order_queues[tg_id].popleft()
await self.handle_order_update(msg, tg_id)
except Exception as e:
logger.error("Error processing order queue %s: %s", tg_id, e)
await asyncio.sleep(0.01)
async def _process_execution_queue(self, tg_id):
"""Continuously process execution queue for user."""
while tg_id in self.user_sockets:
try:
if self.execution_queues[tg_id]:
msg = self.execution_queues[tg_id].popleft()
await self.handle_execution_update(msg, tg_id)
except Exception as e:
logger.error("Error processing execution queue %s: %s", tg_id, e)
await asyncio.sleep(0.01)
async def close_user_socket(self, tg_id):
"""Gracefully close user connection."""
if tg_id in self.user_sockets:
self.user_sockets.pop(tg_id, None)
for key in (tg_id, tg_id + 1):
task = self.processing_tasks.pop(key, None)
if task and not task.done():
task.cancel()
self.order_queues.pop(tg_id, None)
self.execution_queues.pop(tg_id, None)
self.user_messages.pop(tg_id, None)
self.user_keys.pop(tg_id, None)
logger.info("Cleaned up user %s", tg_id)
async def handle_order_update(self, message, tg_id):
"""Handle order updates."""
await self.message_handler.format_order_update(message, tg_id)
"""Process order updates."""
try:
await self.message_handler.format_order_update(message, tg_id)
except Exception as e:
logger.error("Error handling order update for %s: %s", tg_id, e)
async def handle_execution_update(self, message, tg_id):
"""Handle execution updates."""
await self.message_handler.format_execution_update(message, tg_id)
"""Process execution updates."""
try:
await self.message_handler.format_execution_update(message, tg_id)
except Exception as e:
logger.error("Error handling execution update for %s: %s", tg_id, e)
@staticmethod
async def get_users_from_db():
"""Get all users from the database."""
return await rq.get_users()
"""Fetch all users from database."""
try:
return await rq.get_users()
except Exception as e:
logger.error("Error getting users from DB: %s", e)
return []

View File

@@ -179,3 +179,9 @@ async def calculate_total_budget(
total += r_quantity
return total
async def truncate_float(f, decimals=4):
factor = 10 ** decimals
return int(f * factor) / factor

View File

@@ -121,7 +121,7 @@ async def set_symbol(message: Message, state: FSMContext) -> None:
)
await rq.set_leverage(tg_id=message.from_user.id, leverage=str(max_leverage))
risk_percent = 100 / safe_float(max_leverage)
risk_percent = 10 / 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(

View File

@@ -85,7 +85,19 @@ async def cmd_to_main(message: Message, state: FSMContext) -> None:
None: Exceptions are caught and logged internally.
"""
try:
await user_profile_tg(tg_id=message.from_user.id, message=message)
await state.clear()
user = await rq.get_user(tg_id=message.from_user.id)
if user:
await user_profile_tg(tg_id=message.from_user.id, message=message)
else:
await rq.create_user(
tg_id=message.from_user.id, username=message.from_user.username
)
await rq.set_user_symbol(tg_id=message.from_user.id, symbol="BTCUSDT")
await rq.create_user_additional_settings(tg_id=message.from_user.id)
await rq.create_user_risk_management(tg_id=message.from_user.id)
await rq.create_user_conditional_settings(tg_id=message.from_user.id)
await user_profile_tg(tg_id=message.from_user.id, message=message)
logger.debug(
"Command to_profile_tg processed successfully for user: %s",
message.from_user.id,
@@ -117,9 +129,21 @@ async def profile_bybit(message: Message, state: FSMContext) -> None:
"""
try:
await state.clear()
await user_profile_bybit(
tg_id=message.from_user.id, message=message, state=state
)
user = await rq.get_user(tg_id=message.from_user.id)
if user:
await user_profile_bybit(
tg_id=message.from_user.id, message=message, state=state
)
else:
await rq.create_user(
tg_id=message.from_user.id, username=message.from_user.username
)
await rq.set_user_symbol(tg_id=message.from_user.id, symbol="BTCUSDT")
await rq.create_user_additional_settings(tg_id=message.from_user.id)
await rq.create_user_risk_management(tg_id=message.from_user.id)
await rq.create_user_conditional_settings(tg_id=message.from_user.id)
await user_profile_bybit(
tg_id=message.from_user.id, message=message, state=state)
logger.debug(
"Command to_profile_bybit processed successfully for user: %s",
message.from_user.id,
@@ -150,15 +174,31 @@ async def profile_bybit_callback(
"""
try:
await state.clear()
await user_profile_bybit(
tg_id=callback_query.from_user.id,
message=callback_query.message,
state=state,
)
logger.debug(
"Callback profile_bybit processed successfully for user: %s",
callback_query.from_user.id,
)
user = await rq.get_user(tg_id=callback_query.from_user.id)
if user:
await user_profile_bybit(
tg_id=callback_query.from_user.id,
message=callback_query.message,
state=state,
)
logger.debug(
"Callback profile_bybit processed successfully for user: %s",
callback_query.from_user.id,
)
else:
await rq.create_user(
tg_id=callback_query.from_user.id, username=callback_query.from_user.username
)
await rq.set_user_symbol(tg_id=callback_query.from_user.id, symbol="BTCUSDT")
await rq.create_user_additional_settings(tg_id=callback_query.from_user.id)
await rq.create_user_risk_management(tg_id=callback_query.from_user.id)
await rq.create_user_conditional_settings(tg_id=callback_query.from_user.id)
await user_profile_bybit(
tg_id=callback_query.from_user.id,
message=callback_query.message,
state=state,
)
await callback_query.answer()
except Exception as e:
logger.error(

View File

@@ -43,7 +43,7 @@ async def settings_for_trade_mode(
text="Выберите режим торговли:\n\n"
"Лонг - все сделки серии открываются на покупку.\n"
"Шорт - все сделки серии открываются на продажу.\n"
"Свитч - направление первой сделки серии меняется по переменно.\n",
"Свитч - направление каждой сделки в рамках серии меняется попеременно.\n",
reply_markup=kbi.trade_mode,
)
logger.debug(
@@ -105,10 +105,10 @@ async def trade_mode(callback_query: CallbackQuery, state: FSMContext) -> None:
await state.clear()
@router_additional_settings.callback_query(F.data == "switch_side_start")
async def switch_side_start(callback_query: CallbackQuery, state: FSMContext) -> None:
@router_additional_settings.callback_query(F.data == "switch_side_second")
async def switch_side_second(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the 'switch_side_start' callback query.
Handles the 'switch_side_second' callback query.
Clears the current FSM state, edits the message text to display the switch side start message,
and shows an inline keyboard for selection.
@@ -123,13 +123,13 @@ async def switch_side_start(callback_query: CallbackQuery, state: FSMContext) ->
try:
await state.clear()
await callback_query.message.edit_text(
text="Выберите направление первой сделки серии:\n\n"
text="Выберите направление первой сделки последующих серии:\n\n"
"По направлению - сделка открывается в направлении последней сделки предыдущей серии.\n"
"Противоположно - сделка открывается в противоположном направлении последней сделки предыдущей серии.\n",
reply_markup=kbi.switch_side,
)
logger.debug(
"Command switch_side_start processed successfully for user: %s",
"Command switch_side_second processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
@@ -137,7 +137,7 @@ async def switch_side_start(callback_query: CallbackQuery, state: FSMContext) ->
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
logger.error(
"Error processing command switch_side_start for user %s: %s",
"Error processing command switch_side_second for user %s: %s",
callback_query.from_user.id,
e,
)
@@ -193,6 +193,89 @@ async def switch_side_handler(callback_query: CallbackQuery, state: FSMContext)
await state.clear()
@router_additional_settings.callback_query(F.data == "switch_side_start")
async def switch_side_start(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the 'switch_side_start' callback query.
Clears the current FSM state, edits the message text to display the switch side second message,
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 callback_query.message.edit_text(
text="Выберите направление первой сделки:\n\n", reply_markup=kbi.side_for_switch
)
logger.debug(
"Command switch_side_start processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
logger.error(
"Error processing command switch_side_start for user %s: %s",
callback_query.from_user.id,
e,
)
@router_additional_settings.callback_query(lambda c: c.data == "buy_switch" or c.data == "sell_switch")
async def switch_side_handler_2(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles callback queries related to switch side selection.
Updates FSM context with selected switch side and persists the choice in database.
Sends an acknowledgement to user and clears FSM state afterward.
Args:
callback_query (CallbackQuery): Incoming callback query indicating selected switch side.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
if callback_query.data == "sell_switch":
side = "Sell"
side_rus = "Продажа"
else:
side = "Buy"
side_rus = "Покупка"
req = await rq.set_side(
tg_id=callback_query.from_user.id, side=side
)
if not req:
await callback_query.answer(
text="Произошла ошибка при смене направления. Пожалуйста, попробуйте позже."
)
return
await callback_query.answer(text=f"Выбрано: {side_rus}")
logger.debug(
"Switch side changed successfully for user: %s", callback_query.from_user.id
)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при смене направления.")
logger.error(
"Error processing set switch_side for user %s: %s",
callback_query.from_user.id,
e,
)
finally:
await state.clear()
@router_additional_settings.callback_query(F.data == "margin_type")
async def settings_for_margin_type(
callback_query: CallbackQuery, state: FSMContext
@@ -216,6 +299,12 @@ async def settings_for_margin_type(
deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
if deals == "Invalid API key permissions":
await callback_query.answer(
text="API ключ не имеет достаточных прав для смены маржи",
)
return
position = next((d for d in deals if d.get("symbol") == symbol), None)
if position:
@@ -577,7 +666,7 @@ async def set_leverage_handler(message: Message, state: FSMContext) -> None:
text=f"Кредитное плечо успешно установлено на {leverage_float}",
reply_markup=kbi.back_to_additional_settings,
)
risk_percent = 100 / safe_float(leverage_float)
risk_percent = 10 / safe_float(leverage_float)
await rq.set_stop_loss_percent(
tg_id=message.from_user.id, stop_loss_percent=risk_percent)
await rq.set_take_profit_percent(
@@ -593,10 +682,19 @@ async def set_leverage_handler(message: Message, state: FSMContext) -> None:
await state.clear()
except Exception as e:
await message.answer(
text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_additional_settings,
)
errors_text = str(e)
known_errors = {
"Permission denied, please check your API key permissions": "API ключ не имеет достаточных прав для установки кредитного плеча"
}
for key, msg in known_errors.items():
if key in errors_text:
await message.answer(msg, reply_markup=kbi.back_to_additional_settings)
else:
await message.answer(
text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_additional_settings,
)
logger.error(
"Error processing command leverage for user %s: %s", message.from_user.id, e
)

View File

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

View File

@@ -62,10 +62,19 @@ async def additional_settings(callback_query: CallbackQuery, state: FSMContext)
max_bets = additional_data.max_bets_in_series
quantity = f(additional_data.order_quantity)
trigger_price = f(additional_data.trigger_price) or 0
side = additional_data.side
side_map = {
"Buy": "Лонг",
"Sell": "Шорт",
}
side_rus = side_map.get(side, side)
switch_side_mode = ""
side = ""
if trade_mode == "Switch":
switch_side_mode = f"- Направление первой сделки: {switch_side}\n"
side = f"- Направление первой сделки: {side_rus}\n"
switch_side_mode = f"- Направление первой сделки последующих серии: {switch_side}\n"
total_budget = await calculate_total_budget(
quantity=quantity,
@@ -75,6 +84,7 @@ async def additional_settings(callback_query: CallbackQuery, state: FSMContext)
text = (
f"Основные настройки:\n\n"
f"- Режим торговли: {trade_mode_rus}\n"
f"{side}"
f"{switch_side_mode}"
f"- Тип маржи: {margin_type_rus}\n"
f"- Размер кредитного плеча: {leverage:.2f}\n"
@@ -120,12 +130,17 @@ async def risk_management(callback_query: CallbackQuery, state: FSMContext) -> N
commission_fee_rus = (
"Да" if commission_fee == "Yes_commission_fee" else "Нет"
)
commission_place = risk_management_data.commission_place
commission_place_rus = (
"Ставке" if commission_place == "Commission_for_qty" else "Тейк-профиту"
)
await callback_query.message.edit_text(
text=f"Риск-менеджмент:\n\n"
f"- Процент изменения цены для фиксации прибыли: {take_profit_percent:.2f}%\n"
f"- Процент изменения цены для фиксации убытка: {stop_loss_percent:.2f}%\n\n"
f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n",
f"- Процент изменения цены для фиксации прибыли: {take_profit_percent:.2f}%\n"
f"- Процент изменения цены для фиксации убытка: {stop_loss_percent:.2f}%\n\n"
f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n"
f"- Компенсация комиссии по: {commission_place_rus}",
reply_markup=kbi.risk_management,
)
logger.debug(
@@ -164,7 +179,7 @@ async def conditions(callback_query: CallbackQuery, state: FSMContext) -> None:
start_timer = conditional_settings_data.timer_start or 0
await callback_query.message.edit_text(
text="Условия торговли:\n\n"
f"- Таймер для старта: {start_timer} мин.\n",
f"- Таймер для старта: {start_timer} мин.\n",
reply_markup=kbi.conditions,
)
logger.debug(

View File

@@ -38,6 +38,12 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
if deals == "Invalid API key permissions":
await callback_query.answer(
text="API ключ не имеет достаточных прав для запуска торговли",
)
return
position = next((d for d in deals if d.get("symbol") == symbol), None)
if position:
@@ -97,7 +103,8 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
"Limit price is out min price": "Цена лимитного ордера меньше допустимого",
"Limit price is out max price": "Цена лимитного ордера больше допустимого",
"Risk is too high for this trade": "Риск сделки превышает допустимый убыток",
"estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.",
"estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. "
"Проверьте параметры ордера.",
"ab not enough for new order": "Недостаточно средств для создания нового ордера",
"InvalidRequestError": "Произошла ошибка при запуске торговли.",
"Order does not meet minimum order value": "Сумма ставки меньше допустимого для запуска торговли. "
@@ -106,6 +113,11 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
"Qty invalid": "Некорректное значение ставки для данного инструмента",
"The number of contracts exceeds maximum limit allowed": "️️Превышен максимальный лимит ставки",
"The number of contracts exceeds minimum limit allowed": "️️Лимит ставки меньше минимально допустимого",
"Order placement failed as your position may exceed the max":
"Не удалось разместить ордер, так как ваша позиция может превышать максимальный лимит."
"Пожалуйста, уменьшите кредитное плечо, чтобы увеличить максимальное значение",
"Permission denied, please check your API key permissions": "API ключ не имеет достаточных прав для запуска торговли"
}
if res == "OK":
@@ -127,7 +139,16 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
await add_start_task_merged(user_id=callback_query.from_user.id, task=task)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
error_text = str(e)
known_errors = {
"Permission denied, please check your API key permissions": "API ключ не имеет достаточных прав для запуска торговли"
}
for key, msg in known_errors.items():
if key in error_text:
await callback_query.answer(msg)
else:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error(
"Error processing command start_trading for user %s: %s",
callback_query.from_user.id,
@@ -141,7 +162,7 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
lambda c: c.data == "cancel_timer_merged"
)
async def cancel_start_trading(
callback_query: CallbackQuery, state: FSMContext
callback_query: CallbackQuery, state: FSMContext
) -> None:
"""
Handles the "cancel_timer" callback query.

View File

@@ -39,21 +39,15 @@ 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 = await rq.get_user_auto_trading(
tg_id=callback_query.from_user.id, symbol=symbol
await close_position_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol)
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
)
await callback_query.message.edit_text(text=f"Торговля для {symbol} остановлена", 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=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

@@ -94,7 +94,10 @@ def get_additional_settings_keyboard(mode: str
if mode == "Switch":
buttons.append(
[InlineKeyboardButton(text="Направление первой сделки", callback_data="switch_side_start")]
[InlineKeyboardButton(text="Направление первой сделки первой серии", callback_data="switch_side_start")]
)
buttons.append(
[InlineKeyboardButton(text="Направление первой сделки последующих серии", callback_data="switch_side_second")]
)
buttons.append(
@@ -148,6 +151,19 @@ switch_side = InlineKeyboardMarkup(
]
)
side_for_switch = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Лонг", callback_data="buy_switch"),
InlineKeyboardButton(text="Шорт", callback_data="sell_switch"),
],
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
margin_type = InlineKeyboardMarkup(
inline_keyboard=[
[
@@ -194,6 +210,7 @@ risk_management = InlineKeyboardMarkup(
),
],
[InlineKeyboardButton(text="Комиссия биржи", callback_data="commission_fee")],
[InlineKeyboardButton(text="Компенсация комиссии", callback_data="compensation_commission")],
[
InlineKeyboardButton(text="Назад", callback_data="main_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
@@ -223,6 +240,20 @@ commission_fee = InlineKeyboardMarkup(
]
)
commission_place = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="По ставке", callback_data="Commission_for_qty"),
InlineKeyboardButton(text="По тейк-профиту", callback_data="Commission_for_tp"),
],
[
InlineKeyboardButton(text="Назад", callback_data="risk_management"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
# conditions
conditions = InlineKeyboardMarkup(
inline_keyboard=[

View File

@@ -10,10 +10,9 @@ logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("database")
BASE_DIR = Path(__file__).parent.resolve()
DATA_DIR = BASE_DIR / "db"
DATA_DIR.mkdir(parents=True, exist_ok=True)
BASE_DIR.mkdir(parents=True, exist_ok=True)
DATABASE_URL = f"sqlite+aiosqlite:///{DATA_DIR / 'stcs.db'}"
DATABASE_URL = f"sqlite+aiosqlite:///{BASE_DIR / 'stcs.db'}"
async_engine = create_async_engine(
DATABASE_URL,
@@ -39,7 +38,7 @@ async_session = async_sessionmaker(
async def init_db():
try:
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
await conn.run_sync(lambda sync_conn: Base.metadata.create_all(bind=sync_conn, checkfirst=True))
logger.info("Database initialized.")
except Exception as e:
logger.error("Database initialization failed: %s", e)
logger.error("Database initialization failed: %s", e)

View File

@@ -92,6 +92,7 @@ class UserAdditionalSettings(Base):
nullable=False, unique=True)
trade_mode = Column(String, nullable=False, default="Merged_Single")
switch_side = Column(String, nullable=False, default="По направлению")
side = Column(String, nullable=False, default="Buy")
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")
@@ -113,6 +114,7 @@ class UserRiskManagement(Base):
take_profit_percent = Column(Float, nullable=False, default=1)
stop_loss_percent = Column(Float, nullable=False, default=1)
commission_fee = Column(String, nullable=False, default="Yes_commission_fee")
commission_place = Column(String, nullable=False, default="Commission_for_qty")
user = relationship("User", back_populates="user_risk_management")
@@ -152,9 +154,15 @@ class UserDeals(Base):
order_quantity = Column(Float, nullable=True)
martingale_factor = Column(Float, nullable=True)
max_bets_in_series = Column(Integer, nullable=True)
take_profit_percent = Column(Integer, nullable=True)
stop_loss_percent = Column(Integer, nullable=True)
take_profit_percent = Column(Float, nullable=True)
stop_loss_percent = Column(Float, nullable=True)
trigger_price = Column(Float, nullable=True)
current_series = Column(Integer, nullable=True)
commission_fee = Column(String, nullable=True)
commission_place = Column(String, nullable=True)
pnl_series = Column(Float, nullable=True)
take_profit = Column(Float, nullable=False, default=0.0)
stop_loss = Column(Float, nullable=False, default=0.0)
user = relationship("User", back_populates="user_deals")

View File

@@ -86,7 +86,7 @@ async def set_user_api(tg_id: int, api_key: str, api_secret: str) -> bool:
else:
# Creating new record
user_api = UserApi(
user=user, api_key=api_key, api_secret=api_secret
user_id=user.id, api_key=api_key, api_secret=api_secret
)
session.add(user_api)
@@ -141,7 +141,7 @@ async def set_user_symbol(tg_id: int, symbol: str) -> bool:
# Creating new record
user_symbol = UserSymbol(
symbol=symbol,
user=user,
user_id=user.id,
)
session.add(user_symbol)
@@ -197,9 +197,11 @@ async def create_user_additional_settings(tg_id: int) -> None:
# Create the user additional settings
user_additional_settings = UserAdditionalSettings(
user=user,
user_id=user.id,
trade_mode="Long", # Default value
switch_side="По направлению",
side="Buy",
trigger_price=0.0,
margin_type="ISOLATED_MARGIN",
leverage="10",
order_quantity=1.0,
@@ -265,7 +267,7 @@ async def set_trade_mode(tg_id: int, trade_mode: str) -> bool:
# Creating new record
user_additional_settings = UserAdditionalSettings(
trade_mode=trade_mode,
user=user,
user_id=user.id,
)
session.add(user_additional_settings)
@@ -304,7 +306,7 @@ async def set_margin_type(tg_id: int, margin_type: str) -> bool:
# Creating new record
user_additional_settings = UserAdditionalSettings(
margin_type=margin_type,
user=user,
user_id=user.id,
)
session.add(user_additional_settings)
@@ -343,7 +345,7 @@ async def set_switch_side(tg_id: int, switch_side: str) -> bool:
# Creating new record
user_additional_settings = UserAdditionalSettings(
switch_side=switch_side,
user=user,
user_id=user.id,
)
session.add(user_additional_settings)
@@ -358,6 +360,45 @@ async def set_switch_side(tg_id: int, switch_side: str) -> bool:
return False
async def set_side(tg_id: int, side: str) -> bool:
"""
Set side for a user in the database.
:param tg_id: Telegram user ID
:param side: "BUY" or "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.side = side
else:
# Creating new record
user_additional_settings = UserAdditionalSettings(
side=side,
user_id=user.id,
)
session.add(user_additional_settings)
await session.commit()
logger.info("User 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 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.
@@ -382,7 +423,7 @@ async def set_leverage(tg_id: int, leverage: str) -> bool:
# Creating new record
user_additional_settings = UserAdditionalSettings(
leverage=leverage,
user=user,
user_id=user.id,
)
session.add(user_additional_settings)
@@ -421,7 +462,7 @@ async def set_order_quantity(tg_id: int, order_quantity: float) -> bool:
# Creating new record
user_additional_settings = UserAdditionalSettings(
order_quantity=order_quantity,
user=user,
user_id=user.id,
)
session.add(user_additional_settings)
@@ -462,7 +503,7 @@ async def set_martingale_factor(tg_id: int, martingale_factor: float) -> bool:
# Creating new record
user_additional_settings = UserAdditionalSettings(
martingale_factor=martingale_factor,
user=user,
user_id=user.id,
)
session.add(user_additional_settings)
@@ -505,7 +546,7 @@ async def set_max_bets_in_series(tg_id: int, max_bets_in_series: int) -> bool:
# Creating new record
user_additional_settings = UserAdditionalSettings(
max_bets_in_series=max_bets_in_series,
user=user,
user_id=user.id,
)
session.add(user_additional_settings)
@@ -546,7 +587,7 @@ async def set_trigger_price(tg_id: int, trigger_price: float) -> bool:
# Creating new record
user_additional_settings = UserAdditionalSettings(
trigger_price=trigger_price,
user=user,
user_id=user.id,
)
session.add(user_additional_settings)
@@ -586,10 +627,11 @@ async def create_user_risk_management(tg_id: int) -> None:
# Create the user risk management
user_risk_management = UserRiskManagement(
user=user,
user_id=user.id,
take_profit_percent=1.0,
stop_loss_percent=1.0,
commission_fee="Yes_commission_fee",
commission_place="Commission_for_qty"
)
session.add(user_risk_management)
await session.commit()
@@ -650,7 +692,7 @@ async def set_take_profit_percent(tg_id: int, take_profit_percent: float) -> boo
# Creating new record
user_risk_management = UserRiskManagement(
take_profit_percent=take_profit_percent,
user=user,
user_id=user.id,
)
session.add(user_risk_management)
@@ -691,7 +733,7 @@ async def set_stop_loss_percent(tg_id: int, stop_loss_percent: float) -> bool:
# Creating new record
user_risk_management = UserRiskManagement(
stop_loss_percent=stop_loss_percent,
user=user,
user_id=user.id,
)
session.add(user_risk_management)
@@ -732,7 +774,7 @@ async def set_commission_fee(tg_id: int, commission_fee: str) -> bool:
# Creating new record
user_risk_management = UserRiskManagement(
commission_fee=commission_fee,
user=user,
user_id=user.id,
)
session.add(user_risk_management)
@@ -749,6 +791,47 @@ async def set_commission_fee(tg_id: int, commission_fee: str) -> bool:
return False
async def set_commission_place(tg_id: int, commission_place: str) -> bool:
"""
Set commission place for a user in the database.
:param tg_id: Telegram user ID
:param commission_place: Commission place
:return: True if successful, False otherwise
"""
try:
async with async_session() as session:
result = await session.execute(
select(User)
.options(joinedload(User.user_risk_management))
.filter_by(tg_id=tg_id)
)
user = result.scalars().first()
if user:
if user.user_risk_management:
# Updating existing record
user.user_risk_management.commission_place = commission_place
else:
# Creating new record
user_risk_management = UserRiskManagement(
commission_place=commission_place,
user_id=user.id,
)
session.add(user_risk_management)
await session.commit()
logger.info("User commission place updated for user: %s", tg_id)
return True
else:
logger.error("User not found with tg_id: %s", tg_id)
return False
except Exception as e:
logger.error(
"Error adding/updating user commission place for user %s: %s", tg_id, e
)
return False
# USER CONDITIONAL SETTINGS
@@ -772,7 +855,7 @@ async def create_user_conditional_settings(tg_id: int) -> None:
# Create the user conditional settings
user_conditional_settings = UserConditionalSettings(
user=user,
user_id=user.id,
timer_start=0,
timer_end=0,
)
@@ -837,7 +920,7 @@ async def set_start_timer(tg_id: int, timer_start: int) -> bool:
# Creating new record
user_conditional_settings = UserConditionalSettings(
timer_start=timer_start,
user=user,
user_id=user.id,
)
session.add(user_conditional_settings)
@@ -876,7 +959,7 @@ async def set_stop_timer(tg_id: int, timer_end: int) -> bool:
# Creating new record
user_conditional_settings = UserConditionalSettings(
timer_end=timer_end,
user=user,
user_id=user.id,
)
session.add(user_conditional_settings)
@@ -896,6 +979,7 @@ async def set_user_deal(
tg_id: int,
symbol: str,
current_step: int,
current_series: int,
trade_mode: str,
side_mode: str,
margin_type: str,
@@ -906,13 +990,17 @@ async def set_user_deal(
max_bets_in_series: int,
take_profit_percent: int,
stop_loss_percent: int,
base_quantity: float
base_quantity: float,
commission_fee: str,
commission_place: str,
pnl_series: float
):
"""
Set the user deal in the database.
:param tg_id: Telegram user ID
:param symbol: Symbol
:param current_step: Current step
:param current_series: Current series
:param trade_mode: Trade mode
:param side_mode: Side mode
:param margin_type: Margin type
@@ -924,6 +1012,9 @@ async def set_user_deal(
:param take_profit_percent: Take profit percent
:param stop_loss_percent: Stop loss percent
:param base_quantity: Base quantity
:param commission_fee: Commission fee
:param commission_place: Commission place
:param pnl_series: PNL series
:return: bool
"""
try:
@@ -942,6 +1033,7 @@ async def set_user_deal(
if deal:
# Updating existing record
deal.current_step = current_step
deal.current_series = current_series
deal.trade_mode = trade_mode
deal.side_mode = side_mode
deal.margin_type = margin_type
@@ -953,12 +1045,16 @@ async def set_user_deal(
deal.take_profit_percent = take_profit_percent
deal.stop_loss_percent = stop_loss_percent
deal.base_quantity = base_quantity
deal.commission_fee = commission_fee
deal.commission_place = commission_place
deal.pnl_series = pnl_series
else:
# Creating new record
new_deal = UserDeals(
user=user,
user_id=user.id,
symbol=symbol,
current_step=current_step,
current_series=current_series,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
@@ -969,7 +1065,10 @@ async def set_user_deal(
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity
base_quantity=base_quantity,
commission_fee=commission_fee,
commission_place=commission_place,
pnl_series=pnl_series
)
session.add(new_deal)
@@ -1078,6 +1177,91 @@ async def set_last_side_by_symbol(tg_id: int, symbol: str, last_side: str):
return False
async def set_current_series(tg_id: int, symbol: str, current_series: int):
"""Set current series 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.current_series = current_series
else:
logger.error(f"User deal with user_id={user.id} and symbol={symbol} not found")
return False
await session.commit()
logger.info("Set current series for user %s and symbol %s", tg_id, symbol)
return True
except Exception as e:
logger.error("Error setting user deal current series for user %s and symbol %s: %s", tg_id, symbol, e)
return False
async def set_tp_sl_by_symbol(tg_id: int, symbol: str, tp: float, sl: float):
"""Set tp and sl 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.take_profit = tp
record.stop_loss = sl
else:
logger.error(f"User deal with user_id={user.id} and symbol={symbol} not found")
return False
await session.commit()
logger.info("Set tp and sl for user %s and symbol %s", tg_id, symbol)
return True
except Exception as e:
logger.error("Error setting user deal tp and sl for user %s and symbol %s: %s", tg_id, symbol, e)
return False
async def set_pnl_series_by_symbol(tg_id: int, symbol: str, pnl_series: float):
"""Set pnl series 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.pnl_series = pnl_series
else:
logger.error(f"User deal with user_id={user.id} and symbol={symbol} not found")
return False
await session.commit()
logger.info("Set pnl series for user %s and symbol %s", tg_id, symbol)
return True
except Exception as e:
logger.error("Error setting user deal pnl series 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):

View File

@@ -8,7 +8,7 @@ After=syslog.target network-online.target
ExecStart=sudo -u www-data /usr/bin/python3 /var/www/stcs/BybitBot_API.py
PIDFile=/var/run/python/stcs.pid
RemainAfterExit=no
RuntimeMaxSec=3600s
RuntimeMaxSec=604800s
Restart=always
RestartSec=5s

1
run.py
View File

@@ -31,7 +31,6 @@ async def main():
dp = Dispatcher(storage=storage)
dp.include_router(router)
web_socket = WebSocketBot(telegram_bot=bot)
await web_socket.clear_user_sockets()
ws_task = asyncio.create_task(web_socket.run_user_check_loop())
tg_task = asyncio.create_task(dp.start_polling(bot))