research: 一类反手(step57),六年五币双向全正,PF 2.3~3.0

用户问一类二类的识别是不是错了。查下来是代码没写错、信号该反着用。

先排除一个错误猜测:check_bi_div 只比 MACD 面积、不检查创新极值,等于把
缠论背驰定义丢了一半。看着像致命缺陷,实测 93% 的离开笔本来就创了新极值,
解释不了 PF 0.2。

真正的线索是胜率方向:B1 胜率 18.6%,而 SL2/TP3 随机入场约 40%,远低于
随机说明样本带信息只是指反了。拿相反方向重跑出场(路径依赖,不能取负号),
5m/15m 上 PF 2.35~3.03、t +10~+13。

step57 三关全过:5 币全正、前后半段 2.81/2.81 与 2.55/2.51 几乎不变、
2021~2026 连续六年全正、多空两边都正。余量 15m 33~76bp,B4 只有约 3bp。

机理是滞后把信号变成了相反的交易:信号在 leave_bi.sure_time 发出,中位滞后
8~9 根,此时价格已从低点反弹 1.68 ATR。引擎想说抄底,它给的时间戳对应的却是
反弹已走完——在下跌趋势里于此处做空是标准的顺势回调入场。与 B4 的突破延续
是不同的入场原型,可能互补。

