初始提交
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# DeepSeek API配置
|
||||
DEEPSEEK_API_KEY=sk-your_actual_api_key_here
|
||||
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
|
||||
|
||||
# Tushare配置(可选)
|
||||
TUSHARE_TOKEN=your_tushare_token_here
|
||||
|
||||
# 使用方法:
|
||||
# 1. 将此文件复制为 .env
|
||||
# 2. 在 .env 文件中填写真实的API密钥
|
||||
# 3. 确保 .env 文件不被提交到版本控制系统
|
||||
# 邮件通知配置
|
||||
EMAIL_ENABLED=true
|
||||
SMTP_SERVER=smtp.qq.com # 或 smtp.163.com, smtp.gmail.com 等
|
||||
SMTP_PORT=587
|
||||
EMAIL_FROM=your_email@qq.com
|
||||
EMAIL_PASSWORD=your_app_password # 注意:不是邮箱登录密码,是应用专用密码
|
||||
EMAIL_TO=receiver@example.com
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/agentsstock1.rar
|
||||
/agentsstock2.rar
|
||||
/test_akshare.py
|
||||
/test_fixed.py
|
||||
/test_pdf_export.py
|
||||
/test_pe_pb.py
|
||||
/venv
|
||||
/__pycache__
|
||||
/stock_analysis.db
|
||||
/stock_monitor.db
|
||||
# 环境变量文件
|
||||
.env
|
||||
@@ -0,0 +1,105 @@
|
||||
# 🐛 Bug修复记录
|
||||
|
||||
## 2025-10-03 - 修复OpenAI库版本兼容性问题
|
||||
|
||||
### 问题描述
|
||||
部署到服务器时出现错误:
|
||||
```
|
||||
Client.init() got an unexpected keyword argument 'proxies'
|
||||
```
|
||||
|
||||
### 问题原因
|
||||
- `openai==1.3.0`版本过旧
|
||||
- 新版本OpenAI库API有变化,不再支持`proxies`参数
|
||||
- 需要升级到1.12.0或更高版本
|
||||
|
||||
### 解决方案
|
||||
更新`requirements.txt`中的依赖版本:
|
||||
|
||||
```txt
|
||||
# 修复前
|
||||
openai==1.3.0
|
||||
|
||||
# 修复后
|
||||
openai>=1.12.0
|
||||
```
|
||||
|
||||
**升级命令**:
|
||||
```bash
|
||||
pip install --upgrade openai
|
||||
# 或
|
||||
pip install -r requirements.txt --upgrade
|
||||
```
|
||||
|
||||
### 修改文件
|
||||
- `requirements.txt` - 更新所有依赖包版本号
|
||||
|
||||
### 测试验证
|
||||
- ✅ 本地环境升级成功
|
||||
- ✅ API调用正常
|
||||
- ✅ 服务器部署成功
|
||||
|
||||
### 影响范围
|
||||
此修复解决了部署兼容性问题,不影响功能。同时更新了其他依赖包版本,提升系统稳定性。
|
||||
|
||||
---
|
||||
|
||||
## 2025-10-03 - 修复 `fetcher` 未定义错误
|
||||
|
||||
### 问题描述
|
||||
在运行股票分析时出现错误:
|
||||
```
|
||||
name 'fetcher' is not defined
|
||||
```
|
||||
|
||||
### 问题原因
|
||||
在 `app.py` 的 `run_stock_analysis()` 函数中,尝试使用 `fetcher.get_financial_data(symbol)`,但 `fetcher` 变量只在 `get_stock_data()` 函数内部定义,不在 `run_stock_analysis()` 的作用域内。
|
||||
|
||||
### 解决方案
|
||||
在 `run_stock_analysis()` 函数中,在调用 `get_financial_data()` 之前创建 `StockDataFetcher` 实例:
|
||||
|
||||
```python
|
||||
# 修复前
|
||||
financial_data = fetcher.get_financial_data(symbol) # ❌ fetcher未定义
|
||||
|
||||
# 修复后
|
||||
fetcher = StockDataFetcher() # ✅ 先创建实例
|
||||
financial_data = fetcher.get_financial_data(symbol)
|
||||
```
|
||||
|
||||
### 修改文件
|
||||
- `app.py` - 第295行
|
||||
|
||||
### 测试验证
|
||||
- ✅ StockDataFetcher实例化成功
|
||||
- ✅ 股票信息获取正常
|
||||
- ✅ 财务数据获取正常
|
||||
- ✅ 分析流程完整运行
|
||||
|
||||
### 影响范围
|
||||
此修复解决了新增财务数据获取功能后的集成问题,不影响其他功能。
|
||||
|
||||
---
|
||||
|
||||
## 常见错误排查
|
||||
|
||||
### 1. `name 'xxx' is not defined`
|
||||
**原因**:变量未定义或作用域问题
|
||||
**解决**:检查变量定义位置和作用域
|
||||
|
||||
### 2. `module 'xxx' has no attribute 'yyy'`
|
||||
**原因**:API变更或模块版本不匹配
|
||||
**解决**:更新API调用或使用备用方案
|
||||
|
||||
### 3. 网络连接错误
|
||||
**原因**:防火墙、代理或网络不稳定
|
||||
**解决**:检查网络设置,使用重试机制
|
||||
|
||||
### 4. 数据获取失败
|
||||
**原因**:数据源限制或股票代码错误
|
||||
**解决**:验证股票代码格式,使用缓存机制
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2025-10-03
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
# 更新日志
|
||||
|
||||
## 2025-10-03
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
#### 增强基本面分析
|
||||
- 📊 **详细财务数据获取**
|
||||
- 资产负债表:完整的资产和负债结构
|
||||
- 利润表:营收、成本、利润明细
|
||||
- 现金流量表:经营、投资、筹资现金流
|
||||
|
||||
- 💰 **全面财务指标**
|
||||
- 盈利能力:ROE、ROA、毛利率、净利率
|
||||
- 偿债能力:资产负债率、流动比率、速动比率
|
||||
- 运营能力:存货周转率、应收账款周转率、总资产周转率
|
||||
- 成长能力:营收增长率、利润增长率
|
||||
- 每股指标:EPS、每股账面价值、股息率、派息率
|
||||
|
||||
- 🎯 **深度分析维度**
|
||||
- 公司质地分析(业务模式、竞争力、护城河)
|
||||
- 盈利能力深度分析
|
||||
- 财务健康度评估
|
||||
- 成长性分析
|
||||
- 估值分析(历史对比、行业对比)
|
||||
- 投资价值综合判断
|
||||
|
||||
- 🔄 **数据源优化**
|
||||
- A股:支持同花顺财务数据API
|
||||
- 美股:yfinance完整财务报表
|
||||
- 自动处理API变化和异常
|
||||
|
||||
#### 实时监测管理系统
|
||||
- 📊 **监测股票管理**:添加、编辑、删除监测股票
|
||||
- 🎯 **关键位置设置**:进场区间、止盈位、止损位
|
||||
- ⏰ **自定义监测间隔**:30秒至300秒可选
|
||||
- 🎨 **卡片式布局**:现代化的股票监测卡片展示
|
||||
- 🔍 **搜索过滤功能**:快速查找监测股票
|
||||
|
||||
#### 通知系统
|
||||
- 📧 **邮件通知**:价格触发关键位置时发送邮件
|
||||
- 🌐 **网页通知**:实时界面消息提醒
|
||||
- 🔔 **通知开关**:每只股票可独立控制通知
|
||||
- 📱 **通知历史**:查看所有通知记录
|
||||
|
||||
#### PDF报告导出
|
||||
- 📄 **完整分析报告**:一键生成PDF格式报告
|
||||
- 🎨 **中文字体支持**:正确显示中文内容
|
||||
- 📊 **包含所有分析**:技术、基本、资金、风险、情绪分析
|
||||
|
||||
### 🔧 功能改进
|
||||
|
||||
#### 邮件配置
|
||||
- ✅ 从`.env`文件读取邮件配置
|
||||
- ✅ 支持TLS(587端口)和SSL(465端口)
|
||||
- ✅ 测试邮件发送功能
|
||||
- ✅ 邮件配置状态显示
|
||||
- ✅ 详细的配置说明
|
||||
|
||||
#### 监测服务
|
||||
- ✅ 后台线程自动监测
|
||||
- ✅ 启动/停止监测服务
|
||||
- ✅ 手动更新股票价格
|
||||
- ✅ 实时价格显示
|
||||
|
||||
#### 数据库管理
|
||||
- ✅ 监测股票数据持久化
|
||||
- ✅ 价格历史记录
|
||||
- ✅ 通知历史管理
|
||||
- ✅ 标记已读/清空通知
|
||||
|
||||
### 🐛 问题修复
|
||||
- 修复删除股票功能返回值问题
|
||||
- 修复编辑对话框表单提交逻辑
|
||||
- 修复PDF中文乱码问题
|
||||
- 修复报告重复显示问题
|
||||
- 修复按钮key重复问题
|
||||
|
||||
### 📝 配置文件
|
||||
|
||||
#### .env 配置示例
|
||||
```env
|
||||
# DeepSeek API(必需)
|
||||
DEEPSEEK_API_KEY=your_api_key_here
|
||||
|
||||
# 邮件通知(可选)
|
||||
EMAIL_ENABLED=true
|
||||
SMTP_SERVER=smtp.qq.com
|
||||
SMTP_PORT=587
|
||||
EMAIL_FROM=your_email@qq.com
|
||||
EMAIL_PASSWORD=your_authorization_code
|
||||
EMAIL_TO=receiver@example.com
|
||||
```
|
||||
|
||||
### 📚 文档更新
|
||||
- 更新 README.md 添加监测功能说明
|
||||
- 更新系统架构图
|
||||
- 添加邮件配置说明
|
||||
|
||||
### 🎯 下一步计划
|
||||
- [ ] 添加价格预警阈值(百分比)
|
||||
- [ ] 支持多个接收邮箱
|
||||
- [ ] 添加监测统计图表
|
||||
- [ ] 导出监测历史数据
|
||||
- [ ] 微信/钉钉通知集成
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# DeepSeek Reasoner 模型输出不全问题修复说明
|
||||
|
||||
## 问题描述
|
||||
|
||||
使用 `deepseek-chat` 模型输出正常,但使用 `deepseek-reasoner` 模型进行技术面分析、基本面分析等各个分析时,输出文字不全。
|
||||
|
||||
## 问题原因
|
||||
|
||||
**deepseek-reasoner** 模型与 **deepseek-chat** 模型有重要区别:
|
||||
|
||||
1. **响应结构不同**:
|
||||
- `deepseek-chat`: 只返回 `content`(最终答案)
|
||||
- `deepseek-reasoner`: 返回 `reasoning_content`(推理过程)+ `content`(最终答案)
|
||||
|
||||
2. **Token 需求更大**:
|
||||
- reasoner 模型需要输出详细的推理过程,需要更多的 tokens
|
||||
- 原代码中 `max_tokens=2000` 对 reasoner 模型来说太小,导致输出被截断
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 自动调整 max_tokens(已修复)
|
||||
|
||||
在 `deepseek_client.py` 的 `call_api` 方法中:
|
||||
|
||||
```python
|
||||
# 对于 reasoner 模型,自动增加 max_tokens
|
||||
if "reasoner" in model_to_use.lower() and max_tokens <= 2000:
|
||||
max_tokens = 8000 # reasoner 模型需要更多 tokens 来输出推理过程
|
||||
```
|
||||
|
||||
### 2. 正确处理 reasoner 响应(已修复)
|
||||
|
||||
```python
|
||||
# 处理 reasoner 模型的响应
|
||||
message = response.choices[0].message
|
||||
result = ""
|
||||
|
||||
# 检查是否有推理内容
|
||||
if hasattr(message, 'reasoning_content') and message.reasoning_content:
|
||||
result += f"【推理过程】\n{message.reasoning_content}\n\n"
|
||||
|
||||
# 添加最终内容
|
||||
if message.content:
|
||||
result += message.content
|
||||
```
|
||||
|
||||
### 3. 提高各分析模块的 max_tokens(已修复)
|
||||
|
||||
| 分析模块 | 原 max_tokens | 新 max_tokens |
|
||||
|---------|--------------|--------------|
|
||||
| 技术面分析 | 2000 | 8000 (自动) |
|
||||
| 基本面分析 | 2000 | 8000 (自动) |
|
||||
| 资金面分析 | 2000 | 8000 (自动) |
|
||||
| 风险管理分析 | 2000 | 8000 (自动) |
|
||||
| 市场情绪分析 | 2000 | 8000 (自动) |
|
||||
| 综合讨论 | 3000 | 6000 |
|
||||
| 最终决策 | 2000 | 4000 |
|
||||
| 团队讨论 | 3000 | 6000 |
|
||||
|
||||
## 修改的文件
|
||||
|
||||
1. **deepseek_client.py**
|
||||
- `call_api()` 方法:增加 reasoner 检测和 max_tokens 自动调整
|
||||
- `call_api()` 方法:增加 reasoning_content 处理
|
||||
- `comprehensive_discussion()` 方法:max_tokens 从 3000 增加到 6000
|
||||
- `final_decision()` 方法:max_tokens 从 2000 增加到 4000
|
||||
|
||||
2. **ai_agents.py**
|
||||
- `conduct_team_discussion()` 方法:max_tokens 从 3000 增加到 6000
|
||||
|
||||
## 使用建议
|
||||
|
||||
### 1. 模型选择
|
||||
|
||||
- **deepseek-chat**:
|
||||
- 适用场景:快速分析、常规查询
|
||||
- 优点:响应快,成本低
|
||||
- 缺点:推理能力相对较弱
|
||||
|
||||
- **deepseek-reasoner**:
|
||||
- 适用场景:复杂分析、需要深度推理的场景
|
||||
- 优点:推理能力强,分析更深入
|
||||
- 缺点:响应时间较长,token 消耗更多
|
||||
|
||||
### 2. 查看推理过程
|
||||
|
||||
使用 reasoner 模型时,响应中会包含 `【推理过程】` 部分,展示 AI 的思考过程,帮助你理解分析的逻辑。
|
||||
|
||||
### 3. Token 消耗
|
||||
|
||||
reasoner 模型的 token 消耗通常是 chat 模型的 2-4 倍,请注意 API 使用成本。
|
||||
|
||||
## 验证方法
|
||||
|
||||
1. 在应用中选择 "DeepSeek Reasoner (推理增强)" 模型
|
||||
2. 输入股票代码进行分析
|
||||
3. 检查各个分析报告是否完整输出
|
||||
4. 查看是否包含 `【推理过程】` 部分
|
||||
|
||||
## 预期效果
|
||||
|
||||
修复后,使用 deepseek-reasoner 模型应该能够:
|
||||
- ✅ 完整输出技术面分析报告
|
||||
- ✅ 完整输出基本面分析报告
|
||||
- ✅ 完整输出资金面分析报告
|
||||
- ✅ 完整输出风险管理报告
|
||||
- ✅ 完整输出市场情绪分析报告
|
||||
- ✅ 完整输出综合讨论结果
|
||||
- ✅ 完整输出最终投资决策
|
||||
- ✅ 显示推理过程(如果 API 支持)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **API 兼容性**:确保使用的 DeepSeek API 版本支持 reasoner 模型
|
||||
2. **Token 限制**:如果仍然出现截断,可以进一步增加 max_tokens
|
||||
3. **成本控制**:reasoner 模型消耗更多 tokens,注意 API 配额
|
||||
4. **响应时间**:reasoner 模型需要更多时间进行推理,请耐心等待
|
||||
|
||||
## 更新日期
|
||||
|
||||
2025-10-03
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
# 🚀 部署指南
|
||||
|
||||
## 问题解决
|
||||
|
||||
### OpenAI库版本兼容性问题
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
Client.init() got an unexpected keyword argument 'proxies'
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- OpenAI库版本过旧(1.3.0)
|
||||
- 新版本API有变化,不再支持`proxies`参数
|
||||
|
||||
**解决方案**:
|
||||
|
||||
#### 方案1:升级依赖包(推荐)
|
||||
|
||||
```bash
|
||||
# 1. 激活虚拟环境
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# 或
|
||||
.\venv\Scripts\Activate.ps1 # Windows
|
||||
|
||||
# 2. 升级openai库
|
||||
pip install --upgrade openai
|
||||
|
||||
# 3. 或者重新安装所有依赖
|
||||
pip install -r requirements.txt --upgrade
|
||||
```
|
||||
|
||||
#### 方案2:指定版本安装
|
||||
|
||||
```bash
|
||||
pip install openai>=1.12.0
|
||||
```
|
||||
|
||||
#### 方案3:清理后重新安装
|
||||
|
||||
```bash
|
||||
# 1. 卸载旧版本
|
||||
pip uninstall openai -y
|
||||
|
||||
# 2. 安装新版本
|
||||
pip install openai
|
||||
|
||||
# 3. 重新安装所有依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 📋 完整部署流程
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone <your-repo-url>
|
||||
cd agentsstock1
|
||||
|
||||
# 创建虚拟环境
|
||||
python -m venv venv
|
||||
|
||||
# 激活虚拟环境
|
||||
# Windows:
|
||||
.\venv\Scripts\Activate.ps1
|
||||
# Linux/Mac:
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装所有依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 如果遇到问题,逐个安装
|
||||
pip install streamlit requests pandas numpy plotly
|
||||
pip install yfinance akshare openai python-dotenv
|
||||
pip install pytz ta reportlab peewee schedule
|
||||
```
|
||||
|
||||
### 3. 配置环境变量
|
||||
|
||||
```bash
|
||||
# 复制配置文件
|
||||
cp .env.example .env
|
||||
|
||||
# 编辑.env文件
|
||||
# Windows:
|
||||
notepad .env
|
||||
# Linux/Mac:
|
||||
nano .env
|
||||
```
|
||||
|
||||
在`.env`文件中配置:
|
||||
```env
|
||||
# DeepSeek API配置(必需)
|
||||
DEEPSEEK_API_KEY=your_actual_api_key_here
|
||||
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
|
||||
|
||||
# 邮件通知配置(可选)
|
||||
EMAIL_ENABLED=false
|
||||
SMTP_SERVER=smtp.qq.com
|
||||
SMTP_PORT=587
|
||||
EMAIL_FROM=your_email@qq.com
|
||||
EMAIL_PASSWORD=your_authorization_code
|
||||
EMAIL_TO=receiver@example.com
|
||||
```
|
||||
|
||||
### 4. 测试运行
|
||||
|
||||
```bash
|
||||
# 本地测试
|
||||
streamlit run app.py
|
||||
|
||||
# 指定端口
|
||||
streamlit run app.py --server.port 8501
|
||||
```
|
||||
|
||||
### 5. 服务器部署
|
||||
|
||||
#### 使用Streamlit Cloud
|
||||
|
||||
1. 推送代码到GitHub
|
||||
2. 登录 https://streamlit.io/cloud
|
||||
3. 连接GitHub仓库
|
||||
4. 在Settings中配置环境变量
|
||||
5. 部署
|
||||
|
||||
#### 使用Docker
|
||||
|
||||
创建`Dockerfile`:
|
||||
```dockerfile
|
||||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8501
|
||||
|
||||
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
||||
```
|
||||
|
||||
构建和运行:
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t stock-analysis .
|
||||
|
||||
# 运行容器
|
||||
docker run -p 8501:8501 --env-file .env stock-analysis
|
||||
```
|
||||
|
||||
#### 使用PM2(适用于VPS)
|
||||
|
||||
```bash
|
||||
# 安装PM2
|
||||
npm install -g pm2
|
||||
|
||||
# 创建启动脚本 start.sh
|
||||
echo "streamlit run app.py --server.port 8501" > start.sh
|
||||
chmod +x start.sh
|
||||
|
||||
# 使用PM2启动
|
||||
pm2 start start.sh --name stock-analysis
|
||||
|
||||
# 保存PM2配置
|
||||
pm2 save
|
||||
pm2 startup
|
||||
```
|
||||
|
||||
## 🔧 常见部署问题
|
||||
|
||||
### 1. 端口被占用
|
||||
|
||||
**错误**:`Address already in use`
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 更换端口
|
||||
streamlit run app.py --server.port 8502
|
||||
```
|
||||
|
||||
### 2. 依赖安装失败
|
||||
|
||||
**错误**:`No matching distribution found`
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 升级pip
|
||||
pip install --upgrade pip
|
||||
|
||||
# 使用国内镜像源
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
```
|
||||
|
||||
### 3. API连接失败
|
||||
|
||||
**错误**:`Connection timeout`
|
||||
|
||||
**解决**:
|
||||
- 检查网络连接
|
||||
- 验证API Key是否正确
|
||||
- 检查BASE_URL是否正确
|
||||
- 检查防火墙设置
|
||||
|
||||
### 4. 中文字体问题(PDF生成)
|
||||
|
||||
**错误**:`Font not found`
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# Linux服务器安装中文字体
|
||||
sudo apt-get install fonts-wqy-zenhei fonts-wqy-microhei
|
||||
|
||||
# 或手动下载字体
|
||||
wget https://github.com/adobe-fonts/source-han-sans/releases/download/2.004R/SourceHanSansCN.zip
|
||||
unzip SourceHanSansCN.zip -d /usr/share/fonts/
|
||||
fc-cache -fv
|
||||
```
|
||||
|
||||
### 5. 数据库权限问题
|
||||
|
||||
**错误**:`Permission denied: stock_monitor.db`
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 设置正确的权限
|
||||
chmod 666 stock_monitor.db stock_analysis.db
|
||||
chmod 777 . # 确保当前目录可写
|
||||
```
|
||||
|
||||
### 6. 内存不足
|
||||
|
||||
**错误**:`MemoryError`
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 增加系统交换空间
|
||||
sudo fallocate -l 2G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
|
||||
# 或限制Streamlit内存使用
|
||||
streamlit run app.py --server.maxUploadSize 100
|
||||
```
|
||||
|
||||
## 🔐 安全建议
|
||||
|
||||
### 1. 保护敏感信息
|
||||
|
||||
```bash
|
||||
# 确保.env文件在.gitignore中
|
||||
echo ".env" >> .gitignore
|
||||
|
||||
# 不要在代码中硬编码密钥
|
||||
# ❌ BAD
|
||||
API_KEY = "sk-xxxxx"
|
||||
|
||||
# ✅ GOOD
|
||||
API_KEY = os.getenv("DEEPSEEK_API_KEY")
|
||||
```
|
||||
|
||||
### 2. 配置防火墙
|
||||
|
||||
```bash
|
||||
# 只允许特定IP访问
|
||||
# UFW (Ubuntu)
|
||||
sudo ufw allow from YOUR_IP to any port 8501
|
||||
|
||||
# iptables
|
||||
sudo iptables -A INPUT -p tcp --dport 8501 -s YOUR_IP -j ACCEPT
|
||||
```
|
||||
|
||||
### 3. 使用HTTPS
|
||||
|
||||
```bash
|
||||
# 使用nginx反向代理
|
||||
sudo apt-get install nginx
|
||||
|
||||
# 配置nginx
|
||||
sudo nano /etc/nginx/sites-available/stock-analysis
|
||||
```
|
||||
|
||||
nginx配置:
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8501;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 定期更新
|
||||
|
||||
```bash
|
||||
# 更新依赖包
|
||||
pip list --outdated
|
||||
pip install --upgrade package_name
|
||||
|
||||
# 更新系统
|
||||
sudo apt-get update && sudo apt-get upgrade
|
||||
```
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### 1. 启用缓存
|
||||
|
||||
代码中已使用`@st.cache_data`装饰器
|
||||
|
||||
### 2. 配置Streamlit
|
||||
|
||||
创建`.streamlit/config.toml`:
|
||||
```toml
|
||||
[server]
|
||||
port = 8501
|
||||
enableCORS = false
|
||||
enableXsrfProtection = true
|
||||
|
||||
[browser]
|
||||
gatherUsageStats = false
|
||||
|
||||
[client]
|
||||
showErrorDetails = false
|
||||
```
|
||||
|
||||
### 3. 使用CDN
|
||||
|
||||
对于静态资源使用CDN加速
|
||||
|
||||
## 🔄 更新部署
|
||||
|
||||
```bash
|
||||
# 拉取最新代码
|
||||
git pull origin main
|
||||
|
||||
# 更新依赖
|
||||
pip install -r requirements.txt --upgrade
|
||||
|
||||
# 重启服务
|
||||
pm2 restart stock-analysis
|
||||
# 或
|
||||
docker restart stock-analysis
|
||||
```
|
||||
|
||||
## 📝 监控和日志
|
||||
|
||||
### 查看日志
|
||||
|
||||
```bash
|
||||
# PM2日志
|
||||
pm2 logs stock-analysis
|
||||
|
||||
# Docker日志
|
||||
docker logs stock-analysis
|
||||
|
||||
# Streamlit日志
|
||||
tail -f ~/.streamlit/logs/*
|
||||
```
|
||||
|
||||
### 监控性能
|
||||
|
||||
```bash
|
||||
# 系统资源
|
||||
htop
|
||||
|
||||
# PM2监控
|
||||
pm2 monit
|
||||
|
||||
# Docker stats
|
||||
docker stats stock-analysis
|
||||
```
|
||||
|
||||
## 🆘 获取帮助
|
||||
|
||||
如果遇到其他问题:
|
||||
1. 查看BUGFIX.md
|
||||
2. 查看GitHub Issues
|
||||
3. 检查系统日志
|
||||
4. 联系技术支持
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2025-10-03
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
# 📊 基本面分析功能指南
|
||||
|
||||
## 功能概述
|
||||
|
||||
本系统的基本面分析功能已经全面升级,能够获取并分析详细的财务数据,为投资决策提供更深入的支持。
|
||||
|
||||
## 📈 数据获取范围
|
||||
|
||||
### 三大财务报表
|
||||
|
||||
#### 1. 资产负债表
|
||||
- 总资产、总负债、股东权益
|
||||
- 流动资产、非流动资产
|
||||
- 流动负债、非流动负债
|
||||
- 资产负债结构分析
|
||||
|
||||
#### 2. 利润表
|
||||
- 营业收入、营业成本
|
||||
- 毛利润、营业利润、净利润
|
||||
- 各项费用明细
|
||||
- 盈利能力趋势
|
||||
|
||||
#### 3. 现金流量表
|
||||
- 经营活动现金流
|
||||
- 投资活动现金流
|
||||
- 筹资活动现金流
|
||||
- 现金流健康度分析
|
||||
|
||||
### 财务指标体系
|
||||
|
||||
#### 盈利能力指标
|
||||
| 指标 | 说明 | 优秀标准 |
|
||||
|------|------|----------|
|
||||
| ROE(净资产收益率) | 衡量股东投资回报率 | >15% |
|
||||
| ROA(总资产收益率) | 衡量资产使用效率 | >5% |
|
||||
| 销售毛利率 | 产品附加值和定价能力 | 因行业而异 |
|
||||
| 销售净利率 | 综合盈利能力 | >10% |
|
||||
|
||||
#### 偿债能力指标
|
||||
| 指标 | 说明 | 安全标准 |
|
||||
|------|------|----------|
|
||||
| 资产负债率 | 负债占总资产比例 | <60% |
|
||||
| 流动比率 | 短期偿债能力 | >1.5 |
|
||||
| 速动比率 | 更严格的短期偿债能力 | >1.0 |
|
||||
|
||||
#### 运营能力指标
|
||||
| 指标 | 说明 | 意义 |
|
||||
|------|------|------|
|
||||
| 存货周转率 | 存货管理效率 | 越高越好 |
|
||||
| 应收账款周转率 | 账款回收能力 | 越高越好 |
|
||||
| 总资产周转率 | 资产使用效率 | 越高越好 |
|
||||
|
||||
#### 成长能力指标
|
||||
| 指标 | 说明 | 优秀标准 |
|
||||
|------|------|----------|
|
||||
| 营业收入同比增长 | 收入增长速度 | >20% |
|
||||
| 净利润同比增长 | 利润增长速度 | >20% |
|
||||
|
||||
#### 每股指标
|
||||
| 指标 | 说明 | 作用 |
|
||||
|------|------|------|
|
||||
| EPS(每股收益) | 每股盈利能力 | 估值基础 |
|
||||
| 每股账面价值 | 每股净资产 | 安全边际 |
|
||||
| 股息率 | 分红回报率 | 现金回报 |
|
||||
| 派息率 | 分红比例 | 分红政策 |
|
||||
|
||||
## 🎯 分析维度
|
||||
|
||||
### 1. 公司质地分析
|
||||
- **业务模式**:商业模式和盈利来源
|
||||
- **核心竞争力**:技术、品牌、规模优势
|
||||
- **护城河**:竞争壁垒和持续性
|
||||
- **行业地位**:市场份额和龙头地位
|
||||
|
||||
### 2. 盈利能力分析
|
||||
- ROE/ROA水平评估
|
||||
- 毛利率和净利率趋势
|
||||
- 与行业平均水平对比
|
||||
- 盈利质量和持续性判断
|
||||
|
||||
### 3. 财务健康度分析
|
||||
- 资产负债结构合理性
|
||||
- 偿债能力充足性
|
||||
- 现金流充裕程度
|
||||
- 财务风险识别
|
||||
|
||||
### 4. 成长性分析
|
||||
- 收入和利润增长趋势
|
||||
- 增长驱动因素分析
|
||||
- 未来成长空间预测
|
||||
- 行业发展前景评估
|
||||
|
||||
### 5. 估值分析
|
||||
- 当前估值水平(PE、PB)
|
||||
- 历史估值区间对比
|
||||
- 行业估值对比
|
||||
- 合理估值区间判断
|
||||
|
||||
### 6. 投资价值判断
|
||||
- 综合评分(0-100分)
|
||||
- 投资亮点总结
|
||||
- 投资风险提示
|
||||
- 适合的投资者类型
|
||||
|
||||
## 💡 使用建议
|
||||
|
||||
### 看财务数据的顺序
|
||||
|
||||
1. **先看盈利能力**
|
||||
- ROE是否稳定且>15%
|
||||
- 毛利率是否维持高位
|
||||
- 净利率是否稳定增长
|
||||
|
||||
2. **再看成长性**
|
||||
- 营收和利润是否持续增长
|
||||
- 增长是否健康可持续
|
||||
- 是否有新的增长动力
|
||||
|
||||
3. **然后看财务健康度**
|
||||
- 资产负债率是否合理
|
||||
- 流动比率是否充足
|
||||
- 现金流是否健康
|
||||
|
||||
4. **最后看估值**
|
||||
- PE/PB是否处于合理区间
|
||||
- 相对历史估值的位置
|
||||
- 相对行业估值的位置
|
||||
|
||||
### 财务数据解读技巧
|
||||
|
||||
#### 高质量公司特征
|
||||
- ✅ ROE稳定在15%以上
|
||||
- ✅ 毛利率和净利率稳定或上升
|
||||
- ✅ 收入和利润持续增长
|
||||
- ✅ 资产负债率<60%
|
||||
- ✅ 经营现金流充足
|
||||
- ✅ 应收账款周转率高
|
||||
|
||||
#### 风险信号
|
||||
- ⚠️ ROE持续下降
|
||||
- ⚠️ 毛利率大幅下滑
|
||||
- ⚠️ 收入增长但利润不增长
|
||||
- ⚠️ 资产负债率>70%
|
||||
- ⚠️ 经营现金流为负
|
||||
- ⚠️ 应收账款大量增加
|
||||
|
||||
## 📚 数据源说明
|
||||
|
||||
### A股数据
|
||||
- **来源**:AKShare(东方财富、同花顺接口)
|
||||
- **更新频率**:季度财报
|
||||
- **数据滞后**:约1-2个月
|
||||
|
||||
### 美股数据
|
||||
- **来源**:yfinance(Yahoo Finance)
|
||||
- **更新频率**:季度财报
|
||||
- **数据滞后**:约1-2个月
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **财务数据有滞后性**
|
||||
- 季报发布后1-2个月才能获取
|
||||
- 最新数据可能不是最近一个季度
|
||||
|
||||
2. **不同行业标准不同**
|
||||
- 轻资产行业(软件、服务):高ROE、高毛利率
|
||||
- 重资产行业(制造、地产):ROE相对较低
|
||||
- 周期性行业:财务指标波动大
|
||||
|
||||
3. **单一指标不能说明全部问题**
|
||||
- 需要综合多个指标判断
|
||||
- 需要看趋势而非单点数据
|
||||
- 需要与行业对比分析
|
||||
|
||||
4. **特殊情况处理**
|
||||
- 新股可能没有完整财务数据
|
||||
- ST股票财务数据异常
|
||||
- 重组并购期间数据扭曲
|
||||
|
||||
## 🚀 最佳实践
|
||||
|
||||
### 分析流程建议
|
||||
|
||||
1. **快速筛选**(5分钟)
|
||||
- 查看ROE、毛利率、净利率
|
||||
- 看收入和利润增长率
|
||||
- 检查资产负债率
|
||||
|
||||
2. **深入分析**(15分钟)
|
||||
- 分析盈利能力趋势
|
||||
- 评估财务健康度
|
||||
- 研究成长性和驱动因素
|
||||
|
||||
3. **综合判断**(10分钟)
|
||||
- 结合估值分析
|
||||
- 识别投资风险
|
||||
- 做出投资决策
|
||||
|
||||
### 配合其他分析
|
||||
|
||||
基本面分析需要与其他维度结合:
|
||||
- **技术面**:把握买卖时机
|
||||
- **资金面**:确认主力动向
|
||||
- **市场情绪**:理解短期波动
|
||||
- **风险管理**:控制投资风险
|
||||
|
||||
## 📞 常见问题
|
||||
|
||||
### Q1: 为什么有些财务数据显示N/A?
|
||||
A: 可能原因:
|
||||
- 新上市公司数据不全
|
||||
- 数据接口暂时无法获取
|
||||
- 该公司未披露该项数据
|
||||
|
||||
### Q2: 财务数据多久更新一次?
|
||||
A:
|
||||
- 上市公司季度财报发布后1-2个月
|
||||
- 系统实时获取最新可用数据
|
||||
|
||||
### Q3: 如何判断财务数据的可靠性?
|
||||
A:
|
||||
- 对比多个数据源
|
||||
- 关注会计师事务所意见
|
||||
- 留意财务报表附注
|
||||
- 关注媒体和监管公告
|
||||
|
||||
### Q4: ROE多少算优秀?
|
||||
A:
|
||||
- 一般标准:>15%为优秀
|
||||
- 看行业:高科技可以>20%,传统行业10-15%也不错
|
||||
- 看稳定性:持续5年ROE>15%更可靠
|
||||
|
||||
---
|
||||
|
||||
**提示**:财务分析是价值投资的基础,但不是全部。投资决策需要综合考虑多方面因素,理性判断,谨慎决策。
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
# ⚡ 快速修复指南
|
||||
|
||||
## 部署错误:proxies参数问题
|
||||
|
||||
### 🔴 错误信息
|
||||
```
|
||||
分析过程中出现错误: Client.init() got an unexpected keyword argument 'proxies'
|
||||
```
|
||||
|
||||
### ✅ 快速解决(3步)
|
||||
|
||||
#### 步骤1:升级OpenAI库
|
||||
```bash
|
||||
pip install --upgrade openai
|
||||
```
|
||||
|
||||
#### 步骤2:重新安装依赖
|
||||
```bash
|
||||
pip install -r requirements.txt --upgrade
|
||||
```
|
||||
|
||||
#### 步骤3:重启应用
|
||||
```bash
|
||||
streamlit run app.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 其他常见问题快速修复
|
||||
|
||||
### 1. 模块未找到错误
|
||||
|
||||
**错误**:`ModuleNotFoundError: No module named 'xxx'`
|
||||
|
||||
**修复**:
|
||||
```bash
|
||||
pip install xxx
|
||||
# 或重新安装所有依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. API密钥错误
|
||||
|
||||
**错误**:`API Key未配置`
|
||||
|
||||
**修复**:
|
||||
```bash
|
||||
# 创建.env文件
|
||||
cp .env.example .env
|
||||
|
||||
# 编辑.env文件,添加API密钥
|
||||
# DEEPSEEK_API_KEY=your_api_key_here
|
||||
```
|
||||
|
||||
### 3. 端口被占用
|
||||
|
||||
**错误**:`Address already in use`
|
||||
|
||||
**修复**:
|
||||
```bash
|
||||
# 使用其他端口
|
||||
streamlit run app.py --server.port 8502
|
||||
```
|
||||
|
||||
### 4. 数据库锁定错误
|
||||
|
||||
**错误**:`database is locked`
|
||||
|
||||
**修复**:
|
||||
```bash
|
||||
# 停止所有streamlit进程
|
||||
pkill -f streamlit
|
||||
|
||||
# 删除锁文件
|
||||
rm -f *.db-wal *.db-shm
|
||||
|
||||
# 重新启动
|
||||
streamlit run app.py
|
||||
```
|
||||
|
||||
### 5. 权限错误
|
||||
|
||||
**错误**:`Permission denied`
|
||||
|
||||
**修复**:
|
||||
```bash
|
||||
# 设置正确权限
|
||||
chmod 666 *.db
|
||||
chmod 755 .
|
||||
```
|
||||
|
||||
### 6. 中文乱码
|
||||
|
||||
**错误**:PDF中文显示为方框
|
||||
|
||||
**修复**:
|
||||
```bash
|
||||
# Windows: 确保系统安装了中文字体
|
||||
# Linux: 安装中文字体包
|
||||
sudo apt-get install fonts-wqy-zenhei
|
||||
```
|
||||
|
||||
### 7. 网络超时
|
||||
|
||||
**错误**:`Connection timeout`
|
||||
|
||||
**修复**:
|
||||
- 检查网络连接
|
||||
- 验证防火墙设置
|
||||
- 尝试使用代理
|
||||
|
||||
### 8. 内存不足
|
||||
|
||||
**错误**:`MemoryError`
|
||||
|
||||
**修复**:
|
||||
```bash
|
||||
# 限制数据加载大小
|
||||
# 或增加系统内存
|
||||
# 或使用更小的数据周期
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 完整重置流程
|
||||
|
||||
如果多个问题同时出现,可以尝试完整重置:
|
||||
|
||||
```bash
|
||||
# 1. 停止所有进程
|
||||
pkill -f streamlit
|
||||
|
||||
# 2. 清理虚拟环境
|
||||
rm -rf venv/
|
||||
|
||||
# 3. 创建新虚拟环境
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# 或
|
||||
.\venv\Scripts\Activate.ps1 # Windows
|
||||
|
||||
# 4. 升级pip
|
||||
pip install --upgrade pip
|
||||
|
||||
# 5. 重新安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 6. 配置环境变量
|
||||
cp .env.example .env
|
||||
# 编辑.env文件添加API密钥
|
||||
|
||||
# 7. 重新启动
|
||||
streamlit run app.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 需要更多帮助?
|
||||
|
||||
1. 查看 `DEPLOYMENT_GUIDE.md` - 完整部署指南
|
||||
2. 查看 `BUGFIX.md` - 详细错误记录
|
||||
3. 查看 `README.md` - 使用说明
|
||||
4. 检查GitHub Issues
|
||||
|
||||
---
|
||||
|
||||
**提示**:大多数问题都可以通过重新安装依赖或重启应用解决!
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
# 🚀 快速配置指南
|
||||
|
||||
## 1️⃣ 基础配置(必需)
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 配置API密钥
|
||||
在 `.env` 文件中添加:
|
||||
```env
|
||||
DEEPSEEK_API_KEY=your_api_key_here
|
||||
```
|
||||
|
||||
### 启动系统
|
||||
```bash
|
||||
streamlit run app.py
|
||||
```
|
||||
|
||||
访问:http://localhost:8501
|
||||
|
||||
---
|
||||
|
||||
## 2️⃣ 邮件通知配置(可选)
|
||||
|
||||
### QQ邮箱(推荐)
|
||||
|
||||
#### 第一步:获取授权码
|
||||
1. 登录 QQ 邮箱:https://mail.qq.com
|
||||
2. 设置 → 账户 → POP3/IMAP/SMTP/Exchange
|
||||
3. 开启"IMAP/SMTP服务"
|
||||
4. 生成授权码(16位)
|
||||
5. 保存授权码备用
|
||||
|
||||
#### 第二步:配置.env文件
|
||||
在 `.env` 文件中添加:
|
||||
```env
|
||||
EMAIL_ENABLED=true
|
||||
SMTP_SERVER=smtp.qq.com
|
||||
SMTP_PORT=587
|
||||
EMAIL_FROM=your_email@qq.com
|
||||
EMAIL_PASSWORD=your_16_digit_code
|
||||
EMAIL_TO=receiver@example.com
|
||||
```
|
||||
|
||||
#### 第三步:测试邮件
|
||||
1. 进入"实时监测"页面
|
||||
2. 滚动到"通知管理"区域
|
||||
3. 点击"📧 发送测试邮件"
|
||||
4. 检查收件箱(包括垃圾邮件箱)
|
||||
|
||||
### 163邮箱
|
||||
|
||||
#### 配置示例
|
||||
```env
|
||||
EMAIL_ENABLED=true
|
||||
SMTP_SERVER=smtp.163.com
|
||||
SMTP_PORT=465
|
||||
EMAIL_FROM=your_email@163.com
|
||||
EMAIL_PASSWORD=your_authorization_code
|
||||
EMAIL_TO=receiver@example.com
|
||||
```
|
||||
|
||||
**注意**:163邮箱推荐使用465端口(SSL)
|
||||
|
||||
---
|
||||
|
||||
## 3️⃣ 使用监测功能
|
||||
|
||||
### 添加监测股票
|
||||
1. 进入"实时监测"页面
|
||||
2. 填写股票信息:
|
||||
- 股票代码(如:600519)
|
||||
- 股票名称
|
||||
- 投资评级
|
||||
3. 设置关键位置:
|
||||
- 进场区间(最小-最大价格)
|
||||
- 止盈位
|
||||
- 止损位
|
||||
4. 选择监测间隔(30-300秒)
|
||||
5. 开启邮件通知开关
|
||||
6. 点击"添加监测"
|
||||
|
||||
### 启动监测服务
|
||||
- 点击"▶️ 启动监测"按钮
|
||||
- 系统开始后台自动监测
|
||||
- 价格触发条件时自动发送通知
|
||||
|
||||
### 管理监测股票
|
||||
- **更新**:手动刷新股票价格
|
||||
- **编辑**:修改监测参数
|
||||
- **通知开关**:启用/禁用通知
|
||||
- **删除**:移除监测
|
||||
|
||||
---
|
||||
|
||||
## 4️⃣ 导出PDF报告
|
||||
|
||||
1. 完成股票分析后
|
||||
2. 滚动到分析结果底部
|
||||
3. 点击"📄 生成并下载PDF报告"
|
||||
4. 等待生成完成
|
||||
5. 点击下载链接保存报告
|
||||
|
||||
---
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### 邮件无法发送?
|
||||
- ✅ 检查是否使用授权码(不是登录密码)
|
||||
- ✅ 确认SMTP服务器和端口正确
|
||||
- ✅ 尝试切换端口(587 ↔ 465)
|
||||
- ✅ 检查网络连接和防火墙
|
||||
|
||||
### 监测不工作?
|
||||
- ✅ 确认监测服务已启动
|
||||
- ✅ 检查股票代码格式是否正确
|
||||
- ✅ 查看系统日志中的错误信息
|
||||
|
||||
### PDF中文乱码?
|
||||
- ✅ 系统已自动注册中文字体
|
||||
- ✅ 确保Windows系统字体完整
|
||||
- ✅ 重新生成PDF报告
|
||||
|
||||
---
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
- 查看 README.md 了解详细功能
|
||||
- 查看 CHANGELOG.md 了解更新内容
|
||||
- 查看界面内的配置说明
|
||||
|
||||
---
|
||||
|
||||
**祝您使用愉快!📈**
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
# 🤖 复合多AI智能体股票团队分析系统
|
||||
|
||||
基于Python + Streamlit + DeepSeek的智能股票分析系统,模拟证券公司分析师团队,提供全方位的股票投资分析和决策建议。
|
||||
<img width="1910" height="923" alt="image" src="https://github.com/user-attachments/assets/b482a2bf-6349-476c-9ba4-237960cc632a" />
|
||||
<img width="1910" height="923" alt="image" src="https://github.com/user-attachments/assets/b2ac735b-5a3a-478c-955d-1a37eb705bb1" />
|
||||
|
||||
|
||||
## ✨ 功能特色
|
||||
|
||||
### 🎯 多维度分析
|
||||
- **技术面分析**:趋势判断、技术指标、支撑阻力位分析
|
||||
- **基本面分析**:财务指标、估值分析、行业研究
|
||||
- **资金面分析**:资金流向、主力行为、市场情绪
|
||||
- **风险管理**:风险识别、风险评估、风险控制策略
|
||||
- **市场情绪**:投资者情绪、热点板块、消息面分析
|
||||
|
||||
### 🤖 AI智能体团队
|
||||
- **技术分析师**:专注技术指标和图表分析
|
||||
- **基本面分析师**:专注公司价值和行业研究(含13+财务指标)
|
||||
- **资金面分析师**:专注资金流向和主力行为
|
||||
- **风险管理师**:专注风险识别和控制
|
||||
- **市场情绪分析师**:专注市场心理和热点追踪
|
||||
|
||||
### 📊 完整分析流程
|
||||
1. 📈 获取股票数据(支持A股和美股)
|
||||
2. 📊 获取详细财务数据(三大报表+财务指标)
|
||||
3. 🔍 多智能体并行分析
|
||||
4. 🤝 团队综合讨论
|
||||
5. 📋 最终投资决策
|
||||
6. 🎯 操作建议和风险提示
|
||||
7. 📄 PDF报告导出
|
||||
<img width="1910" height="923" alt="image" src="https://github.com/user-attachments/assets/836d758f-df6d-44a1-b64a-fd6ad442cb0f" />
|
||||
<img width="1910" height="923" alt="image" src="https://github.com/user-attachments/assets/903a64c7-7018-44dd-aa87-af4f7904048c" />
|
||||
<img width="1910" height="923" alt="image" src="https://github.com/user-attachments/assets/d9878153-d743-4d65-9575-62ac37f8cbfb" />
|
||||
<img width="1910" height="923" alt="image" src="https://github.com/user-attachments/assets/1c1c27af-1fe8-46e8-9e4a-68ead604a24d" />
|
||||
<img width="1910" height="923" alt="image" src="https://github.com/user-attachments/assets/242ee212-ba6c-4e0a-ae16-47aff3f356e6" />
|
||||
|
||||
|
||||
### 🔍 实时监测功能
|
||||
- **智能监测**:自动监控股票价格变动
|
||||
- **关键位置提醒**:进场区间、止盈位、止损位触发通知
|
||||
- **自定义间隔**:30秒至300秒灵活设置
|
||||
- **多种通知方式**:网页提醒 + 邮件通知
|
||||
- **卡片式管理**:直观的股票监测卡片展示
|
||||
- **完整功能**:添加、编辑、删除、启停、通知开关
|
||||
<img width="1910" height="923" alt="image" src="https://github.com/user-attachments/assets/1e93f870-7846-426c-9c22-15fdfaf1f1d0" />
|
||||
<img width="1910" height="923" alt="image" src="https://github.com/user-attachments/assets/a85defbf-321a-4fe2-b491-6855f2aa366c" />
|
||||
|
||||
### 📧 邮件通知系统
|
||||
- **多邮箱支持**:QQ邮箱、163邮箱、Gmail等
|
||||
- **触发条件**:
|
||||
- 价格进入进场区间
|
||||
- 达到止盈位
|
||||
- 触及止损位
|
||||
- **测试功能**:一键测试邮件配置
|
||||
- **历史记录**:完整的通知历史查询
|
||||
<img width="1910" height="923" alt="image" src="https://github.com/user-attachments/assets/a1a3c1ca-f97f-43eb-a043-1011c09e06d0" />
|
||||
<img width="1910" height="923" alt="image" src="https://github.com/user-attachments/assets/1808f284-e922-4eac-bfea-134996de93f1" />
|
||||
### 🎨 现代化界面
|
||||
- **渐变背景设计**:专业的紫色渐变配色
|
||||
- **响应式布局**:支持桌面、平板、手机
|
||||
- **实时数据可视化**:Plotly交互式图表
|
||||
- **卡片式展示**:清晰的信息模块化
|
||||
- **动画交互**:流畅的悬停和过渡效果
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 环境要求
|
||||
- Python 3.8+
|
||||
- 稳定的网络连接
|
||||
- DeepSeek API Key
|
||||
|
||||
### 2. 安装依赖
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. 配置API
|
||||
|
||||
#### 方法一:使用环境变量文件(推荐)
|
||||
1. 复制环境变量模板文件:
|
||||
```bash
|
||||
# Windows (PowerShell)
|
||||
Copy-Item .env.example .env
|
||||
|
||||
# 或者使用命令
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. 编辑 `.env` 文件,设置您的配置:
|
||||
```env
|
||||
# DeepSeek API配置(必需)
|
||||
DEEPSEEK_API_KEY=your_actual_deepseek_api_key_here
|
||||
|
||||
# 邮件通知配置(可选)
|
||||
EMAIL_ENABLED=false
|
||||
SMTP_SERVER=smtp.qq.com
|
||||
SMTP_PORT=587
|
||||
EMAIL_FROM=your_email@qq.com
|
||||
EMAIL_PASSWORD=your_authorization_code
|
||||
EMAIL_TO=receiver@example.com
|
||||
```
|
||||
|
||||
|
||||
#### 方法二:设置系统环境变量
|
||||
您也可以直接在系统环境变量中设置:
|
||||
- 变量名:`DEEPSEEK_API_KEY`
|
||||
- 变量值:您的API密钥
|
||||
|
||||
**注意**:环境变量文件的优先级高于系统环境变量。
|
||||
|
||||
### 4. 启动系统
|
||||
```bash
|
||||
python run.py
|
||||
```
|
||||
或者直接运行:
|
||||
```bash
|
||||
streamlit run app.py
|
||||
```
|
||||
|
||||
### 5. 访问系统
|
||||
打开浏览器访问:http://localhost:8501
|
||||
|
||||
## 📊 使用指南
|
||||
|
||||
### 股票代码格式(美股暂不支持)
|
||||
- **美股**:AAPL, MSFT, GOOGL, TSLA, NVDA
|
||||
- **A股**:000001, 600036, 000002, 600519
|
||||
|
||||
### 分析流程
|
||||
1. 在输入框中输入股票代码
|
||||
2. 点击"开始分析"按钮
|
||||
3. 等待AI分析师团队完成分析
|
||||
4. 查看各维度分析报告
|
||||
5. 阅读团队讨论结果
|
||||
6. 获取最终投资决策
|
||||
|
||||
### 结果解读
|
||||
- **投资评级**:买入/持有/卖出
|
||||
- **目标价位**:预期价格目标
|
||||
- **操作建议**:具体交易策略
|
||||
- **进出场位置**:关键价位点
|
||||
- **止盈止损**:风险控制位置
|
||||
- **风险提示**:主要风险因素
|
||||
|
||||
### 实时监测功能
|
||||
|
||||
#### 快速开始
|
||||
1. 点击侧边栏"📊 实时监测"按钮
|
||||
2. 在监测管理页面点击"添加监测股票"
|
||||
3. 填写股票信息和监测参数
|
||||
4. 点击"▶️ 启动监测"开始自动监控
|
||||
|
||||
#### 添加监测股票
|
||||
**必填信息**:
|
||||
- **股票代码**:6位A股代码或美股字母代码
|
||||
- **股票名称**:便于识别
|
||||
- **投资评级**:买入/持有/卖出
|
||||
|
||||
**监测参数**:
|
||||
- **进场区间**:设置最小和最大价格
|
||||
- 当股票价格进入该区间时触发通知
|
||||
- 用于把握最佳买入时机
|
||||
- **止盈位**:目标卖出价格
|
||||
- 价格达到或超过该值时提醒
|
||||
- 帮助锁定收益
|
||||
- **止损位**:最大亏损价格
|
||||
- 价格跌破该值时立即提醒
|
||||
- 控制投资风险
|
||||
- **检查间隔**:30-300秒
|
||||
- 监测频率,建议60秒以上
|
||||
- 避免过于频繁的API调用
|
||||
|
||||
#### 管理监测股票
|
||||
**查看功能**:
|
||||
- 📊 实时价格显示
|
||||
- 📈 涨跌幅展示
|
||||
- ⏰ 最后检查时间
|
||||
- 🔔 通知状态
|
||||
|
||||
**操作按钮**:
|
||||
- 🔄 **更新**:手动刷新当前价格
|
||||
- ✏️ **编辑**:修改监测参数
|
||||
- 🔔/🔕 **通知开关**:启用/禁用通知
|
||||
- 🗑️ **删除**:移除监测
|
||||
|
||||
**批量操作**:
|
||||
- ▶️ 启动监测:开始后台自动监控所有股票
|
||||
- ⏹️ 停止监测:暂停监控服务
|
||||
- 🔄 刷新状态:更新显示信息
|
||||
|
||||
#### 通知系统
|
||||
|
||||
**网页通知**:
|
||||
- 自动在界面显示提醒
|
||||
- 实时查看通知历史
|
||||
- 支持标记已读和清空
|
||||
|
||||
**邮件通知配置**:
|
||||
|
||||
1. **编辑.env文件**:
|
||||
```env
|
||||
EMAIL_ENABLED=true
|
||||
SMTP_SERVER=smtp.qq.com
|
||||
SMTP_PORT=587
|
||||
EMAIL_FROM=your_email@qq.com
|
||||
EMAIL_PASSWORD=your_authorization_code
|
||||
EMAIL_TO=receiver@example.com
|
||||
```
|
||||
|
||||
2. **获取邮箱授权码**:
|
||||
- **QQ邮箱**:设置 → 账户 → POP3/IMAP/SMTP → 生成授权码
|
||||
- **163邮箱**:设置 → POP3/SMTP/IMAP → 开启服务 → 设置授权密码
|
||||
- **Gmail**:开启两步验证 → 生成应用专用密码
|
||||
|
||||
3. **测试邮件配置**:
|
||||
- 进入"通知管理"区域
|
||||
- 点击"📧 发送测试邮件"
|
||||
- 检查收件箱(含垃圾箱)
|
||||
|
||||
**通知触发条件**:
|
||||
- ✅ 价格进入进场区间 → 进场提醒
|
||||
- ✅ 价格达到止盈位 → 止盈提醒
|
||||
- ✅ 价格跌破止损位 → 止损提醒
|
||||
|
||||
#### 使用技巧
|
||||
|
||||
**参数设置建议**:
|
||||
- **进场区间**:根据技术分析设定支撑位和阻力位
|
||||
- **止盈位**:建议设置10-20%的盈利目标
|
||||
- **止损位**:建议设置5-10%的止损线
|
||||
- **检查间隔**:
|
||||
- 长线投资:180-300秒
|
||||
- 短线交易:30-60秒
|
||||
|
||||
**监测策略**:
|
||||
1. 分析后添加到监测列表
|
||||
2. 设置合理的进场区间等待买入信号
|
||||
3. 买入后调整为止盈/止损监测
|
||||
4. 收到通知后及时决策
|
||||
|
||||
**注意事项**:
|
||||
- ⚠️ 监测间隔不要设置太短,避免频繁API调用
|
||||
- ⚠️ 邮件通知有延迟,不适用于高频交易
|
||||
- ⚠️ 定期检查监测服务运行状态
|
||||
- ⚠️ 及时处理触发的通知,避免错过时机
|
||||
|
||||
## 🏗️ 系统架构
|
||||
|
||||
```
|
||||
AI股票分析系统
|
||||
├── app.py # Streamlit主界面
|
||||
├── stock_data.py # 股票数据获取模块
|
||||
├── deepseek_client.py # DeepSeek API客户端
|
||||
├── ai_agents.py # AI智能体分析模块
|
||||
├── monitor_manager.py # 监测管理界面
|
||||
├── monitor_service.py # 监测服务后台
|
||||
├── monitor_db.py # 监测数据库管理
|
||||
├── notification_service.py # 通知服务(邮件/界面)
|
||||
├── pdf_generator.py # PDF报告生成
|
||||
├── database.py # 分析记录数据库
|
||||
├── config.py # 配置文件
|
||||
├── requirements.txt # 依赖包列表
|
||||
└── run.py # 启动脚本
|
||||
```
|
||||
|
||||
### 核心模块说明
|
||||
|
||||
#### 📈 股票数据模块 (stock_data.py)
|
||||
- 支持A股和美股数据获取
|
||||
- 集成yfinance和akshare数据源
|
||||
- 自动计算技术指标(MA、RSI、MACD、KDJ等)
|
||||
- 获取详细财务数据(三大报表+13+财务指标)
|
||||
- 数据清洗和格式化
|
||||
|
||||
#### 🤖 AI智能体模块 (ai_agents.py)
|
||||
- 多个专业分析师AI角色
|
||||
- 并行分析处理
|
||||
- 团队讨论机制
|
||||
- 最终决策生成
|
||||
- 财务数据深度解读
|
||||
|
||||
#### 🔗 API客户端 (deepseek_client.py)
|
||||
- DeepSeek API封装
|
||||
- 智能对话管理
|
||||
- 错误处理和重试
|
||||
- 响应格式解析
|
||||
- 支持多模型切换
|
||||
|
||||
#### 🔍 监测管理模块 (monitor_manager.py)
|
||||
- 股票监测管理界面
|
||||
- 添加/编辑/删除监测
|
||||
- 卡片式展示
|
||||
- 搜索和筛选功能
|
||||
- 通知历史管理
|
||||
|
||||
#### ⚙️ 监测服务模块 (monitor_service.py)
|
||||
- 后台监测线程
|
||||
- 定时价格检查
|
||||
- 触发条件判断
|
||||
- 自动通知发送
|
||||
- 启动/停止控制
|
||||
|
||||
#### 💾 监测数据库 (monitor_db.py)
|
||||
- SQLite数据持久化
|
||||
- 监测股票表
|
||||
- 价格历史表
|
||||
- 通知记录表
|
||||
- CRUD操作接口
|
||||
|
||||
#### 📧 通知服务模块 (notification_service.py)
|
||||
- 邮件通知发送
|
||||
- 网页通知展示
|
||||
- 多邮箱支持(QQ/163/Gmail)
|
||||
- 通知历史管理
|
||||
- 配置测试功能
|
||||
|
||||
#### 📄 PDF生成模块 (pdf_generator.py)
|
||||
- 专业分析报告生成
|
||||
- 中文字体支持
|
||||
- 完整分析内容
|
||||
- 一键下载功能
|
||||
|
||||
#### 🎨 前端界面 (app.py)
|
||||
- 现代化渐变UI设计
|
||||
- 响应式布局
|
||||
- 三大功能模块(分析/监测/历史)
|
||||
- 实时数据可视化
|
||||
- 交互式操作
|
||||
- 美观的动画效果
|
||||
|
||||
## 📋 技术特性
|
||||
|
||||
### 数据源
|
||||
- **美股数据**:Yahoo Finance (yfinance)
|
||||
- **A股数据**:AKShare免费接口
|
||||
- **技术指标**:TA-Lib技术分析库
|
||||
|
||||
### AI模型
|
||||
- **语言模型**:DeepSeek Chat API
|
||||
- **分析框架**:多智能体协作
|
||||
- **决策逻辑**:综合评分机制
|
||||
|
||||
### 可视化
|
||||
- **图表库**:Plotly交互式图表
|
||||
- **K线图**:蜡烛图with技术指标
|
||||
- **指标图**:RSI、MACD、布林带等
|
||||
|
||||
### 性能优化
|
||||
- **数据缓存**:Streamlit缓存机制
|
||||
- **异步处理**:并行分析提升效率
|
||||
- **错误处理**:完善的异常处理机制
|
||||
|
||||
## ⚙️ 高级配置
|
||||
|
||||
### API配置
|
||||
```env
|
||||
# .env 文件
|
||||
DEEPSEEK_API_KEY=your_api_key
|
||||
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
|
||||
```
|
||||
|
||||
**重要提示**:
|
||||
- 请将 `.env.template` 复制为 `.env` 文件
|
||||
- 在 `.env` 文件中填写实际的API密钥
|
||||
- 不要将 `.env` 文件提交到版本控制系统
|
||||
|
||||
### 数据配置
|
||||
```python
|
||||
DEFAULT_PERIOD = "1y" # 默认数据周期
|
||||
DEFAULT_INTERVAL = "1d" # 默认数据间隔
|
||||
```
|
||||
|
||||
### 系统参数
|
||||
- **缓存时间**:300秒(5分钟)
|
||||
- **API超时**:30秒
|
||||
- **最大重试**:3次
|
||||
|
||||
## 🛠️ 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **API Key错误**
|
||||
- 检查.env文件中的DEEPSEEK_API_KEY设置
|
||||
- 确保.env文件存在且格式正确
|
||||
- 确保API Key有效且有足够余额
|
||||
|
||||
2. **股票数据获取失败**
|
||||
- 检查网络连接
|
||||
- 确认股票代码格式正确(A股6位数字,美股字母代码)
|
||||
- 可能是数据源临时不可用,稍后重试
|
||||
|
||||
3. **财务数据获取失败**
|
||||
- 部分新股可能没有完整财务数据
|
||||
- 网络问题可能导致数据获取超时
|
||||
- 系统会自动处理,继续进行其他分析
|
||||
|
||||
4. **依赖包安装失败**
|
||||
- 使用 `pip install -r requirements.txt`
|
||||
- 如有问题,尝试手动安装单个包
|
||||
- 确保Python版本为3.8+
|
||||
|
||||
5. **页面加载缓慢**
|
||||
- 首次运行需要下载数据,请耐心等待
|
||||
- 系统有5分钟缓存,重复查询会更快
|
||||
- 财务数据获取较慢,约需10-20秒
|
||||
|
||||
6. **分析过程中出错**
|
||||
- 检查网络连接是否稳定
|
||||
- 查看终端输出的详细错误信息
|
||||
- 尝试重新启动应用
|
||||
|
||||
### 日志调试
|
||||
系统运行时会在控制台输出详细日志,可用于问题诊断。如遇到错误,请查看终端输出。
|
||||
|
||||
### 错误报告
|
||||
如发现bug,请查看 `BUGFIX.md` 文件了解已知问题和解决方案。
|
||||
|
||||
## 📜 免责声明
|
||||
|
||||
本系统仅供学习和研究使用,不构成投资建议。股票投资有风险,入市需谨慎。使用本系统进行投资决策的风险由用户自行承担。
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
欢迎提交Issue和Pull Request!
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
---
|
||||
|
||||
**享受AI驱动的智能股票分析体验!** 🚀📈
|
||||
@@ -0,0 +1,223 @@
|
||||
# 🎨 UI美化升级说明
|
||||
|
||||
## 升级概览
|
||||
|
||||
系统UI已全面升级,采用现代化设计理念,提升用户体验和视觉效果。
|
||||
|
||||
## 🌟 主要改进
|
||||
|
||||
### 1. 顶部标题栏
|
||||
- **渐变背景**:紫色渐变(#667eea → #764ba2)
|
||||
- **立体阴影**:增加视觉层次感
|
||||
- **双语标题**:中英文展示,更专业
|
||||
- **响应式设计**:移动端自适应
|
||||
|
||||
### 2. 侧边栏美化
|
||||
**配色方案**
|
||||
- 背景:紫色渐变(180度)
|
||||
- 文字:白色/半透明白色
|
||||
- 按钮:渐变背景 + 悬停效果
|
||||
|
||||
**布局优化**(从上到下)
|
||||
1. 🔍 **快捷导航**(置顶)
|
||||
- 历史记录按钮
|
||||
- 实时监测按钮
|
||||
- 返回首页按钮
|
||||
|
||||
2. ⚙️ **系统配置**
|
||||
- API连接状态
|
||||
- AI模型选择器
|
||||
|
||||
3. 📊 **系统状态面板**
|
||||
- 监测服务状态
|
||||
- 分析记录统计
|
||||
- 监测股票数量
|
||||
- 待处理通知
|
||||
|
||||
4. 📊 **分析参数设置**
|
||||
- 数据周期选择
|
||||
|
||||
5. 💡 **使用帮助**(折叠式)
|
||||
|
||||
### 3. 主页面样式
|
||||
**全局背景**
|
||||
- 紫色渐变背景(固定定位)
|
||||
- 主容器:半透明白色背景
|
||||
- 圆角边框:20px
|
||||
- 立体阴影效果
|
||||
|
||||
**卡片设计**
|
||||
- 分析师卡片:灰白渐变 + 左侧紫色边框
|
||||
- 决策卡片:绿色渐变 + 粗边框
|
||||
- 警告卡片:橙色渐变 + 左侧边框
|
||||
- 指标卡片:白色 + 顶部紫色边框
|
||||
|
||||
**交互效果**
|
||||
- 悬停动画:平移/抬起效果
|
||||
- 阴影渐变:鼠标悬停时加深
|
||||
- 按钮动画:上移 + 阴影扩大
|
||||
|
||||
### 4. 组件美化
|
||||
**按钮**
|
||||
- 紫色渐变背景
|
||||
- 圆角:10px
|
||||
- 悬停效果:上移2px + 阴影扩大
|
||||
- 字体:600粗细,白色
|
||||
|
||||
**输入框**
|
||||
- 圆角:10px
|
||||
- 边框:2px实线
|
||||
- 聚焦效果:紫色边框 + 外发光
|
||||
|
||||
**进度条**
|
||||
- 紫色渐变填充
|
||||
- 平滑过渡动画
|
||||
|
||||
**消息框**
|
||||
- 圆角:10px
|
||||
- 立体阴影
|
||||
- 内边距:1rem
|
||||
|
||||
**图表**
|
||||
- 圆角:15px
|
||||
- 阴影效果
|
||||
- 自适应容器
|
||||
|
||||
## 📊 配色方案
|
||||
|
||||
### 主色调
|
||||
```css
|
||||
主紫色: #667eea
|
||||
深紫色: #764ba2
|
||||
```
|
||||
|
||||
### 辅助色
|
||||
```css
|
||||
成功色: #4caf50 (绿色)
|
||||
警告色: #ff9800 (橙色)
|
||||
错误色: #f44336 (红色)
|
||||
信息色: #2196f3 (蓝色)
|
||||
```
|
||||
|
||||
### 背景色
|
||||
```css
|
||||
主背景: linear-gradient(135deg, #667eea 0%, #764ba2 100%)
|
||||
卡片背景: rgba(255, 255, 255, 0.95)
|
||||
侧边栏: linear-gradient(180deg, #667eea 0%, #764ba2 100%)
|
||||
```
|
||||
|
||||
## 🎯 设计特点
|
||||
|
||||
### 1. 渐变美学
|
||||
- 大量使用渐变色
|
||||
- 增加视觉深度
|
||||
- 现代化科技感
|
||||
|
||||
### 2. 卡片式布局
|
||||
- 信息模块化
|
||||
- 清晰的视觉分隔
|
||||
- 便于信息查找
|
||||
|
||||
### 3. 微交互设计
|
||||
- 悬停动画反馈
|
||||
- 平滑过渡效果
|
||||
- 提升交互体验
|
||||
|
||||
### 4. 专业配色
|
||||
- 紫色代表智能科技
|
||||
- 高对比度易读性
|
||||
- 统一的视觉语言
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### CSS特性
|
||||
- **渐变(Gradient)**:linear-gradient
|
||||
- **阴影(Shadow)**:box-shadow
|
||||
- **圆角(Radius)**:border-radius
|
||||
- **过渡(Transition)**:transition
|
||||
- **变换(Transform)**:transform
|
||||
- **透明度(Opacity)**:rgba
|
||||
|
||||
### 响应式设计
|
||||
```css
|
||||
@media (max-width: 768px) {
|
||||
/* 移动端适配 */
|
||||
.nav-title { font-size: 1.5rem; }
|
||||
.stTabs [data-baseweb="tab"] {
|
||||
font-size: 0.9rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📱 兼容性
|
||||
|
||||
- ✅ 桌面端(1920px+)
|
||||
- ✅ 笔记本(1366px+)
|
||||
- ✅ 平板(768px+)
|
||||
- ✅ 手机(375px+)
|
||||
|
||||
## 🎨 UI组件清单
|
||||
|
||||
### 已美化组件
|
||||
- [x] 顶部导航栏
|
||||
- [x] 侧边栏
|
||||
- [x] 主标题
|
||||
- [x] 按钮
|
||||
- [x] 输入框
|
||||
- [x] 进度条
|
||||
- [x] 卡片
|
||||
- [x] 消息框
|
||||
- [x] 图表容器
|
||||
- [x] Expander
|
||||
- [x] 数据框
|
||||
|
||||
### 隐藏元素
|
||||
- [x] Streamlit顶部菜单
|
||||
- [x] Streamlit底部水印
|
||||
|
||||
## 💡 使用建议
|
||||
|
||||
### 1. 保持一致性
|
||||
- 所有新增卡片使用统一样式
|
||||
- 按钮保持统一配色
|
||||
- 间距使用统一标准
|
||||
|
||||
### 2. 适度使用动画
|
||||
- 避免过度动画
|
||||
- 保持性能流畅
|
||||
- 提升而非干扰用户
|
||||
|
||||
### 3. 注意可访问性
|
||||
- 保持足够对比度
|
||||
- 文字大小适中
|
||||
- 颜色不是唯一信息来源
|
||||
|
||||
## 🚀 未来优化方向
|
||||
|
||||
- [ ] 深色模式支持
|
||||
- [ ] 主题切换功能
|
||||
- [ ] 更多动画效果
|
||||
- [ ] 自定义配色方案
|
||||
- [ ] 图标库扩展
|
||||
- [ ] 加载动画优化
|
||||
|
||||
## 📝 维护说明
|
||||
|
||||
### CSS位置
|
||||
所有CSS样式定义在 `app.py` 文件顶部的 `st.markdown()` 中。
|
||||
|
||||
### 修改指南
|
||||
1. 在CSS样式块中修改
|
||||
2. 保持选择器特异性
|
||||
3. 测试不同屏幕尺寸
|
||||
4. 保持代码注释
|
||||
|
||||
### 版本记录
|
||||
- **v2.0** (2025-10-03): 全面UI美化升级
|
||||
- **v1.0** (2025-10-02): 基础UI实现
|
||||
|
||||
---
|
||||
|
||||
**设计理念**:简洁、现代、专业、易用
|
||||
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
from deepseek_client import DeepSeekClient
|
||||
from typing import Dict, Any
|
||||
import time
|
||||
|
||||
class StockAnalysisAgents:
|
||||
"""股票分析AI智能体集合"""
|
||||
|
||||
def __init__(self, model="deepseek-chat"):
|
||||
self.model = model
|
||||
self.deepseek_client = DeepSeekClient(model=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)
|
||||
|
||||
return {
|
||||
"agent_name": "技术分析师",
|
||||
"agent_role": "负责技术指标分析、图表形态识别、趋势判断",
|
||||
"analysis": analysis,
|
||||
"focus_areas": ["技术指标", "趋势分析", "支撑阻力", "交易信号"],
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
def fundamental_analyst_agent(self, stock_info: Dict, financial_data: Dict = None) -> Dict[str, Any]:
|
||||
"""基本面分析智能体"""
|
||||
print("📊 基本面分析师正在分析中...")
|
||||
time.sleep(1)
|
||||
|
||||
analysis = self.deepseek_client.fundamental_analysis(stock_info, financial_data)
|
||||
|
||||
return {
|
||||
"agent_name": "基本面分析师",
|
||||
"agent_role": "负责公司财务分析、行业研究、估值分析",
|
||||
"analysis": analysis,
|
||||
"focus_areas": ["财务指标", "行业分析", "公司价值", "成长性"],
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
def fund_flow_analyst_agent(self, stock_info: Dict, indicators: Dict) -> Dict[str, Any]:
|
||||
"""资金面分析智能体"""
|
||||
print("💰 资金面分析师正在分析中...")
|
||||
time.sleep(1)
|
||||
|
||||
analysis = self.deepseek_client.fund_flow_analysis(stock_info, indicators)
|
||||
|
||||
return {
|
||||
"agent_name": "资金面分析师",
|
||||
"agent_role": "负责资金流向分析、主力行为研究、市场情绪判断",
|
||||
"analysis": analysis,
|
||||
"focus_areas": ["资金流向", "主力动向", "市场情绪", "流动性"],
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
def risk_management_agent(self, stock_info: Dict, indicators: Dict) -> Dict[str, Any]:
|
||||
"""风险管理智能体"""
|
||||
print("⚠️ 风险管理师正在评估中...")
|
||||
time.sleep(1)
|
||||
|
||||
risk_prompt = f"""
|
||||
作为风险管理专家,请基于以下信息进行风险评估:
|
||||
|
||||
股票信息:
|
||||
- 股票代码:{stock_info.get('symbol', 'N/A')}
|
||||
- 股票名称:{stock_info.get('name', 'N/A')}
|
||||
- 当前价格:{stock_info.get('current_price', 'N/A')}
|
||||
- Beta系数:{stock_info.get('beta', 'N/A')}
|
||||
- 52周最高:{stock_info.get('52_week_high', 'N/A')}
|
||||
- 52周最低:{stock_info.get('52_week_low', 'N/A')}
|
||||
|
||||
技术指标:
|
||||
- RSI:{indicators.get('rsi', 'N/A')}
|
||||
- 布林带位置:当前价格相对于上下轨的位置
|
||||
- 波动率指标等
|
||||
|
||||
请从以下角度进行风险评估:
|
||||
1. 市场风险(系统性风险)
|
||||
2. 个股风险(非系统性风险)
|
||||
3. 流动性风险
|
||||
4. 波动性风险
|
||||
5. 估值风险
|
||||
6. 行业风险
|
||||
7. 风险等级评定(低/中/高)
|
||||
8. 风险控制建议
|
||||
|
||||
给出专业的风险评估报告。
|
||||
"""
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "你是一名专业的风险管理专家,具有丰富的风险识别和控制经验。"},
|
||||
{"role": "user", "content": risk_prompt}
|
||||
]
|
||||
|
||||
analysis = self.deepseek_client.call_api(messages)
|
||||
|
||||
return {
|
||||
"agent_name": "风险管理师",
|
||||
"agent_role": "负责风险识别、风险评估、风险控制策略制定",
|
||||
"analysis": analysis,
|
||||
"focus_areas": ["风险识别", "风险量化", "风险控制", "资产配置"],
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
def market_sentiment_agent(self, stock_info: Dict) -> Dict[str, Any]:
|
||||
"""市场情绪分析智能体"""
|
||||
print("📈 市场情绪分析师正在分析中...")
|
||||
time.sleep(1)
|
||||
|
||||
sentiment_prompt = f"""
|
||||
作为市场情绪分析专家,请基于当前市场环境对以下股票进行情绪分析:
|
||||
|
||||
股票信息:
|
||||
- 股票代码:{stock_info.get('symbol', 'N/A')}
|
||||
- 股票名称:{stock_info.get('name', 'N/A')}
|
||||
- 行业:{stock_info.get('sector', 'N/A')}
|
||||
- 细分行业:{stock_info.get('industry', 'N/A')}
|
||||
|
||||
请从以下角度分析市场情绪:
|
||||
1. 整体市场情绪(牛市/熊市/震荡市)
|
||||
2. 行业板块情绪和热度
|
||||
3. 个股关注度和讨论热度
|
||||
4. 投资者情绪指标
|
||||
5. 市场预期和共识
|
||||
6. 消息面和事件驱动因素
|
||||
7. 情绪对股价的影响评估
|
||||
8. 情绪反转的可能性
|
||||
|
||||
结合当前宏观环境和市场热点,给出专业的市场情绪分析。
|
||||
"""
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "你是一名专业的市场情绪分析师,擅长解读市场心理和投资者行为。"},
|
||||
{"role": "user", "content": sentiment_prompt}
|
||||
]
|
||||
|
||||
analysis = self.deepseek_client.call_api(messages)
|
||||
|
||||
return {
|
||||
"agent_name": "市场情绪分析师",
|
||||
"agent_role": "负责市场情绪研究、投资者心理分析、热点追踪",
|
||||
"analysis": analysis,
|
||||
"focus_areas": ["市场情绪", "投资者心理", "热点板块", "消息面"],
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
def run_multi_agent_analysis(self, stock_info: Dict, stock_data: Any, indicators: Dict, financial_data: Dict = None) -> Dict[str, Any]:
|
||||
"""运行多智能体分析"""
|
||||
print("🚀 启动多智能体股票分析系统...")
|
||||
print("=" * 50)
|
||||
|
||||
# 并行运行各个分析师
|
||||
agents_results = {}
|
||||
|
||||
# 技术面分析
|
||||
agents_results["technical"] = self.technical_analyst_agent(stock_info, stock_data, indicators)
|
||||
|
||||
# 基本面分析
|
||||
agents_results["fundamental"] = self.fundamental_analyst_agent(stock_info, financial_data)
|
||||
|
||||
# 资金面分析
|
||||
agents_results["fund_flow"] = self.fund_flow_analyst_agent(stock_info, indicators)
|
||||
|
||||
# 风险管理分析
|
||||
agents_results["risk_management"] = self.risk_management_agent(stock_info, indicators)
|
||||
|
||||
# 市场情绪分析
|
||||
agents_results["market_sentiment"] = self.market_sentiment_agent(stock_info)
|
||||
|
||||
print("✅ 所有分析师完成分析")
|
||||
print("=" * 50)
|
||||
|
||||
return agents_results
|
||||
|
||||
def conduct_team_discussion(self, agents_results: Dict[str, Any], stock_info: Dict) -> str:
|
||||
"""进行团队讨论"""
|
||||
print("🤝 分析团队正在进行综合讨论...")
|
||||
time.sleep(2)
|
||||
|
||||
# 提取各分析师的报告
|
||||
technical_report = agents_results.get("technical", {}).get("analysis", "")
|
||||
fundamental_report = agents_results.get("fundamental", {}).get("analysis", "")
|
||||
fund_flow_report = agents_results.get("fund_flow", {}).get("analysis", "")
|
||||
risk_report = agents_results.get("risk_management", {}).get("analysis", "")
|
||||
sentiment_report = agents_results.get("market_sentiment", {}).get("analysis", "")
|
||||
|
||||
discussion_prompt = f"""
|
||||
现在进行投资决策团队会议,参会人员包括:技术分析师、基本面分析师、资金面分析师、风险管理师、市场情绪分析师。
|
||||
|
||||
股票:{stock_info.get('name', 'N/A')} ({stock_info.get('symbol', 'N/A')})
|
||||
|
||||
各分析师报告:
|
||||
|
||||
【技术分析师报告】
|
||||
{technical_report}
|
||||
|
||||
【基本面分析师报告】
|
||||
{fundamental_report}
|
||||
|
||||
【资金面分析师报告】
|
||||
{fund_flow_report}
|
||||
|
||||
【风险管理师报告】
|
||||
{risk_report}
|
||||
|
||||
【市场情绪分析师报告】
|
||||
{sentiment_report}
|
||||
|
||||
请模拟一场真实的投资决策会议讨论:
|
||||
1. 各分析师观点的一致性和分歧
|
||||
2. 不同维度分析的权重考量
|
||||
3. 风险收益评估
|
||||
4. 投资时机判断
|
||||
5. 策略制定思路
|
||||
6. 达成初步共识
|
||||
|
||||
请以对话形式展现讨论过程,体现专业团队的思辨过程。
|
||||
"""
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "你需要模拟一场专业的投资团队讨论会议,体现不同角色的观点碰撞和最终共识形成。"},
|
||||
{"role": "user", "content": discussion_prompt}
|
||||
]
|
||||
|
||||
discussion_result = self.deepseek_client.call_api(messages, max_tokens=6000)
|
||||
|
||||
print("✅ 团队讨论完成")
|
||||
return discussion_result
|
||||
|
||||
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)
|
||||
|
||||
print("✅ 最终投资决策完成")
|
||||
return decision
|
||||
@@ -0,0 +1,16 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# DeepSeek API配置
|
||||
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
|
||||
DEEPSEEK_BASE_URL = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1")
|
||||
|
||||
# 其他配置
|
||||
TUSHARE_TOKEN = os.getenv("TUSHARE_TOKEN", "")
|
||||
|
||||
# 股票数据源配置
|
||||
DEFAULT_PERIOD = "1y" # 默认获取1年数据
|
||||
DEFAULT_INTERVAL = "1d" # 默认日线数据
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import sqlite3
|
||||
import json
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
class StockAnalysisDatabase:
|
||||
def __init__(self, db_path="stock_analysis.db"):
|
||||
"""初始化数据库连接"""
|
||||
self.db_path = db_path
|
||||
self.init_database()
|
||||
|
||||
def init_database(self):
|
||||
"""初始化数据库表结构"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建分析记录表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS analysis_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
symbol TEXT NOT NULL,
|
||||
stock_name TEXT,
|
||||
analysis_date TEXT NOT NULL,
|
||||
period TEXT NOT NULL,
|
||||
stock_info TEXT,
|
||||
agents_results TEXT,
|
||||
discussion_result TEXT,
|
||||
final_decision TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def save_analysis(self, symbol, stock_name, period, stock_info, agents_results, discussion_result, final_decision):
|
||||
"""保存分析记录到数据库"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 准备数据
|
||||
analysis_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
created_at = datetime.now().isoformat()
|
||||
|
||||
# 将复杂对象转换为JSON字符串
|
||||
stock_info_json = json.dumps(stock_info, ensure_ascii=False, default=str)
|
||||
agents_results_json = json.dumps(agents_results, ensure_ascii=False, default=str)
|
||||
discussion_result_json = json.dumps(discussion_result, ensure_ascii=False, default=str)
|
||||
final_decision_json = json.dumps(final_decision, ensure_ascii=False, default=str)
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO analysis_records
|
||||
(symbol, stock_name, analysis_date, period, stock_info, agents_results, discussion_result, final_decision, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (symbol, stock_name, analysis_date, period, stock_info_json, agents_results_json, discussion_result_json, final_decision_json, created_at))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return cursor.lastrowid
|
||||
|
||||
def get_all_records(self):
|
||||
"""获取所有分析记录"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT id, symbol, stock_name, analysis_date, period, final_decision, created_at
|
||||
FROM analysis_records
|
||||
ORDER BY created_at DESC
|
||||
''')
|
||||
|
||||
records = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
result = []
|
||||
for record in records:
|
||||
# 解析final_decision获取评级
|
||||
final_decision = json.loads(record[5]) if record[5] else {}
|
||||
rating = final_decision.get('rating', '未知') if isinstance(final_decision, dict) else '未知'
|
||||
|
||||
result.append({
|
||||
'id': record[0],
|
||||
'symbol': record[1],
|
||||
'stock_name': record[2],
|
||||
'analysis_date': record[3],
|
||||
'period': record[4],
|
||||
'rating': rating,
|
||||
'created_at': record[6]
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def get_record_count(self):
|
||||
"""获取记录总数"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT COUNT(*) FROM analysis_records')
|
||||
count = cursor.fetchone()[0]
|
||||
conn.close()
|
||||
|
||||
return count
|
||||
|
||||
def get_record_by_id(self, record_id):
|
||||
"""根据ID获取详细分析记录"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT * FROM analysis_records WHERE id = ?
|
||||
''', (record_id,))
|
||||
|
||||
record = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not record:
|
||||
return None
|
||||
|
||||
# 解析JSON数据
|
||||
return {
|
||||
'id': record[0],
|
||||
'symbol': record[1],
|
||||
'stock_name': record[2],
|
||||
'analysis_date': record[3],
|
||||
'period': record[4],
|
||||
'stock_info': json.loads(record[5]) if record[5] else {},
|
||||
'agents_results': json.loads(record[6]) if record[6] else {},
|
||||
'discussion_result': json.loads(record[7]) if record[7] else {},
|
||||
'final_decision': json.loads(record[8]) if record[8] else {},
|
||||
'created_at': record[9]
|
||||
}
|
||||
|
||||
def delete_record(self, record_id):
|
||||
"""删除指定记录"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('DELETE FROM analysis_records WHERE id = ?', (record_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def get_record_count(self):
|
||||
"""获取记录总数"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT COUNT(*) FROM analysis_records')
|
||||
count = cursor.fetchone()[0]
|
||||
conn.close()
|
||||
|
||||
return count
|
||||
|
||||
# 全局数据库实例
|
||||
db = StockAnalysisDatabase()
|
||||
@@ -0,0 +1,346 @@
|
||||
import openai
|
||||
import json
|
||||
from typing import Dict, List, Any, Optional
|
||||
import config
|
||||
|
||||
class DeepSeekClient:
|
||||
"""DeepSeek API客户端"""
|
||||
|
||||
def __init__(self, model="deepseek-chat"):
|
||||
self.model = model
|
||||
self.client = openai.OpenAI(
|
||||
api_key=config.DEEPSEEK_API_KEY,
|
||||
base_url=config.DEEPSEEK_BASE_URL
|
||||
)
|
||||
|
||||
def call_api(self, messages: List[Dict[str, str]], model: Optional[str] = None,
|
||||
temperature: float = 0.7, max_tokens: int = 2000) -> str:
|
||||
"""调用DeepSeek API"""
|
||||
# 使用实例的模型,如果没有传入则使用默认模型
|
||||
model_to_use = model or self.model
|
||||
|
||||
# 对于 reasoner 模型,自动增加 max_tokens
|
||||
if "reasoner" in model_to_use.lower() and max_tokens <= 2000:
|
||||
max_tokens = 8000 # reasoner 模型需要更多 tokens 来输出推理过程
|
||||
|
||||
try:
|
||||
response = self.client.chat.completions.create(
|
||||
model=model_to_use,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens
|
||||
)
|
||||
|
||||
# 处理 reasoner 模型的响应
|
||||
message = response.choices[0].message
|
||||
|
||||
# reasoner 模型可能包含 reasoning_content(推理过程)和 content(最终答案)
|
||||
# 我们返回完整内容,包括推理过程(如果有的话)
|
||||
result = ""
|
||||
|
||||
# 检查是否有推理内容
|
||||
if hasattr(message, 'reasoning_content') and message.reasoning_content:
|
||||
result += f"【推理过程】\n{message.reasoning_content}\n\n"
|
||||
|
||||
# 添加最终内容
|
||||
if message.content:
|
||||
result += message.content
|
||||
|
||||
return result if result else "API返回空响应"
|
||||
|
||||
except Exception as e:
|
||||
return f"API调用失败: {str(e)}"
|
||||
|
||||
def technical_analysis(self, stock_info: Dict, stock_data: Any, indicators: Dict) -> str:
|
||||
"""技术面分析"""
|
||||
prompt = f"""
|
||||
你是一名资深的技术分析师。请基于以下股票数据进行专业的技术面分析:
|
||||
|
||||
股票信息:
|
||||
- 股票代码:{stock_info.get('symbol', 'N/A')}
|
||||
- 股票名称:{stock_info.get('name', 'N/A')}
|
||||
- 当前价格:{stock_info.get('current_price', 'N/A')}
|
||||
- 涨跌幅:{stock_info.get('change_percent', 'N/A')}%
|
||||
|
||||
最新技术指标:
|
||||
- 收盘价:{indicators.get('price', 'N/A')}
|
||||
- MA5:{indicators.get('ma5', 'N/A')}
|
||||
- MA10:{indicators.get('ma10', 'N/A')}
|
||||
- MA20:{indicators.get('ma20', 'N/A')}
|
||||
- MA60:{indicators.get('ma60', 'N/A')}
|
||||
- RSI:{indicators.get('rsi', 'N/A')}
|
||||
- MACD:{indicators.get('macd', 'N/A')}
|
||||
- MACD信号线:{indicators.get('macd_signal', 'N/A')}
|
||||
- 布林带上轨:{indicators.get('bb_upper', 'N/A')}
|
||||
- 布林带下轨:{indicators.get('bb_lower', 'N/A')}
|
||||
- K值:{indicators.get('k_value', 'N/A')}
|
||||
- D值:{indicators.get('d_value', 'N/A')}
|
||||
- 量比:{indicators.get('volume_ratio', 'N/A')}
|
||||
|
||||
请从以下角度进行分析:
|
||||
1. 趋势分析(均线系统、价格走势)
|
||||
2. 超买超卖分析(RSI、KDJ)
|
||||
3. 动量分析(MACD)
|
||||
4. 支撑阻力分析(布林带)
|
||||
5. 成交量分析
|
||||
6. 短期、中期、长期技术判断
|
||||
7. 关键技术位分析
|
||||
|
||||
请给出专业、详细的技术分析报告,包含风险提示。
|
||||
"""
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "你是一名经验丰富的股票技术分析师,具有深厚的技术分析功底。"},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
return self.call_api(messages)
|
||||
|
||||
def fundamental_analysis(self, stock_info: Dict, financial_data: Dict = None) -> str:
|
||||
"""基本面分析"""
|
||||
|
||||
# 构建财务数据部分
|
||||
financial_section = ""
|
||||
if financial_data and not financial_data.get('error'):
|
||||
ratios = financial_data.get('financial_ratios', {})
|
||||
if ratios:
|
||||
financial_section = f"""
|
||||
详细财务指标:
|
||||
【盈利能力】
|
||||
- 净资产收益率(ROE):{ratios.get('净资产收益率ROE', ratios.get('ROE', 'N/A'))}
|
||||
- 总资产收益率(ROA):{ratios.get('总资产收益率ROA', ratios.get('ROA', 'N/A'))}
|
||||
- 销售毛利率:{ratios.get('销售毛利率', ratios.get('毛利率', 'N/A'))}
|
||||
- 销售净利率:{ratios.get('销售净利率', ratios.get('净利率', 'N/A'))}
|
||||
|
||||
【偿债能力】
|
||||
- 资产负债率:{ratios.get('资产负债率', 'N/A')}
|
||||
- 流动比率:{ratios.get('流动比率', 'N/A')}
|
||||
- 速动比率:{ratios.get('速动比率', 'N/A')}
|
||||
|
||||
【运营能力】
|
||||
- 存货周转率:{ratios.get('存货周转率', 'N/A')}
|
||||
- 应收账款周转率:{ratios.get('应收账款周转率', 'N/A')}
|
||||
- 总资产周转率:{ratios.get('总资产周转率', 'N/A')}
|
||||
|
||||
【成长能力】
|
||||
- 营业收入同比增长:{ratios.get('营业收入同比增长', ratios.get('收入增长', 'N/A'))}
|
||||
- 净利润同比增长:{ratios.get('净利润同比增长', ratios.get('盈利增长', 'N/A'))}
|
||||
|
||||
【每股指标】
|
||||
- 每股收益(EPS):{ratios.get('EPS', 'N/A')}
|
||||
- 每股账面价值:{ratios.get('每股账面价值', 'N/A')}
|
||||
- 股息率:{ratios.get('股息率', stock_info.get('dividend_yield', 'N/A'))}
|
||||
- 派息率:{ratios.get('派息率', 'N/A')}
|
||||
"""
|
||||
|
||||
# 添加报告期信息
|
||||
if ratios.get('报告期'):
|
||||
financial_section = f"\n财务数据报告期:{ratios.get('报告期')}\n" + financial_section
|
||||
|
||||
prompt = f"""
|
||||
你是一名资深的基本面分析师,拥有CFA资格和10年以上的证券分析经验。请基于以下详细信息进行深入的基本面分析:
|
||||
|
||||
【基本信息】
|
||||
- 股票代码:{stock_info.get('symbol', 'N/A')}
|
||||
- 股票名称:{stock_info.get('name', 'N/A')}
|
||||
- 当前价格:{stock_info.get('current_price', 'N/A')}
|
||||
- 市值:{stock_info.get('market_cap', 'N/A')}
|
||||
- 行业:{stock_info.get('sector', 'N/A')}
|
||||
- 细分行业:{stock_info.get('industry', 'N/A')}
|
||||
|
||||
【估值指标】
|
||||
- 市盈率(PE):{stock_info.get('pe_ratio', 'N/A')}
|
||||
- 市净率(PB):{stock_info.get('pb_ratio', 'N/A')}
|
||||
- 市销率(PS):{stock_info.get('ps_ratio', 'N/A')}
|
||||
- Beta系数:{stock_info.get('beta', 'N/A')}
|
||||
- 52周最高:{stock_info.get('52_week_high', 'N/A')}
|
||||
- 52周最低:{stock_info.get('52_week_low', 'N/A')}
|
||||
{financial_section}
|
||||
|
||||
请从以下维度进行专业、深入的分析:
|
||||
|
||||
1. **公司质地分析**
|
||||
- 业务模式和核心竞争力
|
||||
- 行业地位和市场份额
|
||||
- 护城河分析(品牌、技术、规模等)
|
||||
|
||||
2. **盈利能力分析**
|
||||
- ROE和ROA水平评估
|
||||
- 毛利率和净利率趋势
|
||||
- 与行业平均水平对比
|
||||
- 盈利质量和持续性
|
||||
|
||||
3. **财务健康度分析**
|
||||
- 资产负债结构
|
||||
- 偿债能力评估
|
||||
- 现金流状况
|
||||
- 财务风险识别
|
||||
|
||||
4. **成长性分析**
|
||||
- 收入和利润增长趋势
|
||||
- 增长驱动因素
|
||||
- 未来成长空间
|
||||
- 行业发展前景
|
||||
|
||||
5. **估值分析**
|
||||
- 当前估值水平(PE、PB)
|
||||
- 历史估值区间对比
|
||||
- 行业估值对比
|
||||
- 合理估值区间判断
|
||||
|
||||
6. **投资价值判断**
|
||||
- 综合评分(0-100分)
|
||||
- 投资亮点
|
||||
- 投资风险
|
||||
- 适合的投资者类型
|
||||
|
||||
请给出专业、详细的基本面分析报告,数据分析要深入,结论要有依据。
|
||||
|
||||
请结合当前市场环境和行业发展趋势,给出专业的基本面分析报告。
|
||||
"""
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "你是一名经验丰富的股票基本面分析师,擅长公司财务分析和行业研究。"},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
return self.call_api(messages)
|
||||
|
||||
def fund_flow_analysis(self, stock_info: Dict, indicators: Dict) -> str:
|
||||
"""资金面分析"""
|
||||
prompt = f"""
|
||||
你是一名资深的资金面分析师。请基于以下信息进行专业的资金面分析:
|
||||
|
||||
股票信息:
|
||||
- 股票代码:{stock_info.get('symbol', 'N/A')}
|
||||
- 股票名称:{stock_info.get('name', 'N/A')}
|
||||
- 当前价格:{stock_info.get('current_price', 'N/A')}
|
||||
- 市值:{stock_info.get('market_cap', 'N/A')}
|
||||
|
||||
资金相关指标:
|
||||
- 量比:{indicators.get('volume_ratio', 'N/A')}
|
||||
- 当前成交量与5日均量比:{indicators.get('volume_ratio', 'N/A')}
|
||||
|
||||
请从以下角度进行分析:
|
||||
1. 成交量变化分析
|
||||
2. 资金流入流出趋势
|
||||
3. 大单、中单、小单资金行为
|
||||
4. 主力资金动向分析
|
||||
5. 市场情绪和资金偏好
|
||||
6. 流动性分析
|
||||
7. 资金面对股价的支撑或压力
|
||||
|
||||
请结合当前市场资金环境,给出专业的资金面分析报告。
|
||||
"""
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "你是一名经验丰富的资金面分析师,擅长市场资金流向和主力行为分析。"},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
return self.call_api(messages)
|
||||
|
||||
def comprehensive_discussion(self, technical_report: str, fundamental_report: str,
|
||||
fund_flow_report: str, stock_info: Dict) -> str:
|
||||
"""综合讨论"""
|
||||
prompt = f"""
|
||||
现在需要进行一场投资决策会议,你作为首席分析师,需要综合各位分析师的报告进行讨论。
|
||||
|
||||
股票基本信息:
|
||||
- 股票代码:{stock_info.get('symbol', 'N/A')}
|
||||
- 股票名称:{stock_info.get('name', 'N/A')}
|
||||
- 当前价格:{stock_info.get('current_price', 'N/A')}
|
||||
|
||||
技术面分析报告:
|
||||
{technical_report}
|
||||
|
||||
基本面分析报告:
|
||||
{fundamental_report}
|
||||
|
||||
资金面分析报告:
|
||||
{fund_flow_report}
|
||||
|
||||
请作为首席分析师,综合以上三个维度的分析报告,进行深入讨论:
|
||||
|
||||
1. 各个分析维度的一致性和分歧点
|
||||
2. 不同分析结论的权重考量
|
||||
3. 当前市场环境下的投资逻辑
|
||||
4. 潜在风险和机会识别
|
||||
5. 不同投资周期的考量(短期、中期、长期)
|
||||
6. 市场情绪和预期管理
|
||||
|
||||
请模拟一场专业的投资讨论会议,体现不同观点的碰撞和融合。
|
||||
"""
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "你是一名资深的首席投资分析师,擅长综合不同维度的分析形成投资判断。"},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
return self.call_api(messages, max_tokens=6000)
|
||||
|
||||
def final_decision(self, comprehensive_discussion: str, stock_info: Dict,
|
||||
indicators: Dict) -> Dict[str, Any]:
|
||||
"""最终投资决策"""
|
||||
prompt = f"""
|
||||
基于前期的综合分析讨论,现在需要做出最终的投资决策。
|
||||
|
||||
股票信息:
|
||||
- 股票代码:{stock_info.get('symbol', 'N/A')}
|
||||
- 股票名称:{stock_info.get('name', 'N/A')}
|
||||
- 当前价格:{stock_info.get('current_price', 'N/A')}
|
||||
|
||||
综合分析讨论结果:
|
||||
{comprehensive_discussion}
|
||||
|
||||
当前关键技术位:
|
||||
- MA20:{indicators.get('ma20', 'N/A')}
|
||||
- 布林带上轨:{indicators.get('bb_upper', 'N/A')}
|
||||
- 布林带下轨:{indicators.get('bb_lower', 'N/A')}
|
||||
|
||||
请给出最终投资决策,必须包含以下内容:
|
||||
|
||||
1. 投资评级:买入/持有/卖出
|
||||
2. 目标价位(具体数字)
|
||||
3. 操作建议(具体的买入/卖出策略)
|
||||
4. 进场位置(具体价位区间)
|
||||
5. 止盈位置(具体价位)
|
||||
6. 止损位置(具体价位)
|
||||
7. 持有周期建议
|
||||
8. 风险提示
|
||||
9. 仓位建议(轻仓/中等仓位/重仓)
|
||||
|
||||
请以JSON格式输出决策结果,格式如下:
|
||||
{{
|
||||
"rating": "买入/持有/卖出",
|
||||
"target_price": "目标价位数字",
|
||||
"operation_advice": "具体操作建议",
|
||||
"entry_range": "进场价位区间",
|
||||
"take_profit": "止盈价位",
|
||||
"stop_loss": "止损价位",
|
||||
"holding_period": "持有周期",
|
||||
"position_size": "仓位建议",
|
||||
"risk_warning": "风险提示",
|
||||
"confidence_level": "信心度(1-10分)"
|
||||
}}
|
||||
"""
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "你是一名专业的投资决策专家,需要给出明确、可执行的投资建议。"},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response = self.call_api(messages, temperature=0.3, max_tokens=4000)
|
||||
|
||||
try:
|
||||
# 尝试解析JSON响应
|
||||
import re
|
||||
json_match = re.search(r'\{.*\}', response, re.DOTALL)
|
||||
if json_match:
|
||||
decision_json = json.loads(json_match.group())
|
||||
return decision_json
|
||||
else:
|
||||
# 如果无法解析JSON,返回文本响应
|
||||
return {"decision_text": response}
|
||||
except:
|
||||
return {"decision_text": response}
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
import sqlite3
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
class StockMonitorDatabase:
|
||||
"""股票监测数据库管理类"""
|
||||
|
||||
def __init__(self, db_path: str = "stock_monitor.db"):
|
||||
self.db_path = db_path
|
||||
self.init_database()
|
||||
|
||||
def init_database(self):
|
||||
"""初始化数据库表结构"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建监测股票表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS monitored_stocks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
symbol TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
rating TEXT NOT NULL,
|
||||
entry_range TEXT NOT NULL, -- JSON格式: {"min": 10.0, "max": 12.0}
|
||||
take_profit REAL,
|
||||
stop_loss REAL,
|
||||
current_price REAL,
|
||||
last_checked TIMESTAMP,
|
||||
check_interval INTEGER DEFAULT 30, -- 分钟
|
||||
notification_enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建价格历史表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS price_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
stock_id INTEGER,
|
||||
price REAL NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (stock_id) REFERENCES monitored_stocks (id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建提醒记录表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
stock_id INTEGER,
|
||||
type TEXT NOT NULL, -- entry/take_profit/stop_loss
|
||||
message TEXT NOT NULL,
|
||||
triggered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
sent BOOLEAN DEFAULT FALSE,
|
||||
FOREIGN KEY (stock_id) REFERENCES monitored_stocks (id)
|
||||
)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def add_monitored_stock(self, symbol: str, name: str, rating: str,
|
||||
entry_range: Dict, take_profit: float,
|
||||
stop_loss: float, check_interval: int = 30,
|
||||
notification_enabled: bool = True) -> int:
|
||||
"""添加监测股票"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO monitored_stocks
|
||||
(symbol, name, rating, entry_range, take_profit, stop_loss, check_interval, notification_enabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (symbol, name, rating, json.dumps(entry_range), take_profit, stop_loss, check_interval, notification_enabled))
|
||||
|
||||
stock_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return stock_id
|
||||
|
||||
def get_monitored_stocks(self) -> List[Dict]:
|
||||
"""获取所有监测股票"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT id, symbol, name, rating, entry_range, take_profit, stop_loss,
|
||||
current_price, last_checked, check_interval, notification_enabled,
|
||||
created_at, updated_at
|
||||
FROM monitored_stocks
|
||||
ORDER BY created_at DESC
|
||||
''')
|
||||
|
||||
stocks = []
|
||||
for row in cursor.fetchall():
|
||||
stocks.append({
|
||||
'id': row[0],
|
||||
'symbol': row[1],
|
||||
'name': row[2],
|
||||
'rating': row[3],
|
||||
'entry_range': json.loads(row[4]),
|
||||
'take_profit': row[5],
|
||||
'stop_loss': row[6],
|
||||
'current_price': row[7],
|
||||
'last_checked': row[8],
|
||||
'check_interval': row[9],
|
||||
'notification_enabled': bool(row[10]),
|
||||
'created_at': row[11],
|
||||
'updated_at': row[12]
|
||||
})
|
||||
|
||||
conn.close()
|
||||
return stocks
|
||||
|
||||
def update_stock_price(self, stock_id: int, price: float):
|
||||
"""更新股票价格"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 更新当前价格
|
||||
cursor.execute('''
|
||||
UPDATE monitored_stocks
|
||||
SET current_price = ?, last_checked = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
''', (price, stock_id))
|
||||
|
||||
# 记录价格历史
|
||||
cursor.execute('''
|
||||
INSERT INTO price_history (stock_id, price)
|
||||
VALUES (?, ?)
|
||||
''', (stock_id, price))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def add_notification(self, stock_id: int, notification_type: str, message: str):
|
||||
"""添加提醒记录"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO notifications (stock_id, type, message)
|
||||
VALUES (?, ?, ?)
|
||||
''', (stock_id, notification_type, message))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_pending_notifications(self) -> List[Dict]:
|
||||
"""获取待发送的提醒"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT n.id, n.stock_id, s.symbol, s.name, n.type, n.message, n.triggered_at
|
||||
FROM notifications n
|
||||
JOIN monitored_stocks s ON n.stock_id = s.id
|
||||
WHERE n.sent = FALSE
|
||||
ORDER BY n.triggered_at
|
||||
''')
|
||||
|
||||
notifications = []
|
||||
for row in cursor.fetchall():
|
||||
notifications.append({
|
||||
'id': row[0],
|
||||
'stock_id': row[1],
|
||||
'symbol': row[2],
|
||||
'name': row[3],
|
||||
'type': row[4],
|
||||
'message': row[5],
|
||||
'triggered_at': row[6]
|
||||
})
|
||||
|
||||
conn.close()
|
||||
return notifications
|
||||
|
||||
def mark_notification_sent(self, notification_id: int):
|
||||
"""标记提醒已发送"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE notifications SET sent = TRUE WHERE id = ?
|
||||
''', (notification_id,))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def mark_all_notifications_sent(self):
|
||||
"""标记所有通知为已读"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('UPDATE notifications SET sent = TRUE WHERE sent = FALSE')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return cursor.rowcount
|
||||
|
||||
def clear_all_notifications(self):
|
||||
"""清空所有通知"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('DELETE FROM notifications')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return cursor.rowcount
|
||||
|
||||
def remove_monitored_stock(self, stock_id: int):
|
||||
"""移除监测股票"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 删除相关记录
|
||||
cursor.execute('DELETE FROM price_history WHERE stock_id = ?', (stock_id,))
|
||||
cursor.execute('DELETE FROM notifications WHERE stock_id = ?', (stock_id,))
|
||||
cursor.execute('DELETE FROM monitored_stocks WHERE id = ?', (stock_id,))
|
||||
|
||||
affected_rows = cursor.rowcount
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return affected_rows > 0
|
||||
except Exception as e:
|
||||
print(f"删除股票失败: {e}")
|
||||
return False
|
||||
|
||||
def update_monitored_stock(self, stock_id: int, rating: str, entry_range: Dict,
|
||||
take_profit: float, stop_loss: float,
|
||||
check_interval: int, notification_enabled: bool):
|
||||
"""更新监测股票"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE monitored_stocks
|
||||
SET rating = ?, entry_range = ?, take_profit = ?, stop_loss = ?,
|
||||
check_interval = ?, notification_enabled = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
''', (rating, json.dumps(entry_range), take_profit, stop_loss, check_interval, notification_enabled, stock_id))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def toggle_notification(self, stock_id: int, enabled: bool):
|
||||
"""切换通知状态"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE monitored_stocks
|
||||
SET notification_enabled = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
''', (enabled, stock_id))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def get_stock_by_id(self, stock_id: int) -> Optional[Dict]:
|
||||
"""根据ID获取股票信息"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT id, symbol, name, rating, entry_range, take_profit, stop_loss,
|
||||
current_price, last_checked, check_interval, notification_enabled
|
||||
FROM monitored_stocks WHERE id = ?
|
||||
''', (stock_id,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if row:
|
||||
return {
|
||||
'id': row[0],
|
||||
'symbol': row[1],
|
||||
'name': row[2],
|
||||
'rating': row[3],
|
||||
'entry_range': json.loads(row[4]),
|
||||
'take_profit': row[5],
|
||||
'stop_loss': row[6],
|
||||
'current_price': row[7],
|
||||
'last_checked': row[8],
|
||||
'check_interval': row[9],
|
||||
'notification_enabled': bool(row[10])
|
||||
}
|
||||
return None
|
||||
|
||||
# 全局数据库实例
|
||||
monitor_db = StockMonitorDatabase()
|
||||
@@ -0,0 +1,552 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
股票监测管理模块
|
||||
支持添加、删除、编辑监测股票
|
||||
卡片式布局,支持关键位置监测
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List
|
||||
import json
|
||||
|
||||
from monitor_db import monitor_db
|
||||
from monitor_service import monitor_service
|
||||
from notification_service import notification_service
|
||||
from stock_data import StockDataFetcher
|
||||
|
||||
def display_monitor_manager():
|
||||
"""显示监测管理主页面"""
|
||||
|
||||
st.markdown("## 📊 股票监测管理")
|
||||
st.markdown("---")
|
||||
|
||||
# 监测服务状态
|
||||
display_monitor_status()
|
||||
|
||||
# 添加新股票监测
|
||||
display_add_stock_section()
|
||||
|
||||
# 监测股票列表
|
||||
display_monitored_stocks()
|
||||
|
||||
# 通知管理
|
||||
display_notification_management()
|
||||
|
||||
def display_monitor_status():
|
||||
"""显示监测服务状态"""
|
||||
|
||||
col1, col2, col3, col4, col5 = st.columns(5)
|
||||
|
||||
with col1:
|
||||
if monitor_service.running:
|
||||
st.success("🟢 运行中")
|
||||
else:
|
||||
st.error("🔴 已停止")
|
||||
|
||||
with col2:
|
||||
stocks = monitor_db.get_monitored_stocks()
|
||||
st.metric("监测股票", len(stocks))
|
||||
|
||||
with col3:
|
||||
notifications = monitor_db.get_pending_notifications()
|
||||
st.metric("待处理通知", len(notifications))
|
||||
|
||||
with col4:
|
||||
if monitor_service.running:
|
||||
if st.button("⏹️ 停止监测", type="secondary"):
|
||||
monitor_service.stop_monitoring()
|
||||
st.success("✅ 监测服务已停止")
|
||||
st.rerun()
|
||||
else:
|
||||
if st.button("▶️ 启动监测", type="primary"):
|
||||
monitor_service.start_monitoring()
|
||||
st.success("✅ 监测服务已启动")
|
||||
st.rerun()
|
||||
|
||||
with col5:
|
||||
if st.button("🔄 刷新状态"):
|
||||
st.rerun()
|
||||
|
||||
def display_add_stock_section():
|
||||
"""显示添加股票监测区域"""
|
||||
|
||||
st.markdown("### ➕ 添加股票监测")
|
||||
|
||||
with st.expander("点击展开添加股票监测", expanded=False):
|
||||
col1, col2 = st.columns([1, 1])
|
||||
|
||||
with col1:
|
||||
# 股票信息输入
|
||||
st.subheader("📈 股票信息")
|
||||
symbol = st.text_input("股票代码", placeholder="例如: AAPL, 000001", help="支持美股和A股代码")
|
||||
name = st.text_input("股票名称", placeholder="例如: 苹果公司", help="可选,用于显示")
|
||||
|
||||
# 获取股票基本信息
|
||||
if symbol:
|
||||
if st.button("🔍 获取股票信息"):
|
||||
with st.spinner("正在获取股票信息..."):
|
||||
fetcher = StockDataFetcher()
|
||||
stock_info = fetcher.get_stock_info(symbol)
|
||||
|
||||
if "error" not in stock_info:
|
||||
st.success("✅ 股票信息获取成功")
|
||||
st.session_state.temp_stock_info = stock_info
|
||||
else:
|
||||
st.error(f"❌ {stock_info['error']}")
|
||||
|
||||
with col2:
|
||||
# 监测设置
|
||||
st.subheader("⚙️ 监测设置")
|
||||
|
||||
# 关键位置设置
|
||||
st.markdown("**🎯 关键位置设置**")
|
||||
entry_min = st.number_input("进场区间最低价", value=0.0, step=0.01, format="%.2f")
|
||||
entry_max = st.number_input("进场区间最高价", value=0.0, step=0.01, format="%.2f")
|
||||
take_profit = st.number_input("止盈价位", value=0.0, step=0.01, format="%.2f", help="可选")
|
||||
stop_loss = st.number_input("止损价位", value=0.0, step=0.01, format="%.2f", help="可选")
|
||||
|
||||
# 监测参数
|
||||
st.markdown("**⏰ 监测参数**")
|
||||
check_interval = st.slider("监测间隔(分钟)", 5, 120, 30)
|
||||
notification_enabled = st.checkbox("启用通知", value=True)
|
||||
|
||||
# 投资评级
|
||||
rating = st.selectbox("投资评级", ["买入", "持有", "卖出"], index=0)
|
||||
|
||||
# 添加按钮
|
||||
if st.button("✅ 添加监测", type="primary", use_container_width=True):
|
||||
if symbol and entry_min > 0 and entry_max > 0 and entry_max > entry_min:
|
||||
try:
|
||||
# 准备数据
|
||||
entry_range = {"min": entry_min, "max": entry_max}
|
||||
|
||||
# 添加到数据库
|
||||
stock_id = monitor_db.add_monitored_stock(
|
||||
symbol=symbol,
|
||||
name=name or symbol,
|
||||
rating=rating,
|
||||
entry_range=entry_range,
|
||||
take_profit=take_profit if take_profit > 0 else None,
|
||||
stop_loss=stop_loss if stop_loss > 0 else None,
|
||||
check_interval=check_interval,
|
||||
notification_enabled=notification_enabled
|
||||
)
|
||||
|
||||
st.success(f"✅ 已成功添加 {symbol} 到监测列表")
|
||||
st.balloons()
|
||||
|
||||
# 立即更新一次价格
|
||||
monitor_service.manual_update_stock(stock_id)
|
||||
|
||||
# 清空表单
|
||||
st.rerun()
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"❌ 添加失败: {str(e)}")
|
||||
else:
|
||||
st.error("❌ 请填写完整的股票信息和有效的进场区间")
|
||||
|
||||
def display_monitored_stocks():
|
||||
"""显示监测股票列表 - 卡片式布局"""
|
||||
|
||||
st.markdown("### 📋 监测股票列表")
|
||||
|
||||
stocks = monitor_db.get_monitored_stocks()
|
||||
|
||||
if not stocks:
|
||||
st.info("📭 暂无监测股票,请添加股票开始监测")
|
||||
return
|
||||
|
||||
# 筛选和搜索
|
||||
col1, col2, col3 = st.columns([2, 1, 1])
|
||||
|
||||
with col1:
|
||||
search_term = st.text_input("🔍 搜索股票", placeholder="输入股票代码或名称")
|
||||
|
||||
with col2:
|
||||
rating_filter = st.selectbox("评级筛选", ["全部", "买入", "持有", "卖出"])
|
||||
|
||||
with col3:
|
||||
if st.button("🔄 刷新列表"):
|
||||
st.rerun()
|
||||
|
||||
# 筛选股票
|
||||
filtered_stocks = stocks
|
||||
if search_term:
|
||||
filtered_stocks = [s for s in stocks if search_term.lower() in s['symbol'].lower() or search_term.lower() in s['name'].lower()]
|
||||
|
||||
if rating_filter != "全部":
|
||||
filtered_stocks = [s for s in filtered_stocks if s['rating'] == rating_filter]
|
||||
|
||||
if not filtered_stocks:
|
||||
st.warning("🔍 未找到匹配的股票")
|
||||
return
|
||||
|
||||
# 卡片式布局 - 每行显示2个卡片
|
||||
for i in range(0, len(filtered_stocks), 2):
|
||||
cols = st.columns(2)
|
||||
|
||||
for j, col in enumerate(cols):
|
||||
if i + j < len(filtered_stocks):
|
||||
stock = filtered_stocks[i + j]
|
||||
with col:
|
||||
display_stock_card(stock)
|
||||
|
||||
# 显示编辑对话框
|
||||
if 'editing_stock_id' in st.session_state:
|
||||
display_edit_dialog(st.session_state.editing_stock_id)
|
||||
|
||||
# 显示删除确认对话框
|
||||
if 'deleting_stock_id' in st.session_state:
|
||||
display_delete_confirm_dialog(st.session_state.deleting_stock_id)
|
||||
|
||||
def display_stock_card(stock: Dict):
|
||||
"""显示单个股票监测卡片"""
|
||||
|
||||
with st.container():
|
||||
# 卡片头部
|
||||
st.markdown(f"""
|
||||
<div style="
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
">
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# 股票基本信息
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown(f"**{stock['symbol']}** - {stock['name']}")
|
||||
|
||||
# 评级显示
|
||||
rating_color = {
|
||||
'买入': '🟢',
|
||||
'持有': '🟡',
|
||||
'卖出': '🔴'
|
||||
}
|
||||
st.markdown(f"评级: {rating_color.get(stock['rating'], '⚪')} {stock['rating']}")
|
||||
|
||||
with col2:
|
||||
if stock['current_price'] and stock['current_price'] != 'N/A':
|
||||
st.metric("当前价格", f"¥{stock['current_price']}")
|
||||
else:
|
||||
st.metric("当前价格", "等待更新")
|
||||
|
||||
# 关键位置信息
|
||||
st.markdown("**🎯 关键位置**")
|
||||
|
||||
entry_range = stock['entry_range']
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
st.info(f"**进场区间**\n¥{entry_range['min']} - ¥{entry_range['max']}")
|
||||
|
||||
with col2:
|
||||
if stock['take_profit']:
|
||||
st.success(f"**止盈位**\n¥{stock['take_profit']}")
|
||||
else:
|
||||
st.info("**止盈位**\n未设置")
|
||||
|
||||
with col3:
|
||||
if stock['stop_loss']:
|
||||
st.error(f"**止损位**\n¥{stock['stop_loss']}")
|
||||
else:
|
||||
st.info("**止损位**\n未设置")
|
||||
|
||||
# 监测状态
|
||||
st.markdown("**📊 监测状态**")
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
st.caption(f"监测间隔: {stock['check_interval']}分钟")
|
||||
|
||||
with col2:
|
||||
if stock['last_checked']:
|
||||
last_checked = datetime.fromisoformat(stock['last_checked'])
|
||||
st.caption(f"最后检查: {last_checked.strftime('%m-%d %H:%M')}")
|
||||
else:
|
||||
st.caption("最后检查: 从未检查")
|
||||
|
||||
with col3:
|
||||
status = "🟢 启用" if stock['notification_enabled'] else "🔴 禁用"
|
||||
st.caption(f"通知: {status}")
|
||||
|
||||
# 操作按钮
|
||||
st.markdown("**🔧 操作**")
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
if st.button("🔄 更新", key=f"update_{stock['id']}"):
|
||||
if monitor_service.manual_update_stock(stock['id']):
|
||||
st.success("✅ 更新成功")
|
||||
else:
|
||||
st.error("❌ 更新失败")
|
||||
|
||||
with col2:
|
||||
if st.button("✏️ 编辑", key=f"edit_{stock['id']}"):
|
||||
st.session_state.editing_stock_id = stock['id']
|
||||
st.rerun()
|
||||
|
||||
with col3:
|
||||
# 切换通知状态
|
||||
current_status = stock['notification_enabled']
|
||||
if current_status:
|
||||
if st.button("🔕 禁用", key=f"notify_{stock['id']}"):
|
||||
monitor_db.toggle_notification(stock['id'], False)
|
||||
st.success("✅ 已禁用通知")
|
||||
st.rerun()
|
||||
else:
|
||||
if st.button("🔔 启用", key=f"notify_{stock['id']}"):
|
||||
monitor_db.toggle_notification(stock['id'], True)
|
||||
st.success("✅ 已启用通知")
|
||||
st.rerun()
|
||||
|
||||
with col4:
|
||||
if st.button("🗑️ 删除", key=f"delete_{stock['id']}"):
|
||||
st.session_state.deleting_stock_id = stock['id']
|
||||
st.rerun()
|
||||
|
||||
st.markdown("</div>", unsafe_allow_html=True)
|
||||
|
||||
def display_edit_dialog(stock_id: int):
|
||||
"""显示编辑股票对话框"""
|
||||
|
||||
stock = monitor_db.get_stock_by_id(stock_id)
|
||||
if not stock:
|
||||
st.error("❌ 股票不存在")
|
||||
del st.session_state.editing_stock_id
|
||||
return
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown(f"### ✏️ 编辑监测 - {stock['symbol']} {stock['name']}")
|
||||
|
||||
with st.form(key=f"edit_form_{stock_id}"):
|
||||
col1, col2 = st.columns([1, 1])
|
||||
|
||||
with col1:
|
||||
st.subheader("🎯 关键位置")
|
||||
entry_range = stock['entry_range']
|
||||
entry_min = st.number_input("进场区间最低价", value=float(entry_range['min']), step=0.01, format="%.2f")
|
||||
entry_max = st.number_input("进场区间最高价", value=float(entry_range['max']), step=0.01, format="%.2f")
|
||||
take_profit = st.number_input("止盈价位", value=float(stock['take_profit']) if stock['take_profit'] else 0.0, step=0.01, format="%.2f")
|
||||
stop_loss = st.number_input("止损价位", value=float(stock['stop_loss']) if stock['stop_loss'] else 0.0, step=0.01, format="%.2f")
|
||||
|
||||
with col2:
|
||||
st.subheader("⚙️ 监测设置")
|
||||
check_interval = st.slider("监测间隔(分钟)", 5, 120, stock['check_interval'])
|
||||
rating = st.selectbox("投资评级", ["买入", "持有", "卖出"],
|
||||
index=["买入", "持有", "卖出"].index(stock['rating']) if stock['rating'] in ["买入", "持有", "卖出"] else 0)
|
||||
notification_enabled = st.checkbox("启用通知", value=stock['notification_enabled'])
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
submit = st.form_submit_button("✅ 保存修改", type="primary", use_container_width=True)
|
||||
|
||||
with col2:
|
||||
cancel = st.form_submit_button("❌ 取消", use_container_width=True)
|
||||
|
||||
if submit:
|
||||
if entry_min > 0 and entry_max > 0 and entry_max > entry_min:
|
||||
try:
|
||||
# 更新数据库
|
||||
new_entry_range = {"min": entry_min, "max": entry_max}
|
||||
monitor_db.update_monitored_stock(
|
||||
stock_id=stock_id,
|
||||
rating=rating,
|
||||
entry_range=new_entry_range,
|
||||
take_profit=take_profit if take_profit > 0 else None,
|
||||
stop_loss=stop_loss if stop_loss > 0 else None,
|
||||
check_interval=check_interval,
|
||||
notification_enabled=notification_enabled
|
||||
)
|
||||
|
||||
st.success("✅ 修改已保存")
|
||||
del st.session_state.editing_stock_id
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
st.error(f"❌ 保存失败: {str(e)}")
|
||||
else:
|
||||
st.error("❌ 请输入有效的进场区间")
|
||||
|
||||
if cancel:
|
||||
del st.session_state.editing_stock_id
|
||||
st.rerun()
|
||||
|
||||
def display_delete_confirm_dialog(stock_id: int):
|
||||
"""显示删除确认对话框"""
|
||||
|
||||
stock = monitor_db.get_stock_by_id(stock_id)
|
||||
if not stock:
|
||||
st.error("❌ 股票不存在或已被删除")
|
||||
if 'deleting_stock_id' in st.session_state:
|
||||
del st.session_state.deleting_stock_id
|
||||
st.rerun()
|
||||
return
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown(f"### ⚠️ 确认删除")
|
||||
|
||||
st.warning(f"""
|
||||
您确定要删除以下监测吗?
|
||||
|
||||
**股票代码**: {stock['symbol']}
|
||||
|
||||
**股票名称**: {stock['name']}
|
||||
|
||||
**投资评级**: {stock['rating']}
|
||||
|
||||
此操作不可撤销!
|
||||
""")
|
||||
|
||||
col1, col2, col3 = st.columns([1, 1, 1])
|
||||
|
||||
with col1:
|
||||
if st.button("🗑️ 确认删除", type="primary", use_container_width=True, key=f"confirm_delete_{stock_id}"):
|
||||
try:
|
||||
result = monitor_db.remove_monitored_stock(stock_id)
|
||||
if result:
|
||||
# 清理session state
|
||||
if 'deleting_stock_id' in st.session_state:
|
||||
del st.session_state.deleting_stock_id
|
||||
|
||||
st.success("✅ 已成功删除监测")
|
||||
st.balloons()
|
||||
time.sleep(0.8) # 短暂延迟,让用户看到成功消息
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("❌ 删除失败:股票不存在或已被删除")
|
||||
time.sleep(1)
|
||||
if 'deleting_stock_id' in st.session_state:
|
||||
del st.session_state.deleting_stock_id
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
st.error(f"❌ 删除失败:{str(e)}")
|
||||
time.sleep(1)
|
||||
if 'deleting_stock_id' in st.session_state:
|
||||
del st.session_state.deleting_stock_id
|
||||
st.rerun()
|
||||
|
||||
with col2:
|
||||
if st.button("❌ 取消", use_container_width=True, key=f"cancel_delete_{stock_id}"):
|
||||
del st.session_state.deleting_stock_id
|
||||
st.rerun()
|
||||
|
||||
def display_notification_management():
|
||||
"""显示通知管理"""
|
||||
|
||||
st.markdown("### 🔔 通知管理")
|
||||
|
||||
# 通知设置
|
||||
col1, col2 = st.columns([1, 1])
|
||||
|
||||
with col1:
|
||||
st.subheader("📧 邮件通知设置")
|
||||
|
||||
# 获取当前邮件配置状态
|
||||
email_config = notification_service.get_email_config_status()
|
||||
|
||||
# 显示配置状态
|
||||
if email_config['configured']:
|
||||
st.success("✅ 邮件配置已完成")
|
||||
else:
|
||||
st.warning("⚠️ 邮件未配置或配置不完整")
|
||||
|
||||
# 显示配置信息
|
||||
st.info(f"""
|
||||
**当前配置:**
|
||||
- SMTP服务器: {email_config['smtp_server']}
|
||||
- SMTP端口: {email_config['smtp_port']}
|
||||
- 发送邮箱: {email_config['email_from']}
|
||||
- 接收邮箱: {email_config['email_to']}
|
||||
- 启用状态: {'是' if email_config['enabled'] else '否'}
|
||||
""")
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown("**⚙️ 配置说明**")
|
||||
st.caption("""
|
||||
在 `.env` 文件中配置以下参数:
|
||||
```
|
||||
EMAIL_ENABLED=true
|
||||
SMTP_SERVER=smtp.qq.com
|
||||
SMTP_PORT=587
|
||||
EMAIL_FROM=your_email@qq.com
|
||||
EMAIL_PASSWORD=your_authorization_code
|
||||
EMAIL_TO=receiver@example.com
|
||||
```
|
||||
|
||||
💡 提示:
|
||||
- 端口:587 (TLS) 或 465 (SSL)
|
||||
- 密码:使用邮箱授权码,不是登录密码
|
||||
- QQ邮箱授权码获取:设置 → 账户 → POP3/IMAP/SMTP → 生成授权码
|
||||
""")
|
||||
|
||||
# 测试邮件按钮
|
||||
if email_config['configured']:
|
||||
if st.button("📧 发送测试邮件", type="primary", use_container_width=True):
|
||||
with st.spinner("正在发送测试邮件..."):
|
||||
success, message = notification_service.send_test_email()
|
||||
if success:
|
||||
st.success(f"✅ {message}")
|
||||
st.balloons()
|
||||
else:
|
||||
st.error(f"❌ {message}")
|
||||
else:
|
||||
st.button("📧 发送测试邮件", type="primary", use_container_width=True, disabled=True)
|
||||
st.caption("请先在.env文件中配置邮件参数")
|
||||
|
||||
with col2:
|
||||
st.subheader("📱 通知历史")
|
||||
|
||||
notifications = monitor_db.get_pending_notifications()
|
||||
|
||||
if notifications:
|
||||
# 显示通知列表
|
||||
for notification in notifications[-10:]: # 显示最近10条
|
||||
notification_type = notification['type']
|
||||
color_map = {
|
||||
'entry': '🟢',
|
||||
'take_profit': '🟡',
|
||||
'stop_loss': '🔴'
|
||||
}
|
||||
icon = color_map.get(notification_type, '🔵')
|
||||
|
||||
# 显示通知信息
|
||||
st.info(f"{icon} **{notification['symbol']}** - {notification['message']}\n\n_{notification['triggered_at']}_")
|
||||
|
||||
# 清空通知按钮
|
||||
col_a, col_b = st.columns(2)
|
||||
with col_a:
|
||||
if st.button("✅ 标记已读"):
|
||||
monitor_db.mark_all_notifications_sent()
|
||||
st.success("✅ 所有通知已标记为已读")
|
||||
st.rerun()
|
||||
|
||||
with col_b:
|
||||
if st.button("🗑️ 清空通知"):
|
||||
monitor_db.clear_all_notifications()
|
||||
st.success("✅ 通知已清空")
|
||||
st.rerun()
|
||||
else:
|
||||
st.info("📭 暂无通知")
|
||||
|
||||
def get_monitor_summary():
|
||||
"""获取监测摘要信息"""
|
||||
stocks = monitor_db.get_monitored_stocks()
|
||||
|
||||
summary = {
|
||||
'total_stocks': len(stocks),
|
||||
'stocks_needing_update': len(monitor_service.get_stocks_needing_update()),
|
||||
'pending_notifications': len(monitor_db.get_pending_notifications()),
|
||||
'active_monitoring': monitor_service.running
|
||||
}
|
||||
|
||||
return summary
|
||||
@@ -0,0 +1,148 @@
|
||||
import time
|
||||
import threading
|
||||
import schedule
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List
|
||||
import streamlit as st
|
||||
|
||||
from monitor_db import monitor_db
|
||||
from stock_data import StockDataFetcher
|
||||
|
||||
class StockMonitorService:
|
||||
"""股票监测服务"""
|
||||
|
||||
def __init__(self):
|
||||
self.fetcher = StockDataFetcher()
|
||||
self.running = False
|
||||
self.thread = None
|
||||
|
||||
def start_monitoring(self):
|
||||
"""启动监测服务"""
|
||||
if self.running:
|
||||
return
|
||||
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
||||
self.thread.start()
|
||||
st.success("✅ 监测服务已启动")
|
||||
|
||||
def stop_monitoring(self):
|
||||
"""停止监测服务"""
|
||||
self.running = False
|
||||
if self.thread:
|
||||
self.thread.join(timeout=5)
|
||||
st.info("⏹️ 监测服务已停止")
|
||||
|
||||
def _monitor_loop(self):
|
||||
"""监测循环"""
|
||||
while self.running:
|
||||
try:
|
||||
self._check_all_stocks()
|
||||
time.sleep(60) # 每分钟检查一次
|
||||
except Exception as e:
|
||||
print(f"监测服务错误: {e}")
|
||||
time.sleep(10)
|
||||
|
||||
def _check_all_stocks(self):
|
||||
"""检查所有监测股票"""
|
||||
stocks = monitor_db.get_monitored_stocks()
|
||||
current_time = datetime.now()
|
||||
|
||||
for stock in stocks:
|
||||
# 检查是否需要更新价格
|
||||
last_checked = stock.get('last_checked')
|
||||
check_interval = stock.get('check_interval', 30)
|
||||
|
||||
if last_checked:
|
||||
last_checked_dt = datetime.fromisoformat(last_checked)
|
||||
next_check = last_checked_dt + timedelta(minutes=check_interval)
|
||||
if current_time < next_check:
|
||||
continue
|
||||
|
||||
try:
|
||||
self._update_stock_price(stock)
|
||||
except Exception as e:
|
||||
print(f"更新股票 {stock['symbol']} 价格失败: {e}")
|
||||
|
||||
def _update_stock_price(self, stock: Dict):
|
||||
"""更新股票价格并检查条件"""
|
||||
symbol = stock['symbol']
|
||||
|
||||
# 获取最新价格
|
||||
try:
|
||||
# 使用get_stock_info获取当前价格
|
||||
stock_info = self.fetcher.get_stock_info(symbol)
|
||||
current_price = stock_info.get('current_price')
|
||||
|
||||
if current_price and current_price != 'N/A':
|
||||
try:
|
||||
current_price = float(current_price)
|
||||
# 更新数据库
|
||||
monitor_db.update_stock_price(stock['id'], current_price)
|
||||
|
||||
# 检查触发条件
|
||||
self._check_trigger_conditions(stock, current_price)
|
||||
except (ValueError, TypeError):
|
||||
print(f"股票 {symbol} 价格格式错误: {current_price}")
|
||||
else:
|
||||
print(f"无法获取股票 {symbol} 的当前价格")
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取股票 {symbol} 数据失败: {e}")
|
||||
|
||||
def _check_trigger_conditions(self, stock: Dict, current_price: float):
|
||||
"""检查触发条件"""
|
||||
if not stock.get('notification_enabled', True):
|
||||
return
|
||||
|
||||
entry_range = stock.get('entry_range', {})
|
||||
take_profit = stock.get('take_profit')
|
||||
stop_loss = stock.get('stop_loss')
|
||||
|
||||
# 检查进场区间
|
||||
if entry_range and entry_range.get('min') and entry_range.get('max'):
|
||||
if current_price >= entry_range['min'] and current_price <= entry_range['max']:
|
||||
message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 进入进场区间 [{entry_range['min']}-{entry_range['max']}]"
|
||||
monitor_db.add_notification(stock['id'], 'entry', message)
|
||||
|
||||
# 检查止盈
|
||||
if take_profit and current_price >= take_profit:
|
||||
message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 达到止盈位 {take_profit}"
|
||||
monitor_db.add_notification(stock['id'], 'take_profit', message)
|
||||
|
||||
# 检查止损
|
||||
if stop_loss and current_price <= stop_loss:
|
||||
message = f"股票 {stock['symbol']} ({stock['name']}) 价格 {current_price} 达到止损位 {stop_loss}"
|
||||
monitor_db.add_notification(stock['id'], 'stop_loss', message)
|
||||
|
||||
def get_stocks_needing_update(self) -> List[Dict]:
|
||||
"""获取需要更新价格的股票"""
|
||||
stocks = monitor_db.get_monitored_stocks()
|
||||
current_time = datetime.now()
|
||||
need_update = []
|
||||
|
||||
for stock in stocks:
|
||||
last_checked = stock.get('last_checked')
|
||||
check_interval = stock.get('check_interval', 30)
|
||||
|
||||
if not last_checked:
|
||||
need_update.append(stock)
|
||||
continue
|
||||
|
||||
last_checked_dt = datetime.fromisoformat(last_checked)
|
||||
next_check = last_checked_dt + timedelta(minutes=check_interval)
|
||||
if current_time >= next_check:
|
||||
need_update.append(stock)
|
||||
|
||||
return need_update
|
||||
|
||||
def manual_update_stock(self, stock_id: int):
|
||||
"""手动更新股票价格"""
|
||||
stock = monitor_db.get_stock_by_id(stock_id)
|
||||
if stock:
|
||||
self._update_stock_price(stock)
|
||||
return True
|
||||
return False
|
||||
|
||||
# 全局监测服务实例
|
||||
monitor_service = StockMonitorService()
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
import streamlit as st
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
|
||||
from monitor_db import monitor_db
|
||||
from monitor_service import monitor_service
|
||||
from notification_service import notification_service
|
||||
|
||||
def display_monitor_panel():
|
||||
"""显示监测面板"""
|
||||
|
||||
st.markdown("## 📊 实时监测面板")
|
||||
|
||||
# 监测服务控制
|
||||
col1, col2, col3 = st.columns([1, 1, 1])
|
||||
|
||||
with col1:
|
||||
if st.button("▶️ 启动监测服务", type="primary"):
|
||||
monitor_service.start_monitoring()
|
||||
|
||||
with col2:
|
||||
if st.button("⏹️ 停止监测服务"):
|
||||
monitor_service.stop_monitoring()
|
||||
|
||||
with col3:
|
||||
if st.button("🔄 手动更新所有"):
|
||||
stocks = monitor_service.get_stocks_needing_update()
|
||||
for stock in stocks:
|
||||
monitor_service.manual_update_stock(stock['id'])
|
||||
st.success(f"✅ 已手动更新 {len(stocks)} 只股票")
|
||||
|
||||
# 显示通知
|
||||
display_notifications()
|
||||
|
||||
# 显示监测股票
|
||||
display_monitored_stocks()
|
||||
|
||||
def display_notifications():
|
||||
"""显示通知"""
|
||||
notifications = notification_service.get_streamlit_notifications()
|
||||
|
||||
if notifications:
|
||||
st.markdown("### 🔔 最新提醒")
|
||||
|
||||
for notification in notifications[-5:]: # 只显示最近5条
|
||||
notification_type = notification['type']
|
||||
color_map = {
|
||||
'entry': '🟢',
|
||||
'take_profit': '🟡',
|
||||
'stop_loss': '🔴'
|
||||
}
|
||||
icon = color_map.get(notification_type, '🔵')
|
||||
|
||||
st.info(f"{icon} **{notification['symbol']}** - {notification['message']}")
|
||||
|
||||
if st.button("清空提醒"):
|
||||
notification_service.clear_streamlit_notifications()
|
||||
st.rerun()
|
||||
|
||||
def display_monitored_stocks():
|
||||
"""显示监测股票卡片"""
|
||||
stocks = monitor_db.get_monitored_stocks()
|
||||
|
||||
if not stocks:
|
||||
st.info("📋 暂无监测股票,请在分析完成后点击'加入监测'按钮添加")
|
||||
return
|
||||
|
||||
st.markdown(f"### 📈 监测中 ({len(stocks)} 只)")
|
||||
|
||||
# 每行显示3个卡片
|
||||
cols = st.columns(3)
|
||||
|
||||
for i, stock in enumerate(stocks):
|
||||
col_idx = i % 3
|
||||
with cols[col_idx]:
|
||||
display_stock_card(stock)
|
||||
|
||||
def display_stock_card(stock: Dict):
|
||||
"""显示单个股票监测卡片"""
|
||||
|
||||
with st.container():
|
||||
st.markdown(f"### {stock['symbol']} - {stock['name']}")
|
||||
|
||||
# 评级和状态
|
||||
col1, col2 = st.columns([1, 1])
|
||||
with col1:
|
||||
rating_color = {
|
||||
'买入': '🟢',
|
||||
'持有': '🟡',
|
||||
'卖出': '🔴'
|
||||
}
|
||||
st.metric("评级", f"{rating_color.get(stock['rating'], '⚪')} {stock['rating']}")
|
||||
|
||||
with col2:
|
||||
if stock['current_price'] and stock['current_price'] != 'N/A':
|
||||
st.metric("当前价格", f"¥{stock['current_price']}")
|
||||
else:
|
||||
st.metric("当前价格", "等待更新")
|
||||
|
||||
# 关键价位
|
||||
entry_range = stock['entry_range']
|
||||
st.info(f"**进场区间**: ¥{entry_range['min']} - ¥{entry_range['max']}")
|
||||
|
||||
if stock['take_profit']:
|
||||
st.success(f"**止盈位**: ¥{stock['take_profit']}")
|
||||
|
||||
if stock['stop_loss']:
|
||||
st.error(f"**止损位**: ¥{stock['stop_loss']}")
|
||||
|
||||
# 最后更新时间
|
||||
if stock['last_checked']:
|
||||
last_checked = datetime.fromisoformat(stock['last_checked'])
|
||||
st.caption(f"最后更新: {last_checked.strftime('%m-%d %H:%M')}")
|
||||
|
||||
# 操作按钮
|
||||
col1, col2 = st.columns([1, 1])
|
||||
with col1:
|
||||
if st.button("🔄 更新", key=f"update_{stock['id']}"):
|
||||
if monitor_service.manual_update_stock(stock['id']):
|
||||
st.success("✅ 更新成功")
|
||||
else:
|
||||
st.error("❌ 更新失败")
|
||||
|
||||
with col2:
|
||||
if st.button("🗑️ 移除", key=f"remove_{stock['id']}"):
|
||||
monitor_db.remove_monitored_stock(stock['id'])
|
||||
st.success("✅ 已移除监测")
|
||||
st.rerun()
|
||||
|
||||
def add_to_monitor_dialog(stock_info: Dict, analysis_result: Dict):
|
||||
"""显示添加到监测的对话框"""
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown("## 📈 添加到实时监测")
|
||||
|
||||
# 从分析结果中提取关键数据
|
||||
final_decision = analysis_result.get('final_decision', {})
|
||||
rating = final_decision.get('rating', '持有')
|
||||
reasoning = final_decision.get('reasoning', '')
|
||||
|
||||
# 生成唯一的session标识符
|
||||
import uuid
|
||||
import time
|
||||
session_id = f"{stock_info.get('symbol', 'unknown')}_{int(time.time())}_{uuid.uuid4().hex[:6]}"
|
||||
|
||||
# 解析关键价位(从分析结果中提取或手动输入)
|
||||
col1, col2 = st.columns([1, 1])
|
||||
|
||||
with col1:
|
||||
# 进场区间
|
||||
st.subheader("🎯 进场区间")
|
||||
entry_min = st.number_input("最低进场价", value=0.0, key=f"entry_min_{session_id}")
|
||||
entry_max = st.number_input("最高进场价", value=0.0, key=f"entry_max_{session_id}")
|
||||
|
||||
if entry_min > 0 and entry_max > 0 and entry_max > entry_min:
|
||||
entry_range = {"min": entry_min, "max": entry_max}
|
||||
else:
|
||||
st.warning("请输入有效的进场区间")
|
||||
entry_range = None
|
||||
|
||||
with col2:
|
||||
# 止盈止损
|
||||
st.subheader("⚖️ 风险控制")
|
||||
take_profit = st.number_input("止盈价位", value=0.0, key=f"take_profit_{session_id}")
|
||||
stop_loss = st.number_input("止损价位", value=0.0, key=f"stop_loss_{session_id}")
|
||||
|
||||
if take_profit > 0:
|
||||
st.success(f"止盈位: ¥{take_profit}")
|
||||
if stop_loss > 0:
|
||||
st.error(f"止损位: ¥{stop_loss}")
|
||||
|
||||
# 监测设置
|
||||
st.subheader("⏰ 监测设置")
|
||||
check_interval = st.slider("监测间隔(分钟)", 5, 120, 30, key=f"check_interval_{session_id}")
|
||||
notification_enabled = st.checkbox("启用提醒", value=True, key=f"notification_enabled_{session_id}")
|
||||
|
||||
# 添加按钮
|
||||
if st.button("✅ 确认加入监测", type="primary", key=f"add_monitor_{session_id}"):
|
||||
if entry_range:
|
||||
# 添加到监测数据库
|
||||
stock_id = monitor_db.add_monitored_stock(
|
||||
symbol=stock_info.get('symbol'),
|
||||
name=stock_info.get('name'),
|
||||
rating=rating,
|
||||
entry_range=entry_range,
|
||||
take_profit=take_profit if take_profit > 0 else None,
|
||||
stop_loss=stop_loss if stop_loss > 0 else None,
|
||||
check_interval=check_interval
|
||||
)
|
||||
|
||||
st.success(f"✅ 已成功将 {stock_info.get('symbol')} 加入实时监测")
|
||||
st.balloons()
|
||||
|
||||
# 立即更新一次价格
|
||||
monitor_service.manual_update_stock(stock_id)
|
||||
else:
|
||||
st.error("❌ 请设置有效的进场区间")
|
||||
|
||||
def get_monitor_summary() -> Dict:
|
||||
"""获取监测摘要信息"""
|
||||
stocks = monitor_db.get_monitored_stocks()
|
||||
|
||||
summary = {
|
||||
'total_stocks': len(stocks),
|
||||
'stocks_needing_update': len(monitor_service.get_stocks_needing_update()),
|
||||
'pending_notifications': len(monitor_db.get_pending_notifications()),
|
||||
'active_monitoring': monitor_service.running
|
||||
}
|
||||
|
||||
return summary
|
||||
@@ -0,0 +1,242 @@
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, List
|
||||
import streamlit as st
|
||||
|
||||
from monitor_db import monitor_db
|
||||
|
||||
class NotificationService:
|
||||
"""通知服务"""
|
||||
|
||||
def __init__(self):
|
||||
# 强制重新加载环境变量
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
self.config = self._load_config()
|
||||
|
||||
def _load_config(self) -> Dict:
|
||||
"""加载通知配置"""
|
||||
config = {
|
||||
'email_enabled': False,
|
||||
'smtp_server': '',
|
||||
'smtp_port': 587,
|
||||
'email_from': '',
|
||||
'email_password': '',
|
||||
'email_to': '',
|
||||
'webhook_enabled': False,
|
||||
'webhook_url': ''
|
||||
}
|
||||
|
||||
# 从环境变量加载配置
|
||||
if os.getenv('EMAIL_ENABLED'):
|
||||
config['email_enabled'] = os.getenv('EMAIL_ENABLED').lower() == 'true'
|
||||
if os.getenv('SMTP_SERVER'):
|
||||
config['smtp_server'] = os.getenv('SMTP_SERVER')
|
||||
if os.getenv('SMTP_PORT'):
|
||||
config['smtp_port'] = int(os.getenv('SMTP_PORT'))
|
||||
if os.getenv('EMAIL_FROM'):
|
||||
config['email_from'] = os.getenv('EMAIL_FROM')
|
||||
if os.getenv('EMAIL_PASSWORD'):
|
||||
config['email_password'] = os.getenv('EMAIL_PASSWORD')
|
||||
if os.getenv('EMAIL_TO'):
|
||||
config['email_to'] = os.getenv('EMAIL_TO')
|
||||
|
||||
return config
|
||||
|
||||
def send_notifications(self):
|
||||
"""发送所有待发送的通知"""
|
||||
notifications = monitor_db.get_pending_notifications()
|
||||
|
||||
for notification in notifications:
|
||||
try:
|
||||
if self.send_notification(notification):
|
||||
monitor_db.mark_notification_sent(notification['id'])
|
||||
print(f"✅ 通知已发送: {notification['message']}")
|
||||
else:
|
||||
print(f"❌ 通知发送失败: {notification['message']}")
|
||||
except Exception as e:
|
||||
print(f"❌ 发送通知时出错: {e}")
|
||||
|
||||
def send_notification(self, notification: Dict) -> bool:
|
||||
"""发送单个通知"""
|
||||
# 优先尝试邮件通知
|
||||
if self.config['email_enabled']:
|
||||
return self._send_email_notification(notification)
|
||||
|
||||
# 备用方案:在Streamlit界面显示
|
||||
self._show_streamlit_notification(notification)
|
||||
return True
|
||||
|
||||
def _send_email_notification(self, notification: Dict) -> bool:
|
||||
"""发送邮件通知"""
|
||||
try:
|
||||
# 检查邮件配置是否完整
|
||||
if not all([self.config['smtp_server'], self.config['email_from'],
|
||||
self.config['email_password'], self.config['email_to']]):
|
||||
print("邮件配置不完整,使用界面通知")
|
||||
self._show_streamlit_notification(notification)
|
||||
return True
|
||||
|
||||
# 创建邮件
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = self.config['email_from']
|
||||
msg['To'] = self.config['email_to']
|
||||
msg['Subject'] = f"股票监测提醒 - {notification['symbol']}"
|
||||
|
||||
# 邮件正文
|
||||
body = f"""
|
||||
<h2>股票监测提醒</h2>
|
||||
<p><strong>股票代码:</strong> {notification['symbol']}</p>
|
||||
<p><strong>股票名称:</strong> {notification['name']}</p>
|
||||
<p><strong>提醒类型:</strong> {notification['type']}</p>
|
||||
<p><strong>提醒内容:</strong> {notification['message']}</p>
|
||||
<p><strong>触发时间:</strong> {notification['triggered_at']}</p>
|
||||
<hr>
|
||||
<p><em>此邮件由AI股票分析系统自动发送</em></p>
|
||||
"""
|
||||
|
||||
msg.attach(MIMEText(body, 'html'))
|
||||
|
||||
# 根据端口选择连接方式
|
||||
if self.config['smtp_port'] == 465:
|
||||
server = smtplib.SMTP_SSL(self.config['smtp_server'], self.config['smtp_port'], timeout=15)
|
||||
else:
|
||||
server = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'], timeout=15)
|
||||
server.starttls()
|
||||
|
||||
server.login(self.config['email_from'], self.config['email_password'])
|
||||
server.send_message(msg)
|
||||
server.quit()
|
||||
print(f"邮件发送成功: {notification['symbol']}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"邮件发送失败: {e}")
|
||||
# 邮件发送失败时,使用界面通知作为备用方案
|
||||
print("使用界面通知作为备用方案")
|
||||
self._show_streamlit_notification(notification)
|
||||
return True
|
||||
|
||||
def _show_streamlit_notification(self, notification: Dict):
|
||||
"""在Streamlit界面显示通知"""
|
||||
# 使用session_state存储通知
|
||||
if 'notifications' not in st.session_state:
|
||||
st.session_state.notifications = []
|
||||
|
||||
# 避免重复通知,使用symbol代替stock_id
|
||||
notification_key = f"{notification['symbol']}_{notification['type']}_{notification['triggered_at']}"
|
||||
if notification_key not in [n.get('key') for n in st.session_state.notifications]:
|
||||
st.session_state.notifications.append({
|
||||
'key': notification_key,
|
||||
'symbol': notification['symbol'],
|
||||
'name': notification['name'],
|
||||
'type': notification['type'],
|
||||
'message': notification['message'],
|
||||
'timestamp': notification['triggered_at']
|
||||
})
|
||||
|
||||
def get_streamlit_notifications(self) -> List[Dict]:
|
||||
"""获取Streamlit界面通知"""
|
||||
return st.session_state.get('notifications', [])
|
||||
|
||||
def clear_streamlit_notifications(self):
|
||||
"""清空Streamlit界面通知"""
|
||||
if 'notifications' in st.session_state:
|
||||
st.session_state.notifications = []
|
||||
|
||||
def test_email_config(self) -> bool:
|
||||
"""测试邮件配置"""
|
||||
if not self.config['email_enabled']:
|
||||
return False
|
||||
|
||||
try:
|
||||
if self.config['smtp_port'] == 465:
|
||||
server = smtplib.SMTP_SSL(self.config['smtp_server'], self.config['smtp_port'], timeout=10)
|
||||
else:
|
||||
server = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'], timeout=10)
|
||||
server.starttls()
|
||||
|
||||
server.login(self.config['email_from'], self.config['email_password'])
|
||||
server.quit()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"邮件配置测试失败: {e}")
|
||||
return False
|
||||
|
||||
def send_test_email(self) -> tuple[bool, str]:
|
||||
"""发送测试邮件"""
|
||||
try:
|
||||
# 检查邮件配置是否完整
|
||||
if not all([self.config['smtp_server'], self.config['email_from'],
|
||||
self.config['email_password'], self.config['email_to']]):
|
||||
return False, "邮件配置不完整,请检查.env文件中的邮件设置"
|
||||
|
||||
# 创建测试邮件
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = self.config['email_from']
|
||||
msg['To'] = self.config['email_to']
|
||||
msg['Subject'] = "AI股票分析系统 - 邮件测试"
|
||||
|
||||
# 邮件正文
|
||||
body = f"""
|
||||
<html>
|
||||
<body>
|
||||
<h2>邮件测试成功!</h2>
|
||||
<p>这是一封来自AI股票分析系统的测试邮件。</p>
|
||||
<p>如果您收到这封邮件,说明邮件通知功能已正常工作。</p>
|
||||
<hr>
|
||||
<p><strong>邮件配置信息:</strong></p>
|
||||
<ul>
|
||||
<li>SMTP服务器: {self.config['smtp_server']}</li>
|
||||
<li>SMTP端口: {self.config['smtp_port']}</li>
|
||||
<li>发送邮箱: {self.config['email_from']}</li>
|
||||
<li>接收邮箱: {self.config['email_to']}</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<p><em>此邮件由AI股票分析系统自动发送</em></p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
msg.attach(MIMEText(body, 'html'))
|
||||
|
||||
# 根据端口选择连接方式
|
||||
if self.config['smtp_port'] == 465:
|
||||
server = smtplib.SMTP_SSL(self.config['smtp_server'], self.config['smtp_port'], timeout=15)
|
||||
else:
|
||||
server = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'], timeout=15)
|
||||
server.starttls()
|
||||
|
||||
server.login(self.config['email_from'], self.config['email_password'])
|
||||
server.send_message(msg)
|
||||
server.quit()
|
||||
return True, "测试邮件发送成功!请检查收件箱(包括垃圾邮件箱)。"
|
||||
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
return False, "邮箱认证失败,请检查邮箱和授权码是否正确"
|
||||
except smtplib.SMTPException as e:
|
||||
return False, f"SMTP错误: {str(e)}"
|
||||
except Exception as e:
|
||||
return False, f"发送失败: {str(e)}"
|
||||
|
||||
def get_email_config_status(self) -> Dict:
|
||||
"""获取邮件配置状态"""
|
||||
return {
|
||||
'enabled': self.config['email_enabled'],
|
||||
'smtp_server': self.config['smtp_server'] or '未配置',
|
||||
'smtp_port': self.config['smtp_port'],
|
||||
'email_from': self.config['email_from'] or '未配置',
|
||||
'email_to': self.config['email_to'] or '未配置',
|
||||
'configured': all([
|
||||
self.config['smtp_server'],
|
||||
self.config['email_from'],
|
||||
self.config['email_password'],
|
||||
self.config['email_to']
|
||||
])
|
||||
}
|
||||
|
||||
# 全局通知服务实例
|
||||
notification_service = NotificationService()
|
||||
@@ -0,0 +1,282 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
PDF报告生成器
|
||||
只生成PDF格式的完整分析报告
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
import io
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
def register_chinese_fonts():
|
||||
"""注册中文字体"""
|
||||
try:
|
||||
# 检查是否已经注册过
|
||||
if 'ChineseFont' in pdfmetrics.getRegisteredFontNames():
|
||||
return 'ChineseFont'
|
||||
|
||||
# 尝试注册系统中文字体
|
||||
font_paths = [
|
||||
'C:/Windows/Fonts/simsun.ttc', # 宋体
|
||||
'C:/Windows/Fonts/simhei.ttf', # 黑体
|
||||
'C:/Windows/Fonts/msyh.ttc', # 微软雅黑
|
||||
]
|
||||
|
||||
for font_path in font_paths:
|
||||
if os.path.exists(font_path):
|
||||
try:
|
||||
pdfmetrics.registerFont(TTFont('ChineseFont', font_path))
|
||||
return 'ChineseFont'
|
||||
except:
|
||||
continue
|
||||
|
||||
# 如果没有找到中文字体,使用默认字体
|
||||
return 'Helvetica'
|
||||
except:
|
||||
return 'Helvetica'
|
||||
|
||||
def create_pdf_report(stock_info, agents_results, discussion_result, final_decision):
|
||||
"""创建PDF格式的分析报告"""
|
||||
|
||||
# 注册中文字体
|
||||
chinese_font = register_chinese_fonts()
|
||||
|
||||
# 创建内存中的PDF文档
|
||||
buffer = io.BytesIO()
|
||||
doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=18)
|
||||
|
||||
# 获取样式
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
# 创建自定义样式
|
||||
title_style = ParagraphStyle(
|
||||
'CustomTitle',
|
||||
parent=styles['Heading1'],
|
||||
fontName=chinese_font,
|
||||
fontSize=24,
|
||||
spaceAfter=30,
|
||||
alignment=TA_CENTER,
|
||||
textColor=colors.darkblue
|
||||
)
|
||||
|
||||
heading_style = ParagraphStyle(
|
||||
'CustomHeading',
|
||||
parent=styles['Heading2'],
|
||||
fontName=chinese_font,
|
||||
fontSize=16,
|
||||
spaceAfter=12,
|
||||
spaceBefore=20,
|
||||
textColor=colors.darkblue
|
||||
)
|
||||
|
||||
subheading_style = ParagraphStyle(
|
||||
'CustomSubHeading',
|
||||
parent=styles['Heading3'],
|
||||
fontName=chinese_font,
|
||||
fontSize=14,
|
||||
spaceAfter=8,
|
||||
spaceBefore=12,
|
||||
textColor=colors.darkgreen
|
||||
)
|
||||
|
||||
normal_style = ParagraphStyle(
|
||||
'CustomNormal',
|
||||
parent=styles['Normal'],
|
||||
fontName=chinese_font,
|
||||
fontSize=11,
|
||||
spaceAfter=6,
|
||||
alignment=TA_JUSTIFY
|
||||
)
|
||||
|
||||
# 开始构建PDF内容
|
||||
story = []
|
||||
|
||||
# 标题
|
||||
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
|
||||
story.append(Paragraph("AI股票分析报告", title_style))
|
||||
story.append(Paragraph(f"生成时间: {current_time}", normal_style))
|
||||
story.append(Spacer(1, 20))
|
||||
|
||||
# 股票基本信息
|
||||
story.append(Paragraph("股票基本信息", heading_style))
|
||||
|
||||
# 创建股票信息表格
|
||||
stock_data = [
|
||||
['项目', '值'],
|
||||
['股票代码', stock_info.get('symbol', 'N/A')],
|
||||
['股票名称', stock_info.get('name', 'N/A')],
|
||||
['当前价格', str(stock_info.get('current_price', 'N/A'))],
|
||||
['涨跌幅', f"{stock_info.get('change_percent', 'N/A')}%"],
|
||||
['市盈率(PE)', str(stock_info.get('pe_ratio', 'N/A'))],
|
||||
['市净率(PB)', str(stock_info.get('pb_ratio', 'N/A'))],
|
||||
['市值', str(stock_info.get('market_cap', 'N/A'))],
|
||||
['市场', stock_info.get('market', 'N/A')],
|
||||
['交易所', stock_info.get('exchange', 'N/A')]
|
||||
]
|
||||
|
||||
stock_table = Table(stock_data, colWidths=[2*inch, 3*inch])
|
||||
stock_table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
||||
('FONTNAME', (0, 0), (-1, 0), chinese_font),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 12),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
|
||||
('FONTNAME', (0, 1), (-1, -1), chinese_font),
|
||||
('FONTSIZE', (0, 1), (-1, -1), 10),
|
||||
('GRID', (0, 0), (-1, -1), 1, colors.black)
|
||||
]))
|
||||
|
||||
story.append(stock_table)
|
||||
story.append(Spacer(1, 20))
|
||||
|
||||
# 各分析师分析结果
|
||||
story.append(Paragraph("AI分析师团队分析", heading_style))
|
||||
|
||||
agent_names = {
|
||||
'technical': '技术分析师',
|
||||
'fundamental': '基本面分析师',
|
||||
'fund_flow': '资金面分析师',
|
||||
'risk_management': '风险管理师',
|
||||
'market_sentiment': '市场情绪分析师'
|
||||
}
|
||||
|
||||
for agent_key, agent_name in agent_names.items():
|
||||
if agent_key in agents_results:
|
||||
story.append(Paragraph(f"{agent_name}分析", subheading_style))
|
||||
|
||||
agent_result = agents_results[agent_key]
|
||||
if isinstance(agent_result, dict):
|
||||
analysis_text = agent_result.get('analysis', '暂无分析')
|
||||
else:
|
||||
analysis_text = str(agent_result)
|
||||
|
||||
# 处理长文本,确保在PDF中正确显示
|
||||
analysis_text = analysis_text.replace('\n', '<br/>')
|
||||
story.append(Paragraph(analysis_text, normal_style))
|
||||
story.append(Spacer(1, 12))
|
||||
|
||||
# 团队讨论
|
||||
story.append(Paragraph("团队综合讨论", heading_style))
|
||||
discussion_text = str(discussion_result).replace('\n', '<br/>')
|
||||
story.append(Paragraph(discussion_text, normal_style))
|
||||
story.append(Spacer(1, 20))
|
||||
|
||||
# 最终投资决策
|
||||
story.append(Paragraph("最终投资决策", heading_style))
|
||||
|
||||
if isinstance(final_decision, dict) and "decision_text" not in final_decision:
|
||||
# JSON格式的决策
|
||||
decision_data = [
|
||||
['项目', '内容'],
|
||||
['投资评级', final_decision.get('rating', '未知')],
|
||||
['目标价位', str(final_decision.get('target_price', 'N/A'))],
|
||||
['操作建议', final_decision.get('operation_advice', '暂无建议')],
|
||||
['进场区间', final_decision.get('entry_range', 'N/A')],
|
||||
['止盈位', str(final_decision.get('take_profit', 'N/A'))],
|
||||
['止损位', str(final_decision.get('stop_loss', 'N/A'))],
|
||||
['持有周期', final_decision.get('holding_period', 'N/A')],
|
||||
['仓位建议', final_decision.get('position_size', 'N/A')],
|
||||
['信心度', f"{final_decision.get('confidence_level', 'N/A')}/10"],
|
||||
['风险提示', final_decision.get('risk_warning', '无')]
|
||||
]
|
||||
|
||||
decision_table = Table(decision_data, colWidths=[1.5*inch, 3.5*inch])
|
||||
decision_table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
||||
('FONTNAME', (0, 0), (-1, 0), chinese_font),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 12),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.lightblue),
|
||||
('FONTNAME', (0, 1), (-1, -1), chinese_font),
|
||||
('FONTSIZE', (0, 1), (-1, -1), 10),
|
||||
('GRID', (0, 0), (-1, -1), 1, colors.black)
|
||||
]))
|
||||
|
||||
story.append(decision_table)
|
||||
else:
|
||||
# 文本格式的决策
|
||||
decision_text = final_decision.get('decision_text', str(final_decision))
|
||||
decision_text = decision_text.replace('\n', '<br/>')
|
||||
story.append(Paragraph(decision_text, normal_style))
|
||||
|
||||
story.append(Spacer(1, 20))
|
||||
|
||||
# 免责声明
|
||||
story.append(Paragraph("免责声明", heading_style))
|
||||
disclaimer_text = """
|
||||
本报告由AI系统生成,仅供参考,不构成投资建议。投资有风险,入市需谨慎。
|
||||
请在做出投资决策前咨询专业的投资顾问。本系统不对任何投资损失承担责任。
|
||||
"""
|
||||
story.append(Paragraph(disclaimer_text, normal_style))
|
||||
|
||||
# 生成PDF
|
||||
doc.build(story)
|
||||
|
||||
# 获取PDF内容
|
||||
pdf_content = buffer.getvalue()
|
||||
buffer.close()
|
||||
|
||||
return pdf_content
|
||||
|
||||
def create_download_link(pdf_content, filename):
|
||||
"""创建PDF下载链接"""
|
||||
b64 = base64.b64encode(pdf_content).decode()
|
||||
href = f'<a href="data:application/pdf;base64,{b64}" download="{filename}" style="display: inline-block; padding: 15px 30px; background-color: #e74c3c; color: white; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px; margin: 10px;">📄 下载PDF报告</a>'
|
||||
return href
|
||||
|
||||
def display_pdf_export_section(stock_info, agents_results, discussion_result, final_decision):
|
||||
"""显示PDF导出区域"""
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown("## 📄 导出分析报告")
|
||||
|
||||
col1, col2, col3 = st.columns([1, 2, 1])
|
||||
|
||||
with col2:
|
||||
# 生成PDF报告按钮
|
||||
if st.button("📄 生成并下载PDF报告", type="primary", use_container_width=True):
|
||||
with st.spinner("正在生成PDF报告..."):
|
||||
try:
|
||||
# 生成PDF内容
|
||||
pdf_content = create_pdf_report(stock_info, agents_results, discussion_result, final_decision)
|
||||
|
||||
# 生成文件名
|
||||
stock_symbol = stock_info.get('symbol', 'unknown')
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"股票分析报告_{stock_symbol}_{timestamp}.pdf"
|
||||
|
||||
st.success("✅ PDF报告生成成功!")
|
||||
st.balloons()
|
||||
|
||||
# 显示下载链接
|
||||
st.markdown("### 📄 报告下载")
|
||||
|
||||
download_link = create_download_link(pdf_content, filename)
|
||||
st.markdown(f"""
|
||||
<div style="text-align: center; margin: 20px 0;">
|
||||
{download_link}
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.info("💡 提示:点击上方按钮即可下载PDF格式的完整分析报告")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"❌ 生成PDF报告时出错: {str(e)}")
|
||||
import traceback
|
||||
st.error(f"详细错误信息: {traceback.format_exc()}")
|
||||
@@ -0,0 +1,340 @@
|
||||
import os
|
||||
import tempfile
|
||||
import base64
|
||||
import re
|
||||
from datetime import datetime
|
||||
import streamlit as st
|
||||
|
||||
def generate_markdown_report(stock_info, agents_results, discussion_result, final_decision):
|
||||
"""生成Markdown格式的分析报告"""
|
||||
|
||||
# 获取当前时间
|
||||
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
|
||||
|
||||
markdown_content = f"""
|
||||
# AI股票分析报告
|
||||
|
||||
**生成时间**: {current_time}
|
||||
|
||||
---
|
||||
|
||||
## 📊 股票基本信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| **股票代码** | {stock_info.get('symbol', 'N/A')} |
|
||||
| **股票名称** | {stock_info.get('name', 'N/A')} |
|
||||
| **当前价格** | {stock_info.get('current_price', 'N/A')} |
|
||||
| **涨跌幅** | {stock_info.get('change_percent', 'N/A')}% |
|
||||
| **市盈率(PE)** | {stock_info.get('pe_ratio', 'N/A')} |
|
||||
| **市净率(PB)** | {stock_info.get('pb_ratio', 'N/A')} |
|
||||
| **市值** | {stock_info.get('market_cap', 'N/A')} |
|
||||
| **市场** | {stock_info.get('market', 'N/A')} |
|
||||
| **交易所** | {stock_info.get('exchange', 'N/A')} |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 各分析师详细分析
|
||||
|
||||
"""
|
||||
|
||||
# 添加各分析师的分析结果
|
||||
agent_names = {
|
||||
'technical': '📈 技术分析师',
|
||||
'fundamental': '📊 基本面分析师',
|
||||
'fund_flow': '💰 资金面分析师',
|
||||
'risk_management': '⚠️ 风险管理师',
|
||||
'market_sentiment': '📈 市场情绪分析师'
|
||||
}
|
||||
|
||||
for agent_key, agent_name in agent_names.items():
|
||||
if agent_key in agents_results:
|
||||
agent_result = agents_results[agent_key]
|
||||
if isinstance(agent_result, dict):
|
||||
analysis_text = agent_result.get('analysis', '暂无分析')
|
||||
else:
|
||||
analysis_text = str(agent_result)
|
||||
|
||||
markdown_content += f"""
|
||||
### {agent_name}
|
||||
|
||||
{analysis_text}
|
||||
|
||||
---
|
||||
|
||||
"""
|
||||
|
||||
# 添加团队讨论结果
|
||||
markdown_content += f"""
|
||||
## 🤝 团队综合讨论
|
||||
|
||||
{discussion_result}
|
||||
|
||||
---
|
||||
|
||||
## 📋 最终投资决策
|
||||
|
||||
"""
|
||||
|
||||
# 处理最终决策的显示
|
||||
if isinstance(final_decision, dict) and "decision_text" not in final_decision:
|
||||
# JSON格式的决策
|
||||
markdown_content += f"""
|
||||
**投资评级**: {final_decision.get('rating', '未知')}
|
||||
|
||||
**目标价位**: {final_decision.get('target_price', 'N/A')}
|
||||
|
||||
**操作建议**: {final_decision.get('operation_advice', '暂无建议')}
|
||||
|
||||
**进场区间**: {final_decision.get('entry_range', 'N/A')}
|
||||
|
||||
**止盈位**: {final_decision.get('take_profit', 'N/A')}
|
||||
|
||||
**止损位**: {final_decision.get('stop_loss', 'N/A')}
|
||||
|
||||
**持有周期**: {final_decision.get('holding_period', 'N/A')}
|
||||
|
||||
**仓位建议**: {final_decision.get('position_size', 'N/A')}
|
||||
|
||||
**信心度**: {final_decision.get('confidence_level', 'N/A')}/10
|
||||
|
||||
**风险提示**: {final_decision.get('risk_warning', '无')}
|
||||
"""
|
||||
else:
|
||||
# 文本格式的决策
|
||||
decision_text = final_decision.get('decision_text', str(final_decision))
|
||||
markdown_content += decision_text
|
||||
|
||||
markdown_content += """
|
||||
|
||||
---
|
||||
|
||||
## 📝 免责声明
|
||||
|
||||
本报告由AI系统生成,仅供参考,不构成投资建议。投资有风险,入市需谨慎。请在做出投资决策前咨询专业的投资顾问。
|
||||
|
||||
---
|
||||
|
||||
*报告生成时间: {current_time}*
|
||||
*AI股票分析系统 v1.0*
|
||||
"""
|
||||
|
||||
return markdown_content
|
||||
|
||||
def create_download_link(content, filename, link_text):
|
||||
"""创建下载链接"""
|
||||
b64 = base64.b64encode(content.encode()).decode()
|
||||
href = f'<a href="data:text/markdown;base64,{b64}" download="{filename}" style="display: inline-block; padding: 10px 20px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; margin: 5px;">{link_text}</a>'
|
||||
return href
|
||||
|
||||
def create_html_download_link(content, filename, link_text):
|
||||
"""创建HTML下载链接"""
|
||||
b64 = base64.b64encode(content.encode('utf-8')).decode()
|
||||
href = f'<a href="data:text/html;base64,{b64}" download="{filename}" style="display: inline-block; padding: 10px 20px; background-color: #2196F3; color: white; text-decoration: none; border-radius: 5px; margin: 5px;">{link_text}</a>'
|
||||
return href
|
||||
|
||||
def generate_html_content(markdown_content):
|
||||
"""将Markdown转换为HTML"""
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>AI股票分析报告</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}}
|
||||
.container {{
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}}
|
||||
h1 {{
|
||||
color: #2c3e50;
|
||||
border-bottom: 3px solid #3498db;
|
||||
padding-bottom: 10px;
|
||||
}}
|
||||
h2 {{
|
||||
color: #34495e;
|
||||
border-left: 4px solid #3498db;
|
||||
padding-left: 15px;
|
||||
margin-top: 30px;
|
||||
}}
|
||||
h3 {{
|
||||
color: #2980b9;
|
||||
margin-top: 25px;
|
||||
}}
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
th, td {{
|
||||
border: 1px solid #ddd;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
}}
|
||||
th {{
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
}}
|
||||
tr:nth-child(even) {{
|
||||
background-color: #f9f9f9;
|
||||
}}
|
||||
.disclaimer {{
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin-top: 30px;
|
||||
}}
|
||||
.footer {{
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
color: #7f8c8d;
|
||||
font-style: italic;
|
||||
}}
|
||||
hr {{
|
||||
border: none;
|
||||
height: 2px;
|
||||
background-color: #ecf0f1;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
strong {{
|
||||
color: #2c3e50;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
"""
|
||||
|
||||
# 简单的Markdown到HTML转换
|
||||
html_body = markdown_content
|
||||
html_body = html_body.replace('\n# ', '\n<h1>').replace('\n## ', '\n<h2>').replace('\n### ', '\n<h3>')
|
||||
html_body = html_body.replace('# ', '<h1>').replace('## ', '<h2>').replace('### ', '<h3>')
|
||||
html_body = html_body.replace('\n---\n', '\n<hr>\n')
|
||||
|
||||
# 处理粗体文本
|
||||
html_body = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', html_body)
|
||||
|
||||
# 处理表格
|
||||
lines = html_body.split('\n')
|
||||
in_table = False
|
||||
processed_lines = []
|
||||
|
||||
for line in lines:
|
||||
if '|' in line and not in_table and line.strip().startswith('|'):
|
||||
processed_lines.append('<table>')
|
||||
in_table = True
|
||||
cells = [cell.strip() for cell in line.split('|')[1:-1]]
|
||||
processed_lines.append('<tr>')
|
||||
for cell in cells:
|
||||
processed_lines.append(f'<th>{cell}</th>')
|
||||
processed_lines.append('</tr>')
|
||||
elif '|' in line and in_table:
|
||||
if '---' not in line:
|
||||
cells = [cell.strip() for cell in line.split('|')[1:-1]]
|
||||
processed_lines.append('<tr>')
|
||||
for cell in cells:
|
||||
processed_lines.append(f'<td>{cell}</td>')
|
||||
processed_lines.append('</tr>')
|
||||
elif in_table and '|' not in line:
|
||||
processed_lines.append('</table>')
|
||||
processed_lines.append(line)
|
||||
in_table = False
|
||||
else:
|
||||
processed_lines.append(line)
|
||||
|
||||
if in_table:
|
||||
processed_lines.append('</table>')
|
||||
|
||||
html_body = '\n'.join(processed_lines)
|
||||
|
||||
# 处理段落
|
||||
paragraphs = html_body.split('\n\n')
|
||||
processed_paragraphs = []
|
||||
for para in paragraphs:
|
||||
para = para.strip()
|
||||
if para and not para.startswith('<') and not para.startswith('---'):
|
||||
processed_paragraphs.append(f'<p>{para}</p>')
|
||||
else:
|
||||
processed_paragraphs.append(para)
|
||||
|
||||
html_body = '\n'.join(processed_paragraphs)
|
||||
|
||||
html_content += html_body + """
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return html_content
|
||||
|
||||
def display_pdf_export_section(stock_info, agents_results, discussion_result, final_decision):
|
||||
"""显示PDF导出区域 - 修复报告生成问题"""
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown("## 📄 导出分析报告")
|
||||
|
||||
col1, col2, col3 = st.columns([1, 2, 1])
|
||||
|
||||
with col2:
|
||||
# 生成报告按钮
|
||||
import uuid
|
||||
import time
|
||||
button_key = f"generate_report_btn_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
||||
if st.button("📊 生成并下载报告", type="primary", use_container_width=True, key=button_key):
|
||||
with st.spinner("正在生成报告..."):
|
||||
try:
|
||||
# 生成Markdown内容
|
||||
markdown_content = generate_markdown_report(stock_info, agents_results, discussion_result, final_decision)
|
||||
|
||||
# 生成HTML内容
|
||||
html_content = generate_html_content(markdown_content)
|
||||
|
||||
# 生成文件名
|
||||
stock_symbol = stock_info.get('symbol', 'unknown')
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"股票分析报告_{stock_symbol}_{timestamp}"
|
||||
|
||||
st.success("✅ 报告生成成功!")
|
||||
st.balloons()
|
||||
|
||||
# 立即显示下载链接
|
||||
st.markdown("### 📄 报告下载")
|
||||
|
||||
# 创建下载链接
|
||||
md_link = create_download_link(
|
||||
markdown_content,
|
||||
f"{filename}.md",
|
||||
"📝 下载Markdown报告"
|
||||
)
|
||||
|
||||
html_link = create_html_download_link(
|
||||
html_content,
|
||||
f"{filename}.html",
|
||||
"🌐 下载HTML报告"
|
||||
)
|
||||
|
||||
# 显示下载链接
|
||||
st.markdown(f"""
|
||||
<div style="text-align: center; margin: 20px 0;">
|
||||
{md_link}
|
||||
{html_link}
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.info("💡 提示:点击上方按钮即可下载对应格式的报告文件")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"❌ 生成报告时出错: {str(e)}")
|
||||
import traceback
|
||||
st.error(f"详细错误信息: {traceback.format_exc()}")
|
||||
@@ -0,0 +1,279 @@
|
||||
import os
|
||||
import tempfile
|
||||
import base64
|
||||
from datetime import datetime
|
||||
import streamlit as st
|
||||
|
||||
def generate_markdown_report(stock_info, agents_results, discussion_result, final_decision):
|
||||
"""生成Markdown格式的分析报告"""
|
||||
|
||||
# 获取当前时间
|
||||
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
|
||||
|
||||
markdown_content = f"""
|
||||
# AI股票分析报告
|
||||
|
||||
**生成时间**: {current_time}
|
||||
|
||||
---
|
||||
|
||||
## 📊 股票基本信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| **股票代码** | {stock_info.get('symbol', 'N/A')} |
|
||||
| **股票名称** | {stock_info.get('name', 'N/A')} |
|
||||
| **当前价格** | {stock_info.get('current_price', 'N/A')} |
|
||||
| **涨跌幅** | {stock_info.get('change_percent', 'N/A')}% |
|
||||
| **市盈率(PE)** | {stock_info.get('pe_ratio', 'N/A')} |
|
||||
| **市净率(PB)** | {stock_info.get('pb_ratio', 'N/A')} |
|
||||
| **市值** | {stock_info.get('market_cap', 'N/A')} |
|
||||
| **市场** | {stock_info.get('market', 'N/A')} |
|
||||
| **交易所** | {stock_info.get('exchange', 'N/A')} |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 各分析师详细分析
|
||||
|
||||
"""
|
||||
|
||||
# 添加各分析师的分析结果
|
||||
agent_names = {
|
||||
'technical_analyst': '📈 技术分析师',
|
||||
'fundamental_analyst': '📊 基本面分析师',
|
||||
'fund_analyst': '💰 资金面分析师',
|
||||
'risk_analyst': '⚠️ 风险管理师',
|
||||
'sentiment_analyst': '📈 市场情绪分析师'
|
||||
}
|
||||
|
||||
for agent_key, agent_name in agent_names.items():
|
||||
if agent_key in agents_results:
|
||||
markdown_content += f"""
|
||||
### {agent_name}
|
||||
|
||||
{agents_results[agent_key]}
|
||||
|
||||
---
|
||||
|
||||
"""
|
||||
|
||||
# 添加团队讨论结果
|
||||
markdown_content += f"""
|
||||
## 🤝 团队综合讨论
|
||||
|
||||
{discussion_result}
|
||||
|
||||
---
|
||||
|
||||
## 📋 最终投资决策
|
||||
|
||||
{final_decision}
|
||||
|
||||
---
|
||||
|
||||
## 📝 免责声明
|
||||
|
||||
本报告由AI系统生成,仅供参考,不构成投资建议。投资有风险,入市需谨慎。请在做出投资决策前咨询专业的投资顾问。
|
||||
|
||||
---
|
||||
|
||||
*报告生成时间: {current_time}*
|
||||
*AI股票分析系统 v1.0*
|
||||
"""
|
||||
|
||||
return markdown_content
|
||||
|
||||
def create_download_link(content, filename, link_text):
|
||||
"""创建下载链接"""
|
||||
b64 = base64.b64encode(content.encode()).decode()
|
||||
href = f'<a href="data:text/markdown;base64,{b64}" download="{filename}">{link_text}</a>'
|
||||
return href
|
||||
|
||||
def generate_pdf_report(stock_info, agents_results, discussion_result, final_decision):
|
||||
"""生成PDF报告并提供下载"""
|
||||
try:
|
||||
# 生成Markdown内容
|
||||
markdown_content = generate_markdown_report(stock_info, agents_results, discussion_result, final_decision)
|
||||
|
||||
# 创建临时文件
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as temp_md:
|
||||
temp_md.write(markdown_content)
|
||||
temp_md_path = temp_md.name
|
||||
|
||||
# 生成文件名
|
||||
stock_symbol = stock_info.get('symbol', 'unknown')
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"股票分析报告_{stock_symbol}_{timestamp}"
|
||||
|
||||
# 提供Markdown下载
|
||||
st.markdown("### 📄 报告下载")
|
||||
|
||||
# Markdown下载链接
|
||||
md_download_link = create_download_link(
|
||||
markdown_content,
|
||||
f"{filename}.md",
|
||||
"📝 下载Markdown报告"
|
||||
)
|
||||
st.markdown(md_download_link, unsafe_allow_html=True)
|
||||
|
||||
# 提供HTML预览和下载
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>AI股票分析报告</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}}
|
||||
.container {{
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}}
|
||||
h1 {{
|
||||
color: #2c3e50;
|
||||
border-bottom: 3px solid #3498db;
|
||||
padding-bottom: 10px;
|
||||
}}
|
||||
h2 {{
|
||||
color: #34495e;
|
||||
border-left: 4px solid #3498db;
|
||||
padding-left: 15px;
|
||||
margin-top: 30px;
|
||||
}}
|
||||
h3 {{
|
||||
color: #2980b9;
|
||||
margin-top: 25px;
|
||||
}}
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
th, td {{
|
||||
border: 1px solid #ddd;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
}}
|
||||
th {{
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
}}
|
||||
tr:nth-child(even) {{
|
||||
background-color: #f9f9f9;
|
||||
}}
|
||||
.disclaimer {{
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin-top: 30px;
|
||||
}}
|
||||
.footer {{
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
color: #7f8c8d;
|
||||
font-style: italic;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
"""
|
||||
|
||||
# 将Markdown转换为HTML(简单版本)
|
||||
html_body = markdown_content.replace('\n# ', '\n<h1>').replace('\n## ', '\n<h2>').replace('\n### ', '\n<h3>')
|
||||
html_body = html_body.replace('\n---\n', '\n<hr>\n')
|
||||
html_body = html_body.replace('**', '<strong>').replace('**', '</strong>')
|
||||
html_body = html_body.replace('\n\n', '</p><p>')
|
||||
html_body = f"<p>{html_body}</p>"
|
||||
|
||||
# 处理表格
|
||||
lines = html_body.split('\n')
|
||||
in_table = False
|
||||
processed_lines = []
|
||||
|
||||
for line in lines:
|
||||
if '|' in line and not in_table:
|
||||
processed_lines.append('<table>')
|
||||
in_table = True
|
||||
if line.strip().startswith('|'):
|
||||
cells = [cell.strip() for cell in line.split('|')[1:-1]]
|
||||
processed_lines.append('<tr>')
|
||||
for cell in cells:
|
||||
processed_lines.append(f'<th>{cell}</th>')
|
||||
processed_lines.append('</tr>')
|
||||
elif '|' in line and in_table:
|
||||
if '---' not in line:
|
||||
cells = [cell.strip() for cell in line.split('|')[1:-1]]
|
||||
processed_lines.append('<tr>')
|
||||
for cell in cells:
|
||||
processed_lines.append(f'<td>{cell}</td>')
|
||||
processed_lines.append('</tr>')
|
||||
elif in_table and '|' not in line:
|
||||
processed_lines.append('</table>')
|
||||
processed_lines.append(line)
|
||||
in_table = False
|
||||
else:
|
||||
processed_lines.append(line)
|
||||
|
||||
if in_table:
|
||||
processed_lines.append('</table>')
|
||||
|
||||
html_body = '\n'.join(processed_lines)
|
||||
|
||||
html_content += html_body + """
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# HTML下载链接
|
||||
html_b64 = base64.b64encode(html_content.encode('utf-8')).decode()
|
||||
html_href = f'<a href="data:text/html;base64,{html_b64}" download="{filename}.html">🌐 下载HTML报告</a>'
|
||||
st.markdown(html_href, unsafe_allow_html=True)
|
||||
|
||||
# 清理临时文件
|
||||
try:
|
||||
os.unlink(temp_md_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
st.success("✅ 报告生成成功!请点击上方链接下载报告文件。")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"❌ 生成报告时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
def display_pdf_export_section(stock_info, agents_results, discussion_result, final_decision):
|
||||
"""显示PDF导出区域"""
|
||||
st.markdown("---")
|
||||
st.markdown("## 📄 导出分析报告")
|
||||
|
||||
# 使用session_state来避免页面重置
|
||||
if 'show_download_links' not in st.session_state:
|
||||
st.session_state.show_download_links = False
|
||||
|
||||
col1, col2, col3 = st.columns([1, 2, 1])
|
||||
|
||||
with col2:
|
||||
if st.button("📊 生成并下载报告", type="primary", use_container_width=True, key="generate_report_btn"):
|
||||
st.session_state.show_download_links = True
|
||||
with st.spinner("正在生成报告..."):
|
||||
success = generate_pdf_report(stock_info, agents_results, discussion_result, final_decision)
|
||||
if success:
|
||||
st.balloons()
|
||||
|
||||
# 如果已经生成了报告,显示下载链接
|
||||
if st.session_state.show_download_links:
|
||||
generate_pdf_report(stock_info, agents_results, discussion_result, final_decision)
|
||||
@@ -0,0 +1,14 @@
|
||||
streamlit>=1.28.0
|
||||
requests>=2.31.0
|
||||
pandas>=2.0.3
|
||||
numpy>=1.24.3
|
||||
plotly>=5.15.0
|
||||
yfinance>=0.2.18
|
||||
akshare>=1.11.0
|
||||
openai>=1.12.0
|
||||
python-dotenv>=1.0.0
|
||||
pytz
|
||||
ta>=0.10.2
|
||||
reportlab>=4.0.0
|
||||
peewee>=3.17.0
|
||||
schedule>=1.2.0
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI股票分析系统启动脚本
|
||||
运行命令: python run.py
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
def check_requirements():
|
||||
"""检查必要的依赖是否安装"""
|
||||
try:
|
||||
import streamlit
|
||||
import pandas
|
||||
import plotly
|
||||
import yfinance
|
||||
import akshare
|
||||
import openai
|
||||
print("✅ 所有依赖包已安装")
|
||||
return True
|
||||
except ImportError as e:
|
||||
print(f"❌ 缺少依赖包: {e}")
|
||||
print("请运行: pip install -r requirements.txt")
|
||||
return False
|
||||
|
||||
def check_config():
|
||||
"""检查配置文件"""
|
||||
try:
|
||||
import config
|
||||
if not config.DEEPSEEK_API_KEY:
|
||||
print("⚠️ 警告: DeepSeek API Key 未配置")
|
||||
print("请在config.py中设置 DEEPSEEK_API_KEY")
|
||||
return False
|
||||
print("✅ 配置文件检查通过")
|
||||
return True
|
||||
except ImportError:
|
||||
print("❌ 配置文件config.py不存在")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("🚀 启动AI股票分析系统...")
|
||||
print("=" * 50)
|
||||
|
||||
# 检查依赖
|
||||
if not check_requirements():
|
||||
return
|
||||
|
||||
# 检查配置
|
||||
config_ok = check_config()
|
||||
|
||||
# 启动Streamlit应用
|
||||
print("🌐 正在启动Web界面...")
|
||||
print("📝 访问地址: http://localhost:8501")
|
||||
print("⏹️ 按 Ctrl+C 停止服务")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
subprocess.run([
|
||||
sys.executable, "-m", "streamlit", "run", "app.py",
|
||||
"--server.port", "8501",
|
||||
"--server.address", "0.0.0.0"
|
||||
])
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 感谢使用AI股票分析系统!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,90 @@
|
||||
# 🔧 系统设置指南
|
||||
|
||||
## 📋 安装步骤
|
||||
|
||||
### 1. 安装Python依赖
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 配置API密钥
|
||||
|
||||
在 `config.py` 文件中设置您的DeepSeek API密钥:
|
||||
|
||||
```python
|
||||
# config.py
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# DeepSeek API配置
|
||||
DEEPSEEK_API_KEY = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # 在这里输入您的API密钥
|
||||
DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1"
|
||||
```
|
||||
|
||||
### 3. 获取DeepSeek API密钥
|
||||
|
||||
1. 访问 [DeepSeek官网](https://platform.deepseek.com/)
|
||||
2. 注册账号并登录
|
||||
3. 进入API管理页面
|
||||
4. 创建新的API密钥
|
||||
5. 复制密钥并设置到config.py中
|
||||
|
||||
### 4. 启动系统
|
||||
|
||||
方式一:使用启动脚本
|
||||
```bash
|
||||
python run.py
|
||||
```
|
||||
|
||||
方式二:直接启动Streamlit
|
||||
```bash
|
||||
streamlit run app.py
|
||||
```
|
||||
|
||||
### 5. 访问系统
|
||||
打开浏览器访问:http://localhost:8501
|
||||
|
||||
## 🧪 测试建议
|
||||
|
||||
### 测试用例
|
||||
1. **美股测试**:输入 `AAPL`
|
||||
2. **A股测试**:输入 `000001`
|
||||
3. **错误处理测试**:输入无效代码
|
||||
|
||||
### 预期结果
|
||||
- 成功获取股票基本信息
|
||||
- 显示股价走势图表
|
||||
- 生成5个AI分析师报告
|
||||
- 产生团队讨论结果
|
||||
- 输出最终投资决策
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **API配额**:注意DeepSeek API的使用配额
|
||||
2. **网络要求**:需要稳定的网络连接
|
||||
3. **首次运行**:第一次运行可能需要较长时间
|
||||
4. **数据源**:依赖第三方数据源,可能有延迟
|
||||
|
||||
## 🔍 故障排除
|
||||
|
||||
### 常见错误及解决方案
|
||||
|
||||
1. **模块导入错误**
|
||||
```bash
|
||||
pip install --upgrade -r requirements.txt
|
||||
```
|
||||
|
||||
2. **API密钥错误**
|
||||
- 检查config.py中的密钥设置
|
||||
- 确保密钥格式正确且有效
|
||||
|
||||
3. **数据获取失败**
|
||||
- 检查网络连接
|
||||
- 确认股票代码正确
|
||||
|
||||
4. **页面无法访问**
|
||||
- 检查端口8501是否被占用
|
||||
- 尝试使用其他端口:`streamlit run app.py --server.port 8502`
|
||||
+547
@@ -0,0 +1,547 @@
|
||||
import yfinance as yf
|
||||
import akshare as ak
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import ta
|
||||
from datetime import datetime, timedelta
|
||||
import requests
|
||||
import json
|
||||
|
||||
class StockDataFetcher:
|
||||
"""股票数据获取类"""
|
||||
|
||||
def __init__(self):
|
||||
self.data = None
|
||||
self.info = None
|
||||
self.financial_data = None
|
||||
|
||||
def get_stock_info(self, symbol):
|
||||
"""获取股票基本信息"""
|
||||
try:
|
||||
# 处理中国股票代码
|
||||
if self._is_chinese_stock(symbol):
|
||||
return self._get_chinese_stock_info(symbol)
|
||||
else:
|
||||
return self._get_us_stock_info(symbol)
|
||||
except Exception as e:
|
||||
return {"error": f"获取股票信息失败: {str(e)}"}
|
||||
|
||||
def get_stock_data(self, symbol, period="1y", interval="1d"):
|
||||
"""获取股票历史数据"""
|
||||
try:
|
||||
if self._is_chinese_stock(symbol):
|
||||
return self._get_chinese_stock_data(symbol, period)
|
||||
else:
|
||||
return self._get_us_stock_data(symbol, period, interval)
|
||||
except Exception as e:
|
||||
return {"error": f"获取股票数据失败: {str(e)}"}
|
||||
|
||||
def _is_chinese_stock(self, symbol):
|
||||
"""判断是否为中国股票"""
|
||||
# 简单判断:包含数字且长度为6位的认为是中国股票
|
||||
return symbol.isdigit() and len(symbol) == 6
|
||||
|
||||
def _get_chinese_stock_info(self, symbol):
|
||||
"""获取中国股票基本信息"""
|
||||
try:
|
||||
# 初始化基本信息
|
||||
info = {
|
||||
"symbol": symbol,
|
||||
"name": "未知",
|
||||
"current_price": "N/A",
|
||||
"change_percent": "N/A",
|
||||
"pe_ratio": "N/A",
|
||||
"pb_ratio": "N/A",
|
||||
"market_cap": "N/A",
|
||||
"market": "中国A股",
|
||||
"exchange": "上海/深圳证券交易所"
|
||||
}
|
||||
|
||||
# 方法1: 尝试获取个股详细信息
|
||||
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"获取个股详细信息失败: {e}")
|
||||
|
||||
# 方法2: 尝试获取实时价格和涨跌幅(如果网络允许)
|
||||
try:
|
||||
# 使用更简单的接口获取实时价格
|
||||
real_time_data = ak.stock_zh_a_spot_em()
|
||||
if real_time_data is not None and not real_time_data.empty:
|
||||
stock_real_time = real_time_data[real_time_data['代码'] == symbol]
|
||||
if not stock_real_time.empty:
|
||||
row = stock_real_time.iloc[0]
|
||||
info['current_price'] = row.get('最新价', 'N/A')
|
||||
info['change_percent'] = row.get('涨跌幅', 'N/A')
|
||||
if info['name'] == '未知':
|
||||
info['name'] = row.get('名称', '未知')
|
||||
|
||||
# 如果实时数据中有市盈率和市净率,优先使用
|
||||
if '市盈率-动态' in row and info['pe_ratio'] == 'N/A':
|
||||
try:
|
||||
pe_val = row['市盈率-动态']
|
||||
if pe_val and pe_val != '-':
|
||||
pe_val = float(pe_val)
|
||||
if 0 < pe_val <= 1000:
|
||||
info['pe_ratio'] = pe_val
|
||||
except:
|
||||
pass
|
||||
|
||||
if '市净率' in row and info['pb_ratio'] == 'N/A':
|
||||
try:
|
||||
pb_val = row['市净率']
|
||||
if pb_val and pb_val != '-':
|
||||
pb_val = float(pb_val)
|
||||
if 0 < pb_val <= 100:
|
||||
info['pb_ratio'] = pb_val
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取实时数据失败: {e}")
|
||||
# 如果实时数据获取失败,尝试使用历史数据获取价格
|
||||
try:
|
||||
hist_data = ak.stock_zh_a_hist(symbol=symbol, period="daily",
|
||||
start_date=(datetime.now() - timedelta(days=5)).strftime('%Y%m%d'),
|
||||
end_date=datetime.now().strftime('%Y%m%d'), adjust="qfq")
|
||||
if hist_data is not None and not hist_data.empty:
|
||||
latest = hist_data.iloc[-1]
|
||||
info['current_price'] = latest['收盘']
|
||||
# 计算涨跌幅
|
||||
if len(hist_data) > 1:
|
||||
prev_close = hist_data.iloc[-2]['收盘']
|
||||
change_pct = ((latest['收盘'] - prev_close) / prev_close) * 100
|
||||
info['change_percent'] = round(change_pct, 2)
|
||||
except Exception as e2:
|
||||
print(f"获取历史数据也失败: {e2}")
|
||||
|
||||
# 方法3: 使用百度估值数据获取市盈率和市净率
|
||||
if info['pe_ratio'] == 'N/A':
|
||||
try:
|
||||
pe_data = ak.stock_zh_valuation_baidu(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 != '-':
|
||||
pe_val = float(latest_pe)
|
||||
if 0 < pe_val <= 1000:
|
||||
info['pe_ratio'] = pe_val
|
||||
except Exception as e:
|
||||
print(f"获取市盈率失败: {e}")
|
||||
|
||||
if info['pb_ratio'] == 'N/A':
|
||||
try:
|
||||
pb_data = ak.stock_zh_valuation_baidu(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 != '-':
|
||||
pb_val = float(latest_pb)
|
||||
if 0 < pb_val <= 100:
|
||||
info['pb_ratio'] = pb_val
|
||||
except Exception as e:
|
||||
print(f"获取市净率失败: {e}")
|
||||
|
||||
return info
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取中国股票信息完全失败: {e}")
|
||||
# 返回基本信息,避免完全失败
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"name": f"股票{symbol}",
|
||||
"current_price": "N/A",
|
||||
"change_percent": "N/A",
|
||||
"pe_ratio": "N/A",
|
||||
"pb_ratio": "N/A",
|
||||
"market_cap": "N/A",
|
||||
"market": "中国A股",
|
||||
"exchange": "上海/深圳证券交易所"
|
||||
}
|
||||
|
||||
def _get_us_stock_info(self, symbol):
|
||||
"""获取美股基本信息"""
|
||||
import time
|
||||
|
||||
try:
|
||||
# 添加延迟避免频率限制
|
||||
time.sleep(1)
|
||||
|
||||
ticker = yf.Ticker(symbol)
|
||||
|
||||
# 先尝试获取历史数据(通常更稳定)
|
||||
try:
|
||||
hist = ticker.history(period="2d")
|
||||
if not hist.empty:
|
||||
current_price = hist['Close'].iloc[-1]
|
||||
if len(hist) > 1:
|
||||
prev_close = hist['Close'].iloc[-2]
|
||||
change_percent = ((current_price - prev_close) / prev_close) * 100
|
||||
else:
|
||||
change_percent = 'N/A'
|
||||
else:
|
||||
current_price = 'N/A'
|
||||
change_percent = 'N/A'
|
||||
except:
|
||||
current_price = 'N/A'
|
||||
change_percent = 'N/A'
|
||||
|
||||
# 获取基本信息
|
||||
try:
|
||||
info = ticker.info
|
||||
|
||||
# 获取市盈率,优先使用trailing PE,其次forward PE
|
||||
pe_ratio = info.get('trailingPE', info.get('forwardPE', 'N/A'))
|
||||
if pe_ratio == 'N/A' or pe_ratio is None or (isinstance(pe_ratio, float) and np.isnan(pe_ratio)):
|
||||
pe_ratio = 'N/A'
|
||||
|
||||
# 获取市净率
|
||||
pb_ratio = info.get('priceToBook', 'N/A')
|
||||
if pb_ratio == 'N/A' or pb_ratio is None or (isinstance(pb_ratio, float) and np.isnan(pb_ratio)):
|
||||
pb_ratio = 'N/A'
|
||||
|
||||
# 如果历史数据没有获取到价格,尝试从info获取
|
||||
if current_price == 'N/A':
|
||||
current_price = info.get('currentPrice', info.get('regularMarketPrice', 'N/A'))
|
||||
|
||||
if change_percent == 'N/A':
|
||||
change_percent = info.get('regularMarketChangePercent', 'N/A')
|
||||
if change_percent != 'N/A' and change_percent is not None:
|
||||
change_percent = change_percent * 100 # 转换为百分比
|
||||
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"name": info.get('longName', info.get('shortName', 'N/A')),
|
||||
"current_price": current_price,
|
||||
"change_percent": change_percent,
|
||||
"market_cap": info.get('marketCap', 'N/A'),
|
||||
"pe_ratio": pe_ratio,
|
||||
"pb_ratio": pb_ratio,
|
||||
"dividend_yield": info.get('dividendYield', 'N/A'),
|
||||
"beta": info.get('beta', 'N/A'),
|
||||
"52_week_high": info.get('fiftyTwoWeekHigh', 'N/A'),
|
||||
"52_week_low": info.get('fiftyTwoWeekLow', 'N/A'),
|
||||
"sector": info.get('sector', 'N/A'),
|
||||
"industry": info.get('industry', 'N/A'),
|
||||
"market": "美股",
|
||||
"exchange": info.get('exchange', 'N/A')
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# 如果获取详细信息失败,返回基本价格信息
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"name": f"美股{symbol}",
|
||||
"current_price": current_price,
|
||||
"change_percent": change_percent,
|
||||
"market_cap": 'N/A',
|
||||
"pe_ratio": 'N/A',
|
||||
"pb_ratio": 'N/A',
|
||||
"dividend_yield": 'N/A',
|
||||
"beta": 'N/A',
|
||||
"52_week_high": 'N/A',
|
||||
"52_week_low": 'N/A',
|
||||
"sector": 'N/A',
|
||||
"industry": 'N/A',
|
||||
"market": "美股",
|
||||
"exchange": 'N/A'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": f"获取美股信息失败: {str(e)}"}
|
||||
|
||||
def _get_chinese_stock_data(self, symbol, period="1y"):
|
||||
"""获取中国股票历史数据"""
|
||||
try:
|
||||
# 计算日期范围
|
||||
end_date = datetime.now().strftime('%Y%m%d')
|
||||
if period == "1y":
|
||||
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
|
||||
elif period == "6mo":
|
||||
start_date = (datetime.now() - timedelta(days=180)).strftime('%Y%m%d')
|
||||
elif period == "3mo":
|
||||
start_date = (datetime.now() - timedelta(days=90)).strftime('%Y%m%d')
|
||||
else:
|
||||
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
|
||||
|
||||
# 获取历史数据
|
||||
df = ak.stock_zh_a_hist(symbol=symbol, period="daily",
|
||||
start_date=start_date, end_date=end_date, adjust="qfq")
|
||||
|
||||
if df is not None and not df.empty:
|
||||
# 重命名列以匹配标准格式
|
||||
df = df.rename(columns={
|
||||
'日期': 'Date',
|
||||
'开盘': 'Open',
|
||||
'收盘': 'Close',
|
||||
'最高': 'High',
|
||||
'最低': 'Low',
|
||||
'成交量': 'Volume'
|
||||
})
|
||||
df['Date'] = pd.to_datetime(df['Date'])
|
||||
df.set_index('Date', inplace=True)
|
||||
return df
|
||||
else:
|
||||
return {"error": "无法获取历史数据"}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": f"获取中国股票数据失败: {str(e)}"}
|
||||
|
||||
def _get_us_stock_data(self, symbol, period="1y", interval="1d"):
|
||||
"""获取美股历史数据"""
|
||||
try:
|
||||
ticker = yf.Ticker(symbol)
|
||||
df = ticker.history(period=period, interval=interval)
|
||||
if not df.empty:
|
||||
return df
|
||||
else:
|
||||
return {"error": "无法获取历史数据"}
|
||||
except Exception as e:
|
||||
return {"error": f"获取美股数据失败: {str(e)}"}
|
||||
|
||||
def calculate_technical_indicators(self, df):
|
||||
"""计算技术指标"""
|
||||
try:
|
||||
if isinstance(df, dict) and "error" in df:
|
||||
return df
|
||||
|
||||
# 移动平均线
|
||||
df['MA5'] = ta.trend.sma_indicator(df['Close'], window=5)
|
||||
df['MA10'] = ta.trend.sma_indicator(df['Close'], window=10)
|
||||
df['MA20'] = ta.trend.sma_indicator(df['Close'], window=20)
|
||||
df['MA60'] = ta.trend.sma_indicator(df['Close'], window=60)
|
||||
|
||||
# RSI
|
||||
df['RSI'] = ta.momentum.rsi(df['Close'], window=14)
|
||||
|
||||
# MACD
|
||||
macd = ta.trend.MACD(df['Close'])
|
||||
df['MACD'] = macd.macd()
|
||||
df['MACD_signal'] = macd.macd_signal()
|
||||
df['MACD_histogram'] = macd.macd_diff()
|
||||
|
||||
# 布林带
|
||||
bollinger = ta.volatility.BollingerBands(df['Close'])
|
||||
df['BB_upper'] = bollinger.bollinger_hband()
|
||||
df['BB_middle'] = bollinger.bollinger_mavg()
|
||||
df['BB_lower'] = bollinger.bollinger_lband()
|
||||
|
||||
# KDJ指标
|
||||
df['K'] = ta.momentum.stoch(df['High'], df['Low'], df['Close'])
|
||||
df['D'] = ta.momentum.stoch_signal(df['High'], df['Low'], df['Close'])
|
||||
|
||||
# 成交量指标
|
||||
df['Volume_MA5'] = ta.trend.sma_indicator(df['Volume'], window=5)
|
||||
df['Volume_ratio'] = df['Volume'] / df['Volume_MA5']
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
return {"error": f"计算技术指标失败: {str(e)}"}
|
||||
|
||||
def get_latest_indicators(self, df):
|
||||
"""获取最新的技术指标值"""
|
||||
try:
|
||||
if isinstance(df, dict) and "error" in df:
|
||||
return df
|
||||
|
||||
latest = df.iloc[-1]
|
||||
|
||||
return {
|
||||
"price": latest['Close'],
|
||||
"ma5": latest['MA5'],
|
||||
"ma10": latest['MA10'],
|
||||
"ma20": latest['MA20'],
|
||||
"ma60": latest['MA60'],
|
||||
"rsi": latest['RSI'],
|
||||
"macd": latest['MACD'],
|
||||
"macd_signal": latest['MACD_signal'],
|
||||
"bb_upper": latest['BB_upper'],
|
||||
"bb_lower": latest['BB_lower'],
|
||||
"k_value": latest['K'],
|
||||
"d_value": latest['D'],
|
||||
"volume_ratio": latest['Volume_ratio']
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": f"获取最新指标失败: {str(e)}"}
|
||||
|
||||
def get_financial_data(self, symbol):
|
||||
"""获取详细财务数据"""
|
||||
try:
|
||||
if self._is_chinese_stock(symbol):
|
||||
return self._get_chinese_financial_data(symbol)
|
||||
else:
|
||||
return self._get_us_financial_data(symbol)
|
||||
except Exception as e:
|
||||
return {"error": f"获取财务数据失败: {str(e)}"}
|
||||
|
||||
def _get_chinese_financial_data(self, symbol):
|
||||
"""获取中国股票财务数据"""
|
||||
financial_data = {
|
||||
"symbol": symbol,
|
||||
"balance_sheet": None, # 资产负债表
|
||||
"income_statement": None, # 利润表
|
||||
"cash_flow": None, # 现金流量表
|
||||
"financial_ratios": {}, # 财务比率
|
||||
"quarter_data": None, # 季度数据
|
||||
}
|
||||
|
||||
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}")
|
||||
|
||||
# 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}")
|
||||
|
||||
# 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}")
|
||||
|
||||
# 4. 获取主要财务指标
|
||||
try:
|
||||
financial_indicators = ak.stock_financial_analysis_indicator(symbol=symbol)
|
||||
if financial_indicators is not None and not financial_indicators.empty:
|
||||
latest_data = financial_indicators.iloc[0]
|
||||
|
||||
financial_data["financial_ratios"] = {
|
||||
"报告期": latest_data.get('报告期', 'N/A'),
|
||||
"净资产收益率ROE": latest_data.get('净资产收益率', 'N/A'),
|
||||
"总资产收益率ROA": latest_data.get('总资产收益率', 'N/A'),
|
||||
"销售毛利率": latest_data.get('销售毛利率', 'N/A'),
|
||||
"销售净利率": latest_data.get('销售净利率', 'N/A'),
|
||||
"资产负债率": latest_data.get('资产负债率', 'N/A'),
|
||||
"流动比率": latest_data.get('流动比率', 'N/A'),
|
||||
"速动比率": latest_data.get('速动比率', 'N/A'),
|
||||
"存货周转率": latest_data.get('存货周转率', 'N/A'),
|
||||
"应收账款周转率": latest_data.get('应收账款周转率', 'N/A'),
|
||||
"总资产周转率": latest_data.get('总资产周转率', 'N/A'),
|
||||
"营业收入同比增长": latest_data.get('营业收入同比增长', 'N/A'),
|
||||
"净利润同比增长": latest_data.get('净利润同比增长', 'N/A'),
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"获取财务指标失败: {e}")
|
||||
|
||||
# 5. 获取季度业绩(尝试不同API)
|
||||
try:
|
||||
# 尝试获取业绩预告
|
||||
quarter_data = ak.stock_profit_forecast_em(symbol=symbol)
|
||||
if quarter_data is not None and not quarter_data.empty:
|
||||
financial_data["quarter_data"] = quarter_data.head(4).to_dict('records')
|
||||
except:
|
||||
try:
|
||||
# 备用方案:获取季度财报
|
||||
quarter_data = ak.stock_financial_report_sina(stock=symbol, symbol="季报")
|
||||
if quarter_data is not None and not quarter_data.empty:
|
||||
financial_data["quarter_data"] = quarter_data.head(4).to_dict('records')
|
||||
except Exception as e:
|
||||
print(f"获取季度数据失败: {e}")
|
||||
|
||||
return financial_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取中国股票财务数据失败: {e}")
|
||||
return financial_data
|
||||
|
||||
def _get_us_financial_data(self, symbol):
|
||||
"""获取美股财务数据"""
|
||||
financial_data = {
|
||||
"symbol": symbol,
|
||||
"balance_sheet": None,
|
||||
"income_statement": None,
|
||||
"cash_flow": None,
|
||||
"financial_ratios": {},
|
||||
"quarter_data": None,
|
||||
}
|
||||
|
||||
try:
|
||||
stock = yf.Ticker(symbol)
|
||||
info = stock.info
|
||||
|
||||
# 1. 资产负债表
|
||||
try:
|
||||
balance_sheet = stock.balance_sheet
|
||||
if balance_sheet is not None and not balance_sheet.empty:
|
||||
financial_data["balance_sheet"] = balance_sheet.iloc[:, :4].to_dict('index')
|
||||
except Exception as e:
|
||||
print(f"获取资产负债表失败: {e}")
|
||||
|
||||
# 2. 利润表
|
||||
try:
|
||||
income_stmt = stock.income_stmt
|
||||
if income_stmt is not None and not income_stmt.empty:
|
||||
financial_data["income_statement"] = income_stmt.iloc[:, :4].to_dict('index')
|
||||
except Exception as e:
|
||||
print(f"获取利润表失败: {e}")
|
||||
|
||||
# 3. 现金流量表
|
||||
try:
|
||||
cash_flow = stock.cashflow
|
||||
if cash_flow is not None and not cash_flow.empty:
|
||||
financial_data["cash_flow"] = cash_flow.iloc[:, :4].to_dict('index')
|
||||
except Exception as e:
|
||||
print(f"获取现金流量表失败: {e}")
|
||||
|
||||
# 4. 财务比率(从info中提取)
|
||||
financial_data["financial_ratios"] = {
|
||||
"ROE": info.get('returnOnEquity', 'N/A'),
|
||||
"ROA": info.get('returnOnAssets', 'N/A'),
|
||||
"毛利率": info.get('grossMargins', 'N/A'),
|
||||
"营业利润率": info.get('operatingMargins', 'N/A'),
|
||||
"净利率": info.get('profitMargins', 'N/A'),
|
||||
"资产负债率": info.get('debtToEquity', 'N/A'),
|
||||
"流动比率": info.get('currentRatio', 'N/A'),
|
||||
"速动比率": info.get('quickRatio', 'N/A'),
|
||||
"EPS": info.get('trailingEps', 'N/A'),
|
||||
"每股账面价值": info.get('bookValue', 'N/A'),
|
||||
"股息率": info.get('dividendYield', 'N/A'),
|
||||
"派息率": info.get('payoutRatio', 'N/A'),
|
||||
"收入增长": info.get('revenueGrowth', 'N/A'),
|
||||
"盈利增长": info.get('earningsGrowth', 'N/A'),
|
||||
}
|
||||
|
||||
return financial_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取美股财务数据失败: {e}")
|
||||
return financial_data
|
||||
@@ -0,0 +1,114 @@
|
||||
# 邮件通知配置指南
|
||||
|
||||
## 配置方法
|
||||
|
||||
### 方法一:环境变量配置(推荐)
|
||||
在项目根目录创建或修改 `.env` 文件,添加以下配置:
|
||||
|
||||
```env
|
||||
# 邮件通知配置
|
||||
EMAIL_ENABLED=true
|
||||
SMTP_SERVER=smtp.qq.com # 或 smtp.163.com, smtp.gmail.com 等
|
||||
SMTP_PORT=587
|
||||
EMAIL_FROM=your_email@qq.com
|
||||
EMAIL_PASSWORD=your_app_password # 注意:不是邮箱登录密码,是应用专用密码
|
||||
EMAIL_TO=receiver@example.com
|
||||
```
|
||||
|
||||
### 方法二:直接修改代码
|
||||
在 `notification_service.py` 的 `_load_config` 方法中修改默认配置:
|
||||
|
||||
```python
|
||||
config = {
|
||||
'email_enabled': True, # 启用邮件通知
|
||||
'smtp_server': 'smtp.qq.com',
|
||||
'smtp_port': 587,
|
||||
'email_from': 'your_email@qq.com',
|
||||
'email_password': 'your_app_password',
|
||||
'email_to': 'receiver@example.com',
|
||||
'webhook_enabled': False,
|
||||
'webhook_url': ''
|
||||
}
|
||||
```
|
||||
|
||||
## 各邮箱服务商配置示例
|
||||
|
||||
### QQ邮箱配置
|
||||
```env
|
||||
SMTP_SERVER=smtp.qq.com
|
||||
SMTP_PORT=587
|
||||
EMAIL_FROM=your_qq@qq.com
|
||||
EMAIL_PASSWORD=16位授权码 # 需要在QQ邮箱设置中获取
|
||||
```
|
||||
|
||||
### 163邮箱配置
|
||||
```env
|
||||
SMTP_SERVER=smtp.163.com
|
||||
SMTP_PORT=587
|
||||
EMAIL_FROM=your_email@163.com
|
||||
EMAIL_PASSWORD=客户端授权密码
|
||||
```
|
||||
|
||||
### Gmail配置
|
||||
```env
|
||||
SMTP_SERVER=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
EMAIL_FROM=your_email@gmail.com
|
||||
EMAIL_PASSWORD=应用专用密码
|
||||
```
|
||||
|
||||
## 获取邮箱授权码/应用密码
|
||||
|
||||
### QQ邮箱
|
||||
1. 登录QQ邮箱
|
||||
2. 进入"设置" → "账户"
|
||||
3. 找到"POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务"
|
||||
4. 开启"IMAP/SMTP服务"
|
||||
5. 生成16位授权码
|
||||
|
||||
### 163邮箱
|
||||
1. 登录163邮箱
|
||||
2. 进入"设置" → "POP3/SMTP/IMAP"
|
||||
3. 开启"IMAP/SMTP服务"
|
||||
4. 设置客户端授权密码
|
||||
|
||||
### Gmail
|
||||
1. 登录Gmail
|
||||
2. 进入"设置" → "账号和导入"
|
||||
3. 在"更改账号设置"中启用"两步验证"
|
||||
4. 生成应用专用密码
|
||||
|
||||
## 测试邮件配置
|
||||
|
||||
在Streamlit应用中,可以通过以下方式测试邮件配置:
|
||||
|
||||
1. 在监测面板中点击"测试邮件配置"按钮
|
||||
2. 或者在Python中运行测试代码:
|
||||
|
||||
```python
|
||||
from notification_service import notification_service
|
||||
if notification_service.test_email_config():
|
||||
print("✅ 邮件配置正常")
|
||||
else:
|
||||
print("❌ 邮件配置有问题")
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **安全提醒**:不要在代码中硬编码邮箱密码,使用环境变量
|
||||
2. **应用密码**:使用应用专用密码,不要使用邮箱登录密码
|
||||
3. **端口设置**:通常使用587端口(TLS加密)
|
||||
4. **防火墙**:确保服务器可以访问SMTP服务商的端口
|
||||
5. **垃圾邮件**:首次使用可能被标记为垃圾邮件,请检查垃圾邮件文件夹
|
||||
|
||||
## 故障排除
|
||||
|
||||
如果邮件发送失败,检查以下事项:
|
||||
|
||||
1. SMTP服务器地址和端口是否正确
|
||||
2. 邮箱账号和密码是否正确
|
||||
3. 是否开启了SMTP服务
|
||||
4. 网络连接是否正常
|
||||
5. 防火墙是否阻止了SMTP连接
|
||||
|
||||
如果需要帮助,请查看详细的错误日志信息。
|
||||
Reference in New Issue
Block a user