This commit is contained in:
aaron 2026-02-06 15:52:14 +08:00
parent e408d1c9e4
commit dba001e624
3 changed files with 328 additions and 9 deletions

View File

@ -96,6 +96,10 @@ class Settings(BaseSettings):
# 飞书机器人配置
feishu_webhook_url: str = "https://open.feishu.cn/open-apis/bot/v2/hook/8a1dcf69-6753-41e2-a393-edc4f7822db0"
# Telegram 机器人配置
telegram_bot_token: str = "" # 从 @BotFather 获取
telegram_channel_id: str = "" # 频道 ID如 @your_channel 或 -1001234567890
# 加密货币交易智能体配置
crypto_symbols: str = "BTCUSDT,ETHUSDT,BNBUSDT,SOLUSDT" # 监控的交易对,逗号分隔
crypto_analysis_interval: int = 60 # 分析间隔(秒)

View File

@ -10,6 +10,7 @@ from app.utils.logger import logger
from app.config import get_settings
from app.services.binance_service import binance_service
from app.services.feishu_service import get_feishu_service
from app.services.telegram_service import get_telegram_service
from app.crypto_agent.signal_analyzer import SignalAnalyzer
from app.crypto_agent.strategy import TrendFollowingStrategy
@ -22,6 +23,7 @@ class CryptoAgent:
self.settings = get_settings()
self.binance = binance_service
self.feishu = get_feishu_service()
self.telegram = get_telegram_service()
self.analyzer = SignalAnalyzer()
self.strategy = TrendFollowingStrategy()
@ -69,12 +71,13 @@ class CryptoAgent:
logger.info(f" LLM阈值: {self.llm_threshold * 100:.0f}%")
logger.info("=" * 60 + "\n")
# 发送启动通知
# 发送启动通知(飞书 + Telegram
await self.feishu.send_text(
f"🚀 加密货币智能体已启动\n"
f"监控交易对: {', '.join(self.symbols)}\n"
f"运行时间: 每5分钟整点 (:00, :05, :10, ...)"
)
await self.telegram.send_startup_notification(self.symbols)
while self.running:
try:
@ -149,12 +152,15 @@ class CryptoAgent:
logger.info(f" 方向: {trend_icon} | 强度: {trend_strength} | 阶段: {phase_text}")
# 3. 检查趋势变化
last_direction = self.last_trends.get(symbol, {})
if isinstance(last_direction, dict):
last_direction = last_direction.get('direction', 'neutral')
if last_direction and last_direction != trend_direction:
await self._handle_trend_change(symbol, last_direction, trend_direction, data)
# 3. 检查趋势变化(只有之前有记录时才检查,避免启动时误报)
if symbol in self.last_trends:
last_trend = self.last_trends[symbol]
if isinstance(last_trend, dict):
last_direction = last_trend.get('direction', 'neutral')
else:
last_direction = last_trend
if last_direction != trend_direction:
await self._handle_trend_change(symbol, last_direction, trend_direction, data)
self.last_trends[symbol] = trend
@ -246,16 +252,17 @@ class CryptoAgent:
else:
signal['llm_analysis'] = llm_result.get('summary', llm_result.get('raw', '')[:200])
# 8. 发送飞书通知(置信度仍然足够高时)
# 8. 发送通知(飞书 + Telegram置信度仍然足够高时)
if signal['confidence'] >= 50:
await self.feishu.send_trading_signal(signal)
await self.telegram.send_trading_signal(signal)
# 9. 更新状态
self.last_signals[symbol] = signal
self.signal_cooldown[symbol] = datetime.now()
action_text = '买入' if signal['action'] == 'buy' else '卖出'
logger.info(f" ✅ 已发送 {action_text} 信号通知")
logger.info(f" ✅ 已发送 {action_text} 信号通知(飞书+Telegram")
else:
logger.info(f" ⏸️ 置信度不足({signal['confidence']}%),不发送通知")
else:
@ -329,6 +336,7 @@ class CryptoAgent:
"""处理趋势变化"""
price = float(data['1h'].iloc[-1]['close'])
await self.feishu.send_trend_change(symbol, old_trend, new_trend, price)
await self.telegram.send_trend_change(symbol, old_trend, new_trend, price)
logger.info(f"{symbol} 趋势变化: {old_trend} -> {new_trend}")
async def analyze_once(self, symbol: str) -> Dict[str, Any]:

