"""快速三类买卖点:不等笔确认,突破回抽当根即入场。 引擎的 B3/S3 要等 pullback_bi.sure_time(回拉笔被确认),滞后 9~10 根, 此时价格已从回抽低点反弹完毕,入场价被吃掉。 但三买的形态条件本身是实时可判的: 中枢已成 -> 收盘突破 zg -> 收盘未跌回中枢 -> 重新上行 最后一步发生的当根就能下单,滞后约 2 根。 require_touch 控制是否强求回抽碰到中枢边界。step32/33 的实测表明 强求反而更差:这等于排除掉「突破后一去不回头」的强势段,而那正是 缠论里最强的趋势形态。故默认 False(笔数 +21%、滞后 -1 根、PF 2.26→2.37)。 判据本身(收盘越过前一根极值)没有独立预测力:裸用 15 万笔样本 PF 0.95, 把中枢换成「近20根高点」这类伪阻力位后 PF 0.90。alpha 全部来自中枢结构, 判据只负责在这个已知价位上确认动能恢复。它也不能用于识别笔端点—— 入场前的回抽极值命中笔端点±2根的比例 19.3%,低于随机基准 22%。 全部判定只使用当根及之前的数据,无未来函数。 """ from __future__ import annotations import numpy as np import pandas as pd def find_fast_bsp3( df: pd.DataFrame, zones: pd.DataFrame, scan: int = 200, pullback_win: int = 30, tol: float = -1.0, max_per_zone: int = 1, diag: dict | None = None, require_touch: bool = False, ) -> pd.DataFrame: """扫描每个中枢,找突破后回抽不回中枢的入场点。 zones 需含 zg / zd / available_ts,且 available_ts 已是可用时刻。 max_per_zone > 1 时,同一中枢在首次入场后继续往后找二次、三次突破回抽, 用来检验「趋势里同一中枢反复给机会」是否值得做。 tol 是「算作回抽中」的边界容差。require_touch=False 时它不再是入场门槛, 仅决定哪些K线被视为回抽中而跳过转强判定,故 tol 越大入场越晚。 默认负值 = 完全禁用该跳过,突破后每根都检查转强,滞后压到 2.2 根。 step34 实测滞后与收益严格单调:9.9根 PF1.88 / 5.8根 2.37 / 2.2根 2.86。 返回列: entry_idx 实时可下单的K线 direction +1 三买 / -1 三卖 bo_idx 突破根 pb_idx 回抽极值根 lag entry_idx - bo_idx depth 回抽深度(相对中枢边界,负值表示曾插入中枢) occ 这是该中枢的第几次入场 """ if zones.empty: return pd.DataFrame() ts = df["timestamp"].to_numpy() close = df["close"].to_numpy(dtype=float) high = df["high"].to_numpy(dtype=float) low = df["low"].to_numpy(dtype=float) n = len(df) rows = [] def note(key: str) -> None: if diag is not None: diag[key] = diag.get(key, 0) + 1 for zone_i, (_, z) in enumerate(zones.iterrows()): note("中枢总数") zg, zd = float(z["zg"]), float(z["zd"]) if zg <= zd: note("×无效中枢") continue start = int(np.searchsorted(ts, z["available_ts"], side="left")) if start >= n - 2: note("×中枢太靠后") continue # 允许多次入场时按比例放宽扫描窗口,否则后几次机会会被窗口截断 scan_end = min(start + scan * max_per_zone, n) cursor = start for occ in range(1, max_per_zone + 1): if cursor >= n - 2: break # 第一步:找突破。要求突破前确实待在中枢内,避免把远处的价格当突破。 was_inside = False bo_idx, d = None, 0 for j in range(cursor, scan_end): c = close[j] if zd <= c <= zg: was_inside = True continue if not was_inside: continue bo_idx, d = j, (1 if c > zg else -1) break if bo_idx is None: if occ == 1: note("×窗口内未突破") break edge = zg if d == 1 else zd # 第二步:突破后监控回抽,回抽不跌回中枢且重新顺势 -> 入场 touched = False pb_idx = None pb_ext = None entry_idx = None fell_back = False for j in range(bo_idx + 1, min(bo_idx + pullback_win + 1, n)): # 收盘跌回中枢 -> 突破失效 if zd <= close[j] <= zg: fell_back = True break # 回抽触及边界附近(允许 tol 的毛刺) near = (low[j] <= edge * (1 + tol)) if d == 1 else (high[j] >= edge * (1 - tol)) if near: touched = True ext = low[j] if d == 1 else high[j] if pb_ext is None or ((ext < pb_ext) if d == 1 else (ext > pb_ext)): pb_ext, pb_idx = ext, j continue # 回抽后重新顺势:收盘创出前一根之上(三买)/ 之下(三卖) # require_touch=False 时不强求回抽碰到中枢边界, # 这样「突破后一去不回头」的强势段也能收进来。 if (touched and pb_idx is not None) or not require_touch: go = close[j] > high[j - 1] if d == 1 else close[j] < low[j - 1] if go: entry_idx = j break if entry_idx is None: if occ == 1: note("×突破后跌回中枢" if fell_back else "×回抽未触及边界" if not touched else "×触及边界但未转强") # 这次突破没走成,从突破点之后继续找下一次 cursor = bo_idx + 1 continue if pb_ext is None: # 未触及边界就转强(require_touch=False):取突破至入场间的实际极值 seg = slice(bo_idx + 1, entry_idx + 1) pb_ext = float(low[seg].min() if d == 1 else high[seg].max()) pb_idx = int((low[seg].argmin() if d == 1 else high[seg].argmax()) + bo_idx + 1) if occ == 1: note("√成交") # 回抽深度:>0 表示未插入中枢,越大表示回抽越浅 depth = (pb_ext - zg) / zg if d == 1 else (zd - pb_ext) / zd rows.append({ "entry_idx": entry_idx, "direction": d, "bo_idx": bo_idx, "pb_idx": pb_idx, "lag": entry_idx - bo_idx, "depth": depth, "zg": zg, "zd": zd, "width_pct": (zg - zd) / close[bo_idx], "occ": occ, "zone_i": zone_i, }) cursor = entry_idx + 1 out = pd.DataFrame(rows) if out.empty: return out # 相邻中枢可能突破到同一根K线,同一时刻只能有一个仓位,保留最早成型的那个 return (out.sort_values(["entry_idx", "occ", "zone_i"]) .drop_duplicates("entry_idx", keep="first") .reset_index(drop=True))