← Back
"""
Trading Signal Listener Bot (Web Scraping Edition)

Monitors Profit_GAME Telegram channel via public web preview (t.me/s/),
parses signals, fetches Binance data, and sends alerts to Rick via Telegram bot.

Supports /stats and /history commands.

No userbot login needed — zero risk of Telegram ban.

Usage:
  python bot.py
"""

import asyncio
import logging
import os
import re
import sys
from datetime import datetime, timezone, timedelta

import aiohttp
from bs4 import BeautifulSoup

from config import (
    MONITORED_CHANNELS,
    RICK_CHAT_ID,
    ALERT_BOT_TOKEN,
    MIN_MOVE_PERCENT,
    HIGH_PRIORITY_PERCENT,
    POLL_INTERVAL,
    TRADING_ENABLED,
    TRADE_SIGNALS,
    SKIP_TICKERS,
    DIGASH_ENABLED,
    DIGASH_SL_BUFFER_PCT,
    DIGASH_RR_TP1,
    DIGASH_RR_TP2,
    DIGASH_RR_TP3,
    DIGASH_ALLOWED_TIMEFRAMES,
)
from channel_parser import parse_profit_game_post, classify_signal
from binance_data import analyze_momentum, find_trading_pair
from digash_parser import parse_digash_message, determine_direction, calculate_sl_from_level
from history import (
    record_signal, format_stats_message, load_history, save_history,
    get_pending_checks, update_outcome,
)

# Auto-trading modules (lazy init)
position_manager = None
scalp_manager = None
gerchik_manager = None
tmm = None

# Watchlist: coins in approaching zone, waiting for cross
# { "BTCUSDT": { "side": "BUY", "added": datetime, "symbol": ..., "ticker": ... } }
watchlist = {}
WATCHLIST_MAX_AGE_HOURS = 4  # Remove after 4 hours
WATCHLIST_CHECK_INTERVAL = 30  # Check every 30 seconds

WATCHLIST_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "watchlist.json")


def save_watchlist():
    """Persist watchlist to file."""
    try:
        data = {}
        for sym, info in watchlist.items():
            data[sym] = {
                "ticker": info["ticker"],
                "symbol": info["symbol"],
                "side": info["side"],
                "added": info["added"].isoformat(),
                "wt1_15m": info.get("wt1_15m", 0),
                "wt1_1h": info.get("wt1_1h", 0),
                "zone": info.get("zone", ""),
            }
        with open(WATCHLIST_FILE, "w") as f:
            import json
            json.dump(data, f, indent=2)
    except Exception as e:
        logging.getLogger(__name__).error(f"Failed to save watchlist: {e}")


def load_watchlist():
    """Load watchlist from file on startup."""
    global watchlist
    if not os.path.exists(WATCHLIST_FILE):
        return
    try:
        import json
        with open(WATCHLIST_FILE, "r") as f:
            data = json.load(f)
        now = now_vancouver()
        for sym, info in data.items():
            added = datetime.fromisoformat(info["added"])
            age_hours = (now - added).total_seconds() / 3600
            if age_hours > WATCHLIST_MAX_AGE_HOURS:
                continue  # Skip expired
            watchlist[sym] = {
                "ticker": info["ticker"],
                "symbol": info["symbol"],
                "side": info["side"],
                "added": added,
                "wt1_15m": info.get("wt1_15m", 0),
                "wt1_1h": info.get("wt1_1h", 0),
                "zone": info.get("zone", ""),
            }
        if watchlist:
            logging.getLogger(__name__).info(f"Recovered {len(watchlist)} watchlist entries from file")
    except Exception as e:
        logging.getLogger(__name__).error(f"Failed to load watchlist: {e}")

# Logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(name)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("signal-bot")

# Vancouver timezone (PDT = UTC-7, PST = UTC-8)
VANCOUVER_TZ = timezone(timedelta(hours=-7))

# Track processed message IDs to avoid duplicates
seen_message_ids = set()

# Track last bot update_id for commands
last_update_id = 0


def now_vancouver() -> datetime:
    """Get current time in Vancouver."""
    return datetime.now(VANCOUVER_TZ)


async def fetch_channel_posts(session: aiohttp.ClientSession, channel: str) -> list[dict]:
    """Fetch latest posts from a public Telegram channel via web preview."""
    url = f"https://t.me/s/{channel}"
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    }

    try:
        async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as resp:
            if resp.status != 200:
                logger.warning(f"Failed to fetch {url}: HTTP {resp.status}")
                return []
            html = await resp.text()
    except Exception as e:
        logger.warning(f"Error fetching {url}: {e}")
        return []

    soup = BeautifulSoup(html, "html.parser")
    posts = []

    for widget in soup.select(".tgme_widget_message"):
        msg_id = widget.get("data-post", "")
        text_div = widget.select_one(".tgme_widget_message_text")
        text = text_div.get_text(separator="\n").strip() if text_div else ""

        # Extract timestamp
        time_el = widget.select_one("time")
        timestamp = time_el.get("datetime", "") if time_el else ""

        if msg_id and text:
            posts.append({
                "id": msg_id,
                "text": text,
                "timestamp": timestamp,
                "channel": channel,
            })

    return posts


async def send_telegram_alert(session: aiohttp.ClientSession, text: str):
    """Send alert to Rick via alert bot."""
    url = f"https://api.telegram.org/bot{ALERT_BOT_TOKEN}/sendMessage"
    payload = {
        "chat_id": RICK_CHAT_ID,
        "text": text,
    }

    try:
        async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=10)) as resp:
            if resp.status != 200:
                body = await resp.text()
                logger.error(f"Failed to send Telegram alert: {resp.status} {body}")
            else:
                logger.info("Alert sent to Rick ✅")
    except Exception as e:
        logger.error(f"Error sending Telegram alert: {e}")


