初始提交

This commit is contained in:
oficcejo
2025-10-04 11:01:37 +08:00
commit f52c3ef5b3
29 changed files with 7023 additions and 0 deletions
+18
View File
@@ -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
View File
@@ -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
+105
View File
@@ -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
View File
@@ -0,0 +1,106 @@
# 更新日志
## 2025-10-03
### ✨ 新增功能
#### 增强基本面分析
- 📊 **详细财务数据获取**
- 资产负债表:完整的资产和负债结构
- 利润表:营收、成本、利润明细
- 现金流量表:经营、投资、筹资现金流
- 💰 **全面财务指标**
- 盈利能力:ROE、ROA、毛利率、净利率
- 偿债能力:资产负债率、流动比率、速动比率
- 运营能力:存货周转率、应收账款周转率、总资产周转率
- 成长能力:营收增长率、利润增长率
- 每股指标:EPS、每股账面价值、股息率、派息率
- 🎯 **深度分析维度**
- 公司质地分析(业务模式、竞争力、护城河)
- 盈利能力深度分析
- 财务健康度评估
- 成长性分析
- 估值分析(历史对比、行业对比)
- 投资价值综合判断
- 🔄 **数据源优化**
- A股:支持同花顺财务数据API
- 美股:yfinance完整财务报表
- 自动处理API变化和异常
#### 实时监测管理系统
- 📊 **监测股票管理**:添加、编辑、删除监测股票
- 🎯 **关键位置设置**:进场区间、止盈位、止损位
-**自定义监测间隔**30秒至300秒可选
- 🎨 **卡片式布局**:现代化的股票监测卡片展示
- 🔍 **搜索过滤功能**:快速查找监测股票
#### 通知系统
- 📧 **邮件通知**:价格触发关键位置时发送邮件
- 🌐 **网页通知**:实时界面消息提醒
- 🔔 **通知开关**:每只股票可独立控制通知
- 📱 **通知历史**:查看所有通知记录
#### PDF报告导出
- 📄 **完整分析报告**:一键生成PDF格式报告
- 🎨 **中文字体支持**:正确显示中文内容
- 📊 **包含所有分析**:技术、基本、资金、风险、情绪分析
### 🔧 功能改进
#### 邮件配置
- ✅ 从`.env`文件读取邮件配置
- ✅ 支持TLS587端口)和SSL465端口)
- ✅ 测试邮件发送功能
- ✅ 邮件配置状态显示
- ✅ 详细的配置说明
#### 监测服务
- ✅ 后台线程自动监测
- ✅ 启动/停止监测服务
- ✅ 手动更新股票价格
- ✅ 实时价格显示
#### 数据库管理
- ✅ 监测股票数据持久化
- ✅ 价格历史记录
- ✅ 通知历史管理
- ✅ 标记已读/清空通知
### 🐛 问题修复
- 修复删除股票功能返回值问题
- 修复编辑对话框表单提交逻辑
- 修复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 添加监测功能说明
- 更新系统架构图
- 添加邮件配置说明
### 🎯 下一步计划
- [ ] 添加价格预警阈值(百分比)
- [ ] 支持多个接收邮箱
- [ ] 添加监测统计图表
- [ ] 导出监测历史数据
- [ ] 微信/钉钉通知集成
+122
View File
@@ -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
+397
View File
@@ -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
+236
View File
@@ -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个月
### 美股数据
- **来源**yfinanceYahoo 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
View File
@@ -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
View File
@@ -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 了解更新内容
- 查看界面内的配置说明
---
**祝您使用愉快!📈**
+433
View File
@@ -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驱动的智能股票分析体验!** 🚀📈
+223
View File
@@ -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
View File
@@ -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
+1119
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -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
View File
@@ -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()
+346
View File
@@ -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
View File
@@ -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()
+552
View File
@@ -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
+148
View File
@@ -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
View File
@@ -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
+242
View File
@@ -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()
+282
View File
@@ -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()}")
+340
View File
@@ -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()}")
+279
View File
@@ -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)
+14
View File
@@ -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
+69
View File
@@ -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()
+90
View File
@@ -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
View File
@@ -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
+114
View File
@@ -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连接
如果需要帮助,请查看详细的错误日志信息。