commit f52c3ef5b34a960402b3d38fd7e0438a0a82d27e Author: oficcejo Date: Sat Oct 4 11:01:37 2025 +0800 初始提交 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..11b1ca7 --- /dev/null +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..984e139 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/BUGFIX.md b/BUGFIX.md new file mode 100644 index 0000000..6067d4d --- /dev/null +++ b/BUGFIX.md @@ -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 + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ab2b9cc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,106 @@ +# 更新日志 + +## 2025-10-03 + +### ✨ 新增功能 + +#### 增强基本面分析 +- 📊 **详细财务数据获取** + - 资产负债表:完整的资产和负债结构 + - 利润表:营收、成本、利润明细 + - 现金流量表:经营、投资、筹资现金流 + +- 💰 **全面财务指标** + - 盈利能力:ROE、ROA、毛利率、净利率 + - 偿债能力:资产负债率、流动比率、速动比率 + - 运营能力:存货周转率、应收账款周转率、总资产周转率 + - 成长能力:营收增长率、利润增长率 + - 每股指标:EPS、每股账面价值、股息率、派息率 + +- 🎯 **深度分析维度** + - 公司质地分析(业务模式、竞争力、护城河) + - 盈利能力深度分析 + - 财务健康度评估 + - 成长性分析 + - 估值分析(历史对比、行业对比) + - 投资价值综合判断 + +- 🔄 **数据源优化** + - A股:支持同花顺财务数据API + - 美股:yfinance完整财务报表 + - 自动处理API变化和异常 + +#### 实时监测管理系统 +- 📊 **监测股票管理**:添加、编辑、删除监测股票 +- 🎯 **关键位置设置**:进场区间、止盈位、止损位 +- ⏰ **自定义监测间隔**:30秒至300秒可选 +- 🎨 **卡片式布局**:现代化的股票监测卡片展示 +- 🔍 **搜索过滤功能**:快速查找监测股票 + +#### 通知系统 +- 📧 **邮件通知**:价格触发关键位置时发送邮件 +- 🌐 **网页通知**:实时界面消息提醒 +- 🔔 **通知开关**:每只股票可独立控制通知 +- 📱 **通知历史**:查看所有通知记录 + +#### PDF报告导出 +- 📄 **完整分析报告**:一键生成PDF格式报告 +- 🎨 **中文字体支持**:正确显示中文内容 +- 📊 **包含所有分析**:技术、基本、资金、风险、情绪分析 + +### 🔧 功能改进 + +#### 邮件配置 +- ✅ 从`.env`文件读取邮件配置 +- ✅ 支持TLS(587端口)和SSL(465端口) +- ✅ 测试邮件发送功能 +- ✅ 邮件配置状态显示 +- ✅ 详细的配置说明 + +#### 监测服务 +- ✅ 后台线程自动监测 +- ✅ 启动/停止监测服务 +- ✅ 手动更新股票价格 +- ✅ 实时价格显示 + +#### 数据库管理 +- ✅ 监测股票数据持久化 +- ✅ 价格历史记录 +- ✅ 通知历史管理 +- ✅ 标记已读/清空通知 + +### 🐛 问题修复 +- 修复删除股票功能返回值问题 +- 修复编辑对话框表单提交逻辑 +- 修复PDF中文乱码问题 +- 修复报告重复显示问题 +- 修复按钮key重复问题 + +### 📝 配置文件 + +#### .env 配置示例 +```env +# DeepSeek API(必需) +DEEPSEEK_API_KEY=your_api_key_here + +# 邮件通知(可选) +EMAIL_ENABLED=true +SMTP_SERVER=smtp.qq.com +SMTP_PORT=587 +EMAIL_FROM=your_email@qq.com +EMAIL_PASSWORD=your_authorization_code +EMAIL_TO=receiver@example.com +``` + +### 📚 文档更新 +- 更新 README.md 添加监测功能说明 +- 更新系统架构图 +- 添加邮件配置说明 + +### 🎯 下一步计划 +- [ ] 添加价格预警阈值(百分比) +- [ ] 支持多个接收邮箱 +- [ ] 添加监测统计图表 +- [ ] 导出监测历史数据 +- [ ] 微信/钉钉通知集成 + diff --git a/DEEPSEEK_REASONER_FIX.md b/DEEPSEEK_REASONER_FIX.md new file mode 100644 index 0000000..6c4386f --- /dev/null +++ b/DEEPSEEK_REASONER_FIX.md @@ -0,0 +1,122 @@ +# DeepSeek Reasoner 模型输出不全问题修复说明 + +## 问题描述 + +使用 `deepseek-chat` 模型输出正常,但使用 `deepseek-reasoner` 模型进行技术面分析、基本面分析等各个分析时,输出文字不全。 + +## 问题原因 + +**deepseek-reasoner** 模型与 **deepseek-chat** 模型有重要区别: + +1. **响应结构不同**: + - `deepseek-chat`: 只返回 `content`(最终答案) + - `deepseek-reasoner`: 返回 `reasoning_content`(推理过程)+ `content`(最终答案) + +2. **Token 需求更大**: + - reasoner 模型需要输出详细的推理过程,需要更多的 tokens + - 原代码中 `max_tokens=2000` 对 reasoner 模型来说太小,导致输出被截断 + +## 解决方案 + +### 1. 自动调整 max_tokens(已修复) + +在 `deepseek_client.py` 的 `call_api` 方法中: + +```python +# 对于 reasoner 模型,自动增加 max_tokens +if "reasoner" in model_to_use.lower() and max_tokens <= 2000: + max_tokens = 8000 # reasoner 模型需要更多 tokens 来输出推理过程 +``` + +### 2. 正确处理 reasoner 响应(已修复) + +```python +# 处理 reasoner 模型的响应 +message = response.choices[0].message +result = "" + +# 检查是否有推理内容 +if hasattr(message, 'reasoning_content') and message.reasoning_content: + result += f"【推理过程】\n{message.reasoning_content}\n\n" + +# 添加最终内容 +if message.content: + result += message.content +``` + +### 3. 提高各分析模块的 max_tokens(已修复) + +| 分析模块 | 原 max_tokens | 新 max_tokens | +|---------|--------------|--------------| +| 技术面分析 | 2000 | 8000 (自动) | +| 基本面分析 | 2000 | 8000 (自动) | +| 资金面分析 | 2000 | 8000 (自动) | +| 风险管理分析 | 2000 | 8000 (自动) | +| 市场情绪分析 | 2000 | 8000 (自动) | +| 综合讨论 | 3000 | 6000 | +| 最终决策 | 2000 | 4000 | +| 团队讨论 | 3000 | 6000 | + +## 修改的文件 + +1. **deepseek_client.py** + - `call_api()` 方法:增加 reasoner 检测和 max_tokens 自动调整 + - `call_api()` 方法:增加 reasoning_content 处理 + - `comprehensive_discussion()` 方法:max_tokens 从 3000 增加到 6000 + - `final_decision()` 方法:max_tokens 从 2000 增加到 4000 + +2. **ai_agents.py** + - `conduct_team_discussion()` 方法:max_tokens 从 3000 增加到 6000 + +## 使用建议 + +### 1. 模型选择 + +- **deepseek-chat**: + - 适用场景:快速分析、常规查询 + - 优点:响应快,成本低 + - 缺点:推理能力相对较弱 + +- **deepseek-reasoner**: + - 适用场景:复杂分析、需要深度推理的场景 + - 优点:推理能力强,分析更深入 + - 缺点:响应时间较长,token 消耗更多 + +### 2. 查看推理过程 + +使用 reasoner 模型时,响应中会包含 `【推理过程】` 部分,展示 AI 的思考过程,帮助你理解分析的逻辑。 + +### 3. Token 消耗 + +reasoner 模型的 token 消耗通常是 chat 模型的 2-4 倍,请注意 API 使用成本。 + +## 验证方法 + +1. 在应用中选择 "DeepSeek Reasoner (推理增强)" 模型 +2. 输入股票代码进行分析 +3. 检查各个分析报告是否完整输出 +4. 查看是否包含 `【推理过程】` 部分 + +## 预期效果 + +修复后,使用 deepseek-reasoner 模型应该能够: +- ✅ 完整输出技术面分析报告 +- ✅ 完整输出基本面分析报告 +- ✅ 完整输出资金面分析报告 +- ✅ 完整输出风险管理报告 +- ✅ 完整输出市场情绪分析报告 +- ✅ 完整输出综合讨论结果 +- ✅ 完整输出最终投资决策 +- ✅ 显示推理过程(如果 API 支持) + +## 注意事项 + +1. **API 兼容性**:确保使用的 DeepSeek API 版本支持 reasoner 模型 +2. **Token 限制**:如果仍然出现截断,可以进一步增加 max_tokens +3. **成本控制**:reasoner 模型消耗更多 tokens,注意 API 配额 +4. **响应时间**:reasoner 模型需要更多时间进行推理,请耐心等待 + +## 更新日期 + +2025-10-03 + diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..0d45e39 --- /dev/null +++ b/DEPLOYMENT_GUIDE.md @@ -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 +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 + diff --git a/FINANCIAL_ANALYSIS_GUIDE.md b/FINANCIAL_ANALYSIS_GUIDE.md new file mode 100644 index 0000000..a2e5864 --- /dev/null +++ b/FINANCIAL_ANALYSIS_GUIDE.md @@ -0,0 +1,236 @@ +# 📊 基本面分析功能指南 + +## 功能概述 + +本系统的基本面分析功能已经全面升级,能够获取并分析详细的财务数据,为投资决策提供更深入的支持。 + +## 📈 数据获取范围 + +### 三大财务报表 + +#### 1. 资产负债表 +- 总资产、总负债、股东权益 +- 流动资产、非流动资产 +- 流动负债、非流动负债 +- 资产负债结构分析 + +#### 2. 利润表 +- 营业收入、营业成本 +- 毛利润、营业利润、净利润 +- 各项费用明细 +- 盈利能力趋势 + +#### 3. 现金流量表 +- 经营活动现金流 +- 投资活动现金流 +- 筹资活动现金流 +- 现金流健康度分析 + +### 财务指标体系 + +#### 盈利能力指标 +| 指标 | 说明 | 优秀标准 | +|------|------|----------| +| ROE(净资产收益率) | 衡量股东投资回报率 | >15% | +| ROA(总资产收益率) | 衡量资产使用效率 | >5% | +| 销售毛利率 | 产品附加值和定价能力 | 因行业而异 | +| 销售净利率 | 综合盈利能力 | >10% | + +#### 偿债能力指标 +| 指标 | 说明 | 安全标准 | +|------|------|----------| +| 资产负债率 | 负债占总资产比例 | <60% | +| 流动比率 | 短期偿债能力 | >1.5 | +| 速动比率 | 更严格的短期偿债能力 | >1.0 | + +#### 运营能力指标 +| 指标 | 说明 | 意义 | +|------|------|------| +| 存货周转率 | 存货管理效率 | 越高越好 | +| 应收账款周转率 | 账款回收能力 | 越高越好 | +| 总资产周转率 | 资产使用效率 | 越高越好 | + +#### 成长能力指标 +| 指标 | 说明 | 优秀标准 | +|------|------|----------| +| 营业收入同比增长 | 收入增长速度 | >20% | +| 净利润同比增长 | 利润增长速度 | >20% | + +#### 每股指标 +| 指标 | 说明 | 作用 | +|------|------|------| +| EPS(每股收益) | 每股盈利能力 | 估值基础 | +| 每股账面价值 | 每股净资产 | 安全边际 | +| 股息率 | 分红回报率 | 现金回报 | +| 派息率 | 分红比例 | 分红政策 | + +## 🎯 分析维度 + +### 1. 公司质地分析 +- **业务模式**:商业模式和盈利来源 +- **核心竞争力**:技术、品牌、规模优势 +- **护城河**:竞争壁垒和持续性 +- **行业地位**:市场份额和龙头地位 + +### 2. 盈利能力分析 +- ROE/ROA水平评估 +- 毛利率和净利率趋势 +- 与行业平均水平对比 +- 盈利质量和持续性判断 + +### 3. 财务健康度分析 +- 资产负债结构合理性 +- 偿债能力充足性 +- 现金流充裕程度 +- 财务风险识别 + +### 4. 成长性分析 +- 收入和利润增长趋势 +- 增长驱动因素分析 +- 未来成长空间预测 +- 行业发展前景评估 + +### 5. 估值分析 +- 当前估值水平(PE、PB) +- 历史估值区间对比 +- 行业估值对比 +- 合理估值区间判断 + +### 6. 投资价值判断 +- 综合评分(0-100分) +- 投资亮点总结 +- 投资风险提示 +- 适合的投资者类型 + +## 💡 使用建议 + +### 看财务数据的顺序 + +1. **先看盈利能力** + - ROE是否稳定且>15% + - 毛利率是否维持高位 + - 净利率是否稳定增长 + +2. **再看成长性** + - 营收和利润是否持续增长 + - 增长是否健康可持续 + - 是否有新的增长动力 + +3. **然后看财务健康度** + - 资产负债率是否合理 + - 流动比率是否充足 + - 现金流是否健康 + +4. **最后看估值** + - PE/PB是否处于合理区间 + - 相对历史估值的位置 + - 相对行业估值的位置 + +### 财务数据解读技巧 + +#### 高质量公司特征 +- ✅ ROE稳定在15%以上 +- ✅ 毛利率和净利率稳定或上升 +- ✅ 收入和利润持续增长 +- ✅ 资产负债率<60% +- ✅ 经营现金流充足 +- ✅ 应收账款周转率高 + +#### 风险信号 +- ⚠️ ROE持续下降 +- ⚠️ 毛利率大幅下滑 +- ⚠️ 收入增长但利润不增长 +- ⚠️ 资产负债率>70% +- ⚠️ 经营现金流为负 +- ⚠️ 应收账款大量增加 + +## 📚 数据源说明 + +### A股数据 +- **来源**:AKShare(东方财富、同花顺接口) +- **更新频率**:季度财报 +- **数据滞后**:约1-2个月 + +### 美股数据 +- **来源**:yfinance(Yahoo Finance) +- **更新频率**:季度财报 +- **数据滞后**:约1-2个月 + +## ⚠️ 注意事项 + +1. **财务数据有滞后性** + - 季报发布后1-2个月才能获取 + - 最新数据可能不是最近一个季度 + +2. **不同行业标准不同** + - 轻资产行业(软件、服务):高ROE、高毛利率 + - 重资产行业(制造、地产):ROE相对较低 + - 周期性行业:财务指标波动大 + +3. **单一指标不能说明全部问题** + - 需要综合多个指标判断 + - 需要看趋势而非单点数据 + - 需要与行业对比分析 + +4. **特殊情况处理** + - 新股可能没有完整财务数据 + - ST股票财务数据异常 + - 重组并购期间数据扭曲 + +## 🚀 最佳实践 + +### 分析流程建议 + +1. **快速筛选**(5分钟) + - 查看ROE、毛利率、净利率 + - 看收入和利润增长率 + - 检查资产负债率 + +2. **深入分析**(15分钟) + - 分析盈利能力趋势 + - 评估财务健康度 + - 研究成长性和驱动因素 + +3. **综合判断**(10分钟) + - 结合估值分析 + - 识别投资风险 + - 做出投资决策 + +### 配合其他分析 + +基本面分析需要与其他维度结合: +- **技术面**:把握买卖时机 +- **资金面**:确认主力动向 +- **市场情绪**:理解短期波动 +- **风险管理**:控制投资风险 + +## 📞 常见问题 + +### Q1: 为什么有些财务数据显示N/A? +A: 可能原因: +- 新上市公司数据不全 +- 数据接口暂时无法获取 +- 该公司未披露该项数据 + +### Q2: 财务数据多久更新一次? +A: +- 上市公司季度财报发布后1-2个月 +- 系统实时获取最新可用数据 + +### Q3: 如何判断财务数据的可靠性? +A: +- 对比多个数据源 +- 关注会计师事务所意见 +- 留意财务报表附注 +- 关注媒体和监管公告 + +### Q4: ROE多少算优秀? +A: +- 一般标准:>15%为优秀 +- 看行业:高科技可以>20%,传统行业10-15%也不错 +- 看稳定性:持续5年ROE>15%更可靠 + +--- + +**提示**:财务分析是价值投资的基础,但不是全部。投资决策需要综合考虑多方面因素,理性判断,谨慎决策。 + diff --git a/QUICK_FIX.md b/QUICK_FIX.md new file mode 100644 index 0000000..2c94410 --- /dev/null +++ b/QUICK_FIX.md @@ -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 + +--- + +**提示**:大多数问题都可以通过重新安装依赖或重启应用解决! + diff --git a/QUICK_START.md b/QUICK_START.md new file mode 100644 index 0000000..f31510f --- /dev/null +++ b/QUICK_START.md @@ -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 了解更新内容 +- 查看界面内的配置说明 + +--- + +**祝您使用愉快!📈** + diff --git a/README.md b/README.md new file mode 100644 index 0000000..68550ab --- /dev/null +++ b/README.md @@ -0,0 +1,433 @@ +# 🤖 复合多AI智能体股票团队分析系统 + +基于Python + Streamlit + DeepSeek的智能股票分析系统,模拟证券公司分析师团队,提供全方位的股票投资分析和决策建议。 +image +image + + +## ✨ 功能特色 + +### 🎯 多维度分析 +- **技术面分析**:趋势判断、技术指标、支撑阻力位分析 +- **基本面分析**:财务指标、估值分析、行业研究 +- **资金面分析**:资金流向、主力行为、市场情绪 +- **风险管理**:风险识别、风险评估、风险控制策略 +- **市场情绪**:投资者情绪、热点板块、消息面分析 + +### 🤖 AI智能体团队 +- **技术分析师**:专注技术指标和图表分析 +- **基本面分析师**:专注公司价值和行业研究(含13+财务指标) +- **资金面分析师**:专注资金流向和主力行为 +- **风险管理师**:专注风险识别和控制 +- **市场情绪分析师**:专注市场心理和热点追踪 + +### 📊 完整分析流程 +1. 📈 获取股票数据(支持A股和美股) +2. 📊 获取详细财务数据(三大报表+财务指标) +3. 🔍 多智能体并行分析 +4. 🤝 团队综合讨论 +5. 📋 最终投资决策 +6. 🎯 操作建议和风险提示 +7. 📄 PDF报告导出 +image +image +image +image +image + + +### 🔍 实时监测功能 +- **智能监测**:自动监控股票价格变动 +- **关键位置提醒**:进场区间、止盈位、止损位触发通知 +- **自定义间隔**:30秒至300秒灵活设置 +- **多种通知方式**:网页提醒 + 邮件通知 +- **卡片式管理**:直观的股票监测卡片展示 +- **完整功能**:添加、编辑、删除、启停、通知开关 +image +image + +### 📧 邮件通知系统 +- **多邮箱支持**:QQ邮箱、163邮箱、Gmail等 +- **触发条件**: + - 价格进入进场区间 + - 达到止盈位 + - 触及止损位 +- **测试功能**:一键测试邮件配置 +- **历史记录**:完整的通知历史查询 +image +image +### 🎨 现代化界面 +- **渐变背景设计**:专业的紫色渐变配色 +- **响应式布局**:支持桌面、平板、手机 +- **实时数据可视化**: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驱动的智能股票分析体验!** 🚀📈 diff --git a/UI_IMPROVEMENTS.md b/UI_IMPROVEMENTS.md new file mode 100644 index 0000000..346b6ef --- /dev/null +++ b/UI_IMPROVEMENTS.md @@ -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实现 + +--- + +**设计理念**:简洁、现代、专业、易用 + diff --git a/ai_agents.py b/ai_agents.py new file mode 100644 index 0000000..0cd5bef --- /dev/null +++ b/ai_agents.py @@ -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 diff --git a/app.py b/app.py new file mode 100644 index 0000000..829b827 --- /dev/null +++ b/app.py @@ -0,0 +1,1119 @@ +import streamlit as st +import plotly.graph_objects as go +import plotly.express as px +import pandas as pd +import json +from datetime import datetime +import time +import base64 +import os + +from stock_data import StockDataFetcher +from ai_agents import StockAnalysisAgents +from pdf_generator import display_pdf_export_section +from database import db +from monitor_manager import display_monitor_manager, get_monitor_summary +from monitor_service import monitor_service +from notification_service import notification_service + +# 页面配置 +st.set_page_config( + page_title="复合多AI智能体股票团队分析系统", + page_icon="📈", + layout="wide", + initial_sidebar_state="expanded" +) + +# 模型选择器 +def model_selector(): + """模型选择器""" + st.sidebar.markdown("---") + st.sidebar.subheader("🤖 AI模型选择") + + model_options = { + "deepseek-chat": "DeepSeek Chat (默认)", + "deepseek-reasoner": "DeepSeek Reasoner (推理增强)" + } + + selected_model = st.sidebar.selectbox( + "选择AI模型", + options=list(model_options.keys()), + format_func=lambda x: model_options[x], + help="DeepSeek Reasoner提供更强的推理能力,但响应时间可能更长" + ) + + return selected_model + +# 自定义CSS样式 - 专业版 +st.markdown(""" + +""", unsafe_allow_html=True) + +def main(): + # 顶部标题栏 + st.markdown(""" +
+

