This commit is contained in:
aaron 2025-11-02 10:41:17 +08:00
parent 6ec1feb2a7
commit ae928c198b
28 changed files with 3064 additions and 1287 deletions

267
CRYPTO_README.md Normal file
View 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来改进此功能!
## 许可
与主项目保持一致

View File

@ -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

View File

@ -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. 实盘交易请谨慎,注意风险控制
3. 本系统仅供学习和研究使用,不构成投资建议
4. 实盘交易请谨慎,注意风险控制
5. 确保配置文件中的TuShare Token和数据库连接信息正确
## 许可证

View File

@ -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()

View File

@ -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
View 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
# 必须以空行结尾

View File

@ -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
View 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)

View File

@ -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}

View File

@ -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()

View File

@ -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__":

View File

@ -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()

View File

@ -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
View 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)

View File

@ -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("市场舆情概览获取完成")

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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]:
"""

View File

@ -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

View 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

View File

@ -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', '热门股票')}`
"""
for i, signal in enumerate(group_signals, start_idx + 1):
# 提取关键时间信息
pattern_date = signal['pattern_date']
confirmation_date = signal['confirmation_date']
markdown_text += "\n**✅ 确认信号详情:**\n"
# 获取形态类型信息
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 = '数据异常'
# 添加当前组的信号详情
for i, signal in enumerate(group_signals, start_idx + 1):
if signal['is_new_format']:
# 新格式:创新高回踩确认
markdown_text += f"""
{i}. **{signal['stock_code']} - {signal['stock_name']}** 🎯
- 📅 模式日期: `{signal['pattern_date']}`
- 确认日期: `{signal['confirmation_date']}`
- 💰 原突破价: `{signal['original_breakout_price']:.2f}`
- 🌟 创新高价: `{signal['price']:.2f}`
- 🎯 阴线高点: `{signal['yin_high']:.2f}`
- 确认用时: `{signal['confirmation_days']}`
- 📏 回踩距离: `{signal['pullback_distance']:.2f}%`
- 📊 周期: `{signal['timeframe']}` | 换手: `{signal['turnover']:.2f}%`
- 📈 EMA20: `{signal['ema20_status']}`
"""
else:
# 旧格式:兼容显示
markdown_text += f"""
{i}. **{signal['stock_code']} - {signal['stock_name']}**
- K线时间: `{signal['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
View 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 "=========================================="

View File

@ -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 "👁️ 监控日志输出..."

View File

@ -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)

View File

@ -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>