This commit is contained in:
aaron 2025-10-01 09:58:52 +08:00
parent 88137ef91f
commit 138a3007fd
42 changed files with 5022 additions and 4452 deletions

View File

@ -7,4 +7,12 @@ MYSQL_DATABASE=tradingai
# Flask应用配置
FLASK_ENV=production
PYTHONPATH=/app
PYTHONPATH=/app
# 市场扫描服务配置
MARKET_SCAN_STOCKS=200
LOG_LEVEL=INFO
# 钉钉通知配置(已启用)
DINGTALK_WEBHOOK_URL=https://oapi.dingtalk.com/robot/send?access_token=50ad2c14e3c8bf7e262ba837dc2a35cb420228ee4165abd69a9e678c901e120e
DINGTALK_SECRET=SEC6e9dbd71d4addd2c4e673fb72d686293b342da5ae48da2f8ec788a68de99f981

4
.gitignore vendored
View File

@ -9,8 +9,8 @@ __pycache__/
.env
.venv
# Runtime data and databases
data/
# Runtime data and databases (exclude source code)
/data/
logs/
*.log
*.db

View File

@ -11,12 +11,13 @@ ENV FLASK_APP=web/mysql_app.py
ENV FLASK_ENV=production
ENV TZ=Asia/Shanghai
# 安装系统依赖包括MySQL客户端库
# 安装系统依赖包括MySQL客户端库和cron
RUN apt-get update && apt-get install -y \
gcc \
g++ \
curl \
tzdata \
cron \
default-libmysqlclient-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*

View File

@ -1,10 +1,10 @@
# A股量化交易系统
一个基于Python的A股市场监控和选股量化程序使用adata数据源。
一个基于Python的A股市场监控和选股量化程序使用TuShare数据源。
## 功能特性
- 📈 **实时数据获取**: 使用adata获取A股实时行情数据
- 📈 **实时数据获取**: 使用TuShare获取A股实时行情数据
- 🔍 **舆情分析**: 北向资金、融资融券、热点股票、龙虎榜数据分析
- 📊 **股票筛选**: 基于技术指标和基本面的智能选股
- 💰 **市场监控**: 实时监控价格变动、成交量异常、资金流向
@ -22,7 +22,7 @@ TradingAI/
├── src/ # 源代码
│ ├── data/ # 数据获取模块
│ │ ├── __init__.py
│ │ ├── data_fetcher.py # 行情数据获取
│ │ ├── tushare_fetcher.py # 行情数据获取
│ │ └── sentiment_fetcher.py # 舆情数据获取
│ ├── strategy/ # 策略模块
│ ├── monitor/ # 监控模块
@ -79,9 +79,9 @@ python main.py
### 获取实时行情
```python
from src.data.data_fetcher import ADataFetcher
from src.data.tushare_fetcher import TushareFetcher
fetcher = ADataFetcher()
fetcher = TushareFetcher()
# 获取单只股票实时数据
data = fetcher.get_realtime_data("000001.SZ")
@ -167,7 +167,7 @@ hist_data = fetcher.get_historical_data(
## 注意事项
1. 首次使用需要确保网络连接正常,adata需要从网络获取数据
1. 首次使用需要确保网络连接正常,TuShare需要从网络获取数据
2. 请合理使用数据接口,避免频繁请求
3. 舆情数据仅供参考,投资需谨慎
4. 本系统仅供学习和研究使用,不构成投资建议

View File

@ -91,12 +91,12 @@ python main.py
### 3. 程序化使用
```python
from src.data.data_fetcher import ADataFetcher
from src.data.tushare_fetcher import TushareFetcher
from src.utils.notification import NotificationManager
from src.strategy.kline_pattern_strategy import KLinePatternStrategy
# 初始化组件
data_fetcher = ADataFetcher()
data_fetcher = TushareFetcher()
notification_manager = NotificationManager(notification_config)
strategy_config = {
@ -153,7 +153,7 @@ market_results = strategy.scan_market(max_stocks=20)
### 1. 数据源
- 优先使用adata真实数据
- 优先使用TuShare真实数据
- 如无法获取真实数据,会生成模拟数据进行测试
- 模拟数据中会人为插入形态信号用于验证策略逻辑
@ -228,7 +228,7 @@ market_results = strategy.scan_market(max_stocks=20)
### 常见问题
1. **无法获取数据**: 检查网络连接和adata配置
1. **无法获取数据**: 检查网络连接和TuShare配置
2. **钉钉通知失败**: 验证webhook地址和安全设置
3. **策略未启用**: 检查配置文件中的 `enabled` 设置
4. **内存占用过高**: 减少 `scan_stocks_count``analysis_days`

174
USAGE.md Normal file
View File

@ -0,0 +1,174 @@
# A股量化交易策略系统
## 项目简介
本项目是一个模块化的A股量化交易策略执行系统支持定时执行多种策略任务主要包括
- 📊 **数据获取**: 基于TuShare的股票数据获取
- 🎯 **股票池管理**: 多种股票筛选规则(热榜、龙头股等)
- 📈 **策略分析**: K线形态识别等技术分析策略
- ⏰ **任务调度**: 灵活的定时任务执行
- 📱 **结果通知**: 多种通知方式支持
## 快速开始
### 启动策略测试系统
```bash
python main.py
```
### 基本命令
```bash
# 分析单只股票
scan 000001.SZ
# 扫描热门股票(默认20只)
market 30
# 查看可用股票池规则
pools
# 执行策略任务
task tushare_hot 15
# 查看定时任务示例
schedule
# 显示帮助
help
# 退出程序
quit
```
## 系统架构
```
数据层: TushareFetcher + StockPoolManager
策略层: BaseStrategy + KLinePatternStrategy
执行层: TaskScheduler + StrategyExecutor
通知层: NotificationManager
```
### 核心组件
- **TushareFetcher**: TuShare API数据获取器
- **StockPoolManager**: 股票池管理器,支持多种筛选规则
- **KLinePatternStrategy**: K线形态识别策略
- **StrategyExecutor**: 策略执行协调器
- **TaskScheduler**: 定时任务调度器
## 股票池规则
- `tushare_hot`: 同花顺热榜
- `combined_hot`: 合并热门(同花顺+东财)
- `leading_stocks`: 龙头牛股
- 支持自定义股票列表
## 策略功能
### K线形态策略
- 识别"两阳线+阴线+阳线"突破形态
- 创新高回踩确认机制
- 多时间周期分析支持
- 回踩监控功能
## 配置文件
主要配置文件位于 `config/config.yaml`
```yaml
# TuShare配置
tushare:
token: "your_token_here"
# 策略配置
strategy:
kline_pattern:
min_entity_ratio: 0.55
final_yang_min_ratio: 0.40
max_turnover_ratio: 40.0
timeframes: ["daily"]
# 通知配置
notification:
dingtalk:
enabled: true
webhook_url: "your_webhook_url"
```
## 定时任务示例
查看 `examples/` 目录下的配置示例:
- `examples/new_architecture_example.py`: 完整架构演示
- `examples/task_config_examples.py`: 常见任务配置
## 使用示例
### 1. 分析单只股票
```bash
> scan 000001.SZ
🔍 分析股票: 000001.SZ
----------------------------------------
📊 DAILY: 发现 1 个信号
1. 2024-01-15 | 两阳+阴+阳突破 | 价格: 12.50元
📈 总计: 1 个信号
```
### 2. 扫描市场热门股票
```bash
> market 20
🌍 扫描市场热门股票 (前20只)
--------------------------------------------------
📊 扫描结果:
股票池: 同花顺热榜
总扫描: 20 只
有信号: 3 只
信号数: 5 个
耗时: 15.23 秒
```
### 3. 执行策略任务
```bash
> task leading_stocks 10
⚡ 执行策略任务
股票池规则: leading_stocks
最大股票数: 10
----------------------------------------
✅ 任务完成:
任务ID: manual_leading_stocks_10
成功: 是
耗时: 8.45 秒
信号数: 2 个
```
## 开发指南
### 添加新策略
1. 继承 `BaseStrategy` 基类
2. 实现 `analyze_stock()` 方法
3. 注册到 `StrategyExecutor`
### 添加新股票池规则
1. 继承 `StockPoolRule` 基类
2. 实现 `get_stocks()` 方法
3. 注册到 `StockPoolManager`
## 注意事项
- 确保配置文件中的TuShare token有效
- 策略分析需要足够的历史数据
- 定时任务需要稳定的网络连接
- 建议在交易时间外进行大量数据扫描
## 许可证
MIT License

20
crontab/market-scanner Normal file
View File

@ -0,0 +1,20 @@
# 市场扫描定时任务配置
# 格式: 分钟 小时 日 月 星期 命令
# 时区: Asia/Shanghai
# 每个工作日开盘前扫描 (09:00)
#0 9 * * 1-5 cd /app && python market_scanner.py 200 >> /app/logs/cron.log 2>&1
# 每个工作日午休时间扫描 (12:30)
#30 12 * * 1-5 cd /app && python market_scanner.py 100 >> /app/logs/cron.log 2>&1
# 每个工作日收盘后扫描 (15:30)
30 15 * * 1-5 cd /app && python market_scanner.py 300 >> /app/logs/cron.log 2>&1
# 每周末进行一次深度扫描 (周六 10:00)
#0 10 * * 6 cd /app && python market_scanner.py 500 >> /app/logs/cron.log 2>&1
# 高频监控 - 每30分钟扫描一次热门股票 (交易时间内: 9:30-15:00)
# 注释掉避免过于频繁,需要时可以开启
# 30 9-14 * * 1-5 cd /app && python market_scanner.py 50 >> /app/logs/cron.log 2>&1
# 0 10-14 * * 1-5 cd /app && python market_scanner.py 50 >> /app/logs/cron.log 2>&1

View File

@ -27,23 +27,26 @@ services:
retries: 3
start_period: 40s
# 数据采集服务(可选)
trading-collector:
# 市场扫描定时任务服务
trading-market-scanner:
build: .
container_name: trading-ai-collector-mysql
container_name: trading-ai-market-scanner
volumes:
- ./config:/app/config
- ./logs:/app/logs
- ./crontab:/app/crontab
environment:
- PYTHONPATH=/app
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- MARKET_SCAN_STOCKS=${MARKET_SCAN_STOCKS:-200}
# 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}
# 运行数据采集脚本
command: ["python", "main.py", "scanmarket"]
# 运行定时市场扫描
command: ["bash", "/app/start_market_scanner.sh"]
restart: unless-stopped
depends_on:
- trading-web

View File

@ -58,7 +58,7 @@ database:
path: "data/trading.db"
data_source:
provider: "adata"
provider: "tushare"
strategy:
kline_pattern:

View File

@ -0,0 +1,206 @@
# 市场扫描Docker定时任务配置
## 概述
本文档描述如何使用Docker容器定时执行`main.py market 200`命令进行股票市场扫描分析。
## 服务组件
### 1. 市场扫描服务 (trading-market-scanner)
- **容器名称**: `trading-ai-market-scanner`
- **功能**: 定时执行股票市场扫描和K线形态分析
- **扫描对象**: 同花顺热榜前N只股票
- **通知**: 支持钉钉webhook通知
### 2. 主要文件
- `market_scanner.py` - 市场扫描脚本
- `start_market_scanner.sh` - 容器启动脚本
- `crontab/market-scanner` - 定时任务配置
- `docker-compose.yml` - Docker编排配置
## 定时任务配置
### 默认调度计划
```bash
# 每个工作日开盘前扫描 (09:00) - 200只股票
0 9 * * 1-5 cd /app && python market_scanner.py 200
# 每个工作日午休时间扫描 (12:30) - 100只股票
30 12 * * 1-5 cd /app && python market_scanner.py 100
# 每个工作日收盘后扫描 (15:30) - 200只股票
30 15 * * 1-5 cd /app && python market_scanner.py 200
# 每周末进行一次深度扫描 (周六 10:00) - 500只股票
0 10 * * 6 cd /app && python market_scanner.py 500
```
### 自定义调度
可通过修改 `crontab/market-scanner` 文件来自定义定时任务:
```bash
# 格式: 分钟 小时 日 月 星期 命令
# 每15分钟扫描一次 (交易时间内)
*/15 9-15 * * 1-5 cd /app && python market_scanner.py 50
# 每小时扫描一次
0 * * * * cd /app && python market_scanner.py 100
```
## 环境变量配置
### 必要配置
| 变量名 | 默认值 | 说明 |
|--------|--------|------|
| `MARKET_SCAN_STOCKS` | 200 | 扫描的股票数量 |
| `LOG_LEVEL` | INFO | 日志级别 |
| `MYSQL_HOST` | - | MySQL主机地址 |
| `MYSQL_PORT` | 26558 | MySQL端口 |
| `MYSQL_USER` | root | MySQL用户名 |
| `MYSQL_PASSWORD` | - | MySQL密码 |
| `MYSQL_DATABASE` | tradingai | 数据库名 |
### 可选配置
| 变量名 | 说明 |
|--------|------|
| `DINGTALK_WEBHOOK_URL` | 钉钉通知webhook地址 |
## 部署步骤
### 1. 环境配置
复制并编辑环境变量文件:
```bash
cp .env.example .env
# 编辑 .env 文件,配置数据库连接和扫描参数
```
### 2. 启动服务
```bash
# 启动市场扫描服务
docker-compose up -d trading-market-scanner
# 查看服务状态
docker-compose ps
# 查看日志
docker-compose logs -f trading-market-scanner
```
### 3. 验证部署
```bash
# 查看容器状态
docker ps | grep trading-ai-market-scanner
# 查看定时任务配置
docker exec trading-ai-market-scanner cat /etc/cron.d/market-scanner
# 查看扫描日志
docker exec trading-ai-market-scanner tail -f /app/logs/market_scanner.log
```
## 日志管理
### 日志文件位置
- `/app/logs/market_scanner.log` - 市场扫描日志
- `/app/logs/cron.log` - Cron执行日志
- `/app/logs/scanner_startup.log` - 服务启动日志
### 日志轮转
- 日志文件按日轮转
- 保留30天历史日志
- 自动压缩旧日志文件
## 监控和运维
### 健康检查
服务包含自动重启机制,如果进程异常退出会自动重启。
### 手动执行扫描
```bash
# 进入容器手动执行扫描
docker exec -it trading-ai-market-scanner python market_scanner.py 100
# 查看扫描结果
docker exec trading-ai-market-scanner tail -20 /app/logs/market_scanner.log
```
### 修改定时任务
1. 编辑 `crontab/market-scanner` 文件
2. 重启容器使配置生效:
```bash
docker-compose restart trading-market-scanner
```
### 调试模式
设置更详细的日志级别:
```bash
# 在 .env 文件中设置
LOG_LEVEL=DEBUG
# 重启服务
docker-compose restart trading-market-scanner
```
## 注意事项
1. **时区设置**: 容器使用 Asia/Shanghai 时区
2. **资源消耗**: 扫描大量股票会消耗较多CPU和内存
3. **网络依赖**: 需要稳定的网络连接访问股票数据API
4. **数据库连接**: 确保MySQL数据库可正常连接
5. **存储空间**: 定期清理日志文件避免磁盘空间不足
## 故障排除
### 常见问题
1. **容器启动失败**
```bash
# 查看启动日志
docker-compose logs trading-market-scanner
```
2. **定时任务不执行**
```bash
# 检查cron服务状态
docker exec trading-ai-market-scanner service cron status
# 查看cron日志
docker exec trading-ai-market-scanner tail -f /app/logs/cron.log
```
3. **数据库连接失败**
```bash
# 检查环境变量配置
docker exec trading-ai-market-scanner env | grep MYSQL
```
4. **内存不足**
```bash
# 减少扫描股票数量
MARKET_SCAN_STOCKS=50
```
### 性能优化
1. 根据服务器性能调整扫描频率和股票数量
2. 设置合理的日志级别避免过多日志输出
3. 定期清理历史数据和日志文件
4. 监控服务器资源使用情况

215
docs/NEW_ARCHITECTURE.md Normal file
View File

@ -0,0 +1,215 @@
# 新架构说明文档
## 概述
本项目已重构为模块化的策略执行系统,支持定时执行多个策略任务。新架构分为四个层次:
1. **数据层**: TushareFetcher + StockPoolManager
2. **策略层**: BaseStrategy + 具体策略实现
3. **执行层**: TaskScheduler + StrategyExecutor
4. **通知层**: NotificationManager
## 核心工作流程
每个策略任务都遵循以下标准流程:
```
1. 根据规则获取股票池 → 2. 传递给策略进行分析 → 3. 以多种方式呈现或通知结果
```
## 模块详解
### 1. 数据层
#### TushareFetcher
- 负责从TuShare API获取股票数据
- 支持历史K线、热榜数据、基本信息等
- 已集成token管理和缓存机制
#### StockPoolManager
- 管理不同的股票池获取规则
- 内置规则:
- `tushare_hot`: 同花顺热榜
- `combined_hot`: 合并热门(同花顺+东财)
- `leading_stocks`: 龙头牛股
- 支持自定义股票列表
### 2. 策略层
#### BaseStrategy (抽象基类)
- 定义统一的策略接口
- 标准化输入输出格式
- 提供股票池批量分析能力
#### KLinePatternStrategy
- 实现K线形态识别策略
- 继承自BaseStrategy
- 支持多时间周期分析
- 集成回踩监控功能
### 3. 执行层
#### StrategyExecutor
- 协调股票池获取和策略分析
- 管理策略注册和执行
- 统一的结果处理和通知
#### TaskScheduler
- 支持多种调度规则
- 任务执行历史和统计
- 灵活的任务管理
### 4. 通知层
#### NotificationManager
- 支持多种通知方式
- 策略结果汇总推送
- 特殊事件提醒
## 快速开始
### 基础使用
```python
# 1. 初始化组件
from src.data.tushare_fetcher import TushareFetcher
from src.data.stock_pool_manager import StockPoolManager
from src.strategy.kline_pattern_strategy import KLinePatternStrategy
from src.execution.strategy_executor import StrategyExecutor
from src.utils.notification import NotificationManager
# 创建实例
fetcher = TushareFetcher()
pool_manager = StockPoolManager(fetcher)
notification_manager = NotificationManager(config)
executor = StrategyExecutor(pool_manager, notification_manager)
# 2. 注册策略
strategy = KLinePatternStrategy(fetcher, notification_manager, config)
executor.register_strategy("kline_pattern", strategy)
# 3. 执行任务
result = executor.execute_task(
task_id="test_task",
strategy_id="kline_pattern",
stock_pool_rule="tushare_hot",
max_stocks=20
)
```
### 定时任务
```python
from src.execution.task_scheduler import TaskScheduler
# 创建调度器
scheduler = TaskScheduler()
# 添加定时任务
task_func = executor.create_task_function(
strategy_id="kline_pattern",
stock_pool_rule="tushare_hot",
max_stocks=30
)
scheduler.add_task(
task_id="morning_scan",
name="晨间扫描",
func=task_func,
schedule_rule="weekdays at 09:00"
)
# 启动调度器
scheduler.start()
```
## 支持的调度规则
- `"every 30 minutes"` - 每30分钟
- `"every 1 hour"` - 每1小时
- `"daily at 09:30"` - 每日09:30
- `"weekdays at 14:00"` - 工作日14:00
- `"monday at 10:00"` - 每周一10:00
## 配置示例
### 策略配置
```python
strategy_config = {
'min_entity_ratio': 0.55,
'final_yang_min_ratio': 0.40,
'max_turnover_ratio': 40.0,
'timeframes': ['daily'],
'pullback_tolerance': 0.02,
'monitor_days': 30
}
```
### 股票池参数
```python
# 同花顺热榜
{"limit": 50}
# 合并热门
{"limit_per_source": 30, "final_limit": 50}
# 龙头股
{"top_boards": 8, "stocks_per_board": 3, "min_score": 70.0}
```
## 扩展指南
### 添加新的股票池规则
1. 继承 `StockPoolRule` 基类
2. 实现 `get_stocks()``get_rule_name()` 方法
3. 注册到 `StockPoolManager`
```python
class MyCustomRule(StockPoolRule):
def get_stocks(self, fetcher, **kwargs):
# 自定义获取逻辑
return stock_list
def get_rule_name(self):
return "我的自定义规则"
# 注册
pool_manager.register_rule("my_rule", MyCustomRule())
```
### 添加新策略
1. 继承 `BaseStrategy` 基类
2. 实现 `analyze_stock()``get_strategy_description()` 方法
3. 注册到 `StrategyExecutor`
```python
class MyStrategy(BaseStrategy):
def analyze_stock(self, stock_code, timeframes=None):
# 实现分析逻辑
return {timeframe: StrategyResult(...)}
def get_strategy_description(self):
return "我的策略描述"
# 注册
executor.register_strategy("my_strategy", MyStrategy(...))
```
## 示例文件
- `examples/new_architecture_example.py` - 完整架构演示
- `examples/task_config_examples.py` - 常见任务配置示例
## 优势总结
**模块化设计**: 职责清晰,易于维护扩展
**灵活配置**: 支持多种股票池和调度规则
**标准化接口**: 统一的策略和结果格式
**任务调度**: 强大的定时执行能力
**统一通知**: 完善的结果推送机制
**缓存优化**: 避免重复API调用
**错误处理**: 完整的异常处理和日志记录
新架构完全满足了原始需求:通过简单配置实现复杂的多策略定时执行和结果通知系统。

View File

@ -0,0 +1,288 @@
#!/usr/bin/env python3
"""
新架构集成示例
展示如何使用重构后的模块化架构进行策略执行和任务调度
"""
import sys
import time
from pathlib import Path
from datetime import datetime
# 添加src目录到路径
current_dir = Path(__file__).parent
src_dir = current_dir.parent / "src"
sys.path.insert(0, str(src_dir))
from loguru import logger
from src.data.tushare_fetcher import TushareFetcher
from src.data.stock_pool_manager import StockPoolManager
from src.strategy.kline_pattern_strategy import KLinePatternStrategy
from src.execution.strategy_executor import StrategyExecutor
from src.execution.task_scheduler import TaskScheduler
from src.utils.notification import NotificationManager
from src.utils.config_loader import config_loader
def demo_new_architecture():
"""演示新架构的完整工作流程"""
print("=" * 80)
print("🚀 新架构演示 - 模块化策略执行系统")
print("=" * 80)
print("📋 系统架构:")
print(" 数据层: TushareFetcher + StockPoolManager")
print(" 策略层: BaseStrategy + KLinePatternStrategy")
print(" 执行层: TaskScheduler + StrategyExecutor")
print(" 通知层: NotificationManager")
print()
# 1. 初始化所有组件
print("📦 第1步: 初始化所有组件")
print("-" * 60)
# 数据层
fetcher = TushareFetcher()
pool_manager = StockPoolManager(fetcher)
# 通知层
notification_config = config_loader.get('notification', {})
notification_manager = NotificationManager(notification_config)
# 策略层
strategy_config = {
'min_entity_ratio': 0.55,
'final_yang_min_ratio': 0.40,
'max_turnover_ratio': 40.0,
'timeframes': ['daily'],
'pullback_tolerance': 0.02,
'monitor_days': 30,
'pullback_confirmation_days': 7
}
kline_strategy = KLinePatternStrategy(
data_fetcher=fetcher,
notification_manager=notification_manager,
config=strategy_config
)
# 执行层
executor = StrategyExecutor(pool_manager, notification_manager)
scheduler = TaskScheduler()
print("✅ 所有组件初始化完成")
print()
# 2. 注册策略到执行器
print("📋 第2步: 注册策略")
print("-" * 60)
executor.register_strategy("kline_pattern", kline_strategy)
print("已注册策略:")
for strategy_id, strategy_name in executor.get_registered_strategies().items():
print(f" {strategy_id}: {strategy_name}")
print()
# 3. 展示股票池规则
print("🎯 第3步: 可用股票池规则")
print("-" * 60)
available_rules = pool_manager.get_available_rules()
print("可用规则:")
for rule_id, rule_name in available_rules.items():
print(f" {rule_id}: {rule_name}")
print()
# 4. 手动执行单个任务
print("⚡ 第4步: 手动执行策略任务")
print("-" * 60)
task_id = f"manual_task_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
print(f"执行任务: {task_id}")
print("参数:")
print(f" 策略: kline_pattern")
print(f" 股票池规则: tushare_hot")
print(f" 最大股票数: 5")
print()
result = executor.execute_task(
task_id=task_id,
strategy_id="kline_pattern",
stock_pool_rule="tushare_hot",
stock_pool_params={"limit": 10},
max_stocks=5,
send_notification=False # 演示时不发送通知
)
# 显示执行结果
print("📊 执行结果摘要:")
summary = result.get_summary()
for key, value in summary.items():
print(f" {key}: {value}")
print()
# 5. 设置定时任务
print("⏰ 第5步: 设置定时任务")
print("-" * 60)
# 创建任务函数
task_function_1 = executor.create_task_function(
strategy_id="kline_pattern",
stock_pool_rule="tushare_hot",
stock_pool_params={"limit": 20},
max_stocks=10,
send_notification=False
)
task_function_2 = executor.create_task_function(
strategy_id="kline_pattern",
stock_pool_rule="combined_hot",
stock_pool_params={"limit_per_source": 15, "final_limit": 25},
max_stocks=15,
send_notification=False
)
# 添加定时任务
scheduler.add_task(
task_id="hot_stocks_scan",
name="同花顺热榜K线形态扫描",
func=task_function_1,
schedule_rule="every 30 minutes",
enabled=False # 演示时不启用
)
scheduler.add_task(
task_id="combined_hot_scan",
name="合并热门股票K线形态扫描",
func=task_function_2,
schedule_rule="daily at 09:30",
enabled=False # 演示时不启用
)
scheduler.add_task(
task_id="leading_stocks_scan",
name="龙头股K线形态扫描",
func=executor.create_task_function(
strategy_id="kline_pattern",
stock_pool_rule="leading_stocks",
stock_pool_params={"top_boards": 5, "stocks_per_board": 2},
max_stocks=10,
send_notification=False
),
schedule_rule="weekdays at 14:30",
enabled=False # 演示时不启用
)
print("已添加定时任务:")
task_status = scheduler.get_task_status()
for task_id, status in task_status.items():
print(f" {task_id}:")
print(f" 名称: {status['name']}")
print(f" 规则: {status['schedule_rule']}")
print(f" 状态: {status['status']}")
print(f" 启用: {status['enabled']}")
print()
# 6. 演示立即执行定时任务
print("🎬 第6步: 演示立即执行定时任务")
print("-" * 60)
print("立即执行任务: hot_stocks_scan")
success = scheduler.execute_task_now("hot_stocks_scan")
print(f"执行结果: {'成功' if success else '失败'}")
print()
# 7. 显示任务执行历史
print("📈 第7步: 任务执行历史")
print("-" * 60)
updated_status = scheduler.get_task_status()
for task_id, status in updated_status.items():
if status['total_executions'] > 0:
print(f"任务: {status['name']}")
print(f" 总执行次数: {status['total_executions']}")
print(f" 成功率: {status['success_rate']:.1f}%")
print(f" 最后执行: {status['last_execution_time']}")
print()
# 8. 系统功能总结
print("🎯 第8步: 新架构优势总结")
print("-" * 60)
advantages = """
模块化设计:
数据获取股票池管理策略执行任务调度完全分离
每个模块职责单一易于维护和扩展
支持插件化添加新的股票池规则和策略
灵活的股票池管理:
支持多种数据源同花顺热榜合并热门龙头股
可配置参数股票数量筛选条件等
易于添加新的股票池规则
标准化的策略接口:
BaseStrategy抽象基类统一策略接口
StrategyResult标准化输出格式
支持多时间周期分析
强大的任务调度:
支持多种调度规则间隔时间每日定时工作日定时
任务执行历史和成功率统计
支持立即执行和定时执行
统一的执行协调:
StrategyExecutor协调股票池获取策略分析结果通知
完整的执行结果记录和统计
支持并发执行多个策略
完善的通知系统:
支持多种通知方式
策略结果汇总推送
回踩提醒等特殊通知
"""
print(advantages)
print("🚀 架构使用示例:")
example_usage = """
# 基础用法 - 立即执行
result = executor.execute_task(
task_id="my_task",
strategy_id="kline_pattern",
stock_pool_rule="tushare_hot",
max_stocks=20
)
# 定时任务 - 每天9:30执行
scheduler.add_task(
task_id="morning_scan",
name="晨间形态扫描",
func=executor.create_task_function(...),
schedule_rule="daily at 09:30"
)
# 启动调度器
scheduler.start()
"""
print(example_usage)
print("=" * 80)
print("🎉 新架构演示完成!")
print("💡 现在可以通过简单配置实现复杂的策略执行和调度")
print("🔧 支持灵活的股票池规则和多策略并行执行")
print("📱 统一的结果通知和监控体系")
print("=" * 80)
if __name__ == "__main__":
# 设置日志
logger.remove()
logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}")
try:
demo_new_architecture()
except Exception as e:
logger.error(f"演示过程中发生错误: {e}")
import traceback
traceback.print_exc()

