07.4.4.1.11 — Advanced Trend Quality & EMA Distance Layer
This commit is contained in:
@@ -1,364 +0,0 @@
|
||||
# app/src/telegram/handlers/auto.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from src.telegram.ui.common import mode_line
|
||||
from src.trading.auto.runner import AutoTradeRunner
|
||||
from src.trading.auto.service import AutoTradeService
|
||||
from src.telegram.handlers.system import open_auto_settings
|
||||
from src.integrations.exchange.service import ExchangeService
|
||||
from src.telegram.ui.currency_ui import format_usd_amount
|
||||
|
||||
|
||||
router = Router(name="auto")
|
||||
|
||||
|
||||
# красивое отображение стратегии
|
||||
def _strategy_label(strategy: str | None) -> str:
|
||||
mapping = {
|
||||
"TREND": "📈 Trend Following",
|
||||
"GRID": "🧩 Grid Trading",
|
||||
"SCALP": "⚡ Scalping",
|
||||
}
|
||||
return mapping.get(strategy or "", "—")
|
||||
|
||||
|
||||
# красивое отображение статуса
|
||||
def _status_label(status: str) -> str:
|
||||
mapping = {
|
||||
"OFF": "⚪ Выключена",
|
||||
"OBSERVING": "👀 Наблюдение",
|
||||
"RUNNING": "🟢 Активна",
|
||||
}
|
||||
return mapping.get(status, status)
|
||||
|
||||
|
||||
# красивое отображение сигнала
|
||||
def _signal_label(signal: str | None) -> str:
|
||||
mapping = {
|
||||
"BUY": "🟢 BUY",
|
||||
"SELL": "🔴 SELL",
|
||||
"HOLD": "🟡 HOLD",
|
||||
}
|
||||
return mapping.get(signal or "", "—")
|
||||
|
||||
|
||||
# красивое отображение решения
|
||||
def _decision_label(status: str) -> str:
|
||||
mapping = {
|
||||
"WAITING": "🟡 Ожидание",
|
||||
"CONFIRMING": "🟠 Подтверждение",
|
||||
"READY": "🟢 Готово к входу",
|
||||
"BLOCKED": "🔴 Заблокировано",
|
||||
}
|
||||
return mapping.get(status, status)
|
||||
|
||||
|
||||
# компактное значение или заглушка
|
||||
def _value_or_dash(value: object) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return str(value)
|
||||
|
||||
|
||||
# формат цены
|
||||
def _price_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.2f}"
|
||||
|
||||
|
||||
# текущая цена инструмента
|
||||
def _market_price_or_dash(symbol: str | None) -> str:
|
||||
if not symbol:
|
||||
return "—"
|
||||
|
||||
try:
|
||||
ticker = ExchangeService().get_price(symbol)
|
||||
return f"$ {format_usd_amount(ticker.price)}"
|
||||
except Exception:
|
||||
return "—"
|
||||
|
||||
|
||||
# формат USD
|
||||
def _usd_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.2f} USD"
|
||||
|
||||
|
||||
# формат размера позиции
|
||||
def _size_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.8f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
# формат плеча
|
||||
def _leverage_or_dash(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"{value:.1f}x"
|
||||
|
||||
|
||||
# формат торгового инструмента для UI
|
||||
def _format_symbol(symbol: str | None) -> str:
|
||||
if not symbol:
|
||||
return "—"
|
||||
|
||||
base_symbol = symbol.split("_", 1)[0]
|
||||
parts = base_symbol.split("/", 1)
|
||||
|
||||
if len(parts) == 2:
|
||||
return f"{parts[0]} / {parts[1]}"
|
||||
|
||||
return base_symbol
|
||||
|
||||
|
||||
# стратегия для компактного UI
|
||||
def _compact_strategy(strategy: str | None) -> str:
|
||||
if not strategy:
|
||||
return "—"
|
||||
return strategy.upper()
|
||||
|
||||
|
||||
# плечо для компактного UI
|
||||
def _compact_leverage(value: float | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
return f"x{value:g}"
|
||||
|
||||
|
||||
# проверка, настроена ли автоторговля минимально
|
||||
def _is_auto_configured(state) -> bool:
|
||||
return bool(
|
||||
state.symbol
|
||||
and state.strategy
|
||||
and state.risk_percent is not None
|
||||
)
|
||||
|
||||
|
||||
# строка инструмента / стратегии / плеча
|
||||
def _context_line(state) -> str:
|
||||
symbol = _format_symbol(state.symbol)
|
||||
strategy = _compact_strategy(state.strategy)
|
||||
leverage = _compact_leverage(state.leverage)
|
||||
|
||||
if leverage == "—":
|
||||
return f"{symbol} · {strategy}"
|
||||
|
||||
return f"{symbol} · {strategy} · {leverage}"
|
||||
|
||||
|
||||
# клавиатура автоторговли
|
||||
def _auto_keyboard() -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.button(text="▶️ Start", callback_data="auto:start")
|
||||
builder.button(text="👀 Watch", callback_data="auto:observe")
|
||||
builder.button(text="🛑 Stop", callback_data="auto:stop")
|
||||
builder.button(text="🛠️ Настройки", callback_data="settings:auto")
|
||||
|
||||
builder.adjust(3, 1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
# собрать текст экрана
|
||||
def _build_auto_text() -> str:
|
||||
service = AutoTradeService()
|
||||
state = service.get_state()
|
||||
|
||||
account_mode = "DEMO" if "DEMO" in mode_line().upper() else "LIVE"
|
||||
risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "—"
|
||||
configured = _is_auto_configured(state)
|
||||
price = _market_price_or_dash(state.symbol)
|
||||
|
||||
status_line = {
|
||||
"OFF": "⚪ Off",
|
||||
"OBSERVING": "👀 Watch",
|
||||
"RUNNING": "🟢 On",
|
||||
}.get(state.status, state.status)
|
||||
|
||||
header = (
|
||||
f"<b>🤖 Автоторговля · {status_line}</b>\n"
|
||||
f"🔸 {account_mode} аккаунт\n\n"
|
||||
)
|
||||
|
||||
if state.status == "OFF":
|
||||
if not configured:
|
||||
return (
|
||||
f"{header}"
|
||||
"⚠️ Не настроена\n"
|
||||
"Настрой параметры"
|
||||
)
|
||||
|
||||
return (
|
||||
f"{header}"
|
||||
f"{_context_line(state)}\n"
|
||||
f"Price: {price}\n"
|
||||
f"Risk: {risk}"
|
||||
)
|
||||
|
||||
position_line = (
|
||||
f"Pos: {_value_or_dash(state.position_side)} | "
|
||||
f"PnL: {_usd_or_dash(state.unrealized_pnl_usd)}"
|
||||
)
|
||||
|
||||
if state.position_side != "NONE" and state.entry_price is not None:
|
||||
position_line = (
|
||||
f"Pos: {_value_or_dash(state.position_side)} | "
|
||||
f"Entry: $ {_price_or_dash(state.entry_price)} | "
|
||||
f"PnL: {_usd_or_dash(state.unrealized_pnl_usd)}"
|
||||
)
|
||||
|
||||
return (
|
||||
f"{header}"
|
||||
f"{_context_line(state)}\n"
|
||||
f"Price: {price}\n\n"
|
||||
f"{_signal_label(state.last_signal)} ×{state.last_signal_repeat_count} "
|
||||
f"· {state.decision_status}\n\n"
|
||||
f"{position_line}\n"
|
||||
f"Risk: {risk}"
|
||||
)
|
||||
|
||||
|
||||
# отрисовать live-экран автоторговли
|
||||
async def _render_auto_screen(
|
||||
target_message: Message,
|
||||
*,
|
||||
edit_mode: bool,
|
||||
) -> None:
|
||||
text = _build_auto_text()
|
||||
|
||||
if edit_mode:
|
||||
try:
|
||||
await target_message.edit_text(text, reply_markup=_auto_keyboard())
|
||||
except TelegramBadRequest as exc:
|
||||
if "message is not modified" not in str(exc).lower():
|
||||
raise
|
||||
|
||||
AutoTradeRunner.register_screen(
|
||||
bot=target_message.bot,
|
||||
chat_id=target_message.chat.id,
|
||||
message_id=target_message.message_id,
|
||||
render_text=_build_auto_text,
|
||||
render_markup=_auto_keyboard,
|
||||
)
|
||||
return
|
||||
|
||||
sent_message = await target_message.answer(text, reply_markup=_auto_keyboard())
|
||||
|
||||
AutoTradeRunner.register_screen(
|
||||
bot=sent_message.bot,
|
||||
chat_id=sent_message.chat.id,
|
||||
message_id=sent_message.message_id,
|
||||
render_text=_build_auto_text,
|
||||
render_markup=_auto_keyboard,
|
||||
)
|
||||
|
||||
|
||||
# открыть экран из главного меню
|
||||
@router.message(F.text.in_({"🤖 Автоторговля", "🤖 Авто"}))
|
||||
async def open_auto(message: Message, state: FSMContext) -> None:
|
||||
await state.clear()
|
||||
|
||||
AutoTradeRunner.set_current_screen("auto")
|
||||
|
||||
current_state = AutoTradeService().get_state()
|
||||
if current_state.status in {"RUNNING", "OBSERVING"}:
|
||||
await AutoTradeRunner.delete_registered_screen(
|
||||
bot=message.bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
|
||||
await _render_auto_screen(message, edit_mode=False)
|
||||
|
||||
|
||||
# открыть экран через callback
|
||||
@router.callback_query(F.data == "auto:home")
|
||||
async def open_auto_from_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
await state.clear()
|
||||
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
AutoTradeRunner.set_current_screen("auto")
|
||||
await _render_auto_screen(callback.message, edit_mode=True)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
# включить активную торговлю
|
||||
@router.callback_query(F.data == "auto:start")
|
||||
async def auto_start(callback: CallbackQuery) -> None:
|
||||
service = AutoTradeService()
|
||||
state = service.get_state()
|
||||
|
||||
if not _is_auto_configured(state):
|
||||
await callback.answer(
|
||||
"Сначала настрой параметры автоторговли",
|
||||
show_alert=True,
|
||||
)
|
||||
|
||||
if callback.message is not None:
|
||||
await open_auto_settings(callback)
|
||||
|
||||
return
|
||||
|
||||
_, message = service.start()
|
||||
|
||||
AutoTradeRunner.set_current_screen("auto")
|
||||
AutoTradeRunner.start()
|
||||
|
||||
if callback.message is not None:
|
||||
await _render_auto_screen(callback.message, edit_mode=True)
|
||||
|
||||
await callback.answer(message)
|
||||
|
||||
|
||||
# включить наблюдение
|
||||
@router.callback_query(F.data == "auto:observe")
|
||||
async def auto_observe(callback: CallbackQuery) -> None:
|
||||
service = AutoTradeService()
|
||||
state = service.get_state()
|
||||
|
||||
if not _is_auto_configured(state):
|
||||
await callback.answer(
|
||||
"Сначала настрой параметры автоторговли",
|
||||
show_alert=True,
|
||||
)
|
||||
|
||||
if callback.message is not None:
|
||||
await open_auto_settings(callback)
|
||||
|
||||
return
|
||||
|
||||
_, message = service.observe()
|
||||
|
||||
AutoTradeRunner.set_current_screen("auto")
|
||||
AutoTradeRunner.start()
|
||||
|
||||
if callback.message is not None:
|
||||
await _render_auto_screen(callback.message, edit_mode=True)
|
||||
|
||||
await callback.answer(message)
|
||||
|
||||
|
||||
# выключить автоторговлю
|
||||
@router.callback_query(F.data == "auto:stop")
|
||||
async def auto_stop(callback: CallbackQuery) -> None:
|
||||
service = AutoTradeService()
|
||||
_, message = service.stop()
|
||||
|
||||
AutoTradeRunner.stop()
|
||||
|
||||
if callback.message is not None:
|
||||
await _render_auto_screen(callback.message, edit_mode=True)
|
||||
|
||||
await callback.answer(message)
|
||||
@@ -1,11 +1,11 @@
|
||||
# app/src/telegram/handlers/auto/main.py
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from aiogram.types import CallbackQuery, InaccessibleMessage, Message
|
||||
|
||||
from src.telegram.handlers.auto.ui import (
|
||||
auto_diagnostics_keyboard,
|
||||
@@ -24,6 +24,20 @@ from src.trading.diagnostics.snapshot import SemanticDiagnosticSnapshotBuilder
|
||||
router = Router(name="auto")
|
||||
|
||||
|
||||
def _require_message(
|
||||
callback: CallbackQuery,
|
||||
) -> Message | None:
|
||||
message = callback.message
|
||||
|
||||
if (
|
||||
message is None
|
||||
or isinstance(message, InaccessibleMessage)
|
||||
):
|
||||
return None
|
||||
|
||||
return message
|
||||
|
||||
|
||||
async def render_auto_screen(
|
||||
target_message: Message,
|
||||
*,
|
||||
@@ -38,8 +52,13 @@ async def render_auto_screen(
|
||||
if "message is not modified" not in str(exc).lower():
|
||||
raise
|
||||
|
||||
bot = target_message.bot
|
||||
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
AutoTradeRunner.register_screen(
|
||||
bot=target_message.bot,
|
||||
bot=bot,
|
||||
chat_id=target_message.chat.id,
|
||||
message_id=target_message.message_id,
|
||||
render_text=build_auto_text,
|
||||
@@ -53,9 +72,13 @@ async def render_auto_screen(
|
||||
return
|
||||
|
||||
sent_message = await target_message.answer(text, reply_markup=auto_keyboard())
|
||||
bot = sent_message.bot
|
||||
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
AutoTradeRunner.register_screen(
|
||||
bot=sent_message.bot,
|
||||
bot=bot,
|
||||
chat_id=sent_message.chat.id,
|
||||
message_id=sent_message.message_id,
|
||||
render_text=build_auto_text,
|
||||
@@ -68,29 +91,43 @@ async def render_auto_screen(
|
||||
)
|
||||
|
||||
|
||||
async def _prepare_auto_from_message(message: Message) -> None:
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="auto",
|
||||
bot=message.bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
async def _prepare_auto_from_message(message: Message) -> bool:
|
||||
bot = message.bot
|
||||
|
||||
|
||||
async def _prepare_auto_from_callback(callback: CallbackQuery) -> bool:
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
if bot is None:
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="auto",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _prepare_auto_from_callback(callback: CallbackQuery) -> bool:
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return False
|
||||
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
await callback.answer("Bot недоступен", show_alert=True)
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="auto",
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
keep_message_id=message.message_id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def build_auto_diagnostics_text() -> str:
|
||||
service = AutoTradeService()
|
||||
@@ -118,10 +155,28 @@ async def render_auto_diagnostics_screen(
|
||||
error_text = str(exc).lower()
|
||||
|
||||
if "message to edit not found" in error_text:
|
||||
await target_message.answer(
|
||||
sent_message = await target_message.answer(
|
||||
text,
|
||||
reply_markup=auto_diagnostics_keyboard(),
|
||||
)
|
||||
|
||||
bot = sent_message.bot
|
||||
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
AutoTradeRunner.register_screen(
|
||||
bot=bot,
|
||||
chat_id=sent_message.chat.id,
|
||||
message_id=sent_message.message_id,
|
||||
render_text=build_auto_diagnostics_text,
|
||||
render_markup=auto_diagnostics_keyboard,
|
||||
)
|
||||
|
||||
ActiveScreenManager.register(
|
||||
screen="auto_diagnostics",
|
||||
message=sent_message,
|
||||
)
|
||||
return
|
||||
|
||||
if "message is not modified" in error_text:
|
||||
@@ -129,8 +184,13 @@ async def render_auto_diagnostics_screen(
|
||||
|
||||
raise
|
||||
|
||||
bot = target_message.bot
|
||||
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
AutoTradeRunner.register_screen(
|
||||
bot=target_message.bot,
|
||||
bot=bot,
|
||||
chat_id=target_message.chat.id,
|
||||
message_id=target_message.message_id,
|
||||
render_text=build_auto_diagnostics_text,
|
||||
@@ -147,7 +207,8 @@ async def render_auto_diagnostics_screen(
|
||||
async def open_auto(message: Message, state: FSMContext) -> None:
|
||||
await state.clear()
|
||||
|
||||
await _prepare_auto_from_message(message)
|
||||
if not await _prepare_auto_from_message(message):
|
||||
return
|
||||
|
||||
await render_auto_screen(message, edit_mode=False)
|
||||
|
||||
@@ -159,7 +220,13 @@ async def open_auto_from_callback(callback: CallbackQuery, state: FSMContext) ->
|
||||
if not await _prepare_auto_from_callback(callback):
|
||||
return
|
||||
|
||||
await render_auto_screen(callback.message, edit_mode=True)
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
await render_auto_screen(message, edit_mode=True)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -174,20 +241,22 @@ async def auto_start(callback: CallbackQuery) -> None:
|
||||
show_alert=True,
|
||||
)
|
||||
|
||||
if callback.message is not None:
|
||||
if _require_message(callback) is not None:
|
||||
await open_auto_settings(callback)
|
||||
|
||||
return
|
||||
|
||||
_, message = service.start()
|
||||
_, message_text = service.start()
|
||||
|
||||
if callback.message is not None:
|
||||
await _prepare_auto_from_callback(callback)
|
||||
await render_auto_screen(callback.message, edit_mode=True)
|
||||
if await _prepare_auto_from_callback(callback):
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is not None:
|
||||
await render_auto_screen(message, edit_mode=True)
|
||||
|
||||
AutoTradeRunner.start()
|
||||
|
||||
await callback.answer(message)
|
||||
await callback.answer(message_text)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "auto:observe")
|
||||
@@ -201,48 +270,60 @@ async def auto_observe(callback: CallbackQuery) -> None:
|
||||
show_alert=True,
|
||||
)
|
||||
|
||||
if callback.message is not None:
|
||||
if _require_message(callback) is not None:
|
||||
await open_auto_settings(callback)
|
||||
|
||||
return
|
||||
|
||||
_, message = service.observe()
|
||||
_, message_text = service.observe()
|
||||
|
||||
if callback.message is not None:
|
||||
await _prepare_auto_from_callback(callback)
|
||||
await render_auto_screen(callback.message, edit_mode=True)
|
||||
if await _prepare_auto_from_callback(callback):
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is not None:
|
||||
await render_auto_screen(message, edit_mode=True)
|
||||
|
||||
AutoTradeRunner.start()
|
||||
|
||||
await callback.answer(message)
|
||||
await callback.answer(message_text)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "auto:stop")
|
||||
async def auto_stop(callback: CallbackQuery) -> None:
|
||||
service = AutoTradeService()
|
||||
_, message = service.stop()
|
||||
_, message_text = service.stop()
|
||||
|
||||
AutoTradeRunner.stop()
|
||||
|
||||
if callback.message is not None:
|
||||
await _prepare_auto_from_callback(callback)
|
||||
await render_auto_screen(callback.message, edit_mode=True)
|
||||
if await _prepare_auto_from_callback(callback):
|
||||
message = _require_message(callback)
|
||||
|
||||
await callback.answer(message)
|
||||
if message is not None:
|
||||
await render_auto_screen(message, edit_mode=True)
|
||||
|
||||
await callback.answer(message_text)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "auto:diagnostics")
|
||||
async def open_auto_diagnostics(callback: CallbackQuery) -> None:
|
||||
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
|
||||
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
await callback.answer("Bot недоступен", show_alert=True)
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="auto_diagnostics",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
keep_message_id=message.message_id,
|
||||
)
|
||||
|
||||
await render_auto_diagnostics_screen(callback.message)
|
||||
await render_auto_diagnostics_screen(message)
|
||||
await callback.answer()
|
||||
@@ -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:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.types import Message
|
||||
|
||||
from src.core.config import load_settings
|
||||
from src.core.numbers import safe_float
|
||||
from src.core.types import JsonList, NumericLike
|
||||
from src.trading.debug.execution import DebugExecutionEngine
|
||||
from src.trading.debug.service import DebugTradeService
|
||||
from src.trading.debug.state import DebugTradeState
|
||||
@@ -18,7 +21,7 @@ router = Router(name="debug")
|
||||
|
||||
|
||||
def _debug_enabled() -> bool:
|
||||
return load_settings().debug_enabled
|
||||
return bool(load_settings().debug_enabled)
|
||||
|
||||
|
||||
def _debug_help_text() -> str:
|
||||
@@ -80,6 +83,7 @@ async def debug_auto(message: Message) -> None:
|
||||
|
||||
if command == "reset":
|
||||
state = service.reset()
|
||||
|
||||
await message.answer(
|
||||
"✅ [DEBUG] Runtime reset\n\n"
|
||||
f"{_debug_state_text(state)}"
|
||||
@@ -88,6 +92,7 @@ async def debug_auto(message: Message) -> None:
|
||||
|
||||
if command == "off":
|
||||
state = service.stop()
|
||||
|
||||
await message.answer(
|
||||
"✅ [DEBUG] Runtime stopped\n\n"
|
||||
f"{_debug_state_text(state)}"
|
||||
@@ -97,17 +102,20 @@ async def debug_auto(message: Message) -> None:
|
||||
if command == "state":
|
||||
state = service.get_state()
|
||||
service.update_market()
|
||||
|
||||
await message.answer(_debug_state_text(state))
|
||||
return
|
||||
|
||||
if command == "hold":
|
||||
seconds = _parse_int(parts, index=2, default=335)
|
||||
|
||||
state = service.set_signal_duration(
|
||||
signal="HOLD",
|
||||
seconds=seconds,
|
||||
confidence=0.0,
|
||||
force_ready=False,
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
f"✅ [DEBUG] HOLD {seconds}s\n\n"
|
||||
f"{_debug_state_text(state)}"
|
||||
@@ -182,15 +190,31 @@ async def debug_auto(message: Message) -> None:
|
||||
|
||||
if command == "long":
|
||||
state, result = service.open_long()
|
||||
await message.answer(_execution_result_text("OPEN LONG", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"OPEN LONG",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if command == "short":
|
||||
state, result = service.open_short()
|
||||
await message.answer(_execution_result_text("OPEN SHORT", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"OPEN SHORT",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
await message.answer(f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}")
|
||||
await message.answer(
|
||||
f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}"
|
||||
)
|
||||
|
||||
|
||||
@router.message(F.text.startswith("/debug_exec"))
|
||||
@@ -211,43 +235,82 @@ async def debug_exec(message: Message) -> None:
|
||||
if command == "state":
|
||||
state = service.get_state()
|
||||
service.update_market()
|
||||
|
||||
await message.answer(_debug_state_text(state))
|
||||
return
|
||||
|
||||
if command == "buy":
|
||||
state, result = service.open_long()
|
||||
await message.answer(_execution_result_text("EXEC BUY / LONG", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"EXEC BUY / LONG",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if command == "sell":
|
||||
state, result = service.open_short()
|
||||
await message.answer(_execution_result_text("EXEC SELL / SHORT", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"EXEC SELL / SHORT",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if command == "flip":
|
||||
state, result = service.flip()
|
||||
await message.answer(_execution_result_text("EXEC AUTO FLIP", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"EXEC AUTO FLIP",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if command == "close":
|
||||
state, result = service.close(reason="DEBUG_CLOSE")
|
||||
await message.answer(_execution_result_text("EXEC CLOSE", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"EXEC CLOSE",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if command == "process":
|
||||
state, result = service.process()
|
||||
await message.answer(_execution_result_text("EXEC PROCESS", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"EXEC PROCESS",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if command == "update":
|
||||
state = service.update_market()
|
||||
|
||||
await message.answer(
|
||||
"✅ [DEBUG] Market update\n\n"
|
||||
f"{_debug_state_text(state)}"
|
||||
)
|
||||
return
|
||||
|
||||
await message.answer(f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}")
|
||||
await message.answer(
|
||||
f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}"
|
||||
)
|
||||
|
||||
|
||||
@router.message(F.text.startswith("/debug_live"))
|
||||
@@ -264,8 +327,9 @@ async def debug_live(message: Message) -> None:
|
||||
"/debug_exec sell\n"
|
||||
"/debug_exec flip\n"
|
||||
"/debug_exec close\n\n"
|
||||
"Live-мониторинг для изолированного debug будет добавлен в следующем пакете "
|
||||
"через отдельный DebugTradeRunner и отдельный Debug Auto экран."
|
||||
"Live-мониторинг для изолированного debug будет добавлен "
|
||||
"в следующем пакете через отдельный DebugTradeRunner "
|
||||
"и отдельный Debug Auto экран."
|
||||
)
|
||||
|
||||
|
||||
@@ -275,19 +339,30 @@ async def debug_signal(message: Message) -> None:
|
||||
await message.answer("Debug mode выключен.")
|
||||
return
|
||||
|
||||
signal, confidence, repeat_count, error = _parse_debug_signal_args(message.text)
|
||||
signal, confidence, repeat_count, error = _parse_debug_signal_args(
|
||||
message.text,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
await message.answer(f"⛔️ {error}\n\n{_debug_help_text()}")
|
||||
await message.answer(
|
||||
f"⛔️ {error}\n\n{_debug_help_text()}"
|
||||
)
|
||||
return
|
||||
|
||||
service = DebugTradeService()
|
||||
|
||||
state = service.set_signal(
|
||||
signal=signal,
|
||||
confidence=confidence,
|
||||
repeat_count=repeat_count,
|
||||
reason=f"[DEBUG] LEGACY FORCE {signal} {confidence:.2f} ×{repeat_count}",
|
||||
force_ready=signal in {"BUY", "SELL"} and repeat_count >= 2,
|
||||
reason=(
|
||||
f"[DEBUG] LEGACY FORCE "
|
||||
f"{signal} {confidence:.2f} ×{repeat_count}"
|
||||
),
|
||||
force_ready=(
|
||||
signal in {"BUY", "SELL"}
|
||||
and repeat_count >= 2
|
||||
),
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
@@ -303,6 +378,7 @@ async def debug_ready(message: Message) -> None:
|
||||
return
|
||||
|
||||
service = DebugTradeService()
|
||||
|
||||
state = service.set_signal_duration(
|
||||
signal="BUY",
|
||||
seconds=15,
|
||||
@@ -323,13 +399,16 @@ async def debug_state(message: Message) -> None:
|
||||
return
|
||||
|
||||
service = DebugTradeService()
|
||||
|
||||
state = service.get_state()
|
||||
service.update_market()
|
||||
|
||||
await message.answer(_debug_state_text(state))
|
||||
|
||||
|
||||
def _debug_state_text(state: DebugTradeState) -> str:
|
||||
def _debug_state_text(
|
||||
state: DebugTradeState,
|
||||
) -> str:
|
||||
position = state.position
|
||||
|
||||
duration = _signal_duration_text(state)
|
||||
@@ -348,7 +427,7 @@ def _debug_state_text(state: DebugTradeState) -> str:
|
||||
f"Signal: {_signal_icon(state.last_signal)} {state.last_signal}\n"
|
||||
f"Duration: {duration}\n"
|
||||
f"Repeats: {state.last_signal_repeat_count}\n"
|
||||
f"Confidence: {state.last_signal_confidence:.2f}\n"
|
||||
f"Confidence: {safe_float(state.last_signal_confidence) or 0.0:.2f}\n"
|
||||
f"Decision: {state.decision_status}\n"
|
||||
f"Ready: {state.is_signal_ready}\n"
|
||||
f"Reason: {state.last_signal_reason or '—'}\n\n"
|
||||
@@ -385,53 +464,95 @@ def _execution_result_text(
|
||||
)
|
||||
|
||||
|
||||
def _parse_debug_signal_args(raw_text: str | None) -> tuple[str, float, int, str | None]:
|
||||
def _parse_debug_signal_args(
|
||||
raw_text: str | None,
|
||||
) -> tuple[str, float, int, str | None]:
|
||||
parts = (raw_text or "").split()
|
||||
|
||||
signal = parts[1].upper() if len(parts) > 1 else "BUY"
|
||||
if signal not in {"BUY", "SELL", "HOLD"}:
|
||||
return "BUY", 0.9, 2, "SIGNAL должен быть BUY, SELL или HOLD."
|
||||
|
||||
try:
|
||||
confidence = float(parts[2]) if len(parts) > 2 else 0.9
|
||||
except ValueError:
|
||||
return "BUY", 0.9, 2, "CONFIDENCE должен быть числом от 0.00 до 1.00."
|
||||
if signal not in {"BUY", "SELL", "HOLD"}:
|
||||
return (
|
||||
"BUY",
|
||||
0.9,
|
||||
2,
|
||||
"SIGNAL должен быть BUY, SELL или HOLD.",
|
||||
)
|
||||
|
||||
confidence = _parse_float(parts, index=2, default=0.9)
|
||||
|
||||
if confidence < 0 or confidence > 1:
|
||||
return "BUY", 0.9, 2, "CONFIDENCE должен быть от 0.00 до 1.00."
|
||||
return (
|
||||
"BUY",
|
||||
0.9,
|
||||
2,
|
||||
"CONFIDENCE должен быть от 0.00 до 1.00.",
|
||||
)
|
||||
|
||||
try:
|
||||
repeat_count = int(parts[3]) if len(parts) > 3 else 2
|
||||
except ValueError:
|
||||
return "BUY", 0.9, 2, "REPEATS должен быть целым числом."
|
||||
repeat_count = _parse_int(parts, index=3, default=2)
|
||||
|
||||
if repeat_count < 1:
|
||||
return "BUY", 0.9, 2, "REPEATS должен быть больше или равен 1."
|
||||
return (
|
||||
"BUY",
|
||||
0.9,
|
||||
2,
|
||||
"REPEATS должен быть больше или равен 1.",
|
||||
)
|
||||
|
||||
return signal, confidence, repeat_count, None
|
||||
|
||||
|
||||
def _parse_int(parts: list[str], *, index: int, default: int) -> int:
|
||||
def _parse_int(
|
||||
parts: JsonList,
|
||||
*,
|
||||
index: int,
|
||||
default: int,
|
||||
) -> int:
|
||||
try:
|
||||
return int(parts[index])
|
||||
except (IndexError, TypeError, ValueError):
|
||||
value = parts[index]
|
||||
except (IndexError, TypeError):
|
||||
return default
|
||||
|
||||
number = safe_float(value)
|
||||
|
||||
def _parse_float(parts: list[str], *, index: int, default: float) -> float:
|
||||
try:
|
||||
return float(parts[index])
|
||||
except (IndexError, TypeError, ValueError):
|
||||
if number is None:
|
||||
return default
|
||||
|
||||
return int(number)
|
||||
|
||||
def _signal_duration_text(state: DebugTradeState) -> str:
|
||||
started_at = state.signal_started_at
|
||||
|
||||
def _parse_float(
|
||||
parts: JsonList,
|
||||
*,
|
||||
index: int,
|
||||
default: float,
|
||||
) -> float:
|
||||
try:
|
||||
value = parts[index]
|
||||
except (IndexError, TypeError):
|
||||
return default
|
||||
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return default
|
||||
|
||||
return number
|
||||
|
||||
|
||||
def _signal_duration_text(
|
||||
state: DebugTradeState,
|
||||
) -> str:
|
||||
started_at = safe_float(state.signal_started_at)
|
||||
|
||||
if started_at is not None:
|
||||
total_seconds = max(0, int(__import__("time").monotonic() - float(started_at)))
|
||||
total_seconds = max(
|
||||
0,
|
||||
int(time.monotonic() - started_at),
|
||||
)
|
||||
else:
|
||||
total_seconds = max(0, (state.last_signal_repeat_count or 0) * 5)
|
||||
repeats = state.last_signal_repeat_count or 0
|
||||
total_seconds = max(0, int(repeats) * 5)
|
||||
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
@@ -446,73 +567,98 @@ def _signal_duration_text(state: DebugTradeState) -> str:
|
||||
return f"{seconds}с"
|
||||
|
||||
|
||||
def _signal_icon(signal: str | None) -> str:
|
||||
def _signal_icon(
|
||||
signal: str | None,
|
||||
) -> str:
|
||||
mapping = {
|
||||
"BUY": "🟢",
|
||||
"SELL": "🔴",
|
||||
"HOLD": "🟡",
|
||||
}
|
||||
|
||||
return mapping.get(signal or "", "⚪")
|
||||
|
||||
|
||||
def _format_leverage(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
def _format_leverage(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return "x—"
|
||||
|
||||
return f"x{float(value):g}"
|
||||
return f"x{number:g}"
|
||||
|
||||
|
||||
def _format_crypto_size(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
def _format_crypto_size(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return "—"
|
||||
|
||||
number = float(value)
|
||||
return f"{number:.5f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def _format_percent(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
def _format_percent(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return "off"
|
||||
|
||||
number = float(value)
|
||||
|
||||
if abs(number - round(number)) < 1e-9:
|
||||
if math.isclose(number, round(number), abs_tol=1e-9):
|
||||
return f"{int(round(number))}%"
|
||||
|
||||
return f"{number:.2f}".rstrip("0").rstrip(".") + "%"
|
||||
|
||||
|
||||
def _format_money_compact(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
def _format_money_compact(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return "—"
|
||||
|
||||
number = float(value)
|
||||
|
||||
if abs(number - round(number)) < 1e-9:
|
||||
if math.isclose(number, round(number), abs_tol=1e-9):
|
||||
return f"{number:,.0f}".replace(",", " ")
|
||||
|
||||
return f"{number:,.2f}".replace(",", " ").rstrip("0").rstrip(".")
|
||||
return (
|
||||
f"{number:,.2f}"
|
||||
.replace(",", " ")
|
||||
.rstrip("0")
|
||||
.rstrip(".")
|
||||
)
|
||||
|
||||
|
||||
def _format_usd_or_dash(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
def _format_usd_or_dash(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
if safe_float(value) is None:
|
||||
return "—"
|
||||
|
||||
return f"$ {_format_money_compact(value)}"
|
||||
|
||||
|
||||
def _format_usd_or_off(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
def _format_usd_or_off(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
if safe_float(value) is None:
|
||||
return "off"
|
||||
|
||||
return f"$ {_format_money_compact(value)}"
|
||||
|
||||
|
||||
def _format_signed_usd(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
def _format_signed_usd(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
amount = safe_float(value)
|
||||
|
||||
amount = float(value)
|
||||
if amount is None:
|
||||
return "—"
|
||||
|
||||
if amount > 0:
|
||||
return f"🟢 +$ {_format_money_compact(amount)}"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# app/src/telegram/handlers/home.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message
|
||||
@@ -11,16 +13,33 @@ from src.telegram.menus import HOME_TEXT
|
||||
router = Router(name="home")
|
||||
|
||||
|
||||
@router.message(F.text == "🏠 Главная")
|
||||
async def open_home(message: Message, state: FSMContext) -> None:
|
||||
await state.clear()
|
||||
async def _prepare_home_from_message(
|
||||
message: Message,
|
||||
) -> bool:
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="home",
|
||||
bot=message.bot,
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@router.message(F.text == "🏠 Главная")
|
||||
async def open_home(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
|
||||
if not await _prepare_home_from_message(message):
|
||||
return
|
||||
|
||||
sent_message = await message.answer(HOME_TEXT)
|
||||
|
||||
ActiveScreenManager.register(
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
# app/src/telegram/handlers/journal.py
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import BufferedInputFile, CallbackQuery, Message
|
||||
from aiogram.types import (
|
||||
BufferedInputFile,
|
||||
CallbackQuery,
|
||||
InaccessibleMessage,
|
||||
Message,
|
||||
)
|
||||
|
||||
from src.core.numbers import safe_float
|
||||
from src.core.types import JsonDict, NumericLike
|
||||
from src.telegram.handlers.journal_ui import (
|
||||
PAGE_SIZE,
|
||||
build_actions_keyboard,
|
||||
@@ -23,12 +30,26 @@ from src.trading.journal.service import JournalService
|
||||
router = Router(name="journal")
|
||||
|
||||
|
||||
def _require_message(
|
||||
callback: CallbackQuery,
|
||||
) -> Message | None:
|
||||
message = callback.message
|
||||
|
||||
if (
|
||||
message is None
|
||||
or isinstance(message, InaccessibleMessage)
|
||||
):
|
||||
return None
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def _user_id_from_message(message: Message) -> int | None:
|
||||
return message.from_user.id if message.from_user else None
|
||||
|
||||
|
||||
def _chat_id_from_message(message: Message) -> int | None:
|
||||
return message.chat.id if message.chat else None
|
||||
def _chat_id_from_message(message: Message) -> int:
|
||||
return message.chat.id
|
||||
|
||||
|
||||
def _user_id_from_callback(callback: CallbackQuery) -> int | None:
|
||||
@@ -36,17 +57,38 @@ def _user_id_from_callback(callback: CallbackQuery) -> int | None:
|
||||
|
||||
|
||||
def _chat_id_from_callback(callback: CallbackQuery) -> int | None:
|
||||
if callback.message and callback.message.chat:
|
||||
return callback.message.chat.id
|
||||
message = _require_message(callback)
|
||||
|
||||
return None
|
||||
if message is None:
|
||||
return None
|
||||
|
||||
return message.chat.id
|
||||
|
||||
|
||||
def _parse_page(value: NumericLike | None) -> int | None:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return None
|
||||
|
||||
return int(number)
|
||||
|
||||
|
||||
def _journal_payload(**values: object) -> JsonDict:
|
||||
return dict(values)
|
||||
|
||||
|
||||
def _register_journal_screen(message: Message) -> None:
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
LiveScreenRunner.unregister_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
)
|
||||
|
||||
ScreenRegistry.unregister_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
@@ -55,7 +97,7 @@ def _register_journal_screen(message: Message) -> None:
|
||||
ScreenRegistry.register_screen(
|
||||
StaticScreen(
|
||||
screen="journal",
|
||||
bot=message.bot,
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
)
|
||||
@@ -67,6 +109,54 @@ def _register_journal_screen(message: Message) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def _prepare_journal_from_message(
|
||||
message: Message,
|
||||
) -> bool:
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="journal",
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _prepare_journal_from_callback(
|
||||
callback: CallbackQuery,
|
||||
) -> bool:
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer(
|
||||
"Сообщение недоступно",
|
||||
show_alert=True,
|
||||
)
|
||||
return False
|
||||
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
await callback.answer(
|
||||
"Bot недоступен",
|
||||
show_alert=True,
|
||||
)
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="journal",
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
keep_message_id=message.message_id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _show_journal_page(
|
||||
target_message: Message,
|
||||
*,
|
||||
@@ -85,33 +175,37 @@ async def _show_journal_page(
|
||||
kb = build_keyboard(page, total_pages)
|
||||
|
||||
if edit_mode:
|
||||
await target_message.edit_text(text, reply_markup=kb)
|
||||
await target_message.edit_text(
|
||||
text,
|
||||
reply_markup=kb,
|
||||
)
|
||||
_register_journal_screen(target_message)
|
||||
return
|
||||
|
||||
sent_message = await target_message.answer(text, reply_markup=kb)
|
||||
sent_message = await target_message.answer(
|
||||
text,
|
||||
reply_markup=kb,
|
||||
)
|
||||
_register_journal_screen(sent_message)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "journal:actions")
|
||||
async def journal_actions(callback: CallbackQuery) -> None:
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
if not await _prepare_journal_from_callback(callback):
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="journal",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
message = _require_message(callback)
|
||||
|
||||
await callback.message.edit_text(
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
await message.edit_text(
|
||||
render_actions(),
|
||||
reply_markup=build_actions_keyboard(),
|
||||
)
|
||||
|
||||
_register_journal_screen(callback.message)
|
||||
_register_journal_screen(message)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
@@ -120,11 +214,8 @@ async def journal_actions(callback: CallbackQuery) -> None:
|
||||
async def open_journal(message: Message, state: FSMContext) -> None:
|
||||
await state.clear()
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="journal",
|
||||
bot=message.bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
if not await _prepare_journal_from_message(message):
|
||||
return
|
||||
|
||||
await _show_journal_page(
|
||||
message,
|
||||
@@ -134,22 +225,23 @@ async def open_journal(message: Message, state: FSMContext) -> None:
|
||||
|
||||
|
||||
@router.callback_query(F.data == "monitoring:journal")
|
||||
async def open_journal_from_monitoring(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
async def open_journal_from_monitoring(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
if not await _prepare_journal_from_callback(callback):
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="journal",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
await _show_journal_page(
|
||||
callback.message,
|
||||
message,
|
||||
page=1,
|
||||
edit_mode=True,
|
||||
)
|
||||
@@ -165,6 +257,7 @@ async def journal_noop(callback: CallbackQuery) -> None:
|
||||
@router.callback_query(F.data == "journal:export_csv")
|
||||
async def export_journal_csv(callback: CallbackQuery) -> None:
|
||||
service = JournalService()
|
||||
message = _require_message(callback)
|
||||
|
||||
try:
|
||||
data = service.export_csv()
|
||||
@@ -173,8 +266,8 @@ async def export_journal_csv(callback: CallbackQuery) -> None:
|
||||
filename=service.build_export_filename("csv"),
|
||||
)
|
||||
|
||||
if callback.message is not None:
|
||||
await callback.message.answer_document(document=document)
|
||||
if message is not None:
|
||||
await message.answer_document(document=document)
|
||||
|
||||
service.log_ui_info(
|
||||
event_type="journal_exported",
|
||||
@@ -183,10 +276,11 @@ async def export_journal_csv(callback: CallbackQuery) -> None:
|
||||
action="export_csv",
|
||||
user_id=_user_id_from_callback(callback),
|
||||
chat_id=_chat_id_from_callback(callback),
|
||||
payload={"format": "csv"},
|
||||
payload=_journal_payload(format="csv"),
|
||||
)
|
||||
|
||||
await callback.answer("CSV экспортирован")
|
||||
|
||||
except Exception as exc:
|
||||
service.log_ui_error(
|
||||
event_type="journal_export_error",
|
||||
@@ -195,15 +289,20 @@ async def export_journal_csv(callback: CallbackQuery) -> None:
|
||||
action="export_csv",
|
||||
user_id=_user_id_from_callback(callback),
|
||||
chat_id=_chat_id_from_callback(callback),
|
||||
payload={"format": "csv"},
|
||||
payload=_journal_payload(format="csv"),
|
||||
raw_error=str(exc),
|
||||
)
|
||||
await callback.answer("Не удалось экспортировать CSV", show_alert=True)
|
||||
|
||||
await callback.answer(
|
||||
"Не удалось экспортировать CSV",
|
||||
show_alert=True,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "journal:export_xlsx")
|
||||
async def export_journal_xlsx(callback: CallbackQuery) -> None:
|
||||
service = JournalService()
|
||||
message = _require_message(callback)
|
||||
|
||||
try:
|
||||
data = service.export_xlsx()
|
||||
@@ -212,8 +311,8 @@ async def export_journal_xlsx(callback: CallbackQuery) -> None:
|
||||
filename=service.build_export_filename("xlsx"),
|
||||
)
|
||||
|
||||
if callback.message is not None:
|
||||
await callback.message.answer_document(document=document)
|
||||
if message is not None:
|
||||
await message.answer_document(document=document)
|
||||
|
||||
service.log_ui_info(
|
||||
event_type="journal_exported",
|
||||
@@ -222,10 +321,11 @@ async def export_journal_xlsx(callback: CallbackQuery) -> None:
|
||||
action="export_xlsx",
|
||||
user_id=_user_id_from_callback(callback),
|
||||
chat_id=_chat_id_from_callback(callback),
|
||||
payload={"format": "xlsx"},
|
||||
payload=_journal_payload(format="xlsx"),
|
||||
)
|
||||
|
||||
await callback.answer("Excel экспортирован")
|
||||
|
||||
except Exception as exc:
|
||||
service.log_ui_error(
|
||||
event_type="journal_export_error",
|
||||
@@ -234,56 +334,56 @@ async def export_journal_xlsx(callback: CallbackQuery) -> None:
|
||||
action="export_xlsx",
|
||||
user_id=_user_id_from_callback(callback),
|
||||
chat_id=_chat_id_from_callback(callback),
|
||||
payload={"format": "xlsx"},
|
||||
payload=_journal_payload(format="xlsx"),
|
||||
raw_error=str(exc),
|
||||
)
|
||||
await callback.answer("Не удалось экспортировать Excel", show_alert=True)
|
||||
|
||||
await callback.answer(
|
||||
"Не удалось экспортировать Excel",
|
||||
show_alert=True,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "journal:clear_confirm")
|
||||
async def clear_journal_confirm(callback: CallbackQuery) -> None:
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
if not await _prepare_journal_from_callback(callback):
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="journal",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
service = JournalService()
|
||||
total_count = service.get_total_count()
|
||||
|
||||
await callback.message.edit_text(
|
||||
await message.edit_text(
|
||||
render_clear_confirm(total_count=total_count),
|
||||
reply_markup=build_clear_confirm_keyboard(),
|
||||
)
|
||||
|
||||
_register_journal_screen(callback.message)
|
||||
_register_journal_screen(message)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "journal:clear")
|
||||
async def clear_journal(callback: CallbackQuery) -> None:
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
if not await _prepare_journal_from_callback(callback):
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="journal",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
service = JournalService()
|
||||
deleted_count = service.clear_all()
|
||||
total_count = service.get_total_count()
|
||||
|
||||
await callback.message.edit_text(
|
||||
await message.edit_text(
|
||||
render_clear_confirm(
|
||||
total_count=total_count,
|
||||
deleted_count=deleted_count,
|
||||
@@ -291,29 +391,27 @@ async def clear_journal(callback: CallbackQuery) -> None:
|
||||
reply_markup=build_clear_confirm_keyboard(),
|
||||
)
|
||||
|
||||
_register_journal_screen(callback.message)
|
||||
_register_journal_screen(message)
|
||||
|
||||
await callback.answer(f"Удалено: {deleted_count}")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "journal:clear_older:90")
|
||||
async def clear_journal_older_90(callback: CallbackQuery) -> None:
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
if not await _prepare_journal_from_callback(callback):
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="journal",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
service = JournalService()
|
||||
deleted_count = service.clear_older_than_days(90)
|
||||
total_count = service.get_total_count()
|
||||
|
||||
await callback.message.edit_text(
|
||||
await message.edit_text(
|
||||
render_clear_confirm(
|
||||
total_count=total_count,
|
||||
deleted_count=deleted_count if deleted_count > 0 else None,
|
||||
@@ -322,34 +420,37 @@ async def clear_journal_older_90(callback: CallbackQuery) -> None:
|
||||
reply_markup=build_clear_confirm_keyboard(),
|
||||
)
|
||||
|
||||
_register_journal_screen(callback.message)
|
||||
_register_journal_screen(message)
|
||||
|
||||
await callback.answer(f"Удалено: {deleted_count}")
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("journal:"))
|
||||
async def paginate(callback: CallbackQuery) -> None:
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
if not await _prepare_journal_from_callback(callback):
|
||||
return
|
||||
|
||||
page_raw = callback.data.split(":", 1)[1]
|
||||
message = _require_message(callback)
|
||||
|
||||
try:
|
||||
page = int(page_raw)
|
||||
except ValueError:
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
data = callback.data or ""
|
||||
parts = data.split(":", 1)
|
||||
|
||||
if len(parts) < 2:
|
||||
await callback.answer("Неизвестное действие", show_alert=True)
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="journal",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
page = _parse_page(parts[1])
|
||||
|
||||
if page is None:
|
||||
await callback.answer("Неизвестное действие", show_alert=True)
|
||||
return
|
||||
|
||||
await _show_journal_page(
|
||||
callback.message,
|
||||
message,
|
||||
page=page,
|
||||
edit_mode=True,
|
||||
)
|
||||
|
||||
@@ -10,6 +10,8 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from src.core.config import load_settings
|
||||
from src.core.event_titles import event_title
|
||||
from src.core.numbers import safe_float
|
||||
from src.core.types import JsonDict, JsonList, NumericLike
|
||||
|
||||
|
||||
PAGE_SIZE = 5
|
||||
@@ -30,26 +32,34 @@ TECH_TO_HUMAN_MESSAGES = {
|
||||
}
|
||||
|
||||
|
||||
def build_keyboard(page: int, total_pages: int) -> InlineKeyboardMarkup:
|
||||
def build_keyboard(
|
||||
page: int,
|
||||
total_pages: int,
|
||||
) -> InlineKeyboardMarkup:
|
||||
current_page = max(1, int(page))
|
||||
pages_count = max(1, int(total_pages))
|
||||
|
||||
kb = InlineKeyboardBuilder()
|
||||
|
||||
if page > 1:
|
||||
if current_page > 1:
|
||||
kb.button(text="⏮️", callback_data="journal:1")
|
||||
kb.button(text="⬅️", callback_data=f"journal:{page - 1}")
|
||||
kb.button(text="⬅️", callback_data=f"journal:{current_page - 1}")
|
||||
|
||||
kb.button(text=f"{page}/{total_pages}", callback_data="journal:noop")
|
||||
kb.button(text=f"{current_page}/{pages_count}", callback_data="journal:noop")
|
||||
|
||||
if page < total_pages:
|
||||
kb.button(text="➡️", callback_data=f"journal:{page + 1}")
|
||||
if current_page < pages_count:
|
||||
kb.button(text="➡️", callback_data=f"journal:{current_page + 1}")
|
||||
|
||||
kb.button(text="📤 Экспорт", callback_data="journal:actions")
|
||||
kb.button(text="🛠️ Настройки", callback_data="settings:journal")
|
||||
kb.button(text="📊 К мониторингу", callback_data="monitoring:home")
|
||||
|
||||
nav_count = 1
|
||||
if page > 1:
|
||||
|
||||
if current_page > 1:
|
||||
nav_count += 2
|
||||
if page < total_pages:
|
||||
|
||||
if current_page < pages_count:
|
||||
nav_count += 1
|
||||
|
||||
kb.adjust(nav_count, 2, 1)
|
||||
@@ -83,33 +93,51 @@ def build_clear_confirm_keyboard() -> InlineKeyboardMarkup:
|
||||
return kb.as_markup()
|
||||
|
||||
|
||||
def _format_int(
|
||||
value: NumericLike | None,
|
||||
*,
|
||||
default: int = 0,
|
||||
) -> int:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return default
|
||||
|
||||
return int(number)
|
||||
|
||||
|
||||
def render_clear_confirm(
|
||||
*,
|
||||
total_count: int,
|
||||
deleted_count: int | None = None,
|
||||
no_old_records_days: int | None = None,
|
||||
total_count: NumericLike,
|
||||
deleted_count: NumericLike | None = None,
|
||||
no_old_records_days: NumericLike | None = None,
|
||||
) -> str:
|
||||
total = _format_int(total_count)
|
||||
|
||||
lines = [
|
||||
"<b>⚠️ Очистить журнал</b>",
|
||||
"",
|
||||
"<b>СИСТЕМА</b> · Настройки · Журнал",
|
||||
"",
|
||||
f"📄 Записей: {total_count}",
|
||||
f"📄 Записей: {total}",
|
||||
]
|
||||
|
||||
if deleted_count is not None:
|
||||
lines.append(f"🧹 Удалено записей: {deleted_count}")
|
||||
lines.append(f"🧹 Удалено записей: {_format_int(deleted_count)}")
|
||||
|
||||
if no_old_records_days is not None:
|
||||
lines.append(f"📭 Нет записей старше {no_old_records_days} дней")
|
||||
lines.append(
|
||||
f"📭 Нет записей старше {_format_int(no_old_records_days)} дней"
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _parse_local_datetime(value: str) -> datetime | None:
|
||||
def _parse_local_datetime(value: object) -> datetime | None:
|
||||
try:
|
||||
settings = load_settings()
|
||||
dt = datetime.fromisoformat(value)
|
||||
raw_value = str(value or "")
|
||||
dt = datetime.fromisoformat(raw_value)
|
||||
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
|
||||
@@ -135,39 +163,52 @@ def _date_group_label(dt: datetime | None) -> str:
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _time_label(dt: datetime | None, raw_value: str) -> str:
|
||||
def _time_label(
|
||||
dt: datetime | None,
|
||||
raw_value: object,
|
||||
) -> str:
|
||||
if dt is None:
|
||||
return raw_value
|
||||
return str(raw_value or "")
|
||||
|
||||
return dt.strftime("%H:%M:%S")
|
||||
|
||||
|
||||
def _event_title(event_type: str) -> str:
|
||||
return event_title(event_type)
|
||||
def _event_title(event_type: object) -> str:
|
||||
return event_title(str(event_type or ""))
|
||||
|
||||
|
||||
def _humanize_message(message: str) -> str:
|
||||
lower = message.lower()
|
||||
for k, v in TECH_TO_HUMAN_MESSAGES.items():
|
||||
if k in lower:
|
||||
return v
|
||||
return message
|
||||
def _humanize_message(message: object) -> str:
|
||||
text = str(message or "")
|
||||
lower = text.lower()
|
||||
|
||||
for technical_text, human_text in TECH_TO_HUMAN_MESSAGES.items():
|
||||
if technical_text in lower:
|
||||
return human_text
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def _payload(event: dict) -> dict:
|
||||
def _payload(event: JsonDict) -> JsonDict:
|
||||
payload = event.get("payload")
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
if isinstance(payload, dict):
|
||||
return payload
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _render_auto_signal(event: dict, created_time: str) -> list[str]:
|
||||
level = str(event.get("level", "INFO")).upper()
|
||||
def _render_auto_signal(
|
||||
event: JsonDict,
|
||||
created_time: str,
|
||||
) -> list[str]:
|
||||
level = str(event.get("level") or "INFO").upper()
|
||||
icon = LEVEL_ICONS.get(level, "•")
|
||||
title = _event_title(str(event.get("event_type", "")))
|
||||
message = _humanize_message(str(event.get("message", "")))
|
||||
title = _event_title(event.get("event_type"))
|
||||
message = _humanize_message(event.get("message"))
|
||||
|
||||
lines = [
|
||||
f"{icon} <b>{level}</b> · {title}",
|
||||
f"{created_time}",
|
||||
created_time,
|
||||
]
|
||||
|
||||
if message:
|
||||
@@ -176,15 +217,18 @@ def _render_auto_signal(event: dict, created_time: str) -> list[str]:
|
||||
return lines
|
||||
|
||||
|
||||
def _render_default_event(event: dict, created_time: str) -> list[str]:
|
||||
level = str(event.get("level", "INFO")).upper()
|
||||
def _render_default_event(
|
||||
event: JsonDict,
|
||||
created_time: str,
|
||||
) -> list[str]:
|
||||
level = str(event.get("level") or "INFO").upper()
|
||||
icon = LEVEL_ICONS.get(level, "•")
|
||||
title = _event_title(str(event.get("event_type", "")))
|
||||
message = _humanize_message(str(event.get("message", "")))
|
||||
title = _event_title(event.get("event_type"))
|
||||
message = _humanize_message(event.get("message"))
|
||||
|
||||
lines = [
|
||||
f"{icon} <b>{level}</b> · {title}",
|
||||
f"{created_time}",
|
||||
created_time,
|
||||
]
|
||||
|
||||
if message:
|
||||
@@ -193,7 +237,11 @@ def _render_default_event(event: dict, created_time: str) -> list[str]:
|
||||
return lines
|
||||
|
||||
|
||||
def render(events, page, total_pages):
|
||||
def render(
|
||||
events: JsonList,
|
||||
page: NumericLike,
|
||||
total_pages: NumericLike,
|
||||
) -> str:
|
||||
lines = [
|
||||
"<b>📒 Журнал</b>",
|
||||
"",
|
||||
@@ -205,10 +253,15 @@ def render(events, page, total_pages):
|
||||
lines.append("Событий пока нет.")
|
||||
return "\n".join(lines)
|
||||
|
||||
current_group = None
|
||||
current_group: str | None = None
|
||||
|
||||
for event in events:
|
||||
raw_created_at = str(event.get("created_at", ""))
|
||||
for raw_event in events:
|
||||
if not isinstance(raw_event, dict):
|
||||
continue
|
||||
|
||||
event: JsonDict = raw_event
|
||||
|
||||
raw_created_at = event.get("created_at") or ""
|
||||
dt = _parse_local_datetime(raw_created_at)
|
||||
group_label = _date_group_label(dt)
|
||||
created_time = _time_label(dt, raw_created_at)
|
||||
@@ -218,7 +271,7 @@ def render(events, page, total_pages):
|
||||
lines.append(f"<b>{group_label}</b>")
|
||||
lines.append("")
|
||||
|
||||
event_type = str(event.get("event_type", ""))
|
||||
event_type = str(event.get("event_type") or "")
|
||||
|
||||
if event_type in {"signal_summary", "signal_ready"}:
|
||||
lines.extend(_render_auto_signal(event, created_time))
|
||||
|
||||
@@ -4,9 +4,16 @@ from __future__ import annotations
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
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 NumericLike
|
||||
from src.integrations.exchange.exceptions import ExchangeError
|
||||
from src.integrations.exchange.service import ExchangeService
|
||||
from src.telegram.live.active_screen import ActiveScreenManager
|
||||
@@ -26,6 +33,20 @@ _last_market_prices: dict[str, float] = {}
|
||||
_last_market_directions: dict[str, str] = {}
|
||||
|
||||
|
||||
def _require_message(
|
||||
callback: CallbackQuery,
|
||||
) -> Message | None:
|
||||
message = callback.message
|
||||
|
||||
if (
|
||||
message is None
|
||||
or isinstance(message, InaccessibleMessage)
|
||||
):
|
||||
return None
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def _market_keyboard() -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="📊 К мониторингу", callback_data="monitoring:home")
|
||||
@@ -35,22 +56,27 @@ def _market_keyboard() -> InlineKeyboardMarkup:
|
||||
|
||||
def _build_market_text(
|
||||
*,
|
||||
ticker_price: float,
|
||||
ticker_price: NumericLike,
|
||||
name: str,
|
||||
market_type: str,
|
||||
base_asset: str,
|
||||
quote_asset: str,
|
||||
) -> str:
|
||||
price = safe_float(ticker_price)
|
||||
|
||||
if price is None:
|
||||
price = 0.0
|
||||
|
||||
previous_price = _last_market_prices.get(name)
|
||||
price_direction = _last_market_directions.get(name, "▲")
|
||||
|
||||
if previous_price is not None:
|
||||
if ticker_price > previous_price:
|
||||
if price > previous_price:
|
||||
price_direction = "🔺"
|
||||
elif ticker_price < previous_price:
|
||||
elif price < previous_price:
|
||||
price_direction = "🔻"
|
||||
|
||||
_last_market_prices[name] = ticker_price
|
||||
_last_market_prices[name] = price
|
||||
_last_market_directions[name] = price_direction
|
||||
|
||||
type_map = {
|
||||
@@ -64,7 +90,7 @@ def _build_market_text(
|
||||
f"{mode_line()}"
|
||||
"\n"
|
||||
f"<b>{base_asset} / {quote_asset}</b> ({market_type_ru})\n\n"
|
||||
f"<b>$ {format_usd_amount(ticker_price)}</b> {price_direction}\n\n"
|
||||
f"<b>$ {format_usd_amount(price)}</b> {price_direction}\n\n"
|
||||
f"{now_line()}"
|
||||
)
|
||||
|
||||
@@ -87,9 +113,21 @@ def _build_market_live_text() -> str:
|
||||
|
||||
symbol_info = validation.symbol_info
|
||||
market_type = symbol_info.market_type if symbol_info else "n/a"
|
||||
base_asset = symbol_info.base_asset if symbol_info and symbol_info.base_asset else "n/a"
|
||||
quote_asset = symbol_info.quote_asset if symbol_info and symbol_info.quote_asset else "n/a"
|
||||
name = symbol_info.name if symbol_info and symbol_info.name else ticker.symbol
|
||||
base_asset = (
|
||||
symbol_info.base_asset
|
||||
if symbol_info and symbol_info.base_asset
|
||||
else "n/a"
|
||||
)
|
||||
quote_asset = (
|
||||
symbol_info.quote_asset
|
||||
if symbol_info and symbol_info.quote_asset
|
||||
else "n/a"
|
||||
)
|
||||
name = (
|
||||
symbol_info.name
|
||||
if symbol_info and symbol_info.name
|
||||
else ticker.symbol
|
||||
)
|
||||
|
||||
return _build_market_text(
|
||||
ticker_price=ticker.price,
|
||||
@@ -101,10 +139,16 @@ def _build_market_live_text() -> str:
|
||||
|
||||
|
||||
def _register_market_live_screen(message: Message) -> None:
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
LiveScreenRunner.unregister_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
)
|
||||
|
||||
ScreenRegistry.unregister_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
@@ -113,7 +157,7 @@ def _register_market_live_screen(message: Message) -> None:
|
||||
LiveScreenRunner.register_screen(
|
||||
LiveScreen(
|
||||
screen="market",
|
||||
bot=message.bot,
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
render_text=_build_market_live_text,
|
||||
@@ -121,9 +165,58 @@ def _register_market_live_screen(message: Message) -> None:
|
||||
interval_seconds=5,
|
||||
)
|
||||
)
|
||||
|
||||
LiveScreenRunner.start("market")
|
||||
|
||||
|
||||
async def _prepare_market_from_message(
|
||||
message: Message,
|
||||
) -> bool:
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="market",
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _prepare_market_from_callback(
|
||||
callback: CallbackQuery,
|
||||
) -> bool:
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer(
|
||||
"Сообщение недоступно",
|
||||
show_alert=True,
|
||||
)
|
||||
return False
|
||||
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
await callback.answer(
|
||||
"Bot недоступен",
|
||||
show_alert=True,
|
||||
)
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="market",
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
keep_message_id=message.message_id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _render_market_screen(
|
||||
target_message: Message,
|
||||
*,
|
||||
@@ -184,9 +277,21 @@ async def _render_market_screen(
|
||||
|
||||
symbol_info = validation.symbol_info
|
||||
market_type = symbol_info.market_type if symbol_info else "n/a"
|
||||
base_asset = symbol_info.base_asset if symbol_info and symbol_info.base_asset else "n/a"
|
||||
quote_asset = symbol_info.quote_asset if symbol_info and symbol_info.quote_asset else "n/a"
|
||||
name = symbol_info.name if symbol_info and symbol_info.name else ticker.symbol
|
||||
base_asset = (
|
||||
symbol_info.base_asset
|
||||
if symbol_info and symbol_info.base_asset
|
||||
else "n/a"
|
||||
)
|
||||
quote_asset = (
|
||||
symbol_info.quote_asset
|
||||
if symbol_info and symbol_info.quote_asset
|
||||
else "n/a"
|
||||
)
|
||||
name = (
|
||||
symbol_info.name
|
||||
if symbol_info and symbol_info.name
|
||||
else ticker.symbol
|
||||
)
|
||||
|
||||
text = _build_market_text(
|
||||
ticker_price=ticker.price,
|
||||
@@ -205,7 +310,7 @@ async def _render_market_screen(
|
||||
chat_id=chat_id,
|
||||
payload={
|
||||
"symbol": ticker.symbol,
|
||||
"price": ticker.price,
|
||||
"price": safe_float(ticker.price),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -223,11 +328,8 @@ async def _render_market_screen(
|
||||
async def open_market(message: Message, state: FSMContext) -> None:
|
||||
await state.clear()
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="market",
|
||||
bot=message.bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
if not await _prepare_market_from_message(message):
|
||||
return
|
||||
|
||||
user_id = message.from_user.id if message.from_user else None
|
||||
chat_id = message.chat.id if message.chat else None
|
||||
@@ -263,32 +365,38 @@ async def open_market(message: Message, state: FSMContext) -> None:
|
||||
|
||||
|
||||
@router.callback_query(F.data == "monitoring:market")
|
||||
async def open_market_from_monitoring(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
async def open_market_from_monitoring(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
if not await _prepare_market_from_callback(callback):
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="market",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer(
|
||||
"Сообщение недоступно",
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
user_id = callback.from_user.id if callback.from_user else None
|
||||
chat_id = callback.message.chat.id if callback.message.chat else None
|
||||
chat_id = message.chat.id
|
||||
|
||||
try:
|
||||
await _render_market_screen(
|
||||
callback.message,
|
||||
message,
|
||||
user_id=user_id,
|
||||
chat_id=chat_id,
|
||||
edit_mode=True,
|
||||
action="open_from_monitoring",
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
except ExchangeError as exc:
|
||||
JournalService().log_ui_error(
|
||||
event_type="market_open_error",
|
||||
@@ -312,32 +420,38 @@ async def open_market_from_monitoring(callback: CallbackQuery, state: FSMContext
|
||||
|
||||
|
||||
@router.callback_query(F.data == "market:retry")
|
||||
async def retry_market(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
async def retry_market(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
if not await _prepare_market_from_callback(callback):
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="market",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer(
|
||||
"Сообщение недоступно",
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
|
||||
user_id = callback.from_user.id if callback.from_user else None
|
||||
chat_id = callback.message.chat.id if callback.message.chat else None
|
||||
chat_id = message.chat.id
|
||||
|
||||
try:
|
||||
await _render_market_screen(
|
||||
callback.message,
|
||||
message,
|
||||
user_id=user_id,
|
||||
chat_id=chat_id,
|
||||
edit_mode=True,
|
||||
action="retry",
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
except ExchangeError as exc:
|
||||
JournalService().log_ui_error(
|
||||
event_type="market_retry_error",
|
||||
|
||||
@@ -4,7 +4,12 @@ from __future__ import annotations
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
|
||||
from aiogram.types import (
|
||||
CallbackQuery,
|
||||
InaccessibleMessage,
|
||||
InlineKeyboardMarkup,
|
||||
Message,
|
||||
)
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from src.telegram.live.active_screen import ActiveScreenManager
|
||||
@@ -14,6 +19,20 @@ from src.telegram.live.runner import LiveScreenRunner, ScreenRegistry, StaticScr
|
||||
router = Router(name="monitoring")
|
||||
|
||||
|
||||
def _require_message(
|
||||
callback: CallbackQuery,
|
||||
) -> Message | None:
|
||||
message = callback.message
|
||||
|
||||
if (
|
||||
message is None
|
||||
or isinstance(message, InaccessibleMessage)
|
||||
):
|
||||
return None
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def _monitoring_keyboard() -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="💼 Портфель", callback_data="monitoring:portfolio")
|
||||
@@ -31,10 +50,16 @@ def _monitoring_text() -> str:
|
||||
|
||||
|
||||
def _register_monitoring_screen(message: Message) -> None:
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
LiveScreenRunner.unregister_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
)
|
||||
|
||||
ScreenRegistry.unregister_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
@@ -43,23 +68,65 @@ def _register_monitoring_screen(message: Message) -> None:
|
||||
ScreenRegistry.register_screen(
|
||||
StaticScreen(
|
||||
screen="monitoring",
|
||||
bot=message.bot,
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.message(F.text == "📊 Мониторинг")
|
||||
async def open_monitoring(message: Message, state: FSMContext) -> None:
|
||||
await state.clear()
|
||||
async def _prepare_monitoring_from_message(
|
||||
message: Message,
|
||||
) -> bool:
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="monitoring",
|
||||
bot=message.bot,
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _prepare_monitoring_from_callback(
|
||||
callback: CallbackQuery,
|
||||
) -> bool:
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return False
|
||||
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
await callback.answer("Bot недоступен", show_alert=True)
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="monitoring",
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
keep_message_id=message.message_id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@router.message(F.text == "📊 Мониторинг")
|
||||
async def open_monitoring(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
|
||||
if not await _prepare_monitoring_from_message(message):
|
||||
return
|
||||
|
||||
sent_message = await message.answer(
|
||||
_monitoring_text(),
|
||||
reply_markup=_monitoring_keyboard(),
|
||||
@@ -74,30 +141,31 @@ async def open_monitoring(message: Message, state: FSMContext) -> None:
|
||||
|
||||
|
||||
@router.callback_query(F.data == "monitoring:home")
|
||||
async def open_monitoring_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
async def open_monitoring_callback(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
if not await _prepare_monitoring_from_callback(callback):
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="monitoring",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
message = _require_message(callback)
|
||||
|
||||
await callback.message.edit_text(
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
await message.edit_text(
|
||||
_monitoring_text(),
|
||||
reply_markup=_monitoring_keyboard(),
|
||||
)
|
||||
|
||||
_register_monitoring_screen(callback.message)
|
||||
_register_monitoring_screen(message)
|
||||
|
||||
ActiveScreenManager.register(
|
||||
screen="monitoring",
|
||||
message=callback.message,
|
||||
message=message,
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
@@ -4,9 +4,16 @@ from __future__ import annotations
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
|
||||
from aiogram.types import (
|
||||
CallbackQuery,
|
||||
InaccessibleMessage,
|
||||
InlineKeyboardMarkup,
|
||||
Message,
|
||||
)
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from src.core.numbers import safe_float
|
||||
from src.core.types import JsonDict, NumericLike
|
||||
from src.integrations.exchange.exceptions import ExchangeError
|
||||
from src.integrations.exchange.models import BalanceSummary
|
||||
from src.integrations.exchange.service import ExchangeService
|
||||
@@ -39,13 +46,32 @@ PINNED_ORDER = {
|
||||
}
|
||||
|
||||
|
||||
def _compact_amount(currency: str, value: float) -> str:
|
||||
def _require_message(
|
||||
callback: CallbackQuery,
|
||||
) -> Message | None:
|
||||
message = callback.message
|
||||
|
||||
if (
|
||||
message is None
|
||||
or isinstance(message, InaccessibleMessage)
|
||||
):
|
||||
return None
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def _payload(**values: object) -> JsonDict:
|
||||
return dict(values)
|
||||
|
||||
|
||||
def _compact_amount(currency: str, value: NumericLike) -> str:
|
||||
number = safe_float(value) or 0.0
|
||||
currency = currency.upper()
|
||||
|
||||
if currency in {"USD", "USDT", "EUR"}:
|
||||
return format_usd_amount(value)
|
||||
return format_usd_amount(number)
|
||||
|
||||
text = f"{value:.8f}"
|
||||
text = f"{number:.8f}"
|
||||
|
||||
if "." in text:
|
||||
integer, frac = text.split(".")
|
||||
@@ -103,7 +129,11 @@ def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
|
||||
)
|
||||
return text, _portfolio_keyboard()
|
||||
|
||||
visible_balances = [item for item in balances if not is_zero_balance(item)]
|
||||
visible_balances = [
|
||||
item
|
||||
for item in balances
|
||||
if not is_zero_balance(item)
|
||||
]
|
||||
visible_balances = sort_balances(visible_balances)
|
||||
|
||||
if not visible_balances:
|
||||
@@ -128,8 +158,13 @@ def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
|
||||
|
||||
for item in visible_balances:
|
||||
currency = item.currency.upper()
|
||||
total = balance_total(item)
|
||||
estimated_usd = estimate_balance_usd(item, exchange_service, price_cache)
|
||||
total = safe_float(balance_total(item)) or 0.0
|
||||
locked = safe_float(item.locked) or 0.0
|
||||
estimated_usd = estimate_balance_usd(
|
||||
item,
|
||||
exchange_service,
|
||||
price_cache,
|
||||
)
|
||||
|
||||
if estimated_usd is not None:
|
||||
total_estimated_usd += estimated_usd
|
||||
@@ -139,8 +174,8 @@ def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
|
||||
|
||||
line = f"{currency}: {_compact_amount(currency, total)}"
|
||||
|
||||
if item.locked > 0:
|
||||
line += f" · locked {_compact_amount(currency, item.locked)}"
|
||||
if locked > 0:
|
||||
line += f" · locked {_compact_amount(currency, locked)}"
|
||||
|
||||
if estimated_usd is not None and currency not in {"USD", "USDT"}:
|
||||
line += f" ≈ $ {format_usd_amount(estimated_usd)}"
|
||||
@@ -157,7 +192,10 @@ def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
|
||||
|
||||
if has_any_estimate:
|
||||
lines.insert(3, "")
|
||||
lines.insert(3, f"Оценка: <b>≈ $ {format_usd_amount(total_estimated_usd)}</b>")
|
||||
lines.insert(
|
||||
3,
|
||||
f"Оценка: <b>≈ $ {format_usd_amount(total_estimated_usd)}</b>",
|
||||
)
|
||||
|
||||
if missing_estimate_assets:
|
||||
lines.append(f"Нет оценки: {', '.join(missing_estimate_assets)}")
|
||||
@@ -184,10 +222,16 @@ def _portfolio_live_markup() -> InlineKeyboardMarkup:
|
||||
|
||||
|
||||
def _register_portfolio_live_screen(message: Message) -> None:
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
LiveScreenRunner.unregister_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
)
|
||||
|
||||
ScreenRegistry.unregister_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
@@ -196,7 +240,7 @@ def _register_portfolio_live_screen(message: Message) -> None:
|
||||
LiveScreenRunner.register_screen(
|
||||
LiveScreen(
|
||||
screen="portfolio",
|
||||
bot=message.bot,
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
render_text=_portfolio_live_text,
|
||||
@@ -204,9 +248,52 @@ def _register_portfolio_live_screen(message: Message) -> None:
|
||||
interval_seconds=10,
|
||||
)
|
||||
)
|
||||
|
||||
LiveScreenRunner.start("portfolio")
|
||||
|
||||
|
||||
async def _prepare_portfolio_from_message(
|
||||
message: Message,
|
||||
) -> bool:
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="portfolio",
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _prepare_portfolio_from_callback(
|
||||
callback: CallbackQuery,
|
||||
) -> bool:
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return False
|
||||
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
await callback.answer("Bot недоступен", show_alert=True)
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="portfolio",
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
keep_message_id=message.message_id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _render_portfolio_screen(
|
||||
target_message: Message,
|
||||
*,
|
||||
@@ -241,24 +328,25 @@ async def _render_portfolio_screen(
|
||||
await target_message.edit_text(text, reply_markup=reply_markup)
|
||||
_register_portfolio_live_screen(target_message)
|
||||
ActiveScreenManager.register(screen="portfolio", message=target_message)
|
||||
else:
|
||||
sent_message = await target_message.answer(text, reply_markup=reply_markup)
|
||||
_register_portfolio_live_screen(sent_message)
|
||||
ActiveScreenManager.register(screen="portfolio", message=sent_message)
|
||||
return
|
||||
|
||||
sent_message = await target_message.answer(text, reply_markup=reply_markup)
|
||||
_register_portfolio_live_screen(sent_message)
|
||||
ActiveScreenManager.register(screen="portfolio", message=sent_message)
|
||||
|
||||
|
||||
@router.message(F.text == "💼 Портфель")
|
||||
async def open_portfolio(message: Message, state: FSMContext) -> None:
|
||||
async def open_portfolio(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="portfolio",
|
||||
bot=message.bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
if not await _prepare_portfolio_from_message(message):
|
||||
return
|
||||
|
||||
user_id = message.from_user.id if message.from_user else None
|
||||
chat_id = message.chat.id if message.chat else None
|
||||
chat_id = message.chat.id
|
||||
|
||||
try:
|
||||
await _render_portfolio_screen(
|
||||
@@ -291,32 +379,34 @@ async def open_portfolio(message: Message, state: FSMContext) -> None:
|
||||
|
||||
|
||||
@router.callback_query(F.data == "monitoring:portfolio")
|
||||
async def open_portfolio_from_monitoring(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
async def open_portfolio_from_monitoring(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
if not await _prepare_portfolio_from_callback(callback):
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="portfolio",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
user_id = callback.from_user.id if callback.from_user else None
|
||||
chat_id = callback.message.chat.id if callback.message.chat else None
|
||||
chat_id = message.chat.id
|
||||
|
||||
try:
|
||||
await _render_portfolio_screen(
|
||||
callback.message,
|
||||
message,
|
||||
user_id=user_id,
|
||||
chat_id=chat_id,
|
||||
edit_mode=True,
|
||||
action="open_from_monitoring",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
except ExchangeError as exc:
|
||||
JournalService().log_ui_error(
|
||||
event_type="portfolio_open_error",
|
||||
@@ -340,32 +430,34 @@ async def open_portfolio_from_monitoring(callback: CallbackQuery, state: FSMCont
|
||||
|
||||
|
||||
@router.callback_query(F.data == "portfolio:retry")
|
||||
async def retry_portfolio(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
async def retry_portfolio(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
if not await _prepare_portfolio_from_callback(callback):
|
||||
return
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen="portfolio",
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
user_id = callback.from_user.id if callback.from_user else None
|
||||
chat_id = callback.message.chat.id if callback.message.chat else None
|
||||
chat_id = message.chat.id
|
||||
|
||||
try:
|
||||
await _render_portfolio_screen(
|
||||
callback.message,
|
||||
message,
|
||||
user_id=user_id,
|
||||
chat_id=chat_id,
|
||||
edit_mode=True,
|
||||
action="retry",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
except ExchangeError as exc:
|
||||
JournalService().log_ui_error(
|
||||
event_type="portfolio_retry_error",
|
||||
|
||||
@@ -15,30 +15,42 @@ from src.telegram.menus import MAIN_MENU_TEXT
|
||||
router = Router(name="start")
|
||||
|
||||
|
||||
@router.message(Command("start"))
|
||||
async def cmd_start(message: Message, state: FSMContext) -> None:
|
||||
# Глобальный экран: всегда выходим из текущего FSM-сценария.
|
||||
await state.clear()
|
||||
async def _show_main_menu(
|
||||
message: Message,
|
||||
) -> None:
|
||||
await message.answer(
|
||||
MAIN_MENU_TEXT,
|
||||
reply_markup=build_main_menu_keyboard(),
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("start"))
|
||||
async def cmd_start(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
|
||||
await _show_main_menu(message)
|
||||
|
||||
|
||||
@router.message(Command("menu"))
|
||||
async def cmd_menu(message: Message, state: FSMContext) -> None:
|
||||
# Глобальный экран: всегда выходим из текущего FSM-сценария.
|
||||
async def cmd_menu(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
MAIN_MENU_TEXT,
|
||||
reply_markup=build_main_menu_keyboard(),
|
||||
)
|
||||
|
||||
await _show_main_menu(message)
|
||||
|
||||
|
||||
@router.message(Command("help"))
|
||||
async def cmd_help(message: Message, state: FSMContext) -> None:
|
||||
# Глобальный экран: всегда выходим из текущего FSM-сценария.
|
||||
async def cmd_help(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
build_system_text(),
|
||||
reply_markup=build_main_menu_keyboard(),
|
||||
@@ -46,10 +58,10 @@ async def cmd_help(message: Message, state: FSMContext) -> None:
|
||||
|
||||
|
||||
@router.message(F.text == "Меню")
|
||||
async def menu_shortcut(message: Message, state: FSMContext) -> None:
|
||||
# Глобальный экран: всегда выходим из текущего FSM-сценария.
|
||||
async def menu_shortcut(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
MAIN_MENU_TEXT,
|
||||
reply_markup=build_main_menu_keyboard(),
|
||||
)
|
||||
|
||||
await _show_main_menu(message)
|
||||
@@ -4,9 +4,11 @@ from __future__ import annotations
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message, InaccessibleMessage
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from src.core.numbers import safe_float
|
||||
from src.core.types import JsonDict, NumericLike
|
||||
from src.core.config import load_settings
|
||||
from src.core.constants import APP_NAME, APP_VERSION
|
||||
from src.core.system_status import build_system_text, get_system_snapshot, has_system_alerts
|
||||
@@ -19,6 +21,20 @@ from src.trading.journal.service import JournalService
|
||||
router = Router(name="system")
|
||||
|
||||
|
||||
def _require_message(
|
||||
callback: CallbackQuery,
|
||||
) -> Message | None:
|
||||
message = callback.message
|
||||
|
||||
if (
|
||||
message is None
|
||||
or isinstance(message, InaccessibleMessage)
|
||||
):
|
||||
return None
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def _system_keyboard() -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🛠️ Настройки", callback_data="system:management")
|
||||
@@ -37,6 +53,11 @@ def _system_alert_keyboard() -> InlineKeyboardMarkup:
|
||||
|
||||
|
||||
def _register_system_screen(message: Message, screen: str = "system") -> None:
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
LiveScreenRunner.unregister_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
@@ -49,7 +70,7 @@ def _register_system_screen(message: Message, screen: str = "system") -> None:
|
||||
ScreenRegistry.register_screen(
|
||||
StaticScreen(
|
||||
screen=screen,
|
||||
bot=message.bot,
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
message_id=message.message_id,
|
||||
)
|
||||
@@ -61,27 +82,42 @@ def _register_system_screen(message: Message, screen: str = "system") -> None:
|
||||
)
|
||||
|
||||
|
||||
async def _prepare_system_from_message(message: Message, screen: str = "system") -> None:
|
||||
async def _prepare_system_from_message(message: Message, screen: str = "system") -> bool:
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen=screen,
|
||||
bot=message.bot,
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _prepare_system_from_callback(
|
||||
callback: CallbackQuery,
|
||||
screen: str = "system",
|
||||
) -> bool:
|
||||
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 False
|
||||
|
||||
bot = message.bot
|
||||
|
||||
if bot is None:
|
||||
await callback.answer("Bot недоступен", show_alert=True)
|
||||
return False
|
||||
|
||||
await ActiveScreenManager.prepare_new_screen(
|
||||
screen=screen,
|
||||
bot=callback.message.bot,
|
||||
chat_id=callback.message.chat.id,
|
||||
keep_message_id=callback.message.message_id,
|
||||
bot=bot,
|
||||
chat_id=message.chat.id,
|
||||
keep_message_id=message.message_id,
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -148,7 +184,8 @@ async def _render_system_screen(
|
||||
async def open_system(message: Message, state: FSMContext) -> None:
|
||||
await state.clear()
|
||||
|
||||
await _prepare_system_from_message(message, screen="system")
|
||||
if not await _prepare_system_from_message(message, screen="system"):
|
||||
return
|
||||
|
||||
user_id = message.from_user.id if message.from_user else None
|
||||
chat_id = message.chat.id if message.chat else None
|
||||
@@ -169,16 +206,23 @@ async def retry_system(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="system"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
user_id = callback.from_user.id if callback.from_user else None
|
||||
chat_id = callback.message.chat.id if callback.message.chat else None
|
||||
chat_id = message.chat.id
|
||||
|
||||
await _render_system_screen(
|
||||
callback.message,
|
||||
message,
|
||||
edit_mode=True,
|
||||
user_id=user_id,
|
||||
chat_id=chat_id,
|
||||
action="retry",
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -201,8 +245,14 @@ async def open_system_management(callback: CallbackQuery) -> None:
|
||||
builder.button(text="⬅️ Назад", callback_data="system:back")
|
||||
builder.adjust(2, 2, 1)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="system")
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="system")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -225,7 +275,11 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
||||
leverage_ready = state.leverage is not None
|
||||
|
||||
is_trend_strategy = (state.strategy or "").upper() == "TREND"
|
||||
sl_ready = state.stop_loss_percent is not None and state.stop_loss_percent > 0
|
||||
|
||||
sl_ready = (
|
||||
state.stop_loss_percent is not None
|
||||
and state.stop_loss_percent > 0
|
||||
)
|
||||
|
||||
is_configured = (
|
||||
strategy_ready
|
||||
@@ -248,67 +302,136 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
||||
if base.endswith(suffix) and len(base) > len(suffix):
|
||||
base = base[: -len(suffix)]
|
||||
break
|
||||
|
||||
symbol = base
|
||||
|
||||
risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "—"
|
||||
leverage = f"x{state.leverage:g}" if state.leverage is not None else "—"
|
||||
max_reserved = (
|
||||
f"{state.max_reserved_balance_percent:g}%"
|
||||
if state.max_reserved_balance_percent is not None
|
||||
else "off"
|
||||
)
|
||||
sl = f"{state.stop_loss_percent:g}%" if state.stop_loss_percent is not None else "off"
|
||||
tp = f"{state.take_profit_percent:g}%" if state.take_profit_percent is not None else "off"
|
||||
ml = f"{state.max_loss_usd:g} USD" if state.max_loss_usd is not None else "off"
|
||||
risk = _format_number(state.risk_percent, suffix="%", default="—")
|
||||
|
||||
leverage_value = safe_float(state.leverage)
|
||||
leverage = f"x{leverage_value:g}" if leverage_value is not None else "—"
|
||||
|
||||
max_reserved = _format_percent_setting(state.max_reserved_balance_percent)
|
||||
|
||||
sl = _format_percent_setting(state.stop_loss_percent)
|
||||
|
||||
tp = _format_percent_setting(state.take_profit_percent)
|
||||
|
||||
ml_value = safe_float(state.max_loss_usd)
|
||||
ml = f"{ml_value:g} USD" if ml_value is not None else "off"
|
||||
|
||||
strategy_icon = "✅" if strategy_ready else "⚠️"
|
||||
symbol_icon = "✅" if symbol_ready else "⚠️"
|
||||
risk_icon = "✅" if risk_ready else "⚠️"
|
||||
leverage_icon = "✅" if leverage_ready else "⚠️"
|
||||
sl_icon = "✅" if sl_ready else "⚠️"
|
||||
|
||||
if is_trend_strategy:
|
||||
risk_controls_block = (
|
||||
"<b>Защита позиции:</b>\n"
|
||||
f"{sl_icon} Stop Loss · <b>{'required' if not sl_ready else sl}</b>\n"
|
||||
f"✅ Take Profit · {tp}\n"
|
||||
f"✅ Max Loss · {ml}"
|
||||
)
|
||||
if is_trend_strategy and not sl_ready:
|
||||
sl_icon = "⛔️"
|
||||
else:
|
||||
risk_controls_block = (
|
||||
"<b>Защита позиции:</b>\n"
|
||||
f"✅ Stop Loss · {sl}\n"
|
||||
f"✅ Take Profit · {tp}\n"
|
||||
f"✅ Max Loss · {ml}"
|
||||
sl_icon = (
|
||||
"✅"
|
||||
if state.stop_loss_percent is not None
|
||||
else "⚠️"
|
||||
)
|
||||
|
||||
config_status = "✅ Все параметры настроены" if is_configured else "⚠️ Настрой все параметры"
|
||||
tp_icon = (
|
||||
"✅"
|
||||
if state.take_profit_percent is not None
|
||||
else "⚠️"
|
||||
)
|
||||
|
||||
ml_icon = (
|
||||
"✅"
|
||||
if state.max_loss_usd is not None
|
||||
else "⚠️"
|
||||
)
|
||||
|
||||
risk_controls_block = (
|
||||
"<b>Защита позиции:</b>\n"
|
||||
f"{sl_icon} Stop Loss · {sl}\n"
|
||||
f"{tp_icon} Take Profit · {tp}\n"
|
||||
f"{ml_icon} Max Loss · {ml}"
|
||||
)
|
||||
|
||||
settings_status_icon = "✅" if is_configured else "⛔️"
|
||||
|
||||
config_status = (
|
||||
""
|
||||
if is_configured
|
||||
else "\n\nНастрой все параметры"
|
||||
)
|
||||
|
||||
text = (
|
||||
"<b>🤖 Автоторговля</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки\n\n"
|
||||
f"<b>СИСТЕМА</b> · Настройки {settings_status_icon}\n\n"
|
||||
f"{strategy_icon} Стратегия: <b>{strategy}</b>\n"
|
||||
f"{symbol_icon} Актив: <b>{symbol}</b>\n"
|
||||
f"{risk_icon} Риск на сделку: <b>{risk}</b>\n"
|
||||
f"{leverage_icon} Плечо: <b>{leverage}</b>\n\n"
|
||||
f"✅ Лимит на сделку: <b>{max_reserved}</b>\n\n"
|
||||
f"{risk_controls_block}\n\n"
|
||||
f"{risk_controls_block}"
|
||||
f"{config_status}"
|
||||
)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🧠 Стратегия", callback_data="settings:auto_strategy")
|
||||
builder.button(text="💱 Актив", callback_data="settings:auto_symbol")
|
||||
builder.button(text="⚙️ Плечо", callback_data="settings:auto_leverage")
|
||||
builder.button(text="🏦 Лимит", callback_data="settings:auto_max_reserved")
|
||||
builder.button(text="🛡️ Риск", callback_data="settings:auto_risk")
|
||||
builder.button(text="🧯 Защита", callback_data="auto:risk")
|
||||
builder.button(text="🤖 Автоторговля", callback_data="auto:home")
|
||||
builder.button(text="⬅️ Назад", callback_data="system:management")
|
||||
|
||||
builder.button(
|
||||
text="🧠 Стратегия",
|
||||
callback_data="settings:auto_strategy",
|
||||
)
|
||||
|
||||
builder.button(
|
||||
text="💱 Актив",
|
||||
callback_data="settings:auto_symbol",
|
||||
)
|
||||
|
||||
builder.button(
|
||||
text="⚙️ Плечо",
|
||||
callback_data="settings:auto_leverage",
|
||||
)
|
||||
|
||||
builder.button(
|
||||
text="🏦 Лимит",
|
||||
callback_data="settings:auto_max_reserved",
|
||||
)
|
||||
|
||||
builder.button(
|
||||
text="🛡️ Риск",
|
||||
callback_data="settings:auto_risk",
|
||||
)
|
||||
|
||||
builder.button(
|
||||
text="🧯 Защита",
|
||||
callback_data="auto:risk",
|
||||
)
|
||||
|
||||
builder.button(
|
||||
text="🤖 Автоторговля",
|
||||
callback_data="auto:home",
|
||||
)
|
||||
|
||||
builder.button(
|
||||
text="⬅️ Назад",
|
||||
callback_data="system:management",
|
||||
)
|
||||
|
||||
builder.adjust(2, 2, 2, 2)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="settings_auto")
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
await message.edit_text(
|
||||
text,
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
_register_system_screen(
|
||||
message,
|
||||
screen="settings_auto",
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -316,6 +439,12 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
||||
async def open_auto_strategy_settings(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"<b>🧠 Стратегия</b>\n\n"
|
||||
@@ -330,8 +459,8 @@ async def open_auto_strategy_settings(callback: CallbackQuery) -> None:
|
||||
builder.button(text="⬅️ Назад", callback_data="settings:auto")
|
||||
builder.adjust(3, 1)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="settings_auto")
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="settings_auto")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -340,7 +469,7 @@ def _log_auto_setting_updated(
|
||||
event_type: str = "auto_settings_updated",
|
||||
message: str,
|
||||
action: str,
|
||||
payload: dict,
|
||||
payload: JsonDict,
|
||||
) -> None:
|
||||
try:
|
||||
JournalService().log_ui_info(
|
||||
@@ -370,9 +499,38 @@ def _human_symbol(symbol: str | None) -> str:
|
||||
return base
|
||||
|
||||
|
||||
def _format_number(
|
||||
value: NumericLike | None,
|
||||
*,
|
||||
suffix: str = "",
|
||||
default: str = "—",
|
||||
) -> str:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return default
|
||||
|
||||
return f"{number:g}{suffix}"
|
||||
|
||||
|
||||
def _format_percent_setting(value: NumericLike | None) -> str:
|
||||
return _format_number(value, suffix="%", default="off")
|
||||
|
||||
|
||||
def _parse_callback_float(value: object) -> float | None:
|
||||
return safe_float(value)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("settings:auto_strategy:"))
|
||||
async def set_auto_strategy(callback: CallbackQuery) -> None:
|
||||
strategy = callback.data.split(":", 2)[2].upper()
|
||||
data = callback.data or ""
|
||||
parts = data.split(":", 2)
|
||||
|
||||
if len(parts) < 3:
|
||||
await callback.answer("Некорректное значение стратегии", show_alert=True)
|
||||
return
|
||||
|
||||
strategy = parts[2].upper()
|
||||
|
||||
service = AutoTradeService()
|
||||
state = service.get_state()
|
||||
@@ -399,6 +557,12 @@ async def open_auto_symbol_settings(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"<b>💱 Актив</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||
@@ -413,14 +577,21 @@ async def open_auto_symbol_settings(callback: CallbackQuery) -> None:
|
||||
builder.button(text="⬅️ Назад", callback_data="settings:auto")
|
||||
builder.adjust(2, 2, 1)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="settings_auto")
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="settings_auto")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("settings:auto_symbol:"))
|
||||
async def set_auto_symbol(callback: CallbackQuery) -> None:
|
||||
symbol = callback.data.split(":", 2)[2]
|
||||
data = callback.data or ""
|
||||
parts = data.split(":", 2)
|
||||
|
||||
if len(parts) < 3:
|
||||
await callback.answer("Некорректное значение актива", show_alert=True)
|
||||
return
|
||||
|
||||
symbol = parts[2]
|
||||
|
||||
service = AutoTradeService()
|
||||
state = service.get_state()
|
||||
@@ -447,6 +618,12 @@ async def open_auto_risk_settings(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"<b>🛡️ Риск на сделку</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||
@@ -460,14 +637,25 @@ async def open_auto_risk_settings(callback: CallbackQuery) -> None:
|
||||
builder.button(text="⬅️ Назад", callback_data="settings:auto")
|
||||
builder.adjust(3, 1)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="settings_auto")
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="settings_auto")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("settings:auto_risk:"))
|
||||
async def set_auto_risk(callback: CallbackQuery) -> None:
|
||||
risk = float(callback.data.split(":", 2)[2])
|
||||
data = callback.data or ""
|
||||
parts = data.split(":", 2)
|
||||
|
||||
if len(parts) < 3:
|
||||
await callback.answer("Некорректное значение риска", show_alert=True)
|
||||
return
|
||||
|
||||
risk = _parse_callback_float(parts[2])
|
||||
|
||||
if risk is None:
|
||||
await callback.answer("Некорректное значение риска", show_alert=True)
|
||||
return
|
||||
|
||||
service = AutoTradeService()
|
||||
state = service.get_state()
|
||||
@@ -494,6 +682,12 @@ async def open_auto_leverage_settings(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"<b>⚙️ Плечо</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||
@@ -510,14 +704,25 @@ async def open_auto_leverage_settings(callback: CallbackQuery) -> None:
|
||||
builder.button(text="⬅️ Назад", callback_data="settings:auto")
|
||||
builder.adjust(3, 3, 1)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="settings_auto")
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="settings_auto")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("settings:auto_leverage:"))
|
||||
async def set_auto_leverage(callback: CallbackQuery) -> None:
|
||||
leverage = float(callback.data.split(":", 2)[2])
|
||||
data = callback.data or ""
|
||||
parts = data.split(":", 2)
|
||||
|
||||
if len(parts) < 3:
|
||||
await callback.answer("Некорректное значение плеча", show_alert=True)
|
||||
return
|
||||
|
||||
leverage = _parse_callback_float(parts[2])
|
||||
|
||||
if leverage is None:
|
||||
await callback.answer("Некорректное значение плеча", show_alert=True)
|
||||
return
|
||||
|
||||
service = AutoTradeService()
|
||||
state = service.get_state()
|
||||
@@ -544,6 +749,12 @@ async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"<b>🏦 Лимит на сделку</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||
@@ -559,15 +770,26 @@ async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None:
|
||||
builder.button(text="⬅️ Назад", callback_data="settings:auto")
|
||||
builder.adjust(2, 2, 1, 1)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="settings_auto")
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="settings_auto")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("settings:auto_max_reserved:"))
|
||||
async def set_auto_max_reserved(callback: CallbackQuery) -> None:
|
||||
raw_value = callback.data.split(":", 2)[2]
|
||||
value = None if raw_value == "off" else float(raw_value)
|
||||
data = callback.data or ""
|
||||
parts = data.split(":", 2)
|
||||
|
||||
if len(parts) < 3:
|
||||
await callback.answer("Некорректные данные кнопки", show_alert=True)
|
||||
return
|
||||
|
||||
raw_value = parts[2]
|
||||
value = None if raw_value == "off" else _parse_callback_float(raw_value)
|
||||
|
||||
if raw_value != "off" and value is None:
|
||||
await callback.answer("Некорректное значение лимита", show_alert=True)
|
||||
return
|
||||
|
||||
service = AutoTradeService()
|
||||
state = service.get_state()
|
||||
@@ -596,6 +818,12 @@ async def open_trade_settings(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="settings_trade"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"<b>💹 Торговля</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки\n\n"
|
||||
@@ -610,8 +838,8 @@ async def open_trade_settings(callback: CallbackQuery) -> None:
|
||||
builder.button(text="💹 Торговля", callback_data="trade:home")
|
||||
builder.adjust(2)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="settings_trade")
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="settings_trade")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -620,6 +848,12 @@ async def open_general_settings(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="settings_general"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"<b>🌍 Общие</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки\n\n"
|
||||
@@ -633,8 +867,8 @@ async def open_general_settings(callback: CallbackQuery) -> None:
|
||||
builder.button(text="⬅️ Назад", callback_data="system:management")
|
||||
builder.adjust(1)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="settings_general")
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="settings_general")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -643,6 +877,12 @@ async def open_journal_settings(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="settings_journal"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
service = JournalService()
|
||||
total = service.get_total_count()
|
||||
|
||||
@@ -664,8 +904,8 @@ async def open_journal_settings(callback: CallbackQuery) -> None:
|
||||
builder.button(text="📒 Журнал", callback_data="journal:1")
|
||||
builder.adjust(2, 2, 2)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="settings_journal")
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="settings_journal")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -674,6 +914,12 @@ async def open_journal_archive_settings(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="settings_journal"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"<b>🗄 Архив</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Журнал\n\n"
|
||||
@@ -688,8 +934,8 @@ async def open_journal_archive_settings(callback: CallbackQuery) -> None:
|
||||
builder.button(text="📒 Журнал", callback_data="journal:1")
|
||||
builder.adjust(2)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="settings_journal")
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="settings_journal")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -698,6 +944,12 @@ async def open_journal_limit_settings(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="settings_journal"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"<b>📦 Лимит</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Журнал\n\n"
|
||||
@@ -713,8 +965,8 @@ async def open_journal_limit_settings(callback: CallbackQuery) -> None:
|
||||
builder.button(text="⬅️ Назад", callback_data="settings:journal")
|
||||
builder.adjust(2, 2, 1)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="settings_journal")
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="settings_journal")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -723,6 +975,12 @@ async def open_journal_retention_settings(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="settings_journal"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
text = (
|
||||
"<b>⏳ Хранение</b>\n\n"
|
||||
"<b>СИСТЕМА</b> · Настройки · Журнал\n\n"
|
||||
@@ -738,8 +996,8 @@ async def open_journal_retention_settings(callback: CallbackQuery) -> None:
|
||||
builder.button(text="⬅️ Назад", callback_data="settings:journal")
|
||||
builder.adjust(2, 2, 1)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="settings_journal")
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="settings_journal")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -756,11 +1014,17 @@ async def back_to_system(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="system"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
user_id = callback.from_user.id if callback.from_user else None
|
||||
chat_id = callback.message.chat.id if callback.message.chat else None
|
||||
chat_id = message.chat.id
|
||||
|
||||
await _render_system_screen(
|
||||
callback.message,
|
||||
message,
|
||||
edit_mode=True,
|
||||
user_id=user_id,
|
||||
chat_id=chat_id,
|
||||
@@ -774,6 +1038,12 @@ async def open_system_about(callback: CallbackQuery) -> None:
|
||||
if not await _prepare_system_from_callback(callback, screen="system_about"):
|
||||
return
|
||||
|
||||
message = _require_message(callback)
|
||||
|
||||
if message is None:
|
||||
await callback.answer("Сообщение недоступно", show_alert=True)
|
||||
return
|
||||
|
||||
settings = load_settings()
|
||||
journal = JournalService()
|
||||
|
||||
@@ -783,7 +1053,7 @@ async def open_system_about(callback: CallbackQuery) -> None:
|
||||
screen="system",
|
||||
action="about",
|
||||
user_id=callback.from_user.id if callback.from_user else None,
|
||||
chat_id=callback.message.chat.id if callback.message.chat else None,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
|
||||
text = (
|
||||
@@ -801,6 +1071,6 @@ async def open_system_about(callback: CallbackQuery) -> None:
|
||||
builder.button(text="⬅️ Назад", callback_data="system:back")
|
||||
builder.adjust(1)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(callback.message, screen="system_about")
|
||||
await message.edit_text(text, reply_markup=builder.as_markup())
|
||||
_register_system_screen(message, screen="system_about")
|
||||
await callback.answer()
|
||||
@@ -4,9 +4,10 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
from collections.abc import Callable
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
|
||||
@@ -17,7 +18,7 @@ class LiveScreen:
|
||||
chat_id: int
|
||||
message_id: int
|
||||
render_text: Callable[[], str]
|
||||
render_markup: Callable[[], object]
|
||||
render_markup: Callable[[], InlineKeyboardMarkup | None]
|
||||
interval_seconds: int = 5
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user