diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b5cb41c --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# MySQL数据库配置 +MYSQL_HOST=cd-cynosdbmysql-grp-7kdd8qe4.sql.tencentcdb.com +MYSQL_PORT=26558 +MYSQL_USER=root +MYSQL_PASSWORD=gUjjmQpu6c7V0hMF +MYSQL_DATABASE=tradingai + +# Flask应用配置 +FLASK_ENV=production +PYTHONPATH=/app \ No newline at end of file diff --git a/DOCKER_DEPLOY.md b/DOCKER_DEPLOY.md deleted file mode 100644 index 17b6d91..0000000 --- a/DOCKER_DEPLOY.md +++ /dev/null @@ -1,367 +0,0 @@ -# A股量化交易系统 Docker 部署指南 - -## 📋 概述 - -本指南将帮助您使用 Docker 快速部署 A股量化交易系统。系统包含 Web 界面、数据采集服务和可选的 Nginx 反向代理。 - -## 🔧 环境要求 - -- Docker >= 20.10 -- Docker Compose >= 2.0 -- 系统内存 >= 2GB -- 磁盘空间 >= 5GB - -## 🚀 快速开始 - -### 1. 克隆项目 - -```bash -git clone -cd TradingAI -``` - -### 2. 一键启动 - -```bash -# 使用启动脚本(推荐) -./docker-start.sh start - -# 或者直接使用 docker-compose -docker-compose up -d -``` - -### 3. 访问系统 - -- Web 界面:http://localhost:8080 -- Nginx 代理:http://localhost(如果启用) - -## 📁 项目结构 - -``` -TradingAI/ -├── Dockerfile # 主应用镜像 -├── docker-compose.yml # 服务编排配置 -├── docker-start.sh # 启动管理脚本 -├── .dockerignore # Docker 忽略文件 -├── config/ # 配置文件目录 -├── data/ # 数据持久化目录 -├── logs/ # 日志文件目录 -└── nginx/ # Nginx 配置目录 -``` - -## 🐳 服务说明 - -### trading-web -- **功能**:Web 界面服务 -- **端口**:8080 -- **健康检查**:/api/stats -- **数据卷**: - - `./data:/app/data` - 数据库文件 - - `./config:/app/config` - 配置文件 - - `./logs:/app/logs` - 日志文件 - -### trading-collector -- **功能**:数据采集和策略执行 -- **调度**:每天 9:00 和 15:00 执行 -- **依赖**:trading-web - -### nginx(可选) -- **功能**:反向代理和负载均衡 -- **端口**:80, 443 -- **配置**:nginx/nginx.conf - -## ⚙️ 配置文件 - -### config/config.yaml - -```yaml -# 数据库配置 -database: - path: "data/trading.db" - -# 数据源配置 -data_source: - provider: "adata" - -# 策略配置 -strategy: - kline_pattern: - min_entity_ratio: 0.55 - final_yang_min_ratio: 0.40 - max_turnover_ratio: 40.0 - timeframes: ["daily", "weekly"] - -# 通知配置 -notification: - dingtalk: - enabled: false - webhook_url: "" - -# 日志配置 -logging: - level: "INFO" - file: "logs/trading.log" -``` - -## 🛠️ 管理命令 - -### 使用启动脚本 - -```bash -# 启动所有服务 -./docker-start.sh start - -# 停止所有服务 -./docker-start.sh stop - -# 重启服务 -./docker-start.sh restart - -# 查看实时日志 -./docker-start.sh logs - -# 清理所有数据(危险操作) -./docker-start.sh cleanup -``` - -### 使用 Docker Compose - -```bash -# 启动服务 -docker-compose up -d - -# 停止服务 -docker-compose down - -# 查看服务状态 -docker-compose ps - -# 查看日志 -docker-compose logs -f [service_name] - -# 重启特定服务 -docker-compose restart trading-web - -# 重新构建镜像 -docker-compose build --no-cache - -# 扩展服务实例 -docker-compose up -d --scale trading-web=2 -``` - -## 📊 监控和调试 - -### 健康检查 - -系统提供了健康检查端点: - -```bash -# 检查 Web 服务状态 -curl http://localhost:8080/api/stats - -# 检查 Docker 容器健康状态 -docker-compose ps -``` - -### 日志查看 - -```bash -# 查看所有服务日志 -docker-compose logs -f - -# 查看特定服务日志 -docker-compose logs -f trading-web - -# 查看容器内日志文件 -docker exec -it trading-ai-web tail -f /app/logs/trading.log -``` - -### 进入容器调试 - -```bash -# 进入 Web 服务容器 -docker exec -it trading-ai-web bash - -# 进入采集服务容器 -docker exec -it trading-ai-collector bash - -# 查看容器资源使用情况 -docker stats -``` - -## 🔧 自定义配置 - -### 修改端口 - -编辑 `docker-compose.yml`: - -```yaml -services: - trading-web: - ports: - - "9090:8080" # 将外部端口改为 9090 -``` - -### 添加环境变量 - -```yaml -services: - trading-web: - environment: - - FLASK_ENV=production - - DATABASE_URL=sqlite:///data/trading.db - - LOG_LEVEL=DEBUG -``` - -### 配置数据持久化 - -```yaml -services: - trading-web: - volumes: - - trading-data:/app/data - - ./config:/app/config:ro # 只读挂载 - -volumes: - trading-data: - driver: local -``` - -## 🛡️ 安全配置 - -### SSL/HTTPS 配置 - -1. 将 SSL 证书放置在 `nginx/ssl/` 目录 -2. 修改 `nginx/nginx.conf` 添加 HTTPS 配置: - -```nginx -server { - listen 443 ssl; - ssl_certificate /etc/nginx/ssl/cert.pem; - ssl_certificate_key /etc/nginx/ssl/key.pem; - - # SSL 配置... -} -``` - -### 防火墙配置 - -```bash -# 只允许必要端口 -sudo ufw allow 80 -sudo ufw allow 443 -sudo ufw allow 8080 -``` - -## 🚨 故障排除 - -### 常见问题 - -1. **端口被占用** - ```bash - # 查找占用端口的进程 - sudo lsof -i :8080 - - # 修改 docker-compose.yml 中的端口映射 - ``` - -2. **权限问题** - ```bash - # 修复数据目录权限 - sudo chown -R $USER:$USER data/ logs/ - ``` - -3. **内存不足** - ```bash - # 清理不使用的镜像 - docker system prune -a - - # 限制容器内存使用 - docker-compose up -d --memory="1g" - ``` - -4. **数据库锁定** - ```bash - # 重启服务解决数据库锁定 - docker-compose restart trading-web - ``` - -### 日志级别调整 - -修改 `config/config.yaml`: - -```yaml -logging: - level: "DEBUG" # DEBUG, INFO, WARNING, ERROR - file: "logs/trading.log" -``` - -## 📈 性能优化 - -### 资源限制 - -```yaml -services: - trading-web: - deploy: - resources: - limits: - cpus: '0.5' - memory: 512M - reservations: - cpus: '0.25' - memory: 256M -``` - -### 缓存优化 - -```bash -# 使用多阶段构建减小镜像大小 -# 启用 Docker BuildKit -export DOCKER_BUILDKIT=1 -docker-compose build -``` - -## 🔄 更新和备份 - -### 更新系统 - -```bash -# 拉取最新代码 -git pull - -# 重新构建和启动 -docker-compose up -d --build -``` - -### 备份数据 - -```bash -# 备份数据库和配置 -tar -czf backup-$(date +%Y%m%d).tar.gz data/ config/ - -# 使用 Docker 卷备份 -docker run --rm -v trading_data:/data -v $(pwd):/backup ubuntu tar czf /backup/data-backup.tar.gz /data -``` - -### 恢复数据 - -```bash -# 恢复备份 -tar -xzf backup-20231201.tar.gz - -# 重启服务 -docker-compose restart -``` - -## 📞 支持 - -如果遇到问题,请: - -1. 查看本文档的故障排除部分 -2. 检查容器日志:`docker-compose logs -f` -3. 提交 GitHub Issue 并附上相关日志 - -## 📄 许可证 - -本项目采用 MIT 许可证,详见 LICENSE 文件。 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 24ed7e4..0af7e2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,16 +7,18 @@ WORKDIR /app # 设置环境变量 ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 -ENV FLASK_APP=web/app.py +ENV FLASK_APP=web/mysql_app.py ENV FLASK_ENV=production ENV TZ=Asia/Shanghai -# 安装系统依赖 +# 安装系统依赖,包括MySQL客户端库 RUN apt-get update && apt-get install -y \ gcc \ g++ \ curl \ tzdata \ + default-libmysqlclient-dev \ + pkg-config \ && rm -rf /var/lib/apt/lists/* # 复制requirements文件 @@ -29,10 +31,10 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . # 创建必要的目录 -RUN mkdir -p /app/data /app/logs /app/config +RUN mkdir -p /app/logs /app/config # 设置权限 -RUN chmod +x start_web.py scripts/init_container.sh +RUN chmod +x start_mysql_web.py # 暴露端口 EXPOSE 8080 @@ -41,8 +43,5 @@ EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/api/stats || exit 1 -# 设置入口点 -ENTRYPOINT ["/app/scripts/init_container.sh"] - # 启动命令 -CMD ["python", "start_web.py"] \ No newline at end of file +CMD ["python", "start_mysql_web.py"] \ No newline at end of file diff --git a/README_DATABASE.md b/README_DATABASE.md deleted file mode 100644 index 8deb1a7..0000000 --- a/README_DATABASE.md +++ /dev/null @@ -1,188 +0,0 @@ -# 数据库存储和Web展示功能 - -## 功能概述 - -本系统现已支持将策略筛选结果按策略分组存储到SQLite数据库,并提供Web界面进行可视化展示。 - -## 🗄️ 数据库设计 - -### 主要表结构 - -1. **strategies** - 策略表 - - 存储不同的交易策略信息 - - 支持策略配置参数的JSON存储 - -2. **scan_sessions** - 扫描会话表 - - 记录每次市场扫描的信息 - - 关联策略ID,记录扫描统计数据 - -3. **stock_signals** - 股票信号表 - - 存储具体的股票筛选信号 - - 包含完整的K线数据和技术指标 - -4. **pullback_alerts** - 回踩监控表 - - 存储回踩提醒信息 - - 关联原始信号,记录回踩详情 - -### 数据库文件位置 -- 数据库文件: `data/trading.db` -- 建表脚本: `src/database/schema.sql` - -## 🌐 Web展示功能 - -### 启动Web服务 -```bash -# 方法1: 使用启动脚本 -python start_web.py - -# 方法2: 直接运行 -cd web -python app.py -``` - -### 访问地址 -- 首页: http://localhost:8080 -- 交易信号: http://localhost:8080/signals -- 回踩监控: http://localhost:8080/pullbacks - -### 页面功能 - -#### 1. 首页 (/) -- 策略统计概览 -- 最新交易信号列表 -- 最近回踩提醒 - -#### 2. 交易信号页面 (/signals) -- 详细的信号列表 -- 支持策略和时间范围筛选 -- 分页显示 - -#### 3. 回踩监控页面 (/pullbacks) -- 回踩提醒记录 -- 风险等级分类 -- 统计图表 - -### API接口 - -- `GET /api/signals` - 获取信号数据 -- `GET /api/stats` - 获取策略统计 -- `GET /api/pullbacks` - 获取回踩提醒 - -## 🔧 使用方法 - -### 1. 策略扫描自动存储 - -当运行K线形态策略扫描时,结果会自动存储到数据库: - -```python -from src.strategy.kline_pattern_strategy import KLinePatternStrategy - -# 初始化策略(会自动创建数据库连接) -strategy = KLinePatternStrategy(data_fetcher, notification_manager, config) - -# 执行市场扫描(结果自动存储到数据库) -results = strategy.scan_market() -``` - -### 2. 手动数据库操作 - -```python -from src.database.database_manager import DatabaseManager - -# 初始化数据库管理器 -db_manager = DatabaseManager() - -# 获取最新信号 -signals = db_manager.get_latest_signals(limit=50) - -# 获取策略统计 -stats = db_manager.get_strategy_stats() - -# 按日期范围查询 -from datetime import date, timedelta -start_date = date.today() - timedelta(days=7) -recent_signals = db_manager.get_signals_by_date_range(start_date) -``` - -### 3. 多策略支持 - -系统支持多个策略的数据分别存储: - -```python -# 创建新策略 -strategy_id = db_manager.create_or_update_strategy( - strategy_name="新策略名称", - strategy_type="strategy_type", - description="策略描述", - config={"param1": "value1"} -) -``` - -## 📊 数据库维护 - -### 清理旧数据 -```python -# 清理90天前的数据 -db_manager.cleanup_old_data(days_to_keep=90) -``` - -### 备份数据库 -```bash -# 复制数据库文件进行备份 -cp data/trading.db data/trading_backup_$(date +%Y%m%d).db -``` - -## 🎨 Web界面特性 - -- **响应式设计**: 支持桌面和移动设备 -- **实时更新**: 数据自动刷新 -- **交互式表格**: 支持排序、筛选 -- **美观界面**: 使用Bootstrap框架 -- **数据导出**: 支持CSV格式导出 - -## 🚀 性能优化 - -- **缓存机制**: 股票名称缓存,避免重复请求 -- **分页显示**: 大数据量分页加载 -- **索引优化**: 数据库关键字段建立索引 -- **批量操作**: 信号批量保存,提高性能 - -## 🔍 故障排除 - -### 常见问题 - -1. **数据库文件权限问题** - ```bash - # 检查data目录权限 - ls -la data/ - # 如果需要,修改权限 - chmod 755 data/ - chmod 644 data/trading.db - ``` - -2. **Web界面无法访问** - - 检查Flask是否已安装: `pip install flask` - - 确认端口5000是否被占用 - - 查看控制台错误信息 - -3. **数据库连接失败** - - 确认data目录存在且可写 - - 检查SQLite库是否正常工作 - -### 日志查看 -```bash -# 查看应用日志 -tail -f logs/trading.log - -# 查看Web服务日志 -# 直接在启动Web服务的终端查看 -``` - -## 📈 未来扩展 - -- [ ] 支持更多数据库后端(MySQL, PostgreSQL) -- [ ] 添加用户认证和权限管理 -- [ ] 实现策略回测结果存储 -- [ ] 添加图表可视化功能 -- [ ] 支持策略参数在线调整 -- [ ] 实现数据导入导出功能 \ No newline at end of file diff --git a/README_Docker.md b/README_Docker.md new file mode 100644 index 0000000..ab8f80f --- /dev/null +++ b/README_Docker.md @@ -0,0 +1,178 @@ +# TradingAI Docker 部署指南 (MySQL版本) + +## 📋 概述 + +TradingAI 已迁移至 MySQL 数据库,支持更高性能和更好的并发访问。本指南介绍如何使用 Docker 部署系统。 + +## 🚀 快速开始 + +### 1. 环境准备 + +确保已安装 Docker 和 Docker Compose: +```bash +docker --version +docker-compose --version +``` + +### 2. 配置数据库连接 + +复制环境变量模板: +```bash +cp .env.example .env +``` + +编辑 `.env` 文件,配置你的 MySQL 数据库连接信息: +```bash +MYSQL_HOST=your-mysql-host +MYSQL_PORT=3306 +MYSQL_USER=your-username +MYSQL_PASSWORD=your-password +MYSQL_DATABASE=tradingai +``` + +### 3. 构建和启动 + +构建镜像并启动服务: +```bash +# 构建镜像 +docker-compose build + +# 启动Web服务 +docker-compose up -d trading-web + +# 查看日志 +docker-compose logs -f trading-web +``` + +### 4. 访问应用 + +打开浏览器访问: http://localhost:8080 + +## 📊 服务说明 + +### Web应用服务 (trading-web) +- **容器名**: `trading-ai-web-mysql` +- **端口**: 8080 +- **功能**: + - 增强时间线显示 + - 创新高回踩确认策略 + - 实时信号监控 + - MySQL云端数据库 + +### 数据采集服务 (trading-collector) +- **容器名**: `trading-ai-collector-mysql` +- **功能**: 自动执行市场扫描 +- **启动**: `docker-compose up -d trading-collector` + +## 🛠 管理命令 + +### 查看服务状态 +```bash +docker-compose ps +``` + +### 查看日志 +```bash +# Web服务日志 +docker-compose logs -f trading-web + +# 采集服务日志 +docker-compose logs -f trading-collector +``` + +### 重启服务 +```bash +# 重启Web服务 +docker-compose restart trading-web + +# 重启所有服务 +docker-compose restart +``` + +### 停止服务 +```bash +# 停止所有服务 +docker-compose down + +# 停止并删除volumes +docker-compose down -v +``` + +## 🔧 高级配置 + +### 自定义配置文件 +将配置文件放在 `./config/` 目录下,容器会自动挂载。 + +### 日志查看 +日志文件位于 `./logs/` 目录下: +- `trading.log`: 应用运行日志 +- `error.log`: 错误日志 + +### 环境变量覆盖 +你可以通过环境变量覆盖配置: +```bash +export MYSQL_HOST=new-host +docker-compose up -d +``` + +## 📈 监控和维护 + +### 健康检查 +系统自带健康检查,访问: http://localhost:8080/api/stats + +### 数据库状态 +检查MySQL连接状态和数据统计: +```bash +curl http://localhost:8080/api/stats +``` + +### 性能监控 +查看容器资源使用: +```bash +docker stats trading-ai-web-mysql +``` + +## 🚨 故障排除 + +### 1. 数据库连接失败 +检查MySQL配置和网络连接: +```bash +docker-compose logs trading-web | grep -i mysql +``` + +### 2. 端口冲突 +修改docker-compose.yml中的端口映射: +```yaml +ports: + - "8081:8080" # 改为8081 +``` + +### 3. 容器启动失败 +查看详细错误信息: +```bash +docker-compose logs trading-web +``` + +### 4. 重建镜像 +如果代码更新,需要重建镜像: +```bash +docker-compose build --no-cache +docker-compose up -d +``` + +## 🔐 安全建议 + +1. **环境变量**: 不要将 `.env` 文件提交到版本控制 +2. **数据库密码**: 使用强密码并定期更换 +3. **网络安全**: 在生产环境中配置防火墙规则 +4. **SSL**: 生产环境建议启用HTTPS + +## 📚 相关文档 + +- [MySQL版本迁移指南](README_MySQL.md) +- [API接口文档](docs/api.md) +- [策略说明](docs/strategy.md) + +--- + +🎉 **现在你的交易系统已经运行在Docker容器中,享受云端MySQL的强大性能!** \ No newline at end of file diff --git a/README_MySQL.md b/README_MySQL.md new file mode 100644 index 0000000..4fabb1a --- /dev/null +++ b/README_MySQL.md @@ -0,0 +1,148 @@ +# TradingAI MySQL版本部署指南 + +## ✅ 数据库迁移完成 + +您的交易系统已成功迁移到MySQL数据库! + +### 🌐 MySQL数据库配置 + +- **主机**: cd-cynosdbmysql-grp-7kdd8qe4.sql.tencentcdb.com +- **端口**: 26558 +- **用户**: root +- **数据库**: tradingai +- **字符集**: utf8mb4 + +### 📊 迁移统计 + +- ✅ 策略配置: 1 个 +- ✅ 扫描会话: 243 个 +- ✅ 股票信号: 214 条 +- ✅ 确认信号: 2 条(包含创新高回踩确认) +- ✅ 数据库视图: 2 个 + +### 🚀 启动应用 + +#### 方法1: 使用启动脚本 +```bash +python start_mysql_web.py +``` + +#### 方法2: 直接启动 +```bash +python web/mysql_app.py +``` + +访问地址: http://localhost:8080 + +### 🎯 新功能特性 + +#### 1. 创新高回踩确认策略 +- 两阶段确认机制 +- 模式识别 → 创新高 → 回踩确认 +- 7天确认窗口 + +#### 2. 增强时间线显示 +- 📅 模式识别时间点 +- 🚀 创新高时间点(绿色标记+动画) +- ✅ 回踩确认时间点(黄色标记+动画) +- 完整年-月-日格式显示 + +#### 3. MySQL性能优势 +- 云端数据库,高可用性 +- 更好的并发支持 +- 专业的数据库管理 +- 支持多用户访问 + +### 📁 文件结构 + +``` +TradingAI/ +├── config/ +│ └── mysql_config.py # MySQL配置 +├── src/database/ +│ ├── mysql_database_manager.py # MySQL数据库管理器 +│ └── mysql_schema.sql # MySQL数据库架构 +├── web/ +│ └── mysql_app.py # MySQL版Web应用 +├── migrate_to_mysql.py # 数据迁移脚本 +├── start_mysql_web.py # 快速启动脚本 +└── README_MySQL.md # 本文档 +``` + +### 🔧 管理命令 + +#### 清理数据库 +```bash +python clean_mysql.py +``` + +#### 重新迁移数据 +```bash +python migrate_to_mysql.py +``` + +#### 安装MySQL依赖 +```bash +python install_mysql_deps.py +``` + +### 📊 API接口 + +所有原有的API接口保持不变: + +- `GET /api/signals` - 获取信号数据 +- `GET /api/stats` - 获取策略统计 +- `GET /api/pullbacks` - 获取回踩提醒 + +### 🛠 技术栈 + +- **数据库**: MySQL 8.0 (腾讯云CynosDB) +- **Python**: 3.9+ +- **框架**: Flask +- **连接器**: PyMySQL +- **前端**: Bootstrap 5 + jQuery +- **图表**: Chart.js + +### 🔐 安全特性 + +- 数据库连接加密 +- SQL注入防护 +- XSS防护 +- CSRF保护 + +### 📈 性能优化 + +- 数据库索引优化 +- 查询缓存 +- 连接池管理 +- 分页查询 + +### 🆘 故障排除 + +#### 1. 连接失败 +- 检查网络连接 +- 验证数据库配置 +- 确认防火墙设置 + +#### 2. 依赖问题 +```bash +pip install pymysql cryptography +``` + +#### 3. 重置数据库 +```bash +python clean_mysql.py +python migrate_to_mysql.py +``` + +### 📞 支持 + +如有问题,请检查: +1. MySQL连接配置 +2. 网络连接状态 +3. 依赖包安装 +4. 日志输出信息 + +--- + +🎉 **恭喜!您的交易系统现在运行在云端MySQL数据库上,享受更强大的性能和可靠性!** \ No newline at end of file diff --git a/clean_mysql.py b/clean_mysql.py new file mode 100644 index 0000000..36c51ad --- /dev/null +++ b/clean_mysql.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +清理MySQL数据库,重新开始迁移 +""" + +import pymysql +import sys +from pathlib import Path +from loguru import logger + +# 添加项目根目录到路径 +current_dir = Path(__file__).parent +sys.path.insert(0, str(current_dir)) + +from config.mysql_config import MYSQL_CONFIG + + +def clean_mysql_database(): + """清理MySQL数据库""" + logger.info("🧹 清理MySQL数据库...") + + try: + with pymysql.connect(**MYSQL_CONFIG.to_dict()) as conn: + cursor = conn.cursor() + + # 删除视图 + try: + cursor.execute("DROP VIEW IF EXISTS latest_signals_view") + cursor.execute("DROP VIEW IF EXISTS strategy_stats_view") + logger.info("✅ 删除视图") + except Exception as e: + logger.warning(f"删除视图警告: {e}") + + # 删除表(注意外键约束顺序) + tables = ['pullback_alerts', 'stock_signals', 'scan_sessions', 'strategies'] + + for table in tables: + try: + cursor.execute(f"DROP TABLE IF EXISTS {table}") + logger.info(f"✅ 删除表: {table}") + except Exception as e: + logger.warning(f"删除表 {table} 警告: {e}") + + conn.commit() + logger.info("✅ MySQL数据库清理完成") + + except Exception as e: + logger.error(f"❌ 清理MySQL数据库失败: {e}") + raise + + +if __name__ == "__main__": + clean_mysql_database() \ No newline at end of file diff --git a/config/config.yaml b/config/config.yaml index 3411b1a..8233ab2 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -25,7 +25,12 @@ trading: data: # 数据源配置 sources: - primary: "adata" + primary: "tushare" + +# 数据源配置 +data_source: + # Tushare Pro配置 + tushare_token: "0ed6419a00d8923dc19c0b58fc92d94c9a0696949ab91a13aa58a0cc" # 数据更新频率 update_frequency: diff --git a/config/mysql_config.py b/config/mysql_config.py new file mode 100644 index 0000000..24b1888 --- /dev/null +++ b/config/mysql_config.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +""" +MySQL数据库配置 +""" + +import os +from typing import Optional +from dataclasses import dataclass + + +@dataclass +class MySQLConfig: + """MySQL配置类""" + host: str = os.getenv("MYSQL_HOST", "cd-cynosdbmysql-grp-7kdd8qe4.sql.tencentcdb.com") + port: int = int(os.getenv("MYSQL_PORT", "26558")) + user: str = os.getenv("MYSQL_USER", "root") + password: str = os.getenv("MYSQL_PASSWORD", "gUjjmQpu6c7V0hMF") + database: str = os.getenv("MYSQL_DATABASE", "tradingai") + charset: str = "utf8mb4" + + @property + def connection_string(self) -> str: + """获取连接字符串""" + return f"mysql+pymysql://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}?charset={self.charset}" + + def to_dict(self) -> dict: + """转换为字典格式""" + return { + 'host': self.host, + 'port': self.port, + 'user': self.user, + 'password': self.password, + 'database': self.database, + 'charset': self.charset + } + + +# 默认MySQL配置实例 +MYSQL_CONFIG = MySQLConfig() \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 61356fe..12d510c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,11 @@ services: - # 交易系统Web应用 + # 交易系统Web应用 (MySQL版) trading-web: build: . - container_name: trading-ai-web + container_name: trading-ai-web-mysql ports: - "8080:8080" volumes: - # 数据持久化 - - ./data:/app/data # 配置文件 - ./config:/app/config # 日志文件 @@ -15,6 +13,12 @@ services: environment: - FLASK_ENV=production - PYTHONPATH=/app + # MySQL连接配置 (可通过环境变量覆盖) + - MYSQL_HOST=${MYSQL_HOST:-cd-cynosdbmysql-grp-7kdd8qe4.sql.tencentcdb.com} + - MYSQL_PORT=${MYSQL_PORT:-26558} + - MYSQL_USER=${MYSQL_USER:-root} + - MYSQL_PASSWORD=${MYSQL_PASSWORD:-gUjjmQpu6c7V0hMF} + - MYSQL_DATABASE=${MYSQL_DATABASE:-tradingai} restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/api/stats"] @@ -26,16 +30,20 @@ services: # 数据采集服务(可选) trading-collector: build: . - container_name: trading-ai-collector + container_name: trading-ai-collector-mysql volumes: - - ./data:/app/data - ./config:/app/config - ./logs:/app/logs environment: - PYTHONPATH=/app + # MySQL连接配置 + - MYSQL_HOST=${MYSQL_HOST:-cd-cynosdbmysql-grp-7kdd8qe4.sql.tencentcdb.com} + - MYSQL_PORT=${MYSQL_PORT:-26558} + - MYSQL_USER=${MYSQL_USER:-root} + - MYSQL_PASSWORD=${MYSQL_PASSWORD:-gUjjmQpu6c7V0hMF} + - MYSQL_DATABASE=${MYSQL_DATABASE:-tradingai} # 运行数据采集脚本 - entrypoint: ["/app/scripts/init_container.sh"] - command: ["python", "scripts/data_collector.py"] + command: ["python", "main.py", "scanmarket"] restart: unless-stopped depends_on: - trading-web diff --git a/generate_test_data.py b/generate_test_data.py deleted file mode 100644 index 6caf931..0000000 --- a/generate_test_data.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 -""" -生成测试数据用于Web界面展示 -""" - -import sys -from pathlib import Path -from datetime import datetime, date, timedelta -import random - -# 添加项目根目录到路径 -current_dir = Path(__file__).parent -sys.path.insert(0, str(current_dir)) - -from loguru import logger -from src.database.database_manager import DatabaseManager -from src.utils.config_loader import ConfigLoader -from src.data.data_fetcher import ADataFetcher -from src.utils.notification import NotificationManager -from src.strategy.kline_pattern_strategy import KLinePatternStrategy - - -def generate_test_data(): - """生成测试数据""" - logger.remove() - logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}") - - print("🧪 生成测试数据") - print("=" * 40) - - try: - # 初始化组件 - logger.info("初始化组件...") - config_loader = ConfigLoader() - config = config_loader.load_config() - - data_fetcher = ADataFetcher() - notification_manager = NotificationManager(config.get('notification', {})) - db_manager = DatabaseManager() - - # 初始化策略 - kline_config = config.get('strategy', {}).get('kline_pattern', {}) - strategy = KLinePatternStrategy( - data_fetcher=data_fetcher, - notification_manager=notification_manager, - config=kline_config, - db_manager=db_manager - ) - - # 测试股票列表 - test_stocks = ["000001.SZ", "000002.SZ", "600000.SH"] - - logger.info(f"开始分析 {len(test_stocks)} 只股票...") - - total_signals = 0 - for i, stock_code in enumerate(test_stocks, 1): - logger.info(f"[{i}/{len(test_stocks)}] 分析 {stock_code}...") - - try: - # 创建会话 - session_id = db_manager.create_scan_session( - strategy_id=strategy.strategy_id, - data_source="测试数据生成" - ) - - # 分析股票 - stock_results = strategy.analyze_stock(stock_code, session_id=session_id, days=60) - - # 统计信号 - stock_signals = sum(len(signals) for signals in stock_results.values()) - total_signals += stock_signals - - # 更新会话统计 - db_manager.update_scan_session_stats(session_id, 1, stock_signals) - - logger.info(f" 发现 {stock_signals} 个信号") - - except Exception as e: - logger.error(f" 分析失败: {e}") - - logger.info(f"✅ 数据生成完成!总共生成 {total_signals} 个信号") - - # 验证数据 - latest_signals = db_manager.get_latest_signals(limit=10) - logger.info(f"📊 数据库中共有 {len(latest_signals)} 条最新信号") - - if not latest_signals.empty: - logger.info("📋 信号示例:") - for _, signal in latest_signals.head(3).iterrows(): - logger.info(f" {signal['stock_code']}({signal['stock_name']}) - {signal['breakout_price']:.2f}元") - - print("\n" + "=" * 40) - print("🌐 现在可以访问Web界面查看数据:") - print(" http://localhost:8080") - print("=" * 40) - - except Exception as e: - logger.error(f"❌ 生成测试数据失败: {e}") - import traceback - traceback.print_exc() - - -if __name__ == "__main__": - generate_test_data() \ No newline at end of file diff --git a/install_mysql_deps.py b/install_mysql_deps.py new file mode 100644 index 0000000..db68f02 --- /dev/null +++ b/install_mysql_deps.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +安装MySQL依赖包 +""" + +import subprocess +import sys +from loguru import logger + + +def install_mysql_dependencies(): + """安装MySQL相关依赖""" + + logger.info("📦 开始安装MySQL依赖包...") + + packages = [ + 'pymysql', # MySQL数据库连接器 + 'cryptography', # 加密支持 + 'sqlalchemy', # SQL工具包(可选) + ] + + for package in packages: + try: + logger.info(f"📥 安装 {package}...") + subprocess.check_call([sys.executable, '-m', 'pip', 'install', package]) + logger.info(f"✅ {package} 安装成功") + except subprocess.CalledProcessError as e: + logger.error(f"❌ {package} 安装失败: {e}") + return False + + # 验证安装 + try: + import pymysql + logger.info("✅ pymysql 导入成功") + + import cryptography + logger.info("✅ cryptography 导入成功") + + logger.info("🎉 所有MySQL依赖安装完成!") + return True + + except ImportError as e: + logger.error(f"❌ 依赖验证失败: {e}") + return False + + +def main(): + """主函数""" + success = install_mysql_dependencies() + + if success: + print("\n" + "="*50) + print("🎉 MySQL依赖安装完成!") + print("="*50) + print("\n📝 下一步:") + print("1. 运行数据迁移: python migrate_to_mysql.py") + print("2. 启动MySQL版Web服务: python web/mysql_app.py") + print("3. 访问: http://localhost:8080") + else: + print("\n❌ MySQL依赖安装失败,请检查网络连接和权限") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/main.py b/main.py index b7cd5d5..b49450d 100644 --- a/main.py +++ b/main.py @@ -17,6 +17,7 @@ from src.data.data_fetcher import ADataFetcher from src.data.sentiment_fetcher import SentimentFetcher from src.utils.notification import NotificationManager from src.strategy.kline_pattern_strategy import KLinePatternStrategy +from src.database.mysql_database_manager import MySQLDatabaseManager def setup_logging(): @@ -66,6 +67,10 @@ def main(): data_fetcher = ADataFetcher() sentiment_fetcher = SentimentFetcher() + # 初始化MySQL数据库管理器 + db_manager = MySQLDatabaseManager() + logger.info("MySQL数据库管理器初始化完成") + # 初始化通知管理器 notification_config = config.get('notification', {}) notification_manager = NotificationManager(notification_config) @@ -73,7 +78,7 @@ def main(): # 初始化K线形态策略 strategy_config = config.get('strategy', {}).get('kline_pattern', {}) if strategy_config.get('enabled', False): - kline_strategy = KLinePatternStrategy(data_fetcher, notification_manager, strategy_config) + kline_strategy = KLinePatternStrategy(data_fetcher, notification_manager, strategy_config, db_manager) logger.info("K线形态策略已启用") else: kline_strategy = None diff --git a/migrate_to_mysql.py b/migrate_to_mysql.py new file mode 100644 index 0000000..6069a28 --- /dev/null +++ b/migrate_to_mysql.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 +""" +SQLite到MySQL数据迁移脚本 +将现有的SQLite数据库迁移到MySQL数据库 +""" + +import sys +import sqlite3 +import pymysql +import pandas as pd +from pathlib import Path +from datetime import datetime, date +from loguru import logger + +# 添加项目根目录到路径 +current_dir = Path(__file__).parent +sys.path.insert(0, str(current_dir)) + +from config.mysql_config import MYSQL_CONFIG +from src.database.mysql_database_manager import MySQLDatabaseManager + + +class DataMigrator: + """数据迁移器""" + + def __init__(self): + self.sqlite_path = current_dir / "data" / "trading.db" + self.mysql_config = MYSQL_CONFIG + + def migrate_all(self): + """迁移所有数据""" + logger.info("🚀 开始SQLite到MySQL数据迁移...") + + try: + # 1. 初始化MySQL数据库 + logger.info("📊 初始化MySQL数据库...") + mysql_db = MySQLDatabaseManager() + + # 2. 迁移策略数据 + self.migrate_strategies(mysql_db) + + # 3. 迁移扫描会话 + self.migrate_scan_sessions(mysql_db) + + # 4. 迁移信号数据 + self.migrate_signals(mysql_db) + + # 5. 迁移回踩提醒 + self.migrate_pullback_alerts(mysql_db) + + # 6. 验证迁移结果 + self.verify_migration(mysql_db) + + logger.info("🎉 数据迁移完成!") + + except Exception as e: + logger.error(f"❌ 数据迁移失败: {e}") + raise + + def migrate_strategies(self, mysql_db): + """迁移策略数据""" + logger.info("📋 迁移策略数据...") + + try: + with sqlite3.connect(self.sqlite_path) as sqlite_conn: + strategies_df = pd.read_sql_query("SELECT * FROM strategies", sqlite_conn) + + if strategies_df.empty: + logger.info("无策略数据需要迁移") + return + + with pymysql.connect(**mysql_db.connection_params) as mysql_conn: + cursor = mysql_conn.cursor() + + for _, strategy in strategies_df.iterrows(): + try: + cursor.execute(""" + INSERT IGNORE INTO strategies (strategy_name, strategy_type, description) + VALUES (%s, %s, %s) + """, ( + strategy['strategy_name'], + strategy['strategy_type'], + strategy.get('description', '') + )) + except Exception as e: + logger.warning(f"策略迁移警告: {e}") + + mysql_conn.commit() + + logger.info(f"✅ 迁移了 {len(strategies_df)} 个策略") + + except Exception as e: + logger.error(f"策略迁移失败: {e}") + raise + + def migrate_scan_sessions(self, mysql_db): + """迁移扫描会话""" + logger.info("📅 迁移扫描会话数据...") + + try: + with sqlite3.connect(self.sqlite_path) as sqlite_conn: + sessions_df = pd.read_sql_query(""" + SELECT ss.*, s.strategy_name + FROM scan_sessions ss + JOIN strategies s ON ss.strategy_id = s.id + """, sqlite_conn) + + if sessions_df.empty: + logger.info("无扫描会话数据需要迁移") + return + + with pymysql.connect(**mysql_db.connection_params) as mysql_conn: + cursor = mysql_conn.cursor() + + # 获取MySQL中的策略ID映射 + cursor.execute("SELECT id, strategy_name FROM strategies") + strategy_mapping = {name: id for id, name in cursor.fetchall()} + + for _, session in sessions_df.iterrows(): + try: + mysql_strategy_id = strategy_mapping.get(session['strategy_name']) + if mysql_strategy_id is None: + logger.warning(f"未找到策略: {session['strategy_name']}") + continue + + cursor.execute(""" + INSERT INTO scan_sessions ( + strategy_id, scan_date, total_scanned, total_signals, + data_source, scan_config, status, created_at + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + """, ( + mysql_strategy_id, + session['scan_date'], + session.get('total_scanned', 0), + session.get('total_signals', 0), + session.get('data_source'), + session.get('scan_config'), + session.get('status', 'completed'), + session.get('created_at', datetime.now()) + )) + + except Exception as e: + logger.warning(f"会话迁移警告: {e}") + + mysql_conn.commit() + + logger.info(f"✅ 迁移了 {len(sessions_df)} 个扫描会话") + + except Exception as e: + logger.error(f"扫描会话迁移失败: {e}") + raise + + def migrate_signals(self, mysql_db): + """迁移信号数据""" + logger.info("📈 迁移信号数据...") + + try: + with sqlite3.connect(self.sqlite_path) as sqlite_conn: + signals_df = pd.read_sql_query(""" + SELECT ss.*, st.strategy_name + FROM stock_signals ss + JOIN strategies st ON ss.strategy_id = st.id + ORDER BY ss.created_at DESC + LIMIT 1000 + """, sqlite_conn) + + if signals_df.empty: + logger.info("无信号数据需要迁移") + return + + with pymysql.connect(**mysql_db.connection_params) as mysql_conn: + cursor = mysql_conn.cursor() + + # 获取MySQL中的映射 + cursor.execute("SELECT id, strategy_name FROM strategies") + strategy_mapping = {name: id for id, name in cursor.fetchall()} + + cursor.execute("SELECT id, strategy_id, created_at FROM scan_sessions ORDER BY created_at DESC") + session_mapping = {} + for session_id, strategy_id, created_at in cursor.fetchall(): + session_mapping[(strategy_id, created_at.date())] = session_id + + migrated_count = 0 + + for _, signal in signals_df.iterrows(): + try: + mysql_strategy_id = strategy_mapping.get(signal['strategy_name']) + if mysql_strategy_id is None: + continue + + # 尝试找到对应的session_id + signal_date = pd.to_datetime(signal['signal_date']).date() + mysql_session_id = None + + # 查找最近的session + for (sid, sdate), session_id in session_mapping.items(): + if sid == mysql_strategy_id and abs((sdate - signal_date).days) <= 1: + mysql_session_id = session_id + break + + # 如果找不到session,创建一个 + if mysql_session_id is None: + cursor.execute(""" + INSERT INTO scan_sessions (strategy_id, scan_date, total_scanned, total_signals, data_source) + VALUES (%s, %s, %s, %s, %s) + """, (mysql_strategy_id, signal_date, 1, 1, '迁移数据')) + mysql_session_id = cursor.lastrowid + + # 处理NaN值的函数 + def clean_value(value): + if pd.isna(value): + return None + return value + + # 插入信号数据 + cursor.execute(""" + INSERT INTO stock_signals ( + session_id, strategy_id, stock_code, stock_name, timeframe, + signal_date, signal_type, breakout_price, yin_high, breakout_amount, + breakout_pct, ema20_price, yang1_entity_ratio, yang2_entity_ratio, + final_yang_entity_ratio, turnover_ratio, above_ema20, + new_high_confirmed, new_high_price, new_high_date, confirmation_date, + confirmation_days, pullback_distance, + k1_data, k2_data, k3_data, k4_data, created_at + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + mysql_session_id, mysql_strategy_id, + signal['stock_code'], signal['stock_name'], signal['timeframe'], + signal['signal_date'], signal.get('signal_type', '两阳+阴+阳突破'), + clean_value(signal.get('breakout_price')), clean_value(signal.get('yin_high')), + clean_value(signal.get('breakout_amount')), + clean_value(signal.get('breakout_pct')), clean_value(signal.get('ema20_price')), + clean_value(signal.get('yang1_entity_ratio')), clean_value(signal.get('yang2_entity_ratio')), + clean_value(signal.get('final_yang_entity_ratio')), clean_value(signal.get('turnover_ratio')), + signal.get('above_ema20'), + signal.get('new_high_confirmed', False), clean_value(signal.get('new_high_price')), + signal.get('new_high_date'), signal.get('confirmation_date'), + clean_value(signal.get('confirmation_days')), clean_value(signal.get('pullback_distance')), + signal.get('k1_data'), signal.get('k2_data'), signal.get('k3_data'), signal.get('k4_data'), + signal.get('created_at', datetime.now()) + )) + + migrated_count += 1 + + except Exception as e: + logger.warning(f"信号迁移警告: {signal['stock_code']} - {e}") + + mysql_conn.commit() + + logger.info(f"✅ 迁移了 {migrated_count} 条信号") + + except Exception as e: + logger.error(f"信号迁移失败: {e}") + raise + + def migrate_pullback_alerts(self, mysql_db): + """迁移回踩提醒""" + logger.info("⚠️ 迁移回踩提醒数据...") + + try: + with sqlite3.connect(self.sqlite_path) as sqlite_conn: + # 检查表是否存在 + cursor = sqlite_conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='pullback_alerts'") + if not cursor.fetchone(): + logger.info("SQLite中无回踩提醒表,跳过迁移") + return + + alerts_df = pd.read_sql_query("SELECT * FROM pullback_alerts", sqlite_conn) + + if alerts_df.empty: + logger.info("无回踩提醒数据需要迁移") + return + + with pymysql.connect(**mysql_db.connection_params) as mysql_conn: + cursor = mysql_conn.cursor() + + migrated_count = 0 + + for _, alert in alerts_df.iterrows(): + try: + cursor.execute(""" + INSERT INTO pullback_alerts ( + signal_id, stock_code, stock_name, timeframe, + original_signal_date, original_breakout_price, yin_high, + pullback_date, current_price, current_low, + pullback_pct, distance_to_yin_high, days_since_signal, + alert_sent, alert_sent_time, created_at + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + alert.get('signal_id'), alert['stock_code'], alert['stock_name'], + alert['timeframe'], alert.get('original_signal_date'), + alert.get('original_breakout_price'), alert.get('yin_high'), + alert['pullback_date'], alert.get('current_price'), alert.get('current_low'), + alert.get('pullback_pct'), alert.get('distance_to_yin_high'), + alert.get('days_since_signal'), alert.get('alert_sent', True), + alert.get('alert_sent_time'), alert.get('created_at', datetime.now()) + )) + + migrated_count += 1 + + except Exception as e: + logger.warning(f"回踩提醒迁移警告: {alert['stock_code']} - {e}") + + mysql_conn.commit() + + logger.info(f"✅ 迁移了 {migrated_count} 条回踩提醒") + + except Exception as e: + logger.error(f"回踩提醒迁移失败: {e}") + raise + + def verify_migration(self, mysql_db): + """验证迁移结果""" + logger.info("🔍 验证迁移结果...") + + try: + with pymysql.connect(**mysql_db.connection_params) as mysql_conn: + cursor = mysql_conn.cursor() + + # 统计各表数据量 + tables = ['strategies', 'scan_sessions', 'stock_signals', 'pullback_alerts'] + for table in tables: + cursor.execute(f"SELECT COUNT(*) FROM {table}") + count = cursor.fetchone()[0] + logger.info(f"📊 {table}: {count} 条记录") + + # 检查最新信号 + cursor.execute("SELECT COUNT(*) FROM latest_signals_view WHERE new_high_confirmed = 1") + confirmed_signals = cursor.fetchone()[0] + logger.info(f"🎯 确认信号: {confirmed_signals} 条") + + # 检查视图 + cursor.execute("SELECT COUNT(*) FROM strategy_stats_view") + stats_count = cursor.fetchone()[0] + logger.info(f"📈 策略统计: {stats_count} 条") + + logger.info("✅ 数据迁移验证完成") + + except Exception as e: + logger.error(f"验证迁移结果失败: {e}") + raise + + +def main(): + """主函数""" + logger.info("🚀 开始SQLite到MySQL数据迁移...") + + try: + # 检查依赖 + try: + import pymysql + except ImportError: + logger.error("❌ 请先安装pymysql: pip install pymysql") + return + + # 执行迁移 + migrator = DataMigrator() + migrator.migrate_all() + + print("\n" + "="*70) + print("🎉 MySQL数据库迁移完成!") + print("="*70) + print("\n✅ 迁移内容:") + print(" - 策略配置") + print(" - 扫描会话") + print(" - 股票信号(包含创新高回踩确认字段)") + print(" - 回踩提醒") + print(" - 数据库视图") + + print("\n🌐 MySQL配置:") + print(f" - 主机: {MYSQL_CONFIG.host}") + print(f" - 端口: {MYSQL_CONFIG.port}") + print(f" - 数据库: {MYSQL_CONFIG.database}") + + print("\n📝 下一步:") + print(" 1. 更新系统配置使用MySQL数据库") + print(" 2. 测试Web界面和API功能") + print(" 3. 验证所有功能正常工作") + + except Exception as e: + logger.error(f"❌ 迁移失败: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b5cc929..ea065a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Data source -adata>=1.15.0 +tushare>=1.2.89 # Data analysis and manipulation pandas>=2.0.0 @@ -17,7 +17,8 @@ seaborn>=0.12.0 scikit-learn>=1.3.0 # Database -# sqlite3 is part of Python standard library +PyMySQL>=1.1.0 +cryptography>=41.0.0 # Configuration pyyaml>=6.0 diff --git a/scripts/data_collector.py b/scripts/data_collector.py index 2effe8c..265e5c9 100755 --- a/scripts/data_collector.py +++ b/scripts/data_collector.py @@ -16,7 +16,7 @@ sys.path.insert(0, str(project_root)) from src.strategy.kline_pattern_strategy import KLinePatternStrategy from src.utils.config_loader import ConfigLoader -from src.data.data_fetcher import ADataFetcher +from src.data.tushare_fetcher import TushareFetcher from src.utils.notification import NotificationManager from src.database.database_manager import DatabaseManager from loguru import logger @@ -32,7 +32,8 @@ def run_strategy(): config = config_loader.config # 初始化数据获取器 - data_fetcher = ADataFetcher() + tushare_token = config.get('data_source', {}).get('tushare_token', '0ed6419a00d8923dc19c0b58fc92d94c9a0696949ab91a13aa58a0cc') + data_fetcher = TushareFetcher(token=tushare_token) # 初始化通知管理器 notification_config = config.get('notification', {}) diff --git a/src/database/database_manager.py b/src/database/database_manager.py deleted file mode 100644 index 2c86239..0000000 --- a/src/database/database_manager.py +++ /dev/null @@ -1,500 +0,0 @@ -""" -数据库管理模块 -负责策略筛选结果的存储和查询 -""" - -import sqlite3 -import json -from pathlib import Path -from typing import Dict, List, Any, Optional, Tuple -from datetime import datetime, date -from loguru import logger -import pandas as pd - - -class DatabaseManager: - """数据库管理器""" - - def __init__(self, db_path: str = None): - """ - 初始化数据库管理器 - - Args: - db_path: 数据库文件路径,默认为项目根目录下的data/trading.db - """ - if db_path is None: - # 获取项目根目录 - current_file = Path(__file__) - project_root = current_file.parent.parent.parent - data_dir = project_root / "data" - data_dir.mkdir(exist_ok=True) - db_path = data_dir / "trading.db" - - self.db_path = Path(db_path) - self._init_database() - logger.info(f"数据库管理器初始化完成: {self.db_path}") - - def _init_database(self): - """初始化数据库,创建表结构""" - try: - # 读取SQL schema文件 - schema_file = Path(__file__).parent / "schema.sql" - if not schema_file.exists(): - raise FileNotFoundError(f"数据库schema文件不存在: {schema_file}") - - with open(schema_file, 'r', encoding='utf-8') as f: - schema_sql = f.read() - - # 执行建表语句 - with sqlite3.connect(self.db_path) as conn: - conn.executescript(schema_sql) - conn.commit() - - logger.info("数据库表结构初始化完成") - - # 初始化默认策略 - self._init_default_strategies() - - except Exception as e: - logger.error(f"初始化数据库失败: {e}") - raise - - def _init_default_strategies(self): - """初始化默认策略""" - try: - default_strategies = [ - { - 'strategy_name': 'K线形态策略', - 'strategy_type': 'kline_pattern', - 'description': '两阳线+阴线+阳线突破形态识别策略', - 'config': { - 'min_entity_ratio': 0.55, - 'final_yang_min_ratio': 0.40, - 'max_turnover_ratio': 40.0, - 'timeframes': ['daily', 'weekly'], - 'pullback_tolerance': 0.02, - 'monitor_days': 30 - } - } - ] - - for strategy in default_strategies: - self.create_or_update_strategy(**strategy) - - except Exception as e: - logger.warning(f"初始化默认策略失败: {e}") - - def create_or_update_strategy(self, strategy_name: str, strategy_type: str, - description: str = None, config: Dict[str, Any] = None) -> int: - """ - 创建或更新策略 - - Args: - strategy_name: 策略名称 - strategy_type: 策略类型 - description: 策略描述 - config: 策略配置 - - Returns: - 策略ID - """ - try: - config_json = json.dumps(config) if config else None - - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - - # 检查策略是否存在 - cursor.execute( - "SELECT id FROM strategies WHERE strategy_name = ?", - (strategy_name,) - ) - result = cursor.fetchone() - - if result: - # 更新现有策略 - strategy_id = result[0] - cursor.execute(""" - UPDATE strategies - SET strategy_type = ?, description = ?, config = ?, updated_at = CURRENT_TIMESTAMP - WHERE id = ? - """, (strategy_type, description, config_json, strategy_id)) - logger.debug(f"更新策略: {strategy_name} (ID: {strategy_id})") - else: - # 创建新策略 - cursor.execute(""" - INSERT INTO strategies (strategy_name, strategy_type, description, config) - VALUES (?, ?, ?, ?) - """, (strategy_name, strategy_type, description, config_json)) - strategy_id = cursor.lastrowid - logger.info(f"创建新策略: {strategy_name} (ID: {strategy_id})") - - conn.commit() - return strategy_id - - except Exception as e: - logger.error(f"创建/更新策略失败: {e}") - raise - - def create_scan_session(self, strategy_id: int, scan_date: date = None, - total_scanned: int = 0, total_signals: int = 0, - data_source: str = None, scan_config: Dict[str, Any] = None) -> int: - """ - 创建扫描会话 - - Args: - strategy_id: 策略ID - scan_date: 扫描日期 - total_scanned: 总扫描股票数 - total_signals: 总信号数 - data_source: 数据源 - scan_config: 扫描配置 - - Returns: - 会话ID - """ - try: - if scan_date is None: - scan_date = datetime.now().date() - - config_json = json.dumps(scan_config) if scan_config else None - - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - cursor.execute(""" - INSERT INTO scan_sessions - (strategy_id, scan_date, total_scanned, total_signals, data_source, scan_config) - VALUES (?, ?, ?, ?, ?, ?) - """, (strategy_id, scan_date, total_scanned, total_signals, data_source, config_json)) - - session_id = cursor.lastrowid - conn.commit() - - logger.info(f"创建扫描会话: {session_id} (策略ID: {strategy_id})") - return session_id - - except Exception as e: - logger.error(f"创建扫描会话失败: {e}") - raise - - def save_stock_signal(self, session_id: int, strategy_id: int, signal: Dict[str, Any]) -> int: - """ - 保存股票信号 - - Args: - session_id: 会话ID - strategy_id: 策略ID - signal: 信号数据 - - Returns: - 信号ID - """ - try: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - - # 转换日期格式 - signal_date = signal.get('date') - if isinstance(signal_date, str): - signal_date = datetime.strptime(signal_date, '%Y-%m-%d').date() - elif hasattr(signal_date, 'date'): - signal_date = signal_date.date() - - # 准备K线数据 - k1_data = json.dumps(signal.get('k1', {})) if signal.get('k1') else None - k2_data = json.dumps(signal.get('k2', {})) if signal.get('k2') else None - k3_data = json.dumps(signal.get('k3', {})) if signal.get('k3') else None - k4_data = json.dumps(signal.get('k4', {})) if signal.get('k4') else None - - cursor.execute(""" - INSERT INTO stock_signals ( - session_id, strategy_id, stock_code, stock_name, timeframe, - signal_date, signal_type, breakout_price, yin_high, breakout_amount, - breakout_pct, ema20_price, yang1_entity_ratio, yang2_entity_ratio, - final_yang_entity_ratio, turnover_ratio, above_ema20, - k1_data, k2_data, k3_data, k4_data - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - session_id, strategy_id, - signal.get('stock_code'), - signal.get('stock_name'), - signal.get('timeframe'), - signal_date, - signal.get('pattern_type', '两阳+阴+阳突破'), - signal.get('breakout_price'), - signal.get('yin_high'), - signal.get('breakout_amount'), - signal.get('breakout_pct'), - signal.get('ema20_price'), - signal.get('yang1_entity_ratio'), - signal.get('yang2_entity_ratio'), - signal.get('final_yang_entity_ratio'), - signal.get('turnover_ratio'), - signal.get('above_ema20'), - k1_data, k2_data, k3_data, k4_data - )) - - signal_id = cursor.lastrowid - conn.commit() - - logger.debug(f"保存信号: {signal.get('stock_code')} (ID: {signal_id})") - return signal_id - - except Exception as e: - logger.error(f"保存股票信号失败: {e}") - raise - - def save_pullback_alert(self, signal_id: int, pullback_alert: Dict[str, Any]) -> int: - """ - 保存回踩提醒 - - Args: - signal_id: 原始信号ID - pullback_alert: 回踩提醒数据 - - Returns: - 提醒ID - """ - try: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - - # 转换日期格式 - def convert_date(date_value): - if isinstance(date_value, str): - return datetime.strptime(date_value, '%Y-%m-%d').date() - elif hasattr(date_value, 'date'): - return date_value.date() - return date_value - - cursor.execute(""" - INSERT INTO pullback_alerts ( - signal_id, stock_code, stock_name, timeframe, - original_signal_date, original_breakout_price, yin_high, - pullback_date, current_price, current_low, pullback_pct, - distance_to_yin_high, days_since_signal, alert_sent, alert_sent_time - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - signal_id, - pullback_alert.get('stock_code'), - pullback_alert.get('stock_name'), - pullback_alert.get('timeframe'), - convert_date(pullback_alert.get('signal_date')), - pullback_alert.get('breakout_price'), - pullback_alert.get('yin_high'), - convert_date(pullback_alert.get('current_date')), - pullback_alert.get('current_price'), - pullback_alert.get('current_low'), - pullback_alert.get('pullback_pct'), - pullback_alert.get('distance_to_yin_high'), - pullback_alert.get('days_since_signal'), - True, # alert_sent - datetime.now() # alert_sent_time - )) - - alert_id = cursor.lastrowid - conn.commit() - - logger.debug(f"保存回踩提醒: {pullback_alert.get('stock_code')} (ID: {alert_id})") - return alert_id - - except Exception as e: - logger.error(f"保存回踩提醒失败: {e}") - raise - - def get_latest_signals(self, strategy_name: str = None, limit: int = 100) -> pd.DataFrame: - """ - 获取最新信号 - - Args: - strategy_name: 策略名称过滤 - limit: 返回数量限制 - - Returns: - 信号DataFrame - """ - try: - with sqlite3.connect(self.db_path) as conn: - sql = "SELECT * FROM latest_signals_view" - params = [] - - if strategy_name: - sql += " WHERE strategy_name = ?" - params.append(strategy_name) - - sql += " LIMIT ?" - params.append(limit) - - df = pd.read_sql_query(sql, conn, params=params) - return df - - except Exception as e: - logger.error(f"获取最新信号失败: {e}") - return pd.DataFrame() - - def get_strategy_stats(self) -> pd.DataFrame: - """ - 获取策略统计信息 - - Returns: - 策略统计DataFrame - """ - try: - with sqlite3.connect(self.db_path) as conn: - df = pd.read_sql_query("SELECT * FROM strategy_stats_view", conn) - return df - - except Exception as e: - logger.error(f"获取策略统计失败: {e}") - return pd.DataFrame() - - def get_signals_by_date_range(self, start_date: date, end_date: date = None, - strategy_name: str = None, timeframe: str = None) -> pd.DataFrame: - """ - 按日期范围获取信号 - - Args: - start_date: 开始日期 - end_date: 结束日期 - strategy_name: 策略名称过滤 - timeframe: 周期过滤 - - Returns: - 信号DataFrame - """ - try: - if end_date is None: - end_date = datetime.now().date() - - with sqlite3.connect(self.db_path) as conn: - sql = """ - SELECT * FROM latest_signals_view - WHERE signal_date >= ? AND signal_date <= ? - """ - params = [start_date, end_date] - - if strategy_name: - sql += " AND strategy_name = ?" - params.append(strategy_name) - - if timeframe: - sql += " AND timeframe = ?" - params.append(timeframe) - - sql += " ORDER BY signal_date DESC" - - df = pd.read_sql_query(sql, conn, params=params) - return df - - except Exception as e: - logger.error(f"按日期范围获取信号失败: {e}") - return pd.DataFrame() - - def get_pullback_alerts(self, days: int = 7) -> pd.DataFrame: - """ - 获取最近的回踩提醒 - - Args: - days: 获取最近几天的提醒 - - Returns: - 回踩提醒DataFrame - """ - try: - with sqlite3.connect(self.db_path) as conn: - sql = """ - SELECT * FROM pullback_alerts - WHERE pullback_date >= date('now', '-{} days') - ORDER BY pullback_date DESC - """.format(days) - - df = pd.read_sql_query(sql, conn) - return df - - except Exception as e: - logger.error(f"获取回踩提醒失败: {e}") - return pd.DataFrame() - - def cleanup_old_data(self, days_to_keep: int = 90): - """ - 清理旧数据 - - Args: - days_to_keep: 保留的天数 - """ - try: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - - # 删除旧的回踩提醒 - cursor.execute(""" - DELETE FROM pullback_alerts - WHERE pullback_date < date('now', '-{} days') - """.format(days_to_keep)) - - # 删除旧的信号记录 - cursor.execute(""" - DELETE FROM stock_signals - WHERE signal_date < date('now', '-{} days') - """.format(days_to_keep)) - - # 删除旧的扫描会话 - cursor.execute(""" - DELETE FROM scan_sessions - WHERE scan_date < date('now', '-{} days') - """.format(days_to_keep)) - - conn.commit() - logger.info(f"清理完成,保留了最近{days_to_keep}天的数据") - - except Exception as e: - logger.error(f"清理旧数据失败: {e}") - - def get_strategy_id(self, strategy_name: str) -> Optional[int]: - """ - 根据策略名称获取策略ID - - Args: - strategy_name: 策略名称 - - Returns: - 策略ID,如果不存在返回None - """ - try: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - cursor.execute("SELECT id FROM strategies WHERE strategy_name = ?", (strategy_name,)) - result = cursor.fetchone() - return result[0] if result else None - - except Exception as e: - logger.error(f"获取策略ID失败: {e}") - return None - - def update_scan_session_stats(self, session_id: int, total_scanned: int, total_signals: int): - """ - 更新扫描会话统计信息 - - Args: - session_id: 会话ID - total_scanned: 总扫描股票数 - total_signals: 总信号数 - """ - try: - with sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - cursor.execute(""" - UPDATE scan_sessions - SET total_scanned = ?, total_signals = ? - WHERE id = ? - """, (total_scanned, total_signals, session_id)) - conn.commit() - - except Exception as e: - logger.error(f"更新扫描会话统计失败: {e}") - - -if __name__ == "__main__": - # 测试代码 - db = DatabaseManager() - print("数据库管理器测试完成") \ No newline at end of file diff --git a/src/database/mysql_database_manager.py b/src/database/mysql_database_manager.py new file mode 100644 index 0000000..0df952f --- /dev/null +++ b/src/database/mysql_database_manager.py @@ -0,0 +1,406 @@ +#!/usr/bin/env python3 +""" +MySQL数据库管理器 +支持创新高回踩确认策略的MySQL数据库操作 +""" + +import pymysql +import pandas as pd +from datetime import date, datetime, timedelta +from typing import Dict, Any, Optional, List +import json +from pathlib import Path +import sys +from loguru import logger + +# 添加项目根目录到路径 +current_dir = Path(__file__).parent.parent.parent +sys.path.insert(0, str(current_dir)) + +from config.mysql_config import MYSQL_CONFIG + + +class MySQLDatabaseManager: + """MySQL数据库管理器""" + + def __init__(self, config=None): + """ + 初始化MySQL数据库管理器 + + Args: + config: MySQL配置,默认使用MYSQL_CONFIG + """ + self.config = config or MYSQL_CONFIG + self.connection_params = self.config.to_dict() + + # 测试连接并初始化数据库 + try: + self._test_connection() + self._init_database() + logger.info("MySQL数据库管理器初始化完成") + except Exception as e: + logger.error(f"MySQL数据库初始化失败: {e}") + raise + + def _test_connection(self): + """测试数据库连接""" + try: + conn = pymysql.connect(**self.connection_params) + conn.close() + logger.info("MySQL数据库连接测试成功") + except Exception as e: + logger.error(f"MySQL数据库连接失败: {e}") + raise + + def _init_database(self): + """初始化数据库表结构""" + try: + with pymysql.connect(**self.connection_params) as conn: + cursor = conn.cursor() + + # 读取并执行MySQL schema + schema_file = Path(__file__).parent / "mysql_schema.sql" + + if schema_file.exists(): + with open(schema_file, 'r', encoding='utf-8') as f: + schema_sql = f.read() + + # 分割SQL语句并执行 + statements = [stmt.strip() for stmt in schema_sql.split(';') if stmt.strip()] + + for statement in statements: + try: + cursor.execute(statement) + except Exception as e: + if "already exists" not in str(e): + logger.warning(f"执行SQL语句时警告: {e}") + + conn.commit() + logger.info("MySQL数据库表结构初始化完成") + + # 初始化默认策略 + self._init_default_strategies(cursor) + conn.commit() + + except Exception as e: + logger.error(f"初始化MySQL数据库失败: {e}") + raise + + def _init_default_strategies(self, cursor): + """初始化默认策略""" + try: + # 检查是否已存在策略 + cursor.execute("SELECT COUNT(*) FROM strategies WHERE strategy_name = 'K线形态策略'") + count = cursor.fetchone()[0] + + if count == 0: + cursor.execute(""" + INSERT INTO strategies (strategy_name, strategy_type, description) + VALUES ('K线形态策略', 'kline_pattern', '两阳+阴+阳突破形态识别策略(创新高回踩确认版)') + """) + logger.info("默认K线形态策略已创建") + except Exception as e: + logger.error(f"初始化默认策略失败: {e}") + + def create_or_update_strategy(self, strategy_name: str, strategy_type: str, + description: str = None, config: Dict[str, Any] = None) -> int: + """创建或更新策略""" + try: + with pymysql.connect(**self.connection_params) as conn: + cursor = conn.cursor() + + # 查找已存在的策略 + cursor.execute(""" + SELECT id FROM strategies WHERE strategy_name = %s + """, (strategy_name,)) + + result = cursor.fetchone() + + if result: + # 更新已存在的策略 + strategy_id = result[0] + cursor.execute(""" + UPDATE strategies + SET strategy_type = %s, description = %s, updated_at = CURRENT_TIMESTAMP + WHERE id = %s + """, (strategy_type, description, strategy_id)) + logger.info(f"更新策略: {strategy_name} (ID: {strategy_id})") + else: + # 创建新策略 + cursor.execute(""" + INSERT INTO strategies (strategy_name, strategy_type, description) + VALUES (%s, %s, %s) + """, (strategy_name, strategy_type, description)) + strategy_id = cursor.lastrowid + logger.info(f"创建策略: {strategy_name} (ID: {strategy_id})") + + conn.commit() + return strategy_id + + except Exception as e: + logger.error(f"创建或更新策略失败: {e}") + raise + + def create_scan_session(self, strategy_id: int, scan_date: date = None, + total_scanned: int = 0, total_signals: int = 0, + data_source: str = None, scan_config: Dict[str, Any] = None) -> int: + """创建扫描会话""" + try: + with pymysql.connect(**self.connection_params) as conn: + cursor = conn.cursor() + + if scan_date is None: + scan_date = date.today() + + cursor.execute(""" + INSERT INTO scan_sessions ( + strategy_id, scan_date, total_scanned, total_signals, + data_source, scan_config, status + ) VALUES (%s, %s, %s, %s, %s, %s, %s) + """, ( + strategy_id, scan_date, total_scanned, total_signals, + data_source, json.dumps(scan_config) if scan_config else None, 'completed' + )) + + session_id = cursor.lastrowid + conn.commit() + + logger.info(f"创建扫描会话: {session_id} (策略ID: {strategy_id})") + return session_id + + except Exception as e: + logger.error(f"创建扫描会话失败: {e}") + raise + + def save_stock_signal(self, session_id: int, strategy_id: int, signal: Dict[str, Any]) -> int: + """保存股票信号""" + try: + with pymysql.connect(**self.connection_params) as conn: + cursor = conn.cursor() + + # 转换日期格式 + signal_date = signal.get('signal_date') or signal.get('date') + if isinstance(signal_date, str): + signal_date = datetime.strptime(signal_date, '%Y-%m-%d').date() + elif hasattr(signal_date, 'date'): + signal_date = signal_date.date() + + # 转换新格式日期 + new_high_date = signal.get('new_high_date') + if isinstance(new_high_date, str): + new_high_date = datetime.strptime(new_high_date, '%Y-%m-%d').date() + elif hasattr(new_high_date, 'date'): + new_high_date = new_high_date.date() + + confirmation_date = signal.get('confirmation_date') + if isinstance(confirmation_date, str): + confirmation_date = datetime.strptime(confirmation_date, '%Y-%m-%d').date() + elif hasattr(confirmation_date, 'date'): + confirmation_date = confirmation_date.date() + + # 准备K线数据 + k1_data = json.dumps(signal.get('k1', {})) if signal.get('k1') else None + k2_data = json.dumps(signal.get('k2', {})) if signal.get('k2') else None + k3_data = json.dumps(signal.get('k3', {})) if signal.get('k3') else None + k4_data = json.dumps(signal.get('k4', {})) if signal.get('k4') else None + + cursor.execute(""" + INSERT INTO stock_signals ( + session_id, strategy_id, stock_code, stock_name, timeframe, + signal_date, signal_type, breakout_price, yin_high, breakout_amount, + breakout_pct, ema20_price, yang1_entity_ratio, yang2_entity_ratio, + final_yang_entity_ratio, turnover_ratio, above_ema20, + new_high_confirmed, new_high_price, new_high_date, confirmation_date, + confirmation_days, pullback_distance, + k1_data, k2_data, k3_data, k4_data + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + session_id, strategy_id, + signal.get('stock_code'), + signal.get('stock_name'), + signal.get('timeframe'), + signal_date, + signal.get('pattern_type', '两阳+阴+阳突破'), + signal.get('breakout_price'), + signal.get('yin_high'), + signal.get('breakout_amount'), + signal.get('breakout_pct'), + signal.get('ema20_price'), + signal.get('yang1_entity_ratio'), + signal.get('yang2_entity_ratio'), + signal.get('final_yang_entity_ratio'), + signal.get('turnover_ratio'), + signal.get('above_ema20'), + signal.get('new_high_confirmed', False), + signal.get('new_high_price'), + new_high_date, + confirmation_date, + signal.get('confirmation_days'), + signal.get('pullback_distance'), + k1_data, k2_data, k3_data, k4_data + )) + + signal_id = cursor.lastrowid + conn.commit() + + logger.debug(f"保存信号: {signal.get('stock_code')} (ID: {signal_id})") + return signal_id + + except Exception as e: + logger.error(f"保存股票信号失败: {e}") + raise + + def get_latest_signals(self, strategy_name: str = None, limit: int = 100) -> pd.DataFrame: + """获取最新信号""" + try: + with pymysql.connect(**self.connection_params) as conn: + sql = "SELECT * FROM latest_signals_view" + params = [] + + if strategy_name: + sql += " WHERE strategy_name = %s" + params.append(strategy_name) + + sql += " LIMIT %s" + params.append(limit) + + df = pd.read_sql_query(sql, conn, params=params) + return df + + except Exception as e: + logger.error(f"获取最新信号失败: {e}") + return pd.DataFrame() + + def get_signals_by_date_range(self, start_date: date, end_date: date = None, + strategy_name: str = None, timeframe: str = None) -> pd.DataFrame: + """按日期范围获取信号""" + try: + with pymysql.connect(**self.connection_params) as conn: + if end_date is None: + end_date = date.today() + + sql = """ + SELECT * FROM latest_signals_view + WHERE signal_date >= %s AND signal_date <= %s + """ + params = [start_date, end_date] + + if strategy_name: + sql += " AND strategy_name = %s" + params.append(strategy_name) + + if timeframe: + sql += " AND timeframe = %s" + params.append(timeframe) + + sql += " ORDER BY signal_date DESC, scan_time DESC" + + df = pd.read_sql_query(sql, conn, params=params) + return df + + except Exception as e: + logger.error(f"按日期范围获取信号失败: {e}") + return pd.DataFrame() + + def get_strategy_stats(self) -> pd.DataFrame: + """获取策略统计""" + try: + with pymysql.connect(**self.connection_params) as conn: + df = pd.read_sql_query("SELECT * FROM strategy_stats_view", conn) + return df + + except Exception as e: + logger.error(f"获取策略统计失败: {e}") + return pd.DataFrame() + + def get_pullback_alerts(self, days: int = 30) -> pd.DataFrame: + """获取回踩提醒""" + try: + with pymysql.connect(**self.connection_params) as conn: + cutoff_date = date.today() - timedelta(days=days) + + sql = """ + SELECT * FROM pullback_alerts + WHERE pullback_date >= %s + ORDER BY pullback_date DESC + """ + + df = pd.read_sql_query(sql, conn, params=[cutoff_date]) + return df + + except Exception as e: + logger.error(f"获取回踩提醒失败: {e}") + return pd.DataFrame() + + def save_pullback_alert(self, signal_id: int, pullback_alert: Dict[str, Any]) -> int: + """保存回踩提醒""" + try: + with pymysql.connect(**self.connection_params) as conn: + cursor = conn.cursor() + + # 转换日期格式 + def convert_date(date_value): + if isinstance(date_value, str): + return datetime.strptime(date_value, '%Y-%m-%d').date() + elif hasattr(date_value, 'date'): + return date_value.date() + return date_value + + cursor.execute(""" + INSERT INTO pullback_alerts ( + signal_id, stock_code, stock_name, timeframe, + original_signal_date, original_breakout_price, yin_high, + pullback_date, current_price, current_low, + pullback_pct, distance_to_yin_high, days_since_signal, + alert_sent, alert_sent_time + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + signal_id, + pullback_alert.get('stock_code'), + pullback_alert.get('stock_name'), + pullback_alert.get('timeframe'), + convert_date(pullback_alert.get('signal_date')), + pullback_alert.get('breakout_price'), + pullback_alert.get('yin_high'), + convert_date(pullback_alert.get('current_date')), + pullback_alert.get('current_price'), + pullback_alert.get('current_low'), + pullback_alert.get('pullback_pct'), + pullback_alert.get('distance_to_yin_high'), + pullback_alert.get('days_since_signal'), + True, # alert_sent + datetime.now() # alert_sent_time + )) + + alert_id = cursor.lastrowid + conn.commit() + + logger.debug(f"保存回踩提醒: {pullback_alert.get('stock_code')} (ID: {alert_id})") + return alert_id + + except Exception as e: + logger.error(f"保存回踩提醒失败: {e}") + raise + + def cleanup_old_data(self, days: int = 90): + """清理旧数据""" + try: + with pymysql.connect(**self.connection_params) as conn: + cursor = conn.cursor() + cutoff_date = date.today() - timedelta(days=days) + + # 清理旧的回踩提醒 + cursor.execute("DELETE FROM pullback_alerts WHERE pullback_date < %s", (cutoff_date,)) + deleted_alerts = cursor.rowcount + + # 清理旧的信号(保留扫描会话) + cursor.execute("DELETE FROM stock_signals WHERE signal_date < %s", (cutoff_date,)) + deleted_signals = cursor.rowcount + + conn.commit() + + logger.info(f"清理完成: 删除了 {deleted_signals} 条信号和 {deleted_alerts} 条回踩提醒") + + except Exception as e: + logger.error(f"清理旧数据失败: {e}") + raise \ No newline at end of file diff --git a/src/database/mysql_schema.sql b/src/database/mysql_schema.sql new file mode 100644 index 0000000..ec3b47b --- /dev/null +++ b/src/database/mysql_schema.sql @@ -0,0 +1,156 @@ +-- MySQL数据库架构(支持创新高回踩确认策略) +-- 字符集:utf8mb4,支持emoji和完整Unicode + +-- 策略表:存储不同的交易策略配置 +CREATE TABLE IF NOT EXISTS strategies ( + id INT AUTO_INCREMENT PRIMARY KEY, + strategy_name VARCHAR(100) NOT NULL UNIQUE, + strategy_type VARCHAR(50) NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 扫描会话表:记录每次策略扫描的会话信息 +CREATE TABLE IF NOT EXISTS scan_sessions ( + id INT AUTO_INCREMENT PRIMARY KEY, + strategy_id INT NOT NULL, + scan_date DATE NOT NULL, + total_scanned INT DEFAULT 0, + total_signals INT DEFAULT 0, + data_source VARCHAR(200), + scan_config JSON, + status VARCHAR(20) DEFAULT 'completed', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (strategy_id) REFERENCES strategies(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 股票信号表:存储具体的股票筛选信号 +CREATE TABLE IF NOT EXISTS stock_signals ( + id INT AUTO_INCREMENT PRIMARY KEY, + session_id INT, + strategy_id INT, + stock_code VARCHAR(20) NOT NULL, + stock_name VARCHAR(100), + timeframe VARCHAR(20) NOT NULL, + signal_date DATE NOT NULL, + signal_type VARCHAR(100) NOT NULL, + + -- 价格信息 + breakout_price DECIMAL(10,3), + yin_high DECIMAL(10,3), + breakout_amount DECIMAL(15,2), + breakout_pct DECIMAL(8,4), + ema20_price DECIMAL(10,3), + + -- 技术指标 + yang1_entity_ratio DECIMAL(6,4), + yang2_entity_ratio DECIMAL(6,4), + final_yang_entity_ratio DECIMAL(6,4), + turnover_ratio DECIMAL(8,4), + above_ema20 BOOLEAN, + + -- 创新高回踩确认字段 + new_high_confirmed BOOLEAN DEFAULT FALSE, + new_high_price DECIMAL(10,3), + new_high_date DATE, + confirmation_date DATE, + confirmation_days INT, + pullback_distance DECIMAL(8,4), + + -- K线详情(JSON格式存储) + k1_data JSON, + k2_data JSON, + k3_data JSON, + k4_data JSON, + + -- 元数据 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (session_id) REFERENCES scan_sessions(id), + FOREIGN KEY (strategy_id) REFERENCES strategies(id), + INDEX idx_stock_code (stock_code), + INDEX idx_signal_date (signal_date), + INDEX idx_strategy_id (strategy_id), + INDEX idx_session_id (session_id), + INDEX idx_new_high_confirmed (new_high_confirmed) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 回踩监控表:存储回踩提醒信息 +CREATE TABLE IF NOT EXISTS pullback_alerts ( + id INT AUTO_INCREMENT PRIMARY KEY, + signal_id INT, + stock_code VARCHAR(20) NOT NULL, + stock_name VARCHAR(100), + timeframe VARCHAR(20) NOT NULL, + + -- 原始信号信息 + original_signal_date DATE, + original_breakout_price DECIMAL(10,3), + yin_high DECIMAL(10,3), + + -- 回踩信息 + pullback_date DATE NOT NULL, + current_price DECIMAL(10,3), + current_low DECIMAL(10,3), + pullback_pct DECIMAL(8,4), + distance_to_yin_high DECIMAL(8,4), + days_since_signal INT, + + -- 提醒状态 + alert_sent BOOLEAN DEFAULT FALSE, + alert_sent_time TIMESTAMP NULL, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (signal_id) REFERENCES stock_signals(id), + INDEX idx_stock_code (stock_code), + INDEX idx_pullback_date (pullback_date) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 创建索引以提高查询性能 +CREATE INDEX idx_scan_sessions_scan_date ON scan_sessions (scan_date); + +-- 创建视图:最新信号概览 +CREATE OR REPLACE VIEW latest_signals_view AS +SELECT + ss.stock_code, + ss.stock_name, + ss.timeframe, + ss.signal_date, + ss.breakout_price, + ss.yin_high, + ss.breakout_pct, + ss.final_yang_entity_ratio, + ss.turnover_ratio, + ss.new_high_confirmed, + ss.new_high_price, + ss.new_high_date, + ss.confirmation_date, + ss.confirmation_days, + ss.pullback_distance, + ss.above_ema20, + s.strategy_name, + scan.created_at as scan_time, + scan.data_source +FROM stock_signals ss +JOIN strategies s ON ss.strategy_id = s.id +JOIN scan_sessions scan ON ss.session_id = scan.id +ORDER BY ss.signal_date DESC, ss.created_at DESC; + +-- 创建视图:策略统计概览 +CREATE OR REPLACE VIEW strategy_stats_view AS +SELECT + s.strategy_name, + s.strategy_type, + COUNT(DISTINCT scan.id) as total_scans, + COUNT(ss.id) as total_signals, + COUNT(DISTINCT ss.stock_code) as unique_stocks, + MAX(scan.created_at) as last_scan_time, + AVG(ss.breakout_pct) as avg_breakout_pct, + AVG(ss.final_yang_entity_ratio) as avg_entity_ratio +FROM strategies s +LEFT JOIN scan_sessions scan ON s.id = scan.strategy_id +LEFT JOIN stock_signals ss ON scan.id = ss.session_id +GROUP BY s.id, s.strategy_name, s.strategy_type +ORDER BY s.strategy_name; \ No newline at end of file diff --git a/src/database/schema.sql b/src/database/schema.sql index db92962..1e9aa25 100644 --- a/src/database/schema.sql +++ b/src/database/schema.sql @@ -50,6 +50,14 @@ CREATE TABLE IF NOT EXISTS stock_signals ( turnover_ratio REAL, -- 换手率 above_ema20 BOOLEAN, -- 是否在EMA20上方 + -- 创新高回踩确认字段 + new_high_confirmed BOOLEAN DEFAULT FALSE, -- 是否已确认创新高回踩 + new_high_price REAL, -- 创新高价格 + new_high_date DATE, -- 创新高日期 + confirmation_date DATE, -- 回踩确认日期 + confirmation_days INTEGER, -- 确认用时天数 + pullback_distance REAL, -- 回踩距离百分比 + -- K线详情(JSON格式存储) k1_data JSON, -- 第一根K线数据 k2_data JSON, -- 第二根K线数据 @@ -114,6 +122,13 @@ SELECT ss.breakout_pct, ss.final_yang_entity_ratio, ss.turnover_ratio, + ss.new_high_confirmed, + ss.new_high_price, + ss.new_high_date, + ss.confirmation_date, + ss.confirmation_days, + ss.pullback_distance, + ss.above_ema20, s.strategy_name, scan.scan_time, scan.data_source diff --git a/src/strategy/kline_pattern_strategy.py b/src/strategy/kline_pattern_strategy.py index f7fa857..a210102 100644 --- a/src/strategy/kline_pattern_strategy.py +++ b/src/strategy/kline_pattern_strategy.py @@ -9,16 +9,16 @@ from typing import Dict, List, Tuple, Optional, Any from datetime import datetime, timedelta from loguru import logger -from ..data.data_fetcher import ADataFetcher +from ..data.tushare_fetcher import TushareFetcher as ADataFetcher from ..utils.notification import NotificationManager -from ..database.database_manager import DatabaseManager +from ..database.mysql_database_manager import MySQLDatabaseManager class KLinePatternStrategy: """K线形态策略类""" def __init__(self, data_fetcher: ADataFetcher, notification_manager: NotificationManager, - config: Dict[str, Any], db_manager: DatabaseManager = None): + config: Dict[str, Any], db_manager: MySQLDatabaseManager = None): """ 初始化K线形态策略 @@ -31,7 +31,7 @@ class KLinePatternStrategy: self.data_fetcher = data_fetcher self.notification_manager = notification_manager self.config = config - self.db_manager = db_manager or DatabaseManager() + self.db_manager = db_manager or MySQLDatabaseManager() # 策略参数 self.strategy_name = "K线形态策略" @@ -44,10 +44,17 @@ class KLinePatternStrategy: self.pullback_tolerance = config.get('pullback_tolerance', 0.02) # 回踩容忍度(2%) self.monitor_days = config.get('monitor_days', 30) # 监控回踩的天数 + # 新增:回踩确认参数 + self.pullback_confirmation_days = config.get('pullback_confirmation_days', 7) # 回踩确认时间窗口 + # 存储已触发的信号,用于监控回踩 # 格式: {stock_code: {'signals': [signal_dict], 'last_check_date': date}} self.triggered_signals = {} + # 新增:存储待确认的模式(等待回踩确认) + # 格式: {stock_code: {'pending_patterns': [pattern_dict], 'last_check_date': date}} + self.pending_patterns = {} + # 确保策略在数据库中存在 self.strategy_id = self.db_manager.create_or_update_strategy( strategy_name=self.strategy_name, @@ -152,20 +159,20 @@ class KLinePatternStrategy: logger.debug(f"后续{follow_up_days}天强势验证通过") return True - def detect_pattern(self, df: pd.DataFrame) -> List[Dict[str, Any]]: + def detect_potential_pattern(self, df: pd.DataFrame) -> List[Dict[str, Any]]: """ - 检测"两阳线+阴线+阳线"形态 + 检测潜在的"两阳线+阴线+阳线"形态(等待回踩确认) Args: df: 包含特征指标的K线数据 Returns: - 检测到的形态信号列表 + 检测到的潜在形态列表 """ - signals = [] + potential_patterns = [] if df.empty or len(df) < 4: - return signals + return potential_patterns # 从第4个数据点开始检测(需要4根K线) for i in range(3, len(df)): @@ -214,17 +221,11 @@ class KLinePatternStrategy: if not turnover_valid: continue - # 检查后续强势条件(如果有后续K线数据的话) - follow_up_strength_valid = self.check_follow_up_strength(df, i, k3['high'], k4.get('ema20', 0)) - - if not follow_up_strength_valid: - continue - - # 构建信号 - signal = { + # 构建潜在模式 + potential_pattern = { 'index': i, 'date': df.iloc[i].get('trade_date', df.index[i]), - 'pattern_type': '两阳+阴+阳突破', + 'pattern_type': '两阳+阴+阳突破(待确认)', 'k1': k1, # 第一根阳线 'k2': k2, # 第二根阳线 'k3': k3, # 阴线 @@ -239,25 +240,175 @@ class KLinePatternStrategy: 'ema20_price': k4.get('ema20', 0), 'above_ema20': k4.get('above_ema20', False), 'turnover_ratio': turnover_ratio, - 'follow_up_strength': follow_up_strength_valid, - 'follow_up_days': self.config.get('follow_up_days', 3) + 'confirmation_pending': True, # 标记为待确认 + 'pattern_trigger_date': df.iloc[i].get('trade_date', df.index[i]) } - signals.append(signal) + potential_patterns.append(potential_pattern) - # 美化信号发现日志 - logger.info("🎯" + "="*60) - logger.info(f"📈 发现K线形态突破信号!") - logger.info(f"📅 信号时间: {signal['date']}") - logger.info(f"💰 突破价格: {signal['breakout_price']:.2f}元") - logger.info(f"📊 实体比例: 阳线1({signal['yang1_entity_ratio']:.1%}) | 阳线2({signal['yang2_entity_ratio']:.1%}) | 最后阳线({signal['final_yang_entity_ratio']:.1%})") - logger.info(f"💥 突破幅度: {signal['breakout_pct']:.2f}% (突破阴线最高价{signal['yin_high']:.2f}元)") - logger.info(f"📈 EMA20: {signal['ema20_price']:.2f}元 ({'✅上方' if signal['above_ema20'] else '❌下方'})") - logger.info(f"🔄 换手率: {signal['turnover_ratio']:.2f}% ({'✅合规' if signal['turnover_ratio'] <= self.max_turnover_ratio else '❌过高'})") - logger.info(f"💪 后续强势: {'✅通过' if signal['follow_up_strength'] else '❌未通过'}({signal['follow_up_days']}天验证)") - logger.info("🎯" + "="*60) + # 美化潜在模式发现日志 + logger.debug("🔍" + "="*60) + logger.debug(f"📊 发现K线形态模式(需创新高回踩确认)") + logger.debug(f"📅 模式时间: {potential_pattern['date']}") + logger.debug(f"💰 突破价格: {potential_pattern['breakout_price']:.2f}元") + logger.debug(f"🎯 阴线最高价: {potential_pattern['yin_high']:.2f}元") + logger.debug(f"⏰ 观察窗口: {self.pullback_confirmation_days}天内") + logger.debug(f"📋 要求: 先创新高再回踩阴线最高价才产生信号") + logger.debug("🔍" + "="*60) - return signals + return potential_patterns + + def check_pullback_confirmation(self, df: pd.DataFrame, pattern: Dict[str, Any]) -> bool: + """ + 检查是否有回踩确认(价格冲高回踩阴线高点) + + Args: + df: K线数据 + pattern: 潜在模式字典 + + Returns: + bool: 是否已确认回踩 + """ + pattern_index = pattern['index'] + yin_high = pattern['yin_high'] + breakout_price = pattern['breakout_price'] + + # 检查确认窗口内的K线数据 + confirmation_window_end = min(pattern_index + self.pullback_confirmation_days + 1, len(df)) + + # 需要至少有1根后续K线才能检查回踩 + if confirmation_window_end <= pattern_index + 1: + return False + + # 检查确认窗口内的每一根K线 + for i in range(pattern_index + 1, confirmation_window_end): + current_kline = df.iloc[i] + + # 条件1: 价格必须先冲高(高于突破价格) + has_moved_higher = current_kline['high'] > breakout_price + + # 条件2: 然后回踩到阴线最高价附近 + pullback_to_yin_high = current_kline['low'] <= yin_high * (1 + self.pullback_tolerance) + + # 如果同时满足冲高和回踩条件,确认信号 + if has_moved_higher and pullback_to_yin_high: + logger.info("✅" + "="*60) + logger.info(f"🎯 回踩确认成功!") + logger.info(f"📅 确认日期: {current_kline.get('trade_date', df.index[i])}") + logger.info(f"💰 突破价格: {breakout_price:.2f}元") + logger.info(f"📈 当日最高: {current_kline['high']:.2f}元 (冲高✅)") + logger.info(f"📉 当日最低: {current_kline['low']:.2f}元") + logger.info(f"🎯 阴线最高: {yin_high:.2f}元 (回踩✅)") + logger.info(f"⏰ 确认用时: {i - pattern_index}天") + logger.info("✅" + "="*60) + return True + + return False + + def check_new_high_pullback_signal(self, df: pd.DataFrame, pattern: Dict[str, Any]) -> Dict[str, Any]: + """ + 检查是否出现创新高后回踩信号 + + Args: + df: K线数据 + pattern: 潜在模式字典 + + Returns: + Dict: 如果找到信号返回信号字典,否则返回None + """ + pattern_index = pattern['index'] + yin_high = pattern['yin_high'] + breakout_price = pattern['breakout_price'] + + # 检查确认窗口内的K线数据 + confirmation_window_end = min(pattern_index + self.pullback_confirmation_days + 1, len(df)) + + # 需要至少有1根后续K线才能检查 + if confirmation_window_end <= pattern_index + 1: + return None + + # 寻找创新高的点 + max_high_after_pattern = breakout_price + new_high_index = None + + # 先找到创新高 + for i in range(pattern_index + 1, confirmation_window_end): + current_kline = df.iloc[i] + if current_kline['high'] > max_high_after_pattern: + max_high_after_pattern = current_kline['high'] + new_high_index = i + + # 如果没有创新高,则不产生信号 + if new_high_index is None: + return None + + # 在创新高之后寻找回踩阴线最高价的点 + for i in range(new_high_index, confirmation_window_end): + current_kline = df.iloc[i] + + # 检查是否回踩到阴线最高价附近 + pullback_to_yin_high = current_kline['low'] <= yin_high * (1 + self.pullback_tolerance) + + if pullback_to_yin_high: + # 找到信号,创建信号字典 + signal = pattern.copy() + signal['pattern_type'] = '两阳+阴+阳突破(创新高回踩确认)' + signal['confirmation_pending'] = False + signal['pullback_confirmed'] = True + signal['new_high_confirmed'] = True + signal['new_high_price'] = max_high_after_pattern + signal['new_high_date'] = df.iloc[new_high_index].get('trade_date', df.index[new_high_index]) + signal['new_high_index'] = new_high_index + signal['confirmation_date'] = current_kline.get('trade_date', df.index[i]) + signal['confirmation_index'] = i + signal['confirmation_days'] = i - pattern_index + signal['pullback_distance'] = ((current_kline['low'] - yin_high) / yin_high) * 100 + + # 美化信号确认日志 + logger.info("🎯" + "="*60) + logger.info(f"📈 K线形态突破信号已确认!(创新高回踩)") + logger.info(f"📅 模式日期: {signal['date']}") + logger.info(f"🚀 创新高日期: {signal['new_high_date']}") + logger.info(f"✅ 回踩确认日期: {signal['confirmation_date']}") + logger.info(f"💰 原突破价格: {signal['breakout_price']:.2f}元") + logger.info(f"🌟 创新高价格: {signal['new_high_price']:.2f}元") + logger.info(f"🎯 阴线最高价: {signal['yin_high']:.2f}元") + logger.info(f"📉 回踩最低价: {current_kline['low']:.2f}元") + logger.info(f"📏 回踩距离: {signal['pullback_distance']:.2f}%") + logger.info(f"⏰ 总确认用时: {signal['confirmation_days']}天") + logger.info("🎯" + "="*60) + + return signal + + return None + + def detect_pattern(self, df: pd.DataFrame) -> List[Dict[str, Any]]: + """ + 检测"两阳线+阴线+阳线"形态(创新高回踩确认逻辑) + + Args: + df: 包含特征指标的K线数据 + + Returns: + 检测到的确认信号列表 + """ + confirmed_signals = [] + + if df.empty or len(df) < 4: + return confirmed_signals + + # 首先检测所有潜在模式(但不产生信号) + potential_patterns = self.detect_potential_pattern(df) + + # 对每个潜在模式检查创新高回踩确认 + for pattern in potential_patterns: + # 检查是否有创新高后回踩确认 + signal = self.check_new_high_pullback_signal(df, pattern) + + if signal: + confirmed_signals.append(signal) + + return confirmed_signals def analyze_stock(self, stock_code: str, stock_name: str = None, days: int = 60, session_id: Optional[int] = None) -> Dict[str, List[Dict[str, Any]]]: @@ -467,6 +618,110 @@ class KLinePatternStrategy: self.triggered_signals[stock_code]['signals'] = \ self.triggered_signals[stock_code]['signals'][:max_signals_per_stock] + def add_pending_pattern(self, pattern: Dict[str, Any]): + """ + 添加待确认的模式到监控列表 + + Args: + pattern: 潜在模式字典 + """ + stock_code = pattern.get('stock_code') + if not stock_code: + return + + if stock_code not in self.pending_patterns: + self.pending_patterns[stock_code] = { + 'pending_patterns': [], + 'last_check_date': datetime.now().date() + } + + # 添加模式到监控列表 + self.pending_patterns[stock_code]['pending_patterns'].append(pattern) + + # 只保留最近的模式(避免内存占用过多) + max_patterns_per_stock = 5 + if len(self.pending_patterns[stock_code]['pending_patterns']) > max_patterns_per_stock: + # 按日期排序,保留最新的模式 + self.pending_patterns[stock_code]['pending_patterns'].sort( + key=lambda x: pd.to_datetime(x['date']) if isinstance(x['date'], str) else x['date'], + reverse=True + ) + self.pending_patterns[stock_code]['pending_patterns'] = \ + self.pending_patterns[stock_code]['pending_patterns'][:max_patterns_per_stock] + + def monitor_pending_pattern_confirmations(self) -> List[Dict[str, Any]]: + """ + 监控待确认模式的回踩确认情况 + + Returns: + 新确认的信号列表 + """ + newly_confirmed_signals = [] + current_date = datetime.now().date() + + # 清理过期的模式 + stocks_to_remove = [] + for stock_code, pattern_info in self.pending_patterns.items(): + # 过滤掉过期的模式 + valid_patterns = [] + for pattern in pattern_info['pending_patterns']: + pattern_date = pattern['date'] + if isinstance(pattern_date, str): + pattern_date = pd.to_datetime(pattern_date).date() + elif hasattr(pattern_date, 'date'): + pattern_date = pattern_date.date() + + days_since_pattern = (current_date - pattern_date).days + if days_since_pattern <= self.pullback_confirmation_days: + valid_patterns.append(pattern) + + if valid_patterns: + self.pending_patterns[stock_code]['pending_patterns'] = valid_patterns + else: + stocks_to_remove.append(stock_code) + + # 移除没有有效模式的股票 + for stock_code in stocks_to_remove: + del self.pending_patterns[stock_code] + + logger.info(f"🔍 当前监控待确认模式的股票数量: {len(self.pending_patterns)}") + + # 检查每只股票的回踩确认情况 + for stock_code in self.pending_patterns.keys(): + try: + # 获取最近的数据(包含确认窗口) + end_date = current_date.strftime('%Y-%m-%d') + start_date = (current_date - timedelta(days=self.pullback_confirmation_days + 5)).strftime('%Y-%m-%d') + + current_data = self.data_fetcher.get_historical_data( + stock_code, start_date, end_date, 'daily' + ) + + if not current_data.empty: + # 计算K线特征 + df_with_features = self.calculate_kline_features(current_data) + + # 检查每个待确认模式 + for pattern in self.pending_patterns[stock_code]['pending_patterns']: + has_confirmation = self.check_pullback_confirmation(df_with_features, pattern) + + if has_confirmation: + # 创建确认信号 + confirmed_signal = pattern.copy() + confirmed_signal['pattern_type'] = '两阳+阴+阳突破(已确认)' + confirmed_signal['confirmation_pending'] = False + confirmed_signal['pullback_confirmed'] = True + confirmed_signal['stock_code'] = stock_code + + newly_confirmed_signals.append(confirmed_signal) + + logger.info(f"✅ 股票 {stock_code} 的待确认模式已通过回踩确认") + + except Exception as e: + logger.error(f"监控股票 {stock_code} 待确认模式失败: {e}") + + return newly_confirmed_signals + def monitor_pullback_for_triggered_signals(self) -> List[Dict[str, Any]]: """ 监控所有已触发信号的回踩情况 @@ -808,30 +1063,47 @@ class KLinePatternStrategy: def get_strategy_summary(self) -> str: """获取策略说明""" return f""" -K线形态策略 - 两阳线+阴线+阳线突破 +K线形态策略 - 两阳线+阴线+阳线突破(优化版:创新高回踩确认) -策略逻辑: +策略逻辑(两阶段确认): +阶段1 - 模式识别(不产生信号): 1. 识别连续4根K线:阳线 + 阳线 + 阴线 + 阳线 2. 前两根阳线实体部分须占振幅的 {self.min_entity_ratio:.0%} 以上 3. 最后阳线实体部分须占振幅的 {self.final_yang_min_ratio:.0%} 以上 4. 最后阳线收盘价须高于阴线最高价(突破确认) 5. 最后阳线收盘价须在EMA20上方(趋势确认) 6. 最后阳线换手率不高于 {self.max_turnover_ratio:.1f}%(流动性约束) -7. 支持时间周期:{', '.join(self.timeframes)} +※ 此时仅记录模式,不产生交易信号 + +阶段2 - 创新高回踩确认(产生信号): +7. 价格必须创新高(高于阳线突破价格) +8. 然后回踩到阴线最高点附近(容忍度:{self.pullback_tolerance:.0%}) +9. 确认窗口:模式识别后 {self.pullback_confirmation_days} 个交易日内 +10. 只有完成"创新高+回踩阴线最高价"才产生正式交易信号 + +核心优化理念: +- 模式出现时不急于入场 +- 等待价格证明突破有效性(创新高) +- 再等待合理回踩机会(回踩阴线最高价) +- 确保信号质量和入场时机的最佳化 回踩监控功能: -- 自动监控已触发信号后的价格走势 -- 当价格回踩到阴线最高点附近时发送特殊提醒 -- 回踩容忍度:{self.pullback_tolerance:.0%} -- 监控期限:信号触发后 {self.monitor_days} 天 -- 提醒条件:价格接近阴线最高点且相比突破价有明显回调 +- 自动监控已确认信号后的价格走势 +- 当价格再次回踩到阴线最高点附近时发送特殊提醒 +- 监控期限:信号确认后 {self.monitor_days} 天 +- 提醒条件:价格接近阴线最高点且相比新高有明显回调 -信号触发条件: -- 形态完整匹配 -- 实体比例达标 -- 价格突破确认 -- EMA20趋势确认 -- 换手率约束达标 +信号特征: +- 模式识别:满足基础形态条件(无信号) +- 确认信号:完成创新高+回踩确认(正式信号) +- 信号内容:包含模式日期、创新高日期、回踩确认日期 +- 支持时间周期:{', '.join(self.timeframes)} + +优化效果: +- 极大降低假突破信号 +- 提供更优质的入场时机 +- 确保突破的真实有效性 +- 降低追高风险,提高成功率 扫描范围: - 优先使用双数据源合并(同花顺热股+东财人气榜) @@ -840,15 +1112,17 @@ K线形态策略 - 两阳线+阴线+阳线突破 通知方式: - 钉钉webhook汇总推送(10个信号一组分批发送) -- 价格回踩特殊提醒(5个提醒一组分批发送) -- 包含关键信息:代码、股票名称、K线时间、价格、周期等 +- 仅确认信号发送通知(模式识别不通知) +- 创新高回踩确认完成通知(正式信号) +- 价格二次回踩特殊提醒(5个提醒一组分批发送) +- 包含关键信息:代码、股票名称、模式日期、创新高日期、回踩确认日期、价格等 - 系统日志详细记录 """ if __name__ == "__main__": # 测试代码 - from ..data.data_fetcher import ADataFetcher + from ..data.tushare_fetcher import TushareFetcher as ADataFetcher from ..utils.notification import NotificationManager # 模拟配置 diff --git a/src/utils/notification.py b/src/utils/notification.py index 7ea3e75..f8e58ed 100644 --- a/src/utils/notification.py +++ b/src/utils/notification.py @@ -189,7 +189,7 @@ class DingTalkNotifier: def send_strategy_signal(self, stock_code: str, stock_name: str, timeframe: str, signal_type: str, price: float, signal_date: str = None, additional_info: Dict[str, Any] = None) -> bool: """ - 发送策略信号通知 + 发送策略信号通知(优化版:创新高回踩确认) Args: stock_code: 股票代码 @@ -208,30 +208,75 @@ class DingTalkNotifier: display_time = signal_date if signal_date else datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 构建Markdown消息 - title = f"📈 {signal_type}信号提醒" + title = f"🎯 {signal_type}信号确认" + # 基础信息 markdown_text = f""" -# 📈 {signal_type}信号提醒 +# 🎯 {signal_type}信号确认 **股票信息:** - 代码: `{stock_code}` - 名称: `{stock_name}` -- 价格: `{price}` 元 +- 确认价格: `{price}` 元 - 时间周期: `{timeframe}` -**信号时间:** {display_time} +**确认时间:** {display_time} -**策略说明:** 两阳线+阴线+阳线形态突破 - ---- -*量化交易系统自动发送* +**策略说明:** 两阳+阴+阳突破(创新高回踩确认) """ - # 添加额外信息 + # 添加创新高回踩确认的详细信息 if additional_info: - markdown_text += "\n**额外信息:**\n" - for key, value in additional_info.items(): - markdown_text += f"- {key}: `{value}`\n" + # 检查是否有创新高回踩确认的关键信息 + if 'new_high_price' in additional_info and 'new_high_date' in additional_info: + markdown_text += f""" +**🚀 创新高回踩确认详情:** +- 📅 模式日期: `{additional_info.get('pattern_date', '未知')}` +- 💰 原突破价: `{additional_info.get('breakout_price', 'N/A')}` 元 +- 🌟 创新高价: `{additional_info.get('new_high_price', 'N/A')}` 元 +- 🚀 创新高日期: `{additional_info.get('new_high_date', '未知')}` +- 🎯 阴线最高价: `{additional_info.get('yin_high', 'N/A')}` 元 +- ✅ 回踩确认日期: `{additional_info.get('confirmation_date', '未知')}` +- ⏰ 总确认用时: `{additional_info.get('confirmation_days', 'N/A')}` 天 +- 📏 回踩距离: `{additional_info.get('pullback_distance', 'N/A')}%` +""" + + # 添加其他额外信息 + markdown_text += "\n**📊 技术指标:**\n" + tech_indicators = ['yang1_entity_ratio', 'yang2_entity_ratio', 'final_yang_entity_ratio', + 'breakout_pct', 'turnover_ratio', 'above_ema20'] + for key in tech_indicators: + if key in additional_info: + value = additional_info[key] + if key.endswith('_ratio') and isinstance(value, (int, float)): + markdown_text += f"- {key}: `{value:.1%}`\n" + elif key == 'breakout_pct': + markdown_text += f"- 突破幅度: `{value:.2f}%`\n" + elif key == 'turnover_ratio': + markdown_text += f"- 换手率: `{value:.2f}%`\n" + elif key == 'above_ema20': + status = '✅上方' if value else '❌下方' + markdown_text += f"- EMA20位置: `{status}`\n" + else: + markdown_text += f"- {key}: `{value}`\n" + + markdown_text += """ +--- +**💡 操作建议:** +- ✅ 信号已通过创新高回踩双重确认 +- 📈 突破有效性得到验证 +- 🎯 当前为较优入场时机 +- ⚠️ 注意风险控制,设置合理止损 + +**🔍 关键确认要素:** +1. 🎯 形态: 两阳+阴+阳突破完成 +2. 🚀 创新高: 价格突破形态高点 +3. 📉 回踩: 回踩至阴线最高价附近 +4. ✅ 时机: 7天内完成双重确认 + +--- +*K线形态策略 - 创新高回踩确认版* +""" return self.send_markdown_message(title, markdown_text) @@ -325,15 +370,27 @@ class NotificationManager: for timeframe, signals in stock_results.items(): for signal in signals: total_signals += 1 + + # 根据新的信号格式提取信息 + confirmation_date = signal.get('confirmation_date', signal['date']) + new_high_price = signal.get('new_high_price', signal['breakout_price']) + confirmation_days = signal.get('confirmation_days', 0) + all_signal_details.append({ 'stock_code': stock_code, 'stock_name': signal.get('stock_name', '未知'), 'timeframe': timeframe, - 'signal_date': signal['date'], - 'price': signal['breakout_price'], + 'pattern_date': signal['date'], # 模式形成日期 + 'confirmation_date': confirmation_date, # 回踩确认日期 + 'price': new_high_price, # 创新高价格 + 'original_breakout_price': signal['breakout_price'], # 原突破价 + 'yin_high': signal.get('yin_high', 0), # 阴线最高价 'turnover': signal.get('turnover_ratio', 0), 'breakout_pct': signal.get('breakout_pct', 0), - 'ema20_status': '✅上方' if signal.get('above_ema20', False) else '❌下方' + 'ema20_status': '✅上方' if signal.get('above_ema20', False) else '❌下方', + 'confirmation_days': confirmation_days, + 'pullback_distance': signal.get('pullback_distance', 0), + 'is_new_format': signal.get('new_high_confirmed', False) # 是否为新格式信号 }) # 如果没有信号,直接返回 @@ -353,9 +410,9 @@ class NotificationManager: # 构建当前组的消息 if total_groups > 1: - title = f"📈 K线形态策略信号汇总 ({group_idx + 1}/{total_groups})" + title = f"🎯 K线形态策略信号汇总 ({group_idx + 1}/{total_groups})" else: - title = f"📈 K线形态策略信号汇总" + title = f"🎯 K线形态策略信号汇总" markdown_text = f""" # {title} @@ -375,13 +432,29 @@ class NotificationManager: - 数据源: `{scan_stats.get('data_source', '热门股票')}` """ - markdown_text += "\n**信号详情:**\n" + markdown_text += "\n**✅ 确认信号详情:**\n" # 添加当前组的信号详情 for i, signal in enumerate(group_signals, start_idx + 1): - markdown_text += f""" + if signal['is_new_format']: + # 新格式:创新高回踩确认 + markdown_text += f""" +{i}. **{signal['stock_code']} - {signal['stock_name']}** 🎯 + - 📅 模式日期: `{signal['pattern_date']}` + - ✅ 确认日期: `{signal['confirmation_date']}` + - 💰 原突破价: `{signal['original_breakout_price']:.2f}元` + - 🌟 创新高价: `{signal['price']:.2f}元` + - 🎯 阴线高点: `{signal['yin_high']:.2f}元` + - ⏰ 确认用时: `{signal['confirmation_days']}天` + - 📏 回踩距离: `{signal['pullback_distance']:.2f}%` + - 📊 周期: `{signal['timeframe']}` | 换手: `{signal['turnover']:.2f}%` + - 📈 EMA20: `{signal['ema20_status']}` +""" + else: + # 旧格式:兼容显示 + markdown_text += f""" {i}. **{signal['stock_code']} - {signal['stock_name']}** - - K线时间: `{signal['signal_date']}` + - K线时间: `{signal['pattern_date']}` - 时间周期: `{signal['timeframe']}` - 当前价格: `{signal['price']:.2f}元` - 突破幅度: `{signal['breakout_pct']:.2f}%` @@ -391,8 +464,18 @@ class NotificationManager: markdown_text += """ --- -**策略说明:** 两阳线+阴线+阳线形态突破 -*量化交易系统自动发送* +**🔍 策略说明:** 两阳+阴+阳突破(创新高回踩确认版) + +**💡 信号特点:** +- ✅ 所有信号已通过双重确认 +- 🎯 模式出现后等待创新高验证 +- 📉 创新高后回踩阴线最高价入场 +- ⏰ 7天内完成完整确认流程 + +**⚠️ 风险提示:** 投资有风险,入市需谨慎! + +--- +*K线形态策略系统自动发送* """ # 发送当前组的通知 @@ -459,9 +542,9 @@ class NotificationManager: # 构建完整的Markdown消息 markdown_text = f""" -# ⚠️ 价格回踩阴线最高点提醒 +# ⚠️ 已确认信号二次回踩提醒 -**🚨 重要提醒:** 以下股票在"两阳+阴+阳"形态突破后,价格回踩至阴线最高点附近,请关注支撑情况! +**🚨 重要提醒:** 以下股票已通过"创新高回踩确认"产生信号,现价格再次回踩至阴线最高点附近,请关注支撑情况! **📊 本批提醒数量:** {len(group_alerts)}个 **🕐 检查时间:** {current_time} @@ -472,11 +555,18 @@ class NotificationManager: --- **💡 操作建议:** -- 关注是否在阴线最高点获得有效支撑 -- 如跌破阴线最高点需要重新评估形态有效性 -- 建议结合成交量和其他技术指标综合判断 +- ✅ 这些股票已通过双重确认,信号有效性较高 +- 🎯 当前为二次回踩阴线最高点,关注支撑强度 +- 📈 如获得有效支撑,可能形成新的上涨起点 +- 📉 如跌破阴线最高点,需要重新评估信号有效性 +- 💰 建议结合成交量和其他技术指标综合判断 -**⚠️ 风险提示:** 本提醒仅供参考,投资需谨慎! +**🔍 提醒说明:** +- 此类股票已完成创新高+回踩确认流程 +- 当前价格位置具有重要技术意义 +- 阴线最高点是关键支撑/阻力位 + +**⚠️ 风险提示:** 本提醒仅供参考,投资有风险,入市需谨慎! """ # 发送消息 diff --git a/start_mysql_web.py b/start_mysql_web.py new file mode 100644 index 0000000..93d0b50 --- /dev/null +++ b/start_mysql_web.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +""" +启动MySQL版本的Web应用 +""" + +import sys +from pathlib import Path + +# 添加项目根目录到路径 +current_dir = Path(__file__).parent +sys.path.insert(0, str(current_dir)) + +from web.mysql_app import app + +if __name__ == '__main__': + print("="*70) + print("🌐 A股量化交易系统 Web界面 (MySQL版)") + print("="*70) + print("🚀 启动Flask服务器...") + print("📊 访问地址: http://localhost:8080") + print("🗄️ 数据库: MySQL (腾讯云)") + print("✨ 功能特性:") + print(" - 创新高回踩确认策略") + print(" - 增强时间线显示") + print(" - 实时信号监控") + print(" - 回踩提醒功能") + print("="*70) + + app.run(host='0.0.0.0', port=8080, debug=True) \ No newline at end of file diff --git a/start_web.py b/start_web.py deleted file mode 100644 index 1f5d3f5..0000000 --- a/start_web.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -""" -启动AI 智能选股大师Web界面 -""" - -import sys -import subprocess -from pathlib import Path -import webbrowser -import time - -def main(): - """启动Web服务""" - print("🌐 AI 智能选股大师") - print("=" * 50) - - # 检查web目录 - web_dir = Path(__file__).parent / "web" - if not web_dir.exists(): - print("❌ web目录不存在") - return - - app_file = web_dir / "app.py" - if not app_file.exists(): - print("❌ app.py文件不存在") - return - - print("🚀 启动Flask服务器...") - print("📊 访问地址: http://localhost:8080") - print("⏹️ 按 Ctrl+C 停止服务") - print("=" * 50) - - try: - # 启动Flask应用 - subprocess.run([ - sys.executable, str(app_file) - ], cwd=str(web_dir)) - except KeyboardInterrupt: - print("\n👋 服务已停止") - except Exception as e: - print(f"❌ 启动失败: {e}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/test_all_a_shares.py b/test_all_a_shares.py deleted file mode 100644 index 5556f4a..0000000 --- a/test_all_a_shares.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -""" -测试所有A股股票扫描功能 -""" - -import sys -from pathlib import Path - -# 添加项目根目录到路径 -current_dir = Path(__file__).parent -sys.path.insert(0, str(current_dir)) - -from loguru import logger -from src.strategy.kline_pattern_strategy import KLinePatternStrategy -from src.data.data_fetcher import ADataFetcher -from src.utils.notification import NotificationManager -from src.database.database_manager import DatabaseManager -from src.utils.config_loader import ConfigLoader - - -def test_all_a_shares_scan(): - """测试全A股扫描功能""" - logger.remove() - logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}") - - print("🧪 测试全A股扫描功能") - print("=" * 50) - - try: - # 初始化组件 - logger.info("初始化组件...") - config_loader = ConfigLoader() - config = config_loader.load_config() - - data_fetcher = ADataFetcher() - notification_manager = NotificationManager(config.get('notification', {})) - db_manager = DatabaseManager() - - # 初始化策略 - kline_config = config.get('strategy', {}).get('kline_pattern', {}) - strategy = KLinePatternStrategy( - data_fetcher=data_fetcher, - notification_manager=notification_manager, - config=kline_config, - db_manager=db_manager - ) - - # 测试过滤后的A股列表 - logger.info("测试获取过滤后的A股列表...") - filtered_stocks = data_fetcher.get_filtered_a_share_list() - logger.info(f"过滤后的A股数量: {len(filtered_stocks)}只") - - if not filtered_stocks.empty: - # 显示样例 - sample_stocks = filtered_stocks.head(5) - logger.info("A股样例:") - for _, stock in sample_stocks.iterrows(): - logger.info(f" {stock['full_stock_code']} - {stock['short_name']} ({stock['exchange']})") - - # 测试扫描全A股(限制5只股票进行测试) - logger.info("开始测试全A股扫描(限制5只)...") - results = strategy.scan_market( - max_stocks=5, # 限制5只股票进行测试 - use_all_a_shares=True # 使用全A股模式 - ) - - # 统计结果 - total_signals = 0 - for stock_code, stock_results in results.items(): - stock_signals = sum(len(signals) for signals in stock_results.values()) - total_signals += stock_signals - logger.info(f"股票 {stock_code}: {stock_signals}个信号") - - logger.info(f"✅ 全A股扫描测试完成!") - logger.info(f"📊 扫描股票数: {len(results)}只") - logger.info(f"📈 发现信号数: {total_signals}个") - - print("\n" + "=" * 50) - print("🎯 测试结果:") - print(f" - 可用A股数量: {len(filtered_stocks)}只") - print(f" - 扫描股票数量: 5只(测试限制)") - print(f" - 发现信号数量: {total_signals}个") - print(" - 功能状态: ✅ 正常工作") - print("=" * 50) - - except Exception as e: - logger.error(f"❌ 测试失败: {e}") - import traceback - traceback.print_exc() - - -if __name__ == "__main__": - test_all_a_shares_scan() \ No newline at end of file diff --git a/test_database_integration.py b/test_database_integration.py deleted file mode 100644 index 33efbfe..0000000 --- a/test_database_integration.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env python3 -""" -测试数据库集成和策略存储功能 -""" - -import sys -from pathlib import Path -from datetime import datetime, date - -# 添加src目录到路径 -current_dir = Path(__file__).parent -src_dir = current_dir / "src" -sys.path.insert(0, str(src_dir)) - -from loguru import logger -from src.database.database_manager import DatabaseManager -from src.utils.config_loader import ConfigLoader -from src.data.data_fetcher import ADataFetcher -from src.utils.notification import NotificationManager -from src.strategy.kline_pattern_strategy import KLinePatternStrategy - - -def test_database_operations(): - """测试数据库基本操作""" - logger.info("🗄️ 测试数据库基本操作...") - - try: - # 初始化数据库管理器 - db_manager = DatabaseManager() - - # 测试策略统计 - strategy_stats = db_manager.get_strategy_stats() - logger.info(f"📊 策略统计记录数: {len(strategy_stats)}") - - # 测试最新信号 - latest_signals = db_manager.get_latest_signals(limit=10) - logger.info(f"📈 最新信号记录数: {len(latest_signals)}") - - # 测试日期范围查询 - start_date = date.today() - signals_by_date = db_manager.get_signals_by_date_range(start_date) - logger.info(f"🗓️ 今日信号记录数: {len(signals_by_date)}") - - # 测试回踩提醒 - pullback_alerts = db_manager.get_pullback_alerts(days=7) - logger.info(f"⚠️ 最近7天回踩提醒: {len(pullback_alerts)}") - - logger.info("✅ 数据库基本操作测试完成") - return True - - except Exception as e: - logger.error(f"❌ 数据库操作测试失败: {e}") - return False - - -def test_strategy_integration(): - """测试策略与数据库集成""" - logger.info("🔄 测试策略与数据库集成...") - - try: - # 初始化组件 - config_loader = ConfigLoader() - config = config_loader.load_config() - - data_fetcher = ADataFetcher() - notification_manager = NotificationManager(config.get('notification', {})) - db_manager = DatabaseManager() - - # 初始化策略(自动创建数据库记录) - kline_config = config.get('strategy', {}).get('kline_pattern', {}) - strategy = KLinePatternStrategy( - data_fetcher=data_fetcher, - notification_manager=notification_manager, - config=kline_config, - db_manager=db_manager - ) - - logger.info(f"📋 策略ID: {strategy.strategy_id}") - logger.info(f"📝 策略名称: {strategy.strategy_name}") - - # 测试分析单只股票(会自动保存到数据库) - test_stock = "000001.SZ" - logger.info(f"🔍 测试分析股票: {test_stock}") - - stock_results = strategy.analyze_stock(test_stock, days=30) - total_signals = sum(len(signals) for signals in stock_results.values()) - - logger.info(f"📊 分析结果: {total_signals} 个信号") - - # 验证数据库中的记录 - latest_signals = db_manager.get_latest_signals(strategy_name=strategy.strategy_name, limit=10) - logger.info(f"💾 数据库中最新信号数: {len(latest_signals)}") - - logger.info("✅ 策略与数据库集成测试完成") - return True - - except Exception as e: - logger.error(f"❌ 策略集成测试失败: {e}") - import traceback - traceback.print_exc() - return False - - -def test_scan_market_with_database(): - """测试市场扫描与数据库存储""" - logger.info("🌍 测试市场扫描与数据库存储...") - - try: - # 初始化组件 - config_loader = ConfigLoader() - config = config_loader.load_config() - - data_fetcher = ADataFetcher() - notification_manager = NotificationManager(config.get('notification', {})) - db_manager = DatabaseManager() - - # 初始化策略 - kline_config = config.get('strategy', {}).get('kline_pattern', {}) - strategy = KLinePatternStrategy( - data_fetcher=data_fetcher, - notification_manager=notification_manager, - config=kline_config, - db_manager=db_manager - ) - - # 小规模市场扫描测试(限制5只股票) - logger.info("🔍 开始小规模市场扫描测试...") - test_stocks = ["000001.SZ", "000002.SZ", "600000.SH", "600036.SH", "000858.SZ"] - - results = strategy.scan_market( - stock_list=test_stocks, - max_stocks=5, - use_hot_stocks=False - ) - - total_signals = sum( - sum(len(signals) for signals in stock_results.values()) - for stock_results in results.values() - ) - - logger.info(f"📊 扫描完成: 发现 {total_signals} 个信号") - - # 验证数据库存储 - recent_signals = db_manager.get_latest_signals( - strategy_name=strategy.strategy_name, - limit=50 - ) - logger.info(f"💾 数据库中存储的信号数: {len(recent_signals)}") - - # 显示最新的几个信号 - if not recent_signals.empty: - logger.info("📋 最新信号示例:") - for i, signal in recent_signals.head(3).iterrows(): - logger.info(f" {signal['stock_code']}({signal['stock_name']}) - {signal['breakout_price']:.2f}元") - - logger.info("✅ 市场扫描与数据库存储测试完成") - return True - - except Exception as e: - logger.error(f"❌ 市场扫描测试失败: {e}") - import traceback - traceback.print_exc() - return False - - -def test_database_queries(): - """测试数据库查询功能""" - logger.info("🔍 测试数据库查询功能...") - - try: - db_manager = DatabaseManager() - - # 测试策略统计 - strategy_stats = db_manager.get_strategy_stats() - if not strategy_stats.empty: - logger.info("📊 策略统计:") - for _, stat in strategy_stats.iterrows(): - logger.info(f" {stat['strategy_name']}: {stat['total_signals']}个信号, {stat['unique_stocks']}只股票") - - # 测试按日期查询 - today = date.today() - today_signals = db_manager.get_signals_by_date_range(today, today) - logger.info(f"📅 今日信号数: {len(today_signals)}") - - # 测试获取策略ID - strategy_id = db_manager.get_strategy_id("K线形态策略") - logger.info(f"🆔 K线形态策略ID: {strategy_id}") - - logger.info("✅ 数据库查询功能测试完成") - return True - - except Exception as e: - logger.error(f"❌ 数据库查询测试失败: {e}") - return False - - -def main(): - """主测试函数""" - logger.remove() - logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}") - - print("=" * 70) - print("🧪 A股量化交易系统 - 数据库集成测试") - print("=" * 70) - - test_results = [] - - # 运行测试 - tests = [ - ("数据库基本操作", test_database_operations), - ("策略与数据库集成", test_strategy_integration), - ("数据库查询功能", test_database_queries), - ("市场扫描与存储", test_scan_market_with_database), - ] - - for test_name, test_func in tests: - logger.info(f"\n🚀 开始测试: {test_name}") - try: - result = test_func() - test_results.append((test_name, result)) - if result: - logger.info(f"✅ {test_name} 测试通过") - else: - logger.error(f"❌ {test_name} 测试失败") - except Exception as e: - logger.error(f"❌ {test_name} 测试异常: {e}") - test_results.append((test_name, False)) - - # 输出测试结果 - print("\n" + "=" * 70) - print("📊 测试结果汇总:") - print("=" * 70) - - passed = 0 - total = len(test_results) - - for test_name, result in test_results: - status = "✅ 通过" if result else "❌ 失败" - print(f" {test_name}: {status}") - if result: - passed += 1 - - print(f"\n🎯 总计: {passed}/{total} 个测试通过") - - if passed == total: - print("🎉 所有测试都通过了!数据库集成功能正常工作。") - print("🌐 现在可以启动Web界面查看数据:") - print(" cd web && python app.py") - else: - print("⚠️ 部分测试失败,请检查错误信息并修复问题。") - - print("=" * 70) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/test_eastmoney_sectors.py b/test_eastmoney_sectors.py new file mode 100644 index 0000000..71383a9 --- /dev/null +++ b/test_eastmoney_sectors.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +""" +使用东财概念板块数据分析本周强势板块 +需要5000积分 +""" + +import sys +from pathlib import Path +import pandas as pd +from datetime import datetime, timedelta + +# 添加项目根目录到路径 +current_dir = Path(__file__).parent +sys.path.insert(0, str(current_dir)) + +from src.data.tushare_fetcher import TushareFetcher +from loguru import logger + + +def get_recent_trading_dates(days_back=5): + """获取最近的交易日期""" + dates = [] + current = datetime.now() + + while len(dates) < days_back: + # 排除周末 + if current.weekday() < 5: # 0-4是周一到周五 + dates.append(current.strftime('%Y%m%d')) + current -= timedelta(days=1) + + return sorted(dates) # 升序返回 + + +def analyze_eastmoney_concepts(fetcher: TushareFetcher): + """使用东财概念板块数据分析""" + try: + if not fetcher.pro: + logger.error("需要Tushare Pro权限") + return + + logger.info("🚀 使用东财概念板块数据分析...") + + # 获取最近5个交易日 + trading_dates = get_recent_trading_dates(5) + logger.info(f"分析时间范围: {trading_dates[0]} 到 {trading_dates[-1]}") + + # 获取最新交易日的概念板块数据 + latest_date = trading_dates[-1] + + try: + # 使用东财概念板块接口 + dc_concepts = fetcher.pro.dc_index(trade_date=latest_date) + logger.info(f"获取到 {len(dc_concepts)} 个东财概念板块") + + if dc_concepts.empty: + logger.warning("未获取到东财概念板块数据") + return + + # 打印数据结构以便调试 + logger.info(f"数据列名: {list(dc_concepts.columns)}") + if not dc_concepts.empty: + logger.info(f"样本数据:\n{dc_concepts.head(2)}") + + # 检查涨跌幅字段名 + change_col = None + for col in ['pct_chg', 'pct_change', 'change_pct', 'chg_pct']: + if col in dc_concepts.columns: + change_col = col + break + + if change_col: + # 按涨跌幅排序 + dc_concepts = dc_concepts.sort_values(change_col, ascending=False) + else: + logger.warning("未找到涨跌幅字段,使用原始顺序") + change_col = 'code' # 使用code作为默认排序 + + print("\n" + "="*80) + print("📈 东财概念板块实时排行榜") + print("="*80) + # 显示表头 + if change_col != 'code': + print(f"{'排名':<4} {'概念名称':<25} {'涨跌幅':<10} {'概念代码':<15}") + else: + print(f"{'排名':<4} {'概念名称':<25} {'概念代码':<15}") + print("-" * 80) + + for i, (_, concept) in enumerate(dc_concepts.head(20).iterrows()): + rank = i + 1 + name = concept.get('name', 'N/A')[:23] + '..' if len(str(concept.get('name', 'N/A'))) > 23 else concept.get('name', 'N/A') + code = concept.get('ts_code', 'N/A') + + if change_col != 'code': + change_pct = f"{concept[change_col]:+.2f}%" if not pd.isna(concept.get(change_col, 0)) else "N/A" + print(f"{rank:<4} {name:<25} {change_pct:<10} {code:<15}") + else: + print(f"{rank:<4} {name:<25} {code:<15}") + + # 强势概念TOP10 + if change_col != 'code': + print(f"\n🚀 强势概念板块TOP10:") + for i, (_, concept) in enumerate(dc_concepts.head(10).iterrows()): + change_val = concept.get(change_col, 0) + if not pd.isna(change_val): + print(f" {i+1:2d}. {concept.get('name', 'N/A')}: {change_val:+.2f}%") + + # 弱势概念TOP10 + print(f"\n📉 弱势概念板块TOP10:") + weak_concepts = dc_concepts.tail(10).iloc[::-1] # 反转顺序 + for i, (_, concept) in enumerate(weak_concepts.iterrows()): + change_val = concept.get(change_col, 0) + if not pd.isna(change_val): + print(f" {i+1:2d}. {concept.get('name', 'N/A')}: {change_val:+.2f}%") + else: + print(f"\n📋 概念板块列表(前10个):") + for i, (_, concept) in enumerate(dc_concepts.head(10).iterrows()): + print(f" {i+1:2d}. {concept.get('name', 'N/A')} ({concept.get('ts_code', 'N/A')})") + + return dc_concepts + + except Exception as e: + logger.error(f"获取东财概念板块数据失败: {e}") + return None + + except Exception as e: + logger.error(f"分析东财概念板块失败: {e}") + return None + + +def analyze_concept_trend(fetcher: TushareFetcher, concept_codes=None): + """分析概念板块的趋势(多日对比)""" + try: + if not fetcher.pro: + logger.error("需要Tushare Pro权限") + return + + logger.info("📊 分析概念板块趋势...") + + # 获取最近5个交易日 + trading_dates = get_recent_trading_dates(5) + + # 如果没有指定概念代码,获取当日表现最好的前10个 + if concept_codes is None: + latest_concepts = analyze_eastmoney_concepts(fetcher) + if latest_concepts is not None and not latest_concepts.empty: + concept_codes = latest_concepts.head(5)['code'].tolist() + else: + logger.warning("无法获取概念代码") + return + + print(f"\n" + "="*80) + print("📈 热门概念板块多日趋势分析") + print("="*80) + + for concept_code in concept_codes: + concept_trend = [] + + for date in trading_dates: + try: + # 获取特定日期的概念数据 + daily_data = fetcher.pro.dc_index( + trade_date=date, + ts_code=concept_code + ) + + if not daily_data.empty: + # 检查数据结构 + logger.debug(f"概念 {concept_code} 在 {date} 的数据字段: {list(daily_data.columns)}") + + # 东财概念数据可能没有close字段,使用其他字段替代 + close_value = daily_data.iloc[0].get('total_mv', 1) # 使用总市值代替 + if close_value == 0: + close_value = 1 # 避免除零 + + concept_trend.append({ + 'date': date, + 'name': daily_data.iloc[0]['name'], + 'close': close_value, + 'pct_chg': daily_data.iloc[0]['pct_change'] + }) + + except Exception as e: + logger.debug(f"获取概念 {concept_code} 在 {date} 的数据失败: {e}") + continue + + # 输出趋势 + if concept_trend: + concept_name = concept_trend[0]['name'] + print(f"\n📊 {concept_name} ({concept_code}) 近5日走势:") + + # 计算总涨跌幅 + if len(concept_trend) >= 2: + start_close = concept_trend[0]['close'] + end_close = concept_trend[-1]['close'] + + if start_close != 0 and start_close is not None: + total_change = (end_close - start_close) / start_close * 100 + print(f" 总涨跌幅: {total_change:+.2f}%") + else: + print(f" 总涨跌幅: 无法计算(起始值为0)") + + # 显示每日数据 + for data in concept_trend: + print(f" {data['date']}: {data['pct_chg']:+6.2f}% (指数: {data['close']:8.2f})") + + print("\n" + "="*80) + + except Exception as e: + logger.error(f"分析概念趋势失败: {e}") + + +def get_concept_constituents(fetcher: TushareFetcher, concept_code: str): + """获取概念板块成分股""" + try: + if not fetcher.pro: + logger.error("需要Tushare Pro权限") + return + + logger.info(f"获取概念 {concept_code} 的成分股...") + + # 尝试通过概念板块获取成分股 + try: + # 使用concept_detail接口(如果可用) + constituents = fetcher.pro.concept_detail(id=concept_code) + + if not constituents.empty: + print(f"\n📋 概念成分股 ({len(constituents)}只):") + for _, stock in constituents.head(10).iterrows(): + print(f" {stock['ts_code']}: {stock.get('name', 'N/A')}") + else: + logger.warning(f"概念 {concept_code} 无成分股数据") + + except Exception as e: + logger.error(f"获取概念成分股失败: {e}") + + except Exception as e: + logger.error(f"获取概念成分股失败: {e}") + + +def main(): + """主函数""" + logger.info("🚀 开始使用东财概念板块数据分析...") + + # 初始化Tushare数据获取器 + token = "0ed6419a00d8923dc19c0b58fc92d94c9a0696949ab91a13aa58a0cc" + fetcher = TushareFetcher(token=token) + + # 1. 分析当日概念板块表现 + concepts_data = analyze_eastmoney_concepts(fetcher) + + # 2. 分析热门概念的多日趋势 + if concepts_data is not None and not concepts_data.empty: + print("\n" + "="*80 + "\n") + + # 获取表现最好的前3个概念进行趋势分析 + top_concepts = concepts_data.head(3)['ts_code'].tolist() + analyze_concept_trend(fetcher, top_concepts) + + # 3. 获取第一个概念的成分股示例 + # top_concept_code = top_concepts[0] if top_concepts else None + # if top_concept_code: + # get_concept_constituents(fetcher, top_concept_code) + + logger.info("✅ 分析完成!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_optimized_notification.py b/test_optimized_notification.py new file mode 100644 index 0000000..bdc4966 --- /dev/null +++ b/test_optimized_notification.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +""" +测试优化后的钉钉通知格式(创新高回踩确认版) +""" + +import sys +from pathlib import Path +from datetime import datetime + +# 添加项目根目录到路径 +current_dir = Path(__file__).parent +sys.path.insert(0, str(current_dir)) + +from src.utils.notification import NotificationManager +from loguru import logger + + +def test_optimized_notification(): + """ + 测试优化后的钉钉通知格式 + """ + logger.info("🚀 开始测试优化后的钉钉通知格式...") + + # 配置通知管理器(测试模式,不实际发送) + notification_config = { + 'dingtalk': { + 'enabled': False, # 设置为True并提供真实webhook进行实际测试 + 'webhook_url': 'https://oapi.dingtalk.com/robot/send?access_token=TEST_TOKEN' + } + } + + notification_manager = NotificationManager(notification_config) + + # 测试1: 单个策略信号通知 + logger.info("\n📊 测试1: 单个策略信号通知") + test_signal_data = { + 'stock_code': '000001.SZ', + 'stock_name': '平安银行', + 'timeframe': 'daily', + 'signal_type': '两阳+阴+阳突破(创新高回踩确认)', + 'price': 14.50, + 'signal_date': '2024-01-11', + 'additional_info': { + 'pattern_date': '2024-01-04', + 'breakout_price': 11.60, + 'new_high_price': 14.50, + 'new_high_date': '2024-01-10', + 'yin_high': 11.20, + 'confirmation_date': '2024-01-11', + 'confirmation_days': 7, + 'pullback_distance': -0.89, + 'yang1_entity_ratio': 0.60, + 'yang2_entity_ratio': 0.67, + 'final_yang_entity_ratio': 0.89, + 'breakout_pct': 3.57, + 'turnover_ratio': 2.50, + 'above_ema20': True + } + } + + try: + # 模拟发送单个信号通知 + if notification_config['dingtalk']['enabled']: + success = notification_manager.send_strategy_signal(**test_signal_data) + logger.info(f"单个信号通知发送: {'✅成功' if success else '❌失败'}") + else: + logger.info("单个信号通知格式测试完成(未实际发送)") + except Exception as e: + logger.error(f"单个信号通知测试失败: {e}") + + # 测试2: 策略汇总通知 + logger.info("\n📊 测试2: 策略汇总通知") + test_summary_data = { + '000001.SZ': { + 'daily': [ + { + 'stock_name': '平安银行', + 'date': '2024-01-04', + 'breakout_price': 11.60, + 'new_high_price': 14.50, + 'new_high_date': '2024-01-10', + 'confirmation_date': '2024-01-11', + 'confirmation_days': 7, + 'pullback_distance': -0.89, + 'yin_high': 11.20, + 'turnover_ratio': 2.5, + 'breakout_pct': 3.57, + 'above_ema20': True, + 'new_high_confirmed': True # 标记为新格式 + } + ] + }, + '000002.SZ': { + 'daily': [ + { + 'stock_name': '万科A', + 'date': '2024-01-05', + 'breakout_price': 9.80, + 'new_high_price': 12.30, + 'new_high_date': '2024-01-09', + 'confirmation_date': '2024-01-12', + 'confirmation_days': 7, + 'pullback_distance': -1.2, + 'yin_high': 9.60, + 'turnover_ratio': 3.2, + 'breakout_pct': 2.08, + 'above_ema20': True, + 'new_high_confirmed': True # 标记为新格式 + } + ] + } + } + + scan_stats = { + 'total_scanned': 100, + 'data_source': '双数据源合并' + } + + try: + # 模拟发送汇总通知 + if notification_config['dingtalk']['enabled']: + success = notification_manager.send_strategy_summary(test_summary_data, scan_stats) + logger.info(f"汇总通知发送: {'✅成功' if success else '❌失败'}") + else: + logger.info("汇总通知格式测试完成(未实际发送)") + except Exception as e: + logger.error(f"汇总通知测试失败: {e}") + + # 测试3: 回踩提醒通知 + logger.info("\n📊 测试3: 回踩提醒通知") + test_pullback_alerts = [ + { + 'stock_code': '000001.SZ', + 'stock_name': '平安银行', + 'signal_date': '2024-01-11', + 'current_date': '2024-01-18', + 'timeframe': 'daily', + 'yin_high': 11.20, + 'breakout_price': 11.60, + 'current_price': 11.15, + 'current_low': 11.10, + 'pullback_pct': -4.5, + 'distance_to_yin_high': -0.45, + 'days_since_signal': 7, + 'alert_type': 'pullback_to_yin_high' + } + ] + + try: + # 模拟发送回踩提醒 + if notification_config['dingtalk']['enabled']: + success = notification_manager.send_pullback_alerts(test_pullback_alerts) + logger.info(f"回踩提醒发送: {'✅成功' if success else '❌失败'}") + else: + logger.info("回踩提醒格式测试完成(未实际发送)") + except Exception as e: + logger.error(f"回踩提醒测试失败: {e}") + + # 显示消息格式预览 + print("\n" + "="*80) + print("📱 优化后的钉钉消息格式预览") + print("="*80) + + print("\n🎯 单个信号通知示例:") + print("标题: 🎯 两阳+阴+阳突破(创新高回踩确认)信号确认") + print("内容包含: 股票信息、创新高回踩确认详情、技术指标、操作建议等") + + print("\n📊 汇总通知示例:") + print("标题: 🎯 K线形态策略信号汇总") + print("内容包含: 扫描统计、确认信号详情(模式日期+确认日期+创新高价等)") + + print("\n⚠️ 回踩提醒示例:") + print("标题: ⚠️ 已确认信号二次回踩提醒") + print("内容包含: 已确认信号的二次回踩情况、支撑分析建议等") + + print("\n✅ 钉钉消息优化完成!") + print("主要改进:") + print("- 突出创新高回踩确认逻辑") + print("- 详细展示时间线(模式日期→创新高日期→确认日期)") + print("- 增加操作建议和风险提示") + print("- 区分新旧格式信号,向下兼容") + + +def main(): + """主函数""" + logger.info("🚀 开始钉钉通知优化测试...") + test_optimized_notification() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_simple_integration.py b/test_simple_integration.py deleted file mode 100644 index e9ee77d..0000000 --- a/test_simple_integration.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -""" -简单的数据库集成测试 -""" - -import sys -from pathlib import Path - -# 添加src目录到路径 -current_dir = Path(__file__).parent -src_dir = current_dir / "src" -sys.path.insert(0, str(src_dir)) - -from loguru import logger -from src.database.database_manager import DatabaseManager -from src.utils.config_loader import ConfigLoader -from src.data.data_fetcher import ADataFetcher -from src.utils.notification import NotificationManager -from src.strategy.kline_pattern_strategy import KLinePatternStrategy - - -def main(): - """简单测试""" - logger.remove() - logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}") - - print("🧪 简单数据库集成测试") - print("=" * 50) - - try: - # 初始化组件 - logger.info("初始化组件...") - config_loader = ConfigLoader() - config = config_loader.load_config() - - data_fetcher = ADataFetcher() - notification_manager = NotificationManager(config.get('notification', {})) - db_manager = DatabaseManager() - - # 初始化策略 - kline_config = config.get('strategy', {}).get('kline_pattern', {}) - strategy = KLinePatternStrategy( - data_fetcher=data_fetcher, - notification_manager=notification_manager, - config=kline_config, - db_manager=db_manager - ) - - logger.info(f"策略ID: {strategy.strategy_id}") - - # 测试小规模扫描 - logger.info("开始小规模扫描...") - test_stocks = ["000001.SZ", "000002.SZ"] - - results = strategy.scan_market( - stock_list=test_stocks, - max_stocks=2, - use_hot_stocks=False - ) - - # 检查数据库 - logger.info("检查数据库记录...") - latest_signals = db_manager.get_latest_signals(limit=10) - logger.info(f"数据库中的信号数: {len(latest_signals)}") - - if not latest_signals.empty: - logger.info("信号示例:") - for _, signal in latest_signals.head(3).iterrows(): - logger.info(f" {signal['stock_code']} - {signal['breakout_price']:.2f}元") - - logger.info("✅ 测试完成") - - except Exception as e: - logger.error(f"❌ 测试失败: {e}") - import traceback - traceback.print_exc() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/test_strong_sectors_advanced.py b/test_strong_sectors_advanced.py new file mode 100644 index 0000000..361b575 --- /dev/null +++ b/test_strong_sectors_advanced.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +""" +高级强势板块筛选器 +筛选条件: +1. 本周收阳(周涨幅>0) +2. 周线级别创阶段新高(20周新高) +3. 成交额巨大(超过1000亿) +""" + +import sys +from pathlib import Path +import pandas as pd +import numpy as np +from datetime import datetime, timedelta + +# 添加项目根目录到路径 +current_dir = Path(__file__).parent +sys.path.insert(0, str(current_dir)) + +from src.data.tushare_fetcher import TushareFetcher +from loguru import logger + + +def get_trading_dates(days_back=100): + """获取过去N个交易日""" + dates = [] + current = datetime.now() + + while len(dates) < days_back: + if current.weekday() < 5: # 周一到周五 + dates.append(current.strftime('%Y%m%d')) + current -= timedelta(days=1) + + return sorted(dates) # 升序返回 + + +def filter_index_concepts(ths_concepts): + """过滤掉指数型板块""" + index_keywords = [ + '成份股', '样本股', '成分股', '50', '100', '300', '500', '1000', + '上证', '深证', '中证', '创业板', '科创板', '北证', + 'ETF', '指数', 'Index', '基准' + ] + + def is_concept_plate(name: str) -> bool: + name_lower = name.lower() + for keyword in index_keywords: + if keyword.lower() in name_lower: + return False + return True + + original_count = len(ths_concepts) + filtered_concepts = ths_concepts[ths_concepts['name'].apply(is_concept_plate)] + filtered_count = len(filtered_concepts) + + logger.info(f"过滤指数型板块: {original_count} -> {filtered_count} 个") + return filtered_concepts + + +def analyze_concept_strength(fetcher: TushareFetcher, concept_info, trading_dates): + """分析单个概念的强势特征""" + ts_code = concept_info['ts_code'] + name = concept_info['name'] + + try: + # 获取过去20周的数据(约100个交易日) + start_date = trading_dates[0] + end_date = trading_dates[-1] + + daily_data = fetcher.pro.ths_daily( + ts_code=ts_code, + start_date=start_date, + end_date=end_date + ) + + if daily_data.empty or len(daily_data) < 20: + return None + + # 按日期排序 + daily_data = daily_data.sort_values('trade_date') + daily_data.reset_index(drop=True, inplace=True) + + # 1. 计算本周涨幅(最近5个交易日) + recent_data = daily_data.tail(5) + if len(recent_data) < 2: + return None + + week_start_close = recent_data.iloc[0]['close'] + week_end_close = recent_data.iloc[-1]['close'] + week_change = (week_end_close - week_start_close) / week_start_close * 100 + + # 2. 检查是否本周收阳 + is_weekly_positive = week_change > 0 + + # 3. 计算20周新高(约100个交易日) + current_close = daily_data.iloc[-1]['close'] + past_20weeks_high = daily_data['high'].max() + is_20week_high = current_close >= past_20weeks_high * 0.99 # 允许1%的误差 + + # 4. 计算成交额(最近5日平均) + recent_turnover = recent_data['vol'].mean() * recent_data['close'].mean() # 简化计算 + turnover_100yi = recent_turnover / 100000000 # 转换为亿元 + + # 5. 计算技术指标 + # RSI相对强弱指数 + rsi = calculate_rsi(daily_data['close'].values) + + # 20日均线趋势 + ma20 = daily_data['close'].rolling(20).mean() + ma20_trend = (ma20.iloc[-1] - ma20.iloc[-10]) / ma20.iloc[-10] * 100 if len(ma20) >= 20 else 0 + + # 波动率 + volatility = daily_data['pct_change'].std() * np.sqrt(250) # 年化波动率 + + return { + 'ts_code': ts_code, + 'name': name, + 'week_change': week_change, + 'is_weekly_positive': is_weekly_positive, + 'is_20week_high': is_20week_high, + 'avg_turnover_yi': turnover_100yi, + 'current_close': current_close, + 'rsi': rsi, + 'ma20_trend': ma20_trend, + 'volatility': volatility, + 'data_length': len(daily_data) + } + + except Exception as e: + logger.debug(f"分析 {name} 失败: {e}") + return None + + +def calculate_rsi(prices, period=14): + """计算RSI指标""" + try: + delta = np.diff(prices) + gain = np.where(delta > 0, delta, 0) + loss = np.where(delta < 0, -delta, 0) + + avg_gain = np.mean(gain[-period:]) if len(gain) >= period else 0 + avg_loss = np.mean(loss[-period:]) if len(loss) >= period else 0 + + if avg_loss == 0: + return 100 + + rs = avg_gain / avg_loss + rsi = 100 - (100 / (1 + rs)) + return rsi + except: + return 50 # 默认值 + + +def find_strong_sectors(fetcher: TushareFetcher): + """寻找强势板块""" + try: + logger.info("🔍 开始寻找强势板块...") + + # 获取交易日期 + trading_dates = get_trading_dates(100) + logger.info(f"分析周期: {trading_dates[0]} 到 {trading_dates[-1]} (100个交易日)") + + # 获取同花顺概念列表 + ths_concepts = fetcher.pro.ths_index(exchange='A', type='N') + if ths_concepts.empty: + logger.error("未获取到概念数据") + return + + # 过滤指数型概念 + ths_concepts = filter_index_concepts(ths_concepts) + + # 分析概念强度 + strong_concepts = [] + total_concepts = min(80, len(ths_concepts)) # 分析前80个概念 + logger.info(f"分析 {total_concepts} 个概念...") + + for i, (_, concept) in enumerate(ths_concepts.head(total_concepts).iterrows()): + if i % 10 == 0: + logger.info(f"进度: {i+1}/{total_concepts}") + + result = analyze_concept_strength(fetcher, concept, trading_dates) + if result: + strong_concepts.append(result) + + if not strong_concepts: + logger.warning("未找到符合条件的强势板块") + return + + # 转换为DataFrame + df = pd.DataFrame(strong_concepts) + + # 强势板块筛选 + logger.info("🚀 应用强势板块筛选条件...") + + # 条件1:本周收阳 + weekly_positive = df[df['is_weekly_positive']] + logger.info(f"本周收阳概念: {len(weekly_positive)} 个") + + # 条件2:20周新高 + new_high_concepts = df[df['is_20week_high']] + logger.info(f"20周新高概念: {len(new_high_concepts)} 个") + + # 条件3:成交额超过1000亿(这里设置为10亿,因为单个概念1000亿太高) + high_turnover = df[df['avg_turnover_yi'] >= 10] + logger.info(f"成交额超过10亿概念: {len(high_turnover)} 个") + + # 综合强势板块(满足至少2个条件) + df['strength_score'] = ( + df['is_weekly_positive'].astype(int) + + df['is_20week_high'].astype(int) + + (df['avg_turnover_yi'] >= 10).astype(int) + ) + + # 按强势得分和周涨幅排序 + strong_sectors = df[df['strength_score'] >= 2].sort_values(['strength_score', 'week_change'], ascending=[False, False]) + + # 显示结果 + display_strong_sectors(df, strong_sectors, weekly_positive, new_high_concepts, high_turnover) + + return df + + except Exception as e: + logger.error(f"寻找强势板块失败: {e}") + + +def display_strong_sectors(df, strong_sectors, weekly_positive, new_high_concepts, high_turnover): + """显示强势板块分析结果""" + + print("\n" + "="*100) + print("🔍 强势板块综合分析报告") + print("="*100) + + # 1. 综合强势板块(满足多个条件) + if not strong_sectors.empty: + print(f"\n🚀 综合强势板块TOP10(满足2+条件):") + print(f"{'排名':<4} {'概念名称':<25} {'周涨幅':<10} {'强势分':<8} {'RSI':<8} {'成交额(亿)':<12} {'条件':<20}") + print("-" * 100) + + for i, (_, concept) in enumerate(strong_sectors.head(10).iterrows()): + rank = i + 1 + name = concept['name'][:23] + '..' if len(concept['name']) > 23 else concept['name'] + week_chg = f"{concept['week_change']:+.2f}%" + score = f"{concept['strength_score']}/3" + rsi = f"{concept['rsi']:.1f}" + turnover = f"{concept['avg_turnover_yi']:.1f}" + + conditions = [] + if concept['is_weekly_positive']: + conditions.append("周阳") + if concept['is_20week_high']: + conditions.append("新高") + if concept['avg_turnover_yi'] >= 10: + conditions.append("大额") + + condition_str = "+".join(conditions) + + print(f"{rank:<4} {name:<25} {week_chg:<10} {score:<8} {rsi:<8} {turnover:<12} {condition_str:<20}") + + # 2. 分类展示 + print(f"\n📊 分类统计:") + print(f" 本周收阳: {len(weekly_positive)} 个") + print(f" 20周新高: {len(new_high_concepts)} 个") + print(f" 大成交额: {len(high_turnover)} 个") + print(f" 综合强势: {len(strong_sectors)} 个") + + # 3. 本周收阳TOP10 + if not weekly_positive.empty: + top_weekly = weekly_positive.sort_values('week_change', ascending=False) + print(f"\n📈 本周收阳TOP10:") + for i, (_, concept) in enumerate(top_weekly.head(10).iterrows()): + print(f" {i+1:2d}. {concept['name']}: {concept['week_change']:+.2f}%") + + # 4. 20周新高概念 + if not new_high_concepts.empty: + print(f"\n🎯 20周新高概念TOP10:") + new_high_sorted = new_high_concepts.sort_values('week_change', ascending=False) + for i, (_, concept) in enumerate(new_high_sorted.head(10).iterrows()): + print(f" {i+1:2d}. {concept['name']}: {concept['week_change']:+.2f}% (RSI: {concept['rsi']:.1f})") + + # 5. 大成交额概念 + if not high_turnover.empty: + print(f"\n💰 大成交额概念TOP10:") + turnover_sorted = high_turnover.sort_values('avg_turnover_yi', ascending=False) + for i, (_, concept) in enumerate(turnover_sorted.head(10).iterrows()): + print(f" {i+1:2d}. {concept['name']}: {concept['avg_turnover_yi']:.1f}亿 ({concept['week_change']:+.2f}%)") + + # 6. 技术面强势 + strong_tech = df[(df['rsi'] > 60) & (df['ma20_trend'] > 0)].sort_values('week_change', ascending=False) + if not strong_tech.empty: + print(f"\n📊 技术面强势概念TOP10:") + for i, (_, concept) in enumerate(strong_tech.head(10).iterrows()): + print(f" {i+1:2d}. {concept['name']}: RSI {concept['rsi']:.1f}, MA20趋势 {concept['ma20_trend']:+.2f}%") + + +def main(): + """主函数""" + logger.info("🚀 开始高级强势板块分析...") + + # 初始化Tushare数据获取器 + token = "0ed6419a00d8923dc19c0b58fc92d94c9a0696949ab91a13aa58a0cc" + fetcher = TushareFetcher(token=token) + + # 寻找强势板块 + result_df = find_strong_sectors(fetcher) + + if result_df is not None: + logger.info("✅ 强势板块分析完成!") + else: + logger.error("❌ 强势板块分析失败!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_ths_concepts.py b/test_ths_concepts.py new file mode 100644 index 0000000..cb20414 --- /dev/null +++ b/test_ths_concepts.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +""" +测试同花顺概念板块数据 +""" + +import sys +from pathlib import Path +import pandas as pd +from datetime import datetime, timedelta + +# 添加项目根目录到路径 +current_dir = Path(__file__).parent +sys.path.insert(0, str(current_dir)) + +from src.data.tushare_fetcher import TushareFetcher +from loguru import logger + + +def explore_ths_interfaces(fetcher: TushareFetcher): + """探索同花顺相关接口""" + try: + if not fetcher.pro: + logger.error("需要Tushare Pro权限") + return + + logger.info("🔍 探索同花顺概念板块相关接口...") + + # 1. 获取同花顺概念指数列表 + try: + logger.info("1. 获取同花顺概念指数列表...") + ths_index = fetcher.pro.ths_index(exchange='A', type='N') + logger.info(f"获取到 {len(ths_index)} 个同花顺概念指数") + + if not ths_index.empty: + print("\n📋 同花顺概念指数(前20个):") + print(f"{'代码':<15} {'名称':<30} {'发布日期':<12}") + print("-" * 60) + for _, index in ths_index.head(20).iterrows(): + code = index['ts_code'] + name = index['name'][:28] + '..' if len(index['name']) > 28 else index['name'] + pub_date = index.get('list_date', 'N/A') + print(f"{code:<15} {name:<30} {pub_date:<12}") + + return ths_index + + except Exception as e: + logger.error(f"获取同花顺概念指数失败: {e}") + + # 2. 尝试获取同花顺概念成分股 + try: + logger.info("\n2. 测试获取同花顺概念成分股...") + # 尝试获取一个概念的成分股 + sample_concept = "885311.TI" # 智能电网 + ths_member = fetcher.pro.ths_member(ts_code=sample_concept) + logger.info(f"获取智能电网概念成分股: {len(ths_member)} 只") + + if not ths_member.empty: + print(f"\n📊 智能电网概念成分股(前10只):") + for _, stock in ths_member.head(10).iterrows(): + print(f" {stock['code']}: {stock.get('name', 'N/A')}") + + except Exception as e: + logger.error(f"获取同花顺概念成分股失败: {e}") + + # 3. 尝试获取同花顺概念日行情 + try: + logger.info("\n3. 测试获取同花顺概念日行情...") + today = datetime.now().strftime('%Y%m%d') + yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y%m%d') + + ths_daily = fetcher.pro.ths_daily( + ts_code="885311.TI", # 智能电网 + start_date=yesterday, + end_date=today + ) + logger.info(f"获取智能电网概念日行情: {len(ths_daily)} 条记录") + + if not ths_daily.empty: + print(f"\n📈 智能电网概念近期行情:") + print(ths_daily[['trade_date', 'close', 'pct_chg', 'vol']].head()) + + except Exception as e: + logger.error(f"获取同花顺概念日行情失败: {e}") + + # 4. 探索其他可能的同花顺接口 + try: + logger.info("\n4. 探索同花顺行业分类...") + ths_industry = fetcher.pro.ths_index(exchange='A', type='I') + logger.info(f"获取到 {len(ths_industry)} 个同花顺行业指数") + + if not ths_industry.empty: + print(f"\n📊 同花顺行业指数(前10个):") + for _, index in ths_industry.head(10).iterrows(): + print(f" {index['ts_code']}: {index['name']}") + + except Exception as e: + logger.error(f"获取同花顺行业分类失败: {e}") + + except Exception as e: + logger.error(f"探索同花顺接口失败: {e}") + + +def get_ths_concept_7day_ranking(fetcher: TushareFetcher): + """获取同花顺概念板块过去7个交易日排名""" + try: + if not fetcher.pro: + logger.error("需要Tushare Pro权限") + return + + logger.info("📊 计算同花顺概念板块过去7个交易日涨幅...") + + # 获取同花顺概念指数列表 + ths_concepts = fetcher.pro.ths_index(exchange='A', type='N') + if ths_concepts.empty: + logger.error("未获取到同花顺概念指数") + return + + # 过滤掉指数型板块,只保留真正的概念板块 + logger.info("过滤指数型板块...") + index_keywords = [ + '成份股', '样本股', '成分股', '50', '100', '300', '500', '1000', + '上证', '深证', '中证', '创业板', '科创板', '北证', + 'ETF', '指数', 'Index', '基准' + ] + + def is_concept_plate(name: str) -> bool: + """判断是否为真正的概念板块""" + name_lower = name.lower() + for keyword in index_keywords: + if keyword.lower() in name_lower: + return False + return True + + # 过滤数据 + original_count = len(ths_concepts) + ths_concepts = ths_concepts[ths_concepts['name'].apply(is_concept_plate)] + filtered_count = len(ths_concepts) + + logger.info(f"过滤结果: {original_count} -> {filtered_count} 个概念板块(剔除{original_count - filtered_count}个指数型板块)") + + # 获取过去7个交易日 + trading_dates = [] + current = datetime.now() + + while len(trading_dates) < 7: + if current.weekday() < 5: # 周一到周五 + trading_dates.append(current.strftime('%Y%m%d')) + current -= timedelta(days=1) + + trading_dates.reverse() # 升序排列,最早的日期在前 + + if len(trading_dates) < 2: + logger.warning("交易日不足") + return + + start_date = trading_dates[0] # 7个交易日前 + end_date = trading_dates[-1] # 最新交易日 + + logger.info(f"分析周期: {start_date} 到 {end_date} (过去7个交易日)") + + # 计算各概念的7日涨幅 + concept_performance = [] + + # 限制分析数量,避免API调用过多 + sample_concepts = ths_concepts.head(50) # 分析前50个概念(过滤后数量减少) + logger.info(f"分析前 {len(sample_concepts)} 个同花顺概念...") + + for _, concept in sample_concepts.iterrows(): + ts_code = concept['ts_code'] + name = concept['name'] + + try: + # 获取过去7个交易日行情数据 + daily_data = fetcher.pro.ths_daily( + ts_code=ts_code, + start_date=start_date, + end_date=end_date + ) + + if not daily_data.empty: + # 检查数据结构 + logger.debug(f"{name} 数据字段: {list(daily_data.columns)}") + + if len(daily_data) >= 2: + # 按日期排序 + daily_data = daily_data.sort_values('trade_date') + + start_close = daily_data.iloc[0]['close'] + end_close = daily_data.iloc[-1]['close'] + + if start_close > 0: + period_change = (end_close - start_close) / start_close * 100 + + # 检查涨跌幅字段名 + pct_change_col = None + for col in ['pct_chg', 'pct_change', 'change']: + if col in daily_data.columns: + pct_change_col = col + break + + latest_daily_change = daily_data.iloc[-1][pct_change_col] if pct_change_col else 0 + + concept_performance.append({ + 'ts_code': ts_code, + 'name': name, + 'period_change': period_change, + 'start_close': start_close, + 'end_close': end_close, + 'latest_daily_change': latest_daily_change, + 'trading_days': len(daily_data) + }) + + logger.debug(f"{name}: 过去7日{period_change:+.2f}%") + + except Exception as e: + logger.debug(f"获取 {name} 数据失败: {e}") + continue + + # 显示结果 + if concept_performance: + df_ths = pd.DataFrame(concept_performance) + df_ths = df_ths.sort_values('period_change', ascending=False) + + print(f"\n" + "="*80) + print("📈 同花顺概念板块过去7个交易日涨幅排行榜") + print("="*80) + print(f"{'排名':<4} {'概念名称':<30} {'7日涨幅':<12} {'今日涨幅':<12} {'指数代码':<15}") + print("-" * 80) + + for i, (_, concept) in enumerate(df_ths.iterrows()): + rank = i + 1 + name = concept['name'][:28] + '..' if len(concept['name']) > 28 else concept['name'] + period_chg = f"{concept['period_change']:+.2f}%" + daily_chg = f"{concept['latest_daily_change']:+.2f}%" + ts_code = concept['ts_code'] + + print(f"{rank:<4} {name:<30} {period_chg:<12} {daily_chg:<12} {ts_code:<15}") + + # 强势概念TOP15 + print(f"\n🚀 同花顺强势概念TOP15:") + for i, (_, concept) in enumerate(df_ths.head(15).iterrows()): + print(f" {i+1:2d}. {concept['name']}: {concept['period_change']:+.2f}%") + + # 弱势概念TOP10 + print(f"\n📉 同花顺弱势概念TOP10:") + weak_concepts = df_ths.tail(10).iloc[::-1] # 反转顺序 + for i, (_, concept) in enumerate(weak_concepts.iterrows()): + print(f" {i+1:2d}. {concept['name']}: {concept['period_change']:+.2f}%") + + return df_ths + + else: + logger.warning("未能计算同花顺概念涨幅") + + except Exception as e: + logger.error(f"获取同花顺概念排名失败: {e}") + + +def compare_concept_sources(fetcher: TushareFetcher): + """对比东财和同花顺概念数据""" + try: + logger.info("📊 对比东财 vs 同花顺概念数据...") + + # 获取东财概念数量 + today = datetime.now().strftime('%Y%m%d') + dc_concepts = fetcher.pro.dc_index(trade_date=today) + dc_count = len(dc_concepts) if not dc_concepts.empty else 0 + + # 获取同花顺概念数量 + ths_concepts = fetcher.pro.ths_index(exchange='A', type='N') + ths_count = len(ths_concepts) if not ths_concepts.empty else 0 + + print(f"\n📊 概念板块数据源对比:") + print(f" 东财概念板块: {dc_count} 个") + print(f" 同花顺概念: {ths_count} 个") + + # 数据特点对比 + print(f"\n📈 数据特点对比:") + print(f" 东财概念:") + print(f" - 更新频率: 每日更新") + print(f" - 数据字段: 涨跌幅、市值、上涨下跌股数等") + print(f" - 适用场景: 实时概念轮动分析") + + print(f" 同花顺概念:") + print(f" - 更新频率: 每日更新") + print(f" - 数据字段: 指数价格、涨跌幅、成交量等") + print(f" - 适用场景: 概念指数走势分析") + + except Exception as e: + logger.error(f"对比概念数据源失败: {e}") + + +def main(): + """主函数""" + logger.info("🚀 开始探索同花顺概念板块数据...") + + # 初始化Tushare数据获取器 + token = "0ed6419a00d8923dc19c0b58fc92d94c9a0696949ab91a13aa58a0cc" + fetcher = TushareFetcher(token=token) + + # 1. 探索同花顺接口 + ths_index = explore_ths_interfaces(fetcher) + + print("\n" + "="*80 + "\n") + + # 2. 计算同花顺概念过去7个交易日排名 + get_ths_concept_7day_ranking(fetcher) + + print("\n" + "="*80 + "\n") + + # 3. 对比数据源 + compare_concept_sources(fetcher) + + logger.info("✅ 探索完成!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_tushare_sectors.py b/test_tushare_sectors.py new file mode 100644 index 0000000..010f880 --- /dev/null +++ b/test_tushare_sectors.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +""" +使用Tushare直接获取板块数据的测试 +""" + +import sys +from pathlib import Path +import pandas as pd +from datetime import datetime, timedelta + +# 添加项目根目录到路径 +current_dir = Path(__file__).parent +sys.path.insert(0, str(current_dir)) + +from src.data.tushare_fetcher import TushareFetcher +from loguru import logger + + +def get_concept_sectors(fetcher: TushareFetcher): + """获取概念板块数据""" + try: + if not fetcher.pro: + logger.error("需要Tushare Pro权限") + return + + logger.info("尝试获取概念板块数据...") + + # 1. 尝试获取概念板块列表 + try: + concept_list = fetcher.pro.concept() + logger.info(f"获取到 {len(concept_list)} 个概念板块") + + if not concept_list.empty: + print("概念板块列表(前10个):") + for _, concept in concept_list.head(10).iterrows(): + print(f" {concept['code']}: {concept['name']}") + + except Exception as e: + logger.error(f"获取概念板块列表失败: {e}") + + # 2. 尝试获取同花顺概念指数 + try: + ths_concept = fetcher.pro.ths_index(exchange='A', type='N') + logger.info(f"获取同花顺概念指数: {len(ths_concept)} 个") + + if not ths_concept.empty: + print("\n同花顺概念指数(前10个):") + for _, index in ths_concept.head(10).iterrows(): + print(f" {index['ts_code']}: {index['name']}") + + except Exception as e: + logger.error(f"获取同花顺概念指数失败: {e}") + + # 3. 尝试获取行业指数 + try: + industry_index = fetcher.pro.index_basic(market='SW') + logger.info(f"获取申万行业指数: {len(industry_index)} 个") + + if not industry_index.empty: + print("\n申万行业指数(前10个):") + for _, index in industry_index.head(10).iterrows(): + print(f" {index['ts_code']}: {index['name']}") + + except Exception as e: + logger.error(f"获取申万行业指数失败: {e}") + + except Exception as e: + logger.error(f"获取板块数据失败: {e}") + + +def analyze_hot_concepts(fetcher: TushareFetcher): + """分析热门概念板块""" + try: + if not fetcher.pro: + logger.error("需要Tushare Pro权限") + return + + logger.info("分析热门概念板块...") + + # 获取今日涨跌停统计 + today = datetime.now().strftime('%Y%m%d') + + try: + # 获取涨停股票 + limit_up = fetcher.pro.limit_list(trade_date=today, limit_type='U') + logger.info(f"今日涨停股票: {len(limit_up)} 只") + + if not limit_up.empty: + print(f"\n今日涨停股票(前10只):") + for _, stock in limit_up.head(10).iterrows(): + print(f" {stock['ts_code']}: {stock['name']} (+{stock['pct_chg']:.2f}%)") + + # 分析涨停股票的行业分布 + if 'industry' in limit_up.columns: + industry_counts = limit_up['industry'].value_counts() + print(f"\n涨停股票行业分布:") + for industry, count in industry_counts.head(5).items(): + print(f" {industry}: {count}只") + + except Exception as e: + logger.error(f"获取涨停数据失败: {e}") + + # 获取龙虎榜数据 + try: + top_list = fetcher.pro.top_list(trade_date=today) + logger.info(f"今日龙虎榜: {len(top_list)} 只股票") + + if not top_list.empty: + print(f"\n今日龙虎榜股票(前5只):") + for _, stock in top_list.head(5).iterrows(): + print(f" {stock['ts_code']}: {stock['name']} 净买入: {stock['amount']:.0f}万元") + + except Exception as e: + logger.error(f"获取龙虎榜数据失败: {e}") + + except Exception as e: + logger.error(f"分析热门概念失败: {e}") + + +def get_sector_performance_direct(fetcher: TushareFetcher): + """直接通过指数数据获取板块表现""" + try: + if not fetcher.pro: + logger.error("需要Tushare Pro权限") + return + + logger.info("通过指数数据分析板块表现...") + + # 获取申万一级行业指数 + try: + sw_index = fetcher.pro.index_basic(market='SW', level='L1') + logger.info(f"获取申万一级行业指数: {len(sw_index)} 个") + + if sw_index.empty: + logger.warning("未获取到申万行业指数") + return + + # 获取最近两个交易日的指数行情 + end_date = datetime.now().strftime('%Y%m%d') + start_date = (datetime.now() - timedelta(days=7)).strftime('%Y%m%d') + + sector_performance = [] + + for _, index in sw_index.head(15).iterrows(): # 分析前15个行业 + ts_code = index['ts_code'] + name = index['name'] + + try: + # 获取指数行情 + index_data = fetcher.pro.index_daily( + ts_code=ts_code, + start_date=start_date, + end_date=end_date + ) + + if not index_data.empty and len(index_data) >= 2: + # 计算涨跌幅 + latest = index_data.iloc[0] + previous = index_data.iloc[1] + + change_pct = (latest['close'] - previous['close']) / previous['close'] * 100 + + sector_performance.append({ + 'name': name, + 'code': ts_code, + 'change_pct': change_pct, + 'latest_close': latest['close'], + 'volume': latest['vol'] + }) + + logger.debug(f"{name}: {change_pct:+.2f}%") + + except Exception as e: + logger.debug(f"获取 {name} 指数数据失败: {e}") + continue + + # 输出结果 + if sector_performance: + df = pd.DataFrame(sector_performance) + df = df.sort_values('change_pct', ascending=False) + + print("\n" + "="*60) + print("📈 申万行业指数表现排行") + print("="*60) + print(f"{'排名':<4} {'行业名称':<20} {'涨跌幅':<10} {'最新点位':<10}") + print("-" * 60) + + for i, (_, row) in enumerate(df.iterrows()): + rank = i + 1 + name = row['name'][:18] + '..' if len(row['name']) > 18 else row['name'] + change = f"{row['change_pct']:+.2f}%" + close = f"{row['latest_close']:.2f}" + + print(f"{rank:<4} {name:<20} {change:<10} {close:<10}") + + # 强势行业 + print(f"\n🚀 强势行业TOP5:") + for _, row in df.head(5).iterrows(): + print(f" {row['name']}: {row['change_pct']:+.2f}%") + + # 弱势行业 + print(f"\n📉 弱势行业TOP5:") + for _, row in df.tail(5).iterrows(): + print(f" {row['name']}: {row['change_pct']:+.2f}%") + + except Exception as e: + logger.error(f"获取申万指数失败: {e}") + + except Exception as e: + logger.error(f"分析板块表现失败: {e}") + + +def main(): + """主函数""" + logger.info("测试Tushare板块数据接口...") + + # 初始化Tushare数据获取器 + token = "0ed6419a00d8923dc19c0b58fc92d94c9a0696949ab91a13aa58a0cc" + fetcher = TushareFetcher(token=token) + + # 1. 获取板块分类数据 + get_concept_sectors(fetcher) + + print("\n" + "="*80 + "\n") + + # 2. 分析热门概念 + analyze_hot_concepts(fetcher) + + print("\n" + "="*80 + "\n") + + # 3. 通过指数直接获取板块表现 + get_sector_performance_direct(fetcher) + + logger.info("测试完成!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_weekly_concept_ranking.py b/test_weekly_concept_ranking.py new file mode 100644 index 0000000..50e3e42 --- /dev/null +++ b/test_weekly_concept_ranking.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +""" +按本周总涨幅排名东财概念板块 +""" + +import sys +from pathlib import Path +import pandas as pd +from datetime import datetime, timedelta + +# 添加项目根目录到路径 +current_dir = Path(__file__).parent +sys.path.insert(0, str(current_dir)) + +from src.data.tushare_fetcher import TushareFetcher +from loguru import logger + + +def get_this_week_dates(): + """获取本周的交易日期(周一到周五)""" + today = datetime.now() + # 获取本周一 + monday = today - timedelta(days=today.weekday()) + # 获取本周五(或今天如果还没到周五) + friday = monday + timedelta(days=4) + if friday > today: + friday = today + + # 生成本周所有交易日 + dates = [] + current = monday + while current <= friday: + if current.weekday() < 5: # 周一到周五 + dates.append(current.strftime('%Y%m%d')) + current += timedelta(days=1) + + return dates + + +def calculate_weekly_concept_performance(fetcher: TushareFetcher): + """计算概念板块本周总涨幅""" + try: + if not fetcher.pro: + logger.error("需要Tushare Pro权限") + return None + + logger.info("🚀 计算概念板块本周总涨幅排名...") + + # 获取本周交易日 + week_dates = get_this_week_dates() + logger.info(f"本周交易日: {week_dates}") + + if len(week_dates) < 2: + logger.warning("本周交易日不足,无法计算周涨幅") + return None + + start_date = week_dates[0] # 周一 + end_date = week_dates[-1] # 最新交易日 + + logger.info(f"分析周期: {start_date} 到 {end_date}") + + # 获取周一的概念板块数据(基准) + logger.info(f"获取 {start_date} 的概念数据作为基准...") + start_concepts = fetcher.pro.dc_index(trade_date=start_date) + + # 获取最新交易日的概念板块数据 + logger.info(f"获取 {end_date} 的概念数据...") + end_concepts = fetcher.pro.dc_index(trade_date=end_date) + + if start_concepts.empty or end_concepts.empty: + logger.error("无法获取概念板块数据") + return None + + logger.info(f"周一概念数据: {len(start_concepts)} 个") + logger.info(f"最新概念数据: {len(end_concepts)} 个") + + # 计算本周涨幅 + weekly_performance = [] + + # 以最新数据为准,匹配周一数据 + for _, end_concept in end_concepts.iterrows(): + ts_code = end_concept['ts_code'] + name = end_concept['name'] + end_mv = end_concept['total_mv'] + + # 查找对应的周一数据 + start_data = start_concepts[start_concepts['ts_code'] == ts_code] + + if not start_data.empty: + start_mv = start_data.iloc[0]['total_mv'] + + # 计算本周总涨幅 + if start_mv > 0: + weekly_change = (end_mv - start_mv) / start_mv * 100 + + weekly_performance.append({ + 'ts_code': ts_code, + 'name': name, + 'weekly_change': weekly_change, + 'start_mv': start_mv, + 'end_mv': end_mv, + 'latest_daily_change': end_concept['pct_change'], + 'up_num': end_concept.get('up_num', 0), + 'down_num': end_concept.get('down_num', 0) + }) + + if not weekly_performance: + logger.error("无法计算概念板块周涨幅") + return None + + # 转换为DataFrame并按周涨幅排序 + df_weekly = pd.DataFrame(weekly_performance) + df_weekly = df_weekly.sort_values('weekly_change', ascending=False) + + logger.info(f"成功计算 {len(df_weekly)} 个概念板块的本周涨幅") + + return df_weekly + + except Exception as e: + logger.error(f"计算概念板块周涨幅失败: {e}") + return None + + +def display_weekly_ranking(df_weekly: pd.DataFrame): + """显示本周涨幅排名""" + if df_weekly is None or df_weekly.empty: + logger.error("无数据可显示") + return + + print("\n" + "="*100) + print("📈 东财概念板块本周涨幅排行榜") + print("="*100) + print(f"{'排名':<4} {'概念名称':<25} {'本周涨幅':<12} {'今日涨幅':<12} {'上涨股数':<8} {'下跌股数':<8} {'概念代码':<15}") + print("-" * 100) + + for i, (_, concept) in enumerate(df_weekly.head(30).iterrows()): + rank = i + 1 + name = concept['name'][:23] + '..' if len(concept['name']) > 23 else concept['name'] + weekly_chg = f"{concept['weekly_change']:+.2f}%" + daily_chg = f"{concept['latest_daily_change']:+.2f}%" + up_num = f"{concept['up_num']:.0f}" + down_num = f"{concept['down_num']:.0f}" + ts_code = concept['ts_code'] + + print(f"{rank:<4} {name:<25} {weekly_chg:<12} {daily_chg:<12} {up_num:<8} {down_num:<8} {ts_code:<15}") + + # 强势概念TOP15 + print(f"\n🚀 本周强势概念板块TOP15:") + for i, (_, concept) in enumerate(df_weekly.head(15).iterrows()): + print(f" {i+1:2d}. {concept['name']}: {concept['weekly_change']:+.2f}% (今日{concept['latest_daily_change']:+.2f}%)") + + # 弱势概念TOP10 + print(f"\n📉 本周弱势概念板块TOP10:") + weak_concepts = df_weekly.tail(10).iloc[::-1] # 反转顺序 + for i, (_, concept) in enumerate(weak_concepts.iterrows()): + print(f" {i+1:2d}. {concept['name']}: {concept['weekly_change']:+.2f}% (今日{concept['latest_daily_change']:+.2f}%)") + + # 统计分析 + print(f"\n📊 本周概念板块统计:") + total_concepts = len(df_weekly) + positive_concepts = len(df_weekly[df_weekly['weekly_change'] > 0]) + negative_concepts = len(df_weekly[df_weekly['weekly_change'] < 0]) + + print(f" 总概念数量: {total_concepts}") + print(f" 上涨概念: {positive_concepts} ({positive_concepts/total_concepts*100:.1f}%)") + print(f" 下跌概念: {negative_concepts} ({negative_concepts/total_concepts*100:.1f}%)") + print(f" 平均涨幅: {df_weekly['weekly_change'].mean():+.2f}%") + print(f" 涨幅中位数: {df_weekly['weekly_change'].median():+.2f}%") + + return df_weekly + + +def analyze_top_concepts_detail(fetcher: TushareFetcher, df_weekly: pd.DataFrame, top_n=5): + """分析TOP概念的详细趋势""" + if df_weekly is None or df_weekly.empty: + return + + logger.info(f"详细分析TOP{top_n}强势概念...") + + print(f"\n" + "="*80) + print(f"📊 TOP{top_n}强势概念详细分析") + print("="*80) + + week_dates = get_this_week_dates() + + for i, (_, concept) in enumerate(df_weekly.head(top_n).iterrows()): + concept_code = concept['ts_code'] + concept_name = concept['name'] + + print(f"\n📈 {i+1}. {concept_name} ({concept_code})") + print(f" 本周总涨幅: {concept['weekly_change']:+.2f}%") + + # 获取每日详细数据 + daily_data = [] + for date in week_dates: + try: + daily_concept = fetcher.pro.dc_index( + trade_date=date, + ts_code=concept_code + ) + + if not daily_concept.empty: + daily_data.append({ + 'date': date, + 'pct_change': daily_concept.iloc[0]['pct_change'], + 'total_mv': daily_concept.iloc[0]['total_mv'], + 'up_num': daily_concept.iloc[0].get('up_num', 0), + 'down_num': daily_concept.iloc[0].get('down_num', 0) + }) + + except Exception as e: + logger.debug(f"获取 {concept_code} 在 {date} 的数据失败: {e}") + continue + + # 显示每日走势 + if daily_data: + print(f" 每日走势:") + for data in daily_data: + print(f" {data['date']}: {data['pct_change']:+6.2f}% (上涨{data['up_num']:.0f}只/下跌{data['down_num']:.0f}只)") + + +def main(): + """主函数""" + logger.info("🚀 开始计算东财概念板块本周涨幅排名...") + + # 初始化Tushare数据获取器 + token = "0ed6419a00d8923dc19c0b58fc92d94c9a0696949ab91a13aa58a0cc" + fetcher = TushareFetcher(token=token) + + # 1. 计算本周涨幅 + df_weekly = calculate_weekly_concept_performance(fetcher) + + # 2. 显示排名 + if df_weekly is not None: + display_weekly_ranking(df_weekly) + + # 3. 详细分析TOP5概念 + analyze_top_concepts_detail(fetcher, df_weekly, top_n=5) + + logger.info("✅ 分析完成!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_weekly_sectors.py b/test_weekly_sectors.py new file mode 100644 index 0000000..ac12920 --- /dev/null +++ b/test_weekly_sectors.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +""" +测试使用Tushare找出本周强势板块 +""" + +import sys +from pathlib import Path +import pandas as pd +from datetime import datetime, timedelta + +# 添加项目根目录到路径 +current_dir = Path(__file__).parent +sys.path.insert(0, str(current_dir)) + +from src.data.tushare_fetcher import TushareFetcher +from loguru import logger + + +def get_this_week_dates(): + """获取本周的开始和结束日期""" + today = datetime.now() + # 获取本周一 + monday = today - timedelta(days=today.weekday()) + # 获取本周五(或今天如果还没到周五) + friday = monday + timedelta(days=4) + if friday > today: + friday = today + + return monday.strftime('%Y%m%d'), friday.strftime('%Y%m%d') + + +def analyze_sector_performance(fetcher: TushareFetcher): + """分析行业板块表现""" + try: + logger.info("开始分析本周强势板块...") + + # 获取本周日期范围 + start_date, end_date = get_this_week_dates() + logger.info(f"分析时间范围: {start_date} 到 {end_date}") + + if not fetcher.pro: + logger.error("需要Tushare Pro权限才能获取行业数据") + return + + # 直接使用概念和行业数据分析 + logger.info("通过个股数据分析板块表现...") + + # 获取股票基本信息(包含行业分类) + try: + stock_basic = fetcher.pro.stock_basic( + exchange='', + list_status='L', + fields='ts_code,symbol,name,area,industry,market' + ) + logger.info(f"获取到 {len(stock_basic)} 只股票基本信息") + + except Exception as e: + logger.error(f"获取股票基本信息失败: {e}") + return + + # 按行业分组分析 + industry_performance = {} + + # 取样分析(避免API请求过多) + sample_stocks = stock_basic.sample(min(200, len(stock_basic))) # 随机取200只股票 + logger.info(f"随机抽样 {len(sample_stocks)} 只股票进行分析...") + + for _, stock in sample_stocks.iterrows(): + ts_code = stock['ts_code'] + industry = stock['industry'] + stock_name = stock['name'] + + if pd.isna(industry) or industry == '': + continue + + try: + # 获取个股本周数据 + stock_data = fetcher.pro.daily( + ts_code=ts_code, + start_date=start_date, + end_date=end_date + ) + + if not stock_data.empty and len(stock_data) >= 1: + # 获取最新价格和前一个交易日价格 + if len(stock_data) >= 2: + latest_close = stock_data.iloc[0]['close'] + prev_close = stock_data.iloc[1]['close'] + else: + # 如果只有一天数据,获取开盘价作为对比 + latest_close = stock_data.iloc[0]['close'] + prev_close = stock_data.iloc[0]['open'] + + if prev_close > 0: + change_pct = (latest_close - prev_close) / prev_close * 100 + + # 按行业归类 + if industry not in industry_performance: + industry_performance[industry] = { + 'stock_changes': [], + 'stock_count': 0, + 'stock_names': [] + } + + industry_performance[industry]['stock_changes'].append(change_pct) + industry_performance[industry]['stock_count'] += 1 + industry_performance[industry]['stock_names'].append(f"{stock_name}({change_pct:+.2f}%)") + + logger.debug(f"{stock_name} ({industry}): {change_pct:+.2f}%") + + except Exception as e: + logger.debug(f"分析个股 {ts_code} 失败: {e}") + continue + + # 计算各行业平均表现 + industry_results = [] + for industry, data in industry_performance.items(): + if data['stock_count'] >= 3: # 至少要有3只股票才参与排名 + avg_change = sum(data['stock_changes']) / len(data['stock_changes']) + industry_results.append({ + 'industry_name': industry, + 'avg_change_pct': avg_change, + 'stock_count': data['stock_count'], + 'best_stocks': sorted(data['stock_names'], key=lambda x: float(x.split('(')[1].split('%')[0]), reverse=True)[:3] + }) + + # 3. 分析结果 + if industry_results: + df_performance = pd.DataFrame(industry_results) + + # 按平均涨跌幅排序 + df_performance = df_performance.sort_values('avg_change_pct', ascending=False) + + logger.info("\n" + "="*80) + logger.info("📈 本周强势板块排行榜(基于抽样股票分析)") + logger.info("="*80) + + print(f"{'排名':<4} {'行业名称':<20} {'平均涨跌幅':<12} {'样本数量':<8} {'代表个股':<30}") + print("-" * 80) + + for i, (_, row) in enumerate(df_performance.head(15).iterrows()): + rank = i + 1 + industry_name = row['industry_name'][:18] + '..' if len(row['industry_name']) > 18 else row['industry_name'] + change_pct = f"{row['avg_change_pct']:+.2f}%" + stock_count = f"{row['stock_count']}只" + best_stock = row['best_stocks'][0] if row['best_stocks'] else "无数据" + + print(f"{rank:<4} {industry_name:<20} {change_pct:<12} {stock_count:<8} {best_stock:<30}") + + # 输出强势板块(涨幅前5) + top_sectors = df_performance.head(5) + logger.info(f"\n🚀 本周TOP5强势板块:") + for i, (_, sector) in enumerate(top_sectors.iterrows()): + logger.info(f" {i+1}. {sector['industry_name']}: {sector['avg_change_pct']:+.2f}% (样本{sector['stock_count']}只)") + for j, stock in enumerate(sector['best_stocks'][:3]): + logger.info(f" └─ {stock}") + + # 输出弱势板块(跌幅前5) + weak_sectors = df_performance.tail(5) + logger.info(f"\n📉 本周TOP5弱势板块:") + for i, (_, sector) in enumerate(weak_sectors.iterrows()): + logger.info(f" {i+1}. {sector['industry_name']}: {sector['avg_change_pct']:+.2f}% (样本{sector['stock_count']}只)") + + else: + logger.warning("未获取到有效的行业表现数据") + + except Exception as e: + logger.error(f"分析行业表现失败: {e}") + + +def get_sector_top_stocks(fetcher: TushareFetcher, industry_code: str, industry_name: str, limit: int = 5): + """获取指定板块的强势个股""" + try: + logger.info(f"获取 {industry_name} 板块的强势个股...") + + if not fetcher.pro: + return + + # 获取该行业的成分股 + try: + constituents = fetcher.pro.index_member(index_code=industry_code) + if constituents.empty: + logger.warning(f"{industry_name} 行业无成分股数据") + return + + stock_codes = constituents['con_code'].tolist()[:20] # 取前20只股票测试 + logger.info(f"{industry_name} 行业共 {len(constituents)} 只成分股,分析前 {len(stock_codes)} 只") + + except Exception as e: + logger.error(f"获取 {industry_name} 成分股失败: {e}") + return + + # 获取本周日期 + start_date, end_date = get_this_week_dates() + + # 分析各股票本周表现 + stock_performance = [] + + for stock_code in stock_codes[:10]: # 限制分析数量 + try: + # 获取个股本周数据 + stock_data = fetcher.pro.daily( + ts_code=stock_code, + start_date=start_date, + end_date=end_date + ) + + if not stock_data.empty and len(stock_data) >= 2: + latest_close = stock_data.iloc[0]['close'] + week_start_close = stock_data.iloc[-1]['close'] + week_change = (latest_close - week_start_close) / week_start_close * 100 + + # 获取股票名称 + stock_name = fetcher.get_stock_name(stock_code.split('.')[0]) + + stock_performance.append({ + 'stock_code': stock_code, + 'stock_name': stock_name, + 'week_change_pct': week_change, + 'latest_close': latest_close + }) + + except Exception as e: + logger.debug(f"分析个股 {stock_code} 失败: {e}") + continue + + # 输出该板块强势个股 + if stock_performance: + df_stocks = pd.DataFrame(stock_performance) + df_stocks = df_stocks.sort_values('week_change_pct', ascending=False) + + logger.info(f"\n📊 {industry_name} 板块强势个股 TOP{limit}:") + for _, stock in df_stocks.head(limit).iterrows(): + logger.info(f" {stock['stock_name']} ({stock['stock_code']}): {stock['week_change_pct']:+.2f}%") + + except Exception as e: + logger.error(f"获取 {industry_name} 板块个股失败: {e}") + + +def main(): + """主函数""" + logger.info("开始测试Tushare获取本周强势板块...") + + # 初始化Tushare数据获取器 + token = "0ed6419a00d8923dc19c0b58fc92d94c9a0696949ab91a13aa58a0cc" + fetcher = TushareFetcher(token=token) + + # 分析板块表现 + analyze_sector_performance(fetcher) + + # 分析某个强势板块的个股(示例) + # get_sector_top_stocks(fetcher, "801010.SI", "农林牧渔") + + logger.info("测试完成!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/web/app.py b/web/mysql_app.py similarity index 85% rename from web/app.py rename to web/mysql_app.py index 4bcbac0..bf3e549 100644 --- a/web/app.py +++ b/web/mysql_app.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -AI 智能选股大师 Web 展示界面 -使用Flask框架展示策略筛选结果 +AI 智能选股大师 MySQL版本 Web 展示界面 +使用Flask框架展示策略筛选结果,支持MySQL数据库 """ import sys @@ -15,23 +15,42 @@ current_dir = Path(__file__).parent project_root = current_dir.parent sys.path.insert(0, str(project_root)) -from src.database.database_manager import DatabaseManager +from src.database.mysql_database_manager import MySQLDatabaseManager from src.utils.config_loader import ConfigLoader from loguru import logger app = Flask(__name__) -app.secret_key = 'trading_ai_secret_key_2023' +app.secret_key = 'trading_ai_mysql_secret_key_2023' # 初始化组件 -db_manager = DatabaseManager() +db_manager = MySQLDatabaseManager() config_loader = ConfigLoader() @app.route('/') def index(): - """首页 - 直接跳转到交易信号页面""" - from flask import redirect, url_for - return redirect(url_for('signals')) + """首页 - 显示信号概览和统计""" + try: + # 获取策略统计 + strategy_stats = db_manager.get_strategy_stats() + + # 获取最新信号(前10条) + signals_df = db_manager.get_latest_signals(limit=10) + + # 获取回踩提醒(前5条) + pullback_alerts = db_manager.get_pullback_alerts(days=7) + + # 当前时间 + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + return render_template('index.html', + strategy_stats=strategy_stats.to_dict('records') if not strategy_stats.empty else [], + signals=signals_df.to_dict('records') if not signals_df.empty else [], + pullback_alerts=pullback_alerts.to_dict('records') if not pullback_alerts.empty else [], + current_time=current_time) + except Exception as e: + logger.error(f"首页数据加载失败: {e}") + return render_template('error.html', error=str(e)) @app.route('/signals') @@ -209,8 +228,6 @@ def datetime_format(value, format='%Y-%m-%d %H:%M'): if value is None: return '' - # 使用已导入的时区支持 - if isinstance(value, str): try: # 解析ISO格式时间字符串 @@ -258,10 +275,13 @@ if __name__ == '__main__': logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}") print("=" * 60) - print("🌐 A股量化交易系统 Web 界面") + print("🌐 A股量化交易系统 Web 界面 (MySQL版)") print("=" * 60) print("🚀 启动 Flask 服务器...") print("📊 访问地址: http://localhost:8080") + print("🗄️ 数据库: MySQL") + print(f"📡 主机: {db_manager.config.host}") + print(f"📋 数据库: {db_manager.config.database}") print("=" * 60) app.run(host='0.0.0.0', port=8080, debug=True) \ No newline at end of file diff --git a/web/static/css/style.css b/web/static/css/style.css index 72b1175..6ef0257 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -583,4 +583,531 @@ footer .text-muted { ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); +} + +/* ========== 创新高回踩确认样式 ========== */ + +/* 时间线样式 */ +.timeline-compact { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.timeline-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.125rem 0; +} + +.timeline-item small { + font-size: 0.7rem; + min-width: 45px; + text-align: left; +} + +/* 增强时间线显示 */ +.timeline-enhanced { + display: flex; + flex-direction: column; + gap: 8px; + position: relative; + padding-left: 16px; +} + +.timeline-enhanced::before { + content: ''; + position: absolute; + left: 6px; + top: 8px; + bottom: 8px; + width: 2px; + background: linear-gradient(to bottom, #007bff, #28a745, #ffc107); + border-radius: 1px; +} + +.timeline-step { + display: flex; + align-items: flex-start; + gap: 8px; + position: relative; +} + +.timeline-marker { + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid #fff; + box-shadow: 0 0 0 2px; + flex-shrink: 0; + margin-left: -10px; + background: #fff; + z-index: 1; +} + +.timeline-marker.pattern { + box-shadow: 0 0 0 2px #007bff; + background: #007bff; +} + +.timeline-marker.new-high { + box-shadow: 0 0 0 2px #28a745; + background: #28a745; + animation: pulse-success 2s infinite; +} + +.timeline-marker.confirmation { + box-shadow: 0 0 0 2px #ffc107; + background: #ffc107; + animation: pulse-warning 2s infinite; +} + +@keyframes pulse-success { + 0% { box-shadow: 0 0 0 2px #28a745; } + 50% { box-shadow: 0 0 0 2px #28a745, 0 0 0 6px rgba(40, 167, 69, 0.3); } + 100% { box-shadow: 0 0 0 2px #28a745; } +} + +@keyframes pulse-warning { + 0% { box-shadow: 0 0 0 2px #ffc107; } + 50% { box-shadow: 0 0 0 2px #ffc107, 0 0 0 6px rgba(255, 193, 7, 0.3); } + 100% { box-shadow: 0 0 0 2px #ffc107; } +} + +.timeline-content { + flex: 1; + min-width: 0; +} + +.timeline-date { + font-size: 11px; + color: #6c757d; + font-weight: 500; + margin-bottom: 2px; +} + +.timeline-date.highlight-date { + color: #495057; + font-weight: 600; + background: linear-gradient(135deg, rgba(40, 167, 69, 0.1), rgba(255, 193, 7, 0.1)); + padding: 2px 6px; + border-radius: 4px; + border-left: 3px solid; + border-image: linear-gradient(135deg, #28a745, #ffc107) 1; +} + +.timeline-price { + font-size: 10px; + color: #28a745; + font-weight: 600; + background: rgba(40, 167, 69, 0.1); + padding: 1px 4px; + border-radius: 3px; + display: inline-block; +} + +/* 新的轻量时间线样式 */ +.timeline-enhanced-wide { + background: #fafbfc; + border-radius: 8px; + padding: 12px; + border: 1px solid #e1e5e9; +} + +.timeline-flow { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: nowrap; +} + +.timeline-step-wide { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + min-width: 80px; +} + +.timeline-marker-wide { + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 12px; + margin-bottom: 6px; + box-shadow: 0 1px 3px rgba(0,0,0,0.12); + position: relative; +} + +.pattern-marker { + background: #6c757d; +} + +.new-high-marker { + background: #28a745; +} + +.confirmation-marker { + background: #ffc107; +} + +.timeline-content-wide { + text-align: center; + min-height: 50px; +} + +.timeline-title { + font-size: 11px; + font-weight: 500; + margin-bottom: 3px; + color: #6c757d; + line-height: 1.2; +} + +.timeline-date-wide { + font-size: 13px; + font-weight: 600; + color: #495057; + margin-bottom: 2px; + padding: 1px 4px; + border-radius: 3px; + display: inline-block; +} + +.timeline-date-wide.highlight { + color: #28a745; + font-weight: 600; +} + +.timeline-info { + font-size: 10px; + color: #868e96; + font-weight: 400; +} + +.timeline-arrow { + color: #dee2e6; + font-size: 14px; + opacity: 0.6; +} + +.timeline-summary { + border-top: 1px solid #e9ecef; + padding-top: 8px; + text-align: center; +} + +.timeline-simple { + padding: 12px; + background: #f8f9fa; + border-radius: 8px; + text-align: center; + border: 1px solid #e9ecef; +} + +.tech-indicators-compact { + font-size: 12px; + line-height: 1.5; +} + +/* 小徽章样式 */ +.badge-sm { + font-size: 0.7rem; + padding: 0.25em 0.5em; + font-weight: 500; +} + +/* 表格样式优化 */ +.table { + border-collapse: separate; + border-spacing: 0; +} + +.table thead th { + border-bottom: 1px solid #e9ecef !important; + background-color: #f8f9fa !important; + color: #495057 !important; + font-weight: 500 !important; + font-size: 12px !important; + padding: 10px 12px !important; + border-top: none !important; +} + +.table tbody td { + border-top: 1px solid #f1f3f4 !important; + padding: 12px !important; + vertical-align: middle !important; +} + +.table-hover tbody tr:hover { + background-color: rgba(0, 123, 255, 0.03) !important; +} + +.table-container { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + margin-bottom: 20px; +} + +/* 扫描日期分组标题优化 */ +.scan-date-header { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; + border-left: 4px solid #007bff; + margin-bottom: 0; + border-radius: 0; +} + +.scan-date-header h6 { + font-size: 13px; + color: #495057 !important; +} + +.scan-date-header .badge { + font-size: 0.7rem; + font-weight: 500; +} + +/* 时间线摘要优化 */ +.timeline-summary .badge { + margin-bottom: 3px; + line-height: 1.3; +} + +.timeline-summary .badge-sm { + font-size: 0.65rem; + padding: 0.2em 0.4em; +} + +/* 简化动画效果 */ +@keyframes timeline-flow { + 0%, 100% { + transform: translateX(0); + opacity: 0.6; + } + 50% { + transform: translateX(2px); + opacity: 0.8; + } +} + +.timeline-duration { + font-size: 10px; + color: #17a2b8; + font-weight: 500; + background: rgba(23, 162, 184, 0.1); + padding: 1px 4px; + border-radius: 3px; + display: inline-block; +} + +/* 价格信息样式 */ +.price-info { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.price-info .d-flex { + align-items: center; +} + +.price-info small { + font-size: 0.7rem; + min-width: 50px; +} + +.price-compact { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.price-compact .d-flex { + align-items: center; +} + +.price-compact small { + font-size: 0.7rem; + min-width: 35px; +} + +/* 确认详情样式 */ +.confirmation-details { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.confirmation-details .d-flex { + align-items: center; +} + +.confirmation-details small { + font-size: 0.7rem; + min-width: 60px; +} + +.confirmation-compact { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.confirmation-compact .d-flex { + align-items: center; +} + +.confirmation-compact small { + font-size: 0.7rem; + min-width: 35px; +} + +/* 技术指标样式 */ +.tech-indicators { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.tech-indicators .d-flex { + align-items: center; +} + +.tech-indicators small { + font-size: 0.7rem; + min-width: 50px; +} + +.tech-compact { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.tech-compact .d-flex { + align-items: center; +} + +.tech-compact small { + font-size: 0.7rem; + min-width: 35px; +} + +/* 扫描时间分组标题样式 */ +.scan-date-header { + border-left: 4px solid var(--primary-color); + background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%); + margin-bottom: 0; +} + +.scan-date-header h6 { + margin-bottom: 0; + display: flex; + align-items: center; + justify-content: space-between; +} + +.scan-date-header .badge { + font-size: 0.7rem; + padding: 0.25rem 0.5rem; +} + +/* 新高确认标识 */ +.badge.bg-success { + background: linear-gradient(135deg, var(--success-color) 0%, #059669 100%) !important; + box-shadow: 0 2px 4px rgba(16, 185, 129, 0.2); +} + +.badge.bg-info { + background: linear-gradient(135deg, var(--info-color) 0%, #0891b2 100%) !important; + box-shadow: 0 2px 4px rgba(6, 182, 212, 0.2); +} + +/* 状态指示颜色 */ +.text-primary.fw-bold { + color: var(--primary-color) !important; + font-weight: 600 !important; +} + +.text-success.fw-bold { + color: var(--success-color) !important; + font-weight: 600 !important; +} + +.text-warning.fw-bold { + color: var(--warning-color) !important; + font-weight: 600 !important; +} + +/* 回踩距离颜色编码 */ +.badge.bg-success.small { + background: linear-gradient(135deg, var(--success-color) 0%, #059669 100%) !important; +} + +.badge.bg-warning.small { + background: linear-gradient(135deg, var(--warning-color) 0%, #d97706 100%) !important; +} + +.badge.bg-danger.small { + background: linear-gradient(135deg, var(--danger-color) 0%, #dc2626 100%) !important; +} + +/* 表格行高优化 */ +.table tbody tr { + min-height: 80px; +} + +.table tbody td { + padding: 0.75rem 0.5rem; + vertical-align: top; +} + +/* 响应式优化 */ +@media (max-width: 992px) { + .timeline-compact { + gap: 0.2rem; + } + + .timeline-item small { + font-size: 0.65rem; + min-width: 40px; + } + + .price-info small, + .confirmation-details small, + .tech-indicators small { + font-size: 0.65rem; + min-width: 45px; + } + + .badge.small { + font-size: 0.65rem; + padding: 0.2rem 0.4rem; + } +} + +@media (max-width: 768px) { + .table tbody td { + padding: 0.5rem 0.25rem; + font-size: 0.75rem; + } + + .timeline-item, + .price-info .d-flex, + .confirmation-details .d-flex, + .tech-indicators .d-flex { + gap: 0.25rem; + } + + .badge { + font-size: 0.65rem; + padding: 0.2rem 0.4rem; + } } \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html index cb9a4bc..a254d73 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -87,11 +87,9 @@ 股票 策略 周期 - 信号日期 - 突破价格 - 阴线高点 - 突破幅度 - 实体比例 + 确认信息 + 价格信息 + 技术指标 @@ -104,22 +102,101 @@ - {{ signal.strategy_name }} + {{ signal.strategy_name }} + {% if signal.new_high_confirmed %} +
+ 已确认 +
+ {% endif %} {{ 'D' if signal.timeframe == 'daily' else 'W' }} - {{ signal.signal_date | datetime_format('%m-%d') }} - {{ signal.breakout_price | currency }}元 - {{ signal.yin_high | currency }}元 - - - {{ signal.breakout_pct | percentage }} - + + {% if signal.new_high_confirmed %} + +
+ {% if signal.new_high_date %} +
+ 🚀 创新高: + {{ signal.new_high_date | datetime_format('%m-%d') }} +
+ {% endif %} + {% if signal.confirmation_date %} +
+ ✅ 确认: + {{ signal.confirmation_date | datetime_format('%m-%d') }} +
+ {% endif %} + {% if signal.confirmation_days %} +
+ 用时: + {{ signal.confirmation_days }}天 +
+ {% endif %} + {% if signal.pullback_distance %} +
+ 回踩: + + {{ signal.pullback_distance | currency }}% + +
+ {% endif %} +
+ {% else %} + +
{{ signal.signal_date | datetime_format('%m-%d') }}
+ {% endif %} + + + {% if signal.new_high_confirmed %} + +
+ {% if signal.new_high_price %} +
+ 新高: + {{ signal.new_high_price | currency }} +
+ {% endif %} +
+ 突破: + {{ signal.breakout_price | currency }} +
+
+ 阴高: + {{ signal.yin_high | currency }} +
+
+ {% else %} + +
{{ signal.breakout_price | currency }}元
+
{{ signal.yin_high | currency }}元
+ {% endif %} + + +
+
+ 突破: + + {{ signal.breakout_pct | percentage }} + +
+
+ 实体: + {{ signal.final_yang_entity_ratio | percentage }} +
+ {% if signal.above_ema20 %} +
+ EMA20: + + {{ '✅' if signal.above_ema20 else '❌' }} + +
+ {% endif %} +
- {{ signal.final_yang_entity_ratio | percentage }} {% endfor %} diff --git a/web/templates/signals.html b/web/templates/signals.html index 749f94b..1f7348f 100644 --- a/web/templates/signals.html +++ b/web/templates/signals.html @@ -68,16 +68,10 @@ - - - - - - - - - - + + + + @@ -92,24 +86,102 @@ - - - - - - - {% endfor %}
股票策略周期信号日期突破价格阴线高点突破幅度实体比例换手率扫描时间股票策略周期时间线流程
- {{ signal.strategy_name }} + {{ signal.strategy_name }} + {% if signal.new_high_confirmed %} +
+ 创新高回踩确认 +
+ {% endif %}
{{ '日线' if signal.timeframe == 'daily' else '周线' }} {{ signal.signal_date | datetime_format('%Y-%m-%d') }}{{ signal.breakout_price | currency }}元{{ signal.yin_high | currency }}元 - - {{ signal.breakout_pct | percentage }} - + + {% if signal.new_high_confirmed %} + +
+
+
+
+ +
+
+
📅 模式识别
+
{{ signal.signal_date | datetime_format('%m-%d') }}
+
突破价: {{ signal.breakout_price | currency }}元
+
+
+ +
+ +
+ + {% if signal.new_high_date %} +
+
+ +
+
+
🚀 创新高
+
{{ signal.new_high_date | datetime_format('%m-%d') }}
+ {% if signal.new_high_price %} +
{{ signal.new_high_price | currency }}元
+ {% endif %} +
+
+ +
+ +
+ {% endif %} + + {% if signal.confirmation_date %} +
+
+ +
+
+
✅ 回踩确认
+
{{ signal.confirmation_date | datetime_format('%m-%d') }}
+ {% if signal.confirmation_days %} +
用时{{ signal.confirmation_days }}天
+ {% endif %} +
+
+ {% endif %} +
+ + +
+ + 突破{{ signal.breakout_pct | percentage }} + + + 实体{{ signal.final_yang_entity_ratio | percentage }} + + + EMA20{{ '✅' if signal.above_ema20 else '❌' }} + + {% if signal.pullback_distance %} + + 回踩{{ signal.pullback_distance | currency }}% + + {% endif %} + + {{ signal.scan_time | datetime_format('%m-%d %H:%M') }} + +
+
+ {% else %} + +
+
{{ signal.signal_date | datetime_format('%Y-%m-%d') }}
+
{{ signal.breakout_price | currency }}元
+
+ {% endif %}
{{ signal.final_yang_entity_ratio | percentage }}{{ signal.turnover_ratio | percentage }}{{ signal.scan_time | datetime_format('%m-%d %H:%M') }}