📈 复合多AI智能体股票团队分析系统

+ +
+ """, unsafe_allow_html=True) + + # 侧边栏 + with st.sidebar: + # 快捷导航 - 移到顶部 + st.markdown("### 🔍 快捷导航") + + if st.button("📖 历史记录", use_container_width=True, key="nav_history"): + st.session_state.show_history = True + if 'show_monitor' in st.session_state: + del st.session_state.show_monitor + + if st.button("📊 实时监测", use_container_width=True, key="nav_monitor"): + st.session_state.show_monitor = True + if 'show_history' in st.session_state: + del st.session_state.show_history + + if st.button("🏠 返回首页", use_container_width=True, key="nav_home"): + if 'show_history' in st.session_state: + del st.session_state.show_history + if 'show_monitor' in st.session_state: + del st.session_state.show_monitor + + st.markdown("---") + + # 系统配置 + st.markdown("### ⚙️ 系统配置") + + # API密钥检查 + api_key_status = check_api_key() + if api_key_status: + st.success("✅ API已连接") + else: + st.error("❌ API未配置") + st.caption("请在.env中配置API密钥") + + st.markdown("---") + + # 模型选择器 + selected_model = model_selector() + st.session_state.selected_model = selected_model + + st.markdown("---") + + # 系统状态面板 + st.markdown("### 📊 系统状态") + + monitor_status = "🟢 运行中" if monitor_service.running else "🔴 已停止" + st.markdown(f"**监测服务**: {monitor_status}") + + try: + from monitor_db import monitor_db + stocks = monitor_db.get_monitored_stocks() + notifications = monitor_db.get_pending_notifications() + record_count = db.get_record_count() + + st.markdown(f"**分析记录**: {record_count}条") + st.markdown(f"**监测股票**: {len(stocks)}只") + st.markdown(f"**待处理**: {len(notifications)}条") + except: + pass + + st.markdown("---") + + # 分析参数设置 + st.markdown("### 📊 分析参数") + period = st.selectbox( + "数据周期", + ["1y", "6mo", "3mo", "1mo"], + index=0, + help="选择历史数据的时间范围" + ) + + st.markdown("---") + + # 帮助信息 + with st.expander("💡 使用帮助"): + st.markdown(""" + **股票代码格式** + - 🇨🇳 A股:6位数字(如600519) + - 🇺🇸 美股:字母代码(如AAPL) + + **功能说明** + - **智能分析**:AI团队深度分析 + - **实时监测**:价格监控与提醒 + - **历史记录**:查看分析历史 + + **AI分析流程** + 1. 数据获取 → 2. 技术分析 + 3. 基本面分析 → 4. 资金分析 + 5. 风险评估 → 6. 情绪分析 + 7. 团队讨论 → 8. 最终决策 + """) + + # 检查是否显示历史记录 + if 'show_history' in st.session_state and st.session_state.show_history: + display_history_records() + return + + # 检查是否显示监测面板 + if 'show_monitor' in st.session_state and st.session_state.show_monitor: + display_monitor_manager() + return + + # 主界面 + col1, col2, col3 = st.columns([2, 1, 1]) + + with col1: + stock_input = st.text_input( + "🔍 请输入股票代码或名称", + placeholder="例如: AAPL, 000001, 600036", + help="支持美股代码(如AAPL)和A股代码(如000001)" + ) + + with col2: + analyze_button = st.button("🚀 开始分析", type="primary", use_container_width=True) + + with col3: + if st.button("🔄 清除缓存", use_container_width=True): + st.cache_data.clear() + st.success("缓存已清除") + + if analyze_button and stock_input: + if not api_key_status: + st.error("❌ 请先配置 DeepSeek API Key") + return + + # 清除之前的分析结果 + if 'analysis_completed' in st.session_state: + del st.session_state.analysis_completed + if 'stock_info' in st.session_state: + del st.session_state.stock_info + if 'agents_results' in st.session_state: + del st.session_state.agents_results + if 'discussion_result' in st.session_state: + del st.session_state.discussion_result + if 'final_decision' in st.session_state: + del st.session_state.final_decision + + run_stock_analysis(stock_input, period) + + # 检查是否有已完成的分析结果 + if 'analysis_completed' in st.session_state and st.session_state.analysis_completed: + # 重新显示分析结果 + stock_info = st.session_state.stock_info + agents_results = st.session_state.agents_results + discussion_result = st.session_state.discussion_result + final_decision = st.session_state.final_decision + + # 重新获取股票数据用于显示图表 + stock_info_current, stock_data, indicators = get_stock_data(stock_info['symbol'], period) + + # 显示股票基本信息 + display_stock_info(stock_info, indicators) + + # 显示股票图表 + if stock_data is not None: + display_stock_chart(stock_data, stock_info) + + # 显示各分析师报告 + display_agents_analysis(agents_results) + + # 显示团队讨论 + display_team_discussion(discussion_result) + + # 显示最终决策 + display_final_decision(final_decision, stock_info, agents_results, discussion_result) + + # 示例和说明 + elif not stock_input: + show_example_interface() + +def check_api_key(): + """检查API密钥是否配置""" + try: + import config + return bool(config.DEEPSEEK_API_KEY and config.DEEPSEEK_API_KEY.strip()) + except: + return False + +@st.cache_data(ttl=300) # 缓存5分钟 +def get_stock_data(symbol, period): + """获取股票数据(带缓存)""" + fetcher = StockDataFetcher() + stock_info = fetcher.get_stock_info(symbol) + stock_data = fetcher.get_stock_data(symbol, period) + + if isinstance(stock_data, dict) and "error" in stock_data: + return stock_info, None, None + + stock_data_with_indicators = fetcher.calculate_technical_indicators(stock_data) + indicators = fetcher.get_latest_indicators(stock_data_with_indicators) + + return stock_info, stock_data_with_indicators, indicators + +def run_stock_analysis(symbol, period): + """运行股票分析""" + + # 进度条 + progress_bar = st.progress(0) + status_text = st.empty() + + try: + # 1. 获取股票数据 + status_text.text("📈 正在获取股票数据...") + progress_bar.progress(10) + + stock_info, stock_data, indicators = get_stock_data(symbol, period) + + if "error" in stock_info: + st.error(f"❌ {stock_info['error']}") + return + + if stock_data is None: + st.error("❌ 无法获取股票历史数据") + return + + # 显示股票基本信息 + display_stock_info(stock_info, indicators) + progress_bar.progress(20) + + # 显示股票图表 + display_stock_chart(stock_data, stock_info) + progress_bar.progress(30) + + # 2. 获取财务数据 + status_text.text("📊 正在获取财务数据...") + fetcher = StockDataFetcher() # 创建fetcher实例 + financial_data = fetcher.get_financial_data(symbol) + progress_bar.progress(35) + + # 3. 初始化AI分析系统 + status_text.text("🤖 正在初始化AI分析系统...") + # 使用选择的模型 + selected_model = st.session_state.get('selected_model', 'deepseek-chat') + agents = StockAnalysisAgents(model=selected_model) + progress_bar.progress(45) + + # 4. 运行多智能体分析 + status_text.text("🔍 AI分析师团队正在分析...") + agents_results = agents.run_multi_agent_analysis(stock_info, stock_data, indicators, financial_data) + progress_bar.progress(70) + + # 显示各分析师报告 + display_agents_analysis(agents_results) + + # 5. 团队讨论 + status_text.text("🤝 分析团队正在讨论...") + discussion_result = agents.conduct_team_discussion(agents_results, stock_info) + progress_bar.progress(85) + + # 显示团队讨论 + display_team_discussion(discussion_result) + + # 6. 最终决策 + status_text.text("📋 正在制定最终投资决策...") + final_decision = agents.make_final_decision(discussion_result, stock_info, indicators) + progress_bar.progress(100) + + # 保存分析结果到session_state + st.session_state.analysis_completed = True + st.session_state.stock_info = stock_info + st.session_state.agents_results = agents_results + st.session_state.discussion_result = discussion_result + st.session_state.final_decision = final_decision + + # 保存到数据库 + try: + db.save_analysis( + symbol=stock_info.get('symbol', ''), + stock_name=stock_info.get('name', ''), + period=period, + stock_info=stock_info, + agents_results=agents_results, + discussion_result=discussion_result, + final_decision=final_decision + ) + st.success("✅ 分析记录已保存到数据库") + except Exception as e: + st.warning(f"⚠️ 保存到数据库时出现错误: {str(e)}") + + # 显示最终决策 + display_final_decision(final_decision, stock_info, agents_results, discussion_result) + + status_text.text("✅ 分析完成!") + time.sleep(1) + status_text.empty() + progress_bar.empty() + + except Exception as e: + st.error(f"❌ 分析过程中出现错误: {str(e)}") + progress_bar.empty() + status_text.empty() + +def display_stock_info(stock_info, indicators): + """显示股票基本信息""" + st.subheader(f"📊 {stock_info.get('name', 'N/A')} ({stock_info.get('symbol', 'N/A')})") + + # 基本信息卡片 + col1, col2, col3, col4, col5 = st.columns(5) + + with col1: + current_price = stock_info.get('current_price', 'N/A') + st.metric("当前价格", f"{current_price}") + + with col2: + change_percent = stock_info.get('change_percent', 'N/A') + if isinstance(change_percent, (int, float)): + st.metric("涨跌幅", f"{change_percent:.2f}%", f"{change_percent:.2f}%") + else: + st.metric("涨跌幅", f"{change_percent}") + + with col3: + pe_ratio = stock_info.get('pe_ratio', 'N/A') + st.metric("市盈率", f"{pe_ratio}") + + with col4: + pb_ratio = stock_info.get('pb_ratio', 'N/A') + st.metric("市净率", f"{pb_ratio}") + + with col5: + market_cap = stock_info.get('market_cap', 'N/A') + if isinstance(market_cap, (int, float)): + market_cap_str = f"{market_cap/1e9:.2f}B" if market_cap > 1e9 else f"{market_cap/1e6:.2f}M" + st.metric("市值", market_cap_str) + else: + st.metric("市值", f"{market_cap}") + + # 技术指标 + if indicators and not isinstance(indicators, dict) or "error" not in indicators: + st.subheader("📈 关键技术指标") + + col1, col2, col3, col4 = st.columns(4) + + with col1: + rsi = indicators.get('rsi', 'N/A') + if isinstance(rsi, (int, float)): + rsi_color = "normal" + if rsi > 70: + rsi_color = "inverse" + elif rsi < 30: + rsi_color = "off" + st.metric("RSI", f"{rsi:.2f}") + else: + st.metric("RSI", f"{rsi}") + + with col2: + ma20 = indicators.get('ma20', 'N/A') + if isinstance(ma20, (int, float)): + st.metric("MA20", f"{ma20:.2f}") + else: + st.metric("MA20", f"{ma20}") + + with col3: + volume_ratio = indicators.get('volume_ratio', 'N/A') + if isinstance(volume_ratio, (int, float)): + st.metric("量比", f"{volume_ratio:.2f}") + else: + st.metric("量比", f"{volume_ratio}") + + with col4: + macd = indicators.get('macd', 'N/A') + if isinstance(macd, (int, float)): + st.metric("MACD", f"{macd:.4f}") + else: + st.metric("MACD", f"{macd}") + +def display_stock_chart(stock_data, stock_info): + """显示股票图表""" + st.subheader("📈 股价走势图") + + # 创建蜡烛图 + fig = go.Figure() + + # 添加蜡烛图 + fig.add_trace(go.Candlestick( + x=stock_data.index, + open=stock_data['Open'], + high=stock_data['High'], + low=stock_data['Low'], + close=stock_data['Close'], + name="K线" + )) + + # 添加移动平均线 + if 'MA5' in stock_data.columns: + fig.add_trace(go.Scatter( + x=stock_data.index, + y=stock_data['MA5'], + name="MA5", + line=dict(color='orange', width=1) + )) + + if 'MA20' in stock_data.columns: + fig.add_trace(go.Scatter( + x=stock_data.index, + y=stock_data['MA20'], + name="MA20", + line=dict(color='blue', width=1) + )) + + if 'MA60' in stock_data.columns: + fig.add_trace(go.Scatter( + x=stock_data.index, + y=stock_data['MA60'], + name="MA60", + line=dict(color='purple', width=1) + )) + + # 布林带 + if 'BB_upper' in stock_data.columns and 'BB_lower' in stock_data.columns: + fig.add_trace(go.Scatter( + x=stock_data.index, + y=stock_data['BB_upper'], + name="布林上轨", + line=dict(color='red', width=1, dash='dash') + )) + fig.add_trace(go.Scatter( + x=stock_data.index, + y=stock_data['BB_lower'], + name="布林下轨", + line=dict(color='green', width=1, dash='dash'), + fill='tonexty', + fillcolor='rgba(0,100,80,0.1)' + )) + + fig.update_layout( + title=f"{stock_info.get('name', 'N/A')} 股价走势", + xaxis_title="日期", + yaxis_title="价格", + height=500, + showlegend=True + ) + + # 生成唯一的key + chart_key = f"main_stock_chart_{stock_info.get('symbol', 'unknown')}_{int(time.time())}" + st.plotly_chart(fig, use_container_width=True, key=chart_key) + + # 成交量图 + if 'Volume' in stock_data.columns: + fig_volume = go.Figure() + fig_volume.add_trace(go.Bar( + x=stock_data.index, + y=stock_data['Volume'], + name="成交量", + marker_color='lightblue' + )) + + fig_volume.update_layout( + title="成交量", + xaxis_title="日期", + yaxis_title="成交量", + height=200 + ) + + # 生成唯一的key + volume_key = f"volume_chart_{stock_info.get('symbol', 'unknown')}_{int(time.time())}" + st.plotly_chart(fig_volume, use_container_width=True, key=volume_key) + +def display_agents_analysis(agents_results): + """显示各分析师报告""" + st.subheader("🤖 AI分析师团队报告") + + # 创建标签页 + tab_names = [] + tab_contents = [] + + for agent_key, agent_result in agents_results.items(): + agent_name = agent_result.get('agent_name', '未知分析师') + tab_names.append(agent_name) + tab_contents.append(agent_result) + + tabs = st.tabs(tab_names) + + for i, tab in enumerate(tabs): + with tab: + agent_result = tab_contents[i] + + # 分析师信息 + st.markdown(f""" +
+

