Download model/classic_patterns.py from sgfw/kronos: direct link, hf CLI and curl.
- Browser
- Download file 29.2 kB
-
https://huggingface.co/spaces/sgfw/kronos/resolve/main/model/classic_patterns.py
- Command line
-
hf download hf://spaces/sgfw/kronos/model/classic_patterns.py
-
curl -L -o classic_patterns.py https://huggingface.co/spaces/sgfw/kronos/resolve/main/model/classic_patterns.py
29.2 kB
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| 经典K线形态识别模块 | |
| 识别老鸭头、红三兵、仙人指路、倍量柱等经典形态 | |
| 结合《量柱擒涨停》《涨停板战法研究合集》等经典理论 | |
| """ | |
| import numpy as np | |
| import pandas as pd | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| class ClassicPatternDetector: | |
| """经典形态识别器""" | |
| def __init__(self): | |
| self.pattern_names = [ | |
| 'old_duck_head', # 老鸭头 | |
| 'three_white_soldiers', # 红三兵 | |
| 'immortal_pointing', # 仙人指路 | |
| 'double_volume', # 倍量柱 | |
| 'ladder_volume', # 梯量柱 | |
| 'golden_cross', # 黄金交叉(均线) | |
| ] | |
| def detect_all(self, df): | |
| """ | |
| 识别所有经典形态,返回检测结果和分析摘要 | |
| Args: | |
| df: DataFrame,需包含 open/high/low/close/volume 列,建议已设置 DatetimeIndex | |
| Returns: | |
| dict: { | |
| 'patterns': { 形态名: { detected, signal, details... } }, | |
| 'summary': str, # 综合分析摘要 | |
| 'score': float, # 综合形态评分 0~100 | |
| 'detected_list': [...] # 已检测到的形态列表 | |
| } | |
| """ | |
| df = df.copy() | |
| if len(df) < 10: | |
| return self._empty_result('数据不足,至少需要10根K线') | |
| # 确保有足够的列 | |
| for col in ['open', 'high', 'low', 'close', 'volume']: | |
| if col not in df.columns: | |
| return self._empty_result(f'缺少 {col} 列') | |
| close = df['close'].values.astype(float) | |
| open_p = df['open'].values.astype(float) | |
| high = df['high'].values.astype(float) | |
| low = df['low'].values.astype(float) | |
| volume = df['volume'].values.astype(float) | |
| # 预计算常用指标 | |
| ma5 = pd.Series(close).rolling(5).mean().values | |
| ma10 = pd.Series(close).rolling(10).mean().values | |
| ma20 = pd.Series(close).rolling(20).mean().values | |
| ma60 = pd.Series(close).rolling(60).mean().values | |
| vol_ma5 = pd.Series(volume).rolling(5).mean().values | |
| vol_ma20 = pd.Series(volume).rolling(20).mean().values | |
| results = {} | |
| # ---- 1. 老鸭头形态 ---- | |
| results['old_duck_head'] = self._detect_old_duck_head( | |
| close, open_p, high, low, volume, ma5, ma10, ma20, ma60, vol_ma5, df | |
| ) | |
| # ---- 2. 红三兵形态 ---- | |
| results['three_white_soldiers'] = self._detect_three_white_soldiers( | |
| close, open_p, high, low, volume, vol_ma5, df | |
| ) | |
| # ---- 3. 仙人指路形态 ---- | |
| results['immortal_pointing'] = self._detect_immortal_pointing( | |
| close, open_p, high, low, volume, vol_ma5, df | |
| ) | |
| # ---- 4. 倍量柱形态 ---- | |
| results['double_volume'] = self._detect_double_volume( | |
| close, open_p, high, low, volume, vol_ma5, vol_ma20, df | |
| ) | |
| # ---- 5. 梯量柱形态 ---- | |
| results['ladder_volume'] = self._detect_ladder_volume( | |
| close, open_p, high, low, volume, vol_ma5, df | |
| ) | |
| # ---- 5b. 波段高位卖点 ---- | |
| results['distribution_top'] = self._detect_distribution_top( | |
| close, open_p, high, low, volume, vol_ma5, vol_ma20, df | |
| ) | |
| # ---- 6. 黄金交叉 ---- | |
| results['golden_cross'] = self._detect_golden_cross( | |
| close, ma5, ma10, ma20, ma60, df | |
| ) | |
| # 汇总 | |
| detected_list = [] | |
| total_score = 0 | |
| score_count = 0 | |
| details_parts = [] | |
| name_map = { | |
| 'old_duck_head': '老鸭头', | |
| 'three_white_soldiers': '红三兵', | |
| 'immortal_pointing': '仙人指路', | |
| 'double_volume': '倍量柱', | |
| 'ladder_volume': '梯量柱', | |
| 'golden_cross': '均线金叉', | |
| 'distribution_top': '波段高位', | |
| } | |
| for key, val in results.items(): | |
| if val.get('detected', False): | |
| item = { | |
| 'name': name_map.get(key, key), | |
| 'key': key, | |
| 'signal': val.get('signal', 'neutral'), | |
| 'description': val.get('description', ''), | |
| 'strength': val.get('strength', 0), | |
| } | |
| # 传递 index/date/price 用于图表标注 | |
| for extra_key in ('index', 'date', 'price', 'vol_ratio'): | |
| if extra_key in val: | |
| item[extra_key] = val[extra_key] | |
| detected_list.append(item) | |
| if 'score' in val: | |
| total_score += val['score'] | |
| score_count += 1 | |
| details_parts.append(f"【{name_map.get(key, key)}】{val.get('description', '')}") | |
| avg_score = total_score / score_count if score_count > 0 else 0 | |
| # 综合信号判断 | |
| bull_signals = sum(1 for d in detected_list if d['signal'] == 'bullish') | |
| bear_signals = sum(1 for d in detected_list if d['signal'] == 'bearish') | |
| if bull_signals >= 2: | |
| overall_signal = 'bullish' | |
| summary = f"检测到 {len(detected_list)} 个经典形态,多方信号占优({bull_signals}个看多),市场偏强势" | |
| elif bear_signals >= 2: | |
| overall_signal = 'bearish' | |
| summary = f"检测到 {len(detected_list)} 个经典形态,空方信号占优({bear_signals}个看空),需注意风险" | |
| elif len(detected_list) > 0: | |
| overall_signal = 'neutral' | |
| summary = f"检测到 {len(detected_list)} 个经典形态,多空力量较为均衡" | |
| else: | |
| overall_signal = 'none' | |
| summary = "近期未检测到明显的经典K线形态,市场处于无明确形态状态" | |
| if details_parts: | |
| summary += "。" + "; ".join(details_parts) | |
| # 生成图表标注用的 markers | |
| buy_markers = [] | |
| sell_markers = [] | |
| for item in detected_list: | |
| if item.get('key') == 'double_volume' and 'index' in item: | |
| buy_markers.append({ | |
| 'index': int(item['index']), | |
| 'date': item.get('date', ''), | |
| 'price': item.get('price', 0), | |
| 'label': '倍量' + ('强势' if item.get('signal') == 'bullish' else '关注'), | |
| 'strength': item.get('strength', 0), | |
| }) | |
| if item.get('key') == 'distribution_top' and 'index' in item: | |
| sell_markers.append({ | |
| 'index': int(item['index']), | |
| 'date': item.get('date', ''), | |
| 'price': item.get('price', 0), | |
| 'label': item.get('label', '波段高位'), | |
| 'strength': item.get('strength', 0), | |
| }) | |
| return { | |
| 'patterns': results, | |
| 'summary': summary, | |
| 'score': round(avg_score, 1), | |
| 'overall_signal': overall_signal, | |
| 'detected_list': detected_list, | |
| 'name_map': name_map, | |
| 'buy_markers': buy_markers, | |
| 'sell_markers': sell_markers, | |
| } | |
| # ======================================================== | |
| # 1. 老鸭头形态 | |
| # ======================================================== | |
| def _detect_old_duck_head(self, close, open_p, high, low, volume, | |
| ma5, ma10, ma20, ma60, vol_ma5, df): | |
| """ | |
| 老鸭头形态识别: | |
| - 股价经过上涨后回调,5日均线、10日均线走平或下行(鸭脖子+鸭头) | |
| - 随后放量突破前高,5日均线重新上穿10日均线(鸭嘴张开) | |
| - 60日均线保持上行作为上升趋势的支撑 | |
| """ | |
| n = len(close) | |
| if n < 60: | |
| return {'detected': False, 'description': '数据不足60根K线,无法判断老鸭头'} | |
| lookback = min(40, n - 20) | |
| result = {'detected': False, 'signal': 'neutral', 'description': '', 'strength': 0} | |
| for i in range(n - lookback, n - 5): | |
| if i < 20: | |
| continue | |
| pre_start = max(0, i - 60) | |
| pre_end = max(0, i - 20) | |
| if pre_end <= pre_start: | |
| continue | |
| region_high = np.max(high[pre_start:pre_end]) | |
| region_high_idx = pre_start + np.argmax(high[pre_start:pre_end]) | |
| if region_high_idx < pre_start + 5: | |
| continue | |
| trough_start = region_high_idx | |
| trough_end = min(n, region_high_idx + 30) | |
| if trough_end - trough_start < 10: | |
| continue | |
| trough_min = np.min(low[trough_start:trough_end]) | |
| trough_min_idx = trough_start + np.argmin(low[trough_start:trough_end]) | |
| pullback_pct = (region_high - trough_min) / region_high if region_high > 0 else 0 | |
| if pullback_pct < 0.03 or pullback_pct > 0.30: | |
| continue | |
| for j in range(max(0, n - 5), n): | |
| if j <= trough_min_idx: | |
| continue | |
| curr_close = close[j] | |
| curr_vol = volume[j] | |
| if curr_close > region_high: | |
| if vol_ma5[j] > 0 and curr_vol > vol_ma5[j] * 1.2: | |
| if ma60[j] > ma60[max(0, j - 5)] if ma60[j] > 0 else False: | |
| strength = min(100, 50 + pullback_pct * 200 + (curr_vol / vol_ma5[j] - 1) * 30) | |
| result = { | |
| 'detected': True, | |
| 'signal': 'bullish', | |
| 'description': f'老鸭头形态:前高{region_high:.2f}回调{pullback_pct*100:.1f}%后放量突破至{curr_close:.2f},60日均线保持上行', | |
| 'strength': round(min(100, strength), 1), | |
| 'score': round(min(100, strength), 1), | |
| 'prev_high': round(region_high, 2), | |
| 'pullback_pct': round(pullback_pct * 100, 1), | |
| 'breakout_price': round(curr_close, 2), | |
| } | |
| return result | |
| result['description'] = '未检测到老鸭头形态(需满足:前期上涨-回调-放量突破)' | |
| return result | |
| # ======================================================== | |
| # 2. 红三兵形态 | |
| # ======================================================== | |
| def _detect_three_white_soldiers(self, close, open_p, high, low, volume, vol_ma5, df): | |
| """ | |
| 红三兵形态识别(标准 + 变体): | |
| - 连续3根阳线(收盘>开盘) | |
| - 每根阳线的开盘价在前一根实体之内或之上 | |
| - 收盘价逐步抬高 | |
| - 可选:伴随温和放量 | |
| """ | |
| n = len(close) | |
| result = {'detected': False, 'signal': 'neutral', 'description': '', 'strength': 0} | |
| for i in range(n - 3, n - 1): | |
| if i < 2: | |
| continue | |
| c0, c1, c2 = close[i-2], close[i-1], close[i] | |
| o0, o1, o2 = open_p[i-2], open_p[i-1], open_p[i] | |
| v0, v1, v2 = volume[i-2], volume[i-1], volume[i] | |
| if not (c0 > o0 and c1 > o1 and c2 > o2): | |
| continue | |
| if not (c2 > c1 > c0): | |
| continue | |
| body_low_0 = min(o0, c0) | |
| body_low_1 = min(o1, c1) | |
| if not (o1 >= body_low_0): | |
| continue | |
| if not (o2 >= body_low_1): | |
| continue | |
| def upper_shadow(o, c, h): | |
| body_top = max(o, c) | |
| return (h - body_top) / (body_top - min(o, c)) if (body_top - min(o, c)) > 0 else 0 | |
| if upper_shadow(o0, c0, high[i-2]) > 0.6: | |
| continue | |
| if upper_shadow(o1, c1, high[i-1]) > 0.6: | |
| continue | |
| if upper_shadow(o2, c2, high[i]) > 0.6: | |
| continue | |
| total_gain = (c2 - o0) / o0 * 100 if o0 > 0 else 0 | |
| vol_increasing = v2 > v1 > v0 | |
| vol_ratio = v2 / vol_ma5[i] if vol_ma5[i] > 0 else 1 | |
| strength = min(100, 40 + total_gain * 8 + (20 if vol_increasing else 0) + min(20, (vol_ratio - 1) * 30)) | |
| desc = f'红三兵形态:连续3根阳线,累计涨幅{total_gain:.1f}%' | |
| if vol_increasing: | |
| desc += ',温和放量确认' | |
| result = { | |
| 'detected': True, | |
| 'signal': 'bullish', | |
| 'description': desc, | |
| 'strength': round(min(100, strength), 1), | |
| 'score': round(min(100, strength), 1), | |
| 'total_gain_pct': round(total_gain, 2), | |
| 'vol_increasing': vol_increasing, | |
| } | |
| return result | |
| result['description'] = '近3日未出现标准红三兵形态' | |
| return result | |
| # ======================================================== | |
| # 3. 仙人指路形态 | |
| # ======================================================== | |
| def _detect_immortal_pointing(self, close, open_p, high, low, volume, vol_ma5, df): | |
| """ | |
| 仙人指路形态识别: | |
| - 前期处于上升趋势 | |
| - 出现一根长上影线K线(上影线长度 > 实体2倍,且 > 下影线2倍) | |
| - 当日收阳或小阴(实体较小,说明上方抛压被消化) | |
| - 随后1~3天内股价突破该上影线高点 | |
| """ | |
| n = len(close) | |
| result = {'detected': False, 'signal': 'neutral', 'description': '', 'strength': 0} | |
| for i in range(n - 4, n - 1): | |
| if i < 10: | |
| continue | |
| o = open_p[i] | |
| c = close[i] | |
| h = high[i] | |
| l = low[i] | |
| body = abs(c - o) | |
| upper_shadow = h - max(o, c) | |
| lower_shadow = min(o, c) - l | |
| if upper_shadow < body * 1.5: | |
| continue | |
| if lower_shadow > upper_shadow * 0.5: | |
| continue | |
| if body > upper_shadow * 0.8: | |
| continue | |
| pre_10_close = close[max(0, i - 10)] | |
| if pre_10_close <= 0 or c / pre_10_close < 1.0: | |
| continue | |
| for j in range(i + 1, min(n, i + 4)): | |
| if close[j] > h: | |
| gain = (close[j] - c) / c * 100 if c > 0 else 0 | |
| strength = min(100, 50 + (upper_shadow / body - 1.5) * 30 + gain * 3) | |
| result = { | |
| 'detected': True, | |
| 'signal': 'bullish', | |
| 'description': f'仙人指路:长上影线试盘后突破影线高点{h:.2f},当前{close[j]:.2f}', | |
| 'strength': round(min(100, strength), 1), | |
| 'score': round(min(100, strength), 1), | |
| 'shadow_high': round(h, 2), | |
| 'breakout_price': round(close[j], 2), | |
| } | |
| return result | |
| if i == n - 2 or i == n - 1: | |
| result = { | |
| 'detected': True, | |
| 'signal': 'bullish', | |
| 'description': f'仙人指路(待确认):出现长上影线试探{h:.2f},等待突破确认', | |
| 'strength': round(45, 1), | |
| 'score': round(45, 1), | |
| 'shadow_high': round(h, 2), | |
| 'pending': True, | |
| } | |
| return result | |
| result['description'] = '近5日未检测到仙人指路形态' | |
| return result | |
| # ======================================================== | |
| # 4. 倍量柱形态 | |
| # ======================================================== | |
| def _detect_double_volume(self, close, open_p, high, low, volume, vol_ma5, vol_ma20, df): | |
| """ | |
| 倍量柱形态识别(参考《量柱擒涨停》第10讲): | |
| - 当日成交量 >= 前一日成交量的1.9倍(接近或超过2倍) | |
| - 当日为阳线(收盘>开盘),表明主力资金大幅流入 | |
| - 倍量柱出现后若缩量不破倍量柱最低价,则是黄金柱的前身 | |
| """ | |
| n = len(close) | |
| result = {'detected': False, 'signal': 'neutral', 'description': '', 'strength': 0} | |
| # Scan all data, collect all double_volume signals | |
| dv_signals = [] | |
| for i in range(1, n): | |
| prev_vol = volume[i - 1] | |
| curr_vol = volume[i] | |
| curr_close = close[i] | |
| curr_open = open_p[i] | |
| curr_low = low[i] | |
| if prev_vol <= 0: | |
| continue | |
| vol_ratio = curr_vol / prev_vol | |
| if vol_ratio < 1.9: | |
| continue | |
| is_bull = curr_close > curr_open | |
| price_change_pct = (curr_close - curr_open) / curr_open * 100 if curr_open > 0 else 0 | |
| protected = True | |
| for j in range(i + 1, min(n, i + 10)): | |
| if low[j] < curr_low: | |
| protected = False | |
| break | |
| signal = 'bullish' if is_bull else 'neutral' | |
| strength = min(100, 40 + (vol_ratio - 1.9) * 30 + (20 if is_bull else 0) + (20 if protected else 0)) | |
| dv_signals.append({ | |
| 'detected': True, | |
| 'signal': signal, | |
| 'description': f'倍量柱:成交量是前一日的{vol_ratio:.1f}倍' + (f',阳线涨幅{price_change_pct:.1f}%' if is_bull else ',阴线需警惕') + (',回调未跌破倍量柱最低价(黄金柱潜力)' if protected else ''), | |
| 'strength': round(min(100, strength), 1), | |
| 'score': round(min(100, strength), 1), | |
| 'vol_ratio': round(vol_ratio, 2), | |
| 'price_change_pct': round(price_change_pct, 2), | |
| 'protected': protected, | |
| 'index': i, | |
| 'date': str(df.index[i])[:10] if hasattr(df.index[i], '__format__') else str(df.index[i])[:10], | |
| 'price': round(float(curr_close), 2), | |
| }) | |
| if dv_signals: | |
| # Return the most recent signal as the primary result | |
| result = dv_signals[-1] | |
| result['_all_signals'] = dv_signals # Pass all for chart markers | |
| return result | |
| result['description'] = '未出现倍量柱(需成交量 >= 前日1.9倍)' | |
| return result | |
| # ======================================================== | |
| # 4b. 波段高位卖点检测(放量+高换手+浪型顶点) | |
| # ======================================================== | |
| def _detect_distribution_top(self, close, open_p, high, low, volume, vol_ma5, vol_ma20, df): | |
| """ | |
| 波段高位卖点识别: | |
| - 成交量显著放大(超过20日均量2倍以上) | |
| - 价格处于近期高点(近20日最高价的95%以上) | |
| - 高换手特征(成交量/流通股本 proxies) | |
| - 配合浪型顶点判断(接近上升浪末端) | |
| """ | |
| n = len(close) | |
| result = {'detected': False, 'signal': 'neutral', 'description': '', 'strength': 0} | |
| if n < 30: | |
| return result | |
| # Scan all data for distribution top signals | |
| dt_signals = [] | |
| for i in range(20, n): | |
| curr_vol = volume[i] | |
| curr_close = close[i] | |
| curr_high = high[i] | |
| curr_open = open_p[i] | |
| # 条件1: 成交量放大(超过20日均量2倍) | |
| if vol_ma20[i] <= 0: | |
| continue | |
| vol_ratio_20 = curr_vol / vol_ma20[i] | |
| if vol_ratio_20 < 2.0: | |
| continue | |
| # 条件2: 价格处于近20日高点区域 | |
| window_high = max(high[max(0, i-19):i+1]) | |
| price_ratio = curr_high / window_high if window_high > 0 else 0 | |
| if price_ratio < 0.95: | |
| continue | |
| # 条件3: 当日或近3日有冲高回落特征 | |
| # (最高价远高于收盘价,上影线较长) | |
| body_top = max(curr_close, curr_open) | |
| upper_shadow = curr_high - body_top | |
| body_size = abs(curr_close - curr_open) | |
| # 上影线大于实体的0.5倍视为有回落特征 | |
| has_reversal = (body_size > 0 and upper_shadow > body_size * 0.5) or (curr_close < curr_open) | |
| # 条件4: 近期有放量滞涨特征(价涨但量更大) | |
| if i >= 2: | |
| recent_vol_avg = (volume[i-2] + volume[i-1] + curr_vol) / 3 | |
| recent_price_change = (close[i] - close[i-3]) / close[i-3] * 100 if close[i-3] > 0 else 0 | |
| # 放量但涨幅不大(滞涨) | |
| stagflation = vol_ratio_20 >= 2.5 and abs(recent_price_change) < 5 | |
| else: | |
| stagflation = False | |
| # 综合评分 | |
| if has_reversal or stagflation: | |
| strength = min(100, 50 + (vol_ratio_20 - 2.0) * 15 + (20 if has_reversal else 0) + (15 if stagflation else 0)) | |
| signal = 'bearish' | |
| else: | |
| strength = min(100, 35 + (vol_ratio_20 - 2.0) * 10) | |
| signal = 'bearish' | |
| label = '放量高位' | |
| desc = f'波段高位:成交量是20日均量的{vol_ratio_20:.1f}倍' | |
| if has_reversal: | |
| desc += ',冲高回落上影线较长' | |
| label = '放量回落' | |
| if stagflation: | |
| desc += ',放量滞涨需警惕' | |
| label = '放量滞涨' | |
| dt_signals.append({ | |
| 'detected': True, | |
| 'signal': signal, | |
| 'description': desc, | |
| 'strength': round(min(100, strength), 1), | |
| 'score': round(min(100, strength), 1), | |
| 'vol_ratio': round(vol_ratio_20, 2), | |
| 'index': i, | |
| 'date': str(df.index[i])[:10] if hasattr(df.index[i], '__format__') else str(df.index[i])[:10], | |
| 'price': round(float(curr_close), 2), | |
| 'label': label, | |
| }) | |
| if dt_signals: | |
| result = dt_signals[-1] | |
| result['_all_signals'] = dt_signals | |
| return result | |
| result['description'] = '未出现波段高位放量信号' | |
| return result | |
| # ======================================================== | |
| # 5. 梯量柱形态 | |
| # ======================================================== | |
| def _detect_ladder_volume(self, close, open_p, high, low, volume, vol_ma5, df): | |
| """ | |
| 梯量柱形态识别(参考《量柱擒涨停》第11讲): | |
| - 连续3~5天成交量逐步放大(阶梯式递增) | |
| - 价格同步上涨或维持 | |
| - "盯三防四":关注第3根梯量柱,防范第4根变化 | |
| """ | |
| n = len(close) | |
| result = {'detected': False, 'signal': 'neutral', 'description': '', 'strength': 0} | |
| for start in range(max(0, n - 15), n - 3): | |
| count = 0 | |
| for k in range(start, min(n, start + 6)): | |
| if k > 0 and volume[k] > volume[k - 1]: | |
| count += 1 | |
| else: | |
| break | |
| if count < 3: | |
| continue | |
| price_up_count = 0 | |
| for k in range(start, start + count): | |
| if close[k] > close[k - 1] if k > 0 else True: | |
| price_up_count += 1 | |
| end_idx = start + count - 1 | |
| total_vol_increase = volume[end_idx] / volume[start] if volume[start] > 0 else 1 | |
| total_price_change = (close[end_idx] - close[start]) / close[start] * 100 if close[start] > 0 else 0 | |
| after_ladder = '' | |
| signal = 'neutral' | |
| strength = 40 | |
| if end_idx + 1 < n: | |
| if end_idx + 1 < n and volume[end_idx + 1] > volume[end_idx]: | |
| signal = 'bullish' | |
| strength = 75 | |
| after_ladder = ',梯量第四柱继续放量(变态行为,偏多)' | |
| elif close[end_idx + 1] >= close[end_idx]: | |
| signal = 'bullish' | |
| strength = 65 | |
| after_ladder = ',缩量但价格维持(主力控盘良好)' | |
| else: | |
| signal = 'neutral' | |
| strength = 40 | |
| after_ladder = ',梯量后回落(正常衰竭)' | |
| else: | |
| after_ladder = ',梯量进行中' | |
| price_sync = price_up_count / count * 100 | |
| if price_sync >= 70: | |
| signal = 'bullish' if signal != 'bearish' else signal | |
| strength += 10 | |
| desc = f'梯量柱:连续{count}天放量递增({total_vol_increase:.1f}倍),价格变化{total_price_change:.1f}%{after_ladder}' | |
| result = { | |
| 'detected': True, | |
| 'signal': signal, | |
| 'description': desc, | |
| 'strength': round(min(100, strength), 1), | |
| 'score': round(min(100, strength), 1), | |
| 'consecutive_days': count, | |
| 'vol_increase_ratio': round(total_vol_increase, 2), | |
| 'price_change_pct': round(total_price_change, 2), | |
| } | |
| return result | |
| result['description'] = '近15日未出现梯量柱形态(需连续3天以上成交量递增)' | |
| return result | |
| # ======================================================== | |
| # 6. 均线金叉 | |
| # ======================================================== | |
| def _detect_golden_cross(self, close, ma5, ma10, ma20, ma60, df): | |
| """ | |
| 均线金叉识别: | |
| - 5日上穿10日(短期金叉) | |
| - 10日上穿20日(中期金叉) | |
| - 20日上穿60日(长期金叉) | |
| """ | |
| n = len(close) | |
| result = {'detected': False, 'signal': 'neutral', 'description': '', 'strength': 0} | |
| crosses = [] | |
| # Scan all data, collect all double_volume signals | |
| dv_signals = [] | |
| for i in range(1, n): | |
| if ma5[i] > 0 and ma10[i] > 0: | |
| if ma5[i - 1] <= ma10[i - 1] and ma5[i] > ma10[i]: | |
| crosses.append(('MA5上穿MA10', i, 'short')) | |
| if ma10[i] > 0 and ma20[i] > 0: | |
| if ma10[i - 1] <= ma20[i - 1] and ma10[i] > ma20[i]: | |
| crosses.append(('MA10上穿MA20', i, 'mid')) | |
| if ma20[i] > 0 and ma60[i] > 0: | |
| if ma20[i - 1] <= ma60[i - 1] and ma20[i] > ma60[i]: | |
| crosses.append(('MA20上穿MA60', i, 'long')) | |
| if crosses: | |
| latest = crosses[-1] | |
| strength_map = {'short': 50, 'mid': 65, 'long': 80} | |
| strength = strength_map.get(latest[2], 50) | |
| if len(crosses) >= 2: | |
| strength = min(95, strength + 15) | |
| cross_names = '、'.join(c[0] for c in crosses) | |
| desc = f'均线金叉:{cross_names}' | |
| result = { | |
| 'detected': True, | |
| 'signal': 'bullish', | |
| 'description': desc, | |
| 'strength': round(min(100, strength), 1), | |
| 'score': round(min(100, strength), 1), | |
| 'crosses': [{'name': c[0], 'type': c[2]} for c in crosses], | |
| } | |
| return result | |
| if n >= 2 and ma5[n-1] > 0 and ma10[n-1] > 0: | |
| if ma5[n-1] > ma10[n-1] and ma10[n-1] > ma20[n-1] if ma20[n-1] > 0 else True: | |
| result['description'] = '均线多头排列中(未出现新金叉)' | |
| else: | |
| result['description'] = '近10日未出现均线金叉' | |
| else: | |
| result['description'] = '数据不足,无法判断均线金叉' | |
| return result | |
| def _empty_result(self, reason): | |
| return { | |
| 'patterns': {}, | |
| 'summary': reason, | |
| 'score': 0, | |
| 'overall_signal': 'none', | |
| 'detected_list': [], | |
| 'name_map': {}, | |
| } | |
| def detect_classic_patterns(df): | |
| """ | |
| 便捷入口:传入 DataFrame,返回形态检测结果 | |
| """ | |
| detector = ClassicPatternDetector() | |
| return detector.detect_all(df) | |