量因子该做成开关不是权重;前序因子不该进仓位

把 step49/50 两个分层结论做成仓位因子做组合层面评估,结果与分层表给人的印象
不一致,两个因子的命运相反。

方法上设了三道约束——分层结论转仓位规则最容易在这三处翻车:

  1 阈值取整数(vr60 切 1/2/4)、权重取整数比,一个参数都不搜。观测到的四分位
    边界在不同时段并不一致(样本外 0.63/1.26/2.28/5.34、发现期 0.62/1.48/
    3.11/7.97),用样本分位数等于在同一批数据上既发现又调参
  2 权重归一化到均值 1,各方案投出去的平均资金相同,均R 才可比
  3 判据是四项一起看:均R / R夏普 / 最大回撤 / 峰值加权并发。只看均值必然误判

全样本 3941 笔,假定滑点 5bp/taker 腿:

                     盈亏平衡滑点  R夏普  回撤R  峰值并发  总R/峰值并发
  等权(现状)           16.7bp   0.420  17.3    7.00      371.4
  仅量因子(加权)       18.2bp   0.442  12.7    7.98      367.4
  仅前序因子             17.0bp   0.421  19.2    8.46      312.6
  硬砍高量档(vr60≥4)   18.6bp   0.496  11.2    6.00      389.3
  硬砍 + 前序权重        19.0bp   0.500  11.0    7.19      332.1

① 前序因子不能做仓位。+6bp 余量在 5bp 滑点假设下只值约 0.011R,而这批信号
按定义 100% 发生在别的币已有仓位时——加权就是在敞口最集中的时刻加杠杆。
盈亏平衡只买到 +0.3bp,峰值并发 7.00→8.46、回撤 17.3→19.2,按峰值保证金归一
后是净负的(371→313)。§3.31 里「可用于加仓位权重」那句作废。出路可能是改用
放宽 ATR 门控兑现(多做几笔而非每笔做大),未测。

② 量因子「不做」优于「少做」。硬砍付笔数 −22%、总R −10%,换回撤 −35% 和峰值
并发 7→6。并发这一项单独就值:§3.31 已把峰值敞口列为扩币的前置约束。

③ 优势的形态是削尾不是抬均值。均R 差在各滑点档几乎恒定(0.085→0.082),但
基数在塌,所以相对优势随成本上升放大:15bp 处总R/回撤 2.57→11.86,靠的是回撤
从 150.5 掉到 60.3。这两个因子买的是尾部风险,不是收益。

可操作口径要用发现期:盈亏平衡滑点样本外 18.2bp、发现期只有 13.5bp(硬砍后
15.6bp),差距就是 2026 的 ATR 压缩。13.5 与 shadow_budget 的 15.19bp 同量级,
互为印证。影子测量要对标 13.5 / 15.6,不是 18.6。