View File

@ -0,0 +1,304 @@
#!/usr/bin/env python3
"""
任务配置示例
展示如何配置不同类型的定时策略任务
"""
import sys
from pathlib import Path
# 添加src目录到路径
current_dir = Path(__file__).parent
src_dir = current_dir.parent / "src"
sys.path.insert(0, str(src_dir))
from loguru import logger
from src.data.tushare_fetcher import TushareFetcher
from src.data.stock_pool_manager import StockPoolManager
from src.strategy.kline_pattern_strategy import KLinePatternStrategy
from src.execution.strategy_executor import StrategyExecutor
from src.execution.task_scheduler import TaskScheduler
from src.utils.notification import NotificationManager
from src.utils.config_loader import config_loader
def setup_common_tasks():
"""设置常见的策略任务配置"""
print("=" * 80)
print("📋 常见策略任务配置示例")
print("=" * 80)
# 初始化组件
fetcher = TushareFetcher()
pool_manager = StockPoolManager(fetcher)
notification_manager = NotificationManager(config_loader.get('notification', {}))
executor = StrategyExecutor(pool_manager, notification_manager)
scheduler = TaskScheduler()
# 策略配置
strategy_config = {
'min_entity_ratio': 0.55,
'final_yang_min_ratio': 0.40,
'max_turnover_ratio': 40.0,
'timeframes': ['daily'],
'pullback_tolerance': 0.02,
'monitor_days': 30,
'pullback_confirmation_days': 7
}
# 注册策略
kline_strategy = KLinePatternStrategy(
data_fetcher=fetcher,
notification_manager=notification_manager,
config=strategy_config
)
executor.register_strategy("kline_pattern", kline_strategy)
print("🎯 任务配置场景:")
print()
# 场景1: 开盘前扫描热门股票
print("📊 场景1: 开盘前热门股票扫描")
print("-" * 60)
task_1 = executor.create_task_function(
strategy_id="kline_pattern",
stock_pool_rule="tushare_hot",
stock_pool_params={"limit": 50},
max_stocks=30,
send_notification=True
)
scheduler.add_task(
task_id="pre_market_hot_scan",
name="开盘前热门股票K线扫描",
func=task_1,
schedule_rule="weekdays at 09:00",
enabled=False
)
print("✅ 配置完成:")
print(" 任务: 开盘前热门股票K线扫描")
print(" 时间: 每个工作日 09:00")
print(" 股票池: 同花顺热榜前50只")
print(" 分析: 最多30只股票")
print(" 通知: 启用")
print()
# 场景2: 午间龙头股扫描
print("🐲 场景2: 午间龙头股扫描")
print("-" * 60)
task_2 = executor.create_task_function(
strategy_id="kline_pattern",
stock_pool_rule="leading_stocks",
stock_pool_params={
"top_boards": 8,
"stocks_per_board": 3,
"min_score": 70.0
},
max_stocks=20,
send_notification=True
)
scheduler.add_task(
task_id="midday_leading_scan",
name="午间龙头股K线扫描",
func=task_2,
schedule_rule="weekdays at 12:30",
enabled=False
)
print("✅ 配置完成:")
print(" 任务: 午间龙头股K线扫描")
print(" 时间: 每个工作日 12:30")
print(" 股票池: 热门板块前8个每板块前3只评分>70")
print(" 分析: 最多20只龙头股")
print(" 通知: 启用")
print()
# 场景3: 收盘后综合扫描
print("🌅 场景3: 收盘后综合扫描")
print("-" * 60)
task_3 = executor.create_task_function(
strategy_id="kline_pattern",
stock_pool_rule="combined_hot",
stock_pool_params={
"limit_per_source": 30,
"final_limit": 50
},
max_stocks=40,
send_notification=True
)
scheduler.add_task(
task_id="after_market_comprehensive_scan",
name="收盘后综合热门股扫描",
func=task_3,
schedule_rule="weekdays at 15:30",
enabled=False
)
print("✅ 配置完成:")
print(" 任务: 收盘后综合热门股扫描")
print(" 时间: 每个工作日 15:30")
print(" 股票池: 合并热门(同花顺+东财)各取30只合并后50只")
print(" 分析: 最多40只股票")
print(" 通知: 启用")
print()
# 场景4: 高频监控
print("⚡ 场景4: 高频监控扫描")
print("-" * 60)
task_4 = executor.create_task_function(
strategy_id="kline_pattern",
stock_pool_rule="tushare_hot",
stock_pool_params={"limit": 20},
max_stocks=15,
send_notification=False # 高频不通知,避免打扰
)
scheduler.add_task(
task_id="high_freq_monitor",
name="高频热门股监控",
func=task_4,
schedule_rule="every 15 minutes",
enabled=False
)
print("✅ 配置完成:")
print(" 任务: 高频热门股监控")
print(" 时间: 每15分钟执行一次")
print(" 股票池: 同花顺热榜前20只")
print(" 分析: 最多15只股票")
print(" 通知: 关闭(避免频繁打扰)")
print()
# 场景5: 自定义股票池
print("🎯 场景5: 自定义股票池扫描")
print("-" * 60)
# 创建自定义股票池
custom_stocks = [
"000001.SZ", # 平安银行
"000002.SZ", # 万科A
"600000.SH", # 浦发银行
"600036.SH", # 招商银行
"000858.SZ", # 五粮液
"600519.SH", # 贵州茅台
"000725.SZ", # 京东方A
"002415.SZ" # 海康威视
]
pool_manager.create_custom_rule("my_watchlist", custom_stocks)
task_5 = executor.create_task_function(
strategy_id="kline_pattern",
stock_pool_rule="my_watchlist",
stock_pool_params={},
max_stocks=len(custom_stocks),
send_notification=True
)
scheduler.add_task(
task_id="custom_watchlist_scan",
name="自选股K线形态扫描",
func=task_5,
schedule_rule="daily at 21:00",
enabled=False
)
print("✅ 配置完成:")
print(" 任务: 自选股K线形态扫描")
print(" 时间: 每日 21:00")
print(f" 股票池: 自定义股票池({len(custom_stocks)}只)")
print(" 分析: 全部自选股")
print(" 通知: 启用")
print()
# 显示所有任务状态
print("📋 所有配置任务总览:")
print("-" * 60)
task_status = scheduler.get_task_status()
for task_id, status in task_status.items():
print(f"🔹 {status['name']}")
print(f" ID: {task_id}")
print(f" 规则: {status['schedule_rule']}")
print(f" 状态: {status['status']}")
print(f" 启用: {'' if status['enabled'] else ''}")
print()
# 使用指南
print("📖 使用指南:")
print("-" * 60)
print("1. 根据实际需求选择合适的任务配置")
print("2. 调整股票池参数和分析数量")
print("3. 设置合适的执行时间")
print("4. 启用需要的任务: scheduler.enable_task('task_id')")
print("5. 启动调度器: scheduler.start()")
print("6. 立即测试: scheduler.execute_task_now('task_id')")
print()
print("⚙️ 高级配置技巧:")
print("-" * 60)
print("• 开盘前(09:00): 扫描热门股,发现隔夜机会")
print("• 午间时段(12:30): 扫描龙头股,捕捉强势股")
print("• 收盘后(15:30): 综合扫描,总结全天机会")
print("• 高频监控(15分钟): 实时跟踪,但关闭通知")
print("• 晚间复盘(21:00): 扫描自选股,制定明日策略")
print()
return scheduler, executor
def demo_task_execution(scheduler, executor):
"""演示任务执行"""
print("🎬 任务执行演示:")
print("-" * 60)
# 立即执行一个任务进行测试
print("立即执行任务: pre_market_hot_scan")
success = scheduler.execute_task_now("pre_market_hot_scan")
print(f"执行结果: {'成功' if success else '失败'}")
print()
# 显示执行统计
task_status = scheduler.get_task_status()
for task_id, status in task_status.items():
if status['total_executions'] > 0:
print(f"📊 任务: {status['name']}")
print(f" 执行次数: {status['total_executions']}")
print(f" 成功率: {status['success_rate']:.1f}%")
if status['last_execution_time']:
print(f" 最后执行: {status['last_execution_time']}")
print()
print("💡 提示: 在生产环境中启用任务并启动调度器")
print(" scheduler.enable_task('task_id')")
print(" scheduler.start()")
if __name__ == "__main__":
# 设置日志
logger.remove()
logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}")
try:
scheduler, executor = setup_common_tasks()
demo_task_execution(scheduler, executor)
print("=" * 80)
print("🎉 任务配置示例演示完成!")
print("💼 可根据实际需求调整参数和时间规则")
print("🚀 启用任务并启动调度器即可自动运行")
print("=" * 80)
except Exception as e:
logger.error(f"演示过程中发生错误: {e}")
import traceback
traceback.print_exc()

