ad3590a2-2133-43ee-8ecd-d64a32760224 # 技术形态识别调研:对称三角形(含三角形家族)如何用 Python 实现 > 目标:把“图上能看出来的三角形”落地为**可计算、可回测、可解释**的规则与代码框架。本文以**对称三角形**为主,同时覆盖**上升三角形/下降三角形**的差异化判定。 --- ## 1. 输入数据与基本约定 ### 1.1 数据字段(最低要求) - OHLC:`open/high/low/close` - 成交量:`volume`(可选但强烈建议,用于形态可靠性/突破确认) - 时间索引:`datetime` 或顺序索引(实现里用 `bar_index = 0..n-1`) ### 1.2 周期选择(建议) - 日线:最常用,适合识别 1~6 个月级别三角形。 - 周线:更稳定,噪声更低,触碰次数更少但信号更“硬”。 ### 1.3 你在图里画的三角形是什么(术语对齐) - **对称三角形(Symmetrical Triangle)**:上边线下倾、下边线上升,区间收敛;本身不天然看涨/看跌,方向以突破为准。 - **上升三角形(Ascending Triangle,看涨更典型)**:上边线近似水平、下边线上升。 - **下降三角形(Descending Triangle,看跌更典型)**:上边线下降、下边线近似水平。 --- ## 2. 从“肉眼形态”到“可编码规则”的拆解 形态识别一般分 4 层: 1) 先找到“有意义的拐点”(枢轴点 / swing points) 2) 用拐点拟合两条趋势线(上沿/下沿) 3) 校验是否满足某类三角形的几何约束(斜率、收敛、触碰次数、包含性) 4) 识别突破与确认(价格 + 成交量 + 失败回撤) 下面给出可落地的判定口径。 ![](对称三角形-几何约束示意.svg) --- ## 3. 枢轴点(Pivot / Fractal)的提取方法 ### 3.1 为什么要用枢轴点 三角形的上沿/下沿本质是“多次触碰的压力/支撑”,用所有 K 线拟合会被噪声拖偏;用“局部极值点”更接近画线逻辑。 ### 3.2 经典实现:左右窗口分形(推荐作为 baseline) 定义窗口 `k`: - **枢轴高点**:`high[i]` 是 `[i-k, i+k]` 区间内的最大值 - **枢轴低点**:`low[i]` 是 `[i-k, i+k]` 区间内的最小值 参数建议: - 日线:`k=3~5` - 周线:`k=2~3` 注意:该方法会引入 `k` 根的“确认延迟”(右侧需要 `k` 根数据才能确认 pivot)。 --- ## 4. 趋势线拟合(上沿/下沿) ### 4.1 最简可用:线性回归/最小二乘 对窗口内的枢轴高点集合 \((x_h, y_h)\) 拟合上沿: \[ y = a_u x + b_u \] 对枢轴低点集合 \((x_l, y_l)\) 拟合下沿: \[ y = a_l x + b_l \] 实现可用: - `numpy.polyfit(x, y, deg=1)` - 或 `scipy.stats.linregress`(可选) ### 4.2 鲁棒拟合(可选增强) 真实行情会有“假突破/尖刺”,可以用: - RANSAC 回归(`sklearn.linear_model.RANSACRegressor`) 来降低离群点影响。 --- ## 5. 三角形家族的“几何判定条件” 下面所有条件都应在一个**滑动窗口**内判断(例如最近 `W=60~180` 根)。 ### 5.1 基础条件(所有三角形通用) - **拐点数量**:至少 `>=2` 个枢轴高点 + `>=2` 个枢轴低点 - **触碰有效性**:枢轴点到趋势线的垂直距离(或相对误差)足够小 - 例如:`abs(pivot_y - line_y) / line_y <= tol`,`tol` 可取 `0.3%~1.0%` - **包含性**:窗口内大多数 K 线价格在两条线之间(允许少量穿越) - **收敛性**:两线间距随时间缩小(后段宽度 < 前段宽度) ### 5.2 对称三角形(Symmetrical) - 斜率:`a_u < 0` 且 `a_l > 0` - 收敛:两线交点(apex)在当前窗口右侧不远处(避免“几乎平行”) - 计算 apex:解 \(a_u x + b_u = a_l x + b_l\) - \(x_{apex} = (b_l - b_u)/(a_u - a_l)\) - 约束:`x_apex` 在窗口右侧的合理范围内,例如 `end < x_apex < end + 2W` - 宽度缩小:`(upper(end) - lower(end)) / (upper(start) - lower(start)) <= shrink_ratio` - `shrink_ratio` 可取 `0.3~0.7` ### 5.3 上升三角形(Ascending,典型看涨) - 上沿近似水平:`abs(a_u) <= slope_eps` - 下沿上升:`a_l > 0` ### 5.4 下降三角形(Descending,典型看跌) - 上沿下降:`a_u < 0` - 下沿近似水平:`abs(a_l) <= slope_eps` 参数建议: - `slope_eps` 需要与价格尺度无关,建议在归一化 \(x \in [0,1]\) 之后拟合(或把斜率换算成“每根 K 的百分比变化”)。 --- ## 6. 成交量与突破确认(把“形态”变成“信号”) ### 6.1 成交量收缩(形态期) 常见经验:收敛阶段成交量逐步下降。可做一个弱约束: - `mean(volume[first_third]) > mean(volume[last_third])` 或 - `volume` 的线性回归斜率为负(噪声大,建议弱约束) ### 6.2 向上突破(看涨确认) 对称三角形/上升三角形的看涨信号可定义为: - `close[t] > upper_line[t] * (1 + break_tol)` - 且成交量:`volume[t] > SMA(volume, n)[t] * vol_k` - 可选:`close` 连续 1~2 根站在线上方(避免一根假突破) 建议: - `break_tol`:`0.2%~0.8%` 或用 `ATR` 的一部分(更稳) - `vol_k`:`1.2~2.0` ### 6.3 失败回撤(False Breakout)处理 突破后若在 `m` 根内收回形态内部(`close < upper_line`),可标记为失败突破或降低评分。 ![](对称三角形-突破与回撤示意.svg) --- ## 7. 量度目标(可解释输出) 经典量度: - 形态初期高度:`H = upper(start) - lower(start)` - 向上突破目标:`target = breakout_price + H` - 向下跌破目标:`target = breakout_price - H` 注意:量度目标是“参考”,实际还需结合前高/筹码/均线等约束。 --- ## 8. Python 实现骨架(pandas/numpy baseline,可直接跑) > 说明:下面代码优先做到“可读、可调参、可画图验算”。性能优化(向量化/numba)可在验证有效后再做。 ```python from __future__ import annotations from dataclasses import dataclass from typing import List, Literal, Optional, Tuple import numpy as np import pandas as pd @dataclass class TriangleCandidate: start: int end: int kind: Literal["sym", "asc", "desc"] upper_coef: Tuple[float, float] # (a_u, b_u) lower_coef: Tuple[float, float] # (a_l, b_l) apex_x: float width_start: float width_end: float def _pivots_fractal(high: np.ndarray, low: np.ndarray, k: int = 3) -> Tuple[np.ndarray, np.ndarray]: """ 返回 pivot_high_idx, pivot_low_idx 需要右侧 k 根确认,所以最后 k 根无法成为 pivot。 """ n = len(high) ph = [] pl = [] for i in range(k, n - k): win_h = high[i - k : i + k + 1] win_l = low[i - k : i + k + 1] if high[i] == np.max(win_h): ph.append(i) if low[i] == np.min(win_l): pl.append(i) return np.array(ph, dtype=int), np.array(pl, dtype=int) def _fit_line(x: np.ndarray, y: np.ndarray) -> Tuple[float, float]: """y = a*x + b""" a, b = np.polyfit(x, y, deg=1) return float(a), float(b) def _line_y(a: float, b: float, x: np.ndarray) -> np.ndarray: return a * x + b def detect_triangles( df: pd.DataFrame, window: int = 120, pivot_k: int = 3, tol: float = 0.006, slope_eps: float = 0.0005, shrink_ratio: float = 0.6, ) -> List[TriangleCandidate]: """ 在最近窗口内寻找三角形候选(返回候选列表,通常取最近一个做信号)。 参数口径: - tol: 枢轴点到趋势线的相对误差容忍(0.6%) - slope_eps: 近似水平线斜率阈值(需结合 x 的尺度;此处 x 用 bar_index) - shrink_ratio: 末端宽度/初端宽度阈值,越小要求越严格 """ high = df["high"].to_numpy(dtype=float) low = df["low"].to_numpy(dtype=float) close = df["close"].to_numpy(dtype=float) n = len(df) ph_idx, pl_idx = _pivots_fractal(high, low, k=pivot_k) out: List[TriangleCandidate] = [] # 只做最近窗口的检测(也可扩展为滑动扫描全历史) end = n - 1 start = max(0, end - window + 1) x = np.arange(n, dtype=float) ph_in = ph_idx[(ph_idx >= start) & (ph_idx <= end)] pl_in = pl_idx[(pl_idx >= start) & (pl_idx <= end)] if len(ph_in) < 2 or len(pl_in) < 2: return out # 拟合两条线 a_u, b_u = _fit_line(x[ph_in], high[ph_in]) a_l, b_l = _fit_line(x[pl_in], low[pl_in]) # apex(两线交点) denom = (a_u - a_l) if abs(denom) < 1e-12: return out apex_x = (b_l - b_u) / denom upper_start = _line_y(a_u, b_u, np.array([start]))[0] lower_start = _line_y(a_l, b_l, np.array([start]))[0] upper_end = _line_y(a_u, b_u, np.array([end]))[0] lower_end = _line_y(a_l, b_l, np.array([end]))[0] width_start = upper_start - lower_start width_end = upper_end - lower_end if width_start <= 0 or width_end <= 0: return out if width_end / width_start > shrink_ratio: return out # 触碰判定:pivot 点距离线足够近 ph_dist = np.abs(high[ph_in] - _line_y(a_u, b_u, x[ph_in])) / np.maximum(_line_y(a_u, b_u, x[ph_in]), 1e-9) pl_dist = np.abs(low[pl_in] - _line_y(a_l, b_l, x[pl_in])) / np.maximum(_line_y(a_l, b_l, x[pl_in]), 1e-9) if (ph_dist <= tol).sum() < 2 or (pl_dist <= tol).sum() < 2: return out # apex 合理性:在窗口右侧一定范围内(可调) if not (end < apex_x < end + 2 * window): return out # 分类 kind: Optional[str] = None if a_u < 0 and a_l > 0: kind = "sym" elif abs(a_u) <= slope_eps and a_l > 0: kind = "asc" elif a_u < 0 and abs(a_l) <= slope_eps: kind = "desc" if kind is None: return out out.append( TriangleCandidate( start=start, end=end, kind=kind, # type: ignore[arg-type] upper_coef=(a_u, b_u), lower_coef=(a_l, b_l), apex_x=float(apex_x), width_start=float(width_start), width_end=float(width_end), ) ) return out def breakout_signal( df: pd.DataFrame, tri: TriangleCandidate, break_tol: float = 0.003, vol_window: int = 20, vol_k: float = 1.5, ) -> Literal["up", "down", "none"]: """ 简化版突破信号: - up: close > upper*(1+break_tol) 且 volume 放大 - down: close < lower*(1-break_tol) 且 volume 放大 """ a_u, b_u = tri.upper_coef a_l, b_l = tri.lower_coef end = tri.end x = float(end) upper = a_u * x + b_u lower = a_l * x + b_l close = float(df["close"].iloc[end]) vol = float(df.get("volume", pd.Series([np.nan] * len(df))).iloc[end]) vol_ma = float(df.get("volume", pd.Series([np.nan] * len(df))).rolling(vol_window).mean().iloc[end]) vol_ok = np.isfinite(vol) and np.isfinite(vol_ma) and (vol > vol_ma * vol_k) if close > upper * (1 + break_tol) and vol_ok: return "up" if close < lower * (1 - break_tol) and vol_ok: return "down" return "none" ``` --- ## 9. 可视化验算(强烈建议做,否则很容易“识别对了但画线不对”) ```python import matplotlib.pyplot as plt def plot_triangle(df, tri): a_u, b_u = tri.upper_coef a_l, b_l = tri.lower_coef start, end = tri.start, tri.end x = np.arange(start, end + 1) upper = a_u * x + b_u lower = a_l * x + b_l close = df["close"].to_numpy() plt.figure(figsize=(12, 5)) plt.plot(np.arange(len(close)), close, linewidth=1, label="close") plt.plot(x, upper, linewidth=2, label="upper") plt.plot(x, lower, linewidth=2, label="lower") plt.axvline(end, color="gray", linestyle="--", linewidth=1) plt.title(f"triangle={tri.kind}, width_end/width_start={tri.width_end/tri.width_start:.2f}") plt.legend() plt.show() ``` --- ## 10. 工程化落地建议(给“评级表/打分维度”用) ### 10.1 输出结构(可解释) 建议把形态识别输出做成结构化字段,方便评分与回测: - `kind`: sym/asc/desc - `touches_upper/touches_lower` - `shrink_ratio = width_end/width_start` - `apex_distance = apex_x - end` - `breakout`: none/up/down - `break_strength`: (close - upper)/ATR 或百分比 - `volume_ratio = volume / MA(volume, n)` - `measured_target`: `breakout_price ± H` ### 10.2 常见坑 - **没有 pivot**:k 取太大/窗口太短,会导致拐点不足。 - **斜率尺度问题**:不同价格/周期导致斜率阈值不可比;建议用归一化横轴或用“每根百分比斜率”。 - **尖刺离群点**:极端上影线会影响拟合;可用 RANSAC 或在 pivot 上做 winsorize。 - **形态过度拟合**:条件太多会导致回测看起来很强但实盘稀疏;建议分层评分(硬条件 + 软条件加分)。 --- ## 11. 下一步(如果要继续深化) - 增加全历史滑动扫描:输出每个窗口的候选并去重(同一形态合并)。 - 引入 ATR 作为容差:`tol = c * ATR/price`,比固定百分比更稳。 - 加入“前置趋势”判定:三角形属于延续还是反转(给评级体系加解释)。 - 将“画线/形态”统一到可视化回放工具(方便人工抽检)。