async def process_digash_signal(session: aiohttp.ClientSession, text: str):
    """
    Process a forwarded Digash formation signal.

    Flow:
    1. Parse formation (type, symbol, levels, timeframe)
    2. Check timeframe is allowed
    3. Get current price from Binance
    4. Determine direction (BUY/SELL) from price vs level
    5. Calculate SL from level, TP from R:R
    6. Open trade via position_manager
    """
    parsed = parse_digash_message(text)
    if not parsed:
        await send_telegram_alert(session, "❌ Не удалось распарсить Digash формацию")
        return

    symbol = parsed["symbol"]
    ticker = parsed["ticker"]
    formation = parsed["formation"]
    levels = parsed["levels"]
    timeframe = parsed["timeframe"]

    # Check timeframe
    if timeframe not in DIGASH_ALLOWED_TIMEFRAMES:
        logger.info(f"Digash skip: {symbol} tf={timeframe} not in allowed {DIGASH_ALLOWED_TIMEFRAMES}")
        await send_telegram_alert(
            session,
            f"📐 Digash: {ticker} ({formation}, {timeframe})\n"
            f"⏭ Скип — таймфрейм {timeframe} не в списке ({', '.join(DIGASH_ALLOWED_TIMEFRAMES)})"
        )
        return

    # Check if trading is enabled and we have position manager
    if not TRADING_ENABLED or not position_manager:
        await send_telegram_alert(
            session,
            f"📐 Digash: {ticker} | {formation} | {timeframe}\n"
            f"Уровни: {', '.join(f'${l}' for l in levels) if levels else 'trendline'}\n"
            f"⚠️ Auto-trading выключен — ручной вход"
        )
        return

    # Skip if already have position
    if symbol in position_manager.positions:
        await send_telegram_alert(session, f"⏭ Digash: {ticker} — уже есть позиция")
        return

    # Get current price
    mark_price = position_manager.trader.get_mark_price(symbol)
    if not mark_price:
        await send_telegram_alert(session, f"❌ Digash: {ticker} — не удалось получить цену")
        return

    # Determine direction
    if formation == "trendline":
        # No level — use short-term momentum
        try:
            analysis = analyze_momentum(symbol, "futures")
            wt_15m = (analysis or {}).get("wavetrend") or {}
            wt_signal = wt_15m.get("signal", "neutral")
            if wt_signal in ("strong_buy", "buy", "weak_buy"):
                side = "BUY"
            elif wt_signal in ("strong_sell", "sell", "weak_sell"):
                side = "SELL"
            else:
                await send_telegram_alert(
                    session,
                    f"📐 Digash: {ticker} | trendline | {timeframe}\n"
                    f"⏭ Направление не определено (WT neutral)\n"
                    f"Цена: ${mark_price:.6f}"
                )
                return
        except Exception as e:
            logger.error(f"Digash momentum check error: {e}")
            await send_telegram_alert(
                session,
                f"📐 Digash: {ticker} | trendline | {timeframe}\n"
                f"⚠️ Ошибка определения направления"
            )
            return

        # For trendline: use fixed % SL/TP (same as WT strategy)
        from position_manager import calculate_levels
        fixed_levels = calculate_levels(mark_price, side)
        sl_price = fixed_levels["sl"]
        tp1_price = fixed_levels["tp1"]
        tp2_price = fixed_levels["tp2"]
        tp3_price = fixed_levels["tp3"]

    else:
        # Bounce or breakout — we have levels
        side = determine_direction(formation, levels, mark_price)
        if not side:
            await send_telegram_alert(
                session,
                f"📐 Digash: {ticker} | {formation} | {timeframe}\n"
                f"Уровни: {', '.join(f'${l}' for l in levels)}\n"
                f"Цена: ${mark_price:.6f}\n"
                f"⏭ Направление не определено (цена слишком близко к уровню)"
            )
            return

        # Calculate SL from level
        sl_price = calculate_sl_from_level(side, levels, formation, DIGASH_SL_BUFFER_PCT)
        if not sl_price or sl_price <= 0:
            await send_telegram_alert(session, f"❌ Digash: {ticker} — не удалось рассчитать SL")
            return

        # Calculate TP from R:R based on SL distance
        sl_distance = abs(mark_price - sl_price)

        if side == "BUY":
            tp1_price = mark_price + sl_distance * DIGASH_RR_TP1
            tp2_price = mark_price + sl_distance * DIGASH_RR_TP2
            tp3_price = mark_price + sl_distance * DIGASH_RR_TP3
        else:
            tp1_price = mark_price - sl_distance * DIGASH_RR_TP1
            tp2_price = mark_price - sl_distance * DIGASH_RR_TP2
            tp3_price = mark_price - sl_distance * DIGASH_RR_TP3

    # Sanity check: SL shouldn't be more than 5% away
    sl_pct = abs(mark_price - sl_price) / mark_price * 100
    if sl_pct > 5.0:
        await send_telegram_alert(
            session,
            f"📐 Digash: {ticker} | {formation} | {timeframe}\n"
            f"⚠️ SL слишком далеко ({sl_pct:.1f}%), скип\n"
            f"Цена: ${mark_price:.6f} | SL: ${sl_price:.6f}"
        )
        return

    if sl_pct < 0.2:
        await send_telegram_alert(
            session,
            f"📐 Digash: {ticker} | {formation} | {timeframe}\n"
            f"⚠️ SL слишком близко ({sl_pct:.2f}%), скип\n"
            f"Цена: ${mark_price:.6f} | SL: ${sl_price:.6f}"
        )
        return

    # Open the trade!
    logger.info(
        f"Digash trade: {side} {symbol} | {formation} | tf={timeframe} | "
        f"price=${mark_price:.6f} | SL=${sl_price:.6f} ({sl_pct:.2f}%)"
    )

    await position_manager.open_trade_with_levels(
        symbol=symbol,
        side=side,
        sl_price=sl_price,
        tp1_price=tp1_price,
        tp2_price=tp2_price,
        tp3_price=tp3_price,
        signal_data={
            "source": "digash",
            "formation": formation,
            "timeframe": timeframe,
            "levels": levels,
            "price_at_signal": mark_price,
            "ticker": ticker,
        },
    )