741
main.py
View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""
A股量化交易主程序
A股量化交易 - 策略测试入口
简化版本专注于策略测试和执行
"""
import sys
@ -13,533 +14,313 @@ sys.path.insert(0, str(src_dir))
from loguru import logger
from src.utils.config_loader import config_loader
from src.data.data_fetcher import ADataFetcher
from src.data.sentiment_fetcher import SentimentFetcher
from src.utils.notification import NotificationManager
from src.data.tushare_fetcher import TushareFetcher
from src.data.stock_pool_manager import StockPoolManager
from src.strategy.kline_pattern_strategy import KLinePatternStrategy
from src.database.mysql_database_manager import MySQLDatabaseManager
from src.execution.strategy_executor import StrategyExecutor
from src.execution.task_scheduler import TaskScheduler
from src.utils.notification import NotificationManager
def setup_logging():
"""设置日志配置"""
log_config = config_loader.get_logging_config()
# 移除默认的控制台日志
"""设置简化的日志配置"""
logger.remove()
# 添加控制台输出
logger.add(
sys.stdout,
level=log_config.get('level', 'INFO'),
format=log_config.get('format', '{time} | {level} | {message}')
level="INFO",
format="{time:HH:mm:ss} | {level} | {message}"
)
# 添加文件输出
log_file = Path(log_config.get('file_path', 'logs/trading.log'))
log_file.parent.mkdir(parents=True, exist_ok=True)
logger.add(
log_file,
level=log_config.get('level', 'INFO'),
format=log_config.get('format', '{time} | {level} | {message}'),
rotation=log_config.get('rotation', '1 day'),
retention=log_config.get('retention', '30 days')
def create_strategy_system():
"""创建策略系统的所有组件"""
# 数据层
fetcher = TushareFetcher()
pool_manager = StockPoolManager(fetcher)
# 通知层
notification_config = config_loader.get('notification', {})
notification_manager = NotificationManager(notification_config)
# 策略层
strategy_config = config_loader.get('strategy', {}).get('kline_pattern', {
'min_entity_ratio': 0.55,
'final_yang_min_ratio': 0.40,
'max_turnover_ratio': 40.0,
'timeframes': ['daily'],
'pullback_tolerance': 0.02,
'monitor_days': 30,
'pullback_confirmation_days': 7
})
# 数据库层
from src.database.mysql_database_manager import MySQLDatabaseManager
db_manager = MySQLDatabaseManager()
kline_strategy = KLinePatternStrategy(
data_fetcher=fetcher,
notification_manager=notification_manager,
config=strategy_config,
db_manager=db_manager
)
logger.info("日志系统初始化完成")
# 执行层
executor = StrategyExecutor(pool_manager, notification_manager)
scheduler = TaskScheduler()
# 注册策略
executor.register_strategy("kline_pattern", kline_strategy)
def main():
"""主函数"""
print("="*60)
print(" A股量化交易系统")
print("="*60)
try:
# 初始化日志
setup_logging()
# 加载配置
config = config_loader.load_config()
logger.info("配置文件加载成功")
# 初始化数据获取器
data_fetcher = ADataFetcher()
sentiment_fetcher = SentimentFetcher()
# 初始化MySQL数据库管理器
db_manager = MySQLDatabaseManager()
logger.info("MySQL数据库管理器初始化完成")
# 初始化通知管理器
notification_config = config.get('notification', {})
notification_manager = NotificationManager(notification_config)
# 初始化K线形态策略
strategy_config = config.get('strategy', {}).get('kline_pattern', {})
if strategy_config.get('enabled', False):
kline_strategy = KLinePatternStrategy(data_fetcher, notification_manager, strategy_config, db_manager)
logger.info("K线形态策略已启用")
else:
kline_strategy = None
logger.info("K线形态策略未启用")
# 显示系统信息
logger.info("系统启动成功")
print("\n系统功能:")
print("1. 数据获取 - 实时行情、历史数据、财务数据")
print("2. 舆情分析 - 北向资金、融资融券、热点股票、龙虎榜")
print("3. K线形态策略 - 两阳线+阴线+阳线突破形态识别")
print("4. 股票筛选 - 基于技术指标和基本面的选股")
print("5. 实时监控 - 价格变动、成交量异常监控")
print("6. 策略回测 - 历史数据验证交易策略")
# 获取市场概况
print("\n正在获取市场概况...")
market_overview = data_fetcher.get_market_overview()
if market_overview:
print(f"\n市场概况 (更新时间: {market_overview.get('update_time', 'N/A')}):")
for market, data in market_overview.items():
if market != 'update_time' and isinstance(data, dict):
price = data.get('close', data.get('current', 'N/A'))
change = data.get('change', 'N/A')
change_pct = data.get('change_pct', 'N/A')
print(f" {market.upper()}: 价格={price}, 涨跌={change}, 涨跌幅={change_pct}%")
print("\n系统就绪,等待指令...")
print("输入 'help' 查看帮助,输入 'quit' 退出程序")
# 简单的交互式命令行
while True:
try:
command = input("\n> ").strip().lower()
if command == 'quit' or command == 'exit':
print("感谢使用A股量化交易系统")
break
elif command == 'help':
print_help()
elif command == 'status':
print_system_status()
elif command.startswith('search '):
keyword = command[7:] # 移除'search '
search_stocks(data_fetcher, keyword)
elif command == 'market':
show_market_overview(data_fetcher)
elif command == 'sentiment':
show_market_sentiment(sentiment_fetcher)
elif command == 'hotstock':
show_hot_stocks(sentiment_fetcher)
elif command == 'northflow':
show_north_flow(sentiment_fetcher)
elif command == 'dragon':
show_dragon_tiger_list(sentiment_fetcher)
elif command.startswith('analyze '):
stock_code = command[8:] # 移除'analyze '
analyze_stock_sentiment(sentiment_fetcher, stock_code)
elif command == 'strategy':
show_strategy_info(kline_strategy)
elif command.startswith('scan '):
stock_code = command[5:] # 移除'scan '
scan_single_stock(kline_strategy, stock_code)
elif command == 'scanmarket':
scan_market_patterns(kline_strategy)
elif command == 'testnotify':
test_notification(notification_manager)
else:
print("未知命令,输入 'help' 查看帮助")
except KeyboardInterrupt:
print("\n\n程序被用户中断")
break
except Exception as e:
logger.error(f"命令执行错误: {e}")
print(f"执行错误: {e}")
except Exception as e:
logger.error(f"程序启动失败: {e}")
print(f"启动失败: {e}")
sys.exit(1)
return {
'fetcher': fetcher,
'pool_manager': pool_manager,
'notification_manager': notification_manager,
'kline_strategy': kline_strategy,
'executor': executor,
'scheduler': scheduler
}
def print_help():
"""打印帮助信息"""
print("\n可用命令:")
print(" help - 显示此帮助信息")
print(" status - 显示系统状态")
print(" market - 显示市场概况")
print(" search <关键词> - 搜索股票")
print(" sentiment - 显示市场舆情综合概览")
print(" hotstock - 显示热门股票排行")
print(" northflow - 显示北向资金流向")
print(" dragon - 显示龙虎榜数据")
print(" analyze <股票代码> - 分析单只股票舆情")
print(" strategy - 显示K线形态策略信息")
print(" scan <股票代码> - 扫描单只股票K线形态")
print(" scanmarket - 扫描市场K线形态")
print(" testnotify - 测试通知功能")
print(" quit/exit - 退出程序")
print("\n🚀 策略测试命令:")
print("-" * 50)
print(" scan <股票代码> - 分析单只股票")
print(" market <数量> - 扫描热门股票(默认20只)")
print(" pools - 查看可用股票池规则")
print(" task <规则> <数量> - 执行策略任务")
print(" schedule - 显示定时任务示例")
print(" help - 显示帮助")
print(" quit - 退出程序")
print("-" * 50)
def print_system_status():
"""显示系统状态"""
config = config_loader.config
print("\n系统状态:")
print(f" 配置文件: 已加载")
print(f" 数据源: {config.get('data', {}).get('sources', {}).get('primary', 'N/A')}")
print(f" 日志级别: {config.get('logging', {}).get('level', 'N/A')}")
print(f" 实时监控: {'启用' if config.get('monitor', {}).get('realtime', {}).get('enabled', False) else '禁用'}")
def main():
"""主函数"""
setup_logging()
def search_stocks(data_fetcher: ADataFetcher, keyword: str):
"""搜索股票"""
if not keyword:
print("请提供搜索关键词")
return
print(f"\n搜索股票: {keyword}")
results = data_fetcher.search_stocks(keyword)
if not results.empty:
print(f"找到 {len(results)} 个结果:")
for idx, row in results.head(10).iterrows(): # 只显示前10个结果
code = row.get('stock_code', 'N/A')
name = row.get('short_name', 'N/A')
print(f" {code} - {name}")
if len(results) > 10:
print(f" ... 还有 {len(results) - 10} 个结果")
else:
print("未找到匹配的股票")
def show_market_overview(data_fetcher: ADataFetcher):
"""显示市场概况"""
print("\n正在获取最新市场数据...")
overview = data_fetcher.get_market_overview()
if overview:
print(f"\n市场概况 (更新时间: {overview.get('update_time', 'N/A')}):")
for market, data in overview.items():
if market != 'update_time' and isinstance(data, dict):
price = data.get('close', data.get('current', 'N/A'))
change = data.get('change', 'N/A')
change_pct = data.get('change_pct', 'N/A')
volume = data.get('volume', 'N/A')
print(f" {market.upper()}: 价格={price}, 涨跌={change}, 涨跌幅={change_pct}%, 成交量={volume}")
else:
print("无法获取市场数据")
def show_market_sentiment(sentiment_fetcher: SentimentFetcher):
"""显示市场舆情综合概览"""
print("\n正在获取市场舆情数据...")
overview = sentiment_fetcher.get_market_sentiment_overview()
if overview:
print(f"\n市场舆情综合概览 (更新时间: {overview.get('update_time', 'N/A')}):")
# 北向资金
if 'north_flow' in overview:
north_data = overview['north_flow']
print(f"\n📊 北向资金:")
print(f" 总净流入: {north_data.get('net_total', 'N/A')} 万元")
print(f" 沪股通: {north_data.get('net_hgt', 'N/A')} 万元")
print(f" 深股通: {north_data.get('net_sgt', 'N/A')} 万元")
print(f" 更新时间: {north_data.get('update_time', 'N/A')}")
# 融资融券
if 'latest_margin' in overview:
margin_data = overview['latest_margin']
print(f"\n📈 融资融券:")
print(f" 融资余额: {margin_data.get('rzye', 'N/A')} 亿元")
print(f" 融券余额: {margin_data.get('rqye', 'N/A')} 亿元")
print(f" 两融余额: {margin_data.get('rzrqye', 'N/A')} 亿元")
# 热门股票前5名
if 'hot_stocks_east' in overview and not overview['hot_stocks_east'].empty:
print(f"\n🔥 东财热门股票TOP5:")
for idx, row in overview['hot_stocks_east'].head(5).iterrows():
code = row.get('stock_code', 'N/A')
name = row.get('short_name', 'N/A')
rank = row.get('rank', idx + 1)
print(f" {rank}. {code} - {name}")
# 热门概念前5名
if 'hot_concepts' in overview and not overview['hot_concepts'].empty:
print(f"\n💡 热门概念TOP5:")
for idx, row in overview['hot_concepts'].head(5).iterrows():
name = row.get('concept_name', 'N/A')
change_pct = row.get('change_pct', 'N/A')
rank = row.get('rank', idx + 1)
print(f" {rank}. {name} (涨跌幅: {change_pct}%)")
# 龙虎榜前3名
if 'dragon_tiger' in overview and not overview['dragon_tiger'].empty:
print(f"\n🐉 今日龙虎榜TOP3:")
for idx, row in overview['dragon_tiger'].head(3).iterrows():
code = row.get('stock_code', 'N/A')
name = row.get('short_name', 'N/A')
reason = row.get('reason', 'N/A')
print(f" {idx + 1}. {code} - {name} ({reason})")
else:
print("无法获取市场舆情数据")
def show_hot_stocks(sentiment_fetcher: SentimentFetcher):
"""显示热门股票排行"""
print("\n正在获取热门股票数据...")
# 东财人气股票
east_stocks = sentiment_fetcher.get_popular_stocks_east_100()
if not east_stocks.empty:
print(f"\n🔥 东财人气股票TOP10:")
for idx, row in east_stocks.head(10).iterrows():
code = row.get('stock_code', 'N/A')
name = row.get('short_name', 'N/A')
rank = row.get('rank', idx + 1)
change_pct = row.get('change_pct', 'N/A')
print(f" {rank}. {code} - {name} (涨跌幅: {change_pct}%)")
# 同花顺热门股票
ths_stocks = sentiment_fetcher.get_hot_stocks_ths_100()
if not ths_stocks.empty:
print(f"\n🌟 同花顺热门股票TOP10:")
for idx, row in ths_stocks.head(10).iterrows():
code = row.get('stock_code', 'N/A')
name = row.get('short_name', 'N/A')
rank = row.get('rank', idx + 1)
change_pct = row.get('change_pct', 'N/A')
print(f" {rank}. {code} - {name} (涨跌幅: {change_pct}%)")
def show_north_flow(sentiment_fetcher: SentimentFetcher):
"""显示北向资金流向"""
print("\n正在获取北向资金数据...")
# 当前流向
current_flow = sentiment_fetcher.get_north_flow_current()
if not current_flow.empty:
print(f"\n💰 当前北向资金流向:")
for idx, row in current_flow.iterrows():
net_total = row.get('net_tgt', 'N/A')
net_hgt = row.get('net_hgt', 'N/A')
net_sgt = row.get('net_sgt', 'N/A')
trade_time = row.get('trade_time', 'N/A')
print(f" 总净流入: {net_total} 万元")
print(f" 沪股通: {net_hgt} 万元")
print(f" 深股通: {net_sgt} 万元")
print(f" 更新时间: {trade_time}")
break # 只显示第一行数据
# 历史流向最近5天
hist_flow = sentiment_fetcher.get_north_flow_history()
if not hist_flow.empty:
print(f"\n📊 最近5天北向资金流向:")
for idx, row in hist_flow.tail(5).iterrows():
date = row.get('trade_date', 'N/A')
net_total = row.get('net_tgt', 'N/A')
print(f" {date}: {net_total} 万元")
def show_dragon_tiger_list(sentiment_fetcher: SentimentFetcher):
"""显示龙虎榜数据"""
print("\n正在获取龙虎榜数据...")
dragon_tiger = sentiment_fetcher.get_dragon_tiger_list_daily()
if not dragon_tiger.empty:
print(f"\n🐉 今日龙虎榜 (共{len(dragon_tiger)}只股票):")
for idx, row in dragon_tiger.head(15).iterrows(): # 显示前15个
code = row.get('stock_code', 'N/A')
name = row.get('short_name', 'N/A')
reason = row.get('reason', 'N/A')
change_pct = row.get('change_pct', 'N/A')
amount = row.get('amount', 'N/A')
print(f" {idx + 1}. {code} - {name}")
print(f" 上榜原因: {reason}")
print(f" 涨跌幅: {change_pct}%, 成交金额: {amount} 万元")
if len(dragon_tiger) > 15:
print(f" ... 还有 {len(dragon_tiger) - 15} 只股票")
else:
print("今日暂无龙虎榜数据")
def analyze_stock_sentiment(sentiment_fetcher: SentimentFetcher, stock_code: str):
"""分析单只股票舆情"""
if not stock_code:
print("请提供股票代码")
return
print(f"\n正在分析股票 {stock_code} 的舆情情况...")
analysis = sentiment_fetcher.analyze_stock_sentiment(stock_code)
if 'error' in analysis:
print(f"分析失败: {analysis['error']}")
return
print(f"\n📊 {stock_code} 舆情分析报告:")
print(f"更新时间: {analysis.get('update_time', 'N/A')}")
# 热度情况
print(f"\n🔥 热度情况:")
print(f" 东财人气榜: {'在榜' if analysis.get('in_popular_east', False) else '不在榜'}")
print(f" 同花顺热门榜: {'在榜' if analysis.get('in_hot_ths', False) else '不在榜'}")
# 龙虎榜情况
if 'dragon_tiger' in analysis and not analysis['dragon_tiger'].empty:
print(f"\n🐉 龙虎榜情况:")
dragon_data = analysis['dragon_tiger'].iloc[0]
reason = dragon_data.get('reason', 'N/A')
amount = dragon_data.get('amount', 'N/A')
print(f" 上榜原因: {reason}")
print(f" 成交金额: {amount} 万元")
else:
print(f"\n🐉 龙虎榜情况: 今日未上榜")
# 风险扫描
if 'risk_scan' in analysis and not analysis['risk_scan'].empty:
print(f"\n⚠️ 风险扫描:")
risk_data = analysis['risk_scan'].iloc[0]
risk_level = risk_data.get('risk_level', 'N/A')
risk_desc = risk_data.get('risk_desc', 'N/A')
print(f" 风险等级: {risk_level}")
print(f" 风险描述: {risk_desc}")
else:
print(f"\n⚠️ 风险扫描: 暂无数据")
def show_strategy_info(kline_strategy: KLinePatternStrategy):
"""显示K线形态策略信息"""
if kline_strategy is None:
print("K线形态策略未启用")
return
print("\n" + "="*60)
print(" K线形态策略信息")
print("="*60)
print(kline_strategy.get_strategy_summary())
def scan_single_stock(kline_strategy: KLinePatternStrategy, stock_code: str):
"""扫描单只股票K线形态"""
if kline_strategy is None:
print("K线形态策略未启用")
return
if not stock_code:
print("请提供股票代码")
return
print(f"\n正在扫描股票 {stock_code} 的K线形态...")
print("=" * 60)
print("🎯 A股量化交易 - 策略测试系统")
print("=" * 60)
try:
results = kline_strategy.analyze_stock(stock_code)
# 初始化系统
logger.info("正在初始化策略系统...")
system = create_strategy_system()
logger.info("✅ 策略系统初始化完成")
print(f"\n📊 {stock_code} K线形态分析结果:")
total_signals = 0
print(f"\n📊 系统组件:")
print(f" ✅ 数据获取器: TushareFetcher")
print(f" ✅ 股票池管理: StockPoolManager")
print(f" ✅ K线策略: KLinePatternStrategy")
print(f" ✅ 执行器: StrategyExecutor")
print(f" ✅ 调度器: TaskScheduler")
for timeframe, signals in results.items():
print(f"\n{timeframe.upper()} 时间周期:")
if signals:
for i, signal in enumerate(signals, 1):
print(f" 信号 {i}:")
print(f" 日期: {signal['date']}")
print(f" 形态: {signal['pattern_type']}")
print(f" 突破价格: {signal['breakout_price']:.2f}")
print(f" 突破幅度: {signal['breakout_pct']:.2f}%")
print(f" 阳线1实体比例: {signal['yang1_entity_ratio']:.1%}")
print(f" 阳线2实体比例: {signal['yang2_entity_ratio']:.1%}")
print(f" EMA20价格: {signal['ema20_price']:.2f}")
print(f" EMA20状态: {'✅ 上方' if signal['above_ema20'] else '❌ 下方'}")
print(f" 换手率: {signal.get('turnover_ratio', 0):.2f}%")
total_signals += len(signals)
print(f" 共发现 {len(signals)} 个信号")
else:
print(" 未发现形态信号")
print_help()
print(f"\n总计发现 {total_signals} 个信号")
# 命令行交互
while True:
try:
command = input("\n> ").strip()
if not command:
continue
if command.lower() in ['quit', 'exit']:
print("👋 感谢使用策略测试系统!")
break
elif command.lower() == 'help':
print_help()
elif command.startswith('scan '):
stock_code = command[5:].strip()
if stock_code:
scan_single_stock(system['kline_strategy'], stock_code)
else:
print("请提供股票代码,如: scan 000001.SZ")
elif command.startswith('market'):
parts = command.split()
max_stocks = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 20
scan_market(system['executor'], max_stocks)
elif command.lower() == 'pools':
show_stock_pools(system['pool_manager'])
elif command.startswith('task '):
parts = command.split()
if len(parts) >= 2:
rule = parts[1]
max_stocks = int(parts[2]) if len(parts) > 2 and parts[2].isdigit() else 10
execute_task(system['executor'], rule, max_stocks)
else:
print("请提供股票池规则,如: task tushare_hot 15")
elif command.lower() == 'schedule':
show_schedule_examples(system['scheduler'], system['executor'])
else:
print("❌ 未知命令,输入 'help' 查看帮助")
except KeyboardInterrupt:
print("\n\n👋 程序被用户中断")
break
except Exception as e:
logger.error(f"命令执行错误: {e}")
print(f"❌ 执行错误: {e}")
except Exception as e:
logger.error(f"扫描股票失败: {e}")
print(f"扫描失败: {e}")
logger.error(f"系统启动失败: {e}")
print(f"❌ 启动失败: {e}")
return 1
return 0
def scan_market_patterns(kline_strategy: KLinePatternStrategy):
"""扫描市场K线形态"""
if kline_strategy is None:
print("K线形态策略未启用")
return
print("\n开始扫描市场K线形态...")
print("⚠️ 注意: 这可能需要较长时间,请耐心等待")
def scan_single_stock(strategy, stock_code):
"""扫描单只股票"""
print(f"\n🔍 分析股票: {stock_code}")
print("-" * 40)
try:
# 获取扫描股票数量配置
scan_count = kline_strategy.config.get('scan_stocks_count', 20)
print(f"扫描股票数量: {scan_count}")
results = strategy.analyze_stock(stock_code)
results = kline_strategy.scan_market(max_stocks=scan_count)
total_signals = 0
for timeframe, result in results.items():
signal_count = result.get_signal_count()
total_signals += signal_count
if results:
print(f"\n📈 市场扫描结果 (发现 {len(results)} 只股票有信号):")
if signal_count > 0:
print(f"📊 {timeframe.upper()}: 发现 {signal_count} 个信号")
for i, signal in enumerate(result.signals, 1):
print(f" {i}. {signal['date']} | {signal['signal_type']} | 价格: {signal['price']:.2f}")
else:
print(f"📭 {timeframe.upper()}: 无信号")
for stock_code, stock_results in results.items():
total_signals = sum(len(signals) for signals in stock_results.values())
print(f"\n股票: {stock_code} (共{total_signals}个信号)")
print(f"\n📈 总计: {total_signals} 个信号")
for timeframe, signals in stock_results.items():
if signals:
print(f" {timeframe}: {len(signals)}个信号")
# 只显示最新的信号
latest_signal = signals[-1]
print(f" 最新: {latest_signal['date']} 突破价格 {latest_signal['breakout_price']:.2f}")
except Exception as e:
logger.error(f"分析失败: {e}")
print(f"❌ 分析失败: {e}")
else:
print("未发现任何K线形态信号")
def scan_market(executor, max_stocks):
"""扫描市场热门股票"""
print(f"\n🌍 扫描市场热门股票 (前{max_stocks}只)")
print("-" * 50)
try:
result = executor.execute_task(
task_id=f"market_scan_{max_stocks}",
strategy_id="kline_pattern",
stock_pool_rule="tushare_hot",
stock_pool_params={"limit": max_stocks * 2},
max_stocks=max_stocks,
send_notification=False
)
summary = result.get_summary()
print(f"📊 扫描结果:")
print(f" 股票池: {summary['stock_pool_rule_display']}")
print(f" 总扫描: {summary['total_stocks_analyzed']}")
print(f" 有信号: {summary['stocks_with_signals']}")
print(f" 信号数: {summary['total_signals_found']}")
print(f" 耗时: {summary['execution_time']:.2f}")
if result.strategy_results:
print(f"\n🎯 信号详情:")
for stock_code, timeframe_results in result.strategy_results.items():
for timeframe, strategy_result in timeframe_results.items():
if strategy_result.get_signal_count() > 0:
stock_name = executor.stock_pool_manager.fetcher.get_stock_name(stock_code)
print(f" 📈 {stock_code}({stock_name}): {strategy_result.get_signal_count()} 个信号")
except Exception as e:
logger.error(f"市场扫描失败: {e}")
print(f"扫描失败: {e}")
print(f"❌ 扫描失败: {e}")
def test_notification(notification_manager: NotificationManager):
"""测试通知功能"""
print("\n正在测试通知功能...")
def show_stock_pools(pool_manager):
"""显示可用股票池规则"""
print(f"\n📋 可用股票池规则:")
print("-" * 40)
rules = pool_manager.get_available_rules()
for rule_id, rule_name in rules.items():
print(f" 🎯 {rule_id}: {rule_name}")
print(f"\n💡 使用方法: task <规则名> <股票数量>")
print(f" 示例: task tushare_hot 20")
def execute_task(executor, rule, max_stocks):
"""执行策略任务"""
print(f"\n⚡ 执行策略任务")
print(f"股票池规则: {rule}")
print(f"最大股票数: {max_stocks}")
print("-" * 40)
try:
# 发送测试消息
success = notification_manager.send_test_message()
if success:
print("✅ 通知测试成功")
else:
print("❌ 通知测试失败,请检查配置")
# 发送策略信号测试
test_success = notification_manager.send_strategy_signal(
stock_code="000001.SZ",
stock_name="平安银行",
timeframe="daily",
signal_type="测试信号",
price=10.50,
signal_date="2024-01-15",
additional_info={
"测试项目": "通知功能",
"发送时间": "现在"
}
result = executor.execute_task(
task_id=f"manual_{rule}_{max_stocks}",
strategy_id="kline_pattern",
stock_pool_rule=rule,
max_stocks=max_stocks,
send_notification=False
)
if test_success:
print("✅ 策略信号通知测试成功")
else:
print("❌ 策略信号通知测试失败")
summary = result.get_summary()
print(f"✅ 任务完成:")
print(f" 任务ID: {summary['task_id']}")
print(f" 成功: {'' if summary['success'] else ''}")
print(f" 耗时: {summary['execution_time']:.2f}")
print(f" 信号数: {summary['total_signals_found']}")
if summary['error']:
print(f" 错误: {summary['error']}")
except Exception as e:
logger.error(f"通知测试失败: {e}")
print(f"测试失败: {e}")
logger.error(f"任务执行失败: {e}")
print(f"❌ 任务失败: {e}")
def show_schedule_examples(scheduler, executor):
"""显示定时任务配置示例"""
print(f"\n⏰ 定时任务配置示例:")
print("-" * 50)
examples = [
{
"name": "开盘前热门股扫描",
"rule": "weekdays at 09:00",
"desc": "每个工作日9点扫描同花顺热榜"
},
{
"name": "午间龙头股扫描",
"rule": "weekdays at 12:30",
"desc": "每个工作日12:30扫描龙头股"
},
{
"name": "收盘后综合扫描",
"rule": "weekdays at 15:30",
"desc": "每个工作日15:30综合扫描"
},
{
"name": "高频监控",
"rule": "every 15 minutes",
"desc": "每15分钟监控一次"
}
]
for i, example in enumerate(examples, 1):
print(f" {i}. {example['name']}")
print(f" 规则: {example['rule']}")
print(f" 说明: {example['desc']}")
print()
print("💡 要启用定时任务,请参考 examples/ 目录下的配置示例")
if __name__ == "__main__":
main()
sys.exit(main())

157
market_scanner.py Normal file
View File

@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""
市场扫描定时任务脚本
专门用于Docker容器中定时执行市场扫描
"""
import sys
import os
from pathlib import Path
# 将src目录添加到Python路径
current_dir = Path(__file__).parent
src_dir = current_dir / "src"
sys.path.insert(0, str(src_dir))
from loguru import logger
from src.utils.config_loader import config_loader
from src.data.tushare_fetcher import TushareFetcher
from src.data.stock_pool_manager import StockPoolManager
from src.strategy.kline_pattern_strategy import KLinePatternStrategy
from src.execution.strategy_executor import StrategyExecutor
from src.utils.notification import NotificationManager
from datetime import datetime
def setup_logging():
"""设置日志配置"""
log_level = os.environ.get('LOG_LEVEL', 'INFO')
logger.remove()
logger.add(
sys.stdout,
level=log_level,
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
)
# 确定日志目录 - Docker环境使用/app/logs本地环境使用./logs
if os.path.exists('/app'):
log_dir = "/app/logs"
else:
log_dir = "./logs"
# 创建日志目录
os.makedirs(log_dir, exist_ok=True)
# 添加文件日志
log_file = os.path.join(log_dir, "market_scanner.log")
logger.add(
log_file,
level=log_level,
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}",
rotation="1 day",
retention="30 days"
)
def create_strategy_system():
"""创建策略系统的所有组件"""
# 数据层
fetcher = TushareFetcher()
pool_manager = StockPoolManager(fetcher)
# 通知层
notification_config = config_loader.get('notification', {})
notification_manager = NotificationManager(notification_config)
# 策略层
strategy_config = config_loader.get('strategy', {}).get('kline_pattern', {
'min_entity_ratio': 0.55,
'final_yang_min_ratio': 0.40,
'max_turnover_ratio': 40.0,
'timeframes': ['daily'],
'pullback_tolerance': 0.02,
'monitor_days': 30,
'pullback_confirmation_days': 7
})
# 数据库层
from src.database.mysql_database_manager import MySQLDatabaseManager
db_manager = MySQLDatabaseManager()
kline_strategy = KLinePatternStrategy(
data_fetcher=fetcher,
notification_manager=notification_manager,
config=strategy_config,
db_manager=db_manager
)
# 执行层
executor = StrategyExecutor(pool_manager, notification_manager)
# 注册策略
executor.register_strategy("kline_pattern", kline_strategy)
return executor
def scan_market(max_stocks=200):
"""执行市场扫描"""
logger.info(f"🚀 开始市场扫描任务 - 扫描前{max_stocks}只热门股票")
try:
# 初始化系统
executor = create_strategy_system()
# 执行扫描任务
task_id = f"market_scan_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
result = executor.execute_task(
task_id=task_id,
strategy_id="kline_pattern",
stock_pool_rule="tushare_hot",
stock_pool_params={"limit": max_stocks},
max_stocks=max_stocks,
send_notification=True # 启用通知
)
summary = result.get_summary()
logger.info(f"✅ 市场扫描完成:")
logger.info(f" 任务ID: {summary['task_id']}")
logger.info(f" 股票池: {summary['stock_pool_rule_display']}")
logger.info(f" 总扫描: {summary['total_stocks_analyzed']}")
logger.info(f" 有信号: {summary['stocks_with_signals']}")
logger.info(f" 信号数: {summary['total_signals_found']}")
logger.info(f" 耗时: {summary['execution_time']:.2f}")
if summary['error']:
logger.error(f" 错误: {summary['error']}")
return 1
return 0
except Exception as e:
logger.error(f"❌ 市场扫描失败: {e}")
return 1
def main():
"""主函数"""
setup_logging()
# 从环境变量或命令行参数获取扫描数量
max_stocks = 200
if len(sys.argv) > 1:
try:
max_stocks = int(sys.argv[1])
except ValueError:
logger.warning(f"无效的股票数量参数: {sys.argv[1]},使用默认值: 200")
# 从环境变量获取
max_stocks = int(os.environ.get('MARKET_SCAN_STOCKS', max_stocks))
logger.info(f"📊 市场扫描参数: 最大股票数={max_stocks}")
return scan_market(max_stocks)
if __name__ == "__main__":
sys.exit(main())

View File

@ -1,99 +0,0 @@
#!/usr/bin/env python3
"""
K线形态策略快速测试
"""
import sys
from pathlib import Path
# 将src目录添加到Python路径
current_dir = Path(__file__).parent
src_dir = current_dir / "src"
sys.path.insert(0, str(src_dir))
from src.data.data_fetcher import ADataFetcher
from src.utils.notification import NotificationManager
from src.strategy.kline_pattern_strategy import KLinePatternStrategy
def quick_test():
"""快速测试策略功能"""
print("🚀 K线形态策略快速测试")
print("=" * 50)
# 配置
strategy_config = {
'min_entity_ratio': 0.55,
'timeframes': ['daily'],
'scan_stocks_count': 3, # 只测试3只股票
'analysis_days': 30
}
notification_config = {
'dingtalk': {
'enabled': False, # 测试时关闭钉钉通知
'webhook_url': ''
}
}
try:
# 初始化组件
print("📊 初始化组件...")
data_fetcher = ADataFetcher()
notification_manager = NotificationManager(notification_config)
strategy = KLinePatternStrategy(data_fetcher, notification_manager, strategy_config)
# 测试1: 单股分析
print("\n🔍 测试1: 单股K线形态分析")
test_stock = "000001.SZ"
results = strategy.analyze_stock(test_stock)
total_signals = sum(len(signals) for signals in results.values())
print(f"{test_stock} 分析完成: {total_signals} 个信号")
# 测试2: 市场扫描
print("\n🌍 测试2: 市场形态扫描")
market_results = strategy.scan_market(max_stocks=3)
total_stocks_with_signals = len(market_results)
total_market_signals = sum(
sum(len(signals) for signals in stock_results.values())
for stock_results in market_results.values()
)
print(f"✅ 市场扫描完成: {total_stocks_with_signals} 只股票有信号,共 {total_market_signals} 个信号")
# 测试3: 通知功能
print("\n📱 测试3: 通知系统")
notification_success = notification_manager.send_strategy_signal(
stock_code="TEST001",
stock_name="测试股票",
timeframe="daily",
signal_type="快速测试信号",
price=12.34,
additional_info={
"测试类型": "快速验证",
"状态": "正常"
}
)
print(f"✅ 通知系统测试完成: {'成功' if notification_success else '失败(正常,未配置钉钉)'}")
print("\n🎉 所有测试通过!")
print("\n📝 使用方法:")
print(" python main.py # 启动完整系统")
print(" python test_strategy.py # 详细功能测试")
print("\n⚙️ 配置钉钉通知:")
print(" 1. 在钉钉群中添加自定义机器人")
print(" 2. 复制webhook地址到 config/config.yaml")
print(" 3. 设置 notification.dingtalk.enabled: true")
except Exception as e:
print(f"❌ 测试失败: {e}")
return False
return True
if __name__ == "__main__":
success = quick_test()
sys.exit(0 if success else 1)