开关先不上:只是 live 信号路径加一行,随时能加。等实测滑点出来再定——远低于
13.5bp 则等权就够,贴着 13.5bp 则这 2.1bp 就是生死线。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-28 05:08:34 +08:00
co-authored by Cursor
parent ef584b8d49
commit d4fbe9d905
2 changed files with 235 additions and 8 deletions
+161
View File
@@ -0,0 +1,161 @@
"""Step 51:把 §3.31 的两个分层结论做成仓位因子,看组合层面到底有没有变好。
已知(都用开仓时可知的信息):
step49 `有前序`(过去 5 分钟别的币先发过)→ 滑点余量 +6bp,6/6 时段同向
step50 信号根低量(vr60 小)→ 余量 +14bpATR 4/4 + 时段 7/7 同向
但「分层看着好」到「加权之后组合更好」不是一回事,中间有三个坎:
1. 两个结论是分开切出来的,相乘等于假设正交且效应可乘。没验过。
2. 加权必然抬高方差。均R 涨了但 R夏普 可能不涨,那就白做。
3. `有前序` 按定义就是「别的币已有仓位时」,给它加权 = 在敞口最集中的
时刻再加杠杆。§3.31 已经把峰值并发列为扩币的前置约束,这里直接顶上去。
所以本步的判据不是均R,是这四项一起看:**均R / R夏普 / 最大回撤 / 峰值加权并发**。
阈值刻意取整数(vr60 的 1、2、4),权重取整数比,**一个参数都不搜**。
观测到的四分位边界在不同时段并不一致(样本外 0.63/1.26/2.28/5.34、
发现期 0.62/1.48/3.11/7.97),用样本分位数等于在同一批数据上既发现又调参。
主检验是滑点敏感性:这两个因子的优势主要在**余量**而非毛R,
所以真正该看的是「假定执行成本越高,加权方案是否衰减得越慢」。
若只在 slip=0 处好、随成本上升优势消失,那这两个因子就不值得做进仓位。
"""
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))
pd.set_option("display.width", 340)
SL = 2.0
GATE_BP = 8.0
IS_START = pd.Timestamp("2026-01-30", tz="Asia/Shanghai")
SLIPS = (0.0, 5.0, 10.0, 15.0, 20.0) # bp / taker 腿
def w_vol(vr: np.ndarray) -> np.ndarray:
"""量比越低给越多。整数阈值 1/2/4,权重 6:5:3:2(均值约 1)。"""
w = np.full(len(vr), 1.0)
w[vr < 1.0] = 1.5
w[(vr >= 1.0) & (vr < 2.0)] = 1.25
w[(vr >= 2.0) & (vr < 4.0)] = 0.75
w[vr >= 4.0] = 0.5
w[~np.isfinite(vr)] = 1.0 # 前 60 根不足时不表态
return w
def w_prev(prev: np.ndarray) -> np.ndarray:
return np.where(prev >= 1, 1.4, 1.0)
def w_cut(vr: np.ndarray) -> np.ndarray:
"""硬砍最高量那一档。用来看「不做」和「少做」差多少。"""
w = np.ones(len(vr))
w[np.isfinite(vr) & (vr >= 4.0)] = 0.0
return w
def peak_concurrent(d: pd.DataFrame, w: np.ndarray) -> float:
"""扫描线求峰值同时在场的加权敞口(单位:等权仓位数)。"""
t0 = d.date.values.astype("datetime64[m]").astype(np.int64)
t1 = t0 + d.hold.values
ev = np.concatenate([np.stack([t0, w], 1), np.stack([t1, -w], 1)])
ev = ev[np.lexsort((ev[:, 1], ev[:, 0]))]
return float(np.max(np.cumsum(ev[:, 1])))
def evaluate(d: pd.DataFrame, w: np.ndarray, slip_bp: float) -> dict:
"""slip_bp 只加在 taker 腿上——止盈是限价,不吃滑点。"""
from lib.exit_model import fee_of, taker_notional
tn = taker_notional(d.r.values, d.c.values)
net = d.g.values - fee_of(d.r.values, d.c.values) - slip_bp * 1e-4 * tn
R = net / (SL * d.atr_pct.values)
m = w > 0
if m.sum() < 50:
return {}
ww = w[m] / w[m].mean() # 归一化:各方案投出去的平均资金相同
wR = ww * R[m]
eq = np.cumsum(wR)
dd = float(np.max(np.maximum.accumulate(eq) - eq))
return {
"笔数": int(m.sum()),
"均R": round(float(wR.mean()), 3),
"R夏普": round(float(wR.mean() / wR.std()), 3),
"总R": round(float(eq[-1]), 1),
"最大回撤R": round(dd, 1),
"总R/回撤": round(eq[-1] / dd, 2) if dd > 0 else np.inf,
"余量bp": round(float((ww * net[m]).sum() / (ww * tn[m]).sum() * 1e4), 2),
"峰值并发": round(peak_concurrent(d[m], ww), 2),
}
def run(d: pd.DataFrame, label: str) -> pd.DataFrame:
vr, pv = d.vr60.values, d.prev.values
schemes = {
"等权(现状)": np.ones(len(d)),
"仅量因子": w_vol(vr),
"仅前序因子": w_prev(pv),
"两因子相乘": w_vol(vr) * w_prev(pv),
"硬砍高量档": w_cut(vr),
}
rows = []
for slip in SLIPS:
for name, w in schemes.items():
r = evaluate(d, w, slip)
if r:
rows.append({"时段": label, "滑点bp": slip, "方案": name, **r})
return pd.DataFrame(rows)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--src", default=str(HERE / "out" / "step50_volume.feather"))
args = ap.parse_args()
from step50_volume import prep
d = pd.read_feather(args.src)
d["date"] = pd.to_datetime(d["date"])
d = prep(d[(d.htf == 1.0) & d.lad & (d.atr_bp >= GATE_BP)].copy())
oos, ins = d[d.date < IS_START], d[d.date >= IS_START]
print(f"实盘口径 {len(d)} 笔 | 样本外 {len(oos)} 发现期 {len(ins)}")
for lab, part in (("样本外", oos), ("发现期", ins), ("全样本", d)):
t = run(part, lab)
print("\n" + "=" * 118)
print(f"########## {lab} ##########")
for slip in SLIPS:
g = t[t.滑点bp == slip].drop(columns=["时段", "滑点bp"])
print(f"\n--- 假定滑点 {slip:.0f}bp / taker 腿 ---")
print(g.to_string(index=False))
print("\n" + "=" * 118)
print("########## 判据:优势是否随执行成本上升而扩大 ##########")
t = run(d, "全样本")
base = t[t.方案 == "等权(现状)"].set_index("滑点bp")
rows = []
for name in t.方案.unique():
if name == "等权(现状)":
continue
g = t[t.方案 == name].set_index("滑点bp")
rows.append({"方案": name, **{
f"{int(s)}bp处均R差": round(g.loc[s, "均R"] - base.loc[s, "均R"], 3)
for s in SLIPS}})
print(pd.DataFrame(rows).to_string(index=False))
if __name__ == "__main__":
main()