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 aiogram import F, Router
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
from src.telegram.ui.common import mode_line
|
||||
from src.trading.auto.runner import AutoTradeRunner
|
||||
from src.trading.auto.service import AutoTradeService
|
||||
|
||||
|
||||
@@ -49,12 +50,9 @@ def _signal_label(signal: str | None) -> str:
|
||||
def _auto_keyboard() -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
# 1 ряд
|
||||
builder.button(text="▶️ Start", callback_data="auto:start")
|
||||
builder.button(text="👀 Watch", callback_data="auto:observe")
|
||||
builder.button(text="🛑 Stop", callback_data="auto:stop")
|
||||
|
||||
# 2 ряд
|
||||
builder.button(text="🛠️ Настройки", callback_data="settings:auto")
|
||||
|
||||
builder.adjust(3, 1)
|
||||
@@ -64,10 +62,6 @@ def _auto_keyboard() -> InlineKeyboardMarkup:
|
||||
# собрать текст экрана
|
||||
def _build_auto_text() -> str:
|
||||
service = AutoTradeService()
|
||||
|
||||
# выполнить один цикл анализа
|
||||
service.run_cycle()
|
||||
|
||||
state = service.get_state()
|
||||
|
||||
strategy = _strategy_label(state.strategy)
|
||||
@@ -86,7 +80,7 @@ def _build_auto_text() -> str:
|
||||
)
|
||||
|
||||
|
||||
# отрисовать экран
|
||||
# отрисовать live-экран автоторговли
|
||||
async def _render_auto_screen(
|
||||
target_message: Message,
|
||||
*,
|
||||
@@ -98,21 +92,47 @@ async def _render_auto_screen(
|
||||
try:
|
||||
await target_message.edit_text(text, reply_markup=_auto_keyboard())
|
||||
except TelegramBadRequest as exc:
|
||||
if "message is not modified" in str(exc).lower():
|
||||
return
|
||||
raise
|
||||
else:
|
||||
await target_message.answer(text, reply_markup=_auto_keyboard())
|
||||
if "message is not modified" not in str(exc).lower():
|
||||
raise
|
||||
|
||||
AutoTradeRunner.register_screen(
|
||||
bot=target_message.bot,
|
||||
chat_id=target_message.chat.id,
|
||||
message_id=target_message.message_id,
|
||||
render_text=_build_auto_text,
|
||||
render_markup=_auto_keyboard,
|
||||
)
|
||||
return
|
||||
|
||||
sent_message = await target_message.answer(text, reply_markup=_auto_keyboard())
|
||||
|
||||
AutoTradeRunner.register_screen(
|
||||
bot=sent_message.bot,
|
||||
chat_id=sent_message.chat.id,
|
||||
message_id=sent_message.message_id,
|
||||
render_text=_build_auto_text,
|
||||
render_markup=_auto_keyboard,
|
||||
)
|
||||
|
||||
|
||||
# открыть экран из меню
|
||||
# открыть экран из главного меню
|
||||
@router.message(F.text.in_({"🤖 Автоторговля", "🤖 Авто"}))
|
||||
async def open_auto(message: Message, state: FSMContext) -> None:
|
||||
await state.clear()
|
||||
|
||||
AutoTradeRunner.set_current_screen("auto")
|
||||
|
||||
current_state = AutoTradeService().get_state()
|
||||
if current_state.status in {"RUNNING", "OBSERVING"}:
|
||||
await AutoTradeRunner.delete_registered_screen(
|
||||
bot=message.bot,
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
|
||||
await _render_auto_screen(message, edit_mode=False)
|
||||
|
||||
|
||||
# открыть экран callback
|
||||
# открыть экран через callback
|
||||
@router.callback_query(F.data == "auto:home")
|
||||
async def open_auto_from_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
await state.clear()
|
||||
@@ -121,6 +141,7 @@ async def open_auto_from_callback(callback: CallbackQuery, state: FSMContext) ->
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
AutoTradeRunner.set_current_screen("auto")
|
||||
await _render_auto_screen(callback.message, edit_mode=True)
|
||||
await callback.answer()
|
||||
|
||||
@@ -131,6 +152,9 @@ async def auto_start(callback: CallbackQuery) -> None:
|
||||
service = AutoTradeService()
|
||||
_, message = service.start()
|
||||
|
||||
AutoTradeRunner.set_current_screen("auto")
|
||||
AutoTradeRunner.start()
|
||||
|
||||
if callback.message is not None:
|
||||
await _render_auto_screen(callback.message, edit_mode=True)
|
||||
|
||||
@@ -143,6 +167,9 @@ async def auto_observe(callback: CallbackQuery) -> None:
|
||||
service = AutoTradeService()
|
||||
_, message = service.observe()
|
||||
|
||||
AutoTradeRunner.set_current_screen("auto")
|
||||
AutoTradeRunner.start()
|
||||
|
||||
if callback.message is not None:
|
||||
await _render_auto_screen(callback.message, edit_mode=True)
|
||||
|
||||
@@ -155,6 +182,8 @@ async def auto_stop(callback: CallbackQuery) -> None:
|
||||
service = AutoTradeService()
|
||||
_, message = service.stop()
|
||||
|
||||
AutoTradeRunner.stop()
|
||||
|
||||
if callback.message is not None:
|
||||
await _render_auto_screen(callback.message, edit_mode=True)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from src.telegram.handlers.journal_ui import (
|
||||
render_actions,
|
||||
)
|
||||
from src.trading.journal.service import JournalService
|
||||
from src.trading.auto.runner import AutoTradeRunner
|
||||
|
||||
|
||||
router = Router(name="journal")
|
||||
@@ -45,6 +46,7 @@ async def _show_journal_page(
|
||||
page: int,
|
||||
edit_mode: bool,
|
||||
) -> None:
|
||||
AutoTradeRunner.set_current_screen("journal")
|
||||
service = JournalService()
|
||||
|
||||
total = service.get_total_count()
|
||||
@@ -64,6 +66,7 @@ async def _show_journal_page(
|
||||
|
||||
@router.callback_query(F.data == "journal:actions")
|
||||
async def journal_actions(callback: CallbackQuery) -> None:
|
||||
AutoTradeRunner.set_current_screen("journal")
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
@@ -178,6 +181,7 @@ async def export_journal_xlsx(callback: CallbackQuery) -> None:
|
||||
|
||||
@router.callback_query(F.data == "journal:clear_confirm")
|
||||
async def clear_journal_confirm(callback: CallbackQuery) -> None:
|
||||
AutoTradeRunner.set_current_screen("journal")
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
@@ -16,6 +16,7 @@ from src.telegram.ui.exchange_error import (
|
||||
show_message_exchange_error,
|
||||
)
|
||||
from src.trading.journal.service import JournalService
|
||||
from src.trading.auto.runner import AutoTradeRunner
|
||||
|
||||
|
||||
router = Router(name="market")
|
||||
@@ -72,6 +73,8 @@ async def _render_market_screen(
|
||||
edit_mode: bool,
|
||||
action: str,
|
||||
) -> None:
|
||||
AutoTradeRunner.set_current_screen("market")
|
||||
|
||||
service = ExchangeService()
|
||||
journal = JournalService()
|
||||
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.journal.service import JournalService
|
||||
from src.trading.auto.runner import AutoTradeRunner
|
||||
|
||||
|
||||
router = Router(name="portfolio")
|
||||
@@ -70,6 +71,8 @@ async def _render_portfolio_screen(
|
||||
edit_mode: bool,
|
||||
action: str,
|
||||
) -> None:
|
||||
AutoTradeRunner.set_current_screen("portfolio")
|
||||
|
||||
service = AccountsService()
|
||||
exchange_service = ExchangeService()
|
||||
journal = JournalService()
|
||||
|
||||
@@ -12,6 +12,7 @@ from src.core.config import load_settings
|
||||
from src.core.constants import APP_NAME, APP_VERSION
|
||||
from src.trading.journal.service import JournalService
|
||||
from src.trading.auto.service import AutoTradeService
|
||||
from src.trading.auto.runner import AutoTradeRunner
|
||||
|
||||
|
||||
router = Router(name="system")
|
||||
@@ -44,6 +45,8 @@ async def _render_system_screen(
|
||||
chat_id: int | None,
|
||||
action: str,
|
||||
) -> None:
|
||||
AutoTradeRunner.set_current_screen("system")
|
||||
|
||||
journal = JournalService()
|
||||
|
||||
journal.log_ui_info(
|
||||
@@ -166,6 +169,7 @@ async def open_system_management(callback: CallbackQuery) -> None:
|
||||
|
||||
@router.callback_query(F.data == "settings:auto")
|
||||
async def open_auto_settings(callback: CallbackQuery) -> None:
|
||||
AutoTradeRunner.set_current_screen("settings_auto")
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
@@ -203,6 +207,7 @@ async def open_auto_settings(callback: CallbackQuery) -> None:
|
||||
|
||||
@router.callback_query(F.data == "settings:auto_strategy")
|
||||
async def open_auto_strategy_settings(callback: CallbackQuery) -> None:
|
||||
AutoTradeRunner.set_current_screen("settings_auto")
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
@@ -232,11 +237,13 @@ async def set_auto_strategy(callback: CallbackQuery) -> None:
|
||||
if callback.message is not None:
|
||||
await open_auto_settings(callback)
|
||||
|
||||
AutoTradeRunner.set_current_screen("settings_auto")
|
||||
await callback.answer("Стратегия обновлена")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "settings:auto_symbol")
|
||||
async def open_auto_symbol_settings(callback: CallbackQuery) -> None:
|
||||
AutoTradeRunner.set_current_screen("settings_auto")
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
@@ -268,11 +275,13 @@ async def set_auto_symbol(callback: CallbackQuery) -> None:
|
||||
if callback.message is not None:
|
||||
await open_auto_settings(callback)
|
||||
|
||||
AutoTradeRunner.set_current_screen("settings_auto")
|
||||
await callback.answer("Инструмент обновлён")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "settings:auto_risk")
|
||||
async def open_auto_risk_settings(callback: CallbackQuery) -> None:
|
||||
AutoTradeRunner.set_current_screen("settings_auto")
|
||||
if callback.message is None:
|
||||
await callback.answer("Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
@@ -302,6 +311,7 @@ async def set_auto_risk(callback: CallbackQuery) -> None:
|
||||
if callback.message is not None:
|
||||
await open_auto_settings(callback)
|
||||
|
||||
AutoTradeRunner.set_current_screen("settings_auto")
|
||||
await callback.answer("Риск обновлён")
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from src.telegram.handlers.trade.new_order import (
|
||||
show_recent_drafts,
|
||||
start_new_order_draft,
|
||||
)
|
||||
from src.trading.auto.runner import AutoTradeRunner
|
||||
|
||||
router = Router(name="trade_main")
|
||||
|
||||
@@ -96,6 +97,8 @@ def _trade_settings_text() -> str:
|
||||
|
||||
@router.message(F.text.in_({"📊 Торговля", "⚡ Торговля", "Торговля"}))
|
||||
async def open_trade(message: Message) -> None:
|
||||
AutoTradeRunner.set_current_screen("trade")
|
||||
|
||||
await message.answer(
|
||||
_trade_home_text(),
|
||||
reply_markup=_trade_home_keyboard(),
|
||||
@@ -107,6 +110,8 @@ async def open_trade_home_callback(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
AutoTradeRunner.set_current_screen("trade")
|
||||
|
||||
await state.clear()
|
||||
await callback.answer()
|
||||
|
||||
@@ -137,6 +142,8 @@ async def open_new_order_from_trade(
|
||||
|
||||
@router.callback_query(F.data == "trade:orders")
|
||||
async def open_orders_from_trade(callback: CallbackQuery) -> None:
|
||||
AutoTradeRunner.set_current_screen("trade")
|
||||
|
||||
await callback.answer()
|
||||
if callback.message is not None:
|
||||
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")
|
||||
async def open_trade_history(callback: CallbackQuery) -> None:
|
||||
AutoTradeRunner.set_current_screen("trade")
|
||||
|
||||
await callback.answer()
|
||||
if callback.message is not None:
|
||||
await callback.message.edit_text(
|
||||
@@ -196,6 +205,8 @@ async def open_canceled_history(callback: CallbackQuery) -> None:
|
||||
|
||||
@router.callback_query(F.data == "trade:settings")
|
||||
async def open_trade_settings(callback: CallbackQuery) -> None:
|
||||
AutoTradeRunner.set_current_screen("trade")
|
||||
|
||||
await callback.answer()
|
||||
if callback.message is not None:
|
||||
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
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
@@ -11,6 +12,8 @@ from src.trading.auto.state import AutoTradeState
|
||||
|
||||
class AutoTradeService:
|
||||
_state = AutoTradeState()
|
||||
_loop_task: asyncio.Task | None = None
|
||||
_loop_interval_seconds = 5
|
||||
|
||||
# получить текущее состояние автоторговли
|
||||
def get_state(self) -> AutoTradeState:
|
||||
@@ -18,18 +21,51 @@ class AutoTradeService:
|
||||
self._state.symbol = load_settings().default_symbol
|
||||
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]:
|
||||
state = self.get_state()
|
||||
|
||||
if state.status == "RUNNING":
|
||||
self.start_loop()
|
||||
return state, "Автоторговля уже активна."
|
||||
|
||||
if state.status == "OBSERVING":
|
||||
state.status = "RUNNING"
|
||||
self.start_loop()
|
||||
return state, "Автоторговля активирована."
|
||||
|
||||
state.status = "RUNNING"
|
||||
self.start_loop()
|
||||
return state, "Автоторговля запущена."
|
||||
|
||||
# включить режим наблюдения
|
||||
@@ -38,9 +74,11 @@ class AutoTradeService:
|
||||
previous_status = state.status
|
||||
|
||||
if previous_status == "OBSERVING":
|
||||
self.start_loop()
|
||||
return state, "Режим наблюдения уже включён."
|
||||
|
||||
state.status = "OBSERVING"
|
||||
self.start_loop()
|
||||
|
||||
if previous_status == "OFF":
|
||||
return state, "Включён режим наблюдения."
|
||||
@@ -52,9 +90,11 @@ class AutoTradeService:
|
||||
state = self.get_state()
|
||||
|
||||
if state.status == "OFF":
|
||||
self.stop_loop()
|
||||
return state, "Автоторговля уже выключена."
|
||||
|
||||
state.status = "OFF"
|
||||
self.stop_loop()
|
||||
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
|
||||
⏳ live cycle
|
||||
✔ asyncio runner
|
||||
✔ 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