View File

@ -40,7 +40,7 @@ trading:
data:
# 数据源配置
sources:
primary: "adata"
primary: "tushare"
# 数据更新频率
update_frequency:

View File

@ -1,863 +0,0 @@
"""
A股数据获取模块
使用adata库获取A股市场数据
"""
import adata
import pandas as pd
from typing import List, Optional, Union
from datetime import datetime, date
import time
from loguru import logger
class ADataFetcher:
"""A股数据获取器"""
def __init__(self):
"""初始化数据获取器"""
self.client = adata
# 股票名称缓存机制
self._stock_name_cache = {}
self._stock_list_cache = None
self._hot_stocks_cache = None
self._east_stocks_cache = None
self._cache_timestamp = None
self._cache_duration = 3600 # 缓存1小时
logger.info("AData客户端初始化完成")
def get_stock_list(self, market: str = "A") -> pd.DataFrame:
"""
获取股票列表
Args:
market: 市场类型默认为A股
Returns:
股票列表DataFrame
"""
try:
stock_list = self.client.stock.info.all_code()
logger.info(f"获取股票列表成功,共{len(stock_list)}只股票")
return stock_list
except Exception as e:
logger.error(f"获取股票列表失败: {e}")
return pd.DataFrame()
def get_filtered_a_share_list(self, exclude_st: bool = True, exclude_bj: bool = True, min_market_cap: float = 2000000000) -> pd.DataFrame:
"""
获取过滤后的A股股票列表
Args:
exclude_st: 是否排除ST股票
exclude_bj: 是否排除北交所股票
min_market_cap: 最小市值要求默认20亿
Returns:
过滤后的股票列表DataFrame
"""
try:
# 获取完整股票列表
all_stocks = self.get_stock_list()
if all_stocks.empty:
return pd.DataFrame()
filtered_stocks = all_stocks.copy()
original_count = len(filtered_stocks)
# 排除北交所股票
if exclude_bj:
before_count = len(filtered_stocks)
filtered_stocks = filtered_stocks[filtered_stocks['exchange'] != 'BJ']
bj_excluded = before_count - len(filtered_stocks)
logger.info(f"排除北交所股票: {bj_excluded}")
# 排除ST股票包含ST、*ST、PT、退市等
if exclude_st:
before_count = len(filtered_stocks)
# 排除包含ST、*ST、PT、退等字符的股票
st_pattern = r'(\*?ST|PT|退|暂停)'
filtered_stocks = filtered_stocks[~filtered_stocks['short_name'].str.contains(st_pattern, na=False, case=False)]
st_excluded = before_count - len(filtered_stocks)
logger.info(f"排除ST等风险股票: {st_excluded}")
# 基于实际市值的筛选
if min_market_cap > 0:
before_count = len(filtered_stocks)
filtered_stocks = self._filter_by_real_market_cap(filtered_stocks, min_market_cap)
cap_excluded = before_count - len(filtered_stocks)
logger.info(f"排除小市值股票(基于实际市值): {cap_excluded}")
# 统计最终结果
final_count = len(filtered_stocks)
excluded_count = original_count - final_count
# 添加完整股票代码(带交易所后缀)
if not filtered_stocks.empty and 'exchange' in filtered_stocks.columns:
filtered_stocks['full_stock_code'] = filtered_stocks.apply(
lambda row: f"{row['stock_code']}.{row['exchange']}", axis=1
)
exchange_counts = filtered_stocks['exchange'].value_counts().to_dict()
exchange_detail = " | ".join([f"{k}: {v}" for k, v in exchange_counts.items()])
logger.info(f"✅ 获取过滤后A股列表成功")
logger.info(f"📊 原始股票: {original_count}只 | 过滤后: {final_count}只 | 排除: {excluded_count}")
logger.info(f"📈 交易所分布: {exchange_detail}")
return filtered_stocks
except Exception as e:
logger.error(f"获取过滤A股列表失败: {e}")
return pd.DataFrame()
def _filter_by_real_market_cap(self, stock_df: pd.DataFrame, min_market_cap: float) -> pd.DataFrame:
"""
基于实际市值筛选股票
由于API限制先使用启发式规则预筛选再对部分股票进行实际市值验证
Args:
stock_df: 股票列表DataFrame
min_market_cap: 最小市值要求
Returns:
过滤后的股票DataFrame
"""
if stock_df.empty:
return stock_df
logger.info(f"开始基于市值筛选股票,阈值: {min_market_cap/100000000:.0f}亿元")
# 步骤1: 使用启发式规则进行预筛选减少API调用量
logger.info("步骤1: 使用启发式规则预筛选...")
pre_filtered = self._filter_by_market_cap_proxy(stock_df, min_market_cap)
if pre_filtered.empty:
logger.warning("启发式预筛选后无股票,返回空结果")
return pd.DataFrame()
logger.info(f"启发式预筛选完成: {len(pre_filtered)}/{len(stock_df)} 只股票")
# 步骤2: 对预筛选的股票进行小批量实际市值验证
logger.info("步骤2: 对预筛选股票进行实际市值验证...")
# 限制验证数量以避免API超时
max_verify_count = min(500, len(pre_filtered)) # 最多验证500只
stocks_to_verify = pre_filtered.head(max_verify_count)
logger.info(f"将验证 {len(stocks_to_verify)} 只股票的实际市值")
valid_stocks = []
total_to_verify = len(stocks_to_verify)
for idx, (_, stock) in enumerate(stocks_to_verify.iterrows()):
stock_code = stock['stock_code']
exchange = stock['exchange']
full_stock_code = f"{stock_code}.{exchange}"
try:
# 获取股本信息
shares_info = self.client.stock.info.get_stock_shares(stock_code=stock_code)
if not shares_info.empty and 'total_share' in shares_info.columns:
total_shares = shares_info.iloc[0]['total_share']
# 获取当前股价
current_price = None
try:
market_data = self.client.stock.market.get_market(full_stock_code)
if not market_data.empty and 'close' in market_data.columns:
current_price = market_data.iloc[0]['close']
except:
pass
# 计算市值
if current_price is not None and total_shares > 0:
market_cap = total_shares * 10000 * current_price # 万股转换为股
if market_cap >= min_market_cap:
stock_with_cap = stock.copy()
stock_with_cap['market_cap'] = market_cap
stock_with_cap['total_shares'] = total_shares
stock_with_cap['current_price'] = current_price
valid_stocks.append(stock_with_cap)
logger.debug(f"{full_stock_code}: 市值{market_cap/100000000:.1f}亿元 {'' if market_cap >= min_market_cap else ''}")
else:
# 如果无法获取实际市值,且预筛选通过,则保留
valid_stocks.append(stock)
logger.debug(f"{full_stock_code}: 无市值数据,保留预筛选结果")
except Exception as e:
# 如果API调用失败且预筛选通过则保留
valid_stocks.append(stock)
logger.debug(f"{full_stock_code}: API失败保留预筛选结果: {e}")
# 显示进度
if (idx + 1) % 50 == 0 or idx + 1 == total_to_verify:
logger.info(f"市值验证进度: {idx + 1}/{total_to_verify} ({(idx + 1)/total_to_verify*100:.1f}%)")
# 添加延时以避免API限制
if idx % 10 == 9: # 每10个请求休息0.1秒
time.sleep(0.1)
# 步骤3: 对于剩余未验证的股票,直接使用预筛选结果
if len(pre_filtered) > max_verify_count:
remaining_stocks = pre_filtered.iloc[max_verify_count:].copy()
for _, stock in remaining_stocks.iterrows():
valid_stocks.append(stock)
logger.info(f"保留 {len(remaining_stocks)} 只未验证股票(基于预筛选结果)")
# 转换为DataFrame
if valid_stocks:
result_df = pd.DataFrame(valid_stocks)
# 确保没有重复
if 'stock_code' in result_df.columns:
result_df = result_df.drop_duplicates(subset=['stock_code'], keep='first')
logger.info(f"✅ 市值筛选完成: {len(result_df)}/{len(stock_df)} 只股票符合要求")
# 统计实际验证vs预筛选的结果
verified_count = min(max_verify_count, len(stocks_to_verify))
unverified_count = len(result_df) - verified_count
logger.info(f"📊 验证详情: 实际验证{verified_count}只, 预筛选保留{unverified_count}")
return result_df
else:
logger.warning("⚠️ 没有股票通过市值筛选")
return pd.DataFrame()
def _filter_by_market_cap_proxy(self, stock_df: pd.DataFrame, min_market_cap: float) -> pd.DataFrame:
"""
基于股票代码的启发式规则筛选大市值股票
Args:
stock_df: 股票列表DataFrame
min_market_cap: 最小市值要求
Returns:
过滤后的股票DataFrame
"""
if stock_df.empty:
return stock_df
# 由于无法直接获取市值数据,使用启发式规则进行筛选
# 注意:这只是一个近似筛选,真实的市值筛选需要实际的市值数据
def is_likely_large_cap(stock_code: str, exchange: str) -> bool:
"""判断股票是否可能是大市值股票"""
code_num = stock_code
if exchange == 'SH': # 上交所
# 主板: 600xxx, 601xxx, 603xxx, 605xxx (通常市值较大)
if code_num.startswith(('600', '601', '603', '605')):
return True
# 科创板: 688xxx (新兴科技公司,市值相对较大)
elif code_num.startswith('688'):
return True
# 其他上交所股票
return False
elif exchange == 'SZ': # 深交所
# 主板: 000xxx, 001xxx (老牌蓝筹,通常市值较大)
if code_num.startswith(('000', '001')):
return True
# 中小板: 002xxx (部分有大市值公司)
elif code_num.startswith('002'):
# 002开头的前1000只股票(002000-002999),上市较早,可能市值较大
try:
code_suffix = int(code_num[3:])
return code_suffix <= 999 # 002000-002999
except:
return False
# 创业板: 300xxx, 301xxx (部分成长为大市值)
elif code_num.startswith(('300', '301')):
# 300开头的前500只股票上市较早部分已成长为大市值
try:
if code_num.startswith('300'):
code_suffix = int(code_num[3:])
return code_suffix <= 499 # 300000-300499
else: # 301xxx较新市值相对较小
return False
except:
return False
return False
return False # 其他情况默认排除
# 应用筛选规则
if min_market_cap >= 2000000000: # 20亿以上
logger.info(f"应用大市值筛选规则(≥{min_market_cap/100000000:.0f}亿元)")
mask = stock_df.apply(lambda row: is_likely_large_cap(row['stock_code'], row['exchange']), axis=1)
return stock_df[mask]
else:
# 小于20亿的筛选条件暂时不实施严格筛选
logger.info(f"市值筛选阈值较低({min_market_cap/100000000:.1f}亿元),保留所有股票")
return stock_df
def get_realtime_data(self, stock_codes: Union[str, List[str]]) -> pd.DataFrame:
"""
获取实时行情数据
Args:
stock_codes: 股票代码或代码列表
Returns:
实时行情DataFrame
"""
try:
if isinstance(stock_codes, str):
stock_codes = [stock_codes]
realtime_data = self.client.stock.market.get_market(stock_codes)
logger.info(f"获取实时数据成功,股票数量: {len(stock_codes)}")
return realtime_data
except Exception as e:
logger.error(f"获取实时数据失败: {e}")
return pd.DataFrame()
def get_historical_data(
self,
stock_code: str,
start_date: Union[str, date],
end_date: Union[str, date],
period: str = "daily"
) -> pd.DataFrame:
"""
获取历史行情数据
Args:
stock_code: 股票代码
start_date: 开始日期
end_date: 结束日期
period: 数据周期 ('daily', 'weekly', 'monthly')
Returns:
历史行情DataFrame
"""
try:
# 转换日期格式
if isinstance(start_date, date):
start_date = start_date.strftime("%Y-%m-%d")
if isinstance(end_date, date):
end_date = end_date.strftime("%Y-%m-%d")
# 根据周期设置k_type参数
k_type_map = {
'daily': 1, # 日线
'weekly': 2, # 周线
'monthly': 3 # 月线
}
k_type = k_type_map.get(period, 1)
# 尝试获取数据
hist_data = pd.DataFrame()
# 方法1: 使用get_market获取指定周期数据
try:
hist_data = self.client.stock.market.get_market(
stock_code,
k_type=k_type,
start_date=start_date,
end_date=end_date
)
except Exception as e:
logger.debug(f"get_market失败: {e}")
# 方法2: 如果方法1失败尝试get_market_bar
if hist_data.empty:
try:
hist_data = self.client.stock.market.get_market_bar(
stock_code=stock_code,
start_date=start_date,
end_date=end_date
)
except Exception as e:
logger.debug(f"get_market_bar失败: {e}")
# 方法3: 如果以上都失败,生成模拟数据用于测试
if hist_data.empty:
logger.warning(f"无法获取{stock_code}真实数据,生成模拟数据用于测试")
hist_data = self._generate_mock_data(stock_code, start_date, end_date)
if not hist_data.empty:
logger.info(f"获取{stock_code}历史数据成功,数据量: {len(hist_data)}")
else:
logger.warning(f"获取{stock_code}历史数据为空")
return hist_data
except Exception as e:
logger.error(f"获取{stock_code}历史数据失败: {e}")
# 返回模拟数据作为后备
return self._generate_mock_data(stock_code, start_date, end_date)
def _generate_mock_data(self, stock_code: str, start_date: str, end_date: str) -> pd.DataFrame:
"""
生成模拟K线数据用于测试
Args:
stock_code: 股票代码
start_date: 开始日期
end_date: 结束日期
Returns:
模拟K线数据
"""
try:
import numpy as np
from datetime import datetime, timedelta
start = datetime.strptime(start_date, "%Y-%m-%d")
end = datetime.strptime(end_date, "%Y-%m-%d")
# 生成交易日期(排除周末)
dates = []
current = start
while current <= end:
if current.weekday() < 5: # 周一到周五
dates.append(current)
current += timedelta(days=1)
if not dates:
return pd.DataFrame()
n = len(dates)
# 生成模拟价格数据 - 创建一个包含我们需要形态的序列
base_price = 10.0
prices = []
# 设置随机种子以获得可重现的结果
np.random.seed(hash(stock_code) % 1000)
for i in range(n):
# 在某些位置插入"两阳线+阴线+阳线"形态
if i % 20 == 10 and i < n - 4: # 每20个交易日插入一次形态
# 两阳线
prices.extend([
base_price + 0.5, # 阳线1
base_price + 1.0, # 阳线2
base_price + 0.3, # 阴线
base_price + 1.5 # 突破阳线
])
i += 3 # 跳过已生成的数据点
else:
# 正常随机价格
change = np.random.uniform(-0.5, 0.5)
base_price = max(5.0, base_price + change) # 确保价格不会太低
prices.append(base_price)
# 确保价格数组长度匹配日期数量
while len(prices) < n:
prices.append(base_price + np.random.uniform(-0.2, 0.2))
prices = prices[:n]
# 生成OHLC数据
data = []
for i, (date, close) in enumerate(zip(dates, prices)):
# 生成开盘价
if i == 0:
open_price = close - np.random.uniform(-0.3, 0.3)
else:
open_price = prices[i-1] + np.random.uniform(-0.2, 0.2)
# 确保高低价格的合理性
high = max(open_price, close) + np.random.uniform(0, 0.5)
low = min(open_price, close) - np.random.uniform(0, 0.3)
# 确保价格顺序正确
low = max(0.1, low) # 确保最低价格为正数
high = max(low + 0.1, high) # 确保最高价高于最低价
data.append({
'trade_date': date.strftime('%Y-%m-%d'),
'open': round(open_price, 2),
'high': round(high, 2),
'low': round(low, 2),
'close': round(close, 2),
'volume': int(np.random.uniform(1000, 10000))
})
mock_df = pd.DataFrame(data)
logger.info(f"生成{stock_code}模拟数据,数据量: {len(mock_df)}")
return mock_df
except Exception as e:
logger.error(f"生成模拟数据失败: {e}")
return pd.DataFrame()
def get_index_data(self, index_code: str = "000001.SH") -> pd.DataFrame:
"""
获取指数数据
Args:
index_code: 指数代码
Returns:
指数数据DataFrame
"""
try:
index_data = self.client.stock.market.get_market(index_code)
logger.info(f"获取指数{index_code}数据成功")
return index_data
except Exception as e:
logger.error(f"获取指数数据失败: {e}")
return pd.DataFrame()
def get_financial_data(self, stock_code: str) -> pd.DataFrame:
"""
获取财务数据
Args:
stock_code: 股票代码
Returns:
财务数据DataFrame
"""
try:
financial_data = self.client.stock.info.financial(stock_code)
logger.info(f"获取{stock_code}财务数据成功")
return financial_data
except Exception as e:
logger.error(f"获取财务数据失败: {e}")
return pd.DataFrame()
def search_stocks(self, keyword: str) -> pd.DataFrame:
"""
搜索股票基于本地股票列表
Args:
keyword: 搜索关键词
Returns:
搜索结果DataFrame
"""
try:
# 获取完整股票列表
all_stocks = self.get_stock_list()
if all_stocks.empty:
return pd.DataFrame()
# 在股票代码和名称中搜索关键词
keyword = str(keyword).strip()
if not keyword:
return pd.DataFrame()
# 支持按代码或名称模糊搜索
mask = (
all_stocks['stock_code'].str.contains(keyword, case=False, na=False) |
all_stocks['short_name'].str.contains(keyword, case=False, na=False)
)
results = all_stocks[mask].copy()
logger.info(f"搜索股票'{keyword}'成功,找到{len(results)}个结果")
return results
except Exception as e:
logger.error(f"搜索股票失败: {e}")
return pd.DataFrame()
def get_hot_stocks_ths(self, limit: int = 100) -> pd.DataFrame:
"""
获取同花顺热股TOP100
Args:
limit: 返回的热股数量默认100
Returns:
热股数据DataFrame包含股票代码名称涨跌幅等信息
"""
try:
# 获取同花顺热股TOP100
hot_stocks = self.client.sentiment.hot.hot_rank_100_ths()
if not hot_stocks.empty:
# 限制返回数量
hot_stocks = hot_stocks.head(limit)
logger.info(f"获取同花顺热股成功,共{len(hot_stocks)}只股票")
return hot_stocks
else:
logger.warning("获取同花顺热股数据为空")
return pd.DataFrame()
except Exception as e:
logger.error(f"获取同花顺热股失败: {e}")
# 返回空DataFrame作为后备
return pd.DataFrame()
def get_popular_stocks_east(self, limit: int = 100) -> pd.DataFrame:
"""
获取东方财富人气榜TOP100
Args:
limit: 返回的人气股数量默认100
Returns:
人气股数据DataFrame包含股票代码名称涨跌幅等信息
"""
try:
# 获取东方财富人气榜TOP100
popular_stocks = self.client.sentiment.hot.pop_rank_100_east()
if not popular_stocks.empty:
# 限制返回数量
popular_stocks = popular_stocks.head(limit)
logger.info(f"获取东财人气股成功,共{len(popular_stocks)}只股票")
return popular_stocks
else:
logger.warning("获取东财人气股数据为空")
return pd.DataFrame()
except Exception as e:
logger.error(f"获取东财人气股失败: {e}")
# 返回空DataFrame作为后备
return pd.DataFrame()
def _is_cache_valid(self) -> bool:
"""检查缓存是否有效"""
if self._cache_timestamp is None:
return False
import time
return (time.time() - self._cache_timestamp) < self._cache_duration
def _update_stock_name_cache(self):
"""更新股票名称缓存"""
try:
import time
# 检查缓存是否有效
if self._is_cache_valid():
return
logger.info("🔄 更新股票名称缓存...")
# 获取热门股票数据并缓存
self._hot_stocks_cache = self.get_hot_stocks_ths(limit=100)
self._east_stocks_cache = self.get_popular_stocks_east(limit=100)
# 清空名称缓存并重新构建
self._stock_name_cache.clear()
# 从热门股票数据中构建缓存
for df, source in [(self._hot_stocks_cache, '同花顺'), (self._east_stocks_cache, '东财')]:
if not df.empty and 'stock_code' in df.columns and 'short_name' in df.columns:
for _, row in df.iterrows():
stock_code = row['stock_code']
stock_name = row['short_name']
if stock_code not in self._stock_name_cache:
self._stock_name_cache[stock_code] = stock_name
# 更新缓存时间戳
self._cache_timestamp = time.time()
logger.info(f"✅ 股票名称缓存更新完成,共缓存 {len(self._stock_name_cache)} 只股票")
except Exception as e:
logger.warning(f"更新股票名称缓存失败: {e}")
def get_stock_name(self, stock_code: str) -> str:
"""
获取股票中文名称带缓存机制
Args:
stock_code: 股票代码
Returns:
股票中文名称如果获取失败返回股票代码
"""
try:
# 更新缓存(如果需要)
self._update_stock_name_cache()
# 从缓存中查找
if stock_code in self._stock_name_cache:
return self._stock_name_cache[stock_code]
# 缓存中没有,尝试搜索功能
search_results = self.search_stocks(stock_code)
if not search_results.empty and 'short_name' in search_results.columns:
stock_name = search_results.iloc[0]['short_name']
# 添加到缓存
self._stock_name_cache[stock_code] = stock_name
return stock_name
# 如果都失败,返回股票代码
logger.debug(f"未能获取{stock_code}的中文名称")
return stock_code
except Exception as e:
logger.debug(f"获取股票{stock_code}名称失败: {e}")
return stock_code
def get_combined_hot_stocks(self, limit_per_source: int = 100, final_limit: int = 150) -> pd.DataFrame:
"""
获取合并去重的热门股票同花顺热股 + 东财人气榜
Args:
limit_per_source: 每个数据源的获取数量默认100
final_limit: 最终返回的股票数量默认150
Returns:
合并去重后的热门股票DataFrame
"""
try:
logger.info("开始获取合并热门股票数据...")
# 获取同花顺热股
ths_stocks = self.get_hot_stocks_ths(limit=limit_per_source)
# 获取东财人气股
east_stocks = self.get_popular_stocks_east(limit=limit_per_source)
combined_stocks = pd.DataFrame()
# 合并数据
if not ths_stocks.empty and not east_stocks.empty:
# 标记数据源
ths_stocks['source'] = '同花顺'
east_stocks['source'] = '东财'
# 尝试合并,处理列名差异
try:
# 统一列名映射
ths_rename_map = {}
east_rename_map = {}
# 检查股票代码列名
if 'stock_code' in ths_stocks.columns:
ths_rename_map['stock_code'] = 'stock_code'
elif 'code' in ths_stocks.columns:
ths_rename_map['code'] = 'stock_code'
if 'stock_code' in east_stocks.columns:
east_rename_map['stock_code'] = 'stock_code'
elif 'code' in east_stocks.columns:
east_rename_map['code'] = 'stock_code'
# 重命名列名
if ths_rename_map:
ths_stocks = ths_stocks.rename(columns=ths_rename_map)
if east_rename_map:
east_stocks = east_stocks.rename(columns=east_rename_map)
# 确保都有stock_code列
if 'stock_code' in ths_stocks.columns and 'stock_code' in east_stocks.columns:
# 合并数据框
combined_stocks = pd.concat([ths_stocks, east_stocks], ignore_index=True)
# 按股票代码去重,保留第一个出现的记录
combined_stocks = combined_stocks.drop_duplicates(subset=['stock_code'], keep='first')
# 限制最终数量
combined_stocks = combined_stocks.head(final_limit)
logger.info(f"合并热门股票成功:同花顺{len(ths_stocks)}只 + 东财{len(east_stocks)}只 → 去重后{len(combined_stocks)}")
else:
logger.warning("股票代码列名不匹配,使用同花顺数据")
combined_stocks = ths_stocks.head(final_limit)
except Exception as merge_error:
logger.error(f"合并数据时出错: {merge_error},使用同花顺数据")
combined_stocks = ths_stocks.head(final_limit)
elif not ths_stocks.empty:
logger.info("仅获取到同花顺数据")
combined_stocks = ths_stocks.head(final_limit)
combined_stocks['source'] = '同花顺'
elif not east_stocks.empty:
logger.info("仅获取到东财数据")
combined_stocks = east_stocks.head(final_limit)
combined_stocks['source'] = '东财'
else:
logger.warning("两个数据源都未获取到数据")
return pd.DataFrame()
return combined_stocks
except Exception as e:
logger.error(f"获取合并热门股票失败: {e}")
return pd.DataFrame()
def get_market_overview(self) -> dict:
"""
获取市场概况
Returns:
市场概况字典
"""
try:
# 获取主要指数数据
sh_index = self.get_index_data("000001.SH") # 上证指数
sz_index = self.get_index_data("399001.SZ") # 深证成指
cyb_index = self.get_index_data("399006.SZ") # 创业板指
overview = {
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"shanghai": sh_index.iloc[0].to_dict() if not sh_index.empty else {},
"shenzhen": sz_index.iloc[0].to_dict() if not sz_index.empty else {},
"chinext": cyb_index.iloc[0].to_dict() if not cyb_index.empty else {}
}
logger.info("获取市场概况成功")
return overview
except Exception as e:
logger.error(f"获取市场概况失败: {e}")
return {}
if __name__ == "__main__":
# 测试代码
fetcher = ADataFetcher()
# 测试获取股票列表
print("测试获取股票列表...")
stock_list = fetcher.get_stock_list()
print(f"股票数量: {len(stock_list)}")
print(stock_list.head())
# 测试同花顺热股
print("\n测试获取同花顺热股TOP10...")
hot_stocks = fetcher.get_hot_stocks_ths(limit=10)
if not hot_stocks.empty:
print(f"同花顺热股数量: {len(hot_stocks)}")
print(hot_stocks.head())
else:
print("未能获取同花顺热股数据")
# 测试东财人气股
print("\n测试获取东财人气股TOP10...")
east_stocks = fetcher.get_popular_stocks_east(limit=10)
if not east_stocks.empty:
print(f"东财人气股数量: {len(east_stocks)}")
print(east_stocks.head())
else:
print("未能获取东财人气股数据")
# 测试合并热门股票
print("\n测试获取合并热门股票TOP15...")
combined_stocks = fetcher.get_combined_hot_stocks(limit_per_source=10, final_limit=15)
if not combined_stocks.empty:
print(f"合并后股票数量: {len(combined_stocks)}")
if 'source' in combined_stocks.columns:
source_counts = combined_stocks['source'].value_counts().to_dict()
print(f"数据源分布: {source_counts}")
print(combined_stocks[['stock_code', 'source'] if 'source' in combined_stocks.columns else ['stock_code']].head())
else:
print("未能获取合并热门股票数据")
# 测试搜索功能
print("\n测试搜索功能...")
search_results = fetcher.search_stocks("平安")
print(search_results.head())
# 测试获取市场概况
print("\n测试获取市场概况...")
overview = fetcher.get_market_overview()
print(overview)