👨‍💼 {agent_result.get('agent_name', '未知')}

+

职责:{agent_result.get('agent_role', '未知')}

+

关注领域:{', '.join(agent_result.get('focus_areas', []))}

+

分析时间:{agent_result.get('timestamp', '未知')}

+
+ """, unsafe_allow_html=True) + + # 分析报告 + st.markdown("**📄 分析报告:**") + st.write(agent_result.get('analysis', '暂无分析')) + +def display_team_discussion(discussion_result): + """显示团队讨论""" + st.subheader("🤝 分析团队讨论") + + st.markdown(""" +
+

💭 团队综合讨论

+

各位分析师正在就该股票进行深入讨论,整合不同维度的分析观点...

+
+ """, unsafe_allow_html=True) + + st.write(discussion_result) + +def display_final_decision(final_decision, stock_info, agents_results=None, discussion_result=None): + """显示最终投资决策""" + st.subheader("📋 最终投资决策") + + if isinstance(final_decision, dict) and "decision_text" not in final_decision: + # JSON格式的决策 + col1, col2 = st.columns([1, 2]) + + with col1: + # 投资评级 + rating = final_decision.get('rating', '未知') + rating_color = {"买入": "🟢", "持有": "🟡", "卖出": "🔴"}.get(rating, "⚪") + + st.markdown(f""" +
+

