成交量假设方向是反的:高量入场毛R 0.794,低量 1.421

用户假设「有资金的趋势才是好趋势」,预期开仓根成交量越大越好。成交量在信号根
收盘时可知,符合 step48 立的「只用开仓时已知信息」纪律,是合法的可交易切法。
10 币 × 80 万根、实盘口径 3941 笔,结论与假设相反。

按 vr60(当根量 / 前 60 根均量)四分位:

  量最低(中位 0.63)  毛R 1.421  余量 27.18bp   ← 样本外
  量最高(中位 5.34)  毛R 0.794  余量 13.47bp

单调递减,且样本内外、两套量比基准(前 10 根 / 前 60 根)全部同向。稳健性达到
step49 那条的标准:ATR 四分位 4/4 同向、逐时段 7/7 同向,不是 ATR 换脸。

机制在出场结构里,伤害全在止损命中率:

  量最低  止盈 33.5%  止损 22.5%  超时 44.0%  赢时均R 1.830  亏时均R −1.098
  量最高  止盈 26.1%  止损 45.3%  超时 28.6%  赢时均R 1.651  亏时均R −1.106

亏损幅度四档全是 −1.10(止损就是止损),赢时均R 只降 10%,止损率翻倍是全部
损失来源。这里有个判别点:若只是「2 ATR 止损相对突然放大的波动太窄」的尺度
错配,超时单应按原比例分流进止盈和止损两侧;实际是超时(−15.4pp)和止盈
(−7.4pp)一起流进止损(+22.8pp)。方向本身在变差,不只是止损太窄。

为什么直觉会反:B4/S4 在突破根上进场。大量根意味着这一冲已经由别人的资金
完成,你在它的收盘价接手。「有资金」要能获利必须在资金到达之前进场,不是同时。

与「有前序」是两件独立的事(有前序组 vr10 中位 1.76 vs 无前序 1.46),可叠加:
低量 × 有前序 253 笔,毛R 1.398、余量 31.00bp,是目前见过最宽的执行容忍度。

