This commit is contained in:
aaron 2025-11-16 10:49:52 +08:00
parent 632a6c2049
commit a05fd69916
8 changed files with 413 additions and 135 deletions

View File

@ -55,6 +55,31 @@ services:
depends_on: depends_on:
- trading-web - trading-web
# 加密货币扫描定时任务服务
trading-crypto-scanner:
build: .
container_name: trading-ai-crypto-scanner
volumes:
- ./config:/app/config
- ./logs:/app/logs
- ./crontab:/app/crontab
environment:
- PYTHONPATH=/app
- TZ=Asia/Shanghai
- OPERATION_KEY=${OPERATION_KEY:-9257}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
# MySQL连接配置
- MYSQL_HOST=${MYSQL_HOST:-cd-cynosdbmysql-grp-7kdd8qe4.sql.tencentcdb.com}
- MYSQL_PORT=${MYSQL_PORT:-26558}
- MYSQL_USER=${MYSQL_USER:-root}
- MYSQL_PASSWORD=${MYSQL_PASSWORD:-gUjjmQpu6c7V0hMF}
- MYSQL_DATABASE=${MYSQL_DATABASE:-tradingai}
# 运行定时加密货币扫描
command: ["bash", "/app/start_crypto_scanner.sh"]
restart: unless-stopped
depends_on:
- trading-web
# # Nginx反向代理可选 # # Nginx反向代理可选
# nginx: # nginx:
# image: nginx:alpine # image: nginx:alpine

13
main.py
View File

@ -155,7 +155,7 @@ def main():
max_stocks = int(parts[2]) if len(parts) > 2 and parts[2].isdigit() else 10 max_stocks = int(parts[2]) if len(parts) > 2 and parts[2].isdigit() else 10
execute_task(system['executor'], rule, max_stocks) execute_task(system['executor'], rule, max_stocks)
else: else:
print("请提供股票池规则,如: task tushare_hot 15") print("请提供股票池规则,如: task index_components 100")
elif command.lower() == 'schedule': elif command.lower() == 'schedule':
show_schedule_examples(system['scheduler'], system['executor']) show_schedule_examples(system['scheduler'], system['executor'])
@ -206,16 +206,16 @@ def scan_single_stock(strategy, stock_code):
def scan_market(executor, max_stocks): def scan_market(executor, max_stocks):
"""扫描市场热门股票""" """扫描主力股票池"""
print(f"\n🌍 扫描市场热门股票 (前{max_stocks}只)") print(f"\n🎯 扫描主力股票池 (沪深300+中证500+创业板指,最多{max_stocks}只)")
print("-" * 50) print("-" * 50)
try: try:
result = executor.execute_task( result = executor.execute_task(
task_id=f"market_scan_{max_stocks}", task_id=f"market_scan_{max_stocks}",
strategy_id="kline_pattern", strategy_id="kline_pattern",
stock_pool_rule="tushare_hot", stock_pool_rule="index_components", # 使用主力股票池
stock_pool_params={"limit": max_stocks * 2}, stock_pool_params={},
max_stocks=max_stocks, max_stocks=max_stocks,
send_notification=False send_notification=False
) )
@ -251,7 +251,8 @@ def show_stock_pools(pool_manager):
print(f" 🎯 {rule_id}: {rule_name}") print(f" 🎯 {rule_id}: {rule_name}")
print(f"\n💡 使用方法: task <规则名> <股票数量>") print(f"\n💡 使用方法: task <规则名> <股票数量>")
print(f" 示例: task tushare_hot 20") print(f" 示例: task index_components 100 # 主力股票池")
print(f" 示例: task tushare_hot 50 # 同花顺热榜")
def execute_task(executor, rule, max_stocks): def execute_task(executor, rule, max_stocks):

View File

@ -95,21 +95,21 @@ def create_strategy_system():
def scan_market(): def scan_market():
"""执行市场扫描 - 分析所有同花顺热榜股票""" """执行市场扫描 - 分析主力股票池沪深300+中证500+创业板指)"""
logger.info(f"🚀 开始市场扫描任务 - 扫描全部同花顺热榜股票") logger.info(f"🚀 开始市场扫描任务 - 扫描主力股票池沪深300+中证500+创业板指)")
try: try:
# 初始化系统 # 初始化系统
executor = create_strategy_system() executor = create_strategy_system()
# 执行扫描任务 - 不限制数量,分析所有股票 # 执行扫描任务 - 使用主力股票池沪深300+中证500+创业板指)
task_id = f"market_scan_{datetime.now().strftime('%Y%m%d_%H%M%S')}" task_id = f"market_scan_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
result = executor.execute_task( result = executor.execute_task(
task_id=task_id, task_id=task_id,
strategy_id="kline_pattern", strategy_id="kline_pattern",
stock_pool_rule="tushare_hot", stock_pool_rule="index_components", # 使用主力股票池
stock_pool_params={}, # 不限制数量 stock_pool_params={}, # 使用默认指数组合
max_stocks=None, # 取消数量限制 max_stocks=None, # 分析所有成分股
send_notification=True # 启用通知 send_notification=True # 启用通知
) )

