diff --git a/README.md b/README.md index 756dc13..ec93969 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ - 初心:在股市摸爬滚打多年,自学自编各种指标,花冤柉钱学习了各种战法各种策略,也曾入各种小班,总是赚少赔多,逐渐失去在股市玩的信心。自从去年deepseek上市,一直探索用ai辅助分析,且近日受tradingagents项目启发(感谢原作),多agent结合跟踪主力资金战法(某指每年收费6000rmb),用各种ai辅助编程,拼凑了这么个小程序,根据软件提供的辅助信息,实盘测试盈率还是挺高的,并且逐步形成了自己的交易系统,近一个月来,账户也慢慢在扰亏为盈。开源此软件的目的,就是为了使像我一样的小散,不再迷范。也许这个软件不能让你发大财,但是他能给你足够的信心。最后提醒:股市有风险,入市需谨慎! +## ⭐20261.25第一更 - 新闻流量监测 📈 + +实时监测百度、微博、东财、财联社、抖音、B站等20个平台热点新闻,调用ai分析对A股板块及股票的影响,生成分析报告 + ## ⭐ 1214更新 - 净利增长策略 📈 新增“净利增长策略”选股板块,专注稳健成长股票: diff --git a/app.py b/app.py index f030097..8636077 100644 --- a/app.py +++ b/app.py @@ -22,6 +22,7 @@ from main_force_ui import display_main_force_selector from sector_strategy_ui import display_sector_strategy from longhubang_ui import display_longhubang from smart_monitor_ui import smart_monitor_ui +from news_flow_ui import display_news_flow_monitor # 页面配置 st.set_page_config( @@ -294,7 +295,7 @@ def main(): if st.button("🏠 股票分析", width='stretch', key="nav_home", help="返回首页,进行单只股票的深度分析"): # 清除所有功能页面标志 for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', - 'show_sector_strategy', 'show_longhubang', 'show_portfolio', 'show_low_price_bull']: + 'show_sector_strategy', 'show_longhubang', 'show_portfolio', 'show_low_price_bull', 'show_news_flow']: if key in st.session_state: del st.session_state[key] @@ -307,28 +308,28 @@ def main(): if st.button("💰 主力选股", width='stretch', key="nav_main_force", help="基于主力资金流向的选股策略"): st.session_state.show_main_force = True for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy', - 'show_longhubang', 'show_portfolio', 'show_low_price_bull']: + 'show_longhubang', 'show_portfolio', 'show_low_price_bull', 'show_news_flow']: if key in st.session_state: del st.session_state[key] if st.button("🐂 低价擒牛", width='stretch', key="nav_low_price_bull", help="低价高成长股票筛选策略"): st.session_state.show_low_price_bull = True for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy', - 'show_longhubang', 'show_portfolio', 'show_main_force', 'show_small_cap', 'show_profit_growth']: + 'show_longhubang', 'show_portfolio', 'show_main_force', 'show_small_cap', 'show_profit_growth', 'show_news_flow']: if key in st.session_state: del st.session_state[key] if st.button("📊 小市值策略", width='stretch', key="nav_small_cap", help="小盘高成长股票筛选策略"): st.session_state.show_small_cap = True for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy', - 'show_longhubang', 'show_portfolio', 'show_main_force', 'show_low_price_bull', 'show_profit_growth']: + 'show_longhubang', 'show_portfolio', 'show_main_force', 'show_low_price_bull', 'show_profit_growth', 'show_news_flow']: if key in st.session_state: del st.session_state[key] if st.button("📈 净利增长", width='stretch', key="nav_profit_growth", help="净利润增长稳健股票筛选策略"): st.session_state.show_profit_growth = True for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy', - 'show_longhubang', 'show_portfolio', 'show_main_force', 'show_low_price_bull', 'show_small_cap']: + 'show_longhubang', 'show_portfolio', 'show_main_force', 'show_low_price_bull', 'show_small_cap', 'show_news_flow']: if key in st.session_state: del st.session_state[key] @@ -339,14 +340,21 @@ def main(): if st.button("🎯 智策板块", width='stretch', key="nav_sector_strategy", help="AI板块策略分析"): st.session_state.show_sector_strategy = True for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', - 'show_longhubang', 'show_portfolio', 'show_smart_monitor', 'show_low_price_bull']: + 'show_longhubang', 'show_portfolio', 'show_smart_monitor', 'show_low_price_bull', 'show_news_flow']: if key in st.session_state: del st.session_state[key] if st.button("🐉 智瞰龙虎", width='stretch', key="nav_longhubang", help="龙虎榜深度分析"): st.session_state.show_longhubang = True for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', - 'show_sector_strategy', 'show_portfolio', 'show_smart_monitor', 'show_low_price_bull']: + 'show_sector_strategy', 'show_portfolio', 'show_smart_monitor', 'show_low_price_bull', 'show_news_flow']: + if key in st.session_state: + del st.session_state[key] + + if st.button("📰 新闻流量", width='stretch', key="nav_news_flow", help="新闻流量监测与短线指导"): + st.session_state.show_news_flow = True + for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', + 'show_sector_strategy', 'show_portfolio', 'show_smart_monitor', 'show_low_price_bull', 'show_longhubang']: if key in st.session_state: del st.session_state[key] @@ -357,21 +365,21 @@ def main(): if st.button("📊 持仓分析", width='stretch', key="nav_portfolio", help="投资组合分析与定时跟踪"): st.session_state.show_portfolio = True for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', - 'show_sector_strategy', 'show_longhubang', 'show_smart_monitor', 'show_low_price_bull']: + 'show_sector_strategy', 'show_longhubang', 'show_smart_monitor', 'show_low_price_bull', 'show_news_flow']: if key in st.session_state: del st.session_state[key] if st.button("🤖 AI盯盘", width='stretch', key="nav_smart_monitor", help="DeepSeek AI自动盯盘决策交易(支持A股T+1)"): st.session_state.show_smart_monitor = True for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', - 'show_sector_strategy', 'show_longhubang', 'show_portfolio', 'show_low_price_bull']: + 'show_sector_strategy', 'show_longhubang', 'show_portfolio', 'show_low_price_bull', 'show_news_flow']: if key in st.session_state: del st.session_state[key] if st.button("📡 实时监测", width='stretch', key="nav_monitor", help="价格监控与预警提醒"): st.session_state.show_monitor = True for key in ['show_history', 'show_main_force', 'show_longhubang', 'show_portfolio', - 'show_config', 'show_sector_strategy', 'show_smart_monitor', 'show_low_price_bull']: + 'show_config', 'show_sector_strategy', 'show_smart_monitor', 'show_low_price_bull', 'show_news_flow']: if key in st.session_state: del st.session_state[key] @@ -381,7 +389,7 @@ def main(): if st.button("📖 历史记录", width='stretch', key="nav_history", help="查看历史分析记录"): st.session_state.show_history = True for key in ['show_monitor', 'show_longhubang', 'show_portfolio', 'show_config', - 'show_main_force', 'show_sector_strategy', 'show_low_price_bull']: + 'show_main_force', 'show_sector_strategy', 'show_low_price_bull', 'show_news_flow']: if key in st.session_state: del st.session_state[key] @@ -389,7 +397,7 @@ def main(): if st.button("⚙️ 环境配置", width='stretch', key="nav_config", help="系统设置与API配置"): st.session_state.show_config = True for key in ['show_history', 'show_monitor', 'show_main_force', 'show_sector_strategy', - 'show_longhubang', 'show_portfolio', 'show_low_price_bull']: + 'show_longhubang', 'show_portfolio', 'show_low_price_bull', 'show_news_flow']: if key in st.session_state: del st.session_state[key] @@ -521,6 +529,11 @@ def main(): display_portfolio_manager() return + # 检查是否显示新闻流量监测 + if 'show_news_flow' in st.session_state and st.session_state.show_news_flow: + display_news_flow_monitor() + return + # 检查是否显示环境配置 if 'show_config' in st.session_state and st.session_state.show_config: display_config_manager() diff --git a/docs/新闻流量监测功能说明.md b/docs/新闻流量监测功能说明.md new file mode 100644 index 0000000..abcb2ef --- /dev/null +++ b/docs/新闻流量监测功能说明.md @@ -0,0 +1,400 @@ +# 新闻流量监测功能说明 + +## 📰 功能概述 + +**新闻流量监测**是基于"流量为王"炒股理念为本系统新增的短线交易指导功能模块。通过实时监测20+主流平台的新闻热点和流量数据,帮助短线投资者把握题材炒作的节奏,识别流量高潮(卖出信号)和流量爆发(买入机会)。 + +--- + +## 🎯 核心理念 + +### "流量为王"投机市场规律 + +``` +接盘总量 = 流量 × 转化率 × 客单价 +``` + +**关键理论:** +1. **流量高潮 = 价格高潮 = 逃命时刻** +2. **流量爆发 = 题材启动 = 进场机会** +3. **流量衰减 = 资金离场 = 减仓信号** + +### 两种流量类型 + +**1. 存量流量型**(快速爆发) +- 特征:顶级流量,快速到位,不需要发酵 +- 案例:政策事件、突发新闻、知名人物发声 +- 时间窗口:短(几小时到几天) +- 策略:手快吃肉,手慢接盘 + +**2. 增量流量型**(裂变传播) +- 特征:初始流量小,有病毒传播能力,指数增长 +- 判断指标:K值(病毒系数)>1.5为指数型爆发 +- 时间窗口:长(几天到几周) +- 策略:埋伏早期,分批进出 + +--- + +## 🚀 功能特性 + +### 1. 多平台数据聚合 + +**支持20+主流平台:** + +| 类别 | 平台 | 权重 | 说明 | +|------|------|------|------| +| 社交媒体 | 微博热搜 | 10 | 核心流量指标 | +| 社交媒体 | 抖音热点 | 9 | 短视频流量 | +| 社交媒体 | 知乎热榜 | 7 | 深度内容流量 | +| 财经平台 | 新浪财经 | 9 | 股市相关 | +| 财经平台 | 东方财富 | 9 | 散户关注度 | +| 财经平台 | 雪球 | 8 | 投资者讨论 | +| 财经平台 | 财联社 | 8 | 专业资讯 | +| 新闻媒体 | 百度热搜 | 8 | 全民热点 | +| 新闻媒体 | 今日头条 | 7 | 算法推荐 | + +### 2. 智能流量分析 + +- **流量得分计算**:综合权重×新闻数量,归一化到0-1000分 +- **流量等级判断**: + - 极高(≥800分):流量爆发,警惕见顶 + - 高(500-800分):流量活跃,关注机会 + - 中(200-500分):流量正常,保持观望 + - 低(<200分):流量低迷,空仓为主 +- **历史对比**:计算当前得分的百分位,了解历史水平 + +### 3. 股票相关新闻提取 + +- 自动识别40+股票关键词(涨停、跌停、概念股、龙头股等) +- 按平台权重和关键词匹配数排序 +- 提取高频提及的个股代码 +- 生成潜力个股TOP10 + +### 4. 热门话题挖掘 + +- 基于jieba分词的话题提取 +- 过滤停用词,保留有效话题 +- 统计话题热度(0-100分) +- 识别热门板块(AI、新能源、半导体等) + +### 5. 交易信号生成 + +**基于流量等级的交易信号:** + +| 流量等级 | 得分范围 | 信号 | 信心度 | 操作建议 | +|----------|----------|------|--------|----------| +| 极高(顶部) | ≥900 | 卖出 | 9/10 | 立即减仓或清仓,锁定利润 | +| 极高(高位) | 800-900 | 观望 | 7/10 | 持仓观望,逢高减仓 | +| 高 | 500-800 | 买入 | 8/10 | 关注龙头股,轻仓试探 | +| 中 | 200-500 | 观望 | 5/10 | 保持观望,等待信号明确 | +| 低 | <200 | 观望 | 3/10 | 空仓观望,等待市场转暖 | + +**输出内容:** +- 整体信号(买入/观望/卖出) +- 信心度(0-10分) +- 热门板块TOP3 +- 潜力个股TOP10 +- 风险提示 +- 操作建议 + +### 6. 流量趋势分析 + +- 7-30天流量趋势图 +- 平均/最高/最低流量得分 +- 趋势判断(上升/下降/平稳) +- 每日统计汇总 + +### 7. 历史记录管理 + +- 完整的监测历史保存 +- 快照详情查看(股票新闻、热门话题) +- 历史数据对比分析 +- 支持按关键词搜索 + +--- + +## 💻 技术架构 + +### 模块组成 + +``` +news_flow_data.py # 数据获取模块 +├── NewsFlowDataFetcher # 新闻数据获取器 + ├── get_platform_news() # 单平台数据 + ├── get_multi_platform_news() # 多平台聚合 + ├── extract_stock_related_news() # 股票新闻提取 + ├── calculate_flow_score() # 流量得分计算 + └── get_hot_topics() # 热门话题挖掘 + +news_flow_db.py # 数据库模块 +├── NewsFlowDatabase # 数据库管理类 + ├── flow_snapshots # 流量快照表 + ├── platform_news # 平台新闻表 + ├── stock_related_news # 股票相关新闻表 + ├── hot_topics # 热门话题表 + └── flow_statistics # 每日统计表 + +news_flow_engine.py # 分析引擎 +├── NewsFlowEngine # 流量分析引擎 + ├── run_full_analysis() # 完整分析流程 + ├── _generate_trading_signals() # 生成交易信号 + ├── get_flow_trend() # 流量趋势分析 + └── compare_with_history() # 历史对比 + +news_flow_ui.py # UI界面 +└── display_news_flow_monitor() # 主界面 + ├── display_realtime_monitor() # 实时监测 + ├── display_flow_trend() # 流量趋势 + ├── display_history_records() # 历史记录 + └── display_settings() # 设置 +``` + +### 数据库表结构 + +**1. flow_snapshots(流量快照表)** +- 记录每次监测的整体情况 +- 字段:total_score, flow_level, social_score, finance_score, analysis等 + +**2. platform_news(平台新闻表)** +- 存储各平台的新闻数据 +- 字段:platform, title, content, url, publish_time等 + +**3. stock_related_news(股票相关新闻表)** +- 股票相关新闻筛选结果 +- 字段:title, matched_keywords, keyword_count等 + +**4. hot_topics(热门话题表)** +- 热门话题词频统计 +- 字段:topic, count, heat + +**5. flow_statistics(每日统计表)** +- 按天汇总的流量统计 +- 字段:date, avg_score, max_score, snapshot_count等 + +--- + +## 📊 使用指南 + +### 快速开始 + +1. **进入新闻流量监测** + - 点击侧边栏"📊 策略分析" → "📰 新闻流量" + +2. **选择监测模式** + - 全平台监测:监测所有20+平台 + - 社交媒体:仅监测微博、抖音、知乎等 + - 财经平台:仅监测新浪财经、东财、雪球等 + - 新闻媒体:仅监测百度、头条、腾讯网等 + - 自定义:手动选择平台 + +3. **开始监测** + - 点击"🚀 开始监测"按钮 + - 等待数据获取和分析(约30-60秒) + +4. **查看结果** + - 流量得分和等级 + - 交易信号和操作建议 + - 热门板块TOP3 + - 潜力个股TOP10 + - 股票相关新闻 + - 热门话题词云 + +### 监测频率建议 + +**盘前监测(9:00-9:25)** +- 目的:了解当日市场热点 +- 重点:关注新增热点题材 +- 操作:制定当日交易计划 + +**盘后监测(15:30-16:00)** +- 目的:复盘当日市场情绪 +- 重点:分析流量变化趋势 +- 操作:调整持仓和策略 + +**晚间监测(20:00-21:00)** +- 目的:准备次日策略 +- 重点:深度研究热点板块 +- 操作:精选次日目标股 + +### 信号解读原则 + +**1. 流量极高(≥900分)** +- **理论**:流量高潮=价格高潮,后面没有更傻的接盘侠 +- **特征**:主流媒体报道、全民讨论、短视频霸屏 +- **操作**:立即减仓,不要贪恋最后一跌 +- **案例**:深度求索概念(2024年) + +**2. 流量高(500-800分)** +- **理论**:流量快速上升,题材正在发酵 +- **特征**:热搜上榜、财经媒体关注、散户开始涌入 +- **操作**:关注龙头股,轻仓试探,设好止损 +- **策略**:先涨为王,名字为王 + +**3. 流量中(200-500分)** +- **理论**:流量正常波动,方向不明确 +- **特征**:常规新闻,没有突出热点 +- **操作**:保持观望,可关注潜力题材 +- **策略**:等待流量信号明确 + +**4. 流量低(<200分)** +- **理论**:流量低迷,市场情绪冷淡 +- **特征**:成交量萎缩、缺乏热点、媒体冷清 +- **操作**:空仓观望,等待市场转暖 +- **策略**:可布局低估值个股 + +### 配合使用建议 + +**与其他功能协同:** + +1. **新闻流量 + 智瞰龙虎** + - 流量识别热点 → 龙虎榜确认游资动向 + - 双重验证,提高成功率 + +2. **新闻流量 + 智策板块** + - 流量发现题材 → 智策分析板块轮动 + - 把握进场和退出时机 + +3. **新闻流量 + 主力选股** + - 流量筛选热点 → 主力资金确认标的 + - 跟随聪明钱,提高胜率 + +4. **新闻流量 + AI盯盘** + - 流量判断大盘情绪 → AI盯盘执行个股交易 + - 自动化交易,解放双手 + +--- + +## ⚠️ 风险提示 + +### 使用注意事项 + +1. **流量≠价格** + - 流量高只代表关注度高,不代表股价一定上涨 + - 必须结合基本面、技术面、资金面综合判断 + +2. **反向指标** + - 当你的反向指标(如擦鞋童、小区大妈)都在讨论某个股票时,往往是见顶信号 + - 记住:连擦鞋童都在炒股了 → 肯尼迪立即清仓 + +3. **流量高潮识别** + - 短视频热搜排名停止上升 + - 评论增速明显放缓 + - 主流媒体不再报道 + - 新用户涌入停滞 + +4. **数据时效性** + - 新闻API数据有一定延迟(约10-30分钟) + - 不适用于超高频交易 + - 建议配合实时盘口观察 + +5. **市场风险** + - 题材炒作具有高风险,可能快速反转 + - 严格执行止损,不要死扛 + - 仓位控制:单只股票不超过30% + - 本功能仅供参考,不构成投资建议 + +### 免责声明 + +本功能基于公开的新闻数据和"流量为王"理论进行分析,所有分析结果仅供参考。股市有风险,投资需谨慎。使用本功能进行投资决策的风险由用户自行承担。 + +--- + +## 🔧 技术依赖 + +### Python包 +```bash +pip install requests # HTTP请求 +pip install jieba # 中文分词 +pip install plotly # 数据可视化 +pip install pandas # 数据处理 +``` + +### 外部API +- **新闻API**: https://newsapi.ws4.cn/api/v1/dailynews/ +- **免费使用**:无需API Key +- **速率限制**:建议每次请求间隔0.5秒以上 + +--- + +## 📈 实战案例 + +### 案例1:流量爆发 - 深度求索概念(2024年) + +**背景**: +- 东大自研AI大模型技术突破 +- 主流媒体报道,短视频热搜霸榜 +- 带有民族情绪加成 + +**流量特征**: +- 存量流量型(自带顶流) +- 流量得分:950+(极高) +- 全平台关注度爆发 + +**交易信号**: +- 第1天:流量500分,信号"买入",龙头涨停 +- 第2天:流量800分,信号"观望",持仓待涨 +- 第3天:流量950分,信号"卖出",立即减仓 ← **流量高潮!** +- 第4天:流量600分,题材降温,股价回调 + +**结论**:严格按流量信号操作,第3天卖出,规避后续回调。 + +### 案例2:流量裂变 - 新能源题材(持续炒作) + +**背景**: +- 政策支持,产业发展 +- K值>1.5,病毒式传播 + +**流量特征**: +- 增量流量型(裂变传播) +- 流量逐步上升:300 → 500 → 700 → 850 +- 时间跨度:2-3周 + +**交易信号**: +- 第1周:流量300分,埋伏早期 +- 第2周:流量500分,加仓龙头 +- 第3周:流量700分,持仓待涨 +- 第4周:流量850分,逐步减仓 + +**结论**:增量流量型时间窗口长,可分批进出,把握多波段行情。 + +--- + +## 📚 相关文档 + +- **理论基础**: `新闻流量转化炒股法.md` +- **API文档**: `新闻监测API调用说明.md` +- **主程序**: `app.py` +- **数据模块**: `news_flow_data.py` +- **数据库**: `news_flow_db.py` +- **分析引擎**: `news_flow_engine.py` +- **UI界面**: `news_flow_ui.py` + +--- + +## 🎯 后续优化方向 + +1. **AI增强** + - 接入DeepSeek AI分析新闻内容 + - 自动识别题材炒作阶段(萌芽/爆发/高潮/退潮) + +2. **流量追踪** + - 实时监控流量变化(每分钟) + - 流量拐点自动告警 + +3. **个股关联** + - 从新闻中自动提取股票代码 + - 与龙虎榜、主力资金数据关联 + +4. **情绪量化** + - NLP情感分析(正面/负面) + - 社交媒体情绪指数 + +5. **策略回测** + - 历史流量与股价涨跌相关性分析 + - 优化流量阈值和交易信号 + +--- + +**祝你交易顺利!** 📈💰 + +记住:"流量为王,见顶就跑!" diff --git a/docs/新闻流量监测快速开始.md b/docs/新闻流量监测快速开始.md new file mode 100644 index 0000000..934f0ec --- /dev/null +++ b/docs/新闻流量监测快速开始.md @@ -0,0 +1,186 @@ +# 新闻流量监测功能 - 快速开始指南 + +## 🚀 快速开始 + +### 1. 安装依赖 + +```bash +# 激活虚拟环境(如果有) +.\venv\Scripts\Activate.ps1 + +# 安装新增依赖 +pip install jieba>=0.42.1 +``` + +### 2. 启动系统 + +```bash +# 方式1:使用run.py +python run.py + +# 方式2:直接启动streamlit +streamlit run app.py +``` + +### 3. 访问功能 + +1. 打开浏览器访问:http://localhost:8501 +2. 点击侧边栏"📊 策略分析" → "📰 新闻流量" +3. 选择监测模式,点击"🚀 开始监测" + +--- + +## 📊 功能概览 + +### 核心功能 +- ✅ 实时监测20+主流平台新闻热点 +- ✅ 智能计算流量得分(0-1000分) +- ✅ 自动生成交易信号(买入/观望/卖出) +- ✅ 识别热门板块TOP3 +- ✅ 提取潜力个股TOP10 +- ✅ 流量趋势分析(7-30天) +- ✅ 完整的历史记录管理 + +### 支持平台 +**社交媒体**:微博、抖音、知乎、B站 +**财经平台**:新浪财经、东方财富、雪球、财联社 +**新闻媒体**:百度、今日头条、腾讯网 + +--- + +## 💡 使用示例 + +### 场景1:盘前快速监测(推荐) + +**时间**:9:00-9:25 +**目的**:了解当日市场热点 + +``` +1. 进入"📰 新闻流量" +2. 选择"财经平台"模式 +3. 点击"开始监测" +4. 查看: + - 流量得分和等级 + - 交易信号 + - 热门板块 + - 潜力个股 +5. 制定当日交易计划 +``` + +### 场景2:盘后复盘分析 + +**时间**:15:30-16:00 +**目的**:复盘当日市场情绪 + +``` +1. 进入"📰 新闻流量" +2. 选择"全平台监测" +3. 执行监测,查看结果 +4. 切换到"📈 流量趋势"标签 +5. 对比今日与历史流量 +6. 调整持仓和策略 +``` + +### 场景3:深度研究热点题材 + +**时间**:晚间20:00-21:00 +**目的**:准备次日策略 + +``` +1. 全平台监测获取完整数据 +2. 重点关注: + - 股票相关新闻TOP10 + - 热门话题词云 + - 潜力个股列表 +3. 结合"🎯 智瞰龙虎"确认游资动向 +4. 结合"💰 主力选股"验证资金流向 +5. 精选次日目标股 +``` + +--- + +## 🎯 信号解读速查表 + +| 流量得分 | 流量等级 | 交易信号 | 信心度 | 操作 | +|---------|---------|---------|--------|------| +| ≥900 | 极高 | 卖出 | 9/10 | 立即减仓 | +| 800-900 | 极高 | 观望 | 7/10 | 逢高减仓 | +| 500-800 | 高 | 买入 | 8/10 | 轻仓试探 | +| 200-500 | 中 | 观望 | 5/10 | 保持观望 | +| <200 | 低 | 观望 | 3/10 | 空仓观望 | + +--- + +## ⚠️ 重要提示 + +### 必读原则 +1. **流量高潮 = 价格高潮 = 逃命时刻** + 当流量得分≥900时,立即减仓,不要贪恋最后一跌 + +2. **反向指标警惕** + 当你的反向指标(小区大妈、出租车司机)都在讨论某个股票时,往往是见顶信号 + +3. **严格止损** + 题材炒作风险高,务必设置止损位(-5%) + +4. **仓位控制** + 单只股票不超过30%,避免重仓一把梭 + +5. **配合使用** + 结合基本面、技术面、资金面综合判断,不要单纯依赖流量信号 + +### 数据说明 +- 新闻API数据有10-30分钟延迟 +- 不适用于超高频交易 +- 仅供参考,不构成投资建议 + +--- + +## 🔧 故障排查 + +### 问题1:监测失败 +**现象**:点击开始监测后报错 + +**解决方案**: +1. 检查网络连接是否正常 +2. 确认新闻API服务可用(访问:https://newsapi.ws4.cn/api/v1/dailynews/?platform=baidu) +3. 查看终端错误日志 + +### 问题2:jieba未安装 +**现象**:报错"ModuleNotFoundError: No module named 'jieba'" + +**解决方案**: +```bash +pip install jieba +``` + +### 问题3:分析时间过长 +**现象**:监测超过2分钟仍未完成 + +**解决方案**: +1. 减少监测平台数量(选择"财经平台"而非"全平台") +2. 检查网络速度 +3. 等待完成(首次获取需要更长时间) + +--- + +## 📚 深入学习 + +### 相关文档 +- **功能详解**:`docs/新闻流量监测功能说明.md` +- **理论基础**:`新闻流量转化炒股法.md` +- **API文档**:`新闻监测API调用说明.md` + +### 配合使用 +- **智瞰龙虎**:验证游资动向 +- **智策板块**:分析板块轮动 +- **主力选股**:跟踪主力资金 +- **AI盯盘**:自动化交易执行 + +--- + +## 💬 反馈与支持 + +如有问题或建议,请联系:ws3101001@126.com + +**祝你交易顺利!记住:流量为王,见顶就跑!** 📈💰 diff --git a/news_flow.db b/news_flow.db new file mode 100644 index 0000000..5c23853 Binary files /dev/null and b/news_flow.db differ diff --git a/news_flow_agents.py b/news_flow_agents.py new file mode 100644 index 0000000..6ea47ba --- /dev/null +++ b/news_flow_agents.py @@ -0,0 +1,996 @@ +""" +新闻流量智能分析代理模块 +使用DeepSeek进行AI驱动的分析 +包含:板块影响分析、股票推荐、风险评估、投资建议 +""" +import json +import logging +import time +from datetime import datetime +from typing import Dict, List, Optional + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class NewsFlowAgents: + """新闻流量智能分析代理""" + + def __init__(self, model: str = "deepseek-chat"): + """ + 初始化代理 + + Args: + model: 使用的模型,默认 deepseek-chat + """ + self.model = model + self.deepseek_client = None + self._init_client() + + def _init_client(self): + """初始化DeepSeek客户端""" + try: + from deepseek_client import DeepSeekClient + self.deepseek_client = DeepSeekClient(model=self.model) + logger.info(f"✅ DeepSeek客户端初始化成功,模型: {self.model}") + except Exception as e: + logger.error(f"❌ DeepSeek客户端初始化失败: {e}") + self.deepseek_client = None + + def is_available(self) -> bool: + """检查AI是否可用""" + return self.deepseek_client is not None + + def sector_impact_agent(self, hot_topics: List[Dict], + stock_news: List[Dict], + flow_data: Dict = None) -> Dict: + """ + 板块影响分析代理 + + 分析热点可能影响的板块 + + Returns: + { + 'affected_sectors': List[Dict], + 'analysis': str, + 'success': bool, + } + """ + if not self.is_available(): + return self._fallback_sector_analysis(hot_topics, stock_news) + + # 准备数据 + topics_text = '\n'.join([ + f"- {t['topic']} (热度:{t.get('heat', 0)}, 跨{t.get('cross_platform', 0)}平台)" + for t in hot_topics[:20] + ]) + + news_text = '\n'.join([ + f"- [{n.get('platform_name', '')}] {n.get('title', '')}" + for n in stock_news[:15] + ]) + + flow_info = "" + if flow_data: + flow_info = f""" +当前流量状态: +- 流量得分: {flow_data.get('total_score', 'N/A')}/1000 +- 流量等级: {flow_data.get('level', 'N/A')} +- 社交媒体热度: {flow_data.get('social_score', 'N/A')} +- 财经平台热度: {flow_data.get('finance_score', 'N/A')} +""" + + prompt = f"""你是一名资深的A股短线投资分析师,专注于热点题材挖掘和板块轮动分析。 + +【重要】请根据以下全网热点数据,进行深度的A股题材分析: + +=== 全网热门话题TOP20 === +{topics_text} + +=== 股票相关新闻TOP15 === +{news_text} +{flow_info} + +请完成以下分析任务: + +1. **题材挖掘**:从以上热点中挖掘出可能引爆A股的核心题材概念 +2. **板块分析**:分析最可能受益的A股板块(要具体到申万行业或同花顺概念板块) +3. **热度评估**:评估每个板块的潜在炒作热度和持续性 +4. **龙头预判**:推测可能的龙头股特征 + +请以JSON格式输出: +{{ + "hot_themes": [ + {{"theme": "题材名称", "source": "来源热点", "heat_level": "极高/高/中", "sustainability": "持续性评估"}} + ], + "benefited_sectors": [ + {{ + "name": "板块名称(要具体如:AI算力、低空经济、机器人等)", + "impact": "利好", + "confidence": 85, + "reason": "详细分析原因", + "related_concepts": ["相关概念1", "相关概念2"], + "leader_characteristics": "龙头股特征描述" + }} + ], + "damaged_sectors": [ + {{"name": "板块名称", "impact": "利空", "confidence": 60, "reason": "原因"}} + ], + "opportunity_assessment": "今日A股投资机会综合评估(100字以内)", + "trading_suggestion": "短线操作建议", + "key_points": ["核心要点1", "核心要点2", "核心要点3"] +}} + +只输出JSON,不要其他文字。""" + + try: + messages = [ + {"role": "system", "content": "你是专业的A股市场分析师,输出必须是纯JSON格式。"}, + {"role": "user", "content": prompt} + ] + + response = self.deepseek_client.call_api(messages, temperature=0.5, max_tokens=2000) + + # 解析JSON + result = self._parse_json_response(response) + + if result: + return { + 'hot_themes': result.get('hot_themes', []), + 'affected_sectors': result.get('benefited_sectors', []) + result.get('damaged_sectors', []), + 'benefited_sectors': result.get('benefited_sectors', []), + 'damaged_sectors': result.get('damaged_sectors', []), + 'opportunity_assessment': result.get('opportunity_assessment', ''), + 'trading_suggestion': result.get('trading_suggestion', ''), + 'key_points': result.get('key_points', []), + 'success': True, + 'raw_response': response, + } + else: + return self._fallback_sector_analysis(hot_topics, stock_news) + + except Exception as e: + logger.error(f"板块分析失败: {e}") + return self._fallback_sector_analysis(hot_topics, stock_news) + + def stock_recommend_agent(self, hot_sectors: List[Dict], + flow_stage: str, + sentiment_class: str) -> Dict: + """ + 股票推荐代理 + + 基于热门板块和市场状态推荐股票 + + Returns: + { + 'recommended_stocks': List[Dict], + 'strategy': str, + 'success': bool, + } + """ + if not self.is_available(): + return self._fallback_stock_recommend(hot_sectors) + + sectors_text = '\n'.join([ + f"- {s.get('name', '')}:{s.get('impact', '利好')},置信度{s.get('confidence', 50)}%\n 原因:{s.get('reason', '')}\n 龙头特征:{s.get('leader_characteristics', 'N/A')}" + for s in hot_sectors[:5] + ]) + + related_concepts = [] + for s in hot_sectors[:5]: + related_concepts.extend(s.get('related_concepts', [])) + concepts_text = ', '.join(list(set(related_concepts))[:10]) if related_concepts else '无' + + prompt = f"""你是一名资深的A股短线游资操盘手,专注于热点题材龙头股挖掘。 + +=== 当前市场状态 === +- 流量阶段: {flow_stage} +- 情绪状态: {sentiment_class} +- 相关概念: {concepts_text} + +=== 热门受益板块分析 === +{sectors_text} + +=== 选股要求 === +请根据"流量为王"理念,推荐5-8只A股短线标的: + +选股法则(必须遵循): +1. **先涨为王**:优先选择已经启动、走势强势的股票 +2. **名字为王**:股票名称与热点高度相关(如AI概念选"智"字头) +3. **龙头优先**:选择板块内最强势的龙头或人气股 +4. **题材纯正**:主业与热点题材高度相关 +5. **流通盘适中**:30-150亿市值为佳,便于资金操作 + +请以JSON格式输出: +{{ + "recommended_stocks": [ + {{ + "code": "股票代码(6位数字,如000001或600001)", + "name": "股票名称", + "sector": "所属板块", + "market": "沪市/深市/创业板/科创板", + "market_cap": "市值(亿)", + "reason": "推荐理由(与热点的关联性)", + "catalyst": "催化剂/驱动因素", + "strategy": "操作策略(进场/加仓/止损建议)", + "target_space": "目标空间(如15-20%)", + "risk_level": "低/中/高", + "attention_points": ["注意事项1", "注意事项2"] + }} + ], + "overall_strategy": "整体操作策略和仓位建议", + "timing_advice": "最佳介入时机判断", + "risk_warning": "风险提示(必须包含投资风险提醒)" +}} + +【重要】只推荐真实存在的A股股票,代码必须正确。只输出JSON。""" + + try: + messages = [ + {"role": "system", "content": "你是专业的A股投资顾问,只输出纯JSON格式。"}, + {"role": "user", "content": prompt} + ] + + response = self.deepseek_client.call_api(messages, temperature=0.6, max_tokens=2000) + result = self._parse_json_response(response) + + if result: + return { + 'recommended_stocks': result.get('recommended_stocks', []), + 'overall_strategy': result.get('overall_strategy', ''), + 'timing_advice': result.get('timing_advice', ''), + 'risk_warning': result.get('risk_warning', ''), + 'success': True, + 'raw_response': response, + } + else: + return self._fallback_stock_recommend(hot_sectors) + + except Exception as e: + logger.error(f"股票推荐失败: {e}") + return self._fallback_stock_recommend(hot_sectors) + + def risk_assess_agent(self, flow_stage: str, + sentiment_data: Dict, + viral_k: float, + flow_type: str) -> Dict: + """ + 风险评估代理 + + 评估当前市场风险 + + Returns: + { + 'risk_level': str, + 'risk_factors': List[str], + 'risk_score': int, + 'analysis': str, + 'success': bool, + } + """ + if not self.is_available(): + return self._fallback_risk_assess(flow_stage, sentiment_data, viral_k) + + prompt = f"""你是一名专业的风险管理分析师。 + +请根据以下市场数据评估当前投资风险: + +市场状态: +- 流量阶段: {flow_stage} +- 情绪指数: {sentiment_data.get('sentiment_index', 50)} +- 情绪分类: {sentiment_data.get('sentiment_class', '中性')} +- K值(病毒系数): {viral_k} +- 流量类型: {flow_type} + +核心理念: +- 流量高潮 = 价格高潮 = 逃命时刻 +- K值>1.5表示指数型爆发,风险上升 +- 情绪极端(>85或<20)都意味着风险 + +请分析: +1. 当前风险等级(极低/低/中等/高/极高) +2. 主要风险因素 +3. 风险分数(0-100) +4. 详细分析 + +以JSON格式输出: +{{ + "risk_level": "高", + "risk_score": 75, + "risk_factors": ["风险因素1", "风险因素2", ...], + "opportunities": ["机会1", "机会2", ...], + "analysis": "详细分析文字", + "key_warning": "最重要的警告" +}} + +只输出JSON。""" + + try: + messages = [ + {"role": "system", "content": "你是专业的风险管理分析师,只输出纯JSON格式。"}, + {"role": "user", "content": prompt} + ] + + response = self.deepseek_client.call_api(messages, temperature=0.4, max_tokens=1500) + result = self._parse_json_response(response) + + if result: + return { + 'risk_level': result.get('risk_level', '中等'), + 'risk_score': result.get('risk_score', 50), + 'risk_factors': result.get('risk_factors', []), + 'opportunities': result.get('opportunities', []), + 'analysis': result.get('analysis', ''), + 'key_warning': result.get('key_warning', ''), + 'success': True, + 'raw_response': response, + } + else: + return self._fallback_risk_assess(flow_stage, sentiment_data, viral_k) + + except Exception as e: + logger.error(f"风险评估失败: {e}") + return self._fallback_risk_assess(flow_stage, sentiment_data, viral_k) + + def investment_advisor_agent(self, sector_analysis: Dict, + stock_recommend: Dict, + risk_assess: Dict, + flow_data: Dict, + sentiment_data: Dict) -> Dict: + """ + 投资建议代理(综合) + + 综合所有分析给出最终投资建议 + + Returns: + { + 'advice': str, # 买入/持有/观望/回避 + 'confidence': int, + 'summary': str, + 'action_plan': List[str], + 'success': bool, + } + """ + if not self.is_available(): + return self._fallback_investment_advice(risk_assess, flow_data) + + # 构建综合信息 + sectors_text = ', '.join([s.get('name', '') for s in sector_analysis.get('benefited_sectors', [])[:3]]) + stocks_text = ', '.join([f"{s.get('name', '')}({s.get('code', '')})" + for s in stock_recommend.get('recommended_stocks', [])[:3]]) + + prompt = f"""你是一名首席投资策略师,需要给出最终的投资建议。 + +综合分析数据: + +【流量分析】 +- 流量得分: {flow_data.get('total_score', 'N/A')} +- 流量等级: {flow_data.get('level', 'N/A')} + +【情绪分析】 +- 情绪指数: {sentiment_data.get('sentiment_index', 50)} +- 情绪分类: {sentiment_data.get('sentiment_class', '中性')} +- 流量阶段: {sentiment_data.get('flow_stage', '未知')} + +【板块分析】 +- 受益板块: {sectors_text} +- 机会评估: {sector_analysis.get('opportunity_assessment', 'N/A')} + +【股票推荐】 +- 推荐股票: {stocks_text} + +【风险评估】 +- 风险等级: {risk_assess.get('risk_level', '中等')} +- 风险分数: {risk_assess.get('risk_score', 50)} +- 主要风险: {', '.join(risk_assess.get('risk_factors', [])[:3])} + +核心原则(流量为王): +- 流量高潮 = 价格高潮 = 逃命时刻 +- 当热搜、媒体报道、KOL转发同时达到高潮时,就是出货时机 +- 短线操作:快进快出,紧跟龙头 + +请给出最终投资建议: +1. 操作建议(买入/持有/观望/回避) +2. 置信度(0-100) +3. 综合总结 +4. 具体行动计划 + +以JSON格式输出: +{{ + "advice": "观望", + "confidence": 75, + "summary": "综合总结文字", + "action_plan": [ + "行动1", + "行动2", + ... + ], + "position_suggestion": "仓位建议", + "timing": "时机判断", + "key_message": "最重要的一句话" +}} + +只输出JSON。""" + + try: + start_time = time.time() + + messages = [ + {"role": "system", "content": "你是首席投资策略师,必须给出明确的投资建议,只输出纯JSON格式。"}, + {"role": "user", "content": prompt} + ] + + response = self.deepseek_client.call_api(messages, temperature=0.5, max_tokens=2000) + result = self._parse_json_response(response) + + analysis_time = time.time() - start_time + + if result: + return { + 'advice': result.get('advice', '观望'), + 'confidence': result.get('confidence', 50), + 'summary': result.get('summary', ''), + 'action_plan': result.get('action_plan', []), + 'position_suggestion': result.get('position_suggestion', ''), + 'timing': result.get('timing', ''), + 'key_message': result.get('key_message', ''), + 'success': True, + 'analysis_time': round(analysis_time, 2), + 'raw_response': response, + } + else: + return self._fallback_investment_advice(risk_assess, flow_data) + + except Exception as e: + logger.error(f"投资建议生成失败: {e}") + return self._fallback_investment_advice(risk_assess, flow_data) + + def run_full_analysis(self, hot_topics: List[Dict], + stock_news: List[Dict], + flow_data: Dict, + sentiment_data: Dict, + viral_k: float = 1.0, + flow_type: str = "未知") -> Dict: + """ + 运行完整的AI分析 + + Returns: + { + 'sector_analysis': Dict, + 'stock_recommend': Dict, + 'risk_assess': Dict, + 'investment_advice': Dict, + 'success': bool, + 'analysis_time': float, + } + """ + start_time = time.time() + + logger.info("🤖 开始AI分析...") + + # 1. 板块影响分析 + logger.info(" 📊 分析板块影响...") + sector_analysis = self.sector_impact_agent(hot_topics, stock_news, flow_data) + + # 2. 股票推荐 + logger.info(" 📈 生成股票推荐...") + flow_stage = sentiment_data.get('flow_stage', {}).get('stage_name', '未知') + sentiment_class = sentiment_data.get('sentiment', {}).get('sentiment_class', '中性') + stock_recommend = self.stock_recommend_agent( + sector_analysis.get('benefited_sectors', []), + flow_stage, + sentiment_class + ) + + # 3. 风险评估 + logger.info(" ⚠️ 评估风险...") + risk_assess = self.risk_assess_agent( + flow_stage, + sentiment_data.get('sentiment', {}), + viral_k, + flow_type + ) + + # 4. 综合投资建议 + logger.info(" 💡 生成投资建议...") + investment_advice = self.investment_advisor_agent( + sector_analysis, + stock_recommend, + risk_assess, + flow_data, + sentiment_data.get('sentiment', {}) + ) + + total_time = time.time() - start_time + logger.info(f"✅ AI分析完成,耗时 {total_time:.2f} 秒") + + # 汇总结果 + return { + 'sector_analysis': sector_analysis, + 'stock_recommend': stock_recommend, + 'risk_assess': risk_assess, + 'investment_advice': investment_advice, + 'success': all([ + sector_analysis.get('success', False), + stock_recommend.get('success', False), + risk_assess.get('success', False), + investment_advice.get('success', False), + ]), + 'analysis_time': round(total_time, 2), + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + } + + def analyze_sector_deep(self, sector_name: str, related_news: List[Dict], + hot_topics: List[Dict]) -> Dict: + """ + 深度分析单个板块 + + 为每个热门板块单独调用DeepSeek进行深度分析 + """ + if not self.is_available(): + return {'success': False, 'error': 'AI不可用'} + + news_text = '\n'.join([ + f"- [{n.get('platform_name', '')}] {n.get('title', '')}" + for n in related_news[:20] + ]) + + topics_text = '\n'.join([ + f"- {t['topic']} (热度:{t.get('heat', 0)})" + for t in hot_topics[:10] + ]) + + prompt = f"""你是{sector_name}板块的专业分析师。 + +请对以下与{sector_name}相关的新闻进行深度分析: + +【相关新闻】 +{news_text} + +【相关热点话题】 +{topics_text} + +请分析: +1. {sector_name}板块当前的市场热度和关注度 +2. 驱动因素分析(政策/技术/资金/事件) +3. 短期(1-3天)走势预判 +4. 核心龙头股分析(至少3只) +5. 投资建议和风险提示 + +以JSON格式输出: +{{ + "sector_name": "{sector_name}", + "heat_level": "极高/高/中/低", + "heat_score": 85, + "drivers": [ + {{"type": "政策", "content": "具体驱动因素", "impact": "正面/负面"}} + ], + "short_term_outlook": "看涨/震荡/看跌", + "outlook_reason": "预判理由", + "leader_stocks": [ + {{ + "code": "600000", + "name": "股票名称", + "reason": "龙头理由", + "strategy": "操作策略" + }} + ], + "investment_advice": "具体投资建议", + "risk_warning": "风险提示", + "key_indicators": {{ + "关注度": "高", + "资金流向": "净流入", + "情绪指数": 75 + }} +}} + +只输出JSON。""" + + try: + messages = [ + {"role": "system", "content": f"你是{sector_name}板块专业分析师,只输出JSON格式。"}, + {"role": "user", "content": prompt} + ] + + response = self.deepseek_client.call_api(messages, temperature=0.5, max_tokens=2000) + result = self._parse_json_response(response) + + if result: + result['success'] = True + return result + else: + return {'success': False, 'sector_name': sector_name} + + except Exception as e: + logger.error(f"{sector_name}板块分析失败: {e}") + return {'success': False, 'sector_name': sector_name, 'error': str(e)} + + def run_multi_sector_analysis(self, hot_topics: List[Dict], + stock_news: List[Dict], + target_sectors: List[str] = None) -> Dict: + """ + 多板块并行分析 + + 对多个热门板块分别调用DeepSeek进行深度分析 + + Args: + hot_topics: 热门话题列表 + stock_news: 股票相关新闻 + target_sectors: 指定分析的板块列表,为None则自动识别 + + Returns: + { + 'sector_analyses': List[Dict], # 各板块分析结果 + 'summary': str, # 综合总结 + 'top_sectors': List[str], # 最热门板块 + 'success': bool + } + """ + if not self.is_available(): + return {'success': False, 'error': 'AI不可用', 'sector_analyses': []} + + start_time = time.time() + + # 如果没有指定板块,先识别热门板块 + if not target_sectors: + target_sectors = self._identify_hot_sectors(hot_topics, stock_news) + + logger.info(f"🔍 开始分析 {len(target_sectors)} 个热门板块: {target_sectors}") + + # 对每个板块进行深度分析 + sector_analyses = [] + for sector in target_sectors[:5]: # 最多分析5个板块 + logger.info(f" 📊 分析板块: {sector}") + + # 筛选与该板块相关的新闻 + related_news = self._filter_news_by_sector(stock_news, sector) + related_topics = self._filter_topics_by_sector(hot_topics, sector) + + analysis = self.analyze_sector_deep(sector, related_news, related_topics) + if analysis.get('success'): + sector_analyses.append(analysis) + + # 生成综合总结 + summary = self._generate_multi_sector_summary(sector_analyses) + + total_time = time.time() - start_time + logger.info(f"✅ 多板块分析完成,耗时 {total_time:.2f} 秒") + + return { + 'sector_analyses': sector_analyses, + 'summary': summary, + 'top_sectors': target_sectors[:5], + 'analysis_count': len(sector_analyses), + 'analysis_time': round(total_time, 2), + 'success': len(sector_analyses) > 0 + } + + def _identify_hot_sectors(self, hot_topics: List[Dict], stock_news: List[Dict]) -> List[str]: + """识别热门板块""" + # 板块关键词映射 + sector_keywords = { + 'AI人工智能': ['AI', '人工智能', '大模型', 'ChatGPT', '算力', '智能', 'DeepSeek', '机器人'], + '新能源': ['新能源', '光伏', '锂电', '储能', '电池', '充电桩', '风电'], + '半导体芯片': ['芯片', '半导体', '光刻', '封装', '晶圆', '国产替代'], + '医药生物': ['医药', '生物', '疫苗', '创新药', '医疗', 'CXO'], + '消费': ['消费', '白酒', '食品', '零售', '餐饮', '旅游'], + '金融': ['银行', '保险', '券商', '证券', '金融'], + '房地产': ['房地产', '地产', '楼市', '房价'], + '军工': ['军工', '国防', '航空', '航天', '武器'], + '汽车': ['汽车', '新能源车', '智能驾驶', '无人驾驶'], + '低空经济': ['低空', '无人机', '飞行汽车', 'eVTOL'], + '机器人': ['机器人', '人形机器人', '工业机器人', '减速器'], + '数据要素': ['数据', '数据要素', '数据交易', '数字经济'], + } + + # 统计各板块的热度 + sector_scores = {} + + # 从话题中统计 + for topic in hot_topics: + topic_text = topic.get('topic', '') + for sector, keywords in sector_keywords.items(): + for kw in keywords: + if kw in topic_text: + sector_scores[sector] = sector_scores.get(sector, 0) + topic.get('heat', 1) + break + + # 从新闻中统计 + for news in stock_news: + news_text = (news.get('title') or '') + (news.get('content') or '') + for sector, keywords in sector_keywords.items(): + for kw in keywords: + if kw in news_text: + sector_scores[sector] = sector_scores.get(sector, 0) + news.get('weight', 1) + break + + # 按热度排序 + sorted_sectors = sorted(sector_scores.items(), key=lambda x: x[1], reverse=True) + return [s[0] for s in sorted_sectors[:5]] + + def _filter_news_by_sector(self, news_list: List[Dict], sector: str) -> List[Dict]: + """筛选与板块相关的新闻""" + sector_keywords = { + 'AI人工智能': ['AI', '人工智能', '大模型', 'ChatGPT', '算力', '智能', 'DeepSeek', '机器人'], + '新能源': ['新能源', '光伏', '锂电', '储能', '电池', '充电桩', '风电'], + '半导体芯片': ['芯片', '半导体', '光刻', '封装', '晶圆'], + '医药生物': ['医药', '生物', '疫苗', '创新药', '医疗'], + '消费': ['消费', '白酒', '食品', '零售', '餐饮'], + '金融': ['银行', '保险', '券商', '证券', '金融'], + '房地产': ['房地产', '地产', '楼市'], + '军工': ['军工', '国防', '航空', '航天'], + '汽车': ['汽车', '新能源车', '智能驾驶'], + '低空经济': ['低空', '无人机', '飞行汽车'], + '机器人': ['机器人', '人形机器人', '减速器'], + '数据要素': ['数据', '数据要素', '数字经济'], + } + + keywords = sector_keywords.get(sector, [sector]) + related = [] + + for news in news_list: + text = (news.get('title') or '') + (news.get('content') or '') + for kw in keywords: + if kw in text: + related.append(news) + break + + return related[:20] + + def _filter_topics_by_sector(self, topics: List[Dict], sector: str) -> List[Dict]: + """筛选与板块相关的话题""" + sector_keywords = { + 'AI人工智能': ['AI', '人工智能', '大模型', 'ChatGPT', '算力', '智能'], + '新能源': ['新能源', '光伏', '锂电', '储能', '电池'], + '半导体芯片': ['芯片', '半导体', '光刻'], + '医药生物': ['医药', '生物', '疫苗', '医疗'], + '消费': ['消费', '白酒', '食品', '餐饮'], + } + + keywords = sector_keywords.get(sector, [sector]) + related = [] + + for topic in topics: + text = topic.get('topic', '') + for kw in keywords: + if kw in text: + related.append(topic) + break + + return related[:10] + + def _generate_multi_sector_summary(self, sector_analyses: List[Dict]) -> str: + """生成多板块分析总结""" + if not sector_analyses: + return "暂无板块分析数据" + + # 按热度排序 + sorted_analyses = sorted( + sector_analyses, + key=lambda x: x.get('heat_score', 0), + reverse=True + ) + + summary_parts = [] + summary_parts.append(f"共分析{len(sector_analyses)}个热门板块:") + + for i, analysis in enumerate(sorted_analyses[:3], 1): + sector = analysis.get('sector_name', '未知') + heat = analysis.get('heat_level', '中') + outlook = analysis.get('short_term_outlook', '震荡') + summary_parts.append(f"{i}. {sector}(热度{heat},{outlook})") + + return ' '.join(summary_parts) + + def _parse_json_response(self, response: str) -> Optional[Dict]: + """解析JSON响应""" + try: + # 清理响应文本 + text = response.strip() + + # 处理markdown代码块 + if '```json' in text: + text = text.split('```json')[1].split('```')[0] + elif '```' in text: + text = text.split('```')[1].split('```')[0] + + # 移除可能的推理过程 + if '【推理过程】' in text: + parts = text.split('【推理过程】') + text = parts[-1] if len(parts) > 1 else parts[0] + + # 查找JSON部分 + start = text.find('{') + end = text.rfind('}') + 1 + + if start >= 0 and end > start: + json_text = text[start:end] + return json.loads(json_text) + + return None + + except json.JSONDecodeError as e: + logger.error(f"JSON解析失败: {e}") + return None + + # ==================== 降级方法 ==================== + + def _fallback_sector_analysis(self, hot_topics: List[Dict], + stock_news: List[Dict]) -> Dict: + """板块分析降级方法""" + # 基于关键词的简单分析 + sector_keywords = { + 'AI人工智能': ['AI', '人工智能', 'ChatGPT', '大模型', '算力', 'GPT'], + '新能源': ['新能源', '锂电', '光伏', '风电', '储能', '充电桩'], + '半导体': ['芯片', '半导体', '光刻机', '集成电路', '封测'], + '医药生物': ['医药', '疫苗', '创新药', '医疗', '生物'], + '消费': ['消费', '白酒', '食品', '零售', '餐饮'], + '金融': ['银行', '保险', '券商', '金融', '信托'], + } + + sector_hits = {} + for topic in hot_topics: + topic_text = topic.get('topic', '') + heat = topic.get('heat', 0) + for sector, keywords in sector_keywords.items(): + if any(kw in topic_text for kw in keywords): + if sector not in sector_hits: + sector_hits[sector] = 0 + sector_hits[sector] += heat + + # 排序获取TOP板块 + sorted_sectors = sorted(sector_hits.items(), key=lambda x: x[1], reverse=True) + + benefited_sectors = [ + { + 'name': sector, + 'impact': '利好', + 'confidence': min(60, score // 2), + 'reason': f'热点话题关联度较高,热度得分{score}' + } + for sector, score in sorted_sectors[:5] + ] + + return { + 'affected_sectors': benefited_sectors, + 'benefited_sectors': benefited_sectors, + 'damaged_sectors': [], + 'opportunity_assessment': '基于关键词匹配的简单分析,建议参考AI深度分析结果。', + 'key_points': ['AI分析不可用,使用降级方法'], + 'success': True, + 'fallback': True, + } + + def _fallback_stock_recommend(self, hot_sectors: List[Dict]) -> Dict: + """股票推荐降级方法""" + return { + 'recommended_stocks': [], + 'overall_strategy': 'AI分析不可用,建议自行研究热门板块龙头股。', + 'risk_warning': '此为降级结果,请谨慎参考。', + 'success': True, + 'fallback': True, + } + + def _fallback_risk_assess(self, flow_stage: str, + sentiment_data: Dict, + viral_k: float) -> Dict: + """风险评估降级方法""" + risk_score = 50 + risk_factors = [] + + # 基于规则的简单风险评估 + if flow_stage in ['一致', 'consensus']: + risk_score += 30 + risk_factors.append('流量处于一致阶段,可能是顶部') + elif flow_stage in ['退潮', 'decline']: + risk_score += 20 + risk_factors.append('流量正在退潮') + + sentiment_index = sentiment_data.get('sentiment_index', 50) + if sentiment_index > 85: + risk_score += 15 + risk_factors.append('情绪过度乐观') + elif sentiment_index < 20: + risk_score += 10 + risk_factors.append('情绪过度悲观') + + if viral_k > 1.5: + risk_score += 15 + risk_factors.append(f'K值={viral_k},流量指数型增长') + + risk_score = min(100, risk_score) + + if risk_score >= 80: + risk_level = '极高' + elif risk_score >= 60: + risk_level = '高' + elif risk_score >= 40: + risk_level = '中等' + elif risk_score >= 20: + risk_level = '低' + else: + risk_level = '极低' + + return { + 'risk_level': risk_level, + 'risk_score': risk_score, + 'risk_factors': risk_factors, + 'opportunities': [], + 'analysis': '基于规则的简单风险评估,AI分析不可用。', + 'key_warning': '请谨慎参考,建议开启AI分析获取更准确的评估。', + 'success': True, + 'fallback': True, + } + + def _fallback_investment_advice(self, risk_assess: Dict, + flow_data: Dict) -> Dict: + """投资建议降级方法""" + risk_level = risk_assess.get('risk_level', '中等') + + if risk_level in ['极高', '高']: + advice = '回避' + confidence = 70 + summary = '当前风险较高,建议保持观望或减仓。' + elif risk_level == '中等': + advice = '观望' + confidence = 60 + summary = '市场状态中性,建议观望等待更明确的信号。' + else: + advice = '关注' + confidence = 55 + summary = '风险较低,可关注热点板块机会。' + + return { + 'advice': advice, + 'confidence': confidence, + 'summary': summary, + 'action_plan': ['AI分析不可用,请自行判断'], + 'position_suggestion': '建议仓位不超过30%', + 'timing': '等待确认信号', + 'key_message': '此为降级结果,请谨慎参考。', + 'success': True, + 'fallback': True, + } + + +# 全局实例 +news_flow_agents = NewsFlowAgents() + + +# 测试代码 +if __name__ == "__main__": + print("=== 测试新闻流量智能分析代理 ===") + + # 检查AI是否可用 + if news_flow_agents.is_available(): + print("✅ AI客户端可用") + else: + print("⚠️ AI客户端不可用,将使用降级方法") + + # 模拟数据 + hot_topics = [ + {'topic': 'AI芯片', 'heat': 95, 'cross_platform': 5}, + {'topic': '新能源汽车', 'heat': 80, 'cross_platform': 4}, + {'topic': '涨停板', 'heat': 75, 'cross_platform': 3}, + ] + + stock_news = [ + {'platform_name': '东方财富', 'title': 'AI概念股集体大涨,龙头股涨停'}, + {'platform_name': '雪球', 'title': '新能源板块反弹,锂电池领涨'}, + ] + + flow_data = { + 'total_score': 650, + 'level': '高', + } + + sentiment_data = { + 'sentiment': {'sentiment_index': 72, 'sentiment_class': '乐观'}, + 'flow_stage': {'stage_name': '加速'}, + } + + # 运行板块分析 + print("\n--- 板块影响分析 ---") + sector_result = news_flow_agents.sector_impact_agent(hot_topics, stock_news, flow_data) + print(f"受益板块: {[s.get('name', '') for s in sector_result.get('benefited_sectors', [])]}") + print(f"是否降级: {sector_result.get('fallback', False)}") diff --git a/news_flow_alert.py b/news_flow_alert.py new file mode 100644 index 0000000..0c76dc4 --- /dev/null +++ b/news_flow_alert.py @@ -0,0 +1,514 @@ +""" +新闻流量预警系统模块 +实现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}") diff --git a/news_flow_data.py b/news_flow_data.py new file mode 100644 index 0000000..0bfe83a --- /dev/null +++ b/news_flow_data.py @@ -0,0 +1,702 @@ +""" +新闻流量数据获取模块 +用于获取各大平台的热点新闻和流量数据 +支持22个平台,包含排名、K值计算等功能 +""" +import requests +import logging +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Tuple +import time +from collections import Counter + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class NewsFlowDataFetcher: + """新闻流量数据获取器""" + + def __init__(self): + self.base_url = "https://newsapi.ws4.cn/api/v1/dailynews/" + self.timeout = 10 + + # 支持的平台配置 - 扩展到22个平台 + self.platforms = { + # 社交媒体平台(核心流量指标)- 8个 + 'weibo': {'name': '微博热搜', 'category': 'social', 'weight': 10, 'influence': 'high'}, + 'douyin': {'name': '抖音热点', 'category': 'social', 'weight': 9, 'influence': 'high'}, + 'zhihu': {'name': '知乎热榜', 'category': 'social', 'weight': 7, 'influence': 'medium'}, + 'bilibili': {'name': '哔哩哔哩', 'category': 'social', 'weight': 6, 'influence': 'medium'}, + 'xiaohongshu': {'name': '小红书', 'category': 'social', 'weight': 7, 'influence': 'medium'}, + 'kuaishou': {'name': '快手', 'category': 'social', 'weight': 6, 'influence': 'medium'}, + 'tieba': {'name': '百度贴吧', 'category': 'social', 'weight': 5, 'influence': 'low'}, + 'weixin': {'name': '微信热点', 'category': 'social', 'weight': 8, 'influence': 'high'}, + + # 新闻媒体平台 - 6个 + 'baidu': {'name': '百度热搜', 'category': 'news', 'weight': 8, 'influence': 'high'}, + 'jinritoutiao': {'name': '今日头条', 'category': 'news', 'weight': 7, 'influence': 'high'}, + 'tenxunwang': {'name': '腾讯网', 'category': 'news', 'weight': 6, 'influence': 'medium'}, + 'netease': {'name': '网易新闻', 'category': 'news', 'weight': 6, 'influence': 'medium'}, + 'ifeng': {'name': '凤凰网', 'category': 'news', 'weight': 5, 'influence': 'medium'}, + 'sina': {'name': '新浪新闻', 'category': 'news', 'weight': 6, 'influence': 'medium'}, + + # 财经平台(股市相关)- 5个 + 'sina_finance': {'name': '新浪财经', 'category': 'finance', 'weight': 9, 'influence': 'high'}, + 'eastmoney': {'name': '东方财富', 'category': 'finance', 'weight': 9, 'influence': 'high'}, + 'xueqiu': {'name': '雪球', 'category': 'finance', 'weight': 8, 'influence': 'high'}, + 'cls': {'name': '财联社', 'category': 'finance', 'weight': 8, 'influence': 'high'}, + 'wallstreetcn': {'name': '华尔街见闻', 'category': 'finance', 'weight': 7, 'influence': 'medium'}, + + # 科技平台 - 3个 + 'tskr': {'name': '36氪', 'category': 'tech', 'weight': 6, 'influence': 'medium'}, + 'sspai': {'name': '少数派', 'category': 'tech', 'weight': 5, 'influence': 'low'}, + 'juejin': {'name': '掘金', 'category': 'tech', 'weight': 5, 'influence': 'low'}, + } + + # 平台类别权重(用于转化率计算) + self.category_weights = { + 'finance': 1.5, # 财经平台转化率高 + 'social': 1.2, # 社交媒体传播快 + 'news': 1.0, # 新闻媒体正常 + 'tech': 0.8, # 科技平台相关性低 + } + + # 停用词(过滤无意义的词) + self.stop_words = { + '的', '是', '在', '了', '和', '与', '等', '为', '将', '被', + '有', '一', '个', '上', '下', '中', '大', '新', '年', '月', '日', + '这', '那', '其', '之', '也', '要', '就', '不', '我', '你', '他', + '来', '去', '到', '说', '会', '能', '都', '对', '着', '让', + '从', '以', '及', '或', '如', '还', '没', '很', '更', '最', + } + + def get_platform_news(self, platform: str) -> Dict: + """ + 获取单个平台的新闻数据 + + Args: + platform: 平台代码(如 'weibo', 'baidu'等) + + Returns: + { + 'success': bool, + 'platform': str, + 'platform_name': str, + 'category': str, + 'weight': int, + 'influence': str, + 'data': List[Dict], + 'count': int, + 'fetch_time': str, + 'error': str (如果失败) + } + """ + try: + url = f"{self.base_url}?platform={platform}" + + logger.info(f"正在获取 {platform} 平台数据...") + response = requests.get(url, timeout=self.timeout) + response.raise_for_status() + + data = response.json() + + if data.get('status') == '200': + news_list = data.get('data', []) + platform_info = self.platforms.get(platform, {}) + + # 为每条新闻添加排名信息 + for i, news in enumerate(news_list): + news['rank'] = i + 1 + news['platform'] = platform + + return { + 'success': True, + 'platform': platform, + 'platform_name': platform_info.get('name', platform), + 'category': platform_info.get('category', 'other'), + 'weight': platform_info.get('weight', 5), + 'influence': platform_info.get('influence', 'medium'), + 'data': news_list, + 'count': len(news_list), + 'fetch_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + } + else: + return { + 'success': False, + 'platform': platform, + 'error': f"API返回错误: {data.get('msg', '未知错误')}" + } + + except requests.exceptions.Timeout: + return { + 'success': False, + 'platform': platform, + 'error': f"请求超时({self.timeout}秒)" + } + except requests.exceptions.ConnectionError: + return { + 'success': False, + 'platform': platform, + 'error': "网络连接失败" + } + except Exception as e: + return { + 'success': False, + 'platform': platform, + 'error': f"获取数据失败: {str(e)}" + } + + def get_multi_platform_news(self, platforms: List[str] = None, + category: str = None) -> Dict: + """ + 获取多个平台的新闻数据 + + Args: + platforms: 平台列表,None表示获取所有 + category: 按类别筛选('social', 'news', 'finance', 'tech') + + Returns: + { + 'success': bool, + 'total_platforms': int, + 'success_count': int, + 'failed_count': int, + 'platforms_data': List[Dict], + 'fetch_time': str + } + """ + # 确定要获取的平台列表 + if platforms is None: + if category: + target_platforms = [ + p for p, info in self.platforms.items() + if info.get('category') == category + ] + else: + target_platforms = list(self.platforms.keys()) + else: + target_platforms = platforms + + results = [] + success_count = 0 + failed_count = 0 + + for platform in target_platforms: + result = self.get_platform_news(platform) + results.append(result) + + if result['success']: + success_count += 1 + else: + failed_count += 1 + + # 避免请求过快,休息0.3秒 + time.sleep(0.3) + + return { + 'success': success_count > 0, + 'total_platforms': len(target_platforms), + 'success_count': success_count, + 'failed_count': failed_count, + 'platforms_data': results, + 'fetch_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + } + + def extract_stock_related_news(self, platforms_data: List[Dict], + keywords: List[str] = None) -> List[Dict]: + """ + 从新闻数据中提取股票相关的新闻 + + Args: + platforms_data: 平台数据列表 + keywords: 股票相关关键词列表 + + Returns: + List[Dict]: 股票相关新闻列表 + """ + if keywords is None: + keywords = [ + # 股市基础词汇 + '股', '股市', '股票', 'A股', '港股', '美股', '创业板', '科创板', '北交所', + '涨停', '跌停', '大涨', '暴涨', '飙升', '暴跌', '涨幅', '跌幅', '翻倍', + '概念股', '龙头股', '妖股', '题材股', '白马股', '蓝筹股', '成长股', + '上市', 'IPO', '重组', '并购', '收购', '增发', '回购', '减持', '增持', + '业绩', '财报', '利好', '利空', '预增', '预减', '盈利', '亏损', + '牛市', '熊市', '反弹', '回调', '震荡', '突破', '新高', + '主力', '游资', '北向资金', '外资', '机构', '资金流入', '资金流出', + '板块', '行业', '赛道', '题材', '轮动', '热点', + + # 热门板块关键词 + '芯片', '半导体', '光刻机', '封装', '存储', + '新能源', '锂电', '光伏', '储能', '风电', '氢能', + 'AI', '人工智能', '大模型', 'ChatGPT', 'DeepSeek', '算力', 'GPU', + '机器人', '人形机器人', '工业机器人', '减速器', '伺服电机', + '低空经济', '无人机', 'eVTOL', '飞行汽车', + '数据要素', '数字经济', '信创', '国产替代', + '医药', '创新药', 'CXO', '医疗器械', '中药', + '消费', '白酒', '食品', '旅游', '免税', + '军工', '国防', '航空', '航天', '船舶', + '汽车', '新能源车', '智能驾驶', '无人驾驶', '充电桩', + '地产', '房地产', '楼市', '房价', + '金融', '银行', '保险', '券商', '证券', + + # 政策相关 + '政策', '利率', '降息', '降准', '货币政策', '财政政策', + '国常会', '证监会', '央行', '发改委', '工信部', + ] + + stock_related = [] + + for platform_data in platforms_data: + if not platform_data.get('success'): + continue + + platform = platform_data['platform'] + platform_name = platform_data['platform_name'] + category = platform_data['category'] + weight = platform_data['weight'] + influence = platform_data.get('influence', 'medium') + + for news in platform_data.get('data', []): + title = news.get('title') or '' + content = news.get('content') or '' + rank = news.get('rank', 99) + + # 检查是否包含股票关键词 + text = f"{title} {content}" + matched_keywords = [kw for kw in keywords if kw in text] + + if matched_keywords: + # 计算综合得分(排名越靠前、权重越高得分越高) + rank_score = max(0, 100 - rank * 2) # 排名1得98分,排名50得0分 + weight_score = weight * 10 + keyword_score = len(matched_keywords) * 5 + total_score = rank_score + weight_score + keyword_score + + stock_related.append({ + 'platform': platform, + 'platform_name': platform_name, + 'category': category, + 'weight': weight, + 'influence': influence, + 'rank': rank, + 'title': title, + 'content': content, + 'url': news.get('url') or '', + 'source': news.get('source') or platform_name, + 'publish_time': news.get('publish_time') or '', + 'matched_keywords': matched_keywords, + 'keyword_count': len(matched_keywords), + 'score': total_score, + }) + + # 按综合得分排序 + stock_related.sort(key=lambda x: x['score'], reverse=True) + + return stock_related + + def calculate_flow_score(self, platforms_data: List[Dict]) -> Dict: + """ + 计算流量得分 + + 根据"流量为王"理念,评估当前市场热度 + + Returns: + { + 'total_score': int, # 总流量得分(0-1000) + 'social_score': int, # 社交媒体流量得分 + 'news_score': int, # 新闻媒体流量得分 + 'finance_score': int, # 财经平台流量得分 + 'tech_score': int, # 科技平台流量得分 + 'level': str, # 流量等级(低/中/高/极高) + 'analysis': str, # 流量分析 + 'platform_details': List[Dict], # 各平台详情 + } + """ + scores = { + 'social': 0, + 'news': 0, + 'finance': 0, + 'tech': 0, + } + + platform_details = [] + + for platform_data in platforms_data: + if not platform_data.get('success'): + continue + + category = platform_data['category'] + weight = platform_data['weight'] + count = platform_data['count'] + platform_name = platform_data['platform_name'] + + # 计算得分:权重 × 新闻数量 + score = weight * count + scores[category] = scores.get(category, 0) + score + + platform_details.append({ + 'platform': platform_data['platform'], + 'platform_name': platform_name, + 'category': category, + 'count': count, + 'score': score, + }) + + # 计算总分 + total_score = sum(scores.values()) + + # 归一化到0-1000 + if total_score > 0: + normalized_score = min(int(total_score / 50), 1000) + else: + normalized_score = 0 + + # 确定流量等级 + if normalized_score >= 800: + level = "极高" + analysis = "流量爆发!市场情绪极度活跃,大量新闻热点,存在热点题材炒作机会。建议:密切关注龙头股,注意追高风险。" + elif normalized_score >= 500: + level = "高" + analysis = "流量较高。市场有明确热点,资金活跃度较好。建议:关注热点板块,注意节奏把握。" + elif normalized_score >= 200: + level = "中" + analysis = "流量正常。市场处于常态,有一定热点但不突出。建议:观望为主,等待明确信号。" + else: + level = "低" + analysis = "流量较低。市场情绪低迷,缺乏热点。建议:控制仓位,等待市场转暖。" + + return { + 'total_score': normalized_score, + 'social_score': scores.get('social', 0), + 'news_score': scores.get('news', 0), + 'finance_score': scores.get('finance', 0), + 'tech_score': scores.get('tech', 0), + 'level': level, + 'analysis': analysis, + 'platform_details': platform_details, + } + + def get_hot_topics(self, platforms_data: List[Dict], top_n: int = 20) -> List[Dict]: + """ + 获取热门话题(基于标题词频分析) + + Returns: + List[Dict]: 热门话题列表 + """ + import jieba + + # 收集所有标题及其来源信息 + all_titles = [] + title_sources = {} # 记录每个标题来自哪些平台 + + for platform_data in platforms_data: + if platform_data.get('success'): + platform_name = platform_data['platform_name'] + for news in platform_data.get('data', []): + title = news.get('title') or '' + if title: + all_titles.append(title) + if title not in title_sources: + title_sources[title] = [] + title_sources[title].append(platform_name) + + # 分词并统计 + word_counter = Counter() + word_sources = {} # 记录每个词出现在哪些平台 + + for title in all_titles: + if title: + words = jieba.cut(title) + for word in words: + if len(word) >= 2 and word not in self.stop_words: + word_counter[word] += 1 + if word not in word_sources: + word_sources[word] = set() + for source in title_sources.get(title, []): + word_sources[word].add(source) + + # 获取TOP N + hot_topics = [] + total_titles = len(all_titles) if all_titles else 1 + + for word, count in word_counter.most_common(top_n): + sources = list(word_sources.get(word, [])) + cross_platform = len(sources) # 跨平台数 + heat = min(int(count / total_titles * 1000), 100) + + # 跨平台加成 + if cross_platform >= 5: + heat = min(heat + 20, 100) + elif cross_platform >= 3: + heat = min(heat + 10, 100) + + hot_topics.append({ + 'topic': word, + 'count': count, + 'heat': heat, + 'cross_platform': cross_platform, + 'sources': sources[:5], # 最多显示5个来源 + }) + + return hot_topics + + def get_platform_ranking(self, platforms_data: List[Dict]) -> List[Dict]: + """ + 获取跨平台热度排名 + + 合并所有平台的新闻,按热度排序 + + Returns: + List[Dict]: 排名列表 + """ + all_news = [] + + for platform_data in platforms_data: + if not platform_data.get('success'): + continue + + platform = platform_data['platform'] + platform_name = platform_data['platform_name'] + weight = platform_data['weight'] + + for news in platform_data.get('data', []): + title = news.get('title') or '' + rank = news.get('rank', 99) + + if not title: + continue + + # 计算综合热度分数 + # 排名越靠前分数越高,平台权重越高分数越高 + heat_score = (100 - rank) * weight + + all_news.append({ + 'title': title, + 'platform': platform, + 'platform_name': platform_name, + 'original_rank': rank, + 'heat_score': heat_score, + 'url': news.get('url') or '', + 'content': news.get('content') or '', + }) + + # 按热度分数排序 + all_news.sort(key=lambda x: x['heat_score'], reverse=True) + + # 添加全局排名 + for i, news in enumerate(all_news): + news['global_rank'] = i + 1 + + return all_news + + def calculate_viral_coefficient(self, current_data: Dict, + previous_data: Dict) -> Dict: + """ + 计算病毒系数K值 + + K值 = 当前传播量 / 上期传播量 + + Args: + current_data: 当前数据 + previous_data: 上期数据 + + Returns: + { + 'k_value': float, # K值 + 'trend': str, # 趋势(指数型/线性/衰减) + 'analysis': str, # 分析 + } + """ + current_score = current_data.get('total_score', 0) + previous_score = previous_data.get('total_score', 0) + + if previous_score == 0: + k_value = 1.0 + trend = "无历史数据" + analysis = "首次采集,无法计算K值" + else: + k_value = round(current_score / previous_score, 2) + + if k_value > 1.5: + trend = "指数型爆发" + analysis = f"K值={k_value},流量正在指数型增长!这是病毒式传播的特征,题材可能进入加速期。" + elif k_value > 1.0: + trend = "线性增长" + analysis = f"K值={k_value},流量稳步增长,题材正在发酵中。" + elif k_value == 1.0: + trend = "平稳" + analysis = f"K值={k_value},流量保持稳定,市场处于平衡状态。" + else: + trend = "衰减" + analysis = f"K值={k_value},流量正在衰减,题材热度下降,注意风险。" + + return { + 'k_value': k_value, + 'current_score': current_score, + 'previous_score': previous_score, + 'trend': trend, + 'analysis': analysis, + } + + def detect_flow_type(self, history_scores: List[int], + current_score: int) -> Dict: + """ + 识别流量类型(存量流量型 vs 增量流量型) + + 存量流量型:出生自带顶流,流量快速到位(政策/大事件) + 增量流量型:初始流量小,具备病毒传播能力(话题发酵) + + Args: + history_scores: 历史得分列表(从旧到新) + current_score: 当前得分 + + Returns: + { + 'flow_type': str, # 存量流量型/增量流量型/未知 + 'characteristics': List[str], # 特征 + 'time_window': str, # 时间窗口建议 + 'operation': str, # 操作建议 + } + """ + if len(history_scores) < 2: + return { + 'flow_type': '未知', + 'characteristics': ['历史数据不足'], + 'time_window': '无法判断', + 'operation': '继续观察', + } + + # 计算初始得分和增长率 + initial_score = history_scores[0] if history_scores else 0 + avg_score = sum(history_scores) / len(history_scores) + + # 计算增长趋势 + growth_rates = [] + for i in range(1, len(history_scores)): + if history_scores[i-1] > 0: + rate = (history_scores[i] - history_scores[i-1]) / history_scores[i-1] + growth_rates.append(rate) + + avg_growth = sum(growth_rates) / len(growth_rates) if growth_rates else 0 + + # 判断流量类型 + if initial_score >= 500: + # 初始热度高 -> 存量流量型 + flow_type = "存量流量型" + characteristics = [ + "初始热度高", + "流量快速到位", + "可能与政策/大事件相关", + ] + time_window = "时间窗口短(2-3天)" + operation = "快进快出,密切关注热度变化" + elif avg_growth > 0.2 and len([r for r in growth_rates if r > 0]) >= 2: + # 持续增长 -> 增量流量型 + flow_type = "增量流量型" + characteristics = [ + "初始热度低", + "逐步攀升", + "具备病毒传播特征", + ] + time_window = "时间窗口长(5-10天)" + operation = "可以埋伏,等待加速" + else: + flow_type = "常规流量" + characteristics = [ + "热度波动正常", + "无明显趋势", + ] + time_window = "无特定窗口" + operation = "保持观望" + + return { + 'flow_type': flow_type, + 'characteristics': characteristics, + 'time_window': time_window, + 'operation': operation, + 'initial_score': initial_score, + 'current_score': current_score, + 'avg_growth': round(avg_growth * 100, 1), # 百分比 + } + + def get_platform_list(self) -> List[Dict]: + """ + 获取所有支持的平台列表 + + Returns: + List[Dict]: 平台列表 + """ + result = [] + for code, info in self.platforms.items(): + result.append({ + 'code': code, + 'name': info['name'], + 'category': info['category'], + 'weight': info['weight'], + 'influence': info.get('influence', 'medium'), + }) + + # 按权重排序 + result.sort(key=lambda x: x['weight'], reverse=True) + return result + + def get_platforms_by_category(self) -> Dict[str, List[str]]: + """ + 按类别获取平台列表 + + Returns: + Dict[str, List[str]]: 类别 -> 平台代码列表 + """ + categories = {} + for code, info in self.platforms.items(): + category = info['category'] + if category not in categories: + categories[category] = [] + categories[category].append(code) + return categories + + +# 测试代码 +if __name__ == "__main__": + fetcher = NewsFlowDataFetcher() + + # 测试获取平台列表 + print("=== 支持的平台列表 ===") + platforms = fetcher.get_platform_list() + print(f"共支持 {len(platforms)} 个平台:") + for p in platforms[:5]: + print(f" - {p['name']} ({p['code']}) - 权重:{p['weight']}") + + # 测试获取单个平台 + print("\n=== 测试获取微博热搜 ===") + result = fetcher.get_platform_news('weibo') + if result['success']: + print(f"✅ 成功获取 {result['count']} 条新闻") + print(f"前3条标题:") + for news in result['data'][:3]: + print(f" {news['rank']}. {news['title']}") + else: + print(f"❌ 失败: {result['error']}") + + print("\n=== 测试获取财经平台 ===") + result = fetcher.get_multi_platform_news(category='finance') + print(f"总共 {result['total_platforms']} 个平台") + print(f"成功 {result['success_count']} 个") + print(f"失败 {result['failed_count']} 个") + + # 提取股票相关新闻 + stock_news = fetcher.extract_stock_related_news(result['platforms_data']) + print(f"\n✅ 提取到 {len(stock_news)} 条股票相关新闻") + + # 计算流量得分 + flow_score = fetcher.calculate_flow_score(result['platforms_data']) + print(f"\n流量得分: {flow_score['total_score']}") + print(f"流量等级: {flow_score['level']}") + + # 获取热门话题 + hot_topics = fetcher.get_hot_topics(result['platforms_data']) + print(f"\n热门话题TOP5:") + for i, topic in enumerate(hot_topics[:5], 1): + print(f" {i}. {topic['topic']} (热度:{topic['heat']}, 跨{topic['cross_platform']}平台)") diff --git a/news_flow_db.py b/news_flow_db.py new file mode 100644 index 0000000..03ea779 --- /dev/null +++ b/news_flow_db.py @@ -0,0 +1,1021 @@ +""" +新闻流量数据库模块 +用于存储和管理新闻流量监测数据 +包含:快照、新闻、情绪、预警、AI分析、定时任务日志 +""" +import sqlite3 +import json +import logging +from datetime import datetime, timedelta +from typing import Dict, List, Optional +from collections import Counter + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class NewsFlowDatabase: + """新闻流量数据库管理类""" + + def __init__(self, db_path: str = "news_flow.db"): + self.db_path = db_path + self.init_database() + + def get_connection(self): + """获取数据库连接""" + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + return conn + + def init_database(self): + """初始化数据库表""" + conn = self.get_connection() + cursor = conn.cursor() + + # 1. 新闻流量快照表(记录每次监测的整体情况) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS flow_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fetch_time TEXT NOT NULL, + total_platforms INTEGER NOT NULL, + success_count INTEGER NOT NULL, + total_score INTEGER NOT NULL, + flow_level TEXT NOT NULL, + social_score INTEGER, + news_score INTEGER, + finance_score INTEGER, + tech_score INTEGER, + analysis TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 2. 平台新闻表(存储各平台的新闻数据) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS platform_news ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_id INTEGER NOT NULL, + platform TEXT NOT NULL, + platform_name TEXT NOT NULL, + category TEXT NOT NULL, + weight INTEGER NOT NULL, + title TEXT NOT NULL, + content TEXT, + url TEXT, + source TEXT, + publish_time TEXT, + rank INTEGER, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (snapshot_id) REFERENCES flow_snapshots(id) + ) + ''') + + # 3. 股票相关新闻表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS stock_related_news ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_id INTEGER NOT NULL, + platform TEXT NOT NULL, + platform_name TEXT NOT NULL, + category TEXT NOT NULL, + weight INTEGER NOT NULL, + title TEXT NOT NULL, + content TEXT, + url TEXT, + source TEXT, + publish_time TEXT, + matched_keywords TEXT, + keyword_count INTEGER, + score INTEGER, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (snapshot_id) REFERENCES flow_snapshots(id) + ) + ''') + + # 4. 热门话题表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS hot_topics ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_id INTEGER NOT NULL, + topic TEXT NOT NULL, + count INTEGER NOT NULL, + heat INTEGER NOT NULL, + cross_platform INTEGER, + sources TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (snapshot_id) REFERENCES flow_snapshots(id) + ) + ''') + + # 5. 监测历史统计表(按天汇总) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS flow_statistics ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL UNIQUE, + avg_score INTEGER, + max_score INTEGER, + min_score INTEGER, + snapshot_count INTEGER, + top_topics TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 6. 情绪指标记录表【新增】 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS sentiment_records ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_id INTEGER, + sentiment_index INTEGER NOT NULL, + sentiment_class TEXT NOT NULL, + flow_stage TEXT NOT NULL, + momentum REAL, + viral_k REAL, + flow_type TEXT, + stage_analysis TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (snapshot_id) REFERENCES flow_snapshots(id) + ) + ''') + + # 7. 预警记录表【新增】 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS flow_alerts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alert_type TEXT NOT NULL, + alert_level TEXT NOT NULL, + title TEXT NOT NULL, + content TEXT, + related_topics TEXT, + trigger_value TEXT, + threshold_value TEXT, + is_notified INTEGER DEFAULT 0, + snapshot_id INTEGER, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (snapshot_id) REFERENCES flow_snapshots(id) + ) + ''') + + # 8. AI分析记录表【新增】 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS ai_analysis ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snapshot_id INTEGER, + affected_sectors TEXT, + recommended_stocks TEXT, + risk_level TEXT, + risk_factors TEXT, + advice TEXT, + confidence INTEGER, + summary TEXT, + raw_response TEXT, + model_used TEXT, + analysis_time REAL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (snapshot_id) REFERENCES flow_snapshots(id) + ) + ''') + + # 9. 定时任务日志表【新增】 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS scheduler_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_name TEXT NOT NULL, + task_type TEXT, + status TEXT NOT NULL, + message TEXT, + duration REAL, + snapshot_id INTEGER, + executed_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 10. 预警配置表【新增】 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS alert_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_key TEXT NOT NULL UNIQUE, + config_value TEXT NOT NULL, + description TEXT, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 初始化预警配置默认值 + default_configs = [ + ('heat_threshold', '800', '热度飙升阈值'), + ('rank_change_threshold', '10', '排名变化阈值'), + ('sentiment_high_threshold', '90', '情绪高位阈值'), + ('sentiment_low_threshold', '20', '情绪低位阈值'), + ('viral_k_threshold', '1.5', 'K值阈值'), + ('alert_enabled', 'true', '预警开关'), + ('notification_enabled', 'true', '通知开关'), + ] + + for key, value, desc in default_configs: + cursor.execute(''' + INSERT OR IGNORE INTO alert_config (config_key, config_value, description) + VALUES (?, ?, ?) + ''', (key, value, desc)) + + # 数据库迁移:添加缺失的列 + self._migrate_database(cursor) + + conn.commit() + conn.close() + logger.info("✅ 新闻流量数据库初始化完成") + + def _migrate_database(self, cursor): + """数据库迁移:添加缺失的列""" + # 定义需要迁移的列 + migrations = [ + # (表名, 列名, 列定义) + ('stock_related_news', 'score', 'INTEGER DEFAULT 0'), + ('stock_related_news', 'rank', 'INTEGER'), + ('platform_news', 'rank', 'INTEGER'), + ('hot_topics', 'cross_platform', 'INTEGER'), + ('hot_topics', 'sources', 'TEXT'), + ] + + for table, column, column_def in migrations: + try: + # 检查列是否存在 + cursor.execute(f"PRAGMA table_info({table})") + columns = [row[1] for row in cursor.fetchall()] + + if column not in columns: + cursor.execute(f"ALTER TABLE {table} ADD COLUMN {column} {column_def}") + logger.info(f"✅ 迁移: 向 {table} 添加列 {column}") + except Exception as e: + logger.warning(f"迁移列 {table}.{column} 时出错: {e}") + + # ==================== 快照相关方法 ==================== + + def save_flow_snapshot(self, flow_data: Dict, platforms_data: List[Dict], + stock_news: List[Dict], hot_topics: List[Dict]) -> int: + """ + 保存完整的流量快照 + + Args: + flow_data: 流量得分数据 + platforms_data: 平台新闻数据 + stock_news: 股票相关新闻 + hot_topics: 热门话题 + + Returns: + snapshot_id: 快照ID + """ + conn = self.get_connection() + cursor = conn.cursor() + + try: + # 1. 保存快照主表 + cursor.execute(''' + INSERT INTO flow_snapshots + (fetch_time, total_platforms, success_count, total_score, flow_level, + social_score, news_score, finance_score, tech_score, analysis) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + len(platforms_data), + sum(1 for p in platforms_data if p.get('success')), + flow_data['total_score'], + flow_data['level'], + flow_data.get('social_score', 0), + flow_data.get('news_score', 0), + flow_data.get('finance_score', 0), + flow_data.get('tech_score', 0), + flow_data.get('analysis', '') + )) + + snapshot_id = cursor.lastrowid + + # 2. 保存平台新闻 + for platform_data in platforms_data: + if not platform_data.get('success'): + continue + + for news in platform_data.get('data', []): + cursor.execute(''' + INSERT INTO platform_news + (snapshot_id, platform, platform_name, category, weight, + title, content, url, source, publish_time, rank) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + snapshot_id, + platform_data['platform'], + platform_data['platform_name'], + platform_data['category'], + platform_data['weight'], + news.get('title') or '', + news.get('content') or '', + news.get('url') or '', + news.get('source') or '', + news.get('publish_time') or '', + news.get('rank', 0) + )) + + # 3. 保存股票相关新闻 + for news in stock_news: + cursor.execute(''' + INSERT INTO stock_related_news + (snapshot_id, platform, platform_name, category, weight, + title, content, url, source, publish_time, matched_keywords, keyword_count, score) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + snapshot_id, + news['platform'], + news['platform_name'], + news['category'], + news['weight'], + news['title'], + news.get('content') or '', + news.get('url') or '', + news.get('source') or '', + news.get('publish_time') or '', + json.dumps(news.get('matched_keywords', []), ensure_ascii=False), + news.get('keyword_count', 0), + news.get('score', 0) + )) + + # 4. 保存热门话题 + for topic in hot_topics: + cursor.execute(''' + INSERT INTO hot_topics + (snapshot_id, topic, count, heat, cross_platform, sources) + VALUES (?, ?, ?, ?, ?, ?) + ''', ( + snapshot_id, + topic['topic'], + topic['count'], + topic['heat'], + topic.get('cross_platform', 0), + json.dumps(topic.get('sources', []), ensure_ascii=False) + )) + + # 5. 更新每日统计 + self._update_daily_statistics(cursor, flow_data['total_score'], hot_topics) + + conn.commit() + logger.info(f"✅ 保存流量快照成功,ID: {snapshot_id}") + return snapshot_id + + except Exception as e: + conn.rollback() + logger.error(f"❌ 保存流量快照失败: {e}") + raise + finally: + conn.close() + + def _update_daily_statistics(self, cursor, score: int, hot_topics: List[Dict]): + """更新每日统计""" + today = datetime.now().strftime('%Y-%m-%d') + + cursor.execute(''' + SELECT avg_score, max_score, min_score, snapshot_count, top_topics + FROM flow_statistics WHERE date = ? + ''', (today,)) + + row = cursor.fetchone() + + if row: + old_avg = row['avg_score'] or 0 + old_count = row['snapshot_count'] or 0 + new_avg = int((old_avg * old_count + score) / (old_count + 1)) + new_max = max(row['max_score'] or 0, score) + new_min = min(row['min_score'] or 999999, score) + + old_topics = json.loads(row['top_topics']) if row['top_topics'] else [] + new_topics = old_topics + [t['topic'] for t in hot_topics[:10]] + topic_counter = Counter(new_topics) + top_topics = [topic for topic, _ in topic_counter.most_common(20)] + + cursor.execute(''' + UPDATE flow_statistics + SET avg_score = ?, max_score = ?, min_score = ?, + snapshot_count = ?, top_topics = ? + WHERE date = ? + ''', (new_avg, new_max, new_min, old_count + 1, + json.dumps(top_topics, ensure_ascii=False), today)) + else: + top_topics = [t['topic'] for t in hot_topics[:20]] + cursor.execute(''' + INSERT INTO flow_statistics + (date, avg_score, max_score, min_score, snapshot_count, top_topics) + VALUES (?, ?, ?, ?, ?, ?) + ''', (today, score, score, score, 1, + json.dumps(top_topics, ensure_ascii=False))) + + def get_latest_snapshot(self) -> Optional[Dict]: + """获取最新的流量快照""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT * FROM flow_snapshots + ORDER BY created_at DESC LIMIT 1 + ''') + + row = cursor.fetchone() + conn.close() + + if row: + return dict(row) + return None + + def get_recent_snapshots(self, limit: int = 10) -> List[Dict]: + """获取最近的流量快照列表""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT * FROM flow_snapshots + ORDER BY created_at DESC LIMIT ? + ''', (limit,)) + + snapshots = [] + for row in cursor.fetchall(): + snapshots.append(dict(row)) + + conn.close() + return snapshots + + def get_snapshot_detail(self, snapshot_id: int) -> Dict: + """获取快照详细信息""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute('SELECT * FROM flow_snapshots WHERE id = ?', (snapshot_id,)) + row = cursor.fetchone() + if not row: + conn.close() + return {} + + snapshot = dict(row) + + cursor.execute(''' + SELECT * FROM stock_related_news + WHERE snapshot_id = ? + ORDER BY COALESCE(score, 0) DESC, weight DESC + ''', (snapshot_id,)) + + stock_news = [] + for row in cursor.fetchall(): + news = dict(row) + news['matched_keywords'] = json.loads(news['matched_keywords']) if news['matched_keywords'] else [] + stock_news.append(news) + + cursor.execute(''' + SELECT * FROM hot_topics + WHERE snapshot_id = ? + ORDER BY heat DESC + ''', (snapshot_id,)) + + hot_topics = [] + for row in cursor.fetchall(): + topic = dict(row) + topic['sources'] = json.loads(topic['sources']) if topic['sources'] else [] + hot_topics.append(topic) + + # 获取情绪记录 + cursor.execute(''' + SELECT * FROM sentiment_records + WHERE snapshot_id = ? + ORDER BY created_at DESC LIMIT 1 + ''', (snapshot_id,)) + sentiment_row = cursor.fetchone() + sentiment = dict(sentiment_row) if sentiment_row else None + + # 获取AI分析 + cursor.execute(''' + SELECT * FROM ai_analysis + WHERE snapshot_id = ? + ORDER BY created_at DESC LIMIT 1 + ''', (snapshot_id,)) + ai_row = cursor.fetchone() + ai_analysis = None + if ai_row: + ai_analysis = dict(ai_row) + ai_analysis['affected_sectors'] = json.loads(ai_analysis['affected_sectors']) if ai_analysis['affected_sectors'] else [] + ai_analysis['recommended_stocks'] = json.loads(ai_analysis['recommended_stocks']) if ai_analysis['recommended_stocks'] else [] + ai_analysis['risk_factors'] = json.loads(ai_analysis['risk_factors']) if ai_analysis['risk_factors'] else [] + + conn.close() + + return { + 'snapshot': snapshot, + 'stock_news': stock_news, + 'hot_topics': hot_topics, + 'sentiment': sentiment, + 'ai_analysis': ai_analysis, + } + + def get_history_snapshots(self, limit: int = 50) -> List[Dict]: + """获取历史快照列表""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT id, fetch_time, total_score, flow_level, + success_count, total_platforms, analysis + FROM flow_snapshots + ORDER BY created_at DESC + LIMIT ? + ''', (limit,)) + + snapshots = [dict(row) for row in cursor.fetchall()] + conn.close() + + return snapshots + + def get_daily_statistics(self, days: int = 7) -> List[Dict]: + """获取每日统计数据""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT * FROM flow_statistics + ORDER BY date DESC + LIMIT ? + ''', (days,)) + + stats = [] + for row in cursor.fetchall(): + stat = dict(row) + stat['top_topics'] = json.loads(stat['top_topics']) if stat['top_topics'] else [] + stats.append(stat) + + conn.close() + return stats + + def get_recent_scores(self, hours: int = 24) -> List[Dict]: + """获取最近N小时的得分记录""" + conn = self.get_connection() + cursor = conn.cursor() + + since = (datetime.now() - timedelta(hours=hours)).strftime('%Y-%m-%d %H:%M:%S') + + cursor.execute(''' + SELECT id, fetch_time, total_score, flow_level + FROM flow_snapshots + WHERE fetch_time >= ? + ORDER BY fetch_time ASC + ''', (since,)) + + scores = [dict(row) for row in cursor.fetchall()] + conn.close() + return scores + + def search_stock_news(self, keyword: str, limit: int = 50) -> List[Dict]: + """搜索股票相关新闻""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT srn.*, fs.fetch_time, fs.flow_level + FROM stock_related_news srn + JOIN flow_snapshots fs ON srn.snapshot_id = fs.id + WHERE srn.title LIKE ? OR srn.content LIKE ? + ORDER BY srn.created_at DESC + LIMIT ? + ''', (f'%{keyword}%', f'%{keyword}%', limit)) + + results = [] + for row in cursor.fetchall(): + news = dict(row) + news['matched_keywords'] = json.loads(news['matched_keywords']) if news['matched_keywords'] else [] + results.append(news) + + conn.close() + return results + + # ==================== 情绪记录相关方法 ==================== + + def save_sentiment_record(self, snapshot_id: int, sentiment_data: Dict) -> int: + """保存情绪记录""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO sentiment_records + (snapshot_id, sentiment_index, sentiment_class, flow_stage, + momentum, viral_k, flow_type, stage_analysis) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + snapshot_id, + sentiment_data.get('sentiment_index', 50), + sentiment_data.get('sentiment_class', '中性'), + sentiment_data.get('flow_stage', '未知'), + sentiment_data.get('momentum', 0), + sentiment_data.get('viral_k', 1.0), + sentiment_data.get('flow_type', '未知'), + sentiment_data.get('stage_analysis', '') + )) + + record_id = cursor.lastrowid + conn.commit() + conn.close() + + return record_id + + def get_sentiment_history(self, limit: int = 50) -> List[Dict]: + """获取情绪历史记录""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT sr.*, fs.fetch_time, fs.total_score + FROM sentiment_records sr + LEFT JOIN flow_snapshots fs ON sr.snapshot_id = fs.id + ORDER BY sr.created_at DESC + LIMIT ? + ''', (limit,)) + + records = [dict(row) for row in cursor.fetchall()] + conn.close() + return records + + def get_latest_sentiment(self) -> Optional[Dict]: + """获取最新情绪记录""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT sr.*, fs.fetch_time, fs.total_score, fs.flow_level + FROM sentiment_records sr + LEFT JOIN flow_snapshots fs ON sr.snapshot_id = fs.id + ORDER BY sr.created_at DESC + LIMIT 1 + ''') + + row = cursor.fetchone() + conn.close() + + return dict(row) if row else None + + # ==================== 预警相关方法 ==================== + + def save_alert(self, alert_data: Dict) -> int: + """保存预警记录""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO flow_alerts + (alert_type, alert_level, title, content, related_topics, + trigger_value, threshold_value, is_notified, snapshot_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + alert_data['alert_type'], + alert_data.get('alert_level', 'info'), + alert_data['title'], + alert_data.get('content', ''), + json.dumps(alert_data.get('related_topics', []), ensure_ascii=False), + str(alert_data.get('trigger_value', '')), + str(alert_data.get('threshold_value', '')), + 1 if alert_data.get('is_notified') else 0, + alert_data.get('snapshot_id') + )) + + alert_id = cursor.lastrowid + conn.commit() + conn.close() + + return alert_id + + def get_alerts(self, days: int = 7, alert_type: str = None) -> List[Dict]: + """获取预警记录""" + conn = self.get_connection() + cursor = conn.cursor() + + since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') + + if alert_type: + cursor.execute(''' + SELECT * FROM flow_alerts + WHERE created_at >= ? AND alert_type = ? + ORDER BY created_at DESC + ''', (since, alert_type)) + else: + cursor.execute(''' + SELECT * FROM flow_alerts + WHERE created_at >= ? + ORDER BY created_at DESC + ''', (since,)) + + alerts = [] + for row in cursor.fetchall(): + alert = dict(row) + alert['related_topics'] = json.loads(alert['related_topics']) if alert['related_topics'] else [] + alerts.append(alert) + + conn.close() + return alerts + + def get_unnotified_alerts(self) -> List[Dict]: + """获取未通知的预警""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT * FROM flow_alerts + WHERE is_notified = 0 + ORDER BY created_at DESC + ''') + + alerts = [] + for row in cursor.fetchall(): + alert = dict(row) + alert['related_topics'] = json.loads(alert['related_topics']) if alert['related_topics'] else [] + alerts.append(alert) + + conn.close() + return alerts + + def mark_alert_notified(self, alert_id: int): + """标记预警为已通知""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + UPDATE flow_alerts SET is_notified = 1 WHERE id = ? + ''', (alert_id,)) + + conn.commit() + conn.close() + + # ==================== AI分析相关方法 ==================== + + def save_ai_analysis(self, snapshot_id: int, analysis_data: Dict) -> int: + """保存AI分析结果""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO ai_analysis + (snapshot_id, affected_sectors, recommended_stocks, risk_level, + risk_factors, advice, confidence, summary, raw_response, model_used, analysis_time) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + snapshot_id, + json.dumps(analysis_data.get('affected_sectors', []), ensure_ascii=False), + json.dumps(analysis_data.get('recommended_stocks', []), ensure_ascii=False), + analysis_data.get('risk_level', '未知'), + json.dumps(analysis_data.get('risk_factors', []), ensure_ascii=False), + analysis_data.get('advice', '观望'), + analysis_data.get('confidence', 50), + analysis_data.get('summary', ''), + analysis_data.get('raw_response', ''), + analysis_data.get('model_used', 'deepseek-chat'), + analysis_data.get('analysis_time', 0) + )) + + analysis_id = cursor.lastrowid + conn.commit() + conn.close() + + return analysis_id + + def get_latest_ai_analysis(self) -> Optional[Dict]: + """获取最新AI分析结果""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT aa.*, fs.fetch_time, fs.total_score, fs.flow_level + FROM ai_analysis aa + LEFT JOIN flow_snapshots fs ON aa.snapshot_id = fs.id + ORDER BY aa.created_at DESC + LIMIT 1 + ''') + + row = cursor.fetchone() + conn.close() + + if row: + analysis = dict(row) + analysis['affected_sectors'] = json.loads(analysis['affected_sectors']) if analysis['affected_sectors'] else [] + analysis['recommended_stocks'] = json.loads(analysis['recommended_stocks']) if analysis['recommended_stocks'] else [] + analysis['risk_factors'] = json.loads(analysis['risk_factors']) if analysis['risk_factors'] else [] + return analysis + return None + + def get_ai_analysis_history(self, limit: int = 20) -> List[Dict]: + """获取AI分析历史""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT aa.*, fs.fetch_time, fs.total_score, fs.flow_level + FROM ai_analysis aa + LEFT JOIN flow_snapshots fs ON aa.snapshot_id = fs.id + ORDER BY aa.created_at DESC + LIMIT ? + ''', (limit,)) + + results = [] + for row in cursor.fetchall(): + analysis = dict(row) + analysis['affected_sectors'] = json.loads(analysis['affected_sectors']) if analysis['affected_sectors'] else [] + analysis['recommended_stocks'] = json.loads(analysis['recommended_stocks']) if analysis['recommended_stocks'] else [] + analysis['risk_factors'] = json.loads(analysis['risk_factors']) if analysis['risk_factors'] else [] + results.append(analysis) + + conn.close() + return results + + # ==================== 定时任务日志相关方法 ==================== + + def save_scheduler_log(self, task_name: str, task_type: str, + status: str, message: str = '', + duration: float = 0, snapshot_id: int = None) -> int: + """保存定时任务日志""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO scheduler_logs + (task_name, task_type, status, message, duration, snapshot_id) + VALUES (?, ?, ?, ?, ?, ?) + ''', (task_name, task_type, status, message, duration, snapshot_id)) + + log_id = cursor.lastrowid + conn.commit() + conn.close() + + return log_id + + def get_scheduler_logs(self, days: int = 7, task_type: str = None) -> List[Dict]: + """获取定时任务日志""" + conn = self.get_connection() + cursor = conn.cursor() + + since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') + + if task_type: + cursor.execute(''' + SELECT * FROM scheduler_logs + WHERE executed_at >= ? AND task_type = ? + ORDER BY executed_at DESC + ''', (since, task_type)) + else: + cursor.execute(''' + SELECT * FROM scheduler_logs + WHERE executed_at >= ? + ORDER BY executed_at DESC + ''', (since,)) + + logs = [dict(row) for row in cursor.fetchall()] + conn.close() + return logs + + # ==================== 预警配置相关方法 ==================== + + def get_alert_config(self, key: str) -> Optional[str]: + """获取预警配置""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT config_value FROM alert_config WHERE config_key = ? + ''', (key,)) + + row = cursor.fetchone() + conn.close() + + return row['config_value'] if row else None + + def set_alert_config(self, key: str, value: str, description: str = None): + """设置预警配置""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + INSERT OR REPLACE INTO alert_config (config_key, config_value, description, updated_at) + VALUES (?, ?, ?, ?) + ''', (key, value, description, datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) + + conn.commit() + conn.close() + + def get_all_alert_configs(self) -> Dict[str, str]: + """获取所有预警配置""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute('SELECT config_key, config_value FROM alert_config') + + configs = {row['config_key']: row['config_value'] for row in cursor.fetchall()} + conn.close() + return configs + + +# 全局数据库实例 +news_flow_db = NewsFlowDatabase() + + +# 测试代码 +if __name__ == "__main__": + print("=== 测试新闻流量数据库 ===") + + # 测试保存快照 + flow_data = { + 'total_score': 650, + 'social_score': 200, + 'news_score': 180, + 'finance_score': 220, + 'tech_score': 50, + 'level': '高', + 'analysis': '流量较高,市场活跃' + } + + platforms_data = [{ + 'success': True, + 'platform': 'weibo', + 'platform_name': '微博热搜', + 'category': 'social', + 'weight': 10, + 'data': [ + {'title': '某某股票大涨', 'content': '今日涨停', 'url': 'http://example.com', + 'source': '微博', 'publish_time': '2026-01-25 10:00:00', 'rank': 1} + ] + }] + + stock_news = [{ + 'platform': 'weibo', + 'platform_name': '微博热搜', + 'category': 'social', + 'weight': 10, + 'title': '某某股票大涨', + 'content': '今日涨停', + 'url': 'http://example.com', + 'source': '微博', + 'publish_time': '2026-01-25 10:00:00', + 'matched_keywords': ['股票', '涨停'], + 'keyword_count': 2, + 'score': 100 + }] + + hot_topics = [ + {'topic': 'AI', 'count': 50, 'heat': 95, 'cross_platform': 5, 'sources': ['微博', '抖音']}, + {'topic': '新能源', 'count': 30, 'heat': 80, 'cross_platform': 3, 'sources': ['微博']} + ] + + snapshot_id = news_flow_db.save_flow_snapshot(flow_data, platforms_data, stock_news, hot_topics) + print(f"✅ 保存快照成功,ID: {snapshot_id}") + + # 测试保存情绪记录 + sentiment_data = { + 'sentiment_index': 75, + 'sentiment_class': '乐观', + 'flow_stage': '加速', + 'momentum': 1.5, + 'viral_k': 1.2, + 'flow_type': '增量流量型', + 'stage_analysis': '流量正在快速上升' + } + sentiment_id = news_flow_db.save_sentiment_record(snapshot_id, sentiment_data) + print(f"✅ 保存情绪记录成功,ID: {sentiment_id}") + + # 测试保存预警 + alert_data = { + 'alert_type': 'heat_surge', + 'alert_level': 'warning', + 'title': '热度飙升预警', + 'content': '当前流量得分650,超过阈值500', + 'related_topics': ['AI', '新能源'], + 'trigger_value': 650, + 'threshold_value': 500, + 'snapshot_id': snapshot_id + } + alert_id = news_flow_db.save_alert(alert_data) + print(f"✅ 保存预警成功,ID: {alert_id}") + + # 测试保存AI分析 + ai_data = { + 'affected_sectors': [{'name': 'AI', 'impact': '利好', 'reason': '政策支持'}], + 'recommended_stocks': [{'code': '000001', 'name': '平安银行', 'reason': '龙头'}], + 'risk_level': '中等', + 'risk_factors': ['追高风险', '流动性风险'], + 'advice': '观望', + 'confidence': 75, + 'summary': '当前市场热度较高,建议观望', + 'model_used': 'deepseek-chat', + 'analysis_time': 2.5 + } + ai_id = news_flow_db.save_ai_analysis(snapshot_id, ai_data) + print(f"✅ 保存AI分析成功,ID: {ai_id}") + + # 测试保存任务日志 + log_id = news_flow_db.save_scheduler_log( + '热点同步', 'sync_hotspots', 'success', + '成功同步22个平台', 5.2, snapshot_id + ) + print(f"✅ 保存任务日志成功,ID: {log_id}") + + # 测试获取详情 + detail = news_flow_db.get_snapshot_detail(snapshot_id) + print(f"\n快照详情:") + print(f" 流量得分: {detail['snapshot']['total_score']}") + print(f" 情绪指数: {detail['sentiment']['sentiment_index'] if detail['sentiment'] else 'N/A'}") + print(f" AI建议: {detail['ai_analysis']['advice'] if detail['ai_analysis'] else 'N/A'}") diff --git a/news_flow_engine.py b/news_flow_engine.py new file mode 100644 index 0000000..e575331 --- /dev/null +++ b/news_flow_engine.py @@ -0,0 +1,620 @@ +""" +新闻流量分析引擎 +基于"流量为王"理念的短线炒股指导系统 +整合数据获取、流量模型、情绪分析、AI分析、预警系统 +""" +import logging +import time +from datetime import datetime, timedelta +from typing import Dict, List, Optional + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class NewsFlowEngine: + """新闻流量分析引擎""" + + def __init__(self): + """初始化引擎""" + # 核心模块 + self.fetcher = None + self.model = None + self.sentiment = None + self.agents = None + self.alerts = None + self.db = None + + self._init_modules() + logger.info("✅ 新闻流量引擎初始化完成") + + def _init_modules(self): + """初始化所有模块""" + try: + from news_flow_data import NewsFlowDataFetcher + self.fetcher = NewsFlowDataFetcher() + except Exception as e: + logger.error(f"数据获取模块初始化失败: {e}") + + try: + from news_flow_model import NewsFlowModel + self.model = NewsFlowModel() + except Exception as e: + logger.error(f"流量模型模块初始化失败: {e}") + + try: + from news_flow_sentiment import SentimentAnalyzer + self.sentiment = SentimentAnalyzer() + except Exception as e: + logger.error(f"情绪分析模块初始化失败: {e}") + + try: + from news_flow_agents import NewsFlowAgents + self.agents = NewsFlowAgents() + except Exception as e: + logger.error(f"AI分析模块初始化失败: {e}") + + try: + from news_flow_alert import NewsFlowAlertSystem + self.alerts = NewsFlowAlertSystem() + except Exception as e: + logger.error(f"预警系统模块初始化失败: {e}") + + try: + from news_flow_db import news_flow_db + self.db = news_flow_db + except Exception as e: + logger.error(f"数据库模块初始化失败: {e}") + + def run_quick_analysis(self, platforms: List[str] = None, + category: str = None) -> Dict: + """ + 运行快速分析(不含AI) + + 用于定时同步和快速查看 + + Returns: + { + 'success': bool, + 'snapshot_id': int, + 'flow_data': Dict, + 'model_data': Dict, + 'sentiment_data': Dict, + 'stock_news': List, + 'hot_topics': List, + 'fetch_time': str, + } + """ + try: + logger.info("🚀 开始快速分析...") + start_time = time.time() + + # 1. 获取多平台新闻数据 + logger.info("📊 获取新闻数据...") + if not self.fetcher: + return {'success': False, 'error': '数据获取模块不可用'} + + multi_result = self.fetcher.get_multi_platform_news( + platforms=platforms, category=category + ) + + if not multi_result['success']: + return {'success': False, 'error': '获取新闻数据失败'} + + platforms_data = multi_result['platforms_data'] + success_count = multi_result['success_count'] + + # 2. 提取股票相关新闻 + logger.info("🔍 提取股票相关新闻...") + stock_news = self.fetcher.extract_stock_related_news(platforms_data) + + # 3. 获取热门话题 + logger.info("🔥 分析热门话题...") + hot_topics = self.fetcher.get_hot_topics(platforms_data, top_n=20) + + # 4. 计算流量得分(基础) + logger.info("📈 计算流量得分...") + flow_data = self.fetcher.calculate_flow_score(platforms_data) + + # 5. 运行流量模型 + logger.info("🔬 运行流量模型...") + history_scores = self._get_history_scores(hours=24) + model_data = None + if self.model: + model_data = self.model.run_full_model( + platforms_data, hot_topics, history_scores + ) + + # 6. 情绪分析 + logger.info("💭 分析市场情绪...") + sentiment_data = None + if self.sentiment: + history_sentiments = self._get_history_sentiments(limit=10) + sentiment_data = self.sentiment.run_full_sentiment_analysis( + platforms_data, stock_news, history_scores, + flow_data['total_score'], history_sentiments + ) + + # 7. 保存到数据库 + logger.info("💾 保存分析结果...") + snapshot_id = None + if self.db: + snapshot_id = self.db.save_flow_snapshot( + flow_data, platforms_data, stock_news, hot_topics + ) + + # 保存情绪记录 + if sentiment_data and snapshot_id: + sentiment_record = { + 'sentiment_index': sentiment_data.get('sentiment', {}).get('sentiment_index', 50), + 'sentiment_class': sentiment_data.get('sentiment', {}).get('sentiment_class', '中性'), + 'flow_stage': sentiment_data.get('flow_stage', {}).get('stage_name', '未知'), + 'momentum': sentiment_data.get('momentum', {}).get('momentum', 1.0), + 'viral_k': model_data.get('viral_k', {}).get('k_value', 1.0) if model_data else 1.0, + 'flow_type': model_data.get('flow_type', {}).get('flow_type', '未知') if model_data else '未知', + 'stage_analysis': sentiment_data.get('flow_stage', {}).get('analysis', ''), + } + self.db.save_sentiment_record(snapshot_id, sentiment_record) + + duration = time.time() - start_time + logger.info(f"✅ 快速分析完成,耗时 {duration:.2f} 秒") + + return { + 'success': True, + 'snapshot_id': snapshot_id, + 'success_count': success_count, + 'flow_data': flow_data, + 'model_data': model_data, + 'sentiment_data': sentiment_data, + 'stock_news': stock_news, + 'hot_topics': hot_topics, + 'platforms_data': platforms_data, + 'fetch_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'duration': round(duration, 2), + } + + except Exception as e: + logger.error(f"❌ 快速分析失败: {e}") + return {'success': False, 'error': str(e)} + + def run_full_analysis(self, platforms: List[str] = None, + category: str = None, + include_ai: bool = True) -> Dict: + """ + 运行完整分析(含AI) + + Returns: + { + 'success': bool, + 'snapshot_id': int, + 'flow_data': Dict, + 'model_data': Dict, + 'sentiment_data': Dict, + 'ai_analysis': Dict, + 'trading_signals': Dict, + 'stock_news': List, + 'hot_topics': List, + } + """ + try: + logger.info("🚀 开始完整分析...") + start_time = time.time() + + # 1. 先运行快速分析 + quick_result = self.run_quick_analysis(platforms, category) + + if not quick_result['success']: + return quick_result + + # 2. AI智能分析 + ai_analysis = None + if include_ai: + if not self.agents: + logger.warning("⚠️ AI代理模块未初始化") + elif not self.agents.is_available(): + logger.warning("⚠️ DeepSeek API不可用,请检查API密钥配置") + else: + logger.info("🤖 运行AI分析...") + + model_data = quick_result.get('model_data', {}) + sentiment_data = quick_result.get('sentiment_data', {}) + + # 基础AI分析 + ai_analysis = self.agents.run_full_analysis( + quick_result['hot_topics'], + quick_result['stock_news'], + quick_result['flow_data'], + sentiment_data, + viral_k=model_data.get('viral_k', {}).get('k_value', 1.0) if model_data else 1.0, + flow_type=model_data.get('flow_type', {}).get('flow_type', '未知') if model_data else '未知', + ) + + # 多板块深度分析(多次调用DeepSeek) + logger.info("🔍 开始多板块深度分析...") + multi_sector_analysis = self.agents.run_multi_sector_analysis( + quick_result['hot_topics'], + quick_result['stock_news'] + ) + + # 合并多板块分析结果 + if ai_analysis and multi_sector_analysis.get('success'): + ai_analysis['multi_sector'] = multi_sector_analysis + + # 保存AI分析结果 + if ai_analysis and self.db and quick_result.get('snapshot_id'): + ai_record = { + 'affected_sectors': ai_analysis.get('sector_analysis', {}).get('benefited_sectors', []), + 'recommended_stocks': ai_analysis.get('stock_recommend', {}).get('recommended_stocks', []), + 'risk_level': ai_analysis.get('risk_assess', {}).get('risk_level', '未知'), + 'risk_factors': ai_analysis.get('risk_assess', {}).get('risk_factors', []), + 'advice': ai_analysis.get('investment_advice', {}).get('advice', '观望'), + 'confidence': ai_analysis.get('investment_advice', {}).get('confidence', 50), + 'summary': ai_analysis.get('investment_advice', {}).get('summary', ''), + 'model_used': 'deepseek-chat', + 'analysis_time': ai_analysis.get('analysis_time', 0), + } + self.db.save_ai_analysis(quick_result['snapshot_id'], ai_record) + + # 3. 生成交易信号 + trading_signals = self._generate_trading_signals( + quick_result.get('flow_data', {}), + quick_result.get('model_data', {}), + quick_result.get('sentiment_data', {}), + ai_analysis + ) + + duration = time.time() - start_time + logger.info(f"✅ 完整分析完成,耗时 {duration:.2f} 秒") + + return { + 'success': True, + 'snapshot_id': quick_result.get('snapshot_id'), + 'flow_data': quick_result.get('flow_data'), + 'model_data': quick_result.get('model_data'), + 'sentiment_data': quick_result.get('sentiment_data'), + 'ai_analysis': ai_analysis, + 'trading_signals': trading_signals, + 'stock_news': quick_result.get('stock_news'), + 'hot_topics': quick_result.get('hot_topics'), + 'platforms_data': quick_result.get('platforms_data'), + 'fetch_time': quick_result.get('fetch_time'), + 'duration': round(duration, 2), + } + + except Exception as e: + logger.error(f"❌ 完整分析失败: {e}") + return {'success': False, 'error': str(e)} + + def run_alert_check(self) -> Dict: + """ + 运行预警检查 + + Returns: + { + 'success': bool, + 'alerts': List[Dict], + } + """ + try: + logger.info("⚠️ 开始预警检查...") + + if not self.alerts: + return {'success': False, 'error': '预警系统不可用'} + + # 获取当前数据 + quick_result = self.run_quick_analysis() + + if not quick_result['success']: + return {'success': False, 'error': quick_result.get('error')} + + # 获取历史数据 + history_data = self._get_previous_snapshot() + + # 构建检查数据 + current_data = { + 'flow_data': quick_result.get('flow_data', {}), + 'hot_topics': quick_result.get('hot_topics', []), + 'viral_k': quick_result.get('model_data', {}).get('viral_k', {}), + 'flow_stage': quick_result.get('sentiment_data', {}).get('flow_stage', {}), + } + + # 检查预警 + alerts = self.alerts.check_alerts( + current_data, + history_data, + quick_result.get('sentiment_data'), + quick_result.get('snapshot_id') + ) + + logger.info(f"✅ 预警检查完成,触发 {len(alerts)} 个预警") + + return { + 'success': True, + 'alerts': alerts, + 'snapshot_id': quick_result.get('snapshot_id'), + } + + except Exception as e: + logger.error(f"❌ 预警检查失败: {e}") + return {'success': False, 'error': str(e)} + + def get_dashboard_data(self) -> Dict: + """ + 获取仪表盘数据 + + Returns: + { + 'latest_snapshot': Dict, + 'latest_sentiment': Dict, + 'latest_ai_analysis': Dict, + 'recent_alerts': List, + 'flow_trend': Dict, + 'scheduler_status': Dict, + } + """ + try: + data = {} + + if self.db: + # 最新快照 + data['latest_snapshot'] = self.db.get_latest_snapshot() + + # 最新情绪 + data['latest_sentiment'] = self.db.get_latest_sentiment() + + # 最新AI分析 + data['latest_ai_analysis'] = self.db.get_latest_ai_analysis() + + # 最近预警 + data['recent_alerts'] = self.db.get_alerts(days=1) + + # 流量趋势(7天) + data['flow_trend'] = self.get_flow_trend(days=7) + + # 调度器状态 + try: + from news_flow_scheduler import news_flow_scheduler + data['scheduler_status'] = news_flow_scheduler.get_status() + except: + data['scheduler_status'] = None + + return data + + except Exception as e: + logger.error(f"获取仪表盘数据失败: {e}") + return {} + + def get_flow_trend(self, days: int = 7) -> Dict: + """获取流量趋势""" + if not self.db: + return {'dates': [], 'scores': [], 'trend': '无数据'} + + stats = self.db.get_daily_statistics(days) + + if not stats: + return {'dates': [], 'scores': [], 'trend': '无数据', 'analysis': '暂无历史数据'} + + # 反转(从旧到新) + stats.reverse() + + dates = [s['date'] for s in stats] + avg_scores = [s['avg_score'] for s in stats] + max_scores = [s['max_score'] for s in stats] + min_scores = [s['min_score'] for s in stats] + + # 判断趋势 + if len(avg_scores) >= 3: + recent_avg = sum(avg_scores[-3:]) / 3 + earlier_avg = sum(avg_scores[:3]) / 3 + + if recent_avg > earlier_avg * 1.2: + trend = '上升' + analysis = f"近期流量持续上升(近3日均值{recent_avg:.0f} > 前3日均值{earlier_avg:.0f}),市场热度升温。" + elif recent_avg < earlier_avg * 0.8: + trend = '下降' + analysis = f"近期流量持续下降(近3日均值{recent_avg:.0f} < 前3日均值{earlier_avg:.0f}),市场热度降温。" + else: + trend = '平稳' + analysis = f"近期流量波动不大(近3日均值{recent_avg:.0f} ≈ 前3日均值{earlier_avg:.0f}),市场处于平衡状态。" + else: + trend = '数据不足' + analysis = '历史数据不足,无法判断趋势' + + return { + 'dates': dates, + 'avg_scores': avg_scores, + 'max_scores': max_scores, + 'min_scores': min_scores, + 'trend': trend, + 'analysis': analysis, + } + + def _generate_trading_signals(self, flow_data: Dict, + model_data: Dict, + sentiment_data: Dict, + ai_analysis: Dict = None) -> Dict: + """生成交易信号""" + signals = { + 'overall_signal': '观望', + 'confidence': 50, + 'risk_level': '中等', + 'hot_sectors': [], + 'operation_advice': '', + 'key_message': '', + } + + # 获取各项指标 + total_score = flow_data.get('total_score', 0) + flow_level = flow_data.get('level', '中') + + sentiment_index = 50 + flow_stage = '未知' + if sentiment_data: + sentiment_index = sentiment_data.get('sentiment', {}).get('sentiment_index', 50) + flow_stage = sentiment_data.get('flow_stage', {}).get('stage_name', '未知') + + viral_k = 1.0 + if model_data: + viral_k = model_data.get('viral_k', {}).get('k_value', 1.0) + + # 核心判断逻辑 + if flow_stage in ['一致', 'consensus']: + # 流量高潮 = 逃命时刻 + signals['overall_signal'] = '卖出' + signals['confidence'] = 90 + signals['risk_level'] = '极高' + signals['key_message'] = '⚠️ 流量高潮 = 价格高潮 = 逃命时刻!立即减仓或清仓!' + signals['operation_advice'] = '立即减仓或清仓,锁定利润。不要贪婪,不要犹豫。' + + elif flow_stage in ['退潮', 'decline']: + signals['overall_signal'] = '观望' + signals['confidence'] = 80 + signals['risk_level'] = '高' + signals['key_message'] = '流量退潮,及时止盈止损' + signals['operation_advice'] = '持仓者及时止盈止损,空仓者继续观望。' + + elif flow_stage in ['加速', 'acceleration'] and viral_k > 1.2: + signals['overall_signal'] = '买入' + signals['confidence'] = 75 + signals['risk_level'] = '中等' + signals['key_message'] = '流量加速期,可参与龙头' + signals['operation_advice'] = '关注龙头股,轻仓试探。设置止损位(-5%),止盈位(+15%)。' + + elif flow_stage in ['启动', 'startup']: + signals['overall_signal'] = '关注' + signals['confidence'] = 65 + signals['risk_level'] = '低' + signals['key_message'] = '流量启动期,可以关注' + signals['operation_advice'] = '密切关注,等待确认后介入。' + + elif flow_level == "极高" and sentiment_index > 85: + signals['overall_signal'] = '观望' + signals['confidence'] = 70 + signals['risk_level'] = '高' + signals['key_message'] = '流量极高+情绪过热,追高风险大' + signals['operation_advice'] = '不建议追高,等待回调机会。' + + else: + signals['overall_signal'] = '观望' + signals['confidence'] = 50 + signals['risk_level'] = '中等' + signals['key_message'] = '市场无明确方向,保持观望' + signals['operation_advice'] = '保持观望,等待流量信号明确。' + + # 整合AI分析结果 + if ai_analysis: + advice = ai_analysis.get('investment_advice', {}) + if advice.get('advice'): + signals['ai_advice'] = advice.get('advice') + signals['ai_confidence'] = advice.get('confidence', 50) + signals['ai_summary'] = advice.get('summary', '') + + sectors = ai_analysis.get('sector_analysis', {}).get('benefited_sectors', []) + signals['hot_sectors'] = sectors[:3] + + return signals + + def _get_history_scores(self, hours: int = 24) -> List[int]: + """获取历史流量得分""" + if not self.db: + return [] + + scores = self.db.get_recent_scores(hours) + return [s['total_score'] for s in scores] + + def _get_history_sentiments(self, limit: int = 10) -> List[Dict]: + """获取历史情绪记录""" + if not self.db: + return [] + + return self.db.get_sentiment_history(limit) + + def _get_previous_snapshot(self) -> Optional[Dict]: + """获取上一次快照""" + if not self.db: + return None + + snapshots = self.db.get_history_snapshots(limit=2) + if len(snapshots) >= 2: + detail = self.db.get_snapshot_detail(snapshots[1]['id']) + return { + 'hot_topics': detail.get('hot_topics', []), + 'snapshot': detail.get('snapshot', {}), + } + return None + + def compare_with_history(self, current_score: int) -> Dict: + """与历史数据对比""" + if not self.db: + return { + 'percentile': 50, + 'level_description': '无历史对比', + 'comparison': '暂无足够的历史数据进行对比' + } + + stats = self.db.get_daily_statistics(30) + + if not stats: + return { + 'percentile': 50, + 'level_description': '无历史对比', + 'comparison': '暂无足够的历史数据进行对比' + } + + all_scores = [] + for stat in stats: + all_scores.extend([stat['avg_score'], stat['max_score'], stat['min_score']]) + + all_scores.sort() + + lower_count = sum(1 for s in all_scores if s < current_score) + percentile = int(lower_count / len(all_scores) * 100) if all_scores else 50 + + if percentile >= 90: + level_description = "极高水平" + comparison = f"当前流量得分{current_score}处于历史极高水平(超过{percentile}%的历史记录),流量极度爆发!" + elif percentile >= 70: + level_description = "较高水平" + comparison = f"当前流量得分{current_score}处于历史较高水平(超过{percentile}%的历史记录),流量活跃。" + elif percentile >= 30: + level_description = "正常水平" + comparison = f"当前流量得分{current_score}处于历史正常水平(超过{percentile}%的历史记录)。" + else: + level_description = "较低水平" + comparison = f"当前流量得分{current_score}处于历史较低水平(仅超过{percentile}%的历史记录),流量低迷。" + + return { + 'percentile': percentile, + 'level_description': level_description, + 'comparison': comparison + } + + +# 全局引擎实例 +news_flow_engine = NewsFlowEngine() + + +# 测试代码 +if __name__ == "__main__": + print("=== 测试新闻流量分析引擎 ===") + + # 运行快速分析 + print("\n--- 快速分析 ---") + result = news_flow_engine.run_quick_analysis(category='finance') + + if result['success']: + print(f"✅ 分析成功!快照ID: {result.get('snapshot_id')}") + print(f"\n流量得分: {result['flow_data']['total_score']}") + print(f"流量等级: {result['flow_data']['level']}") + print(f"股票相关新闻: {len(result['stock_news'])} 条") + print(f"热门话题: {len(result['hot_topics'])} 个") + + if result.get('sentiment_data'): + sentiment = result['sentiment_data'].get('sentiment', {}) + print(f"\n情绪指数: {sentiment.get('sentiment_index', 'N/A')}") + print(f"情绪分类: {sentiment.get('sentiment_class', 'N/A')}") + + flow_stage = result['sentiment_data'].get('flow_stage', {}) + print(f"流量阶段: {flow_stage.get('stage_name', 'N/A')}") + else: + print(f"❌ 分析失败: {result.get('error')}") diff --git a/news_flow_model.py b/news_flow_model.py new file mode 100644 index 0000000..2274a78 --- /dev/null +++ b/news_flow_model.py @@ -0,0 +1,557 @@ +""" +新闻流量模型计算模块 +核心公式:接盘总量 = 流量 × 转化率 × 客单价 +实现流量为王理念的量化分析 +""" +import logging +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Tuple + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class NewsFlowModel: + """新闻流量模型计算器""" + + def __init__(self): + # 平台类别权重(用于转化率计算) + self.category_weights = { + 'finance': 1.5, # 财经平台转化率高 + 'social': 1.2, # 社交媒体传播快 + 'news': 1.0, # 新闻媒体正常 + 'tech': 0.8, # 科技平台相关性低 + } + + # 话题类型转化率系数 + self.topic_weights = { + # 高转化话题 + '政策': 2.0, + '利好': 1.8, + '涨停': 1.8, + '龙头': 1.7, + '机构': 1.6, + '外资': 1.6, + '北向': 1.6, + '重组': 1.5, + '并购': 1.5, + 'IPO': 1.5, + + # 中转化话题 + '业绩': 1.3, + '财报': 1.3, + '板块': 1.2, + '概念': 1.2, + '题材': 1.2, + + # 一般话题 + '股票': 1.0, + '股市': 1.0, + 'A股': 1.0, + } + + # 基础转化率(万分之一) + self.base_conversion_rate = 0.0001 + + # 平均客单价(元)- 散户平均投资金额 + self.avg_investment = 50000 + + # 时效因子衰减系数(每小时衰减) + self.time_decay_rate = 0.95 + + def calculate_traffic_score(self, platforms_data: List[Dict]) -> Dict: + """ + 计算流量分数 + + 流量分数 = Σ(平台权重 × 热度 × 时效因子) + + Args: + platforms_data: 平台数据列表 + + Returns: + { + 'total_score': int, + 'category_scores': Dict[str, int], + 'platform_details': List[Dict], + 'normalized_score': int, # 归一化到0-1000 + } + """ + category_scores = { + 'social': 0, + 'news': 0, + 'finance': 0, + 'tech': 0, + } + + platform_details = [] + total_raw_score = 0 + + for platform_data in platforms_data: + if not platform_data.get('success'): + continue + + category = platform_data.get('category', 'other') + weight = platform_data.get('weight', 5) + count = platform_data.get('count', 0) + platform_name = platform_data.get('platform_name', '') + + # 计算平台得分:权重 × 新闻数量 × 类别权重 + category_weight = self.category_weights.get(category, 1.0) + platform_score = weight * count * category_weight + + category_scores[category] = category_scores.get(category, 0) + platform_score + total_raw_score += platform_score + + platform_details.append({ + 'platform': platform_data.get('platform', ''), + 'platform_name': platform_name, + 'category': category, + 'count': count, + 'weight': weight, + 'score': int(platform_score), + }) + + # 归一化到0-1000 + normalized_score = min(int(total_raw_score / 50), 1000) + + return { + 'total_score': int(total_raw_score), + 'normalized_score': normalized_score, + 'category_scores': {k: int(v) for k, v in category_scores.items()}, + 'platform_details': platform_details, + } + + def estimate_conversion_rate(self, hot_topics: List[Dict], + category_distribution: Dict[str, int]) -> Dict: + """ + 估算转化率 + + 转化率 = 基础转化率 × 话题系数 × 平台系数 + + Args: + hot_topics: 热门话题列表 + category_distribution: 类别得分分布 + + Returns: + { + 'conversion_rate': float, + 'topic_factor': float, + 'platform_factor': float, + 'analysis': str, + } + """ + # 1. 计算话题系数(基于热门话题的类型) + topic_factor = 1.0 + matched_topics = [] + + for topic in hot_topics[:10]: # 只看TOP10热门话题 + topic_text = topic.get('topic', '') + topic_heat = topic.get('heat', 0) + + for keyword, weight in self.topic_weights.items(): + if keyword in topic_text: + # 热度加权 + heat_bonus = 1 + (topic_heat / 100) * 0.5 + topic_factor = max(topic_factor, weight * heat_bonus) + matched_topics.append({ + 'topic': topic_text, + 'keyword': keyword, + 'factor': weight, + }) + break + + # 2. 计算平台系数(基于类别分布) + total_score = sum(category_distribution.values()) + if total_score > 0: + platform_factor = sum( + (score / total_score) * self.category_weights.get(cat, 1.0) + for cat, score in category_distribution.items() + ) + else: + platform_factor = 1.0 + + # 3. 计算最终转化率 + conversion_rate = self.base_conversion_rate * topic_factor * platform_factor + + # 4. 生成分析 + if conversion_rate >= 0.0003: + analysis = f"转化率极高({conversion_rate:.4%})!话题与股市高度相关,投资者关注度极高。" + elif conversion_rate >= 0.0002: + analysis = f"转化率较高({conversion_rate:.4%})。话题具有较强的股市联动性。" + elif conversion_rate >= 0.0001: + analysis = f"转化率正常({conversion_rate:.4%})。话题与股市有一定关联。" + else: + analysis = f"转化率较低({conversion_rate:.4%})。话题与股市关联度不高。" + + return { + 'conversion_rate': conversion_rate, + 'topic_factor': round(topic_factor, 2), + 'platform_factor': round(platform_factor, 2), + 'matched_topics': matched_topics, + 'analysis': analysis, + } + + def calculate_potential(self, flow_score: int, conversion_rate: float, + avg_investment: float = None) -> Dict: + """ + 计算接盘潜力 + + 核心公式:接盘总量 = 流量 × 转化率 × 客单价 + + Args: + flow_score: 流量分数(代表潜在触达人数,按比例换算) + conversion_rate: 转化率 + avg_investment: 平均客单价(元) + + Returns: + { + 'potential_volume': float, # 接盘总量(亿元) + 'potential_level': str, # 潜力等级 + 'estimated_participants': int, # 预估参与人数 + 'analysis': str, + } + """ + if avg_investment is None: + avg_investment = self.avg_investment + + # 流量分数换算为潜在触达人数(假设满分1000对应1000万人) + potential_reach = flow_score * 10000 # 分数 × 10000 = 潜在触达人数 + + # 预估参与人数 + estimated_participants = int(potential_reach * conversion_rate) + + # 接盘总量 = 参与人数 × 平均投资额(转换为亿元) + potential_volume = (estimated_participants * avg_investment) / 100000000 + + # 确定潜力等级 + if potential_volume >= 100: + potential_level = "超大" + analysis = f"预估接盘资金{potential_volume:.1f}亿元,市场资金充裕,热点题材可能持续发酵。" + elif potential_volume >= 50: + potential_level = "大" + analysis = f"预估接盘资金{potential_volume:.1f}亿元,资金量较大,可支撑短期行情。" + elif potential_volume >= 20: + potential_level = "中" + analysis = f"预估接盘资金{potential_volume:.1f}亿元,资金量适中,行情可能分化。" + elif potential_volume >= 5: + potential_level = "小" + analysis = f"预估接盘资金{potential_volume:.1f}亿元,资金量较小,注意风险。" + else: + potential_level = "极小" + analysis = f"预估接盘资金{potential_volume:.1f}亿元,资金量不足,不建议追高。" + + return { + 'potential_volume': round(potential_volume, 2), + 'potential_level': potential_level, + 'potential_reach': potential_reach, + 'estimated_participants': estimated_participants, + 'avg_investment': avg_investment, + 'analysis': analysis, + } + + def classify_flow_type(self, history_scores: List[int], + current_score: int) -> Dict: + """ + 判断流量类型 + + 存量流量型:出生自带顶流,流量快速到位(政策/大事件) + 增量流量型:初始流量小,具备病毒传播能力(话题发酵) + + Args: + history_scores: 历史得分列表(从旧到新) + current_score: 当前得分 + + Returns: + { + 'flow_type': str, + 'characteristics': List[str], + 'time_window': str, + 'operation': str, + 'confidence': int, + } + """ + if len(history_scores) < 2: + return { + 'flow_type': '未知', + 'characteristics': ['历史数据不足,无法判断'], + 'time_window': '无法判断', + 'operation': '继续观察,积累数据', + 'confidence': 0, + } + + # 计算关键指标 + initial_score = history_scores[0] + avg_score = sum(history_scores) / len(history_scores) + max_score = max(history_scores) + + # 计算增长率序列 + growth_rates = [] + for i in range(1, len(history_scores)): + if history_scores[i-1] > 0: + rate = (history_scores[i] - history_scores[i-1]) / history_scores[i-1] + growth_rates.append(rate) + + avg_growth = sum(growth_rates) / len(growth_rates) if growth_rates else 0 + positive_growth_count = len([r for r in growth_rates if r > 0]) + + # 判断流量类型 + if initial_score >= 500 and (current_score - initial_score) / (initial_score + 1) < 0.3: + # 初始热度高,增长有限 -> 存量流量型 + flow_type = "存量流量型" + characteristics = [ + f"初始热度高({initial_score}分)", + "流量快速到位", + "可能与政策/大事件相关", + "来得快去得也快", + ] + time_window = "时间窗口短(2-3天)" + operation = "快进快出,首日参与最佳,及时止盈" + confidence = 80 + + elif avg_growth > 0.15 and positive_growth_count >= len(growth_rates) * 0.6: + # 持续增长 -> 增量流量型 + flow_type = "增量流量型" + characteristics = [ + f"初始热度较低({initial_score}分)", + f"增长率{avg_growth*100:.1f}%", + "逐步攀升,病毒式传播", + "有埋伏机会", + ] + time_window = "时间窗口长(5-10天)" + operation = "可以埋伏,等待加速,分批建仓" + confidence = 75 + + elif avg_growth < -0.1: + # 持续下降 -> 衰退期 + flow_type = "流量衰退" + characteristics = [ + "热度持续下降", + f"平均跌幅{abs(avg_growth)*100:.1f}%", + "题材热度消退", + ] + time_window = "窗口已关闭" + operation = "及时止盈止损,不宜追入" + confidence = 70 + + else: + # 波动 -> 常规流量 + flow_type = "常规流量" + characteristics = [ + "热度波动正常", + "无明显趋势", + ] + time_window = "无特定窗口" + operation = "保持观望,等待方向明确" + confidence = 50 + + return { + 'flow_type': flow_type, + 'characteristics': characteristics, + 'time_window': time_window, + 'operation': operation, + 'confidence': confidence, + 'initial_score': initial_score, + 'current_score': current_score, + 'avg_growth': round(avg_growth * 100, 1), + 'history_length': len(history_scores), + } + + def calculate_viral_k(self, current_score: int, previous_score: int) -> Dict: + """ + 计算病毒系数K值 + + K值 = 当前传播量 / 上期传播量 + + K > 1.5: 指数型爆发 + K ≈ 1: 线性增长 + K < 1: 自然死亡 + + Returns: + { + 'k_value': float, + 'trend': str, + 'risk_level': str, + 'analysis': str, + } + """ + if previous_score == 0: + return { + 'k_value': 1.0, + 'trend': '无历史数据', + 'risk_level': '未知', + 'analysis': '首次采集,无法计算K值', + } + + k_value = round(current_score / previous_score, 2) + + if k_value > 2.0: + trend = "爆发式增长" + risk_level = "高风险" + analysis = f"K值={k_value},流量正在爆发式增长!这是极端情况,可能接近顶部,注意风险。" + elif k_value > 1.5: + trend = "指数型爆发" + risk_level = "中高风险" + analysis = f"K值={k_value},流量指数型增长!题材正在加速发酵,关注龙头但注意追高风险。" + elif k_value > 1.2: + trend = "快速增长" + risk_level = "中风险" + analysis = f"K值={k_value},流量快速增长,题材正在升温,可适度参与。" + elif k_value > 1.0: + trend = "稳步增长" + risk_level = "低风险" + analysis = f"K值={k_value},流量稳步增长,题材处于发酵期。" + elif k_value == 1.0: + trend = "平稳" + risk_level = "低风险" + analysis = f"K值={k_value},流量保持稳定,市场平衡状态。" + elif k_value > 0.8: + trend = "轻微下降" + risk_level = "中风险" + analysis = f"K值={k_value},流量轻微下降,题材热度开始消退。" + else: + trend = "快速衰减" + risk_level = "高风险" + analysis = f"K值={k_value},流量快速衰减!题材进入退潮期,注意及时止盈止损。" + + return { + 'k_value': k_value, + 'current_score': current_score, + 'previous_score': previous_score, + 'trend': trend, + 'risk_level': risk_level, + 'analysis': analysis, + } + + def run_full_model(self, platforms_data: List[Dict], + hot_topics: List[Dict], + history_scores: List[int] = None) -> Dict: + """ + 运行完整的流量模型分析 + + Returns: + { + 'traffic': Dict, # 流量分析 + 'conversion': Dict, # 转化率分析 + 'potential': Dict, # 接盘潜力 + 'flow_type': Dict, # 流量类型 + 'viral_k': Dict, # K值分析 + 'summary': str, # 总结 + } + """ + # 1. 计算流量分数 + traffic = self.calculate_traffic_score(platforms_data) + current_score = traffic['normalized_score'] + + # 2. 估算转化率 + conversion = self.estimate_conversion_rate( + hot_topics, + traffic['category_scores'] + ) + + # 3. 计算接盘潜力 + potential = self.calculate_potential( + current_score, + conversion['conversion_rate'] + ) + + # 4. 判断流量类型 + if history_scores and len(history_scores) >= 2: + flow_type = self.classify_flow_type(history_scores, current_score) + # 计算K值(与上一次对比) + viral_k = self.calculate_viral_k(current_score, history_scores[-1]) + else: + flow_type = { + 'flow_type': '未知', + 'characteristics': ['历史数据不足'], + 'time_window': '无法判断', + 'operation': '继续观察', + 'confidence': 0, + } + viral_k = { + 'k_value': 1.0, + 'trend': '无历史数据', + 'risk_level': '未知', + 'analysis': '首次分析,无法计算K值', + } + + # 5. 生成总结 + summary = self._generate_summary(traffic, conversion, potential, flow_type, viral_k) + + return { + 'traffic': traffic, + 'conversion': conversion, + 'potential': potential, + 'flow_type': flow_type, + 'viral_k': viral_k, + 'summary': summary, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + } + + def _generate_summary(self, traffic: Dict, conversion: Dict, + potential: Dict, flow_type: Dict, viral_k: Dict) -> str: + """生成分析总结""" + lines = [] + + # 流量概况 + score = traffic['normalized_score'] + if score >= 800: + lines.append(f"【流量】当前流量分数{score},处于极高水平") + elif score >= 500: + lines.append(f"【流量】当前流量分数{score},处于较高水平") + elif score >= 200: + lines.append(f"【流量】当前流量分数{score},处于正常水平") + else: + lines.append(f"【流量】当前流量分数{score},处于低位") + + # 转化率 + lines.append(f"【转化】{conversion['analysis']}") + + # 接盘潜力 + lines.append(f"【潜力】{potential['analysis']}") + + # 流量类型 + if flow_type['flow_type'] != '未知': + lines.append(f"【类型】{flow_type['flow_type']},{flow_type['operation']}") + + # K值 + if viral_k['k_value'] != 1.0 or viral_k['trend'] != '无历史数据': + lines.append(f"【趋势】K值={viral_k['k_value']},{viral_k['trend']}") + + return '\n'.join(lines) + + +# 全局实例 +news_flow_model = NewsFlowModel() + + +# 测试代码 +if __name__ == "__main__": + print("=== 测试新闻流量模型 ===") + + # 模拟数据 + platforms_data = [ + {'success': True, 'platform': 'weibo', 'platform_name': '微博', + 'category': 'social', 'weight': 10, 'count': 50}, + {'success': True, 'platform': 'eastmoney', 'platform_name': '东方财富', + 'category': 'finance', 'weight': 9, 'count': 30}, + {'success': True, 'platform': 'baidu', 'platform_name': '百度', + 'category': 'news', 'weight': 8, 'count': 40}, + ] + + hot_topics = [ + {'topic': 'AI芯片', 'heat': 95, 'count': 50}, + {'topic': '新能源政策', 'heat': 80, 'count': 30}, + {'topic': '涨停板', 'heat': 75, 'count': 25}, + ] + + history_scores = [300, 350, 420, 500] + + # 运行完整分析 + result = news_flow_model.run_full_model(platforms_data, hot_topics, history_scores) + + print(f"\n流量分数: {result['traffic']['normalized_score']}") + print(f"转化率: {result['conversion']['conversion_rate']:.4%}") + print(f"接盘潜力: {result['potential']['potential_volume']:.1f}亿元 ({result['potential']['potential_level']})") + print(f"流量类型: {result['flow_type']['flow_type']}") + print(f"K值: {result['viral_k']['k_value']} ({result['viral_k']['trend']})") + print(f"\n===总结===\n{result['summary']}") diff --git a/news_flow_pdf.py b/news_flow_pdf.py new file mode 100644 index 0000000..be58f77 --- /dev/null +++ b/news_flow_pdf.py @@ -0,0 +1,491 @@ +""" +新闻流量分析PDF报告生成器 +生成包含AI分析结果的PDF格式报告 +""" + +import io +import os +import tempfile +from datetime import datetime +from typing import Dict, List, Optional + +from reportlab.lib.pagesizes import A4 +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.units import inch, cm +from reportlab.lib import colors +from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont + +import logging + +logger = logging.getLogger(__name__) + + +class NewsFlowPDFGenerator: + """新闻流量分析PDF报告生成器""" + + def __init__(self): + self.chinese_font = self._register_chinese_fonts() + self.styles = self._create_styles() + + def _register_chinese_fonts(self) -> str: + """注册中文字体""" + try: + if 'ChineseFont' in pdfmetrics.getRegisteredFontNames(): + return 'ChineseFont' + + # 字体路径列表 + font_paths = [ + 'C:/Windows/Fonts/simsun.ttc', + 'C:/Windows/Fonts/simhei.ttf', + 'C:/Windows/Fonts/msyh.ttc', + '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', + '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', + ] + + for font_path in font_paths: + if os.path.exists(font_path): + try: + pdfmetrics.registerFont(TTFont('ChineseFont', font_path)) + return 'ChineseFont' + except: + continue + + return 'Helvetica' + except: + return 'Helvetica' + + def _create_styles(self) -> Dict: + """创建PDF样式""" + styles = getSampleStyleSheet() + + # 标题样式 + styles.add(ParagraphStyle( + name='ChineseTitle', + fontName=self.chinese_font, + fontSize=24, + alignment=TA_CENTER, + spaceAfter=30, + textColor=colors.HexColor('#1a1a2e') + )) + + # 副标题样式 + styles.add(ParagraphStyle( + name='ChineseSubtitle', + fontName=self.chinese_font, + fontSize=14, + alignment=TA_CENTER, + spaceAfter=20, + textColor=colors.HexColor('#666666') + )) + + # 章节标题 + styles.add(ParagraphStyle( + name='ChineseHeading', + fontName=self.chinese_font, + fontSize=16, + spaceBefore=20, + spaceAfter=10, + textColor=colors.HexColor('#2d3436'), + borderPadding=5, + )) + + # 正文样式 + styles.add(ParagraphStyle( + name='ChineseBody', + fontName=self.chinese_font, + fontSize=11, + leading=18, + alignment=TA_JUSTIFY, + spaceBefore=6, + spaceAfter=6, + )) + + # 小标题 + styles.add(ParagraphStyle( + name='ChineseSmallHeading', + fontName=self.chinese_font, + fontSize=12, + spaceBefore=10, + spaceAfter=5, + textColor=colors.HexColor('#0984e3'), + )) + + # 重点提示 + styles.add(ParagraphStyle( + name='ChineseHighlight', + fontName=self.chinese_font, + fontSize=11, + leading=16, + backColor=colors.HexColor('#fff3cd'), + borderPadding=8, + spaceBefore=10, + spaceAfter=10, + )) + + # 风险警告 + styles.add(ParagraphStyle( + name='ChineseWarning', + fontName=self.chinese_font, + fontSize=10, + leading=14, + backColor=colors.HexColor('#f8d7da'), + borderPadding=8, + spaceBefore=10, + spaceAfter=10, + textColor=colors.HexColor('#721c24'), + )) + + return styles + + def generate_report(self, analysis_result: Dict) -> Optional[str]: + """ + 生成PDF分析报告 + + Args: + analysis_result: 完整分析结果 + + Returns: + PDF文件路径 + """ + try: + # 创建临时文件 + temp_dir = tempfile.gettempdir() + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + pdf_path = os.path.join(temp_dir, f'news_flow_report_{timestamp}.pdf') + + # 创建PDF文档 + doc = SimpleDocTemplate( + pdf_path, + pagesize=A4, + rightMargin=50, + leftMargin=50, + topMargin=50, + bottomMargin=50 + ) + + # 构建内容 + content = [] + + # 封面 + content.extend(self._build_cover(analysis_result)) + content.append(PageBreak()) + + # 概要 + content.extend(self._build_summary(analysis_result)) + content.append(Spacer(1, 20)) + + # 流量分析 + content.extend(self._build_flow_analysis(analysis_result)) + content.append(Spacer(1, 20)) + + # AI分析结果 + content.extend(self._build_ai_analysis(analysis_result)) + content.append(PageBreak()) + + # 板块深度分析 + content.extend(self._build_sector_analysis(analysis_result)) + content.append(Spacer(1, 20)) + + # 股票推荐 + content.extend(self._build_stock_recommendations(analysis_result)) + content.append(Spacer(1, 20)) + + # 风险提示 + content.extend(self._build_risk_warning(analysis_result)) + + # 生成PDF + doc.build(content) + + logger.info(f"✅ PDF报告生成成功: {pdf_path}") + return pdf_path + + except Exception as e: + logger.error(f"❌ PDF生成失败: {e}") + return None + + def _build_cover(self, result: Dict) -> List: + """构建封面""" + content = [] + + content.append(Spacer(1, 100)) + content.append(Paragraph("新闻流量分析报告", self.styles['ChineseTitle'])) + content.append(Spacer(1, 30)) + + # 生成时间 + fetch_time = result.get('fetch_time', datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + content.append(Paragraph(f"生成时间:{fetch_time}", self.styles['ChineseSubtitle'])) + + # 分析耗时 + duration = result.get('duration', 0) + content.append(Paragraph(f"分析耗时:{duration} 秒", self.styles['ChineseSubtitle'])) + + content.append(Spacer(1, 50)) + + # 核心指标概览 + flow_data = result.get('flow_data', {}) + sentiment_data = result.get('sentiment_data', {}) + + overview_text = f""" + 流量得分:{flow_data.get('total_score', 0)}/1000 ({flow_data.get('level', '中')})
+ 情绪指数:{sentiment_data.get('sentiment', {}).get('sentiment_index', 50)}/100
+ 流量阶段:{sentiment_data.get('flow_stage', {}).get('stage_name', '未知')} + """ + content.append(Paragraph(overview_text, self.styles['ChineseBody'])) + + return content + + def _build_summary(self, result: Dict) -> List: + """构建分析概要""" + content = [] + + content.append(Paragraph("📊 分析概要", self.styles['ChineseHeading'])) + + trading_signals = result.get('trading_signals', {}) + ai_analysis = result.get('ai_analysis', {}) + + # 交易信号 + signal = trading_signals.get('overall_signal', '观望') + confidence = trading_signals.get('confidence', 50) + + content.append(Paragraph( + f"【AI建议】{signal}(置信度:{confidence}%)", + self.styles['ChineseHighlight'] + )) + + # 核心提示 + key_message = trading_signals.get('key_message', '') + if key_message: + content.append(Paragraph(f"核心提示:{key_message}", self.styles['ChineseBody'])) + + # AI总结 + advice = ai_analysis.get('investment_advice', {}) + summary = advice.get('summary', '') + if summary: + content.append(Paragraph(f"AI总结:{summary}", self.styles['ChineseBody'])) + + return content + + def _build_flow_analysis(self, result: Dict) -> List: + """构建流量分析""" + content = [] + + content.append(Paragraph("📈 流量分析", self.styles['ChineseHeading'])) + + flow_data = result.get('flow_data', {}) + model_data = result.get('model_data', {}) + + # 流量得分表格 + flow_table_data = [ + ['指标', '数值', '等级'], + ['总流量得分', str(flow_data.get('total_score', 0)), flow_data.get('level', '中')], + ['社交媒体', str(flow_data.get('social_score', 0)), '-'], + ['财经平台', str(flow_data.get('finance_score', 0)), '-'], + ['新闻媒体', str(flow_data.get('news_score', 0)), '-'], + ] + + table = Table(flow_table_data, colWidths=[150, 100, 100]) + table.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#4a69bd')), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), + ('FONTNAME', (0, 0), (-1, -1), self.chinese_font), + ('FONTSIZE', (0, 0), (-1, -1), 10), + ('ALIGN', (0, 0), (-1, -1), 'CENTER'), + ('GRID', (0, 0), (-1, -1), 1, colors.grey), + ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.whitesmoke, colors.white]), + ])) + content.append(table) + + # K值分析 + viral_k = model_data.get('viral_k', {}) + if viral_k: + k_value = viral_k.get('k_value', 1.0) + trend = viral_k.get('trend', '稳定') + content.append(Spacer(1, 10)) + content.append(Paragraph( + f"K值(病毒系数):{k_value:.2f} - {trend}", + self.styles['ChineseBody'] + )) + + return content + + def _build_ai_analysis(self, result: Dict) -> List: + """构建AI分析结果""" + content = [] + + content.append(Paragraph("🤖 AI智能分析", self.styles['ChineseHeading'])) + + ai_analysis = result.get('ai_analysis', {}) + if not ai_analysis: + content.append(Paragraph("暂无AI分析数据", self.styles['ChineseBody'])) + return content + + # 受益板块 + sector_analysis = ai_analysis.get('sector_analysis', {}) + benefited_sectors = sector_analysis.get('benefited_sectors', []) + + if benefited_sectors: + content.append(Paragraph("受益板块分析", self.styles['ChineseSmallHeading'])) + + sector_table_data = [['板块', '置信度', '分析理由']] + for sector in benefited_sectors[:5]: + sector_table_data.append([ + sector.get('name', ''), + f"{sector.get('confidence', 0)}%", + sector.get('reason', '')[:40] + '...' if len(sector.get('reason', '')) > 40 else sector.get('reason', '') + ]) + + table = Table(sector_table_data, colWidths=[100, 60, 280]) + table.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#00b894')), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), + ('FONTNAME', (0, 0), (-1, -1), self.chinese_font), + ('FONTSIZE', (0, 0), (-1, -1), 9), + ('ALIGN', (0, 0), (1, -1), 'CENTER'), + ('ALIGN', (2, 0), (2, -1), 'LEFT'), + ('GRID', (0, 0), (-1, -1), 1, colors.grey), + ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.whitesmoke, colors.white]), + ])) + content.append(table) + + # 热门题材 + hot_themes = sector_analysis.get('hot_themes', []) + if hot_themes: + content.append(Spacer(1, 15)) + content.append(Paragraph("今日热门题材", self.styles['ChineseSmallHeading'])) + + themes_text = [] + for theme in hot_themes[:5]: + themes_text.append(f"• {theme.get('theme', '')} ({theme.get('heat_level', '中')})") + + content.append(Paragraph('
'.join(themes_text), self.styles['ChineseBody'])) + + return content + + def _build_sector_analysis(self, result: Dict) -> List: + """构建板块深度分析""" + content = [] + + ai_analysis = result.get('ai_analysis', {}) + multi_sector = ai_analysis.get('multi_sector', {}) + sector_analyses = multi_sector.get('sector_analyses', []) + + if not sector_analyses: + return content + + content.append(Paragraph("🎯 板块深度分析", self.styles['ChineseHeading'])) + content.append(Paragraph( + f"共分析 {len(sector_analyses)} 个热门板块", + self.styles['ChineseBody'] + )) + + for sector in sector_analyses[:5]: + sector_name = sector.get('sector_name', '未知') + heat_level = sector.get('heat_level', '中') + outlook = sector.get('short_term_outlook', '震荡') + + content.append(Spacer(1, 10)) + content.append(Paragraph( + f"【{sector_name}】热度:{heat_level} | 短期预判:{outlook}", + self.styles['ChineseSmallHeading'] + )) + + # 预判理由 + outlook_reason = sector.get('outlook_reason', '') + if outlook_reason: + content.append(Paragraph(f"预判理由:{outlook_reason}", self.styles['ChineseBody'])) + + # 龙头股 + leaders = sector.get('leader_stocks', []) + if leaders: + leader_names = [f"{s.get('code', '')}{s.get('name', '')}" for s in leaders[:3]] + content.append(Paragraph(f"龙头股:{', '.join(leader_names)}", self.styles['ChineseBody'])) + + # 投资建议 + advice = sector.get('investment_advice', '') + if advice: + content.append(Paragraph(f"建议:{advice}", self.styles['ChineseBody'])) + + return content + + def _build_stock_recommendations(self, result: Dict) -> List: + """构建股票推荐""" + content = [] + + ai_analysis = result.get('ai_analysis', {}) + stock_recommend = ai_analysis.get('stock_recommend', {}) + recommended_stocks = stock_recommend.get('recommended_stocks', []) + + if not recommended_stocks: + return content + + content.append(Paragraph("💰 AI选股推荐", self.styles['ChineseHeading'])) + + # 股票推荐表格 + stock_table_data = [['代码', '名称', '板块', '风险', '推荐理由']] + for stock in recommended_stocks[:8]: + stock_table_data.append([ + stock.get('code', ''), + stock.get('name', ''), + stock.get('sector', ''), + stock.get('risk_level', '中'), + stock.get('reason', '')[:30] + '...' if len(stock.get('reason', '')) > 30 else stock.get('reason', '') + ]) + + table = Table(stock_table_data, colWidths=[60, 70, 70, 40, 200]) + table.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#e17055')), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), + ('FONTNAME', (0, 0), (-1, -1), self.chinese_font), + ('FONTSIZE', (0, 0), (-1, -1), 9), + ('ALIGN', (0, 0), (3, -1), 'CENTER'), + ('ALIGN', (4, 0), (4, -1), 'LEFT'), + ('GRID', (0, 0), (-1, -1), 1, colors.grey), + ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.whitesmoke, colors.white]), + ])) + content.append(table) + + # 整体策略 + overall_strategy = stock_recommend.get('overall_strategy', '') + if overall_strategy: + content.append(Spacer(1, 10)) + content.append(Paragraph(f"整体策略:{overall_strategy}", self.styles['ChineseBody'])) + + return content + + def _build_risk_warning(self, result: Dict) -> List: + """构建风险提示""" + content = [] + + content.append(Paragraph("⚠️ 风险提示", self.styles['ChineseHeading'])) + + ai_analysis = result.get('ai_analysis', {}) + risk_assess = ai_analysis.get('risk_assess', {}) + + risk_level = risk_assess.get('risk_level', '中等') + risk_score = risk_assess.get('risk_score', 50) + risk_factors = risk_assess.get('risk_factors', []) + + content.append(Paragraph( + f"风险等级:{risk_level}(分数:{risk_score}/100)", + self.styles['ChineseBody'] + )) + + if risk_factors: + factors_text = '
'.join([f"• {f}" for f in risk_factors[:5]]) + content.append(Paragraph(f"风险因素:
{factors_text}", self.styles['ChineseBody'])) + + # 免责声明 + disclaimer = """ + 【免责声明】 + 本报告由AI自动生成,仅供参考,不构成任何投资建议。 + 股市有风险,投资需谨慎。请投资者根据自身情况独立判断, + 理性投资,自负盈亏。本报告作者及生成系统不对投资决策 + 产生的任何损失承担责任。 + """ + content.append(Spacer(1, 20)) + content.append(Paragraph(disclaimer, self.styles['ChineseWarning'])) + + return content diff --git a/news_flow_scheduler.py b/news_flow_scheduler.py new file mode 100644 index 0000000..61753e9 --- /dev/null +++ b/news_flow_scheduler.py @@ -0,0 +1,383 @@ +""" +新闻流量定时任务调度模块 +实现三种定时任务:热点同步(30分钟)、预警生成(1小时)、深度分析(2小时) +""" +import schedule +import threading +import time +import logging +from datetime import datetime +from typing import Dict, List, Optional, Callable + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class NewsFlowScheduler: + """新闻流量定时任务调度器""" + + # 任务类型定义 + TASK_TYPES = { + 'sync_hotspots': { + 'name': '热点同步', + 'interval': 30, # 分钟 + 'description': '同步22个平台的热点数据', + }, + 'generate_alerts': { + 'name': '预警生成', + 'interval': 60, # 分钟 + 'description': '检查预警条件并生成预警', + }, + 'deep_analysis': { + 'name': '深度分析', + 'interval': 120, # 分钟 + 'description': '运行完整的AI分析', + }, + } + + def __init__(self): + """初始化调度器""" + self.running = False + self.thread = None + self.lock = threading.Lock() + + # 任务配置 + self.task_enabled = { + 'sync_hotspots': True, + 'generate_alerts': True, + 'deep_analysis': True, + } + + # 任务间隔(分钟) + self.task_intervals = { + 'sync_hotspots': 30, + 'generate_alerts': 60, + 'deep_analysis': 120, + } + + # 上次运行时间 + self.last_run_times = {} + + # 依赖模块 + self.engine = None + self.db = None + self.alert_system = None + + self._init_dependencies() + logger.info("[新闻流量] 调度器初始化完成") + + 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 news_flow_alert import news_flow_alert_system + self.alert_system = news_flow_alert_system + except Exception as e: + logger.warning(f"预警系统初始化失败: {e}") + + def _get_engine(self): + """延迟加载引擎(避免循环导入)""" + if self.engine is None: + try: + from news_flow_engine import news_flow_engine + self.engine = news_flow_engine + except Exception as e: + logger.error(f"引擎模块初始化失败: {e}") + return self.engine + + def start(self): + """启动调度器""" + if self.running: + logger.warning("[新闻流量] 调度器已在运行") + return + + with self.lock: + # 清除旧任务 + self._clear_jobs() + + # 注册任务 + if self.task_enabled.get('sync_hotspots'): + interval = self.task_intervals.get('sync_hotspots', 30) + job = schedule.every(interval).minutes.do(self._run_sync_hotspots) + job.tag('news_flow', 'sync_hotspots') + logger.info(f"[新闻流量] 注册热点同步任务,间隔{interval}分钟") + + if self.task_enabled.get('generate_alerts'): + interval = self.task_intervals.get('generate_alerts', 60) + job = schedule.every(interval).minutes.do(self._run_generate_alerts) + job.tag('news_flow', 'generate_alerts') + logger.info(f"[新闻流量] 注册预警生成任务,间隔{interval}分钟") + + if self.task_enabled.get('deep_analysis'): + interval = self.task_intervals.get('deep_analysis', 120) + job = schedule.every(interval).minutes.do(self._run_deep_analysis) + job.tag('news_flow', 'deep_analysis') + logger.info(f"[新闻流量] 注册深度分析任务,间隔{interval}分钟") + + # 启动调度线程 + self.running = True + self.thread = threading.Thread(target=self._schedule_loop, daemon=True) + self.thread.start() + + logger.info("[新闻流量] 调度器已启动") + + def stop(self): + """停止调度器""" + with self.lock: + self.running = False + self._clear_jobs() + logger.info("[新闻流量] 调度器已停止") + + def _clear_jobs(self): + """清除本模块的任务""" + jobs_to_remove = [job for job in schedule.jobs if 'news_flow' in job.tags] + for job in jobs_to_remove: + schedule.cancel_job(job) + + def _schedule_loop(self): + """调度循环""" + while self.running: + try: + schedule.run_pending() + except Exception as e: + logger.error(f"[新闻流量] 调度循环异常: {e}") + time.sleep(30) # 每30秒检查一次 + + def _log_task(self, task_name: str, task_type: str, + status: str, message: str = '', + duration: float = 0, snapshot_id: int = None): + """记录任务日志""" + try: + if self.db: + self.db.save_scheduler_log( + task_name, task_type, status, message, duration, snapshot_id + ) + except Exception as e: + logger.error(f"记录任务日志失败: {e}") + + def _run_sync_hotspots(self): + """运行热点同步任务""" + task_type = 'sync_hotspots' + task_name = self.TASK_TYPES[task_type]['name'] + + logger.info(f"[新闻流量] 开始执行: {task_name}") + start_time = time.time() + + try: + engine = self._get_engine() + if not engine: + raise Exception("引擎模块不可用") + + # 运行快速分析(仅数据同步和基础计算) + result = engine.run_quick_analysis() + + duration = time.time() - start_time + + if result.get('success'): + snapshot_id = result.get('snapshot_id') + message = f"成功同步{result.get('success_count', 0)}个平台数据" + self._log_task(task_name, task_type, 'success', message, duration, snapshot_id) + logger.info(f"[新闻流量] {task_name}完成: {message}") + else: + message = result.get('error', '未知错误') + self._log_task(task_name, task_type, 'failed', message, duration) + logger.error(f"[新闻流量] {task_name}失败: {message}") + + self.last_run_times[task_type] = datetime.now() + + except Exception as e: + duration = time.time() - start_time + message = str(e) + self._log_task(task_name, task_type, 'error', message, duration) + logger.error(f"[新闻流量] {task_name}异常: {e}") + + def _run_generate_alerts(self): + """运行预警生成任务""" + task_type = 'generate_alerts' + task_name = self.TASK_TYPES[task_type]['name'] + + logger.info(f"[新闻流量] 开始执行: {task_name}") + start_time = time.time() + + try: + engine = self._get_engine() + if not engine: + raise Exception("引擎模块不可用") + + # 运行预警检查 + result = engine.run_alert_check() + + duration = time.time() - start_time + + if result.get('success'): + alert_count = len(result.get('alerts', [])) + message = f"生成{alert_count}个预警" + self._log_task(task_name, task_type, 'success', message, duration) + logger.info(f"[新闻流量] {task_name}完成: {message}") + + # 发送通知 + if alert_count > 0 and self.alert_system: + self.alert_system.send_notification(result['alerts']) + else: + message = result.get('error', '未知错误') + self._log_task(task_name, task_type, 'failed', message, duration) + logger.error(f"[新闻流量] {task_name}失败: {message}") + + self.last_run_times[task_type] = datetime.now() + + except Exception as e: + duration = time.time() - start_time + message = str(e) + self._log_task(task_name, task_type, 'error', message, duration) + logger.error(f"[新闻流量] {task_name}异常: {e}") + + def _run_deep_analysis(self): + """运行深度分析任务""" + task_type = 'deep_analysis' + task_name = self.TASK_TYPES[task_type]['name'] + + logger.info(f"[新闻流量] 开始执行: {task_name}") + start_time = time.time() + + try: + engine = self._get_engine() + if not engine: + raise Exception("引擎模块不可用") + + # 运行完整分析(包含AI) + result = engine.run_full_analysis(include_ai=True) + + duration = time.time() - start_time + + if result.get('success'): + snapshot_id = result.get('snapshot_id') + advice = result.get('ai_analysis', {}).get('investment_advice', {}).get('advice', 'N/A') + message = f"深度分析完成,建议:{advice}" + self._log_task(task_name, task_type, 'success', message, duration, snapshot_id) + logger.info(f"[新闻流量] {task_name}完成: {message}") + else: + message = result.get('error', '未知错误') + self._log_task(task_name, task_type, 'failed', message, duration) + logger.error(f"[新闻流量] {task_name}失败: {message}") + + self.last_run_times[task_type] = datetime.now() + + except Exception as e: + duration = time.time() - start_time + message = str(e) + self._log_task(task_name, task_type, 'error', message, duration) + logger.error(f"[新闻流量] {task_name}异常: {e}") + + # ==================== 手动触发方法 ==================== + + def run_sync_now(self) -> Dict: + """立即执行热点同步""" + logger.info("[新闻流量] 手动触发热点同步") + self._run_sync_hotspots() + return {'success': True, 'message': '热点同步已执行'} + + def run_alerts_now(self) -> Dict: + """立即执行预警生成""" + logger.info("[新闻流量] 手动触发预警生成") + self._run_generate_alerts() + return {'success': True, 'message': '预警生成已执行'} + + def run_analysis_now(self) -> Dict: + """立即执行深度分析""" + logger.info("[新闻流量] 手动触发深度分析") + self._run_deep_analysis() + return {'success': True, 'message': '深度分析已执行'} + + # ==================== 配置方法 ==================== + + def set_task_enabled(self, task_type: str, enabled: bool): + """设置任务开关""" + if task_type in self.task_enabled: + self.task_enabled[task_type] = enabled + logger.info(f"[新闻流量] 任务 {task_type} {'启用' if enabled else '禁用'}") + + # 如果调度器正在运行,重新注册任务 + if self.running: + self.stop() + self.start() + + def set_task_interval(self, task_type: str, interval: int): + """设置任务间隔(分钟)""" + if task_type in self.task_intervals: + self.task_intervals[task_type] = interval + logger.info(f"[新闻流量] 任务 {task_type} 间隔设置为 {interval} 分钟") + + # 如果调度器正在运行,重新注册任务 + if self.running: + self.stop() + self.start() + + def get_status(self) -> Dict: + """获取调度器状态""" + return { + 'running': self.running, + 'task_enabled': self.task_enabled.copy(), + 'task_intervals': self.task_intervals.copy(), + 'last_run_times': { + k: v.strftime('%Y-%m-%d %H:%M:%S') if v else None + for k, v in self.last_run_times.items() + }, + 'next_run_times': self._get_next_run_times(), + } + + def _get_next_run_times(self) -> Dict: + """获取下次运行时间""" + next_times = {} + for job in schedule.jobs: + if 'news_flow' in job.tags: + for tag in job.tags: + if tag in self.TASK_TYPES: + next_times[tag] = str(job.next_run) + return next_times + + def get_task_logs(self, days: int = 7, task_type: str = None) -> List[Dict]: + """获取任务日志""" + if self.db: + return self.db.get_scheduler_logs(days, task_type) + return [] + + +# 全局实例 +news_flow_scheduler = NewsFlowScheduler() + + +# 测试代码 +if __name__ == "__main__": + print("=== 测试新闻流量调度器 ===") + + # 获取状态 + status = news_flow_scheduler.get_status() + print(f"\n调度器状态:") + print(f" 运行中: {status['running']}") + print(f" 任务配置: {status['task_enabled']}") + print(f" 任务间隔: {status['task_intervals']}") + + # 启动调度器 + print("\n启动调度器...") + news_flow_scheduler.start() + + # 再次获取状态 + status = news_flow_scheduler.get_status() + print(f"\n调度器状态:") + print(f" 运行中: {status['running']}") + print(f" 下次运行: {status['next_run_times']}") + + # 等待一会儿 + print("\n等待5秒...") + time.sleep(5) + + # 停止调度器 + print("\n停止调度器...") + news_flow_scheduler.stop() + + print("\n测试完成") diff --git a/news_flow_sentiment.py b/news_flow_sentiment.py new file mode 100644 index 0000000..dc25adf --- /dev/null +++ b/news_flow_sentiment.py @@ -0,0 +1,562 @@ +""" +新闻流量情绪分析模块 +实现情绪指数、情绪分类、流量阶段判断、情绪动量计算 +""" +import logging +from datetime import datetime +from typing import Dict, List, Optional, Tuple +import statistics + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class SentimentAnalyzer: + """情绪分析器""" + + def __init__(self): + # 情绪分类阈值 + self.sentiment_thresholds = { + 'extremely_pessimistic': 20, # 极度悲观 + 'pessimistic': 40, # 悲观 + 'neutral': 60, # 中性 + 'optimistic': 80, # 乐观 + 'extremely_optimistic': 100, # 极度乐观 + } + + # 流量阶段定义 + self.flow_stages = { + 'startup': '启动', # 刚开始发酵 + 'acceleration': '加速', # 增速加快 + 'divergence': '分歧', # 多空分歧 + 'consensus': '一致', # 流量高潮(危险!) + 'decline': '退潮', # 热度下降 + } + + # 阶段判断阈值 + self.stage_thresholds = { + 'startup_growth': 0.20, # 启动期增速阈值 + 'acceleration_growth': 0.20, # 加速期增速阈值 + 'divergence_volatility': 0.30, # 分歧期波动率阈值 + 'consensus_k': 1.5, # 一致期K值阈值 + 'decline_growth': -0.20, # 退潮期增速阈值 + } + + # 正面/负面关键词(用于情绪分析) + self.positive_keywords = [ + '利好', '大涨', '暴涨', '涨停', '新高', '突破', '牛市', + '反弹', '加仓', '买入', '增持', '推荐', '看好', '机遇', + '政策支持', '业绩预增', '超预期', '景气度', '高增长', + ] + + self.negative_keywords = [ + '利空', '大跌', '暴跌', '跌停', '新低', '破位', '熊市', + '回调', '减仓', '卖出', '减持', '风险', '看空', '危机', + '政策收紧', '业绩下滑', '不及预期', '亏损', '退市', + ] + + def calculate_sentiment_index(self, platforms_data: List[Dict], + stock_news: List[Dict] = None) -> Dict: + """ + 计算情绪指数(0-100) + + 基于以下因素: + 1. 流量规模(占40%) + 2. 财经平台活跃度(占30%) + 3. 正负面关键词比例(占30%) + + Returns: + { + 'sentiment_index': int, + 'flow_factor': int, + 'finance_factor': int, + 'keyword_factor': int, + 'sentiment_class': str, + 'analysis': str, + } + """ + # 1. 流量规模因子(40%) + total_news = 0 + finance_news = 0 + + for platform_data in platforms_data: + if not platform_data.get('success'): + continue + count = platform_data.get('count', 0) + total_news += count + if platform_data.get('category') == 'finance': + finance_news += count + + # 流量分数:基于新闻数量 + if total_news >= 500: + flow_factor = 90 + elif total_news >= 300: + flow_factor = 70 + elif total_news >= 150: + flow_factor = 50 + elif total_news >= 50: + flow_factor = 30 + else: + flow_factor = 10 + + # 2. 财经平台活跃度因子(30%) + if total_news > 0: + finance_ratio = finance_news / total_news + finance_factor = min(int(finance_ratio * 200), 100) + else: + finance_factor = 50 + + # 3. 关键词情绪因子(30%) + positive_count = 0 + negative_count = 0 + + if stock_news: + for news in stock_news: + title = news.get('title', '') + content = news.get('content', '') + text = f"{title} {content}" + + for kw in self.positive_keywords: + if kw in text: + positive_count += 1 + break + + for kw in self.negative_keywords: + if kw in text: + negative_count += 1 + break + + total_sentiment_news = positive_count + negative_count + if total_sentiment_news > 0: + positive_ratio = positive_count / total_sentiment_news + keyword_factor = int(positive_ratio * 100) + else: + keyword_factor = 50 # 中性 + + # 4. 综合计算情绪指数 + sentiment_index = int( + flow_factor * 0.4 + + finance_factor * 0.3 + + keyword_factor * 0.3 + ) + + # 限制范围 + sentiment_index = max(0, min(100, sentiment_index)) + + # 5. 情绪分类 + sentiment_class = self.classify_sentiment(sentiment_index) + + # 6. 生成分析 + analysis = self._generate_sentiment_analysis( + sentiment_index, sentiment_class, + flow_factor, finance_factor, keyword_factor + ) + + return { + 'sentiment_index': sentiment_index, + 'flow_factor': flow_factor, + 'finance_factor': finance_factor, + 'keyword_factor': keyword_factor, + 'positive_count': positive_count, + 'negative_count': negative_count, + 'sentiment_class': sentiment_class, + 'analysis': analysis, + } + + def classify_sentiment(self, index: int) -> str: + """ + 情绪分类 + + 极度悲观(<20) / 悲观(20-40) / 中性(40-60) / 乐观(60-80) / 极度乐观(>80) + """ + if index < 20: + return "极度悲观" + elif index < 40: + return "悲观" + elif index < 60: + return "中性" + elif index < 80: + return "乐观" + else: + return "极度乐观" + + def _generate_sentiment_analysis(self, index: int, sentiment_class: str, + flow: int, finance: int, keyword: int) -> str: + """生成情绪分析文本""" + if sentiment_class == "极度乐观": + return f"情绪指数{index},市场极度乐观!流量爆发({flow}),财经活跃({finance})。警告:可能是情绪顶部,注意获利了结。" + elif sentiment_class == "乐观": + return f"情绪指数{index},市场情绪乐观。题材正在发酵,可关注龙头机会,但需注意节奏。" + elif sentiment_class == "中性": + return f"情绪指数{index},市场情绪中性。缺乏明确方向,建议观望为主。" + elif sentiment_class == "悲观": + return f"情绪指数{index},市场情绪偏悲观。负面因素较多,控制仓位,等待转机。" + else: # 极度悲观 + return f"情绪指数{index},市场极度悲观!恐慌情绪蔓延。可能是超跌机会,但需谨慎左侧布局。" + + def determine_flow_stage(self, history_scores: List[int], + current_score: int, + current_k: float = None) -> Dict: + """ + 判断流量阶段 + + 流量阶段: + 1. 启动 - 刚开始发酵,关注 + 2. 加速 - 增速加快,参与 + 3. 分歧 - 多空分歧,谨慎 + 4. 一致 - 流量高潮,危险!准备跑路 + 5. 退潮 - 热度下降,及时止盈止损 + + Returns: + { + 'stage': str, + 'stage_name': str, + 'confidence': int, + 'signal': str, # 关注/参与/谨慎/危险/离场 + 'analysis': str, + } + """ + if len(history_scores) < 3: + return { + 'stage': 'unknown', + 'stage_name': '未知', + 'confidence': 0, + 'signal': '观察', + 'analysis': '历史数据不足,继续观察积累数据', + } + + # 计算增长率序列 + all_scores = history_scores + [current_score] + growth_rates = [] + for i in range(1, len(all_scores)): + if all_scores[i-1] > 0: + rate = (all_scores[i] - all_scores[i-1]) / all_scores[i-1] + growth_rates.append(rate) + + # 计算关键指标 + recent_growth_rates = growth_rates[-3:] if len(growth_rates) >= 3 else growth_rates + avg_growth = sum(recent_growth_rates) / len(recent_growth_rates) if recent_growth_rates else 0 + + # 波动率(标准差) + if len(recent_growth_rates) >= 2: + volatility = statistics.stdev(recent_growth_rates) + else: + volatility = 0 + + # 计算K值(如果没有提供) + if current_k is None and len(all_scores) >= 2: + previous_score = all_scores[-2] + current_k = current_score / previous_score if previous_score > 0 else 1.0 + elif current_k is None: + current_k = 1.0 + + # 判断上升/下降趋势 + positive_count = sum(1 for r in recent_growth_rates if r > 0) + negative_count = sum(1 for r in recent_growth_rates if r < 0) + + # 阶段判断逻辑 + stage = 'unknown' + stage_name = '未知' + confidence = 50 + signal = '观察' + analysis = '' + + # 1. 一致阶段(最危险)- K值>1.5 且 高增速 + if current_k >= 1.5 and avg_growth > 0.3: + stage = 'consensus' + stage_name = '一致' + confidence = 90 + signal = '危险' + analysis = f"流量高潮!K值={current_k:.2f},增速{avg_growth*100:.1f}%。市场一致看多,这往往是顶部信号。立即减仓或清仓!" + + # 2. 退潮阶段 - 连续下降 + elif negative_count >= 2 and avg_growth < -0.15: + stage = 'decline' + stage_name = '退潮' + confidence = 85 + signal = '离场' + analysis = f"流量退潮!连续下降,增速{avg_growth*100:.1f}%。题材热度消退,及时止盈止损,不要恋战。" + + # 3. 分歧阶段 - 高波动,涨跌交替 + elif volatility > 0.25 and positive_count > 0 and negative_count > 0: + stage = 'divergence' + stage_name = '分歧' + confidence = 75 + signal = '谨慎' + analysis = f"多空分歧!波动率{volatility*100:.1f}%,市场观点不一。高抛低吸,控制仓位,设好止损。" + + # 4. 加速阶段 - 持续上涨,增速加快 + elif avg_growth > 0.2 and current_k > 1.1 and positive_count >= 2: + stage = 'acceleration' + stage_name = '加速' + confidence = 80 + signal = '参与' + analysis = f"流量加速!增速{avg_growth*100:.1f}%,K值{current_k:.2f}。题材正在快速发酵,可参与龙头,但注意仓位。" + + # 5. 启动阶段 - 刚开始上涨 + elif avg_growth > 0.05 and positive_count >= 2: + stage = 'startup' + stage_name = '启动' + confidence = 70 + signal = '关注' + analysis = f"流量启动!增速{avg_growth*100:.1f}%,题材刚开始发酵。可以关注,等待确认后介入。" + + # 6. 其他情况 + else: + stage = 'stable' + stage_name = '平稳' + confidence = 60 + signal = '观察' + analysis = f"流量平稳。增速{avg_growth*100:.1f}%,无明显趋势。保持观望,等待方向明确。" + + return { + 'stage': stage, + 'stage_name': stage_name, + 'confidence': confidence, + 'signal': signal, + 'analysis': analysis, + 'avg_growth': round(avg_growth * 100, 1), + 'volatility': round(volatility * 100, 1), + 'current_k': round(current_k, 2), + } + + def calculate_momentum(self, history_data: List[Dict]) -> Dict: + """ + 计算情绪动量 + + 情绪动量 = 当前变化速率 / 平均变化速率 + + 动量 > 1.5: 情绪加速 + 动量 ≈ 1: 情绪稳定 + 动量 < 0.5: 情绪减速 + + Args: + history_data: 历史数据列表,每项包含 'sentiment_index' 和 'timestamp' + + Returns: + { + 'momentum': float, + 'momentum_level': str, + 'trend': str, + 'analysis': str, + } + """ + if len(history_data) < 3: + return { + 'momentum': 1.0, + 'momentum_level': '正常', + 'trend': '数据不足', + 'analysis': '历史数据不足,无法计算动量', + } + + # 提取情绪指数序列 + sentiment_values = [d.get('sentiment_index', 50) for d in history_data] + + # 计算变化率序列 + changes = [] + for i in range(1, len(sentiment_values)): + change = sentiment_values[i] - sentiment_values[i-1] + changes.append(abs(change)) + + # 计算平均变化率 + avg_change = sum(changes) / len(changes) if changes else 1 + + # 计算当前变化率(最近的变化) + current_change = abs(changes[-1]) if changes else 0 + + # 计算动量 + if avg_change > 0: + momentum = round(current_change / avg_change, 2) + else: + momentum = 1.0 + + # 确定动量级别和趋势 + if momentum >= 2.0: + momentum_level = '极高' + trend = '急剧变化' + analysis = f"情绪动量{momentum},情绪正在急剧变化!市场可能出现转折点,密切关注。" + elif momentum >= 1.5: + momentum_level = '高' + trend = '加速变化' + analysis = f"情绪动量{momentum},情绪变化正在加速。趋势可能强化或反转。" + elif momentum >= 0.8: + momentum_level = '正常' + trend = '稳定' + analysis = f"情绪动量{momentum},情绪变化平稳,市场处于正常状态。" + elif momentum >= 0.3: + momentum_level = '低' + trend = '减速' + analysis = f"情绪动量{momentum},情绪变化正在减速,可能进入盘整期。" + else: + momentum_level = '极低' + trend = '停滞' + analysis = f"情绪动量{momentum},情绪几乎没有变化,市场陷入僵局。" + + # 判断方向 + if len(sentiment_values) >= 2: + direction = sentiment_values[-1] - sentiment_values[-2] + if direction > 0: + trend += "(向上)" + elif direction < 0: + trend += "(向下)" + + return { + 'momentum': momentum, + 'momentum_level': momentum_level, + 'trend': trend, + 'current_change': current_change, + 'avg_change': round(avg_change, 1), + 'analysis': analysis, + } + + def run_full_sentiment_analysis(self, platforms_data: List[Dict], + stock_news: List[Dict], + history_scores: List[int], + current_score: int, + history_sentiments: List[Dict] = None) -> Dict: + """ + 运行完整的情绪分析 + + Returns: + { + 'sentiment': Dict, # 情绪指数 + 'flow_stage': Dict, # 流量阶段 + 'momentum': Dict, # 情绪动量 + 'summary': str, # 总结 + 'risk_level': str, # 风险等级 + 'advice': str, # 操作建议 + } + """ + # 1. 计算情绪指数 + sentiment = self.calculate_sentiment_index(platforms_data, stock_news) + + # 2. 判断流量阶段 + flow_stage = self.determine_flow_stage(history_scores, current_score) + + # 3. 计算情绪动量 + if history_sentiments and len(history_sentiments) >= 3: + momentum = self.calculate_momentum(history_sentiments) + else: + momentum = { + 'momentum': 1.0, + 'momentum_level': '正常', + 'trend': '数据不足', + 'analysis': '历史情绪数据不足', + } + + # 4. 综合风险评估 + risk_level, advice = self._assess_risk(sentiment, flow_stage, momentum) + + # 5. 生成总结 + summary = self._generate_summary(sentiment, flow_stage, momentum, risk_level) + + return { + 'sentiment': sentiment, + 'flow_stage': flow_stage, + 'momentum': momentum, + 'summary': summary, + 'risk_level': risk_level, + 'advice': advice, + 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + } + + def _assess_risk(self, sentiment: Dict, flow_stage: Dict, + momentum: Dict) -> Tuple[str, str]: + """综合风险评估""" + risk_score = 0 + + # 情绪因素 + sentiment_index = sentiment['sentiment_index'] + if sentiment_index > 85: + risk_score += 3 # 过度乐观 + elif sentiment_index < 25: + risk_score += 2 # 过度悲观 + + # 流量阶段因素 + stage = flow_stage['stage'] + if stage == 'consensus': + risk_score += 4 # 一致阶段最危险 + elif stage == 'decline': + risk_score += 3 + elif stage == 'divergence': + risk_score += 2 + + # 动量因素 + momentum_value = momentum['momentum'] + if momentum_value > 2.0: + risk_score += 2 # 变化过快 + + # 确定风险等级和建议 + if risk_score >= 7: + risk_level = "极高" + advice = "立即减仓或清仓!市场处于极端状态,控制风险为第一要务。" + elif risk_score >= 5: + risk_level = "高" + advice = "谨慎操作,建议减仓。不追高,设置严格止损。" + elif risk_score >= 3: + risk_level = "中等" + advice = "正常操作,注意仓位控制。逢高减仓,逢低观察。" + elif risk_score >= 1: + risk_level = "低" + advice = "可适度参与,关注龙头机会。" + else: + risk_level = "极低" + advice = "风险较低,可积极参与,但仍需设置止损。" + + return risk_level, advice + + def _generate_summary(self, sentiment: Dict, flow_stage: Dict, + momentum: Dict, risk_level: str) -> str: + """生成综合总结""" + lines = [ + f"【情绪】{sentiment['sentiment_class']}({sentiment['sentiment_index']}分)", + f"【阶段】{flow_stage['stage_name']}期,信号:{flow_stage['signal']}", + f"【动量】{momentum['momentum_level']},趋势{momentum['trend']}", + f"【风险】{risk_level}", + ] + + return '\n'.join(lines) + + +# 全局实例 +sentiment_analyzer = SentimentAnalyzer() + + +# 测试代码 +if __name__ == "__main__": + print("=== 测试情绪分析模块 ===") + + # 模拟数据 + platforms_data = [ + {'success': True, 'category': 'finance', 'count': 100}, + {'success': True, 'category': 'social', 'count': 200}, + {'success': True, 'category': 'news', 'count': 150}, + ] + + stock_news = [ + {'title': 'AI板块大涨,龙头股涨停', 'content': '利好政策推动'}, + {'title': '新能源概念股反弹', 'content': '业绩预增超预期'}, + {'title': '市场观望情绪浓厚', 'content': '回调风险加大'}, + ] + + history_scores = [300, 350, 420, 500, 580] + current_score = 650 + + history_sentiments = [ + {'sentiment_index': 55}, + {'sentiment_index': 60}, + {'sentiment_index': 68}, + {'sentiment_index': 72}, + ] + + # 运行完整分析 + result = sentiment_analyzer.run_full_sentiment_analysis( + platforms_data, stock_news, history_scores, current_score, history_sentiments + ) + + print(f"\n情绪指数: {result['sentiment']['sentiment_index']} ({result['sentiment']['sentiment_class']})") + print(f"流量阶段: {result['flow_stage']['stage_name']} - {result['flow_stage']['signal']}") + print(f"情绪动量: {result['momentum']['momentum']} ({result['momentum']['momentum_level']})") + print(f"风险等级: {result['risk_level']}") + print(f"\n===总结===\n{result['summary']}") + print(f"\n操作建议: {result['advice']}") diff --git a/news_flow_ui.py b/news_flow_ui.py new file mode 100644 index 0000000..0a9760e --- /dev/null +++ b/news_flow_ui.py @@ -0,0 +1,1229 @@ +""" +新闻流量监测UI界面 +为短线炒股提供新闻流量分析和交易指导 +包含:仪表盘、实时监测、预警中心、趋势分析、历史记录、设置 +""" +import streamlit as st +import plotly.graph_objects as go +import plotly.express as px +import pandas as pd +from datetime import datetime, timedelta +import time + + +def display_news_flow_monitor(): + """显示新闻流量监测主界面""" + st.title("📰 新闻流量监测") + + # 功能说明 + with st.expander("💡 功能说明 - 流量为王理念", expanded=False): + st.markdown(""" + ### 核心公式 + **接盘总量 = 流量 × 转化率 × 客单价** + + ### 核心理念 + - **流量高潮 = 价格高潮 = 逃命时刻** + - 当热搜、媒体报道、KOL转发同时达到高潮时,就是出货时机 + - 短线操作:快进快出,紧跟龙头 + + ### 流量阶段 + 1. **启动** - 刚开始发酵,可关注 + 2. **加速** - 增速加快,可参与 + 3. **分歧** - 多空分歧,需谨慎 + 4. **一致** - 流量高潮,危险!准备跑路 + 5. **退潮** - 热度下降,及时止盈止损 + + ### 流量类型 + - **存量流量型**:出生自带顶流(政策/大事件),时间窗口短(2-3天) + - **增量流量型**:逐步发酵(话题传播),时间窗口长(5-10天) + """) + + # 标签页 + tabs = st.tabs([ + "📊 仪表盘", + "🔥 实时监测", + "⚠️ 预警中心", + "📈 趋势分析", + "📚 历史记录", + "⚙️ 设置" + ]) + + with tabs[0]: + display_dashboard() + + with tabs[1]: + display_realtime_monitor() + + with tabs[2]: + display_alert_center() + + with tabs[3]: + display_trend_analysis() + + with tabs[4]: + display_history_records() + + with tabs[5]: + display_settings() + + +def display_dashboard(): + """显示仪表盘""" + st.subheader("📊 流量仪表盘") + + try: + from news_flow_engine import news_flow_engine + dashboard_data = news_flow_engine.get_dashboard_data() + except Exception as e: + st.error(f"获取仪表盘数据失败: {e}") + return + + # 核心指标卡片 + col1, col2, col3, col4 = st.columns(4) + + latest = dashboard_data.get('latest_snapshot', {}) + sentiment = dashboard_data.get('latest_sentiment', {}) + ai = dashboard_data.get('latest_ai_analysis', {}) + + with col1: + score = latest.get('total_score', 0) if latest else 0 + level = latest.get('flow_level', '无数据') if latest else '无数据' + st.metric("流量得分", f"{score}", delta=level) + + with col2: + sent_idx = sentiment.get('sentiment_index', 50) if sentiment else 50 + sent_class = sentiment.get('sentiment_class', '中性') if sentiment else '中性' + st.metric("情绪指数", f"{sent_idx}", delta=sent_class) + + with col3: + stage = sentiment.get('flow_stage', '未知') if sentiment else '未知' + k_val = sentiment.get('viral_k', 1.0) if sentiment else 1.0 + st.metric("流量阶段", stage, delta=f"K值:{k_val}") + + with col4: + advice = ai.get('advice', '观望') if ai else '观望' + confidence = ai.get('confidence', 50) if ai else 50 + st.metric("AI建议", advice, delta=f"置信度:{confidence}%") + + st.divider() + + # 两列布局 + left_col, right_col = st.columns([2, 1]) + + with left_col: + # 流量趋势图 + st.markdown("#### 流量趋势(7天)") + trend = dashboard_data.get('flow_trend', {}) + + if trend.get('dates'): + fig = go.Figure() + fig.add_trace(go.Scatter( + x=trend['dates'], + y=trend['avg_scores'], + mode='lines+markers', + name='平均得分', + line=dict(color='#1f77b4', width=2) + )) + fig.add_trace(go.Scatter( + x=trend['dates'], + y=trend['max_scores'], + mode='lines', + name='最高得分', + line=dict(color='#ff7f0e', dash='dash') + )) + fig.update_layout( + height=250, + margin=dict(l=0, r=0, t=20, b=0), + legend=dict(orientation="h", yanchor="bottom", y=1.02) + ) + st.plotly_chart(fig, width='stretch') + st.caption(f"趋势: {trend.get('trend', '无数据')} - {trend.get('analysis', '')[:50]}...") + else: + st.info("暂无趋势数据,请先执行监测") + + with right_col: + # 最近预警 + st.markdown("#### 最近预警") + alerts = dashboard_data.get('recent_alerts', []) + + if alerts: + for alert in alerts[:5]: + level = alert.get('alert_level', 'info') + icon = {'danger': '🔴', 'warning': '🟠', 'info': '🔵'}.get(level, '⚪') + st.markdown(f"{icon} **{alert.get('title', '')[:20]}...**") + st.caption(alert.get('created_at', '')[:16]) + else: + st.info("暂无预警") + + st.divider() + + # 热点词云和TOP100新闻 + display_wordcloud_and_top_news() + + st.divider() + + # 调度器状态 + scheduler = dashboard_data.get('scheduler_status', {}) + if scheduler: + st.markdown("#### ⏰ 定时任务状态") + cols = st.columns(4) + cols[0].metric("运行状态", "运行中" if scheduler.get('running') else "已停止") + + next_runs = scheduler.get('next_run_times', {}) + cols[1].caption(f"热点同步: {next_runs.get('sync_hotspots', 'N/A')[:16] if next_runs.get('sync_hotspots') else 'N/A'}") + cols[2].caption(f"预警生成: {next_runs.get('generate_alerts', 'N/A')[:16] if next_runs.get('generate_alerts') else 'N/A'}") + cols[3].caption(f"深度分析: {next_runs.get('deep_analysis', 'N/A')[:16] if next_runs.get('deep_analysis') else 'N/A'}") + + +def display_wordcloud_and_top_news(): + """显示热点词云和跨平台TOP100新闻""" + + # 尝试获取最新数据 + try: + from news_flow_db import news_flow_db + from news_flow_data import NewsFlowDataFetcher + + # 获取最近的快照 + recent_snapshots = news_flow_db.get_recent_snapshots(limit=1) + + if not recent_snapshots: + st.info("暂无数据,请先执行实时监测") + return + + snapshot_id = recent_snapshots[0]['id'] + detail = news_flow_db.get_snapshot_detail(snapshot_id) + + hot_topics = detail.get('hot_topics', []) + + except Exception as e: + st.warning(f"获取数据失败: {e}") + return + + # 两列布局 + col1, col2 = st.columns([1, 1]) + + with col1: + st.markdown("#### ☁️ 热点词云") + + if hot_topics: + # 使用Plotly生成词云效果(气泡图模拟) + import random + + # 准备数据 + words_data = [] + for i, topic in enumerate(hot_topics[:30]): + words_data.append({ + 'word': topic.get('topic', '')[:10], + 'heat': topic.get('heat', 10), + 'x': random.uniform(0, 100), + 'y': random.uniform(0, 100), + }) + + if words_data: + df_words = pd.DataFrame(words_data) + + # 创建气泡图模拟词云 + fig = px.scatter( + df_words, + x='x', + y='y', + size='heat', + text='word', + color='heat', + color_continuous_scale='YlOrRd', + size_max=60 + ) + + fig.update_traces( + textposition='middle center', + textfont=dict(size=12), + marker=dict(opacity=0.7) + ) + + fig.update_layout( + height=350, + showlegend=False, + xaxis=dict(visible=False), + yaxis=dict(visible=False), + margin=dict(l=0, r=0, t=0, b=0), + coloraxis_showscale=False + ) + + st.plotly_chart(fig, width='stretch', key="wordcloud_chart") + else: + st.info("暂无热点词云数据") + + with col2: + st.markdown("#### 📊 热点话题TOP20") + + if hot_topics: + for i, topic in enumerate(hot_topics[:20], 1): + heat = topic.get('heat', 0) + cross = topic.get('cross_platform', 0) + + # 热度条 + heat_pct = min(heat, 100) + st.markdown(f"**{i}. {topic.get('topic', '')[:15]}**") + st.progress(heat_pct / 100) + st.caption(f"热度: {heat} | 跨{cross}个平台") + else: + st.info("暂无热点话题") + + st.divider() + + # 跨平台热点新闻TOP100 + st.markdown("#### 📰 跨平台热点新闻TOP100") + + try: + # 获取所有平台新闻 + fetcher = NewsFlowDataFetcher() + multi_result = fetcher.get_multi_platform_news() + + if multi_result.get('success'): + # 汇总所有新闻 + all_news = [] + for platform_data in multi_result.get('platforms_data', []): + if platform_data.get('success'): + platform_name = platform_data.get('platform_name', '') + category = platform_data.get('category', '') + weight = platform_data.get('weight', 5) + + for news in platform_data.get('data', []): + rank = news.get('rank', 99) + # 计算综合得分 + score = (100 - rank) * weight + + all_news.append({ + '排名': len(all_news) + 1, + '平台': platform_name, + '类别': {'finance': '财经', 'social': '社交', 'news': '新闻', 'tech': '科技'}.get(category, '其他'), + '标题': (news.get('title') or '')[:40], + '平台排名': rank, + '综合分': score, + }) + + # 按综合分排序 + all_news.sort(key=lambda x: x['综合分'], reverse=True) + + # 取TOP100 + top_100 = all_news[:100] + + # 重新编排名 + for i, news in enumerate(top_100, 1): + news['排名'] = i + + if top_100: + df_news = pd.DataFrame(top_100) + + # 添加筛选 + filter_col1, filter_col2 = st.columns([1, 3]) + with filter_col1: + category_filter = st.selectbox( + "按类别筛选", + ["全部", "财经", "社交", "新闻", "科技"], + key="news_category_filter" + ) + + if category_filter != "全部": + df_news = df_news[df_news['类别'] == category_filter] + + st.dataframe( + df_news[['排名', '平台', '类别', '标题', '综合分']], + width='stretch', + hide_index=True, + height=400 + ) + + st.caption(f"共 {len(df_news)} 条新闻 | 数据来源: {multi_result.get('success_count', 0)} 个平台") + else: + st.info("暂无新闻数据") + else: + st.warning("获取新闻数据失败") + + except Exception as e: + st.error(f"加载新闻失败: {e}") + + +def display_realtime_monitor(): + """显示实时监测""" + st.subheader("🔥 实时监测") + + # 监测参数 + col1, col2, col3 = st.columns([1, 1, 1]) + + with col1: + category = st.selectbox( + "平台类别", + ["全部平台", "财经平台", "社交媒体", "新闻媒体", "科技媒体"], + key="monitor_category" + ) + + category_map = { + "全部平台": None, + "财经平台": "finance", + "社交媒体": "social", + "新闻媒体": "news", + "科技媒体": "tech", + } + + with col2: + st.write("") + st.write("") + run_btn = st.button("🚀 开始AI智能分析", type="primary", width='stretch') + + with col3: + st.write("") + st.write("") + # 空占位 + + if run_btn: + with st.spinner("🤖 AI正在分析全网热点新闻..."): + try: + from news_flow_engine import news_flow_engine + + cat = category_map.get(category) + + # 显示分析进度 + progress_bar = st.progress(0) + status_text = st.empty() + + status_text.text("📊 获取多平台新闻数据...") + progress_bar.progress(10) + + result = news_flow_engine.run_full_analysis(category=cat, include_ai=True) + + progress_bar.progress(100) + status_text.empty() + progress_bar.empty() + + if result['success']: + st.session_state['news_flow_result'] = result + # 清除之前的PDF缓存 + if 'news_flow_pdf_data' in st.session_state: + del st.session_state['news_flow_pdf_data'] + st.success(f"✅ AI分析完成!耗时 {result.get('duration', 0):.1f} 秒") + else: + st.error(f"❌ 分析失败: {result.get('error')}") + + except Exception as e: + st.error(f"❌ 分析异常: {e}") + + # 显示结果 + if 'news_flow_result' in st.session_state: + display_analysis_results(st.session_state['news_flow_result']) + + +def display_analysis_results(result: dict): + """显示分析结果""" + st.divider() + + flow_data = result.get('flow_data', {}) + model_data = result.get('model_data', {}) + sentiment_data = result.get('sentiment_data', {}) + ai_analysis = result.get('ai_analysis') + trading_signals = result.get('trading_signals', {}) + + # 核心指标 + st.markdown("### 📊 核心指标") + + cols = st.columns(5) + + with cols[0]: + score = flow_data.get('total_score', 0) + level = flow_data.get('level', '中') + level_color = {'极高': '🔴', '高': '🟠', '中': '🟡', '低': '🔵'}.get(level, '⚪') + st.metric("流量得分", f"{score}", delta=f"{level_color} {level}") + + with cols[1]: + if sentiment_data: + sent = sentiment_data.get('sentiment', {}) + st.metric("情绪指数", f"{sent.get('sentiment_index', 50)}", + delta=sent.get('sentiment_class', '中性')) + + with cols[2]: + if sentiment_data: + stage = sentiment_data.get('flow_stage', {}) + signal = stage.get('signal', '观察') + st.metric("流量阶段", stage.get('stage_name', '未知'), delta=signal) + + with cols[3]: + if model_data: + viral = model_data.get('viral_k', {}) + st.metric("K值", f"{viral.get('k_value', 1.0)}", delta=viral.get('trend', '')) + + with cols[4]: + signal = trading_signals.get('overall_signal', '观望') + conf = trading_signals.get('confidence', 50) + signal_color = {'卖出': '🔴', '观望': '🟡', '买入': '🟢', '关注': '🔵'}.get(signal, '⚪') + st.metric("交易信号", f"{signal_color} {signal}", delta=f"置信度:{conf}%") + + # 关键提示 + key_msg = trading_signals.get('key_message', '') + if key_msg: + if '危险' in key_msg or '逃命' in key_msg: + st.error(f"⚠️ {key_msg}") + elif '加速' in key_msg or '买入' in key_msg: + st.success(f"💡 {key_msg}") + else: + st.info(f"💡 {key_msg}") + + st.divider() + + # 详细分析(两列) + left_col, right_col = st.columns(2) + + with left_col: + # 流量模型 + st.markdown("#### 🔬 流量模型分析") + + if model_data: + # 潜力计算 + potential = model_data.get('potential', {}) + conversion = model_data.get('conversion', {}) + + st.markdown(f""" + - **接盘潜力**: {potential.get('potential_volume', 0):.1f}亿元 ({potential.get('potential_level', '未知')}) + - **转化率**: {conversion.get('conversion_rate', 0):.4%} + - **流量类型**: {model_data.get('flow_type', {}).get('flow_type', '未知')} + """) + + # 流量类型详情 + flow_type = model_data.get('flow_type', {}) + if flow_type.get('characteristics'): + st.caption(f"特征: {', '.join(flow_type.get('characteristics', [])[:2])}") + st.caption(f"时间窗口: {flow_type.get('time_window', 'N/A')}") + + # 情绪分析 + st.markdown("#### 💭 情绪分析") + + if sentiment_data: + sentiment = sentiment_data.get('sentiment', {}) + flow_stage = sentiment_data.get('flow_stage', {}) + momentum = sentiment_data.get('momentum', {}) + + st.markdown(f""" + - **情绪分类**: {sentiment.get('sentiment_class', '中性')} ({sentiment.get('sentiment_index', 50)}分) + - **流量阶段**: {flow_stage.get('stage_name', '未知')} - {flow_stage.get('signal', '观察')} + - **情绪动量**: {momentum.get('momentum', 1.0)} ({momentum.get('trend', '稳定')}) + - **风险等级**: {sentiment_data.get('risk_level', '中等')} + """) + + st.caption(sentiment_data.get('advice', '')) + + with right_col: + # AI分析(如果有) + if ai_analysis: + st.markdown("#### 🤖 AI智能分析") + + # 投资建议 + advice = ai_analysis.get('investment_advice', {}) + if advice: + advice_emoji = {'买入': '🟢', '持有': '🔵', '观望': '🟡', '回避': '🔴'}.get(advice.get('advice', '观望'), '⚪') + st.markdown(f"**{advice_emoji} AI建议**: {advice.get('advice', '观望')} (置信度: {advice.get('confidence', 50)}%)") + + key_msg = advice.get('key_message', '') + if key_msg: + st.info(f"💡 {key_msg}") + + if advice.get('action_plan'): + with st.expander("📋 行动计划"): + for i, plan in enumerate(advice.get('action_plan', [])[:5], 1): + st.write(f"{i}. {plan}") + + # 风险评估 + risk = ai_analysis.get('risk_assess', {}) + if risk: + risk_color = {'低': '🟢', '中': '🟡', '高': '🔴'}.get(risk.get('risk_level', '中'), '🟡') + st.markdown(f"**{risk_color} 风险等级**: {risk.get('risk_level', '未知')} (分数: {risk.get('risk_score', 50)}/100)") + + else: + st.info("未运行AI分析。选择'完整分析(含AI)'模式获取AI智能分析。") + + st.divider() + + # AI详细分析区域(如果有AI分析) + if ai_analysis: + st.markdown("### 📊 AI深度分析报告") + + # 热门题材 + sector_analysis = ai_analysis.get('sector_analysis', {}) + hot_themes = sector_analysis.get('hot_themes', []) + + if hot_themes: + st.markdown("#### 🔥 今日热门题材") + theme_cols = st.columns(min(len(hot_themes), 4)) + for i, theme in enumerate(hot_themes[:4]): + with theme_cols[i]: + heat_emoji = {'极高': '🔴', '高': '🟠', '中': '🟡'}.get(theme.get('heat_level', '中'), '🟡') + st.metric( + theme.get('theme', '未知'), + f"{heat_emoji} {theme.get('heat_level', '中')}", + delta=theme.get('sustainability', '') + ) + st.caption(f"题材来源: {', '.join([t.get('source', '')[:15] for t in hot_themes[:3]])}") + + # 受益板块详细分析 + benefited_sectors = sector_analysis.get('benefited_sectors', []) + if benefited_sectors: + st.markdown("#### 📈 受益板块分析") + for sector in benefited_sectors[:5]: + with st.expander(f"**{sector.get('name', '')}** - 置信度 {sector.get('confidence', 0)}%"): + st.write(f"**分析**: {sector.get('reason', '')}") + if sector.get('related_concepts'): + st.write(f"**相关概念**: {', '.join(sector.get('related_concepts', []))}") + if sector.get('leader_characteristics'): + st.write(f"**龙头特征**: {sector.get('leader_characteristics', '')}") + + # 多板块深度分析(多次AI调用结果) + multi_sector = ai_analysis.get('multi_sector', {}) + sector_analyses = multi_sector.get('sector_analyses', []) + + if sector_analyses: + st.markdown("#### 🎯 板块深度分析(多Agent分析)") + st.caption(f"共分析 {len(sector_analyses)} 个热门板块,耗时 {multi_sector.get('analysis_time', 0)} 秒") + + # 板块核心指标卡片 + sector_cols = st.columns(min(len(sector_analyses), 4)) + for i, sector in enumerate(sector_analyses[:4]): + with sector_cols[i]: + heat_emoji = {'极高': '🔴', '高': '🟠', '中': '🟡', '低': '🔵'}.get( + sector.get('heat_level', '中'), '🟡' + ) + outlook_emoji = {'看涨': '📈', '震荡': '📊', '看跌': '📉'}.get( + sector.get('short_term_outlook', '震荡'), '📊' + ) + + st.metric( + sector.get('sector_name', '未知'), + f"{heat_emoji} 热度{sector.get('heat_score', 50)}", + delta=f"{outlook_emoji} {sector.get('short_term_outlook', '震荡')}" + ) + + # 显示关键指标 + indicators = sector.get('key_indicators', {}) + if indicators: + indicator_text = ' | '.join([f"{k}:{v}" for k, v in list(indicators.items())[:3]]) + st.caption(indicator_text) + + # 详细分析展开 + for sector in sector_analyses: + with st.expander(f"📊 {sector.get('sector_name', '')} - 详细分析"): + # 驱动因素 + drivers = sector.get('drivers', []) + if drivers: + st.markdown("**驱动因素**:") + for driver in drivers[:3]: + impact_emoji = '✅' if driver.get('impact') == '正面' else '❌' + st.write(f"- {impact_emoji} [{driver.get('type', '')}] {driver.get('content', '')}") + + # 预判理由 + st.markdown(f"**短期预判**: {sector.get('short_term_outlook', '震荡')} - {sector.get('outlook_reason', '')}") + + # 龙头股 + leaders = sector.get('leader_stocks', []) + if leaders: + st.markdown("**板块龙头股**:") + leader_data = [] + for stock in leaders[:5]: + leader_data.append({ + '代码': stock.get('code', ''), + '名称': stock.get('name', ''), + '理由': stock.get('reason', '')[:30], + '策略': stock.get('strategy', '')[:30], + }) + if leader_data: + st.dataframe(pd.DataFrame(leader_data), width='stretch', hide_index=True) + + # 投资建议 + st.info(f"💡 {sector.get('investment_advice', '')}") + st.warning(f"⚠️ {sector.get('risk_warning', '')}") + + # 股票推荐 + stock_recommend = ai_analysis.get('stock_recommend', {}) + recommended_stocks = stock_recommend.get('recommended_stocks', []) + + if recommended_stocks: + st.markdown("#### 💰 AI选股推荐") + st.warning("⚠️ 以下为AI分析结果,仅供参考,不构成投资建议。股市有风险,投资需谨慎!") + + # 用表格展示推荐股票 + stock_data = [] + for stock in recommended_stocks[:8]: + stock_data.append({ + '代码': stock.get('code', ''), + '名称': stock.get('name', ''), + '板块': stock.get('sector', ''), + '风险': stock.get('risk_level', '中'), + '目标空间': stock.get('target_space', '-'), + '推荐理由': stock.get('reason', '')[:30] + '...' if len(stock.get('reason', '')) > 30 else stock.get('reason', ''), + }) + + if stock_data: + df_stocks = pd.DataFrame(stock_data) + st.dataframe(df_stocks, width='stretch', hide_index=True) + + # 展开查看详情 + with st.expander("📋 查看详细推荐理由"): + for stock in recommended_stocks[:8]: + st.markdown(f"**{stock.get('code', '')} {stock.get('name', '')}**") + st.write(f"- 板块: {stock.get('sector', '')}") + st.write(f"- 理由: {stock.get('reason', '')}") + st.write(f"- 催化剂: {stock.get('catalyst', '')}") + st.write(f"- 策略: {stock.get('strategy', '')}") + if stock.get('attention_points'): + st.write(f"- 注意: {', '.join(stock.get('attention_points', []))}") + st.divider() + + # 整体策略 + if stock_recommend.get('overall_strategy'): + st.info(f"📊 **整体策略**: {stock_recommend.get('overall_strategy', '')}") + if stock_recommend.get('timing_advice'): + st.caption(f"⏰ 时机建议: {stock_recommend.get('timing_advice', '')}") + if stock_recommend.get('risk_warning'): + st.error(f"⚠️ {stock_recommend.get('risk_warning', '')}") + + # 机会评估 + opportunity = sector_analysis.get('opportunity_assessment', '') + trading_suggestion = sector_analysis.get('trading_suggestion', '') + if opportunity or trading_suggestion: + st.markdown("#### 💡 综合评估") + if opportunity: + st.write(opportunity) + if trading_suggestion: + st.success(f"📌 {trading_suggestion}") + + st.divider() + + # 热门话题和新闻 + col1, col2 = st.columns(2) + + with col1: + st.markdown("#### 🔥 热门话题TOP10") + hot_topics = result.get('hot_topics', [])[:10] + + if hot_topics: + df = pd.DataFrame([{ + '话题': t['topic'], + '热度': t['heat'], + '跨平台': t.get('cross_platform', 0), + } for t in hot_topics]) + + fig = px.bar(df, x='热度', y='话题', orientation='h', + color='热度', color_continuous_scale='Oranges') + fig.update_layout(height=300, margin=dict(l=0, r=0, t=0, b=0)) + st.plotly_chart(fig, width='stretch') + + with col2: + st.markdown("#### 📰 股票相关新闻TOP5") + stock_news = result.get('stock_news', [])[:5] + + if stock_news: + for news in stock_news: + st.markdown(f"**[{news.get('platform_name', '')}]** {news.get('title', '')[:40]}...") + st.caption(f"关键词: {', '.join(news.get('matched_keywords', [])[:3])}") + else: + st.info("暂无股票相关新闻") + + # PDF导出区域 + display_pdf_export_section(result) + + +def display_pdf_export_section(result: dict): + """显示PDF导出区域""" + st.divider() + st.markdown("### 📄 导出报告") + + col1, col2, col3 = st.columns([3, 1, 1]) + + with col1: + st.write("将分析报告导出为PDF文件,方便保存和分享") + + with col2: + if st.button("📥 生成PDF报告", type="primary", key="gen_pdf_btn"): + with st.spinner("正在生成PDF报告..."): + try: + from news_flow_pdf import NewsFlowPDFGenerator + generator = NewsFlowPDFGenerator() + pdf_path = generator.generate_report(result) + + if pdf_path: + with open(pdf_path, "rb") as f: + pdf_bytes = f.read() + + # 保存到session_state + st.session_state.news_flow_pdf_data = pdf_bytes + st.session_state.news_flow_pdf_filename = f"新闻流量分析报告_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" + + st.success("✅ PDF报告生成成功!") + st.rerun() + else: + st.error("❌ PDF生成失败") + except Exception as e: + st.error(f"❌ PDF生成失败: {str(e)}") + + with col3: + # 如果已经生成了PDF,显示下载按钮 + if 'news_flow_pdf_data' in st.session_state: + st.download_button( + label="💾 下载PDF", + data=st.session_state.news_flow_pdf_data, + file_name=st.session_state.news_flow_pdf_filename, + mime="application/pdf", + key="download_pdf_btn" + ) + + +def display_alert_center(): + """显示预警中心""" + st.subheader("⚠️ 预警中心") + + try: + from news_flow_alert import news_flow_alert_system + from news_flow_db import news_flow_db + except Exception as e: + st.error(f"模块加载失败: {e}") + return + + # 预警统计 + summary = news_flow_alert_system.get_alert_summary(days=7) + + cols = st.columns(4) + cols[0].metric("总预警数", summary.get('total_count', 0)) + cols[1].metric("危险预警", summary.get('danger_count', 0)) + cols[2].metric("警告预警", summary.get('warning_count', 0)) + cols[3].metric("提示预警", summary.get('info_count', 0)) + + st.divider() + + # 预警列表 + col1, col2 = st.columns([3, 1]) + + with col2: + days = st.selectbox("时间范围", [1, 3, 7, 30], index=2, key="alert_days") + alert_type = st.selectbox("预警类型", ["全部", "热度飙升", "流量高潮", "情绪极值", "病毒传播", "流量退潮"], key="alert_type_filter") + + type_map = { + "热度飙升": "heat_surge", + "流量高潮": "flow_peak", + "情绪极值": "sentiment_extreme", + "病毒传播": "viral_spread", + "流量退潮": "flow_decline", + } + + filter_type = type_map.get(alert_type) if alert_type != "全部" else None + alerts = news_flow_alert_system.get_alert_history(days=days, alert_type=filter_type) + + with col1: + if alerts: + for alert in alerts[:20]: + level = alert.get('alert_level', 'info') + icon = {'danger': '🔴', 'warning': '🟠', 'info': '🔵'}.get(level, '⚪') + level_name = {'danger': '危险', 'warning': '警告', 'info': '提示'}.get(level, '未知') + + with st.expander(f"{icon} [{level_name}] {alert.get('title', '')[:40]}..."): + st.markdown(alert.get('content', '')) + st.caption(f"时间: {alert.get('created_at', '')} | 类型: {alert.get('alert_type', '')}") + else: + st.info("暂无预警记录") + + st.divider() + + # 预警配置 + st.markdown("#### ⚙️ 预警阈值配置") + + config = news_flow_alert_system.get_threshold_config() + + col1, col2, col3 = st.columns(3) + + with col1: + heat_th = st.number_input("热度阈值", value=int(config.get('heat_threshold', 800)), + min_value=100, max_value=1000, key="heat_th") + k_th = st.number_input("K值阈值", value=float(config.get('viral_k_threshold', 1.5)), + min_value=1.0, max_value=3.0, step=0.1, key="k_th") + + with col2: + sent_high = st.number_input("情绪高位阈值", value=int(config.get('sentiment_high_threshold', 90)), + min_value=70, max_value=100, key="sent_high") + sent_low = st.number_input("情绪低位阈值", value=int(config.get('sentiment_low_threshold', 20)), + min_value=0, max_value=30, key="sent_low") + + with col3: + rank_th = st.number_input("排名变化阈值", value=int(config.get('rank_change_threshold', 10)), + min_value=5, max_value=50, key="rank_th") + + if st.button("保存配置", key="save_alert_config"): + news_flow_alert_system.set_threshold('heat_threshold', heat_th) + news_flow_alert_system.set_threshold('viral_k_threshold', k_th) + news_flow_alert_system.set_threshold('sentiment_high_threshold', sent_high) + news_flow_alert_system.set_threshold('sentiment_low_threshold', sent_low) + news_flow_alert_system.set_threshold('rank_change_threshold', rank_th) + st.success("配置已保存") + + +def display_trend_analysis(): + """显示趋势分析""" + st.subheader("📈 趋势分析") + + try: + from news_flow_engine import news_flow_engine + from news_flow_db import news_flow_db + except Exception as e: + st.error(f"模块加载失败: {e}") + return + + # 时间范围选择 + days = st.slider("分析天数", min_value=3, max_value=30, value=7, key="trend_days") + + # 流量趋势 + trend = news_flow_engine.get_flow_trend(days=days) + + col1, col2 = st.columns([2, 1]) + + with col1: + st.markdown("#### 流量趋势图") + + if trend.get('dates'): + fig = go.Figure() + + # 平均得分 + fig.add_trace(go.Scatter( + x=trend['dates'], + y=trend['avg_scores'], + mode='lines+markers', + name='平均得分', + line=dict(color='#1f77b4', width=2), + fill='tozeroy', + fillcolor='rgba(31, 119, 180, 0.1)' + )) + + # 最高得分 + fig.add_trace(go.Scatter( + x=trend['dates'], + y=trend['max_scores'], + mode='lines', + name='最高得分', + line=dict(color='#ff7f0e', dash='dash') + )) + + # 最低得分 + fig.add_trace(go.Scatter( + x=trend['dates'], + y=trend.get('min_scores', []), + mode='lines', + name='最低得分', + line=dict(color='#2ca02c', dash='dot') + )) + + fig.update_layout( + height=350, + margin=dict(l=0, r=0, t=20, b=0), + legend=dict(orientation="h", yanchor="bottom", y=1.02), + yaxis_title="流量得分" + ) + st.plotly_chart(fig, width='stretch') + else: + st.info("暂无趋势数据") + + with col2: + st.markdown("#### 趋势分析") + st.markdown(f"**趋势方向**: {trend.get('trend', '无数据')}") + st.markdown(trend.get('analysis', '暂无分析')) + + st.divider() + + # 情绪趋势 + st.markdown("#### 情绪趋势") + + sentiments = news_flow_db.get_sentiment_history(limit=days * 3) + + if sentiments: + df = pd.DataFrame([{ + '时间': s.get('fetch_time', s.get('created_at', ''))[:16], + '情绪指数': s.get('sentiment_index', 50), + 'K值': s.get('viral_k', 1.0), + '阶段': s.get('flow_stage', '未知'), + } for s in sentiments]) + + if not df.empty: + df = df.sort_values('时间') + + fig = go.Figure() + + fig.add_trace(go.Scatter( + x=df['时间'], + y=df['情绪指数'], + mode='lines+markers', + name='情绪指数', + line=dict(color='#9467bd', width=2) + )) + + # 添加阈值线 + fig.add_hline(y=80, line_dash="dash", line_color="red", + annotation_text="乐观区", annotation_position="right") + fig.add_hline(y=20, line_dash="dash", line_color="green", + annotation_text="悲观区", annotation_position="right") + + fig.update_layout( + height=250, + margin=dict(l=0, r=0, t=20, b=0), + yaxis_title="情绪指数" + ) + st.plotly_chart(fig, width='stretch') + else: + st.info("暂无情绪数据") + + st.divider() + + # 每日统计表格 + st.markdown("#### 每日统计") + + stats = news_flow_db.get_daily_statistics(days) + + if stats: + df = pd.DataFrame([{ + '日期': s['date'], + '平均得分': s['avg_score'], + '最高得分': s['max_score'], + '最低得分': s['min_score'], + '采集次数': s['snapshot_count'], + '热门话题': ', '.join(s.get('top_topics', [])[:3]), + } for s in stats]) + + st.dataframe(df, width='stretch', hide_index=True) + else: + st.info("暂无统计数据") + + +def display_history_records(): + """显示历史记录""" + st.subheader("📚 历史记录") + + try: + from news_flow_db import news_flow_db + except Exception as e: + st.error(f"模块加载失败: {e}") + return + + # 获取历史快照 + snapshots = news_flow_db.get_history_snapshots(limit=50) + + if not snapshots: + st.info("暂无历史记录,请先执行监测") + return + + # 快照列表 + st.markdown("#### 分析报告列表") + + for snapshot in snapshots[:20]: + score = snapshot.get('total_score', 0) + level = snapshot.get('flow_level', '中') + level_icon = {'极高': '🔴', '高': '🟠', '中': '🟡', '低': '🔵'}.get(level, '⚪') + + with st.expander(f"{level_icon} {snapshot.get('fetch_time', '')} - 流量得分: {score} ({level})"): + # 获取详情 + detail = news_flow_db.get_snapshot_detail(snapshot['id']) + + if detail: + col1, col2 = st.columns(2) + + with col1: + st.markdown("**基本信息**") + st.markdown(f"- 流量得分: {score}") + st.markdown(f"- 流量等级: {level}") + st.markdown(f"- 平台数: {snapshot.get('total_platforms', 0)}") + st.markdown(f"- 成功数: {snapshot.get('success_count', 0)}") + + # 情绪信息 + sentiment = detail.get('sentiment') + if sentiment: + st.markdown("**情绪分析**") + st.markdown(f"- 情绪指数: {sentiment.get('sentiment_index', 'N/A')}") + st.markdown(f"- 情绪分类: {sentiment.get('sentiment_class', 'N/A')}") + st.markdown(f"- 流量阶段: {sentiment.get('flow_stage', 'N/A')}") + + with col2: + # AI分析 + ai = detail.get('ai_analysis') + if ai: + st.markdown("**AI分析结果**") + st.markdown(f"- 投资建议: {ai.get('advice', 'N/A')}") + st.markdown(f"- 置信度: {ai.get('confidence', 'N/A')}%") + st.markdown(f"- 风险等级: {ai.get('risk_level', 'N/A')}") + + if ai.get('summary'): + st.caption(ai['summary'][:100] + "...") + + # 热门话题 + topics = detail.get('hot_topics', [])[:5] + if topics: + st.markdown("**热门话题**") + for t in topics: + st.markdown(f"- {t['topic']} (热度:{t['heat']})") + + st.divider() + + # AI分析历史 + st.markdown("#### AI分析历史") + + ai_history = news_flow_db.get_ai_analysis_history(limit=10) + + if ai_history: + df = pd.DataFrame([{ + '时间': a.get('created_at', '')[:16], + '建议': a.get('advice', 'N/A'), + '置信度': f"{a.get('confidence', 0)}%", + '风险': a.get('risk_level', 'N/A'), + '摘要': (a.get('summary', '') or '')[:30] + "...", + } for a in ai_history]) + + st.dataframe(df, width='stretch', hide_index=True) + else: + st.info("暂无AI分析记录") + + +def display_settings(): + """显示设置""" + st.subheader("⚙️ 设置") + + # 平台配置 + st.markdown("#### 📡 支持的平台") + + try: + from news_flow_data import NewsFlowDataFetcher + fetcher = NewsFlowDataFetcher() + platforms = fetcher.get_platform_list() + + # 按类别分组 + categories = {} + for p in platforms: + cat = p['category'] + if cat not in categories: + categories[cat] = [] + categories[cat].append(p) + + cat_names = { + 'social': '社交媒体', + 'news': '新闻媒体', + 'finance': '财经平台', + 'tech': '科技媒体', + } + + cols = st.columns(len(categories)) + for i, (cat, items) in enumerate(categories.items()): + with cols[i]: + st.markdown(f"**{cat_names.get(cat, cat)}** ({len(items)}个)") + for p in items: + st.caption(f"- {p['name']} (权重:{p['weight']})") + except: + st.warning("无法加载平台列表") + + st.divider() + + # 定时任务配置 + st.markdown("#### ⏰ 定时任务管理") + + try: + from news_flow_scheduler import news_flow_scheduler + + status = news_flow_scheduler.get_status() + + col1, col2 = st.columns([1, 2]) + + with col1: + st.markdown(f"**运行状态**: {'✅ 运行中' if status.get('running') else '⏸️ 已停止'}") + + if status.get('running'): + if st.button("停止调度器", key="stop_scheduler"): + news_flow_scheduler.stop() + st.success("调度器已停止") + st.rerun() + else: + if st.button("启动调度器", key="start_scheduler"): + news_flow_scheduler.start() + st.success("调度器已启动") + st.rerun() + + with col2: + st.markdown("**任务配置**") + + intervals = status.get('task_intervals', {}) + enabled = status.get('task_enabled', {}) + + tasks = [ + ('sync_hotspots', '热点同步', 30), + ('generate_alerts', '预警生成', 60), + ('deep_analysis', '深度分析', 120), + ] + + for task_id, task_name, default_interval in tasks: + col_a, col_b, col_c = st.columns([2, 2, 1]) + + with col_a: + is_enabled = st.checkbox( + task_name, + value=enabled.get(task_id, True), + key=f"task_enabled_{task_id}" + ) + + with col_b: + interval = st.number_input( + "间隔(分钟)", + value=intervals.get(task_id, default_interval), + min_value=5, + max_value=240, + key=f"task_interval_{task_id}", + label_visibility="collapsed" + ) + + with col_c: + if st.button("立即执行", key=f"run_{task_id}"): + if task_id == 'sync_hotspots': + news_flow_scheduler.run_sync_now() + elif task_id == 'generate_alerts': + news_flow_scheduler.run_alerts_now() + else: + news_flow_scheduler.run_analysis_now() + st.success(f"{task_name} 已触发") + + if st.button("保存任务配置", key="save_task_config"): + for task_id, _, _ in tasks: + news_flow_scheduler.set_task_enabled( + task_id, + st.session_state.get(f"task_enabled_{task_id}", True) + ) + news_flow_scheduler.set_task_interval( + task_id, + st.session_state.get(f"task_interval_{task_id}", 30) + ) + st.success("任务配置已保存") + + except Exception as e: + st.warning(f"定时任务模块加载失败: {e}") + + st.divider() + + # 使用建议 + st.markdown("#### 💡 使用建议") + st.markdown(""" + 1. **盘前分析** (09:00前) + - 运行快速分析,了解当日热点 + - 关注流量阶段和情绪指数 + + 2. **盘中监测** (交易时间) + - 开启定时任务自动监测 + - 关注预警通知 + + 3. **盘后复盘** (15:00后) + - 运行完整分析(含AI) + - 分析当日流量变化趋势 + + 4. **风险提示** + - 当流量阶段进入"一致"时,立即警惕 + - 当情绪指数>85或K值>1.5时,注意风险 + - 本工具仅供参考,投资决策需谨慎 + """) + + +# 入口 +if __name__ == "__main__": + display_news_flow_monitor() diff --git a/requirements.txt b/requirements.txt index 5c7930e..c1f5dc0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,5 @@ ta>=0.10.2 reportlab>=4.0.0 peewee>=3.17.0 schedule>=1.2.0 -pywencai>=0.7.0 \ No newline at end of file +pywencai>=0.7.0 +jieba>=0.42.1 \ No newline at end of file diff --git a/新闻流量转化炒股法.md b/新闻流量转化炒股法.md new file mode 100644 index 0000000..e88d632 --- /dev/null +++ b/新闻流量转化炒股法.md @@ -0,0 +1,7 @@ +标题:流量为王:揭秘投机市场的炒作密码 + +简介:从互联网思维拆解投机炒作,揭秘流量如何驱动市场。无论你是币圈玩家、A股散户还是价值投资者,这套框架都能帮你看清游戏规则,少走弯路。 + +============================================================ + +大家好,我是脑总。今天我们聊聊Meme币和A股的热点炒作。我自己虽然不炒这些东西,但因为曾经做过金融科技和数字资产创业,就需要去理解市场、发现需求。对于市场上由来已久、用户量非常大的这类投机行为,我也有过大量思考。而且,我自己作为价值投资者和金融市场从业者,又能以一个或许更宏大的全新视角看待这个问题。我相信今天讲的内容,如果你是玩这个领域的投机者,应该也会有所收获。虽然我不建议你参与,但如果你实在要入局,今天这套框架应该能让你少走很多弯路、少交很多学费。即使你是价值投资者,这套框架也会对你理解投资标的估值变化有极大提升。今天我的理论基础有两点:第一点是所谓的互联网思维,从流量和转化率看待这些毫无价值基础的炒作;第二点是李笑来当年的一句名言:傻子的共识也是共识。只要有足够多的傻子愿意接盘,这个东西就能炒起来。有傻子接盘,聪明人自然会来做流动性套利。互联网金融时代,流量就是流动性。做过互联网的人都知道,电商有一个最基本的公式:GMV等于流量乘以转化率乘以客单价。GMV就是成交总额。这个公式拆开看很简单:你的网站来了多少人就是流量,这些人里面有多少买了东西就是转化率,平均每个人花了多少钱就是客单价。把这三个数乘起来就是你能赚多少钱。现在我告诉你一个降维打击的认知:投机市场完全可以套用这个公式。一个题材、一个Meme能炒多高,取决于接盘流动性总量,而这个总量等于流量乘以转化率,再乘以客单价。流量就是曝光量,有多少人看到了这个消息;转化率就是看到的人里面,有多少傻子会冲进去买相关标的;客单价就是散户平均一个账户能投多少钱。这三个变量决定了最终会有多少资金进场接盘。接盘资金的规模,搭配相关标的的盘子大小,基本上就能算出这个东西的价格天花板大概在哪里。听起来很玄学对不对?别急,我一个一个给你拆解。在拆解之前,你得先搞清楚一件事情:流量有两种类型。第一种叫做存量流量型,出生自带顶流,流量快速到位,不需要发酵。比如某位前国际政要发币就是这种。他本人是全球顶级流量,有几千万社交媒体粉丝,全世界的媒体都盯着他。他一发推,立刻就是十亿级别的曝光量,不需要铺垫、不需要等待,直接到底。不过这个事情在投机上也还是有时间差的:从他发布这个消息,到他的粉丝都看到,再到社交媒体用户都看到,再到各种媒体报道让全世界人民都看到,这中间有时间差,可以用来获利。深度求索概念也是这样。东大自研AI大模型技术突破,这是什么级别的新闻?主流媒体报道,所有媒体跟进,短视频平台热搜霸榜,连不炒股的人都在转发。而且这件事情还带有民族情绪的加成。在部分投资者眼里,咱们东大人搞出来的AI能够取得突破,那么这个叙事实在是太有吸引力了,即使亏钱也仿佛带有某种情怀。流量瞬间爆炸:从海外社交媒体上英语世界AI圈开始发酵,然后国内媒体跟进,再到构造一个“美股调整是被深度求索干下去的”叙事,最后全民讨论进入高潮、出货结束。存量流量型有一个特点:来得快去得也快,时间窗口比较短,你手快吃肉、手慢接盘。第二种叫做增量流量型,初始流量很小,但是有病毒传播的能力,会指数增长。柴犬币早期就是这样:一开始只是一个币圈玩家搞的“狗狗币杀手”,没有什么人知道。但是柴犬这个形象全世界都认识,而且还能蹭到狗狗币的热度,传播门槛几乎为零。买了的人会发推炫耀,没买的人看到了FOMO(害怕错过)也会冲进去,然后这些人再发推、再带来新人,一轮一轮裂变下去,从小圈子传到整个币圈,再传到圈外。几个月时间,市值从几百万美元涨到最高三百多亿美元。A股的热点概念炒作有类似的现象,但是不明显。原因很简单:市值起点太高了。市场上小市值标的,最少也是二三十亿了。对比币圈从零起步的Meme币,这最肥的一段就没有了。所以我虽然不建议你参与这个零和博弈游戏,但是如果你实在要参与,觉得自己能成为镰刀而不是韭菜,那么币圈显然是更好的地方。如果一波流量浪潮能够支撑某个标的涨到几亿美元,对币圈来说就是一个巨大的造富机会,而对于A股就基本上没有机会。增量流量型的特点是:时间窗口长,有埋伏的机会。你可以在早期流量刚爆发的时候进场,等着它反复裂变,往往有多波段,可以反复进出。你看,同样是炒作,节奏完全不一样。你得先判断你面对的是哪一种,才知道该怎么玩。讲到这里你可能会问:这套流量模型是不是只适用于币圈和A股?美股为什么不这么玩?很好的问题。美股确实很少出现币圈、A股那种纯筹码博弈的炒作。即使有题材炒作,也基本上遵循产业逻辑。原因很简单:市场结构完全不一样。首先是市场参与者的结构天壤之别。币圈、A股什么情况?散户主导。币圈玩Meme币的绝大部分是散户。A股虽然散户持股市值占比只有30%-40%,但是交易量占比高达70%以上,市场定价权在散户手里。而且这些散户里面大部分是听消息炒股、追涨杀跌的普通投资者。美股机构主导,机构持股占比70%以上,散户只有30%。这意味着什么?币圈、A股的主要玩家是散户,他们的决策依据是:社交媒体新闻、知名人物发推、交易平台广场、短视频热搜、投资社区讨论、视频号推荐……流量在哪里,资金就在哪里。美股的主要玩家是机构,他们的决策依据是:专业数据终端、公司财报、产业调研。他们不看热搜,看基本面。同样的流量事件,散户和机构的反应完全不同。因为筹码已经被机构拿在手里了,散户更加不敢去炒作那种标的,一拉起来就被机构砸盘。如果你玩过港股就知道,流动性比空气还珍贵。很多股票一天成交量才几万块钱,你一个大户或者机构想止损都止损不了,几百万都得卖大半年才能卖出来,几千万你想都不要想。这个时候要是有“车头”或者说游资,带着散户来给你提供退出的流动性,你肯定是来多少买单全砸给他们了。其次是信息流的分散性。币圈、A股的信息流是高度集中的。什么算顶级流量入口?社交媒体上面几位加密领域的顶流人物;东大的主流新闻节目、短视频热搜、热点榜单……整个市场的散户都在看这些地方。一旦某个题材上了这些热点,立刻就是亿人次的曝光,流量瞬间爆炸。美股的信息流是分散的:彭博、路透、CNBC、华尔街日报、社交媒体、Reddit……信息源太多了,不同圈层看到的信息源完全不一样。机构投资者看彭博终端,散户看CNBC,Reddit用户看华尔街赌场版块。没有一个统一的流量入口能覆盖所有人,很难形成A股那种全民性的题材炒作。即使知名人物发推提到某个公司,影响力也主要在社交媒体圈层,传不到机构投资者那里。机构该看基本面还是看基本面。第三个更致命的是做空机制的制约。币圈的Meme币在链上阶段是完全没有做空机制的。A股做空极其困难:融券标的少,融券费率高。纯粹的题材炒作很难被阻击。所以当年有“奥巴马当选,澳柯玛涨停”、“某位国际政要当选,相关股票涨停”甚至把跨境ETF当成概念股炒出50%以上溢价率的现象,就是因为缺乏做空机制。大家都靠做多获利,所以这样的炒作长期盛行。美股做空机制极其发达:任何股票都可以卖空,借券成本低,而且没有涨跌停限制。一旦出现脱离基本面的炒作,立刻就有做空资金进场套利。最典型的就是做空机构的做空报告。如果一支股票纯粹靠流量炒作,基本面跟不上,都不用做空机构出报告,各种量化套利马上就把炒作资金给收割了。所以即使美股有题材炒作,也必须有产业逻辑支撑,否则撑不住。你可能会说:美股不是也有游戏驿站、AMC这种Meme股吗?没错,这就是美股版的题材炒作。它们的底层逻辑跟A股的题材炒作是一样的,都是流量驱动。游戏驿站为什么能从几美元涨到几百美元?因为Reddit的华尔街赌场版块制造了流量。两百万散户在论坛里面互相鼓励:“我们要逼空华尔街”。这个叙事有情绪共鸣:反精英、反建制,病毒式裂变,一个人能带来好几个新人,最终形成了一波散户对基金的“圣战”。这完全符合我们前面讲的流量模型:流量乘以转化率乘以客单价。但是这种情况发生的机会窗口极其短暂,可以说需要天时地利人和。美股的表现,其实更证明了这个模型的准确性。为什么?因为流量模型的核心是流量乘以转化率乘以客单价。在不同的市场结构下,这三个变量的表现形式不同,但底层逻辑不变。A股是流量集中式爆发:主流媒体加短视频平台,散户主导,高转化率1%-2%,客单价3-5万人民币,结果就是系统性题材炒作、涨停板潮。美股是流量分散式传播:机构主导,低转化率0.1%-0.5%,机构单笔金额更大但是决策更慢,结果就是即使有流量事件,也很难形成系统性炒作。币圈是流量全球性传播:社交媒体加通讯软件,赌徒心态,超高的转化率2%-5%,客单价高度两极分化,结果就是Meme币满天飞,动辄百倍千倍。你看,同样的流量模型,在不同市场结构下,结果完全不同,但是核心逻辑不变:流量在哪里,资金就在哪里。只是流量的形式、转化率、客单价不一样。理解了这个差异之后,你就知道:在A股和币圈,流量为王。主流媒体报道、短视频热搜、知名人物推特,就是你的交易信号。在美股,主要靠产业逻辑,光有流量不够,还得有基本面支撑。游戏驿站、AMC这种纯Meme股,参与要谨慎,因为机构随时会做空阻击。如果你想用流量模型炒股,A股和币圈就是最好的战场。美股更适合做产业研究、基本面分析,流量只是锦上添花。增量流量型能不能炒起来,关键看一个指标:K值。这是互联网增长黑客里面的概念,叫做病毒系数,意思是平均每个用户能够带来几个新用户。K值大于1.5,是指数型爆发,停不下来;K值等于1左右,是线性增长,温吞水;K值小于1,是自然死亡,没救了。套到炒作市场,就是一个参与者平均能带来几个新“韭菜”。比特币的成功,老币圈在传播上对它的叙事改造、降低理解门槛,其实就占了很大一部分。什么数字黄金、长期持有、通往财务自由之路……如果没有这些东西,你去给人家讲什么UTXO、哈希、私钥、公钥,可能它依然是极客圈子里的小众玩具。买了比特币的人,会主动跟所有人讲比特币:朋友聚会讲,家庭聚餐讲,甚至跟出租车司机都要“传教”一番。这就是免费广告。那些区块链技术项目呢?你好意思跟朋友说吗?说了朋友也不感冒,传播不出去。而且比特币戳中了什么情绪?反法币通胀、反政府滥发货币、反华尔街金融霸权——“我们要用去中心化货币对抗美联储印钞机”。这个叙事有感染力,能让人产生共鸣,尤其是08年金融危机之后。你看,这三个条件一满足,K值自然就高。那怎么快速判断一个题材或者Meme的K值呢?问你自己两个问题:我会不会把这个事情转发到群里?我朋友看了会不会也想买?如果两个答案是“会”,那K值非常高,可以等着裂变。如果有一个答案“不是”,K值不够,别碰。但是K值只能判断增量流量型。存量流量型怎么办?靠“眼缘”判断。什么叫“眼缘”判断?就是你一眼就能看出来这个东西的流量天花板有多高。我给你列几个维度:首先看政策事件的级别。东大的政策是分层级的,流量天花板完全不一样。中央级事件,像雄安新区这种,主流媒体报道,所有媒体跟进,社交网络刷屏,流量天花板是全国级,十亿人次曝光。这种有机会成为一段时间的炒作主线。部委级政策,像之前海关总署限定稀土出口这种,流量天花板就是行业级,专业媒体加财经圈,大概几千万人次。这种基本上快速炒几个涨停就拉倒。地方性政策,就是某个省或者市的产业规划,流量天花板是地方级,基本上出不了圈,几百万人次。这种别碰,炒不起来。其次,看热点是全球性热点还是区域性热点。ChatGPT这种就是全球的,所有人都在讨论,而且作为一个技术起点,后续必然还会有源源不断的新技术引爆流量。所以那个时候,任何概念股的炒作都有确定性,可以等待后续一轮接一轮的流量热点。而东大即使是中央层面的特定政策,出不了国门,流量就小一个数量级。再看KOL的流量级别。币圈看KOL更明显:马斯克、某前国际政要,这种是十亿级别的全球媒体关注流量,他们发一条推,可能某个币立刻起飞。币安创始人、以太坊创始人,是覆盖千万级别的币圈用户,有影响力,但是覆盖面比马斯克小一个数量级。中腰部KOL几十万粉丝,只能做局部行情,撑不起大盘。你看,流量是分级的。顶级流量和二级流量,接盘资金可能差十倍、一百倍。我教你一个一眼判断法:看到一个题材或者币,立刻问自己:这件事情能够上主流新闻吗?能上就是全国性流量,深度求索就上了。这个事情马斯克会提吗?会提就是全球性流量,狗狗币就是。这件事情最终会传到街头大妈那里去吗?会的话就是顶流。这个事情只有圈内人知道吗?是的话就出不了圈,不要碰。好,流量说完了,咱们再说转化率。转化率就是“傻子浓度”:看到这个消息的人里面,有多少真的会冲进去买。不同市场、不同题材的“傻子浓度”是不一样的。币圈和A股哪个市场浓度更高?这个我说不准,但是题材之间还是很明显的。比如这些年A股就特别愿意为民族主义叙事买单,所以深度求索、光刻机概念就很容易炒起来。那么你就应该记住:民族主义叙事的“傻子浓度”特别高,相关题材更值得参与博弈。客单价,就是散户平均一个账户能投多少钱。A股散户平均账户大概3到5万人民币。币圈分化严重,平均下来可能也有500到1000美元。这两个数乘起来,就是单个用户的接盘金额。但是光有这三个变量还不够,你还得加两个修正系数。首先是板块的总盘子。你算出来会有10亿资金进场,最终能打到什么高度,需要看板块总盘子有多大。而且最好是盘子适中:盘子太小意味着大资金、大游资没办法深度参与;盘子太大拉不起来。盘子适中才有机会出大行情。再就是竞品分流效应。如果市场同时有几个热点在炒呢?“傻子”的注意力是稀缺资源。如果A股同时有5个题材在抢流量,每个平均分到20%。如果你的故事最性感,可能拿到30%;如果只是跟风,可能只有10%。好,现在我把完整的公式写出来:题材炒作高度等于流量规模乘以转化率乘以客单价,除以板块总盘子,乘以竞品分流折扣。听起来复杂,但实战中你不需要精确计算,只要粗略估算就行了。差一两倍,完全不影响投机。比如某前国际政要发币,最终流通市值150亿美元,你即使估错了,只估到50亿美元,能够在几亿阶段上车,也有十倍空间。这一部分的关键,是判断题材值不值得参与,过滤一切一眼看天花板就很低的噪音题材,只专注于天花板足够高的大题材。这就是第一部分:流量公式算天花板。记住一句话:投机市场就是流量生意。通过估算流量乘以转化率乘以客单价,你就知道天花板大致有多高。根据天花板判断值不值得参与只是第一步。更关键的是,你得知道什么时候跑。我告诉你一个铁律:价格高潮跟流量高潮基本上是同步发生的。为什么?因为这种炒作本质上就是庞氏、击鼓传花,后面的人给前面的人抬轿。一旦没有足够的新增流量,游戏就结束了。这里面有一个正反馈循环:价格拉升吸引眼球,制造流量。一个股票开始涨,涨30%,小圈子知道;涨一倍,行业媒体报道;涨三五倍,短视频热搜、微博热点;涨十倍,主流媒体都来关注了。流量爆发,“傻子”蜂拥,推高价格。热搜一上,散户FOMO情绪爆棚。这波人的资金体量最大,因为基数大,但是也是最后一波,进来就是山顶。流量见顶,资金断流,亏钱效应,死亡螺旋。热搜掉下来,就没有新“韭菜”了。早进来的人开始止盈,晚进来的人套在山顶,割肉出逃。价格跌破重要支撑,技术派也开始跑。最后就是负反馈循环,绵绵阴跌。流量高潮等于价格高潮,等于逃命时刻。绝大多数散户恰恰是在这个时候冲进去的,接最后一棒。关键问题来了:你又不是职业机构,没有全网流量监测系统,你怎么去判断流量是不是见顶了?专业炒家就是掏空自己去感知市场情绪的变化。我这里也可以教你一些简单粗暴的指标。A股指标:第一,看短视频热搜排名和评论增速。不光看排名,更要看评论数的环比增速。环比快速增长,意味着正在快速爆发;环比增长速度放缓,就要小心,随时准备跑路了。一旦增速下降,就要无脑出货。第二,看投资社区股吧的发帖频率。去龙头标的股吧,看每分钟新帖的数量。还有一个更直接的,看新用户占比。高潮期新“韭菜”在涌入,见顶期都是老“韭菜”互相壮胆,没有新增流量了。第三,看主流媒体这种媒体的报道。第一天报道是利好,持续深入报道是高潮,不再报道、凉了,准备跑。媒体的注意力也是有限的,他们不报道说明热度过了。第四,看龙虎榜的变化。早期著名游资席位在买,聪明钱在建仓;中期有进有出混战;晚期游资全在卖,接盘的都是散户营业部。聪明钱跑了你还不跑?币圈指标:第一,看社交媒体趋势持续时间和顶级KOL的转发时间轴。币安创始人、以太坊创始人这种顶级KOL转发就还早;中腰部KOL疯狂转发是中段;你各种群里面的群友都在问,那就该跑了。第二,看链上数据的增速拐点。看新增持币地址数的日环比增速:增速加快是好事,增速放缓危险。还有看大额转账的方向:大量从交易所提币到钱包,是囤货看涨;大量从钱包转回交易所,是准备砸盘,危险。第三,看电报、Discord群的活跃度,消息发送频率是不是在放缓。第四,看中心化交易所的挂单深度。如果买盘深度越来越浅,卖盘越来越厚,没人接盘了,那么就该跑。当然,以上这些流量监测手段,纯属我根据经验想出来的,因为我自己不炒这个。但是根据一些我自己的关键词热度数据复盘过一些炒作,发现一致性非常高,甚至比特币当年也符合这一点。所以我想这就是投机市场的共同特征。你如果想在这种九死一生的市场里面有所成就,那么该花钱买数据就去买数据,该自己下苦功夫建立流量监测模型就要下这个苦功夫。否则就不要去投机。钱哪有那么好赚?任何一门金融功夫,无论是正派还是“邪修”,你不吃一番苦是不可能有所成的。最后讲一条简单粗暴、准确率极高的经验:你要有自己的反向指标。什么叫反向指标?就是那些标志着“傻子浓度”最高的圈层已经进场的信号。一旦这个圈层关注或者进场,肯定完蛋。为什么?因为流量已经扩散到“傻子浓度”最高的圈层了,后面没有更傻的“傻子”了。这个是有历史依据的:1929年大崩盘之前,华尔街大亨约瑟夫·肯尼迪去擦鞋,擦鞋童一边擦一边跟他讲股票,给他推荐买什么。肯尼迪当场就意识到:完了,连擦鞋童都在炒股了。立刻清仓所有股票。几个月后大崩盘,无数人跳楼,肯尼迪全身而退。你要比别人更早意识到这个信号。当全世界都知道这个故事的时候,就是没有退路的时候。这就是第二部分:流量高潮看时机。记住一句话:在投机市场,流量就是你的退路。前面两部分解决了“能炒多高”和“什么时候跑”的问题。现在还剩最后一个问题:该买哪个标的?我先说一个内行看来是常识、但是外行可能会觉得颠覆性的认知:龙头不一定是因果关系最纯正的,而是辨识度最高的,或者说最容易被想起、被发现的。逻辑最硬、基本面最好的不一定是龙头,抢到眼球的才是龙头。因为这种炒作本质上就是流量生意。散户不看基本面,只看涨幅板和热搜。标的选择上面有四大选择法则。法则一:先涨为王。先涨的标的占据心智资源,后来者再正宗也没用。为什么?因为散户每天打开软件,第一件事情就是看涨幅板。谁涨得最猛,散户就点进去看。涨停板列表就是流量入口。散户只有这么多注意力,谁先抓住谁就赢了。法则二:名字最重要。互联网思维就是:每多一步操作,流失50%用户。投机市场也是一样。散户看到热搜,搜索标的,找到代码,下单买入。名字直接相关的,中间少一个环节,转化率就高一倍。而且名字有流量垄断效应。你的名字如果天然就是搜索关键词,你就赢在了起跑线上面了。币圈更夸张,Meme币的名字几乎就是一切。法则三:龙头的分化规律。龙头吃肉,龙二喝汤,龙三舔碗,后面的饿死。但不同市场的分化程度不一样。A股有一个特殊的机制:涨停板。一个题材炒起来,涨停板列表上面可能同时出现10只股票。散户一看:哇这么多涨停的,随便买一个吧。A股的龙二、龙三跟龙一的差距没有那么夸张。比如深度求索概念,可能同时有10只AI股涨停。虽然龙一涨得最多,但是龙二、龙三也能吃到肉,因为涨停板列表给了它们曝光的机会。但有个关键点:龙二、龙三能吃多少,取决于它们在涨停板列表上面能够停留多久。如果第二天就炸板了,流量立刻归零。币圈没有涨停板,链上DEX涨幅板能显示一大堆,而且是实时变动的。龙一吃99%,龙二只能捡点残渣。为什么差距这么大?因为币圈的流量高度集中,散户只盯着涨幅板第一名,第二名根本没人看。而且币圈还有一个恶性循环:龙一涨得猛,吸引更多眼球,更多人买,涨得更猛;龙二涨得慢,没人关注,没人买,彻底凉凉。马太效应在币圈被放大到极致。但是有一个例外:如果“傻子”真的太多了。如果一个题材足够火,火到“傻子”溢出来了,龙二、龙三就有机会。判断标准是:当龙一的流动性已经撑不住新增资金的时候,“傻子”就会分流到龙二。这就是溢出效应。龙一的盘子太小,或者涨得太快,“傻子”的钱没地方去,就只能往龙二、龙三里面塞。币圈也一样。如果行情足够大,不同的头部交易所也会考虑刻意去造不同的龙头,因为交易所他们自己也有流量。比如币安上了龙一,把龙一的用户流量拿到了。这个时候OKX再上龙一,他就拿不到多少用户流量了。那么他可能宁愿上龙二。这种情况下,龙一和龙二的涨幅差距就不会太大。法则四:KOL的造龙权。谁掌握流量入口,谁就能够定义什么是龙头。经典案例就是徐翔时代的宁波涨停板敢死队。他们不是等市场选出龙头来,而是自己做一个龙头出来:提前建仓,拉涨停,吸引眼球,找水军造势,把这支票包装成龙头,散户跟风抬轿子,高位出货。现在的KOL也是一样,只是换了个平台:从股吧、QQ群换成了短视频、视频号、小红书。币圈这边,马斯克发一条推,狗狗币能够涨50%,这就是流量的权力。但是更狠的是那些二线KOL。他们没有马斯克那么大的影响力,但是可以定点爆破。套路是:提前买入一些小市值的Meme币,发推说这个Meme有潜力,粉丝冲进去,币价翻倍,KOL出货。而且这些KOL也可能是互相配合的。现在短视频直播时代,这一点更加明显了。行情火热的时候,那些“头股老师”、“大V老师”直播间里面万人在线,只要荐股,可能几千人同时去下单这支股票,立刻涨停。这已经不是分析了,直接就是操纵。而且这里面可能还很难被监管。比如一个财经博主的MCN公司,能够直接影响到上万人决策,那么能够影响的资金可能是几亿甚至几十亿。它完全可以让全公司一起推某个小盘股,一推一个涨停。你如果是个小白,看到这个情况,是不是当场就激动了,然后就交了几万块钱,进了收费荐股群?人家也没有做老鼠仓,可能还有投顾牌照,你最后亏死也拿它没有办法。所以说,流量话语权就是硬通货,这就是赤裸裸的真相。所以这年头,如果你是一个一无所有的年轻人,想要赚大钱、赚快钱,知道应该去哪些产业了吧?我不是在教你学坏,但是“韭菜”是可怜之人必有可恨之处,谁割不是割,对吧?我不割是没有必要冒任何法律风险。但是如果你想刀口舔血,除了拿自己的钱去这条投机之路上修炼,不要忘了还可以用杠铃策略:做一门副业,赚流量、做KOL。这就是第三部分:标的选择就是眼球竞争。记住一句话:先涨为王,名字为王,谁抢到眼球,谁就是龙头。好,咱们把整个逻辑串一遍。第一层是流量公式算天花板:接盘总量等于流量乘以转化率乘以客单价。先判断是存量流量还是增量流量。存量型看KOL级别、政策级别,一眼判断流量天花板;增量型看K值能不能病毒传播。算出接盘资金总量,匹配盘子大小,基本上就知道大致能够炒多高。第二层是流量高潮看时机:价格高潮等于流量高潮等于逃命时刻。存量型几天决战,看热搜排名、主流媒体报道、龙虎榜,手快有手慢无;增量型等裂变,看增速拐点,第一波小高潮继续拿,第二波大高潮准备跑,第三波出圈高潮立刻跑。第三层是眼球竞争选标的:先涨为王,名字为王,KOL造龙。不要迷信逻辑最硬的,要买最容易被发现的。涨停板列表、涨幅板就是流量入口,谁先上板谁就是龙头。最后执行上面,你应该勤复盘,去找出过去几年所有炒作的历史数据,对着时间节点,按照我前面说的逻辑挨个复盘,看看持续性、看看市值空间、看看当时市场的情绪,然后把这一套内化成肌肉记忆,才有可能有所成就。我之前做过一条视频讲技术分析的,评论区很多抬杠的。最经典的一类抬杠就是说:游资难道不看技术?那些人都赚了这么多钱了,你怎么还敢说技术分析是巫术?我恰恰碰巧真认识几个游资,很幸运在很早的时候就知道了这套游资短线炒作的逻辑,也很早意识到自己并不适合这条道路,没有在这上面浪费青春。很多散户以为,游资炒短线靠的是技术分析、预测短期波动算命,那纯粹就是“以为皇上种地用金锄头,东宫娘娘烙大饼,西宫娘娘卷大葱”。无论是币圈的“喊单”,还是A股的游资,核心做的事情都是在用互联网思维对热点定价。这个东西玩多了就是个经验。很多时候消息一出来,你就知道龙头大概能干到多少市值,成交量最大能干到多大。剩下的无非就是抛弃杂毛,死都要死在龙头上,以及克服恐高心理、参与博弈的胆量。听起来很简单对不对?我今天讲的这一套:识别什么级别的消息能够触发什么级别的资金,不同市场环境下面龙头的合理市值区间,情绪周期的阶段划分——从启动到加速,到分歧到一致,最后退潮……这些东西都好学。但问题是,这些东西只占三成。学会怎么看,不等于你能够干。真正决定上限的是三种天生的能力。第一是对极端不确定性的耐受度。龙头连续涨停,账面翻倍,你敢不敢继续拿?很多人理论上懂龙头,但是仓位一上去,身体就先投降了。我怀疑这都没办法通过认知解决。你的肾上腺素、你的皮质醇分泌,可能就让你不适合做这个。第二是对错过的免疫力。高手对错过机会基本上没有情绪波动。而大部分人会在错过之后急于扳回,然后在下一个不确定性的标的上面重仓爆亏。第三是把博弈当成职业,而不是自我证明。游资是在执行概率,而不是证明自己聪明。但是很多聪明人没有办法接受“我做得对,但是亏了”,需要从交易中获得“我很牛逼”的情感回馈。一旦有了这个需求,交易就变形了:你会为了证明自己要死扛,为了挽回面子而加仓,这些都是死因。所以你看,今天讲的这套流量定价的框架,只是入门票。它能让你看懂游戏规则,但不能让你成为赢家。真正决定你能走多远的,是你的神经系统、你的情绪结构、你的自我认知模式。这些东西可能在你青少年时代就基本上定型了,后天很难改。我的建议很简单:如果你发现自己没有这三种天赋,那么投机市场迟早亏钱,我建议你还是踏踏实实投资。如果你认为自己有这三种天赋,那么我这条视频应该能让你少走好多年弯路。但是你要知道,盈亏同源。这条道路想成功,依然需要运气。如果你没有做好面对运气不好的思想准备,这条路依然不适合你。如果你毅然决然踏上这条道路,那么我祝你好运。以上就是本期视频的全部内容,我们下期再见。 \ No newline at end of file diff --git a/新闻监测API调用说明.md b/新闻监测API调用说明.md new file mode 100644 index 0000000..d95b514 --- /dev/null +++ b/新闻监测API调用说明.md @@ -0,0 +1,666 @@ +# 每日热点新闻 API - 调用说明文档 + +## 📋 目录 +- [基础信息](#基础信息) +- [快速开始](#快速开始) +- [接口说明](#接口说明) +- [支持的平台](#支持的平台) +- [调用示例](#调用示例) +- [错误处理](#错误处理) +- [注意事项](#注意事项) +- [常见问题](#常见问题) + +--- + +## 基础信息 + +### API 地址 +``` +生产环境:https://newsapi.ws4.cn/api/v1/dailynews/ +``` + +### 接口说明 +获取各大平台的热点新闻数据,包括微博热搜、百度热搜、知乎热榜等20+个平台。 + +### 特性 +- ✅ 支持多种主流平台 +- ✅ 实时更新数据 +- ✅ 无需认证即可使用 +- ✅ JSON 格式返回 +- ✅ 支持跨平台聚合查询 + +--- + +## 快速开始 + +### 最简单的调用 +```bash +curl "https://newsapi.ws4.cn/api/v1/dailynews/?platform=baidu" +``` + +### Python 快速示例 +```python +import requests + +response = requests.get("https://newsapi.ws4.cn/api/v1/dailynews/?platform=baidu") +data = response.json() + +print(f"获取到 {len(data['data'])} 条新闻") +``` + +--- + +## 接口说明 + +### 请求信息 +- **方法**: GET +- **Content-Type**: application/json +- **编码**: UTF-8 + +### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | 示例 | +|--------|------|------|------|------| +| platform | String | 是 | 平台代码,支持多个平台用逗号分隔 | `baidu` 或 `baidu,weibo,zhihu` | + +### 返回格式 + +#### 成功响应 +```json +{ + "status": "200", + "data": [ + { + "title": "新闻标题", + "url": "新闻链接", + "content": "新闻内容或描述", + "source": "平台名称", + "publish_time": "2026-01-24 11:52:52" + } + ], + "msg": "success" +} +``` + +#### 字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| status | String | 状态码,200 表示成功 | +| data | Array | 新闻数据列表 | +| data[].title | String | 新闻标题 | +| data[].url | String | 新闻链接 | +| data[].content | String | 新闻内容或描述 | +| data[].source | String | 平台来源 | +| data[].publish_time | String | 发布时间 | +| msg | String | 响应消息 | + +--- + +## 支持的平台 + +| 序号 | 平台名称 | 平台代码 | 说明 | +|------|----------|----------|------| +| 1 | 百度热搜 | `baidu` | 社会热点、娱乐、事件 | +| 2 | 少数派 | `sspai` | 科技、数码、生活方式 | +| 3 | 微博热搜 | `weibo` | 社交媒体热点 | +| 4 | 知乎热榜 | `zhihu` | 问答、深度内容 | +| 5 | 36氪 | `tskr` | 科技创业、商业资讯 | +| 6 | 吾爱破解 | `ftpojie` | 技术、软件、安全 | +| 7 | 哔哩哔哩 | `bilibili` | 视频、动漫、游戏 | +| 8 | 豆瓣 | `douban` | 书影音、文化 | +| 9 | 虎扑 | `hupu` | 体育、游戏 | +| 10 | 百度贴吧 | `tieba` | 兴趣社区 | +| 11 | 掘金 | `juejin` | 编程、技术 | +| 12 | 抖音 | `douyin` | 短视频热点 | +| 13 | V2EX | `v2ex` | 技术、编程 | +| 14 | 今日头条 | `jinritoutiao` | 新闻热点 | +| 15 | Stack Overflow | `stackoverflow` | 编程问答 | +| 16 | GitHub Trending | `github` | 开源项目 | +| 17 | Hacker News | `hackernews` | 科技新闻 | +| 18 | 新浪财经 | `sina_finance` | 财经新闻 | +| 19 | 东方财富 | `eastmoney` | 财经资讯 | +| 20 | 雪球 | `xueqiu` | 股票投资 | +| 21 | 财联社 | `cls` | 财经快讯 | +| 22 | 腾讯网 | `tenxunwang` | 综合新闻 | + +--- + +## 调用示例 + +### 1. cURL + +#### 获取单个平台 +```bash +curl "https://newsapi.ws4.cn/api/v1/dailynews/?platform=baidu" +``` + +#### 获取多个平台 +```bash +curl "https://newsapi.ws4.cn/api/v1/dailynews/?platform=baidu,weibo,zhihu" +``` + +#### 完整示例 +```bash +curl -X GET "https://newsapi.ws4.cn/api/v1/dailynews/?platform=baidu" \ + -H "Content-Type: application/json" +``` + +--- + +### 2. Python + +#### 使用 requests 库 +```python +import requests + +def get_news(platform="baidu"): + url = "https://newsapi.ws4.cn/api/v1/dailynews/" + params = {"platform": platform} + + try: + response = requests.get(url, params=params) + data = response.json() + + if data['status'] == '200': + print(f"获取到 {len(data['data'])} 条新闻") + return data['data'] + else: + print(f"请求失败: {data['msg']}") + return [] + except Exception as e: + print(f"发生错误: {e}") + return [] + +# 使用示例 +news_list = get_news("baidu") +for news in news_list[:5]: + print(f"标题: {news['title']}") + print(f"链接: {news['url']}") + print(f"时间: {news['publish_time']}\n") +``` + +#### 获取多个平台 +```python +def get_multiple_news(platforms): + platforms_str = ",".join(platforms) + url = f"https://newsapi.ws4.cn/api/v1/dailynews/?platform={platforms_str}" + + response = requests.get(url) + return response.json() + +# 使用示例 +data = get_multiple_news(["baidu", "weibo", "zhihu"]) +print(f"总共获取到 {len(data['data'])} 条新闻") +``` + +#### 异步请求(多个平台) +```python +import asyncio +import aiohttp + +async def fetch_news(session, platform): + url = "https://newsapi.ws4.cn/api/v1/dailynews/" + async with session.get(url, params={"platform": platform}) as response: + return await response.json() + +async def get_news_async(platforms): + async with aiohttp.ClientSession() as session: + tasks = [fetch_news(session, p) for p in platforms] + results = await asyncio.gather(*tasks) + return results + +# 使用示例 +import asyncio +platforms = ["baidu", "weibo", "zhihu"] +results = asyncio.run(get_news_async(platforms)) + +for i, result in enumerate(results): + print(f"{platforms[i]}: {len(result['data'])} 条新闻") +``` + +--- + +### 3. JavaScript / Node.js + +#### 使用 fetch(浏览器或现代 Node.js) +```javascript +async function getNews(platform = 'baidu') { + const url = `https://newsapi.ws4.cn/api/v1/dailynews/?platform=${platform}`; + + try { + const response = await fetch(url); + const data = await response.json(); + + if (data.status === '200') { + console.log(`获取到 ${data.data.length} 条新闻`); + return data.data; + } else { + console.log('请求失败:', data.msg); + return []; + } + } catch (error) { + console.log('发生错误:', error); + return []; + } +} + +// 使用示例 +getNews('baidu').then(newsList => { + newsList.slice(0, 5).forEach(news => { + console.log(`标题: ${news.title}`); + console.log(`链接: ${news.url}\n`); + }); +}); +``` + +#### 使用 axios +```javascript +const axios = require('axios'); + +async function getNews(platform) { + try { + const response = await axios.get('https://newsapi.ws4.cn/api/v1/dailynews/', { + params: { platform } + }); + + console.log(`获取到 ${response.data.data.length} 条新闻`); + return response.data.data; + } catch (error) { + console.error('发生错误:', error); + return []; + } +} + +// 使用示例 +getNews('baidu').then(newsList => { + newsList.forEach((news, index) => { + console.log(`${index + 1}. ${news.title}`); + }); +}); +``` + +#### Node.js 使用 https 模块 +```javascript +const https = require('https'); + +function getNews(platform, callback) { + const url = `https://newsapi.ws4.cn/api/v1/dailynews/?platform=${platform}`; + + https.get(url, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + const result = JSON.parse(data); + callback(null, result); + } catch (error) { + callback(error, null); + } + }); + }).on('error', (error) => { + callback(error, null); + }); +} + +// 使用示例 +getNews('baidu', (error, result) => { + if (error) { + console.error('请求失败:', error); + return; + } + + console.log(`获取到 ${result.data.length} 条新闻`); +}); +``` + +--- + +### 4. Java + +#### 使用 HttpURLConnection +```java +import java.io.*; +import java.net.*; +import java.util.*; + +public class NewsApiClient { + + public static String getNews(String platform) { + try { + String url = "https://newsapi.ws4.cn/api/v1/dailynews/?platform=" + platform; + URL urlObj = new URL(url); + HttpURLConnection conn = (HttpURLConnection) urlObj.openConnection(); + + conn.setRequestMethod("GET"); + conn.setRequestProperty("Content-Type", "application/json"); + + int responseCode = conn.getResponseCode(); + BufferedReader in = new BufferedReader( + new InputStreamReader(conn.getInputStream()) + ); + + String inputLine; + StringBuilder response = new StringBuilder(); + + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + + return response.toString(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static void main(String[] args) { + String jsonResponse = getNews("baidu"); + System.out.println(jsonResponse); + } +} +``` + +#### 使用 OkHttp +```java +import okhttp3.*; +import java.io.IOException; + +public class NewsApi { + + private static final OkHttpClient client = new OkHttpClient(); + + public static void getNews(String platform) throws IOException { + HttpUrl url = HttpUrl.parse("https://newsapi.ws4.cn/api/v1/dailynews/") + .newBuilder() + .addQueryParameter("platform", platform) + .build(); + + Request request = new Request.Builder() + .url(url) + .get() + .build(); + + try (Response response = client.newCall(request).execute()) { + String jsonData = response.body().string(); + System.out.println(jsonData); + } + } + + public static void main(String[] args) throws IOException { + getNews("baidu"); + } +} +``` + +--- + +### 5. Go + +```go +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +type News struct { + Title string `json:"title"` + URL string `json:"url"` + Content string `json:"content"` + Source string `json:"source"` + PublishTime string `json:"publish_time"` +} + +type Response struct { + Status string `json:"status"` + Data []News `json:"data"` + Msg string `json:"msg"` +} + +func getNews(platform string) (*Response, error) { + url := fmt.Sprintf("https://newsapi.ws4.cn/api/v1/dailynews/?platform=%s", platform) + + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var result Response + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + return &result, nil +} + +func main() { + data, err := getNews("baidu") + if err != nil { + fmt.Println("Error:", err) + return + } + + fmt.Printf("获取到 %d 条新闻\n", len(data.Data)) + for i, news := range data.Data { + if i >= 5 { + break + } + fmt.Printf("%d. %s\n", i+1, news.Title) + } +} +``` + +--- + +### 6. PHP + +```php + +``` + +--- + +### 7. Ruby + +```ruby +require 'net/http' +require 'json' +require 'uri' + +def get_news(platform = 'baidu') + uri = URI.parse("https://newsapi.ws4.cn/api/v1/dailynews/?platform=#{platform}") + response = Net::HTTP.get_response(uri) + + if response.is_a?(Net::HTTPSuccess) + data = JSON.parse(response.body) + return data + else + puts "请求失败: #{response.code}" + return nil + end +end + +# 使用示例 +result = get_news('baidu') + +if result && result['status'] == '200' + puts "获取到 #{result['data'].size} 条新闻" + + result['data'].first(5).each_with_index do |news, index| + puts "#{index + 1}. #{news['title']}" + end +end +``` + +--- + +## 错误处理 + +### 常见错误码 + +| 状态码 | 说明 | 解决方案 | +|--------|------|----------| +| 200 | 成功 | 正常处理数据 | +| 404 | 平台代码错误 | 检查平台代码是否正确 | +| 500 | 服务器内部错误 | 稍后重试 | + +### 错误处理示例(Python) + +```python +import requests +import time + +def get_news_with_retry(platform, max_retries=3): + url = "https://newsapi.ws4.cn/api/v1/dailynews/" + + for attempt in range(max_retries): + try: + response = requests.get(url, params={"platform": platform}, timeout=10) + + if response.status_code == 200: + data = response.json() + if data.get('status') == '200': + return data.get('data', []) + else: + print(f"API返回错误: {data.get('msg')}") + return [] + elif response.status_code == 404: + print(f"平台代码错误: {platform}") + return [] + elif response.status_code == 500: + print(f"服务器错误,第 {attempt + 1} 次重试...") + time.sleep(2) + else: + print(f"未知错误: {response.status_code}") + return [] + + except requests.exceptions.Timeout: + print(f"请求超时,第 {attempt + 1} 次重试...") + time.sleep(2) + except requests.exceptions.ConnectionError: + print(f"连接错误,第 {attempt + 1} 次重试...") + time.sleep(2) + except Exception as e: + print(f"发生未知错误: {e}") + return [] + + print("达到最大重试次数,请求失败") + return [] +``` + +--- + +## 注意事项 + +### 1. 数据时效性 +- 数据实时更新,建议不要频繁请求 +- 推荐缓存时间:5-10 分钟 +- 不同平台更新频率不同 + +### 2. 请求频率限制 +- 目前没有明确的速率限制 +- 建议合理使用,避免过度请求 +- 生产环境建议加入请求队列 + +### 3. 数据准确性 +- 数据仅供参考,不应作为新闻的主要来源 +- 建议与官方平台数据交叉验证 + +### 4. 合法使用 +- API 仅供合法使用 +- 任何非法使用均不受支持 +- 用户需自行承担使用责任 + +### 5. HTTPS 要求 +- 必须使用 HTTPS 协议 +- HTTP 请求可能会被拒绝 + +--- + +## 常见问题 + +### Q1: 如何获取多个平台的数据? +**A**: 使用逗号分隔多个平台代码 +```bash +curl "https://newsapi.ws4.cn/api/v1/dailynews/?platform=baidu,weibo,zhihu" +``` + +### Q2: 数据多久更新一次? +**A**: 不同平台更新频率不同,一般为 30 分钟到 2 小时不等。建议缓存数据,避免频繁请求。 + +### Q3: 是否需要 API 密钥? +**A**: 目前不需要,接口可以直接调用。 + +### Q4: 支持哪些编程语言? +**A**: 支持所有支持 HTTP 请求的编程语言,本文档提供了 Python、JavaScript、Java、Go、PHP、Ruby 等示例。 + +### Q5: 接口限流吗? +**A**: 目前没有明确的限流策略,但建议合理使用。 + +### Q6: 如何处理中文编码? +**A**: 接口返回的是 UTF-8 编码的 JSON,大多数现代语言都能自动处理。如果遇到乱码,请确保使用 UTF-8 解码。 + +### Q7: 如何获取新闻的详细内容? +**A**: API 只返回标题和链接,详细内容需要访问链接页面获取。 + +### Q8: 数据可以商用吗? +**A**: 数据来源于各公开平台,使用时请遵守各平台的使用协议和相关法律法规。 + +--- + +## 技术支持 + +- **API 文档**: https://newsapi.ws4.cn/docs +- **健康检查**: https://newsapi.ws4.cn/health + +--- + +## 更新日志 + +| 日期 | 版本 | 说明 | +|------|------|------| +| 2026-01-24 | 1.0.0 | 初始版本,支持 20+ 个平台 | + +--- + +**免责声明**: 本 API 提供的信息仅供参考,使用者应从其他平台验证信息的准确性和时效性。