diff --git a/docker-compose.yml b/docker-compose.yml index 12b42ee..5cb692a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,6 +55,31 @@ services: depends_on: - 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: # image: nginx:alpine diff --git a/main.py b/main.py index cfa8bf8..2b84490 100644 --- a/main.py +++ b/main.py @@ -155,7 +155,7 @@ def main(): max_stocks = int(parts[2]) if len(parts) > 2 and parts[2].isdigit() else 10 execute_task(system['executor'], rule, max_stocks) else: - print("请提供股票池规则,如: task tushare_hot 15") + print("请提供股票池规则,如: task index_components 100") elif command.lower() == 'schedule': show_schedule_examples(system['scheduler'], system['executor']) @@ -206,16 +206,16 @@ def scan_single_stock(strategy, stock_code): def scan_market(executor, max_stocks): - """扫描市场热门股票""" - print(f"\n🌍 扫描市场热门股票 (前{max_stocks}只)") + """扫描主力股票池""" + print(f"\n🎯 扫描主力股票池 (沪深300+中证500+创业板指,最多{max_stocks}只)") print("-" * 50) try: result = executor.execute_task( task_id=f"market_scan_{max_stocks}", strategy_id="kline_pattern", - stock_pool_rule="tushare_hot", - stock_pool_params={"limit": max_stocks * 2}, + stock_pool_rule="index_components", # 使用主力股票池 + stock_pool_params={}, max_stocks=max_stocks, send_notification=False ) @@ -251,7 +251,8 @@ def show_stock_pools(pool_manager): print(f" 🎯 {rule_id}: {rule_name}") 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): diff --git a/market_scanner.py b/market_scanner.py index 462c3af..84dad12 100644 --- a/market_scanner.py +++ b/market_scanner.py @@ -95,21 +95,21 @@ def create_strategy_system(): def scan_market(): - """执行市场扫描 - 分析所有同花顺热榜股票""" - logger.info(f"🚀 开始市场扫描任务 - 扫描全部同花顺热榜股票") + """执行市场扫描 - 分析主力股票池(沪深300+中证500+创业板指)""" + logger.info(f"🚀 开始市场扫描任务 - 扫描主力股票池(沪深300+中证500+创业板指)") try: # 初始化系统 executor = create_strategy_system() - # 执行扫描任务 - 不限制数量,分析所有股票 + # 执行扫描任务 - 使用主力股票池(沪深300+中证500+创业板指) 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={}, # 不限制数量 - max_stocks=None, # 取消数量限制 + stock_pool_rule="index_components", # 使用主力股票池 + stock_pool_params={}, # 使用默认指数组合 + max_stocks=None, # 分析所有成分股 send_notification=True # 启用通知 ) diff --git a/src/data/stock_pool_manager.py b/src/data/stock_pool_manager.py index 5bfa6e0..86b2ba9 100644 --- a/src/data/stock_pool_manager.py +++ b/src/data/stock_pool_manager.py @@ -125,6 +125,61 @@ class CustomStockListRule(StockPoolRule): 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: """股票池管理器""" @@ -144,6 +199,7 @@ class StockPoolManager: self.register_rule("tushare_hot", TushareHotStocksRule()) self.register_rule("combined_hot", CombinedHotStocksRule()) self.register_rule("leading_stocks", LeadingStocksRule()) + self.register_rule("index_components", IndexComponentsRule()) # 新增主力股票池 def register_rule(self, rule_name: str, rule: StockPoolRule): """ diff --git a/src/data/tushare_fetcher.py b/src/data/tushare_fetcher.py index 5dc2354..e3af320 100644 --- a/src/data/tushare_fetcher.py +++ b/src/data/tushare_fetcher.py @@ -976,6 +976,167 @@ class TushareFetcher: logger.warning(f"使用默认股票池,包含{len(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: """ 获取市场概况 diff --git a/src/strategy/crypto_kline_pattern_strategy.py b/src/strategy/crypto_kline_pattern_strategy.py index f6a1660..518b97c 100644 --- a/src/strategy/crypto_kline_pattern_strategy.py +++ b/src/strategy/crypto_kline_pattern_strategy.py @@ -268,8 +268,8 @@ class CryptoKLinePatternStrategy(KLinePatternStrategy): # 检测形态(返回已确认信号和形态形成) confirmed_signals, formed_patterns = self.detect_pattern(df_with_features) - # 过滤一周内的信号和形态 - recent_signals = self._filter_recent_signals(confirmed_signals, days=7) + # 过滤3天内的信号,7天内的形态 + recent_signals = self._filter_recent_signals(confirmed_signals, days=3) recent_patterns = self._filter_recent_signals(formed_patterns, days=7) # 处理确认信号格式 diff --git a/src/strategy/kline_pattern_strategy.py b/src/strategy/kline_pattern_strategy.py index 0456f6a..4c32f90 100644 --- a/src/strategy/kline_pattern_strategy.py +++ b/src/strategy/kline_pattern_strategy.py @@ -410,9 +410,9 @@ class KLinePatternStrategy(BaseStrategy): 检测潜在的K线形态(等待回踩确认) 新形态设计:强势上涨 → 多空博弈 → 突破确认 - 形态A:2根连续阳线 + 博弈K线(阴线/十字星) + 突破阳线 - 形态B:1根高实体阳线(实体≥60%) + 博弈K线(阴线/十字星) + 突破阳线 - + 形态A:2根连续阳线 + 1根阴线 + 突破阳线 + 形态B:1根高实体阳线(实体≥60%) + 1根博弈K线(实体≤40%) + 突破阳线 + 重要约束:全程价格不能跌破EMA10支撑 Args: @@ -427,74 +427,57 @@ class KLinePatternStrategy(BaseStrategy): if df.empty or len(df) < 3: return potential_patterns - # 从第4个数据点开始检测(需要至少5根K线:强势2根+博弈2根+突破1根) - for i in range(3, len(df)): + # 从第3个数据点开始检测(需要至少4根K线:强势2根+博弈1根+突破1根) + for i in range(2, len(df)): # 检查是否有足够的K线进行突破确认 if i >= len(df) - 3: continue - + pattern_type = None base_klines = [] - battle_klines = [] + battle_kline = None battle_period_high = 0 - # 形态A:2根连续阳线 + 2-3根博弈K线 - if i >= 4: # 至少需要5根历史K线 + # 形态A:2根连续阳线 + 1根阴线 + if i >= 2: # 至少需要3根历史K线(索引0,1,2) # 检查强势阶段:前2根是否为连续阳线 - strong_start = i - 3 # 强势阶段开始位置 - k1, k2 = df.iloc[strong_start:strong_start+2].to_dict('records') - - 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() + k1 = df.iloc[i-2].to_dict() + k2 = df.iloc[i-1].to_dict() - # 形态B:1根高实体阳线 + 2-3根博弈K线 - if pattern_type is None and i >= 3: # 至少需要4根历史K线 + if k1['is_yang'] and k2['is_yang']: + # 检查博弈阶段:当前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'] + + # 形态B:1根高实体阳线 + 1根博弈K线 + if pattern_type is None and i >= 1: # 至少需要2根历史K线(索引0,1) # 检查强势阶段:前1根是否为高实体阳线 - strong_start = i - 2 # 强势阶段开始位置 - k1 = df.iloc[strong_start].to_dict() - + k1 = df.iloc[i-1].to_dict() + if k1['is_yang'] and k1['entity_ratio'] >= 0.6: - # 检查博弈阶段:接下来的2根K线 - battle_start = strong_start + 1 - 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 = '强势形态B(高实体+博弈)' - base_klines = [k1] - battle_klines = battle_klines_data.to_dict('records') - battle_period_high = battle_klines_data['high'].max() + # 检查博弈阶段:当前K线(索引i) + k_battle = df.iloc[i] + + # 博弈K线条件:实体比例≤40% + if k_battle['entity_ratio'] <= 0.4: + # 检查博弈K线是否跌破EMA10 + if pd.notna(k_battle.get('ema10')) and k_battle['low'] < k_battle.get('ema10', 0): + pass # 博弈K线跌破EMA10,跳过此形态 + else: + pattern_type = '强势形态B(高实体+博弈)' + base_klines = [k1] + battle_kline = k_battle.to_dict() if hasattr(k_battle, 'to_dict') else dict(k_battle) + battle_period_high = k_battle['high'] # 如果没有匹配的基础形态,继续下一次循环 if pattern_type is None: @@ -511,14 +494,14 @@ class KLinePatternStrategy(BaseStrategy): breakout_idx = i + breakout_offset if breakout_idx >= len(df): break # 超出数据范围 - + k_breakout = df.iloc[breakout_idx] # 突破K线必须是大阳线(实体比例≥55%) if not k_breakout['is_yang'] or k_breakout['entity_ratio'] < 0.55: continue - # 检查是否突破博弈阶段最高价(收盘价>最高价) + # 检查是否突破博弈K线最高价(收盘价>最高价) if k_breakout['close'] <= battle_period_high: continue @@ -559,13 +542,13 @@ class KLinePatternStrategy(BaseStrategy): 'pattern_type': f'{pattern_type}+突破(待确认)', 'pattern_subtype': pattern_type, # 记录形态子类型 '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_position': breakout_offset, # 博弈后第几根K线突破(1-3) 'final_yang_entity_ratio': k_breakout['entity_ratio'], 'breakout_price': k_breakout['close'], - 'reference_high': battle_period_high, # 博弈阶段最高价 - 'yin_high': battle_period_high, # 保持兼容性,使用博弈阶段最高价 + 'reference_high': battle_period_high, # 博弈K线最高价 + 'yin_high': battle_period_high, # 保持兼容性,使用博弈K线最高价 '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, 'ema20_price': k_breakout.get('ema20', 0), @@ -573,8 +556,8 @@ class KLinePatternStrategy(BaseStrategy): 'turnover_ratio': turnover_ratio, 'confirmation_pending': True, # 标记为待确认 'pattern_trigger_date': breakout_date, - 'battle_period_days': len(battle_klines), # 博弈阶段天数 - 'avg_battle_entity': avg_battle_entity # 博弈阶段平均实体比例 + 'battle_period_days': 1, # 博弈阶段固定为1天 + 'avg_battle_entity': battle_kline['entity_ratio'] # 博弈K线实体比例 } potential_patterns.append(potential_pattern) @@ -586,12 +569,12 @@ class KLinePatternStrategy(BaseStrategy): logger.debug(f"🏷️ 形态类型: {pattern_type}") logger.debug(f"📅 模式时间: {potential_pattern['date']}") logger.debug(f"💰 突破价格: {potential_pattern['breakout_price']:.2f}元") - logger.debug(f"🎯 博弈阶段最高价: {battle_period_high:.2f}元") - logger.debug(f"📊 博弈阶段: {len(battle_klines)}根K线,平均实体{avg_battle_entity:.1%}") + logger.debug(f"🎯 博弈K线最高价: {battle_period_high:.2f}元") + logger.debug(f"📊 博弈K线: 实体比例{battle_kline['entity_ratio']:.1%}") logger.debug(f"📍 突破位置: 博弈后第{breakout_offset}根K线") logger.debug(f"🛡️ EMA10支撑: 全程保持在EMA10上方") logger.debug(f"⏰ 观察窗口: {self.pullback_confirmation_days}天内") - logger.debug(f"📋 要求: 先创新高再回踩博弈阶段最高价才产生信号") + logger.debug(f"📋 要求: 先创新高再回踩博弈K线最高价才产生信号") logger.debug("🔍" + "="*60) return potential_patterns @@ -1055,8 +1038,8 @@ class KLinePatternStrategy(BaseStrategy): # 传入stock_code以支持1h级别回踩确认 confirmed_signals, formed_patterns = self.detect_pattern(df_with_features, stock_code) - # 过滤一周内的信号和形态 - recent_signals = self._filter_recent_signals(confirmed_signals, days=7) + # 过滤3天内的信号,7天内的形态 + recent_signals = self._filter_recent_signals(confirmed_signals, days=3) recent_patterns = self._filter_recent_signals(formed_patterns, days=7) # 处理确认信号格式 @@ -1196,23 +1179,24 @@ class KLinePatternStrategy(BaseStrategy): {trend_desc} 新形态设计:强势上涨 → 多空博弈 → 突破确认 -【形态A:双阳+博弈】 -1. 强势阶段:2根连续阳线 -2. 博弈阶段:2-3根K线,平均实体≤40%(阴线/十字星/小阳线) -3. 突破确认:博弈后1-3根K线内,大阳线(实体≥55%)突破博弈阶段最高价 +【形态A:双阳+阴线】 +1. 强势阶段:2根连续阳线(不限制实体长短) +2. 博弈阶段:1根阴K线(阴线记录最高价) +3. 突破确认:阴线后1-3根K线内,大阳线(实体≥55%)突破阴K线最高价 【形态B:高实体+博弈】 1. 强势阶段:1根高实体阳线(实体≥60%) -2. 博弈阶段:2-3根K线,平均实体≤40%(阴线/十字星/小阳线) -3. 突破确认:博弈后1-3根K线内,大阳线(实体≥55%)突破博弈阶段最高价 +2. 博弈阶段:1根K线,实体≤40%(阴线/十字星/小阳线) +3. 突破确认:博弈后1-3根K线内,大阳线(实体≥55%)突破博弈K线最高价 【严格约束条件】 - EMA5 > EMA10 > EMA20(三重多头排列)全程检查 - 价格不能跌破EMA10支撑(博弈、突破、回踩全程) -- 价格必须创新高后回踩到博弈阶段最高点附近才产生正式信号 +- 价格必须创新高后回踩到博弈K线最高点附近才产生正式信号 - 回踩容忍度:{self.pullback_tolerance:.0%} - 确认窗口:{self.pullback_confirmation_days}天 - 当前价格过滤:自动过滤价格已跌破入场价5%以上的信号 +- 时间过滤:信号保留3天内,形态保留7天内 【策略优势】 - 真实市场心理:强势→博弈→突破的完整过程 @@ -1565,64 +1549,71 @@ K线形态策略 - 双形态识别(多头排列+创新高回踩确认) 【阶段0 - 多头排列筛选】{trend_summary} -【阶段1 - 模式识别(不产生信号)】 +【阶段1 - 形态识别(不产生信号)】 -形态1:阳+阳+阴+突破 -1. 识别基础形态(前3根K线):阳线 + 阳线 + 阴线 -2. 不限制阳线实体比例(任何阳线都可以) -3. 在第4/5/6根K线中寻找阳线突破阴线最高价 +形态A:双阳+阴线+突破 +1. 强势阶段:2根连续阳线(不限制实体长短) +2. 博弈阶段:1根阴K线(阴线记录最高价) +3. 突破确认:阴线后1-3根K线内,大阳线(实体≥55%)突破阴K线最高价 -形态2:大阳+小实体+突破 -1. 第1根K线:大阳线(实体>55%) -2. 第2根K线:小实体(实体<45%,阴阳不限) -3. 在第4/5/6根K线中寻找大阳线(实体>55%)突破第2根K线最高价 +形态B:高实体+博弈+突破 +1. 强势阶段:1根高实体阳线(实体≥60%) +2. 博弈阶段:1根K线,实体≤40%(阴线/十字星/小阳线) +3. 突破确认:博弈后1-3根K线内,大阳线(实体≥55%)突破博弈K线最高价 -※ 此时仅记录模式,不产生交易信号 +※ 此时仅记录形态,不产生交易信号 -【阶段1 - 创新高回踩确认(产生信号)】 +【阶段2 - 创新高回踩确认(产生信号)】 4. 价格必须创新高(高于突破阳线价格) -5. 然后回踩到参考K线最高点附近(容忍度:{self.pullback_tolerance:.0%}) -6. 确认窗口:模式识别后 {self.pullback_confirmation_days} 个交易日内 -7. 只有完成"创新高+回踩参考价"才产生正式交易信号 +5. 然后回踩到参考K线最高点附近(形态A回踩阴线最高价,形态B回踩博弈K线最高价) +6. 回踩容忍度:{self.pullback_tolerance:.0%} +7. 确认窗口:形态识别后 {self.pullback_confirmation_days} 个交易日内 +8. 只有完成"创新高+回踩参考K线最高价"才产生正式交易信号 +9. 优先使用1小时级别数据实时确认回踩 + +严格约束条件: +- EMA5 > EMA10 > EMA20(三重多头排列)全程检查 +- 价格不能跌破EMA10支撑(博弈、突破、回踩全程) +- 当前价格过滤:自动过滤价格已跌破入场价5%以上的信号 +- 时间过滤:信号保留3天内,形态保留7天内 核心优化理念: -- 支持双形态识别,提高识别覆盖率 -- 形态1:传统形态,适合标准突破 -- 形态2:强势整理,适合快速突破 +- 双形态识别,提高识别覆盖率 +- 形态A:经典"双阳+阴线"突破,适合标准趋势反转 +- 形态B:强势"高实体+博弈"突破,适合快速拉升 - 等待价格证明突破有效性(创新高) -- 再等待合理回踩机会(回踩参考价) +- 再等待合理回踩机会(回踩参考K线最高价) - 确保信号质量和入场时机的最佳化 回踩监控功能: - 自动监控已确认信号后的价格走势 -- 当价格再次回踩到阴线最高点附近时发送特殊提醒 +- 当价格再次回踩到参考K线最高点附近时发送特殊提醒 - 监控期限:信号确认后 {self.monitor_days} 天 -- 提醒条件:价格接近阴线最高点且相比新高有明显回调 +- 提醒条件:价格接近参考K线最高点且相比新高有明显回调 信号特征: -- 模式识别:满足基础形态条件(无信号) +- 形态识别:满足基础形态条件(无信号) - 确认信号:完成创新高+回踩确认(正式信号) -- 信号内容:包含模式日期、突破位置(第几根K线)、创新高日期、回踩确认日期 +- 信号内容:包含形态日期、突破位置(第几根K线)、创新高日期、回踩确认日期 - 支持时间周期:{', '.join(self.timeframes)} 优化效果: -- 基础形态识别率提高(不要求第4根必须突破) +- 多形态识别率提高,覆盖更多机会 - 极大降低假突破信号 - 提供更优质的入场时机 - 确保突破的真实有效性 - 降低追高风险,提高成功率 扫描范围: -- 优先使用双数据源合并(同花顺热股+东财人气榜) -- 自动去重,保留最优质股票 -- 回退到全市场股票列表 +- 主力股票池:沪深300 + 中证500 + 创业板指成分股 +- 约1000-1500只优质股票 通知方式: - 钉钉webhook汇总推送(10个信号一组分批发送) -- 仅确认信号发送通知(模式识别不通知) +- 仅确认信号发送通知(形态识别不通知) - 创新高回踩确认完成通知(正式信号) - 价格二次回踩特殊提醒(5个提醒一组分批发送) -- 包含关键信息:代码、股票名称、模式日期、创新高日期、回踩确认日期、价格等 +- 包含关键信息:代码、股票名称、形态日期、创新高日期、回踩确认日期、价格等 - 系统日志详细记录 """ diff --git a/start_crypto_scanner.sh b/start_crypto_scanner.sh index 400088c..9d4eb6d 100755 --- a/start_crypto_scanner.sh +++ b/start_crypto_scanner.sh @@ -1,7 +1,7 @@ #!/bin/bash -# 加密货币市场扫描启动脚本 -# 用于手动运行或在Docker容器中启动 +# 加密货币市场扫描启动脚本 (Docker定时任务版本) +# 用于在Docker容器中启动定时任务服务 # 设置错误时退出 set -e @@ -12,11 +12,12 @@ cd "$SCRIPT_DIR" # 打印启动信息 echo "==========================================" -echo "🚀 加密货币市场扫描程序启动" +echo "🚀 加密货币市场扫描定时服务启动" echo "==========================================" echo "工作目录: $(pwd)" 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 "==========================================" # 检查Python是否安装 @@ -25,6 +26,12 @@ if ! command -v python &> /dev/null; then exit 1 fi +# 检查加密货币扫描器是否存在 +if [ ! -f "crypto_scanner.py" ]; then + echo "❌ 错误: 未找到crypto_scanner.py" + exit 1 +fi + # 检查依赖包是否安装 echo "📦 检查依赖包..." if ! python -c "import binance" 2>/dev/null; then @@ -35,19 +42,56 @@ fi # 创建日志目录 mkdir -p logs -# 获取命令行参数(扫描交易对数量) -MAX_SYMBOLS=${1:-100} +# 检查cron服务 +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 -# 运行扫描程序 -python crypto_scanner.py "$MAX_SYMBOLS" +# 启动cron服务 +echo "🔄 启动cron定时服务..." +service cron start + +# 显示当前的定时任务 +echo "📋 当前定时任务列表:" +crontab -l -u root 2>/dev/null || 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 \ No newline at end of file