持仓中放量不是出场信号,恰恰是最好那批单子的标记
用户问:既然 step50 证明入场根放量是接盘,那持仓中出现放量根是不是也说明 这一波走完了、该直接平掉?测下来方向是反的,且比入场那条还干净。 规则:持仓期间任一根 vr60 ≥ 阈值就收盘市价平掉(taker)。同根内优先级 止损(盘中)> 目标(盘中)> 放量平仓(收盘)。10 币 3941 笔: 基线(不看量) 毛R 1.068 R夏普 0.627 PF 3.80 余量 18.45bp 均持仓 30.0 vr60≥3 就平 毛R 0.617 R夏普 0.459 PF 2.81 余量 7.81bp 均持仓 11.1 vr60≥5 就平 毛R 0.858 R夏普 0.568 PF 3.37 余量 12.85bp 均持仓 19.8 vr60≥8 就平 毛R 1.009 R夏普 0.617 PF 3.69 余量 16.68bp 均持仓 26.3 阈值越高、触发越少就越接近基线——这条曲线的最优点是「永不触发」,规则纯扣分。 「浮盈才平」的变体把胜率抬到 74.4%(基线 71.7%)而毛R 掉到 0.655,是过早 止盈的教科书特征:胜率上升、期望下降。 根因:vr60≥5 触发的 1769 笔若不平,止盈率 45.7%、止损率 16.6%,而全体基线 是 30.1% / 33.1%。持仓中的放量根标记的是最好的那批单子,平掉每笔让出 +0.484R。 幸存者偏差已控——按基线持仓 ≥K 根分层后 5/5 档同向,放量组止盈率约为无量组 两倍(K=5 时 41.2% vs 19.8%)。 同一个事件入场为负、持仓为正,区别只在站在它的哪一边:入场那根放量你是买方, 持仓中那根是资金来接你的货。用户「有资金的趋势才是好趋势」的直觉成立, 作用点在持仓期而非入场点。 自带逐根模拟器不走 walk_exits,所以先与它对拍基线(毛收益差 <1e-12、出场 原因零分歧),10/10 币通过才往下算。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -396,6 +396,56 @@ vr10 中位 1.76 vs 无前序 1.46)。2×2 交叉,最好的一档是两者
|
|||||||
|
|
||||||
> 尚未做的:把量比做成硬过滤(如砍掉最高四分位)要付 25% 的笔数,
|
> 尚未做的:把量比做成硬过滤(如砍掉最高四分位)要付 25% 的笔数,
|
||||||
> 而 1m 本来就稀疏(10 币约 2.5 笔/天)。**先当仓位权重用,别当开关。**
|
> 而 1m 本来就稀疏(10 币约 2.5 笔/天)。**先当仓位权重用,别当开关。**
|
||||||
|
> (已由 step51 §3.39 定论:该做成开关,不是权重。)
|
||||||
|
|
||||||
|
**❌ 但持仓中放量**不是**该平仓的信号——恰恰相反(step52)**
|
||||||
|
|
||||||
|
用户接着问:既然入场撞上放量是接盘,那持仓中出现放量根是不是也说明这一波
|
||||||
|
走完了、该直接平掉?测下来是反的,而且反得比入场那条还干净。
|
||||||
|
|
||||||
|
规则:持仓期间任一根 `vr60 ≥ 阈值` 就收盘市价平掉(taker)。同根内优先级
|
||||||
|
止损(盘中)> 目标(盘中)> 放量平仓(收盘)。10 币 3941 笔:
|
||||||
|
|
||||||
|
| 全样本 | 毛R | 净均R | R夏普 | PF | 余量bp | 胜率 | 均持仓 |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **基线(不看量)** | **1.068** | **0.940** | **0.627** | **3.80** | **18.45** | 71.7% | 30.0 |
|
||||||
|
| vr60≥3 就平 | 0.617 | 0.475 | 0.459 | 2.81 | 7.81 | 69.7% | 11.1 |
|
||||||
|
| vr60≥5 就平 | 0.858 | 0.723 | 0.568 | 3.37 | 12.85 | 71.7% | 19.8 |
|
||||||
|
| vr60≥8 就平 | 1.009 | 0.878 | 0.617 | 3.69 | 16.68 | 71.9% | 26.3 |
|
||||||
|
|
||||||
|
**阈值越高(触发越少)越接近基线,单调——这条曲线的最优点是「永不触发」。**
|
||||||
|
样本内外一致。「浮盈才平」的变体把胜率抬到 74.4%(基线 71.7%)但毛R 掉到
|
||||||
|
0.655,是过早止盈的教科书特征:**胜率上升、期望下降**。
|
||||||
|
|
||||||
|
根因在这里。vr60≥5 触发的 1769 笔(45%),若不平,最终:
|
||||||
|
|
||||||
|
| | 占比 | 净均R | 对照:全体基线 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 走到 8ATR 止盈 | **45.7%** | +2.640 | 30.1% |
|
||||||
|
| 最终止损 | **16.6%** | −0.422 | 33.1% |
|
||||||
|
| 48 根超时 | 37.8% | +1.283 | 36.8% |
|
||||||
|
|
||||||
|
**持仓中的放量根标记的是最好的那批单子,不是最差的。** 在那里平掉每笔让出
|
||||||
|
+0.484R。幸存者偏差已控(触发组平均已活 9.6 根):按基线持仓 ≥K 根分层后
|
||||||
|
**5/5 档全部同向**,放量组止盈率约为无量组两倍——
|
||||||
|
|
||||||
|
| 存活≥K根 | 放量组止盈率 | 无量组止盈率 | 放量组净均R | 无量组净均R | R差 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 5 | 41.2% | 19.8% | 1.443 | 0.658 | +0.785 |
|
||||||
|
| 10 | 42.7% | 21.4% | 1.599 | 0.898 | +0.700 |
|
||||||
|
| 20 | 34.4% | 20.3% | 1.558 | 1.106 | +0.452 |
|
||||||
|
| 30 | 24.7% | 15.5% | 1.469 | 1.188 | +0.281 |
|
||||||
|
|
||||||
|
**同一个事件,入场时是负的、持仓时是正的,区别只在你站在它的哪一边。**
|
||||||
|
入场那根放量你是买方,付的是资金已经推到的价;持仓中那根放量是资金来接你的
|
||||||
|
货,那是兑现不是离场。用户「有资金的趋势才是好趋势」的直觉**成立,
|
||||||
|
但作用点在持仓期而非入场点**。
|
||||||
|
|
||||||
|
> 未测的建设性方向:既然持仓中放量预示止盈率翻倍,或许该**延长 runner 目标 /
|
||||||
|
> 加仓**而不是平仓。注意基线已经隐含吃到了这部分(它就是拿着不动),
|
||||||
|
> 所以要多拿必须主动改目标位,**这是新参数,要重新回测**。
|
||||||
|
> R差 随 K 递减(0.785 → 0.281)说明越晚出现的放量根剩余空间越小,
|
||||||
|
> 若要做,触发窗口应偏前段。
|
||||||
|
|
||||||
**TRX 应从币池剔除**:实盘口径 208 天只有 **5 笔**,ATR 门控几乎全刷掉
|
**TRX 应从币池剔除**:实盘口径 208 天只有 **5 笔**,ATR 门控几乎全刷掉
|
||||||
(与 §5.3「2026 只剩 4.2%」一致)。实际可用是 10 个币,不是 11 个。
|
(与 §5.3「2026 只剩 4.2%」一致)。实际可用是 10 个币,不是 11 个。
|
||||||
@@ -871,6 +921,7 @@ step41 首轮跑的是错误的 3/1bp(见 §1.3),已用实际费率重算
|
|||||||
| **按 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 |
|
| **挑「信号根成交量大」的信号(有资金推的趋势)** | **方向完全相反**: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 |
|
||||||
|
| **持仓中放量就平仓(把成交量当出场信号)** | **方向也是反的,且比入场那条更干净**:毛R 1.068 → 0.617(vr60≥3)/ 0.858(≥5)/ 1.009(≥8),**阈值越高越接近基线,最优点是「永不触发」**。触发的那批若不平,止盈率 45.7%、止损率 16.6%(全体基线 30.1% / 33.1%),**放量根标记的是最好的单子不是最差的**,平掉每笔让出 +0.484R。幸存者偏差已控(按持仓 ≥K 根分层 5/5 同向)。同一事件入场为负、持仓为正——入场时你是买方,持仓时资金是来接你货的 | step52 §3.31 |
|
||||||
| 线段(`Chan_XD`)做大级别 | 滞后太大,且中枢极少 | 早期,用户也这么说 |
|
| 线段(`Chan_XD`)做大级别 | 滞后太大,且中枢极少 | 早期,用户也这么说 |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1764,6 +1815,7 @@ API 限流风险隔离三个好处。
|
|||||||
| 49 | **「有前序」样本外验证**(10 币 80 万根)→ 通过,余量 +6bp,6/6 时段同向 |
|
| 49 | **「有前序」样本外验证**(10 币 80 万根)→ 通过,余量 +6bp,6/6 时段同向 |
|
||||||
| 50 | **信号根成交量**(用户假设「有资金的趋势才是好趋势」)→ **方向相反**,高量毛R 0.794 vs 低量 1.421,ATR 4/4 + 时段 7/7 同向;根因是在突破根上给别人接盘 |
|
| 50 | **信号根成交量**(用户假设「有资金的趋势才是好趋势」)→ **方向相反**,高量毛R 0.794 vs 低量 1.421,ATR 4/4 + 时段 7/7 同向;根因是在突破根上给别人接盘 |
|
||||||
| 51 | **因子化仓位**(把 49/50 做成权重)→ 量因子该做成**开关**不是权重(硬砍 vs 加权:R夏普 0.496/0.442、并发 6.00/7.98);**前序因子不该进仓位**,边际收益买不起集中度。优势形态是削尾不是抬均值 |
|
| 51 | **因子化仓位**(把 49/50 做成权重)→ 量因子该做成**开关**不是权重(硬砍 vs 加权:R夏普 0.496/0.442、并发 6.00/7.98);**前序因子不该进仓位**,边际收益买不起集中度。优势形态是削尾不是抬均值 |
|
||||||
|
| 52 | **持仓中放量出场**(用户提议)→ 反的,最优点是永不触发;放量根标记的是止盈率翻倍的好单。自带模拟器与 `walk_exits` 逐笔对拍,10/10 币通过 |
|
||||||
|
|
||||||
输出都在 `research/out/`。**2026-08-27 清理过一轮**:step1~20 的输出(早期方法论
|
输出都在 `research/out/`。**2026-08-27 清理过一轮**:step1~20 的输出(早期方法论
|
||||||
已被推翻,含未来函数偏差)与旧口径备份一并删除,只留支撑当前结论的证据。
|
已被推翻,含未来函数偏差)与旧口径备份一并删除,只留支撑当前结论的证据。
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
"""Step 52:持仓中放量就平仓 —— 把成交量当出场信号。
|
||||||
|
|
||||||
|
用户提出:既然 step50 证明「入场根放量 = 给走完这一冲的人接盘」,那持仓过程中
|
||||||
|
出现放量根,是不是也说明这一波被走完了,该直接平掉?
|
||||||
|
|
||||||
|
这和 step50 是同一个机制的延伸,但**方向未知**:放量既可能是衰竭(该走),
|
||||||
|
也可能是突破续势的起点(走了就砍在起涨点)。step50 只证明了「入场时撞上放量
|
||||||
|
不好」,推不出「持仓时撞上放量该跑」——入场是你在接别人的盘,持仓时那根量
|
||||||
|
可能正是把你送上去的那批资金。
|
||||||
|
|
||||||
|
出场腿是 taker(收盘市价),成本按 TIME 计。触发优先级:同一根内止损(盘中)
|
||||||
|
> 目标(盘中)> 放量平仓(收盘)。止损优先是保守侧。
|
||||||
|
|
||||||
|
⚠️ 本步自己写了逐根模拟器,不走 walk_exits。**基线必须与 walk_exits 逐笔
|
||||||
|
相等才继续**——否则后面所有对比都是在和一个错的基线比。
|
||||||
|
"""
|
||||||
|
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, SCALE_AT, RUNNER, MAXB = 2.0, 3.0, 8.0, 48
|
||||||
|
GATE_BP = 8.0
|
||||||
|
THRESHOLDS = (3.0, 5.0, 8.0)
|
||||||
|
OUT = HERE / "out" / "step52_volume_exit.feather"
|
||||||
|
IS_START = pd.Timestamp("2026-01-30", tz="Asia/Shanghai")
|
||||||
|
|
||||||
|
TP, SL_, TIME = 0, 1, 2
|
||||||
|
|
||||||
|
|
||||||
|
def simulate(cdf, sig, vr):
|
||||||
|
"""逐根前推。返回每笔在基线与各放量出场变体下的 (毛收益, 原因, 是否分批)。
|
||||||
|
|
||||||
|
runner 止损位与初始止损同为 2 ATR(HANDOFF §3.5 定的 rstop=SL),
|
||||||
|
所以全程止损线不动,不需要分段处理。
|
||||||
|
"""
|
||||||
|
high = cdf["high"].to_numpy(float)
|
||||||
|
low = cdf["low"].to_numpy(float)
|
||||||
|
open_ = cdf["open"].to_numpy(float)
|
||||||
|
close = cdf["close"].to_numpy(float)
|
||||||
|
atr = cdf["atr"].to_numpy(float)
|
||||||
|
n = len(cdf)
|
||||||
|
|
||||||
|
variants = ["base"]
|
||||||
|
for t in THRESHOLDS:
|
||||||
|
variants += [f"v{t:g}", f"v{t:g}w"]
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for s, d in zip(sig["entry_idx"].astype(int), sig["direction"].astype(int)):
|
||||||
|
e = s + 1
|
||||||
|
if e >= n - 1:
|
||||||
|
continue
|
||||||
|
a = atr[s]
|
||||||
|
if not np.isfinite(a) or a <= 0:
|
||||||
|
continue
|
||||||
|
entry = open_[e]
|
||||||
|
end = min(e + MAXB, n - 1)
|
||||||
|
row = {"sig_idx": s, "atr_pct": a / entry}
|
||||||
|
|
||||||
|
for var in variants:
|
||||||
|
thr = None if var == "base" else float(var[1:].rstrip("w"))
|
||||||
|
only_win = var.endswith("w")
|
||||||
|
scaled = False
|
||||||
|
g = r = None
|
||||||
|
xb = end
|
||||||
|
for j in range(e, end + 1):
|
||||||
|
adv = (high[j] - entry) / a if d == 1 else (entry - low[j]) / a
|
||||||
|
ret = (entry - low[j]) / a if d == 1 else (high[j] - entry) / a
|
||||||
|
# ① 止损(盘中)。同根内优先于目标,保守侧
|
||||||
|
if ret >= SL:
|
||||||
|
hit = -SL * a / entry
|
||||||
|
g = 0.5 * (SCALE_AT * a / entry) + 0.5 * hit if scaled else hit
|
||||||
|
r, xb = SL_, j
|
||||||
|
break
|
||||||
|
# ② 目标(盘中限价)
|
||||||
|
if not scaled and adv >= SCALE_AT:
|
||||||
|
# 同根内可能既到 3 ATR 又到 8 ATR,按先减仓后续跑处理
|
||||||
|
scaled = True
|
||||||
|
if adv >= RUNNER:
|
||||||
|
g = 0.5 * (SCALE_AT * a / entry) + 0.5 * (RUNNER * a / entry)
|
||||||
|
r, xb = TP, j
|
||||||
|
break
|
||||||
|
elif scaled and adv >= RUNNER:
|
||||||
|
g = 0.5 * (SCALE_AT * a / entry) + 0.5 * (RUNNER * a / entry)
|
||||||
|
r, xb = TP, j
|
||||||
|
break
|
||||||
|
# ③ 放量平仓(收盘市价)
|
||||||
|
if thr is not None and np.isfinite(vr[j]) and vr[j] >= thr:
|
||||||
|
px = d * (close[j] - entry) / entry
|
||||||
|
if not only_win or px > 0:
|
||||||
|
g = 0.5 * (SCALE_AT * a / entry) + 0.5 * px if scaled else px
|
||||||
|
r, xb = TIME, j
|
||||||
|
break
|
||||||
|
if g is None: # 超时:末根收盘市价
|
||||||
|
px = d * (close[end] - entry) / entry
|
||||||
|
g = 0.5 * (SCALE_AT * a / entry) + 0.5 * px if scaled else px
|
||||||
|
r, xb = TIME, end
|
||||||
|
row[f"{var}_g"], row[f"{var}_r"] = g, r
|
||||||
|
row[f"{var}_c"] = int(scaled)
|
||||||
|
row[f"{var}_b"] = xb - e + 1
|
||||||
|
rows.append(row)
|
||||||
|
return pd.DataFrame(rows)
|
||||||
|
|
||||||
|
|
||||||
|
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 chanlun import TF_DF
|
||||||
|
from chanlun.analysis.fast_bsp import (
|
||||||
|
add_zone_ladder, attach_htf_agree, attach_zone_ladder,
|
||||||
|
build_htf_zones, find_fast_bsp3, htf_fx_timeline,
|
||||||
|
)
|
||||||
|
from lib.data import fetch_ohlcv
|
||||||
|
from lib.exit_model import cfg_name, walk_exits
|
||||||
|
|
||||||
|
try:
|
||||||
|
df = fetch_ohlcv(f"{sym}/USDT:USDT", "1m", rows)
|
||||||
|
if df is None or len(df) < 50_000:
|
||||||
|
return None
|
||||||
|
chan = TF_DF(df, 1, "1m", lean=True)
|
||||||
|
cdf = chan.dataframe
|
||||||
|
zones = build_htf_zones(cdf, "1m", chan=chan)
|
||||||
|
if zones.empty:
|
||||||
|
return None
|
||||||
|
zl = add_zone_ladder(zones.reset_index(drop=True))
|
||||||
|
sig = find_fast_bsp3(cdf, zl)
|
||||||
|
if sig.empty:
|
||||||
|
return None
|
||||||
|
dh = fetch_ohlcv(f"{sym}/USDT:USDT", "5m", 10 ** 9)
|
||||||
|
ch = TF_DF(dh, 1, "5m", lean=True)
|
||||||
|
sig = attach_zone_ladder(
|
||||||
|
attach_htf_agree(sig, cdf, htf_fx_timeline(ch, ch.dataframe)), zl)
|
||||||
|
|
||||||
|
vr = (cdf["volume"] / cdf["volume"].rolling(60, min_periods=10).mean()
|
||||||
|
).to_numpy(float)
|
||||||
|
res = simulate(cdf, sig, vr)
|
||||||
|
|
||||||
|
# 对拍:基线必须与已验证的 walk_exits 逐笔相等
|
||||||
|
ref = walk_exits(cdf, sig, [SL], [RUNNER], [MAXB], scale_at=SCALE_AT,
|
||||||
|
runners=(RUNNER,), runner_stops=(SL,))
|
||||||
|
cfg = cfg_name(SL, RUNNER, MAXB, SL)
|
||||||
|
if len(ref) != len(res):
|
||||||
|
print(f" {sym} 对拍失败:笔数 {len(ref)} vs {len(res)}", flush=True)
|
||||||
|
return None
|
||||||
|
dg = np.abs(ref[f"{cfg}_g"].to_numpy() - res["base_g"].to_numpy())
|
||||||
|
dr = (ref[f"{cfg}_r"].to_numpy() != res["base_r"].to_numpy()).sum()
|
||||||
|
if dg.max() > 1e-12 or dr:
|
||||||
|
print(f" {sym} 对拍失败:毛收益最大差 {dg.max():.3e},原因分歧 {dr} 笔",
|
||||||
|
flush=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
idx = sig["entry_idx"].to_numpy().astype(int)
|
||||||
|
keep = np.isin(idx, res["sig_idx"].to_numpy())
|
||||||
|
idx = idx[keep]
|
||||||
|
atr = cdf["atr"].to_numpy(float)[idx]
|
||||||
|
close = cdf["close"].to_numpy(float)[idx]
|
||||||
|
out = pd.DataFrame({
|
||||||
|
"sym": sym,
|
||||||
|
"date": cdf["date"].to_numpy()[idx],
|
||||||
|
"atr_bp": atr / close * 1e4,
|
||||||
|
"htf": sig["htf_agree"].to_numpy()[keep],
|
||||||
|
"lad": sig["ladder_ok"].to_numpy()[keep],
|
||||||
|
"vr60_entry": vr[idx],
|
||||||
|
})
|
||||||
|
for c in res.columns:
|
||||||
|
if c not in ("sig_idx",):
|
||||||
|
out[c] = res[c].to_numpy()
|
||||||
|
return out
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {sym} 失败: {e!r}", flush=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def stat(d: pd.DataFrame, var: str, label: str) -> dict:
|
||||||
|
from lib.exit_model import fee_of, taker_notional
|
||||||
|
|
||||||
|
g = d[f"{var}_g"].to_numpy()
|
||||||
|
r, c = d[f"{var}_r"].to_numpy(), d[f"{var}_c"].to_numpy()
|
||||||
|
tn = taker_notional(r, c)
|
||||||
|
net = g - fee_of(r, c)
|
||||||
|
risk = SL * d.atr_pct.to_numpy()
|
||||||
|
R = net / risk
|
||||||
|
w, o = net[net > 0].sum(), -net[net <= 0].sum()
|
||||||
|
return {"方案": label, "笔数": len(d),
|
||||||
|
"胜率": f"{(net > 0).mean()*100:.1f}%",
|
||||||
|
"毛R": round((g / risk).mean(), 3), "净均R": round(R.mean(), 3),
|
||||||
|
"R夏普": round(R.mean() / R.std(ddof=1), 3),
|
||||||
|
"PF": round(w / o, 2) if o > 0 else np.inf,
|
||||||
|
"余量bp": round(net.mean() / tn.mean() * 1e4, 2),
|
||||||
|
"均持仓": round(d[f"{var}_b"].mean(), 1),
|
||||||
|
"触发率": f"{(r == TIME).mean()*100:.0f}%"}
|
||||||
|
|
||||||
|
|
||||||
|
def report(d: pd.DataFrame, label: str) -> None:
|
||||||
|
rows = [stat(d, "base", "基线(不看量)")]
|
||||||
|
for t in THRESHOLDS:
|
||||||
|
rows.append(stat(d, f"v{t:g}", f"vr60≥{t:g} 就平"))
|
||||||
|
rows.append(stat(d, f"v{t:g}w", f"vr60≥{t:g} 且浮盈才平"))
|
||||||
|
print(f"\n--- {label}({len(d)} 笔)---")
|
||||||
|
print(pd.DataFrame(rows).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)}"
|
||||||
|
f"{' ⚠对拍未过' if r is None else ' 对拍通过'}", 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 = d[(d.htf == 1.0) & d.lad & (d.atr_bp >= GATE_BP)].copy()
|
||||||
|
print(f"\n实盘口径 {len(d)} 笔 {d.date.min():%Y-%m-%d} ~ {d.date.max():%Y-%m-%d}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 118)
|
||||||
|
print("########## 放量出场 vs 基线 ##########")
|
||||||
|
report(d[d.date < IS_START], "样本外")
|
||||||
|
report(d[d.date >= IS_START], "发现期")
|
||||||
|
report(d, "全样本")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user