# 对称三角形识别(Symmetrical Triangle)——规则口径 + Python 实现 > 目标:只实现**对称三角形**的自动识别,并输出可解释结果:上沿/下沿、收敛度、触碰次数、是否突破、是否假突破(失败回撤)。 --- ## 1. 概念对齐:上沿、下沿、突破、失败回撤 - **上沿(Upper line / Resistance)** 用“枢轴高点”(一串冲高回落的峰)拟合出的压力线;对称三角形要求它**向右下倾**。 - **下沿(Lower line / Support)** 用“枢轴低点”(一串探底回升的谷)拟合出的支撑线;对称三角形要求它**向右上升**。 - **向上突破(Upward breakout)** 价格从两线之间运行,某一根开始**收盘价明显站上上沿**(不是影线擦边)。 - **向下跌破(Downward breakdown)** 收盘价明显跌到下沿之下。 - **失败回撤 / 假突破(False breakout / Failed retest)** “看起来突破了”,但在接下来 \(m\) 根内又回到三角形内部: - 向上突破后:`close` 又回到 `upper_line` 下方 - 向下跌破后:`close` 又回到 `lower_line` 上方 含义:突破没站稳,信号可靠性下降(回测/复盘里非常重要)。 --- ## 2. 输入数据要求 `df: pandas.DataFrame` 至少包含: - `open`, `high`, `low`, `close` - `volume`(可选;用于“放量确认突破”,没有则跳过确认) 实现里用 `bar_index = 0..n-1` 作为横轴。 --- ## 3. 可编码的识别口径(只针对对称三角形) ### 3.1 枢轴点(Pivot / Fractal) 用左右窗口 `k` 识别局部极值: - 枢轴高点:`high[i]` 是 `[i-k, i+k]` 最大值 - 枢轴低点:`low[i]` 是 `[i-k, i+k]` 最小值 > 直觉:枢轴点就是你手工画线时会“圈出来”的那些关键峰/谷。 ### 3.2 趋势线拟合 在一个检测窗口 `window` 内: - 取该窗口里的枢轴高点集合拟合上沿:\(y = a_u x + b_u\) - 取该窗口里的枢轴低点集合拟合下沿:\(y = a_l x + b_l\) ### 3.3 对称三角形硬条件(建议) 在窗口内必须满足: - **斜率方向**:`a_u < 0` 且 `a_l > 0` - **收敛**:末端宽度显著小于起始宽度 `width_end / width_start <= shrink_ratio` - **触碰次数**:上沿触碰 ≥ 2 且下沿触碰 ≥ 2 触碰判定:`abs(pivot_y - line_y) / line_y <= touch_tol` - **Apex 合理性(可选)**:两线交点在窗口右侧不远处,避免两线近似平行导致的伪形态 ### 3.4 突破/确认/假突破(信号层) 突破(价格): - 向上:`close[t] > upper[t] * (1 + break_tol)` - 向下:`close[t] < lower[t] * (1 - break_tol)` 成交量确认(可选): - `volume[t] > MA(volume, vol_window)[t] * vol_k` 假突破(回测/复盘可用): - 若在 `m` 根内再次回到两线之间,则标记为 `false_breakout=True` --- ## 4. 参数建议(先跑通,再调参) 日线常用默认值(你可以直接用): - `window=120`(约 6 个月) - `pivot_k=3~5` - `touch_tol=0.006`(0.6% 贴线容差) - `shrink_ratio=0.6`(末端宽度 ≤ 起始宽度的 60%) - `break_tol=0.003`(突破需离线 0.3%) - `vol_window=20`, `vol_k=1.5` - `false_break_m=5` --- ## 5. Python 实现(可直接复制使用) ```python from __future__ import annotations from dataclasses import dataclass from typing import Dict, List, Literal, Optional, Tuple import numpy as np import pandas as pd @dataclass class SymTriangleResult: start: int end: int upper_coef: Tuple[float, float] # (a_u, b_u) lower_coef: Tuple[float, float] # (a_l, b_l) width_start: float width_end: float width_ratio: float touches_upper: int touches_lower: int apex_x: float breakout: Literal["up", "down", "none"] breakout_idx: Optional[int] volume_confirmed: Optional[bool] false_breakout: Optional[bool] def pivots_fractal(high: np.ndarray, low: np.ndarray, k: int = 3) -> Tuple[np.ndarray, np.ndarray]: """左右窗口分形:返回 pivot_high_idx, pivot_low_idx。""" n = len(high) ph: List[int] = [] pl: List[int] = [] for i in range(k, n - k): if high[i] == np.max(high[i - k : i + k + 1]): ph.append(i) if low[i] == np.min(low[i - k : i + k + 1]): 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_sym_triangle( df: pd.DataFrame, window: int = 120, pivot_k: int = 3, touch_tol: float = 0.006, shrink_ratio: float = 0.6, break_tol: float = 0.003, vol_window: int = 20, vol_k: float = 1.5, false_break_m: int = 5, ) -> Optional[SymTriangleResult]: """ 只检测“最近一个窗口”是否存在对称三角形,并给出突破/确认/假突破信息。 - 实时:false_breakout 只能返回 None(因为需要未来 m 根确认) - 回测:如果 df 包含突破后的 m 根数据,才会输出 false_breakout True/False """ required = {"open", "high", "low", "close"} if not required.issubset(df.columns): raise ValueError(f"df must contain columns: {sorted(required)}") n = len(df) if n < max(window, 2 * pivot_k + 5): return None high = df["high"].to_numpy(dtype=float) low = df["low"].to_numpy(dtype=float) close = df["close"].to_numpy(dtype=float) has_volume = "volume" in df.columns and df["volume"].notna().any() volume = df["volume"].to_numpy(dtype=float) if has_volume else None ph_idx, pl_idx = pivots_fractal(high, low, k=pivot_k) end = n - 1 start = max(0, end - window + 1) x_all = 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 None # 拟合两条线 a_u, b_u = fit_line(x_all[ph_in], high[ph_in]) a_l, b_l = fit_line(x_all[pl_in], low[pl_in]) # 斜率:对称三角形硬条件 if not (a_u < 0 and a_l > 0): return None # 宽度收敛 upper_start = float(line_y(a_u, b_u, np.array([start]))[0]) lower_start = float(line_y(a_l, b_l, np.array([start]))[0]) upper_end = float(line_y(a_u, b_u, np.array([end]))[0]) lower_end = float(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 None width_ratio = width_end / width_start if width_ratio > shrink_ratio: return None # 触碰次数(用 pivot 点贴线程度判定) ph_dist = np.abs(high[ph_in] - line_y(a_u, b_u, x_all[ph_in])) / np.maximum( line_y(a_u, b_u, x_all[ph_in]), 1e-9 ) pl_dist = np.abs(low[pl_in] - line_y(a_l, b_l, x_all[pl_in])) / np.maximum( line_y(a_l, b_l, x_all[pl_in]), 1e-9 ) touches_upper = int((ph_dist <= touch_tol).sum()) touches_lower = int((pl_dist <= touch_tol).sum()) if touches_upper < 2 or touches_lower < 2: return None # apex(两线交点)仅做合理性输出(可扩展为硬条件) denom = (a_u - a_l) apex_x = float((b_l - b_u) / denom) if abs(denom) > 1e-12 else float("inf") # === 突破判定(用最后一根 close 做实时判断)=== upper_last = upper_end lower_last = lower_end breakout: Literal["up", "down", "none"] = "none" breakout_idx: Optional[int] = None if close[end] > upper_last * (1 + break_tol): breakout = "up" breakout_idx = end elif close[end] < lower_last * (1 - break_tol): breakout = "down" breakout_idx = end # 成交量确认(可选) volume_confirmed: Optional[bool] = None if breakout != "none" and has_volume and volume is not None: vol_ma = pd.Series(volume).rolling(vol_window).mean().to_numpy() if np.isfinite(vol_ma[end]) and vol_ma[end] > 0: volume_confirmed = bool(volume[end] > vol_ma[end] * vol_k) else: volume_confirmed = None # 假突破(需要未来 m 根数据,实时无法得知) false_breakout: Optional[bool] = None if breakout != "none" and breakout_idx is not None: # 如果数据不足以观察未来 m 根,则返回 None if breakout_idx + false_break_m < n: false_breakout = False for t in range(breakout_idx + 1, breakout_idx + false_break_m + 1): upper_t = float(line_y(a_u, b_u, np.array([t]))[0]) lower_t = float(line_y(a_l, b_l, np.array([t]))[0]) if breakout == "up" and close[t] < upper_t: false_breakout = True break if breakout == "down" and close[t] > lower_t: false_breakout = True break else: false_breakout = None return SymTriangleResult( start=start, end=end, upper_coef=(a_u, b_u), lower_coef=(a_l, b_l), width_start=float(width_start), width_end=float(width_end), width_ratio=float(width_ratio), touches_upper=touches_upper, touches_lower=touches_lower, apex_x=apex_x, breakout=breakout, breakout_idx=breakout_idx, volume_confirmed=volume_confirmed, false_breakout=false_breakout, ) def plot_sym_triangle(df: pd.DataFrame, res: SymTriangleResult) -> None: """简单可视化验算(需要 matplotlib)。""" import matplotlib.pyplot as plt close = df["close"].to_numpy(dtype=float) x = np.arange(len(df), dtype=float) a_u, b_u = res.upper_coef a_l, b_l = res.lower_coef start, end = res.start, res.end xw = np.arange(start, end + 1, dtype=float) upper = line_y(a_u, b_u, xw) lower = line_y(a_l, b_l, xw) plt.figure(figsize=(12, 5)) plt.plot(x, close, linewidth=1.2, label="close") plt.plot(xw, upper, linewidth=2, label="upper") plt.plot(xw, lower, linewidth=2, label="lower") plt.axvline(end, color="gray", linestyle="--", linewidth=1) plt.title( f"sym_triangle: width_ratio={res.width_ratio:.2f}, touches=({res.touches_upper},{res.touches_lower}), " f"breakout={res.breakout}, vol_ok={res.volume_confirmed}, false={res.false_breakout}" ) plt.legend() plt.show() ``` --- ## 6. 输出如何用于“评级/打分”(建议字段) 你可以直接把 `SymTriangleResult` 的字段映射为评分输入: - **形态质量**:`width_ratio`(越小越收敛)、`touches_upper/lower`(越多越可信) - **突破质量**:`breakout` + `volume_confirmed`(放量加分) - **风险提示**:`false_breakout`(为 True 则大幅降分;为 None 表示实时待确认) --- ## 7. 你该如何验证“程序画的线”是否接近你手工画线 1) 先挑一个你手工画过的例子(如你图中的那段) 2) 跑 `detect_sym_triangle` 得到 `res` 3) 用 `plot_sym_triangle(df, res)` 画出 close + upper/lower 4) 调整 `pivot_k / touch_tol / window / shrink_ratio`,直到“触点选择”和你肉眼一致