{rating_color} {rating}

+

投资评级

+
+ """, unsafe_allow_html=True) + + # 关键指标 + confidence = final_decision.get('confidence_level', 'N/A') + st.metric("信心度", f"{confidence}/10") + + target_price = final_decision.get('target_price', 'N/A') + st.metric("目标价格", f"{target_price}") + + position_size = final_decision.get('position_size', 'N/A') + st.metric("建议仓位", f"{position_size}") + + with col2: + # 详细建议 + st.markdown("**🎯 操作建议:**") + st.write(final_decision.get('operation_advice', '暂无建议')) + + st.markdown("**📍 关键位置:**") + col2_1, col2_2 = st.columns(2) + + with col2_1: + st.write(f"**进场区间:** {final_decision.get('entry_range', 'N/A')}") + st.write(f"**止盈位:** {final_decision.get('take_profit', 'N/A')}") + + with col2_2: + st.write(f"**止损位:** {final_decision.get('stop_loss', 'N/A')}") + st.write(f"**持有周期:** {final_decision.get('holding_period', 'N/A')}") + + # 风险提示 + risk_warning = final_decision.get('risk_warning', '') + if risk_warning: + st.markdown(f""" +
+

⚠️ 风险提示

+

{risk_warning}

+
+ """, unsafe_allow_html=True) + + else: + # 文本格式的决策 + decision_text = final_decision.get('decision_text', str(final_decision)) + st.write(decision_text) + + # 添加PDF导出功能 + st.markdown("---") + if agents_results and discussion_result: + display_pdf_export_section(stock_info, agents_results, discussion_result, final_decision) + else: + st.warning("⚠️ PDF导出功能需要完整的分析数据") + +def show_example_interface(): + """显示示例界面""" + st.subheader("💡 使用说明") + + col1, col2 = st.columns(2) + + with col1: + st.markdown(""" + ### 🚀 如何使用 + 1. **输入股票代码**:支持美股(如AAPL、MSFT)和A股(如000001、600036) + 2. **点击开始分析**:系统将启动AI分析师团队 + 3. **查看分析报告**:5位专业分析师将从不同角度分析 + 4. **获得投资建议**:获得最终的投资评级和操作建议 + + ### 📊 分析维度 + - **技术面**:趋势、指标、支撑阻力 + - **基本面**:财务、估值、行业分析 + - **资金面**:资金流向、主力行为 + - **风险管理**:风险识别与控制 + - **市场情绪**:情绪指标、热点分析 + """) + + with col2: + st.markdown(""" + ### 📈 示例股票代码 + + **美股热门** + - AAPL (苹果) + - MSFT (微软) + - GOOGL (谷歌) + - TSLA (特斯拉) + - NVDA (英伟达) + + **A股热门** + - 000001 (平安银行) + - 600036 (招商银行) + - 000002 (万科A) + - 600519 (贵州茅台) + - 000858 (五粮液) + """) + + st.info("💡 提示:首次运行需要配置DeepSeek API Key,请在.env中设置DEEPSEEK_API_KEY") + +def display_history_records(): + """显示历史分析记录""" + st.subheader("📚 历史分析记录") + + # 获取所有记录 + records = db.get_all_records() + + if not records: + st.info("📭 暂无历史分析记录") + return + + st.write(f"📊 共找到 {len(records)} 条分析记录") + + # 搜索和筛选 + col1, col2 = st.columns([3, 1]) + with col1: + search_term = st.text_input("🔍 搜索股票代码或名称", placeholder="输入股票代码或名称进行搜索") + with col2: + st.write("") + st.write("") + if st.button("🔄 刷新列表"): + st.rerun() + + # 筛选记录 + filtered_records = records + if search_term: + filtered_records = [ + record for record in records + if search_term.lower() in record['symbol'].lower() or + search_term.lower() in record['stock_name'].lower() + ] + + if not filtered_records: + st.warning("🔍 未找到匹配的记录") + return + + # 显示记录列表 + for record in filtered_records: + # 根据评级设置颜色和图标 + rating = record.get('rating', '未知') + rating_color = { + "买入": "🟢", + "持有": "🟡", + "卖出": "🔴", + "强烈买入": "🟢", + "强烈卖出": "🔴" + }.get(rating, "⚪") + + with st.expander(f"{rating_color} {record['stock_name']} ({record['symbol']}) - {record['analysis_date']}"): + col1, col2, col3, col4 = st.columns([2, 2, 1, 1]) + + with col1: + st.write(f"**股票代码:** {record['symbol']}") + st.write(f"**股票名称:** {record['stock_name']}") + + with col2: + st.write(f"**分析时间:** {record['analysis_date']}") + st.write(f"**数据周期:** {record['period']}") + st.write(f"**投资评级:** **{rating}**") + + with col3: + if st.button("👀 查看详情", key=f"view_{record['id']}"): + st.session_state.viewing_record_id = record['id'] + + with col4: + if st.button("🗑️ 删除", key=f"delete_{record['id']}"): + if db.delete_record(record['id']): + st.success("✅ 记录已删除") + st.rerun() + else: + st.error("❌ 删除失败") + + # 查看详细记录 + if 'viewing_record_id' in st.session_state: + display_record_detail(st.session_state.viewing_record_id) + +def display_record_detail(record_id): + """显示单条记录的详细信息""" + st.markdown("---") + st.subheader("📋 详细分析记录") + + record = db.get_record_by_id(record_id) + if not record: + st.error("❌ 记录不存在") + return + + # 基本信息 + col1, col2, col3 = st.columns(3) + with col1: + st.metric("股票代码", record['symbol']) + with col2: + st.metric("股票名称", record['stock_name']) + with col3: + st.metric("分析时间", record['analysis_date']) + + # 股票基本信息 + st.subheader("📊 股票基本信息") + stock_info = record['stock_info'] + if stock_info: + col1, col2, col3, col4, col5 = st.columns(5) + + with col1: + current_price = stock_info.get('current_price', 'N/A') + st.metric("当前价格", f"{current_price}") + + with col2: + change_percent = stock_info.get('change_percent', 'N/A') + if isinstance(change_percent, (int, float)): + st.metric("涨跌幅", f"{change_percent:.2f}%", f"{change_percent:.2f}%") + else: + st.metric("涨跌幅", f"{change_percent}") + + with col3: + pe_ratio = stock_info.get('pe_ratio', 'N/A') + st.metric("市盈率", f"{pe_ratio}") + + with col4: + pb_ratio = stock_info.get('pb_ratio', 'N/A') + st.metric("市净率", f"{pb_ratio}") + + with col5: + market_cap = stock_info.get('market_cap', 'N/A') + if isinstance(market_cap, (int, float)): + market_cap_str = f"{market_cap/1e9:.2f}B" if market_cap > 1e9 else f"{market_cap/1e6:.2f}M" + st.metric("市值", market_cap_str) + else: + st.metric("市值", f"{market_cap}") + + # 各分析师报告 + st.subheader("🤖 AI分析师团队报告") + agents_results = record['agents_results'] + if agents_results: + tab_names = [] + tab_contents = [] + + for agent_key, agent_result in agents_results.items(): + agent_name = agent_result.get('agent_name', '未知分析师') + tab_names.append(agent_name) + tab_contents.append(agent_result) + + tabs = st.tabs(tab_names) + + for i, tab in enumerate(tabs): + with tab: + agent_result = tab_contents[i] + + st.markdown(f""" +
+

