269 lines
6.7 KiB
Python
269 lines
6.7 KiB
Python
# app/src/trading/debug/runner.py
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import time
|
|
from collections.abc import Callable
|
|
from typing import ClassVar, Protocol
|
|
|
|
from aiogram import Bot
|
|
from aiogram.exceptions import TelegramBadRequest, TelegramRetryAfter
|
|
from aiogram.types import InlineKeyboardMarkup
|
|
|
|
from src.core.telegram_errors import (
|
|
is_message_not_modified,
|
|
is_message_to_edit_not_found,
|
|
)
|
|
from src.integrations.exchange.market_data_runner import MarketDataRunner
|
|
from src.notifications.targets import NotificationTargetRegistry
|
|
from src.trading.debug.service import DebugTradeService
|
|
|
|
|
|
class RenderText(Protocol):
|
|
def __call__(self) -> str: ...
|
|
|
|
|
|
class RenderMarkup(Protocol):
|
|
def __call__(self) -> InlineKeyboardMarkup | None: ...
|
|
|
|
|
|
|
|
class DebugTradeRunner:
|
|
_task: ClassVar[asyncio.Task[None] | None] = None
|
|
|
|
_bot: ClassVar[Bot | None] = None
|
|
_chat_id: ClassVar[int | None] = None
|
|
_message_id: ClassVar[int | None] = None
|
|
|
|
_text_renderer: ClassVar[RenderText | None] = None
|
|
_markup_renderer: ClassVar[RenderMarkup | None] = None
|
|
|
|
_current_screen: ClassVar[str | None] = None
|
|
|
|
_interval_seconds: ClassVar[int] = 5
|
|
_market_interval_seconds: ClassVar[int] = 1
|
|
|
|
_last_text: ClassVar[str | None] = None
|
|
_last_refresh_at: ClassVar[float] = 0.0
|
|
_retry_after_until: ClassVar[float] = 0.0
|
|
|
|
@classmethod
|
|
def register_screen(
|
|
cls,
|
|
*,
|
|
bot: Bot,
|
|
chat_id: int,
|
|
message_id: int,
|
|
render_text: RenderText,
|
|
render_markup: RenderMarkup,
|
|
) -> None:
|
|
cls._bot = bot
|
|
cls._chat_id = chat_id
|
|
cls._message_id = message_id
|
|
cls._text_renderer = render_text
|
|
cls._markup_renderer = render_markup
|
|
cls._last_text = None
|
|
|
|
NotificationTargetRegistry.set_default_chat(
|
|
bot=bot,
|
|
chat_id=chat_id,
|
|
)
|
|
|
|
@classmethod
|
|
def _reset_screen(cls) -> None:
|
|
cls._message_id = None
|
|
cls._text_renderer = None
|
|
cls._markup_renderer = None
|
|
cls._last_text = None
|
|
|
|
@classmethod
|
|
def _reset_runtime(cls) -> None:
|
|
cls._bot = None
|
|
cls._chat_id = None
|
|
cls._current_screen = None
|
|
cls._reset_screen()
|
|
|
|
@classmethod
|
|
def _is_screen_ready(cls) -> bool:
|
|
return (
|
|
cls._bot is not None
|
|
and cls._chat_id is not None
|
|
and cls._message_id is not None
|
|
and cls._text_renderer is not None
|
|
and cls._markup_renderer is not None
|
|
)
|
|
|
|
@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._reset_screen()
|
|
|
|
@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._reset_runtime()
|
|
|
|
@classmethod
|
|
def set_current_screen(cls, screen: str) -> None:
|
|
cls._current_screen = screen
|
|
|
|
@classmethod
|
|
def start(cls) -> None:
|
|
service = DebugTradeService()
|
|
state = service.get_state()
|
|
|
|
state.status = "RUNNING"
|
|
|
|
MarketDataRunner.start(
|
|
symbol_provider=lambda: DebugTradeService().get_state().symbol,
|
|
interval_seconds=cls._market_interval_seconds,
|
|
runtime_key="debug_auto",
|
|
screen="debug_auto",
|
|
action="market_data",
|
|
runtime_label="[DEBUG]",
|
|
)
|
|
|
|
cls._last_text = None
|
|
|
|
if cls._task is not None and not cls._task.done():
|
|
return
|
|
|
|
cls._task = asyncio.create_task(cls._worker())
|
|
|
|
@classmethod
|
|
def stop(cls) -> None:
|
|
MarketDataRunner.stop("debug_auto")
|
|
|
|
if cls._task is None:
|
|
return
|
|
|
|
cls._task.cancel()
|
|
cls._task = None
|
|
|
|
@classmethod
|
|
async def _worker(cls) -> None:
|
|
service = DebugTradeService()
|
|
|
|
while True:
|
|
state = service.get_state()
|
|
|
|
if state.status == "OFF":
|
|
cls._task = None
|
|
MarketDataRunner.stop("debug_auto")
|
|
break
|
|
|
|
service.process()
|
|
await cls.refresh_screen(force=False)
|
|
|
|
await asyncio.sleep(cls._interval_seconds)
|
|
|
|
@classmethod
|
|
async def refresh_screen(
|
|
cls,
|
|
*,
|
|
force: bool = False,
|
|
) -> None:
|
|
if cls._current_screen != "debug_auto":
|
|
return
|
|
|
|
now = time.monotonic()
|
|
|
|
if now < cls._retry_after_until:
|
|
return
|
|
|
|
if (
|
|
not force
|
|
and now - cls._last_refresh_at < cls._interval_seconds
|
|
):
|
|
return
|
|
|
|
if not cls._is_screen_ready():
|
|
return
|
|
|
|
bot = cls._bot
|
|
chat_id = cls._chat_id
|
|
message_id = cls._message_id
|
|
text_renderer = cls._text_renderer
|
|
markup_renderer = cls._markup_renderer
|
|
|
|
if (
|
|
bot is None
|
|
or chat_id is None
|
|
or message_id is None
|
|
or text_renderer is None
|
|
or markup_renderer is None
|
|
):
|
|
return
|
|
|
|
text = text_renderer()
|
|
|
|
if text == cls._last_text:
|
|
return
|
|
|
|
try:
|
|
await bot.edit_message_text(
|
|
chat_id=chat_id,
|
|
message_id=message_id,
|
|
text=text,
|
|
reply_markup=markup_renderer(),
|
|
)
|
|
|
|
cls._last_text = text
|
|
cls._last_refresh_at = now
|
|
|
|
except TelegramRetryAfter as exc:
|
|
cls._retry_after_until = time.monotonic() + exc.retry_after + 5
|
|
|
|
except TelegramBadRequest as exc:
|
|
if is_message_not_modified(exc):
|
|
cls._last_text = text
|
|
cls._last_refresh_at = now
|
|
return
|
|
|
|
if is_message_to_edit_not_found(exc):
|
|
cls._reset_screen()
|
|
return
|
|
|
|
except Exception:
|
|
pass |