Files
Chan/research/step51_factor_sizing.py
T
jackyu66gitandCursor d4fbe9d905 量因子该做成开关不是权重;前序因子不该进仓位
把 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>
2026-08-28 05:08:34 +08:00

162 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()