07.4.4.1.11 — Advanced Trend Quality & EMA Distance Layer
This commit is contained in:
@@ -7,9 +7,16 @@ import asyncio
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.types import (
|
||||
CallbackQuery,
|
||||
InlineKeyboardMarkup,
|
||||
Message,
|
||||
InaccessibleMessage,
|
||||
)
|
||||
|
||||
from src.core.numbers import safe_float
|
||||
from src.core.types import JsonDict, NumericLike
|
||||
from src.trading.auto.runner import AutoTradeRunner
|
||||
from src.trading.auto.service import AutoTradeService
|
||||
from src.trading.journal.service import JournalService
|
||||
@@ -18,17 +25,31 @@ from src.trading.journal.service import JournalService
|
||||
router = Router(name="auto_risk")
|
||||
|
||||
|
||||
def _require_message(
|
||||
callback: CallbackQuery,
|
||||
) -> Message | None:
|
||||
message = callback.message
|
||||
|
||||
if (
|
||||
message is None
|
||||
or isinstance(message, InaccessibleMessage)
|
||||
):
|
||||
return None
|
||||
|
||||
return message
|
||||
|
||||
|
||||
class AutoRiskStates(StatesGroup):
|
||||
waiting_stop_loss = State()
|
||||
waiting_take_profit = State()
|
||||
waiting_max_loss = State()
|
||||
|
||||
|
||||
def _format_number(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
def _format_number(value: NumericLike | None) -> str:
|
||||
number = safe_float(value)
|
||||
|
||||
number = float(value)
|
||||
if number is None:
|
||||
return "—"
|
||||
|
||||
if abs(number - round(number)) < 1e-9:
|
||||
return f"{int(round(number))}"
|
||||
@@ -36,20 +57,26 @@ def _format_number(value: float | int | None) -> str:
|
||||
return f"{number:.2f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def _format_percent(value: float | None) -> str:
|
||||
if value is None:
|
||||
def _format_percent(value: NumericLike | None) -> str:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return "off"
|
||||
return f"{_format_number(value)}%"
|
||||
|
||||
return f"{_format_number(number)}%"
|
||||
|
||||
|
||||
def _format_usd(value: float | None) -> str:
|
||||
if value is None:
|
||||
def _format_usd(value: NumericLike | None) -> str:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return "off"
|
||||
return f"{_format_number(value)} USD"
|
||||
|
||||
return f"{_format_number(number)} USD"
|
||||
|
||||
|
||||
def _rule_icon(value: float | None) -> str:
|
||||
return "✅" if value is not None else "⚠️"
|
||||
def _rule_icon(value: NumericLike | None) -> str:
|
||||
return "✅" if safe_float(value) is not None else "⚠️"
|
||||
|
||||
|
||||
def _risk_keyboard() -> InlineKeyboardMarkup:
|
||||
@@ -96,19 +123,27 @@ def _risk_text(status_message: str | None = None) -> str:
|
||||
return text
|
||||
|
||||
|
||||
async def _render_risk_screen(callback: CallbackQuery) -> None:
|
||||
async def _render_risk_screen(
|
||||
callback: CallbackQuery,
|
||||
) -> None:
|
||||
AutoTradeRunner.set_current_screen("auto_risk")
|
||||
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer(
|
||||
"Сообщение недоступно",
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
_unregister_auto_screen_message(callback)
|
||||
|
||||
await callback.message.edit_text(
|
||||
await message.edit_text(
|
||||
_risk_text(),
|
||||
reply_markup=_risk_keyboard(),
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -121,18 +156,34 @@ async def _render_risk_screen_by_message(
|
||||
) -> None:
|
||||
AutoTradeRunner.set_current_screen("auto_risk")
|
||||
|
||||
data = await state.get_data()
|
||||
chat_id = data.get("risk_chat_id")
|
||||
message_id = data.get("risk_message_id")
|
||||
bot = message.bot
|
||||
|
||||
if chat_id is None or message_id is None:
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
data: JsonDict = await state.get_data()
|
||||
|
||||
raw_chat_id = data.get("risk_chat_id")
|
||||
raw_message_id = data.get("risk_message_id")
|
||||
|
||||
if not isinstance(raw_chat_id, int):
|
||||
await message.answer(
|
||||
_risk_text(status_message=status_message),
|
||||
reply_markup=_risk_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
await message.bot.edit_message_text(
|
||||
if not isinstance(raw_message_id, int):
|
||||
await message.answer(
|
||||
_risk_text(status_message=status_message),
|
||||
reply_markup=_risk_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
chat_id = raw_chat_id
|
||||
message_id = raw_message_id
|
||||
|
||||
await bot.edit_message_text(
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
text=_risk_text(status_message=status_message),
|
||||
@@ -146,7 +197,7 @@ async def _render_risk_screen_by_message(
|
||||
return
|
||||
|
||||
try:
|
||||
await message.bot.edit_message_text(
|
||||
await bot.edit_message_text(
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
text=_risk_text(),
|
||||
@@ -156,33 +207,56 @@ async def _render_risk_screen_by_message(
|
||||
pass
|
||||
|
||||
|
||||
async def _remember_risk_screen(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
if callback.message is None:
|
||||
async def _remember_risk_screen(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
return
|
||||
|
||||
await state.update_data(
|
||||
risk_chat_id=callback.message.chat.id,
|
||||
risk_message_id=callback.message.message_id,
|
||||
risk_chat_id=message.chat.id,
|
||||
risk_message_id=message.message_id,
|
||||
)
|
||||
|
||||
|
||||
def _unregister_auto_screen_message(callback: CallbackQuery) -> None:
|
||||
if callback.message is None:
|
||||
def _unregister_auto_screen_message(
|
||||
callback: CallbackQuery,
|
||||
) -> None:
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
return
|
||||
|
||||
AutoTradeRunner.unregister_screen(
|
||||
chat_id=callback.message.chat.id,
|
||||
message_id=callback.message.message_id,
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
)
|
||||
|
||||
|
||||
def _parse_positive_or_none(raw_text: str | None) -> float | None:
|
||||
def _risk_payload(**values: object) -> JsonDict:
|
||||
return dict(values)
|
||||
|
||||
|
||||
def _parse_positive_or_none(
|
||||
raw_text: str | None,
|
||||
) -> float | None:
|
||||
value_text = (raw_text or "").strip().replace(",", ".")
|
||||
|
||||
if value_text.lower() in {"0", "0.0", "off", "-"}:
|
||||
if value_text.lower() in {
|
||||
"0",
|
||||
"0.0",
|
||||
"off",
|
||||
"-",
|
||||
}:
|
||||
return None
|
||||
|
||||
value = float(value_text)
|
||||
value = safe_float(value_text)
|
||||
|
||||
if value is None:
|
||||
raise ValueError
|
||||
|
||||
if value <= 0:
|
||||
return None
|
||||
@@ -190,16 +264,26 @@ def _parse_positive_or_none(raw_text: str | None) -> float | None:
|
||||
return value
|
||||
|
||||
|
||||
def _validate_percent(value: float | None) -> bool:
|
||||
if value is None:
|
||||
def _validate_percent(
|
||||
value: NumericLike | None,
|
||||
) -> bool:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return True
|
||||
return 0 < value <= 100
|
||||
|
||||
return 0 < number <= 100
|
||||
|
||||
|
||||
def _validate_max_loss(value: float | None) -> bool:
|
||||
if value is None:
|
||||
def _validate_max_loss(
|
||||
value: NumericLike | None,
|
||||
) -> bool:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return True
|
||||
return 0 < value <= 10000
|
||||
|
||||
return 0 < number <= 10000
|
||||
|
||||
|
||||
def _log_risk_updated(action: str) -> None:
|
||||
@@ -216,11 +300,11 @@ def _log_risk_updated(action: str) -> None:
|
||||
),
|
||||
screen="auto",
|
||||
action=action,
|
||||
payload={
|
||||
"stop_loss_percent": state.stop_loss_percent,
|
||||
"take_profit_percent": state.take_profit_percent,
|
||||
"max_loss_usd": state.max_loss_usd,
|
||||
},
|
||||
payload=_risk_payload(
|
||||
stop_loss_percent=state.stop_loss_percent,
|
||||
take_profit_percent=state.take_profit_percent,
|
||||
max_loss_usd=state.max_loss_usd,
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -242,17 +326,26 @@ async def open_auto_risk_from_settings(callback: CallbackQuery, state: FSMContex
|
||||
async def ask_stop_loss(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
AutoTradeRunner.set_current_screen("auto_risk")
|
||||
_unregister_auto_screen_message(callback)
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer(
|
||||
"Сообщение недоступно",
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
await state.set_state(AutoRiskStates.waiting_stop_loss)
|
||||
await _remember_risk_screen(callback, state)
|
||||
|
||||
if callback.message is not None:
|
||||
await callback.message.edit_text(
|
||||
"<b>Stop Loss</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||
"Введите Stop Loss в процентах.\n"
|
||||
"Например: <code>1</code>, <code>0.5</code>, <code>0,5</code>\n\n"
|
||||
"отключить параметр - <code>0</code>"
|
||||
)
|
||||
await message.edit_text(
|
||||
"<b>Stop Loss</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||
"Введите Stop Loss в процентах.\n"
|
||||
"Например: <code>1</code>, <code>0.5</code>, <code>0,5</code>\n\n"
|
||||
"отключить параметр - <code>0</code>"
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
@@ -261,17 +354,26 @@ async def ask_stop_loss(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
async def ask_take_profit(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
AutoTradeRunner.set_current_screen("auto_risk")
|
||||
_unregister_auto_screen_message(callback)
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer(
|
||||
"Сообщение недоступно",
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
await state.set_state(AutoRiskStates.waiting_take_profit)
|
||||
await _remember_risk_screen(callback, state)
|
||||
|
||||
if callback.message is not None:
|
||||
await callback.message.edit_text(
|
||||
"<b>Take Profit</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||
"Введите Take Profit в процентах.\n"
|
||||
"Например: <code>2</code>, <code>1.5</code>, <code>1,5</code>\n\n"
|
||||
"отключить параметр - <code>0</code>"
|
||||
)
|
||||
await message.edit_text(
|
||||
"<b>Take Profit</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||
"Введите Take Profit в процентах.\n"
|
||||
"Например: <code>2</code>, <code>1.5</code>, <code>1,5</code>\n\n"
|
||||
"отключить параметр - <code>0</code>"
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
@@ -280,17 +382,26 @@ async def ask_take_profit(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
async def ask_max_loss(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
AutoTradeRunner.set_current_screen("auto_risk")
|
||||
_unregister_auto_screen_message(callback)
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer(
|
||||
"Сообщение недоступно",
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
await state.set_state(AutoRiskStates.waiting_max_loss)
|
||||
await _remember_risk_screen(callback, state)
|
||||
|
||||
if callback.message is not None:
|
||||
await callback.message.edit_text(
|
||||
"<b>Maximum Loss</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||
"Введите максимальный paper-убыток в USD.\n"
|
||||
"Например: <code>100</code>, <code>50.5</code>, <code>50,5</code>\n\n"
|
||||
"отключить параметр - <code>0</code>"
|
||||
)
|
||||
await message.edit_text(
|
||||
"<b>Maximum Loss</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||
"Введите максимальный paper-убыток в USD.\n"
|
||||
"Например: <code>100</code>, <code>50.5</code>, <code>50,5</code>\n\n"
|
||||
"отключить параметр - <code>0</code>"
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
@@ -301,6 +412,12 @@ async def reset_risk(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
AutoTradeRunner.set_current_screen("auto_risk")
|
||||
_unregister_auto_screen_message(callback)
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
service = AutoTradeService()
|
||||
service.set_stop_loss_percent(None)
|
||||
service.set_take_profit_percent(None)
|
||||
@@ -308,29 +425,26 @@ async def reset_risk(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
|
||||
_log_risk_updated("risk_reset")
|
||||
|
||||
if callback.message is not None:
|
||||
await callback.message.edit_text(
|
||||
_risk_text(status_message="✅ Risk Controls сброшены"),
|
||||
reply_markup=_risk_keyboard(),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
await asyncio.sleep(2.5)
|
||||
|
||||
if getattr(AutoTradeRunner, "_current_screen", None) != "auto_risk":
|
||||
return
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
_risk_text(),
|
||||
reply_markup=_risk_keyboard(),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
await message.edit_text(
|
||||
_risk_text(status_message="✅ Risk Controls сброшены"),
|
||||
reply_markup=_risk_keyboard(),
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
await asyncio.sleep(2.5)
|
||||
|
||||
if getattr(AutoTradeRunner, "_current_screen", None) != "auto_risk":
|
||||
return
|
||||
|
||||
try:
|
||||
await message.edit_text(
|
||||
_risk_text(),
|
||||
reply_markup=_risk_keyboard(),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@router.message(AutoRiskStates.waiting_stop_loss)
|
||||
async def set_stop_loss(message: Message, state: FSMContext) -> None:
|
||||
|
||||
Reference in New Issue
Block a user