async def check_bot_commands(session: aiohttp.ClientSession):
    """Poll for bot commands (/stats, /history) and forwarded Digash signals."""
    global last_update_id

    url = f"https://api.telegram.org/bot{ALERT_BOT_TOKEN}/getUpdates"
    params = {"offset": last_update_id + 1, "timeout": 0, "limit": 10}

    try:
        async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=5)) as resp:
            if resp.status != 200:
                return
            data = await resp.json()
    except Exception:
        return

    if not data.get("ok"):
        return

    for update in data.get("result", []):
        update_id = update["update_id"]
        last_update_id = update_id

        msg = update.get("message", {})
        chat_id = msg.get("chat", {}).get("id")
        text = msg.get("text", "").strip()

        # Only respond to Rick
        if chat_id != RICK_CHAT_ID:
            continue

        # === Detect forwarded Digash formation messages ===
        # Forwarded messages have caption (photo+text) or text
        # Check for forward_from (bot) or forward_origin
        is_forward = "forward_from" in msg or "forward_origin" in msg or "forward_date" in msg
        caption = msg.get("caption", "").strip()

        if is_forward and DIGASH_ENABLED:
            # Try caption first (photo messages), then text
            fwd_text = caption or text
            if fwd_text and "Бинанс" in fwd_text:
                logger.info(f"Digash forwarded message detected: {fwd_text[:80]}")
                try:
                    await process_digash_signal(session, fwd_text)
                except Exception as e:
                    logger.error(f"Digash processing error: {e}", exc_info=True)
                    await send_telegram_alert(session, f"❌ Ошибка Digash: {e}")
                continue

        if text == "/stats":
            stats_msg = format_stats_message("all")
            await send_telegram_alert(session, stats_msg)
            logger.info("Stats sent to Rick")

        elif text == "/today":
            stats_msg = format_stats_message("today")
            await send_telegram_alert(session, stats_msg)
            logger.info("Today stats sent")

        elif text == "/week":
            stats_msg = format_stats_message("week")
            await send_telegram_alert(session, stats_msg)
            logger.info("Week stats sent")

        elif text == "/month":
            stats_msg = format_stats_message("month")
            await send_telegram_alert(session, stats_msg)
            logger.info("Month stats sent")

        elif text == "/history":
            history_msg = format_history_message()
            await send_telegram_alert(session, history_msg)
            logger.info("History sent to Rick")

        elif text == "/positions" or text == "/pos":
            parts = []
            if position_manager:
                parts.append(position_manager.format_positions_message())
            if scalp_manager:
                parts.append(scalp_manager.format_positions_message())
            if gerchik_manager:
                parts.append(gerchik_manager.format_positions_message())
            if parts:
                await send_telegram_alert(session, "\n\n".join(parts))
            else:
                await send_telegram_alert(session, "🤖 Auto-trading выключен")

        elif text.startswith("/close "):
            symbol = text.split(" ", 1)[1].strip().upper()
            if not symbol.endswith("USDT"):
                symbol += "USDT"
            if position_manager:
                ok = await position_manager.close_trade_manual(symbol)
                if not ok:
                    await send_telegram_alert(session, f"❌ Нет открытой позиции {symbol}")
            else:
                await send_telegram_alert(session, "🤖 Auto-trading выключен")

        elif text == "/watchlist" or text == "/wl":
            if watchlist:
                lines = ["👁 Watchlist:\n━━━━━━━━━━━━━━━━━━━━"]
                now = now_vancouver()
                for sym, info in watchlist.items():
                    age = (now - info["added"]).total_seconds() / 3600
                    lines.append(
                        f"{info['ticker']} ({info['side']}) | "
                        f"WT1={info.get('wt1_1h', 0):.0f} | "
                        f"{age:.1f}ч назад"
                    )
                lines.append("━━━━━━━━━━━━━━━━━━━━")
                await send_telegram_alert(session, "\n".join(lines))
            else:
                await send_telegram_alert(session, "👁 Watchlist пуст")

        elif text == "/pnl":
            if tmm and tmm.enabled:
                msg = await asyncio.to_thread(tmm.generate_pnl_by_strategy)
            else:
                msg = "📊 TMM не подключён — /pnl недоступен"
            await send_telegram_alert(session, msg)

        elif text == "/scalp" or text == "/qt":
            if scalp_manager:
                msg = scalp_manager.format_positions_message()
                await send_telegram_alert(session, msg)
            else:
                await send_telegram_alert(session, "⚡ Quick Take скальпер выключен")

        elif text.startswith("/scalp_close ") or text.startswith("/qc "):
            symbol = text.split(" ", 1)[1].strip().upper()
            if not symbol.endswith("USDT"):
                symbol += "USDT"
            if scalp_manager:
                ok = await scalp_manager.close_manual(symbol)
                if not ok:
                    await send_telegram_alert(session, f"❌ Нет скальп-позиции {symbol}")
            else:
                await send_telegram_alert(session, "⚡ Quick Take скальпер выключен")

        elif text == "/gerchik" or text == "/gr":
            if gerchik_manager:
                msg = gerchik_manager.format_positions_message()
                await send_telegram_alert(session, msg)
            else:
                await send_telegram_alert(session, "📐 Gerchik Levels выключен")

        elif text.startswith("/gc "):
            symbol = text.split(" ", 1)[1].strip().upper()
            if not symbol.endswith("USDT"):
                symbol += "USDT"
            if gerchik_manager:
                ok = await gerchik_manager.close_manual(symbol)
                if not ok:
                    await send_telegram_alert(session, f"❌ Нет Gerchik-позиции {symbol}")
            else:
                await send_telegram_alert(session, "📐 Gerchik Levels выключен")

        elif text.startswith("/levels"):
            parts = text.split(" ", 1)
            if len(parts) < 2:
                await send_telegram_alert(session, "Использование: /levels SYMBOL\nПример: /levels AAVE")
            else:
                symbol = parts[1].strip().upper()
                if not symbol.endswith("USDT"):
                    symbol += "USDT"
                try:
                    from gerchik_scanner import scan_levels_for_symbol, fetch_klines
                    from gerchik_models import get_trend_1h

                    trader_client = position_manager.trader.client if position_manager else None
                    if not trader_client:
                        await send_telegram_alert(session, "❌ Трейдер не инициализирован")
                    else:
                        levels = scan_levels_for_symbol(trader_client, symbol)
                        if not levels:
                            await send_telegram_alert(session, f"❌ Уровней для {symbol} не найдено")
                        else:
                            klines = fetch_klines(trader_client, symbol, "1h", 100)
                            price = klines["closes"][-1] if klines["closes"] else 0
                            trend = get_trend_1h(klines["closes"]) if klines["closes"] else "?"

                            lines = [
                                f"📐 Уровни {symbol}\n"
                                f"💰 Цена: ${price:.4f} | Тренд: {trend}\n"
                                f"━━━━━━━━━━━━━━━━━━━━"
                            ]
                            for l in levels[:8]:
                                dist = (l.price - price) / price * 100
                                marker = "🔴" if l.price > price else "🟢"
                                mirror = "🔀" if l.is_mirror else ""
                                fb = "💥" if l.has_false_breakout else ""
                                rnd = "⭕" if l.is_round else ""
                                lines.append(
                                    f"{marker} ${l.price:.4f} ({dist:+.2f}%)\n"
                                    f"   {l.level_type} | {l.touches}кас | 💪{l.strength:.0f} "
                                    f"{mirror}{fb}{rnd}"
                                )
                            lines.append("━━━━━━━━━━━━━━━━━━━━")
                            lines.append("🔀зеркальный 💥ложный пробой ⭕круглый")
                            await send_telegram_alert(session, "\n".join(lines))
                except Exception as e:
                    await send_telegram_alert(session, f"❌ Ошибка: {e}")

        elif text == "/digash":
            digash_status = "✅ ON" if DIGASH_ENABLED else "❌ OFF"
            tfs = ", ".join(DIGASH_ALLOWED_TIMEFRAMES) if DIGASH_ALLOWED_TIMEFRAMES else "none"
            await send_telegram_alert(
                session,
                f"📐 Digash Formations\n"
                f"━━━━━━━━━━━━━━━━━━━━\n"
                f"Статус: {digash_status}\n"
                f"Таймфреймы: {tfs}\n"
                f"SL buffer: {DIGASH_SL_BUFFER_PCT}%\n"
                f"R:R TP1/TP2/TP3: {DIGASH_RR_TP1}/{DIGASH_RR_TP2}/{DIGASH_RR_TP3}\n"
                f"━━━━━━━━━━━━━━━━━━━━\n"
                f"Форвардни сообщение от Digash бота сюда для авто-входа"
            )

        elif text == "/tmm":
            if tmm and tmm.enabled:
                summary = await asyncio.to_thread(tmm.generate_today_summary)
                await send_telegram_alert(session, summary)
            else:
                await send_telegram_alert(session, "📊 TMM journal не подключён")

        elif text == "/start":
            trading_status = "✅ ON" if TRADING_ENABLED else "❌ OFF"
            from scalp_manager import SCALP_ENABLED
            scalp_status = "✅ ON" if SCALP_ENABLED else "❌ OFF"
            from gerchik_config import GERCHIK_ENABLED
            gerchik_status = "✅ ON" if GERCHIK_ENABLED else "❌ OFF"
            tmm_status = "✅ ON" if (tmm and tmm.enabled) else "❌ OFF"
            await send_telegram_alert(
                session,
                f"🤖 Trading Signal Bot\n\n"
                f"📊 WT Strategy: {trading_status}\n"
                f"⚡ Quick Take Scalp: {scalp_status}\n"
                f"📐 Gerchik Levels: {gerchik_status}\n"
                f"📓 TMM Journal: {tmm_status}\n\n"
                f"Команды:\n"
                f"/positions — все позиции\n"
                f"/scalp — скальп позиции\n"
                f"/gerchik — Gerchik позиции\n"
                f"/close SYMBOL — закрыть WT\n"
                f"/qc SYMBOL — закрыть скальп\n"
                f"/gc SYMBOL — закрыть Gerchik\n"
                f"/levels SYMBOL — уровни по Герчику\n"
                f"/pnl — PnL все стратегии\n"
                f"/tmm — TMM weekly summary\n"
                f"/stats — статистика сигналов\n"
                f"/watchlist — вочлист WT"
            )

        elif text.startswith("/"):
            await send_telegram_alert(
                session,
                "Команды:\n"
                "/positions /scalp /gerchik\n"
                "/close /qc /gc SYMBOL\n"
                "/levels SYMBOL\n"
                "/pnl /stats /watchlist"
            )


