288 lines
9.9 KiB
Python
288 lines
9.9 KiB
Python
"""
|
|
Telegram 通知服务 - 通过 Bot API 发送交易信号到频道
|
|
"""
|
|
import httpx
|
|
from typing import Dict, Any, Optional
|
|
from app.utils.logger import logger
|
|
from app.config import get_settings
|
|
|
|
|
|
class TelegramService:
|
|
"""Telegram 机器人通知服务"""
|
|
|
|
def __init__(self, bot_token: str = "", channel_id: str = "", service_type: str = "default"):
|
|
"""
|
|
初始化 Telegram 服务
|
|
|
|
Args:
|
|
bot_token: Telegram Bot Token (从 @BotFather 获取)
|
|
channel_id: 频道 ID (如 @your_channel 或 -1001234567890)
|
|
service_type: 服务类型 (crypto/stock/news/default)
|
|
"""
|
|
settings = get_settings()
|
|
self.bot_token = bot_token or getattr(settings, 'telegram_bot_token', '')
|
|
self.service_type = service_type
|
|
|
|
# 根据服务类型选择频道ID
|
|
if channel_id:
|
|
self.channel_id = channel_id
|
|
else:
|
|
# 根据service_type选择对应的频道
|
|
if service_type == "crypto":
|
|
self.channel_id = getattr(settings, 'telegram_crypto_channel_id', '') or getattr(settings, 'telegram_channel_id', '')
|
|
elif service_type == "stock":
|
|
self.channel_id = getattr(settings, 'telegram_stock_channel_id', '') or getattr(settings, 'telegram_channel_id', '')
|
|
else:
|
|
self.channel_id = getattr(settings, 'telegram_channel_id', '')
|
|
|
|
# 检查配置开关和必要参数是否都有效
|
|
config_enabled = getattr(settings, 'telegram_enabled', True)
|
|
self.enabled = config_enabled and bool(self.bot_token and self.channel_id)
|
|
|
|
if not config_enabled:
|
|
self.api_base = ""
|
|
logger.info("Telegram 通知已通过配置禁用")
|
|
elif self.enabled:
|
|
self.api_base = f"https://api.telegram.org/bot{self.bot_token}"
|
|
logger.info(f"Telegram 通知服务初始化完成 [{service_type}],频道: {self.channel_id}")
|
|
else:
|
|
self.api_base = ""
|
|
logger.warning(f"Telegram Bot Token 或 Channel ID 未配置 [{service_type}],通知功能已禁用")
|
|
|
|
async def send_message(self, text: str, parse_mode: str = "HTML") -> bool:
|
|
"""
|
|
发送文本消息
|
|
|
|
Args:
|
|
text: 消息内容 (支持 HTML 或 Markdown)
|
|
parse_mode: 解析模式 ("HTML" 或 "Markdown")
|
|
|
|
Returns:
|
|
是否发送成功
|
|
"""
|
|
if not self.enabled:
|
|
logger.warning("Telegram 服务未启用,跳过发送")
|
|
return False
|
|
|
|
data = {
|
|
"chat_id": self.channel_id,
|
|
"text": text,
|
|
"parse_mode": parse_mode,
|
|
"disable_web_page_preview": True
|
|
}
|
|
|
|
return await self._send("sendMessage", data)
|
|
|
|
async def send_trading_signal(self, signal: Dict[str, Any]) -> bool:
|
|
"""
|
|
发送交易信号消息
|
|
|
|
Args:
|
|
signal: 交易信号数据
|
|
|
|
Returns:
|
|
是否发送成功
|
|
"""
|
|
if not self.enabled:
|
|
logger.warning("Telegram 服务未启用,跳过发送")
|
|
return False
|
|
|
|
action = signal.get('action', 'hold')
|
|
symbol = signal.get('symbol', 'UNKNOWN')
|
|
price = signal.get('price', 0)
|
|
trend = signal.get('trend', 'neutral')
|
|
confidence = signal.get('confidence', 0)
|
|
signal_type = signal.get('signal_type', 'swing')
|
|
signal_grade = signal.get('signal_grade', 'D')
|
|
indicators = signal.get('indicators', {})
|
|
llm_analysis = signal.get('llm_analysis', '')
|
|
stop_loss = signal.get('stop_loss', 0)
|
|
take_profit = signal.get('take_profit', 0)
|
|
reasons = signal.get('reasons', [])
|
|
|
|
# 信号类型
|
|
type_emoji = "📈" if signal_type == 'short_term' else "📊"
|
|
type_text = "短线信号" if signal_type == 'short_term' else "波段信号"
|
|
type_hint = "快进快出,建议轻仓" if signal_type == 'short_term' else "趋势跟踪,可适当持仓"
|
|
|
|
# 动作
|
|
if action == 'buy':
|
|
action_emoji = "🟢"
|
|
action_text = "买入 / LONG"
|
|
elif action == 'sell':
|
|
action_emoji = "🔴"
|
|
action_text = "卖出 / SHORT"
|
|
else:
|
|
action_emoji = "⚪"
|
|
action_text = "观望 / WAIT"
|
|
|
|
# 趋势
|
|
trend_map = {
|
|
'bullish': '📈 看涨',
|
|
'bearish': '📉 看跌',
|
|
'neutral': '↔️ 震荡'
|
|
}
|
|
trend_text = trend_map.get(trend, '未知')
|
|
|
|
# 等级
|
|
grade_stars = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(signal_grade, '')
|
|
|
|
# 构建精简消息 - 突出核心交易信息
|
|
type_short = "短线" if signal_type == 'short_term' else "波段"
|
|
|
|
lines = [
|
|
f"{action_emoji} <b>{symbol} {action_text}</b>",
|
|
f"",
|
|
f"📊 {type_short} | {signal_grade}{grade_stars} | {confidence}% | {trend_text}",
|
|
f"💰 入场: <code>${price:,.2f}</code>",
|
|
]
|
|
|
|
# 止损止盈(核心点位信息)
|
|
if stop_loss > 0:
|
|
sl_percent = ((stop_loss - price) / price) * 100
|
|
lines.append(f"🛑 止损: <code>${stop_loss:,.2f}</code> ({sl_percent:+.1f}%)")
|
|
if take_profit > 0:
|
|
tp_percent = ((take_profit - price) / price) * 100
|
|
lines.append(f"🎯 止盈: <code>${take_profit:,.2f}</code> ({tp_percent:+.1f}%)")
|
|
|
|
# 触发原因(精简)
|
|
if reasons:
|
|
clean_reasons = []
|
|
for reason in reasons[:2]:
|
|
clean = reason.replace("📊 ", "").replace("📈 ", "").replace("📉 ", "").replace("波段信号: ", "").replace("短线", "").replace("超跌反弹", "").replace("超涨回落", "")
|
|
if clean and len(clean) < 20:
|
|
clean_reasons.append(clean)
|
|
if clean_reasons:
|
|
lines.append(f"📋 {' | '.join(clean_reasons)}")
|
|
|
|
# AI 分析(如果有)
|
|
if llm_analysis:
|
|
analysis_text = llm_analysis[:80] + "..." if len(llm_analysis) > 80 else llm_analysis
|
|
lines.append(f"🤖 {analysis_text}")
|
|
|
|
# 免责声明
|
|
lines.append(f"<i>⚠️ 仅供参考</i>")
|
|
|
|
message = "\n".join(lines)
|
|
return await self.send_message(message, parse_mode="HTML")
|
|
|
|
async def send_trend_change(self, symbol: str, old_trend: str, new_trend: str, price: float) -> bool:
|
|
"""
|
|
发送趋势变化通知
|
|
|
|
Args:
|
|
symbol: 交易对
|
|
old_trend: 旧趋势
|
|
new_trend: 新趋势
|
|
price: 当前价格
|
|
|
|
Returns:
|
|
是否发送成功
|
|
"""
|
|
trend_emoji = {
|
|
'bullish': '📈',
|
|
'bearish': '📉',
|
|
'neutral': '↔️'
|
|
}
|
|
trend_text = {
|
|
'bullish': '看涨',
|
|
'bearish': '看跌',
|
|
'neutral': '震荡'
|
|
}
|
|
|
|
old_emoji = trend_emoji.get(old_trend, '❓')
|
|
new_emoji = trend_emoji.get(new_trend, '❓')
|
|
old_text = trend_text.get(old_trend, old_trend)
|
|
new_text = trend_text.get(new_trend, new_trend)
|
|
|
|
message = f"""🔄 <b>{symbol}</b> 趋势变化
|
|
{old_text}{old_emoji} → {new_text}{new_emoji} | <code>${price:,.2f}</code>"""
|
|
|
|
return await self.send_message(message, parse_mode="HTML")
|
|
|
|
async def send_startup_notification(self, symbols: list) -> bool:
|
|
"""
|
|
发送启动通知
|
|
|
|
Args:
|
|
symbols: 监控的交易对列表
|
|
|
|
Returns:
|
|
是否发送成功
|
|
"""
|
|
message = f"""🚀 <b>加密货币智能体已启动</b>
|
|
|
|
━━━━━━━━━━━━━━━━━━━━
|
|
|
|
📊 <b>监控交易对:</b> {', '.join(symbols)}
|
|
⏰ <b>运行模式:</b> 每5分钟整点执行
|
|
|
|
━━━━━━━━━━━━━━━━━━━━
|
|
|
|
<i>开始监控市场信号...</i>"""
|
|
|
|
return await self.send_message(message, parse_mode="HTML")
|
|
|
|
async def _send(self, method: str, data: Dict[str, Any]) -> bool:
|
|
"""
|
|
发送请求到 Telegram API
|
|
|
|
Args:
|
|
method: API 方法名
|
|
data: 请求数据
|
|
|
|
Returns:
|
|
是否发送成功
|
|
"""
|
|
try:
|
|
url = f"{self.api_base}/{method}"
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.post(
|
|
url,
|
|
json=data,
|
|
timeout=10.0
|
|
)
|
|
|
|
result = response.json()
|
|
|
|
if result.get('ok'):
|
|
logger.info(f"Telegram 消息发送成功")
|
|
return True
|
|
else:
|
|
logger.error(f"Telegram 消息发送失败: {result.get('description', 'Unknown error')}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Telegram 消息发送异常: {e}")
|
|
return False
|
|
|
|
|
|
# 全局实例(延迟初始化)
|
|
_telegram_service: Optional[TelegramService] = None
|
|
_telegram_crypto_service: Optional[TelegramService] = None
|
|
_telegram_stock_service: Optional[TelegramService] = None
|
|
|
|
|
|
def get_telegram_service() -> TelegramService:
|
|
"""获取 Telegram 服务实例(默认)"""
|
|
global _telegram_service
|
|
if _telegram_service is None:
|
|
_telegram_service = TelegramService()
|
|
return _telegram_service
|
|
|
|
|
|
def get_telegram_crypto_service() -> TelegramService:
|
|
"""获取加密货币 Telegram 服务实例"""
|
|
global _telegram_crypto_service
|
|
if _telegram_crypto_service is None:
|
|
_telegram_crypto_service = TelegramService(service_type="crypto")
|
|
return _telegram_crypto_service
|
|
|
|
|
|
def get_telegram_stock_service() -> TelegramService:
|
|
"""获取股票 Telegram 服务实例"""
|
|
global _telegram_stock_service
|
|
if _telegram_stock_service is None:
|
|
_telegram_stock_service = TelegramService(service_type="stock")
|
|
return _telegram_stock_service
|