trading.ai/src/strategy/kline_pattern_strategy.py
2025-09-18 20:45:01 +08:00

828 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
K线形态策略模块
实现"两阳线+阴线+阳线"形态识别策略
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Tuple, Optional, Any
from datetime import datetime, timedelta
from loguru import logger
from ..data.data_fetcher import ADataFetcher
from ..utils.notification import NotificationManager
from ..database.database_manager import DatabaseManager
class KLinePatternStrategy:
"""K线形态策略类"""
def __init__(self, data_fetcher: ADataFetcher, notification_manager: NotificationManager,
config: Dict[str, Any], db_manager: DatabaseManager = None):
"""
初始化K线形态策略
Args:
data_fetcher: 数据获取器
notification_manager: 通知管理器
config: 策略配置
db_manager: 数据库管理器
"""
self.data_fetcher = data_fetcher
self.notification_manager = notification_manager
self.config = config
self.db_manager = db_manager or DatabaseManager()
# 策略参数
self.strategy_name = "K线形态策略"
self.min_entity_ratio = config.get('min_entity_ratio', 0.55) # 前两根阳线实体部分最小比例
self.final_yang_min_ratio = config.get('final_yang_min_ratio', 0.40) # 最后阳线实体部分最小比例
self.max_turnover_ratio = config.get('max_turnover_ratio', 40.0) # 最后阳线最大换手率(%
self.timeframes = config.get('timeframes', ['daily', 'weekly']) # 支持的时间周期
# 回踩监控参数
self.pullback_tolerance = config.get('pullback_tolerance', 0.02) # 回踩容忍度2%
self.monitor_days = config.get('monitor_days', 30) # 监控回踩的天数
# 存储已触发的信号,用于监控回踩
# 格式: {stock_code: {'signals': [signal_dict], 'last_check_date': date}}
self.triggered_signals = {}
# 确保策略在数据库中存在
self.strategy_id = self.db_manager.create_or_update_strategy(
strategy_name=self.strategy_name,
strategy_type="kline_pattern",
description="两阳线+阴线+阳线突破形态识别策略",
config=config
)
logger.info(f"K线形态策略初始化完成 (策略ID: {self.strategy_id})")
def calculate_kline_features(self, df: pd.DataFrame) -> pd.DataFrame:
"""
计算K线特征指标
Args:
df: K线数据DataFrame包含 open, high, low, close 列
Returns:
添加了特征指标的DataFrame
"""
if df.empty or len(df) < 4:
return df
# 确保列名正确
required_cols = ['open', 'high', 'low', 'close']
if not all(col in df.columns for col in required_cols):
logger.warning(f"K线数据缺少必要字段: {required_cols}")
return df
df = df.copy()
# 计算涨跌情况
df['is_yang'] = df['close'] > df['open'] # 阳线
df['is_yin'] = df['close'] < df['open'] # 阴线
# 计算实体部分和振幅
df['entity'] = abs(df['close'] - df['open']) # 实体长度
df['amplitude'] = df['high'] - df['low'] # 振幅
# 计算实体占振幅的比例
df['entity_ratio'] = np.where(df['amplitude'] > 0, df['entity'] / df['amplitude'], 0)
# 计算涨跌幅
df['change_pct'] = (df['close'] - df['open']) / df['open'] * 100
# 计算EMA20指标
df['ema20'] = df['close'].ewm(span=20, adjust=False).mean()
# 判断是否在EMA20上方
df['above_ema20'] = df['close'] > df['ema20']
# 计算换手率如果存在volume和float_share列
if 'volume' in df.columns and 'float_share' in df.columns:
# 换手率 = 成交量 / 流通股本 * 100%
df['turnover_ratio'] = np.where(df['float_share'] > 0,
(df['volume'] / df['float_share']) * 100, 0)
elif 'turnover_ratio' not in df.columns:
# 如果数据中没有换手率设为0不进行此项约束
df['turnover_ratio'] = 0
return df
def detect_pattern(self, df: pd.DataFrame) -> List[Dict[str, Any]]:
"""
检测"两阳线+阴线+阳线"形态
Args:
df: 包含特征指标的K线数据
Returns:
检测到的形态信号列表
"""
signals = []
if df.empty or len(df) < 4:
return signals
# 从第4个数据点开始检测需要4根K线
for i in range(3, len(df)):
# 获取连续4根K线
k1, k2, k3, k4 = df.iloc[i-3:i+1].to_dict('records')
# 检查形态:两阳线 + 阴线 + 阳线
pattern_match = (
k1['is_yang'] and k2['is_yang'] and # 前两根是阳线
k3['is_yin'] and # 第三根是阴线
k4['is_yang'] # 第四根是阳线
)
if not pattern_match:
continue
# 检查前两根阳线的实体比例
yang1_valid = k1['entity_ratio'] >= self.min_entity_ratio
yang2_valid = k2['entity_ratio'] >= self.min_entity_ratio
if not (yang1_valid and yang2_valid):
continue
# 检查最后一根阳线的收盘价是否高于阴线的最高价
breakout_valid = k4['close'] > k3['high']
if not breakout_valid:
continue
# 检查最后一根阳线的实体比例
final_yang_valid = k4['entity_ratio'] >= self.final_yang_min_ratio
if not final_yang_valid:
continue
# 检查最后一根阳线是否在EMA20上方
ema20_valid = k4.get('above_ema20', False)
if not ema20_valid:
continue
# 检查最后一根阳线的换手率
turnover_ratio = k4.get('turnover_ratio', 0)
turnover_valid = turnover_ratio <= self.max_turnover_ratio
if not turnover_valid:
continue
# 构建信号
signal = {
'index': i,
'date': df.iloc[i].get('trade_date', df.index[i]),
'pattern_type': '两阳+阴+阳突破',
'k1': k1, # 第一根阳线
'k2': k2, # 第二根阳线
'k3': k3, # 阴线
'k4': k4, # 突破阳线
'yang1_entity_ratio': k1['entity_ratio'],
'yang2_entity_ratio': k2['entity_ratio'],
'final_yang_entity_ratio': k4['entity_ratio'],
'breakout_price': k4['close'],
'yin_high': k3['high'],
'breakout_amount': k4['close'] - k3['high'],
'breakout_pct': (k4['close'] - k3['high']) / k3['high'] * 100 if k3['high'] > 0 else 0,
'ema20_price': k4.get('ema20', 0),
'above_ema20': k4.get('above_ema20', False),
'turnover_ratio': turnover_ratio
}
signals.append(signal)
# 美化信号发现日志
logger.info("🎯" + "="*60)
logger.info(f"📈 发现K线形态突破信号!")
logger.info(f"📅 信号时间: {signal['date']}")
logger.info(f"💰 突破价格: {signal['breakout_price']:.2f}")
logger.info(f"📊 实体比例: 阳线1({signal['yang1_entity_ratio']:.1%}) | 阳线2({signal['yang2_entity_ratio']:.1%}) | 最后阳线({signal['final_yang_entity_ratio']:.1%})")
logger.info(f"💥 突破幅度: {signal['breakout_pct']:.2f}% (突破阴线最高价{signal['yin_high']:.2f}元)")
logger.info(f"📈 EMA20: {signal['ema20_price']:.2f}元 ({'✅上方' if signal['above_ema20'] else '❌下方'})")
logger.info(f"🔄 换手率: {signal['turnover_ratio']:.2f}% ({'✅合规' if signal['turnover_ratio'] <= self.max_turnover_ratio else '❌过高'})")
logger.info("🎯" + "="*60)
return signals
def analyze_stock(self, stock_code: str, stock_name: str = None, days: int = 60,
session_id: Optional[int] = None) -> Dict[str, List[Dict[str, Any]]]:
"""
分析单只股票的K线形态
Args:
stock_code: 股票代码
stock_name: 股票名称
days: 分析的天数
Returns:
各时间周期的信号字典
"""
results = {}
if stock_name is None:
# 尝试获取股票中文名称
stock_name = self.data_fetcher.get_stock_name(stock_code)
try:
# 计算开始日期,针对不同周期调整时间范围
end_date = datetime.now().strftime('%Y-%m-%d')
for timeframe in self.timeframes:
# 针对1小时周期调整分析天数避免数据量过大
if timeframe == '1h':
# 1小时数据只分析最近7天
analysis_days = min(days, 7)
else:
analysis_days = days
start_date = (datetime.now() - timedelta(days=analysis_days)).strftime('%Y-%m-%d')
logger.info(f"🔍 分析股票: {stock_code}({stock_name}) | 周期: {timeframe}")
# 获取历史数据 - 直接使用adata的原生周期支持
df = self.data_fetcher.get_historical_data(stock_code, start_date, end_date, timeframe)
if df.empty:
logger.warning(f"{stock_code} {timeframe} 数据为空")
results[timeframe] = []
continue
# 计算K线特征
df_with_features = self.calculate_kline_features(df)
# 检测形态
signals = self.detect_pattern(df_with_features)
# 处理信号
for signal in signals:
signal['stock_code'] = stock_code
signal['stock_name'] = stock_name
signal['timeframe'] = timeframe
# 将信号添加到监控列表
self.add_triggered_signal(signal)
# 保存信号到数据库如果提供了session_id
if session_id is not None:
try:
signal_id = self.db_manager.save_stock_signal(
session_id=session_id,
strategy_id=self.strategy_id,
signal=signal
)
signal['signal_id'] = signal_id
logger.debug(f"信号已保存到数据库: {stock_code} (ID: {signal_id})")
except Exception as e:
logger.error(f"保存信号到数据库失败: {e}")
results[timeframe] = signals
# 美化信号统计日志
if signals:
logger.info(f"{stock_code}({stock_name}) {timeframe}周期: 发现 {len(signals)} 个信号")
for i, signal in enumerate(signals, 1):
logger.info(f" 📊 信号{i}: {signal['date']} | 价格: {signal['breakout_price']:.2f}元 | 实体: {signal['final_yang_entity_ratio']:.1%}")
else:
logger.debug(f"📭 {stock_code}({stock_name}) {timeframe}周期: 无信号")
except Exception as e:
logger.error(f"分析股票 {stock_code} 失败: {e}")
for timeframe in self.timeframes:
results[timeframe] = []
return results
def check_pullback_signals(self, stock_code: str, current_data: pd.DataFrame) -> List[Dict[str, Any]]:
"""
检查已触发信号的价格回踩情况
Args:
stock_code: 股票代码
current_data: 当前K线数据
Returns:
回踩提醒信号列表
"""
pullback_alerts = []
if stock_code not in self.triggered_signals:
return pullback_alerts
signals = self.triggered_signals[stock_code]['signals']
current_date = datetime.now().date()
if current_data.empty:
return pullback_alerts
# 获取最新价格
latest_price = current_data.iloc[-1]['close']
latest_low = current_data.iloc[-1]['low']
latest_date = current_data.iloc[-1].get('trade_date', current_data.index[-1])
if isinstance(latest_date, str):
latest_date = pd.to_datetime(latest_date).date()
elif hasattr(latest_date, 'date'):
latest_date = latest_date.date()
for signal in signals:
# 检查信号是否在监控期内
signal_date = signal['date']
if isinstance(signal_date, str):
signal_date = pd.to_datetime(signal_date).date()
elif hasattr(signal_date, 'date'):
signal_date = signal_date.date()
days_since_signal = (current_date - signal_date).days
if days_since_signal > self.monitor_days:
continue
yin_high = signal['yin_high'] # 阴线最高点
breakout_price = signal['breakout_price'] # 突破时价格
# 检查是否发生回踩
# 条件1: 最低价接近或跌破阴线最高点
pullback_to_yin_high = latest_low <= (yin_high * (1 + self.pullback_tolerance))
# 条件2: 当前价格相比突破价格有明显回调
significant_pullback = latest_price < (breakout_price * 0.95) # 回调超过5%
if pullback_to_yin_high and significant_pullback:
# 检查是否已经发送过此类提醒(避免重复)
alert_key = f"{stock_code}_{signal_date}_{latest_date}"
if not hasattr(self, '_sent_pullback_alerts'):
self._sent_pullback_alerts = set()
if alert_key not in self._sent_pullback_alerts:
pullback_alert = {
'stock_code': stock_code,
'stock_name': signal.get('stock_name', ''),
'signal_date': signal_date,
'current_date': latest_date,
'timeframe': signal.get('timeframe', 'daily'),
'yin_high': yin_high,
'breakout_price': breakout_price,
'current_price': latest_price,
'current_low': latest_low,
'pullback_pct': ((latest_price - breakout_price) / breakout_price) * 100,
'distance_to_yin_high': ((latest_low - yin_high) / yin_high) * 100,
'days_since_signal': days_since_signal,
'alert_type': 'pullback_to_yin_high'
}
pullback_alerts.append(pullback_alert)
self._sent_pullback_alerts.add(alert_key)
# 记录回踩提醒日志
logger.warning("⚠️" + "="*60)
logger.warning(f"📉 价格回踩阴线最高点提醒!")
logger.warning(f"📅 原信号时间: {signal_date} | 当前时间: {latest_date}")
logger.warning(f"🏷️ 股票: {stock_code}({signal.get('stock_name', '')})")
logger.warning(f"📊 周期: {signal.get('timeframe', 'daily')}")
logger.warning(f"💰 阴线最高点: {yin_high:.2f}")
logger.warning(f"🚀 当时突破价: {breakout_price:.2f}")
logger.warning(f"💸 当前价格: {latest_price:.2f}元 | 最低: {latest_low:.2f}")
logger.warning(f"📉 回调幅度: {pullback_alert['pullback_pct']:.2f}%")
logger.warning(f"📏 距阴线高点: {pullback_alert['distance_to_yin_high']:.2f}%")
logger.warning(f"⏰ 信号后经过: {days_since_signal}")
logger.warning("⚠️" + "="*60)
return pullback_alerts
def add_triggered_signal(self, signal: Dict[str, Any]):
"""
添加已触发的信号到监控列表
Args:
signal: 信号字典
"""
stock_code = signal.get('stock_code')
if not stock_code:
return
if stock_code not in self.triggered_signals:
self.triggered_signals[stock_code] = {
'signals': [],
'last_check_date': datetime.now().date()
}
# 添加信号到监控列表
self.triggered_signals[stock_code]['signals'].append(signal)
# 只保留最近的信号(避免内存占用过多)
max_signals_per_stock = 10
if len(self.triggered_signals[stock_code]['signals']) > max_signals_per_stock:
# 按日期排序,保留最新的信号
self.triggered_signals[stock_code]['signals'].sort(
key=lambda x: pd.to_datetime(x['date']) if isinstance(x['date'], str) else x['date'],
reverse=True
)
self.triggered_signals[stock_code]['signals'] = \
self.triggered_signals[stock_code]['signals'][:max_signals_per_stock]
def monitor_pullback_for_triggered_signals(self) -> List[Dict[str, Any]]:
"""
监控所有已触发信号的回踩情况
Returns:
所有回踩提醒信号列表
"""
all_pullback_alerts = []
current_date = datetime.now().date()
# 清理过期的信号
stocks_to_remove = []
for stock_code, signal_info in self.triggered_signals.items():
# 过滤掉过期的信号
valid_signals = []
for signal in signal_info['signals']:
signal_date = signal['date']
if isinstance(signal_date, str):
signal_date = pd.to_datetime(signal_date).date()
elif hasattr(signal_date, 'date'):
signal_date = signal_date.date()
days_since_signal = (current_date - signal_date).days
if days_since_signal <= self.monitor_days:
valid_signals.append(signal)
if valid_signals:
self.triggered_signals[stock_code]['signals'] = valid_signals
else:
stocks_to_remove.append(stock_code)
# 移除没有有效信号的股票
for stock_code in stocks_to_remove:
del self.triggered_signals[stock_code]
logger.info(f"🔍 当前监控中的股票数量: {len(self.triggered_signals)}")
# 检查每只股票的回踩情况
for stock_code in self.triggered_signals.keys():
try:
# 获取最近几天的数据
end_date = current_date.strftime('%Y-%m-%d')
start_date = (current_date - timedelta(days=5)).strftime('%Y-%m-%d')
current_data = self.data_fetcher.get_historical_data(
stock_code, start_date, end_date, 'daily'
)
if not current_data.empty:
pullback_alerts = self.check_pullback_signals(stock_code, current_data)
all_pullback_alerts.extend(pullback_alerts)
except Exception as e:
logger.error(f"监控股票 {stock_code} 回踩情况失败: {e}")
# 发送回踩提醒通知
if all_pullback_alerts:
try:
success = self.notification_manager.send_pullback_alerts(all_pullback_alerts)
if success:
logger.info(f"📱 回踩提醒通知发送完成,共{len(all_pullback_alerts)}个提醒")
else:
logger.warning(f"📱 回踩提醒通知发送失败")
except Exception as e:
logger.error(f"发送回踩提醒通知失败: {e}")
return all_pullback_alerts
def _convert_to_weekly(self, daily_df: pd.DataFrame) -> pd.DataFrame:
"""
将日线数据转换为周线数据
Args:
daily_df: 日线数据
Returns:
周线数据
"""
if daily_df.empty:
return daily_df
try:
df = daily_df.copy()
# 确保有trade_date列并设置为索引
if 'trade_date' in df.columns:
df['trade_date'] = pd.to_datetime(df['trade_date'])
df.set_index('trade_date', inplace=True)
# 按周聚合
weekly_df = df.resample('W').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum' if 'volume' in df.columns else 'last'
}).dropna()
# 重置索引保持trade_date列
weekly_df.reset_index(inplace=True)
return weekly_df
except Exception as e:
logger.error(f"转换周线数据失败: {e}")
return pd.DataFrame()
def _convert_to_monthly(self, daily_df: pd.DataFrame) -> pd.DataFrame:
"""
将日线数据转换为月线数据
Args:
daily_df: 日线数据
Returns:
月线数据
"""
if daily_df.empty:
return daily_df
try:
df = daily_df.copy()
# 确保有trade_date列并设置为索引
if 'trade_date' in df.columns:
df['trade_date'] = pd.to_datetime(df['trade_date'])
df.set_index('trade_date', inplace=True)
# 按月聚合
monthly_df = df.resample('ME').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum' if 'volume' in df.columns else 'last'
}).dropna()
# 重置索引保持trade_date列
monthly_df.reset_index(inplace=True)
return monthly_df
except Exception as e:
logger.error(f"转换月线数据失败: {e}")
return pd.DataFrame()
def scan_market(self, stock_list: List[str] = None, max_stocks: int = 100, use_hot_stocks: bool = True, use_combined_sources: bool = True, use_all_a_shares: bool = False) -> Dict[str, Dict[str, List[Dict[str, Any]]]]:
"""
扫描市场中的股票形态
Args:
stock_list: 股票代码列表如果为None则自动选择股票池
max_stocks: 最大扫描股票数量
use_hot_stocks: 是否使用热门股票数据默认True
use_combined_sources: 是否使用合并的双数据源(同花顺+东财默认True
use_all_a_shares: 是否使用所有A股股票(排除北交所和ST),优先级最高
Returns:
所有股票的分析结果
"""
logger.info("🚀" + "="*70)
logger.info("🌍 开始市场K线形态扫描")
logger.info("🚀" + "="*70)
# 创建扫描会话
scan_config = {
'max_stocks': max_stocks,
'use_hot_stocks': use_hot_stocks,
'use_combined_sources': use_combined_sources,
'use_all_a_shares': use_all_a_shares,
'timeframes': self.timeframes
}
session_id = self.db_manager.create_scan_session(
strategy_id=self.strategy_id,
scan_config=scan_config
)
if stock_list is None:
# 优先级1: 使用所有A股股票
if use_all_a_shares:
try:
logger.info("📊 获取所有A股股票数据排除北交所和ST股票...")
filtered_stocks = self.data_fetcher.get_filtered_a_share_list()
if not filtered_stocks.empty:
# 如果max_stocks小于总股票数随机采样
if max_stocks > 0 and max_stocks < len(filtered_stocks):
# 按市值排序或随机选择,这里先随机选择
selected_stocks = filtered_stocks.sample(max_stocks)
stock_list = selected_stocks['full_stock_code'].tolist()
logger.info(f"📈 从{len(filtered_stocks)}只A股中随机选择{len(stock_list)}只进行分析")
else:
stock_list = filtered_stocks['full_stock_code'].tolist()
logger.info(f"📈 分析全部{len(stock_list)}只A股股票")
source_info = "全A股(排除北交所和ST)"
else:
logger.warning("获取A股列表失败回退到热门股票")
use_all_a_shares = False
except Exception as e:
logger.error(f"获取A股列表失败: {e},回退到热门股票")
use_all_a_shares = False
# 优先级2: 使用热门股票数据
if not use_all_a_shares and use_hot_stocks:
try:
if use_combined_sources:
# 使用合并的双数据源
logger.info("获取合并热门股票数据(同花顺+东财)...")
hot_stocks = self.data_fetcher.get_combined_hot_stocks(
limit_per_source=max_stocks,
final_limit=max_stocks
)
source_info = "双数据源合并"
else:
# 仅使用同花顺数据
logger.info("获取同花顺热股TOP100数据...")
hot_stocks = self.data_fetcher.get_hot_stocks_ths(limit=max_stocks)
source_info = "同花顺热股"
if not hot_stocks.empty and 'stock_code' in hot_stocks.columns:
stock_list = hot_stocks['stock_code'].tolist()
# 统计数据源分布
if 'source' in hot_stocks.columns:
source_counts = hot_stocks['source'].value_counts().to_dict()
source_detail = " | ".join([f"{k}: {v}" for k, v in source_counts.items()])
logger.info(f"📊 数据源: {source_info} | 总计: {len(stock_list)}只股票")
logger.info(f"📈 分布详情: {source_detail}")
else:
logger.info(f"📊 数据源: {source_info} | 总计: {len(stock_list)}只股票")
else:
logger.warning("热门股票数据为空,回退到全市场股票")
use_hot_stocks = False
except Exception as e:
logger.error(f"获取热门股票失败: {e},回退到全市场股票")
use_hot_stocks = False
# 优先级3: 如果热股获取失败,使用全市场股票列表
if not use_all_a_shares and not use_hot_stocks:
try:
all_stocks = self.data_fetcher.get_stock_list()
if not all_stocks.empty:
# 随机选择一些股票进行扫描
stock_list = all_stocks['stock_code'].head(max_stocks).tolist()
logger.info(f"使用全市场股票数据,共{len(stock_list)}只股票")
else:
logger.warning("未能获取股票列表")
return {}
except Exception as e:
logger.error(f"获取股票列表失败: {e}")
return {}
results = {}
total_signals = 0
for i, stock_code in enumerate(stock_list):
# 获取股票名称
stock_name = self.data_fetcher.get_stock_name(stock_code)
logger.info(f"⏳ 扫描进度: [{i+1:3d}/{len(stock_list):3d}] 🔍 {stock_code}({stock_name})")
try:
stock_results = self.analyze_stock(stock_code, session_id=session_id)
# 统计信号数量
stock_signal_count = sum(len(signals) for signals in stock_results.values())
if stock_signal_count > 0:
results[stock_code] = stock_results
total_signals += stock_signal_count
except Exception as e:
logger.error(f"扫描股票 {stock_code} 失败: {e}")
continue
# 更新扫描会话统计
try:
self.db_manager.update_scan_session_stats(
session_id=session_id,
total_scanned=len(stock_list),
total_signals=total_signals
)
logger.debug(f"扫描会话统计已更新: {session_id}")
except Exception as e:
logger.error(f"更新扫描会话统计失败: {e}")
# 美化最终扫描结果
logger.info("🎉" + "="*70)
logger.info(f"🌍 市场K线形态扫描完成!")
logger.info(f"📊 扫描统计:")
logger.info(f" 🔍 总扫描股票: {len(stock_list)}")
logger.info(f" 🎯 发现信号: {total_signals}")
logger.info(f" 📈 涉及股票: {len(results)}")
logger.info(f" 💾 扫描会话ID: {session_id}")
if results:
logger.info(f"📋 信号详情:")
signal_count = 0
for stock_code, stock_results in results.items():
stock_name = self.data_fetcher.get_stock_name(stock_code)
for timeframe, signals in stock_results.items():
if signals:
for signal in signals:
signal_count += 1
logger.info(f" 🎯 #{signal_count}: {stock_code}({stock_name}) | {timeframe} | {signal['date']} | {signal['breakout_price']:.2f}")
logger.info("🎉" + "="*70)
# 监控已触发信号的回踩情况
logger.info("🔍 开始监控已触发信号的回踩情况...")
pullback_alerts = self.monitor_pullback_for_triggered_signals()
if pullback_alerts:
logger.info(f"⚠️ 发现 {len(pullback_alerts)} 个回踩提醒")
# 发送汇总通知
if results:
# 判断数据源类型
data_source = '全市场股票'
if stock_list and len(stock_list) <= max_stocks:
if use_hot_stocks:
data_source = '合并热门股票' if use_combined_sources else '热门股票'
scan_stats = {
'total_scanned': len(stock_list),
'data_source': data_source
}
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_summary(self) -> str:
"""获取策略说明"""
return f"""
K线形态策略 - 两阳线+阴线+阳线突破
策略逻辑:
1. 识别连续4根K线阳线 + 阳线 + 阴线 + 阳线
2. 前两根阳线实体部分须占振幅的 {self.min_entity_ratio:.0%} 以上
3. 最后阳线实体部分须占振幅的 {self.final_yang_min_ratio:.0%} 以上
4. 最后阳线收盘价须高于阴线最高价(突破确认)
5. 最后阳线收盘价须在EMA20上方趋势确认
6. 最后阳线换手率不高于 {self.max_turnover_ratio:.1f}%(流动性约束)
7. 支持时间周期:{', '.join(self.timeframes)}
回踩监控功能:
- 自动监控已触发信号后的价格走势
- 当价格回踩到阴线最高点附近时发送特殊提醒
- 回踩容忍度:{self.pullback_tolerance:.0%}
- 监控期限:信号触发后 {self.monitor_days}
- 提醒条件:价格接近阴线最高点且相比突破价有明显回调
信号触发条件:
- 形态完整匹配
- 实体比例达标
- 价格突破确认
- EMA20趋势确认
- 换手率约束达标
扫描范围:
- 优先使用双数据源合并(同花顺热股+东财人气榜)
- 自动去重,保留最优质股票
- 回退到全市场股票列表
通知方式:
- 钉钉webhook汇总推送10个信号一组分批发送
- 价格回踩特殊提醒5个提醒一组分批发送
- 包含关键信息代码、股票名称、K线时间、价格、周期等
- 系统日志详细记录
"""
if __name__ == "__main__":
# 测试代码
from ..data.data_fetcher import ADataFetcher
from ..utils.notification import NotificationManager
# 模拟配置
strategy_config = {
'min_entity_ratio': 0.55,
'timeframes': ['daily']
}
notification_config = {
'dingtalk': {
'enabled': False,
'webhook_url': ''
}
}
# 初始化组件
data_fetcher = ADataFetcher()
notification_manager = NotificationManager(notification_config)
strategy = KLinePatternStrategy(data_fetcher, notification_manager, strategy_config)
print("K线形态策略初始化完成")
print(strategy.get_strategy_summary())