三项验证未做完前不许上实盘:因果性(中枢右边缘已知会重画,需流式回放)、
消融(是不是反弹就做空都赚)、与 B4 的重叠度。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-28 20:12:11 +08:00
co-authored by Cursor
parent 409dc2f020
commit be3602f5ba
3 changed files with 171 additions and 0 deletions
+53
View File
@@ -1086,6 +1086,59 @@ B4 跑出 PF 1.83~2.06 / t +8.5~+10.7。所以 fast B1 的负数不是管线接
头早就写了「B4 不只是 B3 提前几根,两者统计性质符号相反」,一类的实验说明
反过来也成立:**把反转信号提前,它还是反转信号。**
### 3.395 ⭐ 一类反手:识别没错,但发出的时点让它变成了反向信号(step55/57)
用户问「是不是一类二类的识别错了」。查下来结论是:**代码没写错,但信号该反着用。**
**先排除掉一个我自己的错误猜测。** `check_bi_div` 只比 MACD 面积、不检查创新极值,
这是缠论背驰定义(创新极值 **且** 力度减弱)丢了一半。看着像致命缺陷,但实测
**93.2% 的 B1 / 93.4% 的 S1 离开笔本来就创了新极值**,缺口只影响约 7%
解释不了 PF 0.2。**先量再断,别拿理论缺陷当结论。**
**真正的线索是胜率的方向**B1 胜率 18.6%,而 SL2/TP3 下随机入场约 40%。
**远低于随机说明样本带信息,只是指反了**——真正没用的信号会落在 40% 附近。
于是拿相反方向重跑出场(路径依赖,不能把 g 取负号了事):
| 反手 | 5m 胜率 | 5m PF | 5m t值 | 15m PF | 15m t值 |
|---|---|---|---|---|---|
| **B1(反手做空)** | 64.4% | **2.35** | +10.4 | **3.03** | +12.6 |
| **S1(反手做多)** | 67.1% | **2.75** | +12.8 | 2.63 | +9.6 |
| B3/S3(反手) | 45~49% | 1.14~1.24 | +4 | 1.15~1.20 | +4 |
**稳健性(step57)——这是本项目做过的最稳的结果**
| 切法 | 结论 |
|---|---|
| 逐币 | 5 个币全正。15m PF 2.25~4.125m 2.31~2.70 |
| 前后半段 | 15m 2.81 / 2.815m 2.55 / 2.51,几乎不变 |
| 逐年 | 2021~2026 **连续六年全正**15m PF 2.57~2.99 |
| 多空 | 两边都正,排除单边行情 |
| 余量 | 15m 33~76bp、5m 18~28bp。对比 B4 只有约 3bp |
**为什么不是 bug 而要反着用**
代码是照缠论写的:向下离开中枢 + 背驰 → B1 买。**教科书没错,错的是它发信号的
时刻。** 信号在 `leave_bi.sure_time` 发出,中位滞后 8~9 根,此时价格已从低点
**反弹了 1.68 ATR**(§3.393 表)。引擎想说「抄底」,但它给出的时间戳对应的是
「反弹已走完」。在下跌趋势里于此处做空 —— 那是标准的**顺势回调入场**。
所以这一族信号的几何识别是对的,方向标注按理论也是对的,**是滞后把它变成了
相反的交易**。它是一个穿着反转外衣的顺势回调信号。
**它与 B4 不是一回事**:B4 是突破延续(§3.391 实测 mom10 中位 +3.47 ATR
根本不是回调入场),这个是回调延续。两种入场原型不同,**可能互补**。
**仍未验证,落地前必须做完(否则不许上实盘)**
1. **因果性**`zs.is_sure` / `bi.sure_time` 在全量数据上算出,§5.41 已知中枢
右边缘会重画。必须用增量/流式回放确认信号在当时真的发得出来。
*反证据(不能替代验证)*:同一套机器算出的 B3 是 PF 0.64 的烂数,
若存在通用的未来函数,B3 不该这么难看。
2. **消融:是不是「反弹就做空」都赚**。照 §3.4 对 B4 做过的那样,把背驰条件
拿掉、把中枢换成伪阻力位,看 PF 掉不掉。掉不下来就说明 alpha 在几何和
时点上,与背驰无关,那这个信号就该重新命名和重新设计。
3. **与 B4 的重叠度与相关性**。若只是换名字的 B4,就没有增量价值。
### 3.4 alpha 的来源(step32 消融)
逐条拆掉 `fast_bsp3` 的条件后发现:**alpha 完全来自缠论中枢的上下文定位,
+34
View File
@@ -151,6 +151,15 @@ def collect(sym: str, rows: int, tf: str = "1m") -> pd.DataFrame | None:
return None
for k in ("g", "r", "c", "b"):
r[k] = res[f"{cfg}_{k}"].to_numpy()
# 反向对照:胜率远低于随机说明信号带信息但可能指反了。出场是路径依赖的,
# 不能把 g 取负号了事,必须拿相反方向重跑一遍
flip = pd.DataFrame({"entry_idx": r.i_sure.values,
"direction": -r.dir.values})
rf = walk_exits(cdf, flip, [SL], [RUNNER], [MAXB], scale_at=SCALE_AT,
runners=(RUNNER,), runner_stops=(RSTOP,))
if len(rf) == len(r):
for k in ("g", "r", "c", "b"):
r["f_" + k] = rf[f"{cfg}_{k}"].to_numpy()
r["atr_pct"] = atr[r.i_sure.values] / cl[r.i_sure.values]
return r
except Exception as e:
@@ -258,6 +267,31 @@ def profit(d: pd.DataFrame) -> None:
print("\n⚠️ B3 是口径校验锚:HANDOFF §4 记的是 PF 0.66 / 胜率 27.4%"
"这里若明显更好,先怀疑接错了再信 B1/B2。")
if "f_g" not in x.columns:
return
print("\n" + "=" * 100)
print("########## 六、反向对照:信号是「指反了」还是「没有边」 ##########")
print("胜率 18.6% 远低于 SL2/TP3 的随机基准约 40%,说明样本带信息。若只是"
"方向标反了,\n反手做应显著为正;若反手也不赚,那就是入场时点本身"
"已经过期,两边都吃滑点。")
y = x.copy()
y["fnet"] = y.f_g.values - fee_of(y.f_r.values, y.f_c.values)
y["fgR"] = y.f_g.values / (SL * y.atr_pct.values)
rows = []
for name, g in y.groupby("type"):
if len(g) < 40:
continue
w, o = g.fnet[g.fnet > 0].sum(), -g.fnet[g.fnet <= 0].sum()
rows.append({
"类型(反手)": name, "笔数": len(g),
"胜率": f"{(g.fnet > 0).mean()*100:.1f}%",
"毛R": round(g.fgR.mean(), 3),
"PF": round(w / o, 2) if o > 0 else np.inf,
"t值": round(g.fgR.mean() / (g.fgR.std(ddof=1) / np.sqrt(len(g))),
2),
})
print(pd.DataFrame(rows).to_string(index=False))
def main() -> None:
ap = argparse.ArgumentParser()
+84
View File
@@ -0,0 +1,84 @@
"""一类反手(fade B1/S1)是不是真的——逐币、分时段、与 B4 的重叠。
step55 的反向对照给出 5m/15m 上 PF 2.35~3.03、t +10~+13,且多空两边都正。
数字太好,先按 §3.392 的教训做证伪:那次「周末效应」在 1m 上显著,换到
7 年 5m/15m 样本就消失且符号反转。任何单一样本上的漂亮结果都要先过三关。
逐币 5 个币是不是都成立,还是被某一个币带起来的
分时段 前后半段样本各自是否成立(时间外样本)
与 B4 重叠 如果它只是换个名字的 B4,那就没有增量价值
"""
from __future__ import annotations
import sys
import warnings
from pathlib import Path
import numpy as np
import pandas as pd
warnings.filterwarnings("ignore")
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
sys.path.insert(0, str(HERE.parent))
from lib.exit_model import fee_of, taker_notional # noqa: E402
SRC = HERE / "out" / "step55_bsp12.feather"
SL = 2.0
def stats(g: pd.DataFrame) -> dict:
"""反手口径的统计。f_* 是 step55 里用相反方向重跑出场得到的。"""
net = g.f_g.values - fee_of(g.f_r.values, g.f_c.values)
gR = g.f_g.values / (SL * g.atr_pct.values)
R = net / (SL * g.atr_pct.values)
tn = taker_notional(g.f_r.values, g.f_c.values)
w, o = net[net > 0].sum(), -net[net <= 0].sum()
return {
"笔数": len(g), "胜率": f"{(net > 0).mean()*100:.1f}%",
"毛R": round(gR.mean(), 3), "净均R": round(R.mean(), 3),
"PF": round(w / o, 2) if o > 0 else np.inf,
"余量bp": round(net.mean() / tn.mean() * 1e4, 2),
"t值": round(gR.mean() / (gR.std(ddof=1) / np.sqrt(len(g))), 2),
}
def main() -> None:
d = pd.read_feather(SRC)
if "f_g" not in d.columns:
print("step55 数据里没有反手列,先重跑 step55")
return
d["date_ext"] = pd.to_datetime(d["date_ext"])
one = d[d.type.isin(["B1", "S1"])].copy()
for tf, x in one.groupby("tf"):
print("\n" + "#" * 92)
print(f"########## {tf} · 一类反手 ##########")
print("\n【逐币】任何一个币独自撑起来的结果都不能要")
print(pd.DataFrame([{"": s, **stats(g)}
for s, g in x.groupby("sym")]).to_string(index=False))
print("\n【分时段】按信号时间中位数切两半,后半段是时间外样本")
cut = x.date_ext.median()
for nm, g in (("前半", x[x.date_ext <= cut]), ("后半", x[x.date_ext > cut])):
r = stats(g)
print(f" {nm} {g.date_ext.min():%Y-%m-%d}~{g.date_ext.max():%Y-%m-%d}"
f" {r}")
print("\n【分方向】B1反手=做空,S1反手=做多。只有一边成立就是单边行情")
print(pd.DataFrame([{"原类型": t, **stats(g)}
for t, g in x.groupby("type")]).to_string(index=False))
print("\n【逐年】")
rows = []
for y, g in x.groupby(x.date_ext.dt.year):
if len(g) >= 40:
rows.append({"": y, **stats(g)})
if rows:
print(pd.DataFrame(rows).to_string(index=False))
if __name__ == "__main__":
main()