07.4.3.17 — Unified Active Screen Lifecycle
This commit is contained in:
@@ -13,6 +13,7 @@ from src.telegram.handlers.auto.ui import (
|
|||||||
is_auto_configured,
|
is_auto_configured,
|
||||||
)
|
)
|
||||||
from src.telegram.handlers.system import open_auto_settings
|
from src.telegram.handlers.system import open_auto_settings
|
||||||
|
from src.telegram.live.active_screen import ActiveScreenManager
|
||||||
from src.trading.auto.runner import AutoTradeRunner
|
from src.trading.auto.runner import AutoTradeRunner
|
||||||
from src.trading.auto.service import AutoTradeService
|
from src.trading.auto.service import AutoTradeService
|
||||||
|
|
||||||
@@ -41,6 +42,11 @@ async def render_auto_screen(
|
|||||||
render_text=build_auto_text,
|
render_text=build_auto_text,
|
||||||
render_markup=auto_keyboard,
|
render_markup=auto_keyboard,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ActiveScreenManager.register(
|
||||||
|
screen="auto",
|
||||||
|
message=target_message,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
sent_message = await target_message.answer(text, reply_markup=auto_keyboard())
|
sent_message = await target_message.answer(text, reply_markup=auto_keyboard())
|
||||||
@@ -53,17 +59,40 @@ async def render_auto_screen(
|
|||||||
render_markup=auto_keyboard,
|
render_markup=auto_keyboard,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ActiveScreenManager.register(
|
||||||
|
screen="auto",
|
||||||
|
message=sent_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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_callback(callback: CallbackQuery) -> bool:
|
||||||
|
if callback.message is None:
|
||||||
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@router.message(F.text.in_({"🤖 Автоторговля", "🤖 Авто"}))
|
@router.message(F.text.in_({"🤖 Автоторговля", "🤖 Авто"}))
|
||||||
async def open_auto(message: Message, state: FSMContext) -> None:
|
async def open_auto(message: Message, state: FSMContext) -> None:
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
AutoTradeRunner.set_current_screen("auto")
|
await _prepare_auto_from_message(message)
|
||||||
|
|
||||||
await AutoTradeRunner.delete_registered_screen(
|
|
||||||
bot=message.bot,
|
|
||||||
chat_id=message.chat.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
await render_auto_screen(message, edit_mode=False)
|
await render_auto_screen(message, edit_mode=False)
|
||||||
|
|
||||||
@@ -72,11 +101,9 @@ async def open_auto(message: Message, state: FSMContext) -> None:
|
|||||||
async def open_auto_from_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
async def open_auto_from_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
if callback.message is None:
|
if not await _prepare_auto_from_callback(callback):
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
AutoTradeRunner.set_current_screen("auto")
|
|
||||||
await render_auto_screen(callback.message, edit_mode=True)
|
await render_auto_screen(callback.message, edit_mode=True)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
@@ -99,10 +126,10 @@ async def auto_start(callback: CallbackQuery) -> None:
|
|||||||
|
|
||||||
_, message = service.start()
|
_, message = service.start()
|
||||||
|
|
||||||
AutoTradeRunner.set_current_screen("auto")
|
|
||||||
AutoTradeRunner.start()
|
AutoTradeRunner.start()
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
|
await _prepare_auto_from_callback(callback)
|
||||||
await render_auto_screen(callback.message, edit_mode=True)
|
await render_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
await callback.answer(message)
|
await callback.answer(message)
|
||||||
@@ -126,10 +153,10 @@ async def auto_observe(callback: CallbackQuery) -> None:
|
|||||||
|
|
||||||
_, message = service.observe()
|
_, message = service.observe()
|
||||||
|
|
||||||
AutoTradeRunner.set_current_screen("auto")
|
|
||||||
AutoTradeRunner.start()
|
AutoTradeRunner.start()
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
|
await _prepare_auto_from_callback(callback)
|
||||||
await render_auto_screen(callback.message, edit_mode=True)
|
await render_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
await callback.answer(message)
|
await callback.answer(message)
|
||||||
@@ -143,6 +170,7 @@ async def auto_stop(callback: CallbackQuery) -> None:
|
|||||||
AutoTradeRunner.stop()
|
AutoTradeRunner.stop()
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
|
await _prepare_auto_from_callback(callback)
|
||||||
await render_auto_screen(callback.message, edit_mode=True)
|
await render_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
await callback.answer(message)
|
await callback.answer(message)
|
||||||
@@ -13,6 +13,7 @@ from src.telegram.handlers.debug_auto.ui import (
|
|||||||
build_debug_auto_text,
|
build_debug_auto_text,
|
||||||
debug_auto_keyboard,
|
debug_auto_keyboard,
|
||||||
)
|
)
|
||||||
|
from src.telegram.live.active_screen import ActiveScreenManager
|
||||||
from src.trading.debug.runner import DebugTradeRunner
|
from src.trading.debug.runner import DebugTradeRunner
|
||||||
from src.trading.debug.service import DebugTradeService
|
from src.trading.debug.service import DebugTradeService
|
||||||
|
|
||||||
@@ -49,6 +50,11 @@ async def render_debug_auto_screen(
|
|||||||
render_text=build_debug_auto_text,
|
render_text=build_debug_auto_text,
|
||||||
render_markup=debug_auto_keyboard,
|
render_markup=debug_auto_keyboard,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ActiveScreenManager.register(
|
||||||
|
screen="debug_auto",
|
||||||
|
message=target_message,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
sent_message = await target_message.answer(
|
sent_message = await target_message.answer(
|
||||||
@@ -64,17 +70,42 @@ async def render_debug_auto_screen(
|
|||||||
render_markup=debug_auto_keyboard,
|
render_markup=debug_auto_keyboard,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ActiveScreenManager.register(
|
||||||
|
screen="debug_auto",
|
||||||
|
message=sent_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _prepare_debug_auto_from_message(message: Message) -> None:
|
||||||
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
|
screen="debug_auto",
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _prepare_debug_auto_from_callback(callback: CallbackQuery) -> bool:
|
||||||
|
if callback.message is None:
|
||||||
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
|
screen="debug_auto",
|
||||||
|
bot=callback.message.bot,
|
||||||
|
chat_id=callback.message.chat.id,
|
||||||
|
keep_message_id=callback.message.message_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@router.message(F.text.in_({"🧪 Debug Auto", "Debug Auto", "/debug_auto_screen"}))
|
@router.message(F.text.in_({"🧪 Debug Auto", "Debug Auto", "/debug_auto_screen"}))
|
||||||
async def open_debug_auto(message: Message, state: FSMContext) -> None:
|
async def open_debug_auto(message: Message, state: FSMContext) -> None:
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
DebugTradeRunner.set_current_screen("debug_auto")
|
await _prepare_debug_auto_from_message(message)
|
||||||
|
|
||||||
await DebugTradeRunner.delete_registered_screen(
|
DebugTradeRunner.set_current_screen("debug_auto")
|
||||||
bot=message.bot,
|
|
||||||
chat_id=message.chat.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
await render_debug_auto_screen(message, edit_mode=False)
|
await render_debug_auto_screen(message, edit_mode=False)
|
||||||
|
|
||||||
@@ -90,6 +121,7 @@ async def debug_auto_start(callback: CallbackQuery) -> None:
|
|||||||
DebugTradeRunner.start()
|
DebugTradeRunner.start()
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
|
await _prepare_debug_auto_from_callback(callback)
|
||||||
await render_debug_auto_screen(callback.message, edit_mode=True)
|
await render_debug_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
await callback.answer("[DEBUG] Мониторинг запущен.")
|
await callback.answer("[DEBUG] Мониторинг запущен.")
|
||||||
@@ -102,6 +134,7 @@ async def debug_auto_stop(callback: CallbackQuery) -> None:
|
|||||||
DebugTradeService().stop()
|
DebugTradeService().stop()
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
|
await _prepare_debug_auto_from_callback(callback)
|
||||||
await render_debug_auto_screen(callback.message, edit_mode=True)
|
await render_debug_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
await callback.answer("[DEBUG] Мониторинг остановлен.")
|
await callback.answer("[DEBUG] Мониторинг остановлен.")
|
||||||
@@ -116,6 +149,7 @@ async def debug_auto_long(callback: CallbackQuery) -> None:
|
|||||||
DebugTradeRunner.start()
|
DebugTradeRunner.start()
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
|
await _prepare_debug_auto_from_callback(callback)
|
||||||
await render_debug_auto_screen(callback.message, edit_mode=True)
|
await render_debug_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
await callback.answer(result.reason, show_alert=False)
|
await callback.answer(result.reason, show_alert=False)
|
||||||
@@ -130,6 +164,7 @@ async def debug_auto_short(callback: CallbackQuery) -> None:
|
|||||||
DebugTradeRunner.start()
|
DebugTradeRunner.start()
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
|
await _prepare_debug_auto_from_callback(callback)
|
||||||
await render_debug_auto_screen(callback.message, edit_mode=True)
|
await render_debug_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
await callback.answer(result.reason, show_alert=False)
|
await callback.answer(result.reason, show_alert=False)
|
||||||
@@ -144,6 +179,7 @@ async def debug_auto_flip(callback: CallbackQuery) -> None:
|
|||||||
DebugTradeRunner.start()
|
DebugTradeRunner.start()
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
|
await _prepare_debug_auto_from_callback(callback)
|
||||||
await render_debug_auto_screen(callback.message, edit_mode=True)
|
await render_debug_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
await callback.answer(result.reason, show_alert=False)
|
await callback.answer(result.reason, show_alert=False)
|
||||||
@@ -157,6 +193,7 @@ async def debug_auto_close(callback: CallbackQuery) -> None:
|
|||||||
DebugTradeRunner.set_current_screen("debug_auto")
|
DebugTradeRunner.set_current_screen("debug_auto")
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
|
await _prepare_debug_auto_from_callback(callback)
|
||||||
await render_debug_auto_screen(callback.message, edit_mode=True)
|
await render_debug_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
await callback.answer(result.reason, show_alert=False)
|
await callback.answer(result.reason, show_alert=False)
|
||||||
@@ -169,6 +206,7 @@ async def debug_auto_reset(callback: CallbackQuery) -> None:
|
|||||||
_ensure_signal_started_at(state)
|
_ensure_signal_started_at(state)
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
|
await _prepare_debug_auto_from_callback(callback)
|
||||||
await render_debug_auto_screen(callback.message, edit_mode=True)
|
await render_debug_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
await callback.answer("[DEBUG] Runtime reset.")
|
await callback.answer("[DEBUG] Runtime reset.")
|
||||||
@@ -4,6 +4,7 @@ from aiogram import F, Router
|
|||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.live.active_screen import ActiveScreenManager
|
||||||
from src.telegram.menus import HOME_TEXT
|
from src.telegram.menus import HOME_TEXT
|
||||||
|
|
||||||
|
|
||||||
@@ -12,7 +13,17 @@ router = Router(name="home")
|
|||||||
|
|
||||||
@router.message(F.text == "🏠 Главная")
|
@router.message(F.text == "🏠 Главная")
|
||||||
async def open_home(message: Message, state: FSMContext) -> None:
|
async def open_home(message: Message, state: FSMContext) -> None:
|
||||||
# Глобальный экран: всегда выходим из текущего FSM-сценария.
|
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
await message.answer(HOME_TEXT)
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
|
screen="home",
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
sent_message = await message.answer(HOME_TEXT)
|
||||||
|
|
||||||
|
ActiveScreenManager.register(
|
||||||
|
screen="home",
|
||||||
|
message=sent_message,
|
||||||
|
)
|
||||||
@@ -8,16 +8,16 @@ from aiogram.types import BufferedInputFile, CallbackQuery, Message
|
|||||||
|
|
||||||
from src.telegram.handlers.journal_ui import (
|
from src.telegram.handlers.journal_ui import (
|
||||||
PAGE_SIZE,
|
PAGE_SIZE,
|
||||||
|
build_actions_keyboard,
|
||||||
build_clear_confirm_keyboard,
|
build_clear_confirm_keyboard,
|
||||||
build_keyboard,
|
build_keyboard,
|
||||||
render,
|
render,
|
||||||
render_clear_confirm,
|
|
||||||
build_actions_keyboard,
|
|
||||||
render_actions,
|
render_actions,
|
||||||
|
render_clear_confirm,
|
||||||
)
|
)
|
||||||
|
from src.telegram.live.active_screen import ActiveScreenManager
|
||||||
from src.telegram.live.runner import LiveScreenRunner, ScreenRegistry, StaticScreen
|
from src.telegram.live.runner import LiveScreenRunner, ScreenRegistry, StaticScreen
|
||||||
from src.trading.journal.service import JournalService
|
from src.trading.journal.service import JournalService
|
||||||
from src.trading.auto.runner import AutoTradeRunner
|
|
||||||
|
|
||||||
|
|
||||||
router = Router(name="journal")
|
router = Router(name="journal")
|
||||||
@@ -38,16 +38,41 @@ def _user_id_from_callback(callback: CallbackQuery) -> int | None:
|
|||||||
def _chat_id_from_callback(callback: CallbackQuery) -> int | None:
|
def _chat_id_from_callback(callback: CallbackQuery) -> int | None:
|
||||||
if callback.message and callback.message.chat:
|
if callback.message and callback.message.chat:
|
||||||
return callback.message.chat.id
|
return callback.message.chat.id
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _register_journal_screen(message: Message) -> None:
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
ScreenRegistry.register_screen(
|
||||||
|
StaticScreen(
|
||||||
|
screen="journal",
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
message_id=message.message_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ActiveScreenManager.register(
|
||||||
|
screen="journal",
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _show_journal_page(
|
async def _show_journal_page(
|
||||||
target_message: Message,
|
target_message: Message,
|
||||||
*,
|
*,
|
||||||
page: int,
|
page: int,
|
||||||
edit_mode: bool,
|
edit_mode: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
AutoTradeRunner.set_current_screen("journal")
|
|
||||||
service = JournalService()
|
service = JournalService()
|
||||||
|
|
||||||
total = service.get_total_count()
|
total = service.get_total_count()
|
||||||
@@ -61,65 +86,41 @@ async def _show_journal_page(
|
|||||||
|
|
||||||
if edit_mode:
|
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
|
||||||
|
|
||||||
LiveScreenRunner.unregister_message(
|
sent_message = await target_message.answer(text, reply_markup=kb)
|
||||||
chat_id=target_message.chat.id,
|
_register_journal_screen(sent_message)
|
||||||
message_id=target_message.message_id,
|
|
||||||
)
|
|
||||||
ScreenRegistry.unregister_message(
|
|
||||||
chat_id=target_message.chat.id,
|
|
||||||
message_id=target_message.message_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
ScreenRegistry.register_screen(
|
|
||||||
StaticScreen(
|
|
||||||
screen="journal",
|
|
||||||
bot=target_message.bot,
|
|
||||||
chat_id=target_message.chat.id,
|
|
||||||
message_id=target_message.message_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
sent_message = await target_message.answer(text, reply_markup=kb)
|
|
||||||
|
|
||||||
LiveScreenRunner.unregister_message(
|
|
||||||
chat_id=sent_message.chat.id,
|
|
||||||
message_id=sent_message.message_id,
|
|
||||||
)
|
|
||||||
ScreenRegistry.unregister_message(
|
|
||||||
chat_id=sent_message.chat.id,
|
|
||||||
message_id=sent_message.message_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
ScreenRegistry.register_screen(
|
|
||||||
StaticScreen(
|
|
||||||
screen="journal",
|
|
||||||
bot=sent_message.bot,
|
|
||||||
chat_id=sent_message.chat.id,
|
|
||||||
message_id=sent_message.message_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "journal:actions")
|
@router.callback_query(F.data == "journal:actions")
|
||||||
async def journal_actions(callback: CallbackQuery) -> None:
|
async def journal_actions(callback: CallbackQuery) -> None:
|
||||||
AutoTradeRunner.set_current_screen("journal")
|
|
||||||
if callback.message is None:
|
if callback.message is None:
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
render_actions(),
|
render_actions(),
|
||||||
reply_markup=build_actions_keyboard(),
|
reply_markup=build_actions_keyboard(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_register_journal_screen(callback.message)
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.message(F.text == "📒 Журнал")
|
@router.message(F.text == "📒 Журнал")
|
||||||
async def open_journal(message: Message, state: FSMContext) -> None:
|
async def open_journal(message: Message, state: FSMContext) -> None:
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
await ScreenRegistry.delete_screen(
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
screen="journal",
|
screen="journal",
|
||||||
bot=message.bot,
|
bot=message.bot,
|
||||||
chat_id=message.chat.id,
|
chat_id=message.chat.id,
|
||||||
@@ -149,10 +150,11 @@ async def open_journal_from_monitoring(callback: CallbackQuery, state: FSMContex
|
|||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
await ScreenRegistry.delete_screen(
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
screen="journal",
|
screen="journal",
|
||||||
bot=callback.message.bot,
|
bot=callback.message.bot,
|
||||||
chat_id=callback.message.chat.id,
|
chat_id=callback.message.chat.id,
|
||||||
|
keep_message_id=callback.message.message_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
JournalService().log_ui_info(
|
JournalService().log_ui_info(
|
||||||
@@ -169,6 +171,7 @@ async def open_journal_from_monitoring(callback: CallbackQuery, state: FSMContex
|
|||||||
page=1,
|
page=1,
|
||||||
edit_mode=True,
|
edit_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@@ -255,11 +258,17 @@ async def export_journal_xlsx(callback: CallbackQuery) -> None:
|
|||||||
|
|
||||||
@router.callback_query(F.data == "journal:clear_confirm")
|
@router.callback_query(F.data == "journal:clear_confirm")
|
||||||
async def clear_journal_confirm(callback: CallbackQuery) -> None:
|
async def clear_journal_confirm(callback: CallbackQuery) -> None:
|
||||||
AutoTradeRunner.set_current_screen("journal")
|
|
||||||
if callback.message is None:
|
if callback.message is None:
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
service = JournalService()
|
service = JournalService()
|
||||||
total_count = service.get_total_count()
|
total_count = service.get_total_count()
|
||||||
|
|
||||||
@@ -267,6 +276,9 @@ async def clear_journal_confirm(callback: CallbackQuery) -> None:
|
|||||||
render_clear_confirm(total_count=total_count),
|
render_clear_confirm(total_count=total_count),
|
||||||
reply_markup=build_clear_confirm_keyboard(),
|
reply_markup=build_clear_confirm_keyboard(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_register_journal_screen(callback.message)
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@@ -276,6 +288,13 @@ async def clear_journal(callback: CallbackQuery) -> None:
|
|||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
service = JournalService()
|
service = JournalService()
|
||||||
deleted_count = service.clear_all()
|
deleted_count = service.clear_all()
|
||||||
total_count = service.get_total_count()
|
total_count = service.get_total_count()
|
||||||
@@ -287,6 +306,9 @@ async def clear_journal(callback: CallbackQuery) -> None:
|
|||||||
),
|
),
|
||||||
reply_markup=build_clear_confirm_keyboard(),
|
reply_markup=build_clear_confirm_keyboard(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_register_journal_screen(callback.message)
|
||||||
|
|
||||||
await callback.answer(f"Удалено: {deleted_count}")
|
await callback.answer(f"Удалено: {deleted_count}")
|
||||||
|
|
||||||
|
|
||||||
@@ -296,6 +318,13 @@ async def clear_journal_older_90(callback: CallbackQuery) -> None:
|
|||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
service = JournalService()
|
service = JournalService()
|
||||||
deleted_count = service.clear_older_than_days(90)
|
deleted_count = service.clear_older_than_days(90)
|
||||||
total_count = service.get_total_count()
|
total_count = service.get_total_count()
|
||||||
@@ -308,6 +337,9 @@ async def clear_journal_older_90(callback: CallbackQuery) -> None:
|
|||||||
),
|
),
|
||||||
reply_markup=build_clear_confirm_keyboard(),
|
reply_markup=build_clear_confirm_keyboard(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_register_journal_screen(callback.message)
|
||||||
|
|
||||||
await callback.answer(f"Удалено: {deleted_count}")
|
await callback.answer(f"Удалено: {deleted_count}")
|
||||||
|
|
||||||
|
|
||||||
@@ -325,9 +357,17 @@ async def paginate(callback: CallbackQuery) -> None:
|
|||||||
await callback.answer("Неизвестное действие", show_alert=True)
|
await callback.answer("Неизвестное действие", show_alert=True)
|
||||||
return
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
await _show_journal_page(
|
await _show_journal_page(
|
||||||
callback.message,
|
callback.message,
|
||||||
page=page,
|
page=page,
|
||||||
edit_mode=True,
|
edit_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
@@ -9,6 +9,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|||||||
|
|
||||||
from src.integrations.exchange.exceptions import ExchangeError
|
from src.integrations.exchange.exceptions import ExchangeError
|
||||||
from src.integrations.exchange.service import ExchangeService
|
from src.integrations.exchange.service import ExchangeService
|
||||||
|
from src.telegram.live.active_screen import ActiveScreenManager
|
||||||
from src.telegram.live.runner import LiveScreen, LiveScreenRunner, ScreenRegistry
|
from src.telegram.live.runner import LiveScreen, LiveScreenRunner, ScreenRegistry
|
||||||
from src.telegram.ui.common import mode_line, now_line
|
from src.telegram.ui.common import mode_line, now_line
|
||||||
from src.telegram.ui.currency_ui import format_usd_amount
|
from src.telegram.ui.currency_ui import format_usd_amount
|
||||||
@@ -17,7 +18,6 @@ from src.telegram.ui.exchange_error import (
|
|||||||
show_callback_exchange_error,
|
show_callback_exchange_error,
|
||||||
show_message_exchange_error,
|
show_message_exchange_error,
|
||||||
)
|
)
|
||||||
from src.trading.auto.runner import AutoTradeRunner
|
|
||||||
from src.trading.journal.service import JournalService
|
from src.trading.journal.service import JournalService
|
||||||
|
|
||||||
|
|
||||||
@@ -26,7 +26,6 @@ _last_market_prices: dict[str, float] = {}
|
|||||||
_last_market_directions: dict[str, str] = {}
|
_last_market_directions: dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
# клавиатура экрана рынка
|
|
||||||
def _market_keyboard() -> InlineKeyboardMarkup:
|
def _market_keyboard() -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
builder.button(text="📊 К мониторингу", callback_data="monitoring:home")
|
builder.button(text="📊 К мониторингу", callback_data="monitoring:home")
|
||||||
@@ -34,7 +33,6 @@ def _market_keyboard() -> InlineKeyboardMarkup:
|
|||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
# собрать текст рынка по готовым данным
|
|
||||||
def _build_market_text(
|
def _build_market_text(
|
||||||
*,
|
*,
|
||||||
ticker_price: float,
|
ticker_price: float,
|
||||||
@@ -71,7 +69,6 @@ def _build_market_text(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# собрать актуальный live-текст рынка
|
|
||||||
def _build_market_live_text() -> str:
|
def _build_market_live_text() -> str:
|
||||||
service = ExchangeService()
|
service = ExchangeService()
|
||||||
requested_symbol = service.settings.default_symbol
|
requested_symbol = service.settings.default_symbol
|
||||||
@@ -103,7 +100,6 @@ def _build_market_live_text() -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# зарегистрировать сообщение как live-экран рынка
|
|
||||||
def _register_market_live_screen(message: Message) -> None:
|
def _register_market_live_screen(message: Message) -> None:
|
||||||
LiveScreenRunner.unregister_message(
|
LiveScreenRunner.unregister_message(
|
||||||
chat_id=message.chat.id,
|
chat_id=message.chat.id,
|
||||||
@@ -128,7 +124,6 @@ def _register_market_live_screen(message: Message) -> None:
|
|||||||
LiveScreenRunner.start("market")
|
LiveScreenRunner.start("market")
|
||||||
|
|
||||||
|
|
||||||
# отрисовать экран рынка
|
|
||||||
async def _render_market_screen(
|
async def _render_market_screen(
|
||||||
target_message: Message,
|
target_message: Message,
|
||||||
*,
|
*,
|
||||||
@@ -137,8 +132,6 @@ async def _render_market_screen(
|
|||||||
edit_mode: bool,
|
edit_mode: bool,
|
||||||
action: str,
|
action: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
AutoTradeRunner.set_current_screen("market")
|
|
||||||
|
|
||||||
service = ExchangeService()
|
service = ExchangeService()
|
||||||
journal = JournalService()
|
journal = JournalService()
|
||||||
requested_symbol = service.settings.default_symbol
|
requested_symbol = service.settings.default_symbol
|
||||||
@@ -179,9 +172,11 @@ async def _render_market_screen(
|
|||||||
if edit_mode:
|
if edit_mode:
|
||||||
await target_message.edit_text(text, reply_markup=_market_keyboard())
|
await target_message.edit_text(text, reply_markup=_market_keyboard())
|
||||||
_register_market_live_screen(target_message)
|
_register_market_live_screen(target_message)
|
||||||
|
ActiveScreenManager.register(screen="market", message=target_message)
|
||||||
else:
|
else:
|
||||||
sent_message = await target_message.answer(text, reply_markup=_market_keyboard())
|
sent_message = await target_message.answer(text, reply_markup=_market_keyboard())
|
||||||
_register_market_live_screen(sent_message)
|
_register_market_live_screen(sent_message)
|
||||||
|
ActiveScreenManager.register(screen="market", message=sent_message)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -217,17 +212,18 @@ async def _render_market_screen(
|
|||||||
if edit_mode:
|
if edit_mode:
|
||||||
await target_message.edit_text(text, reply_markup=_market_keyboard())
|
await target_message.edit_text(text, reply_markup=_market_keyboard())
|
||||||
_register_market_live_screen(target_message)
|
_register_market_live_screen(target_message)
|
||||||
|
ActiveScreenManager.register(screen="market", message=target_message)
|
||||||
else:
|
else:
|
||||||
sent_message = await target_message.answer(text, reply_markup=_market_keyboard())
|
sent_message = await target_message.answer(text, reply_markup=_market_keyboard())
|
||||||
_register_market_live_screen(sent_message)
|
_register_market_live_screen(sent_message)
|
||||||
|
ActiveScreenManager.register(screen="market", message=sent_message)
|
||||||
|
|
||||||
|
|
||||||
# открыть рынок из главного меню
|
|
||||||
@router.message(F.text == "📈 Рынок")
|
@router.message(F.text == "📈 Рынок")
|
||||||
async def open_market(message: Message, state: FSMContext) -> None:
|
async def open_market(message: Message, state: FSMContext) -> None:
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
await LiveScreenRunner.delete_screen(
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
screen="market",
|
screen="market",
|
||||||
bot=message.bot,
|
bot=message.bot,
|
||||||
chat_id=message.chat.id,
|
chat_id=message.chat.id,
|
||||||
@@ -266,7 +262,6 @@ async def open_market(message: Message, state: FSMContext) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# открыть рынок из экрана мониторинга
|
|
||||||
@router.callback_query(F.data == "monitoring:market")
|
@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()
|
await state.clear()
|
||||||
@@ -275,10 +270,11 @@ async def open_market_from_monitoring(callback: CallbackQuery, state: FSMContext
|
|||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
await LiveScreenRunner.delete_screen(
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
screen="market",
|
screen="market",
|
||||||
bot=callback.message.bot,
|
bot=callback.message.bot,
|
||||||
chat_id=callback.message.chat.id,
|
chat_id=callback.message.chat.id,
|
||||||
|
keep_message_id=callback.message.message_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
user_id = callback.from_user.id if callback.from_user else None
|
user_id = callback.from_user.id if callback.from_user else None
|
||||||
@@ -315,7 +311,6 @@ async def open_market_from_monitoring(callback: CallbackQuery, state: FSMContext
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# обновить рынок вручную
|
|
||||||
@router.callback_query(F.data == "market:retry")
|
@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()
|
await state.clear()
|
||||||
@@ -324,6 +319,13 @@ async def retry_market(callback: CallbackQuery, state: FSMContext) -> None:
|
|||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
user_id = callback.from_user.id if callback.from_user else None
|
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 = callback.message.chat.id if callback.message.chat else None
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,13 @@ from aiogram.fsm.context import FSMContext
|
|||||||
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
|
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
from src.telegram.live.active_screen import ActiveScreenManager
|
||||||
from src.telegram.live.runner import LiveScreenRunner, ScreenRegistry, StaticScreen
|
from src.telegram.live.runner import LiveScreenRunner, ScreenRegistry, StaticScreen
|
||||||
from src.trading.auto.runner import AutoTradeRunner
|
|
||||||
|
|
||||||
|
|
||||||
router = Router(name="monitoring")
|
router = Router(name="monitoring")
|
||||||
|
|
||||||
|
|
||||||
# клавиатура экрана мониторинга
|
|
||||||
def _monitoring_keyboard() -> InlineKeyboardMarkup:
|
def _monitoring_keyboard() -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
builder.button(text="💼 Портфель", callback_data="monitoring:portfolio")
|
builder.button(text="💼 Портфель", callback_data="monitoring:portfolio")
|
||||||
@@ -24,7 +23,6 @@ def _monitoring_keyboard() -> InlineKeyboardMarkup:
|
|||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
# текст экрана мониторинга
|
|
||||||
def _monitoring_text() -> str:
|
def _monitoring_text() -> str:
|
||||||
return (
|
return (
|
||||||
"<b>📊 Мониторинг</b>\n\n"
|
"<b>📊 Мониторинг</b>\n\n"
|
||||||
@@ -32,7 +30,6 @@ def _monitoring_text() -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# зарегистрировать сообщение как статичный экран мониторинга
|
|
||||||
def _register_monitoring_screen(message: Message) -> None:
|
def _register_monitoring_screen(message: Message) -> None:
|
||||||
LiveScreenRunner.unregister_message(
|
LiveScreenRunner.unregister_message(
|
||||||
chat_id=message.chat.id,
|
chat_id=message.chat.id,
|
||||||
@@ -53,13 +50,11 @@ def _register_monitoring_screen(message: Message) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# открыть мониторинг из главного меню
|
|
||||||
@router.message(F.text == "📊 Мониторинг")
|
@router.message(F.text == "📊 Мониторинг")
|
||||||
async def open_monitoring(message: Message, state: FSMContext) -> None:
|
async def open_monitoring(message: Message, state: FSMContext) -> None:
|
||||||
await state.clear()
|
await state.clear()
|
||||||
AutoTradeRunner.set_current_screen("monitoring")
|
|
||||||
|
|
||||||
await ScreenRegistry.delete_screen(
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
screen="monitoring",
|
screen="monitoring",
|
||||||
bot=message.bot,
|
bot=message.bot,
|
||||||
chat_id=message.chat.id,
|
chat_id=message.chat.id,
|
||||||
@@ -72,21 +67,37 @@ async def open_monitoring(message: Message, state: FSMContext) -> None:
|
|||||||
|
|
||||||
_register_monitoring_screen(sent_message)
|
_register_monitoring_screen(sent_message)
|
||||||
|
|
||||||
|
ActiveScreenManager.register(
|
||||||
|
screen="monitoring",
|
||||||
|
message=sent_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# вернуться на экран мониторинга из callback
|
|
||||||
@router.callback_query(F.data == "monitoring:home")
|
@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()
|
await state.clear()
|
||||||
AutoTradeRunner.set_current_screen("monitoring")
|
|
||||||
|
|
||||||
if callback.message is None:
|
if callback.message is None:
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
_monitoring_text(),
|
_monitoring_text(),
|
||||||
reply_markup=_monitoring_keyboard(),
|
reply_markup=_monitoring_keyboard(),
|
||||||
)
|
)
|
||||||
|
|
||||||
_register_monitoring_screen(callback.message)
|
_register_monitoring_screen(callback.message)
|
||||||
|
|
||||||
|
ActiveScreenManager.register(
|
||||||
|
screen="monitoring",
|
||||||
|
message=callback.message,
|
||||||
|
)
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
@@ -10,13 +10,12 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|||||||
from src.integrations.exchange.exceptions import ExchangeError
|
from src.integrations.exchange.exceptions import ExchangeError
|
||||||
from src.integrations.exchange.models import BalanceSummary
|
from src.integrations.exchange.models import BalanceSummary
|
||||||
from src.integrations.exchange.service import ExchangeService
|
from src.integrations.exchange.service import ExchangeService
|
||||||
|
from src.telegram.live.active_screen import ActiveScreenManager
|
||||||
from src.telegram.live.runner import LiveScreen, LiveScreenRunner, ScreenRegistry
|
from src.telegram.live.runner import LiveScreen, LiveScreenRunner, ScreenRegistry
|
||||||
from src.telegram.ui.common import mode_line, now_line
|
from src.telegram.ui.common import mode_line, now_line
|
||||||
from src.telegram.ui.currency_ui import format_usd_amount
|
|
||||||
from src.telegram.ui.currency_ui import (
|
from src.telegram.ui.currency_ui import (
|
||||||
balance_total,
|
balance_total,
|
||||||
estimate_balance_usd,
|
estimate_balance_usd,
|
||||||
format_amount,
|
|
||||||
format_usd_amount,
|
format_usd_amount,
|
||||||
is_zero_balance,
|
is_zero_balance,
|
||||||
)
|
)
|
||||||
@@ -26,7 +25,6 @@ from src.telegram.ui.exchange_error import (
|
|||||||
show_message_exchange_error,
|
show_message_exchange_error,
|
||||||
)
|
)
|
||||||
from src.trading.accounts.service import AccountsService
|
from src.trading.accounts.service import AccountsService
|
||||||
from src.trading.auto.runner import AutoTradeRunner
|
|
||||||
from src.trading.journal.service import JournalService
|
from src.trading.journal.service import JournalService
|
||||||
|
|
||||||
|
|
||||||
@@ -40,7 +38,7 @@ PINNED_ORDER = {
|
|||||||
"ETH": 4,
|
"ETH": 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
# компактное форматирование количества
|
|
||||||
def _compact_amount(currency: str, value: float) -> str:
|
def _compact_amount(currency: str, value: float) -> str:
|
||||||
currency = currency.upper()
|
currency = currency.upper()
|
||||||
|
|
||||||
@@ -65,7 +63,7 @@ def _compact_amount(currency: str, value: float) -> str:
|
|||||||
|
|
||||||
return f"{int(text):,}".replace(",", " ")
|
return f"{int(text):,}".replace(",", " ")
|
||||||
|
|
||||||
# клавиатура портфеля
|
|
||||||
def _portfolio_keyboard() -> InlineKeyboardMarkup:
|
def _portfolio_keyboard() -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
builder.button(text="📊 К мониторингу", callback_data="monitoring:home")
|
builder.button(text="📊 К мониторингу", callback_data="monitoring:home")
|
||||||
@@ -73,7 +71,6 @@ def _portfolio_keyboard() -> InlineKeyboardMarkup:
|
|||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
# клавиатура портфеля при частичной загрузке
|
|
||||||
def _portfolio_warning_keyboard() -> InlineKeyboardMarkup:
|
def _portfolio_warning_keyboard() -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
builder.button(text="🔁 Обновить", callback_data="portfolio:retry")
|
builder.button(text="🔁 Обновить", callback_data="portfolio:retry")
|
||||||
@@ -82,7 +79,6 @@ def _portfolio_warning_keyboard() -> InlineKeyboardMarkup:
|
|||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
# сортировка активов в портфеле
|
|
||||||
def sort_balances(items: list[BalanceSummary]) -> list[BalanceSummary]:
|
def sort_balances(items: list[BalanceSummary]) -> list[BalanceSummary]:
|
||||||
def sort_key(item: BalanceSummary) -> tuple[int, str]:
|
def sort_key(item: BalanceSummary) -> tuple[int, str]:
|
||||||
currency = item.currency.upper()
|
currency = item.currency.upper()
|
||||||
@@ -92,7 +88,6 @@ def sort_balances(items: list[BalanceSummary]) -> list[BalanceSummary]:
|
|||||||
return sorted(items, key=sort_key)
|
return sorted(items, key=sort_key)
|
||||||
|
|
||||||
|
|
||||||
# собрать актуальный live-текст портфеля
|
|
||||||
def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
|
def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
|
||||||
service = AccountsService()
|
service = AccountsService()
|
||||||
exchange_service = ExchangeService()
|
exchange_service = ExchangeService()
|
||||||
@@ -131,8 +126,6 @@ def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
|
|||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
asset_blocks: list[list[str]] = []
|
|
||||||
|
|
||||||
for item in visible_balances:
|
for item in visible_balances:
|
||||||
currency = item.currency.upper()
|
currency = item.currency.upper()
|
||||||
total = balance_total(item)
|
total = balance_total(item)
|
||||||
@@ -149,7 +142,7 @@ def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
|
|||||||
if item.locked > 0:
|
if item.locked > 0:
|
||||||
line += f" · locked {_compact_amount(currency, item.locked)}"
|
line += f" · locked {_compact_amount(currency, item.locked)}"
|
||||||
|
|
||||||
if estimated_usd is not None and currency not in {'USD', 'USDT'}:
|
if estimated_usd is not None and currency not in {"USD", "USDT"}:
|
||||||
line += f" ≈ $ {format_usd_amount(estimated_usd)}"
|
line += f" ≈ $ {format_usd_amount(estimated_usd)}"
|
||||||
|
|
||||||
if currency == "BTC" and any("USD:" in x or "USDT:" in x for x in lines):
|
if currency == "BTC" and any("USD:" in x or "USDT:" in x for x in lines):
|
||||||
@@ -169,9 +162,6 @@ def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
|
|||||||
if missing_estimate_assets:
|
if missing_estimate_assets:
|
||||||
lines.append(f"Нет оценки: {', '.join(missing_estimate_assets)}")
|
lines.append(f"Нет оценки: {', '.join(missing_estimate_assets)}")
|
||||||
|
|
||||||
for block in asset_blocks:
|
|
||||||
lines.extend(block)
|
|
||||||
|
|
||||||
lines.extend(["", now_line()])
|
lines.extend(["", now_line()])
|
||||||
|
|
||||||
reply_markup = (
|
reply_markup = (
|
||||||
@@ -183,19 +173,16 @@ def _build_portfolio_live_text() -> tuple[str, InlineKeyboardMarkup]:
|
|||||||
return "\n".join(lines).rstrip(), reply_markup
|
return "\n".join(lines).rstrip(), reply_markup
|
||||||
|
|
||||||
|
|
||||||
# текст live-экрана портфеля
|
|
||||||
def _portfolio_live_text() -> str:
|
def _portfolio_live_text() -> str:
|
||||||
text, _ = _build_portfolio_live_text()
|
text, _ = _build_portfolio_live_text()
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
# клавиатура live-экрана портфеля
|
|
||||||
def _portfolio_live_markup() -> InlineKeyboardMarkup:
|
def _portfolio_live_markup() -> InlineKeyboardMarkup:
|
||||||
_, markup = _build_portfolio_live_text()
|
_, markup = _build_portfolio_live_text()
|
||||||
return markup
|
return markup
|
||||||
|
|
||||||
|
|
||||||
# зарегистрировать сообщение как live-экран портфеля
|
|
||||||
def _register_portfolio_live_screen(message: Message) -> None:
|
def _register_portfolio_live_screen(message: Message) -> None:
|
||||||
LiveScreenRunner.unregister_message(
|
LiveScreenRunner.unregister_message(
|
||||||
chat_id=message.chat.id,
|
chat_id=message.chat.id,
|
||||||
@@ -220,7 +207,6 @@ def _register_portfolio_live_screen(message: Message) -> None:
|
|||||||
LiveScreenRunner.start("portfolio")
|
LiveScreenRunner.start("portfolio")
|
||||||
|
|
||||||
|
|
||||||
# отрисовать экран портфеля
|
|
||||||
async def _render_portfolio_screen(
|
async def _render_portfolio_screen(
|
||||||
target_message: Message,
|
target_message: Message,
|
||||||
*,
|
*,
|
||||||
@@ -229,8 +215,6 @@ async def _render_portfolio_screen(
|
|||||||
edit_mode: bool,
|
edit_mode: bool,
|
||||||
action: str,
|
action: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
AutoTradeRunner.set_current_screen("portfolio")
|
|
||||||
|
|
||||||
journal = JournalService()
|
journal = JournalService()
|
||||||
|
|
||||||
journal.log_ui_info(
|
journal.log_ui_info(
|
||||||
@@ -256,17 +240,18 @@ async def _render_portfolio_screen(
|
|||||||
if edit_mode:
|
if edit_mode:
|
||||||
await target_message.edit_text(text, reply_markup=reply_markup)
|
await target_message.edit_text(text, reply_markup=reply_markup)
|
||||||
_register_portfolio_live_screen(target_message)
|
_register_portfolio_live_screen(target_message)
|
||||||
|
ActiveScreenManager.register(screen="portfolio", message=target_message)
|
||||||
else:
|
else:
|
||||||
sent_message = await target_message.answer(text, reply_markup=reply_markup)
|
sent_message = await target_message.answer(text, reply_markup=reply_markup)
|
||||||
_register_portfolio_live_screen(sent_message)
|
_register_portfolio_live_screen(sent_message)
|
||||||
|
ActiveScreenManager.register(screen="portfolio", message=sent_message)
|
||||||
|
|
||||||
|
|
||||||
# открыть портфель из меню
|
|
||||||
@router.message(F.text == "💼 Портфель")
|
@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 state.clear()
|
||||||
|
|
||||||
await LiveScreenRunner.delete_screen(
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
screen="portfolio",
|
screen="portfolio",
|
||||||
bot=message.bot,
|
bot=message.bot,
|
||||||
chat_id=message.chat.id,
|
chat_id=message.chat.id,
|
||||||
@@ -305,7 +290,6 @@ async def open_portfolio(message: Message, state: FSMContext) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# открыть портфель из экрана мониторинга
|
|
||||||
@router.callback_query(F.data == "monitoring:portfolio")
|
@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()
|
await state.clear()
|
||||||
@@ -314,10 +298,11 @@ async def open_portfolio_from_monitoring(callback: CallbackQuery, state: FSMCont
|
|||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
await LiveScreenRunner.delete_screen(
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
screen="portfolio",
|
screen="portfolio",
|
||||||
bot=callback.message.bot,
|
bot=callback.message.bot,
|
||||||
chat_id=callback.message.chat.id,
|
chat_id=callback.message.chat.id,
|
||||||
|
keep_message_id=callback.message.message_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
user_id = callback.from_user.id if callback.from_user else None
|
user_id = callback.from_user.id if callback.from_user else None
|
||||||
@@ -354,7 +339,6 @@ async def open_portfolio_from_monitoring(callback: CallbackQuery, state: FSMCont
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# обновить портфель вручную
|
|
||||||
@router.callback_query(F.data == "portfolio:retry")
|
@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()
|
await state.clear()
|
||||||
@@ -363,6 +347,13 @@ async def retry_portfolio(callback: CallbackQuery, state: FSMContext) -> None:
|
|||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
user_id = callback.from_user.id if callback.from_user else None
|
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 = callback.message.chat.id if callback.message.chat else None
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ from aiogram.fsm.context import FSMContext
|
|||||||
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
|
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
from src.core.system_status import build_system_text, get_system_snapshot, has_system_alerts
|
|
||||||
from src.core.config import load_settings
|
from src.core.config import load_settings
|
||||||
from src.core.constants import APP_NAME, APP_VERSION
|
from src.core.constants import APP_NAME, APP_VERSION
|
||||||
from src.trading.journal.service import JournalService
|
from src.core.system_status import build_system_text, get_system_snapshot, has_system_alerts
|
||||||
|
from src.telegram.live.active_screen import ActiveScreenManager
|
||||||
|
from src.telegram.live.runner import LiveScreenRunner, ScreenRegistry, StaticScreen
|
||||||
from src.trading.auto.service import AutoTradeService
|
from src.trading.auto.service import AutoTradeService
|
||||||
from src.trading.auto.runner import AutoTradeRunner
|
from src.trading.journal.service import JournalService
|
||||||
|
|
||||||
|
|
||||||
router = Router(name="system")
|
router = Router(name="system")
|
||||||
@@ -35,6 +36,57 @@ def _system_alert_keyboard() -> InlineKeyboardMarkup:
|
|||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
def _register_system_screen(message: Message, screen: str = "system") -> None:
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
ScreenRegistry.register_screen(
|
||||||
|
StaticScreen(
|
||||||
|
screen=screen,
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
message_id=message.message_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ActiveScreenManager.register(
|
||||||
|
screen=screen,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _prepare_system_from_message(message: Message, screen: str = "system") -> None:
|
||||||
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
|
screen=screen,
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _prepare_system_from_callback(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
screen: str = "system",
|
||||||
|
) -> bool:
|
||||||
|
if callback.message is None:
|
||||||
|
await callback.answer("Сообщение не найдено", 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def _render_system_screen(
|
async def _render_system_screen(
|
||||||
target_message: Message,
|
target_message: Message,
|
||||||
*,
|
*,
|
||||||
@@ -43,8 +95,6 @@ async def _render_system_screen(
|
|||||||
chat_id: int | None,
|
chat_id: int | None,
|
||||||
action: str,
|
action: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
AutoTradeRunner.set_current_screen("system")
|
|
||||||
|
|
||||||
journal = JournalService()
|
journal = JournalService()
|
||||||
|
|
||||||
journal.log_ui_info(
|
journal.log_ui_info(
|
||||||
@@ -96,14 +146,19 @@ async def _render_system_screen(
|
|||||||
|
|
||||||
if edit_mode:
|
if edit_mode:
|
||||||
await target_message.edit_text(text, reply_markup=reply_markup)
|
await target_message.edit_text(text, reply_markup=reply_markup)
|
||||||
else:
|
_register_system_screen(target_message, screen="system")
|
||||||
await target_message.answer(text, reply_markup=reply_markup)
|
return
|
||||||
|
|
||||||
|
sent_message = await target_message.answer(text, reply_markup=reply_markup)
|
||||||
|
_register_system_screen(sent_message, screen="system")
|
||||||
|
|
||||||
|
|
||||||
@router.message(F.text.in_({"🖥️ Система"}))
|
@router.message(F.text.in_({"🖥️ Система"}))
|
||||||
async def open_system(message: Message, state: FSMContext) -> None:
|
async def open_system(message: Message, state: FSMContext) -> None:
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
|
await _prepare_system_from_message(message, screen="system")
|
||||||
|
|
||||||
user_id = message.from_user.id if message.from_user else None
|
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 if message.chat else None
|
||||||
|
|
||||||
@@ -120,8 +175,7 @@ async def open_system(message: Message, state: FSMContext) -> None:
|
|||||||
async def retry_system(callback: CallbackQuery, state: FSMContext) -> None:
|
async def retry_system(callback: CallbackQuery, state: FSMContext) -> None:
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
if callback.message is None:
|
if not await _prepare_system_from_callback(callback, screen="system"):
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
user_id = callback.from_user.id if callback.from_user else None
|
user_id = callback.from_user.id if callback.from_user else None
|
||||||
@@ -139,8 +193,7 @@ async def retry_system(callback: CallbackQuery, state: FSMContext) -> None:
|
|||||||
|
|
||||||
@router.callback_query(F.data == "system:management")
|
@router.callback_query(F.data == "system:management")
|
||||||
async def open_system_management(callback: CallbackQuery) -> None:
|
async def open_system_management(callback: CallbackQuery) -> None:
|
||||||
if callback.message is None:
|
if not await _prepare_system_from_callback(callback, screen="system"):
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
@@ -155,29 +208,18 @@ async def open_system_management(callback: CallbackQuery) -> None:
|
|||||||
builder.button(text="🌍 Общие", callback_data="settings:general")
|
builder.button(text="🌍 Общие", callback_data="settings:general")
|
||||||
builder.button(text="📒 Журнал", callback_data="settings:journal")
|
builder.button(text="📒 Журнал", callback_data="settings:journal")
|
||||||
builder.button(text="⬅️ Назад", callback_data="system:back")
|
builder.button(text="⬅️ Назад", callback_data="system:back")
|
||||||
|
|
||||||
builder.adjust(2, 2, 1)
|
builder.adjust(2, 2, 1)
|
||||||
|
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
text,
|
_register_system_screen(callback.message, screen="system")
|
||||||
reply_markup=builder.as_markup(),
|
|
||||||
)
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:auto")
|
@router.callback_query(F.data == "settings:auto")
|
||||||
async def open_auto_settings(callback: CallbackQuery) -> None:
|
async def open_auto_settings(callback: CallbackQuery) -> None:
|
||||||
AutoTradeRunner.set_current_screen("settings_auto")
|
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
|
||||||
|
|
||||||
if callback.message is None:
|
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
AutoTradeRunner.unregister_screen(
|
|
||||||
chat_id=callback.message.chat.id,
|
|
||||||
message_id=callback.message.message_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
state = AutoTradeService().get_state()
|
state = AutoTradeService().get_state()
|
||||||
|
|
||||||
strategy_map = {
|
strategy_map = {
|
||||||
@@ -192,10 +234,7 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
leverage_ready = state.leverage is not None
|
leverage_ready = state.leverage is not None
|
||||||
|
|
||||||
is_trend_strategy = (state.strategy or "").upper() == "TREND"
|
is_trend_strategy = (state.strategy or "").upper() == "TREND"
|
||||||
sl_ready = (
|
sl_ready = state.stop_loss_percent is not None and state.stop_loss_percent > 0
|
||||||
state.stop_loss_percent is not None
|
|
||||||
and state.stop_loss_percent > 0
|
|
||||||
)
|
|
||||||
|
|
||||||
is_configured = (
|
is_configured = (
|
||||||
strategy_ready
|
strategy_ready
|
||||||
@@ -206,7 +245,6 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
strategy = strategy_map.get(state.strategy or "", "—")
|
strategy = strategy_map.get(state.strategy or "", "—")
|
||||||
|
|
||||||
symbol = "—"
|
symbol = "—"
|
||||||
|
|
||||||
if state.symbol:
|
if state.symbol:
|
||||||
@@ -219,9 +257,8 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
if base.endswith(suffix) and len(base) > len(suffix):
|
if base.endswith(suffix) and len(base) > len(suffix):
|
||||||
base = base[: -len(suffix)]
|
base = base[: -len(suffix)]
|
||||||
break
|
break
|
||||||
|
|
||||||
symbol = base
|
symbol = base
|
||||||
|
|
||||||
risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "—"
|
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 "—"
|
leverage = f"x{state.leverage:g}" if state.leverage is not None else "—"
|
||||||
max_reserved = (
|
max_reserved = (
|
||||||
@@ -229,23 +266,9 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
if state.max_reserved_balance_percent is not None
|
if state.max_reserved_balance_percent is not None
|
||||||
else "off"
|
else "off"
|
||||||
)
|
)
|
||||||
sl = (
|
sl = f"{state.stop_loss_percent:g}%" if state.stop_loss_percent is not None else "off"
|
||||||
f"{state.stop_loss_percent:g}%"
|
tp = f"{state.take_profit_percent:g}%" if state.take_profit_percent is not None else "off"
|
||||||
if state.stop_loss_percent is not None
|
ml = f"{state.max_loss_usd:g} USD" if state.max_loss_usd is not None else "off"
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
strategy_icon = "✅" if strategy_ready else "⚠️"
|
strategy_icon = "✅" if strategy_ready else "⚠️"
|
||||||
symbol_icon = "✅" if symbol_ready else "⚠️"
|
symbol_icon = "✅" if symbol_ready else "⚠️"
|
||||||
@@ -268,11 +291,7 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
f"✅ Max Loss · {ml}"
|
f"✅ Max Loss · {ml}"
|
||||||
)
|
)
|
||||||
|
|
||||||
config_status = (
|
config_status = "✅ Все параметры настроены" if is_configured else "⚠️ Настрой все параметры"
|
||||||
"✅ Все параметры настроены"
|
|
||||||
if is_configured
|
|
||||||
else "⚠️ Настрой все параметры"
|
|
||||||
)
|
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
"<b>🤖 Автоторговля</b>\n\n"
|
"<b>🤖 Автоторговля</b>\n\n"
|
||||||
@@ -298,14 +317,13 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
builder.adjust(2, 2, 2, 2)
|
builder.adjust(2, 2, 2, 2)
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
|
_register_system_screen(callback.message, screen="settings_auto")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:auto_strategy")
|
@router.callback_query(F.data == "settings:auto_strategy")
|
||||||
async def open_auto_strategy_settings(callback: CallbackQuery) -> None:
|
async def open_auto_strategy_settings(callback: CallbackQuery) -> None:
|
||||||
AutoTradeRunner.set_current_screen("settings_auto")
|
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
|
||||||
if callback.message is None:
|
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
@@ -322,6 +340,7 @@ async def open_auto_strategy_settings(callback: CallbackQuery) -> None:
|
|||||||
builder.adjust(3, 1)
|
builder.adjust(3, 1)
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
|
_register_system_screen(callback.message, screen="settings_auto")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@@ -330,22 +349,15 @@ async def set_auto_strategy(callback: CallbackQuery) -> None:
|
|||||||
strategy = callback.data.split(":", 2)[2]
|
strategy = callback.data.split(":", 2)[2]
|
||||||
AutoTradeService().set_strategy(strategy.upper())
|
AutoTradeService().set_strategy(strategy.upper())
|
||||||
|
|
||||||
if callback.message is not None:
|
await open_auto_settings(callback)
|
||||||
await open_auto_settings(callback)
|
|
||||||
|
|
||||||
AutoTradeRunner.set_current_screen("settings_auto")
|
|
||||||
await callback.answer("Стратегия обновлена")
|
await callback.answer("Стратегия обновлена")
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:auto_symbol")
|
@router.callback_query(F.data == "settings:auto_symbol")
|
||||||
async def open_auto_symbol_settings(callback: CallbackQuery) -> None:
|
async def open_auto_symbol_settings(callback: CallbackQuery) -> None:
|
||||||
AutoTradeRunner.set_current_screen("settings_auto")
|
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
|
||||||
if callback.message is None:
|
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
settings = load_settings()
|
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
"<b>💱 Актив</b>\n\n"
|
"<b>💱 Актив</b>\n\n"
|
||||||
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
|
||||||
@@ -353,32 +365,15 @@ async def open_auto_symbol_settings(callback: CallbackQuery) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
builder.button(text="BTC", callback_data="settings:auto_symbol:BTC/USD_LEVERAGE")
|
||||||
builder.button(
|
builder.button(text="ETH", callback_data="settings:auto_symbol:ETH/USD_LEVERAGE")
|
||||||
text="BTC",
|
builder.button(text="LTC", callback_data="settings:auto_symbol:LTC/USD_LEVERAGE")
|
||||||
callback_data="settings:auto_symbol:BTC/USD_LEVERAGE",
|
builder.button(text="XRP", callback_data="settings:auto_symbol:XRP/USD_LEVERAGE")
|
||||||
)
|
|
||||||
|
|
||||||
builder.button(
|
|
||||||
text="ETH",
|
|
||||||
callback_data="settings:auto_symbol:ETH/USD_LEVERAGE",
|
|
||||||
)
|
|
||||||
|
|
||||||
builder.button(
|
|
||||||
text="LTC",
|
|
||||||
callback_data="settings:auto_symbol:LTC/USD_LEVERAGE",
|
|
||||||
)
|
|
||||||
|
|
||||||
builder.button(
|
|
||||||
text="XRP",
|
|
||||||
callback_data="settings:auto_symbol:XRP/USD_LEVERAGE",
|
|
||||||
)
|
|
||||||
|
|
||||||
builder.button(text="⬅️ Назад", callback_data="settings:auto")
|
builder.button(text="⬅️ Назад", callback_data="settings:auto")
|
||||||
|
|
||||||
builder.adjust(2, 2, 1)
|
builder.adjust(2, 2, 1)
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
|
_register_system_screen(callback.message, screen="settings_auto")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@@ -387,18 +382,13 @@ async def set_auto_symbol(callback: CallbackQuery) -> None:
|
|||||||
symbol = callback.data.split(":", 2)[2]
|
symbol = callback.data.split(":", 2)[2]
|
||||||
AutoTradeService().set_symbol(symbol)
|
AutoTradeService().set_symbol(symbol)
|
||||||
|
|
||||||
if callback.message is not None:
|
await open_auto_settings(callback)
|
||||||
await open_auto_settings(callback)
|
|
||||||
|
|
||||||
AutoTradeRunner.set_current_screen("settings_auto")
|
|
||||||
await callback.answer("Актив обновлён")
|
await callback.answer("Актив обновлён")
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:auto_risk")
|
@router.callback_query(F.data == "settings:auto_risk")
|
||||||
async def open_auto_risk_settings(callback: CallbackQuery) -> None:
|
async def open_auto_risk_settings(callback: CallbackQuery) -> None:
|
||||||
AutoTradeRunner.set_current_screen("settings_auto")
|
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
|
||||||
if callback.message is None:
|
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
@@ -415,6 +405,7 @@ async def open_auto_risk_settings(callback: CallbackQuery) -> None:
|
|||||||
builder.adjust(3, 1)
|
builder.adjust(3, 1)
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
|
_register_system_screen(callback.message, screen="settings_auto")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@@ -423,19 +414,13 @@ async def set_auto_risk(callback: CallbackQuery) -> None:
|
|||||||
risk = float(callback.data.split(":", 2)[2])
|
risk = float(callback.data.split(":", 2)[2])
|
||||||
AutoTradeService().set_risk_percent(risk)
|
AutoTradeService().set_risk_percent(risk)
|
||||||
|
|
||||||
if callback.message is not None:
|
await open_auto_settings(callback)
|
||||||
await open_auto_settings(callback)
|
|
||||||
|
|
||||||
AutoTradeRunner.set_current_screen("settings_auto")
|
|
||||||
await callback.answer("Риск обновлён")
|
await callback.answer("Риск обновлён")
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:auto_leverage")
|
@router.callback_query(F.data == "settings:auto_leverage")
|
||||||
async def open_auto_leverage_settings(callback: CallbackQuery) -> None:
|
async def open_auto_leverage_settings(callback: CallbackQuery) -> None:
|
||||||
AutoTradeRunner.set_current_screen("settings_auto")
|
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
|
||||||
|
|
||||||
if callback.message is None:
|
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
@@ -455,6 +440,7 @@ async def open_auto_leverage_settings(callback: CallbackQuery) -> None:
|
|||||||
builder.adjust(3, 3, 1)
|
builder.adjust(3, 3, 1)
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
|
_register_system_screen(callback.message, screen="settings_auto")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@@ -463,17 +449,13 @@ async def set_auto_leverage(callback: CallbackQuery) -> None:
|
|||||||
leverage = float(callback.data.split(":", 2)[2])
|
leverage = float(callback.data.split(":", 2)[2])
|
||||||
AutoTradeService().set_leverage(leverage)
|
AutoTradeService().set_leverage(leverage)
|
||||||
|
|
||||||
if callback.message is not None:
|
await open_auto_settings(callback)
|
||||||
await open_auto_settings(callback)
|
|
||||||
|
|
||||||
AutoTradeRunner.set_current_screen("settings_auto")
|
|
||||||
await callback.answer("Плечо обновлено")
|
await callback.answer("Плечо обновлено")
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:trade")
|
@router.callback_query(F.data == "settings:trade")
|
||||||
async def open_trade_settings(callback: CallbackQuery) -> None:
|
async def open_trade_settings(callback: CallbackQuery) -> None:
|
||||||
if callback.message is None:
|
if not await _prepare_system_from_callback(callback, screen="settings_trade"):
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
@@ -491,13 +473,13 @@ async def open_trade_settings(callback: CallbackQuery) -> None:
|
|||||||
builder.adjust(2)
|
builder.adjust(2)
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
|
_register_system_screen(callback.message, screen="settings_trade")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:general")
|
@router.callback_query(F.data == "settings:general")
|
||||||
async def open_general_settings(callback: CallbackQuery) -> None:
|
async def open_general_settings(callback: CallbackQuery) -> None:
|
||||||
if callback.message is None:
|
if not await _prepare_system_from_callback(callback, screen="settings_general"):
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
@@ -514,13 +496,13 @@ async def open_general_settings(callback: CallbackQuery) -> None:
|
|||||||
builder.adjust(1)
|
builder.adjust(1)
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
|
_register_system_screen(callback.message, screen="settings_general")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:journal")
|
@router.callback_query(F.data == "settings:journal")
|
||||||
async def open_journal_settings(callback: CallbackQuery) -> None:
|
async def open_journal_settings(callback: CallbackQuery) -> None:
|
||||||
if callback.message is None:
|
if not await _prepare_system_from_callback(callback, screen="settings_journal"):
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
service = JournalService()
|
service = JournalService()
|
||||||
@@ -544,17 +526,14 @@ async def open_journal_settings(callback: CallbackQuery) -> None:
|
|||||||
builder.button(text="📒 Журнал", callback_data="journal:1")
|
builder.button(text="📒 Журнал", callback_data="journal:1")
|
||||||
builder.adjust(2, 2, 2)
|
builder.adjust(2, 2, 2)
|
||||||
|
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
text,
|
_register_system_screen(callback.message, screen="settings_journal")
|
||||||
reply_markup=builder.as_markup(),
|
|
||||||
)
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:journal_archive")
|
@router.callback_query(F.data == "settings:journal_archive")
|
||||||
async def open_journal_archive_settings(callback: CallbackQuery) -> None:
|
async def open_journal_archive_settings(callback: CallbackQuery) -> None:
|
||||||
if callback.message is None:
|
if not await _prepare_system_from_callback(callback, screen="settings_journal"):
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
@@ -572,13 +551,13 @@ async def open_journal_archive_settings(callback: CallbackQuery) -> None:
|
|||||||
builder.adjust(2)
|
builder.adjust(2)
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
|
_register_system_screen(callback.message, screen="settings_journal")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:journal_limit")
|
@router.callback_query(F.data == "settings:journal_limit")
|
||||||
async def open_journal_limit_settings(callback: CallbackQuery) -> None:
|
async def open_journal_limit_settings(callback: CallbackQuery) -> None:
|
||||||
if callback.message is None:
|
if not await _prepare_system_from_callback(callback, screen="settings_journal"):
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
@@ -597,13 +576,13 @@ async def open_journal_limit_settings(callback: CallbackQuery) -> None:
|
|||||||
builder.adjust(2, 2, 1)
|
builder.adjust(2, 2, 1)
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
|
_register_system_screen(callback.message, screen="settings_journal")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:journal_retention")
|
@router.callback_query(F.data == "settings:journal_retention")
|
||||||
async def open_journal_retention_settings(callback: CallbackQuery) -> None:
|
async def open_journal_retention_settings(callback: CallbackQuery) -> None:
|
||||||
if callback.message is None:
|
if not await _prepare_system_from_callback(callback, screen="settings_journal"):
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
@@ -622,6 +601,7 @@ async def open_journal_retention_settings(callback: CallbackQuery) -> None:
|
|||||||
builder.adjust(2, 2, 1)
|
builder.adjust(2, 2, 1)
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
|
_register_system_screen(callback.message, screen="settings_journal")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@@ -635,8 +615,7 @@ async def journal_settings_stub(callback: CallbackQuery) -> None:
|
|||||||
|
|
||||||
@router.callback_query(F.data == "system:back")
|
@router.callback_query(F.data == "system:back")
|
||||||
async def back_to_system(callback: CallbackQuery) -> None:
|
async def back_to_system(callback: CallbackQuery) -> None:
|
||||||
if callback.message is None:
|
if not await _prepare_system_from_callback(callback, screen="system"):
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
user_id = callback.from_user.id if callback.from_user else None
|
user_id = callback.from_user.id if callback.from_user else None
|
||||||
@@ -654,8 +633,7 @@ async def back_to_system(callback: CallbackQuery) -> None:
|
|||||||
|
|
||||||
@router.callback_query(F.data == "system:about")
|
@router.callback_query(F.data == "system:about")
|
||||||
async def open_system_about(callback: CallbackQuery) -> None:
|
async def open_system_about(callback: CallbackQuery) -> None:
|
||||||
if callback.message is None:
|
if not await _prepare_system_from_callback(callback, screen="system_about"):
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
settings = load_settings()
|
settings = load_settings()
|
||||||
@@ -685,19 +663,14 @@ async def open_system_about(callback: CallbackQuery) -> None:
|
|||||||
builder.button(text="⬅️ Назад", callback_data="system:back")
|
builder.button(text="⬅️ Назад", callback_data="system:back")
|
||||||
builder.adjust(1)
|
builder.adjust(1)
|
||||||
|
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
text,
|
_register_system_screen(callback.message, screen="system_about")
|
||||||
reply_markup=builder.as_markup(),
|
|
||||||
)
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "settings:auto_max_reserved")
|
@router.callback_query(F.data == "settings:auto_max_reserved")
|
||||||
async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None:
|
async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None:
|
||||||
AutoTradeRunner.set_current_screen("settings_auto")
|
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
|
||||||
|
|
||||||
if callback.message is None:
|
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
@@ -716,6 +689,7 @@ async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None:
|
|||||||
builder.adjust(2, 2, 1, 1)
|
builder.adjust(2, 2, 1, 1)
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
await callback.message.edit_text(text, reply_markup=builder.as_markup())
|
||||||
|
_register_system_screen(callback.message, screen="settings_auto")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@@ -726,8 +700,5 @@ async def set_auto_max_reserved(callback: CallbackQuery) -> None:
|
|||||||
value = None if raw_value == "off" else float(raw_value)
|
value = None if raw_value == "off" else float(raw_value)
|
||||||
AutoTradeService().set_max_reserved_balance_percent(value)
|
AutoTradeService().set_max_reserved_balance_percent(value)
|
||||||
|
|
||||||
if callback.message is not None:
|
await open_auto_settings(callback)
|
||||||
await open_auto_settings(callback)
|
|
||||||
|
|
||||||
AutoTradeRunner.set_current_screen("settings_auto")
|
|
||||||
await callback.answer("Max Reserved обновлён")
|
await callback.answer("Max Reserved обновлён")
|
||||||
@@ -7,12 +7,14 @@ from aiogram.fsm.context import FSMContext
|
|||||||
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
|
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
|
||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
from src.telegram.ui.common import mode_line
|
|
||||||
from src.telegram.handlers.trade.new_order import (
|
from src.telegram.handlers.trade.new_order import (
|
||||||
show_recent_drafts,
|
show_recent_drafts,
|
||||||
start_new_order_draft,
|
start_new_order_draft,
|
||||||
)
|
)
|
||||||
from src.trading.auto.runner import AutoTradeRunner
|
from src.telegram.live.active_screen import ActiveScreenManager
|
||||||
|
from src.telegram.live.runner import LiveScreenRunner, ScreenRegistry, StaticScreen
|
||||||
|
from src.telegram.ui.common import mode_line
|
||||||
|
|
||||||
|
|
||||||
router = Router(name="trade_main")
|
router = Router(name="trade_main")
|
||||||
|
|
||||||
@@ -25,10 +27,6 @@ def _trade_screen(title: str) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# KEYBOARDS
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
def _trade_home_keyboard() -> InlineKeyboardMarkup:
|
def _trade_home_keyboard() -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
builder.button(text="📝 Ордер", callback_data="trade:new_order")
|
builder.button(text="📝 Ордер", callback_data="trade:new_order")
|
||||||
@@ -72,13 +70,10 @@ def _settings_menu_keyboard() -> InlineKeyboardMarkup:
|
|||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# TEXTS
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
def _trade_home_text() -> str:
|
def _trade_home_text() -> str:
|
||||||
return _trade_screen("Основной экран")
|
return _trade_screen("Основной экран")
|
||||||
|
|
||||||
|
|
||||||
def _trade_orders_text() -> str:
|
def _trade_orders_text() -> str:
|
||||||
return _trade_screen("Ордера")
|
return _trade_screen("Ордера")
|
||||||
|
|
||||||
@@ -91,161 +86,226 @@ def _trade_settings_text() -> str:
|
|||||||
return _trade_screen("Настройки")
|
return _trade_screen("Настройки")
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
def _register_trade_screen(message: Message) -> None:
|
||||||
# ENTRY
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
ScreenRegistry.register_screen(
|
||||||
|
StaticScreen(
|
||||||
|
screen="trade",
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
message_id=message.message_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ActiveScreenManager.register(
|
||||||
|
screen="trade",
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _prepare_trade_from_message(message: Message) -> None:
|
||||||
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
|
screen="trade",
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _prepare_trade_from_callback(callback: CallbackQuery) -> bool:
|
||||||
|
if callback.message is None:
|
||||||
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
await ActiveScreenManager.prepare_new_screen(
|
||||||
|
screen="trade",
|
||||||
|
bot=callback.message.bot,
|
||||||
|
chat_id=callback.message.chat.id,
|
||||||
|
keep_message_id=callback.message.message_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@router.message(F.text.in_({"💹 Торговля"}))
|
@router.message(F.text.in_({"💹 Торговля"}))
|
||||||
async def open_trade(message: Message) -> None:
|
async def open_trade(message: Message, state: FSMContext) -> None:
|
||||||
AutoTradeRunner.set_current_screen("trade")
|
await state.clear()
|
||||||
|
await _prepare_trade_from_message(message)
|
||||||
|
|
||||||
await message.answer(
|
sent_message = await message.answer(
|
||||||
_trade_home_text(),
|
_trade_home_text(),
|
||||||
reply_markup=_trade_home_keyboard(),
|
reply_markup=_trade_home_keyboard(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_register_trade_screen(sent_message)
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "trade:home")
|
@router.callback_query(F.data == "trade:home")
|
||||||
async def open_trade_home_callback(
|
async def open_trade_home_callback(
|
||||||
callback: CallbackQuery,
|
callback: CallbackQuery,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
) -> None:
|
) -> None:
|
||||||
AutoTradeRunner.set_current_screen("trade")
|
|
||||||
|
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
|
if not await _prepare_trade_from_callback(callback):
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
_trade_home_text(),
|
||||||
|
reply_markup=_trade_home_keyboard(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_register_trade_screen(callback.message)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
if callback.message is not None:
|
|
||||||
await callback.message.edit_text(
|
|
||||||
_trade_home_text(),
|
|
||||||
reply_markup=_trade_home_keyboard(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# NEW ORDER
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "trade:new_order")
|
@router.callback_query(F.data == "trade:new_order")
|
||||||
async def open_new_order_from_trade(
|
async def open_new_order_from_trade(
|
||||||
callback: CallbackQuery,
|
callback: CallbackQuery,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if not await _prepare_trade_from_callback(callback):
|
||||||
|
return
|
||||||
|
|
||||||
|
await start_new_order_draft(callback.message, state, edit_mode=True)
|
||||||
|
_register_trade_screen(callback.message)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
if callback.message is not None:
|
|
||||||
await start_new_order_draft(callback.message, state, edit_mode=True)
|
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# ORDERS
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "trade:orders")
|
@router.callback_query(F.data == "trade:orders")
|
||||||
async def open_orders_from_trade(callback: CallbackQuery) -> None:
|
async def open_orders_from_trade(callback: CallbackQuery) -> None:
|
||||||
AutoTradeRunner.set_current_screen("trade")
|
if not await _prepare_trade_from_callback(callback):
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
_trade_orders_text(),
|
||||||
|
reply_markup=_orders_menu_keyboard(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_register_trade_screen(callback.message)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
if callback.message is not None:
|
|
||||||
await callback.message.edit_text(
|
|
||||||
_trade_orders_text(),
|
|
||||||
reply_markup=_orders_menu_keyboard(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "trade:orders:drafts")
|
@router.callback_query(F.data == "trade:orders:drafts")
|
||||||
async def open_drafts_from_orders(callback: CallbackQuery) -> None:
|
async def open_drafts_from_orders(callback: CallbackQuery) -> None:
|
||||||
|
if not await _prepare_trade_from_callback(callback):
|
||||||
|
return
|
||||||
|
|
||||||
|
await show_recent_drafts(callback.message, edit_mode=True, page=1)
|
||||||
|
_register_trade_screen(callback.message)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
if callback.message is not None:
|
|
||||||
await show_recent_drafts(callback.message, edit_mode=True, page=1)
|
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# HISTORY
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "trade:history")
|
@router.callback_query(F.data == "trade:history")
|
||||||
async def open_trade_history(callback: CallbackQuery) -> None:
|
async def open_trade_history(callback: CallbackQuery) -> None:
|
||||||
AutoTradeRunner.set_current_screen("trade")
|
if not await _prepare_trade_from_callback(callback):
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
_trade_history_text(),
|
||||||
|
reply_markup=_history_menu_keyboard(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_register_trade_screen(callback.message)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
if callback.message is not None:
|
|
||||||
await callback.message.edit_text(
|
|
||||||
_trade_history_text(),
|
|
||||||
reply_markup=_history_menu_keyboard(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "trade:history:filled")
|
@router.callback_query(F.data == "trade:history:filled")
|
||||||
async def open_filled_history(callback: CallbackQuery) -> None:
|
async def open_filled_history(callback: CallbackQuery) -> None:
|
||||||
|
if not await _prepare_trade_from_callback(callback):
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
"<b>💹 Торговля — История</b>\n\n"
|
||||||
|
"Шаг 1/1: Исполненные\n"
|
||||||
|
"Раздел в разработке.",
|
||||||
|
reply_markup=_trade_home_button(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_register_trade_screen(callback.message)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
if callback.message is not None:
|
|
||||||
await callback.message.edit_text(
|
|
||||||
"<b>💹 Торговля — История</b>\n\n"
|
|
||||||
"Шаг 1/1: Исполненные\n"
|
|
||||||
"Раздел в разработке.",
|
|
||||||
reply_markup=_trade_home_button(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "trade:history:canceled")
|
@router.callback_query(F.data == "trade:history:canceled")
|
||||||
async def open_canceled_history(callback: CallbackQuery) -> None:
|
async def open_canceled_history(callback: CallbackQuery) -> None:
|
||||||
|
if not await _prepare_trade_from_callback(callback):
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
"<b>💹 Торговля — История</b>\n\n"
|
||||||
|
"Шаг 1/1: Отменённые\n"
|
||||||
|
"Раздел в разработке.",
|
||||||
|
reply_markup=_trade_home_button(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_register_trade_screen(callback.message)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
if callback.message is not None:
|
|
||||||
await callback.message.edit_text(
|
|
||||||
"<b>💹 Торговля — История</b>\n\n"
|
|
||||||
"Шаг 1/1: Отменённые\n"
|
|
||||||
"Раздел в разработке.",
|
|
||||||
reply_markup=_trade_home_button(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# SETTINGS
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "trade:settings")
|
@router.callback_query(F.data == "trade:settings")
|
||||||
async def open_trade_settings(callback: CallbackQuery) -> None:
|
async def open_trade_settings(callback: CallbackQuery) -> None:
|
||||||
AutoTradeRunner.set_current_screen("trade")
|
if not await _prepare_trade_from_callback(callback):
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
_trade_settings_text(),
|
||||||
|
reply_markup=_settings_menu_keyboard(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_register_trade_screen(callback.message)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
if callback.message is not None:
|
|
||||||
await callback.message.edit_text(
|
|
||||||
_trade_settings_text(),
|
|
||||||
reply_markup=_settings_menu_keyboard(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "trade:settings:params")
|
@router.callback_query(F.data == "trade:settings:params")
|
||||||
async def open_trade_settings_params(callback: CallbackQuery) -> None:
|
async def open_trade_settings_params(callback: CallbackQuery) -> None:
|
||||||
|
if not await _prepare_trade_from_callback(callback):
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
"<b>💹 Торговля — Настройки</b>\n\n"
|
||||||
|
"Шаг 1/1: Параметры ордера\n"
|
||||||
|
"Раздел в разработке.",
|
||||||
|
reply_markup=_trade_home_button(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_register_trade_screen(callback.message)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
if callback.message is not None:
|
|
||||||
await callback.message.edit_text(
|
|
||||||
"<b>💹 Торговля — Настройки</b>\n\n"
|
|
||||||
"Шаг 1/1: Параметры ордера\n"
|
|
||||||
"Раздел в разработке.",
|
|
||||||
reply_markup=_trade_home_button(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "trade:settings:mode")
|
@router.callback_query(F.data == "trade:settings:mode")
|
||||||
async def open_trade_settings_mode(callback: CallbackQuery) -> None:
|
async def open_trade_settings_mode(callback: CallbackQuery) -> None:
|
||||||
|
if not await _prepare_trade_from_callback(callback):
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
"<b>💹 Торговля — Настройки</b>\n\n"
|
||||||
|
"Шаг 1/1: Режим работы\n"
|
||||||
|
"Текущий режим: <b>demo</b>",
|
||||||
|
reply_markup=_trade_home_button(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_register_trade_screen(callback.message)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
if callback.message is not None:
|
|
||||||
await callback.message.edit_text(
|
|
||||||
"<b>💹 Торговля — Настройки</b>\n\n"
|
|
||||||
"Шаг 1/1: Режим работы\n"
|
|
||||||
"Текущий режим: <b>demo</b>",
|
|
||||||
reply_markup=_trade_home_button(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "trade:settings:help")
|
@router.callback_query(F.data == "trade:settings:help")
|
||||||
async def open_trade_settings_help(callback: CallbackQuery) -> None:
|
async def open_trade_settings_help(callback: CallbackQuery) -> None:
|
||||||
await callback.answer()
|
if not await _prepare_trade_from_callback(callback):
|
||||||
if callback.message is not None:
|
return
|
||||||
await callback.message.edit_text(
|
|
||||||
"<b>💹 Торговля — Справка</b>\n\n"
|
await callback.message.edit_text(
|
||||||
"Шаг 1/1: Информация\n"
|
"<b>💹 Торговля — Справка</b>\n\n"
|
||||||
"Раздел в разработке.",
|
"Шаг 1/1: Информация\n"
|
||||||
reply_markup=_trade_home_button(),
|
"Раздел в разработке.",
|
||||||
)
|
reply_markup=_trade_home_button(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_register_trade_screen(callback.message)
|
||||||
|
await callback.answer()
|
||||||
100
app/src/telegram/live/active_screen.py
Normal file
100
app/src/telegram/live/active_screen.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# app/src/telegram/live/active_screen.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from aiogram import Bot
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
from src.telegram.live.runner import LiveScreenRunner, ScreenRegistry
|
||||||
|
from src.trading.auto.runner import AutoTradeRunner
|
||||||
|
from src.trading.debug.runner import DebugTradeRunner
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class ActiveScreen:
|
||||||
|
screen: str
|
||||||
|
chat_id: int
|
||||||
|
message_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class ActiveScreenManager:
|
||||||
|
_screens: dict[int, ActiveScreen] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def prepare_new_screen(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
screen: str,
|
||||||
|
bot: Bot,
|
||||||
|
chat_id: int,
|
||||||
|
keep_message_id: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
previous = cls._screens.get(chat_id)
|
||||||
|
|
||||||
|
await AutoTradeRunner.detach_screen(
|
||||||
|
delete_message=True,
|
||||||
|
bot=bot,
|
||||||
|
chat_id=chat_id,
|
||||||
|
keep_message_id=keep_message_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await DebugTradeRunner.detach_screen(
|
||||||
|
delete_message=True,
|
||||||
|
bot=bot,
|
||||||
|
chat_id=chat_id,
|
||||||
|
keep_message_id=keep_message_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await LiveScreenRunner.delete_chat_screens(
|
||||||
|
bot=bot,
|
||||||
|
chat_id=chat_id,
|
||||||
|
keep_message_id=keep_message_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await ScreenRegistry.delete_chat_screens(
|
||||||
|
bot=bot,
|
||||||
|
chat_id=chat_id,
|
||||||
|
keep_message_id=keep_message_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if previous is not None:
|
||||||
|
if keep_message_id is None or previous.message_id != keep_message_id:
|
||||||
|
try:
|
||||||
|
await bot.delete_message(
|
||||||
|
chat_id=previous.chat_id,
|
||||||
|
message_id=previous.message_id,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cls._screens.pop(chat_id, None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
screen: str,
|
||||||
|
message: Message,
|
||||||
|
) -> None:
|
||||||
|
cls._screens[message.chat.id] = ActiveScreen(
|
||||||
|
screen=screen,
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
message_id=message.message_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def unregister(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
chat_id: int,
|
||||||
|
message_id: int,
|
||||||
|
) -> None:
|
||||||
|
current = cls._screens.get(chat_id)
|
||||||
|
|
||||||
|
if current is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if current.message_id == message_id:
|
||||||
|
cls._screens.pop(chat_id, None)
|
||||||
@@ -12,51 +12,29 @@ from aiogram.exceptions import TelegramBadRequest
|
|||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class LiveScreen:
|
class LiveScreen:
|
||||||
# имя live-экрана: market / portfolio
|
|
||||||
screen: str
|
screen: str
|
||||||
|
|
||||||
# Telegram bot instance
|
|
||||||
bot: Bot
|
bot: Bot
|
||||||
|
|
||||||
# чат, где находится live-экран
|
|
||||||
chat_id: int
|
chat_id: int
|
||||||
|
|
||||||
# сообщение, которое нужно автообновлять
|
|
||||||
message_id: int
|
message_id: int
|
||||||
|
|
||||||
# функция сборки текста экрана
|
|
||||||
render_text: Callable[[], str]
|
render_text: Callable[[], str]
|
||||||
|
|
||||||
# функция сборки клавиатуры экрана
|
|
||||||
render_markup: Callable[[], object]
|
render_markup: Callable[[], object]
|
||||||
|
|
||||||
# интервал обновления в секундах
|
|
||||||
interval_seconds: int = 5
|
interval_seconds: int = 5
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class StaticScreen:
|
class StaticScreen:
|
||||||
# имя статичного экрана: journal / monitoring / etc
|
|
||||||
screen: str
|
screen: str
|
||||||
|
|
||||||
# Telegram bot instance
|
|
||||||
bot: Bot
|
bot: Bot
|
||||||
|
|
||||||
# чат, где находится экран
|
|
||||||
chat_id: int
|
chat_id: int
|
||||||
|
|
||||||
# сообщение экрана
|
|
||||||
message_id: int
|
message_id: int
|
||||||
|
|
||||||
|
|
||||||
class ScreenRegistry:
|
class ScreenRegistry:
|
||||||
_screens: dict[str, list[StaticScreen]] = {}
|
_screens: dict[str, list[StaticScreen]] = {}
|
||||||
|
|
||||||
# зарегистрировать статичный экран
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_screen(cls, static_screen: StaticScreen) -> None:
|
def register_screen(cls, static_screen: StaticScreen) -> None:
|
||||||
screens = cls._screens.setdefault(static_screen.screen, [])
|
screens = cls._screens.setdefault(static_screen.screen, [])
|
||||||
|
|
||||||
screens[:] = [
|
screens[:] = [
|
||||||
item
|
item
|
||||||
for item in screens
|
for item in screens
|
||||||
@@ -65,27 +43,17 @@ class ScreenRegistry:
|
|||||||
and item.message_id == static_screen.message_id
|
and item.message_id == static_screen.message_id
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
screens.append(static_screen)
|
screens.append(static_screen)
|
||||||
|
|
||||||
# удалить конкретное сообщение из всех статичных экранов без удаления из Telegram
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def unregister_message(
|
def unregister_message(cls, *, chat_id: int, message_id: int) -> None:
|
||||||
cls,
|
|
||||||
*,
|
|
||||||
chat_id: int,
|
|
||||||
message_id: int,
|
|
||||||
) -> None:
|
|
||||||
empty_screens: list[str] = []
|
empty_screens: list[str] = []
|
||||||
|
|
||||||
for screen, screens in cls._screens.items():
|
for screen, screens in cls._screens.items():
|
||||||
screens[:] = [
|
screens[:] = [
|
||||||
item
|
item
|
||||||
for item in screens
|
for item in screens
|
||||||
if not (
|
if not (item.chat_id == chat_id and item.message_id == message_id)
|
||||||
item.chat_id == chat_id
|
|
||||||
and item.message_id == message_id
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if not screens:
|
if not screens:
|
||||||
@@ -94,15 +62,8 @@ class ScreenRegistry:
|
|||||||
for screen in empty_screens:
|
for screen in empty_screens:
|
||||||
cls._screens.pop(screen, None)
|
cls._screens.pop(screen, None)
|
||||||
|
|
||||||
# удалить старые статичные экраны указанного типа
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def delete_screen(
|
async def delete_screen(cls, *, screen: str, bot: Bot, chat_id: int) -> None:
|
||||||
cls,
|
|
||||||
*,
|
|
||||||
screen: str,
|
|
||||||
bot: Bot,
|
|
||||||
chat_id: int,
|
|
||||||
) -> None:
|
|
||||||
screens = cls._screens.get(screen, [])
|
screens = cls._screens.get(screen, [])
|
||||||
remaining: list[StaticScreen] = []
|
remaining: list[StaticScreen] = []
|
||||||
|
|
||||||
@@ -124,16 +85,52 @@ class ScreenRegistry:
|
|||||||
else:
|
else:
|
||||||
cls._screens.pop(screen, None)
|
cls._screens.pop(screen, None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def delete_chat_screens(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
bot: Bot,
|
||||||
|
chat_id: int,
|
||||||
|
keep_message_id: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
empty_screens: list[str] = []
|
||||||
|
|
||||||
|
for screen, screens in cls._screens.items():
|
||||||
|
remaining: list[StaticScreen] = []
|
||||||
|
|
||||||
|
for static_screen in screens:
|
||||||
|
if static_screen.chat_id != chat_id:
|
||||||
|
remaining.append(static_screen)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if keep_message_id is not None and static_screen.message_id == keep_message_id:
|
||||||
|
remaining.append(static_screen)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.delete_message(
|
||||||
|
chat_id=static_screen.chat_id,
|
||||||
|
message_id=static_screen.message_id,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cls._screens[screen] = remaining
|
||||||
|
|
||||||
|
if not remaining:
|
||||||
|
empty_screens.append(screen)
|
||||||
|
|
||||||
|
for screen in empty_screens:
|
||||||
|
cls._screens.pop(screen, None)
|
||||||
|
|
||||||
|
|
||||||
class LiveScreenRunner:
|
class LiveScreenRunner:
|
||||||
_screens: dict[str, list[LiveScreen]] = {}
|
_screens: dict[str, list[LiveScreen]] = {}
|
||||||
_tasks: dict[str, asyncio.Task] = {}
|
_tasks: dict[str, asyncio.Task] = {}
|
||||||
|
|
||||||
# зарегистрировать live-экран
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_screen(cls, live_screen: LiveScreen) -> None:
|
def register_screen(cls, live_screen: LiveScreen) -> None:
|
||||||
screens = cls._screens.setdefault(live_screen.screen, [])
|
screens = cls._screens.setdefault(live_screen.screen, [])
|
||||||
|
|
||||||
screens[:] = [
|
screens[:] = [
|
||||||
item
|
item
|
||||||
for item in screens
|
for item in screens
|
||||||
@@ -142,27 +139,17 @@ class LiveScreenRunner:
|
|||||||
and item.message_id == live_screen.message_id
|
and item.message_id == live_screen.message_id
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
screens.append(live_screen)
|
screens.append(live_screen)
|
||||||
|
|
||||||
# удалить конкретное сообщение из всех live-экранов без удаления из Telegram
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def unregister_message(
|
def unregister_message(cls, *, chat_id: int, message_id: int) -> None:
|
||||||
cls,
|
|
||||||
*,
|
|
||||||
chat_id: int,
|
|
||||||
message_id: int,
|
|
||||||
) -> None:
|
|
||||||
empty_screens: list[str] = []
|
empty_screens: list[str] = []
|
||||||
|
|
||||||
for screen, screens in cls._screens.items():
|
for screen, screens in cls._screens.items():
|
||||||
screens[:] = [
|
screens[:] = [
|
||||||
item
|
item
|
||||||
for item in screens
|
for item in screens
|
||||||
if not (
|
if not (item.chat_id == chat_id and item.message_id == message_id)
|
||||||
item.chat_id == chat_id
|
|
||||||
and item.message_id == message_id
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if not screens:
|
if not screens:
|
||||||
@@ -170,16 +157,10 @@ class LiveScreenRunner:
|
|||||||
|
|
||||||
for screen in empty_screens:
|
for screen in empty_screens:
|
||||||
cls._screens.pop(screen, None)
|
cls._screens.pop(screen, None)
|
||||||
|
cls.stop(screen)
|
||||||
|
|
||||||
# удалить все live-экраны указанного типа из Telegram
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def delete_screen(
|
async def delete_screen(cls, *, screen: str, bot: Bot, chat_id: int) -> None:
|
||||||
cls,
|
|
||||||
*,
|
|
||||||
screen: str,
|
|
||||||
bot: Bot,
|
|
||||||
chat_id: int,
|
|
||||||
) -> None:
|
|
||||||
screens = cls._screens.get(screen, [])
|
screens = cls._screens.get(screen, [])
|
||||||
remaining: list[LiveScreen] = []
|
remaining: list[LiveScreen] = []
|
||||||
|
|
||||||
@@ -200,8 +181,47 @@ class LiveScreenRunner:
|
|||||||
cls._screens[screen] = remaining
|
cls._screens[screen] = remaining
|
||||||
else:
|
else:
|
||||||
cls._screens.pop(screen, None)
|
cls._screens.pop(screen, None)
|
||||||
|
cls.stop(screen)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def delete_chat_screens(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
bot: Bot,
|
||||||
|
chat_id: int,
|
||||||
|
keep_message_id: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
empty_screens: list[str] = []
|
||||||
|
|
||||||
|
for screen, screens in cls._screens.items():
|
||||||
|
remaining: list[LiveScreen] = []
|
||||||
|
|
||||||
|
for live_screen in screens:
|
||||||
|
if live_screen.chat_id != chat_id:
|
||||||
|
remaining.append(live_screen)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if keep_message_id is not None and live_screen.message_id == keep_message_id:
|
||||||
|
remaining.append(live_screen)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.delete_message(
|
||||||
|
chat_id=live_screen.chat_id,
|
||||||
|
message_id=live_screen.message_id,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cls._screens[screen] = remaining
|
||||||
|
|
||||||
|
if not remaining:
|
||||||
|
empty_screens.append(screen)
|
||||||
|
|
||||||
|
for screen in empty_screens:
|
||||||
|
cls._screens.pop(screen, None)
|
||||||
|
cls.stop(screen)
|
||||||
|
|
||||||
# запустить автообновление группы экранов
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def start(cls, screen: str) -> None:
|
def start(cls, screen: str) -> None:
|
||||||
task = cls._tasks.get(screen)
|
task = cls._tasks.get(screen)
|
||||||
@@ -211,7 +231,6 @@ class LiveScreenRunner:
|
|||||||
|
|
||||||
cls._tasks[screen] = asyncio.create_task(cls._worker(screen))
|
cls._tasks[screen] = asyncio.create_task(cls._worker(screen))
|
||||||
|
|
||||||
# остановить автообновление группы экранов
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def stop(cls, screen: str) -> None:
|
def stop(cls, screen: str) -> None:
|
||||||
task = cls._tasks.get(screen)
|
task = cls._tasks.get(screen)
|
||||||
@@ -222,14 +241,16 @@ class LiveScreenRunner:
|
|||||||
task.cancel()
|
task.cancel()
|
||||||
cls._tasks.pop(screen, None)
|
cls._tasks.pop(screen, None)
|
||||||
|
|
||||||
# фоновый цикл обновления группы экранов
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _worker(cls, screen: str) -> None:
|
async def _worker(cls, screen: str) -> None:
|
||||||
while True:
|
while True:
|
||||||
|
if screen not in cls._screens:
|
||||||
|
cls._tasks.pop(screen, None)
|
||||||
|
return
|
||||||
|
|
||||||
await cls._refresh_screen(screen)
|
await cls._refresh_screen(screen)
|
||||||
await asyncio.sleep(cls._screen_interval(screen))
|
await asyncio.sleep(cls._screen_interval(screen))
|
||||||
|
|
||||||
# получить интервал обновления группы экранов
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _screen_interval(cls, screen: str) -> int:
|
def _screen_interval(cls, screen: str) -> int:
|
||||||
screens = cls._screens.get(screen, [])
|
screens = cls._screens.get(screen, [])
|
||||||
@@ -238,11 +259,11 @@ class LiveScreenRunner:
|
|||||||
|
|
||||||
return screens[0].interval_seconds
|
return screens[0].interval_seconds
|
||||||
|
|
||||||
# обновить все Telegram-сообщения live-экрана
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _refresh_screen(cls, screen: str) -> None:
|
async def _refresh_screen(cls, screen: str) -> None:
|
||||||
screens = cls._screens.get(screen, [])
|
screens = cls._screens.get(screen, [])
|
||||||
if not screens:
|
if not screens:
|
||||||
|
cls._screens.pop(screen, None)
|
||||||
return
|
return
|
||||||
|
|
||||||
alive_screens: list[LiveScreen] = []
|
alive_screens: list[LiveScreen] = []
|
||||||
|
|||||||
@@ -97,6 +97,39 @@ class AutoTradeRunner:
|
|||||||
cls._render_markup = None
|
cls._render_markup = None
|
||||||
cls._last_text = None
|
cls._last_text = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def detach_screen(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
delete_message: bool = False,
|
||||||
|
bot: Bot | None = None,
|
||||||
|
chat_id: int | None = None,
|
||||||
|
keep_message_id: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
if (
|
||||||
|
delete_message
|
||||||
|
and bot is not None
|
||||||
|
and cls._chat_id is not None
|
||||||
|
and cls._message_id is not None
|
||||||
|
and cls._chat_id == chat_id
|
||||||
|
and cls._message_id != keep_message_id
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
await bot.delete_message(
|
||||||
|
chat_id=cls._chat_id,
|
||||||
|
message_id=cls._message_id,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cls._bot = None
|
||||||
|
cls._chat_id = None
|
||||||
|
cls._message_id = None
|
||||||
|
cls._render_text = None
|
||||||
|
cls._render_markup = None
|
||||||
|
cls._current_screen = None
|
||||||
|
cls._last_text = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_current_screen(cls, screen: str) -> None:
|
def set_current_screen(cls, screen: str) -> None:
|
||||||
cls._current_screen = screen
|
cls._current_screen = screen
|
||||||
@@ -594,10 +627,6 @@ class AutoTradeRunner:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _refresh_screen(cls, *, force: bool = False) -> None:
|
async def _refresh_screen(cls, *, force: bool = False) -> None:
|
||||||
if cls._current_screen != "auto":
|
|
||||||
cls._log_refresh_skip("current_screen_not_auto")
|
|
||||||
return
|
|
||||||
|
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
|
|
||||||
if now < cls._retry_after_until:
|
if now < cls._retry_after_until:
|
||||||
|
|||||||
@@ -74,6 +74,39 @@ class DebugTradeRunner:
|
|||||||
cls._render_markup = None
|
cls._render_markup = None
|
||||||
cls._last_text = None
|
cls._last_text = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def detach_screen(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
delete_message: bool = False,
|
||||||
|
bot: Bot | None = None,
|
||||||
|
chat_id: int | None = None,
|
||||||
|
keep_message_id: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
if (
|
||||||
|
delete_message
|
||||||
|
and bot is not None
|
||||||
|
and cls._chat_id is not None
|
||||||
|
and cls._message_id is not None
|
||||||
|
and cls._chat_id == chat_id
|
||||||
|
and cls._message_id != keep_message_id
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
await bot.delete_message(
|
||||||
|
chat_id=cls._chat_id,
|
||||||
|
message_id=cls._message_id,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cls._bot = None
|
||||||
|
cls._chat_id = None
|
||||||
|
cls._message_id = None
|
||||||
|
cls._render_text = None
|
||||||
|
cls._render_markup = None
|
||||||
|
cls._current_screen = None
|
||||||
|
cls._last_text = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_current_screen(cls, screen: str) -> None:
|
def set_current_screen(cls, screen: str) -> None:
|
||||||
cls._current_screen = screen
|
cls._current_screen = screen
|
||||||
|
|||||||
@@ -302,6 +302,19 @@
|
|||||||
- removed shared market cache collisions
|
- removed shared market cache collisions
|
||||||
- stabilized AUTO/DEBUG UI market rendering
|
- stabilized AUTO/DEBUG UI market rendering
|
||||||
|
|
||||||
|
#### 07.4.3.17 — Unified Active Screen Lifecycle
|
||||||
|
|
||||||
|
- внедрён единый lifecycle основных экранов
|
||||||
|
- реализовано автоматическое закрытие предыдущего экрана
|
||||||
|
- устранено накопление Telegram UI-экранов
|
||||||
|
- унифицировано поведение всех основных экранов
|
||||||
|
- разделены UI lifecycle и background runtime
|
||||||
|
- сохранена фоновая работа AutoTradeRunner
|
||||||
|
- сохранена фоновая работа DebugTradeRunner
|
||||||
|
- стабилизирована работа live-экранов
|
||||||
|
- подготовлена архитектура для Telegram push-уведомлений
|
||||||
|
- подготовлена база для runtime event notifications
|
||||||
|
|
||||||
### 07.4.4
|
### 07.4.4
|
||||||
⏳ Grid Strategy
|
⏳ Grid Strategy
|
||||||
|
|
||||||
|
|||||||
@@ -287,6 +287,18 @@
|
|||||||
- removed shared market cache collisions
|
- removed shared market cache collisions
|
||||||
- stabilized AUTO/DEBUG UI market rendering
|
- stabilized AUTO/DEBUG UI market rendering
|
||||||
|
|
||||||
|
#### 07.4.3.17 — Unified Active Screen Lifecycle
|
||||||
|
- внедрён единый lifecycle основных экранов
|
||||||
|
- реализовано автоматическое закрытие предыдущего экрана
|
||||||
|
- устранено накопление Telegram UI-экранов
|
||||||
|
- унифицировано поведение всех основных экранов
|
||||||
|
- разделены UI lifecycle и background runtime
|
||||||
|
- сохранена фоновая работа AutoTradeRunner
|
||||||
|
- сохранена фоновая работа DebugTradeRunner
|
||||||
|
- стабилизирована работа live-экранов
|
||||||
|
- подготовлена архитектура для Telegram push-уведомлений
|
||||||
|
- подготовлена база для runtime event notifications
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 07.4.4
|
### 07.4.4
|
||||||
|
|||||||
84
docs/stages/ROADMAP_UPDATE_07.4.3.17.md
Normal file
84
docs/stages/ROADMAP_UPDATE_07.4.3.17.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# ROADMAP UPDATE — Этап 07.4.3.17
|
||||||
|
|
||||||
|
## Завершено
|
||||||
|
|
||||||
|
### Active Screen Lifecycle
|
||||||
|
- внедрён единый lifecycle основных экранов;
|
||||||
|
- реализовано автоматическое закрытие предыдущего экрана;
|
||||||
|
- устранено накопление Telegram UI-сообщений;
|
||||||
|
- унифицировано поведение всех основных экранов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Обновлённая архитектура
|
||||||
|
|
||||||
|
### ActiveScreenManager
|
||||||
|
Теперь отвечает за:
|
||||||
|
- регистрацию активного экрана;
|
||||||
|
- удаление предыдущего экрана;
|
||||||
|
- переключение UI.
|
||||||
|
|
||||||
|
### LiveScreenRunner
|
||||||
|
Теперь отвечает только за:
|
||||||
|
- автообновление live-данных;
|
||||||
|
- worker loop;
|
||||||
|
- refresh Telegram message.
|
||||||
|
|
||||||
|
### Trading Runtime
|
||||||
|
Теперь полностью независим от UI:
|
||||||
|
- AutoTradeRunner;
|
||||||
|
- DebugTradeRunner;
|
||||||
|
- future push-events.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Исправленные разделы
|
||||||
|
|
||||||
|
### Основные экраны
|
||||||
|
- Главная
|
||||||
|
- Мониторинг
|
||||||
|
- Рынок
|
||||||
|
- Портфель
|
||||||
|
- Журнал
|
||||||
|
- Торговля
|
||||||
|
- Система
|
||||||
|
- Автоторговля
|
||||||
|
- Debug Auto
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Новое поведение системы
|
||||||
|
|
||||||
|
### UI
|
||||||
|
- всегда существует только один основной экран;
|
||||||
|
- при открытии нового старый удаляется автоматически.
|
||||||
|
|
||||||
|
### Background Runtime
|
||||||
|
- продолжает работать независимо от UI;
|
||||||
|
- готов к Telegram push-событиям.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Следующий этап
|
||||||
|
|
||||||
|
## Planned
|
||||||
|
|
||||||
|
### Telegram Event Push Layer
|
||||||
|
Планируется внедрение:
|
||||||
|
- уведомлений об открытии позиции;
|
||||||
|
- уведомлений о закрытии позиции;
|
||||||
|
- уведомлений о сигналах;
|
||||||
|
- runtime alerts;
|
||||||
|
- system alerts;
|
||||||
|
- execution events.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектурная готовность
|
||||||
|
|
||||||
|
Система теперь готова к:
|
||||||
|
- background runtime;
|
||||||
|
- multi-event notifications;
|
||||||
|
- persistent runtime state;
|
||||||
|
- restart-safe workflows;
|
||||||
|
- production lifecycle management.
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# Stage 07.4.3.17 - Active Screen Lifecycle & UI Refresh Policy
|
||||||
|
|
||||||
|
### Цель этапа
|
||||||
|
|
||||||
|
Привести Telegram UI к единой архитектуре экранов:
|
||||||
|
|
||||||
|
- в чате существует только один основной экран;
|
||||||
|
- при открытии нового экрана предыдущий автоматически удаляется;
|
||||||
|
- live-экраны продолжают работать независимо от UI;
|
||||||
|
- фоновые процессы не зависят от существования Telegram-сообщения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Проблема до исправления
|
||||||
|
|
||||||
|
## Поведение экранов было разным
|
||||||
|
|
||||||
|
### Автоторговля
|
||||||
|
- открытие другого экрана удаляло экран автоторговли.
|
||||||
|
|
||||||
|
### Мониторинг
|
||||||
|
- повторное открытие удаляло старую копию;
|
||||||
|
- но другие экраны не закрывали мониторинг.
|
||||||
|
|
||||||
|
### Торговля / Система
|
||||||
|
- можно было открыть бесконечное количество копий;
|
||||||
|
- старые экраны не очищались.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Целевая архитектура
|
||||||
|
|
||||||
|
## Новая политика UI
|
||||||
|
|
||||||
|
При открытии любого основного экрана:
|
||||||
|
|
||||||
|
1. предыдущий основной экран удаляется;
|
||||||
|
2. новый экран становится активным;
|
||||||
|
3. live-обновления продолжают работать;
|
||||||
|
4. фоновые процессы не останавливаются.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Введён ActiveScreenManager
|
||||||
|
|
||||||
|
## Ответственность
|
||||||
|
|
||||||
|
`ActiveScreenManager` теперь управляет:
|
||||||
|
|
||||||
|
- текущим активным экраном;
|
||||||
|
- удалением предыдущего экрана;
|
||||||
|
- переключением между основными UI-разделами.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Разделение ответственности
|
||||||
|
|
||||||
|
## ActiveScreenManager
|
||||||
|
|
||||||
|
Отвечает только за lifecycle UI:
|
||||||
|
|
||||||
|
- какой экран сейчас активен;
|
||||||
|
- какой экран удалить;
|
||||||
|
- какой экран зарегистрировать.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LiveScreenRunner
|
||||||
|
|
||||||
|
Отвечает только за:
|
||||||
|
|
||||||
|
- автообновление live-экранов;
|
||||||
|
- refresh Telegram message;
|
||||||
|
- worker loop.
|
||||||
|
|
||||||
|
Больше НЕ управляет жизненным циклом экранов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AutoTradeRunner / DebugTradeRunner
|
||||||
|
|
||||||
|
Отвечают только за:
|
||||||
|
|
||||||
|
- фоновые процессы;
|
||||||
|
- runtime state;
|
||||||
|
- сигналы;
|
||||||
|
- торговую логику.
|
||||||
|
|
||||||
|
Больше НЕ зависят от существования Telegram UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Исправленные обработчики
|
||||||
|
|
||||||
|
Обновлены:
|
||||||
|
|
||||||
|
- home.py
|
||||||
|
- monitoring.py
|
||||||
|
- market.py
|
||||||
|
- portfolio.py
|
||||||
|
- journal.py
|
||||||
|
- trade/main.py
|
||||||
|
- system.py
|
||||||
|
- auto/main.py
|
||||||
|
- debug_auto/main.py
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Итоговое поведение
|
||||||
|
|
||||||
|
## Теперь работает правильно
|
||||||
|
|
||||||
|
### Любой основной экран:
|
||||||
|
- закрывает предыдущий экран;
|
||||||
|
- оставляет только один UI-экран в чате.
|
||||||
|
|
||||||
|
### Live-экраны:
|
||||||
|
- продолжают обновляться;
|
||||||
|
- не плодят копии сообщений.
|
||||||
|
|
||||||
|
### Автоторговля:
|
||||||
|
- продолжает работать после закрытия UI;
|
||||||
|
- готова к будущим Telegram push-уведомлениям.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Архитектурная схема
|
||||||
|
|
||||||
|
```text
|
||||||
|
UI Screen Lifecycle
|
||||||
|
↓
|
||||||
|
ActiveScreenManager
|
||||||
|
↓
|
||||||
|
удаляет предыдущий основной экран
|
||||||
|
↓
|
||||||
|
рендер нового экрана
|
||||||
|
↓
|
||||||
|
Runner обновляет только live-data
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Результат этапа
|
||||||
|
|
||||||
|
## Система стала:
|
||||||
|
|
||||||
|
- предсказуемой;
|
||||||
|
- масштабируемой;
|
||||||
|
- пригодной для push-событий;
|
||||||
|
- пригодной для background runtime;
|
||||||
|
- устойчивой к накоплению Telegram-сообщений.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Следующие этапы
|
||||||
|
|
||||||
|
Дальше можно безопасно внедрять:
|
||||||
|
|
||||||
|
- push-уведомления;
|
||||||
|
- сигналы;
|
||||||
|
- алерты;
|
||||||
|
- события открытия/закрытия позиций;
|
||||||
|
- восстановление состояния после рестарта;
|
||||||
|
- runtime monitoring.
|
||||||
Reference in New Issue
Block a user