View File

@ -0,0 +1,263 @@
"""
股票池管理器
负责根据不同规则获取和管理股票池
"""
from typing import List, Dict, Any, Optional
import pandas as pd
from loguru import logger
from abc import ABC, abstractmethod
from src.data.tushare_fetcher import TushareFetcher
class StockPoolRule(ABC):
"""股票池规则抽象基类"""
@abstractmethod
def get_stocks(self, fetcher: TushareFetcher, **kwargs) -> List[str]:
"""
获取股票列表
Args:
fetcher: 数据获取器
**kwargs: 规则参数
Returns:
股票代码列表
"""
pass
@abstractmethod
def get_rule_name(self) -> str:
"""获取规则名称"""
pass
class TushareHotStocksRule(StockPoolRule):
"""同花顺热榜股票池规则"""
def get_stocks(self, fetcher: TushareFetcher, limit: int = 50, **kwargs) -> List[str]:
"""获取同花顺热榜股票"""
try:
hot_stocks = fetcher.get_hot_stocks_ths(limit=limit)
if not hot_stocks.empty and 'stock_code' in hot_stocks.columns:
stocks = hot_stocks['stock_code'].tolist()
logger.info(f"✅ 同花顺热榜获取成功: {len(stocks)}只股票")
return stocks
else:
logger.warning("同花顺热榜数据为空")
return []
except Exception as e:
logger.error(f"获取同花顺热榜失败: {e}")
return []
def get_rule_name(self) -> str:
return "同花顺热榜"
class CombinedHotStocksRule(StockPoolRule):
"""合并热门股票池规则(同花顺+东财)"""
def get_stocks(self, fetcher: TushareFetcher, limit_per_source: int = 30, final_limit: int = 50, **kwargs) -> List[str]:
"""获取合并热门股票"""
try:
combined_stocks = fetcher.get_combined_hot_stocks(
limit_per_source=limit_per_source,
final_limit=final_limit
)
if not combined_stocks.empty and 'stock_code' in combined_stocks.columns:
stocks = combined_stocks['stock_code'].tolist()
logger.info(f"✅ 合并热门股票获取成功: {len(stocks)}只股票")
return stocks
else:
logger.warning("合并热门股票数据为空")
return []
except Exception as e:
logger.error(f"获取合并热门股票失败: {e}")
return []
def get_rule_name(self) -> str:
return "合并热门股票"
class LeadingStocksRule(StockPoolRule):
"""龙头牛股股票池规则"""
def get_stocks(self, fetcher: TushareFetcher, top_boards: int = 8, stocks_per_board: int = 3, min_score: float = 60.0, **kwargs) -> List[str]:
"""获取龙头牛股"""
try:
result = fetcher.get_leading_stocks_from_hot_boards(
top_boards=top_boards,
stocks_per_board=stocks_per_board,
min_score=min_score
)
if 'error' not in result and not result['top_leading_stocks'].empty:
stocks = result['top_leading_stocks']['stock_code'].tolist()
logger.info(f"✅ 龙头牛股获取成功: {len(stocks)}只股票")
return stocks
else:
logger.warning("龙头牛股数据为空")
return []
except Exception as e:
logger.error(f"获取龙头牛股失败: {e}")
return []
def get_rule_name(self) -> str:
return "龙头牛股"
class CustomStockListRule(StockPoolRule):
"""自定义股票列表规则"""
def __init__(self, stock_list: List[str]):
self.stock_list = stock_list
def get_stocks(self, fetcher: TushareFetcher, **kwargs) -> List[str]:
"""返回自定义股票列表"""
logger.info(f"✅ 使用自定义股票列表: {len(self.stock_list)}只股票")
return self.stock_list.copy()
def get_rule_name(self) -> str:
return "自定义股票列表"
class StockPoolManager:
"""股票池管理器"""
def __init__(self, fetcher: TushareFetcher):
"""
初始化股票池管理器
Args:
fetcher: TuShare数据获取器
"""
self.fetcher = fetcher
self.rules: Dict[str, StockPoolRule] = {}
self._register_default_rules()
def _register_default_rules(self):
"""注册默认规则"""
self.register_rule("tushare_hot", TushareHotStocksRule())
self.register_rule("combined_hot", CombinedHotStocksRule())
self.register_rule("leading_stocks", LeadingStocksRule())
def register_rule(self, rule_name: str, rule: StockPoolRule):
"""
注册股票池规则
Args:
rule_name: 规则名称
rule: 规则实例
"""
self.rules[rule_name] = rule
logger.info(f"注册股票池规则: {rule_name} - {rule.get_rule_name()}")
def get_stock_pool(self, rule_name: str, **kwargs) -> Dict[str, Any]:
"""
根据规则获取股票池
Args:
rule_name: 规则名称
**kwargs: 规则参数
Returns:
包含股票列表和元信息的字典
"""
if rule_name not in self.rules:
logger.error(f"未找到股票池规则: {rule_name}")
return {
'stocks': [],
'rule_name': rule_name,
'rule_display_name': '未知规则',
'total_count': 0,
'success': False,
'error': f'未找到规则: {rule_name}'
}
rule = self.rules[rule_name]
try:
logger.info(f"🔍 执行股票池规则: {rule.get_rule_name()}")
stocks = rule.get_stocks(self.fetcher, **kwargs)
return {
'stocks': stocks,
'rule_name': rule_name,
'rule_display_name': rule.get_rule_name(),
'total_count': len(stocks),
'success': True,
'parameters': kwargs
}
except Exception as e:
logger.error(f"执行股票池规则失败 {rule.get_rule_name()}: {e}")
return {
'stocks': [],
'rule_name': rule_name,
'rule_display_name': rule.get_rule_name(),
'total_count': 0,
'success': False,
'error': str(e)
}
def get_available_rules(self) -> Dict[str, str]:
"""
获取可用的规则列表
Returns:
规则名称到显示名称的映射
"""
return {name: rule.get_rule_name() for name, rule in self.rules.items()}
def create_custom_rule(self, rule_name: str, stock_list: List[str]):
"""
创建自定义股票列表规则
Args:
rule_name: 规则名称
stock_list: 股票代码列表
"""
custom_rule = CustomStockListRule(stock_list)
self.register_rule(rule_name, custom_rule)
if __name__ == "__main__":
# 测试股票池管理器
from loguru import logger
import sys
logger.remove()
logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}")
# 初始化
fetcher = TushareFetcher()
pool_manager = StockPoolManager(fetcher)
print("=" * 60)
print("📊 股票池管理器测试")
print("=" * 60)
# 显示可用规则
print("可用规则:")
for rule_id, rule_name in pool_manager.get_available_rules().items():
print(f" {rule_id}: {rule_name}")
# 测试同花顺热榜
print(f"\n测试同花顺热榜:")
result = pool_manager.get_stock_pool("tushare_hot", limit=10)
if result['success']:
print(f"✅ 获取成功: {result['total_count']}只股票")
print(f"前5只: {result['stocks'][:5]}")
else:
print(f"❌ 获取失败: {result['error']}")
# 测试自定义规则
print(f"\n测试自定义规则:")
custom_stocks = ["000001.SZ", "000002.SZ", "600000.SH"]
pool_manager.create_custom_rule("my_custom", custom_stocks)
result = pool_manager.get_stock_pool("my_custom")
if result['success']:
print(f"✅ 自定义规则: {result['total_count']}只股票")
print(f"股票: {result['stocks']}")

1560
src/data/tushare_fetcher.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,8 @@ import json
from pathlib import Path
import sys
from loguru import logger
from sqlalchemy import create_engine
import warnings
# 添加项目根目录到路径
current_dir = Path(__file__).parent.parent.parent
@ -33,6 +35,9 @@ class MySQLDatabaseManager:
self.config = config or MYSQL_CONFIG
self.connection_params = self.config.to_dict()
# 创建SQLAlchemy引擎用于pandas操作
self._create_sqlalchemy_engine()
# 测试连接并初始化数据库
try:
self._test_connection()
@ -42,6 +47,62 @@ class MySQLDatabaseManager:
logger.error(f"MySQL数据库初始化失败: {e}")
raise
def _create_sqlalchemy_engine(self):
"""创建SQLAlchemy引擎"""
try:
# 构建连接字符串
connection_string = (
f"mysql+pymysql://{self.config.user}:{self.config.password}"
f"@{self.config.host}:{self.config.port}/{self.config.database}"
f"?charset={self.config.charset}"
)
# 创建引擎
self.engine = create_engine(
connection_string,
pool_pre_ping=True,
pool_recycle=3600,
echo=False
)
# 抑制pandas SQLAlchemy警告
warnings.filterwarnings('ignore',
message='pandas only supports SQLAlchemy connectable.*',
category=UserWarning)
except Exception as e:
logger.error(f"创建SQLAlchemy引擎失败: {e}")
self.engine = None
def _execute_query_with_engine(self, sql: str, params: list = None) -> pd.DataFrame:
"""使用适当的引擎执行查询"""
try:
if self.engine and params:
# 对于SQLAlchemy将%s替换为实际值仅适用于简单参数
formatted_sql = sql
for param in params:
if isinstance(param, str):
# 字符串参数需要加引号
formatted_sql = formatted_sql.replace('%s', f"'{param}'", 1)
elif isinstance(param, (date, datetime)):
# 日期参数需要转换为正确格式并加引号
formatted_sql = formatted_sql.replace('%s', f"'{param.strftime('%Y-%m-%d')}'", 1)
else:
# 数值参数直接替换
formatted_sql = formatted_sql.replace('%s', str(param), 1)
return pd.read_sql_query(formatted_sql, self.engine)
elif self.engine:
return pd.read_sql_query(sql, self.engine)
else:
# 回退到pymysql
with pymysql.connect(**self.connection_params) as conn:
return pd.read_sql_query(sql, conn, params=params)
except Exception as e:
# 如果SQLAlchemy失败回退到pymysql
logger.warning(f"SQLAlchemy查询失败回退到pymysql: {e}")
with pymysql.connect(**self.connection_params) as conn:
return pd.read_sql_query(sql, conn, params=params)
def _test_connection(self):
"""测试数据库连接"""
try:
@ -72,7 +133,13 @@ class MySQLDatabaseManager:
try:
cursor.execute(statement)
except Exception as e:
if "already exists" not in str(e):
# 忽略常见的重复创建警告
error_str = str(e)
if "Duplicate key name" in error_str:
# 特别处理重复索引键名只记录debug级别
logger.debug(f"索引已存在,跳过: {e}")
elif ("already exists" not in error_str and
"Table" not in error_str):
logger.warning(f"执行SQL语句时警告: {e}")
conn.commit()
@ -172,6 +239,22 @@ class MySQLDatabaseManager:
logger.error(f"创建扫描会话失败: {e}")
raise
def update_scan_session_stats(self, session_id: int, total_scanned: int, total_signals: int):
"""更新扫描会话统计"""
try:
with pymysql.connect(**self.connection_params) as conn:
cursor = conn.cursor()
cursor.execute("""
UPDATE scan_sessions
SET total_scanned = %s, total_signals = %s
WHERE id = %s
""", (total_scanned, total_signals, session_id))
conn.commit()
logger.debug(f"更新扫描会话统计: session_id={session_id}, 扫描={total_scanned}, 信号={total_signals}")
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:
@ -253,19 +336,17 @@ class MySQLDatabaseManager:
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 = []
sql = "SELECT * FROM latest_signals_view"
params = []
if strategy_name:
sql += " WHERE strategy_name = %s"
params.append(strategy_name)
if strategy_name:
sql += " WHERE strategy_name = %s"
params.append(strategy_name)
sql += " LIMIT %s"
params.append(limit)
sql += " LIMIT %s"
params.append(limit)
df = pd.read_sql_query(sql, conn, params=params)
return df
return self._execute_query_with_engine(sql, params)
except Exception as e:
logger.error(f"获取最新信号失败: {e}")
@ -275,28 +356,26 @@ class MySQLDatabaseManager:
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()
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]
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 strategy_name:
sql += " AND strategy_name = %s"
params.append(strategy_name)
if timeframe:
sql += " AND timeframe = %s"
params.append(timeframe)
if timeframe:
sql += " AND timeframe = %s"
params.append(timeframe)
sql += " ORDER BY signal_date DESC, scan_time DESC"
sql += " ORDER BY signal_date DESC, scan_time DESC"
df = pd.read_sql_query(sql, conn, params=params)
return df
return self._execute_query_with_engine(sql, params)
except Exception as e:
logger.error(f"按日期范围获取信号失败: {e}")
@ -305,9 +384,8 @@ class MySQLDatabaseManager:
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
sql = "SELECT * FROM strategy_stats_view"
return self._execute_query_with_engine(sql)
except Exception as e:
logger.error(f"获取策略统计失败: {e}")
@ -316,17 +394,15 @@ class MySQLDatabaseManager:
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)
cutoff_date = date.today() - timedelta(days=days)
sql = """
SELECT * FROM pullback_alerts
WHERE pullback_date >= %s
ORDER BY pullback_date DESC
"""
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
return self._execute_query_with_engine(sql, [cutoff_date])
except Exception as e:
logger.error(f"获取回踩提醒失败: {e}")

View File

@ -108,8 +108,8 @@ CREATE TABLE IF NOT EXISTS pullback_alerts (
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 INDEX idx_scan_sessions_scan_date ON scan_sessions (scan_date);
-- 创建视图:最新信号概览
CREATE OR REPLACE VIEW latest_signals_view AS

View File

@ -0,0 +1,382 @@
"""
策略执行器
负责协调股票池获取和策略分析的执行流程
"""
from typing import Dict, List, Any, Optional
import time
from datetime import datetime
from loguru import logger
from src.data.stock_pool_manager import StockPoolManager
from src.strategy.base_strategy import BaseStrategy, StrategyResult
from src.utils.notification import NotificationManager
class ExecutionResult:
"""执行结果"""
def __init__(self,
task_id: str,
strategy_name: str,
stock_pool_rule: str,
start_time: datetime):
"""
初始化执行结果
Args:
task_id: 任务ID
strategy_name: 策略名称
stock_pool_rule: 股票池规则
start_time: 开始时间
"""
self.task_id = task_id
self.strategy_name = strategy_name
self.stock_pool_rule = stock_pool_rule
self.start_time = start_time
self.end_time: Optional[datetime] = None
# 股票池信息
self.stock_pool_info: Dict[str, Any] = {}
self.stock_list: List[str] = []
# 策略分析结果
self.strategy_results: Dict[str, Dict[str, StrategyResult]] = {}
# 统计信息
self.total_stocks_analyzed = 0
self.total_signals_found = 0
self.stocks_with_signals = 0
self.execution_time = 0.0
# 执行状态
self.success = False
self.error: Optional[str] = None
def complete(self, success: bool = True, error: str = None):
"""完成执行"""
self.end_time = datetime.now()
self.execution_time = (self.end_time - self.start_time).total_seconds()
self.success = success
self.error = error
if success:
logger.info(f"✅ 执行完成: {self.task_id} (耗时: {self.execution_time:.2f}秒)")
else:
logger.error(f"❌ 执行失败: {self.task_id} - {error}")
def add_strategy_results(self, results: Dict[str, Dict[str, StrategyResult]], actual_analyzed_count: int = None):
"""添加策略分析结果"""
self.strategy_results = results
# 使用实际分析的股票数量,如果没有提供则使用结果数量
if actual_analyzed_count is not None:
self.total_stocks_analyzed = actual_analyzed_count
else:
self.total_stocks_analyzed = len(self.stock_list)
self.stocks_with_signals = len(results)
# 统计信号总数
for stock_results in results.values():
for timeframe_result in stock_results.values():
self.total_signals_found += timeframe_result.get_signal_count()
def get_summary(self) -> Dict[str, Any]:
"""获取执行摘要"""
return {
'task_id': self.task_id,
'strategy_name': self.strategy_name,
'stock_pool_rule': self.stock_pool_rule,
'stock_pool_rule_display': self.stock_pool_info.get('rule_display_name', ''),
'start_time': self.start_time.isoformat(),
'end_time': self.end_time.isoformat() if self.end_time else None,
'execution_time': self.execution_time,
'success': self.success,
'error': self.error,
'total_stocks_in_pool': len(self.stock_list),
'total_stocks_analyzed': self.total_stocks_analyzed,
'stocks_with_signals': self.stocks_with_signals,
'total_signals_found': self.total_signals_found
}
class StrategyExecutor:
"""策略执行器"""
def __init__(self,
stock_pool_manager: StockPoolManager,
notification_manager: NotificationManager):
"""
初始化策略执行器
Args:
stock_pool_manager: 股票池管理器
notification_manager: 通知管理器
"""
self.stock_pool_manager = stock_pool_manager
self.notification_manager = notification_manager
self.registered_strategies: Dict[str, BaseStrategy] = {}
def register_strategy(self, strategy_id: str, strategy: BaseStrategy):
"""
注册策略
Args:
strategy_id: 策略唯一标识
strategy: 策略实例
"""
self.registered_strategies[strategy_id] = strategy
logger.info(f"注册策略: {strategy_id} - {strategy.get_strategy_name()}")
def execute_task(self,
task_id: str,
strategy_id: str,
stock_pool_rule: str,
stock_pool_params: Dict[str, Any] = None,
max_stocks: int = None,
send_notification: bool = True) -> ExecutionResult:
"""
执行策略分析任务
Args:
task_id: 任务唯一标识
strategy_id: 策略ID
stock_pool_rule: 股票池规则名称
stock_pool_params: 股票池参数
max_stocks: 最大分析股票数量
send_notification: 是否发送通知
Returns:
执行结果
"""
start_time = datetime.now()
# 检查策略是否已注册
if strategy_id not in self.registered_strategies:
error = f"策略未注册: {strategy_id}"
logger.error(error)
result = ExecutionResult(task_id, strategy_id, stock_pool_rule, start_time)
result.complete(False, error)
return result
strategy = self.registered_strategies[strategy_id]
result = ExecutionResult(task_id, strategy.get_strategy_name(), stock_pool_rule, start_time)
logger.info(f"🚀 开始执行策略任务: {task_id}")
logger.info(f" 📊 策略: {strategy.get_strategy_name()}")
logger.info(f" 🎯 股票池规则: {stock_pool_rule}")
try:
# 第1步: 获取股票池
logger.info("📊 第1步: 获取股票池...")
stock_pool_info = self.stock_pool_manager.get_stock_pool(
stock_pool_rule,
**(stock_pool_params or {})
)
if not stock_pool_info['success']:
error = f"股票池获取失败: {stock_pool_info.get('error', '未知错误')}"
result.complete(False, error)
return result
stock_list = stock_pool_info['stocks']
if not stock_list:
error = "股票池为空"
result.complete(False, error)
return result
result.stock_pool_info = stock_pool_info
result.stock_list = stock_list
logger.info(f"✅ 股票池获取成功: {stock_pool_info['rule_display_name']} - {len(stock_list)}只股票")
# 第2步: 执行策略分析
logger.info("🔍 第2步: 执行策略分析...")
strategy_results = strategy.analyze_stock_pool(stock_list, max_stocks)
# 计算实际分析的股票数量(限制后的数量)
actual_analyzed = min(len(stock_list), max_stocks) if max_stocks else len(stock_list)
result.add_strategy_results(strategy_results, actual_analyzed)
# 第3步: 发送通知
if send_notification and result.total_signals_found > 0:
logger.info("📱 第3步: 发送分析结果通知...")
try:
self._send_notification(result, strategy_results)
except Exception as e:
logger.warning(f"发送通知失败: {e}")
# 完成执行
result.complete(True)
except Exception as e:
result.complete(False, str(e))
return result
def _send_notification(self, result: ExecutionResult, strategy_results: Dict[str, Dict[str, StrategyResult]]):
"""发送分析结果通知"""
# 准备通知数据
summary = result.get_summary()
# 构建策略结果摘要
strategy_summary = {
'strategy_name': result.strategy_name,
'stock_pool_source': result.stock_pool_info.get('rule_display_name', ''),
'total_stocks': result.total_stocks_analyzed,
'stocks_with_signals': result.stocks_with_signals,
'total_signals': result.total_signals_found,
'execution_time': result.execution_time
}
# 发送策略摘要通知
try:
# 转换StrategyResult格式为通知所需格式
all_signals = {}
for stock_code, timeframe_results in strategy_results.items():
for timeframe, strategy_result in timeframe_results.items():
if strategy_result.get_signal_count() > 0:
if stock_code not in all_signals:
all_signals[stock_code] = {}
all_signals[stock_code][timeframe] = strategy_result.signals
success = self.notification_manager.send_strategy_summary(
all_signals,
strategy_summary
)
if success:
logger.info("📱 策略结果通知发送成功")
else:
logger.warning("📱 策略结果通知发送失败")
except Exception as e:
logger.error(f"发送策略摘要通知失败: {e}")
def get_registered_strategies(self) -> Dict[str, str]:
"""
获取已注册的策略列表
Returns:
策略ID到策略名称的映射
"""
return {
strategy_id: strategy.get_strategy_name()
for strategy_id, strategy in self.registered_strategies.items()
}
def create_task_function(self,
strategy_id: str,
stock_pool_rule: str,
stock_pool_params: Dict[str, Any] = None,
max_stocks: int = None,
send_notification: bool = True):
"""
创建用于任务调度的函数
Args:
strategy_id: 策略ID
stock_pool_rule: 股票池规则
stock_pool_params: 股票池参数
max_stocks: 最大股票数量
send_notification: 是否发送通知
Returns:
可调度的任务函数
"""
def task_function():
task_id = f"{strategy_id}_{stock_pool_rule}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
return self.execute_task(
task_id=task_id,
strategy_id=strategy_id,
stock_pool_rule=stock_pool_rule,
stock_pool_params=stock_pool_params,
max_stocks=max_stocks,
send_notification=send_notification
)
return task_function
if __name__ == "__main__":
# 测试策略执行器
from loguru import logger
import sys
logger.remove()
logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}")
# 创建模拟组件
from src.data.tushare_fetcher import TushareFetcher
from src.data.stock_pool_manager import StockPoolManager
from src.strategy.base_strategy import BaseStrategy, StrategyResult
from src.utils.config_loader import config_loader
# 创建测试策略
class TestStrategy(BaseStrategy):
def __init__(self):
super().__init__("测试K线策略", {"timeframes": ["daily"]})
def analyze_stock(self, stock_code: str, timeframes: List[str] = None) -> Dict[str, StrategyResult]:
results = {}
# 模拟分析,只有某些股票有信号
signals = []
if stock_code.endswith(".SH"): # 模拟上海股票有信号
signals = [{
'date': '2024-01-10',
'signal_type': '突破信号',
'price': 15.50
}]
results['daily'] = StrategyResult(
strategy_name=self.strategy_name,
stock_code=stock_code,
timeframe='daily',
signals=signals,
execution_time=0.1
)
return results
def get_strategy_description(self) -> str:
return "测试策略,用于验证执行器功能"
print("=" * 80)
print("🚀 策略执行器测试")
print("=" * 80)
# 初始化组件
fetcher = TushareFetcher()
pool_manager = StockPoolManager(fetcher)
notification_config = config_loader.get('notification', {})
notification_manager = NotificationManager(notification_config)
executor = StrategyExecutor(pool_manager, notification_manager)
# 注册测试策略
test_strategy = TestStrategy()
executor.register_strategy("test_kline", test_strategy)
print("已注册策略:")
for strategy_id, strategy_name in executor.get_registered_strategies().items():
print(f" {strategy_id}: {strategy_name}")
# 执行测试任务
print(f"\n执行测试任务...")
result = executor.execute_task(
task_id="test_task_001",
strategy_id="test_kline",
stock_pool_rule="tushare_hot",
stock_pool_params={"limit": 10},
max_stocks=5,
send_notification=False
)
# 显示结果
print(f"\n执行结果摘要:")
summary = result.get_summary()
for key, value in summary.items():
print(f" {key}: {value}")
print(f"\n策略结果详情:")
for stock_code, timeframe_results in result.strategy_results.items():
for timeframe, strategy_result in timeframe_results.items():
signals = strategy_result.get_signal_count()
print(f" {stock_code} ({timeframe}): {signals} 个信号")

