update
This commit is contained in:
parent
e408d1c9e4
commit
dba001e624
@ -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 # 分析间隔(秒)
|
||||
|
||||
@ -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]:
|
||||
|
||||
307
backend/app/services/telegram_service.py
Normal file
307
backend/app/services/telegram_service.py
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user