View File

@ -125,6 +125,61 @@ class CustomStockListRule(StockPoolRule):
return "自定义股票列表" return "自定义股票列表"
class IndexComponentsRule(StockPoolRule):
"""指数成分股规则(主力股票池)"""
def get_stocks(self, fetcher: TushareFetcher, index_codes: List[str] = None, **kwargs) -> List[str]:
"""
获取指数成分股
Args:
fetcher: 数据获取器
index_codes: 指数代码列表默认使用主力股票池
**kwargs: 其他参数
Returns:
股票代码列表
"""
try:
if index_codes is None:
# 默认使用主力股票池沪深300 + 中证500 + 创业板指
index_codes = ['000300.SH', '000905.SH', '399006.SZ']
logger.info(f"🎯 获取主力股票池: {index_codes}")
# 获取指数成分股数据
components_df = fetcher.get_index_components(index_codes=index_codes, merge=True)
if not components_df.empty and 'stock_code' in components_df.columns:
stocks = components_df['stock_code'].tolist()
# 统计每个指数的成分股数量
if 'index_name' in components_df.columns:
index_stats = components_df.groupby('index_name').size().to_dict()
stats_str = ", ".join([f"{name}:{count}" for name, count in index_stats.items()])
logger.info(f"✅ 主力股票池构建成功: 总计{len(stocks)}只股票 ({stats_str})")
else:
logger.info(f"✅ 主力股票池构建成功: {len(stocks)}只股票")
# 去重并返回
unique_stocks = list(dict.fromkeys(stocks)) # 保持顺序的去重
if len(unique_stocks) != len(stocks):
logger.info(f"🔄 去重后: {len(unique_stocks)}只股票")
return unique_stocks
else:
logger.error("指数成分股数据为空")
return []
except Exception as e:
logger.error(f"获取指数成分股失败: {e}")
return []
def get_rule_name(self) -> str:
return "主力股票池(沪深300+中证500+创业板指)"
class StockPoolManager: class StockPoolManager:
"""股票池管理器""" """股票池管理器"""
@ -144,6 +199,7 @@ class StockPoolManager:
self.register_rule("tushare_hot", TushareHotStocksRule()) self.register_rule("tushare_hot", TushareHotStocksRule())
self.register_rule("combined_hot", CombinedHotStocksRule()) self.register_rule("combined_hot", CombinedHotStocksRule())
self.register_rule("leading_stocks", LeadingStocksRule()) self.register_rule("leading_stocks", LeadingStocksRule())
self.register_rule("index_components", IndexComponentsRule()) # 新增主力股票池
def register_rule(self, rule_name: str, rule: StockPoolRule): def register_rule(self, rule_name: str, rule: StockPoolRule):
""" """

View File

