quant-ai / backend /app /patterns.py
Ypeng12's picture
Implement indicators, Regime Router, Risk Sizing, and Walk-Forward Engine
b2555ee
Raw
History Blame Contribute Delete
13.7 kB
# backend/app/patterns.py
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
# 1. 计算基础数值特征
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)
# 计算趋势上下文 (20日/分钟 EMA vs 50日/分钟 EMA)
ema20 = c.ewm(span=20, adjust=False).mean()
ema50 = c.ewm(span=50, adjust=False).mean()
# slope(ema20, 5) using 5-period linear regression slope
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)
# rv (Relative Volume)
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']
# 2. 单K线与常规形态识别
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)
# 3. 双K线形态识别
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)
# 4. 三日及多日形态识别
# shift values for Day 1 (t-2) and Day 2 (t-1)
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)
)
# 上升三法 / 下降三法 (5日模式)
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
# 5. 缺口与突破模式
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
# 1. 寻找局部波峰(Peaks)与波谷(Troughs)
peaks = [] # 元素格式: (index, price)
troughs = [] # 元素格式: (index, price)
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])))
# 2. 识别 M 顶 (双顶)
# 两峰 P1(t1) 与 P2(t2),中间夹着一谷 T(t_mid)
# 颈线为 T 的最低价。当价格跌破颈线时,确认 M 顶。
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])
# 寻找 t2 之后,价格首次跌破 p_neck 的时刻
# 为防止在历史中过早标出,我们检查 t2 之后的收盘价
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:
# 只在跌破的那一分钟标记 True
df.iloc[i, df.columns.get_loc('Pattern_M_Top')] = True
df.iloc[i, df.columns.get_loc('M_Neckline')] = p_neck
break
# 3. 识别 W 底 (双底)
# 两谷 T1(t1) 与 T2(t2),中间夹着一峰 P(t_mid)
# 颈线为 P 的最高价。当价格突破颈线时,确认 W 底。
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])
# 寻找 t2 之后,价格首次突破 p_neck 的时刻
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...")
# Create a dummy dataframe with OHLCV data
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])