def format_history_message(limit: int = 10) -> str:
    """Format last N signals for Telegram."""
    history = load_history()

    if not history:
        return "📜 История пуста — ещё не было сигналов"

    # Last N entries
    recent = history[-limit:]
    recent.reverse()  # newest first

    lines = [f"📜 Последние {min(limit, len(history))} сигналов:\n━━━━━━━━━━━━━━━━━━━━"]

    for h in recent:
        ts = h.get("timestamp", "")[:16]  # YYYY-MM-DDTHH:MM
        ticker = h.get("ticker", "?")
        price = h.get("price_at_signal", 0)
        wt_sig = h.get("wt_1h_signal", "n/a")
        wt_cross = h.get("wt_1h_cross")
        move_1h = h.get("move_1h", 0)

        # Signal emoji
        sig_map = {
            "strong_buy": "🟢🟢",
            "buy": "🟢",
            "weak_buy": "🟡",
            "strong_sell": "🔴🔴",
            "sell": "🔴",
            "weak_sell": "🟡",
            "approaching_buy": "🟢",
            "approaching_sell": "🔴",
            "neutral": "⚪",
        }
        emoji = sig_map.get(wt_sig, "⚪")

        cross_str = ""
        if wt_cross == "bullish_cross":
            cross_str = " ⬆️"
        elif wt_cross == "bearish_cross":
            cross_str = " ⬇️"

        line = f"{emoji} {ticker} ${price:.4f} | 1H: {move_1h:+.1f}%{cross_str} | {ts[5:]}"
        lines.append(line)

    lines.append("━━━━━━━━━━━━━━━━━━━━")
    return "\n".join(lines)


def format_wavetrend(wt: dict | None, timeframe: str = "15m") -> str:
    """Format WaveTrend data for alert."""
    if not wt:
        return f"  {timeframe}: n/a"

    wt1 = wt["wt1"]
    wt2 = wt["wt2"]
    signal = wt["signal"]
    zone = wt["zone"]
    cross = wt.get("cross")

    # Zone emoji (two-tier levels)
    zone_map = {
        "overbought_strong": "🔴🔴",
        "overbought": "🔴",
        "oversold_strong": "🟢🟢",
        "oversold": "🟢",
        "neutral": "⚪",
    }
    zone_str = zone_map.get(zone, "⚪")

    # Cross — the main signal
    cross_str = ""
    if cross == "bullish_cross":
        cross_str = "⬆️ CROSS UP"
    elif cross == "bearish_cross":
        cross_str = "⬇️ CROSS DOWN"

    # Signal verdict
    signal_map = {
        "strong_buy": "🟢🟢 STRONG BUY",
        "buy": "🟢 BUY",
        "weak_buy": "🟡 Weak buy (вне зоны)",
        "strong_sell": "🔴🔴 STRONG SELL",
        "sell": "🔴 SELL",
        "weak_sell": "🟡 Weak sell (вне зоны)",
        "approaching_buy": "🟢 В зоне OS, жду cross up",
        "approaching_sell": "🔴 В зоне OB, жду cross down",
        "neutral": "⚪ —",
    }
    signal_str = signal_map.get(signal, "⚪")

    line = f"  {timeframe}: {zone_str} WT1={wt1:.0f} | WT2={wt2:.0f}"
    if cross_str:
        line += f" {cross_str}"
    line += f"\n       {signal_str}"

    return line