👨‍💼 {agent_result.get('agent_name', '未知')}

+

职责:{agent_result.get('agent_role', '未知')}

+

关注领域:{', '.join(agent_result.get('focus_areas', []))}

+
+ """, unsafe_allow_html=True) + + st.markdown("**📄 分析报告:**") + st.write(agent_result.get('analysis', '暂无分析')) + + # 团队讨论 + st.subheader("🤝 分析团队讨论") + discussion_result = record['discussion_result'] + if discussion_result: + st.markdown(""" +
+

💭 团队综合讨论

+
+ """, unsafe_allow_html=True) + st.write(discussion_result) + + # 最终决策 + st.subheader("📋 最终投资决策") + final_decision = record['final_decision'] + if final_decision: + if isinstance(final_decision, dict) and "decision_text" not in final_decision: + col1, col2 = st.columns([1, 2]) + + with col1: + rating = final_decision.get('rating', '未知') + rating_color = {"买入": "🟢", "持有": "🟡", "卖出": "🔴"}.get(rating, "⚪") + + st.markdown(f""" +
+

{rating_color} {rating}

+

投资评级

+
+ """, unsafe_allow_html=True) + + confidence = final_decision.get('confidence_level', 'N/A') + st.metric("信心度", f"{confidence}/10") + + target_price = final_decision.get('target_price', 'N/A') + st.metric("目标价格", f"{target_price}") + + position_size = final_decision.get('position_size', 'N/A') + st.metric("建议仓位", f"{position_size}") + + with col2: + st.markdown("**🎯 操作建议:**") + st.write(final_decision.get('operation_advice', '暂无建议')) + + st.markdown("**📍 关键位置:**") + col2_1, col2_2 = st.columns(2) + + with col2_1: + st.write(f"**进场区间:** {final_decision.get('entry_range', 'N/A')}") + st.write(f"**止盈位:** {final_decision.get('take_profit', 'N/A')}") + + with col2_2: + st.write(f"**止损位:** {final_decision.get('stop_loss', 'N/A')}") + st.write(f"**持有周期:** {final_decision.get('holding_period', 'N/A')}") + else: + decision_text = final_decision.get('decision_text', str(final_decision)) + st.write(decision_text) + + # 返回按钮 + st.markdown("---") + if st.button("⬅️ 返回历史记录列表"): + if 'viewing_record_id' in st.session_state: + del st.session_state.viewing_record_id + st.rerun() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..124b90f --- /dev/null +++ b/config.py @@ -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" # 默认日线数据 diff --git a/database.py b/database.py new file mode 100644 index 0000000..342738b --- /dev/null +++ b/database.py @@ -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() \ No newline at end of file diff --git a/deepseek_client.py b/deepseek_client.py new file mode 100644 index 0000000..8679595 --- /dev/null +++ b/deepseek_client.py @@ -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} diff --git a/monitor_db.py b/monitor_db.py new file mode 100644 index 0000000..22a9d7a --- /dev/null +++ b/monitor_db.py @@ -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() \ No newline at end of file diff --git a/monitor_manager.py b/monitor_manager.py new file mode 100644 index 0000000..a44613c --- /dev/null +++ b/monitor_manager.py @@ -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""" +
+ """, 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("
", 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 diff --git a/monitor_service.py b/monitor_service.py new file mode 100644 index 0000000..3dae2de --- /dev/null +++ b/monitor_service.py @@ -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() \ No newline at end of file diff --git a/monitor_ui.py b/monitor_ui.py new file mode 100644 index 0000000..a6962f2 --- /dev/null +++ b/monitor_ui.py @@ -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 \ No newline at end of file diff --git a/notification_service.py b/notification_service.py new file mode 100644 index 0000000..0a29896 --- /dev/null +++ b/notification_service.py @@ -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""" +