View File

@ -0,0 +1,396 @@
"""
任务调度器
负责定时执行策略分析任务
"""
import schedule
import time
import threading
from typing import Dict, List, Callable, Any, Optional
from datetime import datetime, timedelta
from loguru import logger
from dataclasses import dataclass
from enum import Enum
class TaskStatus(Enum):
"""任务状态"""
PENDING = "pending" # 等待中
RUNNING = "running" # 运行中
COMPLETED = "completed" # 已完成
FAILED = "failed" # 失败
DISABLED = "disabled" # 已禁用
@dataclass
class TaskExecution:
"""任务执行记录"""
task_id: str
start_time: datetime
end_time: Optional[datetime] = None
status: TaskStatus = TaskStatus.RUNNING
result: Any = None
error: Optional[str] = None
duration: Optional[float] = None
def complete(self, result: Any = None):
"""标记任务完成"""
self.end_time = datetime.now()
self.status = TaskStatus.COMPLETED
self.result = result
if self.start_time:
self.duration = (self.end_time - self.start_time).total_seconds()
def fail(self, error: str):
"""标记任务失败"""
self.end_time = datetime.now()
self.status = TaskStatus.FAILED
self.error = error
if self.start_time:
self.duration = (self.end_time - self.start_time).total_seconds()
class ScheduledTask:
"""调度任务"""
def __init__(self,
task_id: str,
name: str,
func: Callable,
schedule_rule: str,
enabled: bool = True,
max_history: int = 50):
"""
初始化调度任务
Args:
task_id: 任务唯一标识
name: 任务名称
func: 执行函数
schedule_rule: 调度规则 (: "every 30 minutes", "daily at 09:30")
enabled: 是否启用
max_history: 最大历史记录数量
"""
self.task_id = task_id
self.name = name
self.func = func
self.schedule_rule = schedule_rule
self.enabled = enabled
self.max_history = max_history
# 执行历史
self.execution_history: List[TaskExecution] = []
self.current_execution: Optional[TaskExecution] = None
# 统计信息
self.total_executions = 0
self.successful_executions = 0
self.failed_executions = 0
self.last_execution_time: Optional[datetime] = None
self.next_execution_time: Optional[datetime] = None
def execute(self) -> TaskExecution:
"""执行任务"""
if not self.enabled:
logger.warning(f"任务已禁用: {self.name}")
return None
# 创建执行记录
execution = TaskExecution(
task_id=self.task_id,
start_time=datetime.now()
)
self.current_execution = execution
logger.info(f"🚀 开始执行任务: {self.name}")
try:
# 执行任务函数
result = self.func()
execution.complete(result)
logger.info(f"✅ 任务执行成功: {self.name} (耗时: {execution.duration:.2f}秒)")
self.successful_executions += 1
except Exception as e:
error_msg = str(e)
execution.fail(error_msg)
logger.error(f"❌ 任务执行失败: {self.name} - {error_msg}")
self.failed_executions += 1
finally:
# 更新统计信息
self.total_executions += 1
self.last_execution_time = execution.start_time
self.current_execution = None
# 添加到历史记录
self.execution_history.append(execution)
# 限制历史记录数量
if len(self.execution_history) > self.max_history:
self.execution_history = self.execution_history[-self.max_history:]
return execution
def get_success_rate(self) -> float:
"""获取成功率"""
if self.total_executions == 0:
return 0.0
return self.successful_executions / self.total_executions * 100
def get_status(self) -> TaskStatus:
"""获取任务状态"""
if not self.enabled:
return TaskStatus.DISABLED
if self.current_execution:
return TaskStatus.RUNNING
return TaskStatus.PENDING
def get_last_execution(self) -> Optional[TaskExecution]:
"""获取最后一次执行记录"""
return self.execution_history[-1] if self.execution_history else None
class TaskScheduler:
"""任务调度器"""
def __init__(self):
"""初始化调度器"""
self.tasks: Dict[str, ScheduledTask] = {}
self.running = False
self.scheduler_thread: Optional[threading.Thread] = None
def add_task(self,
task_id: str,
name: str,
func: Callable,
schedule_rule: str,
enabled: bool = True) -> bool:
"""
添加调度任务
Args:
task_id: 任务唯一标识
name: 任务名称
func: 执行函数
schedule_rule: 调度规则
enabled: 是否启用
Returns:
是否添加成功
"""
try:
# 创建任务
task = ScheduledTask(task_id, name, func, schedule_rule, enabled)
# 解析并设置调度规则
self._parse_schedule_rule(task, schedule_rule)
# 添加到任务列表
self.tasks[task_id] = task
logger.info(f"✅ 添加调度任务: {name} - {schedule_rule}")
return True
except Exception as e:
logger.error(f"❌ 添加调度任务失败: {name} - {e}")
return False
def _parse_schedule_rule(self, task: ScheduledTask, rule: str):
"""
解析调度规则并设置到schedule库
Args:
task: 任务实例
rule: 调度规则字符串
"""
rule = rule.lower().strip()
# 解析不同的调度规则格式
if rule.startswith("every"):
# 格式: "every 30 minutes", "every 1 hour", "every 2 days"
parts = rule.split()
if len(parts) >= 3:
interval = int(parts[1])
unit = parts[2].rstrip('s') # 移除复数s
if unit == "second":
schedule.every(interval).seconds.do(task.execute)
elif unit == "minute":
schedule.every(interval).minutes.do(task.execute)
elif unit == "hour":
schedule.every(interval).hours.do(task.execute)
elif unit == "day":
schedule.every(interval).days.do(task.execute)
elif unit == "week":
schedule.every(interval).weeks.do(task.execute)
elif "daily at" in rule:
# 格式: "daily at 09:30", "daily at 14:00"
time_str = rule.split("at")[1].strip()
schedule.every().day.at(time_str).do(task.execute)
elif "weekdays at" in rule:
# 格式: "weekdays at 09:30"
time_str = rule.split("at")[1].strip()
schedule.every().monday.at(time_str).do(task.execute)
schedule.every().tuesday.at(time_str).do(task.execute)
schedule.every().wednesday.at(time_str).do(task.execute)
schedule.every().thursday.at(time_str).do(task.execute)
schedule.every().friday.at(time_str).do(task.execute)
elif "monday at" in rule:
time_str = rule.split("at")[1].strip()
schedule.every().monday.at(time_str).do(task.execute)
elif "tuesday at" in rule:
time_str = rule.split("at")[1].strip()
schedule.every().tuesday.at(time_str).do(task.execute)
# 可以继续添加更多规则...
else:
raise ValueError(f"不支持的调度规则: {rule}")
def remove_task(self, task_id: str) -> bool:
"""
移除任务
Args:
task_id: 任务ID
Returns:
是否移除成功
"""
if task_id in self.tasks:
# 清除schedule中的任务
schedule.clear(tag=task_id)
del self.tasks[task_id]
logger.info(f"✅ 移除任务: {task_id}")
return True
else:
logger.warning(f"⚠️ 任务不存在: {task_id}")
return False
def enable_task(self, task_id: str) -> bool:
"""启用任务"""
if task_id in self.tasks:
self.tasks[task_id].enabled = True
logger.info(f"✅ 启用任务: {task_id}")
return True
return False
def disable_task(self, task_id: str) -> bool:
"""禁用任务"""
if task_id in self.tasks:
self.tasks[task_id].enabled = False
logger.info(f"⏸️ 禁用任务: {task_id}")
return True
return False
def start(self):
"""启动调度器"""
if self.running:
logger.warning("调度器已在运行中")
return
self.running = True
self.scheduler_thread = threading.Thread(target=self._run_scheduler, daemon=True)
self.scheduler_thread.start()
logger.info(f"🚀 任务调度器已启动,共 {len(self.tasks)} 个任务")
def stop(self):
"""停止调度器"""
self.running = False
if self.scheduler_thread:
self.scheduler_thread.join(timeout=5)
logger.info("⏹️ 任务调度器已停止")
def _run_scheduler(self):
"""运行调度器主循环"""
logger.info("📅 调度器主循环已启动")
while self.running:
try:
schedule.run_pending()
time.sleep(1)
except Exception as e:
logger.error(f"调度器运行异常: {e}")
time.sleep(5)
def get_task_status(self) -> Dict[str, Dict[str, Any]]:
"""获取所有任务状态"""
status = {}
for task_id, task in self.tasks.items():
last_execution = task.get_last_execution()
status[task_id] = {
'name': task.name,
'schedule_rule': task.schedule_rule,
'enabled': task.enabled,
'status': task.get_status().value,
'total_executions': task.total_executions,
'success_rate': task.get_success_rate(),
'last_execution_time': task.last_execution_time.isoformat() if task.last_execution_time else None,
'last_execution_result': last_execution.status.value if last_execution else None
}
return status
def execute_task_now(self, task_id: str) -> bool:
"""立即执行指定任务"""
if task_id not in self.tasks:
logger.error(f"任务不存在: {task_id}")
return False
task = self.tasks[task_id]
execution = task.execute()
return execution.status == TaskStatus.COMPLETED if execution else False
if __name__ == "__main__":
# 测试任务调度器
from loguru import logger
import sys
logger.remove()
logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}")
def test_task_1():
"""测试任务1"""
logger.info("执行测试任务1")
time.sleep(0.5)
return {"result": "task1_completed"}
def test_task_2():
"""测试任务2"""
logger.info("执行测试任务2")
return {"result": "task2_completed"}
# 创建调度器
scheduler = TaskScheduler()
# 添加任务
scheduler.add_task("task1", "测试任务1", test_task_1, "every 10 seconds")
scheduler.add_task("task2", "测试任务2", test_task_2, "every 30 seconds")
print("=" * 60)
print("📅 任务调度器测试")
print("=" * 60)
# 显示任务状态
print("任务状态:")
for task_id, status in scheduler.get_task_status().items():
print(f" {task_id}: {status['name']} - {status['schedule_rule']}")
# 立即执行任务测试
print("\n立即执行任务测试:")
scheduler.execute_task_now("task1")
# 可以取消下面的注释来测试定时执行
# print("\n启动调度器...")
# scheduler.start()
# time.sleep(60) # 运行1分钟
# scheduler.stop()

View File

@ -0,0 +1,257 @@
"""
策略基类
定义所有技术分析策略的通用接口
"""
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional
import pandas as pd
from loguru import logger
from datetime import datetime
class StrategyResult:
"""策略执行结果"""
def __init__(self,
strategy_name: str,
stock_code: str,
timeframe: str,
signals: List[Dict[str, Any]] = None,
success: bool = True,
error: str = None,
execution_time: float = 0.0):
"""
初始化策略结果
Args:
strategy_name: 策略名称
stock_code: 股票代码
timeframe: 时间周期
signals: 信号列表
success: 是否成功
error: 错误信息
execution_time: 执行时间
"""
self.strategy_name = strategy_name
self.stock_code = stock_code
self.timeframe = timeframe
self.signals = signals or []
self.success = success
self.error = error
self.execution_time = execution_time
self.timestamp = datetime.now()
def has_signals(self) -> bool:
"""是否有信号"""
return len(self.signals) > 0
def get_signal_count(self) -> int:
"""获取信号数量"""
return len(self.signals)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
return {
'strategy_name': self.strategy_name,
'stock_code': self.stock_code,
'timeframe': self.timeframe,
'signals': self.signals,
'signal_count': self.get_signal_count(),
'success': self.success,
'error': self.error,
'execution_time': self.execution_time,
'timestamp': self.timestamp.isoformat()
}
class BaseStrategy(ABC):
"""策略基类"""
def __init__(self, strategy_name: str, config: Dict[str, Any]):
"""
初始化策略
Args:
strategy_name: 策略名称
config: 策略配置
"""
self.strategy_name = strategy_name
self.config = config
self.timeframes = config.get('timeframes', ['daily'])
@abstractmethod
def analyze_stock(self, stock_code: str, timeframes: List[str] = None) -> Dict[str, StrategyResult]:
"""
分析单只股票
Args:
stock_code: 股票代码
timeframes: 时间周期列表如果为None则使用策略默认周期
Returns:
时间周期到策略结果的映射
"""
pass
def analyze_stock_pool(self, stock_list: List[str], max_stocks: int = None) -> Dict[str, Dict[str, StrategyResult]]:
"""
分析股票池
Args:
stock_list: 股票代码列表
max_stocks: 最大分析股票数量
Returns:
股票代码到时间周期结果映射的字典
"""
if max_stocks and len(stock_list) > max_stocks:
stock_list = stock_list[:max_stocks]
logger.info(f"限制分析股票数量为: {max_stocks}")
results = {}
total_signals = 0
logger.info(f"🔍 开始分析股票池,共 {len(stock_list)} 只股票")
for i, stock_code in enumerate(stock_list, 1):
try:
logger.info(f"⏳ 分析进度: [{i:3d}/{len(stock_list):3d}] 🔍 {stock_code}")
# 分析单只股票
stock_results = self.analyze_stock(stock_code)
# 统计信号数量
stock_signal_count = sum(
result.get_signal_count() for result in stock_results.values()
)
if stock_signal_count > 0:
results[stock_code] = stock_results
total_signals += stock_signal_count
logger.info(f"{stock_code} 发现 {stock_signal_count} 个信号")
except Exception as e:
logger.error(f"❌ 分析股票 {stock_code} 失败: {e}")
continue
logger.info(f"🎉 股票池分析完成: 扫描 {len(stock_list)} 只,发现 {total_signals} 个信号,涉及 {len(results)} 只股票")
return results
def get_strategy_name(self) -> str:
"""获取策略名称"""
return self.strategy_name
def get_timeframes(self) -> List[str]:
"""获取支持的时间周期"""
return self.timeframes
def get_config(self) -> Dict[str, Any]:
"""获取策略配置"""
return self.config
@abstractmethod
def get_strategy_description(self) -> str:
"""获取策略描述"""
pass
def format_results_summary(self, results: Dict[str, Dict[str, StrategyResult]]) -> str:
"""
格式化结果摘要
Args:
results: 分析结果
Returns:
格式化的摘要字符串
"""
if not results:
return f"📊 {self.strategy_name}: 未发现任何信号"
total_stocks = len(results)
total_signals = sum(
sum(result.get_signal_count() for result in stock_results.values())
for stock_results in results.values()
)
summary = f"📊 {self.strategy_name} 分析结果:\n"
summary += f" 🎯 发现信号: {total_signals}\n"
summary += f" 📈 涉及股票: {total_stocks}\n"
# 按信号数量排序显示前5只股票
stock_signal_counts = []
for stock_code, stock_results in results.items():
signal_count = sum(result.get_signal_count() for result in stock_results.values())
stock_signal_counts.append((stock_code, signal_count))
stock_signal_counts.sort(key=lambda x: x[1], reverse=True)
summary += f"\n 🔥 信号最多的股票:\n"
for i, (stock_code, signal_count) in enumerate(stock_signal_counts[:5], 1):
summary += f" {i}. {stock_code}: {signal_count} 个信号\n"
return summary
if __name__ == "__main__":
# 测试策略基类功能
from loguru import logger
import sys
logger.remove()
logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}")
# 创建一个测试策略
class TestStrategy(BaseStrategy):
def __init__(self):
super().__init__("测试策略", {"timeframes": ["daily", "weekly"]})
def analyze_stock(self, stock_code: str, timeframes: List[str] = None) -> Dict[str, StrategyResult]:
# 模拟分析结果
results = {}
for timeframe in self.timeframes:
# 模拟生成一些信号
signals = []
if stock_code.startswith("000001"): # 模拟只有某些股票有信号
signals = [
{
'date': '2024-01-10',
'signal_type': '买入信号',
'price': 10.50,
'confidence': 0.85
}
]
results[timeframe] = StrategyResult(
strategy_name=self.strategy_name,
stock_code=stock_code,
timeframe=timeframe,
signals=signals,
execution_time=0.1
)
return results
def get_strategy_description(self) -> str:
return "这是一个测试策略,用于验证基类功能"
# 测试
print("=" * 60)
print("📊 策略基类测试")
print("=" * 60)
strategy = TestStrategy()
test_stocks = ["000001.SZ", "000002.SZ", "600000.SH"]
print(f"策略名称: {strategy.get_strategy_name()}")
print(f"支持周期: {strategy.get_timeframes()}")
print(f"策略描述: {strategy.get_strategy_description()}")
# 测试股票池分析
results = strategy.analyze_stock_pool(test_stocks)
print(f"\n{strategy.format_results_summary(results)}")
# 测试单股分析
single_result = strategy.analyze_stock("000001.SZ")
print(f"\n单股分析结果: {single_result['daily'].to_dict()}")

View File

