← Назад
""" 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...")