股票监测提醒

+

股票代码: {notification['symbol']}

+

股票名称: {notification['name']}

+

提醒类型: {notification['type']}

+

提醒内容: {notification['message']}

+

触发时间: {notification['triggered_at']}

+
+

此邮件由AI股票分析系统自动发送

+ """ + + 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""" + + +

邮件测试成功!

+

这是一封来自AI股票分析系统的测试邮件。

+

如果您收到这封邮件,说明邮件通知功能已正常工作。

+
+

邮件配置信息:

+ +
+

此邮件由AI股票分析系统自动发送

+ + + """ + + 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() \ No newline at end of file diff --git a/pdf_generator.py b/pdf_generator.py new file mode 100644 index 0000000..0bb0ada --- /dev/null +++ b/pdf_generator.py @@ -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', '
') + story.append(Paragraph(analysis_text, normal_style)) + story.append(Spacer(1, 12)) + + # 团队讨论 + story.append(Paragraph("团队综合讨论", heading_style)) + discussion_text = str(discussion_result).replace('\n', '
') + 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', '
') + 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'📄 下载PDF报告' + 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""" +
+ {download_link} +
+ """, 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()}") diff --git a/pdf_generator_fixed.py b/pdf_generator_fixed.py new file mode 100644 index 0000000..ff49c28 --- /dev/null +++ b/pdf_generator_fixed.py @@ -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'{link_text}' + return href + +def create_html_download_link(content, filename, link_text): + """创建HTML下载链接""" + b64 = base64.b64encode(content.encode('utf-8')).decode() + href = f'{link_text}' + return href + +def generate_html_content(markdown_content): + """将Markdown转换为HTML""" + html_content = f""" + + + + + AI股票分析报告 + + + +
+""" + + # 简单的Markdown到HTML转换 + html_body = markdown_content + html_body = html_body.replace('\n# ', '\n

').replace('\n## ', '\n

').replace('\n### ', '\n

') + html_body = html_body.replace('# ', '

').replace('## ', '

').replace('### ', '

') + html_body = html_body.replace('\n---\n', '\n
\n') + + # 处理粗体文本 + html_body = re.sub(r'\*\*(.*?)\*\*', r'\1', 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('') + in_table = True + cells = [cell.strip() for cell in line.split('|')[1:-1]] + processed_lines.append('') + for cell in cells: + processed_lines.append(f'') + processed_lines.append('') + elif '|' in line and in_table: + if '---' not in line: + cells = [cell.strip() for cell in line.split('|')[1:-1]] + processed_lines.append('') + for cell in cells: + processed_lines.append(f'') + processed_lines.append('') + elif in_table and '|' not in line: + processed_lines.append('
{cell}
{cell}
') + processed_lines.append(line) + in_table = False + else: + processed_lines.append(line) + + if in_table: + processed_lines.append('') + + 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'

{para}

') + else: + processed_paragraphs.append(para) + + html_body = '\n'.join(processed_paragraphs) + + html_content += html_body + """ +