@ -976,6 +976,167 @@ class TushareFetcher:
logger.warning(f"使用默认股票池,包含{len(df)}只股票") logger.warning(f"使用默认股票池,包含{len(df)}只股票")
return df return df
def get_index_components(self, index_codes: List[str] = None, merge: bool = True) -> pd.DataFrame:
"""
获取指数成分股
Args:
index_codes: 指数代码列表默认['000300.SH', '000905.SH', '399006.SZ']
merge: 是否合并多个指数的成分股
Returns:
指数成分股DataFrame包含stock_code, stock_name, index_code等字段
"""
try:
if index_codes is None:
# 默认使用沪深300 + 中证500 + 创业板指
index_codes = [
'000300.SH', # 沪深300
'000905.SH', # 中证500
'399006.SZ' # 创业板指
]
logger.info(f"📊 获取指数成分股: {index_codes}")
all_components = []
for index_code in index_codes:
try:
if self.pro:
# 获取指数成分股
components = self.pro.index_weight(
index_code=index_code,
trade_date=None # 获取最新的成分股
)
if not components.empty:
# 添加指数标识
components['index_code'] = index_code
components['index_name'] = self._get_index_name(index_code)
all_components.append(components)
logger.info(f"{index_code} 成分股: {len(components)}")
else:
logger.warning(f"⚠️ {index_code} 成分股数据为空")
else:
logger.error("获取指数成分股需要TuShare Pro权限")
except Exception as e:
logger.error(f"获取{index_code}成分股失败: {e}")
continue
if not all_components:
logger.error("所有指数成分股获取失败,使用备用股票池")
return self._get_fallback_index_components()
# 合并所有指数成分股
if merge:
combined_df = pd.concat(all_components, ignore_index=True)
# 去重(同一只股票可能在多个指数中)
unique_stocks = combined_df.drop_duplicates(subset=['con_code'], keep='first')
# 统一字段名
result = unique_stocks.rename(columns={
'con_code': 'stock_code',
'con_name': 'stock_name'
}).copy()
logger.info(f"🎯 主力股票池构建完成: 总计{len(result)}只股票")
else:
result = all_components
return result
except Exception as e:
logger.error(f"获取指数成分股失败: {e}")
return self._get_fallback_index_components()
def _get_index_name(self, index_code: str) -> str:
"""获取指数名称"""
index_names = {
'000300.SH': '沪深300',
'000905.SH': '中证500',
'399006.SZ': '创业板指',
'000852.SH': '中证1000',
'399001.SZ': '深证成指',
'000001.SH': '上证指数',
'000688.SH': '科创50'
}
return index_names.get(index_code, index_code)
def _get_fallback_index_components(self) -> pd.DataFrame:
"""
备用指数成分股当主接口失败时使用
包含主要的沪深300中证500创业板指代表性股票
"""
try:
# 构建一个包含各行业龙头的股票池
fallback_stocks = [
# 沪深300权重股
{'stock_code': '600519.SH', 'stock_name': '贵州茅台', 'index_code': '000300.SH'},
{'stock_code': '000858.SZ', 'stock_name': '五粮液', 'index_code': '000300.SH'},
{'stock_code': '601318.SH', 'stock_name': '中国平安', 'index_code': '000300.SH'},
{'stock_code': '600036.SH', 'stock_name': '招商银行', 'index_code': '000300.SH'},
{'stock_code': '000001.SZ', 'stock_name': '平安银行', 'index_code': '000300.SH'},
{'stock_code': '002415.SZ', 'stock_name': '海康威视', 'index_code': '000300.SH'},
{'stock_code': '600887.SH', 'stock_name': '伊利股份', 'index_code': '000300.SH'},
{'stock_code': '000002.SZ', 'stock_name': '万科A', 'index_code': '000300.SH'},
{'stock_code': '600276.SH', 'stock_name': '恒瑞医药', 'index_code': '000300.SH'},
{'stock_code': '601166.SH', 'stock_name': '兴业银行', 'index_code': '000300.SH'},
# 中证500代表股
{'stock_code': '002594.SZ', 'stock_name': 'BYD', 'index_code': '000905.SH'},
{'stock_code': '000876.SZ', 'stock_name': '新希望', 'index_code': '000905.SH'},
{'stock_code': '002304.SZ', 'stock_name': '洋河股份', 'index_code': '000905.SH'},
{'stock_code': '000063.SZ', 'stock_name': '中兴通讯', 'index_code': '000905.SH'},
{'stock_code': '600031.SH', 'stock_name': '三一重工', 'index_code': '000905.SH'},
{'stock_code': '000895.SZ', 'stock_name': '双汇发展', 'index_code': '000905.SH'},
{'stock_code': '600009.SH', 'stock_name': '上海机场', 'index_code': '000905.SH'},
# 创业板指代表股
{'stock_code': '300750.SZ', 'stock_name': '宁德时代', 'index_code': '399006.SZ'},
{'stock_code': '300059.SZ', 'stock_name': '东方财富', 'index_code': '399006.SZ'},
{'stock_code': '300014.SZ', 'stock_name': '亿纬锂能', 'index_code': '399006.SZ'},
{'stock_code': '300015.SZ', 'stock_name': '爱尔眼科', 'index_code': '399006.SZ'},
{'stock_code': '300142.SZ', 'stock_name': '沃森生物', 'index_code': '399006.SZ'},
{'stock_code': '300274.SZ', 'stock_name': '阳光电源', 'index_code': '399006.SZ'},
{'stock_code': '300316.SZ', 'stock_name': '晶盛机电', 'index_code': '399006.SZ'},
]
# 添加更多各行业代表股
additional_stocks = [
# 科技股
{'stock_code': '600570.SH', 'stock_name': '恒生电子', 'index_code': '000300.SH'},
{'stock_code': '002230.SZ', 'stock_name': '科大讯飞', 'index_code': '000905.SH'},
{'stock_code': '300433.SZ', 'stock_name': '蓝思科技', 'index_code': '399006.SZ'},
# 新能源
{'stock_code': '002129.SZ', 'stock_name': '中环股份', 'index_code': '000905.SH'},
{'stock_code': '300207.SZ', 'stock_name': '欣旺达', 'index_code': '399006.SZ'},
# 医药
{'stock_code': '300760.SZ', 'stock_name': '迈瑞医疗', 'index_code': '399006.SZ'},
{'stock_code': '000661.SZ', 'stock_name': '长春高新', 'index_code': '000300.SH'},
# 消费
{'stock_code': '603288.SH', 'stock_name': '海天味业', 'index_code': '000300.SH'},
{'stock_code': '002311.SZ', 'stock_name': '海大集团', 'index_code': '000905.SH'},
]
fallback_stocks.extend(additional_stocks)
df = pd.DataFrame(fallback_stocks)
df['index_name'] = df['index_code'].apply(self._get_index_name)
df['source'] = '备用指数成分股'
logger.warning(f"使用备用指数成分股,包含{len(df)}只股票")
return df
except Exception as e:
logger.error(f"构建备用指数成分股失败: {e}")
return pd.DataFrame()
def get_market_overview(self) -> dict: def get_market_overview(self) -> dict:
""" """
获取市场概况 获取市场概况

View File

