update1
This commit is contained in:
parent
6ec1feb2a7
commit
ae928c198b
267
CRYPTO_README.md
Normal file
267
CRYPTO_README.md
Normal file
@ -0,0 +1,267 @@
|
||||
# 加密货币市场K线形态策略使用指南
|
||||
|
||||
本项目已支持将K线形态策略应用于加密货币市场(Binance),可以同时扫描A股和加密货币市场。
|
||||
|
||||
## 新增功能
|
||||
|
||||
### 1. Binance数据获取器
|
||||
- **文件**: `src/data/binance_fetcher.py`
|
||||
- **功能**:
|
||||
- 获取Binance交易所的K线数据
|
||||
- 支持多种时间周期(1d, 4h, 1h等)
|
||||
- 获取热门交易对(按24h交易量排序)
|
||||
- 支持交易对搜索和筛选
|
||||
- 自动缓存机制,减少API调用
|
||||
|
||||
### 2. 加密货币K线形态策略
|
||||
- **文件**: `src/strategy/crypto_kline_pattern_strategy.py`
|
||||
- **特点**:
|
||||
- 继承自原有的`KLinePatternStrategy`
|
||||
- 适配加密货币市场特点(24小时交易、高波动性)
|
||||
- 支持USDT、BTC等多种计价货币
|
||||
- 可配置最小交易量过滤
|
||||
|
||||
### 3. 加密货币市场扫描脚本
|
||||
- **文件**: `crypto_scanner.py`
|
||||
- **功能**: 扫描Binance热门交易对,识别K线形态信号
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装python-binance库
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
或单独安装:
|
||||
```bash
|
||||
pip install python-binance>=1.0.19
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 1. 编辑配置文件
|
||||
打开 `config/config.yaml`,配置Binance API(可选):
|
||||
|
||||
```yaml
|
||||
data_source:
|
||||
# Binance API配置(获取公开数据不需要API密钥)
|
||||
binance_api_key: "" # 可选
|
||||
binance_api_secret: "" # 可选
|
||||
binance_testnet: false # 是否使用测试网
|
||||
```
|
||||
|
||||
### 2. 策略参数配置
|
||||
在 `config/config.yaml` 中配置加密货币策略参数:
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
crypto_kline_pattern:
|
||||
enabled: true # 是否启用
|
||||
min_entity_ratio: 0.55 # 阳线实体最小占比55%
|
||||
final_yang_min_ratio: 0.40 # 突破阳线实体最小占比40%
|
||||
max_turnover_ratio: 100.0 # 最大换手率(加密货币较高)
|
||||
timeframes: ["4hour", "daily", "weekly"] # 时间周期:4小时、日线、周线
|
||||
scan_symbols_count: 100 # 扫描交易对数量
|
||||
quote_asset: "USDT" # 计价货币
|
||||
min_volume_usdt: 1000000 # 最小24h交易量(美元)
|
||||
|
||||
# 回踩监控配置
|
||||
pullback_tolerance: 0.02 # 回踩容忍度2%
|
||||
monitor_days: 30 # 监控天数
|
||||
pullback_confirmation_days: 7 # 回踩确认天数
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 方法1: 使用启动脚本(推荐)
|
||||
```bash
|
||||
# 扫描100个热门交易对
|
||||
./start_crypto_scanner.sh
|
||||
|
||||
# 扫描50个热门交易对
|
||||
./start_crypto_scanner.sh 50
|
||||
|
||||
# 扫描200个热门交易对
|
||||
./start_crypto_scanner.sh 200
|
||||
```
|
||||
|
||||
### 方法2: 直接运行Python脚本
|
||||
```bash
|
||||
# 扫描100个热门交易对(默认)
|
||||
python crypto_scanner.py
|
||||
|
||||
# 扫描指定数量的交易对
|
||||
python crypto_scanner.py 50
|
||||
```
|
||||
|
||||
### 方法3: 在代码中使用
|
||||
```python
|
||||
from src.data.binance_fetcher import BinanceFetcher
|
||||
from src.strategy.crypto_kline_pattern_strategy import CryptoKLinePatternStrategy
|
||||
from src.utils.notification import NotificationManager
|
||||
|
||||
# 初始化数据获取器
|
||||
data_fetcher = BinanceFetcher()
|
||||
|
||||
# 初始化策略
|
||||
strategy_config = {
|
||||
'min_entity_ratio': 0.55,
|
||||
'timeframes': ['4hour', 'daily', 'weekly'], # 4小时、日线、周线
|
||||
'quote_asset': 'USDT'
|
||||
}
|
||||
notification_config = {'dingtalk': {'enabled': False}}
|
||||
notification_manager = NotificationManager(notification_config)
|
||||
|
||||
strategy = CryptoKLinePatternStrategy(
|
||||
data_fetcher,
|
||||
notification_manager,
|
||||
strategy_config
|
||||
)
|
||||
|
||||
# 扫描市场
|
||||
results = strategy.scan_market(max_symbols=100)
|
||||
```
|
||||
|
||||
## 定时任务配置
|
||||
|
||||
### 使用crontab定时扫描
|
||||
编辑 `crontab/crypto-scanner` 文件已包含预设的定时任务:
|
||||
|
||||
```bash
|
||||
# 每天早上8点扫描
|
||||
0 8 * * * root cd /app && python crypto_scanner.py 100
|
||||
|
||||
# 每天下午4点扫描
|
||||
0 16 * * * root cd /app && python crypto_scanner.py 100
|
||||
|
||||
# 每天晚上12点扫描
|
||||
0 0 * * * root cd /app && python crypto_scanner.py 100
|
||||
|
||||
# 每4小时扫描(适合4小时K线)
|
||||
0 */4 * * * root cd /app && python crypto_scanner.py 50
|
||||
|
||||
# 周日深度扫描
|
||||
0 10 * * 0 root cd /app && python crypto_scanner.py 200
|
||||
```
|
||||
|
||||
加载定时任务:
|
||||
```bash
|
||||
# 在Docker容器中
|
||||
crontab crontab/crypto-scanner
|
||||
|
||||
# 或在本地系统
|
||||
crontab -e
|
||||
# 然后复制crypto-scanner内容
|
||||
```
|
||||
|
||||
## 策略说明
|
||||
|
||||
### K线形态识别
|
||||
与A股策略相同,识别"两阳线+阴线+突破阳线"形态:
|
||||
|
||||
1. **基础形态**(前3根K线): 阳线 + 阳线 + 阴线
|
||||
2. **前两根阳线**: 实体部分占振幅55%以上
|
||||
3. **突破确认**: 第4/5/6根K线中任意一根突破阴线最高价
|
||||
4. **突破阳线**: 实体占振幅40%以上,收盘在EMA20上方
|
||||
5. **回踩确认**: 价格先创新高,再回踩到阴线最高价附近
|
||||
|
||||
### 加密货币市场特点
|
||||
- **24小时交易**: 没有开盘收盘限制
|
||||
- **高波动性**: 换手率阈值设置较高(100%)
|
||||
- **支持多周期**: 4小时线(4h)、日线(1d)、周线(1w)
|
||||
- **实时数据**: 可以随时获取最新K线数据
|
||||
|
||||
## 输出结果
|
||||
|
||||
### 日志文件
|
||||
- 位置: `logs/crypto_scanner_YYYY-MM-DD.log`
|
||||
- 包含: 详细的扫描过程、信号发现、错误信息
|
||||
|
||||
### 数据库存储
|
||||
扫描结果会自动保存到MySQL数据库:
|
||||
- 扫描会话信息
|
||||
- 发现的信号详情
|
||||
- 形态形成记录
|
||||
|
||||
### 钉钉通知
|
||||
如果配置了钉钉webhook,会自动发送:
|
||||
- 信号汇总通知(每10个信号一组)
|
||||
- 回踩提醒(每5个提醒一组)
|
||||
- 包含交易对、价格、形态类型等关键信息
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 如何选择扫描的交易对数量?
|
||||
- **日常监控**: 50-100个交易对,速度快
|
||||
- **全面扫描**: 100-200个交易对,覆盖更全
|
||||
- **深度分析**: 200+个交易对,周末或特殊时段
|
||||
|
||||
### 2. 需要Binance API密钥吗?
|
||||
- **不需要**: 获取K线、ticker等公开数据不需要API密钥
|
||||
- **需要**: 如果要查询账户信息、下单等操作才需要
|
||||
|
||||
### 3. 时间周期如何选择?
|
||||
- **weekly(周线)**: 适合长线信号,信号质量最高,趋势更明确
|
||||
- **daily(日线)**: 适合中线信号,平衡信号质量和频率
|
||||
- **4hour(4小时)**: 适合短线交易,信号更频繁,适合波段操作
|
||||
|
||||
### 4. 与A股策略有什么区别?
|
||||
- **数据源**: Binance API vs Tushare API
|
||||
- **交易时间**: 24小时 vs 交易日限制
|
||||
- **换手率**: 阈值更高,适应加密货币高波动
|
||||
- **计价单位**: USDT/BTC vs 人民币
|
||||
|
||||
### 5. 如何同时运行A股和加密货币扫描?
|
||||
两个脚本独立运行,互不影响:
|
||||
```bash
|
||||
# A股扫描
|
||||
./start_market_scanner.sh 200
|
||||
|
||||
# 加密货币扫描
|
||||
./start_crypto_scanner.sh 100
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **API限制**: Binance对API调用频率有限制,建议控制扫描频率
|
||||
2. **网络连接**: 需要稳定的网络连接访问Binance API
|
||||
3. **数据准确性**: 加密货币市场波动大,建议结合其他指标综合判断
|
||||
4. **风险提示**: 本工具仅用于技术分析,不构成投资建议
|
||||
|
||||
## 技术架构
|
||||
|
||||
```
|
||||
TradingAI/
|
||||
├── src/
|
||||
│ ├── data/
|
||||
│ │ ├── binance_fetcher.py # Binance数据获取器(新增)
|
||||
│ │ └── tushare_fetcher.py # A股数据获取器(原有)
|
||||
│ └── strategy/
|
||||
│ ├── crypto_kline_pattern_strategy.py # 加密货币策略(新增)
|
||||
│ └── kline_pattern_strategy.py # A股策略(原有)
|
||||
├── crypto_scanner.py # 加密货币扫描脚本(新增)
|
||||
├── market_scanner.py # A股扫描脚本(原有)
|
||||
├── start_crypto_scanner.sh # 加密货币启动脚本(新增)
|
||||
├── start_market_scanner.sh # A股启动脚本(原有)
|
||||
└── crontab/
|
||||
├── crypto-scanner # 加密货币定时任务(新增)
|
||||
└── market-scanner # A股定时任务(原有)
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.1.0 (2025-10-12)
|
||||
- ✨ 新增Binance数据获取器
|
||||
- ✨ 新增加密货币K线形态策略
|
||||
- ✨ 新增加密货币市场扫描脚本
|
||||
- ✨ 新增定时任务配置
|
||||
- 📝 更新配置文件支持加密货币
|
||||
- 📝 新增使用文档
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交Issue和Pull Request来改进此功能!
|
||||
|
||||
## 许可
|
||||
|
||||
与主项目保持一致
|
||||
@ -36,7 +36,8 @@ COPY . .
|
||||
RUN mkdir -p /app/logs /app/config
|
||||
|
||||
# 设置权限
|
||||
RUN chmod +x start_mysql_web.py
|
||||
RUN chmod +x start_mysql_web.py && \
|
||||
chmod +x start_market_scanner.sh
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
54
README.md
54
README.md
@ -22,14 +22,21 @@ TradingAI/
|
||||
├── src/ # 源代码
|
||||
│ ├── data/ # 数据获取模块
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── tushare_fetcher.py # 行情数据获取
|
||||
│ │ └── sentiment_fetcher.py # 舆情数据获取
|
||||
│ │ ├── tushare_fetcher.py # A股行情数据获取
|
||||
│ │ ├── binance_fetcher.py # 加密货币数据获取
|
||||
│ │ └── stock_pool_manager.py # 股票池管理
|
||||
│ ├── strategy/ # 策略模块
|
||||
│ ├── monitor/ # 监控模块
|
||||
│ │ ├── kline_pattern_strategy.py # K线形态策略
|
||||
│ │ └── crypto_kline_pattern_strategy.py # 加密货币K线策略
|
||||
│ ├── database/ # 数据库模块
|
||||
│ │ └── mysql_database_manager.py
|
||||
│ ├── execution/ # 执行模块
|
||||
│ │ ├── strategy_executor.py # 策略执行器
|
||||
│ │ └── task_scheduler.py # 任务调度器
|
||||
│ └── utils/ # 工具模块
|
||||
│ ├── __init__.py
|
||||
│ └── config_loader.py
|
||||
├── tests/ # 测试文件
|
||||
│ ├── config_loader.py
|
||||
│ └── notification.py
|
||||
├── logs/ # 日志文件
|
||||
└── data/ # 数据文件
|
||||
```
|
||||
@ -165,13 +172,42 @@ hist_data = fetcher.get_historical_data(
|
||||
- [ ] Web界面
|
||||
- [ ] 报警通知系统
|
||||
|
||||
## Docker部署
|
||||
|
||||
项目支持Docker容器化部署,包含Web界面和定时扫描功能:
|
||||
|
||||
### 启动服务
|
||||
```bash
|
||||
# 启动所有服务
|
||||
docker-compose up -d
|
||||
|
||||
# 仅启动Web服务
|
||||
docker-compose up -d trading-web
|
||||
|
||||
# 仅启动市场扫描服务
|
||||
docker-compose up -d trading-market-scanner
|
||||
```
|
||||
|
||||
### 服务说明
|
||||
- **trading-web**: Web界面服务 (端口8080)
|
||||
- **trading-market-scanner**: 定时市场扫描服务 (每晚8点执行)
|
||||
|
||||
### 查看日志
|
||||
```bash
|
||||
# 查看Web服务日志
|
||||
docker-compose logs -f trading-web
|
||||
|
||||
# 查看扫描服务日志
|
||||
docker-compose logs -f trading-market-scanner
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 首次使用需要确保网络连接正常,TuShare需要从网络获取数据
|
||||
2. 请合理使用数据接口,避免频繁请求
|
||||
3. 舆情数据仅供参考,投资需谨慎
|
||||
4. 本系统仅供学习和研究使用,不构成投资建议
|
||||
5. 实盘交易请谨慎,注意风险控制
|
||||
2. 请合理使用数据接口,避免频繁请求
|
||||
3. 本系统仅供学习和研究使用,不构成投资建议
|
||||
4. 实盘交易请谨慎,注意风险控制
|
||||
5. 确保配置文件中的TuShare Token和数据库连接信息正确
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
清理MySQL数据库,重新开始迁移
|
||||
"""
|
||||
|
||||
import pymysql
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
|
||||
# 添加项目根目录到路径
|
||||
current_dir = Path(__file__).parent
|
||||
sys.path.insert(0, str(current_dir))
|
||||
|
||||
from config.mysql_config import MYSQL_CONFIG
|
||||
|
||||
|
||||
def clean_mysql_database():
|
||||
"""清理MySQL数据库"""
|
||||
logger.info("🧹 清理MySQL数据库...")
|
||||
|
||||
try:
|
||||
with pymysql.connect(**MYSQL_CONFIG.to_dict()) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 删除视图
|
||||
try:
|
||||
cursor.execute("DROP VIEW IF EXISTS latest_signals_view")
|
||||
cursor.execute("DROP VIEW IF EXISTS strategy_stats_view")
|
||||
logger.info("✅ 删除视图")
|
||||
except Exception as e:
|
||||
logger.warning(f"删除视图警告: {e}")
|
||||
|
||||
# 删除表(注意外键约束顺序)
|
||||
tables = ['pullback_alerts', 'stock_signals', 'scan_sessions', 'strategies']
|
||||
|
||||
for table in tables:
|
||||
try:
|
||||
cursor.execute(f"DROP TABLE IF EXISTS {table}")
|
||||
logger.info(f"✅ 删除表: {table}")
|
||||
except Exception as e:
|
||||
logger.warning(f"删除表 {table} 警告: {e}")
|
||||
|
||||
conn.commit()
|
||||
logger.info("✅ MySQL数据库清理完成")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 清理MySQL数据库失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
clean_mysql_database()
|
||||
@ -32,6 +32,11 @@ data_source:
|
||||
# Tushare Pro配置
|
||||
tushare_token: "0ed6419a00d8923dc19c0b58fc92d94c9a0696949ab91a13aa58a0cc"
|
||||
|
||||
# Binance API配置
|
||||
binance_api_key: "" # Binance API Key (可选,公开数据不需要)
|
||||
binance_api_secret: "" # Binance API Secret (可选,公开数据不需要)
|
||||
binance_testnet: false # 是否使用测试网
|
||||
|
||||
# 数据更新频率
|
||||
update_frequency:
|
||||
realtime: "1min" # 实时数据更新频率
|
||||
@ -69,6 +74,39 @@ strategy:
|
||||
# 回踩监控配置
|
||||
pullback_tolerance: 0.02 # 回踩容忍度(2%),价格接近阴线最高点的阈值
|
||||
monitor_days: 30 # 监控回踩的天数(信号触发后30天内监控)
|
||||
pullback_confirmation_days: 7 # 回踩确认窗口天数(模式识别后N天内检查创新高回踩)
|
||||
use_1h_confirmation: false # 是否使用1h级别数据确认回踩(Tushare分钟线接口有频率限制:每分钟最多2次)
|
||||
max_turnover_ratio: 40.0 # 最后阳线最大换手率(%)
|
||||
|
||||
# 多头排列配置
|
||||
bull_alignment:
|
||||
enabled: true # 是否启用多头排列过滤
|
||||
ema_periods: [5, 10, 20] # 使用的EMA周期(短、中、长)
|
||||
check_slope: true # 是否检查均线向上倾斜
|
||||
slope_lookback: 3 # 斜率计算回看周期(根K线)
|
||||
|
||||
# 加密货币K线形态策略配置
|
||||
crypto_kline_pattern:
|
||||
enabled: true # 是否启用加密货币K线形态策略
|
||||
min_entity_ratio: 0.55 # 前两根阳线实体最小占振幅比例(55%)
|
||||
final_yang_min_ratio: 0.40 # 最后阳线实体最小占振幅比例(40%)
|
||||
max_turnover_ratio: 100.0 # 最大换手率(加密货币通常更高)
|
||||
timeframes: ["4hour", "daily", "weekly"] # 支持的时间周期:4小时、日线、周线
|
||||
scan_symbols_count: 100 # 扫描交易对数量限制
|
||||
quote_asset: "USDT" # 计价货币
|
||||
min_volume_usdt: 1000000 # 最小24h交易量(USDT)
|
||||
|
||||
# 回踩监控配置
|
||||
pullback_tolerance: 0.02 # 回踩容忍度(2%)
|
||||
monitor_days: 30 # 监控天数
|
||||
pullback_confirmation_days: 7 # 回踩确认天数
|
||||
|
||||
# 多头排列配置
|
||||
bull_alignment:
|
||||
enabled: true # 是否启用多头排列过滤
|
||||
ema_periods: [5, 10, 20] # 使用的EMA周期(短、中、长)
|
||||
check_slope: true # 是否检查均线向上倾斜
|
||||
slope_lookback: 3 # 斜率计算回看周期(根K线)
|
||||
|
||||
# 监控配置
|
||||
monitor:
|
||||
|
||||
27
crontab/crypto-scanner
Normal file
27
crontab/crypto-scanner
Normal file
@ -0,0 +1,27 @@
|
||||
# 加密货币市场扫描定时任务配置
|
||||
# 格式: 分钟 小时 日 月 星期 用户 命令
|
||||
# 时区: Asia/Shanghai (加密货币市场24小时交易)
|
||||
|
||||
# 设置环境变量
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/bin:/usr/bin:/bin
|
||||
PYTHONPATH=/app
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# 每天早上8点扫描一次 (可以看隔夜行情)
|
||||
0 8 * * * root cd /app && /usr/local/bin/python crypto_scanner.py 100 >> /app/logs/crypto_cron.log 2>&1
|
||||
|
||||
# 每天下午4点扫描一次 (下午行情)
|
||||
0 16 * * * root cd /app && /usr/local/bin/python crypto_scanner.py 100 >> /app/logs/crypto_cron.log 2>&1
|
||||
|
||||
# 每天晚上12点扫描一次 (晚间行情)
|
||||
0 0 * * * root cd /app && /usr/local/bin/python crypto_scanner.py 100 >> /app/logs/crypto_cron.log 2>&1
|
||||
|
||||
# 高频监控 - 每4小时扫描一次热门币种 (24小时运行)
|
||||
# 适合4小时K线形态策略
|
||||
# 0 */4 * * * root cd /app && /usr/local/bin/python crypto_scanner.py 50 >> /app/logs/crypto_cron.log 2>&1
|
||||
|
||||
# 周末深度扫描 - 每周日上午10点扫描更多交易对
|
||||
0 10 * * 0 root cd /app && /usr/local/bin/python crypto_scanner.py 200 >> /app/logs/crypto_cron.log 2>&1
|
||||
|
||||
# 必须以空行结尾
|
||||
@ -1,20 +1,14 @@
|
||||
# 市场扫描定时任务配置
|
||||
# 格式: 分钟 小时 日 月 星期 命令
|
||||
# 格式: 分钟 小时 日 月 星期 用户 命令
|
||||
# 时区: Asia/Shanghai
|
||||
|
||||
# 每个工作日开盘前扫描 (09:00)
|
||||
#0 9 * * 1-5 cd /app && python market_scanner.py 200 >> /app/logs/cron.log 2>&1
|
||||
# 设置环境变量
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/bin:/usr/bin:/bin
|
||||
PYTHONPATH=/app
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# 每个工作日午休时间扫描 (12:30)
|
||||
#30 12 * * 1-5 cd /app && python market_scanner.py 100 >> /app/logs/cron.log 2>&1
|
||||
# 每晚8点进行全面市场扫描 (不限制股票数量)
|
||||
0 20 * * * root cd /app && /usr/local/bin/python market_scanner.py >> /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
|
||||
# 必须以空行结尾
|
||||
|
||||
161
crypto_scanner.py
Normal file
161
crypto_scanner.py
Normal file
@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
加密货币市场扫描脚本
|
||||
使用K线形态策略扫描Binance交易对
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.data.binance_fetcher import BinanceFetcher
|
||||
from src.strategy.crypto_kline_pattern_strategy import CryptoKLinePatternStrategy
|
||||
from src.utils.notification import NotificationManager
|
||||
from src.database.mysql_database_manager import MySQLDatabaseManager
|
||||
from src.utils.config_loader import config_loader
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def setup_logger():
|
||||
"""配置日志"""
|
||||
logger.remove() # 移除默认处理器
|
||||
|
||||
# 控制台日志
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
||||
level="INFO"
|
||||
)
|
||||
|
||||
# 文件日志
|
||||
log_dir = project_root / "logs"
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
logger.add(
|
||||
log_dir / "crypto_scanner_{time:YYYY-MM-DD}.log",
|
||||
rotation="00:00",
|
||||
retention="30 days",
|
||||
encoding="utf-8",
|
||||
level="DEBUG"
|
||||
)
|
||||
|
||||
logger.info("日志系统初始化完成")
|
||||
|
||||
|
||||
def main(max_symbols: int = 100):
|
||||
"""
|
||||
主函数
|
||||
|
||||
Args:
|
||||
max_symbols: 最大扫描交易对数量,默认100
|
||||
"""
|
||||
logger.info("="*80)
|
||||
logger.info("🚀 加密货币市场扫描程序启动")
|
||||
logger.info(f"📊 扫描交易对数量: {max_symbols}")
|
||||
logger.info("="*80)
|
||||
|
||||
try:
|
||||
# 1. 加载配置
|
||||
logger.info("📋 加载配置文件...")
|
||||
config = config_loader.load_config()
|
||||
|
||||
# 策略配置
|
||||
strategy_config = {
|
||||
'min_entity_ratio': 0.55, # 前两根阳线实体最小比例
|
||||
'final_yang_min_ratio': 0.40, # 最后阳线实体最小比例
|
||||
'max_turnover_ratio': 100.0, # 最大换手率(加密货币通常更高)
|
||||
'pullback_tolerance': 0.02, # 回踩容忍度2%
|
||||
'monitor_days': 30, # 监控天数
|
||||
'pullback_confirmation_days': 7, # 回踩确认天数
|
||||
'timeframes': ['4hour', 'daily', 'weekly'], # 时间周期:4小时、日线、周线
|
||||
'quote_asset': 'USDT', # 计价货币
|
||||
'min_volume_usdt': 1000000 # 最小24h交易量
|
||||
}
|
||||
|
||||
# 通知配置
|
||||
notification_config = config.get('notification', {
|
||||
'dingtalk': {
|
||||
'enabled': True,
|
||||
'webhook_url': config.get('notification', {}).get('dingtalk', {}).get('webhook_url', '')
|
||||
}
|
||||
})
|
||||
|
||||
# 2. 初始化组件
|
||||
logger.info("🔧 初始化系统组件...")
|
||||
|
||||
# 数据获取器
|
||||
data_fetcher = BinanceFetcher()
|
||||
|
||||
# 通知管理器
|
||||
notification_manager = NotificationManager(notification_config)
|
||||
|
||||
# 数据库管理器
|
||||
db_manager = MySQLDatabaseManager()
|
||||
|
||||
# 策略
|
||||
strategy = CryptoKLinePatternStrategy(
|
||||
data_fetcher=data_fetcher,
|
||||
notification_manager=notification_manager,
|
||||
config=strategy_config,
|
||||
db_manager=db_manager
|
||||
)
|
||||
|
||||
logger.info("✅ 系统组件初始化完成")
|
||||
|
||||
# 3. 打印策略描述
|
||||
logger.info("\n" + "="*80)
|
||||
logger.info(strategy.get_strategy_description())
|
||||
logger.info("="*80 + "\n")
|
||||
|
||||
# 4. 执行市场扫描
|
||||
logger.info("🔍 开始扫描加密货币市场...")
|
||||
results = strategy.scan_market(max_symbols=max_symbols)
|
||||
|
||||
# 5. 汇总结果
|
||||
total_signals = sum(
|
||||
sum(result.get_signal_count() for result in symbol_results.values())
|
||||
for symbol_results in results.values()
|
||||
)
|
||||
|
||||
total_patterns = sum(
|
||||
sum(result.get_pattern_count() for result in symbol_results.values())
|
||||
for symbol_results in results.values()
|
||||
)
|
||||
|
||||
logger.info("\n" + "="*80)
|
||||
logger.info("✅ 加密货币市场扫描完成")
|
||||
logger.info(f"📊 扫描结果汇总:")
|
||||
logger.info(f" - 扫描交易对: {max_symbols} 个")
|
||||
logger.info(f" - 发现信号: {total_signals} 个")
|
||||
logger.info(f" - 形态形成: {total_patterns} 个")
|
||||
logger.info(f" - 涉及交易对: {len(results)} 个")
|
||||
logger.info("="*80)
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 程序执行失败: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 设置日志
|
||||
setup_logger()
|
||||
|
||||
# 从命令行参数获取扫描数量
|
||||
max_symbols = 100
|
||||
if len(sys.argv) > 1:
|
||||
try:
|
||||
max_symbols = int(sys.argv[1])
|
||||
logger.info(f"📊 使用命令行参数: 扫描 {max_symbols} 个交易对")
|
||||
except ValueError:
|
||||
logger.warning(f"⚠️ 无效的参数: {sys.argv[1]},使用默认值100")
|
||||
|
||||
# 执行扫描
|
||||
main(max_symbols=max_symbols)
|
||||
@ -42,7 +42,7 @@ services:
|
||||
- TZ=Asia/Shanghai
|
||||
- OPERATION_KEY=${OPERATION_KEY:-9257}
|
||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||
- MARKET_SCAN_STOCKS=${MARKET_SCAN_STOCKS:-200}
|
||||
# - MARKET_SCAN_STOCKS=${MARKET_SCAN_STOCKS:-200} # 已移除数量限制
|
||||
# MySQL连接配置
|
||||
- MYSQL_HOST=${MYSQL_HOST:-cd-cynosdbmysql-grp-7kdd8qe4.sql.tencentcdb.com}
|
||||
- MYSQL_PORT=${MYSQL_PORT:-26558}
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
安装MySQL依赖包
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def install_mysql_dependencies():
|
||||
"""安装MySQL相关依赖"""
|
||||
|
||||
logger.info("📦 开始安装MySQL依赖包...")
|
||||
|
||||
packages = [
|
||||
'pymysql', # MySQL数据库连接器
|
||||
'cryptography', # 加密支持
|
||||
'sqlalchemy', # SQL工具包(可选)
|
||||
]
|
||||
|
||||
for package in packages:
|
||||
try:
|
||||
logger.info(f"📥 安装 {package}...")
|
||||
subprocess.check_call([sys.executable, '-m', 'pip', 'install', package])
|
||||
logger.info(f"✅ {package} 安装成功")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"❌ {package} 安装失败: {e}")
|
||||
return False
|
||||
|
||||
# 验证安装
|
||||
try:
|
||||
import pymysql
|
||||
logger.info("✅ pymysql 导入成功")
|
||||
|
||||
import cryptography
|
||||
logger.info("✅ cryptography 导入成功")
|
||||
|
||||
logger.info("🎉 所有MySQL依赖安装完成!")
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"❌ 依赖验证失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
success = install_mysql_dependencies()
|
||||
|
||||
if success:
|
||||
print("\n" + "="*50)
|
||||
print("🎉 MySQL依赖安装完成!")
|
||||
print("="*50)
|
||||
print("\n📝 下一步:")
|
||||
print("1. 运行数据迁移: python migrate_to_mysql.py")
|
||||
print("2. 启动MySQL版Web服务: python web/mysql_app.py")
|
||||
print("3. 访问: http://localhost:8080")
|
||||
else:
|
||||
print("\n❌ MySQL依赖安装失败,请检查网络连接和权限")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -94,22 +94,22 @@ def create_strategy_system():
|
||||
return executor
|
||||
|
||||
|
||||
def scan_market(max_stocks=200):
|
||||
"""执行市场扫描"""
|
||||
logger.info(f"🚀 开始市场扫描任务 - 扫描前{max_stocks}只热门股票")
|
||||
def scan_market():
|
||||
"""执行市场扫描 - 分析所有同花顺热榜股票"""
|
||||
logger.info(f"🚀 开始市场扫描任务 - 扫描全部同花顺热榜股票")
|
||||
|
||||
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,
|
||||
stock_pool_params={}, # 不限制数量
|
||||
max_stocks=None, # 取消数量限制
|
||||
send_notification=True # 启用通知
|
||||
)
|
||||
|
||||
@ -137,20 +137,7 @@ 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)
|
||||
return scan_market()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -1,387 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SQLite到MySQL数据迁移脚本
|
||||
将现有的SQLite数据库迁移到MySQL数据库
|
||||
"""
|
||||
|
||||
import sys
|
||||
import sqlite3
|
||||
import pymysql
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
from datetime import datetime, date
|
||||
from loguru import logger
|
||||
|
||||
# 添加项目根目录到路径
|
||||
current_dir = Path(__file__).parent
|
||||
sys.path.insert(0, str(current_dir))
|
||||
|
||||
from config.mysql_config import MYSQL_CONFIG
|
||||
from src.database.mysql_database_manager import MySQLDatabaseManager
|
||||
|
||||
|
||||
class DataMigrator:
|
||||
"""数据迁移器"""
|
||||
|
||||
def __init__(self):
|
||||
self.sqlite_path = current_dir / "data" / "trading.db"
|
||||
self.mysql_config = MYSQL_CONFIG
|
||||
|
||||
def migrate_all(self):
|
||||
"""迁移所有数据"""
|
||||
logger.info("🚀 开始SQLite到MySQL数据迁移...")
|
||||
|
||||
try:
|
||||
# 1. 初始化MySQL数据库
|
||||
logger.info("📊 初始化MySQL数据库...")
|
||||
mysql_db = MySQLDatabaseManager()
|
||||
|
||||
# 2. 迁移策略数据
|
||||
self.migrate_strategies(mysql_db)
|
||||
|
||||
# 3. 迁移扫描会话
|
||||
self.migrate_scan_sessions(mysql_db)
|
||||
|
||||
# 4. 迁移信号数据
|
||||
self.migrate_signals(mysql_db)
|
||||
|
||||
# 5. 迁移回踩提醒
|
||||
self.migrate_pullback_alerts(mysql_db)
|
||||
|
||||
# 6. 验证迁移结果
|
||||
self.verify_migration(mysql_db)
|
||||
|
||||
logger.info("🎉 数据迁移完成!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 数据迁移失败: {e}")
|
||||
raise
|
||||
|
||||
def migrate_strategies(self, mysql_db):
|
||||
"""迁移策略数据"""
|
||||
logger.info("📋 迁移策略数据...")
|
||||
|
||||
try:
|
||||
with sqlite3.connect(self.sqlite_path) as sqlite_conn:
|
||||
strategies_df = pd.read_sql_query("SELECT * FROM strategies", sqlite_conn)
|
||||
|
||||
if strategies_df.empty:
|
||||
logger.info("无策略数据需要迁移")
|
||||
return
|
||||
|
||||
with pymysql.connect(**mysql_db.connection_params) as mysql_conn:
|
||||
cursor = mysql_conn.cursor()
|
||||
|
||||
for _, strategy in strategies_df.iterrows():
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT IGNORE INTO strategies (strategy_name, strategy_type, description)
|
||||
VALUES (%s, %s, %s)
|
||||
""", (
|
||||
strategy['strategy_name'],
|
||||
strategy['strategy_type'],
|
||||
strategy.get('description', '')
|
||||
))
|
||||
except Exception as e:
|
||||
logger.warning(f"策略迁移警告: {e}")
|
||||
|
||||
mysql_conn.commit()
|
||||
|
||||
logger.info(f"✅ 迁移了 {len(strategies_df)} 个策略")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"策略迁移失败: {e}")
|
||||
raise
|
||||
|
||||
def migrate_scan_sessions(self, mysql_db):
|
||||
"""迁移扫描会话"""
|
||||
logger.info("📅 迁移扫描会话数据...")
|
||||
|
||||
try:
|
||||
with sqlite3.connect(self.sqlite_path) as sqlite_conn:
|
||||
sessions_df = pd.read_sql_query("""
|
||||
SELECT ss.*, s.strategy_name
|
||||
FROM scan_sessions ss
|
||||
JOIN strategies s ON ss.strategy_id = s.id
|
||||
""", sqlite_conn)
|
||||
|
||||
if sessions_df.empty:
|
||||
logger.info("无扫描会话数据需要迁移")
|
||||
return
|
||||
|
||||
with pymysql.connect(**mysql_db.connection_params) as mysql_conn:
|
||||
cursor = mysql_conn.cursor()
|
||||
|
||||
# 获取MySQL中的策略ID映射
|
||||
cursor.execute("SELECT id, strategy_name FROM strategies")
|
||||
strategy_mapping = {name: id for id, name in cursor.fetchall()}
|
||||
|
||||
for _, session in sessions_df.iterrows():
|
||||
try:
|
||||
mysql_strategy_id = strategy_mapping.get(session['strategy_name'])
|
||||
if mysql_strategy_id is None:
|
||||
logger.warning(f"未找到策略: {session['strategy_name']}")
|
||||
continue
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO scan_sessions (
|
||||
strategy_id, scan_date, total_scanned, total_signals,
|
||||
data_source, scan_config, status, created_at
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (
|
||||
mysql_strategy_id,
|
||||
session['scan_date'],
|
||||
session.get('total_scanned', 0),
|
||||
session.get('total_signals', 0),
|
||||
session.get('data_source'),
|
||||
session.get('scan_config'),
|
||||
session.get('status', 'completed'),
|
||||
session.get('created_at', datetime.now())
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"会话迁移警告: {e}")
|
||||
|
||||
mysql_conn.commit()
|
||||
|
||||
logger.info(f"✅ 迁移了 {len(sessions_df)} 个扫描会话")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"扫描会话迁移失败: {e}")
|
||||
raise
|
||||
|
||||
def migrate_signals(self, mysql_db):
|
||||
"""迁移信号数据"""
|
||||
logger.info("📈 迁移信号数据...")
|
||||
|
||||
try:
|
||||
with sqlite3.connect(self.sqlite_path) as sqlite_conn:
|
||||
signals_df = pd.read_sql_query("""
|
||||
SELECT ss.*, st.strategy_name
|
||||
FROM stock_signals ss
|
||||
JOIN strategies st ON ss.strategy_id = st.id
|
||||
ORDER BY ss.created_at DESC
|
||||
LIMIT 1000
|
||||
""", sqlite_conn)
|
||||
|
||||
if signals_df.empty:
|
||||
logger.info("无信号数据需要迁移")
|
||||
return
|
||||
|
||||
with pymysql.connect(**mysql_db.connection_params) as mysql_conn:
|
||||
cursor = mysql_conn.cursor()
|
||||
|
||||
# 获取MySQL中的映射
|
||||
cursor.execute("SELECT id, strategy_name FROM strategies")
|
||||
strategy_mapping = {name: id for id, name in cursor.fetchall()}
|
||||
|
||||
cursor.execute("SELECT id, strategy_id, created_at FROM scan_sessions ORDER BY created_at DESC")
|
||||
session_mapping = {}
|
||||
for session_id, strategy_id, created_at in cursor.fetchall():
|
||||
session_mapping[(strategy_id, created_at.date())] = session_id
|
||||
|
||||
migrated_count = 0
|
||||
|
||||
for _, signal in signals_df.iterrows():
|
||||
try:
|
||||
mysql_strategy_id = strategy_mapping.get(signal['strategy_name'])
|
||||
if mysql_strategy_id is None:
|
||||
continue
|
||||
|
||||
# 尝试找到对应的session_id
|
||||
signal_date = pd.to_datetime(signal['signal_date']).date()
|
||||
mysql_session_id = None
|
||||
|
||||
# 查找最近的session
|
||||
for (sid, sdate), session_id in session_mapping.items():
|
||||
if sid == mysql_strategy_id and abs((sdate - signal_date).days) <= 1:
|
||||
mysql_session_id = session_id
|
||||
break
|
||||
|
||||
# 如果找不到session,创建一个
|
||||
if mysql_session_id is None:
|
||||
cursor.execute("""
|
||||
INSERT INTO scan_sessions (strategy_id, scan_date, total_scanned, total_signals, data_source)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
""", (mysql_strategy_id, signal_date, 1, 1, '迁移数据'))
|
||||
mysql_session_id = cursor.lastrowid
|
||||
|
||||
# 处理NaN值的函数
|
||||
def clean_value(value):
|
||||
if pd.isna(value):
|
||||
return None
|
||||
return value
|
||||
|
||||
# 插入信号数据
|
||||
cursor.execute("""
|
||||
INSERT INTO stock_signals (
|
||||
session_id, strategy_id, stock_code, stock_name, timeframe,
|
||||
signal_date, signal_type, breakout_price, yin_high, breakout_amount,
|
||||
breakout_pct, ema20_price, yang1_entity_ratio, yang2_entity_ratio,
|
||||
final_yang_entity_ratio, turnover_ratio, above_ema20,
|
||||
new_high_confirmed, new_high_price, new_high_date, confirmation_date,
|
||||
confirmation_days, pullback_distance,
|
||||
k1_data, k2_data, k3_data, k4_data, created_at
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (
|
||||
mysql_session_id, mysql_strategy_id,
|
||||
signal['stock_code'], signal['stock_name'], signal['timeframe'],
|
||||
signal['signal_date'], signal.get('signal_type', '两阳+阴+阳突破'),
|
||||
clean_value(signal.get('breakout_price')), clean_value(signal.get('yin_high')),
|
||||
clean_value(signal.get('breakout_amount')),
|
||||
clean_value(signal.get('breakout_pct')), clean_value(signal.get('ema20_price')),
|
||||
clean_value(signal.get('yang1_entity_ratio')), clean_value(signal.get('yang2_entity_ratio')),
|
||||
clean_value(signal.get('final_yang_entity_ratio')), clean_value(signal.get('turnover_ratio')),
|
||||
signal.get('above_ema20'),
|
||||
signal.get('new_high_confirmed', False), clean_value(signal.get('new_high_price')),
|
||||
signal.get('new_high_date'), signal.get('confirmation_date'),
|
||||
clean_value(signal.get('confirmation_days')), clean_value(signal.get('pullback_distance')),
|
||||
signal.get('k1_data'), signal.get('k2_data'), signal.get('k3_data'), signal.get('k4_data'),
|
||||
signal.get('created_at', datetime.now())
|
||||
))
|
||||
|
||||
migrated_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"信号迁移警告: {signal['stock_code']} - {e}")
|
||||
|
||||
mysql_conn.commit()
|
||||
|
||||
logger.info(f"✅ 迁移了 {migrated_count} 条信号")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"信号迁移失败: {e}")
|
||||
raise
|
||||
|
||||
def migrate_pullback_alerts(self, mysql_db):
|
||||
"""迁移回踩提醒"""
|
||||
logger.info("⚠️ 迁移回踩提醒数据...")
|
||||
|
||||
try:
|
||||
with sqlite3.connect(self.sqlite_path) as sqlite_conn:
|
||||
# 检查表是否存在
|
||||
cursor = sqlite_conn.cursor()
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='pullback_alerts'")
|
||||
if not cursor.fetchone():
|
||||
logger.info("SQLite中无回踩提醒表,跳过迁移")
|
||||
return
|
||||
|
||||
alerts_df = pd.read_sql_query("SELECT * FROM pullback_alerts", sqlite_conn)
|
||||
|
||||
if alerts_df.empty:
|
||||
logger.info("无回踩提醒数据需要迁移")
|
||||
return
|
||||
|
||||
with pymysql.connect(**mysql_db.connection_params) as mysql_conn:
|
||||
cursor = mysql_conn.cursor()
|
||||
|
||||
migrated_count = 0
|
||||
|
||||
for _, alert in alerts_df.iterrows():
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT INTO pullback_alerts (
|
||||
signal_id, stock_code, stock_name, timeframe,
|
||||
original_signal_date, original_breakout_price, yin_high,
|
||||
pullback_date, current_price, current_low,
|
||||
pullback_pct, distance_to_yin_high, days_since_signal,
|
||||
alert_sent, alert_sent_time, created_at
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (
|
||||
alert.get('signal_id'), alert['stock_code'], alert['stock_name'],
|
||||
alert['timeframe'], alert.get('original_signal_date'),
|
||||
alert.get('original_breakout_price'), alert.get('yin_high'),
|
||||
alert['pullback_date'], alert.get('current_price'), alert.get('current_low'),
|
||||
alert.get('pullback_pct'), alert.get('distance_to_yin_high'),
|
||||
alert.get('days_since_signal'), alert.get('alert_sent', True),
|
||||
alert.get('alert_sent_time'), alert.get('created_at', datetime.now())
|
||||
))
|
||||
|
||||
migrated_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"回踩提醒迁移警告: {alert['stock_code']} - {e}")
|
||||
|
||||
mysql_conn.commit()
|
||||
|
||||
logger.info(f"✅ 迁移了 {migrated_count} 条回踩提醒")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"回踩提醒迁移失败: {e}")
|
||||
raise
|
||||
|
||||
def verify_migration(self, mysql_db):
|
||||
"""验证迁移结果"""
|
||||
logger.info("🔍 验证迁移结果...")
|
||||
|
||||
try:
|
||||
with pymysql.connect(**mysql_db.connection_params) as mysql_conn:
|
||||
cursor = mysql_conn.cursor()
|
||||
|
||||
# 统计各表数据量
|
||||
tables = ['strategies', 'scan_sessions', 'stock_signals', 'pullback_alerts']
|
||||
for table in tables:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table}")
|
||||
count = cursor.fetchone()[0]
|
||||
logger.info(f"📊 {table}: {count} 条记录")
|
||||
|
||||
# 检查最新信号
|
||||
cursor.execute("SELECT COUNT(*) FROM latest_signals_view WHERE new_high_confirmed = 1")
|
||||
confirmed_signals = cursor.fetchone()[0]
|
||||
logger.info(f"🎯 确认信号: {confirmed_signals} 条")
|
||||
|
||||
# 检查视图
|
||||
cursor.execute("SELECT COUNT(*) FROM strategy_stats_view")
|
||||
stats_count = cursor.fetchone()[0]
|
||||
logger.info(f"📈 策略统计: {stats_count} 条")
|
||||
|
||||
logger.info("✅ 数据迁移验证完成")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"验证迁移结果失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
logger.info("🚀 开始SQLite到MySQL数据迁移...")
|
||||
|
||||
try:
|
||||
# 检查依赖
|
||||
try:
|
||||
import pymysql
|
||||
except ImportError:
|
||||
logger.error("❌ 请先安装pymysql: pip install pymysql")
|
||||
return
|
||||
|
||||
# 执行迁移
|
||||
migrator = DataMigrator()
|
||||
migrator.migrate_all()
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("🎉 MySQL数据库迁移完成!")
|
||||
print("="*70)
|
||||
print("\n✅ 迁移内容:")
|
||||
print(" - 策略配置")
|
||||
print(" - 扫描会话")
|
||||
print(" - 股票信号(包含创新高回踩确认字段)")
|
||||
print(" - 回踩提醒")
|
||||
print(" - 数据库视图")
|
||||
|
||||
print("\n🌐 MySQL配置:")
|
||||
print(f" - 主机: {MYSQL_CONFIG.host}")
|
||||
print(f" - 端口: {MYSQL_CONFIG.port}")
|
||||
print(f" - 数据库: {MYSQL_CONFIG.database}")
|
||||
|
||||
print("\n📝 下一步:")
|
||||
print(" 1. 更新系统配置使用MySQL数据库")
|
||||
print(" 2. 测试Web界面和API功能")
|
||||
print(" 3. 验证所有功能正常工作")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 迁移失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,5 +1,6 @@
|
||||
# Data source
|
||||
tushare>=1.2.89
|
||||
python-binance>=1.0.19
|
||||
|
||||
# Data analysis and manipulation
|
||||
pandas>=2.0.0
|
||||
@ -18,7 +19,8 @@ scikit-learn>=1.3.0
|
||||
|
||||
# Database
|
||||
PyMySQL>=1.1.0
|
||||
SQLAlchemy>=2.0.0
|
||||
|
||||
>=2.0.0
|
||||
pymysql[rsa]>=1.1.0
|
||||
mysql-connector-python>=8.0.33
|
||||
cryptography>=41.0.0
|
||||
|
||||
495
src/data/binance_fetcher.py
Normal file
495
src/data/binance_fetcher.py
Normal file
@ -0,0 +1,495 @@
|
||||
"""
|
||||
加密货币数据获取模块
|
||||
使用Binance API获取加密货币市场数据
|
||||
"""
|
||||
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
import pandas as pd
|
||||
from typing import List, Optional, Union
|
||||
from datetime import datetime, timedelta
|
||||
import time
|
||||
from loguru import logger
|
||||
from functools import wraps
|
||||
from src.utils.config_loader import config_loader
|
||||
|
||||
|
||||
def retry_on_failure(retries: int = 3, delay: float = 1.0):
|
||||
"""
|
||||
重试装饰器,用于网络请求失败时自动重试
|
||||
|
||||
Args:
|
||||
retries: 重试次数
|
||||
delay: 重试间隔(秒)
|
||||
"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(retries + 1):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
if attempt < retries:
|
||||
logger.warning(f"{func.__name__} 第{attempt + 1}次调用失败: {e}, {delay}秒后重试...")
|
||||
time.sleep(delay)
|
||||
else:
|
||||
logger.error(f"{func.__name__} 已重试{retries}次仍然失败: {e}")
|
||||
|
||||
raise last_exception
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class BinanceFetcher:
|
||||
"""Binance数据获取器"""
|
||||
|
||||
def __init__(self, api_key: str = None, api_secret: str = None, testnet: bool = False):
|
||||
"""
|
||||
初始化Binance数据获取器
|
||||
|
||||
Args:
|
||||
api_key: Binance API Key,如果为None则从配置文件读取
|
||||
api_secret: Binance API Secret,如果为None则从配置文件读取
|
||||
testnet: 是否使用测试网
|
||||
"""
|
||||
# 如果没有传入密钥,从配置文件读取
|
||||
if api_key is None or api_secret is None:
|
||||
api_key = config_loader.get('data_source.binance_api_key', '')
|
||||
api_secret = config_loader.get('data_source.binance_api_secret', '')
|
||||
|
||||
if api_key and api_secret:
|
||||
logger.info("✅ 从配置文件读取Binance API密钥成功")
|
||||
else:
|
||||
logger.warning("⚠️ 配置文件中未找到Binance API密钥,将使用公开接口")
|
||||
|
||||
self.api_key = api_key
|
||||
self.api_secret = api_secret
|
||||
self.testnet = testnet
|
||||
|
||||
# 初始化客户端
|
||||
try:
|
||||
if testnet:
|
||||
self.client = Client(api_key, api_secret, testnet=True)
|
||||
logger.info("✅ Binance测试网客户端初始化完成")
|
||||
else:
|
||||
self.client = Client(api_key, api_secret)
|
||||
logger.info("✅ Binance客户端初始化完成")
|
||||
|
||||
# 测试连接
|
||||
self.client.ping()
|
||||
logger.info("✅ Binance API连接成功")
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"❌ Binance API初始化失败: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Binance客户端初始化失败: {e}")
|
||||
raise
|
||||
|
||||
# 币种名称缓存机制
|
||||
self._symbol_cache = {}
|
||||
self._ticker_cache = None
|
||||
self._cache_timestamp = None
|
||||
self._cache_duration = 300 # 缓存5分钟
|
||||
|
||||
def clear_caches(self):
|
||||
"""清除所有缓存"""
|
||||
self._symbol_cache.clear()
|
||||
self._ticker_cache = None
|
||||
self._cache_timestamp = None
|
||||
logger.info("🔄 已清除所有币种数据缓存")
|
||||
|
||||
@retry_on_failure(retries=2, delay=1.0)
|
||||
def get_exchange_info(self) -> dict:
|
||||
"""
|
||||
获取交易所信息
|
||||
|
||||
Returns:
|
||||
交易所信息字典
|
||||
"""
|
||||
try:
|
||||
info = self.client.get_exchange_info()
|
||||
logger.info("获取Binance交易所信息成功")
|
||||
return info
|
||||
except Exception as e:
|
||||
logger.error(f"获取交易所信息失败: {e}")
|
||||
return {}
|
||||
|
||||
@retry_on_failure(retries=2, delay=1.0)
|
||||
def get_all_tickers(self) -> pd.DataFrame:
|
||||
"""
|
||||
获取所有交易对的行情信息
|
||||
|
||||
Returns:
|
||||
行情信息DataFrame
|
||||
"""
|
||||
try:
|
||||
current_time = time.time()
|
||||
|
||||
# 检查缓存是否有效
|
||||
if (self._ticker_cache is not None and
|
||||
self._cache_timestamp is not None and
|
||||
current_time - self._cache_timestamp < self._cache_duration):
|
||||
logger.debug(f"🔄 使用缓存的ticker数据 ({len(self._ticker_cache)} 个交易对)")
|
||||
return self._ticker_cache.copy()
|
||||
|
||||
# 获取最新数据
|
||||
tickers = self.client.get_ticker()
|
||||
df = pd.DataFrame(tickers)
|
||||
|
||||
# 数据类型转换
|
||||
numeric_cols = ['priceChange', 'priceChangePercent', 'weightedAvgPrice',
|
||||
'prevClosePrice', 'lastPrice', 'lastQty', 'bidPrice',
|
||||
'askPrice', 'openPrice', 'highPrice', 'lowPrice', 'volume',
|
||||
'quoteVolume', 'count']
|
||||
|
||||
for col in numeric_cols:
|
||||
if col in df.columns:
|
||||
df[col] = pd.to_numeric(df[col], errors='coerce')
|
||||
|
||||
# 更新缓存
|
||||
self._ticker_cache = df.copy()
|
||||
self._cache_timestamp = current_time
|
||||
|
||||
logger.info(f"获取所有ticker成功,共{len(df)}个交易对")
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取所有ticker失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_top_volume_symbols(self, quote_asset: str = 'USDT', limit: int = 100,
|
||||
exclude_leverage: bool = True) -> List[str]:
|
||||
"""
|
||||
获取交易量最大的交易对
|
||||
|
||||
Args:
|
||||
quote_asset: 计价货币,默认USDT
|
||||
limit: 返回数量
|
||||
exclude_leverage: 是否排除杠杆代币
|
||||
|
||||
Returns:
|
||||
交易对列表
|
||||
"""
|
||||
try:
|
||||
logger.info(f"🔥 获取{quote_asset}交易量TOP{limit}的交易对...")
|
||||
|
||||
# 获取所有ticker
|
||||
tickers_df = self.get_all_tickers()
|
||||
|
||||
if tickers_df.empty:
|
||||
logger.error("获取ticker数据为空")
|
||||
return []
|
||||
|
||||
# 筛选指定计价货币的交易对
|
||||
filtered = tickers_df[tickers_df['symbol'].str.endswith(quote_asset)].copy()
|
||||
|
||||
# 排除杠杆代币
|
||||
if exclude_leverage:
|
||||
# 排除包含UP, DOWN, BULL, BEAR等杠杆代币标识
|
||||
leverage_patterns = ['UP', 'DOWN', 'BULL', 'BEAR']
|
||||
for pattern in leverage_patterns:
|
||||
filtered = filtered[~filtered['symbol'].str.contains(pattern)]
|
||||
|
||||
# 按24小时交易量排序
|
||||
filtered = filtered.sort_values('quoteVolume', ascending=False)
|
||||
|
||||
# 获取前N个交易对
|
||||
top_symbols = filtered.head(limit)['symbol'].tolist()
|
||||
|
||||
logger.info(f"✅ 获取TOP{limit}交易对成功")
|
||||
return top_symbols
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取TOP交易对失败: {e}")
|
||||
return []
|
||||
|
||||
def get_historical_klines(self, symbol: str, interval: str,
|
||||
start_time: Union[str, datetime] = None,
|
||||
end_time: Union[str, datetime] = None,
|
||||
limit: int = 500) -> pd.DataFrame:
|
||||
"""
|
||||
获取历史K线数据
|
||||
|
||||
Args:
|
||||
symbol: 交易对符号,如'BTCUSDT'
|
||||
interval: K线周期,如'1d', '4h', '1h'
|
||||
start_time: 开始时间(datetime对象或字符串)
|
||||
end_time: 结束时间(datetime对象或字符串)
|
||||
limit: 数据条数限制,默认500
|
||||
|
||||
Returns:
|
||||
K线数据DataFrame
|
||||
"""
|
||||
try:
|
||||
# 处理时间格式 - Binance接受多种格式,包括字符串格式的日期
|
||||
# 如 "1 Dec, 2017" 或 "2017-12-01" 或时间戳
|
||||
if isinstance(start_time, datetime):
|
||||
start_time_str = start_time.strftime('%d %b, %Y')
|
||||
elif isinstance(start_time, str):
|
||||
# 如果是带连字符的格式,尝试转换为Binance接受的格式
|
||||
if '-' in start_time:
|
||||
try:
|
||||
dt = pd.to_datetime(start_time)
|
||||
start_time_str = dt.strftime('%d %b, %Y')
|
||||
except:
|
||||
start_time_str = start_time
|
||||
else:
|
||||
start_time_str = start_time
|
||||
else:
|
||||
start_time_str = None
|
||||
|
||||
if isinstance(end_time, datetime):
|
||||
end_time_str = end_time.strftime('%d %b, %Y')
|
||||
elif isinstance(end_time, str):
|
||||
# 如果是带连字符的格式,尝试转换为Binance接受的格式
|
||||
if '-' in end_time:
|
||||
try:
|
||||
dt = pd.to_datetime(end_time)
|
||||
end_time_str = dt.strftime('%d %b, %Y')
|
||||
except:
|
||||
end_time_str = end_time
|
||||
else:
|
||||
end_time_str = end_time
|
||||
else:
|
||||
end_time_str = None
|
||||
|
||||
# 获取K线数据
|
||||
if start_time_str and end_time_str:
|
||||
klines = self.client.get_historical_klines(
|
||||
symbol, interval, start_time_str, end_time_str
|
||||
)
|
||||
elif start_time_str:
|
||||
klines = self.client.get_historical_klines(
|
||||
symbol, interval, start_time_str
|
||||
)
|
||||
else:
|
||||
klines = self.client.get_klines(symbol=symbol, interval=interval, limit=limit)
|
||||
|
||||
if not klines:
|
||||
logger.warning(f"{symbol} {interval} K线数据为空")
|
||||
return pd.DataFrame()
|
||||
|
||||
# 转换为DataFrame
|
||||
df = pd.DataFrame(klines, columns=[
|
||||
'open_time', 'open', 'high', 'low', 'close', 'volume',
|
||||
'close_time', 'quote_volume', 'trades', 'taker_buy_base',
|
||||
'taker_buy_quote', 'ignore'
|
||||
])
|
||||
|
||||
# 数据类型转换
|
||||
numeric_cols = ['open', 'high', 'low', 'close', 'volume', 'quote_volume']
|
||||
for col in numeric_cols:
|
||||
df[col] = pd.to_numeric(df[col], errors='coerce')
|
||||
|
||||
# 转换时间戳为日期
|
||||
df['open_time'] = pd.to_datetime(df['open_time'], unit='ms')
|
||||
df['close_time'] = pd.to_datetime(df['close_time'], unit='ms')
|
||||
df['trade_date'] = df['open_time'].dt.strftime('%Y-%m-%d')
|
||||
df['date'] = df['trade_date'] # 兼容策略代码
|
||||
|
||||
# 按时间升序排列
|
||||
df = df.sort_values('open_time')
|
||||
|
||||
logger.info(f"获取{symbol} {interval}历史K线成功,数据量: {len(df)}")
|
||||
return df
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"获取{symbol} K线数据失败(API错误): {e}")
|
||||
return pd.DataFrame()
|
||||
except Exception as e:
|
||||
logger.error(f"获取{symbol} K线数据失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_symbol_info(self, symbol: str) -> dict:
|
||||
"""
|
||||
获取交易对信息
|
||||
|
||||
Args:
|
||||
symbol: 交易对符号
|
||||
|
||||
Returns:
|
||||
交易对信息字典
|
||||
"""
|
||||
try:
|
||||
if symbol in self._symbol_cache:
|
||||
return self._symbol_cache[symbol]
|
||||
|
||||
info = self.client.get_symbol_info(symbol)
|
||||
self._symbol_cache[symbol] = info
|
||||
return info
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"获取{symbol}信息失败: {e}")
|
||||
return {}
|
||||
|
||||
def get_symbol_name(self, symbol: str) -> str:
|
||||
"""
|
||||
获取交易对显示名称
|
||||
|
||||
Args:
|
||||
symbol: 交易对符号
|
||||
|
||||
Returns:
|
||||
交易对名称
|
||||
"""
|
||||
# 加密货币直接返回交易对符号
|
||||
return symbol
|
||||
|
||||
def convert_timeframe(self, timeframe: str) -> str:
|
||||
"""
|
||||
转换时间周期格式(从策略格式到Binance格式)
|
||||
|
||||
Args:
|
||||
timeframe: 策略时间周期,如'daily', 'weekly', 'hourly'
|
||||
|
||||
Returns:
|
||||
Binance时间周期,如'1d', '1w', '1h'
|
||||
"""
|
||||
mapping = {
|
||||
'daily': '1d',
|
||||
'weekly': '1w',
|
||||
'monthly': '1M',
|
||||
'hourly': '1h',
|
||||
'4hour': '4h',
|
||||
'15min': '15m',
|
||||
'5min': '5m'
|
||||
}
|
||||
return mapping.get(timeframe, '1d')
|
||||
|
||||
def get_24h_stats(self, symbol: str = None) -> pd.DataFrame:
|
||||
"""
|
||||
获取24小时统计数据
|
||||
|
||||
Args:
|
||||
symbol: 交易对符号,如果为None则获取所有交易对
|
||||
|
||||
Returns:
|
||||
统计数据DataFrame
|
||||
"""
|
||||
try:
|
||||
if symbol:
|
||||
stats = self.client.get_ticker(symbol=symbol)
|
||||
df = pd.DataFrame([stats])
|
||||
else:
|
||||
stats = self.client.get_ticker()
|
||||
df = pd.DataFrame(stats)
|
||||
|
||||
logger.info(f"获取24小时统计数据成功")
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取24小时统计数据失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_account_balance(self) -> pd.DataFrame:
|
||||
"""
|
||||
获取账户余额(需要API权限)
|
||||
|
||||
Returns:
|
||||
账户余额DataFrame
|
||||
"""
|
||||
try:
|
||||
if not self.api_key or not self.api_secret:
|
||||
logger.warning("未配置API密钥,无法获取账户余额")
|
||||
return pd.DataFrame()
|
||||
|
||||
account = self.client.get_account()
|
||||
balances = account['balances']
|
||||
|
||||
df = pd.DataFrame(balances)
|
||||
df['free'] = pd.to_numeric(df['free'])
|
||||
df['locked'] = pd.to_numeric(df['locked'])
|
||||
|
||||
# 只显示非零余额
|
||||
df = df[(df['free'] > 0) | (df['locked'] > 0)]
|
||||
|
||||
logger.info(f"获取账户余额成功,共{len(df)}个币种有余额")
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取账户余额失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_market_overview(self) -> dict:
|
||||
"""
|
||||
获取市场概况
|
||||
|
||||
Returns:
|
||||
市场概况字典
|
||||
"""
|
||||
try:
|
||||
# 获取主要币种的24小时数据
|
||||
major_symbols = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT']
|
||||
overview = {
|
||||
'update_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'major_coins': {}
|
||||
}
|
||||
|
||||
for symbol in major_symbols:
|
||||
stats = self.client.get_ticker(symbol=symbol)
|
||||
overview['major_coins'][symbol] = stats
|
||||
|
||||
logger.info("获取市场概况成功")
|
||||
return overview
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取市场概况失败: {e}")
|
||||
return {}
|
||||
|
||||
def search_symbols(self, keyword: str, quote_asset: str = 'USDT') -> List[str]:
|
||||
"""
|
||||
搜索交易对
|
||||
|
||||
Args:
|
||||
keyword: 搜索关键词
|
||||
quote_asset: 计价货币
|
||||
|
||||
Returns:
|
||||
匹配的交易对列表
|
||||
"""
|
||||
try:
|
||||
tickers_df = self.get_all_tickers()
|
||||
|
||||
if tickers_df.empty:
|
||||
return []
|
||||
|
||||
# 筛选计价货币
|
||||
filtered = tickers_df[tickers_df['symbol'].str.endswith(quote_asset)]
|
||||
|
||||
# 搜索关键词
|
||||
keyword = keyword.upper()
|
||||
results = filtered[filtered['symbol'].str.contains(keyword)]['symbol'].tolist()
|
||||
|
||||
logger.info(f"搜索'{keyword}'找到{len(results)}个交易对")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"搜索交易对失败: {e}")
|
||||
return []
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
print("初始化Binance数据获取器...")
|
||||
fetcher = BinanceFetcher()
|
||||
|
||||
# 测试获取TOP交易对
|
||||
print("\n测试获取TOP10交易对...")
|
||||
top_symbols = fetcher.get_top_volume_symbols(limit=10)
|
||||
print(f"TOP10交易对: {top_symbols}")
|
||||
|
||||
# 测试获取K线数据
|
||||
if top_symbols:
|
||||
print(f"\n测试获取{top_symbols[0]}的日线数据...")
|
||||
klines = fetcher.get_historical_klines(top_symbols[0], '1d', limit=100)
|
||||
print(klines.head())
|
||||
print(f"数据量: {len(klines)}")
|
||||
|
||||
# 测试市场概况
|
||||
print("\n测试获取市场概况...")
|
||||
overview = fetcher.get_market_overview()
|
||||
print(overview)
|
||||
@ -1,347 +0,0 @@
|
||||
"""
|
||||
A股舆情数据获取模块
|
||||
基于adata库获取市场舆情、资金流向、热点数据等
|
||||
"""
|
||||
|
||||
import adata
|
||||
import pandas as pd
|
||||
from typing import List, Optional, Union
|
||||
from datetime import datetime, date
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class SentimentFetcher:
|
||||
"""舆情数据获取器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化舆情数据获取器"""
|
||||
self.client = adata
|
||||
logger.info("舆情数据获取器初始化完成")
|
||||
|
||||
# ========== 解禁数据 ==========
|
||||
def get_stock_lifting_last_month(self) -> pd.DataFrame:
|
||||
"""
|
||||
获取上个月股票解禁数据
|
||||
|
||||
Returns:
|
||||
解禁数据DataFrame,包含字段:
|
||||
- stock_code: 股票代码
|
||||
- short_name: 股票简称
|
||||
- lift_date: 解禁日期
|
||||
- volume: 解禁数量
|
||||
- amount: 解禁金额
|
||||
- ratio: 解禁比例
|
||||
- price: 解禁价格
|
||||
"""
|
||||
try:
|
||||
lifting_data = self.client.sentiment.stock_lifting_last_month()
|
||||
logger.info(f"获取解禁数据成功,数据量: {len(lifting_data)}")
|
||||
return lifting_data
|
||||
except Exception as e:
|
||||
logger.error(f"获取解禁数据失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
# ========== 两融数据 ==========
|
||||
def get_securities_margin(self, start_date: str = '2022-01-01') -> pd.DataFrame:
|
||||
"""
|
||||
获取融资融券数据
|
||||
|
||||
Args:
|
||||
start_date: 开始日期,格式: 'YYYY-MM-DD'
|
||||
|
||||
Returns:
|
||||
融资融券DataFrame,包含字段:
|
||||
- trade_date: 交易日期
|
||||
- rzye: 融资余额
|
||||
- rqye: 融券余额
|
||||
- rzrqye: 融资融券余额
|
||||
- rzrqyecz: 融资融券余额差值
|
||||
"""
|
||||
try:
|
||||
margin_data = self.client.sentiment.securities_margin(start_date=start_date)
|
||||
logger.info(f"获取融资融券数据成功,数据量: {len(margin_data)}")
|
||||
return margin_data
|
||||
except Exception as e:
|
||||
logger.error(f"获取融资融券数据失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
# ========== 北向资金 ==========
|
||||
def get_north_flow_current(self) -> pd.DataFrame:
|
||||
"""
|
||||
获取当前北向资金流向
|
||||
|
||||
Returns:
|
||||
当前北向资金流向DataFrame
|
||||
"""
|
||||
try:
|
||||
north_flow = self.client.sentiment.north.north_flow_current()
|
||||
logger.info("获取当前北向资金流向成功")
|
||||
return north_flow
|
||||
except Exception as e:
|
||||
logger.error(f"获取当前北向资金流向失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_north_flow_min(self) -> pd.DataFrame:
|
||||
"""
|
||||
获取北向资金分钟级流向数据
|
||||
|
||||
Returns:
|
||||
北向资金分钟级流向DataFrame
|
||||
"""
|
||||
try:
|
||||
north_flow_min = self.client.sentiment.north.north_flow_min()
|
||||
logger.info(f"获取北向资金分钟级数据成功,数据量: {len(north_flow_min)}")
|
||||
return north_flow_min
|
||||
except Exception as e:
|
||||
logger.error(f"获取北向资金分钟级数据失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_north_flow_history(self, start_date: str = None) -> pd.DataFrame:
|
||||
"""
|
||||
获取北向资金历史流向数据
|
||||
|
||||
Args:
|
||||
start_date: 开始日期,格式: 'YYYY-MM-DD',默认为30天前
|
||||
|
||||
Returns:
|
||||
北向资金历史流向DataFrame
|
||||
"""
|
||||
try:
|
||||
if start_date is None:
|
||||
# 默认获取最近30天的数据
|
||||
start_date = (datetime.now() - pd.Timedelta(days=30)).strftime('%Y-%m-%d')
|
||||
|
||||
north_flow_hist = self.client.sentiment.north.north_flow(start_date=start_date)
|
||||
logger.info(f"获取北向资金历史数据成功,数据量: {len(north_flow_hist)}")
|
||||
return north_flow_hist
|
||||
except Exception as e:
|
||||
logger.error(f"获取北向资金历史数据失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
# ========== 热点股票 ==========
|
||||
def get_popular_stocks_east_100(self) -> pd.DataFrame:
|
||||
"""
|
||||
获取东方财富人气股票排行榜前100
|
||||
|
||||
Returns:
|
||||
人气股票排行DataFrame
|
||||
"""
|
||||
try:
|
||||
popular_stocks = self.client.sentiment.hot.pop_rank_100_east()
|
||||
logger.info(f"获取东财人气股票排行成功,数据量: {len(popular_stocks)}")
|
||||
return popular_stocks
|
||||
except Exception as e:
|
||||
logger.error(f"获取东财人气股票排行失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_hot_stocks_ths_100(self) -> pd.DataFrame:
|
||||
"""
|
||||
获取同花顺热门股票排行榜前100
|
||||
|
||||
Returns:
|
||||
热门股票排行DataFrame
|
||||
"""
|
||||
try:
|
||||
hot_stocks = self.client.sentiment.hot.hot_rank_100_ths()
|
||||
logger.info(f"获取同花顺热门股票排行成功,数据量: {len(hot_stocks)}")
|
||||
return hot_stocks
|
||||
except Exception as e:
|
||||
logger.error(f"获取同花顺热门股票排行失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_hot_concept_ths_20(self) -> pd.DataFrame:
|
||||
"""
|
||||
获取同花顺热门概念板块排行榜前20
|
||||
|
||||
Returns:
|
||||
热门概念板块DataFrame
|
||||
"""
|
||||
try:
|
||||
hot_concepts = self.client.sentiment.hot.hot_concept_20_ths()
|
||||
logger.info(f"获取同花顺热门概念排行成功,数据量: {len(hot_concepts)}")
|
||||
return hot_concepts
|
||||
except Exception as e:
|
||||
logger.error(f"获取同花顺热门概念排行失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
# ========== 龙虎榜 ==========
|
||||
def get_dragon_tiger_list_daily(self, report_date: str = None) -> pd.DataFrame:
|
||||
"""
|
||||
获取每日龙虎榜数据
|
||||
|
||||
Args:
|
||||
report_date: 报告日期,格式: 'YYYY-MM-DD',默认为当日
|
||||
|
||||
Returns:
|
||||
龙虎榜DataFrame
|
||||
"""
|
||||
try:
|
||||
if report_date is None:
|
||||
report_date = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
dragon_tiger = self.client.sentiment.hot.list_a_list_daily(report_date=report_date)
|
||||
logger.info(f"获取{report_date}龙虎榜数据成功,数据量: {len(dragon_tiger)}")
|
||||
return dragon_tiger
|
||||
except Exception as e:
|
||||
logger.error(f"获取龙虎榜数据失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_stock_dragon_tiger_info(self, stock_code: str, report_date: str = None) -> pd.DataFrame:
|
||||
"""
|
||||
获取单只股票龙虎榜详细信息
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
report_date: 报告日期,格式: 'YYYY-MM-DD',默认为当日
|
||||
|
||||
Returns:
|
||||
单股龙虎榜详细信息DataFrame
|
||||
"""
|
||||
try:
|
||||
if report_date is None:
|
||||
report_date = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
stock_info = self.client.sentiment.hot.get_a_list_info(
|
||||
stock_code=stock_code,
|
||||
report_date=report_date
|
||||
)
|
||||
logger.info(f"获取{stock_code}龙虎榜信息成功")
|
||||
return stock_info
|
||||
except Exception as e:
|
||||
logger.error(f"获取{stock_code}龙虎榜信息失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
# ========== 风险扫描 ==========
|
||||
def get_stock_risk_scan(self, stock_code: str) -> pd.DataFrame:
|
||||
"""
|
||||
获取单只股票风险扫描数据
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
|
||||
Returns:
|
||||
股票风险扫描DataFrame
|
||||
"""
|
||||
try:
|
||||
risk_data = self.client.sentiment.mine.mine_clearance_tdx(stock_code=stock_code)
|
||||
logger.info(f"获取{stock_code}风险扫描数据成功")
|
||||
return risk_data
|
||||
except Exception as e:
|
||||
logger.error(f"获取{stock_code}风险扫描数据失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
# ========== 综合分析方法 ==========
|
||||
def get_market_sentiment_overview(self) -> dict:
|
||||
"""
|
||||
获取市场舆情综合概览
|
||||
|
||||
Returns:
|
||||
市场舆情概览字典
|
||||
"""
|
||||
try:
|
||||
overview = {}
|
||||
|
||||
# 北向资金情况
|
||||
north_current = self.get_north_flow_current()
|
||||
if not north_current.empty:
|
||||
north_data = north_current.iloc[0]
|
||||
overview['north_flow'] = {
|
||||
'net_total': north_data.get('net_tgt', 0), # 总净流入
|
||||
'net_hgt': north_data.get('net_hgt', 0), # 沪股通净流入
|
||||
'net_sgt': north_data.get('net_sgt', 0), # 深股通净流入
|
||||
'update_time': north_data.get('trade_time', 'N/A')
|
||||
}
|
||||
|
||||
# 融资融券情况(最近7天)
|
||||
recent_date = (datetime.now() - pd.Timedelta(days=7)).strftime('%Y-%m-%d')
|
||||
margin_data = self.get_securities_margin(start_date=recent_date)
|
||||
if not margin_data.empty:
|
||||
overview['latest_margin'] = margin_data.tail(1).iloc[0].to_dict()
|
||||
|
||||
# 热门股票
|
||||
overview['hot_stocks_east'] = self.get_popular_stocks_east_100().head(10)
|
||||
overview['hot_stocks_ths'] = self.get_hot_stocks_ths_100().head(10)
|
||||
|
||||
# 热门概念
|
||||
overview['hot_concepts'] = self.get_hot_concept_ths_20().head(10)
|
||||
|
||||
# 今日龙虎榜
|
||||
overview['dragon_tiger'] = self.get_dragon_tiger_list_daily().head(10)
|
||||
|
||||
overview['update_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
logger.info("获取市场舆情概览成功")
|
||||
return overview
|
||||
except Exception as e:
|
||||
logger.error(f"获取市场舆情概览失败: {e}")
|
||||
return {}
|
||||
|
||||
def analyze_stock_sentiment(self, stock_code: str) -> dict:
|
||||
"""
|
||||
分析单只股票的舆情情况
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
|
||||
Returns:
|
||||
股票舆情分析结果字典
|
||||
"""
|
||||
try:
|
||||
analysis = {
|
||||
'stock_code': stock_code,
|
||||
'update_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
# 风险扫描
|
||||
analysis['risk_scan'] = self.get_stock_risk_scan(stock_code)
|
||||
|
||||
# 龙虎榜信息
|
||||
analysis['dragon_tiger'] = self.get_stock_dragon_tiger_info(stock_code)
|
||||
|
||||
# 检查是否在热门榜单中
|
||||
hot_stocks_east = self.get_popular_stocks_east_100()
|
||||
hot_stocks_ths = self.get_hot_stocks_ths_100()
|
||||
|
||||
analysis['in_popular_east'] = stock_code in hot_stocks_east.get('stock_code', []).values if not hot_stocks_east.empty else False
|
||||
analysis['in_hot_ths'] = stock_code in hot_stocks_ths.get('stock_code', []).values if not hot_stocks_ths.empty else False
|
||||
|
||||
logger.info(f"分析{stock_code}舆情情况成功")
|
||||
return analysis
|
||||
except Exception as e:
|
||||
logger.error(f"分析{stock_code}舆情情况失败: {e}")
|
||||
return {'stock_code': stock_code, 'error': str(e)}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
fetcher = SentimentFetcher()
|
||||
|
||||
print("="*50)
|
||||
print("测试舆情数据获取功能")
|
||||
print("="*50)
|
||||
|
||||
# 测试北向资金
|
||||
print("\n1. 测试北向资金数据...")
|
||||
north_current = fetcher.get_north_flow_current()
|
||||
print(f"当前北向资金数据: {len(north_current)} 条")
|
||||
if not north_current.empty:
|
||||
print(north_current.head())
|
||||
|
||||
# 测试热门股票
|
||||
print("\n2. 测试热门股票排行...")
|
||||
hot_stocks = fetcher.get_popular_stocks_east_100()
|
||||
print(f"东财人气股票: {len(hot_stocks)} 条")
|
||||
if not hot_stocks.empty:
|
||||
print(hot_stocks.head())
|
||||
|
||||
# 测试龙虎榜
|
||||
print("\n3. 测试龙虎榜数据...")
|
||||
dragon_tiger = fetcher.get_dragon_tiger_list_daily()
|
||||
print(f"今日龙虎榜: {len(dragon_tiger)} 条")
|
||||
if not dragon_tiger.empty:
|
||||
print(dragon_tiger.head())
|
||||
|
||||
# 测试市场舆情概览
|
||||
print("\n4. 测试市场舆情概览...")
|
||||
overview = fetcher.get_market_sentiment_overview()
|
||||
print("市场舆情概览获取完成")
|
||||
@ -36,9 +36,12 @@ class StockPoolRule(ABC):
|
||||
class TushareHotStocksRule(StockPoolRule):
|
||||
"""同花顺热榜股票池规则"""
|
||||
|
||||
def get_stocks(self, fetcher: TushareFetcher, limit: int = 50, **kwargs) -> List[str]:
|
||||
def get_stocks(self, fetcher: TushareFetcher, limit: int = None, **kwargs) -> List[str]:
|
||||
"""获取同花顺热榜股票"""
|
||||
try:
|
||||
# 如果没有指定limit,获取所有可用的热榜股票(默认最大2000)
|
||||
if limit is None:
|
||||
limit = 2000
|
||||
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()
|
||||
|
||||
@ -5,6 +5,7 @@ A股数据获取模块
|
||||
|
||||
import tushare as ts
|
||||
import pandas as pd
|
||||
import re
|
||||
from typing import List, Optional, Union
|
||||
from datetime import datetime, date, timedelta
|
||||
import time
|
||||
@ -86,6 +87,10 @@ class TushareFetcher:
|
||||
self._cache_timestamp = None
|
||||
self._cache_duration = 3600 # 缓存1小时
|
||||
|
||||
# 分钟线接口调用频率控制(每分钟最多2次)
|
||||
self._minute_data_call_times = []
|
||||
self._minute_data_max_calls_per_minute = 2
|
||||
|
||||
def clear_caches(self):
|
||||
"""清除所有缓存"""
|
||||
self._stock_name_cache.clear()
|
||||
@ -220,8 +225,9 @@ class TushareFetcher:
|
||||
# 排除ST股票
|
||||
if exclude_st:
|
||||
before_count = len(filtered_stocks)
|
||||
st_pattern = r'(\*?ST|PT|退|暂停)'
|
||||
filtered_stocks = filtered_stocks[~filtered_stocks['short_name'].str.contains(st_pattern, na=False, case=False)]
|
||||
# 使用(?:...)非捕获组避免警告
|
||||
st_pattern = r'(?:\*?ST|PT|退|暂停)'
|
||||
filtered_stocks = filtered_stocks[~filtered_stocks['short_name'].str.contains(st_pattern, na=False, case=False, regex=True)]
|
||||
st_excluded = before_count - len(filtered_stocks)
|
||||
logger.info(f"排除ST等风险股票: {st_excluded}只")
|
||||
|
||||
@ -371,7 +377,7 @@ class TushareFetcher:
|
||||
stock_code: 股票代码
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
period: 数据周期 ('daily', 'weekly', 'monthly')
|
||||
period: 数据周期 ('daily', 'weekly', 'monthly', '60min', '30min', '15min', '5min', '1min')
|
||||
|
||||
Returns:
|
||||
历史行情DataFrame
|
||||
@ -417,6 +423,34 @@ class TushareFetcher:
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
elif period in ('60min', '30min', '15min', '5min', '1min'):
|
||||
# 使用分钟线数据接口
|
||||
# 注意:分钟线数据需要特殊权限,且有调用限制:每分钟最多2次
|
||||
|
||||
# 频率限制检查
|
||||
current_time = time.time()
|
||||
# 清理60秒前的调用记录
|
||||
self._minute_data_call_times = [t for t in self._minute_data_call_times if current_time - t < 60]
|
||||
|
||||
# 检查是否超过限制
|
||||
if len(self._minute_data_call_times) >= self._minute_data_max_calls_per_minute:
|
||||
wait_time = 60 - (current_time - self._minute_data_call_times[0])
|
||||
logger.warning(f"分钟线接口频率限制:每分钟最多{self._minute_data_max_calls_per_minute}次,需等待 {wait_time:.1f} 秒")
|
||||
time.sleep(wait_time + 1) # 多等1秒确保安全
|
||||
# 清理过期记录
|
||||
current_time = time.time()
|
||||
self._minute_data_call_times = [t for t in self._minute_data_call_times if current_time - t < 60]
|
||||
|
||||
# 调用接口
|
||||
hist_data = self.pro.stk_mins(
|
||||
ts_code=ts_code,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
freq=period
|
||||
)
|
||||
|
||||
# 记录调用时间
|
||||
self._minute_data_call_times.append(time.time())
|
||||
else:
|
||||
hist_data = self.pro.daily(
|
||||
ts_code=ts_code,
|
||||
@ -602,21 +636,79 @@ class TushareFetcher:
|
||||
df = df.drop_duplicates(subset=['ts_code'], keep='first')
|
||||
logger.info(f"去重后数据: {len(df)} 条")
|
||||
|
||||
# 2. 按rank排序,处理排名异常
|
||||
# 2. 过滤退市和ST股票
|
||||
if 'ts_name' in df.columns:
|
||||
before_filter = len(df)
|
||||
# 过滤包含以下关键词的股票:退市、ST、*ST、PT
|
||||
# 使用(?:...)非捕获组避免警告
|
||||
filter_pattern = r'(?:退市|^\*?ST|^ST|^PT|暂停)'
|
||||
df = df[~df['ts_name'].str.contains(filter_pattern, na=False, case=False, regex=True)]
|
||||
filtered_count = before_filter - len(df)
|
||||
if filtered_count > 0:
|
||||
logger.info(f"过滤退市/ST股票: {filtered_count} 只")
|
||||
|
||||
# 3. 按rank排序,处理排名异常
|
||||
if 'rank' in df.columns:
|
||||
df = df.sort_values('rank')
|
||||
# 重新分配连续排名
|
||||
df['original_rank'] = df['rank'] # 保留原始排名
|
||||
df['rank'] = range(1, len(df) + 1) # 重新分配连续排名
|
||||
|
||||
# 3. 只取前limit条
|
||||
df = df.head(limit)
|
||||
|
||||
# 4. 统一列名格式
|
||||
# 4. 统一列名格式(先改名,方便后续处理)
|
||||
if 'ts_code' in df.columns:
|
||||
df.rename(columns={'ts_code': 'stock_code', 'ts_name': 'short_name'}, inplace=True)
|
||||
|
||||
# 5. 添加数据源标识和有用字段
|
||||
# 5. 二次验证:通过TuShare API验证股票状态,过滤ST和退市股票
|
||||
# 为了确保最终有足够数量,验证更多股票
|
||||
if self.pro and 'stock_code' in df.columns:
|
||||
# 计算需要验证的数量:考虑约10%的过滤率,验证limit*1.15的股票
|
||||
verify_count = min(int(limit * 1.15), len(df))
|
||||
df_to_verify = df.head(verify_count)
|
||||
|
||||
valid_stocks = []
|
||||
verified_count = 0
|
||||
|
||||
for idx, row in df_to_verify.iterrows():
|
||||
verified_count += 1
|
||||
try:
|
||||
stock_code = row['stock_code']
|
||||
stock_info = self.pro.stock_basic(ts_code=stock_code, fields='ts_code,name,list_status')
|
||||
|
||||
if not stock_info.empty:
|
||||
info = stock_info.iloc[0]
|
||||
real_name = info['name']
|
||||
list_status = info['list_status']
|
||||
|
||||
# 检查股票状态和名称
|
||||
if list_status == 'L': # 只保留正常上市的股票
|
||||
# 检查真实名称是否包含ST/退市等
|
||||
st_pattern = r'(退市|^\*?ST|^ST|^PT|暂停)'
|
||||
if not re.search(st_pattern, real_name):
|
||||
valid_stocks.append(idx)
|
||||
else:
|
||||
logger.debug(f"二次过滤: {stock_code} - {real_name}")
|
||||
else:
|
||||
logger.debug(f"二次过滤: {stock_code} - 状态:{list_status}")
|
||||
except Exception as e:
|
||||
logger.debug(f"验证股票{row['stock_code']}失败: {e}")
|
||||
continue
|
||||
|
||||
# 如果已经有足够的有效股票,可以提前结束
|
||||
if len(valid_stocks) >= limit:
|
||||
break
|
||||
|
||||
if valid_stocks:
|
||||
df = df.loc[valid_stocks]
|
||||
filtered_count = verified_count - len(valid_stocks)
|
||||
logger.info(f"二次验证: 验证{verified_count}只,过滤{filtered_count}只,剩余{len(df)}只")
|
||||
|
||||
# 6. 最终确保返回limit数量的股票
|
||||
if len(df) < limit:
|
||||
logger.warning(f"过滤后股票不足: 需要{limit}只,实际{len(df)}只")
|
||||
|
||||
df = df.head(limit)
|
||||
|
||||
# 8. 添加数据源标识和有用字段
|
||||
df['source'] = '同花顺热股'
|
||||
|
||||
# 保留有用的原始字段
|
||||
@ -667,13 +759,24 @@ class TushareFetcher:
|
||||
return self._get_fallback_hot_stocks(limit)
|
||||
|
||||
# 数据处理和标准化
|
||||
# 1. 过滤退市和ST股票
|
||||
if 'ts_name' in df.columns:
|
||||
before_filter = len(df)
|
||||
# 使用(?:...)非捕获组避免警告
|
||||
filter_pattern = r'(?:退市|^\*?ST|^ST|^PT|暂停)'
|
||||
df = df[~df['ts_name'].str.contains(filter_pattern, na=False, case=False, regex=True)]
|
||||
filtered_count = before_filter - len(df)
|
||||
if filtered_count > 0:
|
||||
logger.info(f"过滤退市/ST股票: {filtered_count} 只")
|
||||
|
||||
# 2. 限制数量
|
||||
df = df.head(limit)
|
||||
|
||||
# 统一列名格式
|
||||
# 3. 统一列名格式
|
||||
if 'ts_code' in df.columns:
|
||||
df.rename(columns={'ts_code': 'stock_code', 'ts_name': 'short_name'}, inplace=True)
|
||||
|
||||
# 添加数据源标识
|
||||
# 4. 添加数据源标识
|
||||
df['source'] = '东财人气榜'
|
||||
if 'rank' not in df.columns:
|
||||
df['rank'] = range(1, len(df) + 1)
|
||||
|
||||
@ -6,6 +6,7 @@ MySQL数据库管理器
|
||||
|
||||
import pymysql
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Dict, Any, Optional, List
|
||||
import json
|
||||
@ -15,6 +16,22 @@ from loguru import logger
|
||||
from sqlalchemy import create_engine
|
||||
import warnings
|
||||
|
||||
|
||||
class DateTimeEncoder(json.JSONEncoder):
|
||||
"""自定义JSON编码器,处理pandas Timestamp和numpy类型"""
|
||||
def default(self, obj):
|
||||
if isinstance(obj, pd.Timestamp):
|
||||
return obj.strftime('%Y-%m-%d %H:%M:%S')
|
||||
elif isinstance(obj, (datetime, date)):
|
||||
return obj.isoformat()
|
||||
elif isinstance(obj, np.integer):
|
||||
return int(obj)
|
||||
elif isinstance(obj, np.floating):
|
||||
return float(obj)
|
||||
elif isinstance(obj, np.ndarray):
|
||||
return obj.tolist()
|
||||
return super().default(obj)
|
||||
|
||||
# 添加项目根目录到路径
|
||||
current_dir = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(current_dir))
|
||||
@ -255,8 +272,16 @@ class MySQLDatabaseManager:
|
||||
logger.error(f"更新扫描会话统计失败: {e}")
|
||||
raise
|
||||
|
||||
def save_stock_signal(self, session_id: int, strategy_id: int, signal: Dict[str, Any]) -> int:
|
||||
"""保存股票信号"""
|
||||
def save_stock_signal(self, session_id: int, strategy_id: int, signal: Dict[str, Any], asset_type: str = 'stock') -> int:
|
||||
"""
|
||||
保存股票/加密货币信号
|
||||
|
||||
Args:
|
||||
session_id: 扫描会话ID
|
||||
strategy_id: 策略ID
|
||||
signal: 信号字典
|
||||
asset_type: 资产类型,'stock'(股票) 或 'crypto'(加密货币)
|
||||
"""
|
||||
try:
|
||||
with pymysql.connect(**self.connection_params) as conn:
|
||||
cursor = conn.cursor()
|
||||
@ -281,26 +306,27 @@ class MySQLDatabaseManager:
|
||||
elif hasattr(confirmation_date, 'date'):
|
||||
confirmation_date = confirmation_date.date()
|
||||
|
||||
# 准备K线数据
|
||||
k1_data = json.dumps(signal.get('k1', {})) if signal.get('k1') else None
|
||||
k2_data = json.dumps(signal.get('k2', {})) if signal.get('k2') else None
|
||||
k3_data = json.dumps(signal.get('k3', {})) if signal.get('k3') else None
|
||||
k4_data = json.dumps(signal.get('k4', {})) if signal.get('k4') else None
|
||||
# 准备K线数据(使用自定义编码器处理pandas Timestamp)
|
||||
k1_data = json.dumps(signal.get('k1', {}), cls=DateTimeEncoder) if signal.get('k1') else None
|
||||
k2_data = json.dumps(signal.get('k2', {}), cls=DateTimeEncoder) if signal.get('k2') else None
|
||||
k3_data = json.dumps(signal.get('k3', {}), cls=DateTimeEncoder) if signal.get('k3') else None
|
||||
k4_data = json.dumps(signal.get('k4', {}), cls=DateTimeEncoder) if signal.get('k4') else None
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO stock_signals (
|
||||
session_id, strategy_id, stock_code, stock_name, timeframe,
|
||||
session_id, strategy_id, stock_code, stock_name, asset_type, timeframe,
|
||||
signal_date, signal_type, breakout_price, yin_high, breakout_amount,
|
||||
breakout_pct, ema20_price, yang1_entity_ratio, yang2_entity_ratio,
|
||||
final_yang_entity_ratio, turnover_ratio, above_ema20,
|
||||
new_high_confirmed, new_high_price, new_high_date, confirmation_date,
|
||||
confirmation_days, pullback_distance,
|
||||
k1_data, k2_data, k3_data, k4_data
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (
|
||||
session_id, strategy_id,
|
||||
signal.get('stock_code'),
|
||||
signal.get('stock_name'),
|
||||
asset_type, # 新增资产类型
|
||||
signal.get('timeframe'),
|
||||
signal_date,
|
||||
signal.get('pattern_type', '两阳+阴+阳突破'),
|
||||
@ -313,8 +339,8 @@ class MySQLDatabaseManager:
|
||||
signal.get('yang2_entity_ratio'),
|
||||
signal.get('final_yang_entity_ratio'),
|
||||
signal.get('turnover_ratio'),
|
||||
signal.get('above_ema20'),
|
||||
signal.get('new_high_confirmed', False),
|
||||
1 if signal.get('above_ema20') else 0, # 转换布尔值为整数
|
||||
1 if signal.get('new_high_confirmed', False) else 0, # 转换布尔值为整数
|
||||
signal.get('new_high_price'),
|
||||
new_high_date,
|
||||
confirmation_date,
|
||||
@ -333,18 +359,33 @@ class MySQLDatabaseManager:
|
||||
logger.error(f"保存股票信号失败: {e}")
|
||||
raise
|
||||
|
||||
def get_latest_signals(self, strategy_name: str = None, limit: int = 100) -> pd.DataFrame:
|
||||
"""获取最新信号"""
|
||||
def get_latest_signals(self, strategy_name: str = None, asset_type: str = None, limit: int = 100) -> pd.DataFrame:
|
||||
"""
|
||||
获取最新信号
|
||||
|
||||
Args:
|
||||
strategy_name: 策略名称过滤
|
||||
asset_type: 资产类型过滤,'stock'(股票) 或 'crypto'(加密货币)
|
||||
limit: 返回数量限制
|
||||
"""
|
||||
try:
|
||||
sql = """
|
||||
SELECT * FROM latest_signals_view
|
||||
"""
|
||||
params = []
|
||||
conditions = []
|
||||
|
||||
if strategy_name:
|
||||
sql += " WHERE strategy_name = %s"
|
||||
conditions.append("strategy_name = %s")
|
||||
params.append(strategy_name)
|
||||
|
||||
if asset_type:
|
||||
conditions.append("asset_type = %s")
|
||||
params.append(asset_type)
|
||||
|
||||
if conditions:
|
||||
sql += " WHERE " + " AND ".join(conditions)
|
||||
|
||||
sql += " LIMIT %s"
|
||||
params.append(limit)
|
||||
|
||||
@ -355,8 +396,18 @@ class MySQLDatabaseManager:
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_signals_by_date_range(self, start_date: date, end_date: date = None,
|
||||
strategy_name: str = None, timeframe: str = None) -> pd.DataFrame:
|
||||
"""按日期范围获取信号"""
|
||||
strategy_name: str = None, asset_type: str = None,
|
||||
timeframe: str = None) -> pd.DataFrame:
|
||||
"""
|
||||
按日期范围获取信号
|
||||
|
||||
Args:
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
strategy_name: 策略名称过滤
|
||||
asset_type: 资产类型过滤,'stock'(股票) 或 'crypto'(加密货币)
|
||||
timeframe: 时间周期过滤
|
||||
"""
|
||||
try:
|
||||
if end_date is None:
|
||||
end_date = date.today()
|
||||
@ -371,6 +422,10 @@ class MySQLDatabaseManager:
|
||||
sql += " AND strategy_name = %s"
|
||||
params.append(strategy_name)
|
||||
|
||||
if asset_type:
|
||||
sql += " AND asset_type = %s"
|
||||
params.append(asset_type)
|
||||
|
||||
if timeframe:
|
||||
sql += " AND timeframe = %s"
|
||||
params.append(timeframe)
|
||||
|
||||
@ -25,13 +25,14 @@ CREATE TABLE IF NOT EXISTS scan_sessions (
|
||||
FOREIGN KEY (strategy_id) REFERENCES strategies(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 股票信号表:存储具体的股票筛选信号
|
||||
-- 股票信号表:存储具体的股票筛选信号(包括股票和加密货币)
|
||||
CREATE TABLE IF NOT EXISTS stock_signals (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
session_id INT,
|
||||
strategy_id INT,
|
||||
stock_code VARCHAR(20) NOT NULL,
|
||||
stock_name VARCHAR(100),
|
||||
asset_type VARCHAR(20) DEFAULT 'stock' COMMENT '资产类型: stock(股票), crypto(加密货币)',
|
||||
timeframe VARCHAR(20) NOT NULL,
|
||||
signal_date DATE NOT NULL,
|
||||
signal_type VARCHAR(100) NOT NULL,
|
||||
@ -73,7 +74,8 @@ CREATE TABLE IF NOT EXISTS stock_signals (
|
||||
INDEX idx_signal_date (signal_date),
|
||||
INDEX idx_strategy_id (strategy_id),
|
||||
INDEX idx_session_id (session_id),
|
||||
INDEX idx_new_high_confirmed (new_high_confirmed)
|
||||
INDEX idx_new_high_confirmed (new_high_confirmed),
|
||||
INDEX idx_asset_type (asset_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 回踩监控表:存储回踩提醒信息
|
||||
@ -82,6 +84,7 @@ CREATE TABLE IF NOT EXISTS pullback_alerts (
|
||||
signal_id INT,
|
||||
stock_code VARCHAR(20) NOT NULL,
|
||||
stock_name VARCHAR(100),
|
||||
asset_type VARCHAR(20) DEFAULT 'stock' COMMENT '资产类型: stock(股票), crypto(加密货币)',
|
||||
timeframe VARCHAR(20) NOT NULL,
|
||||
|
||||
-- 原始信号信息
|
||||
@ -116,6 +119,7 @@ CREATE OR REPLACE VIEW latest_signals_view AS
|
||||
SELECT
|
||||
ss.stock_code,
|
||||
ss.stock_name,
|
||||
ss.asset_type,
|
||||
ss.timeframe,
|
||||
ss.signal_date,
|
||||
ss.breakout_price,
|
||||
|
||||
@ -198,10 +198,15 @@ class StrategyExecutor:
|
||||
result.add_strategy_results(strategy_results, actual_analyzed)
|
||||
|
||||
# 第3步: 发送通知
|
||||
if send_notification and result.total_signals_found > 0:
|
||||
if send_notification:
|
||||
logger.info("📱 第3步: 发送分析结果通知...")
|
||||
try:
|
||||
self._send_notification(result, strategy_results)
|
||||
# 先发送扫描报告(无论是否有信号)
|
||||
self._send_scan_report(result)
|
||||
|
||||
# 如果有信号,再发送详细信号汇总
|
||||
if result.total_signals_found > 0:
|
||||
self._send_signal_summary(result, strategy_results)
|
||||
except Exception as e:
|
||||
logger.warning(f"发送通知失败: {e}")
|
||||
|
||||
@ -213,42 +218,66 @@ class StrategyExecutor:
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# 发送策略摘要通知
|
||||
def _send_scan_report(self, result: ExecutionResult):
|
||||
"""发送扫描报告(无论是否有信号)"""
|
||||
try:
|
||||
scan_report = {
|
||||
'task_id': result.task_id,
|
||||
'strategy_name': result.strategy_name,
|
||||
'stock_pool_name': result.stock_pool_info.get('rule_display_name', '未知股票池'),
|
||||
'total_stocks_analyzed': result.total_stocks_analyzed,
|
||||
'stocks_with_signals': result.stocks_with_signals,
|
||||
'total_signals_found': result.total_signals_found,
|
||||
'execution_time': result.execution_time,
|
||||
'scan_time': result.start_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'success': result.success,
|
||||
'error': result.error
|
||||
}
|
||||
|
||||
success = self.notification_manager.send_scan_report(scan_report)
|
||||
if success:
|
||||
logger.info("📱 扫描报告发送成功")
|
||||
else:
|
||||
logger.warning("📱 扫描报告发送失败")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送扫描报告失败: {e}")
|
||||
|
||||
def _send_signal_summary(self, result: ExecutionResult, strategy_results: Dict[str, Dict[str, StrategyResult]]):
|
||||
"""发送详细信号汇总(仅在有信号时调用)"""
|
||||
try:
|
||||
# 构建策略结果摘要
|
||||
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
|
||||
}
|
||||
|
||||
# 转换StrategyResult格式为通知所需格式
|
||||
all_signals = {}
|
||||
# 注意:这里传递的是完整的StrategyResult对象,而不仅仅是signals列表
|
||||
all_results = {}
|
||||
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
|
||||
# 只要有信号或形态就添加
|
||||
if strategy_result.get_signal_count() > 0 or strategy_result.get_pattern_count() > 0:
|
||||
if stock_code not in all_results:
|
||||
all_results[stock_code] = {}
|
||||
all_results[stock_code][timeframe] = strategy_result
|
||||
|
||||
success = self.notification_manager.send_strategy_summary(
|
||||
all_signals,
|
||||
all_results,
|
||||
strategy_summary
|
||||
)
|
||||
if success:
|
||||
logger.info("📱 策略结果通知发送成功")
|
||||
logger.info("📱 信号汇总通知发送成功")
|
||||
else:
|
||||
logger.warning("📱 策略结果通知发送失败")
|
||||
logger.warning("📱 信号汇总通知发送失败")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送策略摘要通知失败: {e}")
|
||||
logger.error(f"发送信号汇总通知失败: {e}")
|
||||
|
||||
def get_registered_strategies(self) -> Dict[str, str]:
|
||||
"""
|
||||
|
||||
@ -18,6 +18,7 @@ class StrategyResult:
|
||||
stock_code: str,
|
||||
timeframe: str,
|
||||
signals: List[Dict[str, Any]] = None,
|
||||
patterns: List[Dict[str, Any]] = None,
|
||||
success: bool = True,
|
||||
error: str = None,
|
||||
execution_time: float = 0.0):
|
||||
@ -28,7 +29,8 @@ class StrategyResult:
|
||||
strategy_name: 策略名称
|
||||
stock_code: 股票代码
|
||||
timeframe: 时间周期
|
||||
signals: 信号列表
|
||||
signals: 信号列表(已确认信号)
|
||||
patterns: 形态列表(形态形成但未产生信号)
|
||||
success: 是否成功
|
||||
error: 错误信息
|
||||
execution_time: 执行时间
|
||||
@ -37,6 +39,7 @@ class StrategyResult:
|
||||
self.stock_code = stock_code
|
||||
self.timeframe = timeframe
|
||||
self.signals = signals or []
|
||||
self.patterns = patterns or []
|
||||
self.success = success
|
||||
self.error = error
|
||||
self.execution_time = execution_time
|
||||
@ -46,10 +49,18 @@ class StrategyResult:
|
||||
"""是否有信号"""
|
||||
return len(self.signals) > 0
|
||||
|
||||
def has_patterns(self) -> bool:
|
||||
"""是否有形态"""
|
||||
return len(self.patterns) > 0
|
||||
|
||||
def get_signal_count(self) -> int:
|
||||
"""获取信号数量"""
|
||||
return len(self.signals)
|
||||
|
||||
def get_pattern_count(self) -> int:
|
||||
"""获取形态数量"""
|
||||
return len(self.patterns)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
@ -57,7 +68,9 @@ class StrategyResult:
|
||||
'stock_code': self.stock_code,
|
||||
'timeframe': self.timeframe,
|
||||
'signals': self.signals,
|
||||
'patterns': self.patterns,
|
||||
'signal_count': self.get_signal_count(),
|
||||
'pattern_count': self.get_pattern_count(),
|
||||
'success': self.success,
|
||||
'error': self.error,
|
||||
'execution_time': self.execution_time,
|
||||
@ -111,6 +124,7 @@ class BaseStrategy(ABC):
|
||||
|
||||
results = {}
|
||||
total_signals = 0
|
||||
total_patterns = 0
|
||||
|
||||
logger.info(f"🔍 开始分析股票池,共 {len(stock_list)} 只股票")
|
||||
|
||||
@ -121,21 +135,32 @@ class BaseStrategy(ABC):
|
||||
# 分析单只股票
|
||||
stock_results = self.analyze_stock(stock_code)
|
||||
|
||||
# 统计信号数量
|
||||
# 统计信号和形态数量
|
||||
stock_signal_count = sum(
|
||||
result.get_signal_count() for result in stock_results.values()
|
||||
)
|
||||
stock_pattern_count = sum(
|
||||
result.get_pattern_count() for result in stock_results.values()
|
||||
)
|
||||
|
||||
if stock_signal_count > 0:
|
||||
# 只要有信号或形态就添加到结果中
|
||||
if stock_signal_count > 0 or stock_pattern_count > 0:
|
||||
results[stock_code] = stock_results
|
||||
total_signals += stock_signal_count
|
||||
logger.info(f"✅ {stock_code} 发现 {stock_signal_count} 个信号")
|
||||
total_patterns += stock_pattern_count
|
||||
|
||||
if stock_signal_count > 0 and stock_pattern_count > 0:
|
||||
logger.info(f"✅ {stock_code} 发现 {stock_signal_count} 个信号, {stock_pattern_count} 个形态")
|
||||
elif stock_signal_count > 0:
|
||||
logger.info(f"✅ {stock_code} 发现 {stock_signal_count} 个信号")
|
||||
else:
|
||||
logger.info(f"📊 {stock_code} 发现 {stock_pattern_count} 个形态")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 分析股票 {stock_code} 失败: {e}")
|
||||
continue
|
||||
|
||||
logger.info(f"🎉 股票池分析完成: 扫描 {len(stock_list)} 只,发现 {total_signals} 个信号,涉及 {len(results)} 只股票")
|
||||
logger.info(f"🎉 股票池分析完成: 扫描 {len(stock_list)} 只,发现 {total_signals} 个信号, {total_patterns} 个形态,涉及 {len(results)} 只股票")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
587
src/strategy/crypto_kline_pattern_strategy.py
Normal file
587
src/strategy/crypto_kline_pattern_strategy.py
Normal file
@ -0,0 +1,587 @@
|
||||
"""
|
||||
加密货币K线形态策略模块
|
||||
基于原有K线形态策略,适配加密货币市场
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from typing import Dict, List, Any
|
||||
from datetime import datetime, timedelta
|
||||
from loguru import logger
|
||||
|
||||
from ..data.binance_fetcher import BinanceFetcher
|
||||
from ..utils.notification import NotificationManager
|
||||
from ..database.mysql_database_manager import MySQLDatabaseManager
|
||||
from .kline_pattern_strategy import KLinePatternStrategy
|
||||
from .base_strategy import StrategyResult
|
||||
|
||||
|
||||
class CryptoKLinePatternStrategy(KLinePatternStrategy):
|
||||
"""加密货币K线形态策略类"""
|
||||
|
||||
def __init__(self, data_fetcher: BinanceFetcher, notification_manager: NotificationManager,
|
||||
config: Dict[str, Any], db_manager: MySQLDatabaseManager = None):
|
||||
"""
|
||||
初始化加密货币K线形态策略
|
||||
|
||||
Args:
|
||||
data_fetcher: Binance数据获取器
|
||||
notification_manager: 通知管理器
|
||||
config: 策略配置
|
||||
db_manager: 数据库管理器
|
||||
"""
|
||||
# 先调用父类构造函数初始化基本属性
|
||||
super().__init__(data_fetcher, notification_manager, config, db_manager)
|
||||
|
||||
# 然后替换和定制加密货币特有属性
|
||||
self.data_fetcher = data_fetcher # 替换为Binance数据获取器
|
||||
self.strategy_name = "加密货币K线形态策略"
|
||||
self.timeframes = config.get('timeframes', ['4hour', 'daily', 'weekly'])
|
||||
self.max_turnover_ratio = config.get('max_turnover_ratio', 100.0) # 加密货币换手率通常更高
|
||||
|
||||
# 加密货币特有参数
|
||||
self.quote_asset = config.get('quote_asset', 'USDT') # 计价货币
|
||||
self.min_volume_usdt = config.get('min_volume_usdt', 1000000) # 最小24h交易量(USDT)
|
||||
|
||||
# 热门币种缓存机制
|
||||
self._hot_symbols_cache = None
|
||||
self._cache_timestamp = None
|
||||
self._cache_duration = 300 # 缓存5分钟
|
||||
|
||||
# 更新策略ID为加密货币版本
|
||||
self.strategy_id = self.db_manager.create_or_update_strategy(
|
||||
strategy_name=self.strategy_name,
|
||||
strategy_type="crypto_kline_pattern",
|
||||
description="加密货币强势上涨+多空博弈+突破确认形态识别策略",
|
||||
config=config
|
||||
)
|
||||
|
||||
logger.info(f"加密货币K线形态策略初始化完成 (策略ID: {self.strategy_id})")
|
||||
|
||||
def _get_cached_hot_symbols(self, max_symbols: int) -> List[str]:
|
||||
"""
|
||||
获取缓存的热门交易对列表
|
||||
|
||||
Args:
|
||||
max_symbols: 最大交易对数量
|
||||
|
||||
Returns:
|
||||
交易对列表
|
||||
"""
|
||||
import time
|
||||
current_time = time.time()
|
||||
|
||||
# 检查缓存是否有效
|
||||
if (self._hot_symbols_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_symbols_cache)} 个)")
|
||||
return self._hot_symbols_cache[:max_symbols]
|
||||
|
||||
# 缓存失效或不存在,重新获取
|
||||
logger.info(f"🔥 获取热门交易对 (TOP{max_symbols})...")
|
||||
hot_symbols = self.data_fetcher.get_top_volume_symbols(
|
||||
quote_asset=self.quote_asset,
|
||||
limit=max_symbols * 2 # 多获取一些以备过滤
|
||||
)
|
||||
|
||||
if hot_symbols:
|
||||
self._hot_symbols_cache = hot_symbols
|
||||
self._cache_timestamp = current_time
|
||||
|
||||
logger.info(f"✅ 热门交易对获取成功,已缓存 {len(self._hot_symbols_cache)} 个")
|
||||
return self._hot_symbols_cache[:max_symbols]
|
||||
else:
|
||||
logger.error("❌ 热门交易对数据为空")
|
||||
return []
|
||||
|
||||
def clear_hot_symbols_cache(self):
|
||||
"""清除热门交易对缓存,强制下次重新获取"""
|
||||
self._hot_symbols_cache = None
|
||||
self._cache_timestamp = None
|
||||
logger.info("🔄 热门交易对缓存已清除")
|
||||
|
||||
def _check_current_price_validity(self, symbol: str, entry_price: float, tolerance: float = 0.05) -> bool:
|
||||
"""
|
||||
检查当前价格是否仍然适合入场(不低于入场价的5%)
|
||||
|
||||
Args:
|
||||
symbol: 交易对符号
|
||||
entry_price: 建议入场价格
|
||||
tolerance: 价格下跌容忍度,默认5%
|
||||
|
||||
Returns:
|
||||
bool: 当前价格是否仍然有效
|
||||
"""
|
||||
try:
|
||||
# 获取最新价格数据(使用24小时数据)
|
||||
current_data = self.data_fetcher.get_historical_klines(
|
||||
symbol, '1d', limit=2 # 获取最近2天数据
|
||||
)
|
||||
|
||||
if current_data.empty:
|
||||
logger.warning(f"无法获取 {symbol} 的最新价格数据")
|
||||
return True # 无法获取价格时保守处理,保留信号
|
||||
|
||||
current_price = current_data.iloc[-1]['close']
|
||||
min_valid_price = entry_price * (1 - tolerance)
|
||||
|
||||
is_valid = current_price >= min_valid_price
|
||||
|
||||
if not is_valid:
|
||||
price_drop_pct = (entry_price - current_price) / entry_price * 100
|
||||
logger.info(f"🔻 {symbol} 价格过滤: 当前价 {current_price:.4f} < 入场价 {entry_price:.4f} (下跌{price_drop_pct:.1f}%)")
|
||||
|
||||
return is_valid
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"检查 {symbol} 当前价格失败: {e}")
|
||||
return True # 出错时保守处理,保留信号
|
||||
|
||||
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
|
||||
|
||||
from datetime import datetime, date
|
||||
import pandas as pd
|
||||
|
||||
current_date = datetime.now().date()
|
||||
recent_signals = []
|
||||
price_filtered_count = 0
|
||||
|
||||
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, date):
|
||||
continue
|
||||
|
||||
# 计算信号距今天数
|
||||
days_ago = (current_date - signal_date).days
|
||||
|
||||
# 只保留最近N天内的信号
|
||||
if days_ago <= days:
|
||||
# 检查当前价格是否仍然有效
|
||||
symbol = signal.get('stock_code', '') # 在加密货币中,stock_code存储的是交易对符号
|
||||
entry_price = signal.get('yin_high', 0)
|
||||
|
||||
if symbol and entry_price > 0:
|
||||
if self._check_current_price_validity(symbol, entry_price):
|
||||
recent_signals.append(signal)
|
||||
logger.debug(f"✅ 保留有效信号: {symbol} {signal_date} (距今{days_ago}天)")
|
||||
else:
|
||||
price_filtered_count += 1
|
||||
logger.debug(f"🔻 价格过滤信号: {symbol} {signal_date} (价格已跌破入场价)")
|
||||
else:
|
||||
# 缺少必要信息时保守处理
|
||||
recent_signals.append(signal)
|
||||
logger.debug(f"✅ 保留信号(缺少价格信息): {signal_date} (距今{days_ago}天)")
|
||||
else:
|
||||
logger.debug(f"🗓️ 过滤历史信号: {signal_date} (距今{days_ago}天)")
|
||||
|
||||
# 统计过滤结果
|
||||
time_filtered_count = len(signals) - len(recent_signals) - price_filtered_count
|
||||
if len(recent_signals) != len(signals):
|
||||
logger.info(f"📅 加密货币信号过滤统计: 总共{len(signals)}个 → 保留{len(recent_signals)}个")
|
||||
if time_filtered_count > 0:
|
||||
logger.info(f" 🗓️ 时间过滤: {time_filtered_count}个")
|
||||
if price_filtered_count > 0:
|
||||
logger.info(f" 🔻 价格过滤: {price_filtered_count}个")
|
||||
|
||||
return recent_signals
|
||||
|
||||
def analyze_symbol(self, symbol: str, timeframes: List[str] = None,
|
||||
session_id: int = None) -> Dict[str, StrategyResult]:
|
||||
"""
|
||||
分析单个交易对的K线形态
|
||||
|
||||
Args:
|
||||
symbol: 交易对符号,如'BTCUSDT'
|
||||
timeframes: 时间周期列表,如果为None则使用策略默认周期
|
||||
session_id: 扫描会话ID
|
||||
|
||||
Returns:
|
||||
时间周期到策略结果的映射
|
||||
"""
|
||||
if timeframes is None:
|
||||
timeframes = self.timeframes
|
||||
|
||||
results = {}
|
||||
symbol_name = self.data_fetcher.get_symbol_name(symbol)
|
||||
|
||||
for timeframe in timeframes:
|
||||
start_time = datetime.now()
|
||||
try:
|
||||
# 转换时间周期格式
|
||||
binance_interval = self.data_fetcher.convert_timeframe(timeframe)
|
||||
|
||||
# 计算需要获取的K线数量(使用limit更稳定)
|
||||
if timeframe == 'daily':
|
||||
limit = 60 # 获取60根日线
|
||||
elif timeframe == '4hour':
|
||||
limit = 180 # 获取180根4小时线(约30天)
|
||||
elif timeframe == 'weekly':
|
||||
limit = 26 # 获取26根周线(约半年)
|
||||
else:
|
||||
limit = 100 # 默认100根
|
||||
|
||||
logger.info(f"🔍 分析交易对: {symbol} | 周期: {timeframe}")
|
||||
|
||||
# 获取历史数据(使用limit参数,更可靠)
|
||||
df = self.data_fetcher.get_historical_klines(
|
||||
symbol, binance_interval, limit=limit
|
||||
)
|
||||
|
||||
if df.empty:
|
||||
logger.warning(f"{symbol} {timeframe} 数据为空")
|
||||
results[timeframe] = StrategyResult(
|
||||
strategy_name=self.strategy_name,
|
||||
stock_code=symbol,
|
||||
timeframe=timeframe,
|
||||
signals=[],
|
||||
success=False,
|
||||
error="数据为空",
|
||||
execution_time=(datetime.now() - start_time).total_seconds()
|
||||
)
|
||||
continue
|
||||
|
||||
# 计算K线特征
|
||||
df_with_features = self.calculate_kline_features(df)
|
||||
|
||||
# 检测形态(返回已确认信号和形态形成)
|
||||
confirmed_signals, formed_patterns = self.detect_pattern(df_with_features)
|
||||
|
||||
# 过滤一周内的信号和形态
|
||||
recent_signals = self._filter_recent_signals(confirmed_signals, days=7)
|
||||
recent_patterns = self._filter_recent_signals(formed_patterns, 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_code': symbol, # 添加交易对代码
|
||||
'stock_name': symbol_name,
|
||||
'status': 'confirmed',
|
||||
'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'],
|
||||
'breakout_position': signal.get('breakout_position', 4),
|
||||
'pattern_subtype': signal.get('pattern_subtype', '') # 添加形态子类型
|
||||
}
|
||||
}
|
||||
|
||||
# 添加回踩确认信息
|
||||
if not signal.get('confirmation_pending', True):
|
||||
formatted_signal['details'].update({
|
||||
'new_high_confirmed': signal.get('new_high_confirmed', True),
|
||||
'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'] = symbol
|
||||
signal['stock_name'] = symbol_name
|
||||
signal['timeframe'] = timeframe
|
||||
self.add_triggered_signal(signal)
|
||||
|
||||
# 保存信号到数据库(标记为加密货币)
|
||||
if session_id is not None:
|
||||
try:
|
||||
signal_id = self.db_manager.save_stock_signal(
|
||||
session_id=session_id,
|
||||
strategy_id=self.strategy_id,
|
||||
signal=signal,
|
||||
asset_type='crypto' # 标记为加密货币
|
||||
)
|
||||
logger.debug(f"加密货币信号已保存到数据库: signal_id={signal_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"保存加密货币信号到数据库失败: {e}")
|
||||
|
||||
# 处理形态形成格式
|
||||
formatted_patterns = []
|
||||
for pattern in recent_patterns:
|
||||
formatted_pattern = {
|
||||
'date': pattern['date'],
|
||||
'pattern_type': pattern['pattern_type'],
|
||||
'price': pattern['breakout_price'],
|
||||
'confidence': pattern['final_yang_entity_ratio'],
|
||||
'stock_code': symbol, # 添加交易对代码
|
||||
'stock_name': symbol_name,
|
||||
'status': 'formed',
|
||||
'details': {
|
||||
'yin_high': pattern['yin_high'],
|
||||
'breakout_amount': pattern['breakout_amount'],
|
||||
'breakout_pct': pattern['breakout_pct'],
|
||||
'ema20_price': pattern['ema20_price'],
|
||||
'turnover_ratio': pattern['turnover_ratio'],
|
||||
'breakout_position': pattern.get('breakout_position', 4),
|
||||
'pattern_subtype': pattern.get('pattern_subtype', '') # 添加形态子类型
|
||||
}
|
||||
}
|
||||
|
||||
formatted_patterns.append(formatted_pattern)
|
||||
|
||||
execution_time = (datetime.now() - start_time).total_seconds()
|
||||
results[timeframe] = StrategyResult(
|
||||
strategy_name=self.strategy_name,
|
||||
stock_code=symbol,
|
||||
timeframe=timeframe,
|
||||
signals=formatted_signals,
|
||||
patterns=formatted_patterns,
|
||||
success=True,
|
||||
execution_time=execution_time
|
||||
)
|
||||
|
||||
# 美化统计日志
|
||||
if formatted_signals or formatted_patterns:
|
||||
logger.info(f"✅ {symbol} {timeframe}周期: 信号={len(formatted_signals)}个, 形态={len(formatted_patterns)}个")
|
||||
|
||||
if formatted_signals:
|
||||
logger.info(f" 🎯 已确认信号:")
|
||||
for i, signal in enumerate(formatted_signals, 1):
|
||||
logger.info(f" {i}. {signal['date']} | 价格: {signal['price']:.4f} | {signal['signal_type']}")
|
||||
|
||||
if formatted_patterns:
|
||||
logger.info(f" 📊 形态形成:")
|
||||
for i, pattern in enumerate(formatted_patterns, 1):
|
||||
logger.info(f" {i}. {pattern['date']} | 价格: {pattern['price']:.4f} | {pattern['pattern_type']}")
|
||||
else:
|
||||
logger.debug(f"📭 {symbol} {timeframe}周期: 无信号和形态")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分析交易对 {symbol} {timeframe}周期失败: {e}")
|
||||
execution_time = (datetime.now() - start_time).total_seconds()
|
||||
results[timeframe] = StrategyResult(
|
||||
strategy_name=self.strategy_name,
|
||||
stock_code=symbol,
|
||||
timeframe=timeframe,
|
||||
signals=[],
|
||||
patterns=[],
|
||||
success=False,
|
||||
error=str(e),
|
||||
execution_time=execution_time
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def scan_market(self, symbol_list: List[str] = None, max_symbols: int = 100) -> Dict[str, Dict[str, List[Dict[str, Any]]]]:
|
||||
"""
|
||||
扫描加密货币市场
|
||||
|
||||
Args:
|
||||
symbol_list: 交易对列表,如果为None则使用热门交易对
|
||||
max_symbols: 最大扫描交易对数量
|
||||
|
||||
Returns:
|
||||
所有交易对的分析结果
|
||||
"""
|
||||
logger.info("🚀" + "="*70)
|
||||
logger.info("🌍 开始加密货币市场K线形态扫描")
|
||||
logger.info("🚀" + "="*70)
|
||||
|
||||
# 创建扫描会话
|
||||
scan_config = {
|
||||
'max_symbols': max_symbols,
|
||||
'data_source': 'Binance',
|
||||
'quote_asset': self.quote_asset,
|
||||
'timeframes': self.timeframes
|
||||
}
|
||||
session_id = self.db_manager.create_scan_session(
|
||||
strategy_id=self.strategy_id,
|
||||
scan_config=scan_config
|
||||
)
|
||||
|
||||
if symbol_list is None:
|
||||
# 使用缓存的热门交易对数据
|
||||
symbol_list = self._get_cached_hot_symbols(max_symbols)
|
||||
|
||||
if symbol_list:
|
||||
logger.info(f"📊 数据源: Binance热门交易对 | 扫描交易对: {len(symbol_list)} 个")
|
||||
else:
|
||||
logger.error("❌ 热门交易对数据为空,无法进行扫描")
|
||||
return {}
|
||||
|
||||
results = {}
|
||||
total_signals = 0
|
||||
total_patterns = 0
|
||||
|
||||
for i, symbol in enumerate(symbol_list):
|
||||
logger.info(f"⏳ 扫描进度: [{i+1:3d}/{len(symbol_list):3d}] 🔍 {symbol}")
|
||||
|
||||
try:
|
||||
symbol_results = self.analyze_symbol(symbol, session_id=session_id)
|
||||
|
||||
# 统计信号和形态数量
|
||||
symbol_signal_count = sum(result.get_signal_count() for result in symbol_results.values())
|
||||
symbol_pattern_count = sum(result.get_pattern_count() for result in symbol_results.values())
|
||||
|
||||
# 只保留有确认信号的交易对结果,不包括仅有形态的交易对
|
||||
if symbol_signal_count > 0:
|
||||
results[symbol] = symbol_results
|
||||
total_signals += symbol_signal_count
|
||||
total_patterns += symbol_pattern_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"扫描交易对 {symbol} 失败: {e}")
|
||||
continue
|
||||
|
||||
# 更新扫描会话统计
|
||||
try:
|
||||
self.db_manager.update_scan_session_stats(
|
||||
session_id=session_id,
|
||||
total_scanned=len(symbol_list),
|
||||
total_signals=total_signals
|
||||
)
|
||||
logger.debug(f"扫描会话统计已更新: {session_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"更新扫描会话统计失败: {e}")
|
||||
|
||||
# 美化最终扫描结果
|
||||
logger.info("🎉" + "="*70)
|
||||
logger.info(f"🌍 加密货币市场K线形态扫描完成!")
|
||||
logger.info(f"📊 扫描统计:")
|
||||
logger.info(f" 🔍 总扫描交易对: {len(symbol_list)} 个")
|
||||
logger.info(f" 🎯 确认信号: {total_signals} 个")
|
||||
logger.info(f" 📊 形态形成: {total_patterns} 个")
|
||||
logger.info(f" 📈 涉及交易对: {len(results)} 个")
|
||||
logger.info(f" 💾 扫描会话ID: {session_id}")
|
||||
|
||||
if results:
|
||||
logger.info(f"📋 详细结果:")
|
||||
signal_count = 0
|
||||
pattern_count = 0
|
||||
|
||||
for symbol, symbol_results in results.items():
|
||||
for timeframe, result in symbol_results.items():
|
||||
# 显示确认信号
|
||||
for signal in result.signals:
|
||||
signal_count += 1
|
||||
logger.info(f" 🎯 信号#{signal_count}: {symbol} | {timeframe} | {signal['date']} | {signal['price']:.4f}")
|
||||
|
||||
# 显示形态形成
|
||||
for pattern in result.patterns:
|
||||
pattern_count += 1
|
||||
logger.info(f" 📊 形态#{pattern_count}: {symbol} | {timeframe} | {pattern['date']} | {pattern['price']:.4f}")
|
||||
|
||||
logger.info("🎉" + "="*70)
|
||||
|
||||
# 发送汇总通知
|
||||
if results:
|
||||
scan_stats = {
|
||||
'total_scanned': len(symbol_list),
|
||||
'data_source': f'Binance-{self.quote_asset}'
|
||||
}
|
||||
|
||||
try:
|
||||
success = self.notification_manager.send_strategy_summary(results, scan_stats)
|
||||
if success:
|
||||
logger.info("📱 策略信号汇总通知发送完成")
|
||||
else:
|
||||
logger.warning("📱 策略信号汇总通知发送失败")
|
||||
except Exception as e:
|
||||
logger.error(f"发送汇总通知失败: {e}")
|
||||
|
||||
return results
|
||||
|
||||
def get_strategy_description(self) -> str:
|
||||
"""获取策略描述"""
|
||||
trend_desc = ""
|
||||
if self.strong_trend_enabled:
|
||||
trend_desc = f"""
|
||||
【多头排列先决条件】✅
|
||||
- EMA5 > EMA10 > EMA20(三重多头排列)
|
||||
- 在形态识别、突破确认、回踩信号三个阶段都进行检查
|
||||
- 确保整个过程中始终保持强势趋势
|
||||
"""
|
||||
else:
|
||||
trend_desc = "\n【多头排列先决条件】❌ 未启用\n"
|
||||
|
||||
return f"""加密货币K线形态策略 - 强势上涨+多空博弈+突破确认
|
||||
{trend_desc}
|
||||
新形态设计:强势上涨 → 多空博弈 → 突破确认
|
||||
|
||||
【形态A:双阳+博弈】
|
||||
1. 强势阶段:2根连续阳线
|
||||
2. 博弈阶段:2-3根K线,平均实体≤40%(阴线/十字星/小阳线)
|
||||
3. 突破确认:博弈后1-3根K线内,大阳线(实体≥55%)突破博弈阶段最高价
|
||||
|
||||
【形态B:高实体+博弈】
|
||||
1. 强势阶段:1根高实体阳线(实体≥60%)
|
||||
2. 博弈阶段:2-3根K线,平均实体≤40%(阴线/十字星/小阳线)
|
||||
3. 突破确认:博弈后1-3根K线内,大阳线(实体≥55%)突破博弈阶段最高价
|
||||
|
||||
【严格约束条件】
|
||||
- EMA5 > EMA10 > EMA20(三重多头排列)全程检查
|
||||
- 价格不能跌破EMA10支撑(博弈、突破、回踩全程)
|
||||
- 价格必须创新高后回踩到博弈阶段最高点附近才产生正式信号
|
||||
- 回踩容忍度:{self.pullback_tolerance:.0%}
|
||||
- 确认窗口:{self.pullback_confirmation_days}天
|
||||
|
||||
【加密货币市场特点】
|
||||
- 计价货币: {self.quote_asset}
|
||||
- 最小24h交易量: ${self.min_volume_usdt:,.0f}
|
||||
- 支持时间周期:{', '.join(self.timeframes)}
|
||||
|
||||
【策略优势】
|
||||
- 真实市场心理:强势→博弈→突破的完整过程
|
||||
- 多重验证:EMA多头排列 + EMA10支撑 + 创新高回踩确认
|
||||
- 降低假突破:严格的形态和趋势验证
|
||||
- 优质入场时机:回踩确认提供更好的风险收益比
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
from ..data.binance_fetcher import BinanceFetcher
|
||||
from ..utils.notification import NotificationManager
|
||||
|
||||
# 模拟配置
|
||||
strategy_config = {
|
||||
'min_entity_ratio': 0.55,
|
||||
'timeframes': ['daily', '4hour'],
|
||||
'quote_asset': 'USDT'
|
||||
}
|
||||
|
||||
notification_config = {
|
||||
'dingtalk': {
|
||||
'enabled': False,
|
||||
'webhook_url': ''
|
||||
}
|
||||
}
|
||||
|
||||
# 初始化组件
|
||||
data_fetcher = BinanceFetcher()
|
||||
notification_manager = NotificationManager(notification_config)
|
||||
strategy = CryptoKLinePatternStrategy(data_fetcher, notification_manager, strategy_config)
|
||||
|
||||
print("加密货币K线形态策略初始化完成")
|
||||
print(strategy.get_strategy_description())
|
||||
File diff suppressed because it is too large
Load Diff
@ -189,7 +189,7 @@ class DingTalkNotifier:
|
||||
def send_strategy_signal(self, stock_code: str, stock_name: str, timeframe: str,
|
||||
signal_type: str, price: float, signal_date: str = None, additional_info: Dict[str, Any] = None) -> bool:
|
||||
"""
|
||||
发送策略信号通知(优化版:创新高回踩确认)
|
||||
发送单个策略信号通知(优化版:突出关键时间)
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
@ -204,78 +204,33 @@ class DingTalkNotifier:
|
||||
发送是否成功
|
||||
"""
|
||||
try:
|
||||
# 使用信号时间或当前时间
|
||||
display_time = signal_date if signal_date else datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
# 构建简洁的标题
|
||||
title = f"🎯 {signal_type}信号"
|
||||
|
||||
# 构建Markdown消息
|
||||
title = f"🎯 {signal_type}信号确认"
|
||||
# 获取关键时间信息
|
||||
pattern_date = additional_info.get('pattern_date', signal_date) if additional_info else signal_date
|
||||
confirmation_date = additional_info.get('confirmation_date', signal_date) if additional_info else signal_date
|
||||
new_high_date = additional_info.get('new_high_date', '') if additional_info else ''
|
||||
confirmation_days = additional_info.get('confirmation_days', 0) if additional_info else 0
|
||||
|
||||
# 基础信息
|
||||
# 构建简洁的消息内容,突出关键时间
|
||||
markdown_text = f"""
|
||||
# 🎯 {signal_type}信号确认
|
||||
# 🎯 {signal_type}信号
|
||||
|
||||
**股票信息:**
|
||||
- 代码: `{stock_code}`
|
||||
- 名称: `{stock_name}`
|
||||
- 确认价格: `{price}` 元
|
||||
- 时间周期: `{timeframe}`
|
||||
**{stock_code} {stock_name}** | 周期: `{timeframe}`
|
||||
|
||||
**确认时间:** {display_time}
|
||||
**⏰ 关键时间:**
|
||||
- 📅 形态形成: `{pattern_date}`
|
||||
- 🚀 创新高: `{new_high_date}`
|
||||
- ✅ 回踩确认: `{confirmation_date}`
|
||||
- ⏰ 确认用时: `{confirmation_days}天`
|
||||
|
||||
**策略说明:** 两阳+阴+阳突破(创新高回踩确认)
|
||||
"""
|
||||
|
||||
# 添加创新高回踩确认的详细信息
|
||||
if additional_info:
|
||||
# 检查是否有创新高回踩确认的关键信息
|
||||
if 'new_high_price' in additional_info and 'new_high_date' in additional_info:
|
||||
markdown_text += f"""
|
||||
**🚀 创新高回踩确认详情:**
|
||||
- 📅 模式日期: `{additional_info.get('pattern_date', '未知')}`
|
||||
- 💰 原突破价: `{additional_info.get('breakout_price', 'N/A')}` 元
|
||||
- 🌟 创新高价: `{additional_info.get('new_high_price', 'N/A')}` 元
|
||||
- 🚀 创新高日期: `{additional_info.get('new_high_date', '未知')}`
|
||||
- 🎯 阴线最高价: `{additional_info.get('yin_high', 'N/A')}` 元
|
||||
- ✅ 回踩确认日期: `{additional_info.get('confirmation_date', '未知')}`
|
||||
- ⏰ 总确认用时: `{additional_info.get('confirmation_days', 'N/A')}` 天
|
||||
- 📏 回踩距离: `{additional_info.get('pullback_distance', 'N/A')}%`
|
||||
"""
|
||||
|
||||
# 添加其他额外信息
|
||||
markdown_text += "\n**📊 技术指标:**\n"
|
||||
tech_indicators = ['yang1_entity_ratio', 'yang2_entity_ratio', 'final_yang_entity_ratio',
|
||||
'breakout_pct', 'turnover_ratio', 'above_ema20']
|
||||
for key in tech_indicators:
|
||||
if key in additional_info:
|
||||
value = additional_info[key]
|
||||
if key.endswith('_ratio') and isinstance(value, (int, float)):
|
||||
markdown_text += f"- {key}: `{value:.1%}`\n"
|
||||
elif key == 'breakout_pct':
|
||||
markdown_text += f"- 突破幅度: `{value:.2f}%`\n"
|
||||
elif key == 'turnover_ratio':
|
||||
markdown_text += f"- 换手率: `{value:.2f}%`\n"
|
||||
elif key == 'above_ema20':
|
||||
status = '✅上方' if value else '❌下方'
|
||||
markdown_text += f"- EMA20位置: `{status}`\n"
|
||||
else:
|
||||
markdown_text += f"- {key}: `{value}`\n"
|
||||
|
||||
markdown_text += """
|
||||
---
|
||||
**💡 操作建议:**
|
||||
- ✅ 信号已通过创新高回踩双重确认
|
||||
- 📈 突破有效性得到验证
|
||||
- 🎯 当前为较优入场时机
|
||||
- ⚠️ 注意风险控制,设置合理止损
|
||||
|
||||
**🔍 关键确认要素:**
|
||||
1. 🎯 形态: 两阳+阴+阳突破完成
|
||||
2. 🚀 创新高: 价格突破形态高点
|
||||
3. 📉 回踩: 回踩至阴线最高价附近
|
||||
4. ✅ 时机: 7天内完成双重确认
|
||||
**💰 价格信息:**
|
||||
- 创新高价: `{additional_info.get('new_high_price', price):.2f}元`
|
||||
- 建议入场: `{additional_info.get('yin_high', price):.2f}元`
|
||||
|
||||
---
|
||||
*K线形态策略 - 创新高回踩确认版*
|
||||
*K线形态策略 - 创新高回踩确认*
|
||||
"""
|
||||
|
||||
return self.send_markdown_message(title, markdown_text)
|
||||
@ -342,18 +297,18 @@ class NotificationManager:
|
||||
|
||||
return success
|
||||
|
||||
def send_strategy_summary(self, all_signals: Dict[str, Any], scan_stats: Dict[str, Any] = None) -> bool:
|
||||
def send_strategy_summary(self, all_results: Dict[str, Any], scan_stats: Dict[str, Any] = None) -> bool:
|
||||
"""
|
||||
发送策略信号汇总通知(支持分组发送)
|
||||
发送策略汇总通知(支持分组发送,区分形态形成和信号产生)
|
||||
|
||||
Args:
|
||||
all_signals: 所有信号的汇总数据 {stock_code: {timeframe: [signals]}}
|
||||
all_results: 所有结果数据 {stock_code: {timeframe: StrategyResult}}
|
||||
scan_stats: 扫描统计信息
|
||||
|
||||
Returns:
|
||||
发送是否成功
|
||||
"""
|
||||
if not all_signals:
|
||||
if not all_results:
|
||||
return False
|
||||
|
||||
try:
|
||||
@ -362,18 +317,26 @@ class NotificationManager:
|
||||
import pandas as pd
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 收集所有信号详情
|
||||
# 收集所有信号详情和形态详情
|
||||
all_signal_details = []
|
||||
all_pattern_details = []
|
||||
total_signals = 0
|
||||
total_stocks = len(all_signals)
|
||||
total_patterns = 0
|
||||
total_stocks = len(all_results)
|
||||
|
||||
# 计算一周前的日期
|
||||
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:
|
||||
for stock_code, stock_results in all_results.items():
|
||||
for timeframe, result in stock_results.items():
|
||||
# 调试:检查result类型
|
||||
if not hasattr(result, 'signals'):
|
||||
logger.error(f"错误:result不是StrategyResult对象。stock_code={stock_code}, timeframe={timeframe}, result类型={type(result)}, result值={result}")
|
||||
continue
|
||||
|
||||
# 处理已确认信号
|
||||
for signal in result.signals:
|
||||
# 添加一周内信号过滤
|
||||
signal_date = signal.get('details', {}).get('confirmation_date') or signal['date']
|
||||
|
||||
@ -395,11 +358,12 @@ class NotificationManager:
|
||||
|
||||
total_signals += 1
|
||||
|
||||
# 根据新的信号格式提取信息
|
||||
# 根据信号格式提取信息
|
||||
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)
|
||||
breakout_position = details.get('breakout_position', 4)
|
||||
|
||||
all_signal_details.append({
|
||||
'stock_code': stock_code,
|
||||
@ -412,116 +376,139 @@ class NotificationManager:
|
||||
'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 '❌下方',
|
||||
'breakout_position': breakout_position, # 第几根K线突破
|
||||
'ema20_status': '✅上方' if details.get('ema20_price', 0) > 0 else '❌下方',
|
||||
'confirmation_days': confirmation_days,
|
||||
'pullback_distance': details.get('pullback_distance', 0),
|
||||
'is_new_format': details.get('new_high_confirmed', False) # 是否为新格式信号
|
||||
'is_new_format': details.get('new_high_confirmed', False), # 是否为新格式信号
|
||||
'details': {
|
||||
'pattern_subtype': details.get('pattern_subtype', '') # 保留形态子类型
|
||||
}
|
||||
})
|
||||
|
||||
# 如果没有信号,直接返回
|
||||
# 处理形态形成
|
||||
for pattern in result.patterns:
|
||||
# 添加一周内形态过滤
|
||||
pattern_date = pattern['date']
|
||||
|
||||
# 处理不同的日期格式
|
||||
if isinstance(pattern_date, str):
|
||||
try:
|
||||
pattern_date = pd.to_datetime(pattern_date).date()
|
||||
except:
|
||||
continue
|
||||
elif hasattr(pattern_date, 'date'):
|
||||
pattern_date = pattern_date.date()
|
||||
elif not isinstance(pattern_date, date):
|
||||
continue
|
||||
|
||||
# 只处理一周内的形态
|
||||
if pattern_date < one_week_ago:
|
||||
logger.debug(f"🗓️ 通知过滤历史形态: {stock_code} {pattern_date} (距今{(current_date - pattern_date).days}天)")
|
||||
continue
|
||||
|
||||
total_patterns += 1
|
||||
|
||||
# 根据形态格式提取信息
|
||||
details = pattern.get('details', {})
|
||||
breakout_position = details.get('breakout_position', 4)
|
||||
|
||||
all_pattern_details.append({
|
||||
'stock_code': stock_code,
|
||||
'stock_name': pattern.get('stock_name', '未知'),
|
||||
'timeframe': timeframe,
|
||||
'pattern_date': pattern['date'], # 形态形成日期
|
||||
'price': pattern['price'], # 突破价
|
||||
'yin_high': details.get('yin_high', 0), # 阴线最高价
|
||||
'turnover': details.get('turnover_ratio', 0),
|
||||
'breakout_pct': details.get('breakout_pct', 0),
|
||||
'breakout_position': breakout_position, # 第几根K线突破
|
||||
'ema20_status': '✅上方' if details.get('ema20_price', 0) > 0 else '❌下方'
|
||||
})
|
||||
|
||||
# 如果没有已确认信号,直接返回
|
||||
if total_signals == 0:
|
||||
logger.info("📱 通知过滤: 没有一周内的新信号,不发送通知")
|
||||
logger.info(f"📱 通知过滤: 没有一周内的已确认信号,不发送通知 (待确认形态={total_patterns}个)")
|
||||
logger.info(f"💡 提示: 信号可能被时间或价格过滤器过滤掉了")
|
||||
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)
|
||||
logger.info(f"📊 通知统计: 已确认信号={total_signals}个, 待确认形态={total_patterns}个(不发送)")
|
||||
logger.info(f"✅ 所有确认信号均通过价格有效性检查")
|
||||
|
||||
# 只发送已确认信号通知(不发送待确认形态)
|
||||
success_count = 0
|
||||
|
||||
for group_idx in range(total_groups):
|
||||
start_idx = group_idx * signals_per_group
|
||||
end_idx = min(start_idx + signals_per_group, total_signals)
|
||||
group_signals = all_signal_details[start_idx:end_idx]
|
||||
# 发送已确认信号通知
|
||||
if total_signals > 0:
|
||||
signals_per_group = 10
|
||||
total_signal_groups = math.ceil(total_signals / signals_per_group)
|
||||
|
||||
# 构建当前组的消息
|
||||
if total_groups > 1:
|
||||
title = f"🎯 K线形态策略信号汇总 ({group_idx + 1}/{total_groups})"
|
||||
else:
|
||||
title = f"🎯 K线形态策略信号汇总"
|
||||
for group_idx in range(total_signal_groups):
|
||||
start_idx = group_idx * signals_per_group
|
||||
end_idx = min(start_idx + signals_per_group, total_signals)
|
||||
group_signals = all_signal_details[start_idx:end_idx]
|
||||
|
||||
markdown_text = f"""
|
||||
# 构建信号组消息
|
||||
if total_signal_groups > 1:
|
||||
title = f"🎯 已确认信号 ({group_idx + 1}/{total_signal_groups})"
|
||||
else:
|
||||
title = f"🎯 已确认信号"
|
||||
|
||||
markdown_text = f"""
|
||||
# {title}
|
||||
|
||||
**扫描统计:**
|
||||
- 扫描时间: `{current_time}`
|
||||
- 总信号数: `{total_signals}` 个
|
||||
- 本组信号: `{len(group_signals)}` 个 ({start_idx + 1}-{end_idx})
|
||||
- 涉及股票: `{total_stocks}` 只
|
||||
**📊 本次推送:** {len(group_signals)} 个确认信号
|
||||
|
||||
**🎯 选中股票详情:**
|
||||
"""
|
||||
|
||||
# 添加扫描范围信息
|
||||
if scan_stats:
|
||||
markdown_text += f"""
|
||||
**扫描范围:**
|
||||
- 扫描股票总数: `{scan_stats.get('total_scanned', 'N/A')}`
|
||||
- 数据源: `{scan_stats.get('data_source', '热门股票')}`
|
||||
"""
|
||||
|
||||
markdown_text += "\n**✅ 确认信号详情:**\n"
|
||||
|
||||
# 添加当前组的信号详情
|
||||
for i, signal in enumerate(group_signals, start_idx + 1):
|
||||
if signal['is_new_format']:
|
||||
# 新格式:创新高回踩确认
|
||||
for i, signal in enumerate(group_signals, start_idx + 1):
|
||||
# 提取关键时间信息
|
||||
pattern_date = signal['pattern_date']
|
||||
confirmation_date = signal['confirmation_date']
|
||||
|
||||
# 获取形态类型信息
|
||||
pattern_subtype = signal.get('details', {}).get('pattern_subtype', '')
|
||||
|
||||
# 检查新版本形态标识
|
||||
if pattern_subtype:
|
||||
if '强势形态A' in pattern_subtype or '双阳+博弈' in pattern_subtype:
|
||||
pattern_type = '形态A'
|
||||
elif '强势形态B' in pattern_subtype or '高实体+博弈' in pattern_subtype:
|
||||
pattern_type = '形态B'
|
||||
else:
|
||||
pattern_type = '新形态'
|
||||
else:
|
||||
# 新策略必须有pattern_subtype,如果没有说明是数据问题
|
||||
pattern_type = '数据异常'
|
||||
|
||||
markdown_text += f"""
|
||||
{i}. **{signal['stock_code']} - {signal['stock_name']}** 🎯
|
||||
- 📅 模式日期: `{signal['pattern_date']}`
|
||||
- ✅ 确认日期: `{signal['confirmation_date']}`
|
||||
- 💰 原突破价: `{signal['original_breakout_price']:.2f}元`
|
||||
- 🌟 创新高价: `{signal['price']:.2f}元`
|
||||
- 🎯 阴线高点: `{signal['yin_high']:.2f}元`
|
||||
- ⏰ 确认用时: `{signal['confirmation_days']}天`
|
||||
- 📏 回踩距离: `{signal['pullback_distance']:.2f}%`
|
||||
- 📊 周期: `{signal['timeframe']}` | 换手: `{signal['turnover']:.2f}%`
|
||||
- 📈 EMA20: `{signal['ema20_status']}`
|
||||
"""
|
||||
else:
|
||||
# 旧格式:兼容显示
|
||||
markdown_text += f"""
|
||||
{i}. **{signal['stock_code']} - {signal['stock_name']}**
|
||||
- K线时间: `{signal['pattern_date']}`
|
||||
- 时间周期: `{signal['timeframe']}`
|
||||
- 当前价格: `{signal['price']:.2f}元`
|
||||
- 突破幅度: `{signal['breakout_pct']:.2f}%`
|
||||
- 换手率: `{signal['turnover']:.2f}%`
|
||||
- EMA20: `{signal['ema20_status']}`
|
||||
{i}. **{signal['stock_code']} {signal['stock_name']}** | `{signal['timeframe']}` | {pattern_type}
|
||||
📅 形态: `{pattern_date}` → ✅ 确认: `{confirmation_date}` ({signal['confirmation_days']}天)
|
||||
💰 创新高: `{signal['price']:.2f}元` | 🎯 入场: `{signal['yin_high']:.2f}元`
|
||||
"""
|
||||
|
||||
markdown_text += """
|
||||
---
|
||||
**🔍 策略说明:** 两阳+阴+阳突破(创新高回踩确认版)
|
||||
|
||||
**💡 信号特点:**
|
||||
- ✅ 所有信号已通过双重确认
|
||||
- 🎯 模式出现后等待创新高验证
|
||||
- 📉 创新高后回踩阴线最高价入场
|
||||
- ⏰ 7天内完成完整确认流程
|
||||
|
||||
**⚠️ 风险提示:** 投资有风险,入市需谨慎!
|
||||
markdown_text += """
|
||||
|
||||
---
|
||||
*K线形态策略系统自动发送*
|
||||
**💡 策略说明:** K线形态突破已完成创新高回踩确认
|
||||
|
||||
*多头排列+双形态识别策略*
|
||||
"""
|
||||
|
||||
# 发送当前组的通知
|
||||
if self.dingtalk_notifier:
|
||||
if self.dingtalk_notifier.send_markdown_message(title, markdown_text):
|
||||
success_count += 1
|
||||
logger.info(f"📱 发送信号汇总第{group_idx + 1}组成功 ({len(group_signals)}个信号)")
|
||||
else:
|
||||
logger.error(f"📱 发送信号汇总第{group_idx + 1}组失败")
|
||||
if self.dingtalk_notifier:
|
||||
if self.dingtalk_notifier.send_markdown_message(title, markdown_text):
|
||||
success_count += 1
|
||||
logger.info(f"📱 发送已确认信号第{group_idx + 1}组成功 ({len(group_signals)}个)")
|
||||
else:
|
||||
logger.error(f"📱 发送已确认信号第{group_idx + 1}组失败")
|
||||
|
||||
# 避免发送过快,添加短暂延迟
|
||||
if group_idx < total_groups - 1: # 不是最后一组
|
||||
import time
|
||||
time.sleep(1) # 1秒延迟
|
||||
if group_idx < total_signal_groups - 1:
|
||||
time.sleep(1)
|
||||
|
||||
# 不发送形态形成通知(只发送已确认信号)
|
||||
logger.info(f"📱 通知发送完成: 成功{success_count}组 (已过滤{total_patterns}个待确认形态)")
|
||||
return success_count > 0
|
||||
|
||||
except Exception as e:
|
||||
@ -562,42 +549,23 @@ class NotificationManager:
|
||||
alert_details = []
|
||||
for i, alert in enumerate(group_alerts, 1):
|
||||
alert_detail = f"""
|
||||
**{start_idx + i}. {alert['stock_code']}({alert['stock_name']})**
|
||||
- 📅 原信号: {alert['signal_date']} | 当前: {alert['current_date']}
|
||||
- ⏰ 间隔: {alert['days_since_signal']}天 | 周期: {alert['timeframe']}
|
||||
- 💰 阴线高点: {alert['yin_high']:.2f}元 | 当时突破价: {alert['breakout_price']:.2f}元
|
||||
- 📉 当前价格: {alert['current_price']:.2f}元 | 今日最低: {alert['current_low']:.2f}元
|
||||
- 📊 回调幅度: {alert['pullback_pct']:.2f}% | 距阴线高点: {alert['distance_to_yin_high']:.2f}%
|
||||
{start_idx + i}. **{alert['stock_code']} {alert['stock_name']}**
|
||||
原信号: `{alert['signal_date']}` | 周期: `{alert['timeframe']}` | 间隔: `{alert['days_since_signal']}天`
|
||||
当前价: `{alert['current_price']:.2f}` | 入场价: `{alert['yin_high']:.2f}` | 回调: `{alert['pullback_pct']:.1f}%`
|
||||
"""
|
||||
alert_details.append(alert_detail)
|
||||
|
||||
# 构建完整的Markdown消息
|
||||
markdown_text = f"""
|
||||
# ⚠️ 已确认信号二次回踩提醒
|
||||
# ⚠️ 二次回踩提醒
|
||||
|
||||
**🚨 重要提醒:** 以下股票已通过"创新高回踩确认"产生信号,现价格再次回踩至阴线最高点附近,请关注支撑情况!
|
||||
|
||||
**📊 本批提醒数量:** {len(group_alerts)}个
|
||||
**🕐 检查时间:** {current_time}
|
||||
**提醒:** {len(group_alerts)}个已确认信号二次回踩至入场价附近
|
||||
|
||||
---
|
||||
|
||||
{''.join(alert_details)}
|
||||
|
||||
---
|
||||
**💡 操作建议:**
|
||||
- ✅ 这些股票已通过双重确认,信号有效性较高
|
||||
- 🎯 当前为二次回踩阴线最高点,关注支撑强度
|
||||
- 📈 如获得有效支撑,可能形成新的上涨起点
|
||||
- 📉 如跌破阴线最高点,需要重新评估信号有效性
|
||||
- 💰 建议结合成交量和其他技术指标综合判断
|
||||
|
||||
**🔍 提醒说明:**
|
||||
- 此类股票已完成创新高+回踩确认流程
|
||||
- 当前价格位置具有重要技术意义
|
||||
- 阴线最高点是关键支撑/阻力位
|
||||
|
||||
**⚠️ 风险提示:** 本提醒仅供参考,投资有风险,入市需谨慎!
|
||||
"""
|
||||
|
||||
# 发送消息
|
||||
@ -618,6 +586,125 @@ class NotificationManager:
|
||||
logger.error(f"发送回踩提醒通知异常: {e}")
|
||||
return False
|
||||
|
||||
def send_scan_report(self, scan_result: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
发送市场扫描报告(无论是否有信号都发送)
|
||||
|
||||
Args:
|
||||
scan_result: 扫描结果数据,包含:
|
||||
- task_id: 任务ID
|
||||
- strategy_name: 策略名称
|
||||
- stock_pool_name: 股票池名称
|
||||
- total_stocks_analyzed: 扫描股票总数
|
||||
- stocks_with_signals: 有信号的股票数
|
||||
- total_signals_found: 发现的信号总数
|
||||
- execution_time: 执行耗时(秒)
|
||||
- scan_time: 扫描时间
|
||||
- success: 是否成功
|
||||
- error: 错误信息(如果有)
|
||||
|
||||
Returns:
|
||||
发送是否成功
|
||||
"""
|
||||
if not self.dingtalk_notifier:
|
||||
return False
|
||||
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
# 提取数据
|
||||
task_id = scan_result.get('task_id', 'unknown')
|
||||
strategy_name = scan_result.get('strategy_name', '未知策略')
|
||||
stock_pool_name = scan_result.get('stock_pool_name', '未知股票池')
|
||||
total_analyzed = scan_result.get('total_stocks_analyzed', 0)
|
||||
stocks_with_signals = scan_result.get('stocks_with_signals', 0)
|
||||
total_signals = scan_result.get('total_signals_found', 0)
|
||||
execution_time = scan_result.get('execution_time', 0)
|
||||
scan_time = scan_result.get('scan_time', datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
success = scan_result.get('success', True)
|
||||
error = scan_result.get('error')
|
||||
|
||||
# 根据是否有信号选择标题和图标
|
||||
if total_signals > 0:
|
||||
title = "📊 市场扫描完成 - 发现信号"
|
||||
status_icon = "✅"
|
||||
result_desc = f"发现 **{total_signals}** 个信号,涉及 **{stocks_with_signals}** 只股票"
|
||||
else:
|
||||
title = "📊 市场扫描完成 - 暂无信号"
|
||||
status_icon = "📭"
|
||||
result_desc = "本次扫描**暂无符合条件的信号**"
|
||||
|
||||
# 构建Markdown消息
|
||||
markdown_text = f"""
|
||||
# {status_icon} {title}
|
||||
|
||||
**扫描概要:**
|
||||
- 扫描时间: `{scan_time}`
|
||||
- 任务ID: `{task_id}`
|
||||
- 策略名称: `{strategy_name}`
|
||||
- 数据来源: `{stock_pool_name}`
|
||||
|
||||
**扫描结果:**
|
||||
- 扫描股票总数: `{total_analyzed}` 只
|
||||
- 有信号股票数: `{stocks_with_signals}` 只
|
||||
- 发现信号总数: `{total_signals}` 个
|
||||
- 执行耗时: `{execution_time:.2f}` 秒
|
||||
"""
|
||||
|
||||
# 如果有错误,添加错误信息
|
||||
if not success and error:
|
||||
markdown_text += f"""
|
||||
**⚠️ 执行状态:**
|
||||
- 状态: `失败`
|
||||
- 错误信息: `{error}`
|
||||
"""
|
||||
else:
|
||||
markdown_text += f"""
|
||||
**✅ 执行状态:** 成功完成
|
||||
"""
|
||||
|
||||
# 添加结果说明
|
||||
markdown_text += f"""
|
||||
---
|
||||
**📈 扫描结果:**
|
||||
{result_desc}
|
||||
"""
|
||||
|
||||
# 根据是否有信号添加不同的提示
|
||||
if total_signals > 0:
|
||||
markdown_text += """
|
||||
**💡 温馨提示:**
|
||||
- 详细信号信息请查看后续信号汇总消息
|
||||
- 建议结合市场环境综合判断
|
||||
- 注意风险控制,谨慎操作
|
||||
"""
|
||||
else:
|
||||
markdown_text += """
|
||||
**💡 温馨提示:**
|
||||
- 当前市场暂无符合策略条件的股票
|
||||
- 请继续关注后续扫描结果
|
||||
- 策略会持续监控市场机会
|
||||
"""
|
||||
|
||||
markdown_text += """
|
||||
---
|
||||
*市场扫描系统自动推送*
|
||||
"""
|
||||
|
||||
# 发送消息
|
||||
success = self.dingtalk_notifier.send_markdown_message(title, markdown_text)
|
||||
|
||||
if success:
|
||||
logger.info(f"📱 市场扫描报告发送成功: {total_analyzed}只股票, {total_signals}个信号")
|
||||
else:
|
||||
logger.error(f"📱 市场扫描报告发送失败")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送市场扫描报告异常: {e}")
|
||||
return False
|
||||
|
||||
def send_test_message(self) -> bool:
|
||||
"""发送测试消息"""
|
||||
if self.dingtalk_notifier:
|
||||
|
||||
53
start_crypto_scanner.sh
Executable file
53
start_crypto_scanner.sh
Executable file
@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 加密货币市场扫描启动脚本
|
||||
# 用于手动运行或在Docker容器中启动
|
||||
|
||||
# 设置错误时退出
|
||||
set -e
|
||||
|
||||
# 脚本所在目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# 打印启动信息
|
||||
echo "=========================================="
|
||||
echo "🚀 加密货币市场扫描程序启动"
|
||||
echo "=========================================="
|
||||
echo "工作目录: $(pwd)"
|
||||
echo "Python版本: $(python --version 2>&1)"
|
||||
echo "时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "=========================================="
|
||||
|
||||
# 检查Python是否安装
|
||||
if ! command -v python &> /dev/null; then
|
||||
echo "❌ 错误: 未找到Python"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查依赖包是否安装
|
||||
echo "📦 检查依赖包..."
|
||||
if ! python -c "import binance" 2>/dev/null; then
|
||||
echo "⚠️ 警告: python-binance未安装,正在安装..."
|
||||
pip install -r requirements.txt
|
||||
fi
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p logs
|
||||
|
||||
# 获取命令行参数(扫描交易对数量)
|
||||
MAX_SYMBOLS=${1:-100}
|
||||
|
||||
echo "📊 扫描参数: 最大交易对数量 = $MAX_SYMBOLS"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# 运行扫描程序
|
||||
python crypto_scanner.py "$MAX_SYMBOLS"
|
||||
|
||||
# 打印完成信息
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "✅ 加密货币市场扫描完成"
|
||||
echo "时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "=========================================="
|
||||
@ -14,29 +14,38 @@ apt-get update && apt-get install -y cron
|
||||
echo "⏰ 配置定时任务..."
|
||||
cp /app/crontab/market-scanner /etc/cron.d/market-scanner
|
||||
|
||||
# 设置权限
|
||||
# 设置权限(必须是0644且属于root)
|
||||
chmod 0644 /etc/cron.d/market-scanner
|
||||
chown root:root /etc/cron.d/market-scanner
|
||||
|
||||
# 确保文件以换行符结尾
|
||||
echo "" >> /etc/cron.d/market-scanner
|
||||
|
||||
# 启动cron服务
|
||||
echo "🔄 启动cron守护进程..."
|
||||
service cron start
|
||||
|
||||
# 检查cron服务状态
|
||||
service cron status
|
||||
|
||||
# 显示已配置的任务
|
||||
echo "📋 已配置的定时任务:"
|
||||
crontab -l 2>/dev/null || echo "使用系统cron配置: /etc/cron.d/market-scanner"
|
||||
cat /etc/cron.d/market-scanner
|
||||
|
||||
# 测试cron日志权限
|
||||
touch /app/logs/cron.log
|
||||
chmod 666 /app/logs/cron.log
|
||||
|
||||
# 记录启动信息
|
||||
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}
|
||||
python /app/market_scanner.py
|
||||
|
||||
# 保持容器运行并显示日志
|
||||
echo "👁️ 监控日志输出..."
|
||||
|
||||
@ -55,6 +55,7 @@ def signals():
|
||||
# 获取查询参数
|
||||
strategy_name = request.args.get('strategy', '')
|
||||
timeframe = request.args.get('timeframe', '')
|
||||
asset_type = request.args.get('asset_type', '') # 新增资产类型筛选
|
||||
days = int(request.args.get('days', 7)) # 默认显示7天内的信号
|
||||
page = int(request.args.get('page', 1))
|
||||
per_page = int(request.args.get('per_page', 20))
|
||||
@ -68,6 +69,7 @@ def signals():
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
strategy_name=strategy_name if strategy_name else None,
|
||||
asset_type=asset_type if asset_type else None, # 传递资产类型筛选
|
||||
timeframe=timeframe if timeframe else None
|
||||
)
|
||||
|
||||
@ -126,6 +128,7 @@ def signals():
|
||||
has_next=has_next,
|
||||
total_records=total_records,
|
||||
strategy_name=strategy_name,
|
||||
asset_type=asset_type, # 传递资产类型到模板
|
||||
timeframe=timeframe,
|
||||
days=days,
|
||||
per_page=per_page)
|
||||
|
||||
@ -37,6 +37,12 @@
|
||||
|
||||
<!-- 筛选表单 -->
|
||||
<form method="GET" class="d-flex gap-2 align-items-center">
|
||||
<select name="asset_type" class="form-select form-select-sm">
|
||||
<option value="">所有资产</option>
|
||||
<option value="stock" {% if asset_type == 'stock' %}selected{% endif %}>股票</option>
|
||||
<option value="crypto" {% if asset_type == 'crypto' %}selected{% endif %}>加密货币</option>
|
||||
</select>
|
||||
|
||||
<select name="strategy" class="form-select form-select-sm">
|
||||
<option value="">所有策略</option>
|
||||
<option value="K线形态策略" {% if strategy_name == 'K线形态策略' %}selected{% endif %}>K线形态策略</option>
|
||||
@ -107,9 +113,17 @@
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
{% if signal.asset_type == 'crypto' %}
|
||||
{# 加密货币链接到 Binance #}
|
||||
<a href="https://www.binance.com/zh-CN/trade/{{ signal.stock_code }}" target="_blank" class="stock-code-link me-2">
|
||||
<div class="stock-code-badge bg-warning text-dark">{{ signal.stock_code }}</div>
|
||||
</a>
|
||||
{% else %}
|
||||
{# 股票链接到雪球 #}
|
||||
<a href="https://xueqiu.com/S/{% if signal.stock_code.endswith('.SZ') %}SZ{% elif signal.stock_code.endswith('.SH') %}SH{% elif signal.stock_code.startswith('0') or signal.stock_code.startswith('3') %}SZ{% else %}SH{% endif %}{{ signal.stock_code.split('.')[0] }}" target="_blank" class="stock-code-link me-2">
|
||||
<div class="stock-code-badge">{{ signal.stock_code }}</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="stock-name text-truncate">{{ signal.stock_name or '未知' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -224,7 +238,7 @@
|
||||
<ul class="pagination justify-content-center">
|
||||
<!-- 上一页 -->
|
||||
<li class="page-item {% if not has_prev %}disabled{% endif %}">
|
||||
<a class="page-link" href="{% if has_prev %}{{ url_for('signals', page=current_page-1, strategy=strategy_name, timeframe=timeframe, days=days, per_page=per_page) }}{% else %}#{% endif %}">
|
||||
<a class="page-link" href="{% if has_prev %}{{ url_for('signals', page=current_page-1, strategy=strategy_name, asset_type=asset_type, timeframe=timeframe, days=days, per_page=per_page) }}{% else %}#{% endif %}">
|
||||
<i class="fas fa-chevron-left"></i> 上一页
|
||||
</a>
|
||||
</li>
|
||||
@ -235,7 +249,7 @@
|
||||
|
||||
{% if start_page > 1 %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('signals', page=1, strategy=strategy_name, timeframe=timeframe, days=days, per_page=per_page) }}">1</a>
|
||||
<a class="page-link" href="{{ url_for('signals', page=1, strategy=strategy_name, asset_type=asset_type, timeframe=timeframe, days=days, per_page=per_page) }}">1</a>
|
||||
</li>
|
||||
{% if start_page > 2 %}
|
||||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||||
@ -244,7 +258,7 @@
|
||||
|
||||
{% for page_num in range(start_page, end_page + 1) %}
|
||||
<li class="page-item {% if page_num == current_page %}active{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('signals', page=page_num, strategy=strategy_name, timeframe=timeframe, days=days, per_page=per_page) }}">{{ page_num }}</a>
|
||||
<a class="page-link" href="{{ url_for('signals', page=page_num, strategy=strategy_name, asset_type=asset_type, timeframe=timeframe, days=days, per_page=per_page) }}">{{ page_num }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@ -253,13 +267,13 @@
|
||||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||||
{% endif %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('signals', page=total_pages, strategy=strategy_name, timeframe=timeframe, days=days, per_page=per_page) }}">{{ total_pages }}</a>
|
||||
<a class="page-link" href="{{ url_for('signals', page=total_pages, strategy=strategy_name, asset_type=asset_type, timeframe=timeframe, days=days, per_page=per_page) }}">{{ total_pages }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<!-- 下一页 -->
|
||||
<li class="page-item {% if not has_next %}disabled{% endif %}">
|
||||
<a class="page-link" href="{% if has_next %}{{ url_for('signals', page=current_page+1, strategy=strategy_name, timeframe=timeframe, days=days, per_page=per_page) }}{% else %}#{% endif %}">
|
||||
<a class="page-link" href="{% if has_next %}{{ url_for('signals', page=current_page+1, strategy=strategy_name, asset_type=asset_type, timeframe=timeframe, days=days, per_page=per_page) }}{% else %}#{% endif %}">
|
||||
下一页 <i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user