""" 飞书通知服务 - 通过 Webhook 发送交易信号通知 支持加密货币和股票两个独立的 webhook """ import json import httpx from typing import Dict, Any, Optional from app.utils.logger import logger from app.config import get_settings class FeishuService: """飞书机器人通知服务""" def __init__(self, webhook_url: str = "", service_type: str = "crypto"): """ 初始化飞书服务 Args: webhook_url: 飞书机器人 Webhook URL(如果为空,则根据 service_type 从配置读取) service_type: 服务类型 ("crypto" 或 "stock") """ settings = get_settings() # 如果传入了 webhook_url,直接使用 if webhook_url: self.webhook_url = webhook_url else: # 否则根据服务类型从配置读取 if service_type == "crypto": self.webhook_url = getattr(settings, 'feishu_crypto_webhook_url', '') elif service_type == "stock": self.webhook_url = getattr(settings, 'feishu_stock_webhook_url', '') elif service_type == "news": self.webhook_url = getattr(settings, 'feishu_news_webhook_url', '') else: # 兼容旧配置 self.webhook_url = getattr(settings, 'feishu_webhook_url', '') # 检查配置开关和 webhook_url 是否都有效 config_enabled = getattr(settings, 'feishu_enabled', True) self.enabled = config_enabled and bool(self.webhook_url) self.service_type = service_type if not config_enabled: logger.info(f"飞书通知已通过配置禁用") elif self.enabled: logger.info(f"飞书通知服务初始化完成 ({service_type})") else: logger.warning(f"飞书 Webhook URL 未配置 ({service_type}),通知功能已禁用") async def send_text(self, message: str) -> bool: """ 发送文本消息 Args: message: 消息内容 Returns: 是否发送成功 """ if not self.enabled: logger.warning("飞书服务未启用,跳过发送") return False data = { "msg_type": "text", "content": { "text": message } } return await self._send(data) async def send_card(self, title: str, content: str, color: str = "blue") -> bool: """ 发送卡片消息 Args: title: 卡片标题 content: 卡片内容(支持 Markdown) color: 标题颜色 (blue, green, red, orange, purple) Returns: 是否发送成功 """ if not self.enabled: logger.warning("飞书服务未启用,跳过发送") return False # 颜色映射 color_map = { "blue": "blue", "green": "green", "red": "red", "orange": "orange", "purple": "purple" } data = { "msg_type": "interactive", "card": { "header": { "title": { "tag": "plain_text", "content": title }, "template": color_map.get(color, "blue") }, "elements": [ { "tag": "markdown", "content": content } ] } } return await self._send(data) async def send_trading_signal(self, signal: Dict[str, Any]) -> bool: """ 发送交易信号卡片 Args: signal: 交易信号数据 - symbol: 交易对 - action: 'buy' | 'sell' - price: 当前价格 - trend: 趋势方向 - confidence: 信号强度 (0-100) - signal_type: 'swing' | 'short_term' - signal_grade: 'A' | 'B' | 'C' | 'D' - indicators: 技术指标数据 - llm_analysis: LLM 分析结果(可选) - stop_loss: 建议止损价 - take_profit: 建议止盈价 Returns: 是否发送成功 """ if not self.enabled: logger.warning("飞书服务未启用,跳过发送") 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_text = "📈 短线信号" if signal_type == 'short_term' else "📊 波段信号" type_hint = "(快进快出,建议轻仓)" if signal_type == 'short_term' else "(趋势跟踪,可适当持仓)" # 确定标题和颜色 if action == 'buy': title = f"🟢 买入信号 - {symbol} [{type_text}]" color = "green" action_text = "做多" elif action == 'sell': title = f"🔴 卖出信号 - {symbol} [{type_text}]" color = "red" action_text = "做空" else: title = f"⚪ 观望 - {symbol}" color = "blue" action_text = "观望" # 趋势文本 trend_text = { 'bullish': '看涨 📈', 'bearish': '看跌 📉', 'neutral': '震荡 ↔️' }.get(trend, '未知') # 等级图标 grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(signal_grade, '') type_short = "短线" if signal_type == 'short_term' else "波段" # 构建精简内容 - 突出核心交易信息 content_parts = [ f"**{type_short}** | {signal_grade}{grade_icon} | {confidence}% | {trend_text}", f"**入场**: ${price:,.2f}", ] # 止损止盈(核心点位) if stop_loss > 0: sl_percent = ((stop_loss - price) / price) * 100 content_parts.append(f"**止损**: ${stop_loss:,.2f} ({sl_percent:+.1f}%)") if take_profit > 0: tp_percent = ((take_profit - price) / price) * 100 content_parts.append(f"**止盈**: ${take_profit:,.2f} ({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: content_parts.append(f"**原因**: {' | '.join(clean_reasons)}") # AI 分析 if llm_analysis: analysis_text = llm_analysis[:100] + "..." if len(llm_analysis) > 100 else llm_analysis content_parts.append(f"**AI**: {analysis_text}") # 免责声明 content_parts.append("*⚠️ 仅供参考*") content = "\n".join(content_parts) return await self.send_card(title, content, color) 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': '震荡' } title = f"🔄 {symbol} 趋势变化" content = f"{trend_text.get(old_trend, old_trend)}{trend_emoji.get(old_trend, '')} → {trend_text.get(new_trend, new_trend)}{trend_emoji.get(new_trend, '')} | ${price:,.2f}" return await self.send_card(title, content, "orange") async def _send(self, data: Dict[str, Any]) -> bool: """ 发送消息到飞书 Args: data: 消息数据 Returns: 是否发送成功 """ try: async with httpx.AsyncClient() as client: response = await client.post( self.webhook_url, json=data, headers={"Content-Type": "application/json"}, timeout=10.0 ) result = response.json() if result.get('code') == 0 or result.get('StatusCode') == 0: logger.info("飞书消息发送成功") return True else: logger.error(f"飞书消息发送失败: {result}") return False except Exception as e: logger.error(f"飞书消息发送异常: {e}") return False # 全局实例(延迟初始化)- 分别用于加密货币、股票和新闻 _feishu_crypto_service: Optional[FeishuService] = None _feishu_stock_service: Optional[FeishuService] = None _feishu_news_service: Optional[FeishuService] = None def get_feishu_service() -> FeishuService: """获取飞书服务实例(默认 crypto,保持向后兼容)""" return get_feishu_crypto_service() def get_feishu_crypto_service() -> FeishuService: """获取加密货币飞书服务实例""" global _feishu_crypto_service if _feishu_crypto_service is None: _feishu_crypto_service = FeishuService(service_type="crypto") return _feishu_crypto_service def get_feishu_stock_service() -> FeishuService: """获取股票飞书服务实例""" global _feishu_stock_service if _feishu_stock_service is None: _feishu_stock_service = FeishuService(service_type="stock") return _feishu_stock_service def get_feishu_news_service() -> FeishuService: """获取新闻智能体飞书服务实例""" global _feishu_news_service if _feishu_news_service is None: _feishu_news_service = FeishuService(service_type="news") return _feishu_news_service