+ + +""" + + 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""" +
+ {md_link} + {html_link} +
+ """, unsafe_allow_html=True) + + st.info("💡 提示:点击上方按钮即可下载对应格式的报告文件") + + except Exception as e: + st.error(f"❌ 生成报告时出错: {str(e)}") + import traceback + st.error(f"详细错误信息: {traceback.format_exc()}") \ No newline at end of file diff --git a/pdf_generator_pandoc.py b/pdf_generator_pandoc.py new file mode 100644 index 0000000..66ab10d --- /dev/null +++ b/pdf_generator_pandoc.py @@ -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'{link_text}' + 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""" + + + + + AI股票分析报告 + + + +
+""" + + # 将Markdown转换为HTML(简单版本) + html_body = markdown_content.replace('\n# ', '\n

').replace('\n## ', '\n

').replace('\n### ', '\n

') + html_body = html_body.replace('\n---\n', '\n
\n') + html_body = html_body.replace('**', '').replace('**', '') + html_body = html_body.replace('\n\n', '

') + html_body = f"

{html_body}

" + + # 处理表格 + lines = html_body.split('\n') + in_table = False + processed_lines = [] + + for line in lines: + if '|' in line and not in_table: + processed_lines.append('') + in_table = True + if line.strip().startswith('|'): + cells = [cell.strip() for cell in line.split('|')[1:-1]] + processed_lines.append('') + for cell in cells: + processed_lines.append(f'') + processed_lines.append('') + elif '|' in line and in_table: + if '---' not in line: + cells = [cell.strip() for cell in line.split('|')[1:-1]] + processed_lines.append('') + for cell in cells: + processed_lines.append(f'') + processed_lines.append('') + elif in_table and '|' not in line: + processed_lines.append('
{cell}
{cell}
') + processed_lines.append(line) + in_table = False + else: + processed_lines.append(line) + + if in_table: + processed_lines.append('') + + html_body = '\n'.join(processed_lines) + + html_content += html_body + """ +

+ + +""" + + # HTML下载链接 + html_b64 = base64.b64encode(html_content.encode('utf-8')).decode() + html_href = f'🌐 下载HTML报告' + 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) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..654c51a --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..eaa2b3b --- /dev/null +++ b/run.py @@ -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() diff --git a/setup_guide.md b/setup_guide.md new file mode 100644 index 0000000..d1bd4c7 --- /dev/null +++ b/setup_guide.md @@ -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` diff --git a/stock_data.py b/stock_data.py new file mode 100644 index 0000000..b2eacd4 --- /dev/null +++ b/stock_data.py @@ -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 diff --git a/邮件配置指南.md b/邮件配置指南.md new file mode 100644 index 0000000..bcdd57c --- /dev/null +++ b/邮件配置指南.md @@ -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连接 + +如果需要帮助,请查看详细的错误日志信息。 \ No newline at end of file