research: 一类线重开——病根是选样×止损的交互,不是没机会
用户问「修改 B1/B2 的识别规则呢」,查下来推翻了 §3.399 的关闭结论。 step65 先量机会本身:直接取 seg_list(不经过检测器),线段中位幅度 11.4 ATR, 扣掉 2.9 ATR 确认成本还剩 8.5,是止损的 4.26 倍,93% 的线段装得下。 五币四级别一致。**滞后不是瓶颈**,与原先预期相反。 但这与 §3.397 矛盾(空间在却拿不到),step66 给出解答:之前六次进攻每次只动 一半。完美选样配 2ATR 止损是 0.70/0.83,无选样配结构止损是 0.36, **两个一起是 2.29(5m)/4.11(15m)**,胜率 36%→63% / 44%→70%。是交互不是叠加。 机制来自 MFE/MAE:真底那批逆向行程中位仅 2.24 ATR,2 ATR 止损打掉了 51~58% 的好单;非真底那批逆向行程中位 4.76,放宽只是亏更多。固定 2 ATR 同时做错两件事。 ⚠️ 上界含两个未来函数(线段端点选样、sure_time 入场),是靶子不是策略。 顺带证伪一个我自己的猜测:step58 首轮用 r_scale=True 把 3.9 ATR 止损对应的 runner 目标推到 15.6 ATR(比整段行情还长)。加 --abs-targets 改绝对目标重跑, PF 仍是 0.36,与 r_scale 版完全相同。首轮结论正确,只是理由错了。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+61
-5
@@ -1438,6 +1438,57 @@ step56 的实时版把滞后压到 ≤1 根但无质量过滤(PF 0.40),ste
|
|||||||
同时把止损位推远到 4.87 ATR。用户的悖论方向对,但不是「继承错误」,
|
同时把止损位推远到 4.87 ATR。用户的悖论方向对,但不是「继承错误」,
|
||||||
是「多等的那两笔把信息也等没了」。
|
是「多等的那两笔把信息也等没了」。
|
||||||
|
|
||||||
|
### 3.3992 ⭐ 一类线重开:病根是选样×止损的交互,不是没机会(step65/58/66)
|
||||||
|
|
||||||
|
用户问「那如果修改 B1/B2 的识别规则呢」。这个问题**推翻了 §3.399 的关闭结论**,
|
||||||
|
下面三步是重开的依据。
|
||||||
|
|
||||||
|
**step65:先量机会本身装不装得下确认成本(不经过检测器,直接取 `seg_list`)**
|
||||||
|
|
||||||
|
| 级别 | 线段中位幅度 | 扣 2.9 ATR 确认成本后 | 剩余 > 2ATR 止损 | 剩余/止损 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 5m | 11.4 ATR | 8.5 | **93%** | **4.26x** |
|
||||||
|
| 15m | 10.9 | 8.0 | 93% | 3.99 |
|
||||||
|
| 1h | 11.5 | 8.6 | 95% | 4.30 |
|
||||||
|
| 4h | 12.4 | 9.5 | 96% | 4.74 |
|
||||||
|
|
||||||
|
五币四级别一致(10.5~13.7)。**滞后不是瓶颈**——这与我原先的预期相反。
|
||||||
|
但它与 §3.397 直接矛盾(空间在,却拿不到),矛盾的解答就是下面这条。
|
||||||
|
|
||||||
|
**step58 复测:先排除一个伪解释。** 首轮结构止损用了 `r_scale=True`,
|
||||||
|
3.9 ATR 的止损把 runner 目标推到 15.6 ATR,比 11.4 ATR 的整段行情还长。
|
||||||
|
改成绝对 3/8 ATR 目标重跑,PF 仍是 0.36(与 r_scale 版**完全相同**)。
|
||||||
|
**我预判的这个修正不成立**,首轮结论正确只是理由错了。
|
||||||
|
|
||||||
|
**step66:把两个杠杆同时拧(三者首次组合),天花板一次性打开**
|
||||||
|
|
||||||
|
| 5m | 固定 2ATR | 结构+1.0 |
|
||||||
|
|---|---|---|
|
||||||
|
| 落在真线段端点(28.7%) | PF **0.70** / 胜率 36% | PF **2.29** / 胜率 63% |
|
||||||
|
| 没落在(71.3%) | 0.08 | 0.14 |
|
||||||
|
|
||||||
|
15m:真底那批 **0.83 → 4.11**,胜率 44% → 70%。
|
||||||
|
|
||||||
|
**这是交互效应,不是叠加**——前三次进攻各自只动了一半,所以全部失败:
|
||||||
|
|
||||||
|
| 只做完美选样(step60) | 只放宽止损(step58) | **两个一起(step66)** |
|
||||||
|
|---|---|---|
|
||||||
|
| 0.70 / 0.83 | 0.36 | **2.29 / 4.11** |
|
||||||
|
|
||||||
|
MFE/MAE 给出机制:真底那批**逆向行程中位 2.24 ATR(5m)/ 2.08(15m)**,
|
||||||
|
即 2 ATR 止损**打掉了 51~58% 的好单**;而非真底那批逆向行程中位 4.76/4.93,
|
||||||
|
放宽只是亏更多。**固定 2 ATR 同时做错两件事:杀掉对的单,又留不住错的单。**
|
||||||
|
|
||||||
|
⚠️ **这个 2.29/4.11 里有两个未来函数,不是策略,是上界**:
|
||||||
|
(a) 选样用了线段端点;(b) 入场仍在 `sure_time`,而 §3.396 证明实盘还要再晚 15 根。
|
||||||
|
它证明的是**失败原因可修(选样精度 + 止损位),而非这个级别没得做**。
|
||||||
|
|
||||||
|
**由此产生的两个待跑实验:**
|
||||||
|
1. **实时过滤器 + 结构止损**(唯一值得跑的组合)。step62 的 `ext_run` 实时可算、
|
||||||
|
已把精度做到 47.9%,但当时配 2 ATR 止损所以只到 0.54。天花板说这个配错了。
|
||||||
|
2. **超时从 48 根放宽**。线段中位时长 **113 根**,48 根超时在行情走完 42% 时就平仓;
|
||||||
|
真底那批 MFE 中位仅 2.37 ATR,很可能就是被截断的(而非行情本身小)。
|
||||||
|
|
||||||
### 3.4 alpha 的来源(step32 消融)
|
### 3.4 alpha 的来源(step32 消融)
|
||||||
|
|
||||||
逐条拆掉 `fast_bsp3` 的条件后发现:**alpha 完全来自缠论中枢的上下文定位,
|
逐条拆掉 `fast_bsp3` 的条件后发现:**alpha 完全来自缠论中枢的上下文定位,
|
||||||
@@ -2649,11 +2700,16 @@ API 限流风险隔离三个好处。
|
|||||||
§3.396 证明 `sure_time` 对一类是后视产物(PF 2.35 → 0.92)。B4 用的是
|
§3.396 证明 `sure_time` 对一类是后视产物(PF 2.35 → 0.92)。B4 用的是
|
||||||
同一个 `sure_time` 字段,**在这个回放出结果前,所有 B4 的历史 PF 都待定**。
|
同一个 `sure_time` 字段,**在这个回放出结果前,所有 B4 的历史 PF 都待定**。
|
||||||
若 B4 也栽,整条线要重估;若过了,B4 是目前唯一活着的信号
|
若 B4 也栽,整条线要重估;若过了,B4 是目前唯一活着的信号
|
||||||
- [x] **一二类买卖点全线关闭**(step55~62 一类、step64 二类,见 §3.393~3.3991)。
|
- [ ] ⭐ **一类线已重开,跑「实时过滤器 + 结构止损」**(§3.3992)。
|
||||||
一类是**信号指反**(t = −17~−22,有信息但方向错,错在滞后),
|
§3.399 的关闭结论**已被 step66 推翻**:之前六次进攻每次只动一半,
|
||||||
二类是**信号消失**(两个方向 t 都贴零,且入场离结构止损位 4.87 ATR)。
|
完美选样配 2ATR 止损是 0.70,无选样配结构止损是 0.36,**两个一起是 2.29/4.11**。
|
||||||
六次独立进攻全部止步 PF 1.0 以下,**含用未来函数选样也只到 0.83**。
|
要跑的是 step62 的 `ext_run` 实时过滤器(精度 28.7% → 47.9%)配结构止损,
|
||||||
**不要重开**,除非能从机制上解决「等笔确认」带来的滞后
|
看能兑现多少上界。注意上界含两个未来函数,兑现后必须再过因果回放
|
||||||
|
- [ ] **一类超时从 48 根放宽**(§3.3992)。线段中位时长 113 根,
|
||||||
|
48 根在行情走完 42% 时平仓,真底那批 MFE 中位仅 2.37 ATR 疑似被截断
|
||||||
|
- [x] **二类关闭**(step64,§3.3991)。与一类不同,二类是**信号消失**而非信号指反:
|
||||||
|
两个方向 t 都贴零,且入场离结构止损位 4.87 ATR(一类 2.90),
|
||||||
|
§3.3992 的结构止损解法对它不适用
|
||||||
- [x] **step39**:1m 时点重建 + 窗口扫描 → **2000 根窗口饱和,计算 0.20s,
|
- [x] **step39**:1m 时点重建 + 窗口扫描 → **2000 根窗口饱和,计算 0.20s,
|
||||||
假阳性 0%,1m 回测口径可信**
|
假阳性 0%,1m 回测口径可信**
|
||||||
- [ ] **对齐 step23 与 step42 的 1m 数字**(3.91bp vs 11.06bp)。差距可能来自
|
- [ ] **对齐 step23 与 step42 的 1m 数字**(3.91bp vs 11.06bp)。差距可能来自
|
||||||
|
|||||||
@@ -117,6 +117,12 @@ def main() -> None:
|
|||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("--tf", default="5m")
|
ap.add_argument("--tf", default="5m")
|
||||||
ap.add_argument("--rows", type=int, default=200_000)
|
ap.add_argument("--rows", type=int, default=200_000)
|
||||||
|
# step65 发现线段中位幅度只有 11.4 ATR,而 r_scale 会把 3.9 ATR 的结构止损
|
||||||
|
# 对应的 runner 目标推到 15.6 ATR —— 比整段行情还长,永远打不到。
|
||||||
|
# 本脚本首轮全程用了 r_scale=True,等于「放宽止损」和「目标够不着」同时生效,
|
||||||
|
# 两个效应互相抵消,那轮的结论无效。绝对目标才是与 11.4 ATR 相容的口径。
|
||||||
|
ap.add_argument("--abs-targets", action="store_true",
|
||||||
|
help="目标位用绝对 ATR(3/8)而非按止损等比放大")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
d = pd.read_feather(SRC)
|
d = pd.read_feather(SRC)
|
||||||
@@ -149,7 +155,7 @@ def main() -> None:
|
|||||||
df = fetch_ohlcv(f"{sym}/USDT:USDT", args.tf, args.rows)
|
df = fetch_ohlcv(f"{sym}/USDT:USDT", args.tf, args.rows)
|
||||||
cache[(sym, args.tf)] = TF_DF(df, 1, args.tf).dataframe
|
cache[(sym, args.tf)] = TF_DF(df, 1, args.tf).dataframe
|
||||||
|
|
||||||
print("目标随止损等比放大,恒定 1.5R 减半 / 4R 收尾,与实盘的 2/3/8 同构。")
|
print(f"目标位口径:{'绝对 3/8 ATR(装得进 11.4 ATR 的线段)' if args.abs_targets else '随止损等比放大(恒定 1.5R/4R)'}")
|
||||||
for flip in (False, True):
|
for flip in (False, True):
|
||||||
for label, margin in [("固定2ATR", None), ("结构+0.25", 0.25),
|
for label, margin in [("固定2ATR", None), ("结构+0.25", 0.25),
|
||||||
("结构+0.5", 0.5), ("结构+1.0", 1.0),
|
("结构+0.5", 0.5), ("结构+1.0", 1.0),
|
||||||
@@ -166,7 +172,7 @@ def main() -> None:
|
|||||||
# 入场价另一侧同样宽度处,否则两组比的不是同一个东西
|
# 入场价另一侧同样宽度处,否则两组比的不是同一个东西
|
||||||
sl = (np.full(len(x), 2.0) if margin is None
|
sl = (np.full(len(x), 2.0) if margin is None
|
||||||
else (x.to_ext.values + margin))
|
else (x.to_ext.values + margin))
|
||||||
res = simulate(cdf, t, sl)
|
res = simulate(cdf, t, sl, r_scale=not args.abs_targets)
|
||||||
ok = res.g.notna().values
|
ok = res.g.notna().values
|
||||||
gs.append(res.g[ok])
|
gs.append(res.g[ok])
|
||||||
rs.append(res.r[ok])
|
rs.append(res.r[ok])
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
"""改识别规则之前,先量机会本身装不装得下确认成本。
|
||||||
|
|
||||||
|
用户问:改 B1/B2 的识别规则行不行?
|
||||||
|
|
||||||
|
「改识别规则」有两种含义,第一种已经被测过上限:
|
||||||
|
(a) 换规则**挑**出更好的 B1 —— step60 用未来函数只留极值落在真线段底 ±2 根内
|
||||||
|
的信号,这是任何识别规则的理论最好情况,PF 仅 0.70~0.83。这一类改法封死。
|
||||||
|
|
||||||
|
但 0.83 这个上限本身是**结果**,背后是一个从没直接测过的量:
|
||||||
|
|
||||||
|
一段线段从真底走到真顶,一共有几个 ATR?而等笔确认要花掉 2.9 个。
|
||||||
|
|
||||||
|
这决定了 (b) 类改法(改**入场时点/构造**,而非改选样)有没有空间:
|
||||||
|
|
||||||
|
线段幅度 4 ATR -> 进场吃掉 2.9,剩 1.1 去扛 2 ATR 止损,**任何规则都救不活**
|
||||||
|
线段幅度 12 ATR -> 2.9 只是小费,值得继续改规则
|
||||||
|
|
||||||
|
注意这测的是**市场**,不是我们的检测器:标准答案直接取 `seg_list` 的真实端点,
|
||||||
|
完全不经过 `find_all_bsp`。所以结论对「换任何一套识别规则」都成立。
|
||||||
|
|
||||||
|
而且这个比值大概率**随级别变化**(ATR 与线段幅度未必同比例缩放),所以跑
|
||||||
|
5m/15m/1h/4h。真正的产出不是「改不改规则」,而是**「在哪个级别上改才有意义」**。
|
||||||
|
|
||||||
|
预算恒等式(每根线段一行):
|
||||||
|
可捕获 = 线段幅度 − 确认成本 需要 > 止损 才有正期望的可能
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||||
|
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))
|
||||||
|
|
||||||
|
OUT = HERE / "out" / "step65_room.feather"
|
||||||
|
SL_ATR = 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def collect(sym: str, tf: str, rows: int) -> pd.DataFrame | None:
|
||||||
|
from chanlun import TF_DF
|
||||||
|
from chanlun.core.ChanEnum import Chan_SEG_DIR
|
||||||
|
from lib.data import fetch_ohlcv
|
||||||
|
|
||||||
|
try:
|
||||||
|
df = fetch_ohlcv(f"{sym}/USDT:USDT", tf, rows)
|
||||||
|
if df is None or len(df) < 3_000:
|
||||||
|
return None
|
||||||
|
chan = TF_DF(df, 1, tf, lean=False)
|
||||||
|
cdf = chan.dataframe
|
||||||
|
segs = [s for s in (getattr(chan, "seg_list", []) or [])
|
||||||
|
if s.end_time is not None and s.start_time is not None]
|
||||||
|
if len(segs) < 20:
|
||||||
|
return None
|
||||||
|
|
||||||
|
dser = pd.to_datetime(cdf["date"])
|
||||||
|
if dser.dt.tz is not None:
|
||||||
|
dser = dser.dt.tz_localize(None)
|
||||||
|
didx = pd.DatetimeIndex(dser)
|
||||||
|
n = len(cdf)
|
||||||
|
atr = cdf["atr"].to_numpy(float)
|
||||||
|
hi, lo = cdf["high"].to_numpy(float), cdf["low"].to_numpy(float)
|
||||||
|
|
||||||
|
def to_i(ts) -> int:
|
||||||
|
t = pd.Timestamp(ts)
|
||||||
|
return int(didx.searchsorted(t.tz_localize(None) if t.tz else t))
|
||||||
|
|
||||||
|
rec = []
|
||||||
|
for sg in segs:
|
||||||
|
i0, i1 = to_i(sg.start_time), to_i(sg.end_time)
|
||||||
|
if not (0 <= i0 < i1 < n) or i1 - i0 < 2:
|
||||||
|
continue
|
||||||
|
a = atr[i0]
|
||||||
|
if not np.isfinite(a) or a <= 0:
|
||||||
|
continue
|
||||||
|
up = sg.dir != Chan_SEG_DIR.DOWN
|
||||||
|
# 从线段起点(上一段的真实反转点)到终点的幅度
|
||||||
|
span = ((hi[i0:i1 + 1].max() - lo[i0]) if up
|
||||||
|
else (hi[i0] - lo[i0:i1 + 1].min()))
|
||||||
|
rec.append({
|
||||||
|
"sym": sym, "tf": tf, "dir": 1 if up else -1,
|
||||||
|
"bars": i1 - i0,
|
||||||
|
"span_atr": span / a,
|
||||||
|
"atr_pct": a / float(cdf["close"].to_numpy(float)[i0]),
|
||||||
|
})
|
||||||
|
return pd.DataFrame(rec) if rec else None
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f" {sym} {tf} 失败: {type(e).__name__}: {e}", flush=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
TFO = {"5m": 0, "15m": 1, "1h": 2, "4h": 3}
|
||||||
|
|
||||||
|
|
||||||
|
def report(d: pd.DataFrame, cost: float) -> None:
|
||||||
|
print("\n" + "=" * 96)
|
||||||
|
print(f"【一】线段幅度 vs 确认成本(成本按一类实测的 {cost} ATR 计)")
|
||||||
|
print("=" * 96)
|
||||||
|
rows = []
|
||||||
|
for tf, x in sorted(d.groupby("tf"), key=lambda kv: TFO.get(kv[0], 9)):
|
||||||
|
q = x.span_atr.quantile([.25, .5, .75]).values
|
||||||
|
room = x.span_atr - cost
|
||||||
|
rows.append({
|
||||||
|
"级别": tf, "线段数": len(x),
|
||||||
|
"幅度Q1": round(q[0], 1), "幅度中位": round(q[1], 1),
|
||||||
|
"幅度Q3": round(q[2], 1),
|
||||||
|
"确认后剩余(中位)": round(q[1] - cost, 1),
|
||||||
|
f"剩余>止损{SL_ATR}": f"{(room > SL_ATR).mean()*100:.0f}%",
|
||||||
|
"剩余/止损": round((q[1] - cost) / SL_ATR, 2),
|
||||||
|
"中位时长(根)": int(x.bars.median()),
|
||||||
|
})
|
||||||
|
print(pd.DataFrame(rows).to_string(index=False))
|
||||||
|
print(f"""
|
||||||
|
「剩余/止损」是这张表的结论行:真底进场、扣掉确认成本后,还剩几倍止损的空间。
|
||||||
|
< 1 机会装不下确认成本,**换任何识别规则都没用**
|
||||||
|
1~2 勉强打平,要求选样精度极高(step60 实测最好 30%)
|
||||||
|
> 2 有空间,值得改规则/改入场构造""")
|
||||||
|
|
||||||
|
print("\n【二】把确认成本当变量:多低才够用")
|
||||||
|
rows = []
|
||||||
|
for tf, x in sorted(d.groupby("tf"), key=lambda kv: TFO.get(kv[0], 9)):
|
||||||
|
r = {"级别": tf}
|
||||||
|
for c in (0.0, 1.0, 2.0, 2.9):
|
||||||
|
r[f"成本{c}"] = round((x.span_atr.median() - c) / SL_ATR, 2)
|
||||||
|
rows.append(r)
|
||||||
|
print(pd.DataFrame(rows).to_string(index=False))
|
||||||
|
print("\n 成本 0 = 完美实时(在真底那根就进)。若连成本 0 那列都 < 2,"
|
||||||
|
"\n 说明**不是滞后的问题,是这个级别的线段本身就太小**。")
|
||||||
|
|
||||||
|
print("\n【三】逐币(中位幅度 ATR),看结论是否普适")
|
||||||
|
print(d.pivot_table(index="sym", columns="tf", values="span_atr",
|
||||||
|
aggfunc="median").round(1)
|
||||||
|
.reindex(columns=[t for t in TFO if t in set(d.tf)])
|
||||||
|
.to_string())
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--symbols", default="BTC,ETH,SOL,LINK,DOGE")
|
||||||
|
ap.add_argument("--tfs", default="5m,15m,1h,4h")
|
||||||
|
ap.add_argument("--rows", type=int, default=200_000)
|
||||||
|
ap.add_argument("--cost", type=float, default=2.9)
|
||||||
|
ap.add_argument("--workers", type=int, default=2)
|
||||||
|
ap.add_argument("--reuse", action="store_true")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.reuse and OUT.exists():
|
||||||
|
report(pd.read_feather(OUT), args.cost)
|
||||||
|
return
|
||||||
|
syms = [s.strip() for s in args.symbols.split(",")]
|
||||||
|
tfs = [t.strip() for t in args.tfs.split(",")]
|
||||||
|
parts = []
|
||||||
|
with ProcessPoolExecutor(max_workers=args.workers) as ex:
|
||||||
|
fut = {ex.submit(collect, s, t, args.rows): (s, t)
|
||||||
|
for s in syms for t in tfs}
|
||||||
|
for i, f in enumerate(as_completed(fut), 1):
|
||||||
|
r = f.result()
|
||||||
|
s, t = fut[f]
|
||||||
|
print(f" [{i}/{len(fut)}] {s} {t} "
|
||||||
|
f"{0 if r is None else len(r)}", flush=True)
|
||||||
|
if r is not None:
|
||||||
|
parts.append(r)
|
||||||
|
if not parts:
|
||||||
|
print("无结果")
|
||||||
|
return
|
||||||
|
d = pd.concat(parts, ignore_index=True)
|
||||||
|
OUT.parent.mkdir(exist_ok=True)
|
||||||
|
d.to_feather(OUT)
|
||||||
|
report(d, args.cost)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
"""改识别规则到底有没有用:把天花板一次性测到顶。
|
||||||
|
|
||||||
|
用户问「改 B1/B2 的识别规则呢」。step65 让这个问题变得可判定,但也制造了一个
|
||||||
|
必须解释的矛盾:
|
||||||
|
|
||||||
|
线段中位 11.4 ATR,进场花掉 2.9,剩 8.5 —— 是 2 ATR 止损的 4.26 倍。
|
||||||
|
可 step60 用未来函数选出真底的那批 B1,PF 仍只有 0.70~0.83。
|
||||||
|
|
||||||
|
空间明明在,就是拿不到。两种解释对用户的问题给出**相反**的答案:
|
||||||
|
|
||||||
|
选样问题:只有 30% 的 B1 落在真线段底,那 8.5 ATR 只存在于这 30% 里,
|
||||||
|
其余 70% 是趋势中途,后面根本没有行情 -> **改识别规则有用**
|
||||||
|
路径问题:就算落在真底,11.4 ATR 是净幅度,路上的回撤照样打掉止损
|
||||||
|
-> **改识别规则没用**
|
||||||
|
|
||||||
|
区分只需要一个从没跑过的组合:**未来函数选样 + 结构止损 + 绝对目标**。
|
||||||
|
三样单独都失败过(step60 的 0.83 / step58 的 0.36 / 本轮 abs 的 0.36),
|
||||||
|
组合起来没试过。它测的不是某条规则,是**所有识别规则的上界**:
|
||||||
|
|
||||||
|
上界 < 1.0 -> 路径问题,一类线彻底关闭,别再改规则
|
||||||
|
上界 > 1.5 -> 选样问题,改识别规则有用,且这里就是要够到的目标
|
||||||
|
|
||||||
|
同时量 MFE 做交叉验证:真底那批到底跑没跑出 8.5 ATR。
|
||||||
|
跑出来了却还是亏 -> 铁证是止损/路径
|
||||||
|
根本没跑出来 -> 说明 11.4 ATR 那个测量传导不到这批信号上,step65 要重解释
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
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))
|
||||||
|
|
||||||
|
SRC = HERE / "out" / "step64_b2.feather"
|
||||||
|
MAXB = 48
|
||||||
|
|
||||||
|
|
||||||
|
def mfe_mae(cdf: pd.DataFrame, idx: np.ndarray, dirs: np.ndarray,
|
||||||
|
atr: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
"""入场后 MAXB 根内最远的顺向/逆向行程(ATR)。"""
|
||||||
|
hi, lo = cdf["high"].to_numpy(float), cdf["low"].to_numpy(float)
|
||||||
|
op = cdf["open"].to_numpy(float)
|
||||||
|
n = len(cdf)
|
||||||
|
fe = np.full(len(idx), np.nan)
|
||||||
|
ae = np.full(len(idx), np.nan)
|
||||||
|
for k, (i, d, a) in enumerate(zip(idx, dirs, atr)):
|
||||||
|
e = int(i) + 1
|
||||||
|
if e >= n or not np.isfinite(a) or a <= 0:
|
||||||
|
continue
|
||||||
|
j = min(e + MAXB, n)
|
||||||
|
px = op[e]
|
||||||
|
if d == 1:
|
||||||
|
fe[k] = (hi[e:j].max() - px) / a
|
||||||
|
ae[k] = (px - lo[e:j].min()) / a
|
||||||
|
else:
|
||||||
|
fe[k] = (px - lo[e:j].min()) / a
|
||||||
|
ae[k] = (hi[e:j].max() - px) / a
|
||||||
|
return fe, ae
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--tf", default="5m")
|
||||||
|
ap.add_argument("--rows", type=int, default=200_000)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
from chanlun import TF_DF
|
||||||
|
from lib.data import fetch_ohlcv
|
||||||
|
from step58_struct_stop import simulate, stats
|
||||||
|
|
||||||
|
d = pd.read_feather(SRC)
|
||||||
|
d = d[(d.tf == args.tf) & d.type.isin(["B1", "S1"])].copy()
|
||||||
|
d["to_ext"] = (d.entry_px - d.own_ext) * d.dir / d.atr_at_entry
|
||||||
|
print(f"[识别规则天花板] {args.tf} · 一类 {len(d)} 笔 · "
|
||||||
|
f"其中落在真线段端点 {d.hit.sum()} 笔 ({d.hit.mean()*100:.1f}%)\n")
|
||||||
|
|
||||||
|
cache = {}
|
||||||
|
for sym in sorted(d.sym.unique()):
|
||||||
|
df = fetch_ohlcv(f"{sym}/USDT:USDT", args.tf, args.rows)
|
||||||
|
cache[sym] = TF_DF(df, 1, args.tf).dataframe
|
||||||
|
|
||||||
|
print("=" * 96)
|
||||||
|
print("【一】MFE 交叉验证:真底那批到底跑没跑出 step65 说的 8.5 ATR")
|
||||||
|
print("=" * 96)
|
||||||
|
rows = []
|
||||||
|
for k, g in d.groupby(d.hit):
|
||||||
|
fe, ae = [], []
|
||||||
|
for sym, x in g.groupby("sym"):
|
||||||
|
f_, a_ = mfe_mae(cache[sym], x.i_sure.values, x.dir.values,
|
||||||
|
x.atr_at_entry.values)
|
||||||
|
fe.append(f_)
|
||||||
|
ae.append(a_)
|
||||||
|
fe, ae = np.concatenate(fe), np.concatenate(ae)
|
||||||
|
ok = np.isfinite(fe)
|
||||||
|
rows.append({
|
||||||
|
"落在真线段端点": "是" if k else "否", "笔数": int(ok.sum()),
|
||||||
|
"MFE中位": round(float(np.median(fe[ok])), 2),
|
||||||
|
"MFE≥3ATR": f"{(fe[ok] >= 3).mean()*100:.0f}%",
|
||||||
|
"MFE≥8ATR": f"{(fe[ok] >= 8).mean()*100:.0f}%",
|
||||||
|
"MAE中位": round(float(np.median(ae[ok])), 2),
|
||||||
|
"MAE≥2ATR(会被现止损打掉)": f"{(ae[ok] >= 2).mean()*100:.0f}%",
|
||||||
|
})
|
||||||
|
print(pd.DataFrame(rows).to_string(index=False))
|
||||||
|
print("""
|
||||||
|
判读:若「是」那行 MFE 中位远低于 8.5,说明 step65 的线段幅度传导不到
|
||||||
|
信号上(进场时行情已经走掉了大半);若 MFE 够大而 MAE 也大,则是路径问题。""")
|
||||||
|
|
||||||
|
print("\n" + "=" * 96)
|
||||||
|
print("【二】天花板:未来函数选样 + 结构止损 + 绝对目标(三者首次组合)")
|
||||||
|
print("=" * 96)
|
||||||
|
rows = []
|
||||||
|
for k, g in d.groupby(d.hit):
|
||||||
|
for label, margin in [("固定2ATR", None), ("结构+0.5", 0.5),
|
||||||
|
("结构+1.0", 1.0), ("结构+1.5", 1.5)]:
|
||||||
|
gs, rs, cs, ap_, sl_ = [], [], [], [], []
|
||||||
|
for sym, x in g.groupby("sym"):
|
||||||
|
x = x.reset_index(drop=True)
|
||||||
|
t = pd.DataFrame({"entry_idx": x.i_sure.values,
|
||||||
|
"direction": x.dir.values,
|
||||||
|
"atr_at_entry": x.atr_at_entry.values})
|
||||||
|
sl = (np.full(len(x), 2.0) if margin is None
|
||||||
|
else (x.to_ext.values + margin))
|
||||||
|
res = simulate(cache[sym], t, sl, r_scale=False)
|
||||||
|
ok = res.g.notna().values
|
||||||
|
gs.append(res.g[ok])
|
||||||
|
rs.append(res.r[ok])
|
||||||
|
cs.append(res.c[ok])
|
||||||
|
ap_.append((x.atr_at_entry.values / x.entry_px.values)[ok])
|
||||||
|
sl_.append(sl[ok])
|
||||||
|
rows.append({
|
||||||
|
"落在真线段端点": "是" if k else "否", "止损": label,
|
||||||
|
**stats(pd.concat(gs, ignore_index=True),
|
||||||
|
pd.concat(rs, ignore_index=True),
|
||||||
|
pd.concat(cs, ignore_index=True),
|
||||||
|
np.concatenate(ap_), np.concatenate(sl_))})
|
||||||
|
print(pd.DataFrame(rows).to_string(index=False))
|
||||||
|
print("""
|
||||||
|
「是」那几行就是**任何识别规则的上界**(因为选样已经用了未来函数):
|
||||||
|
< 1.0 路径问题,一类线关闭,改规则无解
|
||||||
|
> 1.5 选样问题,改识别规则有用,且这就是要够到的目标""")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user