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