def format_alert(signal: dict, analysis: dict, classification: dict) -> str:
    """Format a trading alert message for Telegram."""
    ticker = signal["ticker"]
    market = signal["market"] or "unknown"
    market_emoji = "📊" if market == "futures" else "🛒"

    price = analysis.get("price", 0)
    move_15m = analysis.get("move_15m", 0)
    move_1h = analysis.get("move_1h", 0)
    trend_15m = analysis.get("trend_15m", "?")
    trend_1h = analysis.get("trend_1h", "?")
    vol_ratio = analysis.get("volume_ratio", 0)
    vol_spike = "🔥 YES" if analysis.get("volume_spike") else "—"

    priority = classification["priority"]
    emoji = classification["emoji"]
    description = classification["description"]

    trend_emoji = {"up": "📈", "down": "📉", "flat": "➡️"}.get(trend_15m, "❓")
    trend_1h_emoji = {"up": "📈", "down": "📉", "flat": "➡️"}.get(trend_1h, "❓")

    now = now_vancouver()

    # WaveTrend section
    wt_15m = analysis.get("wavetrend")
    wt_1h = analysis.get("wavetrend_1h")

    wt_section = (
        f"\n🌊 WaveTrend:\n"
        f"{format_wavetrend(wt_15m, '15m')}\n"
        f"{format_wavetrend(wt_1h, '1H')}\n"
    )

    # WaveTrend verdict (1H is more reliable, use as primary)
    wt_verdict = ""
    best_wt = wt_1h or wt_15m
    if best_wt:
        sig = best_wt["signal"]
        if sig == "strong_buy":
            wt_verdict = "\n💡 WT: CROSS UP в зоне oversold → ЛОНГ"
        elif sig == "buy":
            wt_verdict = "\n💡 WT: CROSS UP в зоне oversold → Лонг"
        elif sig == "weak_buy":
            wt_verdict = "\n💡 WT: Cross up, но вне зоны — слабый сигнал"
        elif sig == "strong_sell":
            wt_verdict = "\n💡 WT: CROSS DOWN в зоне overbought → ШОРТ/ВЫХОД"
        elif sig == "sell":
            wt_verdict = "\n💡 WT: CROSS DOWN в overbought → Шорт/Выход"
        elif sig == "weak_sell":
            wt_verdict = "\n💡 WT: Cross down, но вне зоны — слабый сигнал"
        elif sig == "approaching_buy":
            if ticker in SKIP_TICKERS:
                wt_verdict = "\n💡 WT: В зоне oversold (skip — тяжёлая монета)"
            else:
                wt_verdict = "\n💡 WT: В зоне oversold, ждём cross up для входа"
        elif sig == "approaching_sell":
            if ticker in SKIP_TICKERS:
                wt_verdict = "\n💡 WT: В зоне overbought (skip — тяжёлая монета)"
            else:
                wt_verdict = "\n💡 WT: В зоне overbought, ждём cross down для выхода"

    msg = (
        f"{emoji} SIGNAL: {ticker}/USDT ({market.upper()})\n"
        f"━━━━━━━━━━━━━━━━━━━━\n"
        f"⏰ {now.strftime('%H:%M:%S')} Vancouver\n"
        f"{market_emoji} Market: {market.upper()}\n"
        f"💰 Price: ${price:.6f}\n"
        f"\n"
        f"📊 Movement:\n"
        f"  15m: {move_15m:+.2f}% {trend_emoji}\n"
        f"  1H:  {move_1h:+.2f}% {trend_1h_emoji}\n"
        f"  Vol: {vol_ratio:.1f}x avg {vol_spike}\n"
        f"{wt_section}"
        f"🎯 Priority: {priority}\n"
        f"📋 Strategy: {description}"
        f"{wt_verdict}\n"
        f"━━━━━━━━━━━━━━━━━━━━"
    )

    return msg


