stock-ai-agent/backend/app/services/feishu_service.py
2026-02-06 09:30:15 +08:00

325 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
飞书通知服务 - 通过 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 = ""):
"""
初始化飞书服务
Args:
webhook_url: 飞书机器人 Webhook URL
"""
settings = get_settings()
self.webhook_url = webhook_url or getattr(settings, 'feishu_webhook_url', '')
self.enabled = bool(self.webhook_url)
if self.enabled:
logger.info("飞书通知服务初始化完成")
else:
logger.warning("飞书 Webhook URL 未配置,通知功能已禁用")
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, '')
# 构建内容
content_parts = [
f"**信号类型**: {type_text} {type_hint}",
f"**信号等级**: {signal_grade} {grade_icon}",
f"**当前价格**: ${price:,.2f}",
f"**趋势方向**: {trend_text}",
f"**置信度**: {confidence}%",
]
# 添加触发原因
if reasons:
content_parts.extend([
"",
"---",
"",
"**触发原因**:",
])
for reason in reasons[:5]: # 最多显示5个原因
content_parts.append(f"{reason}")
content_parts.extend([
"",
"---",
"",
"**技术指标**:"
])
# 添加技术指标
if indicators:
rsi = indicators.get('rsi', 0)
macd = indicators.get('macd', 0)
macd_signal = indicators.get('macd_signal', 0)
rsi_status = "超卖 ↑" if rsi < 30 else ("超买 ↓" if rsi > 70 else "中性")
macd_status = "金叉" if macd > macd_signal else "死叉"
content_parts.extend([
f"• RSI(14): {rsi:.1f} ({rsi_status})",
f"• MACD: {macd_status}",
])
if 'k' in indicators:
content_parts.append(f"• KDJ: K={indicators['k']:.1f}, D={indicators['d']:.1f}")
# 添加 LLM 分析
if llm_analysis:
content_parts.extend([
"",
"---",
"",
"**AI 分析**:",
llm_analysis[:200] + "..." if len(llm_analysis) > 200 else llm_analysis
])
# 添加止损止盈建议
if stop_loss > 0 or take_profit > 0:
content_parts.extend([
"",
"---",
"",
"**风险管理**:"
])
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}%)")
# 添加免责声明
content_parts.extend([
"",
"---",
"",
"*⚠️ 仅供参考,不构成投资建议*"
])
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"""**{symbol}** 趋势发生变化
**变化**: {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_service: Optional[FeishuService] = None
def get_feishu_service() -> FeishuService:
"""获取飞书服务实例"""
global _feishu_service
if _feishu_service is None:
_feishu_service = FeishuService()
return _feishu_service