@ -9,15 +9,16 @@ from typing import Dict, List, Tuple, Optional, Any
from datetime import datetime, timedelta
from loguru import logger
from ..data.tushare_fetcher import TushareFetcher as ADataFetcher
from ..data.tushare_fetcher import TushareFetcher
from ..utils.notification import NotificationManager
from ..database.mysql_database_manager import MySQLDatabaseManager
from .base_strategy import BaseStrategy, StrategyResult
class KLinePatternStrategy:
class KLinePatternStrategy(BaseStrategy):
"""K线形态策略类"""
def __init__(self, data_fetcher: ADataFetcher, notification_manager: NotificationManager,
def __init__(self, data_fetcher: TushareFetcher, notification_manager: NotificationManager,
config: Dict[str, Any], db_manager: MySQLDatabaseManager = None):
"""
初始化K线形态策略
@ -28,17 +29,22 @@ class KLinePatternStrategy:
config: 策略配置
db_manager: 数据库管理器
"""
# 初始化基类
super().__init__("K线形态策略", config)
self.data_fetcher = data_fetcher
self.notification_manager = notification_manager
self.config = config
self.db_manager = db_manager or MySQLDatabaseManager()
# 策略参数
self.strategy_name = "K线形态策略"
self.min_entity_ratio = config.get('min_entity_ratio', 0.55) # 前两根阳线实体部分最小比例
self.final_yang_min_ratio = config.get('final_yang_min_ratio', 0.40) # 最后阳线实体部分最小比例
self.max_turnover_ratio = config.get('max_turnover_ratio', 40.0) # 最后阳线最大换手率(%
self.timeframes = config.get('timeframes', ['daily', 'weekly']) # 支持的时间周期
# 热榜缓存机制
self._hot_stocks_cache = None
self._cache_timestamp = None
self._cache_duration = 300 # 缓存5分钟
# 回踩监控参数
self.pullback_tolerance = config.get('pullback_tolerance', 0.02) # 回踩容忍度2%
@ -51,10 +57,6 @@ class KLinePatternStrategy:
# 格式: {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,
@ -65,6 +67,47 @@ class KLinePatternStrategy:
logger.info(f"K线形态策略初始化完成 (策略ID: {self.strategy_id})")
def _get_cached_hot_stocks(self, max_stocks: int) -> List[str]:
"""
获取缓存的热榜股票列表
Args:
max_stocks: 最大股票数量
Returns:
股票代码列表
"""
import time
current_time = time.time()
# 检查缓存是否有效
if (self._hot_stocks_cache is not None and
self._cache_timestamp is not None and
current_time - self._cache_timestamp < self._cache_duration):
logger.info(f"🔄 使用缓存的同花顺热榜数据 ({len(self._hot_stocks_cache)} 只股票)")
return self._hot_stocks_cache[:max_stocks]
# 缓存失效或不存在,重新获取
logger.info(f"🔥 获取同花顺热榜股票 (前{max_stocks}只)...")
hot_stocks = self.data_fetcher.get_hot_stocks_ths(limit=max_stocks * 2) # 多获取一些以备不同max_stocks需求
if not hot_stocks.empty and 'stock_code' in hot_stocks.columns:
self._hot_stocks_cache = hot_stocks['stock_code'].tolist()
self._cache_timestamp = current_time
logger.info(f"✅ 同花顺热榜获取成功,已缓存 {len(self._hot_stocks_cache)} 只股票")
return self._hot_stocks_cache[:max_stocks]
else:
logger.error("❌ 同花顺热榜数据为空")
return []
def clear_hot_stocks_cache(self):
"""清除热榜缓存,强制下次重新获取"""
self._hot_stocks_cache = None
self._cache_timestamp = None
logger.info("🔄 热榜缓存已清除")
def calculate_kline_features(self, df: pd.DataFrame) -> pd.DataFrame:
"""
计算K线特征指标
@ -117,47 +160,6 @@ class KLinePatternStrategy:
return df
def check_follow_up_strength(self, df: pd.DataFrame, pattern_index: int, yin_high: float, ema20_price: float) -> bool:
"""
检查形态发生后的后续强势条件
Args:
df: K线数据DataFrame
pattern_index: 形态完成的索引位置第4根阳线的位置
yin_high: 阴线的最高价
ema20_price: 形态完成时的EMA20价格
Returns:
bool: 是否满足后续强势条件
"""
# 获取配置的后续验证天数默认为3天
follow_up_days = self.config.get('follow_up_days', 3)
# 检查是否有足够的后续数据
if pattern_index + follow_up_days >= len(df):
# 如果没有足够的后续数据,说明是最新的形态,暂时通过验证
return True
# 检查后续N天的K线
for j in range(1, follow_up_days + 1):
if pattern_index + j >= len(df):
break
next_kline = df.iloc[pattern_index + j]
# 检查1: 最低价不能回踩阴线最高价
if next_kline['low'] <= yin_high:
logger.debug(f"后续第{j}天最低价{next_kline['low']:.2f}回踩阴线最高价{yin_high:.2f},不符合强势条件")
return False
# 检查2: 收盘价不能跌破EMA20
next_ema20 = next_kline.get('ema20', ema20_price)
if next_kline['close'] <= next_ema20:
logger.debug(f"后续第{j}天收盘价{next_kline['close']:.2f}跌破EMA20价格{next_ema20:.2f},不符合强势条件")
return False
logger.debug(f"后续{follow_up_days}天强势验证通过")
return True
def detect_potential_pattern(self, df: pd.DataFrame) -> List[Dict[str, Any]]:
"""
@ -382,6 +384,52 @@ class KLinePatternStrategy:
return None
def _filter_recent_signals(self, signals: List[Dict[str, Any]], days: int = 7) -> List[Dict[str, Any]]:
"""
过滤最近N天内产生的信号
Args:
signals: 信号列表
days: 最近天数默认7天
Returns:
过滤后的信号列表
"""
if not signals:
return signals
current_date = datetime.now().date()
recent_signals = []
for signal in signals:
signal_date = signal.get('confirmation_date') or signal.get('date')
# 处理不同的日期格式
if isinstance(signal_date, str):
try:
signal_date = pd.to_datetime(signal_date).date()
except:
continue
elif hasattr(signal_date, 'date'):
signal_date = signal_date.date()
elif not isinstance(signal_date, datetime.date):
continue
# 计算信号距今天数
days_ago = (current_date - signal_date).days
# 只保留最近N天内的信号
if days_ago <= days:
recent_signals.append(signal)
logger.debug(f"✅ 保留近期信号: {signal_date} (距今{days_ago}天)")
else:
logger.debug(f"🗓️ 过滤历史信号: {signal_date} (距今{days_ago}天)")
if len(recent_signals) != len(signals):
logger.info(f"📅 信号过滤: 总共{len(signals)}个 → 近{days}天内{len(recent_signals)}")
return recent_signals
def detect_pattern(self, df: pd.DataFrame) -> List[Dict[str, Any]]:
"""
检测"两阳线+阴线+阳线"形态创新高回踩确认逻辑
@ -410,40 +458,45 @@ class KLinePatternStrategy:
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]]]:
def analyze_stock(self, stock_code: str, timeframes: List[str] = None, session_id: int = None) -> Dict[str, StrategyResult]:
"""
分析单只股票的K线形态
分析单只股票的K线形态 - 继承自BaseStrategy
Args:
stock_code: 股票代码
stock_name: 股票名称
days: 分析的天数
timeframes: 时间周期列表如果为None则使用策略默认周期
Returns:
各时间周期的信号字典
时间周期到策略结果的映射
"""
if timeframes is None:
timeframes = self.timeframes
results = {}
stock_name = self.data_fetcher.get_stock_name(stock_code)
if stock_name is None:
# 尝试获取股票中文名称
stock_name = self.data_fetcher.get_stock_name(stock_code)
try:
# 计算开始日期,针对不同周期调整时间范围
end_date = datetime.now().strftime('%Y-%m-%d')
for timeframe in self.timeframes:
analysis_days = days
start_date = (datetime.now() - timedelta(days=analysis_days)).strftime('%Y-%m-%d')
for timeframe in timeframes:
start_time = datetime.now()
try:
# 计算开始日期
end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=60)).strftime('%Y-%m-%d')
logger.info(f"🔍 分析股票: {stock_code}({stock_name}) | 周期: {timeframe}")
# 获取历史数据 - 直接使用adata的原生周期支持
# 获取历史数据
df = self.data_fetcher.get_historical_data(stock_code, start_date, end_date, timeframe)
if df.empty:
logger.warning(f"{stock_code} {timeframe} 数据为空")
results[timeframe] = []
results[timeframe] = StrategyResult(
strategy_name=self.strategy_name,
stock_code=stock_code,
timeframe=timeframe,
signals=[],
success=False,
error="数据为空",
execution_time=(datetime.now() - start_time).total_seconds()
)
continue
# 计算K线特征
@ -452,16 +505,46 @@ class KLinePatternStrategy:
# 检测形态
signals = self.detect_pattern(df_with_features)
# 处理信号
for signal in signals:
# 过滤一周内的信号
recent_signals = self._filter_recent_signals(signals, days=7)
# 处理信号格式
formatted_signals = []
for signal in recent_signals:
formatted_signal = {
'date': signal['date'],
'signal_type': signal['pattern_type'],
'price': signal['breakout_price'],
'confidence': signal['final_yang_entity_ratio'],
'stock_name': stock_name, # 添加股票名称
'details': {
'yin_high': signal['yin_high'],
'breakout_amount': signal['breakout_amount'],
'breakout_pct': signal['breakout_pct'],
'ema20_price': signal['ema20_price'],
'turnover_ratio': signal['turnover_ratio']
}
}
# 如果是确认信号,添加确认信息
if not signal.get('confirmation_pending', True):
formatted_signal['details'].update({
'new_high_price': signal.get('new_high_price'),
'new_high_date': signal.get('new_high_date'),
'confirmation_date': signal.get('confirmation_date'),
'confirmation_days': signal.get('confirmation_days'),
'pullback_distance': signal.get('pullback_distance')
})
formatted_signals.append(formatted_signal)
# 将信号添加到监控列表
signal['stock_code'] = stock_code
signal['stock_name'] = stock_name
signal['timeframe'] = timeframe
# 将信号添加到监控列表
self.add_triggered_signal(signal)
# 保存信号到数据库如果提供了session_id
# 保存信号到数据库
if session_id is not None:
try:
signal_id = self.db_manager.save_stock_signal(
@ -469,28 +552,59 @@ class KLinePatternStrategy:
strategy_id=self.strategy_id,
signal=signal
)
signal['signal_id'] = signal_id
logger.debug(f"信号已保存到数据库: {stock_code} (ID: {signal_id})")
logger.debug(f"信号已保存到数据库: signal_id={signal_id}")
except Exception as e:
logger.error(f"保存信号到数据库失败: {e}")
results[timeframe] = signals
execution_time = (datetime.now() - start_time).total_seconds()
results[timeframe] = StrategyResult(
strategy_name=self.strategy_name,
stock_code=stock_code,
timeframe=timeframe,
signals=formatted_signals,
success=True,
execution_time=execution_time
)
# 美化信号统计日志
if signals:
logger.info(f"{stock_code}({stock_name}) {timeframe}周期: 发现 {len(signals)} 个信号")
for i, signal in enumerate(signals, 1):
logger.info(f" 📊 信号{i}: {signal['date']} | 价格: {signal['breakout_price']:.2f}元 | 实体: {signal['final_yang_entity_ratio']:.1%}")
if formatted_signals:
logger.info(f"{stock_code}({stock_name}) {timeframe}周期: 发现 {len(formatted_signals)} 个信号")
for i, signal in enumerate(formatted_signals, 1):
logger.info(f" 📊 信号{i}: {signal['date']} | 价格: {signal['price']:.2f}元 | 置信度: {signal['confidence']:.1%}")
else:
logger.debug(f"📭 {stock_code}({stock_name}) {timeframe}周期: 无信号")
except Exception as e:
logger.error(f"分析股票 {stock_code} 失败: {e}")
for timeframe in self.timeframes:
results[timeframe] = []
except Exception as e:
logger.error(f"分析股票 {stock_code} {timeframe}周期失败: {e}")
execution_time = (datetime.now() - start_time).total_seconds()
results[timeframe] = StrategyResult(
strategy_name=self.strategy_name,
stock_code=stock_code,
timeframe=timeframe,
signals=[],
success=False,
error=str(e),
execution_time=execution_time
)
return results
def get_strategy_description(self) -> str:
"""获取策略描述"""
return f"""K线形态策略 - 两阳线+阴线+阳线突破(创新高回踩确认)
该策略通过识别特定的K线形态来发现股票突破机会
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)}
"""
def check_pullback_signals(self, stock_code: str, current_data: pd.DataFrame) -> List[Dict[str, Any]]:
"""
检查已触发信号的价格回踩情况
@ -618,109 +732,7 @@ 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]]:
"""
@ -790,108 +802,27 @@ class KLinePatternStrategy:
return all_pullback_alerts
def _convert_to_weekly(self, daily_df: pd.DataFrame) -> pd.DataFrame:
def scan_market(self, stock_list: List[str] = None, max_stocks: int = 100) -> Dict[str, Dict[str, List[Dict[str, Any]]]]:
"""
将日线数据转换为周线数据
扫描市场中的股票形态 - 只使用同花顺热榜股票
Args:
daily_df: 日线数据
Returns:
周线数据
"""
if daily_df.empty:
return daily_df
try:
df = daily_df.copy()
# 确保有trade_date列并设置为索引
if 'trade_date' in df.columns:
df['trade_date'] = pd.to_datetime(df['trade_date'])
df.set_index('trade_date', inplace=True)
# 按周聚合
weekly_df = df.resample('W').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum' if 'volume' in df.columns else 'last'
}).dropna()
# 重置索引保持trade_date列
weekly_df.reset_index(inplace=True)
return weekly_df
except Exception as e:
logger.error(f"转换周线数据失败: {e}")
return pd.DataFrame()
def _convert_to_monthly(self, daily_df: pd.DataFrame) -> pd.DataFrame:
"""
将日线数据转换为月线数据
Args:
daily_df: 日线数据
Returns:
月线数据
"""
if daily_df.empty:
return daily_df
try:
df = daily_df.copy()
# 确保有trade_date列并设置为索引
if 'trade_date' in df.columns:
df['trade_date'] = pd.to_datetime(df['trade_date'])
df.set_index('trade_date', inplace=True)
# 按月聚合
monthly_df = df.resample('ME').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum' if 'volume' in df.columns else 'last'
}).dropna()
# 重置索引保持trade_date列
monthly_df.reset_index(inplace=True)
return monthly_df
except Exception as e:
logger.error(f"转换月线数据失败: {e}")
return pd.DataFrame()
def scan_market(self, stock_list: List[str] = None, max_stocks: int = 100, use_hot_stocks: bool = True, use_combined_sources: bool = True, use_all_a_shares: bool = False) -> Dict[str, Dict[str, List[Dict[str, Any]]]]:
"""
扫描市场中的股票形态
Args:
stock_list: 股票代码列表如果为None则自动选择股票池
stock_list: 股票代码列表如果为None则使用同花顺热榜
max_stocks: 最大扫描股票数量
use_hot_stocks: 是否使用热门股票数据默认True
use_combined_sources: 是否使用合并的双数据源同花顺+东财默认True
use_all_a_shares: 是否使用所有A股股票(排除北交所和ST)优先级最高
Returns:
所有股票的分析结果
"""
logger.info("🚀" + "="*70)
logger.info("🌍 开始市场K线形态扫描")
logger.info("🌍 开始市场K线形态扫描 - 只使用同花顺热榜")
logger.info("🚀" + "="*70)
# 创建扫描会话
scan_config = {
'max_stocks': max_stocks,
'use_hot_stocks': use_hot_stocks,
'use_combined_sources': use_combined_sources,
'use_all_a_shares': use_all_a_shares,
'data_source': '同花顺热榜',
'timeframes': self.timeframes
}
session_id = self.db_manager.create_scan_session(
@ -900,81 +831,14 @@ class KLinePatternStrategy:
)
if stock_list is None:
# 优先级1: 使用所有A股股票
if use_all_a_shares:
try:
logger.info("📊 获取所有A股股票数据排除北交所和ST股票...")
filtered_stocks = self.data_fetcher.get_filtered_a_share_list()
# 使用缓存的同花顺热榜数据
stock_list = self._get_cached_hot_stocks(max_stocks)
if not filtered_stocks.empty:
# 如果max_stocks小于总股票数随机采样
if max_stocks > 0 and max_stocks < len(filtered_stocks):
# 按市值排序或随机选择,这里先随机选择
selected_stocks = filtered_stocks.sample(max_stocks)
stock_list = selected_stocks['full_stock_code'].tolist()
logger.info(f"📈 从{len(filtered_stocks)}只A股中随机选择{len(stock_list)}只进行分析")
else:
stock_list = filtered_stocks['full_stock_code'].tolist()
logger.info(f"📈 分析全部{len(stock_list)}只A股股票")
source_info = "全A股(排除北交所和ST)"
else:
logger.warning("获取A股列表失败回退到热门股票")
use_all_a_shares = False
except Exception as e:
logger.error(f"获取A股列表失败: {e},回退到热门股票")
use_all_a_shares = False
# 优先级2: 使用热门股票数据
if not use_all_a_shares and use_hot_stocks:
try:
if use_combined_sources:
# 使用合并的双数据源
logger.info("获取合并热门股票数据(同花顺+东财)...")
hot_stocks = self.data_fetcher.get_combined_hot_stocks(
limit_per_source=max_stocks,
final_limit=max_stocks
)
source_info = "双数据源合并"
else:
# 仅使用同花顺数据
logger.info("获取同花顺热股TOP100数据...")
hot_stocks = self.data_fetcher.get_hot_stocks_ths(limit=max_stocks)
source_info = "同花顺热股"
if not hot_stocks.empty and 'stock_code' in hot_stocks.columns:
stock_list = hot_stocks['stock_code'].tolist()
# 统计数据源分布
if 'source' in hot_stocks.columns:
source_counts = hot_stocks['source'].value_counts().to_dict()
source_detail = " | ".join([f"{k}: {v}" for k, v in source_counts.items()])
logger.info(f"📊 数据源: {source_info} | 总计: {len(stock_list)}只股票")
logger.info(f"📈 分布详情: {source_detail}")
else:
logger.info(f"📊 数据源: {source_info} | 总计: {len(stock_list)}只股票")
else:
logger.warning("热门股票数据为空,回退到全市场股票")
use_hot_stocks = False
except Exception as e:
logger.error(f"获取热门股票失败: {e},回退到全市场股票")
use_hot_stocks = False
# 优先级3: 如果热股获取失败,使用全市场股票列表
if not use_all_a_shares and not use_hot_stocks:
try:
all_stocks = self.data_fetcher.get_stock_list()
if not all_stocks.empty:
# 随机选择一些股票进行扫描
stock_list = all_stocks['stock_code'].head(max_stocks).tolist()
logger.info(f"使用全市场股票数据,共{len(stock_list)}只股票")
else:
logger.warning("未能获取股票列表")
return {}
except Exception as e:
logger.error(f"获取股票列表失败: {e}")
return {}
if stock_list:
logger.info(f"📊 数据源: 同花顺热榜 | 扫描股票: {len(stock_list)}")
else:
logger.error("❌ 同花顺热榜数据为空,无法进行扫描")
return {}
results = {}
total_signals = 0
@ -1038,15 +902,9 @@ class KLinePatternStrategy:
# 发送汇总通知
if results:
# 判断数据源类型
data_source = '全市场股票'
if stock_list and len(stock_list) <= max_stocks:
if use_hot_stocks:
data_source = '合并热门股票' if use_combined_sources else '热门股票'
scan_stats = {
'total_scanned': len(stock_list),
'data_source': data_source
'data_source': '同花顺热榜'
}
try:
@ -1119,10 +977,77 @@ K线形态策略 - 两阳线+阴线+阳线突破(优化版:创新高回踩
- 系统日志详细记录
"""
def analyze_stock_pool(self, stock_list: List[str], max_stocks: int = None) -> Dict[str, Dict[str, StrategyResult]]:
"""
分析股票池带数据库会话管理
Args:
stock_list: 股票代码列表
max_stocks: 最大分析股票数量
Returns:
股票代码到时间周期结果映射的字典
"""
if max_stocks and len(stock_list) > max_stocks:
stock_list = stock_list[:max_stocks]
logger.info(f"限制分析股票数量为: {max_stocks}")
# 创建扫描会话
scan_config = {
'max_stocks': max_stocks,
'timeframes': self.timeframes
}
session_id = self.db_manager.create_scan_session(
strategy_id=self.strategy_id,
scan_config=scan_config
)
logger.debug(f"创建扫描会话: session_id={session_id}")
results = {}
total_signals = 0
logger.info(f"🔍 开始分析股票池,共 {len(stock_list)} 只股票")
for i, stock_code in enumerate(stock_list, 1):
try:
logger.info(f"⏳ 分析进度: [{i:3d}/{len(stock_list):3d}] 🔍 {stock_code}")
# 分析单只股票传递session_id
stock_results = self.analyze_stock(stock_code, session_id=session_id)
# 统计信号数量
stock_signal_count = sum(
result.get_signal_count() for result in stock_results.values()
)
if stock_signal_count > 0:
results[stock_code] = stock_results
total_signals += stock_signal_count
logger.info(f"{stock_code} 发现 {stock_signal_count} 个信号")
except Exception as e:
logger.error(f"❌ 分析股票 {stock_code} 失败: {e}")
continue
# 更新扫描会话统计
try:
self.db_manager.update_scan_session_stats(
session_id=session_id,
total_scanned=len(stock_list),
total_signals=total_signals
)
logger.debug(f"更新扫描会话统计: session_id={session_id}, 扫描={len(stock_list)}, 信号={total_signals}")
except Exception as e:
logger.error(f"更新扫描会话统计失败: {e}")
logger.info(f"🎉 股票池分析完成: 扫描 {len(stock_list)} 只,发现 {total_signals} 个信号,涉及 {len(results)} 只股票")
return results
if __name__ == "__main__":
# 测试代码
from ..data.tushare_fetcher import TushareFetcher as ADataFetcher
from ..data.tushare_fetcher import TushareFetcher
from ..utils.notification import NotificationManager
# 模拟配置
@ -1139,7 +1064,7 @@ if __name__ == "__main__":
}
# 初始化组件
data_fetcher = ADataFetcher()
data_fetcher = TushareFetcher()
notification_manager = NotificationManager(notification_config)
strategy = KLinePatternStrategy(data_fetcher, notification_manager, strategy_config)

View File

@ -96,6 +96,10 @@ class ConfigLoader:
"""获取日志配置"""
return self.get('logging', {})
def get_tushare_token(self) -> str:
"""获取TuShare token"""
return self.get('data_source.tushare_token', '')
# 全局配置实例
config_loader = ConfigLoader()

View File

@ -357,8 +357,9 @@ class NotificationManager:
return False
try:
from datetime import datetime
from datetime import datetime, date
import math
import pandas as pd
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 收集所有信号详情
@ -366,15 +367,39 @@ class NotificationManager:
total_signals = 0
total_stocks = len(all_signals)
# 计算一周前的日期
current_date = date.today()
one_week_ago = current_date - pd.Timedelta(days=7)
for stock_code, stock_results in all_signals.items():
for timeframe, signals in stock_results.items():
for signal in signals:
# 添加一周内信号过滤
signal_date = signal.get('details', {}).get('confirmation_date') or signal['date']
# 处理不同的日期格式
if isinstance(signal_date, str):
try:
signal_date = pd.to_datetime(signal_date).date()
except:
continue
elif hasattr(signal_date, 'date'):
signal_date = signal_date.date()
elif not isinstance(signal_date, date):
continue
# 只处理一周内的信号
if signal_date < one_week_ago:
logger.debug(f"🗓️ 通知过滤历史信号: {stock_code} {signal_date} (距今{(current_date - signal_date).days}天)")
continue
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)
details = signal.get('details', {})
confirmation_date = details.get('confirmation_date', signal['date'])
new_high_price = details.get('new_high_price', signal['price'])
confirmation_days = details.get('confirmation_days', 0)
all_signal_details.append({
'stock_code': stock_code,
@ -383,20 +408,26 @@ class NotificationManager:
'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 '❌下方',
'original_breakout_price': signal['price'], # 原突破价
'yin_high': details.get('yin_high', 0), # 阴线最高价
'turnover': details.get('turnover_ratio', 0),
'breakout_pct': details.get('breakout_pct', 0),
'ema20_status': '✅上方' if details.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) # 是否为新格式信号
'pullback_distance': details.get('pullback_distance', 0),
'is_new_format': details.get('new_high_confirmed', False) # 是否为新格式信号
})
# 如果没有信号,直接返回
if total_signals == 0:
logger.info("📱 通知过滤: 没有一周内的新信号,不发送通知")
return True
# 记录过滤后的信号统计
original_signals = sum(len(signals) for stock_results in all_signals.values() for signals in stock_results.values())
if original_signals > total_signals:
logger.info(f"📅 通知信号过滤: 原始{original_signals}个 → 一周内{total_signals}")
# 按10个信号为一组分批发送
signals_per_group = 10
total_groups = math.ceil(total_signals / signals_per_group)

43
start_market_scanner.sh Executable file
View File

@ -0,0 +1,43 @@
#!/bin/bash
# 市场扫描服务启动脚本
echo "🚀 启动市场扫描定时任务服务..."
# 创建日志目录
mkdir -p /app/logs
# 安装cron
echo "📦 安装cron服务..."
apt-get update && apt-get install -y cron
# 复制crontab配置
echo "⏰ 配置定时任务..."
cp /app/crontab/market-scanner /etc/cron.d/market-scanner
# 设置权限
chmod 0644 /etc/cron.d/market-scanner
# 启动cron服务
echo "🔄 启动cron守护进程..."
service cron start
# 显示已配置的任务
echo "📋 已配置的定时任务:"
crontab -l 2>/dev/null || echo "使用系统cron配置: /etc/cron.d/market-scanner"
cat /etc/cron.d/market-scanner
# 记录启动信息
echo "$(date): 市场扫描服务启动完成" >> /app/logs/scanner_startup.log
echo "✅ 市场扫描定时任务服务启动完成"
echo "📊 扫描参数: MARKET_SCAN_STOCKS=${MARKET_SCAN_STOCKS:-200}"
echo "📝 日志文件: /app/logs/market_scanner.log"
echo "⏰ Cron日志: /app/logs/cron.log"
# 执行一次初始扫描
echo "🔍 执行初始市场扫描..."
python /app/market_scanner.py ${MARKET_SCAN_STOCKS:-200}
# 保持容器运行并显示日志
echo "👁️ 监控日志输出..."
tail -f /app/logs/market_scanner.log /app/logs/cron.log

View File