async def process_post(session: aiohttp.ClientSession, post: dict):
    """Process a single channel post."""
    text = post["text"]

    # Parse the signal
    signal = parse_profit_game_post(text)
    if not signal:
        return

    ticker = signal["ticker"]
    logger.info(f"Processing signal: {ticker} ({signal['market']})")

    # Find trading pair on Binance (spot + futures)
    found = find_trading_pair(ticker)
    if not found:
        logger.warning(f"No trading pair found for {ticker}")
        await send_telegram_alert(
            session,
            f"⚠️ {ticker} — актив в ИГРЕ, но пара не найдена на Binance (ни спот, ни фьючи)"
        )
        return

    pair, market_type = found
    logger.info(f"Found {pair} on {market_type}")

    # Override market type from channel with actual Binance market
    signal["market"] = market_type

    # Fetch Binance data
    analysis = analyze_momentum(pair, market_type)
    if not analysis["has_data"]:
        logger.warning(f"No Binance data for {pair}")
        await send_telegram_alert(
            session,
            f"⚠️ {ticker} ({pair}) — нет данных на Binance"
        )
        return

    # Get move from Binance
    move = analysis.get("move_15m", 0)
    move_1h = analysis.get("move_1h", 0)
    max_move = max(abs(move), abs(move_1h))

    classify_move = move_1h if abs(move_1h) > abs(move) else move
    classification = classify_signal(classify_move)

    # Filter noise
    if max_move < MIN_MOVE_PERCENT:
        logger.info(f"Skipping {ticker}: move {move:.1f}% / {move_1h:.1f}% < {MIN_MOVE_PERCENT}% threshold")
        return

    # Record to history
    record_signal(signal, analysis, classification)

    # Format and send alert
    alert = format_alert(signal, analysis, classification)
    await send_telegram_alert(session, alert)
    logger.info(f"Alert sent for {ticker} (priority: {classification['priority']})")

    # === Auto-trading hook ===
    # Primary: 15m WT cross → entry. Confirmation: 1H not opposing.
    if TRADING_ENABLED and position_manager and ticker not in SKIP_TICKERS:
        wt_15m = analysis.get("wavetrend") or {}
        wt_1h = analysis.get("wavetrend_1h") or {}
        wt_15m_signal = wt_15m.get("signal", "neutral")
        wt_1h_wt1 = wt_1h.get("wt1", 0)

        symbol = analysis.get("symbol", f"{ticker}USDT")
        is_futures = analysis.get("market_type") == "futures"

        # Log skipped weak signals for analytics
        if wt_15m_signal not in TRADE_SIGNALS and wt_15m_signal in (
            "weak_buy", "weak_sell", "strong_buy", "buy", "strong_sell", "sell"
        ) and is_futures:
            logger.info(f"⏭ Filtered out {ticker}: {wt_15m_signal} not in TRADE_SIGNALS")

        if wt_15m_signal in TRADE_SIGNALS and is_futures:
            # 15m has a cross — determine direction
            if wt_15m_signal in ("strong_buy", "buy", "weak_buy"):
                side = "BUY"
                confirms = wt_1h_wt1 < 20  # 1H not overbought
            else:
                side = "SELL"
                confirms = wt_1h_wt1 > -20  # 1H not oversold

            if confirms:
                logger.info(f"15m CROSS + 1H OK: 15m={wt_15m_signal}, 1H WT1={wt_1h_wt1:.0f}")
                await position_manager.open_trade(symbol, side, {
                    "ticker": ticker,
                    "wt_15m_signal": wt_15m_signal,
                    "wt_1h_signal": wt_1h.get("signal", "neutral"),
                    "wt1_15m": wt_15m.get("wt1", 0),
                    "wt1_1h": wt_1h_wt1,
                    "price": analysis.get("price", 0),
                })
            else:
                logger.info(f"15m CROSS but 1H opposes: 15m={wt_15m_signal} ({side}), 1H WT1={wt_1h_wt1:.0f}")
                await send_telegram_alert(session,
                    f"⏭ Skip {ticker}: 15m={wt_15m_signal} но 1H против (WT1={wt_1h_wt1:.0f})"
                )

        # Watchlist: 15m in approaching zone — monitor for cross
        elif wt_15m_signal in ("approaching_buy", "approaching_sell") and is_futures:
            if symbol not in watchlist and symbol not in position_manager.positions:
                expected_side = "BUY" if wt_15m_signal == "approaching_buy" else "SELL"
                watchlist[symbol] = {
                    "ticker": ticker,
                    "symbol": symbol,
                    "side": expected_side,
                    "added": now_vancouver(),
                    "wt1_15m": wt_15m.get("wt1", 0),
                    "wt1_1h": wt_1h_wt1,
                    "zone": wt_15m.get("zone", ""),
                }
                logger.info(f"Watchlist ADD: {symbol} ({expected_side}), 15m WT1={wt_15m.get('wt1', 0):.0f}")
                save_watchlist()
                await send_telegram_alert(session,
                    f"👁 Watchlist: {ticker} ({expected_side})\n"
                    f"15m WT1={wt_15m.get('wt1', 0):.0f} в зоне {wt_15m.get('zone', '')}\n"
                    f"Мониторю до cross (макс {WATCHLIST_MAX_AGE_HOURS}ч)"
                )


async def check_watchlist(session: aiohttp.ClientSession):
    """Check watchlist coins for WT cross — enter trade if cross appears."""
    global watchlist

    if not watchlist or not position_manager:
        return

    now = now_vancouver()
    to_remove = []

    for symbol, info in list(watchlist.items()):
        # Check age — remove stale entries
        age_hours = (now - info["added"]).total_seconds() / 3600
        if age_hours > WATCHLIST_MAX_AGE_HOURS:
            to_remove.append(symbol)
            logger.info(f"Watchlist EXPIRE: {symbol} ({age_hours:.1f}h)")
            continue

        # Skip if already have position
        if symbol in position_manager.positions:
            to_remove.append(symbol)
            continue

        # Fetch fresh WT data
        try:
            analysis = analyze_momentum(symbol, "futures")
            if not analysis.get("has_data"):
                continue

            wt_15m = analysis.get("wavetrend") or {}
            wt_1h = analysis.get("wavetrend_1h") or {}
            wt_15m_signal = wt_15m.get("signal", "neutral")
            wt_1h_wt1 = wt_1h.get("wt1", 0)

            expected_side = info["side"]

            # Check if 15m cross appeared!
            cross_buy = {"strong_buy", "buy", "weak_buy"}
            cross_sell = {"strong_sell", "sell", "weak_sell"}

            got_cross = False
            if expected_side == "BUY" and wt_15m_signal in cross_buy:
                got_cross = True
            elif expected_side == "SELL" and wt_15m_signal in cross_sell:
                got_cross = True

            if got_cross:
                # 1H confirmation: not opposing
                if expected_side == "BUY":
                    confirms = wt_1h_wt1 < 20
                else:
                    confirms = wt_1h_wt1 > -20

                if confirms:
                    logger.info(f"Watchlist CROSS: {symbol} {expected_side} — 15m={wt_15m_signal}, 1H WT1={wt_1h_wt1:.0f}")
                    await send_telegram_alert(session,
                        f"🎯 Watchlist CROSS: {info['ticker']} ({expected_side})\n"
                        f"15m: {wt_15m_signal} | 1H WT1={wt_1h_wt1:.0f}\n"
                        f"Ждали {age_hours:.1f}ч — ВХОДИМ!"
                    )
                    await position_manager.open_trade(symbol, expected_side, {
                        "ticker": info["ticker"],
                        "wt_15m_signal": wt_15m_signal,
                        "wt_1h_signal": wt_1h.get("signal", "neutral"),
                        "wt1_15m": wt_15m.get("wt1", 0),
                        "wt1_1h": wt_1h_wt1,
                        "price": analysis.get("price", 0),
                        "source": "watchlist",
                    })
                    to_remove.append(symbol)
                else:
                    logger.info(f"Watchlist CROSS but 1H opposes: {symbol} 15m={wt_15m_signal}, 1H WT1={wt_1h_wt1:.0f}")

            # Zone exit: only remove if WT1 moved far from zone (not just barely out)
            # BUY watchlist: remove if WT1 > -30 (well above oversold)
            # SELL watchlist: remove if WT1 < 30 (well below overbought)
            elif expected_side == "BUY" and wt_15m.get("wt1", 0) > -30:
                to_remove.append(symbol)
                logger.info(f"Watchlist REMOVE: {symbol} WT1={wt_15m.get('wt1', 0):.0f} left oversold zone")
            elif expected_side == "SELL" and wt_15m.get("wt1", 0) < 30:
                to_remove.append(symbol)
                logger.info(f"Watchlist REMOVE: {symbol} WT1={wt_15m.get('wt1', 0):.0f} left overbought zone")

        except Exception as e:
            logger.error(f"Watchlist check error for {symbol}: {e}")

    for symbol in to_remove:
        watchlist.pop(symbol, None)
    if to_remove:
        save_watchlist()


