开仓前那段:量要小、势要有但不能过头,换掉 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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user