import requests import html from loguru import logger from datetime import datetime class TelegramNotifier: """ Telegram 消息推送模块 支持信号推送、预警推送和市场异常提醒 """ def __init__(self, config: dict): self.config = config self.token = config.get("bot_token") self.chat_id = config.get("chat_id") self.proxy = config.get("proxy") self.session = requests.Session() if self.proxy: if not self.proxy.startswith("http"): self.proxy = f"http://{self.proxy}" self.session.proxies = {"http": self.proxy, "https": self.proxy} logger.info("Telegram 通知器初始化完成。") @staticmethod def _escape_html(text: str) -> str: """Escape HTML special characters""" if not isinstance(text, str): text = str(text) return html.escape(text, quote=False) def _send_message(self, text: str): """发送 Telegram 消息的主函数 (支持多个 ID)""" if not self.token or not self.chat_id: logger.warning("未配置 Telegram Token 或 Chat ID,无法发送消息。") return False # 支持逗号分隔的多个 ID chat_ids = str(self.chat_id).replace(" ", "").split(",") url = f"https://api.telegram.org/bot{self.token}/sendMessage" all_successful = True for cid in chat_ids: if not cid: continue payload = { "chat_id": cid, "text": text, "parse_mode": "HTML", "disable_web_page_preview": True, } try: response = self.session.post(url, json=payload, timeout=10) if response.status_code != 200: error_msg = response.text if "chat not found" in error_msg.lower(): logger.error( f"Telegram 消息发送给 {cid} 失败 (400): Chat ID {cid} 无效或机器人尚未被加入该聊天。请在 Telegram 中发送 /id 给机器人确认正确的 Chat ID。" ) else: logger.error( f"Telegram 消息发送给 {cid} 失败 ({response.status_code}): {error_msg}" ) all_successful = False else: logger.info(f"Telegram 消息发送给 {cid} 成功。") except Exception as e: logger.error(f"Telegram 消息发送给 {cid} 异常: {e}") all_successful = False return all_successful def send_signal( self, market_name: str, full_title: str, option: str, score: float, prediction: str, confidence: int, analysis_list: list, price: float, market_url: str, local_time: str = None, target_date: str = None, ): """发送交易信号推送""" stars = "⭐" * int(score) + "☆" * (5 - int(score)) timestamp_utc = datetime.utcnow().strftime("%H:%M") analysis_text = "\n".join( [ f"✅ {self._escape_html(item)}" if "✅" not in item else item for item in analysis_list ] ) local_time_text = ( f"🕒 当地时间: {self._escape_html(local_time)}\n" if local_time else "" ) target_date_text = self._escape_html(target_date) if target_date else "待定" text = ( f"🎯 交易信号 #{self._escape_html(market_name.split(' ')[0])}\n\n" f"📍 城市: {self._escape_html(market_name)}\n" f"🏆 市场: {self._escape_html(full_title)}\n" f"📝 选项: {self._escape_html(option)}\n" f"💰 当前价格: {price}¢\n" f"═══════════════════\n" f"📊 信号评分: {stars} ({score}/5)\n" f"🤖 模型预测: {self._escape_html(prediction)}\n" f"📈 置信度: {confidence}%\n\n" f"分析汇总:\n" f"{analysis_text}\n" f"═══════════════════\n" f"{local_time_text}" f"📅 结算日期: {target_date_text}\n" f"🔗 点击进入市场\n\n" f"⏰ 信号时间: {timestamp_utc} UTC" ) return self._send_message(text) def send_combined_alert( self, city: str, alerts: list, local_time: str = None, forecast_temp: str = None, total_volume: float = 0, brackets_count: int = 0, strategy_tips: list = None, metar_data: dict = None, ): """发送简约版合并预警 (含 METAR 航空气象数据)""" if not alerts: return from datetime import datetime, timedelta # UTC+8 北京时间 now_bj = datetime.utcnow() + timedelta(hours=8) timestamp_bj = now_bj.strftime("%H:%M") # 1. METAR 航空气象数据区块 metar_text = "" if metar_data and metar_data.get("current", {}).get("temp") is not None: icao = metar_data.get("icao", "N/A") temp = metar_data["current"]["temp"] unit = "°F" if metar_data.get("unit") == "fahrenheit" else "°C" # 解析观测时间 (格式: 2026-02-07T11:00:00.000Z) obs_time_raw = metar_data.get("observation_time", "") if "T" in obs_time_raw: obs_time = obs_time_raw.split("T")[1][:5] + " UTC" else: obs_time = obs_time_raw or "N/A" # 可选:风速信息 wind_kt = metar_data["current"].get("wind_speed_kt") wind_text = f" | 风速:{wind_kt}kt" if wind_kt else "" metar_text = ( f"✈️ 机场实测 ({icao}):\n" f" 🌡️ {temp:.1f}{unit}{wind_text}\n" f" 🕐 观测: {obs_time}\n\n" ) # 2. 信号详情构建 items_text = "" for a in alerts: items_text += f"{a['msg']}\n\n" # 3. 策略建议(如果有) tips_text = "" if strategy_tips: tips_text = ( "💡 策略建议:\n" + "\n".join([f"• {self._escape_html(tip)}" for tip in strategy_tips]) + "\n\n" ) # 4. 总体布局 text = ( f"🔔 城市监控报告 #{self._escape_html(city)}\n\n" f"📍 城市: {self._escape_html(city)}\n" f"{metar_text}" f"📊 实时异动:\n" f"{items_text}" f"{tips_text}" f"═══════════════════\n" f"🕒 当地时间: {self._escape_html(local_time or 'N/A')}\n" f"⏰ 预警时间: {timestamp_bj} (北京时间)" ) return self._send_message(text) def send_anomaly( self, city_tag: str, market_name: str, detected_anomaly: str, stats: dict, whales: list, current_price: float, local_time: str = None, ): """发送市场异常推送""" from datetime import datetime, timedelta # UTC+8 北京时间 timestamp_bj = (datetime.utcnow() + timedelta(hours=8)).strftime("%H:%M") whale_text = "\n".join([f"- {self._escape_html(w)}" for w in whales]) stats_text = "\n".join( [ f"{self._escape_html(k)}: {self._escape_html(v)}" for k, v in stats.items() ] ) local_time_text = ( f"🕒 当地时间: {self._escape_html(local_time)}\n" if local_time else "" ) text = ( f"👀 市场异常 #{self._escape_html(city_tag)}\n\n" f"📍 城市: {self._escape_html(city_tag)}\n" f"🏆 市场: {self._escape_html(market_name)}\n\n" f"🚨 检测到异常:\n" f"{self._escape_html(detected_anomaly)}\n" f"{stats_text}\n\n" f"🐋 大户动向:\n" f"{whale_text}\n\n" f"💰 当前价格: {current_price}¢\n" f"═══════════════════\n" f"{local_time_text}" f"⏰ 信号时间: {timestamp_bj} (北京时间)" ) return self._send_message(text) def send_alert( self, city_tag: str, market_name: str, price: float, trigger: str, prev_price: float, change: str, quick_analysis: list, local_time: str = None, ): """发送价格预警推送""" from datetime import datetime, timedelta # UTC+8 北京时间 timestamp_bj = (datetime.utcnow() + timedelta(hours=8)).strftime("%H:%M") analysis_text = "\n".join( [f"- {self._escape_html(item)}" for item in quick_analysis] ) local_time_text = ( f"🕒 当地时间: {self._escape_html(local_time)}\n" if local_time else "" ) text = ( f"⚡ 价格预警 #{self._escape_html(city_tag)}\n\n" f"📍 城市: {self._escape_html(city_tag)}\n" f"🏆 市场: {self._escape_html(market_name)}\n" f"💰 报价: {price}¢ ↗️\n\n" f"触发条件: {self._escape_html(trigger)}\n" f"变动详情: {prev_price}¢ -> {price}¢ ({self._escape_html(change)})\n\n" f"📊 快速分析:\n" f"{analysis_text}\n\n" f"═══════════════════\n" f"{local_time_text}" f"⏰ 预警时间: {timestamp_bj} (北京时间)" ) return self._send_message(text)