| |
|
|
| import pandas as pd |
| import numpy as np |
|
|
| def detect_candlestick_patterns(df): |
| """ |
| 检测 K 线数值特征与 22 种 K 线及形态模式: |
| - Feature Columns: |
| 'range', 'body', 'body_ratio', 'upper_shadow', 'lower_shadow', |
| 'upper_ratio', 'lower_ratio', 'gap_up', 'gap_down', |
| 'trend_up_ctx', 'trend_down_ctx', 'rv' |
| |
| - Pattern Columns: |
| 'Pattern_Long_Bull', 'Pattern_Long_Bear', 'Pattern_Doji', 'Pattern_Hammer', |
| 'Pattern_Hanging_Man', 'Pattern_Inverted_Hammer', 'Pattern_Shooting_Star', |
| 'Pattern_Marubozu', 'Pattern_Spinning_Top', 'Pattern_Bullish_Engulfing', |
| 'Pattern_Bearish_Engulfing', 'Pattern_Piercing', 'Pattern_Dark_Cloud_Cover', |
| 'Pattern_Harami', 'Pattern_Morning_Star', 'Pattern_Evening_Star', |
| 'Pattern_Three_White_Soldiers', 'Pattern_Three_Black_Crows', |
| 'Pattern_Rising_Three_Methods', 'Pattern_Falling_Three_Methods', |
| 'Pattern_Gap_Breakout', 'Pattern_Exhaustion_Gap' |
| """ |
| df = df.copy() |
| |
| |
| if len(df) < 5: |
| |
| for col in [ |
| 'range', 'body', 'body_ratio', 'upper_shadow', 'lower_shadow', |
| 'upper_ratio', 'lower_ratio', 'gap_up', 'gap_down', |
| 'trend_up_ctx', 'trend_down_ctx', 'rv', |
| 'Pattern_Long_Bull', 'Pattern_Long_Bear', 'Pattern_Doji', 'Pattern_Hammer', |
| 'Pattern_Hanging_Man', 'Pattern_Inverted_Hammer', 'Pattern_Shooting_Star', |
| 'Pattern_Marubozu', 'Pattern_Spinning_Top', 'Pattern_Bullish_Engulfing', |
| 'Pattern_Bearish_Engulfing', 'Pattern_Piercing', 'Pattern_Dark_Cloud_Cover', |
| 'Pattern_Harami', 'Pattern_Morning_Star', 'Pattern_Evening_Star', |
| 'Pattern_Three_White_Soldiers', 'Pattern_Three_Black_Crows', |
| 'Pattern_Rising_Three_Methods', 'Pattern_Falling_Three_Methods', |
| 'Pattern_Gap_Breakout', 'Pattern_Exhaustion_Gap' |
| ]: |
| df[col] = False |
| return df |
|
|
| o = df['Open'] |
| h = df['High'] |
| l = df['Low'] |
| c = df['Close'] |
| v = df['Volume'] |
| |
| eps = 1e-8 |
| |
| |
| df['range'] = h - l |
| df['body'] = (c - o).abs() |
| df['body_ratio'] = df['body'] / np.maximum(df['range'], eps) |
| df['upper_shadow'] = h - np.maximum(o, c) |
| df['lower_shadow'] = np.minimum(o, c) - l |
| df['upper_ratio'] = df['upper_shadow'] / np.maximum(df['body'], eps) |
| df['lower_ratio'] = df['lower_shadow'] / np.maximum(df['body'], eps) |
| df['gap_up'] = l > h.shift(1) |
| df['gap_down'] = h < l.shift(1) |
| |
| |
| ema20 = c.ewm(span=20, adjust=False).mean() |
| ema50 = c.ewm(span=50, adjust=False).mean() |
| |
| |
| slope_ema20 = (2 * ema20 + ema20.shift(1) - ema20.shift(3) - 2 * ema20.shift(4)) / 10.0 |
| |
| df['trend_up_ctx'] = (ema20 > ema50) & (slope_ema20 > 0) |
| df['trend_down_ctx'] = (ema20 < ema50) & (slope_ema20 < 0) |
| |
| |
| df['rv'] = v / np.maximum(v.rolling(window=20, min_periods=1).mean(), eps) |
|
|
| |
| bull = c > o |
| bear = c < o |
| body_ratio = df['body_ratio'] |
| upper_ratio = df['upper_ratio'] |
| lower_ratio = df['lower_ratio'] |
| trend_up_ctx = df['trend_up_ctx'] |
| trend_down_ctx = df['trend_down_ctx'] |
| gap_up = df['gap_up'] |
| gap_down = df['gap_down'] |
| rv = df['rv'] |
| |
| |
| df['Pattern_Long_Bull'] = bull & (body_ratio >= 0.6) & (upper_ratio <= 0.3) & (lower_ratio <= 0.3) |
| df['Pattern_Long_Bear'] = bear & (body_ratio >= 0.6) & (upper_ratio <= 0.3) & (lower_ratio <= 0.3) |
| df['Pattern_Doji'] = body_ratio <= 0.1 |
| df['Pattern_Hammer'] = (lower_ratio >= 2.0) & (upper_ratio <= 0.5) & (body_ratio <= 0.35) & trend_down_ctx |
| df['Pattern_Hanging_Man'] = (lower_ratio >= 2.0) & (upper_ratio <= 0.5) & (body_ratio <= 0.35) & trend_up_ctx |
| df['Pattern_Inverted_Hammer'] = (upper_ratio >= 2.0) & (lower_ratio <= 0.5) & (body_ratio <= 0.35) & trend_down_ctx |
| df['Pattern_Shooting_Star'] = (upper_ratio >= 2.0) & (lower_ratio <= 0.5) & (body_ratio <= 0.35) & trend_up_ctx |
| df['Pattern_Marubozu'] = (body_ratio >= 0.8) & (upper_ratio <= 0.1) & (lower_ratio <= 0.1) |
| df['Pattern_Spinning_Top'] = (body_ratio > 0.1) & (body_ratio < 0.35) & (upper_ratio > 0.5) & (lower_ratio > 0.5) |
| |
| |
| prev_close = c.shift(1) |
| prev_open = o.shift(1) |
| |
| df['Pattern_Bullish_Engulfing'] = (prev_close < prev_open) & bull & (o <= prev_close) & (c >= prev_open) |
| df['Pattern_Bearish_Engulfing'] = (prev_close > prev_open) & bear & (o >= prev_close) & (c <= prev_open) |
| |
| prev_mid = (prev_open + prev_close) / 2 |
| df['Pattern_Piercing'] = (prev_close < prev_open) & bull & (o < prev_close) & (c >= prev_mid) & (c < prev_open) |
| df['Pattern_Dark_Cloud_Cover'] = (prev_close > prev_open) & bear & (o > prev_close) & (c <= prev_mid) & (c > prev_open) |
| |
| curr_body_high = np.maximum(o, c) |
| curr_body_low = np.minimum(o, c) |
| prev_body_high = np.maximum(prev_open, prev_close) |
| prev_body_low = np.minimum(prev_open, prev_close) |
| df['Pattern_Harami'] = (curr_body_low >= prev_body_low) & (curr_body_high <= prev_body_high) |
| |
| |
| |
| b1_open, b1_close = o.shift(2), c.shift(2) |
| b1_body = df['body'].shift(2) |
| b1_range = df['range'].shift(2) |
| b1_body_ratio = b1_body / np.maximum(b1_range, eps) |
| b1_bear = b1_close < b1_open |
| b1_bull = b1_close > b1_open |
| |
| b2_open, b2_close = o.shift(1), c.shift(1) |
| b2_body = df['body'].shift(1) |
| b2_range = df['range'].shift(1) |
| b2_body_ratio = b2_body / np.maximum(b2_range, eps) |
| |
| |
| first_big_down = b1_bear & (b1_body_ratio >= 0.6) |
| second_small = b2_body_ratio <= 0.25 |
| third_big_up = bull & (body_ratio >= 0.6) |
| reclaim = c >= (b1_open + b1_close) / 2 |
| df['Pattern_Morning_Star'] = first_big_down & second_small & third_big_up & reclaim |
| |
| |
| first_big_up = b1_bull & (b1_body_ratio >= 0.6) |
| third_big_down = bear & (body_ratio >= 0.6) |
| decline = c <= (b1_open + b1_close) / 2 |
| df['Pattern_Evening_Star'] = first_big_up & second_small & third_big_down & decline |
| |
| |
| df['Pattern_Three_White_Soldiers'] = ( |
| b1_bull & (b2_close > b2_open) & bull & |
| (c > b2_close) & (b2_close > b1_close) & |
| (b2_open >= b1_open) & (b2_open <= b1_close) & |
| (o >= b2_open) & (o <= b2_close) |
| ) |
| |
| |
| df['Pattern_Three_Black_Crows'] = ( |
| b1_bear & (b2_close < b2_open) & bear & |
| (c < b2_close) & (b2_close < b1_close) & |
| (b2_open <= b1_open) & (b2_open >= b1_close) & |
| (o <= b2_open) & (o >= b2_close) |
| ) |
| |
| |
| c4, o4, h4, l4 = c.shift(4), o.shift(4), h.shift(4), l.shift(4) |
| c3, o3, h3, l3 = c.shift(3), o.shift(3), h.shift(3), l.shift(3) |
| c2, o2, h2, l2 = c.shift(2), o.shift(2), h.shift(2), l.shift(2) |
| c1, o1, h1, l1 = c.shift(1), o.shift(1), h.shift(1), l.shift(1) |
| |
| |
| day1_up = (c4 > o4) & ((c4 - o4) / np.maximum(h4 - l4, eps) >= 0.5) |
| day234_inside_up = ( |
| (l3 > l4) & (h3 < h4) & (abs(c3-o3) < (c4-o4)*0.5) & |
| (l2 > l4) & (h2 < h4) & (abs(c2-o2) < (c4-o4)*0.5) & |
| (l1 > l4) & (h1 < h4) & (abs(c1-o1) < (c4-o4)*0.5) |
| ) |
| day5_up = (c > o) & (c > c4) & ((c - o) / np.maximum(h - l, eps) >= 0.5) |
| df['Pattern_Rising_Three_Methods'] = day1_up & day234_inside_up & day5_up |
| |
| |
| day1_dn = (c4 < o4) & ((o4 - c4) / np.maximum(h4 - l4, eps) >= 0.5) |
| day234_inside_dn = ( |
| (l3 > l4) & (h3 < h4) & (abs(c3-o3) < (o4-c4)*0.5) & |
| (l2 > l4) & (h2 < h4) & (abs(c2-o2) < (o4-c4)*0.5) & |
| (l1 > l4) & (h1 < h4) & (abs(c1-o1) < (o4-c4)*0.5) |
| ) |
| day5_dn = (c < o) & (c < c4) & ((o - c) / np.maximum(h - l, eps) >= 0.5) |
| df['Pattern_Falling_Three_Methods'] = day1_dn & day234_inside_dn & day5_dn |
| |
| |
| df['Pattern_Gap_Breakout'] = gap_up & (rv >= 1.5) & trend_up_ctx |
| df['Pattern_Exhaustion_Gap'] = (gap_up & (rv >= 1.5) & bear) | (gap_down & (rv >= 1.5) & bull) |
|
|
| return df |
|
|
| def detect_double_tops_bottoms(df, window=5, threshold=0.015): |
| """ |
| 检测 M顶 (Double Top) 与 W底 (Double Bottom) |
| |
| 参数: |
| - window: 局部最高/最低点寻找的窗口宽度 |
| - threshold: 两个波峰/波谷价格差异的百分比上限 (默认 1.5%) |
| |
| 返回: |
| - 'Pattern_M_Top': bool (在颈线跌破点标记为 True) |
| - 'Pattern_W_Bottom': bool (在颈线突破点标记为 True) |
| - 'M_Neckline': float (记录颈线价格,没有则为 NaN) |
| - 'W_Neckline': float (记录颈线价格,没有则为 NaN) |
| """ |
| df = df.copy() |
| df['Pattern_M_Top'] = False |
| df['Pattern_W_Bottom'] = False |
| df['M_Neckline'] = np.nan |
| df['W_Neckline'] = np.nan |
| |
| if len(df) < window * 4: |
| return df |
| |
| close = df['Close'].values |
| high = df['High'].values |
| low = df['Low'].values |
| |
| |
| peaks = [] |
| troughs = [] |
| |
| for i in range(window, len(df) - window): |
| |
| is_peak = True |
| for w in range(1, window + 1): |
| if high[i] < high[i-w] or high[i] < high[i+w]: |
| is_peak = False |
| break |
| if is_peak: |
| peaks.append((i, float(high[i]))) |
| |
| |
| is_trough = True |
| for w in range(1, window + 1): |
| if low[i] > low[i-w] or low[i] > low[i+w]: |
| is_trough = False |
| break |
| if is_trough: |
| troughs.append((i, float(low[i]))) |
|
|
| |
| |
| |
| for p_idx in range(len(peaks) - 1): |
| t1, p1 = peaks[p_idx] |
| t2, p2 = peaks[p_idx + 1] |
| |
| |
| if abs(p1 - p2) / max(p1, p2) <= threshold: |
| |
| t_mid_candidates = [t for t in troughs if t1 < t[0] < t2] |
| if t_mid_candidates: |
| |
| t_neck, p_neck = min(t_mid_candidates, key=lambda x: x[1]) |
| |
| |
| |
| for i in range(t2, len(df)): |
| |
| limit_high = max(p1, p2) * 1.01 |
| if close[i] > limit_high: |
| break |
| |
| if close[i] < p_neck: |
| |
| df.iloc[i, df.columns.get_loc('Pattern_M_Top')] = True |
| df.iloc[i, df.columns.get_loc('M_Neckline')] = p_neck |
| break |
|
|
| |
| |
| |
| for t_idx in range(len(troughs) - 1): |
| t1, tr1 = troughs[t_idx] |
| t2, tr2 = troughs[t_idx + 1] |
| |
| |
| if abs(tr1 - tr2) / max(tr1, tr2) <= threshold: |
| |
| p_mid_candidates = [p for p in peaks if t1 < p[0] < t2] |
| if p_mid_candidates: |
| t_neck, p_neck = max(p_mid_candidates, key=lambda x: x[1]) |
| |
| |
| for i in range(t2, len(df)): |
| |
| limit_low = min(tr1, tr2) * 0.99 |
| if close[i] < limit_low: |
| break |
| |
| if close[i] > p_neck: |
| df.iloc[i, df.columns.get_loc('Pattern_W_Bottom')] = True |
| df.iloc[i, df.columns.get_loc('W_Neckline')] = p_neck |
| break |
| |
| return df |
|
|
| def analyze_patterns(df): |
| """ |
| 一键运行所有形态检测,合并返回 |
| """ |
| df = detect_candlestick_patterns(df) |
| df = detect_double_tops_bottoms(df) |
| return df |
|
|
| if __name__ == "__main__": |
| print("Testing patterns.py...") |
| |
| dates = pd.date_range("2026-06-01", periods=10) |
| data = { |
| "Open": [100.0, 102.0, 101.0, 105.0, 104.0, 106.0, 107.0, 105.0, 108.0, 110.0], |
| "High": [103.0, 104.0, 102.0, 106.0, 105.0, 107.0, 108.0, 106.0, 109.0, 112.0], |
| "Low": [99.0, 101.0, 100.0, 104.0, 103.0, 105.0, 106.0, 104.0, 107.0, 109.0], |
| "Close": [102.0, 101.5, 101.8, 104.5, 104.2, 106.8, 105.5, 104.8, 108.5, 111.0], |
| "Volume": [1000, 1500, 1200, 2000, 1100, 1300, 900, 1400, 2200, 3000] |
| } |
| df_test = pd.DataFrame(data, index=dates) |
| res = analyze_patterns(df_test) |
| print("Columns in result:", res.columns.tolist()) |
| print("\nFirst row features:\n", res.iloc[-1]) |
|
|