把 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>
162 lines
6.1 KiB
Python
162 lines
6.1 KiB
Python
"""Step 51:把 §3.31 的两个分层结论做成仓位因子,看组合层面到底有没有变好。
|
||
|
||
已知(都用开仓时可知的信息):
|
||
step49 `有前序`(过去 5 分钟别的币先发过)→ 滑点余量 +6bp,6/6 时段同向
|
||
step50 信号根低量(vr60 小)→ 余量 +14bp,ATR 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()
|