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
+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()