← Back
"""
Squeeze-VWAP Bot — Telegram Bot
==================================
Команды для управления и мониторинга.
Использует python-telegram-bot v20+ (async).

Отличия от WT Bot:
- /watchlist вместо /whitelist
- /pos показывает Z-VWAP + score
- /status показывает Squeeze-VWAP стратегию
"""

import logging
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes

from src.config import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, MAX_POSITIONS

logger = logging.getLogger("bot")


class TelegramBot:
    def __init__(self, manager=None, screener=None, exchange=None, tmm=None):
        self.manager = manager
        self.screener = screener
        self.exchange = exchange
        self.tmm = tmm  # TMMClient
        self.z_manager = None  # ZatochkiManager — set from main.py
        self.app = None
        self._chat_id = TELEGRAM_CHAT_ID

    async def start(self):
        """Init and start the bot."""
        self.app = Application.builder().token(TELEGRAM_BOT_TOKEN).build()

        self.app.add_handler(CommandHandler("start", self._cmd_start))
        self.app.add_handler(CommandHandler("help", self._cmd_help))
        self.app.add_handler(CommandHandler("positions", self._cmd_positions))
        self.app.add_handler(CommandHandler("pos", self._cmd_positions))
        self.app.add_handler(CommandHandler("pnl", self._cmd_pnl))
        self.app.add_handler(CommandHandler("watchlist", self._cmd_watchlist))
        self.app.add_handler(CommandHandler("wl", self._cmd_watchlist))
        self.app.add_handler(CommandHandler("balance", self._cmd_balance))
        self.app.add_handler(CommandHandler("bal", self._cmd_balance))
        self.app.add_handler(CommandHandler("status", self._cmd_status))
        self.app.add_handler(CommandHandler("scan", self._cmd_scan))
        self.app.add_handler(CommandHandler("tmm", self._cmd_tmm))
        # Zatochki commands
        self.app.add_handler(CommandHandler("zstatus", self._cmd_zstatus))
        self.app.add_handler(CommandHandler("zstats", self._cmd_zstats))
        self.app.add_handler(CommandHandler("zpos", self._cmd_zstatus))

        await self.app.initialize()
        await self.app.start()
        await self.app.updater.start_polling(drop_pending_updates=True)
        logger.info("Telegram bot started")

    async def stop(self):
        """Stop the bot."""
        if self.app:
            await self.app.updater.stop()
            await self.app.stop()
            await self.app.shutdown()

    # ============================================================
    # NOTIFY
    # ============================================================

    async def send_message(self, text):
        """Send message to chat."""
        if self.app and self._chat_id:
            try:
                await self.app.bot.send_message(
                    chat_id=self._chat_id,
                    text=text,
                    parse_mode="Markdown",
                )
            except Exception as e:
                logger.error(f"Send message error: {e}")
                try:
                    await self.app.bot.send_message(
                        chat_id=self._chat_id,
                        text=text,
                    )
                except Exception:
                    pass

    # ============================================================
    # COMMANDS
    # ============================================================

    async def _cmd_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        await update.message.reply_text(
            "🤖 Squeeze-VWAP Bot — Active\n"
            "Strategy: Squeeze + Z-VWAP + Waddah 5m\n"
            "SL 1.5% / Dynamic TP (Z→fair value)\n\n"
            "Commands: /help"
        )

    async def _cmd_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        await update.message.reply_text(
            "📋 *Commands:*\n\n"
            "/positions (/pos) — open positions\n"
            "/pnl — PnL summary\n"
            "/watchlist (/wl) — current watchlist\n"
            "/balance (/bal) — USDT balance\n"
            "/status — bot status\n"
            "/scan — force market scan\n"
            "/tmm — TMM journal summary\n",
            parse_mode="Markdown",
        )

    async def _cmd_positions(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        if not self.manager:
            await update.message.reply_text("No manager")
            return
        info = self.manager.get_positions_info()
        try:
            await update.message.reply_text(info, parse_mode="Markdown")
        except Exception:
            await update.message.reply_text(info)

    async def _cmd_pnl(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        if not self.manager:
            await update.message.reply_text("No data")
            return
        summary = self.manager.get_pnl_summary()
        try:
            await update.message.reply_text(summary, parse_mode="Markdown")
        except Exception:
            await update.message.reply_text(summary)

    async def _cmd_watchlist(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        if not self.screener:
            await update.message.reply_text("No screener")
            return

        wl = self.screener.get_watchlist()
        if not wl:
            await update.message.reply_text("📭 Watchlist empty")
            return

        lines = [f"📋 *Watchlist ({len(wl)}):*\n"]
        for e in wl:
            dir_str = "🟢 LONG" if e.get("direction") == 1 else "🔴 SHORT"
            sqz = "SQZ" if e.get("is_squeeze") else ("REL" if e.get("squeeze_released") else "---")
            lines.append(
                f"*{e['symbol']}* {dir_str} score={e.get('score', '?')}/5\n"
                f"  Z={e.get('z_score', 0):+.2f} Sqz={sqz} "
                f"Wad={'STR' if e.get('waddah_strong') else 'low'}"
            )

        text = "\n".join(lines)
        try:
            await update.message.reply_text(text, parse_mode="Markdown")
        except Exception:
            await update.message.reply_text(text)

    async def _cmd_balance(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        if not self.exchange:
            await update.message.reply_text("No exchange")
            return
        try:
            balance = self.exchange.get_balance()
            positions = len(self.manager.positions) if self.manager else 0
            await update.message.reply_text(
                f"💰 *Balance:* ${balance:.2f} USDT\n"
                f"📊 Positions: {positions}/{MAX_POSITIONS}",
                parse_mode="Markdown",
            )
        except Exception as e:
            await update.message.reply_text(f"Error: {e}")

    async def _cmd_status(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        wl_count = len(self.screener.get_watchlist()) if self.screener else 0
        pos_count = len(self.manager.positions) if self.manager else 0

        await update.message.reply_text(
            f"🤖 *Squeeze-VWAP Bot Status*\n\n"
            f"Strategy: Squeeze + Z-VWAP + Waddah 5m\n"
            f"SL: 1.5% | TP: Dynamic (Z→0.5) | Cap: 3%\n"
            f"Leverage: 10x | Size: $5\n"
            f"Time stop: 15min | BE: +0.7%\n\n"
            f"Watchlist: {wl_count}\n"
            f"Positions: {pos_count}/{MAX_POSITIONS}\n"
            f"Status: ✅ Running",
            parse_mode="Markdown",
        )

    async def _cmd_scan(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Force a market scan."""
        if not self.screener:
            await update.message.reply_text("No screener")
            return
        await update.message.reply_text("🔍 Scanning...")
        try:
            new = self.screener.run_scan()
            wl = self.screener.get_watchlist()
            await update.message.reply_text(
                f"✅ Scan done\n"
                f"New: {len(new)} | Watchlist: {len(wl)}"
            )
        except Exception as e:
            await update.message.reply_text(f"❌ Scan error: {e}")

    async def _cmd_tmm(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """TMM journal summary."""
        if not self.tmm:
            await update.message.reply_text("TMM not configured")
            return
        try:
            summary = self.tmm.generate_summary()
            await update.message.reply_text(summary, parse_mode="Markdown")
        except Exception:
            try:
                summary = self.tmm.generate_summary()
                await update.message.reply_text(summary)
            except Exception as e:
                await update.message.reply_text(f"TMM error: {e}")

    # ============================================================
    # ZATOCHKI COMMANDS
    # ============================================================

    async def _cmd_zstatus(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Zatochki positions."""
        if not self.z_manager:
            await update.message.reply_text("Zatochki not initialized")
            return
        info = self.z_manager.get_positions_info()
        try:
            await update.message.reply_text(info, parse_mode="Markdown")
        except Exception:
            await update.message.reply_text(info)

    async def _cmd_zstats(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Zatochki trade stats."""
        if not self.z_manager:
            await update.message.reply_text("Zatochki not initialized")
            return
        stats = self.z_manager.get_stats()
        try:
            await update.message.reply_text(stats, parse_mode="Markdown")
        except Exception:
            await update.message.reply_text(stats)

📜 Git History

c6f6bd5chore: initial commit — version control setup5 weeks ago
Show last diff
Loading...