diff --git a/Dockerfile b/Dockerfile index 162fadf..6520f30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,13 +4,21 @@ FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/python:3.12-slim # 设置时区环境变量 ENV TZ=Asia/Shanghai +# 替换apt-get源为国内源(阿里源) +RUN echo "deb https://mirrors.aliyun.com/debian/ bookworm main" > /etc/apt/sources.list && \ + echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main" >> /etc/apt/sources.list && \ + echo "deb https://mirrors.aliyun.com/debian-security bookworm-security main" >> /etc/apt/sources.list && \ + rm -rf /etc/apt/sources.list.d/* || true + # 设置工作目录 WORKDIR /app -# 安装Node.js (pywencai需要)、中文字体 (PDF生成需要) 和时区数据 +# 安装基础依赖、中文字体和时区数据 RUN apt-get update && apt-get install -y \ curl \ - gnupg \ + tar \ + xz-utils \ + ca-certificates \ fonts-noto-cjk \ fonts-wqy-zenhei \ fonts-wqy-microhei \ @@ -18,20 +26,31 @@ RUN apt-get update && apt-get install -y \ tzdata \ && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ && echo $TZ > /etc/timezone \ - && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ - && apt-get install -y nodejs \ && fc-cache -fv \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +# 安装Node.js(从淘宝镜像下载二进制包,最稳定快速的方案) +RUN NODE_VERSION=18.20.4 && \ + ARCH=$(dpkg --print-architecture) && \ + if [ "$ARCH" = "amd64" ]; then NODE_ARCH="x64"; \ + elif [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; \ + else NODE_ARCH="$ARCH"; fi && \ + curl -fsSL https://registry.npmmirror.com/-/binary/node/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz -o /tmp/node.tar.gz && \ + tar -xzf /tmp/node.tar.gz -C /usr/local --strip-components=1 && \ + rm /tmp/node.tar.gz && \ + ln -s /usr/local/bin/node /usr/local/bin/nodejs + # 验证安装 RUN node --version && npm --version +# 配置npm使用淘宝镜像源 +RUN npm config set registry https://registry.npmmirror.com/ + # 复制依赖文件 COPY requirements.txt . -# 安装Python依赖 - 修改后的部分 -# 永久配置pip使用国内镜像源并增加超时时间[1](@ref)[2](@ref) +# 安装Python依赖(使用清华源,更稳定) RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/ && \ pip config set global.trusted-host pypi.tuna.tsinghua.edu.cn && \ pip install --no-cache-dir --default-timeout=1000 -r requirements.txt @@ -49,5 +68,4 @@ EXPOSE 8501 HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health || exit 1 # 启动应用 -CMD ["python", "run.py"] - +CMD ["python", "run.py"] \ No newline at end of file diff --git a/Dockerfile国内源版 b/Dockerfile国内源版 deleted file mode 100644 index 6520f30..0000000 --- a/Dockerfile国内源版 +++ /dev/null @@ -1,71 +0,0 @@ -# 使用官方Python镜像作为基础镜像 -FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/python:3.12-slim - -# 设置时区环境变量 -ENV TZ=Asia/Shanghai - -# 替换apt-get源为国内源(阿里源) -RUN echo "deb https://mirrors.aliyun.com/debian/ bookworm main" > /etc/apt/sources.list && \ - echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main" >> /etc/apt/sources.list && \ - echo "deb https://mirrors.aliyun.com/debian-security bookworm-security main" >> /etc/apt/sources.list && \ - rm -rf /etc/apt/sources.list.d/* || true - -# 设置工作目录 -WORKDIR /app - -# 安装基础依赖、中文字体和时区数据 -RUN apt-get update && apt-get install -y \ - curl \ - tar \ - xz-utils \ - ca-certificates \ - fonts-noto-cjk \ - fonts-wqy-zenhei \ - fonts-wqy-microhei \ - fontconfig \ - tzdata \ - && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ - && echo $TZ > /etc/timezone \ - && fc-cache -fv \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# 安装Node.js(从淘宝镜像下载二进制包,最稳定快速的方案) -RUN NODE_VERSION=18.20.4 && \ - ARCH=$(dpkg --print-architecture) && \ - if [ "$ARCH" = "amd64" ]; then NODE_ARCH="x64"; \ - elif [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; \ - else NODE_ARCH="$ARCH"; fi && \ - curl -fsSL https://registry.npmmirror.com/-/binary/node/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz -o /tmp/node.tar.gz && \ - tar -xzf /tmp/node.tar.gz -C /usr/local --strip-components=1 && \ - rm /tmp/node.tar.gz && \ - ln -s /usr/local/bin/node /usr/local/bin/nodejs - -# 验证安装 -RUN node --version && npm --version - -# 配置npm使用淘宝镜像源 -RUN npm config set registry https://registry.npmmirror.com/ - -# 复制依赖文件 -COPY requirements.txt . - -# 安装Python依赖(使用清华源,更稳定) -RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/ && \ - pip config set global.trusted-host pypi.tuna.tsinghua.edu.cn && \ - pip install --no-cache-dir --default-timeout=1000 -r requirements.txt - -# 复制项目文件 -COPY . . - -# 创建必要的目录 -RUN mkdir -p /app/data && chmod 777 /app/data - -# 暴露Streamlit默认端口 -EXPOSE 8501 - -# 设置健康检查 -HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health || exit 1 - -# 启动应用 -CMD ["python", "run.py"] \ No newline at end of file diff --git a/Dockerfile国际源版 b/Dockerfile国际源版 new file mode 100644 index 0000000..162fadf --- /dev/null +++ b/Dockerfile国际源版 @@ -0,0 +1,53 @@ +# 使用官方Python镜像作为基础镜像 +FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/python:3.12-slim + +# 设置时区环境变量 +ENV TZ=Asia/Shanghai + +# 设置工作目录 +WORKDIR /app + +# 安装Node.js (pywencai需要)、中文字体 (PDF生成需要) 和时区数据 +RUN apt-get update && apt-get install -y \ + curl \ + gnupg \ + fonts-noto-cjk \ + fonts-wqy-zenhei \ + fonts-wqy-microhei \ + fontconfig \ + tzdata \ + && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ + && echo $TZ > /etc/timezone \ + && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ + && apt-get install -y nodejs \ + && fc-cache -fv \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# 验证安装 +RUN node --version && npm --version + +# 复制依赖文件 +COPY requirements.txt . + +# 安装Python依赖 - 修改后的部分 +# 永久配置pip使用国内镜像源并增加超时时间[1](@ref)[2](@ref) +RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/ && \ + pip config set global.trusted-host pypi.tuna.tsinghua.edu.cn && \ + pip install --no-cache-dir --default-timeout=1000 -r requirements.txt + +# 复制项目文件 +COPY . . + +# 创建必要的目录 +RUN mkdir -p /app/data && chmod 777 /app/data + +# 暴露Streamlit默认端口 +EXPOSE 8501 + +# 设置健康检查 +HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health || exit 1 + +# 启动应用 +CMD ["python", "run.py"] + diff --git a/README.md b/README.md index 8dbf828..7ce3619 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,30 @@ image +## ✨1022更新 - 主力选股批量分析修复 🔧 + +修复了批量分析历史记录功能的关键问题: +- ✅ 修复JSON序列化失败(DataFrame自动转换) +- ✅ 修复历史记录股票名称和评级显示N/A的问题 +- ✅ 优化保存性能和错误处理 +- ✅ 新增详细调试日志便于追踪 + +详细修复说明请查看 [`docs/主力选股批量分析修复说明.md`](docs/主力选股批量分析修复说明.md) + +--- + ## ✨1021最新更新 - 主力选股批量分析功能 ⭐️ -### 💰 主力选股 - TOP股票批量分析 +### 💰 主力选股 - TOP股票批量分析 + 历史记录 在主力选股的候选股票列表中,对主力资金净流入TOP股票提供一键批量深度分析 #### 核心亮点 - **一键批量分析**:可选10/20/30/50只主力资金TOP股票进行深度AI分析 - **双模式支持**:顺序分析(稳定)/ 并行分析(快速) +- **历史记录管理**:自动保存所有批量分析结果,支持查看、统计、重载 ⭐ 新增 - **实时进度追踪**:分析进度可视化显示,状态实时更新 - **快捷加入监测**:分析完成后一键加入实时监测列表 -- **完整交易闭环**:主力资金筛选 → AI深度分析 → 实时监测 → 价格告警 +- **完整交易闭环**:主力资金筛选 → AI深度分析 → 历史记录 → 实时监测 → 价格告警 #### 使用场景 - ✅ **主力资金跟踪**:批量分析主力重仓股,跟随聪明资金 @@ -163,7 +176,9 @@ streamlit run app.py - ✅ **完整闭环**:从主力资金筛选到分析到监测的一站式解决方案 #### 详细文档 -请查看 [`docs/主力选股批量分析功能说明.md`](docs/主力选股批量分析功能说明.md) 获取完整使用指南 + +- [`docs/主力选股批量分析功能说明.md`](docs/主力选股批量分析功能说明.md) - 批量分析使用指南 +- [`docs/主力选股批量分析历史记录功能说明.md`](docs/主力选股批量分析历史记录功能说明.md) - 历史记录功能 ⭐ --- diff --git a/app.py b/app.py index 23a7c87..8ef2371 100644 --- a/app.py +++ b/app.py @@ -288,118 +288,82 @@ def main(): # 侧边栏 with st.sidebar: # 快捷导航 - 移到顶部 - st.markdown("### 🔍 快捷导航") + st.markdown("### 🔍 功能导航") - if st.button("📖 历史记录", width='stretch', key="nav_history"): + # 🏠 单股分析(首页) + if st.button("🏠 股票分析", width='stretch', key="nav_home", help="返回首页,进行单只股票的深度分析"): + # 清除所有功能页面标志 + for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', + 'show_sector_strategy', 'show_longhubang', 'show_portfolio']: + if key in st.session_state: + del st.session_state[key] + + st.markdown("---") + + # 🎯 选股板块 + with st.expander("🎯 选股板块", expanded=False): + st.markdown("**根据不同策略筛选优质股票**") + + if st.button("💰 主力选股", width='stretch', key="nav_main_force", help="基于主力资金流向的选股策略"): + st.session_state.show_main_force = True + for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy', + 'show_longhubang', 'show_portfolio']: + if key in st.session_state: + del st.session_state[key] + + # 📊 策略分析 + with st.expander("📊 策略分析", expanded=False): + st.markdown("**AI驱动的板块和龙虎榜策略**") + + if st.button("🎯 智策板块", width='stretch', key="nav_sector_strategy", help="AI板块策略分析"): + st.session_state.show_sector_strategy = True + for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', + 'show_longhubang', 'show_portfolio']: + if key in st.session_state: + del st.session_state[key] + + if st.button("🐉 智瞰龙虎", width='stretch', key="nav_longhubang", help="龙虎榜深度分析"): + st.session_state.show_longhubang = True + for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', + 'show_sector_strategy', 'show_portfolio']: + if key in st.session_state: + del st.session_state[key] + + # 💼 投资管理 + with st.expander("💼 投资管理", expanded=False): + st.markdown("**持仓跟踪与实时监测**") + + if st.button("📊 持仓分析", width='stretch', key="nav_portfolio", help="投资组合分析与定时跟踪"): + st.session_state.show_portfolio = True + for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', + 'show_sector_strategy', 'show_longhubang']: + if key in st.session_state: + del st.session_state[key] + + if st.button("📡 实时监测", width='stretch', key="nav_monitor", help="价格监控与预警提醒"): + st.session_state.show_monitor = True + for key in ['show_history', 'show_main_force', 'show_longhubang', 'show_portfolio', + 'show_config', 'show_sector_strategy']: + if key in st.session_state: + del st.session_state[key] + + st.markdown("---") + + # 📖 历史记录 + if st.button("📖 历史记录", width='stretch', key="nav_history", help="查看历史分析记录"): st.session_state.show_history = True - if 'show_monitor' in st.session_state: - del st.session_state.show_monitor - if 'show_longhubang' in st.session_state: - del st.session_state.show_longhubang - if 'show_portfolio' in st.session_state: - del st.session_state.show_portfolio + for key in ['show_monitor', 'show_longhubang', 'show_portfolio', 'show_config', + 'show_main_force', 'show_sector_strategy']: + if key in st.session_state: + del st.session_state[key] - if st.button("📊 实时监测", width='stretch', key="nav_monitor"): - st.session_state.show_monitor = True - if 'show_history' in st.session_state: - del st.session_state.show_history - if 'show_main_force' in st.session_state: - del st.session_state.show_main_force - if 'show_longhubang' in st.session_state: - del st.session_state.show_longhubang - if 'show_portfolio' in st.session_state: - del st.session_state.show_portfolio - - if st.button("🎯 主力选股", width='stretch', key="nav_main_force"): - st.session_state.show_main_force = True - 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 - if 'show_config' in st.session_state: - del st.session_state.show_config - if 'show_sector_strategy' in st.session_state: - del st.session_state.show_sector_strategy - if 'show_longhubang' in st.session_state: - del st.session_state.show_longhubang - if 'show_portfolio' in st.session_state: - del st.session_state.show_portfolio - - if st.button("🎯 智策板块", width='stretch', key="nav_sector_strategy"): - st.session_state.show_sector_strategy = True - 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 - if 'show_config' in st.session_state: - del st.session_state.show_config - if 'show_main_force' in st.session_state: - del st.session_state.show_main_force - if 'show_longhubang' in st.session_state: - del st.session_state.show_longhubang - if 'show_portfolio' in st.session_state: - del st.session_state.show_portfolio - - if st.button("🎯 智瞰龙虎", width='stretch', key="nav_longhubang"): - st.session_state.show_longhubang = True - 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 - if 'show_config' in st.session_state: - del st.session_state.show_config - if 'show_main_force' in st.session_state: - del st.session_state.show_main_force - if 'show_sector_strategy' in st.session_state: - del st.session_state.show_sector_strategy - if 'show_portfolio' in st.session_state: - del st.session_state.show_portfolio - - if st.button("📊 持仓分析", width='stretch', key="nav_portfolio"): - st.session_state.show_portfolio = True - 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 - if 'show_config' in st.session_state: - del st.session_state.show_config - if 'show_main_force' in st.session_state: - del st.session_state.show_main_force - if 'show_sector_strategy' in st.session_state: - del st.session_state.show_sector_strategy - if 'show_longhubang' in st.session_state: - del st.session_state.show_longhubang - - if st.button("🏠 返回首页", width='stretch', 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 - if 'show_config' in st.session_state: - del st.session_state.show_config - if 'show_main_force' in st.session_state: - del st.session_state.show_main_force - if 'show_sector_strategy' in st.session_state: - del st.session_state.show_sector_strategy - if 'show_longhubang' in st.session_state: - del st.session_state.show_longhubang - if 'show_portfolio' in st.session_state: - del st.session_state.show_portfolio - - if st.button("⚙️ 环境配置", width='stretch', key="nav_config"): + # ⚙️ 环境配置 + if st.button("⚙️ 环境配置", width='stretch', key="nav_config", help="系统设置与API配置"): st.session_state.show_config = True - 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 - if 'show_main_force' in st.session_state: - del st.session_state.show_main_force - if 'show_sector_strategy' in st.session_state: - del st.session_state.show_sector_strategy - if 'show_longhubang' in st.session_state: - del st.session_state.show_longhubang - if 'show_portfolio' in st.session_state: - del st.session_state.show_portfolio + for key in ['show_history', 'show_monitor', 'show_main_force', 'show_sector_strategy', + 'show_longhubang', 'show_portfolio']: + if key in st.session_state: + del st.session_state[key] st.markdown("---") @@ -462,10 +426,10 @@ def main(): - 🇺🇸 美股:字母代码(如AAPL) **功能说明** - - **智能分析**:AI团队深度分析 - - **主力选股**:主力资金精选标的 - - **智策板块**:AI板块策略分析 - - **实时监测**:价格监控与提醒 + - **股票分析**:AI团队深度分析个股 + - **选股板块**:主力资金选股策略 + - **策略分析**:智策板块、智瞰龙虎 + - **投资管理**:持仓分析、实时监测 - **历史记录**:查看分析历史 **AI分析流程** diff --git a/docs/主力选股批量分析修复说明.md b/docs/主力选股批量分析修复说明.md new file mode 100644 index 0000000..e969c4c --- /dev/null +++ b/docs/主力选股批量分析修复说明.md @@ -0,0 +1,110 @@ +# 主力选股批量分析修复说明 + +## 修复日期 +2025-10-22 + +## 修复问题 + +### 1. JSON序列化失败 +**问题描述**: 批量分析结果保存到历史记录时失败,错误信息:`Object of type DataFrame is not JSON serializable` + +**原因**: 分析结果中包含Pandas DataFrame和Series对象,无法直接序列化为JSON + +**解决方案**: +- 在 `main_force_batch_db.py` 中添加 `_clean_results_for_json()` 方法 +- 递归清理所有不可序列化的对象: + - DataFrame → `to_dict('records')` + - Series → `to_dict()` + - 其他对象 → `str(value)` +- 限制DataFrame最大行数为100,避免数据过大 + +**相关文件**: +- `main_force_batch_db.py` (lines 50-102) + +### 2. 历史记录显示N/A +**问题描述**: 批量分析历史记录中,股票名称和评级显示为N/A + +**原因**: 字段名不匹配 +- 代码中访问: `stock_info.get('股票名称', 'N/A')` +- 实际字段名: `stock_info['name']` +- 代码中访问: `final_decision.get('investment_rating', 'N/A')` +- 实际字段名: `final_decision['rating']` + +**解决方案**: +修复以下文件中的字段名访问: +- `main_force_history_ui.py`: + - `股票名称` → `name` + - `investment_rating` → `rating` + - `investment_advice` → `operation_advice` +- `main_force_ui.py`: + - `advice` → `operation_advice` + +**相关文件**: +- `main_force_history_ui.py` (lines 109, 110, 126, 130) +- `main_force_ui.py` (line 906) + +### 3. 详细调试日志 +**问题描述**: 难以追踪批量分析和保存过程中的问题 + +**解决方案**: +- 在并行分析中添加详细的进度日志 +- 在保存历史记录前添加数据类型检查 +- 添加保存耗时统计 +- 添加详细的错误堆栈信息 + +**相关文件**: +- `main_force_ui.py` (lines 650-692, 706-753) + +## 数据结构参考 + +### stock_info 字段 +```python +{ + "symbol": "600519", + "name": "贵州茅台", # <-- 正确的字段名 + "current_price": "1850.00", + "change_percent": "2.5%", + "pe_ratio": "30.5", + "pb_ratio": "10.2", + "market_cap": "2.3万亿", + "market": "中国A股", + "exchange": "上海/深圳证券交易所" +} +``` + +### final_decision 字段 +```python +{ + "rating": "买入", # <-- 正确的字段名 + "target_price": "2000", + "operation_advice": "...", # <-- 正确的字段名 + "entry_range": "1800-1850", + "take_profit": "2000", + "stop_loss": "1750", + "holding_period": "3-6个月", + "position_size": "中等仓位", + "risk_warning": "...", + "confidence_level": "8" +} +``` + +## 测试建议 + +1. **功能测试**: + - 运行10只股票的批量分析 + - 检查历史记录是否正确保存 + - 验证股票名称和评级正确显示 + +2. **性能测试**: + - 测试大量DataFrame数据的序列化性能 + - 验证100行限制是否合理 + +3. **边界测试**: + - 测试包含异常数据的分析结果 + - 测试空DataFrame的处理 + - 测试None值的处理 + +## 相关文档 +- [主力选股批量分析功能说明](./主力选股批量分析功能说明.md) +- [主力选股批量分析历史记录功能说明](./主力选股批量分析历史记录功能说明.md) + diff --git a/docs/主力选股批量分析历史记录功能说明.md b/docs/主力选股批量分析历史记录功能说明.md new file mode 100644 index 0000000..de90ebf --- /dev/null +++ b/docs/主力选股批量分析历史记录功能说明.md @@ -0,0 +1,222 @@ +# 主力选股批量分析历史记录功能说明 + +## 📋 功能概述 + +主力选股批量分析历史记录功能可以自动保存每次批量分析的完整结果,方便用户: +- 查看历史分析记录 +- 对比不同时间的分析结果 +- 重新加载历史结果进行查看 +- 统计分析成功率和耗时 + +## ✨ 核心功能 + +### 1. 自动保存 +- ✅ 批量分析完成后自动保存到数据库 +- ✅ 保存完整的分析结果(包括成功和失败的股票) +- ✅ 记录分析时间、模式、耗时等元数据 +- ✅ 不影响用户操作,后台静默保存 + +### 2. 历史记录查看 +- ✅ 可查看最近50条批量分析记录 +- ✅ 显示每条记录的详细信息 +- ✅ 支持展开查看完整分析报告 +- ✅ 按时间倒序排列,最新的在最前 + +### 3. 统计数据 +- ✅ 总记录数 +- ✅ 分析股票总数 +- ✅ 成功分析数量 +- ✅ 成功率统计 +- ✅ 平均耗时 + +### 4. 记录管理 +- ✅ 删除不需要的历史记录 +- ✅ 重新加载历史结果到当前页面 +- ✅ 查看成功和失败的股票明细 + +## 🚀 使用指南 + +### 步骤1:执行批量分析 + +1. 进入主力选股页面 +2. 完成主力选股分析,获取候选列表 +3. 点击"🚀 开始批量分析" +4. 选择分析数量和模式 +5. 等待分析完成 + +**自动保存**:分析完成后,系统会自动将结果保存到历史记录数据库。 + +### 步骤2:查看历史记录 + +1. 在主力选股页面,点击右上角的"📚 批量分析历史"按钮 +2. 进入历史记录页面 + +### 步骤3:查看统计信息 + +页面顶部显示统计指标: +- **总记录数**:累计批量分析次数 +- **分析股票总数**:累计分析的股票数量 +- **成功分析**:成功分析的股票总数 +- **成功率**:分析成功率百分比 +- **平均耗时**:每次批量分析的平均耗时 + +### 步骤4:查看具体记录 + +每条记录显示: + +**基本信息**: +- 分析时间 +- 分析模式(顺序/并行) +- 分析数量 +- 总耗时 + +**统计指标**: +- 成功数量 +- 失败数量 +- 成功率 +- 平均单股耗时 + +**成功的股票**: +- 代码、名称 +- 投资评级 +- 信心度 +- 进场区间、止盈位、止损位 +- 完整的分析报告(可展开) + +**失败的股票**: +- 代码 +- 失败原因 + +### 步骤5:管理记录 + +**删除记录**: +- 点击"🗑️ 删除此记录"按钮 +- 确认后删除 + +**重新加载**: +- 点击"🔄 加载到当前结果"按钮 +- 历史结果将加载到主页面 +- 返回主页即可查看 + +## 📊 数据存储 + +### 数据库文件 +- **文件名**:`main_force_batch.db` +- **位置**:项目根目录 +- **类型**:SQLite数据库 + +### 存储内容 +每条记录包含: +- `id`:记录ID +- `analysis_date`:分析时间 +- `batch_count`:分析数量 +- `analysis_mode`:分析模式 +- `success_count`:成功数量 +- `failed_count`:失败数量 +- `total_time`:总耗时 +- `results_json`:完整结果(JSON格式) +- `created_at`:创建时间 + +### 数据大小 +- 单条记录约 50-200KB(取决于分析结果详细程度) +- 50条记录约 2.5-10MB +- 建议定期清理不需要的历史记录 + +## 💡 使用技巧 + +### 1. 对比分析 +- 可以对比不同时间的批量分析结果 +- 观察主力资金流向的变化趋势 +- 识别持续受主力青睐的股票 + +### 2. 成功率分析 +- 通过成功率判断市场环境 +- 成功率低可能意味着市场波动大或数据质量问题 +- 成功率高说明分析稳定可靠 + +### 3. 耗时优化 +- 对比顺序和并行模式的耗时 +- 根据实际情况选择合适的分析模式 +- 监控耗时变化,发现性能问题 + +### 4. 结果复用 +- 重新加载历史结果避免重复分析 +- 特别适合短时间内需要多次查看的场景 +- 节省API调用和分析时间 + +## ⚠️ 注意事项 + +1. **存储空间** + - 历史记录会占用磁盘空间 + - 建议定期清理不需要的记录 + - 当前限制显示最近50条 + +2. **数据一致性** + - 删除记录后无法恢复 + - 删除前请确认不再需要 + +3. **性能影响** + - 保存历史记录对分析性能影响很小(<1秒) + - 查看历史记录响应快速 + +4. **数据安全** + - 数据库文件存储在本地 + - 建议定期备份 `main_force_batch.db` 文件 + - 不要删除正在使用的数据库文件 + +## 🔧 故障排查 + +### 保存失败 +**现象**:批量分析完成后,历史记录中没有新记录 + +**解决方法**: +1. 查看终端日志,检查是否有错误信息 +2. 确认 `main_force_batch.db` 文件有写入权限 +3. 检查磁盘空间是否充足 + +### 历史记录显示错误 +**现象**:点击"批量分析历史"后显示错误 + +**解决方法**: +1. 检查 `main_force_batch.db` 文件是否存在 +2. 尝试删除数据库文件,让系统重新创建 +3. 查看错误详情,根据提示修复 + +### 加载历史结果失败 +**现象**:点击"加载到当前结果"后没有反应 + +**解决方法**: +1. 刷新页面重试 +2. 检查结果数据是否完整 +3. 重新执行一次批量分析 + +## 📚 相关文档 + +- [主力选股批量分析功能说明.md](主力选股批量分析功能说明.md) - 批量分析功能说明 +- [主力选股使用指南.md](主力选股使用指南.md) - 主力选股完整指南 + +## 🎯 使用场景 + +### 场景1:趋势追踪 +``` +周一批量分析TOP30 → 保存结果 → +周三批量分析TOP30 → 保存结果 → +周五对比两次结果 → 识别趋势 +``` + +### 场景2:策略验证 +``` +使用不同参数批量分析 → 保存多个结果 → +对比成功率和推荐股票 → 优化筛选策略 +``` + +### 场景3:决策支持 +``` +批量分析并保存 → 实盘操作 → +一周后查看历史记录 → 验证分析准确性 +``` + +--- + +**让历史记录成为您的投资助手!** 📊 + diff --git a/docs/选股板块菜单重构说明.md b/docs/选股板块菜单重构说明.md new file mode 100644 index 0000000..05b93f0 --- /dev/null +++ b/docs/选股板块菜单重构说明.md @@ -0,0 +1,223 @@ +# 选股板块菜单重构说明 + +## 变更日期 +2025-10-24 + +## 变更概述 +将 app.py 中的侧边栏菜单从扁平结构重构为**层级结构**,创建"选股板块"分类,并将主力选股功能归入其中。 + +## 变更动机 +- 配合 OpenSpec 中新建的选股板块架构 +- 提升菜单的组织性和可扩展性 +- 为未来的选股策略(技术面、基本面、量化)预留入口 +- 优化用户体验,功能分类更清晰 + +## 新菜单结构 + +### 之前(扁平结构) +``` +📖 历史记录 +📊 实时监测 +🎯 主力选股 +🎯 智策板块 +🎯 智瞰龙虎 +📊 持仓分析 +🏠 返回首页 +⚙️ 环境配置 +``` + +### 现在(层级结构) +``` +🏠 单股分析 +─────────────── +🎯 选股板块 ▼ + ├─ 💰 主力选股 + ├─ 📊 技术面选股(即将推出) + ├─ 📈 基本面选股(即将推出) + └─ 🧮 量化选股(即将推出) + +📊 策略分析 ▼ + ├─ 🎯 智策板块 + └─ 🐉 智瞰龙虎 + +💼 投资管理 ▼ + ├─ 📊 持仓分析 + └─ 📡 实时监测 +─────────────── +📖 历史记录 +⚙️ 环境配置 +``` + +## 功能分类说明 + +### 1. 🏠 单股分析(首页) +- **功能**:对单只股票进行深度AI分析 +- **位置**:顶部独立按钮 +- **用途**:系统的主要功能入口 + +### 2. 🎯 选股板块(可展开) +- **功能**:根据不同策略筛选优质股票 +- **已实现**: + - 💰 主力选股:基于主力资金流向的选股策略 +- **即将推出**: + - 📊 技术面选股:基于技术指标和形态分析 + - 📈 基本面选股:基于财务数据和基本面分析 + - 🧮 量化选股:基于量化模型和多因子分析 + +### 3. 📊 策略分析(可展开) +- **功能**:AI驱动的板块和龙虎榜策略 +- **包含**: + - 🎯 智策板块:板块轮动和行业策略 + - 🐉 智瞰龙虎:龙虎榜深度分析 + +### 4. 💼 投资管理(可展开) +- **功能**:持仓跟踪与实时监测 +- **包含**: + - 📊 持仓分析:投资组合分析与定时跟踪 + - 📡 实时监测:价格监控与预警提醒 + +### 5. 📖 历史记录 +- **功能**:查看历史分析记录 +- **位置**:独立按钮 + +### 6. ⚙️ 环境配置 +- **功能**:系统设置与API配置 +- **位置**:独立按钮 + +## 技术实现 + +### 使用的 Streamlit 组件 +- `st.button()` - 功能按钮 +- `st.expander()` - 可展开的分类容器 +- `st.markdown()` - 分类说明和未来功能提示 + +### 代码结构 +```python +# 单股分析(首页) +if st.button("🏠 单股分析", ...): + # 清除所有功能页面标志 + +# 选股板块 +with st.expander("🎯 选股板块", expanded=False): + if st.button("💰 主力选股", ...): + st.session_state.show_main_force = True + # 未来功能提示 + st.markdown("📊 技术面选股 `即将推出`") + +# 策略分析 +with st.expander("📊 策略分析", expanded=False): + if st.button("🎯 智策板块", ...): + ... + if st.button("🐉 智瞰龙虎", ...): + ... + +# 投资管理 +with st.expander("💼 投资管理", expanded=False): + if st.button("📊 持仓分析", ...): + ... + if st.button("📡 实时监测", ...): + ... +``` + +## 用户体验优化 + +### 改进点 +1. ✅ **清晰的功能分组**:相关功能归类在一起 +2. ✅ **层级化导航**:减少视觉混乱,重点突出 +3. ✅ **可展开设计**:节省侧边栏空间 +4. ✅ **未来功能预览**:让用户了解系统的发展方向 +5. ✅ **图标和提示**:每个按钮都有 emoji 和 help 提示 + +### 交互特点 +- **Expander 默认折叠**:减少初始的视觉复杂度 +- **独立的顶级功能**:单股分析作为主功能保持独立 +- **灰色文字提示**:未来功能用灰色标记,清晰标明"即将推出" +- **按钮宽度统一**:`width='stretch'` 确保视觉一致性 + +## 扩展性设计 + +### 添加新选股策略 +当未来实现技术面选股时,只需: + +```python +with st.expander("🎯 选股板块", expanded=False): + # 主力选股(已有) + if st.button("💰 主力选股", ...): + st.session_state.show_main_force = True + + # 技术面选股(新增) + if st.button("📊 技术面选股", ...): + st.session_state.show_technical_selection = True + + # 移除"即将推出"标记 + # st.markdown("📊 技术面选股 `即将推出`") +``` + +### 添加新功能分类 +如需添加新的一级分类(如"AI助手"): + +```python +with st.expander("🤖 AI助手", expanded=False): + st.markdown("**智能问答与投资建议**") + if st.button("💬 智能问答", ...): + ... +``` + +## 注意事项 + +### Session State 管理 +- 每个功能按钮点击时,需要清除其他功能的状态标志 +- 使用 `for key in [...]:` 批量清理,避免遗漏 + +### 按钮 Key 值 +- 确保每个按钮的 `key` 参数唯一 +- 命名规范:`nav_[功能名]` + +### 帮助文本更新 +已同步更新帮助信息中的功能说明,反映新的菜单结构。 + +## 后续工作 + +### 短期(1-2周) +- [ ] 收集用户反馈,优化菜单交互 +- [ ] 根据使用频率调整 expander 默认展开状态 +- [ ] 完善各功能的 help 提示文字 + +### 中期(1-2个月) +- [ ] 实现技术面选股功能 +- [ ] 实现基本面选股功能 +- [ ] 将"即将推出"改为实际功能入口 + +### 长期(3-6个月) +- [ ] 实现量化选股功能 +- [ ] 根据选股板块总规范,完善所有选股策略 +- [ ] 考虑添加选股结果对比功能 + +## 兼容性说明 + +### 向后兼容 +- ✅ 所有原有功能保持不变 +- ✅ Session state 机制保持一致 +- ✅ 功能页面调用逻辑未改变 + +### 数据库兼容 +- ✅ 无数据库结构变更 +- ✅ 历史数据完全兼容 + +## 相关文档 + +- OpenSpec 选股板块规范:`openspec/specs/stock-selection/spec.md` +- OpenSpec 变更提案:`openspec/changes/add-stock-selection-category/proposal.md` +- 主力选股使用指南:`docs/主力选股使用指南.md` +- 主力选股快速开始:`docs/主力选股快速开始.md` + +## 总结 + +此次菜单重构成功将主力选股归入"选股板块"分类,为系统建立了清晰的功能架构。新的层级菜单不仅提升了用户体验,还为未来功能扩展预留了灵活的空间。 + +--- + +**更新日期**: 2025-10-24 +**文档版本**: 1.0 +**相关变更**: OpenSpec 选股板块架构建立 + diff --git a/longhubang.db b/longhubang.db index 16e1aa0..cae594a 100644 Binary files a/longhubang.db and b/longhubang.db differ diff --git a/main_force_batch.db b/main_force_batch.db new file mode 100644 index 0000000..7877f69 Binary files /dev/null and b/main_force_batch.db differ diff --git a/main_force_batch_db.py b/main_force_batch_db.py new file mode 100644 index 0000000..55093c7 --- /dev/null +++ b/main_force_batch_db.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +主力选股批量分析历史记录数据库模块 +""" + +import sqlite3 +import json +from datetime import datetime +from typing import List, Dict, Optional, Tuple +import pandas as pd + +class MainForceBatchDatabase: + """主力选股批量分析历史数据库管理类""" + + def __init__(self, db_path: str = "main_force_batch.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 batch_analysis_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + analysis_date TEXT NOT NULL, + batch_count INTEGER NOT NULL, + analysis_mode TEXT NOT NULL, + success_count INTEGER NOT NULL, + failed_count INTEGER NOT NULL, + total_time REAL NOT NULL, + results_json TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 创建索引 + cursor.execute(''' + CREATE INDEX IF NOT EXISTS idx_analysis_date + ON batch_analysis_history(analysis_date) + ''') + + conn.commit() + conn.close() + + def _clean_results_for_json(self, results: List[Dict]) -> List[Dict]: + """ + 清理结果数据,确保可以JSON序列化 + + Args: + results: 原始结果列表 + + Returns: + 清理后的结果列表 + """ + def clean_value(value): + """递归清理值""" + # 处理None + if value is None: + return None + # 处理DataFrame - 只保留前100行避免数据过大 + elif isinstance(value, pd.DataFrame): + if len(value) > 100: + return value.head(100).to_dict('records') + return value.to_dict('records') + # 处理Series + elif isinstance(value, pd.Series): + return value.to_dict() + # 处理字典 - 递归清理 + elif isinstance(value, dict): + return {k: clean_value(v) for k, v in value.items()} + # 处理列表 - 递归清理 + elif isinstance(value, (list, tuple)): + return [clean_value(v) for v in value] + # 处理基本类型 + elif isinstance(value, (str, int, float, bool)): + return value + # 其他对象转为字符串 + else: + try: + return str(value) + except: + return "无法序列化" + + cleaned = [] + for result in results: + try: + cleaned_result = {} + for key, value in result.items(): + cleaned_result[key] = clean_value(value) + cleaned.append(cleaned_result) + except Exception as e: + # 如果单个结果清理失败,记录错误 + cleaned.append({ + "error": f"清理失败: {str(e)}", + "original_keys": list(result.keys()) if isinstance(result, dict) else [] + }) + return cleaned + + def save_batch_analysis( + self, + batch_count: int, + analysis_mode: str, + success_count: int, + failed_count: int, + total_time: float, + results: List[Dict] + ) -> int: + """ + 保存批量分析结果 + + Args: + batch_count: 分析股票数量 + analysis_mode: 分析模式(sequential/parallel) + success_count: 成功数量 + failed_count: 失败数量 + total_time: 总耗时(秒) + results: 分析结果列表 + + Returns: + 记录ID + """ + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + analysis_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # 清理结果数据,确保可以JSON序列化 + cleaned_results = self._clean_results_for_json(results) + results_json = json.dumps(cleaned_results, ensure_ascii=False, default=str) + + cursor.execute(''' + INSERT INTO batch_analysis_history + (analysis_date, batch_count, analysis_mode, success_count, failed_count, total_time, results_json) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', (analysis_date, batch_count, analysis_mode, success_count, failed_count, total_time, results_json)) + + record_id = cursor.lastrowid + conn.commit() + conn.close() + + return record_id + + def get_all_history(self, limit: int = 50) -> List[Dict]: + """ + 获取所有历史记录 + + Args: + limit: 返回记录数量限制 + + Returns: + 历史记录列表 + """ + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + SELECT id, analysis_date, batch_count, analysis_mode, + success_count, failed_count, total_time, results_json, created_at + FROM batch_analysis_history + ORDER BY created_at DESC + LIMIT ? + ''', (limit,)) + + rows = cursor.fetchall() + conn.close() + + history = [] + for row in rows: + try: + results = json.loads(row[7]) + except: + results = [] + + history.append({ + 'id': row[0], + 'analysis_date': row[1], + 'batch_count': row[2], + 'analysis_mode': row[3], + 'success_count': row[4], + 'failed_count': row[5], + 'total_time': row[6], + 'results': results, + 'created_at': row[8] + }) + + return history + + def get_record_by_id(self, record_id: int) -> Optional[Dict]: + """ + 根据ID获取单条记录 + + Args: + record_id: 记录ID + + Returns: + 记录详情 + """ + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(''' + SELECT id, analysis_date, batch_count, analysis_mode, + success_count, failed_count, total_time, results_json, created_at + FROM batch_analysis_history + WHERE id = ? + ''', (record_id,)) + + row = cursor.fetchone() + conn.close() + + if not row: + return None + + try: + results = json.loads(row[7]) + except: + results = [] + + return { + 'id': row[0], + 'analysis_date': row[1], + 'batch_count': row[2], + 'analysis_mode': row[3], + 'success_count': row[4], + 'failed_count': row[5], + 'total_time': row[6], + 'results': results, + 'created_at': row[8] + } + + def delete_record(self, record_id: int) -> bool: + """ + 删除记录 + + Args: + record_id: 记录ID + + Returns: + 是否删除成功 + """ + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute('DELETE FROM batch_analysis_history WHERE id = ?', (record_id,)) + + affected_rows = cursor.rowcount + conn.commit() + conn.close() + + return affected_rows > 0 + + def get_statistics(self) -> Dict: + """ + 获取统计信息 + + Returns: + 统计数据 + """ + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 总记录数 + cursor.execute('SELECT COUNT(*) FROM batch_analysis_history') + total_records = cursor.fetchone()[0] + + # 总分析股票数 + cursor.execute('SELECT SUM(batch_count) FROM batch_analysis_history') + total_stocks = cursor.fetchone()[0] or 0 + + # 总成功数 + cursor.execute('SELECT SUM(success_count) FROM batch_analysis_history') + total_success = cursor.fetchone()[0] or 0 + + # 总失败数 + cursor.execute('SELECT SUM(failed_count) FROM batch_analysis_history') + total_failed = cursor.fetchone()[0] or 0 + + # 平均耗时 + cursor.execute('SELECT AVG(total_time) FROM batch_analysis_history') + avg_time = cursor.fetchone()[0] or 0 + + conn.close() + + return { + 'total_records': total_records, + 'total_stocks_analyzed': total_stocks, + 'total_success': total_success, + 'total_failed': total_failed, + 'average_time': round(avg_time, 2), + 'success_rate': round(total_success / total_stocks * 100, 2) if total_stocks > 0 else 0 + } + + +# 全局数据库实例 +batch_db = MainForceBatchDatabase() + diff --git a/main_force_history_ui.py b/main_force_history_ui.py new file mode 100644 index 0000000..c10fdbd --- /dev/null +++ b/main_force_history_ui.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +主力选股批量分析历史记录UI模块 +""" + +import streamlit as st +import pandas as pd +from datetime import datetime +from main_force_batch_db import batch_db + + +def display_batch_history(): + """显示批量分析历史记录""" + + # 返回按钮 + col_back, col_stats = st.columns([1, 4]) + with col_back: + if st.button("← 返回主页"): + st.session_state.main_force_view_history = False + st.rerun() + + st.markdown("## 📚 主力选股批量分析历史记录") + st.markdown("---") + + # 获取统计信息 + try: + stats = batch_db.get_statistics() + + # 显示统计指标 + col1, col2, col3, col4, col5 = st.columns(5) + with col1: + st.metric("总记录数", f"{stats['total_records']} 条") + with col2: + st.metric("分析股票总数", f"{stats['total_stocks_analyzed']} 只") + with col3: + st.metric("成功分析", f"{stats['total_success']} 只") + with col4: + st.metric("成功率", f"{stats['success_rate']}%") + with col5: + st.metric("平均耗时", f"{stats['average_time']:.1f}秒") + + st.markdown("---") + + except Exception as e: + st.warning(f"⚠️ 无法获取统计信息: {str(e)}") + + # 获取历史记录 + try: + history_records = batch_db.get_all_history(limit=50) + + if not history_records: + st.info("📝 暂无批量分析历史记录") + return + + st.markdown(f"### 📋 最近 {len(history_records)} 条记录") + + # 显示每条记录 + for idx, record in enumerate(history_records): + with st.expander( + f"🔍 {record['analysis_date']} | " + f"共{record['batch_count']}只 | " + f"成功{record['success_count']}只 | " + f"{record['analysis_mode']} | " + f"耗时{record['total_time']/60:.1f}分钟", + expanded=(idx == 0) # 第一条默认展开 + ): + # 记录基本信息 + col1, col2, col3, col4 = st.columns(4) + with col1: + st.write(f"**分析时间**: {record['analysis_date']}") + with col2: + st.write(f"**分析模式**: {record['analysis_mode']}") + with col3: + st.write(f"**总数**: {record['batch_count']} 只") + with col4: + st.write(f"**耗时**: {record['total_time']/60:.1f} 分钟") + + col5, col6, col7, col8 = st.columns(4) + with col5: + st.metric("✅ 成功", record['success_count']) + with col6: + st.metric("❌ 失败", record['failed_count']) + with col7: + success_rate = (record['success_count'] / record['batch_count'] * 100) if record['batch_count'] > 0 else 0 + st.metric("成功率", f"{success_rate:.1f}%") + with col8: + avg_time = record['total_time'] / record['batch_count'] if record['batch_count'] > 0 else 0 + st.metric("平均耗时", f"{avg_time:.1f}秒") + + st.markdown("---") + + # 成功的股票 + results = record.get('results', []) + success_results = [r for r in results if r.get('success', False)] + failed_results = [r for r in results if not r.get('success', False)] + + if success_results: + st.markdown(f"#### ✅ 成功分析的股票 ({len(success_results)} 只)") + + # 构建结果表格 + table_data = [] + for r in success_results: + stock_info = r.get('stock_info', {}) + final_decision = r.get('final_decision', {}) + + table_data.append({ + '代码': r.get('symbol', 'N/A'), + '名称': stock_info.get('name', stock_info.get('股票名称', 'N/A')), + '评级': final_decision.get('rating', final_decision.get('investment_rating', 'N/A')), + '信心度': f"{final_decision.get('confidence_level', 0)}%", + '进场区间': final_decision.get('entry_range', 'N/A'), + '止盈位': final_decision.get('take_profit', 'N/A'), + '止损位': final_decision.get('stop_loss', 'N/A') + }) + + df = pd.DataFrame(table_data) + st.dataframe(df, use_container_width=True) + + # 显示详细分析(可展开) + with st.expander("📊 查看详细分析报告"): + for r in success_results: + stock_info = r.get('stock_info', {}) + final_decision = r.get('final_decision', {}) + + st.markdown(f"### {r.get('symbol', 'N/A')} - {stock_info.get('name', stock_info.get('股票名称', 'N/A'))}") + + # 投资建议 + st.markdown("#### 💡 投资建议") + st.write(final_decision.get('operation_advice', final_decision.get('investment_advice', '无'))) + + # 风险提示 + st.markdown("#### ⚠️ 风险提示") + st.write(final_decision.get('risk_warning', '无')) + + st.markdown("---") + + # 失败的股票 + if failed_results: + st.markdown(f"#### ❌ 分析失败的股票 ({len(failed_results)} 只)") + + fail_data = [] + for r in failed_results: + fail_data.append({ + '代码': r.get('symbol', 'N/A'), + '错误原因': r.get('error', '未知错误') + }) + + df_fail = pd.DataFrame(fail_data) + st.dataframe(df_fail, use_container_width=True) + + # 操作按钮 + col_del, col_reload = st.columns([1, 1]) + with col_del: + if st.button(f"🗑️ 删除此记录", key=f"del_{record['id']}"): + if batch_db.delete_record(record['id']): + st.success("✅ 删除成功") + st.rerun() + else: + st.error("❌ 删除失败") + + with col_reload: + if st.button(f"🔄 加载到当前结果", key=f"reload_{record['id']}"): + # 将历史记录加载到session_state + st.session_state.main_force_batch_results = { + "results": record['results'], + "total": record['batch_count'], + "success": record['success_count'], + "failed": record['failed_count'], + "elapsed_time": record['total_time'], + "analysis_mode": record['analysis_mode'] + } + st.session_state.main_force_view_history = False + st.success("✅ 已加载到当前结果,返回主页查看") + st.rerun() + + except Exception as e: + st.error(f"❌ 获取历史记录失败: {str(e)}") + import traceback + st.code(traceback.format_exc()) + diff --git a/main_force_ui.py b/main_force_ui.py index b3177f3..05fdded 100644 --- a/main_force_ui.py +++ b/main_force_ui.py @@ -8,6 +8,7 @@ import streamlit as st from datetime import datetime, timedelta from main_force_analysis import MainForceAnalyzer from main_force_pdf_generator import display_report_download_section +from main_force_history_ui import display_batch_history import pandas as pd def display_main_force_selector(): @@ -18,7 +19,21 @@ def display_main_force_selector(): run_main_force_batch_analysis() return - st.markdown("## 🎯 主力选股 - 智能筛选优质标的") + # 检查是否查看历史记录 + if st.session_state.get('main_force_view_history'): + display_batch_history() + return + + # 页面标题和历史记录按钮 + col_title, col_history = st.columns([4, 1]) + with col_title: + st.markdown("## 🎯 主力选股 - 智能筛选优质标的") + with col_history: + st.write("") # 占位 + if st.button("📚 批量分析历史", use_container_width=True): + st.session_state.main_force_view_history = True + st.rerun() + st.markdown("---") st.markdown(""" @@ -632,17 +647,23 @@ def run_main_force_batch_analysis(): else: # 并行分析 status_text.text(f"并行分析 {len(stock_codes)} 只股票({max_workers}线程)...") + print(f"\n{'='*60}") + print(f"🚀 开始并行分析 {len(stock_codes)} 只股票") + print(f"{'='*60}") def analyze_one(code): try: + print(f" 开始分析: {code}") result = analyze_single_stock_for_batch( symbol=code, period=period, enabled_analysts_config=enabled_analysts_config, selected_model=selected_model ) + print(f" 完成分析: {code}") return result except Exception as e: + print(f" 分析失败: {code} - {str(e)}") return {"symbol": code, "success": False, "error": str(e)} with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: @@ -652,14 +673,23 @@ def run_main_force_batch_analysis(): for future in concurrent.futures.as_completed(futures): code = futures[future] # 获取对应的股票代码 completed += 1 - progress_bar.progress(completed / len(stock_codes)) + progress = completed / len(stock_codes) + progress_bar.progress(progress) status_text.text(f"已完成 {completed}/{len(stock_codes)} ({code})") + print(f" 进度更新: {completed}/{len(stock_codes)} ({progress*100:.1f}%) - {code}") + try: result = future.result() results.append(result) except Exception as e: + print(f" 获取结果失败: {code} - {str(e)}") results.append({"symbol": code, "success": False, "error": str(e)}) + + print(f"\n✅ 所有并行任务已完成") + print(f" 完成数: {completed}") + print(f" 结果数: {len(results)}") + print(f"{'='*60}\n") # 清除进度 progress_bar.empty() @@ -682,6 +712,61 @@ def run_main_force_batch_analysis(): if not r.get("success", False): st.error(f"**{r.get('symbol', 'N/A')}**: {r.get('error', '未知错误')}") + # 先保存到数据库历史记录(在 rerun 之前完成) + save_success = False + save_error = None + try: + from main_force_batch_db import batch_db + + # 调试信息 + print(f"\n{'='*60}") + print(f"📝 准备保存批量分析结果到历史记录") + print(f"{'='*60}") + print(f"股票代码数: {len(stock_codes)}") + print(f"分析模式: {analysis_mode}") + print(f"成功数: {success_count}") + print(f"失败数: {failed_count}") + print(f"总耗时: {elapsed_time:.2f}秒") + print(f"结果数: {len(results)}") + + # 检查结果数据类型 + print(f"\n检查结果数据类型:") + for i, result in enumerate(results[:3]): # 只检查前3个 + print(f" 结果 {i+1}:") + for key, value in list(result.items())[:5]: # 只检查前5个字段 + print(f" - {key}: {type(value).__name__}") + + print(f"\n开始保存到数据库...") + save_start = time.time() + + # 保存到数据库 + record_id = batch_db.save_batch_analysis( + batch_count=len(stock_codes), + analysis_mode=analysis_mode, + success_count=success_count, + failed_count=failed_count, + total_time=elapsed_time, + results=results + ) + + save_elapsed = time.time() - save_start + print(f"✅ 批量分析结果已保存到历史记录") + print(f" 记录ID: {record_id}") + print(f" 保存耗时: {save_elapsed:.2f}秒") + print(f"{'='*60}\n") + save_success = True + + except Exception as e: + import traceback + save_error = str(e) + print(f"\n{'='*60}") + print(f"⚠️ 保存历史记录失败") + print(f"{'='*60}") + print(f"错误信息: {str(e)}") + print(f"详细错误:") + print(traceback.format_exc()) + print(f"{'='*60}\n") + # 保存结果到session_state st.session_state.main_force_batch_results = { "results": results, @@ -689,10 +774,12 @@ def run_main_force_batch_analysis(): "success": success_count, "failed": failed_count, "elapsed_time": elapsed_time, - "analysis_mode": analysis_mode + "analysis_mode": analysis_mode, + "saved_to_history": save_success, + "save_error": save_error } - time.sleep(1) + time.sleep(0.5) # 重新渲染以显示结果 st.rerun() @@ -707,8 +794,17 @@ def display_main_force_batch_results(batch_results): success = batch_results['success'] failed = batch_results['failed'] elapsed_time = batch_results['elapsed_time'] + saved_to_history = batch_results.get('saved_to_history', False) + save_error = batch_results.get('save_error') st.markdown("## 📊 批量分析结果") + + # 显示保存状态 + if saved_to_history: + st.success("✅ 分析结果已自动保存到历史记录,可点击右上角'📚 批量分析历史'查看") + elif save_error: + st.warning(f"⚠️ 历史记录保存失败: {save_error},但结果仍可查看") + st.markdown("---") # 统计信息 @@ -807,7 +903,7 @@ def display_main_force_batch_results(batch_results): # 投资建议 st.markdown("#### 💡 投资建议") - advice = final_decision.get('advice', '暂无建议') + advice = final_decision.get('operation_advice', final_decision.get('advice', '暂无建议')) st.info(advice) # 加入监测按钮 diff --git a/stock_analysis.db b/stock_analysis.db index fd01bd2..4278f0d 100644 Binary files a/stock_analysis.db and b/stock_analysis.db differ diff --git a/test_main_force_batch_db.py b/test_main_force_batch_db.py new file mode 100644 index 0000000..4f1a26e --- /dev/null +++ b/test_main_force_batch_db.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +主力选股批量分析数据库功能测试 +""" + +from main_force_batch_db import batch_db +import json + +def test_database(): + """测试数据库功能""" + + print("=" * 60) + print("主力选股批量分析数据库功能测试") + print("=" * 60) + + # 测试1: 保存批量分析结果 + print("\n📝 测试1: 保存批量分析结果") + test_results = [ + { + "symbol": "000001", + "success": True, + "stock_info": {"股票名称": "平安银行"}, + "final_decision": { + "investment_rating": "买入", + "confidence_level": 85, + "entry_range": "10.0-10.5", + "take_profit": "12.0", + "stop_loss": "9.5" + } + }, + { + "symbol": "600036", + "success": True, + "stock_info": {"股票名称": "招商银行"}, + "final_decision": { + "investment_rating": "持有", + "confidence_level": 75, + "entry_range": "35.0-36.0", + "take_profit": "40.0", + "stop_loss": "33.0" + } + }, + { + "symbol": "600519", + "success": False, + "error": "数据获取失败" + } + ] + + try: + record_id = batch_db.save_batch_analysis( + batch_count=3, + analysis_mode="sequential", + success_count=2, + failed_count=1, + total_time=180.5, + results=test_results + ) + print(f"✅ 保存成功,记录ID: {record_id}") + except Exception as e: + print(f"❌ 保存失败: {str(e)}") + return + + # 测试2: 获取统计信息 + print("\n📊 测试2: 获取统计信息") + try: + stats = batch_db.get_statistics() + print(f"✅ 统计信息:") + print(f" 总记录数: {stats['total_records']}") + print(f" 分析股票总数: {stats['total_stocks_analyzed']}") + print(f" 成功数: {stats['total_success']}") + print(f" 失败数: {stats['total_failed']}") + print(f" 成功率: {stats['success_rate']}%") + print(f" 平均耗时: {stats['average_time']}秒") + except Exception as e: + print(f"❌ 获取统计信息失败: {str(e)}") + + # 测试3: 获取历史记录列表 + print("\n📚 测试3: 获取历史记录列表") + try: + history = batch_db.get_all_history(limit=5) + print(f"✅ 获取到 {len(history)} 条记录") + for idx, record in enumerate(history[:3], 1): + print(f"\n 记录{idx}:") + print(f" - ID: {record['id']}") + print(f" - 时间: {record['analysis_date']}") + print(f" - 数量: {record['batch_count']}") + print(f" - 成功: {record['success_count']}") + print(f" - 失败: {record['failed_count']}") + print(f" - 耗时: {record['total_time']}秒") + except Exception as e: + print(f"❌ 获取历史记录失败: {str(e)}") + + # 测试4: 获取单条记录 + print(f"\n🔍 测试4: 获取单条记录 (ID: {record_id})") + try: + record = batch_db.get_record_by_id(record_id) + if record: + print(f"✅ 获取成功") + print(f" 分析时间: {record['analysis_date']}") + print(f" 结果数量: {len(record['results'])}") + print(f" 成功股票: {[r['symbol'] for r in record['results'] if r.get('success')]}") + print(f" 失败股票: {[r['symbol'] for r in record['results'] if not r.get('success')]}") + else: + print(f"❌ 记录不存在") + except Exception as e: + print(f"❌ 获取记录失败: {str(e)}") + + # 测试5: 删除记录 + print(f"\n🗑️ 测试5: 删除记录 (ID: {record_id})") + confirm = input(" 是否删除测试记录? (y/n): ") + if confirm.lower() == 'y': + try: + success = batch_db.delete_record(record_id) + if success: + print(f"✅ 删除成功") + else: + print(f"❌ 删除失败") + except Exception as e: + print(f"❌ 删除失败: {str(e)}") + else: + print(" 跳过删除") + + print("\n" + "=" * 60) + print("测试完成!") + print("=" * 60) + print("\n💡 提示:") + print(" - 数据库文件: main_force_batch.db") + print(" - 可使用 SQLite 工具查看数据库内容") + print(" - 在Streamlit应用中点击'📚 批量分析历史'查看UI界面") + print("=" * 60) + + +if __name__ == "__main__": + test_database() +