开仓前那段:量要小、势要有但不能过头,换掉 vr60 那个开关

用户提出看开仓之前的量与趋势方向。这是第三个位置——step50 管信号根、
step52 管持仓中,这里管入场前。step48 采集端补 vpre10/vpre30/mom10/mom60。

先纠正一个错的心智模型(我和用户都以为的):B4/S4 不是回抽后进场。mom10
中位 +3.47 ATR,为负的只占 6%。信号触发时价格在前 10 根里已经顺着你的方向
走了三个多 ATR,2 ATR 止损是架在一段已经走完的行情后面。这也解释了 step50:
信号根是突破根,放量 = 追在最后一棒上。

① 入场前的量单调,越小越好(样本外毛R,Q1→Q4):1.474 / 1.266 / 1.156 /
0.573。控 vr60 后仍成立(低量层 −0.284、高量层 −0.541),与 vr60 相关只有
+0.301,不是同一件事换个说法。

② 入场前的动量是驼峰形,不是单调(mom60 样本外毛R,Q1→Q4):1.252 /
1.473 / 1.141 / 0.603。势要有——完全没动过的 Q1 也不如 Q2;但不能过头——
Q4 在发现期余量只剩 1.44bp,等于不能做。

驼峰形意味着中位数二分法会把它测没:控制表里 mom10 的毛R差是 +0.044,
看着无效,那是二分把 Q1+Q2 和 Q3+Q4 各自平均了。对非单调因子不要用中位数
分层做检验。

③ 砍 mom60≥7 全面优于 §3.39 定的砍 vr60≥4(发现期):

  等权          保留 100%  盈亏平衡 13.5bp  R夏普 0.325  回撤 16.8  总R 596.6
  砍 vr60≥4     保留  69%  盈亏平衡 15.6bp  R夏普 0.411  回撤 10.8  总R 514.9
  砍 mom60≥7    保留  78%  盈亏平衡 17.5bp  R夏普 0.476  回撤  7.7  总R 657.7

每一项都赢,还多留 9 个点的笔数。更要紧的是总R 比不砍还高——被砍掉那 22%
期望为负,砍掉不是花钱买稳健,是纯赚。样本外同向。所以这个开关不需要等
影子测量:它在 0~20bp 每一档都不输等权。

附滑点决策表:5~12bp 区间砍 mom60≥7 通吃,只有 ≥15bp 才该上「三个都砍」,
而那时策略本身已在生死线上。

阈值 7 和 1.5 是贴着 Q4 边界取的整数,不是搜出来的,但也不是完全无关于数据
(看过分位表才取的整),上线前应确认阈值附近没有断崖敏感。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-28 15:55:49 +08:00
co-authored by Cursor
parent 431e776905
commit 06928d1d5f
3 changed files with 298 additions and 8 deletions
+29 -2
View File
@@ -75,6 +75,26 @@ def collect(sym: str, rows: int) -> pd.DataFrame | None:
idx = sig["entry_idx"].to_numpy().astype(int)
atr = cdf["atr"].to_numpy(float)[idx]
close = cdf["close"].to_numpy(float)[idx]
vol = cdf["volume"]
vr = vol / vol.rolling(60, min_periods=10).mean()
vr60 = vr.to_numpy(float)
# shift(1) 把信号根自己排除在外
vpre10 = vr.rolling(10, min_periods=5).mean().shift(1).to_numpy(float)
vpre30 = vr.rolling(30, min_periods=15).mean().shift(1).to_numpy(float)
c_all = cdf["close"].to_numpy(float)
a_all = cdf["atr"].to_numpy(float)
d_all = sig["direction"].to_numpy().astype(int)
def mom(k: int) -> np.ndarray:
"""顺方向动量(ATR 单位)。用全序列算好再取下标,避免逐笔切片。"""
prev = np.concatenate([np.full(k, np.nan), c_all[:-k]])
with np.errstate(invalid="ignore", divide="ignore"):
m = (c_all - prev) / a_all
out = np.full(len(idx), np.nan)
out[:] = m[idx] * d_all
return out
return pd.DataFrame({
"sym": sym,
"date": cdf["date"].to_numpy()[idx],
@@ -91,8 +111,15 @@ def collect(sym: str, rows: int) -> pd.DataFrame | None:
# 信号根的相对成交量。当根已收盘,开仓时可知,是合规的可交易信息。
# vr10 是引擎自带口径(前 10 根均量),vr60 换个基准做稳健性对照。
"vr10": cdf["volume_ratio"].to_numpy(float)[idx],
"vr60": (cdf["volume"] / cdf["volume"].rolling(60, min_periods=10).mean()
).to_numpy(float)[idx],
"vr60": vr60[idx],
# 开仓**之前**那段的量与走势。vpre 已 shift(1),不含信号根本身——
# 信号根的量单独由 vr60 承担,两者混在一起就分不清是哪一段在起作用。
"vpre10": vpre10[idx],
"vpre30": vpre30[idx],
# 顺方向动量,ATR 为单位。B4/S4 是回抽后转强,所以 mom10 多为负
# (入场前那几根逆着你走);负得多 = 回抽深。
"mom10": mom(10),
"mom60": mom(60),
})
except Exception as e:
print(f" {sym} 失败: {e!r}", flush=True)