async def check_outcomes():
    """Check pending signal outcomes (price after 15m/1h/4h)."""
    from binance_data import check_futures_symbol_exists
    from binance.client import Client as BinanceClient

    pending = get_pending_checks()
    if not pending:
        return

    bc = BinanceClient()
    history = load_history()
    updated = False

    for item in pending:
        entry = item["entry"]
        age = item["age_minutes"]
        pair = entry.get("pair", "")

        if not pair:
            continue

        # Get current price
        try:
            ft = bc.futures_symbol_ticker(symbol=pair)
            current_price = float(ft["price"])
        except Exception:
            try:
                tk = bc.get_ticker(symbol=pair)
                current_price = float(tk["lastPrice"])
            except Exception:
                continue

        # Update timeframes that are due
        changed = False

        if not entry.get("checked_15m") and age >= 15:
            result = update_outcome(entry, "15m", current_price)
            logger.info(f"Outcome 15m for {entry['ticker']}: {result} ({entry.get('outcome_15m_pct', 0):+.2f}%)")
            changed = True

        if not entry.get("checked_1h") and age >= 60:
            result = update_outcome(entry, "1h", current_price)
            logger.info(f"Outcome 1H for {entry['ticker']}: {result} ({entry.get('outcome_1h_pct', 0):+.2f}%)")
            changed = True

        if not entry.get("checked_4h") and age >= 240:
            result = update_outcome(entry, "4h", current_price)
            logger.info(f"Outcome 4H for {entry['ticker']}: {result} ({entry.get('outcome_4h_pct', 0):+.2f}%)")
            changed = True

        if changed:
            # Update the entry in history list
            for i, h in enumerate(history):
                if h["id"] == entry["id"]:
                    history[i] = entry
                    break
            updated = True

    if updated:
        save_history(history)


# Counter for outcome checks (every 5 minutes, not every 30s)
outcome_check_counter = 0


async def _poll_loop_inner(session: aiohttp.ClientSession):
    """Inner poll loop that uses a provided session."""
    global seen_message_ids, outcome_check_counter

    # First run: load existing messages as "seen" (don't alert on old ones)
    for channel in MONITORED_CHANNELS:
        posts = await fetch_channel_posts(session, channel)
        for post in posts:
            seen_message_ids.add(post["id"])
        logger.info(f"Loaded {len(posts)} existing posts from @{channel} (marked as seen)")

    # Poll loop
    while True:
        try:
            # Check for bot commands
            await check_bot_commands(session)

            # Check channels for new signals
            for channel in MONITORED_CHANNELS:
                posts = await fetch_channel_posts(session, channel)

                new_posts = [p for p in posts if p["id"] not in seen_message_ids]

                for post in new_posts:
                    seen_message_ids.add(post["id"])
                    logger.info(f"New post from @{channel}: {post['text'][:80]}...")
                    await process_post(session, post)

            # Check watchlist for crosses (every cycle = 30s)
            if TRADING_ENABLED and watchlist:
                try:
                    await check_watchlist(session)
                except Exception as e:
                    logger.error(f"Watchlist check error: {e}")

            # Check outcomes every 5 min (10 cycles * 30s)
            outcome_check_counter += 1
            if outcome_check_counter >= 10:
                outcome_check_counter = 0
                try:
                    await check_outcomes()
                except Exception as e:
                    logger.error(f"Outcome check error: {e}")

            # Keep seen set reasonable
            if len(seen_message_ids) > 500:
                seen_message_ids = set(list(seen_message_ids)[-200:])

        except Exception as e:
            logger.error(f"Error in poll loop: {e}", exc_info=True)

        await asyncio.sleep(POLL_INTERVAL)


async def poll_loop():
    """Standalone poll loop (when trading is disabled)."""
    global seen_message_ids, outcome_check_counter

    logger.info("=" * 50)
    logger.info("Trading Signal Listener starting (signals only, no auto-trade)")
    logger.info(f"Monitoring channels: {MONITORED_CHANNELS}")
    logger.info(f"Poll interval: {POLL_INTERVAL}s")
    logger.info("=" * 50)

    async with aiohttp.ClientSession() as session:
        now = now_vancouver()
        await send_telegram_alert(
            session,
            f"🤖 Signal Listener запущен ({now.strftime('%H:%M')} Vancouver)\n"
            f"📡 Мониторю: {', '.join(MONITORED_CHANNELS)}\n"
            f"⏱ Интервал: {POLL_INTERVAL}s\n"
            f"💰 Auto-trading: OFF\n"
            f"\n"
            f"/stats /today /week /month /history"
        )

        await _poll_loop_inner(session)


async def _tmm_scheduled_reports(session):
    """
    Scheduled TMM reports:
    - Sunday 21:00 Vancouver → weekly summary
    - Last day of month 21:00 → monthly summary
    - Also retries pending tags every 30s
    """
    import calendar
    last_weekly_sent = None
    last_monthly_sent = None

    while True:
        try:
            now = now_vancouver()

            # Retry pending tags
            if tmm and tmm._pending_tags:
                await asyncio.to_thread(tmm.retry_pending_tags)

            # Sunday 21:00 Vancouver — weekly report
            if now.weekday() == 6 and now.hour == 21 and now.minute < 2:
                week_key = now.strftime("%Y-%W")
                if last_weekly_sent != week_key:
                    summary = await asyncio.to_thread(tmm.generate_weekly_summary)
                    await send_telegram_alert(session, f"📅 Авто-отчёт (неделя)\n\n{summary}")
                    last_weekly_sent = week_key
                    logger.info("TMM: weekly report sent")

            # Last day of month 21:00 — monthly report
            _, last_day = calendar.monthrange(now.year, now.month)
            if now.day == last_day and now.hour == 21 and now.minute < 2:
                month_key = now.strftime("%Y-%m")
                if last_monthly_sent != month_key:
                    summary = await asyncio.to_thread(tmm.generate_monthly_summary)
                    await send_telegram_alert(session, f"📅 Авто-отчёт (месяц)\n\n{summary}")
                    last_monthly_sent = month_key
                    logger.info("TMM: monthly report sent")

        except Exception as e:
            logger.error(f"TMM scheduled reports error: {e}")

        await asyncio.sleep(30)