@ -268,8 +268,8 @@ class CryptoKLinePatternStrategy(KLinePatternStrategy):
# 检测形态(返回已确认信号和形态形成) # 检测形态(返回已确认信号和形态形成)
confirmed_signals, formed_patterns = self.detect_pattern(df_with_features) confirmed_signals, formed_patterns = self.detect_pattern(df_with_features)
# 过滤一周内的信号和形态 # 过滤3天内的信号7天内的形态
recent_signals = self._filter_recent_signals(confirmed_signals, days=7) recent_signals = self._filter_recent_signals(confirmed_signals, days=3)
recent_patterns = self._filter_recent_signals(formed_patterns, days=7) recent_patterns = self._filter_recent_signals(formed_patterns, days=7)
# 处理确认信号格式 # 处理确认信号格式

View File

@ -410,9 +410,9 @@ class KLinePatternStrategy(BaseStrategy):
检测潜在的K线形态等待回踩确认 检测潜在的K线形态等待回踩确认
新形态设计强势上涨 多空博弈 突破确认 新形态设计强势上涨 多空博弈 突破确认
形态A2根连续阳线 + 博弈K线阴线/十字星 + 突破阳线 形态A2根连续阳线 + 1根阴线 + 突破阳线
形态B1根高实体阳线实体60% + 博弈K线阴线/十字星 + 突破阳线 形态B1根高实体阳线实体60% + 1根博弈K线实体40% + 突破阳线
重要约束全程价格不能跌破EMA10支撑 重要约束全程价格不能跌破EMA10支撑
Args: Args:
@ -427,74 +427,57 @@ class KLinePatternStrategy(BaseStrategy):
if df.empty or len(df) < 3: if df.empty or len(df) < 3:
return potential_patterns return potential_patterns
# 从第4个数据点开始检测需要至少5根K线强势2根+博弈2根+突破1根 # 从第3个数据点开始检测需要至少4根K线强势2根+博弈1根+突破1根
for i in range(3, len(df)): for i in range(2, len(df)):
# 检查是否有足够的K线进行突破确认 # 检查是否有足够的K线进行突破确认
if i >= len(df) - 3: if i >= len(df) - 3:
continue continue
pattern_type = None pattern_type = None
base_klines = [] base_klines = []
battle_klines = [] battle_kline = None
battle_period_high = 0 battle_period_high = 0
# 形态A2根连续阳线 + 2-3根博弈K线 # 形态A2根连续阳线 + 1根阴线
if i >= 4: # 至少需要5根历史K线 if i >= 2: # 至少需要3根历史K线索引0,1,2
# 检查强势阶段前2根是否为连续阳线 # 检查强势阶段前2根是否为连续阳线
strong_start = i - 3 # 强势阶段开始位置 k1 = df.iloc[i-2].to_dict()
k1, k2 = df.iloc[strong_start:strong_start+2].to_dict('records') k2 = df.iloc[i-1].to_dict()
if k1['is_yang'] and k2['is_yang']:
# 检查博弈阶段接下来的2根K线
battle_start = strong_start + 2
battle_klines_data = df.iloc[battle_start:i+1]
# 博弈阶段条件平均实体比例≤40%,没有明显方向性
battle_entity_ratios = battle_klines_data['entity_ratio'].tolist()
avg_battle_entity = sum(battle_entity_ratios) / len(battle_entity_ratios)
# 检查博弈阶段是否跌破EMA10
battle_min_low = battle_klines_data['low'].min()
battle_ema10_values = battle_klines_data['ema10'].dropna()
if len(battle_ema10_values) > 0:
battle_min_ema10 = battle_ema10_values.min()
if battle_min_low < battle_min_ema10:
continue # 博弈阶段跌破EMA10跳过此形态
if avg_battle_entity <= 0.4: # 博弈阶段整体实体较小
pattern_type = '强势形态A(双阳+博弈)'
base_klines = [k1, k2]
battle_klines = battle_klines_data.to_dict('records')
battle_period_high = battle_klines_data['high'].max()
# 形态B1根高实体阳线 + 2-3根博弈K线 if k1['is_yang'] and k2['is_yang']:
if pattern_type is None and i >= 3: # 至少需要4根历史K线 # 检查博弈阶段当前K线索引i必须是阴线
k_battle = df.iloc[i]
# 博弈K线条件必须是阴线
if k_battle['is_yin']:
# 检查博弈阴线是否跌破EMA10
if pd.notna(k_battle.get('ema10')) and k_battle['low'] < k_battle.get('ema10', 0):
pass # 博弈阴线跌破EMA10跳过形态A继续尝试形态B
else:
pattern_type = '强势形态A(双阳+阴线)'
base_klines = [k1, k2]
battle_kline = k_battle.to_dict() if hasattr(k_battle, 'to_dict') else dict(k_battle)
battle_period_high = k_battle['high']
# 形态B1根高实体阳线 + 1根博弈K线
if pattern_type is None and i >= 1: # 至少需要2根历史K线索引0,1
# 检查强势阶段前1根是否为高实体阳线 # 检查强势阶段前1根是否为高实体阳线
strong_start = i - 2 # 强势阶段开始位置 k1 = df.iloc[i-1].to_dict()
k1 = df.iloc[strong_start].to_dict()
if k1['is_yang'] and k1['entity_ratio'] >= 0.6: if k1['is_yang'] and k1['entity_ratio'] >= 0.6:
# 检查博弈阶段接下来的2根K线 # 检查博弈阶段当前K线索引i
battle_start = strong_start + 1 k_battle = df.iloc[i]
battle_klines_data = df.iloc[battle_start:i+1]
# 博弈K线条件实体比例≤40%
# 博弈阶段条件平均实体比例≤40% if k_battle['entity_ratio'] <= 0.4:
battle_entity_ratios = battle_klines_data['entity_ratio'].tolist() # 检查博弈K线是否跌破EMA10
avg_battle_entity = sum(battle_entity_ratios) / len(battle_entity_ratios) if pd.notna(k_battle.get('ema10')) and k_battle['low'] < k_battle.get('ema10', 0):
pass # 博弈K线跌破EMA10跳过此形态
# 检查博弈阶段是否跌破EMA10 else:
battle_min_low = battle_klines_data['low'].min() pattern_type = '强势形态B(高实体+博弈)'
battle_ema10_values = battle_klines_data['ema10'].dropna() base_klines = [k1]
if len(battle_ema10_values) > 0: battle_kline = k_battle.to_dict() if hasattr(k_battle, 'to_dict') else dict(k_battle)
battle_min_ema10 = battle_ema10_values.min() battle_period_high = k_battle['high']
if battle_min_low < battle_min_ema10:
continue # 博弈阶段跌破EMA10跳过此形态
if avg_battle_entity <= 0.4: # 博弈阶段整体实体较小
pattern_type = '强势形态B(高实体+博弈)'
base_klines = [k1]
battle_klines = battle_klines_data.to_dict('records')
battle_period_high = battle_klines_data['high'].max()
# 如果没有匹配的基础形态,继续下一次循环 # 如果没有匹配的基础形态,继续下一次循环
if pattern_type is None: if pattern_type is None:
@ -511,14 +494,14 @@ class KLinePatternStrategy(BaseStrategy):
breakout_idx = i + breakout_offset breakout_idx = i + breakout_offset
if breakout_idx >= len(df): if breakout_idx >= len(df):
break # 超出数据范围 break # 超出数据范围
k_breakout = df.iloc[breakout_idx] k_breakout = df.iloc[breakout_idx]
# 突破K线必须是大阳线实体比例≥55% # 突破K线必须是大阳线实体比例≥55%
if not k_breakout['is_yang'] or k_breakout['entity_ratio'] < 0.55: if not k_breakout['is_yang'] or k_breakout['entity_ratio'] < 0.55:
continue continue
# 检查是否突破博弈阶段最高价(收盘价>最高价) # 检查是否突破博弈K线最高价(收盘价>最高价)
if k_breakout['close'] <= battle_period_high: if k_breakout['close'] <= battle_period_high:
continue continue
@ -559,13 +542,13 @@ class KLinePatternStrategy(BaseStrategy):
'pattern_type': f'{pattern_type}+突破(待确认)', 'pattern_type': f'{pattern_type}+突破(待确认)',
'pattern_subtype': pattern_type, # 记录形态子类型 'pattern_subtype': pattern_type, # 记录形态子类型
'base_klines': base_klines, # 强势上涨K线 'base_klines': base_klines, # 强势上涨K线
'battle_klines': battle_klines, # 博弈阶段K线2-3根) 'battle_klines': [battle_kline], # 博弈K线1根)
'breakout_kline': k_breakout.to_dict() if hasattr(k_breakout, 'to_dict') else dict(k_breakout), # 突破阳线 'breakout_kline': k_breakout.to_dict() if hasattr(k_breakout, 'to_dict') else dict(k_breakout), # 突破阳线
'breakout_position': breakout_offset, # 博弈后第几根K线突破1-3 'breakout_position': breakout_offset, # 博弈后第几根K线突破1-3
'final_yang_entity_ratio': k_breakout['entity_ratio'], 'final_yang_entity_ratio': k_breakout['entity_ratio'],
'breakout_price': k_breakout['close'], 'breakout_price': k_breakout['close'],
'reference_high': battle_period_high, # 博弈阶段最高价 'reference_high': battle_period_high, # 博弈K线最高价
'yin_high': battle_period_high, # 保持兼容性,使用博弈阶段最高价 'yin_high': battle_period_high, # 保持兼容性,使用博弈K线最高价
'breakout_amount': k_breakout['close'] - battle_period_high, 'breakout_amount': k_breakout['close'] - battle_period_high,
'breakout_pct': (k_breakout['close'] - battle_period_high) / battle_period_high * 100 if battle_period_high > 0 else 0, 'breakout_pct': (k_breakout['close'] - battle_period_high) / battle_period_high * 100 if battle_period_high > 0 else 0,
'ema20_price': k_breakout.get('ema20', 0), 'ema20_price': k_breakout.get('ema20', 0),
@ -573,8 +556,8 @@ class KLinePatternStrategy(BaseStrategy):
'turnover_ratio': turnover_ratio, 'turnover_ratio': turnover_ratio,
'confirmation_pending': True, # 标记为待确认 'confirmation_pending': True, # 标记为待确认
'pattern_trigger_date': breakout_date, 'pattern_trigger_date': breakout_date,
'battle_period_days': len(battle_klines), # 博弈阶段天数 'battle_period_days': 1, # 博弈阶段固定为1天
'avg_battle_entity': avg_battle_entity # 博弈阶段平均实体比例 'avg_battle_entity': battle_kline['entity_ratio'] # 博弈K线实体比例
} }
potential_patterns.append(potential_pattern) potential_patterns.append(potential_pattern)
@ -586,12 +569,12 @@ class KLinePatternStrategy(BaseStrategy):
logger.debug(f"🏷️ 形态类型: {pattern_type}") logger.debug(f"🏷️ 形态类型: {pattern_type}")
logger.debug(f"📅 模式时间: {potential_pattern['date']}") logger.debug(f"📅 模式时间: {potential_pattern['date']}")
logger.debug(f"💰 突破价格: {potential_pattern['breakout_price']:.2f}") logger.debug(f"💰 突破价格: {potential_pattern['breakout_price']:.2f}")
logger.debug(f"🎯 博弈阶段最高价: {battle_period_high:.2f}") logger.debug(f"🎯 博弈K线最高价: {battle_period_high:.2f}")
logger.debug(f"📊 博弈阶段: {len(battle_klines)}根K线平均实体{avg_battle_entity:.1%}") logger.debug(f"📊 博弈K线: 实体比例{battle_kline['entity_ratio']:.1%}")
logger.debug(f"📍 突破位置: 博弈后第{breakout_offset}根K线") logger.debug(f"📍 突破位置: 博弈后第{breakout_offset}根K线")
logger.debug(f"🛡️ EMA10支撑: 全程保持在EMA10上方") logger.debug(f"🛡️ EMA10支撑: 全程保持在EMA10上方")
logger.debug(f"⏰ 观察窗口: {self.pullback_confirmation_days}天内") logger.debug(f"⏰ 观察窗口: {self.pullback_confirmation_days}天内")
logger.debug(f"📋 要求: 先创新高再回踩博弈阶段最高价才产生信号") logger.debug(f"📋 要求: 先创新高再回踩博弈K线最高价才产生信号")
logger.debug("🔍" + "="*60) logger.debug("🔍" + "="*60)
return potential_patterns return potential_patterns
@ -1055,8 +1038,8 @@ class KLinePatternStrategy(BaseStrategy):
# 传入stock_code以支持1h级别回踩确认 # 传入stock_code以支持1h级别回踩确认
confirmed_signals, formed_patterns = self.detect_pattern(df_with_features, stock_code) confirmed_signals, formed_patterns = self.detect_pattern(df_with_features, stock_code)
# 过滤一周内的信号和形态 # 过滤3天内的信号7天内的形态
recent_signals = self._filter_recent_signals(confirmed_signals, days=7) recent_signals = self._filter_recent_signals(confirmed_signals, days=3)
recent_patterns = self._filter_recent_signals(formed_patterns, days=7) recent_patterns = self._filter_recent_signals(formed_patterns, days=7)
# 处理确认信号格式 # 处理确认信号格式
@ -1196,23 +1179,24 @@ class KLinePatternStrategy(BaseStrategy):
{trend_desc} {trend_desc}
新形态设计强势上涨 多空博弈 突破确认 新形态设计强势上涨 多空博弈 突破确认
形态A双阳+博弈 形态A双阳+阴线
1. 强势阶段2根连续阳线 1. 强势阶段2根连续阳线不限制实体长短
2. 博弈阶段2-3根K线平均实体40%阴线/十字星/小阳线 2. 博弈阶段1根阴K线阴线记录最高价
3. 突破确认博弈后1-3根K线内大阳线实体55%突破博弈阶段最高价 3. 突破确认阴线后1-3根K线内大阳线实体55%突破阴K线最高价
形态B高实体+博弈 形态B高实体+博弈
1. 强势阶段1根高实体阳线实体60% 1. 强势阶段1根高实体阳线实体60%
2. 博弈阶段2-3根K线平均实体40%阴线/十字星/小阳线 2. 博弈阶段1根K线实体40%阴线/十字星/小阳线
3. 突破确认博弈后1-3根K线内大阳线实体55%突破博弈阶段最高价 3. 突破确认博弈后1-3根K线内大阳线实体55%突破博弈K线最高价
严格约束条件 严格约束条件
- EMA5 > EMA10 > EMA20三重多头排列全程检查 - EMA5 > EMA10 > EMA20三重多头排列全程检查
- 价格不能跌破EMA10支撑博弈突破回踩全程 - 价格不能跌破EMA10支撑博弈突破回踩全程
- 价格必须创新高后回踩到博弈阶段最高点附近才产生正式信号 - 价格必须创新高后回踩到博弈K线最高点附近才产生正式信号
- 回踩容忍度{self.pullback_tolerance:.0%} - 回踩容忍度{self.pullback_tolerance:.0%}
- 确认窗口{self.pullback_confirmation_days} - 确认窗口{self.pullback_confirmation_days}
- 当前价格过滤自动过滤价格已跌破入场价5%以上的信号 - 当前价格过滤自动过滤价格已跌破入场价5%以上的信号
- 时间过滤信号保留3天内形态保留7天内
策略优势 策略优势
- 真实市场心理强势博弈突破的完整过程 - 真实市场心理强势博弈突破的完整过程
@ -1565,64 +1549,71 @@ K线形态策略 - 双形态识别(多头排列+创新高回踩确认)
阶段0 - 多头排列筛选{trend_summary} 阶段0 - 多头排列筛选{trend_summary}
阶段1 - 模式识别不产生信号 阶段1 - 形态识别不产生信号
形态1+++突破 形态A双阳+阴线+突破
1. 识别基础形态前3根K线阳线 + 阳线 + 阴线 1. 强势阶段2根连续阳线不限制实体长短
2. 不限制阳线实体比例任何阳线都可以 2. 博弈阶段1根阴K线阴线记录最高价
3. 在第4/5/6根K线中寻找阳线突破阴线最高价 3. 突破确认阴线后1-3根K线内大阳线实体55%突破阴K线最高价
形态2大阳+小实体+突破 形态B高实体+博弈+突破
1. 第1根K线大阳线实体>55% 1. 强势阶段1根高实体阳线实体60%
2. 第2根K线小实体实体<45%阴阳不限 2. 博弈阶段1根K线实体40%阴线/十字星/小阳线
3. 在第4/5/6根K线中寻找大阳线实体>55%突破第2根K线最高价 3. 突破确认博弈后1-3根K线内大阳线实体55%突破博弈K线最高价
此时仅记录模式不产生交易信号 此时仅记录形态不产生交易信号
阶段1 - 创新高回踩确认产生信号 阶段2 - 创新高回踩确认产生信号
4. 价格必须创新高高于突破阳线价格 4. 价格必须创新高高于突破阳线价格
5. 然后回踩到参考K线最高点附近容忍度{self.pullback_tolerance:.0%} 5. 然后回踩到参考K线最高点附近形态A回踩阴线最高价形态B回踩博弈K线最高价
6. 确认窗口模式识别后 {self.pullback_confirmation_days} 个交易日内 6. 回踩容忍度{self.pullback_tolerance:.0%}
7. 只有完成"创新高+回踩参考价"才产生正式交易信号 7. 确认窗口形态识别后 {self.pullback_confirmation_days} 个交易日内
8. 只有完成"创新高+回踩参考K线最高价"才产生正式交易信号
9. 优先使用1小时级别数据实时确认回踩
严格约束条件
- EMA5 > EMA10 > EMA20三重多头排列全程检查
- 价格不能跌破EMA10支撑博弈突破回踩全程
- 当前价格过滤自动过滤价格已跌破入场价5%以上的信号
- 时间过滤信号保留3天内形态保留7天内
核心优化理念 核心优化理念
- 支持双形态识别提高识别覆盖率 - 双形态识别提高识别覆盖率
- 形态1传统形态适合标准突破 - 形态A经典"双阳+阴线"突破适合标准趋势反转
- 形态2强势整理适合快速突破 - 形态B强势"高实体+博弈"突破适合快速拉升
- 等待价格证明突破有效性创新高 - 等待价格证明突破有效性创新高
- 再等待合理回踩机会回踩参考价 - 再等待合理回踩机会回踩参考K线最高
- 确保信号质量和入场时机的最佳化 - 确保信号质量和入场时机的最佳化
回踩监控功能 回踩监控功能
- 自动监控已确认信号后的价格走势 - 自动监控已确认信号后的价格走势
- 当价格再次回踩到线最高点附近时发送特殊提醒 - 当价格再次回踩到参考K线最高点附近时发送特殊提醒
- 监控期限信号确认后 {self.monitor_days} - 监控期限信号确认后 {self.monitor_days}
- 提醒条件价格接近线最高点且相比新高有明显回调 - 提醒条件价格接近参考K线最高点且相比新高有明显回调
信号特征 信号特征
- 模式识别满足基础形态条件无信号 - 形态识别满足基础形态条件无信号
- 确认信号完成创新高+回踩确认正式信号 - 确认信号完成创新高+回踩确认正式信号
- 信号内容包含模式日期突破位置第几根K线创新高日期回踩确认日期 - 信号内容包含形态日期突破位置第几根K线创新高日期回踩确认日期
- 支持时间周期{', '.join(self.timeframes)} - 支持时间周期{', '.join(self.timeframes)}
优化效果 优化效果
- 基础形态识别率提高不要求第4根必须突破 - 多形态识别率提高覆盖更多机会
- 极大降低假突破信号 - 极大降低假突破信号
- 提供更优质的入场时机 - 提供更优质的入场时机
- 确保突破的真实有效性 - 确保突破的真实有效性
- 降低追高风险提高成功率 - 降低追高风险提高成功率
扫描范围 扫描范围
- 优先使用双数据源合并同花顺热股+东财人气榜 - 主力股票池沪深300 + 中证500 + 创业板指成分股
- 自动去重保留最优质股票 - 约1000-1500只优质股票
- 回退到全市场股票列表
通知方式 通知方式
- 钉钉webhook汇总推送10个信号一组分批发送 - 钉钉webhook汇总推送10个信号一组分批发送
- 仅确认信号发送通知模式识别不通知 - 仅确认信号发送通知形态识别不通知
- 创新高回踩确认完成通知正式信号 - 创新高回踩确认完成通知正式信号
- 价格二次回踩特殊提醒5个提醒一组分批发送 - 价格二次回踩特殊提醒5个提醒一组分批发送
- 包含关键信息代码股票名称模式日期创新高日期回踩确认日期价格等 - 包含关键信息代码股票名称形态日期创新高日期回踩确认日期价格等
- 系统日志详细记录 - 系统日志详细记录
""" """

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# 加密货币市场扫描启动脚本 # 加密货币市场扫描启动脚本 (Docker定时任务版本)
# 用于手动运行或在Docker容器中启动 # 用于在Docker容器中启动定时任务服务
# 设置错误时退出 # 设置错误时退出
set -e set -e
@ -12,11 +12,12 @@ cd "$SCRIPT_DIR"
# 打印启动信息 # 打印启动信息
echo "==========================================" echo "=========================================="
echo "🚀 加密货币市场扫描程序启动" echo "🚀 加密货币市场扫描定时服务启动"
echo "==========================================" echo "=========================================="
echo "工作目录: $(pwd)" echo "工作目录: $(pwd)"
echo "Python版本: $(python --version 2>&1)" echo "Python版本: $(python --version 2>&1)"
echo "时间: $(date '+%Y-%m-%d %H:%M:%S')" echo "时区: $(date '+%Z %z')"
echo "当前时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "==========================================" echo "=========================================="
# 检查Python是否安装 # 检查Python是否安装
@ -25,6 +26,12 @@ if ! command -v python &> /dev/null; then
exit 1 exit 1
fi fi
# 检查加密货币扫描器是否存在
if [ ! -f "crypto_scanner.py" ]; then
echo "❌ 错误: 未找到crypto_scanner.py"
exit 1
fi
# 检查依赖包是否安装 # 检查依赖包是否安装
echo "📦 检查依赖包..." echo "📦 检查依赖包..."
if ! python -c "import binance" 2>/dev/null; then if ! python -c "import binance" 2>/dev/null; then
@ -35,19 +42,56 @@ fi
# 创建日志目录 # 创建日志目录
mkdir -p logs mkdir -p logs
# 获取命令行参数(扫描交易对数量) # 检查cron服务
MAX_SYMBOLS=${1:-100} if ! command -v cron &> /dev/null; then
echo "⚠️ 警告: cron未安装正在安装..."
apt-get update && apt-get install -y cron
fi
echo "📊 扫描参数: 最大交易对数量 = $MAX_SYMBOLS" # 安装加密货币扫描定时任务
echo "==========================================" echo "📅 安装加密货币扫描定时任务..."
echo "" if [ -f "crontab/crypto-scanner" ]; then
# 复制定时任务配置
cp crontab/crypto-scanner /etc/cron.d/crypto-scanner
chmod 0644 /etc/cron.d/crypto-scanner
echo "✅ 加密货币扫描定时任务已安装"
else
echo "❌ 错误: 未找到crontab/crypto-scanner文件"
exit 1
fi
# 运行扫描程序 # 启动cron服务
python crypto_scanner.py "$MAX_SYMBOLS" echo "🔄 启动cron定时服务..."
service cron start
# 显示当前的定时任务
echo "📋 当前定时任务列表:"
crontab -l -u root 2>/dev/null || echo "暂无定时任务"
# 打印完成信息
echo "" echo ""
echo "==========================================" echo "=========================================="
echo "✅ 加密货币市场扫描完成" echo "✅ 加密货币市场扫描定时服务启动完成"
echo "时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "==========================================" echo "=========================================="
echo "📅 定时任务计划:"
echo " - 每天 08:00: 扫描100个交易对 (隔夜行情)"
echo " - 每天 16:00: 扫描100个交易对 (下午行情)"
echo " - 每天 00:00: 扫描100个交易对 (晚间行情)"
echo " - 每周日 10:00: 扫描200个交易对 (深度扫描)"
echo ""
echo "📊 日志文件: /app/logs/crypto_cron.log"
echo "🔄 服务状态: $(service cron status | head -1)"
echo "=========================================="
# 保持容器运行监控cron服务
echo "🔍 监控cron服务状态..."
while true; do
# 检查cron服务是否运行
if ! service cron status > /dev/null 2>&1; then
echo "⚠️ cron服务停止正在重启..."
service cron start
fi
# 每小时输出一次状态
sleep 3600
echo "$(date '+%Y-%m-%d %H:%M:%S') - 加密货币扫描定时服务正常运行"
done