step48 的采集加 vr10/vr60 两列。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-28 05:08:03 +08:00
co-authored by Cursor
parent 2b9a837a58
commit ef584b8d49
3 changed files with 256 additions and 0 deletions
+61
View File
@@ -342,6 +342,54 @@ ATR 混淆已排除:`有前序` 的 ATR 确实略高(中位 15.04 vs 13.13bp
> 时间验证。** 目前可确定的只有「扎堆整体更好」(簇级也成立); > 时间验证。** 目前可确定的只有「扎堆整体更好」(簇级也成立);
> 「好处全集中在错开那一档」尚待样本外确认。 > 「好处全集中在错开那一档」尚待样本外确认。
**❌ 信号根成交量:假设方向是反的,高量入场明显更差(step50)**
用户假设「有资金的趋势才是好趋势」,预期开仓根成交量越大越好。
成交量在信号根收盘时可知,符合上面那条纪律,是合法的可交易切法。
同一批 3941 笔,按信号根相对成交量四分位:
| vr60 分位(当根量 / 前 60 根均量) | 样本外毛R | 样本外余量 | 发现期毛R | 发现期余量 |
|---|---|---|---|---|
| 量最低(中位 0.63 | **1.421** | **27.18bp** | 1.192 | 20.91bp |
| 量中低 | 1.187 | 20.37bp | 1.133 | 19.80bp |
| 量中高 | 1.067 | 18.69bp | 0.841 | 13.30bp |
| 量最高(中位 5.34 | 0.794 | 13.47bp | 0.634 | 7.94bp |
**单调递减,且样本内外、两套量比基准(前 10 根 / 前 60 根)全部同向。**
稳健性达到了 step49 那条的标准:**ATR 四分位 4/4 同向**(层内毛R 差
0.327 / 0.238 / 0.347 / 0.592),**逐时段 7/7 同向**。不是 ATR 换脸。
机制在出场结构里,**伤害集中在止损命中率**:
| vr60 分位 | 止盈% | 止损% | 超时% | 到 3ATR 减仓率 | 赢时均R | 亏时均R |
|---|---|---|---|---|---|---|
| 量最低 | 33.5 | 22.5 | 44.0 | 77.6% | 1.830 | 1.098 |
| 量最高 | 26.1 | **45.3** | 28.6 | 60.9% | 1.651 | 1.106 |
亏损幅度四档全是 −1.10(止损就是止损),赢时均R 只降 10%。**止损率翻倍**
是全部损失来源。这里有个判别点:若只是「2 ATR 止损相对突然放大的波动太窄」
的尺度错配,超时单应按原比例分流进止盈和止损两侧;实际是超时(−15.4pp)
**和止盈(−7.4pp)一起**流进止损(+22.8pp)。**方向本身在变差,不只是止损太窄**
所以对策是回避而非放宽止损。
为什么直觉会反:B4/S4 是**在突破根上进场**的。大量根意味着这一冲已经由别人
的资金完成了,你在它的收盘价接手——高量不是「资金在推你」,是「你在给推
完的人接盘」。用户的直觉在中长周期趋势跟随上成立,但这个入场点站在它的
对面。**「有资金」要能获利,必须在资金到达之前进场,不是同时。**
`有前序` 是两件独立的事,不是同一效应换个说法(相关性很弱:有前序组
vr10 中位 1.76 vs 无前序 1.46)。2×2 交叉,最好的一档是两者叠加:
| | 笔数 | 占比 | 毛R | 余量bp |
|---|---|---|---|---|
| 低量 × 有前序 | 253 | 6% | **1.398** | **31.00bp** |
| 低量 × 无前序 | 1717 | 44% | 1.092 | 18.35bp |
| 高量 × 有前序 | 382 | 10% | 1.074 | 19.62bp |
| 高量 × 无前序 | 1589 | 40% | 0.988 | 16.39bp |
> 尚未做的:把量比做成硬过滤(如砍掉最高四分位)要付 25% 的笔数,
> 而 1m 本来就稀疏(10 币约 2.5 笔/天)。**先当仓位权重用,别当开关。**
**TRX 应从币池剔除**:实盘口径 208 天只有 **5 笔**ATR 门控几乎全刷掉 **TRX 应从币池剔除**:实盘口径 208 天只有 **5 笔**ATR 门控几乎全刷掉
(与 §5.3「2026 只剩 4.2%」一致)。实际可用是 10 个币,不是 11 个。 (与 §5.3「2026 只剩 4.2%」一致)。实际可用是 10 个币,不是 11 个。
逐币每天笔数:DOGE/AVAX/SOL 约 0.66BTC/LTC 0.45BNB 0.31TRX 0.02。 逐币每天笔数:DOGE/AVAX/SOL 约 0.66BTC/LTC 0.45BNB 0.31TRX 0.02。
@@ -763,6 +811,7 @@ step41 首轮跑的是错误的 3/1bp(见 §1.3),已用实际费率重算
| 刷交易额换 VIP 费率 | 成本收益不划算,见 5.3。**但前提已变**:该结论算于「原始 taker 6bp、全 taker、TP=3.0、无门控」,四个前提现在全不成立(实际 2/0.8bp、混合费率、分批出场、有 ATR 门控)。已在返 50% 的档位上,继续压缩的空间本就有限,但**若要重估需重跑,不要直接引用旧结论** | step23 | | 刷交易额换 VIP 费率 | 成本收益不划算,见 5.3。**但前提已变**:该结论算于「原始 taker 6bp、全 taker、TP=3.0、无门控」,四个前提现在全不成立(实际 2/0.8bp、混合费率、分批出场、有 ATR 门控)。已在返 50% 的档位上,继续压缩的空间本就有限,但**若要重估需重跑,不要直接引用旧结论** | step23 |
| **按 ATR 从大到小选币 / 追高 ATR 时刻** | 门控之上 ATR 高不加分。**币内严格单调递减**:ATR 最低 20% 分位净均R 1.040,最高 20% 只有 0.878。跨币看似正相关(r=+0.684)但**剔除 BTC 后掉到 +0.469**,其余 9 币极差仅 0.089。ATR 唯一真正给的是 bp 余量(机械关系)。且追高 ATR 会撞上流动性墙——现有 11 币全是头部流动性,外推到小市值币时多出的余量会被滑点吃回去。**ATR 是及格线不是排序键** | §3.33 | | **按 ATR 从大到小选币 / 追高 ATR 时刻** | 门控之上 ATR 高不加分。**币内严格单调递减**:ATR 最低 20% 分位净均R 1.040,最高 20% 只有 0.878。跨币看似正相关(r=+0.684)但**剔除 BTC 后掉到 +0.469**,其余 9 币极差仅 0.089。ATR 唯一真正给的是 bp 余量(机械关系)。且追高 ATR 会撞上流动性墙——现有 11 币全是头部流动性,外推到小市值币时多出的余量会被滑点吃回去。**ATR 是及格线不是排序键** | §3.33 |
| **`available_ts` 改取 `bis[2]`(中枢成立即可用)以消除右边缘重画** | 重画确实从 6.5% 降到 1.2%、滞后 2.16→2.01**但 alpha 被打成零**:实盘口径毛 R 0.933 → **0.000**(扣费前就没了 edge),PF 3.22→0.80,余量 15.07→−2.20bp,8/8 币全变差。中枢没发育完就下注,支撑/压力还没立住。**「等中枢最后一笔」就是 alpha 本身,重画是必付代价。**推论:任何以「让信号更早确定」为目标的改动,先测毛 R,不要只看重画率和滞后 | step47 §5.42 | | **`available_ts` 改取 `bis[2]`(中枢成立即可用)以消除右边缘重画** | 重画确实从 6.5% 降到 1.2%、滞后 2.16→2.01**但 alpha 被打成零**:实盘口径毛 R 0.933 → **0.000**(扣费前就没了 edge),PF 3.22→0.80,余量 15.07→−2.20bp,8/8 币全变差。中枢没发育完就下注,支撑/压力还没立住。**「等中枢最后一笔」就是 alpha 本身,重画是必付代价。**推论:任何以「让信号更早确定」为目标的改动,先测毛 R,不要只看重画率和滞后 | step47 §5.42 |
| **挑「信号根成交量大」的信号(有资金推的趋势)** | **方向完全相反**vr60 四分位上毛R 单调 1.421→0.794、余量 27.18→13.47bp,样本内外 / 两套量比基准 / ATR 4 分位 4/4 / 逐时段 7/7 全部同向。止损率 22.5%→45.3% 翻倍,且**止盈率同时下降**(说明是方向变差,不是止损太窄)。根因:B4/S4 在突破根上进场,大量根 = 这一冲已经由别人的资金走完,你在收盘价接盘。**低量那一档才是好的**,可反向用作仓位权重 | step50 §3.31 |
| 线段(`Chan_XD`)做大级别 | 滞后太大,且中枢极少 | 早期,用户也这么说 | | 线段(`Chan_XD`)做大级别 | 滞后太大,且中枢极少 | 早期,用户也这么说 |
--- ---
@@ -1600,6 +1649,13 @@ API 限流风险隔离三个好处。
| 40 | 第四类买卖点的过滤消融(web 端原始 vs 过滤) | | 40 | 第四类买卖点的过滤消融(web 端原始 vs 过滤) |
| 41 | **出场口径全扫**SL × TP × MAX_BARS × 分批 × 剩余半仓止损位 + 真实费率模型)。TP=3 截早了;分批要做但止损保持原位;SL=1.0 最差 | | 41 | **出场口径全扫**SL × TP × MAX_BARS × 分批 × 剩余半仓止损位 + 真实费率模型)。TP=3 截早了;分批要做但止损保持原位;SL=1.0 最差 |
| 42 | **1m 单独定出场**(同一张全网格,11 币 7 年 25476 笔)。1m 右尾是其他级别 2 倍,TP 不能混用;`SL2 分批→8ATR 留损2 48根` 在均R/R夏普/回撤/剔10%PF 四项全赢,滑点余量 11.06→20.12bp`MAX_BARS=48` 反而不用改 | | 42 | **1m 单独定出场**(同一张全网格,11 币 7 年 25476 笔)。1m 右尾是其他级别 2 倍,TP 不能混用;`SL2 分批→8ATR 留损2 48根` 在均R/R夏普/回撤/剔10%PF 四项全赢,滑点余量 11.06→20.12bp`MAX_BARS=48` 反而不用改 |
| 44 | **1m 过滤消融**:浅色信号毛R 只有深色的 1/6,差在质量不在成本,不能做 |
| 45 | 重画审计脚本(时点重建,深色口径)——待跑 |
| 46 | **引擎逐位对拍基线**klu/klc/bi/seg/zs/信号/消费列哈希),重构安全网 |
| 47 | **`available_ts` bis[2] A/B** → 作废,毛R 被打成零(§5.42) |
| 48 | 多币开仓时间分布、并发、簇内顺序(首发是未来信息) |
| 49 | **「有前序」样本外验证**(10 币 80 万根)→ 通过,余量 +6bp,6/6 时段同向 |
| 50 | **信号根成交量**(用户假设「有资金的趋势才是好趋势」)→ **方向相反**,高量毛R 0.794 vs 低量 1.421ATR 4/4 + 时段 7/7 同向;根因是在突破根上给别人接盘 |
输出都在 `research/out/`。**2026-08-27 清理过一轮**step1~20 的输出(早期方法论 输出都在 `research/out/`。**2026-08-27 清理过一轮**step1~20 的输出(早期方法论
已被推翻,含未来函数偏差)与旧口径备份一并删除,只留支撑当前结论的证据。 已被推翻,含未来函数偏差)与旧口径备份一并删除,只留支撑当前结论的证据。
@@ -1688,3 +1744,8 @@ API 限流风险隔离三个好处。
**这条是扩币的前置条件**——加币不分散,只把峰值敞口推得更高 **这条是扩币的前置条件**——加币不分散,只把峰值敞口推得更高
- [ ] 若要扩币:按 3.33 的两条标准筛(ATR 对门控 + 流动性), - [ ] 若要扩币:按 3.33 的两条标准筛(ATR 对门控 + 流动性),
并留一批筛完不看、直接进实盘验证,以保住样本外的说服力 并留一批筛完不看、直接进实盘验证,以保住样本外的说服力
- [ ] **仓位权重现在有两个已验证的输入**(§3.31):`有前序` +6bp 余量、
`低量根` +14bp 余量,且两者独立(低量×有前序 = 31.00bp,最好的一档)。
**都还没做成规则**。做之前要想清楚两件事:一是 1m 本来就稀疏
(10 币约 2.5 笔/天),砍掉高量四分位要付 25% 笔数;
二是权重方案必须整体回测一遍,不能把两个分层结论直接相乘
+5
View File
@@ -88,6 +88,11 @@ def collect(sym: str, rows: int) -> pd.DataFrame | None:
"r": res[f"{cfg}_r"].to_numpy(), "r": res[f"{cfg}_r"].to_numpy(),
"c": res[f"{cfg}_c"].to_numpy(), "c": res[f"{cfg}_c"].to_numpy(),
"atr_pct": atr / close, "atr_pct": atr / close,
# 信号根的相对成交量。当根已收盘,开仓时可知,是合规的可交易信息。
# 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],
}) })
except Exception as e: except Exception as e:
print(f" {sym} 失败: {e!r}", flush=True) print(f" {sym} 失败: {e!r}", flush=True)
+190
View File
@@ -0,0 +1,190 @@
"""Step 50:信号根的成交量是否预测质量 ——「有资金的趋势才是好趋势」。
用户假设:开仓时刻的成交量越大,说明有真金白银在推,趋势更可信。
这个假设满足 §3.31 立的纪律:信号根已收盘,其成交量在开仓那一刻可知,
是合规的可交易信息,不像「我是簇里第几个」那样含未来。
只测两件事,不做穷举(组合空间大,多测必出假阳性):
主假设 相对成交量越高,毛R 与滑点余量越好
次假设 它与 §3.31 的「有前序信号」是同一件事,还是两件独立的事
口径与 step48/49 一致。发现期(2026-01-30 起)与样本外分开报,
主假设若只在其中一边成立就不算通过。
"""
from __future__ import annotations
import argparse
import os
import sys
import warnings
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
import numpy as np
import pandas as pd
warnings.filterwarnings("ignore")
for v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"):
os.environ.setdefault(v, "1")
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
WIN_MIN = 5
OUT = HERE / "out" / "step50_volume.feather"
IS_START = pd.Timestamp("2026-01-30", tz="Asia/Shanghai")
def collect(sym: str, rows: int):
import warnings as _w
_w.filterwarnings("ignore")
sys.path.insert(0, str(HERE))
sys.path.insert(0, str(HERE.parent))
from step48_signal_timing import collect as _c
return _c(sym, rows)
def prep(d: pd.DataFrame) -> pd.DataFrame:
from lib.exit_model import fee_of, taker_notional
d = d.sort_values("date").reset_index(drop=True)
t = d.date.values.astype("datetime64[m]").astype(np.int64)
d["prev"] = np.searchsorted(t, t, "left") - np.searchsorted(t, t - WIN_MIN, "left")
net = d.g.values - fee_of(d.r.values, d.c.values)
d["net"] = net
d["gR"] = d.g.values / (SL * d.atr_pct.values)
d["R"] = net / (SL * d.atr_pct.values)
d["tn"] = taker_notional(d.r.values, d.c.values)
return d
def stat(g: pd.DataFrame, lab: str, denom: int) -> dict:
if len(g) < 25:
return {"分组": lab, "笔数": len(g), "备注": "样本不足"}
w, o = g.net[g.net > 0].sum(), -g.net[g.net <= 0].sum()
return {"分组": lab, "笔数": len(g), "占比": f"{len(g)/denom*100:.0f}%",
"胜率": f"{(g.net > 0).mean()*100:.1f}%",
"毛R": round(g.gR.mean(), 3), "净均R": round(g.R.mean(), 3),
"PF": round(w / o, 2) if o > 0 else np.inf,
"余量bp": round(g.net.mean() / g.tn.mean() * 1e4, 2)}
def by_volume(d: pd.DataFrame, col: str, label: str) -> None:
x = d[d[col].notna() & np.isfinite(d[col])]
if len(x) < 200:
print(f" {label}: 样本不足")
return
x = x.copy()
x["bin"] = pd.qcut(x[col], 4, labels=["量最低", "量中低", "量中高", "量最高"])
rows = [stat(g, str(b), len(x)) for b, g in x.groupby("bin", observed=True)]
t = pd.DataFrame(rows)
med = x.groupby("bin", observed=True)[col].median().round(2).to_dict()
t.insert(1, f"{col}中位", [med.get(b) for b in t["分组"]])
print(f"\n--- {label} ---")
print(t.to_string(index=False))
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--symbols", default="BTC,BNB,ETH,SOL,LINK,LTC,AVAX,XRP,DOGE,ADA")
ap.add_argument("--rows", type=int, default=800_000)
ap.add_argument("--workers", type=int, default=3)
ap.add_argument("--reuse", action="store_true")
args = ap.parse_args()
if args.reuse and OUT.exists():
d = pd.read_feather(OUT)
else:
syms = [s.strip() for s in args.symbols.split(",")]
print(f"[信号根成交量] {len(syms)}× {args.rows} 根 1m\n", flush=True)
parts = []
with ProcessPoolExecutor(max_workers=args.workers) as ex:
fut = {ex.submit(collect, s, args.rows): s for s in syms}
for i, f in enumerate(as_completed(fut), 1):
r = f.result()
print(f" [{i}/{len(syms)}] {fut[f]} {0 if r is None else len(r)}", flush=True)
if r is not None:
parts.append(r)
if not parts:
print("无结果")
return
d = pd.concat(parts, ignore_index=True)
d.to_feather(OUT)
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"\n实盘口径 {len(d)} 笔 | 样本外 {len(oos)} 发现期 {len(ins)}")
print("\n" + "=" * 104)
print("########## 主假设:相对成交量越高越好? ##########")
for lab, part in (("样本外", oos), ("发现期", ins)):
print(f"\n===== {lab} =====")
by_volume(part, "vr10", f"{lab} / 量比 vs 前 10 根")
by_volume(part, "vr60", f"{lab} / 量比 vs 前 60 根(换基准对照)")
print("\n" + "=" * 104)
print("########## 次假设:成交量与「有前序」是不是同一件事 ##########")
x = d[d.vr10.notna() & np.isfinite(d.vr10)].copy()
x["高量"] = x.vr10 >= x.vr10.median()
x["有前序"] = x.prev >= 1
print("\n2×2(全样本)")
rows = []
for hv in (False, True):
for pv in (False, True):
g = x[(x.高量 == hv) & (x.有前序 == pv)]
rows.append(stat(g, f"{'高量' if hv else '低量'} × {'有前序' if pv else '无前序'}", len(x)))
print(pd.DataFrame(rows).to_string(index=False))
print("\n" + "=" * 104)
print("########## 混淆排查:量效应是不是 ATR 效应换了张脸 ##########")
y = d[d.vr60.notna() & np.isfinite(d.vr60)].copy()
y["atrQ"] = pd.qcut(y.atr_bp, 4, labels=["ATR-Q1", "Q2", "Q3", "Q4"])
rows = []
for q, g in y.groupby("atrQ", observed=True):
g = g.copy()
g["vq"] = pd.qcut(g.vr60, 2, labels=["低量", "高量"])
lo, hi = g[g.vq == "低量"], g[g.vq == "高量"]
if min(len(lo), len(hi)) < 25:
continue
rows.append({"ATR分位": str(q), "笔数": len(g),
"低量毛R": round(lo.gR.mean(), 3), "高量毛R": round(hi.gR.mean(), 3),
"毛R差": round(hi.gR.mean() - lo.gR.mean(), 3),
"低量余量": round(lo.net.mean() / lo.tn.mean() * 1e4, 2),
"高量余量": round(hi.net.mean() / hi.tn.mean() * 1e4, 2),
"方向": "低量更好" if lo.gR.mean() > hi.gR.mean() else "高量更好"})
print(pd.DataFrame(rows).to_string(index=False))
print("\n########## 逐时段稳定性(与 step49 同一根标尺)##########")
y["半年"] = y.date.dt.to_period("2Q").astype(str)
rows = []
for p, g in y.groupby("半年", observed=True):
if len(g) < 120:
continue
g = g.copy()
g["vq"] = pd.qcut(g.vr60, 2, labels=["低量", "高量"])
lo, hi = g[g.vq == "低量"], g[g.vq == "高量"]
rows.append({"时段": p, "笔数": len(g),
"低量毛R": round(lo.gR.mean(), 3), "高量毛R": round(hi.gR.mean(), 3),
"低量余量": round(lo.net.mean() / lo.tn.mean() * 1e4, 2),
"高量余量": round(hi.net.mean() / hi.tn.mean() * 1e4, 2),
"方向": "低量更好" if lo.gR.mean() > hi.gR.mean() else "高量更好"})
t = pd.DataFrame(rows)
print(t.to_string(index=False))
if len(t):
print(f"\n{len(t)} 个时段中 {(t.方向 == '低量更好').sum()} 个低量更好")
print("\n两者的相关性")
print(f" 有前序组 vr10 中位 {x[x.有前序].vr10.median():.2f}"
f" vs 无前序组 {x[~x.有前序].vr10.median():.2f}")
print(f" 高量组里有前序占 {x[x.高量].有前序.mean()*100:.1f}%"
f" vs 低量组 {x[~x.高量].有前序.mean()*100:.1f}%")
if __name__ == "__main__":
main()