async def main():
    """Main entry point — start signal listener + WT trader + Quick Take scalper."""
    global position_manager, scalp_manager, gerchik_manager, tmm

    if TRADING_ENABLED:
        try:
            from trader import BinanceFuturesTrader
            from position_manager import PositionManager
            from scalp_manager import ScalpManager, SCALP_ENABLED
            from gerchik_config import GERCHIK_ENABLED
            from gerchik_manager import GerchikManager

            from tmm_client import TMMClient

            trader = BinanceFuturesTrader()
            balance = trader.get_account_balance()
            logger.info(f"Auto-trading ENABLED | Balance: ${balance:.2f} USDT")

            # === Nuclear startup cleanup: cancel ALL orders BEFORE recovery ===
            # Prevents order accumulation from restarts (old algo orders survive cancel_all)
            cleaned = trader.cancel_all_account_orders()
            if cleaned:
                logger.info(f"Startup cleanup: cleared orders on {cleaned} symbols")

            # Init TMM journal
            tmm = TMMClient()

            # Create a shared session wrapper for notify function
            _notify_session = [None]

            async def notify_fn(text):
                if _notify_session[0]:
                    await send_telegram_alert(_notify_session[0], text)

            # Init WT position manager
            position_manager = PositionManager(trader, notify_fn, tmm=tmm)
            position_manager.recover_positions()
            load_watchlist()

            # Init Quick Take scalper
            if SCALP_ENABLED:
                scalp_manager = ScalpManager(trader, notify_fn, tmm=tmm)
                scalp_manager.recover_positions()
                logger.info("Quick Take scalper ENABLED")
            else:
                logger.info("Quick Take scalper DISABLED")

            # Init Gerchik Levels
            if GERCHIK_ENABLED:
                gerchik_manager = GerchikManager(trader, notify_fn, tmm=tmm)
                gerchik_manager.recover_positions()
                from gerchik_config import GERCHIK_ALLOWED_MODELS
                logger.info(f"Gerchik Levels strategy ENABLED (models: {','.join(sorted(GERCHIK_ALLOWED_MODELS))})")
            else:
                logger.info("Gerchik Levels strategy DISABLED")

            # Clean orphaned orders: cancel orders for Binance positions not tracked by any manager
            try:
                all_managed = set()
                if position_manager:
                    all_managed.update(position_manager.positions.keys())
                if scalp_manager:
                    all_managed.update(scalp_manager.positions.keys())
                if gerchik_manager:
                    all_managed.update(gerchik_manager.positions.keys())

                binance_positions = trader.client.futures_position_information()
                for bp in binance_positions:
                    sym = bp["symbol"]
                    amt = float(bp["positionAmt"])
                    if amt != 0 and sym not in all_managed:
                        trader.cancel_all_orders(sym)
                        logger.warning(f"Orphan cleanup: cancelled stale orders for untracked {sym} (qty={amt})")
            except Exception as e:
                logger.error(f"Orphan cleanup failed: {e}")

            # Run all loops concurrently
            async with aiohttp.ClientSession() as session:
                _notify_session[0] = session

                now = now_vancouver()
                from config import TRADE_SIZE_USDT, MAX_LEVERAGE, MAX_OPEN_POSITIONS
                from scalp_manager import SCALP_SIZE_USDT, SCALP_LEVERAGE, SCALP_MAX_POSITIONS, SCALP_TP_PCT, SCALP_SL_PCT
                from gerchik_config import (
                    GERCHIK_SIZE_USDT, GERCHIK_LEVERAGE, GERCHIK_MAX_POSITIONS,
                    GERCHIK_MIN_RR, GERCHIK_SCAN_INTERVAL,
                )

                scalp_line = ""
                if SCALP_ENABLED:
                    scalp_line = (
                        f"\n⚡ Quick Take Scalp: ON\n"
                        f"  ${SCALP_SIZE_USDT}/trade × {SCALP_LEVERAGE}x\n"
                        f"  TP: +{SCALP_TP_PCT}% | SL: -{SCALP_SL_PCT}%\n"
                        f"  Max: {SCALP_MAX_POSITIONS} positions\n"
                        f"  Scan: 40 pairs / 60s\n"
                    )

                gerchik_line = ""
                if GERCHIK_ENABLED:
                    gerchik_line = (
                        f"\n📐 Gerchik Levels: ON\n"
                        f"  ${GERCHIK_SIZE_USDT}/trade × {GERCHIK_LEVERAGE}x\n"
                        f"  Min R:R {GERCHIK_MIN_RR}:1 | 4 модели\n"
                        f"  Max: {GERCHIK_MAX_POSITIONS} positions\n"
                        f"  Scan: {GERCHIK_SCAN_INTERVAL}s\n"
                    )

                await send_telegram_alert(
                    session,
                    f"🤖 Trading Bot запущен\n"
                    f"⏰ {now.strftime('%H:%M')} Vancouver\n"
                    f"📡 Мониторю: {', '.join(MONITORED_CHANNELS)}\n"
                    f"\n"
                    f"📊 WT Strategy: ON\n"
                    f"  ${TRADE_SIZE_USDT}/trade × {MAX_LEVERAGE}x\n"
                    f"  Max: {MAX_OPEN_POSITIONS} positions\n"
                    f"  Balance: ${balance:.2f}"
                    f"{scalp_line}"
                    f"{gerchik_line}\n"
                    f"📋 /positions /scalp /gerchik /pnl"
                )

                # Build task list
                tasks = [
                    _poll_loop_inner(session),
                    position_manager.monitor_loop(),
                ]

                if SCALP_ENABLED and scalp_manager:
                    tasks.append(scalp_manager.monitor_loop())
                    tasks.append(scalp_manager.scan_loop(position_manager.positions))

                if GERCHIK_ENABLED and gerchik_manager:
                    tasks.append(gerchik_manager.monitor_loop())
                    scalp_pos = scalp_manager.positions if scalp_manager else {}
                    tasks.append(gerchik_manager.scan_loop(position_manager.positions, scalp_pos))

                # TMM scheduled reports loop
                if tmm and tmm.enabled:
                    tasks.append(_tmm_scheduled_reports(session))

                await asyncio.gather(*tasks)

        except Exception as e:
            logger.error(f"Failed to init auto-trading: {e}", exc_info=True)
            logger.info("Falling back to signal-only mode")
            await poll_loop()
    else:
        await poll_loop()


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        logger.info("Shutting down...")

📜 Git History

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