stock-ai-agent/backend/app/services/feishu_service.py
2026-02-06 23:11:06 +08:00

279 lines
8.8 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', '')
# 检查配置开关和 webhook_url 是否都有效
config_enabled = getattr(settings, 'feishu_enabled', True)
self.enabled = config_enabled and bool(self.webhook_url)
if not config_enabled:
logger.info("飞书通知已通过配置禁用")
elif 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, '')
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_service: Optional[FeishuService] = None
def get_feishu_service() -> FeishuService:
"""获取飞书服务实例"""
global _feishu_service
if _feishu_service is None:
_feishu_service = FeishuService()
return _feishu_service