View File

@ -0,0 +1,307 @@
"""
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 = ""):
"""
初始化 Telegram 服务
Args:
bot_token: Telegram Bot Token ( @BotFather 获取)
channel_id: 频道 ID ( @your_channel -1001234567890)
"""
settings = get_settings()
self.bot_token = bot_token or getattr(settings, 'telegram_bot_token', '')
self.channel_id = channel_id or getattr(settings, 'telegram_channel_id', '')
self.enabled = bool(self.bot_token and self.channel_id)
if self.enabled:
self.api_base = f"https://api.telegram.org/bot{self.bot_token}"
logger.info(f"Telegram 通知服务初始化完成,频道: {self.channel_id}")
else:
self.api_base = ""
logger.warning("Telegram Bot Token 或 Channel ID 未配置,通知功能已禁用")
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, '')
# 构建消息
lines = [
f"{action_emoji} <b>{action_text}</b> - {symbol}",
f"",
f"━━━━━━━━━━━━━━━━━━━━",
f"",
f"{type_emoji} <b>信号类型:</b> {type_text}",
f"💡 <b>操作建议:</b> {type_hint}",
f"",
f"💰 <b>当前价格:</b> <code>${price:,.2f}</code>",
f"📊 <b>趋势方向:</b> {trend_text}",
f"🎯 <b>置信度:</b> {confidence}%",
f"⭐ <b>信号等级:</b> {signal_grade} {grade_stars}",
]
# 触发原因
if reasons:
lines.extend([
f"",
f"━━━━━━━━━━━━━━━━━━━━",
f"",
f"📋 <b>触发原因:</b>",
])
for reason in reasons[:5]:
# 清理 emoji 避免重复
clean_reason = reason.replace("📊 ", "").replace("📈 ", "").replace("📉 ", "")
lines.append(f"{clean_reason}")
# 技术指标
if indicators:
rsi = indicators.get('rsi', 0)
macd = indicators.get('macd', 0)
macd_signal_val = indicators.get('macd_signal', 0)
k = indicators.get('k', 0)
d = indicators.get('d', 0)
rsi_status = "超卖↑" if rsi < 30 else ("超买↓" if rsi > 70 else "中性")
macd_status = "金叉" if macd > macd_signal_val else "死叉"
lines.extend([
f"",
f"━━━━━━━━━━━━━━━━━━━━",
f"",
f"📉 <b>技术指标:</b>",
f" • RSI(14): {rsi:.1f} ({rsi_status})",
f" • MACD: {macd_status}",
])
if k > 0:
lines.append(f" • KDJ: K={k:.1f}, D={d:.1f}")
# 止损止盈
if stop_loss > 0 or take_profit > 0:
lines.extend([
f"",
f"━━━━━━━━━━━━━━━━━━━━",
f"",
f"🛡️ <b>风险管理:</b>",
])
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}%)")
# AI 分析
if llm_analysis:
analysis_text = llm_analysis[:150] + "..." if len(llm_analysis) > 150 else llm_analysis
lines.extend([
f"",
f"━━━━━━━━━━━━━━━━━━━━",
f"",
f"🤖 <b>AI 分析:</b>",
f"{analysis_text}",
])
# 免责声明
lines.extend([
f"",
f"━━━━━━━━━━━━━━━━━━━━",
f"",
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>
<b>变化:</b> {old_text} {old_emoji} {new_text} {new_emoji}
💰 <b>当前价格:</b> <code>${price:,.2f}</code>
<i>请关注后续交易信号</i>"""
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
def get_telegram_service() -> TelegramService:
"""获取 Telegram 服务实例"""
global _telegram_service
if _telegram_service is None:
_telegram_service = TelegramService()
return _telegram_service