Compare commits

...

13 Commits

Author SHA1 Message Date
songzhuoyuan e59399a4d6 gitea actions
Build and Push Docker Image / build-and-push (push) Failing after 12s
2026-08-11 21:42:43 +08:00
songzhuoyuan ae39742f8a tushare
Build and Push Docker Image / build-and-push (push) Failing after 11s
2026-08-11 21:37:29 +08:00
songzhuoyuan 02c135b465 tushare
Build and Push Docker Image / build-and-push (push) Waiting to run
2026-08-11 21:30:53 +08:00
songzhuoyuan befdc32aea tushare 2026-08-11 20:41:31 +08:00
oficcejo 2ce13e5bae update 2026-02-27 20:16:57 +08:00
oficcejo 9ee27de0ed update 2026-02-27 20:06:45 +08:00
oficcejo 63628ffdd5 update 2026-02-27 15:15:49 +08:00
oficcejo cfda09f3e4 update 2026-02-27 12:20:49 +08:00
oficcejo f3af15ddc8 update 2026-02-27 12:08:00 +08:00
oficcejo bce3b9645f 更新readme 2026-02-09 19:23:11 +08:00
oficcejo 6d47c5ecf4 增加视频教程 2026-02-02 11:18:42 +08:00
oficcejo e7904518d3 增加视频教程 2026-02-02 11:17:49 +08:00
oficcejo 053558169b 增加新闻流量监测板块 2026-01-25 16:56:15 +08:00
56 changed files with 4522 additions and 670 deletions
+4
View File
@@ -15,6 +15,10 @@ DEEPSEEK_API_KEY=your_actual_deepseek_api_key_here
# DeepSeek API基础URL(可选,使用默认值即可) # DeepSeek API基础URL(可选,使用默认值即可)
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
# AI模型名称(可选,支持OpenAI兼容的任意模型)
# 常用模型:deepseek-chat, deepseek-reasoner, qwen-plus, gpt-4o 等
DEFAULT_MODEL_NAME=deepseek-chat
# ========== Tushare数据接口(可选)========== # ========== Tushare数据接口(可选)==========
# Tushare Token(可选,用于获取更多金融数据) # Tushare Token(可选,用于获取更多金融数据)
+32
View File
@@ -0,0 +1,32 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
jobs:
build-and-push:
runs-on: runner
steps:
- name: Checkout repository
run: |
if [ ! -d .git ]; then
git clone --depth 1 --branch main "https://oauth2:${{ secrets.GITEA_TOKEN }}@git.javagood.top/hahaju/aiagents-stock.git" .
fi
- name: Set image tag
run: echo "IMAGE_TAG=$(date +%Y.%m.%d)" >> "$GITHUB_ENV"
- name: Login to Aliyun ACR
run: |
echo "${{ secrets.ALIYUN_REGISTRY_PASSWORD }}" | docker login registry.cn-hangzhou.aliyuncs.com \
-u "${{ secrets.ALIYUN_REGISTRY_USERNAME }}" --password-stdin
- name: Build image
run: |
docker build -t "registry.cn-hangzhou.aliyuncs.com/john_aliyun_service/aiagents-stock:${IMAGE_TAG}" .
- name: Push image
run: |
docker push "registry.cn-hangzhou.aliyuncs.com/john_aliyun_service/aiagents-stock:${IMAGE_TAG}"
+90 -21
View File
@@ -1,14 +1,82 @@
# 🤖 复合多AI智能体股票团队分析系统 # 🤖 复合多AI智能体股票团队分析系统
- 初心:在股市摸爬滚打多年,自学自编各种指标,花冤柉钱学习了各种战法各种策略,也曾入各种小班,总是赚少赔多,逐渐失去在股市玩的信心。自从去年deepseek上市,一直探索用ai辅助分析,且近日受tradingagents项目启发(感谢原作),多agent结合跟踪主力资金战法(某指每年收费6000rmb),用各种ai辅助编程,拼凑了这么个小程序,根据软件提供的辅助信息,实盘测试盈率还是挺高的,并且逐步形成了自己的交易系统,近一个月来,账户也慢慢在扰亏为盈。开源此软件的目的,就是为了使像我一样的小散,不再迷范。也许这个软件不能让你发大财,但是他能给你足够的信心。最后提醒:股市有风险,入市需谨慎! - 初心:在股市摸爬滚打多年,自学自编各种指标,花冤柉钱学习了各种战法各种策略,也曾入各种小班,总是赚少赔多,逐渐失去在股市玩的信心。自从去年deepseek上市,一直探索用ai辅助分析,且近日受tradingagents项目启发(感谢原作),多agent结合跟踪主力资金战法(某指每年收费6000rmb),用各种ai辅助编程,拼凑了这么个小程序,根据软件提供的辅助信息,实盘测试盈率还是挺高的,并且逐步形成了自己的交易系统,近一个月来,账户也慢慢在扰亏为盈。开源此软件的目的,就是为了使像我一样的小散,不再迷范。也许这个软件不能让你发大财,但是他能给你足够的信心。最后提醒:股市有风险,入市需谨慎!
## QQ交流群:1059277514
## B站本地部署教程1https://www.bilibili.com/video/BV1qHFPz9EXY/
## docker部署教程2https://www.bilibili.com/video/BV1j2FNz4EAi/
## 股票知识讲解合集:https://www.bilibili.com/video/BV1Y2FGzzEeS/
## 投资认知提升合集:https://www.bilibili.com/video/BV1ugBMBAEbW
## 价值投资核心逻辑:https://www.bilibili.com/video/BV1eJfxBrEjZ
## ETH币圈超短线项目:https://github.com/oficcejo/okx-short
## ⭐20261.25第一更 - 新闻流量监测 📈 如果你希望能在股市中长久生存下去,建议你能把上面的合集看完,会对你有很大帮助的!
另广告一下,喜欢玩币的可以看看这个项目:https://github.com/oficcejo/okx-short
实时监测百度、微博、东财、财联社、抖音、B站等20个平台热点新闻,调用ai分析对A股板块及股票的影响,生成分析报告。也可使用https://stock-news.ws4.cn网站监测 ## ⭐ 2026.2.27更新 - 低估值价值投资策略 💎
**新增选股板块:基于价值投资核心逻辑的优选策略**
基于视频[《头号投资法则》](https://www.bilibili.com/video/BV1eJfxBrEjZ),通过低估值、高股息、低负债等多维度指标筛选安全边际极高的优质标的。
**核心功能:**
- 筛选条件:**低PE (≤20) + 低PB (≤1.5) + 高股息 (≥1%) + 低负债 (≤30%)**
- 排序机制:按流通市值从小到大排序,精准捕捉被错杀的小盘价值股
- 量化择时:
- **买入**:每日扫描,开盘买入,单股限仓30%,最大持股4只。
- **卖出**:持股满30天到期卖出,或 **RSI(14) > 70** 超买信号触发卖出。
- 自动化工具:支持一键模拟买入、实时指标监测及 PDF/Markdown 报告导出。
---
## ⭐ 2026.2.27更新 - 宏观周期分析 🧭
**全新板块:康波周期 × 美林投资时钟 × 中国政策分析**
基于视频[康波周期理论](https://www.bilibili.com/video/BV1QNcEzREzY)和视频[美林投资时钟](https://www.bilibili.com/video/BV1Zuf5BUEhH),由4位AI分析师协同研判当前宏观经济所处的周期位置!
**核心功能:**
- 🌊 **康波周期分析** — 判断当前处于50-60年长周期的哪个阶段(回升/繁荣/衰退/萧条)
-**美林投资时钟** — 判断当前处于3-5年中短周期的哪个象限(复苏/过热/滞胀/衰退)
- 🏛️ **中国政策分析** — 货币/财政/产业/房地产政策全面解读(中国化第三维度)
- 👔 **首席宏观策略师** — 三维综合研判,"周期仪表盘"资产配置建议
- 📊 **自动采集宏观数据** — GDP、CPI/PPI、PMI、M2、LPR利率、大宗商品等
**适用场景:**
- 战略性资产配置决策(买房/创业/大额投资)
- 中短期投资组合调整
- 理解当前经济环境,避免"在高点接盘、在低点割肉"
---
## ⭐ 2026.2.27更新 - AI模型自由切换 🤖
**重大改进:AI模型全局可配置化**
将所有硬编码的模型名称统一改为从 `.env` 文件动态读取,一键切换任意 OpenAI 兼容大模型,无需修改代码!
**核心变化:**
-**新增 `DEFAULT_MODEL_NAME`** — 在 `.env` 中配置默认 AI 模型
-**移除所有模型选择下拉框** — 龙虎榜、主力选股、智策板块等页面不再需要手动选模型
-**环境配置 UI 新增模型输入** — 在「环境配置」中可直接输入模型名称,附常用模型参考
-**支持任意 OpenAI 兼容模型** — DeepSeek、通义千问、GPT-4o 等一键切换
**切换模型只需一步:**
```env
# .env 文件
DEFAULT_MODEL_NAME="qwen-plus" # 或 deepseek-chat, gpt-4o 等
```
> 💡 修改后重启应用即可生效,侧边栏会显示当前使用的模型名称。
---
## ⭐ 2026.1.25第一更 - 新闻流量监测 📈
实时监测百度、微博、东财、财联社、抖音、B站等20个平台热点新闻,调用ai分析对A股板块及股票的影响,生成分析报告。也可使用https://stock-news.ws4.cn
## ⭐ 1214更新 - 净利增长策略 📈 ## ⭐ 1214更新 - 净利增长策略 📈
新增“净利增长策略”选股板块,专注稳健成长股票: 新增“净利增长策略”选股板块,专注稳健成长股票:回测年化100%+
**核心功能:** **核心功能:**
-**稳健成长** - 净利润增长率≥10%,稳健可持续 -**稳健成长** - 净利润增长率≥10%,稳健可持续
@@ -375,7 +443,7 @@ StockAPI龙虎榜接口(每日更新,免费1000次)
- 灵活设置运行时间(盘前/盘后/晚间) - 灵活设置运行时间(盘前/盘后/晚间)
- 手动触发和状态监控 - 手动触发和状态监控
- **数据来源**:AKShare免费数据(行情、资金、新闻) - **数据来源**:AKShare免费数据(行情、资金、新闻)
- **AI模型**DeepSeek Chat / Reasoner双模型 - **AI模型**支持任意 OpenAI 兼容模型(通过 `.env` 配置切换)
- **报告导出**:支持PDF格式完整报告 - **报告导出**:支持PDF格式完整报告
- **使用场景**:盘前策略、板块轮动、风险规避 - **使用场景**:盘前策略、板块轮动、风险规避
@@ -573,6 +641,11 @@ cp .env.example .env
```env ```env
# DeepSeek API配置(必需) # DeepSeek API配置(必需)
DEEPSEEK_API_KEY=your_actual_deepseek_api_key_here DEEPSEEK_API_KEY=your_actual_deepseek_api_key_here
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
# AI模型名称(可选,支持OpenAI兼容模型)
# 常用:deepseek-chat, deepseek-reasoner, qwen-plus, gpt-4o
DEFAULT_MODEL_NAME=deepseek-chat
# Tushare配置(可选)- 作为降级数据源 # Tushare配置(可选)- 作为降级数据源
TUSHARE_TOKEN=your_tushare_token # 在 https://tushare.pro 注册获取 TUSHARE_TOKEN=your_tushare_token # 在 https://tushare.pro 注册获取
@@ -585,7 +658,7 @@ EMAIL_FROM=your_email@qq.com
EMAIL_PASSWORD=your_authorization_code EMAIL_PASSWORD=your_authorization_code
EMAIL_TO=receiver@example.com EMAIL_TO=receiver@example.com
# Webhook通知配置(可选)⭐️ 新增 - 用于实时监测和智策定时分析 # Webhook通知配置(可选)- 用于实时监测和智策定时分析
WEBHOOK_ENABLED=false WEBHOOK_ENABLED=false
WEBHOOK_TYPE=dingtalk # 或 feishu WEBHOOK_TYPE=dingtalk # 或 feishu
WEBHOOK_URL=your_webhook_url_here WEBHOOK_URL=your_webhook_url_here
@@ -701,9 +774,7 @@ streamlit run app.py
2. **设置分析参数** 2. **设置分析参数**
- **分析模式**:选择"指定日期"或"最近N天" - **分析模式**:选择"指定日期"或"最近N天"
- **日期选择**:建议选择昨天的日期(龙虎榜数据T日更新) - **日期选择**:建议选择昨天的日期(龙虎榜数据T日更新)
- **AI模型** - **AI模型**自动使用 `.env` 中配置的默认模型
- **deepseek-chat**:速度快,8-12分钟完成
- **deepseek-reasoner**:推理深入,12-18分钟完成
3. **开始分析** 3. **开始分析**
- 点击"🚀 开始分析"按钮 - 点击"🚀 开始分析"按钮
@@ -777,12 +848,9 @@ streamlit run app.py
### 智策板块分析使用流程 ⭐️ 全新功能 ### 智策板块分析使用流程 ⭐️ 全新功能
1. **进入智策板块** 1. **进入智策板块**
- 点击侧边栏"🎯 智策板块"按钮 - 点击侧边栏"🎯 智策板块"按钮
- AI模型自动使用 `.env` 中配置的默认模型
2. **选择AI模型** 2. **开始分析**
- **deepseek-chat**:标准模型,速度快(2-3分钟)
- **deepseek-reasoner**:推理增强模型,分析深度更高(3-5分钟)
3. **开始分析**
- 点击"🚀 开始智策分析"按钮 - 点击"🚀 开始智策分析"按钮
- 系统自动执行: - 系统自动执行:
- 获取市场数据(30秒) - 获取市场数据(30秒)
@@ -790,7 +858,7 @@ streamlit run app.py
- 综合研判(30秒) - 综合研判(30秒)
- 生成预测报告 - 生成预测报告
4. **查看分析结果** 3. **查看分析结果**
分析完成后,查看四个标签页: 分析完成后,查看四个标签页:
- **📋 核心预测** - **📋 核心预测**
@@ -813,12 +881,12 @@ streamlit run app.py
- 板块多空信心度对比图 - 板块多空信心度对比图
- 板块热度分布图 - 板块热度分布图
5. **导出PDF报告** 4. **导出PDF报告**
- 点击"📥 生成PDF报告"按钮 - 点击"📥 生成PDF报告"按钮
- 等待生成(3-5秒) - 等待生成(3-5秒)
- 点击"💾 下载PDF"保存 - 点击"💾 下载PDF"保存
6. **配置定时分析** ⭐️ 5. **配置定时分析** ⭐️
展开"⏰ 定时分析设置" 展开"⏰ 定时分析设置"
**步骤1:配置邮件/Webhook** **步骤1:配置邮件/Webhook**
@@ -845,14 +913,14 @@ streamlit run app.py
- 查看运行状态和上次运行时间 - 查看运行状态和上次运行时间
- 需要时可点击"⏹️ 停止定时任务" - 需要时可点击"⏹️ 停止定时任务"
7. **使用技巧** 6. **使用技巧**
- **最佳使用时间**:盘前8:30-9:30或盘后15:30-20:00 - **最佳使用时间**:盘前8:30-9:30或盘后15:30-20:00
- **关注信心度**:≥8分为高信心,可重点关注 - **关注信心度**:≥8分为高信心,可重点关注
- **结合轮动**:重点关注"潜力接力板块",最佳布局时机 - **结合轮动**:重点关注"潜力接力板块",最佳布局时机
- **热度判断**:升温板块有机会,降温板块注意风险 - **热度判断**:升温板块有机会,降温板块注意风险
- **四维共振**:宏观、基本面、资金、情绪共振时信号更强 - **四维共振**:宏观、基本面、资金、情绪共振时信号更强
8. **通知内容示例** 7. **通知内容示例**
**邮件/Webhook推送内容** **邮件/Webhook推送内容**
``` ```
@@ -1196,7 +1264,7 @@ AI股票分析系统
**longhubang_ui.py** **longhubang_ui.py**
- 智瞰龙虎主界面 - 智瞰龙虎主界面
- 分析参数设置(日期/模型) - 分析参数设置
- 实时进度显示 - 实时进度显示
- 四个结果标签页(推荐/报告/数据/图表) - 四个结果标签页(推荐/报告/数据/图表)
- 历史报告查询 - 历史报告查询
@@ -1241,7 +1309,6 @@ AI股票分析系统
**sector_strategy_ui.py** **sector_strategy_ui.py**
- 智策板块主界面 - 智策板块主界面
- AI模型选择
- 分析进度显示 - 分析进度显示
- 四个结果标签页展示 - 四个结果标签页展示
- 定时分析设置面板 - 定时分析设置面板
@@ -1315,7 +1382,8 @@ AI股票分析系统
- **降级机制**TDX → Tushare → AKShare 多层数据源保障 - **降级机制**TDX → Tushare → AKShare 多层数据源保障
### AI模型 ### AI模型
- **语言模型**DeepSeek Chat API - **语言模型**支持任意 OpenAI 兼容模型(DeepSeek、通义千问、GPT-4o 等)
- **模型配置**:通过 `.env` 文件中的 `DEFAULT_MODEL_NAME` 一键切换
- **分析框架**:多智能体协作 - **分析框架**:多智能体协作
- **决策逻辑**:综合评分机制 - **决策逻辑**:综合评分机制
@@ -1336,6 +1404,7 @@ AI股票分析系统
# .env 文件 # .env 文件
DEEPSEEK_API_KEY=your_api_key DEEPSEEK_API_KEY=your_api_key
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
DEFAULT_MODEL_NAME=deepseek-chat # 支持任意OpenAI兼容模型
``` ```
**重要提示** **重要提示**
+29 -34
View File
@@ -1,18 +1,19 @@
from deepseek_client import DeepSeekClient from deepseek_client import DeepSeekClient
from typing import Dict, Any from typing import Dict, Any
import concurrent.futures
import time import time
import config
class StockAnalysisAgents: class StockAnalysisAgents:
"""股票分析AI智能体集合""" """股票分析AI智能体集合"""
def __init__(self, model="deepseek-chat"): def __init__(self, model=None):
self.model = model self.model = model or config.DEFAULT_MODEL_NAME
self.deepseek_client = DeepSeekClient(model=model) self.deepseek_client = DeepSeekClient(model=self.model)
def technical_analyst_agent(self, stock_info: Dict, stock_data: Any, indicators: Dict) -> Dict[str, Any]: def technical_analyst_agent(self, stock_info: Dict, stock_data: Any, indicators: Dict) -> Dict[str, Any]:
"""技术面分析智能体""" """技术面分析智能体"""
print("🔍 技术分析师正在分析中...") print("🔍 技术分析师正在分析中...")
time.sleep(1) # 模拟分析时间
analysis = self.deepseek_client.technical_analysis(stock_info, stock_data, indicators) analysis = self.deepseek_client.technical_analysis(stock_info, stock_data, indicators)
@@ -37,8 +38,6 @@ class StockAnalysisAgents:
else: else:
print(" ⚠ 未获取到季报数据,将基于基本财务数据分析") print(" ⚠ 未获取到季报数据,将基于基本财务数据分析")
time.sleep(1)
analysis = self.deepseek_client.fundamental_analysis(stock_info, financial_data, quarterly_data) analysis = self.deepseek_client.fundamental_analysis(stock_info, financial_data, quarterly_data)
return { return {
@@ -60,8 +59,6 @@ class StockAnalysisAgents:
else: else:
print(" ⚠ 未获取到资金流向数据,将基于技术指标分析") print(" ⚠ 未获取到资金流向数据,将基于技术指标分析")
time.sleep(1)
analysis = self.deepseek_client.fund_flow_analysis(stock_info, indicators, fund_flow_data) analysis = self.deepseek_client.fund_flow_analysis(stock_info, indicators, fund_flow_data)
return { return {
@@ -83,8 +80,6 @@ class StockAnalysisAgents:
else: else:
print(" ⚠ 未获取到风险数据,将基于基本信息分析") print(" ⚠ 未获取到风险数据,将基于基本信息分析")
time.sleep(1)
# 构建风险数据文本 # 构建风险数据文本
risk_data_text = "" risk_data_text = ""
if risk_data and risk_data.get('data_success'): if risk_data and risk_data.get('data_success'):
@@ -226,8 +221,6 @@ class StockAnalysisAgents:
else: else:
print(" ⚠ 未获取到详细情绪数据,将基于基本信息分析") print(" ⚠ 未获取到详细情绪数据,将基于基本信息分析")
time.sleep(1)
# 构建带有市场情绪数据的prompt # 构建带有市场情绪数据的prompt
sentiment_data_text = "" sentiment_data_text = ""
if sentiment_data and sentiment_data.get('data_success'): if sentiment_data and sentiment_data.get('data_success'):
@@ -316,8 +309,6 @@ class StockAnalysisAgents:
else: else:
print(" ⚠ 未获取到新闻数据,将基于基本信息分析") print(" ⚠ 未获取到新闻数据,将基于基本信息分析")
time.sleep(1)
# 构建带有新闻数据的prompt # 构建带有新闻数据的prompt
news_text = "" news_text = ""
if news_data and news_data.get('data_success'): if news_data and news_data.get('data_success'):
@@ -435,32 +426,38 @@ class StockAnalysisAgents:
print(f"📋 参与分析的分析师: {', '.join(active_analysts)}") print(f"📋 参与分析的分析师: {', '.join(active_analysts)}")
print("=" * 50) print("=" * 50)
# 并行运行各个分析师 # 并行运行各个分析师(全部同时启动,等待最慢的一个完成)
agents_results = {} agents_results = {}
# 技术面分析 agent_jobs = []
if enabled_analysts.get('technical', True): if enabled_analysts.get('technical', True):
agents_results["technical"] = self.technical_analyst_agent(stock_info, stock_data, indicators) agent_jobs.append(("technical", lambda: self.technical_analyst_agent(stock_info, stock_data, indicators)))
# 基本面分析
if enabled_analysts.get('fundamental', True): if enabled_analysts.get('fundamental', True):
agents_results["fundamental"] = self.fundamental_analyst_agent(stock_info, financial_data, quarterly_data) agent_jobs.append(("fundamental", lambda: self.fundamental_analyst_agent(stock_info, financial_data, quarterly_data)))
# 资金面分析(传入资金流向数据)
if enabled_analysts.get('fund_flow', True): if enabled_analysts.get('fund_flow', True):
agents_results["fund_flow"] = self.fund_flow_analyst_agent(stock_info, indicators, fund_flow_data) agent_jobs.append(("fund_flow", lambda: self.fund_flow_analyst_agent(stock_info, indicators, fund_flow_data)))
# 风险管理分析(传入风险数据)
if enabled_analysts.get('risk', True): if enabled_analysts.get('risk', True):
agents_results["risk_management"] = self.risk_management_agent(stock_info, indicators, risk_data) agent_jobs.append(("risk_management", lambda: self.risk_management_agent(stock_info, indicators, risk_data)))
# 市场情绪分析(传入市场情绪数据)
if enabled_analysts.get('sentiment', False): if enabled_analysts.get('sentiment', False):
agents_results["market_sentiment"] = self.market_sentiment_agent(stock_info, sentiment_data) agent_jobs.append(("market_sentiment", lambda: self.market_sentiment_agent(stock_info, sentiment_data)))
# 新闻分析(传入新闻数据)
if enabled_analysts.get('news', False): if enabled_analysts.get('news', False):
agents_results["news"] = self.news_analyst_agent(stock_info, news_data) agent_jobs.append(("news", lambda: self.news_analyst_agent(stock_info, news_data)))
with concurrent.futures.ThreadPoolExecutor(max_workers=max(min(len(agent_jobs), 6), 1)) as executor:
future_to_key = {executor.submit(job): key for key, job in agent_jobs}
for future in concurrent.futures.as_completed(future_to_key):
key = future_to_key[future]
try:
agents_results[key] = future.result()
except Exception as e:
print(f"{key} 分析师并行分析失败: {e}")
agents_results[key] = {
"agent_name": key,
"agent_role": "",
"analysis": f"分析失败: {e}",
"focus_areas": [],
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
}
print("✅ 所有已选择的分析师完成分析") print("✅ 所有已选择的分析师完成分析")
print("=" * 50) print("=" * 50)
@@ -470,7 +467,6 @@ class StockAnalysisAgents:
def conduct_team_discussion(self, agents_results: Dict[str, Any], stock_info: Dict) -> str: def conduct_team_discussion(self, agents_results: Dict[str, Any], stock_info: Dict) -> str:
"""进行团队讨论""" """进行团队讨论"""
print("🤝 分析团队正在进行综合讨论...") print("🤝 分析团队正在进行综合讨论...")
time.sleep(2)
# 收集参与分析的分析师名单和报告 # 收集参与分析的分析师名单和报告
participants = [] participants = []
@@ -537,7 +533,6 @@ class StockAnalysisAgents:
def make_final_decision(self, discussion_result: str, stock_info: Dict, indicators: Dict) -> Dict[str, Any]: def make_final_decision(self, discussion_result: str, stock_info: Dict, indicators: Dict) -> Dict[str, Any]:
"""制定最终投资决策""" """制定最终投资决策"""
print("📋 正在制定最终投资决策...") print("📋 正在制定最终投资决策...")
time.sleep(1)
decision = self.deepseek_client.final_decision(discussion_result, stock_info, indicators) decision = self.deepseek_client.final_decision(discussion_result, stock_info, indicators)
+97 -28
View File
@@ -7,8 +7,11 @@ from datetime import datetime
import time import time
import base64 import base64
import os import os
# 从新的配置文件导入model_options import config
from model_config import model_options
# 注入所有外部请求(akshare/tushare等)的默认超时
from http_timeout import install_default_requests_timeout
install_default_requests_timeout()
from stock_data import StockDataFetcher from stock_data import StockDataFetcher
from ai_agents import StockAnalysisAgents from ai_agents import StockAnalysisAgents
@@ -32,22 +35,13 @@ st.set_page_config(
initial_sidebar_state="expanded" initial_sidebar_state="expanded"
) )
# 模型选择器 # 在侧边栏显示当前模型信息(统一使用.env配置)
def model_selector(): def show_current_model_info():
"""模型选择器""" """显示当前使用的AI模型信息"""
st.sidebar.markdown("---") st.sidebar.markdown("---")
st.sidebar.subheader("🤖 AI模型选择") st.sidebar.subheader("🤖 AI模型")
st.sidebar.info(f"当前模型: **{config.DEFAULT_MODEL_NAME}**")
st.sidebar.caption("可在「环境配置」中修改模型名称")
selected_model = st.sidebar.selectbox(
"选择AI模型",
options=list(model_options.keys()),
format_func=lambda x: model_options[x],
help="DeepSeek Reasoner提供更强的推理能力,但响应时间可能更长"
)
return selected_model
# 自定义CSS样式 - 专业版 # 自定义CSS样式 - 专业版
st.markdown(""" st.markdown("""
@@ -286,6 +280,9 @@ def main():
</div> </div>
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
# 学习资源展示
st.info("📺 **新手必看干货**:为了在股市长久生存,建议您观看 👉 [股票知识讲解合集](https://www.bilibili.com/video/BV1Y2FGzzEeS/) 和 [投资认知提升合集](https://www.bilibili.com/video/BV1ugBMBAEbW) 👈,相信会对您有很大帮助!")
# 侧边栏 # 侧边栏
with st.sidebar: with st.sidebar:
# 快捷导航 - 移到顶部 # 快捷导航 - 移到顶部
@@ -295,7 +292,7 @@ def main():
if st.button("🏠 股票分析", width='stretch', key="nav_home", help="返回首页,进行单只股票的深度分析"): if st.button("🏠 股票分析", width='stretch', key="nav_home", help="返回首页,进行单只股票的深度分析"):
# 清除所有功能页面标志 # 清除所有功能页面标志
for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
'show_sector_strategy', 'show_longhubang', 'show_portfolio', 'show_low_price_bull', 'show_news_flow']: 'show_sector_strategy', 'show_longhubang', 'show_portfolio', 'show_low_price_bull', 'show_news_flow', 'show_macro_cycle', 'show_value_stock']:
if key in st.session_state: if key in st.session_state:
del st.session_state[key] del st.session_state[key]
@@ -329,7 +326,14 @@ def main():
if st.button("📈 净利增长", width='stretch', key="nav_profit_growth", help="净利润增长稳健股票筛选策略"): if st.button("📈 净利增长", width='stretch', key="nav_profit_growth", help="净利润增长稳健股票筛选策略"):
st.session_state.show_profit_growth = True st.session_state.show_profit_growth = True
for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy', 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_news_flow']: 'show_longhubang', 'show_portfolio', 'show_main_force', 'show_low_price_bull', 'show_small_cap', 'show_news_flow', 'show_value_stock']:
if key in st.session_state:
del st.session_state[key]
if st.button("💎 低估值策略", width='stretch', key="nav_value_stock", help="低PE+低PB+高股息+低负债 价值投资筛选"):
st.session_state.show_value_stock = 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_profit_growth', 'show_news_flow', 'show_macro_cycle']:
if key in st.session_state: if key in st.session_state:
del st.session_state[key] del st.session_state[key]
@@ -354,7 +358,14 @@ def main():
if st.button("📰 新闻流量", width='stretch', key="nav_news_flow", help="新闻流量监测与短线指导"): if st.button("📰 新闻流量", width='stretch', key="nav_news_flow", help="新闻流量监测与短线指导"):
st.session_state.show_news_flow = True st.session_state.show_news_flow = True
for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', 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']: 'show_sector_strategy', 'show_portfolio', 'show_smart_monitor', 'show_low_price_bull', 'show_longhubang', 'show_macro_cycle']:
if key in st.session_state:
del st.session_state[key]
if st.button("🧭 宏观周期", width='stretch', key="nav_macro_cycle", help="康波周期 × 美林投资时钟 × 政策分析"):
st.session_state.show_macro_cycle = 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', 'show_news_flow']:
if key in st.session_state: if key in st.session_state:
del st.session_state[key] del st.session_state[key]
@@ -416,9 +427,9 @@ def main():
st.markdown("---") st.markdown("---")
# 模型选择器 # 显示当前模型信息
selected_model = model_selector() show_current_model_info()
st.session_state.selected_model = selected_model st.session_state.selected_model = config.DEFAULT_MODEL_NAME
st.markdown("---") st.markdown("---")
@@ -472,7 +483,18 @@ def main():
1. 数据获取 → 2. 技术分析 1. 数据获取 → 2. 技术分析
3. 基本面分析 → 4. 资金分析 3. 基本面分析 → 4. 资金分析
5. 情绪数据(ARBR) → 6. 新闻(qstock) 5. 情绪数据(ARBR) → 6. 新闻(qstock)
7. AI团队分析 → 8. 团队讨论 → 9. 决策 7. AI分析 → 8. 团队讨论 → 9. 决策
""")
# 学习资源
with st.expander("📺 学习视频合集"):
st.markdown("""
**📢 B站干货合集**
如果你希望能在股市中长久生存下去,建议你能把下面的合集看完,会对你有很大帮助的!
- 📚 [股票知识讲解合集](https://www.bilibili.com/video/BV1Y2FGzzEeS/)
- 🧠 [投资认知提升合集](https://www.bilibili.com/video/BV1ugBMBAEbW)
""") """)
# 检查是否显示历史记录 # 检查是否显示历史记录
@@ -508,6 +530,12 @@ def main():
display_profit_growth() display_profit_growth()
return return
# 检查是否显示低估值策略
if 'show_value_stock' in st.session_state and st.session_state.show_value_stock:
from value_stock_ui import display_value_stock
display_value_stock()
return
# 检查是否显示智策板块 # 检查是否显示智策板块
if 'show_sector_strategy' in st.session_state and st.session_state.show_sector_strategy: if 'show_sector_strategy' in st.session_state and st.session_state.show_sector_strategy:
display_sector_strategy() display_sector_strategy()
@@ -534,6 +562,12 @@ def main():
display_news_flow_monitor() display_news_flow_monitor()
return return
# 检查是否显示宏观周期分析
if 'show_macro_cycle' in st.session_state and st.session_state.show_macro_cycle:
from macro_cycle_ui import display_macro_cycle
display_macro_cycle()
return
# 检查是否显示环境配置 # 检查是否显示环境配置
if 'show_config' in st.session_state and st.session_state.show_config: if 'show_config' in st.session_state and st.session_state.show_config:
display_config_manager() display_config_manager()
@@ -825,18 +859,22 @@ def parse_stock_list(stock_input):
return unique_list return unique_list
def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None, selected_model='deepseek-chat'): def analyze_single_stock_for_batch(symbol, period, enabled_analysts_config=None, selected_model=None):
"""单个股票分析(用于批量分析) """单个股票分析(用于批量分析)
Args: Args:
symbol: 股票代码 symbol: 股票代码
period: 数据周期 period: 数据周期
enabled_analysts_config: 分析师配置字典 enabled_analysts_config: 分析师配置字典
selected_model: 选择的AI模型 selected_model: 选择的AI模型,默认从 .env 的 DEFAULT_MODEL_NAME 读取
返回分析结果或错误信息 返回分析结果或错误信息
""" """
try: try:
# 使用默认模型
if selected_model is None:
selected_model = config.DEFAULT_MODEL_NAME
# 使用默认配置 # 使用默认配置
if enabled_analysts_config is None: if enabled_analysts_config is None:
enabled_analysts_config = { enabled_analysts_config = {
@@ -983,7 +1021,7 @@ def run_batch_analysis(stock_list, period, batch_mode="顺序分析"):
'sentiment': st.session_state.get('enable_sentiment', False), 'sentiment': st.session_state.get('enable_sentiment', False),
'news': st.session_state.get('enable_news', False) 'news': st.session_state.get('enable_news', False)
} }
selected_model = st.session_state.get('selected_model', 'deepseek-chat') selected_model = st.session_state.get('selected_model', config.DEFAULT_MODEL_NAME)
# 创建进度显示 # 创建进度显示
st.subheader(f"📊 批量分析进行中 ({batch_mode})") st.subheader(f"📊 批量分析进行中 ({batch_mode})")
@@ -1256,7 +1294,7 @@ def run_stock_analysis(symbol, period):
# 6. 初始化AI分析系统 # 6. 初始化AI分析系统
status_text.text("🤖 正在初始化AI分析系统...") status_text.text("🤖 正在初始化AI分析系统...")
# 使用选择的模型 # 使用选择的模型
selected_model = st.session_state.get('selected_model', 'deepseek-chat') selected_model = st.session_state.get('selected_model', config.DEFAULT_MODEL_NAME)
agents = StockAnalysisAgents(model=selected_model) agents = StockAnalysisAgents(model=selected_model)
progress_bar.progress(55) progress_bar.progress(55)
@@ -2154,6 +2192,37 @@ def display_config_manager():
) )
st.session_state.temp_config["DEEPSEEK_BASE_URL"] = new_base_url st.session_state.temp_config["DEEPSEEK_BASE_URL"] = new_base_url
st.markdown("---")
# AI模型名称
model_name_info = config_info["DEFAULT_MODEL_NAME"]
current_model_name = st.session_state.temp_config.get("DEFAULT_MODEL_NAME", "deepseek-chat")
new_model_name = st.text_input(
f"🤖 {model_name_info['description']}",
value=current_model_name,
help="输入OpenAI兼容的模型名称,修改后重启生效",
key="input_default_model_name"
)
st.session_state.temp_config["DEFAULT_MODEL_NAME"] = new_model_name
if new_model_name:
st.success(f"✅ 当前模型: **{new_model_name}**")
else:
st.warning("⚠️ 未设置模型名称,将使用默认值 deepseek-chat")
st.markdown("""
**常用模型名称参考:**
- `deepseek-chat` — DeepSeek Chat(默认)
- `deepseek-reasoner` — DeepSeek Reasoner(推理增强)
- `qwen-plus` — 通义千问 Plus
- `qwen-turbo` — 通义千问 Turbo
- `gpt-4o` — OpenAI GPT-4o
- `gpt-4o-mini` — OpenAI GPT-4o Mini
> 💡 使用非 DeepSeek 模型时,请同时修改上方的 API地址 和 API密钥
""")
st.info("💡 如何获取DeepSeek API密钥?\n\n1. 访问 https://platform.deepseek.com\n2. 注册/登录账号\n3. 进入API密钥管理页面\n4. 创建新的API密钥\n5. 复制密钥并粘贴到上方输入框") st.info("💡 如何获取DeepSeek API密钥?\n\n1. 访问 https://platform.deepseek.com\n2. 注册/登录账号\n3. 进入API密钥管理页面\n4. 创建新的API密钥\n5. 复制密钥并粘贴到上方输入框")
with tab2: with tab2:
+3
View File
@@ -8,6 +8,9 @@ load_dotenv(override=True)
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "") DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1")
# 默认AI模型名称(支持任何OpenAI兼容的模型)
DEFAULT_MODEL_NAME = os.getenv("DEFAULT_MODEL_NAME", "deepseek-chat")
# 其他配置 # 其他配置
TUSHARE_TOKEN = os.getenv("TUSHARE_TOKEN", "") TUSHARE_TOKEN = os.getenv("TUSHARE_TOKEN", "")
+6
View File
@@ -26,6 +26,12 @@ class ConfigManager:
"required": False, "required": False,
"type": "text" "type": "text"
}, },
"DEFAULT_MODEL_NAME": {
"value": "deepseek-chat",
"description": "AI模型名称(支持OpenAI兼容模型)",
"required": False,
"type": "text"
},
"TUSHARE_TOKEN": { "TUSHARE_TOKEN": {
"value": "", "value": "",
"description": "Tushare数据接口Token(可选)", "description": "Tushare数据接口Token(可选)",
+159 -130
View File
@@ -11,6 +11,10 @@ from dotenv import load_dotenv
# 加载环境变量 # 加载环境变量
load_dotenv() load_dotenv()
# 注入外部请求默认超时(覆盖akshare、tushare等基于requests的调用)
from http_timeout import install_default_requests_timeout
install_default_requests_timeout()
class DataSourceManager: class DataSourceManager:
"""数据源管理器 - 实现akshare与tushare自动切换""" """数据源管理器 - 实现akshare与tushare自动切换"""
@@ -25,7 +29,7 @@ class DataSourceManager:
try: try:
import tushare as ts import tushare as ts
ts.set_token(self.tushare_token) ts.set_token(self.tushare_token)
self.tushare_api = ts.pro_api() self.tushare_api = ts.pro_api(timeout=float(os.getenv('TUSHARE_TIMEOUT', '15')))
self.tushare_available = True self.tushare_available = True
print("✅ Tushare数据源初始化成功") print("✅ Tushare数据源初始化成功")
except Exception as e: except Exception as e:
@@ -36,7 +40,7 @@ class DataSourceManager:
def get_stock_hist_data(self, symbol, start_date=None, end_date=None, adjust='qfq'): def get_stock_hist_data(self, symbol, start_date=None, end_date=None, adjust='qfq'):
""" """
获取股票历史数据优先akshare失败时使用tushare 获取股票历史数据优先tushare失败时使用akshare
Args: Args:
symbol: 股票代码6位数字 symbol: 股票代码6位数字
@@ -55,10 +59,63 @@ class DataSourceManager:
else: else:
end_date = datetime.now().strftime('%Y%m%d') end_date = datetime.now().strftime('%Y%m%d')
# 优先使用akshare # 优先使用tushare
if self.tushare_available:
try:
import tushare as ts
print(f"[Tushare] 正在获取 {symbol} 的历史数据(主要数据源)...")
# 转换股票代码格式(添加市场后缀)
ts_code = self._convert_to_ts_code(symbol)
# 转换复权类型
adj_dict = {'qfq': 'qfq', 'hfq': 'hfq', '': None}
adj = adj_dict.get(adjust, 'qfq')
if adj is None:
# 不复权数据直接使用daily接口
df = self.tushare_api.daily(
ts_code=ts_code,
start_date=start_date,
end_date=end_date
)
else:
# 复权数据使用pro_bardaily接口不支持adj参数)
df = ts.pro_bar(
api=self.tushare_api,
ts_code=ts_code,
start_date=start_date,
end_date=end_date,
adj=adj,
retry_count=1
)
if df is not None and not df.empty:
# 标准化列名和数据格式
df = df.rename(columns={
'trade_date': 'date',
'vol': 'volume',
'amount': 'amount'
})
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values('date')
# 转换成交量单位(tushare单位是手,转换为股)
df['volume'] = df['volume'] * 100
# 转换成交额单位(tushare单位是千元,转换为元)
df['amount'] = df['amount'] * 1000
print(f"[Tushare] ✅ 成功获取 {len(df)} 条数据")
return df
else:
print(f"[Tushare] ❌ 未获取到数据,尝试备用数据源")
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
# tushare失败,回退到akshare
try: try:
import akshare as ak import akshare as ak
print(f"[Akshare] 正在获取 {symbol} 的历史数据...") print(f"[Akshare] 正在获取 {symbol} 的历史数据(备用数据源)...")
df = ak.stock_zh_a_hist( df = ak.stock_zh_a_hist(
symbol=symbol, symbol=symbol,
@@ -86,60 +143,18 @@ class DataSourceManager:
df['date'] = pd.to_datetime(df['date']) df['date'] = pd.to_datetime(df['date'])
print(f"[Akshare] ✅ 成功获取 {len(df)} 条数据") print(f"[Akshare] ✅ 成功获取 {len(df)} 条数据")
return df return df
else:
print(f"[Akshare] ❌ 未获取到数据")
except Exception as e: except Exception as e:
print(f"[Akshare] ❌ 获取失败: {e}") print(f"[Akshare] ❌ 获取失败: {e}")
# akshare失败,尝试tushare
if self.tushare_available:
try:
print(f"[Tushare] 正在获取 {symbol} 的历史数据(备用数据源)...")
# 转换股票代码格式(添加市场后缀)
ts_code = self._convert_to_ts_code(symbol)
# 转换复权类型
adj_dict = {'qfq': 'qfq', 'hfq': 'hfq', '': None}
adj = adj_dict.get(adjust, 'qfq')
# 格式化日期
start = f"{start_date[:4]}-{start_date[4:6]}-{start_date[6:]}" if start_date else None
end = f"{end_date[:4]}-{end_date[4:6]}-{end_date[6:]}" if end_date else None
# 获取数据
df = self.tushare_api.daily(
ts_code=ts_code,
start_date=start_date,
end_date=end_date,
adj=adj
)
if df is not None and not df.empty:
# 标准化列名和数据格式
df = df.rename(columns={
'trade_date': 'date',
'vol': 'volume',
'amount': 'amount'
})
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values('date')
# 转换成交量单位(tushare单位是手,转换为股)
df['volume'] = df['volume'] * 100
# 转换成交额单位(tushare单位是千元,转换为元)
df['amount'] = df['amount'] * 1000
print(f"[Tushare] ✅ 成功获取 {len(df)} 条数据")
return df
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
# 两个数据源都失败 # 两个数据源都失败
print("❌ 所有数据源均获取失败") print("❌ 所有数据源均获取失败")
return None return None
def get_stock_basic_info(self, symbol): def get_stock_basic_info(self, symbol):
""" """
获取股票基本信息优先akshare失败时使用tushare 获取股票基本信息优先tushare失败时使用akshare
Args: Args:
symbol: 股票代码 symbol: 股票代码
@@ -154,10 +169,34 @@ class DataSourceManager:
"market": "未知" "market": "未知"
} }
# 优先使用akshare # 优先使用tushare
if self.tushare_available:
try:
print(f"[Tushare] 正在获取 {symbol} 的基本信息(主要数据源)...")
ts_code = self._convert_to_ts_code(symbol)
df = self.tushare_api.stock_basic(
ts_code=ts_code,
fields='ts_code,name,area,industry,market,list_date'
)
if df is not None and not df.empty:
info['name'] = df.iloc[0]['name']
info['industry'] = df.iloc[0]['industry']
info['market'] = df.iloc[0]['market']
info['list_date'] = df.iloc[0]['list_date']
print(f"[Tushare] ✅ 成功获取基本信息")
return info
else:
print(f"[Tushare] ❌ 未获取到基本信息,尝试备用数据源")
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
# tushare失败,回退akshare
try: try:
import akshare as ak import akshare as ak
print(f"[Akshare] 正在获取 {symbol} 的基本信息...") print(f"[Akshare] 正在获取 {symbol} 的基本信息(备用数据源)...")
stock_info = ak.stock_individual_info_em(symbol=symbol) stock_info = ak.stock_individual_info_em(symbol=symbol)
if stock_info is not None and not stock_info.empty: if stock_info is not None and not stock_info.empty:
@@ -181,33 +220,11 @@ class DataSourceManager:
except Exception as e: except Exception as e:
print(f"[Akshare] ❌ 获取失败: {e}") print(f"[Akshare] ❌ 获取失败: {e}")
# akshare失败,尝试tushare
if self.tushare_available:
try:
print(f"[Tushare] 正在获取 {symbol} 的基本信息(备用数据源)...")
ts_code = self._convert_to_ts_code(symbol)
df = self.tushare_api.stock_basic(
ts_code=ts_code,
fields='ts_code,name,area,industry,market,list_date'
)
if df is not None and not df.empty:
info['name'] = df.iloc[0]['name']
info['industry'] = df.iloc[0]['industry']
info['market'] = df.iloc[0]['market']
info['list_date'] = df.iloc[0]['list_date']
print(f"[Tushare] ✅ 成功获取基本信息")
return info
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
return info return info
def get_realtime_quotes(self, symbol): def get_realtime_quotes(self, symbol):
""" """
获取实时行情数据优先akshare失败时使用tushare 获取实时行情数据优先tushare失败时使用akshare
Args: Args:
symbol: 股票代码 symbol: 股票代码
@@ -217,10 +234,51 @@ class DataSourceManager:
""" """
quotes = {} quotes = {}
# 优先使用akshare # 优先使用tushare
if self.tushare_available:
try:
print(f"[Tushare] 正在获取 {symbol} 的实时行情(主要数据源)...")
ts_code = self._convert_to_ts_code(symbol)
today = datetime.now().strftime('%Y%m%d')
df = self.tushare_api.daily(
ts_code=ts_code,
start_date=today,
end_date=today
)
if df is None or df.empty:
# 非交易日时,获取最近10个交易日的最新数据
start = (datetime.now() - timedelta(days=10)).strftime('%Y%m%d')
df = self.tushare_api.daily(
ts_code=ts_code,
start_date=start,
end_date=today
)
if df is not None and not df.empty:
row = df.iloc[0]
quotes = {
'symbol': symbol,
'price': row['close'],
'change_percent': row['pct_chg'],
'volume': row['vol'] * 100,
'amount': row['amount'] * 1000,
'high': row['high'],
'low': row['low'],
'open': row['open'],
'pre_close': row['pre_close']
}
print(f"[Tushare] ✅ 成功获取实时行情")
return quotes
else:
print(f"[Tushare] ❌ 未获取到实时行情,尝试备用数据源")
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
# tushare失败,回退akshare
try: try:
import akshare as ak import akshare as ak
print(f"[Akshare] 正在获取 {symbol} 的实时行情...") print(f"[Akshare] 正在获取 {symbol} 的实时行情(备用数据源)...")
df = ak.stock_zh_a_spot_em() df = ak.stock_zh_a_spot_em()
stock_df = df[df['代码'] == symbol] stock_df = df[df['代码'] == symbol]
@@ -245,41 +303,11 @@ class DataSourceManager:
except Exception as e: except Exception as e:
print(f"[Akshare] ❌ 获取失败: {e}") print(f"[Akshare] ❌ 获取失败: {e}")
# akshare失败,尝试tushare
if self.tushare_available:
try:
print(f"[Tushare] 正在获取 {symbol} 的实时行情(备用数据源)...")
ts_code = self._convert_to_ts_code(symbol)
df = self.tushare_api.daily(
ts_code=ts_code,
start_date=datetime.now().strftime('%Y%m%d'),
end_date=datetime.now().strftime('%Y%m%d')
)
if df is not None and not df.empty:
row = df.iloc[0]
quotes = {
'symbol': symbol,
'price': row['close'],
'change_percent': row['pct_chg'],
'volume': row['vol'] * 100,
'amount': row['amount'] * 1000,
'high': row['high'],
'low': row['low'],
'open': row['open'],
'pre_close': row['pre_close']
}
print(f"[Tushare] ✅ 成功获取实时行情")
return quotes
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
return quotes return quotes
def get_financial_data(self, symbol, report_type='income'): def get_financial_data(self, symbol, report_type='income'):
""" """
获取财务数据优先akshare失败时使用tushare 获取财务数据优先tushare失败时使用akshare
Args: Args:
symbol: 股票代码 symbol: 股票代码
@@ -288,30 +316,10 @@ class DataSourceManager:
Returns: Returns:
DataFrame: 财务数据 DataFrame: 财务数据
""" """
# 优先使用akshare # 优先使用tushare
try:
import akshare as ak
print(f"[Akshare] 正在获取 {symbol} 的财务数据...")
if report_type == 'income':
df = ak.stock_financial_report_sina(stock=symbol, symbol="利润表")
elif report_type == 'balance':
df = ak.stock_financial_report_sina(stock=symbol, symbol="资产负债表")
elif report_type == 'cashflow':
df = ak.stock_financial_report_sina(stock=symbol, symbol="现金流量表")
else:
df = None
if df is not None and not df.empty:
print(f"[Akshare] ✅ 成功获取财务数据")
return df
except Exception as e:
print(f"[Akshare] ❌ 获取失败: {e}")
# akshare失败,尝试tushare
if self.tushare_available: if self.tushare_available:
try: try:
print(f"[Tushare] 正在获取 {symbol} 的财务数据(备用数据源)...") print(f"[Tushare] 正在获取 {symbol} 的财务数据(主要数据源)...")
ts_code = self._convert_to_ts_code(symbol) ts_code = self._convert_to_ts_code(symbol)
@@ -327,9 +335,31 @@ class DataSourceManager:
if df is not None and not df.empty: if df is not None and not df.empty:
print(f"[Tushare] ✅ 成功获取财务数据") print(f"[Tushare] ✅ 成功获取财务数据")
return df return df
else:
print(f"[Tushare] ❌ 未获取到财务数据,尝试备用数据源")
except Exception as e: except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}") print(f"[Tushare] ❌ 获取失败: {e}")
# tushare失败,回退akshare
try:
import akshare as ak
print(f"[Akshare] 正在获取 {symbol} 的财务数据(备用数据源)...")
if report_type == 'income':
df = ak.stock_financial_report_sina(stock=symbol, symbol="利润表")
elif report_type == 'balance':
df = ak.stock_financial_report_sina(stock=symbol, symbol="资产负债表")
elif report_type == 'cashflow':
df = ak.stock_financial_report_sina(stock=symbol, symbol="现金流量表")
else:
df = None
if df is not None and not df.empty:
print(f"[Akshare] ✅ 成功获取财务数据")
return df
except Exception as e:
print(f"[Akshare] ❌ 获取失败: {e}")
return None return None
def _convert_to_ts_code(self, symbol): def _convert_to_ts_code(self, symbol):
@@ -376,4 +406,3 @@ class DataSourceManager:
# 全局数据源管理器实例 # 全局数据源管理器实例
data_source_manager = DataSourceManager() data_source_manager = DataSourceManager()
+6 -3
View File
@@ -1,16 +1,19 @@
import openai import openai
import json import json
import os
from typing import Dict, List, Any, Optional from typing import Dict, List, Any, Optional
import config import config
class DeepSeekClient: class DeepSeekClient:
"""DeepSeek API客户端""" """DeepSeek API客户端"""
def __init__(self, model="deepseek-chat"): def __init__(self, model=None):
self.model = model self.model = model or config.DEFAULT_MODEL_NAME
self.client = openai.OpenAI( self.client = openai.OpenAI(
api_key=config.DEEPSEEK_API_KEY, api_key=config.DEEPSEEK_API_KEY,
base_url=config.DEEPSEEK_BASE_URL base_url=config.DEEPSEEK_BASE_URL,
timeout=float(os.getenv('DEEPSEEK_TIMEOUT', '180')),
max_retries=int(os.getenv('DEEPSEEK_MAX_RETRIES', '1'))
) )
def call_api(self, messages: List[Dict[str, str]], model: Optional[str] = None, def call_api(self, messages: List[Dict[str, str]], model: Optional[str] = None,
+45 -48
View File
@@ -109,56 +109,54 @@ class FundFlowAkshareDataFetcher:
def _get_individual_fund_flow(self, symbol, market): def _get_individual_fund_flow(self, symbol, market):
"""获取个股资金流向数据(支持akshare和tushare自动切换)""" """获取个股资金流向数据(支持akshare和tushare自动切换)"""
try: try:
# 优先使用akshare的stock_individual_fund_flow接口 # 优先使用tushare
print(f" [Akshare] 正在获取资金流向 (市场: {market})...") if data_source_manager.tushare_available:
try:
print(f" [Tushare] 正在获取资金流向数据(主要数据源)...")
ts_code = data_source_manager._convert_to_ts_code(symbol)
df = ak.stock_individual_fund_flow(stock=symbol, market=market) # 计算日期范围(最近N个交易日)
end_date = datetime.now().strftime('%Y%m%d')
start_date = (datetime.now() - timedelta(days=self.days * 2)).strftime('%Y%m%d')
df = data_source_manager.tushare_api.moneyflow(
ts_code=ts_code,
start_date=start_date,
end_date=end_date
)
if df is not None and not df.empty:
# 标准化列名以匹配akshare格式(金额单位:千元→元)
df = df.rename(columns={
'trade_date': '日期',
'close': '收盘价',
'pct_chg': '涨跌幅',
'net_mf_amount': '主力净流入-净额'
})
df['主力净流入-净额'] = df['主力净流入-净额'] * 1000
df['超大单净流入-净额'] = (df['buy_elg_amount'] - df['sell_elg_amount']) * 1000
df['大单净流入-净额'] = (df['buy_lg_amount'] - df['sell_lg_amount']) * 1000
df['中单净流入-净额'] = (df['buy_md_amount'] - df['sell_md_amount']) * 1000
df['小单净流入-净额'] = (df['buy_sm_amount'] - df['sell_sm_amount']) * 1000
# tushare按日期倒序返回,取最近N天后转为正序(与akshare一致)
df = df.head(self.days)
df = df.iloc[::-1].reset_index(drop=True)
print(f" [Tushare] ✅ 成功获取 {len(df)} 条资金流向数据")
else:
print(f" [Tushare] ❌ 未找到资金流向数据,尝试备用数据源")
df = None
except Exception as te:
print(f" [Tushare] ❌ 获取失败: {te}")
df = None
else:
df = None
# tushare不可用或失败时,回退akshare
if df is None or df.empty: if df is None or df.empty:
print(f" [Akshare] 未找到资金流向数据,尝试备用数据源...") print(f" [Akshare] 正在获取资金流向 (市场: {market})备用数据源...")
df = ak.stock_individual_fund_flow(stock=symbol, market=market)
# akshare失败,尝试tushare if df is None or df.empty:
if data_source_manager.tushare_available: print(f" [Akshare] 未找到资金流向数据")
try:
print(f" [Tushare] 正在获取资金流向数据(备用数据源)...")
ts_code = data_source_manager._convert_to_ts_code(symbol)
# 计算日期范围(最近N个交易日)
end_date = datetime.now().strftime('%Y%m%d')
start_date = (datetime.now() - timedelta(days=self.days * 2)).strftime('%Y%m%d')
# 获取资金流向数据
df = data_source_manager.tushare_api.moneyflow(
ts_code=ts_code,
start_date=start_date,
end_date=end_date
)
if df is not None and not df.empty:
# 标准化列名以匹配akshare格式
df = df.rename(columns={
'trade_date': '日期',
'buy_sm_amount': '小单买入',
'sell_sm_amount': '小单卖出',
'buy_md_amount': '中单买入',
'sell_md_amount': '中单卖出',
'buy_lg_amount': '大单买入',
'sell_lg_amount': '大单卖出',
'buy_elg_amount': '超大单买入',
'sell_elg_amount': '超大单卖出',
'net_mf_amount': '净额'
})
# 限制为最近N天
df = df.head(self.days)
print(f" [Tushare] ✅ 成功获取 {len(df)} 条资金流向数据")
else:
print(f" [Tushare] ❌ 未找到资金流向数据")
return None
except Exception as te:
print(f" [Tushare] ❌ 获取失败: {te}")
return None
else:
return None return None
# akshare 返回的数据是按时间正序排列(从旧到新),所以使用 tail() 获取最近N天的数据 # akshare 返回的数据是按时间正序排列(从旧到新),所以使用 tail() 获取最近N天的数据
@@ -341,4 +339,3 @@ if __name__ == "__main__":
print(f"\n获取失败: {data.get('error', '未知错误')}") print(f"\n获取失败: {data.get('error', '未知错误')}")
print("\n") print("\n")
+77
View File
@@ -0,0 +1,77 @@
"""
统一的外部请求超时控制工具
- install_default_requests_timeout: 给所有基于 requests 的外部请求
aksharetusharepywencai 注入默认超时调用方未指定 timeout 时生效
- call_with_timeout: 在独立线程中执行阻塞调用超过时限立即放弃等待
防止个别环节长时间卡死整个程序
"""
import os
import threading
# 默认连接超时 / 读取超时(单位:秒),可通过环境变量覆盖
DEFAULT_CONNECT_TIMEOUT = float(os.getenv("HTTP_CONNECT_TIMEOUT", "6"))
DEFAULT_READ_TIMEOUT = float(os.getenv("HTTP_READ_TIMEOUT", "20"))
DEFAULT_CALL_TIMEOUT = float(os.getenv("HTTP_CALL_TIMEOUT", "30"))
_installed = False
_install_lock = threading.Lock()
def install_default_requests_timeout(connect_timeout=None, read_timeout=None):
"""给 requests 库注入默认超时(仅当调用方未显式指定 timeout 时生效)。"""
global _installed
with _install_lock:
if _installed:
return
connect = DEFAULT_CONNECT_TIMEOUT if connect_timeout is None else connect_timeout
read = DEFAULT_READ_TIMEOUT if read_timeout is None else read_timeout
try:
import requests.sessions
original_request = requests.sessions.Session.request
def request_with_timeout(self, method, url, **kwargs):
if kwargs.get("timeout") is None:
kwargs["timeout"] = (connect, read)
return original_request(self, method, url, **kwargs)
requests.sessions.Session.request = request_with_timeout
_installed = True
print(f"✅ 已为外部请求注入默认超时(连接 {connect}s / 读取 {read}s")
except Exception as e:
print(f"⚠️ 注入 requests 默认超时失败: {e}")
def call_with_timeout(func, timeout=None, *args, **kwargs):
"""在线程中执行函数,超过 timeout 秒则放弃等待并抛出 TimeoutError。
注意超时后线程会继续在后台运行直至结束守护线程不会影响主程序
"""
if timeout is None:
timeout = DEFAULT_CALL_TIMEOUT
if timeout <= 0:
return func(*args, **kwargs)
result_box = {}
def _run():
try:
result_box["ok"] = True
result_box["value"] = func(*args, **kwargs)
except BaseException as e: # noqa: BLE001
result_box["ok"] = False
result_box["error"] = e
worker = threading.Thread(target=_run, daemon=True)
worker.start()
worker.join(timeout)
if worker.is_alive():
func_name = getattr(func, "__name__", "function")
raise TimeoutError(f"调用 {func_name} 超过 {timeout}s 未返回,已放弃等待")
if result_box.get("ok"):
return result_box["value"]
raise result_box.get("error", RuntimeError("未知错误"))
BIN
View File
Binary file not shown.
+5 -4
View File
@@ -6,15 +6,16 @@
from deepseek_client import DeepSeekClient from deepseek_client import DeepSeekClient
from typing import Dict, Any, List from typing import Dict, Any, List
import time import time
import config
class LonghubangAgents: class LonghubangAgents:
"""龙虎榜AI分析师集合""" """龙虎榜AI分析师集合"""
def __init__(self, model="deepseek-chat"): def __init__(self, model=None):
self.model = model self.model = model or config.DEFAULT_MODEL_NAME
self.deepseek_client = DeepSeekClient(model=model) self.deepseek_client = DeepSeekClient(model=self.model)
print(f"[智瞰龙虎] AI分析师系统初始化 (模型: {model})") print(f"[智瞰龙虎] AI分析师系统初始化 (模型: {self.model})")
def youzi_behavior_analyst(self, longhubang_data: str, summary: Dict) -> Dict[str, Any]: def youzi_behavior_analyst(self, longhubang_data: str, summary: Dict) -> Dict[str, Any]:
""" """
+2 -1
View File
@@ -11,12 +11,13 @@ from typing import Dict, Any, List
from datetime import datetime, timedelta from datetime import datetime, timedelta
import time import time
import logging import logging
import config
class LonghubangEngine: class LonghubangEngine:
"""龙虎榜综合分析引擎""" """龙虎榜综合分析引擎"""
def __init__(self, model="deepseek-chat", db_path='longhubang.db'): def __init__(self, model=None, db_path='longhubang.db'):
""" """
初始化分析引擎 初始化分析引擎
+11 -18
View File
@@ -13,6 +13,7 @@ import base64
from longhubang_engine import LonghubangEngine from longhubang_engine import LonghubangEngine
from longhubang_pdf import LonghubangPDFGenerator from longhubang_pdf import LonghubangPDFGenerator
import config
def display_longhubang(): def display_longhubang():
@@ -110,7 +111,7 @@ def display_analysis_tab():
st.subheader("🔍 龙虎榜综合分析") st.subheader("🔍 龙虎榜综合分析")
# 参数设置 # 参数设置
col1, col2, col3 = st.columns([2, 2, 2]) col1, col2 = st.columns([2, 2])
with col1: with col1:
analysis_mode = st.selectbox( analysis_mode = st.selectbox(
@@ -135,18 +136,8 @@ def display_analysis_tab():
help="分析最近N天的龙虎榜数据" help="分析最近N天的龙虎榜数据"
) )
with col3:
# 导入model_config.py中定义的model_options
from model_config import model_options as app_model_options
selected_model = st.selectbox(
"AI模型",
list(app_model_options.keys()),
format_func=lambda x: app_model_options[x],
help="Reasoner模型提供更强的推理能力"
)
# 分析按钮 # 分析按钮
col1, col2, col3 = st.columns([2, 2, 2]) col1, col2 = st.columns([2, 2])
with col1: with col1:
analyze_button = st.button("🚀 开始分析", type="primary", width='stretch') analyze_button = st.button("🚀 开始分析", type="primary", width='stretch')
@@ -166,12 +157,12 @@ def display_analysis_tab():
if 'longhubang_result' in st.session_state: if 'longhubang_result' in st.session_state:
del st.session_state.longhubang_result del st.session_state.longhubang_result
# 准备参数 # 准备参数(使用.env中配置的默认模型)
if analysis_mode == "指定日期": if analysis_mode == "指定日期":
date_str = selected_date.strftime('%Y-%m-%d') date_str = selected_date.strftime('%Y-%m-%d')
run_longhubang_analysis(model=selected_model, date=date_str) run_longhubang_analysis(date=date_str)
else: else:
run_longhubang_analysis(model=selected_model, days=days) run_longhubang_analysis(days=days)
# 显示分析结果 # 显示分析结果
if 'longhubang_result' in st.session_state: if 'longhubang_result' in st.session_state:
@@ -183,8 +174,10 @@ def display_analysis_tab():
st.error(f"❌ 分析失败: {result.get('error', '未知错误')}") st.error(f"❌ 分析失败: {result.get('error', '未知错误')}")
def run_longhubang_analysis(model="deepseek-chat", date=None, days=1): def run_longhubang_analysis(model=None, date=None, days=1):
"""运行龙虎榜分析""" """运行龙虎榜分析"""
import config
model = model or config.DEFAULT_MODEL_NAME
# 进度显示 # 进度显示
progress_bar = st.progress(0) progress_bar = st.progress(0)
@@ -1397,7 +1390,7 @@ def run_longhubang_batch_analysis():
'sentiment': False, 'sentiment': False,
'news': False 'news': False
}, },
selected_model='deepseek-chat' selected_model=config.DEFAULT_MODEL_NAME
) )
results.append({ results.append({
@@ -1428,7 +1421,7 @@ def run_longhubang_batch_analysis():
'sentiment': False, 'sentiment': False,
'news': False 'news': False
}, },
selected_model='deepseek-chat' selected_model=config.DEFAULT_MODEL_NAME
) )
return {"code": code, "result": result} return {"code": code, "result": result}
except Exception as e: except Exception as e:
+2 -1
View File
@@ -10,6 +10,7 @@ import pywencai
from datetime import datetime from datetime import datetime
from typing import Tuple, Optional from typing import Tuple, Optional
import time import time
from http_timeout import call_with_timeout
class LowPriceBullSelector: class LowPriceBullSelector:
@@ -60,7 +61,7 @@ class LowPriceBullSelector:
print(f"正在调用问财接口...") print(f"正在调用问财接口...")
# 调用pywencai # 调用pywencai
result = pywencai.get(query=query, loop=True) result = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
if result is None: if result is None:
return False, None, "问财接口返回None,请检查网络或稍后重试" return False, None, "问财接口返回None,请检查网络或稍后重试"
+458
View File
@@ -0,0 +1,458 @@
"""
宏观周期分析 - AI智能体模块
包含四位专业分析师康波周期分析师美林时钟分析师中国政策分析师首席宏观策略师
"""
from deepseek_client import DeepSeekClient
from typing import Dict, Any
import time
import config
class MacroCycleAgents:
"""宏观周期AI智能体集合"""
def __init__(self, model=None):
self.model = model or config.DEFAULT_MODEL_NAME
self.deepseek_client = DeepSeekClient(model=self.model)
print(f"[宏观周期] AI智能体系统初始化 (模型: {self.model})")
def kondratieff_wave_agent(self, macro_data_text: str) -> Dict[str, Any]:
"""
康波周期分析师 - 判断当前处于康德拉季耶夫长波的哪个阶段
职责
- 分析当前技术革命阶段第五轮信息技术康波的位置
- 判断回升/繁荣/衰退/萧条四阶段中的位置
- 分析大宗商品与康波的关系
- 给出战略性资产配置方向
"""
print("🌊 康波周期分析师正在分析...")
time.sleep(1)
prompt = f"""
你是一位资深的康德拉季耶夫长波周期研究专家拥有深厚的经济史研究功底深入研究过周金涛先生的"人生发财靠康波"理论
以下是当前中国的宏观经济数据
{macro_data_text}
请你基于康波周期理论对当前全球和中国经济进行深度分析
## 一、康波周期基础判断
### 1. 历史康波定位
回顾历史五轮康波周期蒸汽机铁路钢铁电力化工汽车计算机信息技术判断
- 当前处于第五轮信息技术康波的哪个具体阶段回升期/繁荣期/衰退期/萧条期
- 给出判断依据和关键特征
- 该阶段大约始于何年预计持续到何年
### 2. 技术革命驱动分析
- 第五轮康波的核心技术互联网/移动互联网是否已经进入红利消退期
- AI人工智能新能源生物科技等是否有可能成为第六轮康波的驱动力
- 新旧技术转换的当前进展如何
### 3. 大宗商品与康波
- 当前大宗商品黄金原油铜等的表现是否符合康波阶段特征
- 结合数据判断是否处于康波衰退期的大宗商品超级牛市阶段
## 二、康波阶段特征验证
对照当前经济数据验证是否符合你判断的康波阶段特征
- GDP增速趋势是否匹配
- 通胀水平CPI/PPI是否匹配
- 资产价格股市房地产走势是否匹配
- 就业与社会情绪是否匹配
- 政策环境是否匹配
## 三、康波视角下的战略建议
### 1. 当前阶段的核心策略
- 整体应该进攻还是防守
- 应该积累现金还是配置资产
### 2. 战略资产配置方向
给出康波视角的大类资产配置建议
- 股票配置比例建议及方向
- 债券配置比例建议
- 大宗商品/黄金配置比例建议
- 现金配置比例建议
- 房地产配置建议
### 3. 布局下一轮康波
- 如果第六轮康波即将到来应提前布局哪些方向
- 需要回避哪些夕阳产业
### 4. 对普通人的人生建议
根据周金涛"人生发财靠康波"的理念当前阶段普通人应该
- 在职业选择上注意什么
- 在投资理财上注意什么
- 在消费与储蓄上如何平衡
## 四、康波周期仪表盘
请给出一个简洁的康波定位总结
- 🌊 当前康波阶段[回升期/繁荣期/衰退期/萧条期]
- 📍 阶段进度[初期/中期/末期]
- 预计本阶段剩余时间约X年
- 🎯 核心策略关键词["防守为主,播种未来"]
- 信心度[1-10]
请给出专业深入有数据支撑的分析报告
"""
messages = [
{"role": "system", "content": "你是全球顶尖的康德拉季耶夫长波周期研究专家,深研周金涛的理论体系,擅长将60年长周期框架应用于当前经济形势分析,帮助投资者做出战略性决策。"},
{"role": "user", "content": prompt}
]
analysis = self.deepseek_client.call_api(messages, max_tokens=6000)
print(" ✓ 康波周期分析师分析完成")
return {
"agent_name": "康波周期分析师",
"agent_icon": "🌊",
"agent_role": "判断当前处于康德拉季耶夫长波(50-60年大周期)的哪个阶段,给出战略性资产配置方向",
"analysis": analysis,
"focus_areas": ["康波定位", "技术革命", "大宗商品超级周期", "战略资产配置"],
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
}
def merrill_lynch_clock_agent(self, macro_data_text: str) -> Dict[str, Any]:
"""
美林投资时钟分析师 - 判断当前处于美林时钟的哪个象限
职责
- 根据经济增长和通胀两个维度判断象限
- 结合中国特色政策第三维度
- 给出中短期资产配置建议
"""
print("⏰ 美林时钟分析师正在分析...")
time.sleep(1)
prompt = f"""
你是一位精通美林投资时钟理论的资深资产配置策略师同时深入了解中国经济的特殊性能够将经典美林时钟做"中国化改造"
以下是当前中国的宏观经济数据
{macro_data_text}
请基于美林投资时钟理论对当前中国的经济周期进行分析
## 一、经典美林时钟定位
### 1. 两大核心指标研判
**经济增长维度//**
- GDP增速趋势判断同比环比PMI佐证
- 工业增加值/制造业景气度
- 消费投资出口三驾马车的状态
- 综合判断经济增长向上还是向下
**通货膨胀维度//**
- CPI走势分析核心CPI
- PPI走势分析
- M2/社融与信贷扩张情况
- 综合判断通胀向上还是向下
### 2. 象限定位
根据以上两个维度判断当前处于美林时钟的哪个象限
- 🟢 复苏期增长 + 通胀 配股票
- 🔴 过热期增长 + 通胀 配商品
- 🟡 滞胀期增长 + 通胀 持现金
- 🔵 衰退期增长 + 通胀 配债券
### 3. 时钟转动方向
- 当前正从哪个象限向哪个象限转动
- 预计转动速度如何中国时钟转动通常比美国快
- 是否存在"跳跃""逆转"的可能
## 二、中国化美林时钟(三维分析)
### 1. 第三维度:政策方向
这是中国版美林时钟最重要的增量分析
- 货币政策方向宽松/中性/收紧分析LPRMLF准备金率等
- 财政政策方向积极/稳健/收缩分析专项债减税降费等
- 产业政策方向重点扶持哪些领域
- 房地产政策方向放松还是收紧
- 政策的即时效果和滞后效应
### 2. 中国特色修正
- 中国利率市场化程度对时钟的影响
- 政府干预对时钟转动的扭曲效应
- A股散户占比高对资产定价的影响
- 房地产作为"第五类资产"的配置考量
## 三、资产配置建议
### 1. 基于当前象限的配置
给出各大类资产的具体配置比例建议
| 资产类别 | 配置比例 | 理由 |
|---------|---------|------|
| A股股票 | X% | ... |
| 债券/固收 | X% | ... |
| 大宗商品/黄金 | X% | ... |
| 现金/货币基金 | X% | ... |
| 房地产 | X% | ... |
### 2. 板块/行业建议
- 当前象限最受益的行业/板块3-5
- 应规避的行业/板块2-3
- "政策友好型"重点关注方向
### 3. 时间框架
- 当前配置建议的有效期约多久
- 需要密切关注哪些信号来判断时钟转动
## 四、美林时钟仪表盘
请给出一个简洁的定位总结
- 当前象限[复苏期/过热期/滞胀期/衰退期]
- 📊 经济增长[上行/持平/下行]
- 📈 通胀水平[上行/持平/下行]
- 🏛 政策方向[宽松/中性/收紧]
- 🎯 最优资产[股票/商品/现金/债券]
- 信心度[1-10]
请给出专业数据驱动的分析报告
"""
messages = [
{"role": "system", "content": "你是一位精通美林投资时钟理论的顶级资产配置策略师,擅长将经典框架进行中国化改造,加入政策分析作为第三维度,帮助中国投资者做出精准的中短期资产配置决策。"},
{"role": "user", "content": prompt}
]
analysis = self.deepseek_client.call_api(messages, max_tokens=6000)
print(" ✓ 美林时钟分析师分析完成")
return {
"agent_name": "美林时钟分析师",
"agent_icon": "",
"agent_role": "判断当前处于美林投资时钟的哪个象限(3-5年中短周期),给出资产配置建议",
"analysis": analysis,
"focus_areas": ["经济增长", "通胀水平", "政策方向", "资产配置"],
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
}
def china_policy_agent(self, macro_data_text: str) -> Dict[str, Any]:
"""
中国政策分析师 - 分析中国特色政策环境
职责
- 分析当前政策环境
- 评估政策对周期的影响
- 识别政策驱动的投资机会
"""
print("🏛️ 中国政策分析师正在分析...")
time.sleep(1)
prompt = f"""
你是一位资深的中国宏观政策研究专家深谙中国经济的"有形之手"运作方式擅长从政策信号中挖掘投资机会
以下是当前中国的宏观经济数据和新闻
{macro_data_text}
请从政策角度进行深度分析
## 一、当前政策全景
### 1. 货币政策分析
- 央行近期操作解读降准/降息/MLF/逆回购等
- LPR利率走势及未来方向
- M2增速与社融规模分析
- 流动性环境评估宽松/适度/偏紧
- 对标历史政策宽松周期当前处于什么阶段
### 2. 财政政策分析
- 专项债发行规模和进度
- 减税降费政策力度
- 基建投资方向和力度
- 财政赤字率水平
- 是否有大规模刺激计划的信号
### 3. 产业政策分析
- 当前国家战略重点方向如AI新能源半导体生物医药等
- "专精特新"等政策支持力度
- 国产替代进展
- 数字经济/新质生产力政策
### 4. 房地产政策分析
- 限购限贷政策变化
- 房贷利率走势
- 保交楼/房企纾困进展
- 房地产政策的底线和目标
## 二、政策对周期的影响
### 1. 政策是"顺周期"还是"逆周期"
- 当前的政策是在托底经济还是防止过热
- 政策的主要目标是什么稳增长/防风险/调结构
### 2. 政策对美林时钟的扭曲效应
- 政策宽松是否可能让时钟"跳跃"
- 政策刺激是否可能造成"短暂过热"的假象
- 历史上类似政策环境下市场的表现
### 3. 政策拐点信号
- 需要关注哪些政策信号来判断下一步方向
- 最可能的政策转向时间节点
## 三、政策驱动的投资机会
### 1. "政策友好型"资产
列出当前最受政策青睐的领域和方向
### 2. 政策支持的具体行业/板块
给出3-5个受益于当前政策环境的行业
### 3. 政策风险警示
哪些领域面临政策收紧或监管风险
## 四、政策环境仪表盘
请给出简洁总结
- 🏛 货币政策[极宽松/宽松/中性/偏紧/收紧]
- 💰 财政政策[大力刺激/积极/稳健/收缩]
- 🏭 产业政策重点[3-5个关键词]
- 🏠 房地产政策[大力托底/政策松绑/中性/调控]
- 🎯 政策核心目标[一句话概括]
- 政策转向风险[//]
请给出专业深入的政策分析报告
"""
messages = [
{"role": "system", "content": "你是一位资深的中国宏观经济政策研究专家,深入了解中国政府的经济调控方式和政策意图,擅长从政策中发现投资机会和风险。"},
{"role": "user", "content": prompt}
]
analysis = self.deepseek_client.call_api(messages, max_tokens=5000)
print(" ✓ 中国政策分析师分析完成")
return {
"agent_name": "中国政策分析师",
"agent_icon": "🏛️",
"agent_role": "分析中国特色政策环境,评估政策对周期的影响,识别政策驱动的投资机会",
"analysis": analysis,
"focus_areas": ["货币政策", "财政政策", "产业政策", "房地产政策", "政策拐点"],
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
}
def chief_macro_strategist_agent(self, kondratieff_report: str, merrill_report: str, policy_report: str, macro_data_text: str) -> Dict[str, Any]:
"""
首席宏观策略师 - 综合三位分析师的观点形成最终策略
职责
- 整合康波美林时钟政策三个维度
- 构建"周期仪表盘"
- 给出最终的综合建议
"""
print("👔 首席宏观策略师正在综合研判...")
time.sleep(1)
prompt = f"""
你是一位顶级的首席宏观策略师你需要整合三位分析师的报告形成最终的综合判断和投资策略
康波周期分析师报告
{kondratieff_report}
美林时钟分析师报告
{merrill_report}
中国政策分析师报告
{policy_report}
请你综合以上三份报告进行最终的综合研判
## 一、周期仪表盘(双指针+政策风向标)
构建一个完整的"周期仪表盘"
### 康波指针(战略方向)
- 当前阶段[回升期/繁荣期/衰退期/萧条期]
- 阶段进度[初期/中期/末期]
- 战略方向[进攻/稳健/防守/极度防守]
### 美林指针(战术节奏)
- 当前象限[复苏/过热/滞胀/衰退]
- 转动方向[下一个象限]转动
- 战术方向[积极做多/获利了结/持币观望/配置债券]
### 政策风向标(中国特色)
- 政策方向[强力宽松/宽松/中性/收紧]
- 对周期的影响[加速/减缓/扭曲/逆转]
### 双指针共振分析
- 两个指针方向是否一致
- 共振或矛盾意味着什么
- 综合信号[强烈看多/偏多/中性/偏空/强烈看空]
## 二、综合资产配置建议
### 1. 最终配置方案
结合康波战略+ 美林战术+ 政策催化给出最终的资产配置建议
| 资产类别 | 推荐配置 | 康波逻辑 | 美林逻辑 | 政策逻辑 |
|---------|---------|---------|---------|---------|
| A股/港股 | X% | ... | ... | ... |
| 债券/固收 | X% | ... | ... | ... |
| 黄金 | X% | ... | ... | ... |
| 大宗商品 | X% | ... | ... | ... |
| 现金 | X% | ... | ... | ... |
| 房地产 | X% | ... | ... | ... |
### 2. 重点板块/方向(3-5个)
结合三个维度共振的最优方向
### 3. 风险警示
需要高度警惕的风险因素
## 三、不同人群的具体建议
### 1. 保守型投资者(风险厌恶)
- 资产配置建议
- 核心策略
### 2. 稳健型投资者(平衡风险收益)
- 资产配置建议
- 核心策略
### 3. 进取型投资者(愿承受风险)
- 资产配置建议
- 核心策略
## 四、核心观点总结
请用简洁有力的语言总结
1. **一句话定位**当前经济周期的核心判断
2. **核心策略**用一句话概括应对策略
3. **最大机会**当前最值得把握的方向
4. **最大风险**最需要警惕的风险
5. **时间节点**下一个重要的周期转折点大约在何时
## 五、周金涛名言对照
选择一句最契合当前阶段的周金涛名言并解释为什么这句话适用于当下
请给出权威全面可操作的综合策略报告
"""
messages = [
{"role": "system", "content": "你是一位世界级的首席宏观策略师,擅长将康波长周期、美林投资时钟和中国政策环境三个维度有机结合,为投资者提供既有战略高度又有战术灵活性的综合投资策略。你的判断沉稳、客观、有数据支撑。"},
{"role": "user", "content": prompt}
]
analysis = self.deepseek_client.call_api(messages, max_tokens=6000)
print(" ✓ 首席宏观策略师综合研判完成")
return {
"agent_name": "首席宏观策略师",
"agent_icon": "👔",
"agent_role": "整合康波周期、美林时钟、中国政策三维分析,构建周期仪表盘,给出最终综合策略",
"analysis": analysis,
"focus_areas": ["周期仪表盘", "综合资产配置", "双指针共振", "分人群建议"],
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
}
# 测试
if __name__ == "__main__":
print("=" * 60)
print("测试宏观周期AI智能体系统")
print("=" * 60)
agents = MacroCycleAgents()
print(f"模型: {agents.model}")
print("初始化完成")
+559
View File
@@ -0,0 +1,559 @@
"""
宏观周期分析 - 数据采集模块
采集宏观经济数据GDPCPI/PPIPMI利率M2大宗商品等
用于康波周期和美林投资时钟分析
"""
import akshare as ak
import pandas as pd
from datetime import datetime, timedelta
import warnings
import time
import logging
import traceback
warnings.filterwarnings('ignore')
logger = logging.getLogger(__name__)
class MacroCycleDataFetcher:
"""宏观经济数据采集器"""
def __init__(self):
print("[宏观周期] 数据采集器初始化...")
self.max_retries = 3
def _safe_request(self, func, *args, **kwargs):
"""安全请求,带重试"""
for i in range(self.max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
if i < self.max_retries - 1:
time.sleep(2)
else:
logger.warning(f"请求失败: {e}")
return None
def get_all_macro_data(self) -> dict:
"""
获取所有宏观经济数据
Returns:
dict: 包含多维度宏观数据的字典
"""
print("\n[宏观周期] 开始采集宏观经济数据...")
data = {
"success": False,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"gdp": {},
"cpi_ppi": {},
"pmi": {},
"money_supply": {},
"interest_rate": {},
"market_indices": {},
"commodities": {},
"real_estate": {},
"employment": {},
"news": [],
"errors": []
}
# 1. GDP
print(" 1/9 获取GDP数据...")
try:
gdp_data = self._get_gdp_data()
if gdp_data:
data["gdp"] = gdp_data
print(" ✓ GDP数据获取成功")
except Exception as e:
data["errors"].append(f"GDP: {e}")
print(f" ✗ GDP数据获取失败: {e}")
# 2. CPI/PPI
print(" 2/9 获取CPI/PPI数据...")
try:
cpi_ppi = self._get_cpi_ppi_data()
if cpi_ppi:
data["cpi_ppi"] = cpi_ppi
print(" ✓ CPI/PPI数据获取成功")
except Exception as e:
data["errors"].append(f"CPI/PPI: {e}")
print(f" ✗ CPI/PPI获取失败: {e}")
# 3. PMI
print(" 3/9 获取PMI数据...")
try:
pmi = self._get_pmi_data()
if pmi:
data["pmi"] = pmi
print(" ✓ PMI数据获取成功")
except Exception as e:
data["errors"].append(f"PMI: {e}")
print(f" ✗ PMI获取失败: {e}")
# 4. 货币供应量 M2
print(" 4/9 获取货币供应数据...")
try:
money = self._get_money_supply()
if money:
data["money_supply"] = money
print(" ✓ 货币供应数据获取成功")
except Exception as e:
data["errors"].append(f"货币供应: {e}")
print(f" ✗ 货币供应获取失败: {e}")
# 5. 利率
print(" 5/9 获取利率数据...")
try:
rate = self._get_interest_rate()
if rate:
data["interest_rate"] = rate
print(" ✓ 利率数据获取成功")
except Exception as e:
data["errors"].append(f"利率: {e}")
print(f" ✗ 利率获取失败: {e}")
# 6. 市场指数
print(" 6/9 获取市场指数...")
try:
indices = self._get_market_indices()
if indices:
data["market_indices"] = indices
print(" ✓ 市场指数获取成功")
except Exception as e:
data["errors"].append(f"市场指数: {e}")
print(f" ✗ 市场指数获取失败: {e}")
# 7. 大宗商品
print(" 7/9 获取大宗商品数据...")
try:
commodities = self._get_commodities_data()
if commodities:
data["commodities"] = commodities
print(" ✓ 大宗商品数据获取成功")
except Exception as e:
data["errors"].append(f"大宗商品: {e}")
print(f" ✗ 大宗商品获取失败: {e}")
# 8. 房地产
print(" 8/9 获取房地产数据...")
try:
real_estate = self._get_real_estate_data()
if real_estate:
data["real_estate"] = real_estate
print(" ✓ 房地产数据获取成功")
except Exception as e:
data["errors"].append(f"房地产: {e}")
print(f" ✗ 房地产获取失败: {e}")
# 9. 财经新闻
print(" 9/9 获取财经新闻...")
try:
news = self._get_macro_news()
if news:
data["news"] = news
print(f" ✓ 获取{len(news)}条新闻")
except Exception as e:
data["errors"].append(f"新闻: {e}")
print(f" ✗ 新闻获取失败: {e}")
# 判断是否有足够数据
valid_count = sum(1 for k in ["gdp", "cpi_ppi", "pmi", "money_supply",
"interest_rate", "market_indices", "commodities"]
if data.get(k))
if valid_count >= 3:
data["success"] = True
print(f"\n[宏观周期] 数据采集完成,成功获取 {valid_count}/7 项核心数据")
else:
print(f"\n[宏观周期] 数据不足(仅 {valid_count}/7 项),分析可能不够准确")
data["success"] = True # 仍允许分析
return data
def _get_gdp_data(self) -> dict:
"""获取GDP数据"""
result = {}
try:
# 中国GDP年度
df = self._safe_request(ak.macro_china_gdp)
if df is not None and not df.empty:
recent = df.tail(8)
result["yearly"] = []
for _, row in recent.iterrows():
item = {}
for col in df.columns:
item[col] = str(row[col])
result["yearly"].append(item)
except Exception as e:
logger.warning(f"GDP年度数据获取失败: {e}")
try:
# 季度GDP增速
df = self._safe_request(ak.macro_china_gdp_yearly)
if df is not None and not df.empty:
recent = df.tail(12)
result["quarterly_growth"] = []
for _, row in recent.iterrows():
item = {}
for col in df.columns:
item[col] = str(row[col])
result["quarterly_growth"].append(item)
except Exception as e:
logger.warning(f"GDP季度数据获取失败: {e}")
return result if result else None
def _get_cpi_ppi_data(self) -> dict:
"""获取CPI和PPI数据"""
result = {}
try:
# CPI月度
df = self._safe_request(ak.macro_china_cpi_monthly)
if df is not None and not df.empty:
recent = df.tail(12)
result["cpi_monthly"] = []
for _, row in recent.iterrows():
item = {}
for col in df.columns:
item[col] = str(row[col])
result["cpi_monthly"].append(item)
except Exception as e:
logger.warning(f"CPI数据获取失败: {e}")
try:
# PPI月度
df = self._safe_request(ak.macro_china_ppi_yearly)
if df is not None and not df.empty:
recent = df.tail(12)
result["ppi_monthly"] = []
for _, row in recent.iterrows():
item = {}
for col in df.columns:
item[col] = str(row[col])
result["ppi_monthly"].append(item)
except Exception as e:
logger.warning(f"PPI数据获取失败: {e}")
return result if result else None
def _get_pmi_data(self) -> dict:
"""获取PMI数据"""
result = {}
try:
# 制造业PMI
df = self._safe_request(ak.macro_china_pmi_yearly)
if df is not None and not df.empty:
recent = df.tail(12)
result["manufacturing_pmi"] = []
for _, row in recent.iterrows():
item = {}
for col in df.columns:
item[col] = str(row[col])
result["manufacturing_pmi"].append(item)
except Exception as e:
logger.warning(f"制造业PMI获取失败: {e}")
try:
# 非制造业PMI(财新)
df = self._safe_request(ak.macro_china_cx_pmi_yearly)
if df is not None and not df.empty:
recent = df.tail(12)
result["caixin_pmi"] = []
for _, row in recent.iterrows():
item = {}
for col in df.columns:
item[col] = str(row[col])
result["caixin_pmi"].append(item)
except Exception as e:
logger.warning(f"财新PMI获取失败: {e}")
return result if result else None
def _get_money_supply(self) -> dict:
"""获取货币供应量"""
result = {}
try:
df = self._safe_request(ak.macro_china_money_supply)
if df is not None and not df.empty:
recent = df.tail(12)
result["m2_data"] = []
for _, row in recent.iterrows():
item = {}
for col in df.columns:
item[col] = str(row[col])
result["m2_data"].append(item)
except Exception as e:
logger.warning(f"货币供应数据获取失败: {e}")
return result if result else None
def _get_interest_rate(self) -> dict:
"""获取利率数据"""
result = {}
try:
# LPR利率
df = self._safe_request(ak.macro_china_lpr)
if df is not None and not df.empty:
recent = df.tail(12)
result["lpr"] = []
for _, row in recent.iterrows():
item = {}
for col in df.columns:
item[col] = str(row[col])
result["lpr"].append(item)
except Exception as e:
logger.warning(f"LPR利率获取失败: {e}")
return result if result else None
def _get_market_indices(self) -> dict:
"""获取主要市场指数"""
result = {}
indices = {
"sh_index": "sh000001", # 上证指数
"sz_index": "sz399001", # 深证成指
"cyb_index": "sz399006", # 创业板指
}
for name, code in indices.items():
try:
df = self._safe_request(
ak.stock_zh_index_daily,
symbol=code
)
if df is not None and not df.empty:
latest = df.tail(1).iloc[0]
prev = df.tail(2).iloc[0] if len(df) >= 2 else latest
change_pct = 0
if prev["close"] > 0:
change_pct = (latest["close"] - prev["close"]) / prev["close"] * 100
# 计算近期涨跌
recent_60 = df.tail(60)
pct_60d = 0
if len(recent_60) >= 60:
pct_60d = (latest["close"] - recent_60.iloc[0]["close"]) / recent_60.iloc[0]["close"] * 100
result[name] = {
"close": round(float(latest["close"]), 2),
"change_pct": round(change_pct, 2),
"pct_60d": round(pct_60d, 2),
"high_52w": round(float(df.tail(250)["high"].max()), 2) if len(df) >= 250 else None,
"low_52w": round(float(df.tail(250)["low"].min()), 2) if len(df) >= 250 else None,
}
except Exception as e:
logger.warning(f"指数{name}获取失败: {e}")
return result if result else None
def _get_commodities_data(self) -> dict:
"""获取大宗商品数据"""
result = {}
# 黄金
try:
df = self._safe_request(ak.futures_main_sina, symbol="AU0", start_date=(datetime.now() - timedelta(days=365)).strftime("%Y%m%d"), end_date=datetime.now().strftime("%Y%m%d"))
if df is not None and not df.empty:
latest = df.tail(1).iloc[0]
first = df.head(1).iloc[0]
ytd_pct = (float(latest["收盘价"]) - float(first["收盘价"])) / float(first["收盘价"]) * 100 if float(first["收盘价"]) > 0 else 0
result["gold"] = {
"price": round(float(latest["收盘价"]), 2),
"ytd_change_pct": round(ytd_pct, 2),
"name": "沪金主力"
}
except Exception as e:
logger.warning(f"黄金数据获取失败: {e}")
# 原油
try:
df = self._safe_request(ak.futures_main_sina, symbol="SC0", start_date=(datetime.now() - timedelta(days=365)).strftime("%Y%m%d"), end_date=datetime.now().strftime("%Y%m%d"))
if df is not None and not df.empty:
latest = df.tail(1).iloc[0]
first = df.head(1).iloc[0]
ytd_pct = (float(latest["收盘价"]) - float(first["收盘价"])) / float(first["收盘价"]) * 100 if float(first["收盘价"]) > 0 else 0
result["crude_oil"] = {
"price": round(float(latest["收盘价"]), 2),
"ytd_change_pct": round(ytd_pct, 2),
"name": "原油主力"
}
except Exception as e:
logger.warning(f"原油数据获取失败: {e}")
# 铜
try:
df = self._safe_request(ak.futures_main_sina, symbol="CU0", start_date=(datetime.now() - timedelta(days=365)).strftime("%Y%m%d"), end_date=datetime.now().strftime("%Y%m%d"))
if df is not None and not df.empty:
latest = df.tail(1).iloc[0]
first = df.head(1).iloc[0]
ytd_pct = (float(latest["收盘价"]) - float(first["收盘价"])) / float(first["收盘价"]) * 100 if float(first["收盘价"]) > 0 else 0
result["copper"] = {
"price": round(float(latest["收盘价"]), 2),
"ytd_change_pct": round(ytd_pct, 2),
"name": "沪铜主力"
}
except Exception as e:
logger.warning(f"铜数据获取失败: {e}")
return result if result else None
def _get_real_estate_data(self) -> dict:
"""获取房地产相关数据"""
result = {}
try:
df = self._safe_request(ak.macro_china_real_estate)
if df is not None and not df.empty:
recent = df.tail(12)
result["data"] = []
for _, row in recent.iterrows():
item = {}
for col in df.columns:
item[col] = str(row[col])
result["data"].append(item)
except Exception as e:
logger.warning(f"房地产数据获取失败: {e}")
return result if result else None
def _get_macro_news(self) -> list:
"""获取宏观经济相关新闻"""
news_list = []
try:
df = self._safe_request(ak.stock_info_global_em)
if df is not None and not df.empty:
for _, row in df.head(50).iterrows():
news_list.append({
"title": str(row.get("标题", "")),
"publish_time": str(row.get("发布时间", "")),
"content": str(row.get("概要", ""))[:300]
})
except Exception as e:
logger.warning(f"新闻获取失败: {e}")
return news_list
def format_data_for_ai(self, data: dict) -> str:
"""将数据格式化为AI分析所需的文本"""
parts = []
parts.append(f"===== 宏观经济数据报告 =====")
parts.append(f"数据采集时间: {data.get('timestamp', '未知')}")
parts.append("")
# GDP
if data.get("gdp"):
parts.append("【一、GDP数据】")
gdp = data["gdp"]
if gdp.get("yearly"):
parts.append("近年GDP:")
for item in gdp["yearly"][-4:]:
parts.append(f" {item}")
if gdp.get("quarterly_growth"):
parts.append("季度GDP增速:")
for item in gdp["quarterly_growth"][-8:]:
parts.append(f" {item}")
parts.append("")
# CPI/PPI
if data.get("cpi_ppi"):
parts.append("【二、CPI/PPI通胀数据】")
cp = data["cpi_ppi"]
if cp.get("cpi_monthly"):
parts.append("近12个月CPI:")
for item in cp["cpi_monthly"]:
parts.append(f" {item}")
if cp.get("ppi_monthly"):
parts.append("近12个月PPI:")
for item in cp["ppi_monthly"]:
parts.append(f" {item}")
parts.append("")
# PMI
if data.get("pmi"):
parts.append("【三、PMI景气指数】")
pmi = data["pmi"]
if pmi.get("manufacturing_pmi"):
parts.append("制造业PMI50为荣枯线):")
for item in pmi["manufacturing_pmi"]:
parts.append(f" {item}")
if pmi.get("caixin_pmi"):
parts.append("财新PMI:")
for item in pmi["caixin_pmi"]:
parts.append(f" {item}")
parts.append("")
# 货币供应
if data.get("money_supply"):
parts.append("【四、货币供应量】")
ms = data["money_supply"]
if ms.get("m2_data"):
parts.append("M0/M1/M2数据:")
for item in ms["m2_data"]:
parts.append(f" {item}")
parts.append("")
# 利率
if data.get("interest_rate"):
parts.append("【五、利率数据】")
ir = data["interest_rate"]
if ir.get("lpr"):
parts.append("LPR利率:")
for item in ir["lpr"]:
parts.append(f" {item}")
parts.append("")
# 市场指数
if data.get("market_indices"):
parts.append("【六、市场指数】")
mi = data["market_indices"]
for name, info in mi.items():
label = {"sh_index": "上证指数", "sz_index": "深证成指", "cyb_index": "创业板指"}.get(name, name)
parts.append(f" {label}: {info['close']} (日涨跌: {info['change_pct']:+.2f}%, 60日涨跌: {info.get('pct_60d', 0):+.2f}%)")
if info.get("high_52w"):
parts.append(f" 52周最高: {info['high_52w']} 52周最低: {info['low_52w']}")
parts.append("")
# 大宗商品
if data.get("commodities"):
parts.append("【七、大宗商品】")
for name, info in data["commodities"].items():
parts.append(f" {info['name']}: {info['price']} (年涨跌: {info['ytd_change_pct']:+.2f}%)")
parts.append("")
# 房地产
if data.get("real_estate"):
parts.append("【八、房地产数据】")
re_data = data["real_estate"]
if re_data.get("data"):
for item in re_data["data"][-4:]:
parts.append(f" {item}")
parts.append("")
# 新闻
if data.get("news"):
parts.append("【九、近期宏观经济新闻】")
for idx, news in enumerate(data["news"][:20], 1):
parts.append(f" {idx}. [{news.get('publish_time', '')}] {news.get('title', '')}")
if news.get('content'):
parts.append(f" {news['content'][:150]}")
parts.append("")
return "\n".join(parts)
# 测试
if __name__ == "__main__":
print("=" * 60)
print("测试宏观周期数据采集")
print("=" * 60)
fetcher = MacroCycleDataFetcher()
data = fetcher.get_all_macro_data()
if data.get("success"):
formatted = fetcher.format_data_for_ai(data)
print(formatted[:5000])
print(f"\n... (总长度: {len(formatted)} 字符)")
else:
print(f"数据采集失败: {data.get('errors')}")
+149
View File
@@ -0,0 +1,149 @@
"""
宏观周期分析 - 综合研判引擎
协调数据采集与AI分析生成完整的宏观周期分析报告
"""
from macro_cycle_agents import MacroCycleAgents
from macro_cycle_data import MacroCycleDataFetcher
from typing import Dict, Any
import time
import json
import logging
import config
class MacroCycleEngine:
"""宏观周期综合研判引擎"""
def __init__(self, model=None):
self.model = model or config.DEFAULT_MODEL_NAME
self.agents = MacroCycleAgents(model=self.model)
self.data_fetcher = MacroCycleDataFetcher()
self.logger = logging.getLogger(__name__)
if not self.logger.handlers:
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(levelname)s %(name)s: %(message)s')
print(f"[宏观周期引擎] 初始化完成 (模型: {self.model})")
def run_full_analysis(self, progress_callback=None) -> Dict[str, Any]:
"""
运行完整的宏观周期分析流程
Args:
progress_callback: 进度回调函数 (progress_pct, status_text)
Returns:
完整的分析结果
"""
print("\n" + "=" * 60)
print("🧭 宏观周期分析系统启动")
print("=" * 60)
results = {
"success": False,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"raw_data": {},
"formatted_data": "",
"agents_analysis": {},
"data_errors": []
}
try:
# 阶段1:数据采集
if progress_callback:
progress_callback(5, "📊 正在采集宏观经济数据...")
print("\n[阶段1] 宏观经济数据采集...")
print("-" * 60)
raw_data = self.data_fetcher.get_all_macro_data()
results["raw_data"] = raw_data
results["data_errors"] = raw_data.get("errors", [])
if not raw_data.get("success"):
print("⚠ 数据采集未完全成功,尝试继续分析...")
# 格式化数据
formatted_text = self.data_fetcher.format_data_for_ai(raw_data)
results["formatted_data"] = formatted_text
if progress_callback:
progress_callback(15, "✓ 数据采集完成")
print("✓ 数据采集和格式化完成")
print(f" 数据文本长度: {len(formatted_text)} 字符")
# 阶段2AI智能体分析
print("\n[阶段2] AI智能体分析集群工作中...")
print("-" * 60)
agents_results = {}
# 2.1 康波周期分析师
if progress_callback:
progress_callback(20, "🌊 康波周期分析师正在分析...")
print("1/4 康波周期分析师...")
kondratieff_result = self.agents.kondratieff_wave_agent(formatted_text)
agents_results["kondratieff"] = kondratieff_result
if progress_callback:
progress_callback(35, "✓ 康波分析完成")
# 2.2 美林时钟分析师
if progress_callback:
progress_callback(40, "⏰ 美林时钟分析师正在分析...")
print("2/4 美林时钟分析师...")
merrill_result = self.agents.merrill_lynch_clock_agent(formatted_text)
agents_results["merrill"] = merrill_result
if progress_callback:
progress_callback(55, "✓ 美林时钟分析完成")
# 2.3 中国政策分析师
if progress_callback:
progress_callback(60, "🏛️ 中国政策分析师正在分析...")
print("3/4 中国政策分析师...")
policy_result = self.agents.china_policy_agent(formatted_text)
agents_results["policy"] = policy_result
if progress_callback:
progress_callback(75, "✓ 政策分析完成")
# 2.4 首席宏观策略师(综合三位分析师的报告)
if progress_callback:
progress_callback(80, "👔 首席宏观策略师正在综合研判...")
print("4/4 首席宏观策略师综合研判...")
chief_result = self.agents.chief_macro_strategist_agent(
kondratieff_report=kondratieff_result.get("analysis", ""),
merrill_report=merrill_result.get("analysis", ""),
policy_report=policy_result.get("analysis", ""),
macro_data_text=formatted_text
)
agents_results["chief"] = chief_result
if progress_callback:
progress_callback(95, "✓ 综合研判完成")
results["agents_analysis"] = agents_results
results["success"] = True
print("\n" + "=" * 60)
print("✓ 宏观周期分析完成!")
print("=" * 60)
if progress_callback:
progress_callback(100, "✅ 分析完成!")
except Exception as e:
print(f"\n✗ 分析过程出错: {e}")
import traceback
traceback.print_exc()
results["error"] = str(e)
return results
# 测试
if __name__ == "__main__":
print("=" * 60)
print("测试宏观周期分析引擎")
print("=" * 60)
engine = MacroCycleEngine()
print("引擎初始化完成")
+427
View File
@@ -0,0 +1,427 @@
"""
宏观周期分析 - PDF报告生成模块
生成康波周期 × 美林投资时钟 × 中国政策分析的完整PDF报告
"""
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from datetime import datetime
import os
import tempfile
import re
class MacroCyclePDFGenerator:
"""宏观周期分析PDF报告生成器"""
def __init__(self):
"""初始化PDF生成器"""
self.setup_fonts()
def setup_fonts(self):
"""设置中文字体"""
try:
font_paths = [
'C:/Windows/Fonts/msyh.ttc', # 微软雅黑
'C:/Windows/Fonts/simsun.ttc', # 宋体
'C:/Windows/Fonts/simhei.ttf', # 黑体
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', # Linux
'/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf',
]
for font_path in font_paths:
if os.path.exists(font_path):
try:
pdfmetrics.registerFont(TTFont('ChineseFont', font_path))
self.chinese_font = 'ChineseFont'
print(f"[宏观PDF] 成功加载字体: {font_path}")
return
except:
continue
self.chinese_font = 'Helvetica'
print("[宏观PDF] 警告: 未找到中文字体,使用默认字体")
except Exception as e:
print(f"[宏观PDF] 字体设置失败: {e}")
self.chinese_font = 'Helvetica'
def generate_pdf(self, result_data: dict, output_path: str = None) -> str:
"""
生成宏观周期分析PDF报告
Args:
result_data: 分析结果数据
output_path: 输出路径如果为None则生成临时文件
Returns:
PDF文件路径
"""
try:
if output_path is None:
temp_dir = tempfile.gettempdir()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = os.path.join(temp_dir, f"宏观周期报告_{timestamp}.pdf")
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
rightMargin=0.5*inch,
leftMargin=0.5*inch,
topMargin=0.5*inch,
bottomMargin=0.5*inch
)
story = []
# 1. 标题页
story.extend(self._create_title_page(result_data))
story.append(PageBreak())
# 2. 首席宏观策略师综合研判(最重要,放最前面)
story.extend(self._create_chief_section(result_data))
story.append(PageBreak())
# 3. 康波周期分析
story.extend(self._create_kondratieff_section(result_data))
story.append(PageBreak())
# 4. 美林投资时钟分析
story.extend(self._create_merrill_section(result_data))
story.append(PageBreak())
# 5. 中国政策分析
story.extend(self._create_policy_section(result_data))
# 6. 结束语
story.extend(self._create_ending())
# 生成PDF
doc.build(story)
print(f"[宏观PDF] 报告生成成功: {output_path}")
return output_path
except Exception as e:
print(f"[宏观PDF] 生成失败: {e}")
import traceback
traceback.print_exc()
raise
def _clean_markdown(self, text: str) -> str:
"""清理Markdown标记,转换为适合PDF的纯文本/HTML"""
if not text:
return ""
# 移除markdown粗体 **text** → text
text = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', text)
# 移除markdown斜体 *text* → text
text = re.sub(r'\*(.*?)\*', r'<i>\1</i>', text)
# 移除markdown标题 ## → 空
text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
# 移除markdown链接 [text](url) → text
text = re.sub(r'\[(.*?)\]\(.*?\)', r'\1', text)
# 移除markdown表格分隔线
text = re.sub(r'\|[-:]+\|[-:| ]+\|', '', text)
# 替换换行
text = text.replace('\n', '<br/>')
return text
def _split_text_to_paragraphs(self, text: str, styles: dict, max_chars: int = 0) -> list:
"""将长文本分段为多个Paragraph,避免单段过长溢出"""
elements = []
if not text:
return elements
if max_chars > 0 and len(text) > max_chars:
text = text[:max_chars] + "...(更多内容请查看网页版完整报告)"
# 按段落分割
paragraphs = text.split('\n\n')
for para in paragraphs:
para = para.strip()
if not para:
continue
# 清理markdown
cleaned = self._clean_markdown(para)
if cleaned:
try:
elements.append(Paragraph(cleaned, styles['Small']))
elements.append(Spacer(1, 0.08*inch))
except Exception:
# 如果格式化失败,用纯文本
plain = re.sub(r'<[^>]+>', '', cleaned)
elements.append(Paragraph(plain, styles['Small']))
elements.append(Spacer(1, 0.08*inch))
return elements
def _create_title_page(self, data: dict) -> list:
"""创建标题页"""
styles = self._get_styles()
elements = []
elements.append(Spacer(1, 1.5*inch))
# 主标题
elements.append(Paragraph("宏观周期分析报告", styles['Title']))
elements.append(Spacer(1, 0.3*inch))
# 副标题
elements.append(Paragraph(
"康波周期 × 美林投资时钟 × 中国政策分析",
styles['Heading2']
))
elements.append(Spacer(1, 0.8*inch))
# 报告信息
timestamp = data.get('timestamp', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
info_text = f"""
<para align=center>
<b>生成时间:</b> {timestamp}<br/>
<b>分析框架:</b> 三维周期定位系统<br/>
<b>AI分析师:</b> 4位专业分析师协同研判<br/>
<b>分析维度:</b> 康波长周期 · 美林中短周期 · 中国政策环境<br/>
<b>数据来源:</b> AKShare宏观经济数据
</para>
"""
elements.append(Paragraph(info_text, styles['Normal']))
elements.append(Spacer(1, 0.5*inch))
# 分析师团队
team_text = """
<para align=center>
<b>AI分析师团队:</b><br/>
康波周期分析师 · 美林时钟分析师 · 中国政策分析师 · 首席宏观策略师
</para>
"""
elements.append(Paragraph(team_text, styles['Normal']))
elements.append(Spacer(1, 0.8*inch))
# 免责声明
elements.append(Paragraph(
"<para align=center><i>本报告由AI系统自动生成,仅供学习研究参考,不构成投资建议。<br/>"
"周期理论是认知框架而非精确预测工具。投资有风险,入市需谨慎。</i></para>",
styles['Small']
))
return elements
def _create_chief_section(self, data: dict) -> list:
"""创建首席宏观策略师综合研判部分"""
styles = self._get_styles()
elements = []
elements.append(Paragraph("一、首席宏观策略师 — 综合研判", styles['Heading1']))
elements.append(Spacer(1, 0.15*inch))
elements.append(Paragraph(
"<i>整合康波周期、美林投资时钟、中国政策三个维度,构建周期仪表盘,给出最终综合投资策略。</i>",
styles['Small']
))
elements.append(Spacer(1, 0.15*inch))
chief = data.get('agents_analysis', {}).get('chief', {})
analysis = chief.get('analysis', '暂无分析结果')
elements.extend(self._split_text_to_paragraphs(analysis, styles, max_chars=5000))
return elements
def _create_kondratieff_section(self, data: dict) -> list:
"""创建康波周期分析部分"""
styles = self._get_styles()
elements = []
elements.append(Paragraph("二、康波周期分析 — 60年长周期定位", styles['Heading1']))
elements.append(Spacer(1, 0.15*inch))
elements.append(Paragraph(
"<i>基于康德拉季耶夫长波理论(周金涛\"人生发财靠康波\"),判断当前处于第五轮信息技术康波的阶段位置。</i>",
styles['Small']
))
elements.append(Spacer(1, 0.15*inch))
kondratieff = data.get('agents_analysis', {}).get('kondratieff', {})
analysis = kondratieff.get('analysis', '暂无分析结果')
elements.extend(self._split_text_to_paragraphs(analysis, styles, max_chars=5000))
return elements
def _create_merrill_section(self, data: dict) -> list:
"""创建美林投资时钟分析部分"""
styles = self._get_styles()
elements = []
elements.append(Paragraph("三、美林投资时钟 — 中短周期定位", styles['Heading1']))
elements.append(Spacer(1, 0.15*inch))
elements.append(Paragraph(
"<i>基于经济增长与通胀两大维度,结合中国政策方向(第三维度),判断当前处于美林时钟的哪个象限。</i>",
styles['Small']
))
elements.append(Spacer(1, 0.15*inch))
merrill = data.get('agents_analysis', {}).get('merrill', {})
analysis = merrill.get('analysis', '暂无分析结果')
elements.extend(self._split_text_to_paragraphs(analysis, styles, max_chars=5000))
return elements
def _create_policy_section(self, data: dict) -> list:
"""创建中国政策分析部分"""
styles = self._get_styles()
elements = []
elements.append(Paragraph("四、中国政策环境分析", styles['Heading1']))
elements.append(Spacer(1, 0.15*inch))
elements.append(Paragraph(
"<i>深度分析货币政策、财政政策、产业政策、房地产政策,评估政策对周期的影响和投资机会。</i>",
styles['Small']
))
elements.append(Spacer(1, 0.15*inch))
policy = data.get('agents_analysis', {}).get('policy', {})
analysis = policy.get('analysis', '暂无分析结果')
elements.extend(self._split_text_to_paragraphs(analysis, styles, max_chars=5000))
return elements
def _create_ending(self) -> list:
"""创建结束语"""
styles = self._get_styles()
elements = []
elements.append(Spacer(1, 0.5*inch))
elements.append(Paragraph(
"<para align=center><i>--- 报告结束 ---<br/>"
"本报告由宏观周期AI分析系统自动生成<br/>"
"康波是罗盘,美林是航海图,政策是季风<br/>"
"愿你在经济的海洋中,驶向属于自己的财富彼岸</i></para>",
styles['Normal']
))
return elements
def _get_styles(self) -> dict:
"""获取样式"""
styles = getSampleStyleSheet()
custom_styles = {
'Title': ParagraphStyle(
'MacroTitle',
parent=styles['Title'],
fontName=self.chinese_font,
fontSize=26,
textColor=colors.HexColor('#302b63'),
spaceAfter=30,
alignment=TA_CENTER
),
'Heading1': ParagraphStyle(
'MacroHeading1',
parent=styles['Heading1'],
fontName=self.chinese_font,
fontSize=16,
textColor=colors.HexColor('#0f0c29'),
spaceAfter=12,
spaceBefore=12
),
'Heading2': ParagraphStyle(
'MacroHeading2',
parent=styles['Heading2'],
fontName=self.chinese_font,
fontSize=14,
textColor=colors.HexColor('#302b63'),
spaceAfter=10,
spaceBefore=10,
alignment=TA_CENTER
),
'Normal': ParagraphStyle(
'MacroNormal',
parent=styles['Normal'],
fontName=self.chinese_font,
fontSize=11,
leading=16,
alignment=TA_JUSTIFY
),
'Small': ParagraphStyle(
'MacroSmall',
parent=styles['Normal'],
fontName=self.chinese_font,
fontSize=9,
leading=14,
alignment=TA_LEFT
)
}
return custom_styles
def generate_macro_cycle_markdown(result_data: dict) -> str:
"""生成宏观周期分析的Markdown报告"""
parts = []
timestamp = result_data.get('timestamp', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
parts.append("# 🧭 宏观周期分析报告\n")
parts.append(f"**生成时间**: {timestamp}\n")
parts.append("**分析框架**: 康波周期 × 美林投资时钟 × 中国政策分析\n")
parts.append("---\n")
agents = result_data.get('agents_analysis', {})
# 首席策略师
chief = agents.get('chief', {})
if chief:
parts.append("## 👔 一、首席宏观策略师 — 综合研判\n")
parts.append(chief.get('analysis', '暂无分析结果'))
parts.append("\n\n---\n")
# 康波周期
kondratieff = agents.get('kondratieff', {})
if kondratieff:
parts.append("## 🌊 二、康波周期分析 — 60年长周期定位\n")
parts.append(kondratieff.get('analysis', '暂无分析结果'))
parts.append("\n\n---\n")
# 美林时钟
merrill = agents.get('merrill', {})
if merrill:
parts.append("## ⏰ 三、美林投资时钟 — 中短周期定位\n")
parts.append(merrill.get('analysis', '暂无分析结果'))
parts.append("\n\n---\n")
# 政策分析
policy = agents.get('policy', {})
if policy:
parts.append("## 🏛️ 四、中国政策环境分析\n")
parts.append(policy.get('analysis', '暂无分析结果'))
parts.append("\n\n---\n")
# 免责声明
parts.append("\n> ⚠️ **免责声明**: 本报告由AI系统自动生成,仅供学习研究参考,不构成投资建议。")
parts.append("周期理论是认知框架而非精确预测工具。投资有风险,入市需谨慎。\n")
return "\n".join(parts)
# 测试
if __name__ == "__main__":
print("=" * 60)
print("测试宏观周期PDF生成器")
print("=" * 60)
test_data = {
"success": True,
"timestamp": "2026-02-27 14:00:00",
"agents_analysis": {
"chief": {"analysis": "综合研判测试内容..."},
"kondratieff": {"analysis": "康波分析测试内容..."},
"merrill": {"analysis": "美林时钟测试内容..."},
"policy": {"analysis": "政策分析测试内容..."},
}
}
generator = MacroCyclePDFGenerator()
output_path = generator.generate_pdf(test_data)
print(f"测试PDF生成: {output_path}")
+370
View File
@@ -0,0 +1,370 @@
"""
宏观周期分析 - UI界面模块
展示康波周期 + 美林投资时钟 + 中国政策分析的综合结果
"""
import streamlit as st
import time
from datetime import datetime
from macro_cycle_engine import MacroCycleEngine
from macro_cycle_pdf import MacroCyclePDFGenerator, generate_macro_cycle_markdown
def display_macro_cycle():
"""显示宏观周期分析主界面"""
st.markdown("""
<div style="background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
padding: 2rem; border-radius: 15px; margin-bottom: 1.5rem;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);">
<h1 style="color: #fff; margin: 0; font-size: 2rem;">
🧭 宏观周期分析
</h1>
<p style="color: rgba(255,255,255,0.8); margin: 0.5rem 0 0 0; font-size: 1.1rem;">
康波周期 × 美林投资时钟 × 中国政策分析 AI驱动的宏观经济周期研判
</p>
</div>
""", unsafe_allow_html=True)
# 标签页
tab1, tab2 = st.tabs(["📊 周期分析", "📚 理论介绍"])
with tab1:
display_analysis_tab()
with tab2:
display_theory_tab()
def display_analysis_tab():
"""显示分析标签页"""
# 简介
st.markdown("""
> **分析说明**本模块基于视频[康波周期理论](https://www.bilibili.com/video/BV1QNcEzREzY)50-60年长周期和视频[美林投资时钟](https://www.bilibili.com/video/BV1Zuf5BUEhH)3-5年中短周期
> 结合中国政策环境第三维度由4位AI分析师协同研判当前宏观经济所处的周期位置并给出资产配置建议
""")
st.markdown("""
**🤖 AI分析师团队**
- 🌊 **康波周期分析师** 60年长周期战略定位回升繁荣衰退萧条
- **美林时钟分析师** 3-5年中短周期战术定位复苏过热滞胀衰退
- 🏛 **中国政策分析师** 政策第三维度货币/财政/产业/房地产
- 👔 **首席宏观策略师** 三维综合研判最终资产配置建议
""")
st.markdown("---")
# 操作按钮
col1, col2 = st.columns([2, 2])
with col1:
analyze_button = st.button("🚀 开始宏观周期分析", type="primary", key="macro_analyze")
with col2:
if st.button("🔄 清除结果", key="macro_clear"):
if 'macro_cycle_result' in st.session_state:
del st.session_state.macro_cycle_result
st.success("已清除分析结果")
st.rerun()
st.markdown("---")
# 开始分析
if analyze_button:
if 'macro_cycle_result' in st.session_state:
del st.session_state.macro_cycle_result
run_macro_cycle_analysis()
# 显示结果
if 'macro_cycle_result' in st.session_state:
result = st.session_state.macro_cycle_result
if result.get("success"):
display_analysis_results(result)
else:
st.error(f"❌ 分析失败: {result.get('error', '未知错误')}")
def run_macro_cycle_analysis():
"""运行宏观周期分析"""
import config
model = config.DEFAULT_MODEL_NAME
progress_bar = st.progress(0)
status_text = st.empty()
def progress_callback(pct, text):
progress_bar.progress(pct)
status_text.text(text)
try:
engine = MacroCycleEngine(model=model)
result = engine.run_full_analysis(progress_callback=progress_callback)
if result.get("success"):
st.session_state.macro_cycle_result = result
time.sleep(1)
status_text.empty()
progress_bar.empty()
st.rerun()
else:
st.error(f"❌ 分析失败: {result.get('error', '未知错误')}")
except Exception as e:
st.error(f"❌ 分析过程出错: {str(e)}")
import traceback
st.code(traceback.format_exc())
finally:
progress_bar.empty()
status_text.empty()
def display_analysis_results(result):
"""显示分析结果"""
agents = result.get("agents_analysis", {})
timestamp = result.get("timestamp", "")
st.success(f"✅ 分析完成于 {timestamp}")
# 数据采集状态
data_errors = result.get("data_errors", [])
if data_errors:
with st.expander("⚠️ 部分数据采集失败(不影响分析)"):
for err in data_errors:
st.warning(f"{err}")
# 四个分析师报告
report_tabs = st.tabs([
"👔 综合策略",
"🌊 康波周期",
"⏰ 美林时钟",
"🏛️ 政策分析"
])
# Tab 1: 首席宏观策略师(综合)
with report_tabs[0]:
chief = agents.get("chief", {})
if chief:
st.markdown("""
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 1.2rem; border-radius: 12px; margin-bottom: 1rem;
color: white;">
<h3 style="margin: 0; color: white;">👔 首席宏观策略师 综合研判</h3>
<p style="margin: 0.3rem 0 0 0; opacity: 0.9; font-size: 0.95rem;">
整合康波周期 + 美林投资时钟 + 中国政策三维分析给出最终投资策略
</p>
</div>
""", unsafe_allow_html=True)
st.markdown(chief.get("analysis", "暂无分析结果"))
else:
st.info("暂无综合策略分析结果")
# Tab 2: 康波周期分析师
with report_tabs[1]:
kondratieff = agents.get("kondratieff", {})
if kondratieff:
st.markdown("""
<div style="background: linear-gradient(135deg, #0575E6 0%, #021B79 100%);
padding: 1.2rem; border-radius: 12px; margin-bottom: 1rem;
color: white;">
<h3 style="margin: 0; color: white;">🌊 康波周期分析师 60年长周期定位</h3>
<p style="margin: 0.3rem 0 0 0; opacity: 0.9; font-size: 0.95rem;">
基于康德拉季耶夫长波理论判断当前处于第五轮信息技术康波的哪个阶段
</p>
</div>
""", unsafe_allow_html=True)
st.markdown(kondratieff.get("analysis", "暂无分析结果"))
else:
st.info("暂无康波周期分析结果")
# Tab 3: 美林时钟分析师
with report_tabs[2]:
merrill = agents.get("merrill", {})
if merrill:
st.markdown("""
<div style="background: linear-gradient(135deg, #f5af19 0%, #f12711 100%);
padding: 1.2rem; border-radius: 12px; margin-bottom: 1rem;
color: white;">
<h3 style="margin: 0; color: white;"> 美林投资时钟分析师 中短周期定位</h3>
<p style="margin: 0.3rem 0 0 0; opacity: 0.9; font-size: 0.95rem;">
基于经济增长+通胀+政策三维度判断当前处于美林时钟的哪个象限
</p>
</div>
""", unsafe_allow_html=True)
st.markdown(merrill.get("analysis", "暂无分析结果"))
else:
st.info("暂无美林时钟分析结果")
# Tab 4: 中国政策分析师
with report_tabs[3]:
policy = agents.get("policy", {})
if policy:
st.markdown("""
<div style="background: linear-gradient(135deg, #e53935 0%, #b71c1c 100%);
padding: 1.2rem; border-radius: 12px; margin-bottom: 1rem;
color: white;">
<h3 style="margin: 0; color: white;">🏛 中国政策分析师 政策第三维度</h3>
<p style="margin: 0.3rem 0 0 0; opacity: 0.9; font-size: 0.95rem;">
深度分析货币政策财政政策产业政策房地产政策对周期和投资的影响
</p>
</div>
""", unsafe_allow_html=True)
st.markdown(policy.get("analysis", "暂无分析结果"))
else:
st.info("暂无政策分析结果")
# 导出报告
st.markdown("---")
display_pdf_export_section(result)
def display_pdf_export_section(result):
"""显示PDF/Markdown导出部分"""
st.subheader("📄 导出报告")
col1, col2, col3, col4 = st.columns([2, 1, 1, 1])
with col1:
st.write("将宏观周期分析报告导出为PDF或Markdown文件,方便保存和分享")
with col2:
if st.button("📥 生成PDF报告", type="primary", width='content', key="macro_pdf_gen"):
with st.spinner("正在生成PDF报告..."):
try:
generator = MacroCyclePDFGenerator()
pdf_path = generator.generate_pdf(result)
with open(pdf_path, "rb") as f:
pdf_bytes = f.read()
st.session_state.macro_pdf_data = pdf_bytes
ts = result.get('timestamp', datetime.now().strftime('%Y%m%d_%H%M%S')).replace(':', '').replace(' ', '_')
st.session_state.macro_pdf_filename = f"宏观周期报告_{ts}.pdf"
st.success("✅ PDF报告生成成功!")
st.rerun()
except Exception as e:
st.error(f"❌ PDF生成失败: {str(e)}")
with col3:
if st.button("📝 生成Markdown", type="secondary", width='content', key="macro_md_gen"):
with st.spinner("正在生成Markdown报告..."):
try:
markdown_content = generate_macro_cycle_markdown(result)
st.session_state.macro_md_data = markdown_content
ts = result.get('timestamp', datetime.now().strftime('%Y%m%d_%H%M%S')).replace(':', '').replace(' ', '_')
st.session_state.macro_md_filename = f"宏观周期报告_{ts}.md"
st.success("✅ Markdown报告生成成功!")
st.rerun()
except Exception as e:
st.error(f"❌ Markdown生成失败: {str(e)}")
with col4:
if 'macro_pdf_data' in st.session_state:
st.download_button(
label="💾 下载PDF",
data=st.session_state.macro_pdf_data,
file_name=st.session_state.macro_pdf_filename,
mime="application/pdf",
width='content',
key="macro_pdf_dl"
)
if 'macro_md_data' in st.session_state:
st.download_button(
label="💾 下载Markdown",
data=st.session_state.macro_md_data,
file_name=st.session_state.macro_md_filename,
mime="text/markdown",
width='content',
key="macro_md_dl"
)
def display_theory_tab():
"""显示理论介绍标签页"""
st.markdown("""
## 📖 两大周期理论简介
---
### 🌊 康德拉季耶夫长波(康波周期)
**创始人**苏联经济学家尼古拉·康德拉季耶夫1920s
**中国推广者**周金涛"周期天王"中信建投首席经济学家
**核心思想**资本主义经济存在约 **50-60** 的超长周期由重大技术革命驱动
**四个阶段**
| 阶段 | 持续时间 | 特征 | 最优资产 |
|------|---------|------|---------|
| 🌱 **回升期** | ~15 | 新技术商业化经济从底部爬起 | 股票新兴产业 |
| **繁荣期** | ~15 | 技术全面铺开高速增长 | 几乎所有资产 |
| 🍂 **衰退期** | ~10 | 泡沫破裂增速放缓 | 大宗商品现金 |
| **萧条期** | ~10 | 全面收缩资产便宜 | 现金布局未来 |
**历史五轮康波**
1. **1780s-1840s**蒸汽机革命
2. **1840s-1890s**铁路与钢铁
3. **1890s-1940s**电力与化工
4. **1940s-1990s**汽车与计算机
5. **1990s-2050s?**信息技术革命当前
> *"人生发财靠康波。每个人的财富积累,一定不要以为是你多有本事,财富积累完全来源于经济周期运动的时间给你的机会。"* 周金涛
---
### ⏰ 美林投资时钟
**创始人**美林证券分析师2004
**核心指标**经济增长 × 通货膨胀
**四个象限**
| 象限 | 经济 | 通胀 | 最优资产 | 典型特征 |
|------|------|------|---------|---------|
| 🟢 **复苏期** | | | **股票** | 盈利改善利率低 |
| 🔴 **过热期** | | | **大宗商品** | 需求旺盛加息 |
| 🟡 **滞胀期** | | | **现金** | 成本上升利润缩水 |
| 🔵 **衰退期** | | | **债券** | 降息避险需求 |
**中国化改造**
- 增加 **政策方向** 作为第三维度
- 缩短时钟转动周期中国约1-3年一轮美国3-5
- 增加 **房地产** 作为第五类资产
- 重视 **政策友好型** 资产
---
### 🤝 两大理论的结合使用
| 维度 | 康波周期 | 美林时钟 |
|------|---------|---------|
| **时间尺度** | 50-60 | 3-5 |
| **驱动力** | 技术革命供给侧 | 增长+通胀需求侧 |
| **用途** | 人生战略决策 | 投资组合调整 |
| **比喻** | 🔭 望远镜 | 🔬 显微镜 |
| **角色** | 罗盘大方向 | 航海图风浪变化 |
**结合方法**
- 康波定 **战略方向**进攻/防守
- 美林定 **战术节奏**具体配什么
- 政策作为 **催化剂**加速/扭曲周期
> *"双指针一致时信心更强,矛盾时要谨慎。"*
---
### ⚠️ 免责声明
本分析仅供学习研究参考不构成任何投资建议周期理论是认知框架而非精确预测工具
投资有风险入市需谨慎
""")
# 主入口
if __name__ == "__main__":
display_macro_cycle()
+4 -3
View File
@@ -13,15 +13,16 @@ from ai_agents import StockAnalysisAgents
from deepseek_client import DeepSeekClient from deepseek_client import DeepSeekClient
import time import time
import json import json
import config
class MainForceAnalyzer: class MainForceAnalyzer:
"""主力选股分析器 - 批量整体分析""" """主力选股分析器 - 批量整体分析"""
def __init__(self, model='deepseek-chat'): def __init__(self, model=None):
self.selector = main_force_selector self.selector = main_force_selector
self.fetcher = StockDataFetcher() self.fetcher = StockDataFetcher()
self.model = model self.model = model or config.DEFAULT_MODEL_NAME
self.agents = StockAnalysisAgents(model=model) self.agents = StockAnalysisAgents(model=self.model)
self.deepseek_client = self.agents.deepseek_client self.deepseek_client = self.agents.deepseek_client
self.raw_stocks = None self.raw_stocks = None
self.final_recommendations = [] self.final_recommendations = []
+2 -2
View File
@@ -11,6 +11,7 @@ import pywencai
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
import time import time
from http_timeout import call_with_timeout
class MainForceStockSelector: class MainForceStockSelector:
"""主力选股类""" """主力选股类"""
@@ -71,7 +72,7 @@ class MainForceStockSelector:
print(f"查询语句: {query[:100]}...") print(f"查询语句: {query[:100]}...")
try: try:
result = pywencai.get(query=query, loop=True) result = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
if result is None: if result is None:
print(f" ⚠️ 方案{i}返回None,尝试下一个方案") print(f" ⚠️ 方案{i}返回None,尝试下一个方案")
@@ -387,4 +388,3 @@ class MainForceStockSelector:
# 全局实例 # 全局实例
main_force_selector = MainForceStockSelector() main_force_selector = MainForceStockSelector()
+5 -14
View File
@@ -127,25 +127,15 @@ def display_main_force_selector():
step=100.0 step=100.0
) )
# 模型选择
# 导入model_config.py中定义的model_options
from model_config import model_options as app_model_options
model = st.selectbox(
"选择AI模型",
list(app_model_options.keys()),
format_func=lambda x: app_model_options[x],
help="deepseek-chat速度快,deepseek-reasoner推理能力强"
)
st.markdown("---") st.markdown("---")
# 开始分析按钮 # 开始分析按钮(使用.env中配置的默认模型)
if st.button("🚀 开始主力选股", type="primary", width='content'): if st.button("🚀 开始主力选股", type="primary", width='content'):
with st.spinner("正在获取数据并分析,这可能需要几分钟..."): with st.spinner("正在获取数据并分析,这可能需要几分钟..."):
# 创建分析器 # 创建分析器(使用默认模型)
analyzer = MainForceAnalyzer(model=model) analyzer = MainForceAnalyzer()
# 运行分析 # 运行分析
result = analyzer.run_full_analysis( result = analyzer.run_full_analysis(
@@ -613,7 +603,8 @@ def run_main_force_batch_analysis():
'sentiment': False, # 禁用以提升速度 'sentiment': False, # 禁用以提升速度
'news': False # 禁用以提升速度 'news': False # 禁用以提升速度
} }
selected_model = 'deepseek-chat' import config
selected_model = config.DEFAULT_MODEL_NAME
period = '1y' period = '1y'
# 创建进度显示 # 创建进度显示
+160 -86
View File
@@ -329,11 +329,56 @@ class MarketSentimentDataFetcher:
} }
def _get_turnover_rate(self, symbol): def _get_turnover_rate(self, symbol):
"""获取换手率数据(支持akshare和tushare自动切换""" """获取换手率数据(优先tushare,失败时使用akshare"""
try: try:
# 优先使用akshare获取最近的换手率数据 # 优先使用tusharedaily_basic,取最近10个交易日保证非交易日也有数据
print(f" [Akshare] 正在获取换手率数据...") if data_source_manager.tushare_available:
# 获取A股实时行情数据(不需要参数) try:
print(f" [Tushare] 正在获取换手率数据(主要数据源)...")
ts_code = data_source_manager._convert_to_ts_code(symbol)
end_date = datetime.now().strftime('%Y%m%d')
start_date = (datetime.now() - timedelta(days=10)).strftime('%Y%m%d')
df = data_source_manager.tushare_api.daily_basic(
ts_code=ts_code,
start_date=start_date,
end_date=end_date
)
if df is not None and not df.empty:
row = df.iloc[0]
turnover_rate = row.get('turnover_rate', 'N/A')
# 解读换手率
interpretation = ""
if turnover_rate != 'N/A':
try:
turnover = float(turnover_rate)
if turnover > 20:
interpretation = "换手率极高(>20%),资金活跃度极高,可能存在炒作"
elif turnover > 10:
interpretation = "换手率较高(>10%),交易活跃"
elif turnover > 5:
interpretation = "换手率正常(5%-10%),交易适中"
elif turnover > 2:
interpretation = "换手率偏低(2%-5%),交易相对清淡"
else:
interpretation = "换手率很低(<2%),交易清淡"
except:
pass
print(f" [Tushare] ✅ 成功获取换手率: {turnover_rate}%")
return {
"current_turnover_rate": turnover_rate,
"interpretation": interpretation
}
else:
print(f" [Tushare] ❌ 未获取到换手率,尝试备用数据源")
except Exception as te:
print(f" [Tushare] ❌ 获取失败: {te}")
# tushare失败,回退akshare
print(f" [Akshare] 正在获取换手率数据(备用数据源)...")
df = ak.stock_zh_a_spot_em() df = ak.stock_zh_a_spot_em()
if df is not None and not df.empty: if df is not None and not df.empty:
stock_data = df[df['代码'] == symbol] stock_data = df[df['代码'] == symbol]
@@ -367,56 +412,41 @@ class MarketSentimentDataFetcher:
except Exception as e: except Exception as e:
print(f" [Akshare] ❌ 获取换手率失败: {e}") print(f" [Akshare] ❌ 获取换手率失败: {e}")
# akshare失败,尝试tushare
if data_source_manager.tushare_available:
try:
print(f" [Tushare] 正在获取换手率数据(备用数据源)...")
ts_code = data_source_manager._convert_to_ts_code(symbol)
# 获取最近一个交易日的数据
df = data_source_manager.tushare_api.daily_basic(
ts_code=ts_code,
trade_date=datetime.now().strftime('%Y%m%d')
)
if df is not None and not df.empty:
row = df.iloc[0]
turnover_rate = row.get('turnover_rate', 'N/A')
# 解读换手率
interpretation = ""
if turnover_rate != 'N/A':
try:
turnover = float(turnover_rate)
if turnover > 20:
interpretation = "换手率极高(>20%),资金活跃度极高,可能存在炒作"
elif turnover > 10:
interpretation = "换手率较高(>10%),交易活跃"
elif turnover > 5:
interpretation = "换手率正常(5%-10%),交易适中"
elif turnover > 2:
interpretation = "换手率偏低(2%-5%),交易相对清淡"
else:
interpretation = "换手率很低(<2%),交易清淡"
except:
pass
print(f" [Tushare] ✅ 成功获取换手率: {turnover_rate}%")
return {
"current_turnover_rate": turnover_rate,
"interpretation": interpretation
}
except Exception as te:
print(f" [Tushare] ❌ 获取失败: {te}")
return None return None
def _get_market_index_sentiment(self): def _get_market_index_sentiment(self):
"""获取大盘指数情绪(支持akshare和tushare自动切换)""" """获取大盘指数情绪(支持akshare和tushare自动切换)"""
try: try:
# 优先使用akshare获取上证指数实时数据 # 优先使用tushareindex_daily,取最近10个交易日保证非交易日也有数据
print(f" [Akshare] 正在获取大盘指数数据...") if data_source_manager.tushare_available:
# 使用正确的symbol参数 try:
print(f" [Tushare] 正在获取大盘指数数据(主要数据源)...")
# 获取上证指数数据
end_date = datetime.now().strftime('%Y%m%d')
start_date = (datetime.now() - timedelta(days=10)).strftime('%Y%m%d')
df = data_source_manager.tushare_api.index_daily(
ts_code='000001.SH',
start_date=start_date,
end_date=end_date
)
if df is not None and not df.empty:
row = df.iloc[0]
change_pct = row.get('pct_chg', 0)
print(f" [Tushare] ✅ 成功获取大盘指数涨跌幅: {change_pct}%")
return {
"index_name": "上证指数",
"change_percent": change_pct
}
else:
print(f" [Tushare] ❌ 未获取到大盘指数,尝试备用数据源")
except Exception as te:
print(f" [Tushare] ❌ 获取失败: {te}")
# tushare失败,回退akshare
print(f" [Akshare] 正在获取大盘指数数据(备用数据源)...")
df = ak.stock_zh_index_spot_em(symbol="上证系列指数") df = ak.stock_zh_index_spot_em(symbol="上证系列指数")
if df is not None and not df.empty: if df is not None and not df.empty:
# 查找上证指数(代码为000001 # 查找上证指数(代码为000001
@@ -471,30 +501,6 @@ class MarketSentimentDataFetcher:
except Exception as e: except Exception as e:
print(f" [Akshare] ❌ 获取大盘指数失败: {e}") print(f" [Akshare] ❌ 获取大盘指数失败: {e}")
# akshare失败,尝试tushare
if data_source_manager.tushare_available:
try:
print(f" [Tushare] 正在获取大盘指数数据(备用数据源)...")
# 获取上证指数数据
df = data_source_manager.tushare_api.index_daily(
ts_code='000001.SH',
start_date=datetime.now().strftime('%Y%m%d'),
end_date=datetime.now().strftime('%Y%m%d')
)
if df is not None and not df.empty:
row = df.iloc[0]
change_pct = row.get('pct_chg', 0)
print(f" [Tushare] ✅ 成功获取大盘指数涨跌幅: {change_pct}%")
return {
"index_name": "上证指数",
"change_percent": change_pct
}
except Exception as te:
print(f" [Tushare] ❌ 获取失败: {te}")
return None return None
def _get_limit_up_down_stats(self): def _get_limit_up_down_stats(self):
@@ -503,19 +509,50 @@ class MarketSentimentDataFetcher:
# 获取今日涨停和跌停统计 # 获取今日涨停和跌停统计
today = datetime.now().strftime('%Y%m%d') today = datetime.now().strftime('%Y%m%d')
# 获取涨停股票 limit_up_count = 0
try: limit_down_count = 0
limit_up_df = ak.stock_zt_pool_em(date=today)
limit_up_count = len(limit_up_df) if limit_up_df is not None and not limit_up_df.empty else 0
except:
limit_up_count = 0
# 获取跌停股票 # 优先使用tushare的涨跌停列表
try: if data_source_manager.tushare_available:
limit_down_df = ak.stock_zt_pool_dtgc_em(date=today) try:
limit_down_count = len(limit_down_df) if limit_down_df is not None and not limit_down_df.empty else 0 print(f" [Tushare] 正在获取涨跌停数据(主要数据源)...")
except: df_ll = data_source_manager.tushare_api.limit_list_d(trade_date=today)
limit_down_count = 0 if df_ll is not None and not df_ll.empty:
if 'limit_type' in df_ll.columns:
limit_up_count = int((df_ll['limit_type'].fillna('') == 'U').sum())
limit_down_count = int((df_ll['limit_type'].fillna('') == 'D').sum())
print(f" [Tushare] ✅ 成功获取涨跌停: 涨停{limit_up_count} / 跌停{limit_down_count}")
elif 'pct_chg' in df_ll.columns:
limit_up_count = int((df_ll['pct_chg'] >= 9.5).sum())
limit_down_count = int((df_ll['pct_chg'] <= -9.5).sum())
print(f" [Tushare] ✅ 成功获取涨跌停: 涨停{limit_up_count} / 跌停{limit_down_count}")
elif '涨跌幅' in df_ll.columns:
limit_up_count = int((df_ll['涨跌幅'] >= 9.5).sum())
limit_down_count = int((df_ll['涨跌幅'] <= -9.5).sum())
print(f" [Tushare] ✅ 成功获取涨跌停: 涨停{limit_up_count} / 跌停{limit_down_count}")
else:
# 无法识别的列结构,按0处理并回退akshare
print(f" [Tushare] ⚠ 涨跌停返回列无法识别: {list(df_ll.columns)[:10]},尝试备用数据源")
else:
print(f" [Tushare] ❌ 未获取到涨跌停数据,尝试备用数据源")
except Exception as e:
print(f" [Tushare] ❌ 获取涨跌停数据失败: {e}")
# tushare不可用或失败时,回退akshare
if limit_up_count == 0 and limit_down_count == 0:
# 获取涨停股票
try:
limit_up_df = ak.stock_zt_pool_em(date=today)
limit_up_count = len(limit_up_df) if limit_up_df is not None and not limit_up_df.empty else 0
except:
limit_up_count = 0
# 获取跌停股票
try:
limit_down_df = ak.stock_zt_pool_dtgc_em(date=today)
limit_down_count = len(limit_down_df) if limit_down_df is not None and not limit_down_df.empty else 0
except:
limit_down_count = 0
# 计算涨跌停比例 # 计算涨跌停比例
if limit_up_count + limit_down_count > 0: if limit_up_count + limit_down_count > 0:
@@ -549,6 +586,44 @@ class MarketSentimentDataFetcher:
def _get_margin_trading_data(self, symbol): def _get_margin_trading_data(self, symbol):
"""获取融资融券数据""" """获取融资融券数据"""
try: try:
# 优先使用tushare的个股融资融券明细
if data_source_manager.tushare_available:
try:
print(f" [Tushare] 正在获取融资融券数据(主要数据源)...")
ts_code = data_source_manager._convert_to_ts_code(symbol)
end_date = datetime.now().strftime('%Y%m%d')
start_date = (datetime.now() - timedelta(days=15)).strftime('%Y%m%d')
df = data_source_manager.tushare_api.margin_detail(
ts_code=ts_code,
start_date=start_date,
end_date=end_date
)
if df is not None and not df.empty:
latest = df.iloc[0]
margin_balance = latest.get('rzye', 0) or 0
short_balance = latest.get('rqye', 0) or 0
# 解读融资融券
interpretation = []
if margin_balance > short_balance * 10:
interpretation.append("融资余额远大于融券余额,投资者看多情绪强")
elif margin_balance > short_balance * 3:
interpretation.append("融资余额大于融券余额,投资者偏看多")
else:
interpretation.append("融资融券相对平衡")
print(f" [Tushare] ✅ 成功获取融资融券数据")
return {
"margin_balance": margin_balance,
"short_balance": short_balance,
"interpretation": interpretation,
"date": str(latest.get('trade_date', datetime.now().strftime('%Y-%m-%d')))
}
else:
print(f" [Tushare] ❌ 未获取到融资融券数据,尝试备用数据源")
except Exception as e:
print(f" [Tushare] ❌ 获取融资融券数据失败: {e}")
# 获取个股融资融券数据(尝试多个API) # 获取个股融资融券数据(尝试多个API)
try: try:
# 方法1:获取沪深融资融券明细 # 方法1:获取沪深融资融券明细
@@ -762,4 +837,3 @@ if __name__ == "__main__":
print(formatted_text) print(formatted_text)
else: else:
print(f"\n获取失败: {sentiment_data.get('error', '未知错误')}") print(f"\n获取失败: {sentiment_data.get('error', '未知错误')}")
+30 -3
View File
@@ -1,10 +1,13 @@
""" """
模型配置文件 模型配置文件
包含所有可用的AI模型选项 包含所有可用的AI模型选项
支持通过 .env 中的 DEFAULT_MODEL_NAME 自定义默认模型
""" """
import config
model_options = { # 预置模型列表(用户可以在UI中选择)
"deepseek-chat": "DeepSeek Chat (默认)", _preset_models = {
"deepseek-chat": "DeepSeek Chat",
"deepseek-reasoner": "DeepSeek Reasoner (推理增强)", "deepseek-reasoner": "DeepSeek Reasoner (推理增强)",
"qwen-plus": "qwen-plus (阿里百炼)", "qwen-plus": "qwen-plus (阿里百炼)",
"qwen-plus-latest": "qwen-plus-latest (阿里百炼)", "qwen-plus-latest": "qwen-plus-latest (阿里百炼)",
@@ -20,5 +23,29 @@ model_options = {
"zai-org/GLM-4.6": "智谱(硅基流动)", "zai-org/GLM-4.6": "智谱(硅基流动)",
"moonshotai/Kimi-K2-Instruct-0905": "Kimi (硅基流动)", "moonshotai/Kimi-K2-Instruct-0905": "Kimi (硅基流动)",
"Ring-1T": "蚂蚁百灵 (硅基流动)", "Ring-1T": "蚂蚁百灵 (硅基流动)",
"step3": "阶跃星辰(硅基流动)" "step3": "阶跃星辰(硅基流动)",
} }
# 获取 .env 中配置的默认模型名称
_default_model = config.DEFAULT_MODEL_NAME
# 如果 .env 中配置的默认模型不在预置列表中,自动将其加入列表首位
if _default_model and _default_model not in _preset_models:
_preset_models = {_default_model: f"{_default_model} (自定义默认)"} | _preset_models
# 确保默认模型的显示名称带有 "(默认)" 标记
if _default_model in _preset_models:
original_label = _preset_models[_default_model]
if "(默认)" not in original_label:
_preset_models[_default_model] = f"{original_label} (默认)"
# 导出模型选项字典,确保默认模型排在第一位
model_options = {}
if _default_model in _preset_models:
model_options[_default_model] = _preset_models[_default_model]
for k, v in _preset_models.items():
if k not in model_options:
model_options[k] = v
# 导出默认模型名称,供其他模块使用
default_model_name = _default_model
+3 -3
View File
@@ -9,6 +9,7 @@ import sys
import io import io
import warnings import warnings
from datetime import datetime from datetime import datetime
from http_timeout import call_with_timeout
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
@@ -100,7 +101,7 @@ class NewsAnnouncementDataFetcher:
print(f" 使用问财查询: {query}") print(f" 使用问财查询: {query}")
# 使用pywencai查询 # 使用pywencai查询
result = pywencai.get(query=query, loop=True) result = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
if result is None: if result is None:
print(f" 问财查询返回None") print(f" 问财查询返回None")
@@ -191,7 +192,7 @@ class NewsAnnouncementDataFetcher:
print(f" 使用问财查询: {query}") print(f" 使用问财查询: {query}")
# 使用pywencai查询 # 使用pywencai查询
result = pywencai.get(query=query, loop=True) result = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
if result is None: if result is None:
print(f" 问财查询返回None") print(f" 问财查询返回None")
@@ -342,4 +343,3 @@ if __name__ == "__main__":
print(formatted_text) print(formatted_text)
else: else:
print(f"\n获取失败: {data.get('error', '未知错误')}") print(f"\n获取失败: {data.get('error', '未知错误')}")
BIN
View File
Binary file not shown.
+4 -3
View File
@@ -16,14 +16,15 @@ logger = logging.getLogger(__name__)
class NewsFlowAgents: class NewsFlowAgents:
"""新闻流量智能分析代理""" """新闻流量智能分析代理"""
def __init__(self, model: str = "deepseek-chat"): def __init__(self, model: str = None):
""" """
初始化代理 初始化代理
Args: Args:
model: 使用的模型默认 deepseek-chat model: 使用的模型默认 .env DEFAULT_MODEL_NAME 读取
""" """
self.model = model import config
self.model = model or config.DEFAULT_MODEL_NAME
self.deepseek_client = None self.deepseek_client = None
self._init_client() self._init_client()
+1 -1
View File
@@ -766,7 +766,7 @@ class NewsFlowDatabase:
analysis_data.get('confidence', 50), analysis_data.get('confidence', 50),
analysis_data.get('summary', ''), analysis_data.get('summary', ''),
analysis_data.get('raw_response', ''), analysis_data.get('raw_response', ''),
analysis_data.get('model_used', 'deepseek-chat'), analysis_data.get('model_used', 'unknown'),
analysis_data.get('analysis_time', 0) analysis_data.get('analysis_time', 0)
)) ))
+1 -1
View File
@@ -250,7 +250,7 @@ class NewsFlowEngine:
'advice': ai_analysis.get('investment_advice', {}).get('advice', '观望'), 'advice': ai_analysis.get('investment_advice', {}).get('advice', '观望'),
'confidence': ai_analysis.get('investment_advice', {}).get('confidence', 50), 'confidence': ai_analysis.get('investment_advice', {}).get('confidence', 50),
'summary': ai_analysis.get('investment_advice', {}).get('summary', ''), 'summary': ai_analysis.get('investment_advice', {}).get('summary', ''),
'model_used': 'deepseek-chat', 'model_used': getattr(self, 'model', 'unknown'),
'analysis_time': ai_analysis.get('analysis_time', 0), 'analysis_time': ai_analysis.get('analysis_time', 0),
} }
self.db.save_ai_analysis(quick_result['snapshot_id'], ai_record) self.db.save_ai_analysis(quick_result['snapshot_id'], ai_record)
+4 -3
View File
@@ -11,19 +11,20 @@ from datetime import datetime
# 导入必要的模块 # 导入必要的模块
from portfolio_db import portfolio_db from portfolio_db import portfolio_db
import config
class PortfolioManager: class PortfolioManager:
"""持仓管理器类""" """持仓管理器类"""
def __init__(self, model="deepseek-chat"): def __init__(self, model=None):
""" """
初始化持仓管理器 初始化持仓管理器
Args: Args:
model: AI模型deepseek-chat deepseek-reasoner model: AI模型名称默认从 .env DEFAULT_MODEL_NAME 读取
""" """
self.model = model self.model = model or config.DEFAULT_MODEL_NAME
self.db = portfolio_db self.db = portfolio_db
# ==================== 持仓股票管理 ==================== # ==================== 持仓股票管理 ====================
+2 -1
View File
@@ -8,6 +8,7 @@
import logging import logging
from typing import Tuple, Optional from typing import Tuple, Optional
import pandas as pd import pandas as pd
from http_timeout import call_with_timeout
class ProfitGrowthSelector: class ProfitGrowthSelector:
@@ -50,7 +51,7 @@ class ProfitGrowthSelector:
self.logger.info(f"开始执行净利增长选股,查询条件: {query}") self.logger.info(f"开始执行净利增长选股,查询条件: {query}")
# 调用pywencai # 调用pywencai
result = pywencai.get(query=query, loop=True) result = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
if result is None or result.empty: if result is None or result.empty:
self.logger.warning("未获取到符合条件的股票") self.logger.warning("未获取到符合条件的股票")
+84 -7
View File
@@ -1,6 +1,6 @@
""" """
新闻数据获取模块 新闻数据获取模块
使用akshare获取股票的最新新闻信息替代qstock 优先使用tushare失败时使用akshare获取股票的最新新闻信息
""" """
import pandas as pd import pandas as pd
@@ -9,6 +9,7 @@ import io
import warnings import warnings
from datetime import datetime, timedelta from datetime import datetime, timedelta
import akshare as ak import akshare as ak
from data_source_manager import data_source_manager
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
@@ -30,6 +31,9 @@ def _setup_stdout_encoding():
_setup_stdout_encoding() _setup_stdout_encoding()
# 记录tushare news接口是否无权限(避免每次分析都重复请求失败)
_tushare_news_unavailable = False
class QStockNewsDataFetcher: class QStockNewsDataFetcher:
"""新闻数据获取类(使用akshare作为数据源)""" """新闻数据获取类(使用akshare作为数据源)"""
@@ -37,7 +41,7 @@ class QStockNewsDataFetcher:
def __init__(self): def __init__(self):
self.max_items = 30 # 最多获取的新闻数量 self.max_items = 30 # 最多获取的新闻数量
self.available = True self.available = True
print("✓ 新闻数据获取器初始化成功(akshare数据源") print("✓ 新闻数据获取器初始化成功(tushare优先/akshare备用")
def get_stock_news(self, symbol): def get_stock_news(self, symbol):
""" """
@@ -67,7 +71,7 @@ class QStockNewsDataFetcher:
try: try:
# 获取新闻数据 # 获取新闻数据
print(f"📰 正在使用qstock获取 {symbol} 的最新新闻...") print(f"📰 正在获取 {symbol} 的最新新闻...")
news_data = self._get_news_data(symbol) news_data = self._get_news_data(symbol)
if news_data: if news_data:
@@ -89,9 +93,20 @@ class QStockNewsDataFetcher:
return symbol.isdigit() and len(symbol) == 6 return symbol.isdigit() and len(symbol) == 6
def _get_news_data(self, symbol): def _get_news_data(self, symbol):
"""获取新闻数据(使用akshare""" """获取新闻数据(优先tushare,失败时使用akshare"""
try: try:
print(f" 使用 akshare 获取新闻...") # 优先使用tushare新闻接口
tushare_items = self._get_news_from_tushare(symbol)
if tushare_items:
print(f" ✓ 从tushare获取到 {len(tushare_items)} 条相关新闻")
return {
"items": tushare_items,
"count": len(tushare_items),
"query_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
"date_range": "最近新闻"
}
print(f" 使用 akshare 获取新闻(备用数据源)...")
news_items = [] news_items = []
@@ -223,6 +238,69 @@ class QStockNewsDataFetcher:
traceback.print_exc() traceback.print_exc()
return None return None
def _get_news_from_tushare(self, symbol):
"""从tushare获取个股新闻(按股票名称/代码过滤)"""
global _tushare_news_unavailable
try:
if _tushare_news_unavailable or not data_source_manager.tushare_available:
return None
# 获取股票名称
stock_name = None
try:
basic = data_source_manager.get_stock_basic_info(symbol)
if basic and basic.get('name') and basic['name'] != '未知':
stock_name = basic['name']
except Exception as e:
print(f" 获取股票名称失败: {e}")
# 查询最近7天的全市场新闻(东方财富源)
end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
df = data_source_manager.tushare_api.news(
src='eastmoney',
start_date=start_date,
end_date=end_date
)
if df is None or df.empty:
return None
# 按股票代码或名称过滤
mask = df['title'].str.contains(symbol, na=False) | df['title'].str.contains(stock_name, na=False) if stock_name else df['title'].str.contains(symbol, na=False)
if 'content' in df.columns:
mask = mask | df['content'].str.contains(symbol, na=False)
if stock_name:
mask = mask | df['content'].str.contains(stock_name, na=False)
df_filtered = df[mask]
if df_filtered.empty:
return None
news_items = []
for _, row in df_filtered.head(self.max_items).iterrows():
item = {'source': 'tushare-东方财富'}
for col in ['title', 'content', 'pub_time']:
if col in df_filtered.columns:
value = row.get(col)
if value is None or (isinstance(value, float) and pd.isna(value)):
continue
try:
item[col] = str(value)
except:
item[col] = "无法解析"
if len(item) > 1:
news_items.append(item)
return news_items or None
except Exception as e:
error_msg = str(e)
if "权限" in error_msg or "积分" in error_msg:
_tushare_news_unavailable = True
print(" ⚠ tushare news 接口需要较高积分,当前账号无权限,已自动使用 akshare 获取新闻")
else:
print(f" ⚠ 从tushare获取新闻失败: {error_msg}")
return None
def format_news_for_ai(self, data): def format_news_for_ai(self, data):
""" """
将新闻数据格式化为适合AI阅读的文本 将新闻数据格式化为适合AI阅读的文本
@@ -236,7 +314,7 @@ class QStockNewsDataFetcher:
if data.get("news_data"): if data.get("news_data"):
news_data = data["news_data"] news_data = data["news_data"]
text_parts.append(f""" text_parts.append(f"""
最新新闻 - akshare数据源 最新新闻 - tushare/akshare自动切换
查询时间{news_data.get('query_time', 'N/A')} 查询时间{news_data.get('query_time', 'N/A')}
时间范围{news_data.get('date_range', 'N/A')} 时间范围{news_data.get('date_range', 'N/A')}
新闻数量{news_data.get('count', 0)} 新闻数量{news_data.get('count', 0)}
@@ -303,4 +381,3 @@ if __name__ == "__main__":
print(f"\n获取失败: {data.get('error', '未知错误')}") print(f"\n获取失败: {data.get('error', '未知错误')}")
print("\n") print("\n")
+174 -14
View File
@@ -9,6 +9,8 @@ import io
import warnings import warnings
from datetime import datetime from datetime import datetime
import akshare as ak import akshare as ak
from http_timeout import call_with_timeout
from data_source_manager import data_source_manager
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
@@ -67,26 +69,34 @@ class QuarterlyReportDataFetcher:
try: try:
print(f"📊 正在获取 {symbol} 的季报数据...") print(f"📊 正在获取 {symbol} 的季报数据...")
# 获取利润表 # 获取利润表(优先tushare,失败时回退akshare
income_data = self._get_income_statement(symbol) income_data = self._get_income_statement_from_tushare(symbol)
if income_data is None:
income_data = self._get_income_statement(symbol)
if income_data: if income_data:
data["income_statement"] = income_data data["income_statement"] = income_data
print(f" ✓ 成功获取 {len(income_data.get('data', []))} 期利润表数据") print(f" ✓ 成功获取 {len(income_data.get('data', []))} 期利润表数据")
# 获取资产负债表 # 获取资产负债表(优先tushare,失败时回退akshare
balance_data = self._get_balance_sheet(symbol) balance_data = self._get_balance_sheet_from_tushare(symbol)
if balance_data is None:
balance_data = self._get_balance_sheet(symbol)
if balance_data: if balance_data:
data["balance_sheet"] = balance_data data["balance_sheet"] = balance_data
print(f" ✓ 成功获取 {len(balance_data.get('data', []))} 期资产负债表数据") print(f" ✓ 成功获取 {len(balance_data.get('data', []))} 期资产负债表数据")
# 获取现金流量表 # 获取现金流量表(优先tushare,失败时回退akshare
cash_flow_data = self._get_cash_flow(symbol) cash_flow_data = self._get_cash_flow_from_tushare(symbol)
if cash_flow_data is None:
cash_flow_data = self._get_cash_flow(symbol)
if cash_flow_data: if cash_flow_data:
data["cash_flow"] = cash_flow_data data["cash_flow"] = cash_flow_data
print(f" ✓ 成功获取 {len(cash_flow_data.get('data', []))} 期现金流量表数据") print(f" ✓ 成功获取 {len(cash_flow_data.get('data', []))} 期现金流量表数据")
# 获取财务指标 # 获取财务指标(优先tushare,失败时回退akshare
indicators_data = self._get_financial_indicators(symbol) indicators_data = self._get_financial_indicators_from_tushare(symbol)
if indicators_data is None:
indicators_data = self._get_financial_indicators(symbol)
if indicators_data: if indicators_data:
data["financial_indicators"] = indicators_data data["financial_indicators"] = indicators_data
print(f" ✓ 成功获取 {len(indicators_data.get('data', []))} 期财务指标数据") print(f" ✓ 成功获取 {len(indicators_data.get('data', []))} 期财务指标数据")
@@ -108,11 +118,160 @@ class QuarterlyReportDataFetcher:
"""判断是否为中国股票""" """判断是否为中国股票"""
return symbol.isdigit() and len(symbol) == 6 return symbol.isdigit() and len(symbol) == 6
def _convert_ts_records(self, df, field_map, periods):
"""将tushare返回的财务表转换为统一的记录结构"""
try:
data_list = []
for _, row in df.head(periods).iterrows():
item = {}
for ch_name, ts_name in field_map.items():
if ts_name in df.columns:
value = row.get(ts_name)
if value is None or (isinstance(value, float) and pd.isna(value)):
continue
try:
item[ch_name] = str(value)
except:
item[ch_name] = "N/A"
if item:
data_list.append(item)
return {
"data": data_list,
"periods": len(data_list),
"columns": list(field_map.keys()),
"query_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
except Exception as e:
print(f" 转换tushare财务数据异常: {e}")
return None
def _get_income_statement_from_tushare(self, symbol):
"""从tushare获取利润表(优先数据源)"""
try:
if not data_source_manager.tushare_available:
return None
ts_code = data_source_manager._convert_to_ts_code(symbol)
df = data_source_manager.tushare_api.income(ts_code=ts_code)
if df is None or df.empty:
return None
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
field_map = {
'报告期': 'end_date',
'营业总收入': 'total_revenue',
'营业收入': 'revenue',
'营业总成本': 'total_operate_cost',
'营业利润': 'operate_profit',
'利润总额': 'total_profit',
'净利润': 'n_income',
'归属于母公司所有者的净利润': 'n_income_attr_p',
'基本每股收益': 'basic_eps',
'稀释每股收益': 'diluted_eps',
'销售费用': 'sell_exp',
'管理费用': 'admin_exp',
'财务费用': 'fin_exp',
'研发费用': 'rd_exp',
}
result = self._convert_ts_records(df, field_map, self.periods)
if result and result.get('periods'):
print(f" ✓ tushare成功获取 {result['periods']} 期利润表数据")
return result
except Exception as e:
print(f" tushare获取利润表异常: {e}")
return None
def _get_balance_sheet_from_tushare(self, symbol):
"""从tushare获取资产负债表(优先数据源)"""
try:
if not data_source_manager.tushare_available:
return None
ts_code = data_source_manager._convert_to_ts_code(symbol)
df = data_source_manager.tushare_api.balancesheet(ts_code=ts_code)
if df is None or df.empty:
return None
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
field_map = {
'报告期': 'end_date',
'资产总计': 'total_assets',
'流动资产合计': 'total_cur_assets',
'非流动资产合计': 'total_ncur_assets',
'负债合计': 'total_liab',
'流动负债合计': 'total_cur_liab',
'非流动负债合计': 'total_ncur_liab',
'所有者权益合计': 'total_hldr_eqy_inc_min_int',
'归属于母公司股东权益合计': 'total_hldr_eqy_exc_min_int',
}
result = self._convert_ts_records(df, field_map, self.periods)
if result and result.get('periods'):
print(f" ✓ tushare成功获取 {result['periods']} 期资产负债表数据")
return result
except Exception as e:
print(f" tushare获取资产负债表异常: {e}")
return None
def _get_cash_flow_from_tushare(self, symbol):
"""从tushare获取现金流量表(优先数据源)"""
try:
if not data_source_manager.tushare_available:
return None
ts_code = data_source_manager._convert_to_ts_code(symbol)
df = data_source_manager.tushare_api.cashflow(ts_code=ts_code)
if df is None or df.empty:
return None
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
field_map = {
'报告期': 'end_date',
'经营活动产生的现金流量净额': 'n_cashflow_act',
'投资活动产生的现金流量净额': 'n_cashflow_inv_act',
'筹资活动产生的现金流量净额': 'n_cash_flows_fnc_act',
}
result = self._convert_ts_records(df, field_map, self.periods)
if result and result.get('periods'):
print(f" ✓ tushare成功获取 {result['periods']} 期现金流量表数据")
return result
except Exception as e:
print(f" tushare获取现金流量表异常: {e}")
return None
def _get_financial_indicators_from_tushare(self, symbol):
"""从tushare获取财务指标(优先数据源)"""
try:
if not data_source_manager.tushare_available:
return None
ts_code = data_source_manager._convert_to_ts_code(symbol)
df = data_source_manager.tushare_api.fina_indicator(ts_code=ts_code)
if df is None or df.empty:
return None
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
field_map = {
'报告期': 'end_date',
'净资产收益率': 'roe',
'总资产净利率': 'roa',
'销售净利率': 'netprofit_margin',
'销售毛利率': 'grossprofit_margin',
'资产负债率': 'debt_to_assets',
'流动比率': 'current_ratio',
'速动比率': 'quick_ratio',
'应收账款周转率': 'ar_turnover',
'存货周转率': 'inventory_turnover',
'总资产周转率': 'assets_turnover',
'每股收益': 'eps',
'每股净资产': 'bps',
'每股经营现金流': 'cfps',
}
result = self._convert_ts_records(df, field_map, self.periods)
if result and result.get('periods'):
print(f" ✓ tushare成功获取 {result['periods']} 期财务指标数据")
return result
except Exception as e:
print(f" tushare获取财务指标异常: {e}")
return None
def _get_income_statement(self, symbol): def _get_income_statement(self, symbol):
"""获取利润表数据""" """获取利润表数据"""
try: try:
# stock_financial_report_sina - 新浪财经季度利润表 # stock_financial_report_sina - 新浪财经季度利润表
df = ak.stock_financial_report_sina(stock=symbol, symbol="利润表") df = call_with_timeout(ak.stock_financial_report_sina, timeout=25,
stock=symbol, symbol="利润表")
if df is None or df.empty: if df is None or df.empty:
print(f" 未找到利润表数据") print(f" 未找到利润表数据")
@@ -151,7 +310,8 @@ class QuarterlyReportDataFetcher:
"""获取资产负债表数据""" """获取资产负债表数据"""
try: try:
# stock_financial_report_sina - 新浪财经季度资产负债表 # stock_financial_report_sina - 新浪财经季度资产负债表
df = ak.stock_financial_report_sina(stock=symbol, symbol="资产负债表") df = call_with_timeout(ak.stock_financial_report_sina, timeout=25,
stock=symbol, symbol="资产负债表")
if df is None or df.empty: if df is None or df.empty:
print(f" 未找到资产负债表数据") print(f" 未找到资产负债表数据")
@@ -190,7 +350,8 @@ class QuarterlyReportDataFetcher:
"""获取现金流量表数据""" """获取现金流量表数据"""
try: try:
# stock_financial_report_sina - 新浪财经季度现金流量表 # stock_financial_report_sina - 新浪财经季度现金流量表
df = ak.stock_financial_report_sina(stock=symbol, symbol="现金流量表") df = call_with_timeout(ak.stock_financial_report_sina, timeout=25,
stock=symbol, symbol="现金流量表")
if df is None or df.empty: if df is None or df.empty:
print(f" 未找到现金流量表数据") print(f" 未找到现金流量表数据")
@@ -229,7 +390,7 @@ class QuarterlyReportDataFetcher:
"""获取财务指标数据""" """获取财务指标数据"""
try: try:
# 使用stock_financial_abstract替代已失效的stock_financial_analysis_indicator # 使用stock_financial_abstract替代已失效的stock_financial_analysis_indicator
df = ak.stock_financial_abstract(symbol=symbol) df = call_with_timeout(ak.stock_financial_abstract, timeout=25, symbol=symbol)
if df is None or df.empty: if df is None or df.empty:
print(f" 未找到财务指标数据") print(f" 未找到财务指标数据")
@@ -292,7 +453,7 @@ class QuarterlyReportDataFetcher:
text_parts = [] text_parts = []
text_parts.append(f""" text_parts.append(f"""
季度财务报告数据 - akshare数据源 季度财务报告数据 - tushare/akshare自动切换
股票代码{data.get('symbol', 'N/A')} 股票代码{data.get('symbol', 'N/A')}
数据期数最近{self.periods}期季报 数据期数最近{self.periods}期季报
@@ -423,4 +584,3 @@ if __name__ == "__main__":
print(f"\n获取失败: {data.get('error', '未知错误')}") print(f"\n获取失败: {data.get('error', '未知错误')}")
print("\n") print("\n")
+4 -4
View File
@@ -12,6 +12,7 @@ from typing import Dict, Any
import time import time
import warnings import warnings
import os import os
from http_timeout import call_with_timeout
# 屏蔽pywencai的Node.js警告信息(不影响功能) # 屏蔽pywencai的Node.js警告信息(不影响功能)
warnings.filterwarnings('ignore', category=DeprecationWarning) warnings.filterwarnings('ignore', category=DeprecationWarning)
@@ -108,7 +109,7 @@ class RiskDataFetcher:
query = f"{symbol}限售解禁" query = f"{symbol}限售解禁"
# 使用pywencai查询 # 使用pywencai查询
response = pywencai.get(query=query, loop=True) response = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
if response is None: if response is None:
return result return result
@@ -171,7 +172,7 @@ class RiskDataFetcher:
query = f"{symbol}大股东减持公告" query = f"{symbol}大股东减持公告"
# 使用pywencai查询 # 使用pywencai查询
response = pywencai.get(query=query, loop=True) response = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
if response is None: if response is None:
return result return result
@@ -234,7 +235,7 @@ class RiskDataFetcher:
query = f"{symbol}近期重要事件" query = f"{symbol}近期重要事件"
# 使用pywencai查询 # 使用pywencai查询
response = pywencai.get(query=query, loop=True) response = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
if response is None: if response is None:
return result return result
@@ -466,4 +467,3 @@ if __name__ == "__main__":
if risk_data['data_success']: if risk_data['data_success']:
print("\n格式化的风险数据:") print("\n格式化的风险数据:")
print(fetcher.format_risk_data_for_ai(risk_data)) print(fetcher.format_risk_data_for_ai(risk_data))
Binary file not shown.
+5 -4
View File
@@ -6,15 +6,16 @@
from deepseek_client import DeepSeekClient from deepseek_client import DeepSeekClient
from typing import Dict, Any from typing import Dict, Any
import time import time
import config
class SectorStrategyAgents: class SectorStrategyAgents:
"""板块策略AI智能体集合""" """板块策略AI智能体集合"""
def __init__(self, model="deepseek-chat"): def __init__(self, model=None):
self.model = model self.model = model or config.DEFAULT_MODEL_NAME
self.deepseek_client = DeepSeekClient(model=model) self.deepseek_client = DeepSeekClient(model=self.model)
print(f"[智策] AI智能体系统初始化 (模型: {model})") print(f"[智策] AI智能体系统初始化 (模型: {self.model})")
def macro_strategist_agent(self, market_data: Dict, news_data: list) -> Dict[str, Any]: def macro_strategist_agent(self, market_data: Dict, news_data: list) -> Dict[str, Any]:
""" """
+48 -36
View File
@@ -1,6 +1,6 @@
""" """
智策板块数据采集模块 智策板块数据采集模块
使用AKShare获取板块相关数据 优先使用Tushare失败时使用AKShare获取板块相关数据
""" """
import akshare as ak import akshare as ak
@@ -12,6 +12,7 @@ import logging
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
from sector_strategy_db import SectorStrategyDatabase from sector_strategy_db import SectorStrategyDatabase
from data_source_manager import data_source_manager
# 加载环境变量 # 加载环境变量
load_dotenv() load_dotenv()
@@ -254,40 +255,52 @@ class SectorStrategyDataFetcher:
except: except:
pass pass
# 大盘指数 # 大盘指数(优先tushare,失败回退akshare
def _get_index_data(index_code, ts_code, name):
if data_source_manager.tushare_available:
try:
end = datetime.now().strftime('%Y%m%d')
start = (datetime.now() - timedelta(days=10)).strftime('%Y%m%d')
df = data_source_manager.tushare_api.index_daily(
ts_code=ts_code, start_date=start, end_date=end
)
if df is not None and not df.empty:
row = df.iloc[0]
return {
"code": index_code,
"name": name,
"close": row.get('close', 0),
"change_pct": row.get('pct_chg', 0),
"change": row.get('change', 0)
}
print(f" tushare未获取到{name},尝试备用数据源")
except Exception as e:
print(f" tushare获取{name}失败: {e}")
try:
df = self._safe_request(ak.stock_zh_index_spot_em, symbol=name)
if df is not None and not df.empty:
row = df.iloc[0]
return {
"code": index_code,
"name": name,
"close": row.get('最新价', 0),
"change_pct": row.get('涨跌幅', 0),
"change": row.get('涨跌额', 0)
}
except Exception as e:
print(f" akshare获取{name}失败: {e}")
return None
try: try:
# 上证指数 sh_index = _get_index_data("000001", "000001.SH", "上证指数")
df_sh = ak.stock_zh_index_spot_em(symbol="上证指数") if sh_index:
if df_sh is not None and not df_sh.empty: overview["sh_index"] = sh_index
overview["sh_index"] = { sz_index = _get_index_data("399001", "399001.SZ", "深证成指")
"code": "000001", if sz_index:
"name": "上证指数", overview["sz_index"] = sz_index
"close": df_sh.iloc[0].get('最新价', 0), cyb_index = _get_index_data("399006", "399006.SZ", "创业板指")
"change_pct": df_sh.iloc[0].get('涨跌幅', 0), if cyb_index:
"change": df_sh.iloc[0].get('涨跌额', 0) overview["cyb_index"] = cyb_index
}
# 深证成指
df_sz = self._safe_request(ak.stock_zh_index_spot_em, symbol="深证成指")
if df_sz is not None and not df_sz.empty:
overview["sz_index"] = {
"code": "399001",
"name": "深证成指",
"close": df_sz.iloc[0].get('最新价', 0),
"change_pct": df_sz.iloc[0].get('涨跌幅', 0),
"change": df_sz.iloc[0].get('涨跌额', 0)
}
# 创业板指
df_cyb = self._safe_request(ak.stock_zh_index_spot_em, symbol="创业板指")
if df_cyb is not None and not df_cyb.empty:
overview["cyb_index"] = {
"code": "399006",
"name": "创业板指",
"close": df_cyb.iloc[0].get('最新价', 0),
"change_pct": df_cyb.iloc[0].get('涨跌幅', 0),
"change": df_cyb.iloc[0].get('涨跌额', 0)
}
except: except:
pass pass
@@ -310,7 +323,7 @@ class SectorStrategyDataFetcher:
try: try:
import tushare as ts import tushare as ts
ts.set_token(tushare_token) ts.set_token(tushare_token)
self.ts_pro = ts.pro_api() self.ts_pro = ts.pro_api(timeout=float(os.getenv('TUSHARE_TIMEOUT', '15')))
print(" [Tushare] ✅ 初始化成功") print(" [Tushare] ✅ 初始化成功")
except Exception as e: except Exception as e:
print(f" [Tushare] 初始化失败: {e}") print(f" [Tushare] 初始化失败: {e}")
@@ -757,4 +770,3 @@ if __name__ == "__main__":
print(f"\n... (总长度: {len(formatted_text)} 字符)") print(f"\n... (总长度: {len(formatted_text)} 字符)")
else: else:
print(f"\n数据采集失败: {data.get('error', '未知错误')}") print(f"\n数据采集失败: {data.get('error', '未知错误')}")
+6 -5
View File
@@ -11,20 +11,21 @@ import time
import json import json
import pandas as pd import pandas as pd
import logging import logging
import config
class SectorStrategyEngine: class SectorStrategyEngine:
"""板块策略综合研判引擎""" """板块策略综合研判引擎"""
def __init__(self, model="deepseek-chat"): def __init__(self, model=None):
self.model = model self.model = model or config.DEFAULT_MODEL_NAME
self.agents = SectorStrategyAgents(model=model) self.agents = SectorStrategyAgents(model=self.model)
self.deepseek_client = DeepSeekClient(model=model) self.deepseek_client = DeepSeekClient(model=self.model)
self.database = SectorStrategyDatabase() self.database = SectorStrategyDatabase()
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
if not self.logger.handlers: if not self.logger.handlers:
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(levelname)s %(name)s: %(message)s') logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(levelname)s %(name)s: %(message)s')
print(f"[智策引擎] 初始化完成 (模型: {model})") print(f"[智策引擎] 初始化完成 (模型: {self.model})")
def save_raw_data_with_fallback(self, data_type, data_df, data_date=None): def save_raw_data_with_fallback(self, data_type, data_df, data_date=None):
""" """
+1 -1
View File
@@ -128,7 +128,7 @@ class SectorStrategyScheduler:
# 2. 运行AI分析 # 2. 运行AI分析
print("[智策定时] [2/3] AI智能体分析中...") print("[智策定时] [2/3] AI智能体分析中...")
engine = SectorStrategyEngine(model="deepseek-chat") engine = SectorStrategyEngine()
result = engine.run_comprehensive_analysis(data) result = engine.run_comprehensive_analysis(data)
if not result.get("success"): if not result.get("success"):
+8 -20
View File
@@ -112,27 +112,13 @@ def display_analysis_tab():
st.markdown("---") st.markdown("---")
# 模型选择 # 操作按钮
col1, col2, col3 = st.columns([2, 2, 2]) col1, col2 = st.columns([2, 2])
with col1: with col1:
# 导入model_config.py中定义的model_options
from model_config import model_options as app_model_options
selected_model = st.selectbox(
"AI模型",
list(app_model_options.keys()),
format_func=lambda x: app_model_options[x],
help="Reasoner模型提供更强的推理能力"
)
with col2:
st.write("")
st.write("")
analyze_button = st.button("🚀 开始智策分析", type="primary", width='content') analyze_button = st.button("🚀 开始智策分析", type="primary", width='content')
with col3: with col2:
st.write("")
st.write("")
if st.button("🔄 清除结果", width='content'): if st.button("🔄 清除结果", width='content'):
if 'sector_strategy_result' in st.session_state: if 'sector_strategy_result' in st.session_state:
del st.session_state.sector_strategy_result del st.session_state.sector_strategy_result
@@ -141,13 +127,13 @@ def display_analysis_tab():
st.markdown("---") st.markdown("---")
# 开始分析 # 开始分析(使用.env中配置的默认模型)
if analyze_button: if analyze_button:
# 清除之前的结果 # 清除之前的结果
if 'sector_strategy_result' in st.session_state: if 'sector_strategy_result' in st.session_state:
del st.session_state.sector_strategy_result del st.session_state.sector_strategy_result
run_sector_strategy_analysis(selected_model) run_sector_strategy_analysis()
# 显示分析结果 # 显示分析结果
if 'sector_strategy_result' in st.session_state: if 'sector_strategy_result' in st.session_state:
@@ -261,8 +247,10 @@ def display_report_detail(report_id):
st.info("当前版本仅提供报告摘要,详细页面已移除。") st.info("当前版本仅提供报告摘要,详细页面已移除。")
def run_sector_strategy_analysis(model="deepseek-chat"): def run_sector_strategy_analysis(model=None):
"""运行智策分析""" """运行智策分析"""
import config
model = model or config.DEFAULT_MODEL_NAME
# 进度显示 # 进度显示
progress_bar = st.progress(0) progress_bar = st.progress(0)
+2 -1
View File
@@ -8,6 +8,7 @@
import logging import logging
from typing import Tuple, Optional from typing import Tuple, Optional
import pandas as pd import pandas as pd
from http_timeout import call_with_timeout
class SmallCapSelector: class SmallCapSelector:
@@ -54,7 +55,7 @@ class SmallCapSelector:
self.logger.info(f"开始执行小市值策略选股,查询条件: {query}") self.logger.info(f"开始执行小市值策略选股,查询条件: {query}")
# 调用pywencai # 调用pywencai
result = pywencai.get(query=query, loop=True) result = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
if result is None or result.empty: if result is None or result.empty:
self.logger.warning("未获取到符合条件的股票") self.logger.warning("未获取到符合条件的股票")
BIN
View File
Binary file not shown.
+48 -30
View File
@@ -54,7 +54,7 @@ class SmartMonitorDataFetcher:
try: try:
import tushare as ts import tushare as ts
ts.set_token(tushare_token) ts.set_token(tushare_token)
self.ts_pro = ts.pro_api() self.ts_pro = ts.pro_api(timeout=float(os.getenv('TUSHARE_TIMEOUT', '15')))
self.logger.info("Tushare备用数据源初始化成功") self.logger.info("Tushare备用数据源初始化成功")
except Exception as e: except Exception as e:
self.logger.warning(f"Tushare初始化失败: {e}") self.logger.warning(f"Tushare初始化失败: {e}")
@@ -64,7 +64,7 @@ class SmartMonitorDataFetcher:
def get_realtime_quote(self, stock_code: str, retry: int = 1) -> Optional[Dict]: def get_realtime_quote(self, stock_code: str, retry: int = 1) -> Optional[Dict]:
""" """
获取实时行情带重试和降级机制 获取实时行情带重试和降级机制
优先使用TDX失败时降级到AKShare最后降级到Tushare 优先使用TDX失败时降级到Tushare最后降级到AKShare
Args: Args:
stock_code: 股票代码600519 stock_code: 股票代码600519
@@ -82,11 +82,20 @@ class SmartMonitorDataFetcher:
if quote: if quote:
return quote return quote
else: else:
self.logger.warning(f"TDX获取失败 {stock_code},尝试降级到AKShare") self.logger.warning(f"TDX获取失败 {stock_code},尝试降级到Tushare")
except Exception as e: except Exception as e:
self.logger.warning(f"TDX获取异常 {stock_code}: {e},尝试降级到AKShare") self.logger.warning(f"TDX获取异常 {stock_code}: {e},尝试降级到Tushare")
# 方法2: 组合使用AKShare分钟行情 + 基本信息 # 方法2: 降级到Tushare(优先于AKShare
if self.ts_pro:
quote = self._get_realtime_quote_from_tushare(stock_code)
if quote:
return quote
self.logger.warning(f"Tushare获取失败 {stock_code},尝试降级到AKShare")
else:
self.logger.warning(f"未配置Tushare,尝试使用AKShare")
# 方法3: 组合使用AKShare分钟行情 + 基本信息
for attempt in range(retry): for attempt in range(retry):
try: try:
# 1.1 获取股票基本信息(名称) # 1.1 获取股票基本信息(名称)
@@ -167,18 +176,14 @@ class SmartMonitorDataFetcher:
else: else:
self.logger.warning(f"AKShare获取失败 {stock_code}(已重试{retry}次),尝试降级") self.logger.warning(f"AKShare获取失败 {stock_code}(已重试{retry}次),尝试降级")
# 降级到Tushare # 所有数据源都失败
if self.ts_pro: self.logger.error(f"所有数据源都无法获取 {stock_code} 行情")
self.logger.info(f"降级到Tushare获取 {stock_code}...") return None
return self._get_realtime_quote_from_tushare(stock_code)
else:
self.logger.error(f"AKShare失败且未配置Tushare,无法获取 {stock_code} 行情")
return None
def get_technical_indicators(self, stock_code: str, period: str = 'daily', retry: int = 1) -> Optional[Dict]: def get_technical_indicators(self, stock_code: str, period: str = 'daily', retry: int = 1) -> Optional[Dict]:
""" """
计算技术指标带降级机制 计算技术指标带降级机制
优先使用TDX失败时降级到AKShare最后降级到Tushare 优先使用TDX失败时降级到Tushare最后降级到AKShare
Args: Args:
stock_code: 股票代码 stock_code: 股票代码
@@ -197,11 +202,21 @@ class SmartMonitorDataFetcher:
if indicators: if indicators:
return indicators return indicators
else: else:
self.logger.warning(f"TDX计算技术指标失败 {stock_code},尝试降级到AKShare") self.logger.warning(f"TDX计算技术指标失败 {stock_code},尝试降级到Tushare")
except Exception as e: except Exception as e:
self.logger.warning(f"TDX计算技术指标异常 {stock_code}: {e},尝试降级到AKShare") self.logger.warning(f"TDX计算技术指标异常 {stock_code}: {e},尝试降级到Tushare")
# 方法2: 尝试使用AKShare # 方法2: 降级到Tushare(优先于AKShare
if self.ts_pro:
self.logger.info(f"降级到Tushare获取 {stock_code} 历史数据...")
indicators = self._get_technical_indicators_from_tushare(stock_code, period)
if indicators:
return indicators
self.logger.warning(f"Tushare获取技术指标失败 {stock_code},尝试降级到AKShare")
else:
self.logger.warning(f"未配置Tushare,尝试使用AKShare")
# 方法3: 尝试使用AKShare
for attempt in range(retry): for attempt in range(retry):
try: try:
# 获取历史数据(最近200个交易日,用于计算指标) # 获取历史数据(最近200个交易日,用于计算指标)
@@ -237,13 +252,9 @@ class SmartMonitorDataFetcher:
self.logger.warning(f"AKShare获取历史数据失败 {stock_code}(已重试{retry}次),尝试降级到Tushare") self.logger.warning(f"AKShare获取历史数据失败 {stock_code}(已重试{retry}次),尝试降级到Tushare")
break break
# 方法3: 降级到Tushare # 所有数据源都失败
if self.ts_pro: self.logger.error(f"所有数据源都无法获取 {stock_code} 技术指标")
self.logger.info(f"降级到Tushare获取 {stock_code} 历史数据...") return None
return self._get_technical_indicators_from_tushare(stock_code, period)
else:
self.logger.error(f"AKShare失败且未配置Tushare,无法获取 {stock_code} 技术指标")
return None
def _calculate_all_indicators(self, df: pd.DataFrame, stock_code: str) -> Optional[Dict]: def _calculate_all_indicators(self, df: pd.DataFrame, stock_code: str) -> Optional[Dict]:
""" """
@@ -439,6 +450,17 @@ class SmartMonitorDataFetcher:
""" """
import time import time
# 优先使用Tushare个股资金流向
if self.ts_pro:
try:
result = self._get_main_force_from_tushare(stock_code)
if result:
self.logger.info(f"✅ Tushare成功获取 {stock_code} 资金流向(主要数据源)")
return result
self.logger.warning(f"Tushare未获取到资金流向 {stock_code},尝试AKShare")
except Exception as e:
self.logger.warning(f"Tushare获取资金流向失败 {stock_code}: {e}")
for attempt in range(retry): for attempt in range(retry):
try: try:
# 获取个股资金流(新版AKShare API参数调整) # 获取个股资金流(新版AKShare API参数调整)
@@ -495,12 +517,9 @@ class SmartMonitorDataFetcher:
self.logger.warning(f"AKShare获取资金流向失败 {stock_code}(已重试{retry}次),尝试降级到Tushare") self.logger.warning(f"AKShare获取资金流向失败 {stock_code}(已重试{retry}次),尝试降级到Tushare")
break break
# 降级到Tushare # 所有数据源都失败
if self.ts_pro: self.logger.error(f"所有数据源都无法获取 {stock_code} 资金流向")
return self._get_main_force_from_tushare(stock_code) return None
else:
self.logger.error(f"AKShare失败且未配置Tushare,无法获取 {stock_code} 资金流向")
return None
def get_comprehensive_data(self, stock_code: str) -> Dict: def get_comprehensive_data(self, stock_code: str) -> Dict:
""" """
@@ -822,4 +841,3 @@ if __name__ == '__main__':
print("\n主力资金:") print("\n主力资金:")
print(f" 主力净额: {data['main_force']['main_net']:.2f}") print(f" 主力净额: {data['main_force']['main_net']:.2f}")
print(f" 主力动向: {data['main_force']['trend']}") print(f" 主力动向: {data['main_force']['trend']}")
+5 -2
View File
@@ -7,6 +7,7 @@ import logging
from typing import Dict, List, Optional from typing import Dict, List, Optional
from datetime import datetime, time from datetime import datetime, time
import pytz import pytz
import config
class SmartMonitorDeepSeek: class SmartMonitorDeepSeek:
@@ -20,7 +21,7 @@ class SmartMonitorDeepSeek:
api_key: DeepSeek API密钥 api_key: DeepSeek API密钥
""" """
self.api_key = api_key self.api_key = api_key
self.base_url = "https://api.deepseek.com/v1" self.base_url = config.DEEPSEEK_BASE_URL
self.headers = { self.headers = {
"Authorization": f"Bearer {api_key}", "Authorization": f"Bearer {api_key}",
"Content-Type": "application/json" "Content-Type": "application/json"
@@ -138,7 +139,7 @@ class SmartMonitorDeepSeek:
'can_trade': False 'can_trade': False
} }
def chat_completion(self, messages: List[Dict], model: str = "deepseek-chat", def chat_completion(self, messages: List[Dict], model: str = None,
temperature: float = 0.7, max_tokens: int = 2000) -> Dict: temperature: float = 0.7, max_tokens: int = 2000) -> Dict:
""" """
调用DeepSeek API 调用DeepSeek API
@@ -154,6 +155,8 @@ class SmartMonitorDeepSeek:
""" """
import requests import requests
model = model or config.DEFAULT_MODEL_NAME
payload = { payload = {
"model": model, "model": model,
"messages": messages, "messages": messages,
+22 -17
View File
@@ -333,15 +333,26 @@ class SmartMonitorKline:
self.logger.info(f"✅ TDX获取K线数据成功 {stock_code},共{len(df)}") self.logger.info(f"✅ TDX获取K线数据成功 {stock_code},共{len(df)}")
return df return df
else: else:
self.logger.warning(f"TDX未返回K线数据 {stock_code},尝试降级到AKShare") self.logger.warning(f"TDX未返回K线数据 {stock_code},尝试降级到Tushare")
except Exception as e: except Exception as e:
self.logger.warning(f"TDX获取K线数据失败 {stock_code}: {type(e).__name__}, 尝试降级到AKShare") self.logger.warning(f"TDX获取K线数据失败 {stock_code}: {type(e).__name__}, 尝试降级到Tushare")
# 计算日期范围 # 计算日期范围
end_date = datetime.now().strftime('%Y%m%d') end_date = datetime.now().strftime('%Y%m%d')
start_date = (datetime.now() - timedelta(days=days + 30)).strftime('%Y%m%d') # 多取30天以确保足够数据 start_date = (datetime.now() - timedelta(days=days + 30)).strftime('%Y%m%d') # 多取30天以确保足够数据
# 方法2: 尝试使用AKShare获取(只尝试1次,避免IP封禁 # 方法2: 降级到Tushare(优先于AKShare
if data_fetcher and data_fetcher.ts_pro:
self.logger.info(f"降级使用Tushare获取K线数据 {stock_code}")
df = self._get_kline_from_tushare(stock_code, days, data_fetcher.ts_pro)
if df is not None and not df.empty:
self.logger.info(f"✅ Tushare获取K线数据成功 {stock_code},共{len(df)}")
return df
self.logger.warning(f"Tushare未返回K线数据 {stock_code},尝试降级到AKShare")
else:
self.logger.warning(f"未配置Tushare,尝试使用AKShare")
# 方法3: 尝试使用AKShare获取(只尝试1次,避免IP封禁)
try: try:
import akshare as ak import akshare as ak
df = ak.stock_zh_a_hist( df = ak.stock_zh_a_hist(
@@ -358,17 +369,9 @@ class SmartMonitorKline:
self.logger.info(f"✅ AKShare获取K线数据成功 {stock_code},共{len(df)}") self.logger.info(f"✅ AKShare获取K线数据成功 {stock_code},共{len(df)}")
return df return df
else: else:
self.logger.warning(f"AKShare未返回K线数据 {stock_code},尝试降级到Tushare") self.logger.warning(f"AKShare未返回K线数据 {stock_code}")
except Exception as e: except Exception as e:
self.logger.warning(f"AKShare获取K线数据失败 {stock_code}: {type(e).__name__}, 尝试降级到Tushare") self.logger.warning(f"AKShare获取K线数据失败 {stock_code}: {type(e).__name__}")
# 方法3: 降级到Tushare
if data_fetcher and data_fetcher.ts_pro:
self.logger.info(f"降级使用Tushare获取K线数据 {stock_code}")
df = self._get_kline_from_tushare(stock_code, days, data_fetcher.ts_pro)
if df is not None and not df.empty:
self.logger.info(f"✅ Tushare获取K线数据成功 {stock_code},共{len(df)}")
return df
self.logger.error(f"所有数据源都无法获取K线数据 {stock_code}") self.logger.error(f"所有数据源都无法获取K线数据 {stock_code}")
return None return None
@@ -404,12 +407,15 @@ class SmartMonitorKline:
end_date = datetime.now().strftime('%Y%m%d') end_date = datetime.now().strftime('%Y%m%d')
start_date = (datetime.now() - timedelta(days=days + 60)).strftime('%Y%m%d') start_date = (datetime.now() - timedelta(days=days + 60)).strftime('%Y%m%d')
# 获取日K线数据(前复权) # 获取日K线数据(前复权,daily接口不支持adj参数,需用pro_bar
df = ts_pro.daily( import tushare as ts
df = ts.pro_bar(
api=ts_pro,
ts_code=ts_code, ts_code=ts_code,
start_date=start_date, start_date=start_date,
end_date=end_date, end_date=end_date,
adj='qfq' adj='qfq',
retry_count=1
) )
if df is None or df.empty: if df is None or df.empty:
@@ -493,4 +499,3 @@ if __name__ == '__main__':
print("K线图已保存到 test_kline.html") print("K线图已保存到 test_kline.html")
else: else:
print("获取K线数据失败") print("获取K线数据失败")
+40
View File
@@ -0,0 +1,40 @@
@echo off
REM AI股票分析系统启动批处理脚本
REM 适用于Windows 11环境
echo 🚀 正在启动AI股票分析系统...
echo ==================================================
REM 检查是否在虚拟环境中
if "%VIRTUAL_ENV%"=="" (
echo 📦 正在激活Python虚拟环境...
call venv\Scripts\activate.bat
) else (
echo ✅ 已在虚拟环境中
)
REM 检查app.py是否存在
if not exist app.py (
echo ❌ 错误: app.py 文件不存在!
pause
exit /b 1
)
echo 🌐 正在启动Streamlit应用...
echo 📝 访问地址: http://localhost:8503
echo ⏹️ 按 Ctrl+C 停止服务
echo ==================================================
REM 启动Streamlit应用
streamlit run app.py --server.port 8503 --server.address 127.0.0.1
REM 如果出错,暂停以便查看错误信息
if errorlevel 1 (
echo.
echo ❌ 启动失败,请检查错误信息
pause
)
echo.
echo 👋 感谢使用AI股票分析系统!
pause
BIN
View File
Binary file not shown.
+325 -105
View File
@@ -8,10 +8,46 @@ import requests
import json import json
import pywencai import pywencai
from data_source_manager import data_source_manager from data_source_manager import data_source_manager
from http_timeout import call_with_timeout
class StockDataFetcher: class StockDataFetcher:
"""股票数据获取类""" """股票数据获取类"""
# tushare财务字段 -> 中文映射(供AI分析展示)
INCOME_TS_MAP = {
'报告期': 'end_date',
'营业总收入': 'total_revenue',
'营业收入': 'revenue',
'营业总成本': 'total_operate_cost',
'营业利润': 'operate_profit',
'利润总额': 'total_profit',
'净利润': 'n_income',
'归属于母公司所有者的净利润': 'n_income_attr_p',
'基本每股收益': 'basic_eps',
'稀释每股收益': 'diluted_eps',
'销售费用': 'sell_exp',
'管理费用': 'admin_exp',
'财务费用': 'fin_exp',
'研发费用': 'rd_exp',
}
BALANCE_TS_MAP = {
'报告期': 'end_date',
'资产总计': 'total_assets',
'流动资产合计': 'total_cur_assets',
'非流动资产合计': 'total_ncur_assets',
'负债合计': 'total_liab',
'流动负债合计': 'total_cur_liab',
'非流动负债合计': 'total_ncur_liab',
'所有者权益合计': 'total_hldr_eqy_inc_min_int',
'归属于母公司股东权益合计': 'total_hldr_eqy_exc_min_int',
}
CASHFLOW_TS_MAP = {
'报告期': 'end_date',
'经营活动产生的现金流量净额': 'n_cashflow_act',
'投资活动产生的现金流量净额': 'n_cashflow_inv_act',
'筹资活动产生的现金流量净额': 'n_cash_flows_fnc_act',
}
def __init__(self): def __init__(self):
self.data = None self.data = None
self.info = None self.info = None
@@ -89,57 +125,70 @@ class StockDataFetcher:
if basic_info: if basic_info:
info.update(basic_info) info.update(basic_info)
# 方法1: 尝试获取个股详细信息(akshare # 方法1: 获取详细估值信息(优先tushare,失败时回退akshare
try: if (info.get('name') == '未知' or info.get('pe_ratio') == 'N/A' or
stock_info = ak.stock_individual_info_em(symbol=symbol) info.get('pb_ratio') == 'N/A' or info.get('market_cap') == 'N/A'):
if stock_info is not None and not stock_info.empty: # 优先使用tushare daily_basic(一次获取PE/PB/市值)
for _, row in stock_info.iterrows(): if self.data_source_manager.tushare_available:
key = row['item']
value = row['value']
if key == '股票简称':
info['name'] = value
elif key == '总市值':
try:
if value and value != '-':
info['market_cap'] = float(value)
except:
pass
elif key == '市盈率-动态':
try:
if value and value != '-':
pe_value = float(value)
if 0 < pe_value <= 1000:
info['pe_ratio'] = pe_value
except:
pass
elif key == '市净率':
try:
if value and value != '-':
pb_value = float(value)
if 0 < pb_value <= 100:
info['pb_ratio'] = pb_value
except:
pass
except Exception as e:
print(f"[Akshare] 获取个股详细信息失败: {e}")
# 如果akshare失败,尝试从tushare获取
if self.data_source_manager.tushare_available and info['name'] == '未知':
print(f"[Tushare] 尝试获取基本信息(tushare...")
try: try:
print(f"[Tushare] 正在获取 {symbol} 的估值信息(主要数据源)...")
ts_code = self.data_source_manager._convert_to_ts_code(symbol) ts_code = self.data_source_manager._convert_to_ts_code(symbol)
df = self.data_source_manager.tushare_api.daily_basic( df = self.data_source_manager.tushare_api.daily_basic(
ts_code=ts_code, ts_code=ts_code,
trade_date=datetime.now().strftime('%Y%m%d') start_date=(datetime.now() - timedelta(days=10)).strftime('%Y%m%d'),
end_date=datetime.now().strftime('%Y%m%d')
) )
if df is not None and not df.empty: if df is not None and not df.empty:
row = df.iloc[0] row = df.iloc[0]
info['pe_ratio'] = row.get('pe', 'N/A') if info.get('pe_ratio') == 'N/A' and 'pe' in df.columns:
info['pb_ratio'] = row.get('pb', 'N/A') info['pe_ratio'] = row.get('pe', 'N/A')
info['market_cap'] = row.get('total_mv', 'N/A') if info.get('pb_ratio') == 'N/A' and 'pb' in df.columns:
print(f"[Tushare] ✅ 成功获取部分信息") info['pb_ratio'] = row.get('pb', 'N/A')
except Exception as te: if info.get('market_cap') == 'N/A' and 'total_mv' in df.columns:
print(f"[Tushare] ❌ 获取失败: {te}") info['market_cap'] = row.get('total_mv', 'N/A')
print(f"[Tushare] ✅ 成功获取估值信息")
else:
print(f"[Tushare] ❌ 未获取到估值信息,尝试备用数据源")
except Exception as e:
print(f"[Tushare] ❌ 获取估值信息失败: {e}")
# tushare未获取到时,回退akshare
if (info.get('name') == '未知' or info.get('pe_ratio') == 'N/A' or
info.get('pb_ratio') == 'N/A' or info.get('market_cap') == 'N/A'):
try:
print(f"[Akshare] 正在获取 {symbol} 的详细信息(备用数据源)...")
stock_info = ak.stock_individual_info_em(symbol=symbol)
if stock_info is not None and not stock_info.empty:
for _, row in stock_info.iterrows():
key = row['item']
value = row['value']
if key == '股票简称':
info['name'] = value
elif key == '总市值':
try:
if value and value != '-':
info['market_cap'] = float(value)
except:
pass
elif key == '市盈率-动态':
try:
if value and value != '-':
pe_value = float(value)
if 0 < pe_value <= 1000:
info['pe_ratio'] = pe_value
except:
pass
elif key == '市净率':
try:
if value and value != '-':
pb_value = float(value)
if 0 < pb_value <= 100:
info['pb_ratio'] = pb_value
except:
pass
except Exception as e:
print(f"[Akshare] 获取个股详细信息失败: {e}")
# 方法2: 尝试获取历史价格和涨跌幅(如果网络允许) # 方法2: 尝试获取历史价格和涨跌幅(如果网络允许)
# try: # try:
@@ -204,7 +253,10 @@ class StockDataFetcher:
# 方法3: 使用百度估值数据获取市盈率和市净率 # 方法3: 使用百度估值数据获取市盈率和市净率
if info['pe_ratio'] == 'N/A': if info['pe_ratio'] == 'N/A':
try: try:
pe_data = ak.stock_zh_valuation_baidu(symbol=symbol, indicator="市盈率(TTM)") pe_data = call_with_timeout(
ak.stock_zh_valuation_baidu, timeout=15,
symbol=symbol, indicator="市盈率(TTM)"
)
if pe_data is not None and not pe_data.empty: if pe_data is not None and not pe_data.empty:
latest_pe = pe_data.iloc[-1]['value'] latest_pe = pe_data.iloc[-1]['value']
if latest_pe and latest_pe != '-': if latest_pe and latest_pe != '-':
@@ -216,7 +268,10 @@ class StockDataFetcher:
if info['pb_ratio'] == 'N/A': if info['pb_ratio'] == 'N/A':
try: try:
pb_data = ak.stock_zh_valuation_baidu(symbol=symbol, indicator="市净率") pb_data = call_with_timeout(
ak.stock_zh_valuation_baidu, timeout=15,
symbol=symbol, indicator="市净率"
)
if pb_data is not None and not pb_data.empty: if pb_data is not None and not pb_data.empty:
latest_pb = pb_data.iloc[-1]['value'] latest_pb = pb_data.iloc[-1]['value']
if latest_pb and latest_pb != '-': if latest_pb and latest_pb != '-':
@@ -262,6 +317,32 @@ class StockDataFetcher:
"exchange": "香港交易所" "exchange": "香港交易所"
} }
# 优先使用tusharehk_basic + hk_daily
if self.data_source_manager.tushare_available:
try:
print(f"[Tushare] 正在获取港股信息(主要数据源)...")
ts_code = f"{hk_code}.HK"
bdf = self.data_source_manager.tushare_api.hk_basic(ts_code=ts_code)
if bdf is not None and not bdf.empty and 'name' in bdf.columns:
info['name'] = bdf.iloc[0].get('name', '未知')
end = datetime.now().strftime('%Y%m%d')
start = (datetime.now() - timedelta(days=10)).strftime('%Y%m%d')
hdf = self.data_source_manager.tushare_api.hk_daily(
ts_code=ts_code, start_date=start, end_date=end
)
if hdf is not None and not hdf.empty:
latest = hdf.iloc[0]
info['current_price'] = latest.get('close', 'N/A')
info['change_percent'] = latest.get('pct_chg', 'N/A')
if info['current_price'] != 'N/A':
print(f"[Tushare] ✅ 成功获取港股信息")
return info
print(f"[Tushare] ❌ 未获取到港股行情,尝试备用数据源")
except Exception as e:
print(f"[Tushare] 获取港股信息失败: {e}")
# 方法1: 获取港股实时行情 # 方法1: 获取港股实时行情
try: try:
# 使用akshare获取港股实时数据 # 使用akshare获取港股实时数据
@@ -331,12 +412,7 @@ class StockDataFetcher:
def _get_us_stock_info(self, symbol): def _get_us_stock_info(self, symbol):
"""获取美股基本信息""" """获取美股基本信息"""
import time
try: try:
# 添加延迟避免频率限制
time.sleep(1)
ticker = yf.Ticker(symbol) ticker = yf.Ticker(symbol)
# 先尝试获取历史数据(通常更稳定) # 先尝试获取历史数据(通常更稳定)
@@ -487,7 +563,36 @@ class StockDataFetcher:
else: else:
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d') start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
# 获取港股历史数据 # 优先使用tusharehk_daily
if self.data_source_manager.tushare_available:
try:
print(f"[Tushare] 正在获取港股历史数据(主要数据源)...")
ts_code = f"{hk_code}.HK"
df = self.data_source_manager.tushare_api.hk_daily(
ts_code=ts_code,
start_date=start_date,
end_date=end_date
)
if df is not None and not df.empty:
df = df.rename(columns={
'trade_date': 'Date',
'open': 'Open',
'high': 'High',
'low': 'Low',
'close': 'Close',
'vol': 'Volume'
})
df['Date'] = pd.to_datetime(df['Date'])
df = df.sort_values('Date')
df.set_index('Date', inplace=True)
print(f"[Tushare] ✅ 成功获取港股历史数据")
return df
else:
print(f"[Tushare] ❌ 未获取到港股历史数据,尝试备用数据源")
except Exception as e:
print(f"[Tushare] 获取港股历史数据失败: {e}")
# 回退akshare
df = ak.stock_hk_hist(symbol=hk_code, period="daily", df = ak.stock_hk_hist(symbol=hk_code, period="daily",
start_date=start_date, end_date=end_date, adjust="qfq") start_date=start_date, end_date=end_date, adjust="qfq")
@@ -600,6 +705,112 @@ class StockDataFetcher:
except Exception as e: except Exception as e:
return {"error": f"获取财务数据失败: {str(e)}"} return {"error": f"获取财务数据失败: {str(e)}"}
def _convert_ts_financial_records(self, df, field_map, limit=8):
"""将tushare财务表转换为统一的记录列表"""
try:
data_list = []
for _, row in df.head(limit).iterrows():
item = {}
for ch_name, ts_name in field_map.items():
if ts_name in df.columns:
value = row.get(ts_name)
if value is None or (isinstance(value, float) and pd.isna(value)):
continue
try:
item[ch_name] = str(value)
except:
item[ch_name] = "N/A"
if item:
data_list.append(item)
return data_list
except Exception as e:
print(f"转换tushare财务数据异常: {e}")
return None
def _convert_ts_ratios(self, df):
"""将tushare财务指标转换为AI使用的比率字典"""
try:
df = df.sort_values('end_date', ascending=False)
if df.empty:
return {}
row = df.iloc[0]
mapping = {
'净资产收益率(ROE)': 'roe',
'总资产报酬率(ROA)': 'roa',
'销售毛利率': 'grossprofit_margin',
'销售净利率': 'netprofit_margin',
'资产负债率': 'debt_to_assets',
'流动比率': 'current_ratio',
'速动比率': 'quick_ratio',
'存货周转率': 'inventory_turnover',
'应收账款周转率': 'ar_turnover',
'总资产周转率': 'assets_turnover',
'营业收入同比增长': 'or_yoy',
'净利润同比增长': 'netprofit_yoy',
'EPS': 'eps',
}
ratios = {'报告期': str(row.get('end_date', 'N/A'))}
for ch_name, ts_name in mapping.items():
if ts_name in df.columns:
value = row.get(ts_name)
if value is None or (isinstance(value, float) and pd.isna(value)):
ratios[ch_name] = "N/A"
else:
try:
ratios[ch_name] = str(value)
except:
ratios[ch_name] = "N/A"
return ratios
except Exception as e:
print(f"转换tushare财务指标异常: {e}")
return {}
def _get_financial_data_from_tushare(self, symbol):
"""优先从tushare获取财务三表与财务指标"""
try:
if not self.data_source_manager.tushare_available:
return None
ts_code = self.data_source_manager._convert_to_ts_code(symbol)
result = {}
# 利润表
df = self.data_source_manager.tushare_api.income(ts_code=ts_code)
if df is not None and not df.empty:
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
records = self._convert_ts_financial_records(df, self.INCOME_TS_MAP)
if records:
result["income_statement"] = records
# 资产负债表
df = self.data_source_manager.tushare_api.balancesheet(ts_code=ts_code)
if df is not None and not df.empty:
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
records = self._convert_ts_financial_records(df, self.BALANCE_TS_MAP)
if records:
result["balance_sheet"] = records
# 现金流量表
df = self.data_source_manager.tushare_api.cashflow(ts_code=ts_code)
if df is not None and not df.empty:
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
records = self._convert_ts_financial_records(df, self.CASHFLOW_TS_MAP)
if records:
result["cash_flow"] = records
# 财务指标
df = self.data_source_manager.tushare_api.fina_indicator(ts_code=ts_code)
if df is not None and not df.empty:
ratios = self._convert_ts_ratios(df)
if ratios:
result["financial_ratios"] = ratios
if result:
print(f"[Tushare] ✅ 成功获取财务数据(主要数据源)")
return result or None
except Exception as e:
print(f"[Tushare] ❌ 获取财务数据失败: {e}")
return None
def _get_chinese_financial_data(self, symbol): def _get_chinese_financial_data(self, symbol):
"""获取中国股票财务数据""" """获取中国股票财务数据"""
financial_data = { financial_data = {
@@ -612,68 +823,77 @@ class StockDataFetcher:
} }
try: try:
# 1. 获取资产负债表 # 0. 优先使用tushare获取财务三表与财务指标
try: ts_financial = self._get_financial_data_from_tushare(symbol)
balance_sheet = ak.stock_financial_abstract_ths(symbol=symbol, indicator="资产负债表") if ts_financial:
if balance_sheet is not None and not balance_sheet.empty: financial_data.update(ts_financial)
financial_data["balance_sheet"] = balance_sheet.head(8).to_dict('records')
except Exception as e:
print(f"获取资产负债表失败: {e}")
# 2. 获取利润表 # 1. 获取资产负债表(tushare未获取到时回退akshare
try: if financial_data["balance_sheet"] is None:
income_statement = ak.stock_financial_abstract_ths(symbol=symbol, indicator="利润表") try:
if income_statement is not None and not income_statement.empty: balance_sheet = ak.stock_financial_abstract_ths(symbol=symbol, indicator="资产负债表")
financial_data["income_statement"] = income_statement.head(8).to_dict('records') if balance_sheet is not None and not balance_sheet.empty:
except Exception as e: financial_data["balance_sheet"] = balance_sheet.head(8).to_dict('records')
print(f"获取利润表失败: {e}") except Exception as e:
print(f"获取资产负债表失败: {e}")
# 3. 获取现金流量表 # 2. 获取利润表(tushare未获取到时回退akshare
try: if financial_data["income_statement"] is None:
cash_flow = ak.stock_financial_abstract_ths(symbol=symbol, indicator="现金流量表") try:
if cash_flow is not None and not cash_flow.empty: income_statement = ak.stock_financial_abstract_ths(symbol=symbol, indicator="利润表")
financial_data["cash_flow"] = cash_flow.head(8).to_dict('records') if income_statement is not None and not income_statement.empty:
except Exception as e: financial_data["income_statement"] = income_statement.head(8).to_dict('records')
print(f"获取现金流量表失败: {e}") except Exception as e:
print(f"获取利润表失败: {e}")
# 4. 获取主要财务指标 # 3. 获取现金流量表(tushare未获取到时回退akshare
try: if financial_data["cash_flow"] is None:
financial_abstract = ak.stock_financial_abstract(symbol=symbol) try:
if financial_abstract is not None and not financial_abstract.empty: cash_flow = ak.stock_financial_abstract_ths(symbol=symbol, indicator="现金流量表")
# 提取关键财务指标 if cash_flow is not None and not cash_flow.empty:
key_indicators = [ financial_data["cash_flow"] = cash_flow.head(8).to_dict('records')
'净资产收益率(ROE)', '总资产报酬率(ROA)', '销售毛利率', '销售净利率', except Exception as e:
'资产负债率', '流动比率', '速动比率', '存货周转率', '应收账款周转率', print(f"获取现金流量表失败: {e}")
'总资产周转率', '营业收入同比增长', '净利润同比增长'
]
# 筛选出包含关键指标的行 # 4. 获取主要财务指标(tushare未获取到时回退akshare
indicator_rows = financial_abstract[financial_abstract['指标'].isin(key_indicators)] if not financial_data["financial_ratios"]:
try:
financial_abstract = ak.stock_financial_abstract(symbol=symbol)
if financial_abstract is not None and not financial_abstract.empty:
# 提取关键财务指标
key_indicators = [
'净资产收益率(ROE)', '总资产报酬率(ROA)', '销售毛利率', '销售净利率',
'资产负债率', '流动比率', '速动比率', '存货周转率', '应收账款周转率',
'总资产周转率', '营业收入同比增长', '净利润同比增长'
]
if not indicator_rows.empty: # 筛选出包含关键指标的行
# 获取最新的报告期数据(第一列日期) indicator_rows = financial_abstract[financial_abstract['指标'].isin(key_indicators)]
date_columns = [col for col in financial_abstract.columns if col not in ['选项', '指标']]
if date_columns:
latest_date = date_columns[0] # 最新日期列
# 构建财务比率字典 if not indicator_rows.empty:
financial_ratios = {"报告期": latest_date} # 获取最新的报告期数据(第一列日期)
date_columns = [col for col in financial_abstract.columns if col not in ['选项', '指标']]
if date_columns:
latest_date = date_columns[0] # 最新日期列
# 提取每个指标的最新值 # 构建财务比率字典
for _, row in indicator_rows.iterrows(): financial_ratios = {"报告期": latest_date}
indicator_name = row['指标']
value = row.get(latest_date, 'N/A') # 提取每个指标的最新值
if value is not None and not (isinstance(value, float) and pd.isna(value)): for _, row in indicator_rows.iterrows():
try: indicator_name = row['指标']
financial_ratios[indicator_name] = str(value) value = row.get(latest_date, 'N/A')
except: if value is not None and not (isinstance(value, float) and pd.isna(value)):
try:
financial_ratios[indicator_name] = str(value)
except:
financial_ratios[indicator_name] = "N/A"
else:
financial_ratios[indicator_name] = "N/A" financial_ratios[indicator_name] = "N/A"
else:
financial_ratios[indicator_name] = "N/A"
financial_data["financial_ratios"] = financial_ratios financial_data["financial_ratios"] = financial_ratios
except Exception as e: except Exception as e:
print(f"获取财务指标失败: {e}") print(f"获取财务指标失败: {e}")
# 注意:季报数据现在由 quarterly_report_data.py 模块使用 akshare 获取(8期完整季报) # 注意:季报数据现在由 quarterly_report_data.py 模块使用 akshare 获取(8期完整季报)
# 不再使用问财获取季报,避免重复 # 不再使用问财获取季报,避免重复
+1 -1
View File
@@ -14,7 +14,7 @@ from dotenv import load_dotenv
load_dotenv() load_dotenv()
# 获取TDX API URL # 获取TDX API URL
TDX_API_URL = os.getenv('TDX_BASE_URL', 'http://127.0.0.1:5000') TDX_API_URL = os.getenv('TDX_BASE_URL', 'https://tdx.javagood.top')
print("=" * 60) print("=" * 60)
print("TDX API配置测试") print("TDX API配置测试")
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
低估值选股模块
使用pywencai获取低估值优质股票
"""
import pandas as pd
import pywencai
from datetime import datetime
from typing import Tuple, Optional
import time
class ValueStockSelector:
"""低估值选股类"""
def __init__(self):
self.raw_data = None
self.selected_stocks = None
def get_value_stocks(self, top_n: int = 10) -> Tuple[bool, Optional[pd.DataFrame], str]:
"""
获取低估值优质股票
选股策略
- 市盈率 20
- 市净率 1.5
- 股息率 1%
- 资产负债率 30%
- 非ST
- 非科创板
- 非创业板
- 按流通市值由小到大排名
Args:
top_n: 返回前N只股票
Returns:
(success, dataframe, message)
"""
try:
print(f"\n{'='*60}")
print(f"💎 低估值选股 - 数据获取中")
print(f"{'='*60}")
print(f"策略: PE≤20 + PB≤1.5 + 股息率≥1% + 资产负债率≤30%")
print(f"排除: ST、科创板、创业板")
print(f"排序: 按流通市值由小到大")
print(f"目标: 筛选前{top_n}只股票")
# 构建问财查询语句
query = (
"市盈率小于等于20"
"市净率小于等于1.5"
"股息率大于等于1%"
"资产负债率小于等于30%"
"非st"
"非科创板,"
"非创业板,"
"按流通市值由小到大排名"
)
print(f"\n查询语句: {query}")
print(f"正在调用问财接口...")
# 调用pywencai
result = pywencai.get(query=query, loop=True)
if result is None:
return False, None, "问财接口返回None,请检查网络或稍后重试"
# 转换为DataFrame
df_result = self._convert_to_dataframe(result)
if df_result is None or df_result.empty:
return False, None, "未获取到符合条件的股票数据"
print(f"✅ 成功获取 {len(df_result)} 只股票")
# 显示获取到的列名
print(f"\n获取到的数据字段:")
for col in df_result.columns[:15]:
print(f" - {col}")
if len(df_result.columns) > 15:
print(f" ... 还有 {len(df_result.columns) - 15} 个字段")
# 保存原始数据
self.raw_data = df_result
# 取前N只
if len(df_result) > top_n:
selected = df_result.head(top_n)
print(f"\n{len(df_result)} 只股票中选出前 {top_n}")
else:
selected = df_result
print(f"\n{len(df_result)} 只符合条件的股票")
self.selected_stocks = selected
# 显示选中的股票
print(f"\n✅ 选中的股票:")
for idx, row in selected.iterrows():
code = row.get('股票代码', 'N/A')
name = row.get('股票简称', 'N/A')
pe = row.get('市盈率', row.get('市盈率(动态)', 'N/A'))
pb = row.get('市净率', 'N/A')
div_rate = row.get('股息率', 'N/A')
debt_ratio = row.get('资产负债率', 'N/A')
cap = row.get('流通市值', 'N/A')
print(f" {idx+1}. {code} {name} - PE:{pe} PB:{pb} 股息率:{div_rate}% 负债率:{debt_ratio}% 流通市值:{cap}")
print(f"{'='*60}\n")
return True, selected, f"成功筛选出{len(selected)}只低估值优质股票"
except Exception as e:
error_msg = f"获取数据失败: {str(e)}"
print(f"{error_msg}")
import traceback
traceback.print_exc()
return False, None, error_msg
def _convert_to_dataframe(self, result) -> Optional[pd.DataFrame]:
"""将pywencai返回结果转换为DataFrame"""
try:
if isinstance(result, pd.DataFrame):
return result
elif isinstance(result, dict):
if 'data' in result:
return pd.DataFrame(result['data'])
elif 'result' in result:
return pd.DataFrame(result['result'])
else:
return pd.DataFrame(result)
elif isinstance(result, list):
return pd.DataFrame(result)
else:
print(f"⚠️ 未知的数据格式: {type(result)}")
return None
except Exception as e:
print(f"转换DataFrame失败: {e}")
return None
def get_stock_codes(self) -> list:
"""
获取选中股票的代码列表去掉市场后缀
Returns:
股票代码列表
"""
if self.selected_stocks is None or self.selected_stocks.empty:
return []
codes = []
for code in self.selected_stocks['股票代码'].tolist():
if isinstance(code, str):
clean_code = code.split('.')[0] if '.' in code else code
codes.append(clean_code)
else:
codes.append(str(code))
return codes
# 测试
if __name__ == "__main__":
print("=" * 60)
print("测试低估值选股模块")
print("=" * 60)
selector = ValueStockSelector()
success, df, msg = selector.get_value_stocks(top_n=10)
print(f"\n结果: {msg}")
if success and df is not None:
print(f"{len(df)} 只股票")
+290
View File
@@ -0,0 +1,290 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
低估值量化交易策略
实现基于持股周期和RSI超买的买卖择时策略
"""
import pandas as pd
from datetime import datetime, timedelta
from typing import Dict, List, Optional
import logging
from data_source_manager import data_source_manager
class ValueStockStrategy:
"""低估值量化交易策略"""
def __init__(self, initial_capital: float = 1000000.0):
"""
初始化策略
Args:
initial_capital: 初始资金默认100万
"""
self.logger = logging.getLogger(__name__)
# 策略参数
self.initial_capital = initial_capital
self.available_cash = initial_capital
self.max_stocks = 4 # 账户最大持股数
self.max_position_per_stock = 0.3 # 个股最大仓位30%
self.max_daily_buy = 2 # 单日最大买入数
self.holding_period = 30 # 持股周期(天)
self.rsi_period = 14 # RSI计算周期
self.rsi_overbought = 70 # RSI超买阈值
# 持仓信息
self.positions: Dict[str, Dict] = {} # {股票代码: {买入价, 数量, 买入日期, 持有天数}}
self.trade_history: List[Dict] = []
# 当日交易计数
self.daily_buy_count = 0
self.current_date = None
def reset_daily_counter(self, date):
"""重置当日计数器"""
if self.current_date != date:
self.current_date = date
self.daily_buy_count = 0
def can_buy(self, stock_code: str) -> tuple:
"""
检查是否可以买入
Returns:
(是否可买, 原因)
"""
if stock_code in self.positions:
return False, "已持有该股票"
if len(self.positions) >= self.max_stocks:
return False, f"已达最大持股数限制({self.max_stocks}只)"
if self.daily_buy_count >= self.max_daily_buy:
return False, f"今日已达最大买入数限制({self.max_daily_buy}只)"
if self.available_cash <= 0:
return False, "可用资金不足"
return True, "可以买入"
def calculate_buy_amount(self, stock_price: float) -> tuple:
"""
计算买入数量
Args:
stock_price: 股票价格
Returns:
(买入股数, 买入金额)
"""
max_amount = self.available_cash
max_per_stock = self.initial_capital * self.max_position_per_stock
target_amount = min(max_amount, max_per_stock)
# A股100股为1手
shares = int(target_amount / stock_price / 100) * 100
if shares < 100:
return 0, 0
actual_amount = shares * stock_price
return shares, actual_amount
def buy(self, stock_code: str, stock_name: str, price: float, date: str) -> tuple:
"""
执行买入操作
Returns:
(是否成功, 消息, 交易详情)
"""
can, reason = self.can_buy(stock_code)
if not can:
return False, reason, None
shares, amount = self.calculate_buy_amount(price)
if shares == 0:
return False, "资金不足以买入1手", None
# 更新持仓
self.positions[stock_code] = {
'name': stock_name,
'buy_price': price,
'shares': shares,
'amount': amount,
'buy_date': date,
'holding_days': 0
}
self.available_cash -= amount
self.daily_buy_count += 1
trade = {
'action': '买入',
'code': stock_code,
'name': stock_name,
'price': price,
'shares': shares,
'amount': amount,
'date': date,
'reason': '开盘买入信号'
}
self.trade_history.append(trade)
msg = f"买入 {stock_code} {stock_name} {shares}股 @ {price}元, 金额: {amount:.2f}"
return True, msg, trade
def calculate_rsi(self, stock_code: str) -> Optional[float]:
"""
计算股票的RSI指标
Args:
stock_code: 股票代码
Returns:
RSI值 None
"""
try:
# 获取近60天日线数据(tushare优先,akshare备用)
df = data_source_manager.get_stock_hist_data(
symbol=stock_code,
start_date=(datetime.now() - timedelta(days=90)).strftime("%Y%m%d"),
end_date=datetime.now().strftime("%Y%m%d"),
adjust="qfq"
)
if df is None or len(df) < self.rsi_period + 1:
return None
# 计算RSI
close = df['close'].astype(float)
delta = close.diff()
gain = delta.where(delta > 0, 0)
loss = (-delta).where(delta < 0, 0)
avg_gain = gain.rolling(window=self.rsi_period).mean()
avg_loss = loss.rolling(window=self.rsi_period).mean()
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
latest_rsi = rsi.iloc[-1]
return round(float(latest_rsi), 2) if pd.notna(latest_rsi) else None
except Exception as e:
self.logger.warning(f"RSI计算失败 {stock_code}: {e}")
return None
def should_sell(self, stock_code: str, current_date: str = None) -> tuple:
"""
判断是否应该卖出
策略
1. 持股满30天强制卖出
2. RSI超买>70卖出
Returns:
(是否卖出, 原因, RSI值)
"""
if stock_code not in self.positions:
return False, "未持有该股票", None
position = self.positions[stock_code]
position['holding_days'] += 1
# 条件1:持股满30天
if position['holding_days'] >= self.holding_period:
return True, f"持股满{self.holding_period}天,到期卖出", None
# 条件2RSI超买
rsi = self.calculate_rsi(stock_code)
if rsi is not None and rsi > self.rsi_overbought:
return True, f"RSI={rsi} 超买(>{self.rsi_overbought}),卖出离场", rsi
return False, f"继续持有 (已持{position['holding_days']}天, RSI={rsi})", rsi
def sell(self, stock_code: str, price: float, date: str, reason: str = "") -> tuple:
"""
执行卖出操作
Returns:
(是否成功, 消息, 交易详情)
"""
if stock_code not in self.positions:
return False, "未持有该股票", None
position = self.positions[stock_code]
amount = position['shares'] * price
profit = amount - position['amount']
profit_pct = (price - position['buy_price']) / position['buy_price'] * 100
trade = {
'action': '卖出',
'code': stock_code,
'name': position['name'],
'price': price,
'shares': position['shares'],
'amount': amount,
'date': date,
'buy_price': position['buy_price'],
'profit': profit,
'profit_pct': round(profit_pct, 2),
'holding_days': position['holding_days'],
'reason': reason
}
self.trade_history.append(trade)
self.available_cash += amount
del self.positions[stock_code]
emoji = "🟢" if profit >= 0 else "🔴"
msg = f"{emoji} 卖出 {stock_code} {position['name']} {position['shares']}股 @ {price}元, 盈亏: {profit:.2f}元 ({profit_pct:+.2f}%), 原因: {reason}"
return True, msg, trade
def get_portfolio_summary(self) -> Dict:
"""获取投资组合摘要"""
total_position_value = sum(
pos['shares'] * pos['buy_price'] for pos in self.positions.values()
)
total_assets = self.available_cash + total_position_value
# 统计交易
sells = [t for t in self.trade_history if t['action'] == '卖出']
total_profit = sum(t.get('profit', 0) for t in sells)
win_trades = sum(1 for t in sells if t.get('profit', 0) > 0)
total_trades = len(sells)
win_rate = (win_trades / total_trades * 100) if total_trades > 0 else 0
return {
'initial_capital': self.initial_capital,
'available_cash': round(self.available_cash, 2),
'position_value': round(total_position_value, 2),
'total_assets': round(total_assets, 2),
'total_return': round((total_assets - self.initial_capital) / self.initial_capital * 100, 2),
'total_profit': round(total_profit, 2),
'holding_count': len(self.positions),
'max_stocks': self.max_stocks,
'total_trades': total_trades,
'win_trades': win_trades,
'win_rate': round(win_rate, 2)
}
def get_positions(self) -> List[Dict]:
"""获取当前持仓列表"""
positions = []
for code, pos in self.positions.items():
positions.append({
'code': code,
'name': pos['name'],
'buy_price': pos['buy_price'],
'shares': pos['shares'],
'amount': pos['amount'],
'buy_date': pos['buy_date'],
'holding_days': pos['holding_days']
})
return positions
def get_trade_history(self) -> List[Dict]:
"""获取交易历史"""
return self.trade_history
+420
View File
@@ -0,0 +1,420 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
低估值策略UI模块
"""
import streamlit as st
import pandas as pd
from datetime import datetime
from value_stock_selector import ValueStockSelector
from value_stock_strategy import ValueStockStrategy
def display_value_stock():
"""显示低估值选股界面"""
st.markdown("""
<div style="background: linear-gradient(135deg, #1a5276 0%, #2e86c1 50%, #1a5276 100%);
padding: 2rem; border-radius: 15px; margin-bottom: 1.5rem;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);">
<h1 style="color: #fff; margin: 0; font-size: 2rem;">
💎 低估值策略 - 价值投资选股
</h1>
<p style="color: rgba(255,255,255,0.7); margin: 0.5rem 0 0 0; font-size: 0.9rem;">
基于视频 <a href="https://www.bilibili.com/video/BV1eJfxBrEjZ" target="_blank" style="color: #7ec8e3; text-decoration: underline;">头号投资法则</a>
</p>
<p style="color: rgba(255,255,255,0.8); margin: 0.3rem 0 0 0; font-size: 1.1rem;">
低PE + 低PB + 高股息 + 低负债 寻找被市场低估的优质标的
</p>
</div>
""", unsafe_allow_html=True)
st.markdown("---")
st.markdown("""
### 📋 选股策略说明
**筛选条件**
- 市盈率PE 20
- 市净率PB 1.5
- 股息率 1%
- 资产负债率 30%
- 非ST股票
- 非科创板
- 非创业板
- 按流通市值由小到大排名
**量化交易策略**
- 💰 资金量100万元
- 📈 买入时机开盘买入
- 💼 单股最大仓位30%
- 🎯 最大持股数4
- 🛒 每日最多买入2
- 📉 卖出条件①持股满30天到期卖出
- 📉 卖出条件②RSI超买>70卖出
""")
st.markdown("---")
# 参数设置
col1, col2 = st.columns([2, 1])
with col1:
top_n = st.slider(
"筛选数量",
min_value=5,
max_value=20,
value=10,
step=1,
help="选择展示的股票数量",
key="value_stock_top_n"
)
with col2:
st.info(f"💡 将筛选流通市值最小的前{top_n}只低估值股票")
st.markdown("---")
# 开始选股按钮
if st.button("🚀 开始低估值选股", type="primary", width='content', key="value_stock_start"):
with st.spinner("正在获取数据,请稍候..."):
selector = ValueStockSelector()
success, stocks_df, message = selector.get_value_stocks(top_n=top_n)
if success and stocks_df is not None:
st.session_state.value_stocks = stocks_df
st.session_state.value_stock_selector = selector
st.success(f"{message}")
st.rerun()
else:
st.error(f"{message}")
# 显示选股结果
if 'value_stocks' in st.session_state:
display_stock_results(
st.session_state.value_stocks,
st.session_state.get('value_stock_selector')
)
def display_stock_results(stocks_df: pd.DataFrame, selector):
"""显示选股结果"""
st.markdown("---")
st.markdown("## 📊 选股结果")
# 统计信息
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("筛选数量", f"{len(stocks_df)}")
with col2:
pe_col = None
for pattern in ['市盈率', '市盈率(动态)']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
pe_col = matching[0]
break
if pe_col:
valid = pd.to_numeric(stocks_df[pe_col], errors='coerce').dropna()
if len(valid) > 0:
st.metric("平均PE", f"{valid.mean():.1f}")
else:
st.metric("平均PE", "-")
else:
st.metric("平均PE", "-")
with col3:
pb_col = None
matching = [col for col in stocks_df.columns if '市净率' in col]
if matching:
pb_col = matching[0]
valid = pd.to_numeric(stocks_df[pb_col], errors='coerce').dropna()
if len(valid) > 0:
st.metric("平均PB", f"{valid.mean():.2f}")
else:
st.metric("平均PB", "-")
else:
st.metric("平均PB", "-")
with col4:
div_col = None
matching = [col for col in stocks_df.columns if '股息率' in col]
if matching:
div_col = matching[0]
valid = pd.to_numeric(stocks_df[div_col], errors='coerce').dropna()
if len(valid) > 0:
st.metric("平均股息率", f"{valid.mean():.2f}%")
else:
st.metric("平均股息率", "-")
else:
st.metric("平均股息率", "-")
st.markdown("---")
# 显示股票列表
st.markdown("### 📋 精选低估值股票")
for idx, row in stocks_df.iterrows():
code = row.get('股票代码', 'N/A')
name = row.get('股票简称', 'N/A')
# 获取关键指标用于标题
pe_val = ''
for pattern in ['市盈率', '市盈率(动态)']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
v = row.get(matching[0])
if v is not None and not pd.isna(v):
try:
pe_val = f" PE:{float(v):.1f}"
except:
pass
break
pb_val = ''
matching = [col for col in stocks_df.columns if '市净率' in col]
if matching:
v = row.get(matching[0])
if v is not None and not pd.isna(v):
try:
pb_val = f" PB:{float(v):.2f}"
except:
pass
with st.expander(
f"【第{idx+1}名】{code} - {name}{pe_val}{pb_val}",
expanded=(idx < 3)
):
display_stock_detail(row, stocks_df)
# 完整数据表格
st.markdown("---")
st.markdown("### 📊 完整数据表格")
# 选择关键列
display_cols = ['股票代码', '股票简称']
for pattern in ['最新价', '股价']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
display_cols.append(matching[0])
break
for pattern in ['市盈率', '市净率', '股息率', '资产负债率', '流通市值', '所属行业']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
display_cols.append(matching[0])
final_cols = [col for col in display_cols if col in stocks_df.columns]
if final_cols:
st.dataframe(stocks_df[final_cols], width='content', height=400)
csv = stocks_df[final_cols].to_csv(index=False, encoding='utf-8-sig')
st.download_button(
label="📥 下载股票列表CSV",
data=csv,
file_name=f"value_stock_{datetime.now().strftime('%Y%m%d')}.csv",
mime="text/csv",
key="value_csv_download"
)
# 量化交易模拟
st.markdown("---")
display_strategy_simulation(stocks_df, selector)
def display_stock_detail(row: pd.Series, df: pd.DataFrame):
"""显示单个股票详情"""
def is_valid(value):
if value is None:
return False
if isinstance(value, float) and pd.isna(value):
return False
if isinstance(value, str) and value.strip() in ('', 'N/A', 'nan', 'None'):
return False
return True
def fmt(value, suffix=''):
if not is_valid(value):
return "-"
try:
return f"{float(value):.2f}{suffix}"
except:
return str(value) + suffix
# 基本估值数据
col1, col2, col3, col4 = st.columns(4)
with col1:
for p in ['市盈率', '市盈率(动态)']:
m = [c for c in df.columns if p in c]
if m:
st.metric("📊 市盈率(PE)", fmt(row.get(m[0])))
break
with col2:
m = [c for c in df.columns if '市净率' in c]
if m:
st.metric("📊 市净率(PB)", fmt(row.get(m[0])))
with col3:
m = [c for c in df.columns if '股息率' in c]
if m:
st.metric("💰 股息率", fmt(row.get(m[0]), '%'))
with col4:
m = [c for c in df.columns if '资产负债率' in c]
if m:
st.metric("📉 资产负债率", fmt(row.get(m[0]), '%'))
# 补充信息
st.markdown("**其他指标**")
info_parts = []
for pattern in ['最新价', '股价', '流通市值', '总市值', '所属行业', '涨跌幅']:
m = [c for c in df.columns if pattern in c]
if m:
val = row.get(m[0])
if is_valid(val):
info_parts.append(f"**{pattern}**: {val}")
if info_parts:
st.markdown(" | ".join(info_parts))
def display_strategy_simulation(stocks_df: pd.DataFrame, selector):
"""显示量化交易策略模拟"""
st.markdown("## 🎯 策略模拟")
st.info("""
**策略规则**
- 📈 **买入**开盘价买入单股最大仓位30%每日最多买2只
- 📉 **卖出条件①**持股满30天到期自动卖出
- 📉 **卖出条件②**RSI(14) > 70 超买触发卖出
- 🎯 **最大持股**4
- 💰 **初始资金**100万元
""")
col1, col2 = st.columns(2)
with col1:
if st.button("🎮 开始策略模拟", type="primary", width='content', key="value_sim_start"):
st.session_state.show_value_strategy_sim = True
with col2:
pass
if st.session_state.get('show_value_strategy_sim'):
run_strategy_simulation(stocks_df)
def run_strategy_simulation(stocks_df: pd.DataFrame):
"""运行策略模拟"""
st.markdown("---")
st.markdown("### 📈 策略模拟执行")
strategy = ValueStockStrategy(initial_capital=1000000.0)
# 模拟买入
st.markdown("#### 1️⃣ 模拟买入信号")
buy_results = []
current_date = datetime.now().strftime("%Y-%m-%d")
for idx, row in stocks_df.head(strategy.max_daily_buy).iterrows():
code = str(row.get('股票代码', '')).split('.')[0]
name = row.get('股票简称', 'N/A')
# 尝试获取价格
price = 0
for p in ['最新价', '股价']:
m = [c for c in stocks_df.columns if p in c]
if m:
try:
price = float(row.get(m[0], 0))
except:
pass
if price > 0:
break
if price > 0:
success, message, trade = strategy.buy(code, name, price, current_date)
buy_results.append({
'success': success,
'message': message,
'trade': trade
})
for result in buy_results:
if result['success']:
st.success(result['message'])
else:
st.warning(f"⚠️ {result['message']}")
# RSI检查
st.markdown("---")
st.markdown("#### 2️⃣ RSI卖出信号检测")
with st.spinner("正在计算RSI指标..."):
for code, pos in list(strategy.positions.items()):
rsi = strategy.calculate_rsi(code)
if rsi is not None:
if rsi > strategy.rsi_overbought:
st.warning(f"⚠️ {code} {pos['name']} RSI={rsi} > {strategy.rsi_overbought},触发超买卖出信号!")
else:
st.info(f"{code} {pos['name']} RSI={rsi},正常范围")
else:
st.info(f"{code} {pos['name']} RSI计算中...")
# 显示持仓
st.markdown("---")
st.markdown("#### 3️⃣ 当前持仓")
positions = strategy.get_positions()
if positions:
positions_df = pd.DataFrame(positions)
st.dataframe(positions_df, width='content')
else:
st.info("暂无持仓")
# 显示账户摘要
st.markdown("---")
st.markdown("#### 4️⃣ 账户摘要")
summary = strategy.get_portfolio_summary()
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("初始资金", f"{summary['initial_capital']:,.0f}")
with col2:
st.metric("可用资金", f"{summary['available_cash']:,.0f}")
with col3:
st.metric("持仓市值", f"{summary['position_value']:,.0f}")
with col4:
st.metric("总资产", f"{summary['total_assets']:,.0f}")
st.markdown("---")
st.markdown("#### 📝 策略说明")
st.markdown("""
**后续操作**
1. **持有期管理**系统跟踪每只股票的持有天数30天到期
2. **RSI监测**每日收盘后计算RSI(14)
- RSI > 70超买信号提示卖出
- RSI < 30超卖信号可作为加仓参考
3. **轮动买入**卖出后释放资金继续买入新的低估值股票
**风险提示**
- 本策略为模拟演示实际交易存在滑点手续费等成本
- 低估值不代表没有风险价值陷阱需警惕
- 请谨慎评估风险理性投资
""")
# 主入口
if __name__ == "__main__":
display_value_stock()
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据源改造实盘验证脚本
请在有网络的环境运行例如在 PyCharm aiagents-stock 解释器中执行
它会逐项调用各模块的数据获取接口并打印 PASS/FAIL 与实际使用的数据源日志
"""
import os
import sys
PROJECT = os.environ.get("AIAGENTS_STOCK_HOME", "/Users/songzhuoyuan/Desktop/code/python/aiagents-stock")
if not os.path.isdir(PROJECT):
PROJECT = input("请输入项目绝对路径: ").strip()
sys.path.insert(0, PROJECT)
os.chdir(PROJECT)
print("=" * 60)
print("数据源改造验证 - 股票代码统一使用 600637")
print("=" * 60)
from data_source_manager import data_source_manager
print("Tushare Token 已配置:", bool(data_source_manager.tushare_token))
print("Tushare 可用:", data_source_manager.tushare_available)
print()
passed = 0
failed = 0
def test(name, fn):
global passed, failed
try:
result = fn()
if result:
passed += 1
print(f"PASS | {name}")
else:
failed += 1
print(f"FAIL | {name}(返回空/失败)")
except Exception as e:
failed += 1
print(f"FAIL | {name}: {type(e).__name__}: {str(e)[:120]}")
# 1. 数据源管理器(核心)
test("历史数据(1年,前复权)", lambda: data_source_manager.get_stock_hist_data("600637", start_date="20250801", end_date="20260810", adjust="qfq") is not None)
test("基本信息", lambda: data_source_manager.get_stock_basic_info("600637").get("name") != "未知")
test("实时行情", lambda: bool(data_source_manager.get_realtime_quotes("600637")))
test("财务数据(利润表)", lambda: data_source_manager.get_financial_data("600637", "income") is not None)
# 2. 资金流向
def fund_flow_test():
from fund_flow_akshare import FundFlowAkshareDataFetcher
return FundFlowAkshareDataFetcher().get_fund_flow_data("600637").get("data_success")
test("资金流向", fund_flow_test)
# 3. 季报
def quarterly_test():
from quarterly_report_data import QuarterlyReportDataFetcher
return QuarterlyReportDataFetcher().get_quarterly_reports("600637").get("data_success")
test("季报(三表+指标)", quarterly_test)
# 4. 新闻
def news_test():
from qstock_news_data import QStockNewsDataFetcher
return QStockNewsDataFetcher().get_stock_news("600637").get("data_success")
test("个股新闻", news_test)
# 5. 市场情绪
def sentiment_tests():
from market_sentiment_data import MarketSentimentDataFetcher
f = MarketSentimentDataFetcher()
return (f._get_turnover_rate("600637") is not None and
f._get_market_index_sentiment() is not None)
test("情绪-换手率/大盘指数", sentiment_tests)
def limit_test():
from market_sentiment_data import MarketSentimentDataFetcher
return MarketSentimentDataFetcher()._get_limit_up_down_stats() is not None
test("情绪-涨跌停统计", limit_test)
def margin_test():
from market_sentiment_data import MarketSentimentDataFetcher
return MarketSentimentDataFetcher()._get_margin_trading_data("600637") is not None
test("情绪-融资融券", margin_test)
# 6. 综合股票数据(主分析链路)
def stock_data_test():
from stock_data import StockDataFetcher
f = StockDataFetcher()
info = f.get_stock_info("600637")
data = f.get_stock_data("600637", "1y")
fin = f.get_financial_data("600637")
return (isinstance(data, dict) and "error" not in data) and len(fin) > 0
test("主分析链路(信息/行情/财务)", stock_data_test)
# 7. 港股(可选,若积分不足会自动回退akshare)
def hk_test():
from stock_data import StockDataFetcher
f = StockDataFetcher()
data = f.get_stock_data("00700", "1mo")
return isinstance(data, dict) and "error" not in data
test("港股日线(00700)", hk_test)
print()
print("=" * 60)
print(f"验证完成:通过 {passed} 项,失败 {failed}")
if failed:
print("注意:失败项通常表示对应 tushare 接口积分不足或网络问题,程序会自动回退 akshare,不影响使用。")
print("=" * 60)
-7
View File
@@ -1,7 +0,0 @@
@echo off
set VENV_PATH=.\TradEnv
set PYTHON_EXE="%VENV_PATH%\python.exe"
set STREAMLIT_MODULE="streamlit.cli"
cd /d ..\AIagentsStock
%PYTHON_EXE% -m streamlit run app.py
pause