Stage 07.3.1 - auto trading background runner and live screen
This commit is contained in:
@@ -3,12 +3,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from aiogram import F, Router
|
from aiogram import F, Router
|
||||||
|
from aiogram.exceptions import TelegramBadRequest
|
||||||
from aiogram.fsm.context import FSMContext
|
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 aiogram.exceptions import TelegramBadRequest
|
|
||||||
|
|
||||||
from src.telegram.ui.common import mode_line
|
from src.telegram.ui.common import mode_line
|
||||||
|
from src.trading.auto.runner import AutoTradeRunner
|
||||||
from src.trading.auto.service import AutoTradeService
|
from src.trading.auto.service import AutoTradeService
|
||||||
|
|
||||||
|
|
||||||
@@ -49,12 +50,9 @@ def _signal_label(signal: str | None) -> str:
|
|||||||
def _auto_keyboard() -> InlineKeyboardMarkup:
|
def _auto_keyboard() -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
# 1 ряд
|
|
||||||
builder.button(text="▶️ Start", callback_data="auto:start")
|
builder.button(text="▶️ Start", callback_data="auto:start")
|
||||||
builder.button(text="👀 Watch", callback_data="auto:observe")
|
builder.button(text="👀 Watch", callback_data="auto:observe")
|
||||||
builder.button(text="🛑 Stop", callback_data="auto:stop")
|
builder.button(text="🛑 Stop", callback_data="auto:stop")
|
||||||
|
|
||||||
# 2 ряд
|
|
||||||
builder.button(text="🛠️ Настройки", callback_data="settings:auto")
|
builder.button(text="🛠️ Настройки", callback_data="settings:auto")
|
||||||
|
|
||||||
builder.adjust(3, 1)
|
builder.adjust(3, 1)
|
||||||
@@ -64,10 +62,6 @@ def _auto_keyboard() -> InlineKeyboardMarkup:
|
|||||||
# собрать текст экрана
|
# собрать текст экрана
|
||||||
def _build_auto_text() -> str:
|
def _build_auto_text() -> str:
|
||||||
service = AutoTradeService()
|
service = AutoTradeService()
|
||||||
|
|
||||||
# выполнить один цикл анализа
|
|
||||||
service.run_cycle()
|
|
||||||
|
|
||||||
state = service.get_state()
|
state = service.get_state()
|
||||||
|
|
||||||
strategy = _strategy_label(state.strategy)
|
strategy = _strategy_label(state.strategy)
|
||||||
@@ -86,7 +80,7 @@ def _build_auto_text() -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# отрисовать экран
|
# отрисовать live-экран автоторговли
|
||||||
async def _render_auto_screen(
|
async def _render_auto_screen(
|
||||||
target_message: Message,
|
target_message: Message,
|
||||||
*,
|
*,
|
||||||
@@ -98,21 +92,47 @@ async def _render_auto_screen(
|
|||||||
try:
|
try:
|
||||||
await target_message.edit_text(text, reply_markup=_auto_keyboard())
|
await target_message.edit_text(text, reply_markup=_auto_keyboard())
|
||||||
except TelegramBadRequest as exc:
|
except TelegramBadRequest as exc:
|
||||||
if "message is not modified" in str(exc).lower():
|
if "message is not modified" not in str(exc).lower():
|
||||||
return
|
raise
|
||||||
raise
|
|
||||||
else:
|
AutoTradeRunner.register_screen(
|
||||||
await target_message.answer(text, reply_markup=_auto_keyboard())
|
bot=target_message.bot,
|
||||||
|
chat_id=target_message.chat.id,
|
||||||
|
message_id=target_message.message_id,
|
||||||
|
render_text=_build_auto_text,
|
||||||
|
render_markup=_auto_keyboard,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
sent_message = await target_message.answer(text, reply_markup=_auto_keyboard())
|
||||||
|
|
||||||
|
AutoTradeRunner.register_screen(
|
||||||
|
bot=sent_message.bot,
|
||||||
|
chat_id=sent_message.chat.id,
|
||||||
|
message_id=sent_message.message_id,
|
||||||
|
render_text=_build_auto_text,
|
||||||
|
render_markup=_auto_keyboard,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# открыть экран из меню
|
# открыть экран из главного меню
|
||||||
@router.message(F.text.in_({"🤖 Автоторговля", "🤖 Авто"}))
|
@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")
|
||||||
|
|
||||||
|
current_state = AutoTradeService().get_state()
|
||||||
|
if current_state.status in {"RUNNING", "OBSERVING"}:
|
||||||
|
await AutoTradeRunner.delete_registered_screen(
|
||||||
|
bot=message.bot,
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
)
|
||||||
|
|
||||||
await _render_auto_screen(message, edit_mode=False)
|
await _render_auto_screen(message, edit_mode=False)
|
||||||
|
|
||||||
|
|
||||||
# открыть экран callback
|
# открыть экран через callback
|
||||||
@router.callback_query(F.data == "auto:home")
|
@router.callback_query(F.data == "auto:home")
|
||||||
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()
|
||||||
@@ -121,6 +141,7 @@ async def open_auto_from_callback(callback: CallbackQuery, state: FSMContext) ->
|
|||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
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()
|
||||||
|
|
||||||
@@ -131,6 +152,9 @@ async def auto_start(callback: CallbackQuery) -> None:
|
|||||||
service = AutoTradeService()
|
service = AutoTradeService()
|
||||||
_, message = service.start()
|
_, message = service.start()
|
||||||
|
|
||||||
|
AutoTradeRunner.set_current_screen("auto")
|
||||||
|
AutoTradeRunner.start()
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
await _render_auto_screen(callback.message, edit_mode=True)
|
await _render_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
@@ -143,6 +167,9 @@ async def auto_observe(callback: CallbackQuery) -> None:
|
|||||||
service = AutoTradeService()
|
service = AutoTradeService()
|
||||||
_, message = service.observe()
|
_, message = service.observe()
|
||||||
|
|
||||||
|
AutoTradeRunner.set_current_screen("auto")
|
||||||
|
AutoTradeRunner.start()
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
await _render_auto_screen(callback.message, edit_mode=True)
|
await _render_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
@@ -155,6 +182,8 @@ async def auto_stop(callback: CallbackQuery) -> None:
|
|||||||
service = AutoTradeService()
|
service = AutoTradeService()
|
||||||
_, message = service.stop()
|
_, message = service.stop()
|
||||||
|
|
||||||
|
AutoTradeRunner.stop()
|
||||||
|
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
await _render_auto_screen(callback.message, edit_mode=True)
|
await _render_auto_screen(callback.message, edit_mode=True)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from src.telegram.handlers.journal_ui import (
|
|||||||
render_actions,
|
render_actions,
|
||||||
)
|
)
|
||||||
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")
|
||||||
@@ -45,6 +46,7 @@ async def _show_journal_page(
|
|||||||
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()
|
||||||
@@ -64,6 +66,7 @@ async def _show_journal_page(
|
|||||||
|
|
||||||
@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
|
||||||
@@ -178,6 +181,7 @@ 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
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from src.telegram.ui.exchange_error import (
|
|||||||
show_message_exchange_error,
|
show_message_exchange_error,
|
||||||
)
|
)
|
||||||
from src.trading.journal.service import JournalService
|
from src.trading.journal.service import JournalService
|
||||||
|
from src.trading.auto.runner import AutoTradeRunner
|
||||||
|
|
||||||
|
|
||||||
router = Router(name="market")
|
router = Router(name="market")
|
||||||
@@ -72,6 +73,8 @@ 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
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from src.telegram.ui.exchange_error import (
|
|||||||
)
|
)
|
||||||
from src.trading.accounts.service import AccountsService
|
from src.trading.accounts.service import AccountsService
|
||||||
from src.trading.journal.service import JournalService
|
from src.trading.journal.service import JournalService
|
||||||
|
from src.trading.auto.runner import AutoTradeRunner
|
||||||
|
|
||||||
|
|
||||||
router = Router(name="portfolio")
|
router = Router(name="portfolio")
|
||||||
@@ -70,6 +71,8 @@ async def _render_portfolio_screen(
|
|||||||
edit_mode: bool,
|
edit_mode: bool,
|
||||||
action: str,
|
action: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
AutoTradeRunner.set_current_screen("portfolio")
|
||||||
|
|
||||||
service = AccountsService()
|
service = AccountsService()
|
||||||
exchange_service = ExchangeService()
|
exchange_service = ExchangeService()
|
||||||
journal = JournalService()
|
journal = JournalService()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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.trading.journal.service import JournalService
|
||||||
from src.trading.auto.service import AutoTradeService
|
from src.trading.auto.service import AutoTradeService
|
||||||
|
from src.trading.auto.runner import AutoTradeRunner
|
||||||
|
|
||||||
|
|
||||||
router = Router(name="system")
|
router = Router(name="system")
|
||||||
@@ -44,6 +45,8 @@ 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(
|
||||||
@@ -166,6 +169,7 @@ async def open_system_management(callback: CallbackQuery) -> None:
|
|||||||
|
|
||||||
@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 callback.message is None:
|
if callback.message is None:
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
return
|
||||||
@@ -203,6 +207,7 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
|||||||
|
|
||||||
@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 callback.message is None:
|
if callback.message is None:
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
return
|
||||||
@@ -232,11 +237,13 @@ async def set_auto_strategy(callback: CallbackQuery) -> None:
|
|||||||
if callback.message is not None:
|
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 callback.message is None:
|
if callback.message is None:
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
return
|
||||||
@@ -268,11 +275,13 @@ async def set_auto_symbol(callback: CallbackQuery) -> None:
|
|||||||
if callback.message is not None:
|
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 callback.message is None:
|
if callback.message is None:
|
||||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||||
return
|
return
|
||||||
@@ -302,6 +311,7 @@ async def set_auto_risk(callback: CallbackQuery) -> None:
|
|||||||
if callback.message is not None:
|
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("Риск обновлён")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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
|
||||||
|
|
||||||
router = Router(name="trade_main")
|
router = Router(name="trade_main")
|
||||||
|
|
||||||
@@ -96,6 +97,8 @@ def _trade_settings_text() -> str:
|
|||||||
|
|
||||||
@router.message(F.text.in_({"📊 Торговля", "⚡ Торговля", "Торговля"}))
|
@router.message(F.text.in_({"📊 Торговля", "⚡ Торговля", "Торговля"}))
|
||||||
async def open_trade(message: Message) -> None:
|
async def open_trade(message: Message) -> None:
|
||||||
|
AutoTradeRunner.set_current_screen("trade")
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
_trade_home_text(),
|
_trade_home_text(),
|
||||||
reply_markup=_trade_home_keyboard(),
|
reply_markup=_trade_home_keyboard(),
|
||||||
@@ -107,6 +110,8 @@ 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()
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
@@ -137,6 +142,8 @@ async def open_new_order_from_trade(
|
|||||||
|
|
||||||
@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")
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
@@ -158,6 +165,8 @@ async def open_drafts_from_orders(callback: CallbackQuery) -> None:
|
|||||||
|
|
||||||
@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")
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
@@ -196,6 +205,8 @@ async def open_canceled_history(callback: CallbackQuery) -> None:
|
|||||||
|
|
||||||
@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")
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
if callback.message is not None:
|
if callback.message is not None:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
|
|||||||
130
app/src/trading/auto/runner.py
Normal file
130
app/src/trading/auto/runner.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# app/src/trading/auto/runner.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from aiogram import Bot
|
||||||
|
|
||||||
|
from src.trading.auto.service import AutoTradeService
|
||||||
|
|
||||||
|
|
||||||
|
class AutoTradeRunner:
|
||||||
|
_task: asyncio.Task | None = None
|
||||||
|
_bot: Bot | None = None
|
||||||
|
_chat_id: int | None = None
|
||||||
|
_message_id: int | None = None
|
||||||
|
_render_text: Callable[[], str] | None = None
|
||||||
|
_render_markup: Callable[[], object] | None = None
|
||||||
|
_current_screen: str | None = None
|
||||||
|
_interval_seconds = 5
|
||||||
|
|
||||||
|
# зарегистрировать live-экран для автообновления
|
||||||
|
@classmethod
|
||||||
|
def register_screen(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
bot: Bot,
|
||||||
|
chat_id: int,
|
||||||
|
message_id: int,
|
||||||
|
render_text: Callable[[], str],
|
||||||
|
render_markup: Callable[[], object],
|
||||||
|
) -> None:
|
||||||
|
cls._bot = bot
|
||||||
|
cls._chat_id = chat_id
|
||||||
|
cls._message_id = message_id
|
||||||
|
cls._render_text = render_text
|
||||||
|
cls._render_markup = render_markup
|
||||||
|
|
||||||
|
# удалить ранее зарегистрированный live-экран
|
||||||
|
@classmethod
|
||||||
|
async def delete_registered_screen(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
bot: Bot,
|
||||||
|
chat_id: int,
|
||||||
|
) -> None:
|
||||||
|
if cls._chat_id is None or cls._message_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if cls._chat_id != chat_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.delete_message(
|
||||||
|
chat_id=cls._chat_id,
|
||||||
|
message_id=cls._message_id,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cls._message_id = None
|
||||||
|
cls._render_text = None
|
||||||
|
cls._render_markup = None
|
||||||
|
|
||||||
|
# переключить активный экран
|
||||||
|
@classmethod
|
||||||
|
def set_current_screen(cls, screen: str) -> None:
|
||||||
|
cls._current_screen = screen
|
||||||
|
|
||||||
|
# запустить background runner
|
||||||
|
@classmethod
|
||||||
|
def start(cls) -> None:
|
||||||
|
if cls._task is not None and not cls._task.done():
|
||||||
|
return
|
||||||
|
|
||||||
|
cls._task = asyncio.create_task(cls._worker())
|
||||||
|
|
||||||
|
# остановить background runner
|
||||||
|
@classmethod
|
||||||
|
def stop(cls) -> None:
|
||||||
|
if cls._task is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
cls._task.cancel()
|
||||||
|
cls._task = None
|
||||||
|
|
||||||
|
# background loop автоторговли
|
||||||
|
@classmethod
|
||||||
|
async def _worker(cls) -> None:
|
||||||
|
service = AutoTradeService()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
state = service.get_state()
|
||||||
|
|
||||||
|
if state.status == "OFF":
|
||||||
|
cls._task = None
|
||||||
|
break
|
||||||
|
|
||||||
|
service.run_cycle()
|
||||||
|
|
||||||
|
await cls._refresh_screen()
|
||||||
|
await asyncio.sleep(cls._interval_seconds)
|
||||||
|
|
||||||
|
# обновить live-экран Telegram
|
||||||
|
@classmethod
|
||||||
|
async def _refresh_screen(cls) -> None:
|
||||||
|
if cls._current_screen != "auto":
|
||||||
|
return
|
||||||
|
|
||||||
|
if not all(
|
||||||
|
[
|
||||||
|
cls._bot,
|
||||||
|
cls._chat_id,
|
||||||
|
cls._message_id,
|
||||||
|
cls._render_text,
|
||||||
|
cls._render_markup,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await cls._bot.edit_message_text(
|
||||||
|
chat_id=cls._chat_id,
|
||||||
|
message_id=cls._message_id,
|
||||||
|
text=cls._render_text(),
|
||||||
|
reply_markup=cls._render_markup(),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import random
|
import random
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -11,6 +12,8 @@ from src.trading.auto.state import AutoTradeState
|
|||||||
|
|
||||||
class AutoTradeService:
|
class AutoTradeService:
|
||||||
_state = AutoTradeState()
|
_state = AutoTradeState()
|
||||||
|
_loop_task: asyncio.Task | None = None
|
||||||
|
_loop_interval_seconds = 5
|
||||||
|
|
||||||
# получить текущее состояние автоторговли
|
# получить текущее состояние автоторговли
|
||||||
def get_state(self) -> AutoTradeState:
|
def get_state(self) -> AutoTradeState:
|
||||||
@@ -18,18 +21,51 @@ class AutoTradeService:
|
|||||||
self._state.symbol = load_settings().default_symbol
|
self._state.symbol = load_settings().default_symbol
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
# проверить, запущен ли background loop
|
||||||
|
def is_loop_running(self) -> bool:
|
||||||
|
return self._loop_task is not None and not self._loop_task.done()
|
||||||
|
|
||||||
|
# запустить background loop, если он ещё не запущен
|
||||||
|
def start_loop(self) -> None:
|
||||||
|
if self.is_loop_running():
|
||||||
|
return
|
||||||
|
|
||||||
|
self._loop_task = asyncio.create_task(self._loop_worker())
|
||||||
|
|
||||||
|
# остановить background loop
|
||||||
|
def stop_loop(self) -> None:
|
||||||
|
if self._loop_task is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._loop_task.cancel()
|
||||||
|
self._loop_task = None
|
||||||
|
|
||||||
|
# рабочий цикл автоторговли
|
||||||
|
async def _loop_worker(self) -> None:
|
||||||
|
while True:
|
||||||
|
state = self.get_state()
|
||||||
|
|
||||||
|
if state.status == "OFF":
|
||||||
|
break
|
||||||
|
|
||||||
|
self.run_cycle()
|
||||||
|
await asyncio.sleep(self._loop_interval_seconds)
|
||||||
|
|
||||||
# запустить активную торговлю
|
# запустить активную торговлю
|
||||||
def start(self) -> tuple[AutoTradeState, str]:
|
def start(self) -> tuple[AutoTradeState, str]:
|
||||||
state = self.get_state()
|
state = self.get_state()
|
||||||
|
|
||||||
if state.status == "RUNNING":
|
if state.status == "RUNNING":
|
||||||
|
self.start_loop()
|
||||||
return state, "Автоторговля уже активна."
|
return state, "Автоторговля уже активна."
|
||||||
|
|
||||||
if state.status == "OBSERVING":
|
if state.status == "OBSERVING":
|
||||||
state.status = "RUNNING"
|
state.status = "RUNNING"
|
||||||
|
self.start_loop()
|
||||||
return state, "Автоторговля активирована."
|
return state, "Автоторговля активирована."
|
||||||
|
|
||||||
state.status = "RUNNING"
|
state.status = "RUNNING"
|
||||||
|
self.start_loop()
|
||||||
return state, "Автоторговля запущена."
|
return state, "Автоторговля запущена."
|
||||||
|
|
||||||
# включить режим наблюдения
|
# включить режим наблюдения
|
||||||
@@ -38,9 +74,11 @@ class AutoTradeService:
|
|||||||
previous_status = state.status
|
previous_status = state.status
|
||||||
|
|
||||||
if previous_status == "OBSERVING":
|
if previous_status == "OBSERVING":
|
||||||
|
self.start_loop()
|
||||||
return state, "Режим наблюдения уже включён."
|
return state, "Режим наблюдения уже включён."
|
||||||
|
|
||||||
state.status = "OBSERVING"
|
state.status = "OBSERVING"
|
||||||
|
self.start_loop()
|
||||||
|
|
||||||
if previous_status == "OFF":
|
if previous_status == "OFF":
|
||||||
return state, "Включён режим наблюдения."
|
return state, "Включён режим наблюдения."
|
||||||
@@ -52,9 +90,11 @@ class AutoTradeService:
|
|||||||
state = self.get_state()
|
state = self.get_state()
|
||||||
|
|
||||||
if state.status == "OFF":
|
if state.status == "OFF":
|
||||||
|
self.stop_loop()
|
||||||
return state, "Автоторговля уже выключена."
|
return state, "Автоторговля уже выключена."
|
||||||
|
|
||||||
state.status = "OFF"
|
state.status = "OFF"
|
||||||
|
self.stop_loop()
|
||||||
return state, "Автоторговля выключена."
|
return state, "Автоторговля выключена."
|
||||||
|
|
||||||
# установить инструмент
|
# установить инструмент
|
||||||
|
|||||||
22
docs/decisions/0018-single-live-auto-screen.md
Normal file
22
docs/decisions/0018-single-live-auto-screen.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 0018 — Single Live Auto Trading Screen
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Для автоторговли используется только один активный live-экран.
|
||||||
|
|
||||||
|
## Причины
|
||||||
|
|
||||||
|
Telegram не умеет перемещать старое сообщение вниз.
|
||||||
|
|
||||||
|
Чтобы имитировать такое поведение:
|
||||||
|
|
||||||
|
- старый live-экран удаляется;
|
||||||
|
- новый создаётся внизу;
|
||||||
|
- runner регистрирует новый message_id.
|
||||||
|
|
||||||
|
## Последствия
|
||||||
|
|
||||||
|
- нет дублей live-экранов;
|
||||||
|
- автообновление всегда идёт в актуальный экран;
|
||||||
|
- снижается риск Telegram rate limit;
|
||||||
|
- UX выглядит как единый живой dashboard.
|
||||||
@@ -33,10 +33,20 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 07.3.1 — Background loop
|
## 07.3.1 — Background Runner
|
||||||
|
|
||||||
⏳ asyncio loop
|
✔ asyncio runner
|
||||||
⏳ live cycle
|
✔ auto-refresh screen
|
||||||
|
✔ single live auto screen
|
||||||
|
✔ active screen guard
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 07.3.2 — Live Screens
|
||||||
|
|
||||||
|
⏳ live market screen
|
||||||
|
⏳ live portfolio screen
|
||||||
|
⏳ live journal screen
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
87
docs/stages/stage-07_3_1-auto-trading-background-runner.md
Normal file
87
docs/stages/stage-07_3_1-auto-trading-background-runner.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Stage 07.3.1 — Auto Trading Background Runner
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
Реализован live-runner для автоторговли.
|
||||||
|
|
||||||
|
## 1. Background runner
|
||||||
|
|
||||||
|
Добавлен файл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/trading/auto/runner.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Runner отвечает за:
|
||||||
|
|
||||||
|
- background loop;
|
||||||
|
- автообновление Telegram-экрана;
|
||||||
|
- хранение live message;
|
||||||
|
- удаление старого live-экрана;
|
||||||
|
- контроль активного экрана.
|
||||||
|
|
||||||
|
## 2. Live auto screen
|
||||||
|
|
||||||
|
Экран 🤖 Автоторговля стал live-экраном.
|
||||||
|
|
||||||
|
При активном режиме:
|
||||||
|
|
||||||
|
- RUNNING
|
||||||
|
- OBSERVING
|
||||||
|
|
||||||
|
экран обновляется каждые 5 секунд.
|
||||||
|
|
||||||
|
## 3. Auto-refresh UI
|
||||||
|
|
||||||
|
На экране автоматически обновляются:
|
||||||
|
|
||||||
|
- последний анализ;
|
||||||
|
- сигнал стратегии;
|
||||||
|
- статус;
|
||||||
|
- PnL.
|
||||||
|
|
||||||
|
## 4. Single live screen behavior
|
||||||
|
|
||||||
|
Реализовано поведение одного активного live-экрана:
|
||||||
|
|
||||||
|
- если автоторговля уже активна;
|
||||||
|
- пользователь снова открывает 🤖 Автоторговля;
|
||||||
|
- старый live-экран удаляется;
|
||||||
|
- новый экран появляется внизу;
|
||||||
|
- автообновление переключается на новый экран.
|
||||||
|
|
||||||
|
## 5. Active screen guard
|
||||||
|
|
||||||
|
Добавлен контроль активного экрана:
|
||||||
|
|
||||||
|
```text
|
||||||
|
_current_screen
|
||||||
|
```
|
||||||
|
|
||||||
|
Runner обновляет Telegram только если активен экран:
|
||||||
|
|
||||||
|
```text
|
||||||
|
auto
|
||||||
|
```
|
||||||
|
|
||||||
|
Это предотвращает прыжки обратно на экран автоторговли при переходе в другие разделы.
|
||||||
|
|
||||||
|
## 6. Подготовка к LiveScreenRunner
|
||||||
|
|
||||||
|
Текущая реализация подготовила базу для будущего универсального live-runner:
|
||||||
|
|
||||||
|
- 📈 Рынок
|
||||||
|
- 💼 Портфель
|
||||||
|
- 📒 Журнал
|
||||||
|
|
||||||
|
## Commit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Stage 07.3.1 - auto trading background runner and live screen"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Следующий этап
|
||||||
|
|
||||||
|
Stage 07.3.2 — Live screens for Market, Portfolio and Journal
|
||||||
Reference in New Issue
Block a user