Compare commits
13 Commits
a7c53b2579
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e59399a4d6 | |||
| ae39742f8a | |||
| 02c135b465 | |||
| befdc32aea | |||
| 2ce13e5bae | |||
| 9ee27de0ed | |||
| 63628ffdd5 | |||
| cfda09f3e4 | |||
| f3af15ddc8 | |||
| bce3b9645f | |||
| 6d47c5ecf4 | |||
| e7904518d3 | |||
| 053558169b |
@@ -15,6 +15,10 @@ DEEPSEEK_API_KEY=your_actual_deepseek_api_key_here
|
||||
# DeepSeek API基础URL(可选,使用默认值即可)
|
||||
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 Token(可选,用于获取更多金融数据)
|
||||
|
||||
@@ -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}"
|
||||
@@ -1,14 +1,82 @@
|
||||
# 🤖 复合多AI智能体股票团队分析系统
|
||||
|
||||
- 初心:在股市摸爬滚打多年,自学自编各种指标,花冤柉钱学习了各种战法各种策略,也曾入各种小班,总是赚少赔多,逐渐失去在股市玩的信心。自从去年deepseek上市,一直探索用ai辅助分析,且近日受tradingagents项目启发(感谢原作),多agent结合跟踪主力资金战法(某指每年收费6000rmb),用各种ai辅助编程,拼凑了这么个小程序,根据软件提供的辅助信息,实盘测试盈率还是挺高的,并且逐步形成了自己的交易系统,近一个月来,账户也慢慢在扰亏为盈。开源此软件的目的,就是为了使像我一样的小散,不再迷范。也许这个软件不能让你发大财,但是他能给你足够的信心。最后提醒:股市有风险,入市需谨慎!
|
||||
## QQ交流群:1059277514
|
||||
## B站本地部署教程1:https://www.bilibili.com/video/BV1qHFPz9EXY/
|
||||
## docker部署教程2:https://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更新 - 净利增长策略 📈
|
||||
|
||||
新增“净利增长策略”选股板块,专注稳健成长股票:
|
||||
新增“净利增长策略”选股板块,专注稳健成长股票:回测年化100%+
|
||||
|
||||
**核心功能:**
|
||||
- ✅ **稳健成长** - 净利润增长率≥10%,稳健可持续
|
||||
@@ -375,7 +443,7 @@ StockAPI龙虎榜接口(每日更新,免费1000次)
|
||||
- 灵活设置运行时间(盘前/盘后/晚间)
|
||||
- 手动触发和状态监控
|
||||
- **数据来源**:AKShare免费数据(行情、资金、新闻)
|
||||
- **AI模型**:DeepSeek Chat / Reasoner双模型
|
||||
- **AI模型**:支持任意 OpenAI 兼容模型(通过 `.env` 配置切换)
|
||||
- **报告导出**:支持PDF格式完整报告
|
||||
- **使用场景**:盘前策略、板块轮动、风险规避
|
||||
|
||||
@@ -573,6 +641,11 @@ cp .env.example .env
|
||||
```env
|
||||
# DeepSeek API配置(必需)
|
||||
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_TOKEN=your_tushare_token # 在 https://tushare.pro 注册获取
|
||||
@@ -585,7 +658,7 @@ EMAIL_FROM=your_email@qq.com
|
||||
EMAIL_PASSWORD=your_authorization_code
|
||||
EMAIL_TO=receiver@example.com
|
||||
|
||||
# Webhook通知配置(可选)⭐️ 新增 - 用于实时监测和智策定时分析
|
||||
# Webhook通知配置(可选)- 用于实时监测和智策定时分析
|
||||
WEBHOOK_ENABLED=false
|
||||
WEBHOOK_TYPE=dingtalk # 或 feishu
|
||||
WEBHOOK_URL=your_webhook_url_here
|
||||
@@ -701,9 +774,7 @@ streamlit run app.py
|
||||
2. **设置分析参数**
|
||||
- **分析模式**:选择"指定日期"或"最近N天"
|
||||
- **日期选择**:建议选择昨天的日期(龙虎榜数据T日更新)
|
||||
- **AI模型**:
|
||||
- **deepseek-chat**:速度快,8-12分钟完成
|
||||
- **deepseek-reasoner**:推理深入,12-18分钟完成
|
||||
- **AI模型**:自动使用 `.env` 中配置的默认模型
|
||||
|
||||
3. **开始分析**
|
||||
- 点击"🚀 开始分析"按钮
|
||||
@@ -777,12 +848,9 @@ streamlit run app.py
|
||||
### 智策板块分析使用流程 ⭐️ 全新功能
|
||||
1. **进入智策板块**
|
||||
- 点击侧边栏"🎯 智策板块"按钮
|
||||
- AI模型自动使用 `.env` 中配置的默认模型
|
||||
|
||||
2. **选择AI模型**
|
||||
- **deepseek-chat**:标准模型,速度快(2-3分钟)
|
||||
- **deepseek-reasoner**:推理增强模型,分析深度更高(3-5分钟)
|
||||
|
||||
3. **开始分析**
|
||||
2. **开始分析**
|
||||
- 点击"🚀 开始智策分析"按钮
|
||||
- 系统自动执行:
|
||||
- 获取市场数据(30秒)
|
||||
@@ -790,7 +858,7 @@ streamlit run app.py
|
||||
- 综合研判(30秒)
|
||||
- 生成预测报告
|
||||
|
||||
4. **查看分析结果**
|
||||
3. **查看分析结果**
|
||||
分析完成后,查看四个标签页:
|
||||
|
||||
- **📋 核心预测**:
|
||||
@@ -813,12 +881,12 @@ streamlit run app.py
|
||||
- 板块多空信心度对比图
|
||||
- 板块热度分布图
|
||||
|
||||
5. **导出PDF报告**
|
||||
4. **导出PDF报告**
|
||||
- 点击"📥 生成PDF报告"按钮
|
||||
- 等待生成(3-5秒)
|
||||
- 点击"💾 下载PDF"保存
|
||||
|
||||
6. **配置定时分析** ⭐️
|
||||
5. **配置定时分析** ⭐️
|
||||
展开"⏰ 定时分析设置":
|
||||
|
||||
**步骤1:配置邮件/Webhook**
|
||||
@@ -845,14 +913,14 @@ streamlit run app.py
|
||||
- 查看运行状态和上次运行时间
|
||||
- 需要时可点击"⏹️ 停止定时任务"
|
||||
|
||||
7. **使用技巧**
|
||||
6. **使用技巧**
|
||||
- **最佳使用时间**:盘前8:30-9:30或盘后15:30-20:00
|
||||
- **关注信心度**:≥8分为高信心,可重点关注
|
||||
- **结合轮动**:重点关注"潜力接力板块",最佳布局时机
|
||||
- **热度判断**:升温板块有机会,降温板块注意风险
|
||||
- **四维共振**:宏观、基本面、资金、情绪共振时信号更强
|
||||
|
||||
8. **通知内容示例**
|
||||
7. **通知内容示例**
|
||||
|
||||
**邮件/Webhook推送内容**:
|
||||
```
|
||||
@@ -1196,7 +1264,7 @@ AI股票分析系统
|
||||
|
||||
**longhubang_ui.py**
|
||||
- 智瞰龙虎主界面
|
||||
- 分析参数设置(日期/模型)
|
||||
- 分析参数设置
|
||||
- 实时进度显示
|
||||
- 四个结果标签页(推荐/报告/数据/图表)
|
||||
- 历史报告查询
|
||||
@@ -1241,7 +1309,6 @@ AI股票分析系统
|
||||
|
||||
**sector_strategy_ui.py**
|
||||
- 智策板块主界面
|
||||
- AI模型选择
|
||||
- 分析进度显示
|
||||
- 四个结果标签页展示
|
||||
- 定时分析设置面板
|
||||
@@ -1315,7 +1382,8 @@ AI股票分析系统
|
||||
- **降级机制**:TDX → Tushare → AKShare 多层数据源保障
|
||||
|
||||
### AI模型
|
||||
- **语言模型**:DeepSeek Chat API
|
||||
- **语言模型**:支持任意 OpenAI 兼容模型(DeepSeek、通义千问、GPT-4o 等)
|
||||
- **模型配置**:通过 `.env` 文件中的 `DEFAULT_MODEL_NAME` 一键切换
|
||||
- **分析框架**:多智能体协作
|
||||
- **决策逻辑**:综合评分机制
|
||||
|
||||
@@ -1336,6 +1404,7 @@ AI股票分析系统
|
||||
# .env 文件
|
||||
DEEPSEEK_API_KEY=your_api_key
|
||||
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
|
||||
DEFAULT_MODEL_NAME=deepseek-chat # 支持任意OpenAI兼容模型
|
||||
```
|
||||
|
||||
**重要提示**:
|
||||
|
||||
+29
-34
@@ -1,18 +1,19 @@
|
||||
from deepseek_client import DeepSeekClient
|
||||
from typing import Dict, Any
|
||||
import concurrent.futures
|
||||
import time
|
||||
import config
|
||||
|
||||
class StockAnalysisAgents:
|
||||
"""股票分析AI智能体集合"""
|
||||
|
||||
def __init__(self, model="deepseek-chat"):
|
||||
self.model = model
|
||||
self.deepseek_client = DeepSeekClient(model=model)
|
||||
def __init__(self, model=None):
|
||||
self.model = model or config.DEFAULT_MODEL_NAME
|
||||
self.deepseek_client = DeepSeekClient(model=self.model)
|
||||
|
||||
def technical_analyst_agent(self, stock_info: Dict, stock_data: Any, indicators: Dict) -> Dict[str, Any]:
|
||||
"""技术面分析智能体"""
|
||||
print("🔍 技术分析师正在分析中...")
|
||||
time.sleep(1) # 模拟分析时间
|
||||
|
||||
analysis = self.deepseek_client.technical_analysis(stock_info, stock_data, indicators)
|
||||
|
||||
@@ -37,8 +38,6 @@ class StockAnalysisAgents:
|
||||
else:
|
||||
print(" ⚠ 未获取到季报数据,将基于基本财务数据分析")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
analysis = self.deepseek_client.fundamental_analysis(stock_info, financial_data, quarterly_data)
|
||||
|
||||
return {
|
||||
@@ -60,8 +59,6 @@ class StockAnalysisAgents:
|
||||
else:
|
||||
print(" ⚠ 未获取到资金流向数据,将基于技术指标分析")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
analysis = self.deepseek_client.fund_flow_analysis(stock_info, indicators, fund_flow_data)
|
||||
|
||||
return {
|
||||
@@ -83,8 +80,6 @@ class StockAnalysisAgents:
|
||||
else:
|
||||
print(" ⚠ 未获取到风险数据,将基于基本信息分析")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 构建风险数据文本
|
||||
risk_data_text = ""
|
||||
if risk_data and risk_data.get('data_success'):
|
||||
@@ -226,8 +221,6 @@ class StockAnalysisAgents:
|
||||
else:
|
||||
print(" ⚠ 未获取到详细情绪数据,将基于基本信息分析")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 构建带有市场情绪数据的prompt
|
||||
sentiment_data_text = ""
|
||||
if sentiment_data and sentiment_data.get('data_success'):
|
||||
@@ -316,8 +309,6 @@ class StockAnalysisAgents:
|
||||
else:
|
||||
print(" ⚠ 未获取到新闻数据,将基于基本信息分析")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 构建带有新闻数据的prompt
|
||||
news_text = ""
|
||||
if news_data and news_data.get('data_success'):
|
||||
@@ -435,32 +426,38 @@ class StockAnalysisAgents:
|
||||
print(f"📋 参与分析的分析师: {', '.join(active_analysts)}")
|
||||
print("=" * 50)
|
||||
|
||||
# 并行运行各个分析师
|
||||
# 并行运行各个分析师(全部同时启动,等待最慢的一个完成)
|
||||
agents_results = {}
|
||||
|
||||
# 技术面分析
|
||||
agent_jobs = []
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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("=" * 50)
|
||||
@@ -470,7 +467,6 @@ class StockAnalysisAgents:
|
||||
def conduct_team_discussion(self, agents_results: Dict[str, Any], stock_info: Dict) -> str:
|
||||
"""进行团队讨论"""
|
||||
print("🤝 分析团队正在进行综合讨论...")
|
||||
time.sleep(2)
|
||||
|
||||
# 收集参与分析的分析师名单和报告
|
||||
participants = []
|
||||
@@ -537,7 +533,6 @@ class StockAnalysisAgents:
|
||||
def make_final_decision(self, discussion_result: str, stock_info: Dict, indicators: Dict) -> Dict[str, Any]:
|
||||
"""制定最终投资决策"""
|
||||
print("📋 正在制定最终投资决策...")
|
||||
time.sleep(1)
|
||||
|
||||
decision = self.deepseek_client.final_decision(discussion_result, stock_info, indicators)
|
||||
|
||||
|
||||
@@ -7,8 +7,11 @@ from datetime import datetime
|
||||
import time
|
||||
import base64
|
||||
import os
|
||||
# 从新的配置文件导入model_options
|
||||
from model_config import model_options
|
||||
import config
|
||||
|
||||
# 注入所有外部请求(akshare/tushare等)的默认超时
|
||||
from http_timeout import install_default_requests_timeout
|
||||
install_default_requests_timeout()
|
||||
|
||||
from stock_data import StockDataFetcher
|
||||
from ai_agents import StockAnalysisAgents
|
||||
@@ -32,22 +35,13 @@ st.set_page_config(
|
||||
initial_sidebar_state="expanded"
|
||||
)
|
||||
|
||||
# 模型选择器
|
||||
def model_selector():
|
||||
"""模型选择器"""
|
||||
# 在侧边栏显示当前模型信息(统一使用.env配置)
|
||||
def show_current_model_info():
|
||||
"""显示当前使用的AI模型信息"""
|
||||
st.sidebar.markdown("---")
|
||||
st.sidebar.subheader("🤖 AI模型选择")
|
||||
|
||||
|
||||
|
||||
selected_model = st.sidebar.selectbox(
|
||||
"选择AI模型",
|
||||
options=list(model_options.keys()),
|
||||
format_func=lambda x: model_options[x],
|
||||
help="DeepSeek Reasoner提供更强的推理能力,但响应时间可能更长"
|
||||
)
|
||||
|
||||
return selected_model
|
||||
st.sidebar.subheader("🤖 AI模型")
|
||||
st.sidebar.info(f"当前模型: **{config.DEFAULT_MODEL_NAME}**")
|
||||
st.sidebar.caption("可在「环境配置」中修改模型名称")
|
||||
|
||||
# 自定义CSS样式 - 专业版
|
||||
st.markdown("""
|
||||
@@ -286,6 +280,9 @@ def main():
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# 学习资源展示
|
||||
st.info("📺 **新手必看干货**:为了在股市长久生存,建议您观看 👉 [股票知识讲解合集](https://www.bilibili.com/video/BV1Y2FGzzEeS/) 和 [投资认知提升合集](https://www.bilibili.com/video/BV1ugBMBAEbW) 👈,相信会对您有很大帮助!")
|
||||
|
||||
# 侧边栏
|
||||
with st.sidebar:
|
||||
# 快捷导航 - 移到顶部
|
||||
@@ -295,7 +292,7 @@ def main():
|
||||
if st.button("🏠 股票分析", width='stretch', key="nav_home", help="返回首页,进行单只股票的深度分析"):
|
||||
# 清除所有功能页面标志
|
||||
for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
|
||||
'show_sector_strategy', 'show_longhubang', 'show_portfolio', 'show_low_price_bull', 'show_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:
|
||||
del st.session_state[key]
|
||||
|
||||
@@ -329,7 +326,14 @@ def main():
|
||||
if st.button("📈 净利增长", width='stretch', key="nav_profit_growth", help="净利润增长稳健股票筛选策略"):
|
||||
st.session_state.show_profit_growth = True
|
||||
for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy',
|
||||
'show_longhubang', 'show_portfolio', 'show_main_force', 'show_low_price_bull', 'show_small_cap', 'show_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:
|
||||
del st.session_state[key]
|
||||
|
||||
@@ -354,7 +358,14 @@ def main():
|
||||
if st.button("📰 新闻流量", width='stretch', key="nav_news_flow", help="新闻流量监测与短线指导"):
|
||||
st.session_state.show_news_flow = True
|
||||
for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
|
||||
'show_sector_strategy', 'show_portfolio', 'show_smart_monitor', 'show_low_price_bull', 'show_longhubang']:
|
||||
'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:
|
||||
del st.session_state[key]
|
||||
|
||||
@@ -416,9 +427,9 @@ def main():
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# 模型选择器
|
||||
selected_model = model_selector()
|
||||
st.session_state.selected_model = selected_model
|
||||
# 显示当前模型信息
|
||||
show_current_model_info()
|
||||
st.session_state.selected_model = config.DEFAULT_MODEL_NAME
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
@@ -472,7 +483,18 @@ def main():
|
||||
1. 数据获取 → 2. 技术分析
|
||||
3. 基本面分析 → 4. 资金分析
|
||||
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()
|
||||
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:
|
||||
display_sector_strategy()
|
||||
@@ -534,6 +562,12 @@ def main():
|
||||
display_news_flow_monitor()
|
||||
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:
|
||||
display_config_manager()
|
||||
@@ -825,18 +859,22 @@ def parse_stock_list(stock_input):
|
||||
|
||||
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:
|
||||
symbol: 股票代码
|
||||
period: 数据周期
|
||||
enabled_analysts_config: 分析师配置字典
|
||||
selected_model: 选择的AI模型
|
||||
selected_model: 选择的AI模型,默认从 .env 的 DEFAULT_MODEL_NAME 读取
|
||||
|
||||
返回分析结果或错误信息
|
||||
"""
|
||||
try:
|
||||
# 使用默认模型
|
||||
if selected_model is None:
|
||||
selected_model = config.DEFAULT_MODEL_NAME
|
||||
|
||||
# 使用默认配置
|
||||
if enabled_analysts_config is None:
|
||||
enabled_analysts_config = {
|
||||
@@ -983,7 +1021,7 @@ def run_batch_analysis(stock_list, period, batch_mode="顺序分析"):
|
||||
'sentiment': st.session_state.get('enable_sentiment', 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})")
|
||||
@@ -1256,7 +1294,7 @@ def run_stock_analysis(symbol, period):
|
||||
# 6. 初始化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)
|
||||
progress_bar.progress(55)
|
||||
|
||||
@@ -2154,6 +2192,37 @@ def display_config_manager():
|
||||
)
|
||||
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. 复制密钥并粘贴到上方输入框")
|
||||
|
||||
with tab2:
|
||||
|
||||
@@ -8,6 +8,9 @@ load_dotenv(override=True)
|
||||
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
|
||||
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", "")
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@ class ConfigManager:
|
||||
"required": False,
|
||||
"type": "text"
|
||||
},
|
||||
"DEFAULT_MODEL_NAME": {
|
||||
"value": "deepseek-chat",
|
||||
"description": "AI模型名称(支持OpenAI兼容模型)",
|
||||
"required": False,
|
||||
"type": "text"
|
||||
},
|
||||
"TUSHARE_TOKEN": {
|
||||
"value": "",
|
||||
"description": "Tushare数据接口Token(可选)",
|
||||
|
||||
+159
-130
@@ -11,6 +11,10 @@ from dotenv import load_dotenv
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# 注入外部请求默认超时(覆盖akshare、tushare等基于requests的调用)
|
||||
from http_timeout import install_default_requests_timeout
|
||||
install_default_requests_timeout()
|
||||
|
||||
|
||||
class DataSourceManager:
|
||||
"""数据源管理器 - 实现akshare与tushare自动切换"""
|
||||
@@ -25,7 +29,7 @@ class DataSourceManager:
|
||||
try:
|
||||
import tushare as ts
|
||||
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
|
||||
print("✅ Tushare数据源初始化成功")
|
||||
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'):
|
||||
"""
|
||||
获取股票历史数据(优先akshare,失败时使用tushare)
|
||||
获取股票历史数据(优先tushare,失败时使用akshare)
|
||||
|
||||
Args:
|
||||
symbol: 股票代码(6位数字)
|
||||
@@ -55,10 +59,63 @@ class DataSourceManager:
|
||||
else:
|
||||
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_bar(daily接口不支持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:
|
||||
import akshare as ak
|
||||
print(f"[Akshare] 正在获取 {symbol} 的历史数据...")
|
||||
print(f"[Akshare] 正在获取 {symbol} 的历史数据(备用数据源)...")
|
||||
|
||||
df = ak.stock_zh_a_hist(
|
||||
symbol=symbol,
|
||||
@@ -86,60 +143,18 @@ class DataSourceManager:
|
||||
df['date'] = pd.to_datetime(df['date'])
|
||||
print(f"[Akshare] ✅ 成功获取 {len(df)} 条数据")
|
||||
return df
|
||||
else:
|
||||
print(f"[Akshare] ❌ 未获取到数据")
|
||||
except Exception as 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("❌ 所有数据源均获取失败")
|
||||
return None
|
||||
|
||||
def get_stock_basic_info(self, symbol):
|
||||
"""
|
||||
获取股票基本信息(优先akshare,失败时使用tushare)
|
||||
获取股票基本信息(优先tushare,失败时使用akshare)
|
||||
|
||||
Args:
|
||||
symbol: 股票代码
|
||||
@@ -154,10 +169,34 @@ class DataSourceManager:
|
||||
"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:
|
||||
import akshare as ak
|
||||
print(f"[Akshare] 正在获取 {symbol} 的基本信息...")
|
||||
print(f"[Akshare] 正在获取 {symbol} 的基本信息(备用数据源)...")
|
||||
|
||||
stock_info = ak.stock_individual_info_em(symbol=symbol)
|
||||
if stock_info is not None and not stock_info.empty:
|
||||
@@ -181,33 +220,11 @@ class DataSourceManager:
|
||||
except Exception as 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
|
||||
|
||||
def get_realtime_quotes(self, symbol):
|
||||
"""
|
||||
获取实时行情数据(优先akshare,失败时使用tushare)
|
||||
获取实时行情数据(优先tushare,失败时使用akshare)
|
||||
|
||||
Args:
|
||||
symbol: 股票代码
|
||||
@@ -217,10 +234,51 @@ class DataSourceManager:
|
||||
"""
|
||||
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:
|
||||
import akshare as ak
|
||||
print(f"[Akshare] 正在获取 {symbol} 的实时行情...")
|
||||
print(f"[Akshare] 正在获取 {symbol} 的实时行情(备用数据源)...")
|
||||
|
||||
df = ak.stock_zh_a_spot_em()
|
||||
stock_df = df[df['代码'] == symbol]
|
||||
@@ -245,41 +303,11 @@ class DataSourceManager:
|
||||
except Exception as 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
|
||||
|
||||
def get_financial_data(self, symbol, report_type='income'):
|
||||
"""
|
||||
获取财务数据(优先akshare,失败时使用tushare)
|
||||
获取财务数据(优先tushare,失败时使用akshare)
|
||||
|
||||
Args:
|
||||
symbol: 股票代码
|
||||
@@ -288,30 +316,10 @@ class DataSourceManager:
|
||||
Returns:
|
||||
DataFrame: 财务数据
|
||||
"""
|
||||
# 优先使用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}")
|
||||
|
||||
# akshare失败,尝试tushare
|
||||
# 优先使用tushare
|
||||
if self.tushare_available:
|
||||
try:
|
||||
print(f"[Tushare] 正在获取 {symbol} 的财务数据(备用数据源)...")
|
||||
print(f"[Tushare] 正在获取 {symbol} 的财务数据(主要数据源)...")
|
||||
|
||||
ts_code = self._convert_to_ts_code(symbol)
|
||||
|
||||
@@ -327,9 +335,31 @@ class DataSourceManager:
|
||||
if df is not None and not df.empty:
|
||||
print(f"[Tushare] ✅ 成功获取财务数据")
|
||||
return df
|
||||
else:
|
||||
print(f"[Tushare] ❌ 未获取到财务数据,尝试备用数据源")
|
||||
except Exception as 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
|
||||
|
||||
def _convert_to_ts_code(self, symbol):
|
||||
@@ -376,4 +406,3 @@ class DataSourceManager:
|
||||
|
||||
# 全局数据源管理器实例
|
||||
data_source_manager = DataSourceManager()
|
||||
|
||||
|
||||
+6
-3
@@ -1,16 +1,19 @@
|
||||
import openai
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, List, Any, Optional
|
||||
import config
|
||||
|
||||
class DeepSeekClient:
|
||||
"""DeepSeek API客户端"""
|
||||
|
||||
def __init__(self, model="deepseek-chat"):
|
||||
self.model = model
|
||||
def __init__(self, model=None):
|
||||
self.model = model or config.DEFAULT_MODEL_NAME
|
||||
self.client = openai.OpenAI(
|
||||
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,
|
||||
|
||||
+45
-48
@@ -109,56 +109,54 @@ class FundFlowAkshareDataFetcher:
|
||||
def _get_individual_fund_flow(self, symbol, market):
|
||||
"""获取个股资金流向数据(支持akshare和tushare自动切换)"""
|
||||
try:
|
||||
# 优先使用akshare的stock_individual_fund_flow接口
|
||||
print(f" [Akshare] 正在获取资金流向 (市场: {market})...")
|
||||
# 优先使用tushare
|
||||
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:
|
||||
print(f" [Akshare] 未找到资金流向数据,尝试备用数据源...")
|
||||
|
||||
# akshare失败,尝试tushare
|
||||
if data_source_manager.tushare_available:
|
||||
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:
|
||||
print(f" [Akshare] 正在获取资金流向 (市场: {market})(备用数据源)...")
|
||||
df = ak.stock_individual_fund_flow(stock=symbol, market=market)
|
||||
if df is None or df.empty:
|
||||
print(f" [Akshare] 未找到资金流向数据")
|
||||
return None
|
||||
|
||||
# akshare 返回的数据是按时间正序排列(从旧到新),所以使用 tail() 获取最近N天的数据
|
||||
@@ -341,4 +339,3 @@ if __name__ == "__main__":
|
||||
print(f"\n获取失败: {data.get('error', '未知错误')}")
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
统一的外部请求超时控制工具
|
||||
|
||||
- install_default_requests_timeout: 给所有基于 requests 的外部请求
|
||||
(akshare、tushare、pywencai 等)注入默认超时,调用方未指定 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("未知错误"))
|
||||
Binary file not shown.
@@ -6,15 +6,16 @@
|
||||
from deepseek_client import DeepSeekClient
|
||||
from typing import Dict, Any, List
|
||||
import time
|
||||
import config
|
||||
|
||||
|
||||
class LonghubangAgents:
|
||||
"""龙虎榜AI分析师集合"""
|
||||
|
||||
def __init__(self, model="deepseek-chat"):
|
||||
self.model = model
|
||||
self.deepseek_client = DeepSeekClient(model=model)
|
||||
print(f"[智瞰龙虎] AI分析师系统初始化 (模型: {model})")
|
||||
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 youzi_behavior_analyst(self, longhubang_data: str, summary: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
@@ -11,12 +11,13 @@ from typing import Dict, Any, List
|
||||
from datetime import datetime, timedelta
|
||||
import time
|
||||
import logging
|
||||
import config
|
||||
|
||||
|
||||
class LonghubangEngine:
|
||||
"""龙虎榜综合分析引擎"""
|
||||
|
||||
def __init__(self, model="deepseek-chat", db_path='longhubang.db'):
|
||||
def __init__(self, model=None, db_path='longhubang.db'):
|
||||
"""
|
||||
初始化分析引擎
|
||||
|
||||
|
||||
+11
-18
@@ -13,6 +13,7 @@ import base64
|
||||
|
||||
from longhubang_engine import LonghubangEngine
|
||||
from longhubang_pdf import LonghubangPDFGenerator
|
||||
import config
|
||||
|
||||
|
||||
def display_longhubang():
|
||||
@@ -110,7 +111,7 @@ def display_analysis_tab():
|
||||
st.subheader("🔍 龙虎榜综合分析")
|
||||
|
||||
# 参数设置
|
||||
col1, col2, col3 = st.columns([2, 2, 2])
|
||||
col1, col2 = st.columns([2, 2])
|
||||
|
||||
with col1:
|
||||
analysis_mode = st.selectbox(
|
||||
@@ -135,18 +136,8 @@ def display_analysis_tab():
|
||||
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:
|
||||
analyze_button = st.button("🚀 开始分析", type="primary", width='stretch')
|
||||
@@ -166,12 +157,12 @@ def display_analysis_tab():
|
||||
if 'longhubang_result' in st.session_state:
|
||||
del st.session_state.longhubang_result
|
||||
|
||||
# 准备参数
|
||||
# 准备参数(使用.env中配置的默认模型)
|
||||
if analysis_mode == "指定日期":
|
||||
date_str = selected_date.strftime('%Y-%m-%d')
|
||||
run_longhubang_analysis(model=selected_model, date=date_str)
|
||||
run_longhubang_analysis(date=date_str)
|
||||
else:
|
||||
run_longhubang_analysis(model=selected_model, days=days)
|
||||
run_longhubang_analysis(days=days)
|
||||
|
||||
# 显示分析结果
|
||||
if 'longhubang_result' in st.session_state:
|
||||
@@ -183,8 +174,10 @@ def display_analysis_tab():
|
||||
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)
|
||||
@@ -1397,7 +1390,7 @@ def run_longhubang_batch_analysis():
|
||||
'sentiment': False,
|
||||
'news': False
|
||||
},
|
||||
selected_model='deepseek-chat'
|
||||
selected_model=config.DEFAULT_MODEL_NAME
|
||||
)
|
||||
|
||||
results.append({
|
||||
@@ -1428,7 +1421,7 @@ def run_longhubang_batch_analysis():
|
||||
'sentiment': False,
|
||||
'news': False
|
||||
},
|
||||
selected_model='deepseek-chat'
|
||||
selected_model=config.DEFAULT_MODEL_NAME
|
||||
)
|
||||
return {"code": code, "result": result}
|
||||
except Exception as e:
|
||||
|
||||
@@ -10,6 +10,7 @@ import pywencai
|
||||
from datetime import datetime
|
||||
from typing import Tuple, Optional
|
||||
import time
|
||||
from http_timeout import call_with_timeout
|
||||
|
||||
|
||||
class LowPriceBullSelector:
|
||||
@@ -60,7 +61,7 @@ class LowPriceBullSelector:
|
||||
print(f"正在调用问财接口...")
|
||||
|
||||
# 调用pywencai
|
||||
result = pywencai.get(query=query, loop=True)
|
||||
result = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
|
||||
|
||||
if result is None:
|
||||
return False, None, "问财接口返回None,请检查网络或稍后重试"
|
||||
|
||||
@@ -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. 第三维度:政策方向
|
||||
这是中国版美林时钟最重要的增量分析:
|
||||
- 货币政策方向(宽松/中性/收紧?):分析LPR、MLF、准备金率等
|
||||
- 财政政策方向(积极/稳健/收缩?):分析专项债、减税降费等
|
||||
- 产业政策方向:重点扶持哪些领域?
|
||||
- 房地产政策方向:放松还是收紧?
|
||||
- 政策的即时效果和滞后效应
|
||||
|
||||
### 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("初始化完成")
|
||||
@@ -0,0 +1,559 @@
|
||||
"""
|
||||
宏观周期分析 - 数据采集模块
|
||||
采集宏观经济数据(GDP、CPI/PPI、PMI、利率、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("制造业PMI(50为荣枯线):")
|
||||
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')}")
|
||||
@@ -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)} 字符")
|
||||
|
||||
# 阶段2:AI智能体分析
|
||||
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("引擎初始化完成")
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
@@ -13,15 +13,16 @@ from ai_agents import StockAnalysisAgents
|
||||
from deepseek_client import DeepSeekClient
|
||||
import time
|
||||
import json
|
||||
import config
|
||||
|
||||
class MainForceAnalyzer:
|
||||
"""主力选股分析器 - 批量整体分析"""
|
||||
|
||||
def __init__(self, model='deepseek-chat'):
|
||||
def __init__(self, model=None):
|
||||
self.selector = main_force_selector
|
||||
self.fetcher = StockDataFetcher()
|
||||
self.model = model
|
||||
self.agents = StockAnalysisAgents(model=model)
|
||||
self.model = model or config.DEFAULT_MODEL_NAME
|
||||
self.agents = StockAnalysisAgents(model=self.model)
|
||||
self.deepseek_client = self.agents.deepseek_client
|
||||
self.raw_stocks = None
|
||||
self.final_recommendations = []
|
||||
|
||||
@@ -11,6 +11,7 @@ import pywencai
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Tuple
|
||||
import time
|
||||
from http_timeout import call_with_timeout
|
||||
|
||||
class MainForceStockSelector:
|
||||
"""主力选股类"""
|
||||
@@ -71,7 +72,7 @@ class MainForceStockSelector:
|
||||
print(f"查询语句: {query[:100]}...")
|
||||
|
||||
try:
|
||||
result = pywencai.get(query=query, loop=True)
|
||||
result = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
|
||||
|
||||
if result is None:
|
||||
print(f" ⚠️ 方案{i}返回None,尝试下一个方案")
|
||||
@@ -387,4 +388,3 @@ class MainForceStockSelector:
|
||||
|
||||
# 全局实例
|
||||
main_force_selector = MainForceStockSelector()
|
||||
|
||||
|
||||
+5
-14
@@ -127,25 +127,15 @@ def display_main_force_selector():
|
||||
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("---")
|
||||
|
||||
# 开始分析按钮
|
||||
# 开始分析按钮(使用.env中配置的默认模型)
|
||||
if st.button("🚀 开始主力选股", type="primary", width='content'):
|
||||
|
||||
with st.spinner("正在获取数据并分析,这可能需要几分钟..."):
|
||||
|
||||
# 创建分析器
|
||||
analyzer = MainForceAnalyzer(model=model)
|
||||
# 创建分析器(使用默认模型)
|
||||
analyzer = MainForceAnalyzer()
|
||||
|
||||
# 运行分析
|
||||
result = analyzer.run_full_analysis(
|
||||
@@ -613,7 +603,8 @@ def run_main_force_batch_analysis():
|
||||
'sentiment': False, # 禁用以提升速度
|
||||
'news': False # 禁用以提升速度
|
||||
}
|
||||
selected_model = 'deepseek-chat'
|
||||
import config
|
||||
selected_model = config.DEFAULT_MODEL_NAME
|
||||
period = '1y'
|
||||
|
||||
# 创建进度显示
|
||||
|
||||
+160
-86
@@ -329,11 +329,56 @@ class MarketSentimentDataFetcher:
|
||||
}
|
||||
|
||||
def _get_turnover_rate(self, symbol):
|
||||
"""获取换手率数据(支持akshare和tushare自动切换)"""
|
||||
"""获取换手率数据(优先tushare,失败时使用akshare)"""
|
||||
try:
|
||||
# 优先使用akshare获取最近的换手率数据
|
||||
print(f" [Akshare] 正在获取换手率数据...")
|
||||
# 获取A股实时行情数据(不需要参数)
|
||||
# 优先使用tushare(daily_basic,取最近10个交易日保证非交易日也有数据)
|
||||
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=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()
|
||||
if df is not None and not df.empty:
|
||||
stock_data = df[df['代码'] == symbol]
|
||||
@@ -367,56 +412,41 @@ class MarketSentimentDataFetcher:
|
||||
except Exception as 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
|
||||
|
||||
def _get_market_index_sentiment(self):
|
||||
"""获取大盘指数情绪(支持akshare和tushare自动切换)"""
|
||||
try:
|
||||
# 优先使用akshare获取上证指数实时数据
|
||||
print(f" [Akshare] 正在获取大盘指数数据...")
|
||||
# 使用正确的symbol参数
|
||||
# 优先使用tushare(index_daily,取最近10个交易日保证非交易日也有数据)
|
||||
if data_source_manager.tushare_available:
|
||||
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="上证系列指数")
|
||||
if df is not None and not df.empty:
|
||||
# 查找上证指数(代码为000001)
|
||||
@@ -471,30 +501,6 @@ class MarketSentimentDataFetcher:
|
||||
except Exception as 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
|
||||
|
||||
def _get_limit_up_down_stats(self):
|
||||
@@ -503,19 +509,50 @@ class MarketSentimentDataFetcher:
|
||||
# 获取今日涨停和跌停统计
|
||||
today = datetime.now().strftime('%Y%m%d')
|
||||
|
||||
# 获取涨停股票
|
||||
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
|
||||
limit_up_count = 0
|
||||
limit_down_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
|
||||
# 优先使用tushare的涨跌停列表
|
||||
if data_source_manager.tushare_available:
|
||||
try:
|
||||
print(f" [Tushare] 正在获取涨跌停数据(主要数据源)...")
|
||||
df_ll = data_source_manager.tushare_api.limit_list_d(trade_date=today)
|
||||
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:
|
||||
@@ -549,6 +586,44 @@ class MarketSentimentDataFetcher:
|
||||
def _get_margin_trading_data(self, symbol):
|
||||
"""获取融资融券数据"""
|
||||
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)
|
||||
try:
|
||||
# 方法1:获取沪深融资融券明细
|
||||
@@ -762,4 +837,3 @@ if __name__ == "__main__":
|
||||
print(formatted_text)
|
||||
else:
|
||||
print(f"\n获取失败: {sentiment_data.get('error', '未知错误')}")
|
||||
|
||||
|
||||
+30
-3
@@ -1,10 +1,13 @@
|
||||
"""
|
||||
模型配置文件
|
||||
包含所有可用的AI模型选项
|
||||
支持通过 .env 中的 DEFAULT_MODEL_NAME 自定义默认模型
|
||||
"""
|
||||
import config
|
||||
|
||||
model_options = {
|
||||
"deepseek-chat": "DeepSeek Chat (默认)",
|
||||
# 预置模型列表(用户可以在UI中选择)
|
||||
_preset_models = {
|
||||
"deepseek-chat": "DeepSeek Chat",
|
||||
"deepseek-reasoner": "DeepSeek Reasoner (推理增强)",
|
||||
"qwen-plus": "qwen-plus (阿里百炼)",
|
||||
"qwen-plus-latest": "qwen-plus-latest (阿里百炼)",
|
||||
@@ -20,5 +23,29 @@ model_options = {
|
||||
"zai-org/GLM-4.6": "智谱(硅基流动)",
|
||||
"moonshotai/Kimi-K2-Instruct-0905": "Kimi (硅基流动)",
|
||||
"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
|
||||
@@ -9,6 +9,7 @@ import sys
|
||||
import io
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
from http_timeout import call_with_timeout
|
||||
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
@@ -100,7 +101,7 @@ class NewsAnnouncementDataFetcher:
|
||||
print(f" 使用问财查询: {query}")
|
||||
|
||||
# 使用pywencai查询
|
||||
result = pywencai.get(query=query, loop=True)
|
||||
result = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
|
||||
|
||||
if result is None:
|
||||
print(f" 问财查询返回None")
|
||||
@@ -191,7 +192,7 @@ class NewsAnnouncementDataFetcher:
|
||||
print(f" 使用问财查询: {query}")
|
||||
|
||||
# 使用pywencai查询
|
||||
result = pywencai.get(query=query, loop=True)
|
||||
result = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
|
||||
|
||||
if result is None:
|
||||
print(f" 问财查询返回None")
|
||||
@@ -342,4 +343,3 @@ if __name__ == "__main__":
|
||||
print(formatted_text)
|
||||
else:
|
||||
print(f"\n获取失败: {data.get('error', '未知错误')}")
|
||||
|
||||
|
||||
Binary file not shown.
+4
-3
@@ -16,14 +16,15 @@ logger = logging.getLogger(__name__)
|
||||
class NewsFlowAgents:
|
||||
"""新闻流量智能分析代理"""
|
||||
|
||||
def __init__(self, model: str = "deepseek-chat"):
|
||||
def __init__(self, model: str = None):
|
||||
"""
|
||||
初始化代理
|
||||
|
||||
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._init_client()
|
||||
|
||||
|
||||
+1
-1
@@ -766,7 +766,7 @@ class NewsFlowDatabase:
|
||||
analysis_data.get('confidence', 50),
|
||||
analysis_data.get('summary', ''),
|
||||
analysis_data.get('raw_response', ''),
|
||||
analysis_data.get('model_used', 'deepseek-chat'),
|
||||
analysis_data.get('model_used', 'unknown'),
|
||||
analysis_data.get('analysis_time', 0)
|
||||
))
|
||||
|
||||
|
||||
+1
-1
@@ -250,7 +250,7 @@ class NewsFlowEngine:
|
||||
'advice': ai_analysis.get('investment_advice', {}).get('advice', '观望'),
|
||||
'confidence': ai_analysis.get('investment_advice', {}).get('confidence', 50),
|
||||
'summary': ai_analysis.get('investment_advice', {}).get('summary', ''),
|
||||
'model_used': 'deepseek-chat',
|
||||
'model_used': getattr(self, 'model', 'unknown'),
|
||||
'analysis_time': ai_analysis.get('analysis_time', 0),
|
||||
}
|
||||
self.db.save_ai_analysis(quick_result['snapshot_id'], ai_record)
|
||||
|
||||
@@ -11,19 +11,20 @@ from datetime import datetime
|
||||
|
||||
# 导入必要的模块
|
||||
from portfolio_db import portfolio_db
|
||||
import config
|
||||
|
||||
|
||||
class PortfolioManager:
|
||||
"""持仓管理器类"""
|
||||
|
||||
def __init__(self, model="deepseek-chat"):
|
||||
def __init__(self, model=None):
|
||||
"""
|
||||
初始化持仓管理器
|
||||
|
||||
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
|
||||
|
||||
# ==================== 持仓股票管理 ====================
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import logging
|
||||
from typing import Tuple, Optional
|
||||
import pandas as pd
|
||||
from http_timeout import call_with_timeout
|
||||
|
||||
|
||||
class ProfitGrowthSelector:
|
||||
@@ -50,7 +51,7 @@ class ProfitGrowthSelector:
|
||||
self.logger.info(f"开始执行净利增长选股,查询条件: {query}")
|
||||
|
||||
# 调用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:
|
||||
self.logger.warning("未获取到符合条件的股票")
|
||||
|
||||
+84
-7
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
新闻数据获取模块
|
||||
使用akshare获取股票的最新新闻信息(替代qstock)
|
||||
优先使用tushare,失败时使用akshare获取股票的最新新闻信息
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
@@ -9,6 +9,7 @@ import io
|
||||
import warnings
|
||||
from datetime import datetime, timedelta
|
||||
import akshare as ak
|
||||
from data_source_manager import data_source_manager
|
||||
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
@@ -30,6 +31,9 @@ def _setup_stdout_encoding():
|
||||
|
||||
_setup_stdout_encoding()
|
||||
|
||||
# 记录tushare news接口是否无权限(避免每次分析都重复请求失败)
|
||||
_tushare_news_unavailable = False
|
||||
|
||||
|
||||
class QStockNewsDataFetcher:
|
||||
"""新闻数据获取类(使用akshare作为数据源)"""
|
||||
@@ -37,7 +41,7 @@ class QStockNewsDataFetcher:
|
||||
def __init__(self):
|
||||
self.max_items = 30 # 最多获取的新闻数量
|
||||
self.available = True
|
||||
print("✓ 新闻数据获取器初始化成功(akshare数据源)")
|
||||
print("✓ 新闻数据获取器初始化成功(tushare优先/akshare备用)")
|
||||
|
||||
def get_stock_news(self, symbol):
|
||||
"""
|
||||
@@ -67,7 +71,7 @@ class QStockNewsDataFetcher:
|
||||
|
||||
try:
|
||||
# 获取新闻数据
|
||||
print(f"📰 正在使用qstock获取 {symbol} 的最新新闻...")
|
||||
print(f"📰 正在获取 {symbol} 的最新新闻...")
|
||||
news_data = self._get_news_data(symbol)
|
||||
|
||||
if news_data:
|
||||
@@ -89,9 +93,20 @@ class QStockNewsDataFetcher:
|
||||
return symbol.isdigit() and len(symbol) == 6
|
||||
|
||||
def _get_news_data(self, symbol):
|
||||
"""获取新闻数据(使用akshare)"""
|
||||
"""获取新闻数据(优先tushare,失败时使用akshare)"""
|
||||
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 = []
|
||||
|
||||
@@ -223,6 +238,69 @@ class QStockNewsDataFetcher:
|
||||
traceback.print_exc()
|
||||
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):
|
||||
"""
|
||||
将新闻数据格式化为适合AI阅读的文本
|
||||
@@ -236,7 +314,7 @@ class QStockNewsDataFetcher:
|
||||
if data.get("news_data"):
|
||||
news_data = data["news_data"]
|
||||
text_parts.append(f"""
|
||||
【最新新闻 - akshare数据源】
|
||||
【最新新闻 - tushare/akshare自动切换】
|
||||
查询时间:{news_data.get('query_time', 'N/A')}
|
||||
时间范围:{news_data.get('date_range', 'N/A')}
|
||||
新闻数量:{news_data.get('count', 0)}条
|
||||
@@ -303,4 +381,3 @@ if __name__ == "__main__":
|
||||
print(f"\n获取失败: {data.get('error', '未知错误')}")
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
+174
-14
@@ -9,6 +9,8 @@ import io
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
import akshare as ak
|
||||
from http_timeout import call_with_timeout
|
||||
from data_source_manager import data_source_manager
|
||||
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
@@ -67,26 +69,34 @@ class QuarterlyReportDataFetcher:
|
||||
try:
|
||||
print(f"📊 正在获取 {symbol} 的季报数据...")
|
||||
|
||||
# 获取利润表
|
||||
income_data = self._get_income_statement(symbol)
|
||||
# 获取利润表(优先tushare,失败时回退akshare)
|
||||
income_data = self._get_income_statement_from_tushare(symbol)
|
||||
if income_data is None:
|
||||
income_data = self._get_income_statement(symbol)
|
||||
if income_data:
|
||||
data["income_statement"] = income_data
|
||||
print(f" ✓ 成功获取 {len(income_data.get('data', []))} 期利润表数据")
|
||||
|
||||
# 获取资产负债表
|
||||
balance_data = self._get_balance_sheet(symbol)
|
||||
# 获取资产负债表(优先tushare,失败时回退akshare)
|
||||
balance_data = self._get_balance_sheet_from_tushare(symbol)
|
||||
if balance_data is None:
|
||||
balance_data = self._get_balance_sheet(symbol)
|
||||
if balance_data:
|
||||
data["balance_sheet"] = balance_data
|
||||
print(f" ✓ 成功获取 {len(balance_data.get('data', []))} 期资产负债表数据")
|
||||
|
||||
# 获取现金流量表
|
||||
cash_flow_data = self._get_cash_flow(symbol)
|
||||
# 获取现金流量表(优先tushare,失败时回退akshare)
|
||||
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:
|
||||
data["cash_flow"] = cash_flow_data
|
||||
print(f" ✓ 成功获取 {len(cash_flow_data.get('data', []))} 期现金流量表数据")
|
||||
|
||||
# 获取财务指标
|
||||
indicators_data = self._get_financial_indicators(symbol)
|
||||
# 获取财务指标(优先tushare,失败时回退akshare)
|
||||
indicators_data = self._get_financial_indicators_from_tushare(symbol)
|
||||
if indicators_data is None:
|
||||
indicators_data = self._get_financial_indicators(symbol)
|
||||
if indicators_data:
|
||||
data["financial_indicators"] = indicators_data
|
||||
print(f" ✓ 成功获取 {len(indicators_data.get('data', []))} 期财务指标数据")
|
||||
@@ -108,11 +118,160 @@ class QuarterlyReportDataFetcher:
|
||||
"""判断是否为中国股票"""
|
||||
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):
|
||||
"""获取利润表数据"""
|
||||
try:
|
||||
# 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:
|
||||
print(f" 未找到利润表数据")
|
||||
@@ -151,7 +310,8 @@ class QuarterlyReportDataFetcher:
|
||||
"""获取资产负债表数据"""
|
||||
try:
|
||||
# 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:
|
||||
print(f" 未找到资产负债表数据")
|
||||
@@ -190,7 +350,8 @@ class QuarterlyReportDataFetcher:
|
||||
"""获取现金流量表数据"""
|
||||
try:
|
||||
# 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:
|
||||
print(f" 未找到现金流量表数据")
|
||||
@@ -229,7 +390,7 @@ class QuarterlyReportDataFetcher:
|
||||
"""获取财务指标数据"""
|
||||
try:
|
||||
# 使用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:
|
||||
print(f" 未找到财务指标数据")
|
||||
@@ -292,7 +453,7 @@ class QuarterlyReportDataFetcher:
|
||||
|
||||
text_parts = []
|
||||
text_parts.append(f"""
|
||||
【季度财务报告数据 - akshare数据源】
|
||||
【季度财务报告数据 - tushare/akshare自动切换】
|
||||
股票代码:{data.get('symbol', 'N/A')}
|
||||
数据期数:最近{self.periods}期季报
|
||||
|
||||
@@ -423,4 +584,3 @@ if __name__ == "__main__":
|
||||
print(f"\n获取失败: {data.get('error', '未知错误')}")
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from typing import Dict, Any
|
||||
import time
|
||||
import warnings
|
||||
import os
|
||||
from http_timeout import call_with_timeout
|
||||
|
||||
# 屏蔽pywencai的Node.js警告信息(不影响功能)
|
||||
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
||||
@@ -108,7 +109,7 @@ class RiskDataFetcher:
|
||||
query = f"{symbol}限售解禁"
|
||||
|
||||
# 使用pywencai查询
|
||||
response = pywencai.get(query=query, loop=True)
|
||||
response = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
|
||||
|
||||
if response is None:
|
||||
return result
|
||||
@@ -171,7 +172,7 @@ class RiskDataFetcher:
|
||||
query = f"{symbol}大股东减持公告"
|
||||
|
||||
# 使用pywencai查询
|
||||
response = pywencai.get(query=query, loop=True)
|
||||
response = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
|
||||
|
||||
if response is None:
|
||||
return result
|
||||
@@ -234,7 +235,7 @@ class RiskDataFetcher:
|
||||
query = f"{symbol}近期重要事件"
|
||||
|
||||
# 使用pywencai查询
|
||||
response = pywencai.get(query=query, loop=True)
|
||||
response = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
|
||||
|
||||
if response is None:
|
||||
return result
|
||||
@@ -466,4 +467,3 @@ if __name__ == "__main__":
|
||||
if risk_data['data_success']:
|
||||
print("\n格式化的风险数据:")
|
||||
print(fetcher.format_risk_data_for_ai(risk_data))
|
||||
|
||||
|
||||
Binary file not shown.
@@ -6,15 +6,16 @@
|
||||
from deepseek_client import DeepSeekClient
|
||||
from typing import Dict, Any
|
||||
import time
|
||||
import config
|
||||
|
||||
|
||||
class SectorStrategyAgents:
|
||||
"""板块策略AI智能体集合"""
|
||||
|
||||
def __init__(self, model="deepseek-chat"):
|
||||
self.model = model
|
||||
self.deepseek_client = DeepSeekClient(model=model)
|
||||
print(f"[智策] AI智能体系统初始化 (模型: {model})")
|
||||
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 macro_strategist_agent(self, market_data: Dict, news_data: list) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
+48
-36
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
智策板块数据采集模块
|
||||
使用AKShare获取板块相关数据
|
||||
优先使用Tushare,失败时使用AKShare获取板块相关数据
|
||||
"""
|
||||
|
||||
import akshare as ak
|
||||
@@ -12,6 +12,7 @@ import logging
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from sector_strategy_db import SectorStrategyDatabase
|
||||
from data_source_manager import data_source_manager
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
@@ -254,40 +255,52 @@ class SectorStrategyDataFetcher:
|
||||
except:
|
||||
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:
|
||||
# 上证指数
|
||||
df_sh = ak.stock_zh_index_spot_em(symbol="上证指数")
|
||||
if df_sh is not None and not df_sh.empty:
|
||||
overview["sh_index"] = {
|
||||
"code": "000001",
|
||||
"name": "上证指数",
|
||||
"close": df_sh.iloc[0].get('最新价', 0),
|
||||
"change_pct": df_sh.iloc[0].get('涨跌幅', 0),
|
||||
"change": df_sh.iloc[0].get('涨跌额', 0)
|
||||
}
|
||||
|
||||
# 深证成指
|
||||
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)
|
||||
}
|
||||
sh_index = _get_index_data("000001", "000001.SH", "上证指数")
|
||||
if sh_index:
|
||||
overview["sh_index"] = sh_index
|
||||
sz_index = _get_index_data("399001", "399001.SZ", "深证成指")
|
||||
if sz_index:
|
||||
overview["sz_index"] = sz_index
|
||||
cyb_index = _get_index_data("399006", "399006.SZ", "创业板指")
|
||||
if cyb_index:
|
||||
overview["cyb_index"] = cyb_index
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -310,7 +323,7 @@ class SectorStrategyDataFetcher:
|
||||
try:
|
||||
import tushare as ts
|
||||
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] ✅ 初始化成功")
|
||||
except Exception as e:
|
||||
print(f" [Tushare] 初始化失败: {e}")
|
||||
@@ -757,4 +770,3 @@ if __name__ == "__main__":
|
||||
print(f"\n... (总长度: {len(formatted_text)} 字符)")
|
||||
else:
|
||||
print(f"\n数据采集失败: {data.get('error', '未知错误')}")
|
||||
|
||||
|
||||
@@ -11,20 +11,21 @@ import time
|
||||
import json
|
||||
import pandas as pd
|
||||
import logging
|
||||
import config
|
||||
|
||||
|
||||
class SectorStrategyEngine:
|
||||
"""板块策略综合研判引擎"""
|
||||
|
||||
def __init__(self, model="deepseek-chat"):
|
||||
self.model = model
|
||||
self.agents = SectorStrategyAgents(model=model)
|
||||
self.deepseek_client = DeepSeekClient(model=model)
|
||||
def __init__(self, model=None):
|
||||
self.model = model or config.DEFAULT_MODEL_NAME
|
||||
self.agents = SectorStrategyAgents(model=self.model)
|
||||
self.deepseek_client = DeepSeekClient(model=self.model)
|
||||
self.database = SectorStrategyDatabase()
|
||||
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"[智策引擎] 初始化完成 (模型: {model})")
|
||||
print(f"[智策引擎] 初始化完成 (模型: {self.model})")
|
||||
|
||||
def save_raw_data_with_fallback(self, data_type, data_df, data_date=None):
|
||||
"""
|
||||
|
||||
@@ -128,7 +128,7 @@ class SectorStrategyScheduler:
|
||||
|
||||
# 2. 运行AI分析
|
||||
print("[智策定时] [2/3] AI智能体分析中...")
|
||||
engine = SectorStrategyEngine(model="deepseek-chat")
|
||||
engine = SectorStrategyEngine()
|
||||
result = engine.run_comprehensive_analysis(data)
|
||||
|
||||
if not result.get("success"):
|
||||
|
||||
+8
-20
@@ -112,27 +112,13 @@ def display_analysis_tab():
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# 模型选择
|
||||
col1, col2, col3 = st.columns([2, 2, 2])
|
||||
# 操作按钮
|
||||
col1, col2 = st.columns([2, 2])
|
||||
|
||||
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')
|
||||
|
||||
with col3:
|
||||
st.write("")
|
||||
st.write("")
|
||||
with col2:
|
||||
if st.button("🔄 清除结果", width='content'):
|
||||
if 'sector_strategy_result' in st.session_state:
|
||||
del st.session_state.sector_strategy_result
|
||||
@@ -141,13 +127,13 @@ def display_analysis_tab():
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# 开始分析
|
||||
# 开始分析(使用.env中配置的默认模型)
|
||||
if analyze_button:
|
||||
# 清除之前的结果
|
||||
if 'sector_strategy_result' in st.session_state:
|
||||
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:
|
||||
@@ -261,8 +247,10 @@ def display_report_detail(report_id):
|
||||
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)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import logging
|
||||
from typing import Tuple, Optional
|
||||
import pandas as pd
|
||||
from http_timeout import call_with_timeout
|
||||
|
||||
|
||||
class SmallCapSelector:
|
||||
@@ -54,7 +55,7 @@ class SmallCapSelector:
|
||||
self.logger.info(f"开始执行小市值策略选股,查询条件: {query}")
|
||||
|
||||
# 调用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:
|
||||
self.logger.warning("未获取到符合条件的股票")
|
||||
|
||||
Binary file not shown.
+48
-30
@@ -54,7 +54,7 @@ class SmartMonitorDataFetcher:
|
||||
try:
|
||||
import tushare as ts
|
||||
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备用数据源初始化成功")
|
||||
except Exception as 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]:
|
||||
"""
|
||||
获取实时行情(带重试和降级机制)
|
||||
优先使用TDX,失败时降级到AKShare,最后降级到Tushare
|
||||
优先使用TDX,失败时降级到Tushare,最后降级到AKShare
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码(如:600519)
|
||||
@@ -82,11 +82,20 @@ class SmartMonitorDataFetcher:
|
||||
if quote:
|
||||
return quote
|
||||
else:
|
||||
self.logger.warning(f"TDX获取失败 {stock_code},尝试降级到AKShare")
|
||||
self.logger.warning(f"TDX获取失败 {stock_code},尝试降级到Tushare")
|
||||
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):
|
||||
try:
|
||||
# 1.1 获取股票基本信息(名称)
|
||||
@@ -167,18 +176,14 @@ class SmartMonitorDataFetcher:
|
||||
else:
|
||||
self.logger.warning(f"AKShare获取失败 {stock_code}(已重试{retry}次),尝试降级")
|
||||
|
||||
# 降级到Tushare
|
||||
if self.ts_pro:
|
||||
self.logger.info(f"降级到Tushare获取 {stock_code}...")
|
||||
return self._get_realtime_quote_from_tushare(stock_code)
|
||||
else:
|
||||
self.logger.error(f"AKShare失败且未配置Tushare,无法获取 {stock_code} 行情")
|
||||
return None
|
||||
# 所有数据源都失败
|
||||
self.logger.error(f"所有数据源都无法获取 {stock_code} 行情")
|
||||
return None
|
||||
|
||||
def get_technical_indicators(self, stock_code: str, period: str = 'daily', retry: int = 1) -> Optional[Dict]:
|
||||
"""
|
||||
计算技术指标(带降级机制)
|
||||
优先使用TDX,失败时降级到AKShare,最后降级到Tushare
|
||||
优先使用TDX,失败时降级到Tushare,最后降级到AKShare
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
@@ -197,11 +202,21 @@ class SmartMonitorDataFetcher:
|
||||
if indicators:
|
||||
return indicators
|
||||
else:
|
||||
self.logger.warning(f"TDX计算技术指标失败 {stock_code},尝试降级到AKShare")
|
||||
self.logger.warning(f"TDX计算技术指标失败 {stock_code},尝试降级到Tushare")
|
||||
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):
|
||||
try:
|
||||
# 获取历史数据(最近200个交易日,用于计算指标)
|
||||
@@ -237,13 +252,9 @@ class SmartMonitorDataFetcher:
|
||||
self.logger.warning(f"AKShare获取历史数据失败 {stock_code}(已重试{retry}次),尝试降级到Tushare")
|
||||
break
|
||||
|
||||
# 方法3: 降级到Tushare
|
||||
if self.ts_pro:
|
||||
self.logger.info(f"降级到Tushare获取 {stock_code} 历史数据...")
|
||||
return self._get_technical_indicators_from_tushare(stock_code, period)
|
||||
else:
|
||||
self.logger.error(f"AKShare失败且未配置Tushare,无法获取 {stock_code} 技术指标")
|
||||
return None
|
||||
# 所有数据源都失败
|
||||
self.logger.error(f"所有数据源都无法获取 {stock_code} 技术指标")
|
||||
return None
|
||||
|
||||
def _calculate_all_indicators(self, df: pd.DataFrame, stock_code: str) -> Optional[Dict]:
|
||||
"""
|
||||
@@ -439,6 +450,17 @@ class SmartMonitorDataFetcher:
|
||||
"""
|
||||
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):
|
||||
try:
|
||||
# 获取个股资金流(新版AKShare API参数调整)
|
||||
@@ -495,12 +517,9 @@ class SmartMonitorDataFetcher:
|
||||
self.logger.warning(f"AKShare获取资金流向失败 {stock_code}(已重试{retry}次),尝试降级到Tushare")
|
||||
break
|
||||
|
||||
# 降级到Tushare
|
||||
if self.ts_pro:
|
||||
return self._get_main_force_from_tushare(stock_code)
|
||||
else:
|
||||
self.logger.error(f"AKShare失败且未配置Tushare,无法获取 {stock_code} 资金流向")
|
||||
return None
|
||||
# 所有数据源都失败
|
||||
self.logger.error(f"所有数据源都无法获取 {stock_code} 资金流向")
|
||||
return None
|
||||
|
||||
def get_comprehensive_data(self, stock_code: str) -> Dict:
|
||||
"""
|
||||
@@ -822,4 +841,3 @@ if __name__ == '__main__':
|
||||
print("\n主力资金:")
|
||||
print(f" 主力净额: {data['main_force']['main_net']:.2f}万")
|
||||
print(f" 主力动向: {data['main_force']['trend']}")
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import logging
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime, time
|
||||
import pytz
|
||||
import config
|
||||
|
||||
|
||||
class SmartMonitorDeepSeek:
|
||||
@@ -20,7 +21,7 @@ class SmartMonitorDeepSeek:
|
||||
api_key: DeepSeek API密钥
|
||||
"""
|
||||
self.api_key = api_key
|
||||
self.base_url = "https://api.deepseek.com/v1"
|
||||
self.base_url = config.DEEPSEEK_BASE_URL
|
||||
self.headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
@@ -138,7 +139,7 @@ class SmartMonitorDeepSeek:
|
||||
'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:
|
||||
"""
|
||||
调用DeepSeek API
|
||||
@@ -154,6 +155,8 @@ class SmartMonitorDeepSeek:
|
||||
"""
|
||||
import requests
|
||||
|
||||
model = model or config.DEFAULT_MODEL_NAME
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
|
||||
+22
-17
@@ -333,15 +333,26 @@ class SmartMonitorKline:
|
||||
self.logger.info(f"✅ TDX获取K线数据成功 {stock_code},共{len(df)}条")
|
||||
return df
|
||||
else:
|
||||
self.logger.warning(f"TDX未返回K线数据 {stock_code},尝试降级到AKShare")
|
||||
self.logger.warning(f"TDX未返回K线数据 {stock_code},尝试降级到Tushare")
|
||||
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')
|
||||
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:
|
||||
import akshare as ak
|
||||
df = ak.stock_zh_a_hist(
|
||||
@@ -358,17 +369,9 @@ class SmartMonitorKline:
|
||||
self.logger.info(f"✅ AKShare获取K线数据成功 {stock_code},共{len(df)}条")
|
||||
return df
|
||||
else:
|
||||
self.logger.warning(f"AKShare未返回K线数据 {stock_code},尝试降级到Tushare")
|
||||
self.logger.warning(f"AKShare未返回K线数据 {stock_code}")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"AKShare获取K线数据失败 {stock_code}: {type(e).__name__}, 尝试降级到Tushare")
|
||||
|
||||
# 方法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.warning(f"AKShare获取K线数据失败 {stock_code}: {type(e).__name__}")
|
||||
|
||||
self.logger.error(f"所有数据源都无法获取K线数据 {stock_code}")
|
||||
return None
|
||||
@@ -404,12 +407,15 @@ class SmartMonitorKline:
|
||||
end_date = datetime.now().strftime('%Y%m%d')
|
||||
start_date = (datetime.now() - timedelta(days=days + 60)).strftime('%Y%m%d')
|
||||
|
||||
# 获取日K线数据(前复权)
|
||||
df = ts_pro.daily(
|
||||
# 获取日K线数据(前复权,daily接口不支持adj参数,需用pro_bar)
|
||||
import tushare as ts
|
||||
df = ts.pro_bar(
|
||||
api=ts_pro,
|
||||
ts_code=ts_code,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
adj='qfq'
|
||||
adj='qfq',
|
||||
retry_count=1
|
||||
)
|
||||
|
||||
if df is None or df.empty:
|
||||
@@ -493,4 +499,3 @@ if __name__ == '__main__':
|
||||
print("K线图已保存到 test_kline.html")
|
||||
else:
|
||||
print("获取K线数据失败")
|
||||
|
||||
|
||||
@@ -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
|
||||
Binary file not shown.
+325
-105
@@ -8,10 +8,46 @@ import requests
|
||||
import json
|
||||
import pywencai
|
||||
from data_source_manager import data_source_manager
|
||||
from http_timeout import call_with_timeout
|
||||
|
||||
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):
|
||||
self.data = None
|
||||
self.info = None
|
||||
@@ -89,57 +125,70 @@ class StockDataFetcher:
|
||||
if basic_info:
|
||||
info.update(basic_info)
|
||||
|
||||
# 方法1: 尝试获取个股详细信息(akshare)
|
||||
try:
|
||||
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}")
|
||||
# 如果akshare失败,尝试从tushare获取
|
||||
if self.data_source_manager.tushare_available and info['name'] == '未知':
|
||||
print(f"[Tushare] 尝试获取基本信息(tushare)...")
|
||||
# 方法1: 获取详细估值信息(优先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'):
|
||||
# 优先使用tushare daily_basic(一次获取PE/PB/市值)
|
||||
if self.data_source_manager.tushare_available:
|
||||
try:
|
||||
print(f"[Tushare] 正在获取 {symbol} 的估值信息(主要数据源)...")
|
||||
ts_code = self.data_source_manager._convert_to_ts_code(symbol)
|
||||
df = self.data_source_manager.tushare_api.daily_basic(
|
||||
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:
|
||||
row = df.iloc[0]
|
||||
info['pe_ratio'] = row.get('pe', 'N/A')
|
||||
info['pb_ratio'] = row.get('pb', 'N/A')
|
||||
info['market_cap'] = row.get('total_mv', 'N/A')
|
||||
print(f"[Tushare] ✅ 成功获取部分信息")
|
||||
except Exception as te:
|
||||
print(f"[Tushare] ❌ 获取失败: {te}")
|
||||
if info.get('pe_ratio') == 'N/A' and 'pe' in df.columns:
|
||||
info['pe_ratio'] = row.get('pe', 'N/A')
|
||||
if info.get('pb_ratio') == 'N/A' and 'pb' in df.columns:
|
||||
info['pb_ratio'] = row.get('pb', 'N/A')
|
||||
if info.get('market_cap') == 'N/A' and 'total_mv' in df.columns:
|
||||
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: 尝试获取历史价格和涨跌幅(如果网络允许)
|
||||
# try:
|
||||
@@ -204,7 +253,10 @@ class StockDataFetcher:
|
||||
# 方法3: 使用百度估值数据获取市盈率和市净率
|
||||
if info['pe_ratio'] == 'N/A':
|
||||
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:
|
||||
latest_pe = pe_data.iloc[-1]['value']
|
||||
if latest_pe and latest_pe != '-':
|
||||
@@ -216,7 +268,10 @@ class StockDataFetcher:
|
||||
|
||||
if info['pb_ratio'] == 'N/A':
|
||||
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:
|
||||
latest_pb = pb_data.iloc[-1]['value']
|
||||
if latest_pb and latest_pb != '-':
|
||||
@@ -262,6 +317,32 @@ class StockDataFetcher:
|
||||
"exchange": "香港交易所"
|
||||
}
|
||||
|
||||
# 优先使用tushare(hk_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: 获取港股实时行情
|
||||
try:
|
||||
# 使用akshare获取港股实时数据
|
||||
@@ -331,12 +412,7 @@ class StockDataFetcher:
|
||||
|
||||
def _get_us_stock_info(self, symbol):
|
||||
"""获取美股基本信息"""
|
||||
import time
|
||||
|
||||
try:
|
||||
# 添加延迟避免频率限制
|
||||
time.sleep(1)
|
||||
|
||||
ticker = yf.Ticker(symbol)
|
||||
|
||||
# 先尝试获取历史数据(通常更稳定)
|
||||
@@ -487,7 +563,36 @@ class StockDataFetcher:
|
||||
else:
|
||||
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
|
||||
|
||||
# 获取港股历史数据
|
||||
# 优先使用tushare(hk_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",
|
||||
start_date=start_date, end_date=end_date, adjust="qfq")
|
||||
|
||||
@@ -600,6 +705,112 @@ class StockDataFetcher:
|
||||
except Exception as 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):
|
||||
"""获取中国股票财务数据"""
|
||||
financial_data = {
|
||||
@@ -612,68 +823,77 @@ class StockDataFetcher:
|
||||
}
|
||||
|
||||
try:
|
||||
# 1. 获取资产负债表
|
||||
try:
|
||||
balance_sheet = ak.stock_financial_abstract_ths(symbol=symbol, indicator="资产负债表")
|
||||
if balance_sheet is not None and not balance_sheet.empty:
|
||||
financial_data["balance_sheet"] = balance_sheet.head(8).to_dict('records')
|
||||
except Exception as e:
|
||||
print(f"获取资产负债表失败: {e}")
|
||||
# 0. 优先使用tushare获取财务三表与财务指标
|
||||
ts_financial = self._get_financial_data_from_tushare(symbol)
|
||||
if ts_financial:
|
||||
financial_data.update(ts_financial)
|
||||
|
||||
# 2. 获取利润表
|
||||
try:
|
||||
income_statement = ak.stock_financial_abstract_ths(symbol=symbol, indicator="利润表")
|
||||
if income_statement is not None and not income_statement.empty:
|
||||
financial_data["income_statement"] = income_statement.head(8).to_dict('records')
|
||||
except Exception as e:
|
||||
print(f"获取利润表失败: {e}")
|
||||
# 1. 获取资产负债表(tushare未获取到时回退akshare)
|
||||
if financial_data["balance_sheet"] is None:
|
||||
try:
|
||||
balance_sheet = ak.stock_financial_abstract_ths(symbol=symbol, indicator="资产负债表")
|
||||
if balance_sheet is not None and not balance_sheet.empty:
|
||||
financial_data["balance_sheet"] = balance_sheet.head(8).to_dict('records')
|
||||
except Exception as e:
|
||||
print(f"获取资产负债表失败: {e}")
|
||||
|
||||
# 3. 获取现金流量表
|
||||
try:
|
||||
cash_flow = ak.stock_financial_abstract_ths(symbol=symbol, indicator="现金流量表")
|
||||
if cash_flow is not None and not cash_flow.empty:
|
||||
financial_data["cash_flow"] = cash_flow.head(8).to_dict('records')
|
||||
except Exception as e:
|
||||
print(f"获取现金流量表失败: {e}")
|
||||
# 2. 获取利润表(tushare未获取到时回退akshare)
|
||||
if financial_data["income_statement"] is None:
|
||||
try:
|
||||
income_statement = ak.stock_financial_abstract_ths(symbol=symbol, indicator="利润表")
|
||||
if income_statement is not None and not income_statement.empty:
|
||||
financial_data["income_statement"] = income_statement.head(8).to_dict('records')
|
||||
except Exception as e:
|
||||
print(f"获取利润表失败: {e}")
|
||||
|
||||
# 4. 获取主要财务指标
|
||||
try:
|
||||
financial_abstract = ak.stock_financial_abstract(symbol=symbol)
|
||||
if financial_abstract is not None and not financial_abstract.empty:
|
||||
# 提取关键财务指标
|
||||
key_indicators = [
|
||||
'净资产收益率(ROE)', '总资产报酬率(ROA)', '销售毛利率', '销售净利率',
|
||||
'资产负债率', '流动比率', '速动比率', '存货周转率', '应收账款周转率',
|
||||
'总资产周转率', '营业收入同比增长', '净利润同比增长'
|
||||
]
|
||||
# 3. 获取现金流量表(tushare未获取到时回退akshare)
|
||||
if financial_data["cash_flow"] is None:
|
||||
try:
|
||||
cash_flow = ak.stock_financial_abstract_ths(symbol=symbol, indicator="现金流量表")
|
||||
if cash_flow is not None and not cash_flow.empty:
|
||||
financial_data["cash_flow"] = cash_flow.head(8).to_dict('records')
|
||||
except Exception as e:
|
||||
print(f"获取现金流量表失败: {e}")
|
||||
|
||||
# 筛选出包含关键指标的行
|
||||
indicator_rows = financial_abstract[financial_abstract['指标'].isin(key_indicators)]
|
||||
# 4. 获取主要财务指标(tushare未获取到时回退akshare)
|
||||
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:
|
||||
# 获取最新的报告期数据(第一列日期)
|
||||
date_columns = [col for col in financial_abstract.columns if col not in ['选项', '指标']]
|
||||
if date_columns:
|
||||
latest_date = date_columns[0] # 最新日期列
|
||||
# 筛选出包含关键指标的行
|
||||
indicator_rows = financial_abstract[financial_abstract['指标'].isin(key_indicators)]
|
||||
|
||||
# 构建财务比率字典
|
||||
financial_ratios = {"报告期": latest_date}
|
||||
if not indicator_rows.empty:
|
||||
# 获取最新的报告期数据(第一列日期)
|
||||
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():
|
||||
indicator_name = row['指标']
|
||||
value = row.get(latest_date, 'N/A')
|
||||
if value is not None and not (isinstance(value, float) and pd.isna(value)):
|
||||
try:
|
||||
financial_ratios[indicator_name] = str(value)
|
||||
except:
|
||||
# 构建财务比率字典
|
||||
financial_ratios = {"报告期": latest_date}
|
||||
|
||||
# 提取每个指标的最新值
|
||||
for _, row in indicator_rows.iterrows():
|
||||
indicator_name = row['指标']
|
||||
value = row.get(latest_date, 'N/A')
|
||||
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"
|
||||
else:
|
||||
financial_ratios[indicator_name] = "N/A"
|
||||
|
||||
financial_data["financial_ratios"] = financial_ratios
|
||||
except Exception as e:
|
||||
print(f"获取财务指标失败: {e}")
|
||||
financial_data["financial_ratios"] = financial_ratios
|
||||
except Exception as e:
|
||||
print(f"获取财务指标失败: {e}")
|
||||
|
||||
# 注意:季报数据现在由 quarterly_report_data.py 模块使用 akshare 获取(8期完整季报)
|
||||
# 不再使用问财获取季报,避免重复
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# 获取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("TDX API配置测试")
|
||||
|
||||
@@ -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)} 只股票")
|
||||
@@ -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
|
||||
|
||||
# 条件2:RSI超买
|
||||
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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user