update
This commit is contained in:
parent
32a01b7193
commit
88137ef91f
10
.env.example
Normal file
10
.env.example
Normal file
@ -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
|
||||
367
DOCKER_DEPLOY.md
367
DOCKER_DEPLOY.md
@ -1,367 +0,0 @@
|
||||
# A股量化交易系统 Docker 部署指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本指南将帮助您使用 Docker 快速部署 A股量化交易系统。系统包含 Web 界面、数据采集服务和可选的 Nginx 反向代理。
|
||||
|
||||
## 🔧 环境要求
|
||||
|
||||
- Docker >= 20.10
|
||||
- Docker Compose >= 2.0
|
||||
- 系统内存 >= 2GB
|
||||
- 磁盘空间 >= 5GB
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 克隆项目
|
||||
|
||||
```bash
|
||||
git clone <your-repo-url>
|
||||
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 文件。
|
||||
15
Dockerfile
15
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"]
|
||||
CMD ["python", "start_mysql_web.py"]
|
||||
@ -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)
|
||||
- [ ] 添加用户认证和权限管理
|
||||
- [ ] 实现策略回测结果存储
|
||||
- [ ] 添加图表可视化功能
|
||||
- [ ] 支持策略参数在线调整
|
||||
- [ ] 实现数据导入导出功能
|
||||
178
README_Docker.md
Normal file
178
README_Docker.md
Normal file
@ -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的强大性能!**
|
||||
148
README_MySQL.md
Normal file
148
README_MySQL.md
Normal file
@ -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数据库上,享受更强大的性能和可靠性!**
|
||||
53
clean_mysql.py
Normal file
53
clean_mysql.py
Normal file
@ -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()
|
||||
@ -25,7 +25,12 @@ trading:
|
||||
data:
|
||||
# 数据源配置
|
||||
sources:
|
||||
primary: "adata"
|
||||
primary: "tushare"
|
||||
|
||||
# 数据源配置
|
||||
data_source:
|
||||
# Tushare Pro配置
|
||||
tushare_token: "0ed6419a00d8923dc19c0b58fc92d94c9a0696949ab91a13aa58a0cc"
|
||||
|
||||
# 数据更新频率
|
||||
update_frequency:
|
||||
|
||||
39
config/mysql_config.py
Normal file
39
config/mysql_config.py
Normal file
@ -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()
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
65
install_mysql_deps.py
Normal file
65
install_mysql_deps.py
Normal file
@ -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()
|
||||
7
main.py
7
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
|
||||
|
||||
387
migrate_to_mysql.py
Normal file
387
migrate_to_mysql.py
Normal file
@ -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()
|
||||
@ -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
|
||||
|
||||
@ -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', {})
|
||||
|
||||
@ -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("数据库管理器测试完成")
|
||||
406
src/database/mysql_database_manager.py
Normal file
406
src/database/mysql_database_manager.py
Normal file
@ -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
|
||||
156
src/database/mysql_schema.sql
Normal file
156
src/database/mysql_schema.sql
Normal file
@ -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;
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
# 模拟配置
|
||||
|
||||
@ -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:
|
||||
|
||||
---
|
||||
**💡 操作建议:**
|
||||
- 关注是否在阴线最高点获得有效支撑
|
||||
- 如跌破阴线最高点需要重新评估形态有效性
|
||||
- 建议结合成交量和其他技术指标综合判断
|
||||
- ✅ 这些股票已通过双重确认,信号有效性较高
|
||||
- 🎯 当前为二次回踩阴线最高点,关注支撑强度
|
||||
- 📈 如获得有效支撑,可能形成新的上涨起点
|
||||
- 📉 如跌破阴线最高点,需要重新评估信号有效性
|
||||
- 💰 建议结合成交量和其他技术指标综合判断
|
||||
|
||||
**⚠️ 风险提示:** 本提醒仅供参考,投资需谨慎!
|
||||
**🔍 提醒说明:**
|
||||
- 此类股票已完成创新高+回踩确认流程
|
||||
- 当前价格位置具有重要技术意义
|
||||
- 阴线最高点是关键支撑/阻力位
|
||||
|
||||
**⚠️ 风险提示:** 本提醒仅供参考,投资有风险,入市需谨慎!
|
||||
"""
|
||||
|
||||
# 发送消息
|
||||
|
||||
29
start_mysql_web.py
Normal file
29
start_mysql_web.py
Normal file
@ -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)
|
||||
44
start_web.py
44
start_web.py
@ -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()
|
||||
@ -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()
|
||||
@ -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()
|
||||
268
test_eastmoney_sectors.py
Normal file
268
test_eastmoney_sectors.py
Normal file
@ -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()
|
||||
191
test_optimized_notification.py
Normal file
191
test_optimized_notification.py
Normal file
@ -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()
|
||||
@ -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()
|
||||
313
test_strong_sectors_advanced.py
Normal file
313
test_strong_sectors_advanced.py
Normal file
@ -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()
|
||||
318
test_ths_concepts.py
Normal file
318
test_ths_concepts.py
Normal file
@ -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()
|
||||
238
test_tushare_sectors.py
Normal file
238
test_tushare_sectors.py
Normal file
@ -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()
|
||||
244
test_weekly_concept_ranking.py
Normal file
244
test_weekly_concept_ranking.py
Normal file
@ -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()
|
||||
258
test_weekly_sectors.py
Normal file
258
test_weekly_sectors.py
Normal file
@ -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()
|
||||
@ -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)
|
||||
@ -584,3 +584,530 @@ 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;
|
||||
}
|
||||
}
|
||||
@ -87,11 +87,9 @@
|
||||
<th>股票</th>
|
||||
<th>策略</th>
|
||||
<th>周期</th>
|
||||
<th>信号日期</th>
|
||||
<th>突破价格</th>
|
||||
<th>阴线高点</th>
|
||||
<th>突破幅度</th>
|
||||
<th>实体比例</th>
|
||||
<th>确认信息</th>
|
||||
<th>价格信息</th>
|
||||
<th>技术指标</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -104,22 +102,101 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-light-primary">{{ signal.strategy_name }}</span>
|
||||
<span class="badge bg-primary">{{ signal.strategy_name }}</span>
|
||||
{% if signal.new_high_confirmed %}
|
||||
<div class="mt-1">
|
||||
<small class="badge bg-success">已确认</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if signal.timeframe == 'daily' %}primary{% else %}success{% endif %}">
|
||||
{{ 'D' if signal.timeframe == 'daily' else 'W' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-muted">{{ signal.signal_date | datetime_format('%m-%d') }}</td>
|
||||
<td class="text-success fw-bold">{{ signal.breakout_price | currency }}元</td>
|
||||
<td class="text-muted">{{ signal.yin_high | currency }}元</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if signal.breakout_pct and signal.breakout_pct > 3 %}success{% elif signal.breakout_pct and signal.breakout_pct > 1 %}warning{% else %}secondary{% endif %}">
|
||||
{{ signal.breakout_pct | percentage }}
|
||||
</span>
|
||||
<td style="min-width: 160px;">
|
||||
{% if signal.new_high_confirmed %}
|
||||
<!-- 新格式:时间节点信息 -->
|
||||
<div class="timeline-compact">
|
||||
{% if signal.new_high_date %}
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<small class="text-success fw-bold">🚀 创新高:</small>
|
||||
<span class="small fw-bold text-success">{{ signal.new_high_date | datetime_format('%m-%d') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if signal.confirmation_date %}
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<small class="text-warning fw-bold">✅ 确认:</small>
|
||||
<span class="small fw-bold text-warning">{{ signal.confirmation_date | datetime_format('%m-%d') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if signal.confirmation_days %}
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<small class="text-muted">用时:</small>
|
||||
<span class="badge bg-info small">{{ signal.confirmation_days }}天</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if signal.pullback_distance %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<small class="text-muted">回踩:</small>
|
||||
<span class="badge bg-{% if signal.pullback_distance > -2 %}success{% elif signal.pullback_distance > -5 %}warning{% else %}danger{% endif %} small">
|
||||
{{ signal.pullback_distance | currency }}%
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- 旧格式:兼容显示 -->
|
||||
<div class="text-muted">{{ signal.signal_date | datetime_format('%m-%d') }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="min-width: 120px;">
|
||||
{% if signal.new_high_confirmed %}
|
||||
<!-- 新格式:价格信息 -->
|
||||
<div class="price-compact">
|
||||
{% if signal.new_high_price %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<small class="text-muted">新高:</small>
|
||||
<span class="text-success fw-bold small">{{ signal.new_high_price | currency }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<small class="text-muted">突破:</small>
|
||||
<span class="text-primary small">{{ signal.breakout_price | currency }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<small class="text-muted">阴高:</small>
|
||||
<span class="text-secondary small">{{ signal.yin_high | currency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- 旧格式:兼容显示 -->
|
||||
<div class="text-success fw-bold">{{ signal.breakout_price | currency }}元</div>
|
||||
<div class="text-muted small">{{ signal.yin_high | currency }}元</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="min-width: 120px;">
|
||||
<div class="tech-compact">
|
||||
<div class="d-flex justify-content-between">
|
||||
<small class="text-muted">突破:</small>
|
||||
<span class="badge bg-{% if signal.breakout_pct and signal.breakout_pct > 3 %}success{% elif signal.breakout_pct and signal.breakout_pct > 1 %}warning{% else %}secondary{% endif %} small">
|
||||
{{ signal.breakout_pct | percentage }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<small class="text-muted">实体:</small>
|
||||
<span class="small">{{ signal.final_yang_entity_ratio | percentage }}</span>
|
||||
</div>
|
||||
{% if signal.above_ema20 %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<small class="text-muted">EMA20:</small>
|
||||
<span class="badge bg-{% if signal.above_ema20 %}success{% else %}secondary{% endif %} small">
|
||||
{{ '✅' if signal.above_ema20 else '❌' }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-muted">{{ signal.final_yang_entity_ratio | percentage }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@ -68,16 +68,10 @@
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>股票</th>
|
||||
<th>策略</th>
|
||||
<th>周期</th>
|
||||
<th>信号日期</th>
|
||||
<th>突破价格</th>
|
||||
<th>阴线高点</th>
|
||||
<th>突破幅度</th>
|
||||
<th>实体比例</th>
|
||||
<th>换手率</th>
|
||||
<th>扫描时间</th>
|
||||
<th style="width: 250px;">股票</th>
|
||||
<th style="width: 150px;">策略</th>
|
||||
<th style="width: 100px;">周期</th>
|
||||
<th style="width: 500px;">时间线流程</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -92,24 +86,102 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-light-primary">{{ signal.strategy_name }}</span>
|
||||
<span class="badge bg-primary">{{ signal.strategy_name }}</span>
|
||||
{% if signal.new_high_confirmed %}
|
||||
<div class="mt-1">
|
||||
<small class="badge bg-success">创新高回踩确认</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if signal.timeframe == 'daily' %}primary{% else %}success{% endif %}">
|
||||
{{ '日线' if signal.timeframe == 'daily' else '周线' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-muted">{{ signal.signal_date | datetime_format('%Y-%m-%d') }}</td>
|
||||
<td class="text-success fw-bold">{{ signal.breakout_price | currency }}元</td>
|
||||
<td class="text-muted">{{ signal.yin_high | currency }}元</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if signal.breakout_pct and signal.breakout_pct > 3 %}success{% elif signal.breakout_pct and signal.breakout_pct > 1 %}warning{% else %}secondary{% endif %}">
|
||||
{{ signal.breakout_pct | percentage }}
|
||||
</span>
|
||||
<td style="min-width: 500px; padding: 12px 20px;">
|
||||
{% if signal.new_high_confirmed %}
|
||||
<!-- 新格式:优化的创新高回踩确认时间线 -->
|
||||
<div class="timeline-enhanced-wide">
|
||||
<div class="timeline-flow">
|
||||
<div class="timeline-step-wide">
|
||||
<div class="timeline-marker-wide pattern-marker">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
</div>
|
||||
<div class="timeline-content-wide">
|
||||
<div class="timeline-title">📅 模式识别</div>
|
||||
<div class="timeline-date-wide">{{ signal.signal_date | datetime_format('%m-%d') }}</div>
|
||||
<div class="timeline-info">突破价: {{ signal.breakout_price | currency }}元</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-arrow">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
|
||||
{% if signal.new_high_date %}
|
||||
<div class="timeline-step-wide">
|
||||
<div class="timeline-marker-wide new-high-marker">
|
||||
<i class="fas fa-rocket"></i>
|
||||
</div>
|
||||
<div class="timeline-content-wide">
|
||||
<div class="timeline-title">🚀 创新高</div>
|
||||
<div class="timeline-date-wide highlight">{{ signal.new_high_date | datetime_format('%m-%d') }}</div>
|
||||
{% if signal.new_high_price %}
|
||||
<div class="timeline-info">{{ signal.new_high_price | currency }}元</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-arrow">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if signal.confirmation_date %}
|
||||
<div class="timeline-step-wide">
|
||||
<div class="timeline-marker-wide confirmation-marker">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<div class="timeline-content-wide">
|
||||
<div class="timeline-title">✅ 回踩确认</div>
|
||||
<div class="timeline-date-wide highlight">{{ signal.confirmation_date | datetime_format('%m-%d') }}</div>
|
||||
{% if signal.confirmation_days %}
|
||||
<div class="timeline-info">用时{{ signal.confirmation_days }}天</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 技术指标概要 -->
|
||||
<div class="timeline-summary mt-2">
|
||||
<span class="badge bg-{% if signal.breakout_pct and signal.breakout_pct > 3 %}success{% elif signal.breakout_pct and signal.breakout_pct > 1 %}warning{% else %}secondary{% endif %} me-1 badge-sm">
|
||||
突破{{ signal.breakout_pct | percentage }}
|
||||
</span>
|
||||
<span class="badge bg-{% if signal.final_yang_entity_ratio and signal.final_yang_entity_ratio > 0.6 %}success{% elif signal.final_yang_entity_ratio and signal.final_yang_entity_ratio > 0.4 %}warning{% else %}secondary{% endif %} me-1 badge-sm">
|
||||
实体{{ signal.final_yang_entity_ratio | percentage }}
|
||||
</span>
|
||||
<span class="badge bg-{% if signal.above_ema20 %}success{% else %}secondary{% endif %} me-1 badge-sm">
|
||||
EMA20{{ '✅' if signal.above_ema20 else '❌' }}
|
||||
</span>
|
||||
{% if signal.pullback_distance %}
|
||||
<span class="badge bg-{% if signal.pullback_distance > -2 %}success{% elif signal.pullback_distance > -5 %}warning{% else %}danger{% endif %} me-1 badge-sm">
|
||||
回踩{{ signal.pullback_distance | currency }}%
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="badge bg-light text-muted me-1 badge-sm">
|
||||
{{ signal.scan_time | datetime_format('%m-%d %H:%M') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- 旧格式:兼容显示 -->
|
||||
<div class="timeline-simple">
|
||||
<div class="text-muted">{{ signal.signal_date | datetime_format('%Y-%m-%d') }}</div>
|
||||
<div class="text-success fw-bold">{{ signal.breakout_price | currency }}元</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted">{{ signal.final_yang_entity_ratio | percentage }}</td>
|
||||
<td class="text-muted">{{ signal.turnover_ratio | percentage }}</td>
|
||||
<td class="text-muted small">{{ signal.scan_time | datetime_format('%m-%d %H:%M') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user