515 lines
19 KiB
Python
515 lines
19 KiB
Python
"""
|
|
新闻流量预警系统模块
|
|
实现6种预警类型和通知推送
|
|
"""
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class NewsFlowAlertSystem:
|
|
"""新闻流量预警系统"""
|
|
|
|
# 预警类型定义
|
|
ALERT_TYPES = {
|
|
'heat_surge': {
|
|
'name': '热度飙升',
|
|
'level': 'warning',
|
|
'description': '流量得分超过阈值,市场热度异常升高',
|
|
},
|
|
'rank_change': {
|
|
'name': '排名变化',
|
|
'level': 'info',
|
|
'description': '热点排名快速变化',
|
|
},
|
|
'sentiment_extreme': {
|
|
'name': '情绪极值',
|
|
'level': 'warning',
|
|
'description': '情绪指数处于极端状态(过高或过低)',
|
|
},
|
|
'flow_peak': {
|
|
'name': '流量高潮',
|
|
'level': 'danger',
|
|
'description': '进入"一致"阶段,可能是逃命时刻',
|
|
},
|
|
'flow_decline': {
|
|
'name': '流量退潮',
|
|
'level': 'warning',
|
|
'description': '进入"退潮"阶段,注意止盈止损',
|
|
},
|
|
'viral_spread': {
|
|
'name': '病毒传播',
|
|
'level': 'warning',
|
|
'description': 'K值超过阈值,流量呈指数型增长',
|
|
},
|
|
}
|
|
|
|
# 预警级别定义
|
|
ALERT_LEVELS = {
|
|
'info': {'name': '提示', 'color': 'blue', 'priority': 1},
|
|
'warning': {'name': '警告', 'color': 'orange', 'priority': 2},
|
|
'danger': {'name': '危险', 'color': 'red', 'priority': 3},
|
|
}
|
|
|
|
def __init__(self):
|
|
"""初始化预警系统"""
|
|
self.db = None
|
|
self.notification_service = None
|
|
self._init_dependencies()
|
|
|
|
# 默认阈值配置
|
|
self.default_thresholds = {
|
|
'heat_threshold': 800,
|
|
'rank_change_threshold': 10,
|
|
'sentiment_high_threshold': 90,
|
|
'sentiment_low_threshold': 20,
|
|
'viral_k_threshold': 1.5,
|
|
}
|
|
|
|
def _init_dependencies(self):
|
|
"""初始化依赖"""
|
|
try:
|
|
from news_flow_db import news_flow_db
|
|
self.db = news_flow_db
|
|
except Exception as e:
|
|
logger.warning(f"数据库初始化失败: {e}")
|
|
|
|
try:
|
|
from notification_service import notification_service
|
|
self.notification_service = notification_service
|
|
except Exception as e:
|
|
logger.warning(f"通知服务初始化失败: {e}")
|
|
|
|
def get_threshold(self, key: str) -> float:
|
|
"""获取阈值配置"""
|
|
if self.db:
|
|
value = self.db.get_alert_config(key)
|
|
if value:
|
|
try:
|
|
return float(value)
|
|
except ValueError:
|
|
pass
|
|
return self.default_thresholds.get(key, 0)
|
|
|
|
def set_threshold(self, key: str, value: float):
|
|
"""设置阈值配置"""
|
|
if self.db:
|
|
self.db.set_alert_config(key, str(value))
|
|
|
|
def check_alerts(self, current_data: Dict,
|
|
history_data: Dict = None,
|
|
sentiment_data: Dict = None,
|
|
snapshot_id: int = None) -> List[Dict]:
|
|
"""
|
|
检查所有预警条件
|
|
|
|
Args:
|
|
current_data: 当前数据,包含flow_data, hot_topics等
|
|
history_data: 历史数据,用于比较
|
|
sentiment_data: 情绪数据
|
|
snapshot_id: 快照ID
|
|
|
|
Returns:
|
|
List[Dict]: 触发的预警列表
|
|
"""
|
|
alerts = []
|
|
|
|
flow_data = current_data.get('flow_data', {})
|
|
hot_topics = current_data.get('hot_topics', [])
|
|
viral_k = current_data.get('viral_k', {})
|
|
flow_stage = current_data.get('flow_stage', {})
|
|
|
|
# 1. 检查热度飙升
|
|
heat_alert = self._check_heat_surge(flow_data)
|
|
if heat_alert:
|
|
heat_alert['snapshot_id'] = snapshot_id
|
|
alerts.append(heat_alert)
|
|
|
|
# 2. 检查排名变化
|
|
if history_data:
|
|
rank_alert = self._check_rank_change(hot_topics,
|
|
history_data.get('hot_topics', []))
|
|
if rank_alert:
|
|
rank_alert['snapshot_id'] = snapshot_id
|
|
alerts.append(rank_alert)
|
|
|
|
# 3. 检查情绪极值
|
|
if sentiment_data:
|
|
sentiment_alert = self._check_sentiment_extreme(sentiment_data)
|
|
if sentiment_alert:
|
|
sentiment_alert['snapshot_id'] = snapshot_id
|
|
alerts.append(sentiment_alert)
|
|
|
|
# 4. 检查流量高潮(一致阶段)
|
|
peak_alert = self._check_flow_peak(flow_stage, sentiment_data)
|
|
if peak_alert:
|
|
peak_alert['snapshot_id'] = snapshot_id
|
|
alerts.append(peak_alert)
|
|
|
|
# 5. 检查流量退潮
|
|
decline_alert = self._check_flow_decline(flow_stage)
|
|
if decline_alert:
|
|
decline_alert['snapshot_id'] = snapshot_id
|
|
alerts.append(decline_alert)
|
|
|
|
# 6. 检查病毒传播
|
|
viral_alert = self._check_viral_spread(viral_k)
|
|
if viral_alert:
|
|
viral_alert['snapshot_id'] = snapshot_id
|
|
alerts.append(viral_alert)
|
|
|
|
# 按优先级排序
|
|
alerts.sort(key=lambda x: self.ALERT_LEVELS.get(
|
|
x.get('alert_level', 'info'), {}
|
|
).get('priority', 0), reverse=True)
|
|
|
|
# 保存预警到数据库
|
|
if self.db and alerts:
|
|
for alert in alerts:
|
|
self.db.save_alert(alert)
|
|
|
|
return alerts
|
|
|
|
def _check_heat_surge(self, flow_data: Dict) -> Optional[Dict]:
|
|
"""检查热度飙升"""
|
|
threshold = self.get_threshold('heat_threshold')
|
|
current_score = flow_data.get('total_score', 0)
|
|
|
|
if current_score >= threshold:
|
|
return {
|
|
'alert_type': 'heat_surge',
|
|
'alert_level': 'warning',
|
|
'title': f'热度飙升预警:流量得分{current_score}',
|
|
'content': f"当前流量得分{current_score},超过阈值{threshold}。"
|
|
f"市场热度异常升高,可能存在短期机会,但也要注意追高风险。",
|
|
'related_topics': [],
|
|
'trigger_value': current_score,
|
|
'threshold_value': threshold,
|
|
}
|
|
return None
|
|
|
|
def _check_rank_change(self, current_topics: List[Dict],
|
|
previous_topics: List[Dict]) -> Optional[Dict]:
|
|
"""检查排名变化"""
|
|
threshold = int(self.get_threshold('rank_change_threshold'))
|
|
|
|
if not previous_topics:
|
|
return None
|
|
|
|
# 建立之前的排名映射
|
|
prev_ranks = {t.get('topic', ''): i for i, t in enumerate(previous_topics)}
|
|
|
|
# 检查快速上升的话题
|
|
rapid_rise_topics = []
|
|
for i, topic in enumerate(current_topics[:20]):
|
|
topic_name = topic.get('topic', '')
|
|
if topic_name in prev_ranks:
|
|
rank_change = prev_ranks[topic_name] - i
|
|
if rank_change >= threshold:
|
|
rapid_rise_topics.append({
|
|
'topic': topic_name,
|
|
'current_rank': i + 1,
|
|
'previous_rank': prev_ranks[topic_name] + 1,
|
|
'change': rank_change,
|
|
})
|
|
|
|
if rapid_rise_topics:
|
|
topics_text = ', '.join([t['topic'] for t in rapid_rise_topics[:3]])
|
|
return {
|
|
'alert_type': 'rank_change',
|
|
'alert_level': 'info',
|
|
'title': f'排名变化提示:{topics_text}',
|
|
'content': f"{len(rapid_rise_topics)}个话题排名快速上升(上升{threshold}名以上),"
|
|
f"可能是新热点正在发酵。",
|
|
'related_topics': [t['topic'] for t in rapid_rise_topics],
|
|
'trigger_value': len(rapid_rise_topics),
|
|
'threshold_value': threshold,
|
|
}
|
|
return None
|
|
|
|
def _check_sentiment_extreme(self, sentiment_data: Dict) -> Optional[Dict]:
|
|
"""检查情绪极值"""
|
|
high_threshold = self.get_threshold('sentiment_high_threshold')
|
|
low_threshold = self.get_threshold('sentiment_low_threshold')
|
|
|
|
sentiment = sentiment_data.get('sentiment', {})
|
|
sentiment_index = sentiment.get('sentiment_index', 50)
|
|
sentiment_class = sentiment.get('sentiment_class', '中性')
|
|
|
|
if sentiment_index >= high_threshold:
|
|
return {
|
|
'alert_type': 'sentiment_extreme',
|
|
'alert_level': 'warning',
|
|
'title': f'情绪极值警告:{sentiment_class}({sentiment_index}分)',
|
|
'content': f"情绪指数{sentiment_index}分,处于极度乐观状态!"
|
|
f"根据'流量高潮=价格高潮'理论,市场可能接近顶部,注意及时止盈。",
|
|
'related_topics': [],
|
|
'trigger_value': sentiment_index,
|
|
'threshold_value': high_threshold,
|
|
}
|
|
elif sentiment_index <= low_threshold:
|
|
return {
|
|
'alert_type': 'sentiment_extreme',
|
|
'alert_level': 'warning',
|
|
'title': f'情绪极值警告:{sentiment_class}({sentiment_index}分)',
|
|
'content': f"情绪指数{sentiment_index}分,处于极度悲观状态!"
|
|
f"市场恐慌情绪蔓延,可能存在超跌反弹机会,但需谨慎左侧布局。",
|
|
'related_topics': [],
|
|
'trigger_value': sentiment_index,
|
|
'threshold_value': low_threshold,
|
|
}
|
|
return None
|
|
|
|
def _check_flow_peak(self, flow_stage: Dict,
|
|
sentiment_data: Dict = None) -> Optional[Dict]:
|
|
"""
|
|
检查流量高潮(逃命预警)
|
|
|
|
当以下条件同时满足时触发:
|
|
1. 流量阶段 = "一致"
|
|
2. 情绪指数 > 85
|
|
3. K值 > 1.5(可选)
|
|
"""
|
|
stage = flow_stage.get('stage', '')
|
|
stage_name = flow_stage.get('stage_name', '')
|
|
|
|
# 主要触发条件:一致阶段
|
|
if stage not in ['consensus', '一致']:
|
|
return None
|
|
|
|
# 增强条件检查
|
|
sentiment_index = 50
|
|
if sentiment_data:
|
|
sentiment = sentiment_data.get('sentiment', {})
|
|
sentiment_index = sentiment.get('sentiment_index', 50)
|
|
|
|
# 一致阶段就触发危险预警
|
|
return {
|
|
'alert_type': 'flow_peak',
|
|
'alert_level': 'danger',
|
|
'title': '⚠️ 流量高潮预警:准备跑路!',
|
|
'content': f"流量阶段进入【{stage_name}】!这是最危险的信号!\n\n"
|
|
f"根据'流量为王'理论:流量高潮 = 价格高潮 = 逃命时刻\n\n"
|
|
f"当热搜、媒体报道、KOL转发同时达到高潮时,就是出货时机。\n\n"
|
|
f"建议:立即减仓或清仓,锁定利润!",
|
|
'related_topics': [],
|
|
'trigger_value': stage_name,
|
|
'threshold_value': '一致阶段',
|
|
}
|
|
|
|
def _check_flow_decline(self, flow_stage: Dict) -> Optional[Dict]:
|
|
"""检查流量退潮"""
|
|
stage = flow_stage.get('stage', '')
|
|
stage_name = flow_stage.get('stage_name', '')
|
|
avg_growth = flow_stage.get('avg_growth', 0)
|
|
|
|
if stage not in ['decline', '退潮']:
|
|
return None
|
|
|
|
return {
|
|
'alert_type': 'flow_decline',
|
|
'alert_level': 'warning',
|
|
'title': f'流量退潮警告:及时止盈止损',
|
|
'content': f"流量阶段进入【{stage_name}】,增速{avg_growth}%。\n\n"
|
|
f"题材热度正在消退,资金开始撤离。\n\n"
|
|
f"建议:持仓者及时止盈止损,不要恋战。空仓者不要抄底接飞刀。",
|
|
'related_topics': [],
|
|
'trigger_value': avg_growth,
|
|
'threshold_value': '退潮阶段',
|
|
}
|
|
|
|
def _check_viral_spread(self, viral_k: Dict) -> Optional[Dict]:
|
|
"""检查病毒传播"""
|
|
threshold = self.get_threshold('viral_k_threshold')
|
|
k_value = viral_k.get('k_value', 1.0)
|
|
trend = viral_k.get('trend', '')
|
|
|
|
if k_value >= threshold:
|
|
return {
|
|
'alert_type': 'viral_spread',
|
|
'alert_level': 'warning',
|
|
'title': f'病毒传播预警:K值={k_value}',
|
|
'content': f"K值={k_value},趋势:{trend}\n\n"
|
|
f"流量正在指数型增长,这是病毒式传播的特征。\n\n"
|
|
f"题材可能进入加速期,但也要注意:\n"
|
|
f"- K值过高意味着接近顶部的风险增加\n"
|
|
f"- 指数型增长往往伴随着指数型下跌\n"
|
|
f"- 密切关注后续K值变化,一旦开始下降就是离场信号",
|
|
'related_topics': [],
|
|
'trigger_value': k_value,
|
|
'threshold_value': threshold,
|
|
}
|
|
return None
|
|
|
|
def send_notification(self, alerts: List[Dict]) -> bool:
|
|
"""
|
|
发送通知
|
|
|
|
Args:
|
|
alerts: 预警列表
|
|
|
|
Returns:
|
|
bool: 是否发送成功
|
|
"""
|
|
if not alerts:
|
|
return True
|
|
|
|
if not self.notification_service:
|
|
logger.warning("通知服务不可用")
|
|
return False
|
|
|
|
try:
|
|
# 按级别分组
|
|
danger_alerts = [a for a in alerts if a.get('alert_level') == 'danger']
|
|
warning_alerts = [a for a in alerts if a.get('alert_level') == 'warning']
|
|
info_alerts = [a for a in alerts if a.get('alert_level') == 'info']
|
|
|
|
# 构建通知内容
|
|
lines = []
|
|
lines.append("📊 新闻流量预警通知")
|
|
lines.append(f"时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
lines.append("")
|
|
|
|
if danger_alerts:
|
|
lines.append("🔴 【危险预警】")
|
|
for alert in danger_alerts:
|
|
lines.append(f" • {alert['title']}")
|
|
lines.append("")
|
|
|
|
if warning_alerts:
|
|
lines.append("🟠 【警告】")
|
|
for alert in warning_alerts:
|
|
lines.append(f" • {alert['title']}")
|
|
lines.append("")
|
|
|
|
if info_alerts:
|
|
lines.append("🔵 【提示】")
|
|
for alert in info_alerts:
|
|
lines.append(f" • {alert['title']}")
|
|
|
|
message = '\n'.join(lines)
|
|
|
|
# 发送通知
|
|
# 使用危险级别发送最高优先级预警
|
|
if danger_alerts:
|
|
subject = "⚠️ 新闻流量危险预警"
|
|
else:
|
|
subject = "📊 新闻流量预警通知"
|
|
|
|
# 调用通知服务
|
|
success = self.notification_service.send_analysis_result(
|
|
subject=subject,
|
|
content=message
|
|
)
|
|
|
|
# 标记为已通知
|
|
if success and self.db:
|
|
for alert in alerts:
|
|
if 'id' in alert:
|
|
self.db.mark_alert_notified(alert['id'])
|
|
|
|
return success
|
|
|
|
except Exception as e:
|
|
logger.error(f"发送通知失败: {e}")
|
|
return False
|
|
|
|
def get_alert_history(self, days: int = 7,
|
|
alert_type: str = None) -> List[Dict]:
|
|
"""获取预警历史"""
|
|
if self.db:
|
|
return self.db.get_alerts(days, alert_type)
|
|
return []
|
|
|
|
def get_unnotified_alerts(self) -> List[Dict]:
|
|
"""获取未通知的预警"""
|
|
if self.db:
|
|
return self.db.get_unnotified_alerts()
|
|
return []
|
|
|
|
def get_alert_summary(self, days: int = 7) -> Dict:
|
|
"""获取预警统计摘要"""
|
|
alerts = self.get_alert_history(days)
|
|
|
|
# 按类型统计
|
|
type_counts = {}
|
|
for alert in alerts:
|
|
alert_type = alert.get('alert_type', 'unknown')
|
|
type_counts[alert_type] = type_counts.get(alert_type, 0) + 1
|
|
|
|
# 按级别统计
|
|
level_counts = {}
|
|
for alert in alerts:
|
|
level = alert.get('alert_level', 'info')
|
|
level_counts[level] = level_counts.get(level, 0) + 1
|
|
|
|
return {
|
|
'total_count': len(alerts),
|
|
'type_counts': type_counts,
|
|
'level_counts': level_counts,
|
|
'danger_count': level_counts.get('danger', 0),
|
|
'warning_count': level_counts.get('warning', 0),
|
|
'info_count': level_counts.get('info', 0),
|
|
}
|
|
|
|
def get_threshold_config(self) -> Dict:
|
|
"""获取所有阈值配置"""
|
|
return {
|
|
'heat_threshold': self.get_threshold('heat_threshold'),
|
|
'rank_change_threshold': self.get_threshold('rank_change_threshold'),
|
|
'sentiment_high_threshold': self.get_threshold('sentiment_high_threshold'),
|
|
'sentiment_low_threshold': self.get_threshold('sentiment_low_threshold'),
|
|
'viral_k_threshold': self.get_threshold('viral_k_threshold'),
|
|
}
|
|
|
|
|
|
# 全局实例
|
|
news_flow_alert_system = NewsFlowAlertSystem()
|
|
|
|
|
|
# 测试代码
|
|
if __name__ == "__main__":
|
|
print("=== 测试预警系统 ===")
|
|
|
|
# 模拟数据
|
|
current_data = {
|
|
'flow_data': {'total_score': 850, 'level': '极高'},
|
|
'hot_topics': [
|
|
{'topic': 'AI芯片', 'heat': 95},
|
|
{'topic': '新能源', 'heat': 80},
|
|
],
|
|
'viral_k': {'k_value': 1.8, 'trend': '指数型爆发'},
|
|
'flow_stage': {'stage': 'consensus', 'stage_name': '一致', 'avg_growth': 35},
|
|
}
|
|
|
|
sentiment_data = {
|
|
'sentiment': {'sentiment_index': 92, 'sentiment_class': '极度乐观'},
|
|
}
|
|
|
|
history_data = {
|
|
'hot_topics': [
|
|
{'topic': '新能源', 'heat': 70},
|
|
{'topic': 'AI芯片', 'heat': 60},
|
|
],
|
|
}
|
|
|
|
# 检查预警
|
|
alerts = news_flow_alert_system.check_alerts(
|
|
current_data, history_data, sentiment_data
|
|
)
|
|
|
|
print(f"\n触发 {len(alerts)} 个预警:")
|
|
for alert in alerts:
|
|
level_info = NewsFlowAlertSystem.ALERT_LEVELS.get(alert['alert_level'], {})
|
|
print(f"\n[{level_info.get('name', alert['alert_level'])}] {alert['title']}")
|
|
print(f" {alert['content'][:100]}...")
|
|
|
|
# 获取阈值配置
|
|
print("\n当前阈值配置:")
|
|
config = news_flow_alert_system.get_threshold_config()
|
|
for key, value in config.items():
|
|
print(f" {key}: {value}")
|