@ -1,97 +0,0 @@
#!/usr/bin/env python3
"""
测试股票名称获取的缓存优化
"""
import sys
from pathlib import Path
import time
# 添加src目录到路径
current_dir = Path(__file__).parent
src_dir = current_dir / "src"
sys.path.insert(0, str(src_dir))
from loguru import logger
from src.data.data_fetcher import ADataFetcher
def test_stock_name_cache():
"""测试股票名称缓存机制"""
logger.info("🧪 开始测试股票名称缓存优化")
# 初始化数据获取器
data_fetcher = ADataFetcher()
# 测试股票列表
test_stocks = ['000001.SZ', '000002.SZ', '600000.SH', '600036.SH', '000858.SZ']
# 第一次获取股票名称(会触发缓存构建)
logger.info("📊 第一次获取股票名称(构建缓存)...")
start_time = time.time()
names_first = {}
for stock_code in test_stocks:
name = data_fetcher.get_stock_name(stock_code)
names_first[stock_code] = name
logger.info(f" {stock_code}: {name}")
first_duration = time.time() - start_time
logger.info(f"⏱️ 第一次获取耗时: {first_duration:.2f}")
# 等待一秒
time.sleep(1)
# 第二次获取股票名称(应该从缓存读取)
logger.info("📊 第二次获取股票名称(从缓存读取)...")
start_time = time.time()
names_second = {}
for stock_code in test_stocks:
name = data_fetcher.get_stock_name(stock_code)
names_second[stock_code] = name
logger.info(f" {stock_code}: {name}")
second_duration = time.time() - start_time
logger.info(f"⏱️ 第二次获取耗时: {second_duration:.2f}")
# 比较结果
logger.info("📈 性能对比:")
if second_duration < first_duration:
speedup = first_duration / second_duration
logger.info(f"✅ 缓存优化成功! 第二次比第一次快 {speedup:.1f}x")
else:
logger.warning("❌ 缓存优化效果不明显")
# 验证数据一致性
consistent = names_first == names_second
logger.info(f"🔍 数据一致性: {'✅ 一致' if consistent else '❌ 不一致'}")
# 显示缓存状态
logger.info(f"📦 当前缓存中的股票数量: {len(data_fetcher._stock_name_cache)}")
return first_duration, second_duration, consistent
if __name__ == "__main__":
# 设置日志
logger.remove()
logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}")
print("=" * 60)
print("🧪 股票名称缓存优化测试")
print("=" * 60)
try:
first_time, second_time, is_consistent = test_stock_name_cache()
print("\n" + "=" * 60)
print("📊 测试结果总结:")
print(f" 第一次获取耗时: {first_time:.2f}")
print(f" 第二次获取耗时: {second_time:.2f}")
print(f" 性能提升倍数: {first_time/second_time:.1f}x")
print(f" 数据一致性: {'✅ 通过' if is_consistent else '❌ 失败'}")
print("=" * 60)
except Exception as e:
logger.error(f"测试过程中发生错误: {e}")
import traceback
traceback.print_exc()

View File

@ -1,96 +0,0 @@
#!/usr/bin/env python3
"""
测试钉钉通知功能
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from utils.notification import DingTalkNotifier, NotificationManager
import yaml
def test_dingtalk_with_secret():
"""测试带加签的钉钉通知"""
print("🔧 测试钉钉加签功能...")
# 测试加签生成
webhook_url = "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN"
secret = "SEC6e9dbd71d4addd2c4e673fb72d686293b342da5ae48da2f8ec788a68de99f981"
notifier = DingTalkNotifier(webhook_url, secret)
# 生成签名URL
signed_url = notifier._get_signed_url()
print(f"✅ 签名URL生成成功")
print(f"📄 原始URL: {webhook_url}")
print(f"🔐 签名URL: {signed_url}")
# 检查URL格式
if "timestamp=" in signed_url and "sign=" in signed_url:
print("✅ 加签参数正确添加")
else:
print("❌ 加签参数缺失")
return False
return True
def test_notification_manager():
"""测试通知管理器配置"""
print("\n🔧 测试通知管理器配置...")
# 从配置文件读取配置
try:
with open('config/config.yaml', 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
notification_config = config.get('notification', {})
print(f"✅ 配置文件加载成功")
print(f"📄 钉钉配置: {notification_config.get('dingtalk', {})}")
# 创建通知管理器
notifier_manager = NotificationManager(notification_config)
if notifier_manager.dingtalk_notifier:
print("✅ 钉钉通知器初始化成功")
if notifier_manager.dingtalk_notifier.secret:
print("✅ 加签密钥配置正确")
else:
print("❌ 加签密钥未配置")
return False
else:
print("❌ 钉钉通知器未启用")
return False
return True
except Exception as e:
print(f"❌ 配置测试失败: {e}")
return False
def main():
print("=" * 60)
print(" 钉钉通知功能测试")
print("=" * 60)
# 测试加签功能
test1_passed = test_dingtalk_with_secret()
# 测试配置管理
test2_passed = test_notification_manager()
print("\n" + "=" * 60)
print("测试结果:")
print(f"🔐 加签功能测试: {'✅ 通过' if test1_passed else '❌ 失败'}")
print(f"⚙️ 配置管理测试: {'✅ 通过' if test2_passed else '❌ 失败'}")
if test1_passed and test2_passed:
print("\n🎉 所有测试通过!钉钉通知功能配置正确")
print("💡 注意: 需要提供完整的webhook URL才能发送实际消息")
else:
print("\n❌ 部分测试失败,请检查配置")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@ -1,268 +0,0 @@
#!/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()

View File

@ -1,191 +0,0 @@
#!/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()

View File

@ -1,179 +0,0 @@
#!/usr/bin/env python3
"""
测试价格回踩阴线最高点提醒功能
"""
import sys
from pathlib import Path
import pandas as pd
from datetime import datetime, timedelta
# 添加src目录到路径
current_dir = Path(__file__).parent
src_dir = current_dir / "src"
sys.path.insert(0, str(src_dir))
from loguru import logger
from src.utils.config_loader import config_loader
from src.data.data_fetcher import ADataFetcher
from src.utils.notification import NotificationManager
from src.strategy.kline_pattern_strategy import KLinePatternStrategy
def create_test_signal():
"""创建一个测试用的K线形态信号"""
test_signal = {
'stock_code': '000001.SZ',
'stock_name': '平安银行',
'date': datetime.now() - timedelta(days=5), # 5天前的信号
'timeframe': 'daily',
'breakout_price': 15.50, # 突破价格
'yin_high': 15.20, # 阴线最高点
'pattern_type': '两阳+阴+阳突破'
}
return test_signal
def create_test_pullback_data():
"""创建模拟回踩数据 - 模拟价格回踩到阴线最高点附近"""
dates = pd.date_range(start=datetime.now() - timedelta(days=3), end=datetime.now(), freq='D')
# 模拟价格回踩阴线最高点的情况
test_data = []
yin_high = 15.20 # 阴线最高点价格
initial_price = 15.50 # 突破价格
# 设计价格走势:从突破价格逐步回调到阴线最高点附近
price_path = [15.45, 15.35, 15.25, 15.18] # 最后一个价格接近阴线最高点
for i, date in enumerate(dates):
if i < len(price_path):
close_price = price_path[i]
else:
close_price = yin_high - 0.02 # 稍低于阴线最高点
low_price = close_price - 0.03 # 最低价更接近阴线最高点
test_data.append({
'trade_date': date,
'open': close_price + 0.02,
'high': close_price + 0.05,
'low': low_price,
'close': close_price,
'volume': 1000000
})
return pd.DataFrame(test_data)
def test_pullback_detection():
"""测试回踩检测功能"""
logger.info("🧪 开始测试价格回踩阴线最高点提醒功能")
# 初始化配置
config = config_loader.load_config()
# 初始化组件
data_fetcher = ADataFetcher()
notification_config = config.get('notification', {})
notification_manager = NotificationManager(notification_config)
# 获取K线形态策略配置
kline_config = config.get('strategy', {}).get('kline_pattern', {})
strategy = KLinePatternStrategy(data_fetcher, notification_manager, kline_config)
# 创建测试信号
test_signal = create_test_signal()
logger.info(f"📊 创建测试信号: {test_signal['stock_code']}({test_signal['stock_name']})")
logger.info(f" - 信号日期: {test_signal['date']}")
logger.info(f" - 突破价格: {test_signal['breakout_price']}")
logger.info(f" - 阴线最高点: {test_signal['yin_high']}")
# 添加到策略的监控列表
strategy.add_triggered_signal(test_signal)
logger.info("✅ 测试信号已添加到监控列表")
# 创建模拟回踩数据
test_pullback_data = create_test_pullback_data()
logger.info(f"📈 创建模拟K线数据{len(test_pullback_data)}条记录")
print("\n模拟K线数据:")
for _, row in test_pullback_data.iterrows():
print(f" {row['trade_date'].strftime('%Y-%m-%d')}: "
f"{row['open']:.2f}{row['high']:.2f}{row['low']:.2f}{row['close']:.2f}")
# 检测回踩情况
logger.info("🔍 开始检测回踩情况...")
pullback_alerts = strategy.check_pullback_signals(test_signal['stock_code'], test_pullback_data)
if pullback_alerts:
logger.info(f"⚠️ 检测到 {len(pullback_alerts)} 个回踩提醒")
for i, alert in enumerate(pullback_alerts, 1):
logger.info(f" 提醒{i}: {alert['stock_code']} - 当前价格{alert['current_price']:.2f}"
f"回调{alert['pullback_pct']:.2f}%,距阴线高点{alert['distance_to_yin_high']:.2f}%")
# 测试通知发送(如果启用了钉钉通知)
if notification_config.get('dingtalk', {}).get('enabled', False):
logger.info("📱 测试发送回踩提醒通知...")
success = notification_manager.send_pullback_alerts(pullback_alerts)
if success:
logger.info("✅ 回踩提醒通知发送成功")
else:
logger.warning("❌ 回踩提醒通知发送失败")
else:
logger.info(" 钉钉通知未启用,跳过通知发送测试")
else:
logger.info(" 未检测到回踩情况")
# 测试完整的监控流程
logger.info("\n🔍 测试完整的回踩监控流程...")
all_pullback_alerts = strategy.monitor_pullback_for_triggered_signals()
logger.info("🎯 测试完成!")
if all_pullback_alerts:
logger.info(f"✅ 成功检测到 {len(all_pullback_alerts)} 个回踩提醒")
else:
logger.info(" 当前监控中无回踩情况")
def test_strategy_config():
"""测试策略配置是否正确加载"""
logger.info("🔧 测试策略配置加载...")
config = config_loader.load_config()
kline_config = config.get('strategy', {}).get('kline_pattern', {})
logger.info("📋 当前K线形态策略配置:")
logger.info(f" - 启用状态: {kline_config.get('enabled', False)}")
logger.info(f" - 前两阳线实体比例: {kline_config.get('min_entity_ratio', 0.55)}")
logger.info(f" - 最后阳线实体比例: {kline_config.get('final_yang_min_ratio', 0.40)}")
logger.info(f" - 回踩容忍度: {kline_config.get('pullback_tolerance', 0.02)}")
logger.info(f" - 监控天数: {kline_config.get('monitor_days', 30)}")
logger.info(f" - 支持时间周期: {kline_config.get('timeframes', ['daily'])}")
if __name__ == "__main__":
# 设置日志
logger.remove()
logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}")
print("=" * 60)
print("🧪 价格回踩阴线最高点提醒功能测试")
print("=" * 60)
try:
# 测试配置加载
test_strategy_config()
print()
# 测试回踩检测功能
test_pullback_detection()
except Exception as e:
logger.error(f"测试过程中发生错误: {e}")
import traceback
traceback.print_exc()
print("\n" + "=" * 60)
print("🏁 测试结束")
print("=" * 60)

View File

@ -1,135 +0,0 @@
#!/usr/bin/env python3
"""
舆情数据功能测试脚本
"""
import sys
from pathlib import Path
# 将src目录添加到Python路径
current_dir = Path(__file__).parent
src_dir = current_dir / "src"
sys.path.insert(0, str(src_dir))
from src.data.sentiment_fetcher import SentimentFetcher
def test_sentiment_features():
"""测试舆情功能"""
print("="*60)
print(" A股舆情数据功能测试")
print("="*60)
fetcher = SentimentFetcher()
# 1. 测试北向资金
print("\n🌊 1. 北向资金数据测试")
print("-" * 30)
current_flow = fetcher.get_north_flow_current()
if not current_flow.empty:
row = current_flow.iloc[0]
print(f"总净流入: {row.get('net_tgt', 'N/A')} 万元")
print(f"沪股通: {row.get('net_hgt', 'N/A')} 万元")
print(f"深股通: {row.get('net_sgt', 'N/A')} 万元")
print(f"更新时间: {row.get('trade_time', 'N/A')}")
else:
print("未获取到当前北向资金数据")
# 2. 测试热门股票
print("\n🔥 2. 热门股票数据测试")
print("-" * 30)
hot_stocks = fetcher.get_popular_stocks_east_100()
if not hot_stocks.empty:
print(f"东财人气股票TOP5:")
for idx, row in hot_stocks.head(5).iterrows():
code = row.get('stock_code', 'N/A')
name = row.get('short_name', 'N/A')
print(f" {idx + 1}. {code} - {name}")
else:
print("未获取到热门股票数据")
# 3. 测试龙虎榜
print("\n🐉 3. 龙虎榜数据测试")
print("-" * 30)
dragon_tiger = fetcher.get_dragon_tiger_list_daily()
if not dragon_tiger.empty:
print(f"今日龙虎榜 (共{len(dragon_tiger)}只股票):")
for idx, row in dragon_tiger.head(5).iterrows():
code = row.get('stock_code', 'N/A')
name = row.get('short_name', 'N/A')
reason = row.get('reason', 'N/A')
print(f" {idx + 1}. {code} - {name}")
print(f" 上榜原因: {reason}")
else:
print("今日无龙虎榜数据")
# 4. 测试热门概念
print("\n💡 4. 热门概念数据测试")
print("-" * 30)
try:
hot_concepts = fetcher.get_hot_concept_ths_20()
if not hot_concepts.empty:
print(f"同花顺热门概念TOP5:")
for idx, row in hot_concepts.head(5).iterrows():
name = row.get('concept_name', 'N/A')
change_pct = row.get('change_pct', 'N/A')
print(f" {idx + 1}. {name} (涨跌幅: {change_pct}%)")
else:
print("未获取到热门概念数据")
except Exception as e:
print(f"热门概念获取失败: {e}")
# 5. 测试市场舆情综合概览
print("\n📊 5. 市场舆情综合概览测试")
print("-" * 30)
try:
overview = fetcher.get_market_sentiment_overview()
if overview:
print("✅ 市场舆情概览获取成功")
# 北向资金
if 'north_flow' in overview:
north_data = overview['north_flow']
print(f"北向资金: 总净流入 {north_data.get('net_total', 'N/A')} 万元")
# 热门股票
if 'hot_stocks_east' in overview and not overview['hot_stocks_east'].empty:
count = len(overview['hot_stocks_east'])
print(f"热门股票: 获取到 {count}")
# 龙虎榜
if 'dragon_tiger' in overview and not overview['dragon_tiger'].empty:
count = len(overview['dragon_tiger'])
print(f"龙虎榜: 获取到 {count}")
else:
print("市场舆情概览获取失败")
except Exception as e:
print(f"市场舆情概览测试失败: {e}")
# 6. 测试个股舆情分析
print("\n🔍 6. 个股舆情分析测试")
print("-" * 30)
test_stock = "000001.SZ" # 平安银行
try:
analysis = fetcher.analyze_stock_sentiment(test_stock)
if 'error' not in analysis:
print(f"{test_stock} 舆情分析成功")
print(f"东财人气榜: {'在榜' if analysis.get('in_popular_east', False) else '不在榜'}")
print(f"同花顺热门榜: {'在榜' if analysis.get('in_hot_ths', False) else '不在榜'}")
if 'dragon_tiger' in analysis and not analysis['dragon_tiger'].empty:
print("✅ 今日上榜龙虎榜")
else:
print("❌ 今日未上榜龙虎榜")
else:
print(f"个股舆情分析失败: {analysis.get('error', '未知错误')}")
except Exception as e:
print(f"个股舆情分析测试失败: {e}")
print("\n" + "="*60)
print(" 舆情数据功能测试完成")
print("="*60)
if __name__ == "__main__":
test_sentiment_features()

View File

@ -1,185 +0,0 @@
#!/usr/bin/env python3
"""
K线形态策略测试脚本
"""
import sys
from pathlib import Path
import pandas as pd
import numpy as np
# 将src目录添加到Python路径
current_dir = Path(__file__).parent
src_dir = current_dir / "src"
sys.path.insert(0, str(src_dir))
from src.data.data_fetcher import ADataFetcher
from src.utils.notification import NotificationManager
from src.strategy.kline_pattern_strategy import KLinePatternStrategy
def create_test_kline_data():
"""创建测试K线数据 - 包含两阳线+阴线+阳线形态"""
dates = pd.date_range('2023-01-01', periods=10, freq='D')
# 模拟K线数据
test_data = {
'trade_date': dates,
'open': [10.0, 10.5, 11.0, 12.0, 11.5, 11.0, 10.5, 11.0, 11.8, 12.5],
'high': [10.8, 11.2, 11.8, 12.5, 12.0, 11.5, 11.2, 11.5, 12.2, 13.0],
'low': [9.8, 10.3, 10.8, 11.8, 10.8, 10.5, 10.2, 10.8, 11.6, 12.3],
'close':[10.6, 11.0, 11.5, 12.2, 11.0, 10.8, 11.2, 11.3, 12.0, 12.8],
'volume': [1000] * 10
}
df = pd.DataFrame(test_data)
print("测试K线数据:")
print(df)
print()
return df
def test_pattern_detection():
"""测试形态检测功能"""
print("="*60)
print(" K线形态检测功能测试")
print("="*60)
# 创建测试配置
strategy_config = {
'min_entity_ratio': 0.55,
'timeframes': ['daily'],
'scan_stocks_count': 10,
'analysis_days': 60
}
notification_config = {
'dingtalk': {
'enabled': False,
'webhook_url': ''
}
}
# 初始化组件
data_fetcher = ADataFetcher()
notification_manager = NotificationManager(notification_config)
strategy = KLinePatternStrategy(data_fetcher, notification_manager, strategy_config)
print("1. 策略信息:")
print(strategy.get_strategy_summary())
print("\n2. 测试K线特征计算:")
test_df = create_test_kline_data()
df_with_features = strategy.calculate_kline_features(test_df)
print("添加特征后的数据:")
relevant_cols = ['trade_date', 'open', 'high', 'low', 'close', 'is_yang', 'is_yin', 'entity_ratio']
print(df_with_features[relevant_cols])
print("\n3. 测试形态检测:")
signals = strategy.detect_pattern(df_with_features)
if signals:
print(f"发现 {len(signals)} 个形态信号:")
for i, signal in enumerate(signals, 1):
print(f"\n信号 {i}:")
print(f" 日期: {signal['date']}")
print(f" 形态: {signal['pattern_type']}")
print(f" 突破价格: {signal['breakout_price']:.2f}")
print(f" 突破幅度: {signal['breakout_pct']:.2f}%")
print(f" 阳线1实体比例: {signal['yang1_entity_ratio']:.1%}")
print(f" 阳线2实体比例: {signal['yang2_entity_ratio']:.1%}")
else:
print("未发现形态信号")
print("\n4. 测试真实股票数据:")
test_stocks = ["000001.SZ", "000002.SZ"] # 平安银行、万科A
for stock_code in test_stocks:
print(f"\n分析股票: {stock_code}")
try:
results = strategy.analyze_stock(stock_code, days=30) # 分析最近30天
total_signals = sum(len(signals) for signals in results.values())
print(f"总信号数: {total_signals}")
for timeframe, signals in results.items():
if signals:
print(f"{timeframe}: {len(signals)}个信号")
# 显示最新信号
latest = signals[-1]
print(f" 最新: {latest['date']} {latest['breakout_price']:.2f}")
else:
print(f"{timeframe}: 无信号")
except Exception as e:
print(f"分析失败: {e}")
print("\n5. 测试通知功能:")
try:
# 测试日志通知
notification_manager.send_strategy_signal(
stock_code="TEST001",
stock_name="测试股票",
timeframe="daily",
signal_type="测试信号",
price=15.50,
additional_info={
"阳线1实体比例": "65%",
"阳线2实体比例": "70%",
"突破幅度": "2.5%"
}
)
print("✅ 通知功能测试完成(日志记录)")
except Exception as e:
print(f"❌ 通知功能测试失败: {e}")
print("\n" + "="*60)
print(" 策略测试完成")
print("="*60)
def test_weekly_monthly_conversion():
"""测试周线月线转换功能"""
print("\n测试周线/月线数据转换:")
# 创建更多天数的测试数据
dates = pd.date_range('2023-01-01', periods=50, freq='D')
test_data = {
'trade_date': dates,
'open': np.random.uniform(10, 15, 50),
'high': np.random.uniform(15, 20, 50),
'low': np.random.uniform(8, 12, 50),
'close': np.random.uniform(10, 15, 50),
'volume': np.random.randint(1000, 5000, 50)
}
daily_df = pd.DataFrame(test_data)
strategy_config = {'min_entity_ratio': 0.55, 'timeframes': ['daily']}
notification_config = {'dingtalk': {'enabled': False}}
data_fetcher = ADataFetcher()
notification_manager = NotificationManager(notification_config)
strategy = KLinePatternStrategy(data_fetcher, notification_manager, strategy_config)
# 测试周线转换
weekly_df = strategy._convert_to_weekly(daily_df)
print(f"日线数据: {len(daily_df)}")
print(f"周线数据: {len(weekly_df)}")
# 测试月线转换
monthly_df = strategy._convert_to_monthly(daily_df)
print(f"月线数据: {len(monthly_df)}")
if not weekly_df.empty:
print("\n周线数据样本:")
print(weekly_df[['trade_date', 'open', 'high', 'low', 'close']].head())
if __name__ == "__main__":
test_pattern_detection()
test_weekly_monthly_conversion()

View File

@ -1,313 +0,0 @@
#!/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)}")
# 条件220周新高
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()

View File

@ -1,318 +0,0 @@
#!/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()

View File

@ -1,238 +0,0 @@
#!/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()

View File

@ -1,244 +0,0 @@
#!/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()

View File

@ -1,258 +0,0 @@
#!/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()

View File

@ -29,28 +29,9 @@ config_loader = ConfigLoader()
@app.route('/')
def index():
"""首页 - 显示信号概览和统计"""
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))
"""首页 - 重定向到信号页面"""
from flask import redirect, url_for
return redirect(url_for('signals'))
@app.route('/signals')
@ -60,7 +41,7 @@ def signals():
# 获取查询参数
strategy_name = request.args.get('strategy', '')
timeframe = request.args.get('timeframe', '')
days = int(request.args.get('days', 30))
days = int(request.args.get('days', 7)) # 默认显示7天内的信号
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 20))
@ -154,20 +135,6 @@ def signals():
return render_template('error.html', error=str(e))
@app.route('/pullbacks')
def pullbacks():
"""回踩监控页面"""
try:
days = int(request.args.get('days', 30))
pullback_alerts = db_manager.get_pullback_alerts(days=days)
return render_template('pullbacks.html',
pullback_alerts=pullback_alerts.to_dict('records') if not pullback_alerts.empty else [],
days=days)
except Exception as e:
logger.error(f"回踩监控页面数据加载失败: {e}")
return render_template('error.html', error=str(e))
@app.route('/api/signals')
def api_signals():
@ -206,25 +173,11 @@ def api_stats():
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/pullbacks')
def api_pullbacks():
"""API接口 - 获取回踩提醒"""
try:
days = int(request.args.get('days', 7))
pullback_alerts = db_manager.get_pullback_alerts(days=days)
return jsonify({
'success': True,
'data': pullback_alerts.to_dict('records') if not pullback_alerts.empty else []
})
except Exception as e:
logger.error(f"API获取回踩提醒失败: {e}")
return jsonify({'success': False, 'error': str(e)})
@app.template_filter('datetime_format')
def datetime_format(value, format='%Y-%m-%d %H:%M'):
"""日期时间格式化过滤器 - 转换为东八区时间"""
"""日期时间格式化过滤器 - 智能处理时区转换"""
if value is None:
return ''
@ -245,10 +198,17 @@ def datetime_format(value, format='%Y-%m-%d %H:%M'):
if isinstance(value, datetime) and value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
# 转换为东八区时间 (UTC+8)
# 智能时区转换:检查是否已经是东八区时间
if isinstance(value, datetime) and value.tzinfo is not None:
china_tz = timezone(timedelta(hours=8))
value = value.astimezone(china_tz)
# 如果已经是东八区时间,直接使用;否则转换
if value.utcoffset() == timedelta(hours=8):
# 已经是东八区时间,无需转换
pass
else:
# 转换为东八区时间
value = value.astimezone(china_tz)
return value.strftime(format)
@ -284,4 +244,4 @@ if __name__ == '__main__':
print(f"📋 数据库: {db_manager.config.database}")
print("=" * 60)
app.run(host='0.0.0.0', port=8080, debug=True)
app.run(host='0.0.0.0', port=8081, debug=True)