research: 一二类买卖点可行性探针(step55),结论为不可用
用户提出把 B4 的研究思路搬到第一/第二类买卖点,并指定 5m/15m 测。 滞后先于收益测,结论是用户判断正确:B1/B2/B3 滞后中位都是 8~9 根, 三类共用 find_all_bsp 的「中枢 is_sure + 笔 is_sure + sure_time」, 滞后不是区分它们的变量。 收益全负,一类最差:5m 上 B1 PF 0.24 / 胜率 18.6% / t −17.3, S1 PF 0.20 / 胜率 14.8% / t −22.3,15m 同量级。B3 跑出 0.64/0.76、 HANDOFF §4 记的是 0.66,口径校验通过,所以 B1/B2 的数可信。 一类烂得彻底是几何决定的:等 8 根后价格已朝上跑 1.68 ATR,新入场价往下 2 ATR 的止损落在比原始低点还低 0.3 ATR 处,几乎贴着极值。同样的滞后在 顺势突破上只是追高,在逆势反转上是加倍惩罚。 探针里修掉两个会静默出错的地方:cdf.date 是 datetime64[ms] 而 Timestamp.value 是纳秒,手工转 int64 比较会让 searchsorted 全部落到末尾 且不报错(这是之前跑出 0 条的原因);速率对照未按币归一,拿 5 币的数去 比 10 币的 B4 基线等于凭空打对折。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -974,6 +974,68 @@ mom10 同形状(Q2 最好、Q4 最差),样本内外一致。**势要有—
|
|||||||
> **② 在几十个分组里挑出的最显著那个,默认它是噪声,除非它能在另一个总体上
|
> **② 在几十个分组里挑出的最显著那个,默认它是噪声,除非它能在另一个总体上
|
||||||
> 复现。** p 值不做多重比较校正就等于没做检验。
|
> 复现。** p 值不做多重比较校正就等于没做检验。
|
||||||
|
|
||||||
|
### 3.393 一类/二类买卖点:引擎实现下全是负的,一类尤其糟(step55)
|
||||||
|
|
||||||
|
用户提出把 B4 那套研究思路搬到一二类上,并指定用 5m/15m 测。
|
||||||
|
|
||||||
|
**先量滞后,不先跑收益**——`Chan_BSP_TYPE` 的枚举注释里写着 B4 存在的全部意义
|
||||||
|
就是「几何位置同 B3/S3,但不等笔确认」,说明这个项目早就付过等笔确认的学费。
|
||||||
|
|
||||||
|
**① 滞后:三类完全一样(用户的判断,实测确认)**
|
||||||
|
|
||||||
|
| 类型 | 5m 滞后中位 | 15m 滞后中位 | 5m 漂移中位 | 占 2ATR 止损 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| B1 | 8 根 | 9 根 | 1.68 ATR | 84% |
|
||||||
|
| B2 | 8 根 | 8 根 | 2.03 ATR | 101% |
|
||||||
|
| B3 | 8 根 | 9 根 | 1.95 ATR | 98% |
|
||||||
|
|
||||||
|
三类都卡在同一处:`中枢 is_sure` + `笔 is_sure` + `sure_time`。
|
||||||
|
**所以滞后不是区分一二三类的变量**,它是 `find_all_bsp` 这个实现的共同属性。
|
||||||
|
|
||||||
|
⚠️ 「漂移为正的占比 100%」是**定义决定的,不是发现**:笔之所以在那里结束,
|
||||||
|
正是因为价格从极值反向走了,`bi.end_klc` 就是极值点。有信息量的是幅度不是符号。
|
||||||
|
|
||||||
|
**② 收益:全负,一类是最差的一档**(5m / 15m,5 币,出场用实盘的 2/3/8/2/48)
|
||||||
|
|
||||||
|
| 类型 | 5m 笔数 | 5m 胜率 | 5m 毛R | 5m PF | 5m t值 | 15m PF | 15m t值 |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **B1** | 623 | **18.6%** | −0.592 | **0.24** | **−17.3** | 0.21 | −18.9 |
|
||||||
|
| **S1** | 644 | **14.8%** | −0.675 | **0.20** | **−22.3** | 0.23 | −15.9 |
|
||||||
|
| B2 | 293 | 35.8% | −0.104 | 0.73 | −1.4 | 0.85 | −0.8 |
|
||||||
|
| S2 | 288 | 36.8% | −0.115 | 0.82 | −1.6 | 0.75 | −2.4 |
|
||||||
|
| B3 | 2806 | 33.3% | −0.187 | 0.64 | −8.0 | 0.76 | −5.3 |
|
||||||
|
| S3 | 2685 | 35.4% | −0.135 | 0.70 | −5.6 | 0.73 | −4.8 |
|
||||||
|
|
||||||
|
**口径校验通过**:B3 跑出 PF 0.64/0.76,§4 记的引擎 B3 是 0.66 —— 接线是对的,
|
||||||
|
所以 B1/B2 的数可以信。
|
||||||
|
|
||||||
|
**③ 一类为什么烂得这么彻底:它的结构自带反向杠杆**
|
||||||
|
|
||||||
|
B1 买点的理想入场是那一笔的低点。等到 `sure_time` 可以动手时,价格已经**朝上
|
||||||
|
跑了 1.68 ATR**。于是新入场价往下 2 ATR 的止损,落在**比原始低点还低 0.3 ATR**
|
||||||
|
的位置——几乎贴着极值,任何一次回踩都打掉;而 3 ATR 的减半目标从低点算起变成
|
||||||
|
了 4.7 ATR。胜率 15~21% 完美对应这个几何:**它低于随机入场**(SL2/TP3 下随机
|
||||||
|
约 40%)。
|
||||||
|
|
||||||
|
**这和三类的滞后代价不是一回事。** 三类是顺势突破,等 8 根是「追高一点」;
|
||||||
|
一类是逆势抄底,等 8 根是「在反弹了 1.7 ATR 之后才去接刀」——**同样的滞后,
|
||||||
|
在反转型信号上是加倍惩罚**。
|
||||||
|
|
||||||
|
**④ 结论与下一步**
|
||||||
|
|
||||||
|
引擎现成的 B1/B2 **不可用**,且原因是结构性的不是参数性的,调参救不回来。
|
||||||
|
B3 当年的解法不是调参,是**重新定义成实时判据**(B4:收盘突破 zg → 收盘未跌回
|
||||||
|
→ 重新上行,全部当根可判),选样口径随之改变(627 个中枢只认 212 个),
|
||||||
|
统计性质才翻正。
|
||||||
|
|
||||||
|
**但这条路未必能照搬到一类**:B4 能实时判,是因为「突破 + 回踩不破 + 重新上行」
|
||||||
|
每一步都能用收盘价当根判定。而一类的核心是**背驰**,它要求「知道这一笔已经
|
||||||
|
结束」——那本身就是事后信息。要做实时版,得先找到一个不依赖笔端点的力竭代理。
|
||||||
|
|
||||||
|
> 另有一个来自 §3.391 的先验值得先算:**一类按定义就发生在一段已走完的行情
|
||||||
|
> 末端,也就是 `mom60` 最高的那个区间——而那正是我们实测最差的四分位**
|
||||||
|
> (Q4 发现期余量只剩 1.44bp)。真要做实时一类,先验就不利。
|
||||||
|
|
||||||
### 3.4 alpha 的来源(step32 消融)
|
### 3.4 alpha 的来源(step32 消融)
|
||||||
|
|
||||||
逐条拆掉 `fast_bsp3` 的条件后发现:**alpha 完全来自缠论中枢的上下文定位,
|
逐条拆掉 `fast_bsp3` 的条件后发现:**alpha 完全来自缠论中枢的上下文定位,
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
"""Step 55:一/二类买卖点的可行性探针——先量滞后,不急着跑收益。
|
||||||
|
|
||||||
|
用户提出把 B4 那套研究思路搬到一二类上。搬之前必须先过一道闸,理由写在
|
||||||
|
`Chan_BSP_TYPE` 的枚举注释里:**B4 存在的全部意义就是"几何位置同 B3/S3,
|
||||||
|
但不等笔确认"**。也就是说这个项目早就付过"等笔确认"的学费。
|
||||||
|
|
||||||
|
而一类买卖点是最滞后的一种构造:它要中枢 `is_sure`、要离开笔 `is_sure`、
|
||||||
|
还要背驰判定(`check_bi_div` 读 `macd_hist`)。二类更靠后,要在一类之后再走
|
||||||
|
两笔。所以真正的问题不是"一二类赚不赚钱",而是:
|
||||||
|
|
||||||
|
你能在什么时候知道它,那时候价格还在不在。
|
||||||
|
|
||||||
|
`ChanBSP` 给了两个时刻,差值就是答案:
|
||||||
|
klc.end_time = leave_bi.end_klc 的收盘时刻,**极值所在**,理想入场点
|
||||||
|
sure_time = 笔被确认的时刻,**你最早能动手的时刻**
|
||||||
|
|
||||||
|
本步只回答三件事,跑得快、结论硬:
|
||||||
|
1. 一二类各有多少笔(对比 B4 的 5.3 笔/天 / 10 币)
|
||||||
|
2. 滞后多少根
|
||||||
|
3. 这段等待里价格跑掉多少个 ATR —— 这是"入场价漂移",直接从余量里扣
|
||||||
|
|
||||||
|
⚠️ 不在本步做收益回测。滞后若不可接受,收益怎么算都是假的:`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)
|
||||||
|
|
||||||
|
OUT = HERE / "out" / "step55_bsp12.feather"
|
||||||
|
# B4 的对照基线,来自 §3.31 / step48:10 币 1m 实盘口径
|
||||||
|
B4_PER_DAY_10SYM = 5.3
|
||||||
|
# 出场结构与实盘完全一致,见 live/exit_params.py
|
||||||
|
SL, SCALE_AT, RUNNER, RSTOP, MAXB = 2.0, 3.0, 8.0, 2.0, 48
|
||||||
|
|
||||||
|
|
||||||
|
def collect(sym: str, rows: int, tf: str = "1m") -> pd.DataFrame | None:
|
||||||
|
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.core.ChanEnum import Chan_BSP_TYPE
|
||||||
|
from lib.data import fetch_ohlcv
|
||||||
|
from lib.exit_model import cfg_name, walk_exits
|
||||||
|
|
||||||
|
try:
|
||||||
|
df = fetch_ohlcv(f"{sym}/USDT:USDT", tf, rows)
|
||||||
|
if df is None or len(df) < 5_000:
|
||||||
|
return None
|
||||||
|
# 必须走 full:check_bi_div 读 macd_hist,那只在 full 下算。
|
||||||
|
# 而 TF_DF 自己**不填** bsp_list——web 的 analyze.py 是显式调
|
||||||
|
# find_all_bsp 的,这里照抄那条链,口径才对得上
|
||||||
|
chan = TF_DF(df, 1, tf, lean=False)
|
||||||
|
cdf = chan.dataframe
|
||||||
|
bi_zs = chan.cal_bi_zs_list_pure(chan.bi_list)
|
||||||
|
if not bi_zs:
|
||||||
|
return None
|
||||||
|
bsp = chan.find_all_bsp(chan.bi_list, bi_zs)
|
||||||
|
if not bsp:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 索引映射有两个坑,都会静默给出错的下标:
|
||||||
|
# ① cdf.date 带时区(Asia/Shanghai),而 KLC 上的时间是**字符串**且
|
||||||
|
# 不带时区。它们本就是同一个时钟的墙上时间,所以去 tz 而不是硬贴。
|
||||||
|
# ② cdf.date 的单位是 datetime64[ms],`astype("int64")` 给的是毫秒,
|
||||||
|
# 而 `Timestamp.value` 是纳秒,差 1e6 倍——手工转整数会让
|
||||||
|
# searchsorted 全部落到末尾,且不报错。交给 DatetimeIndex 自己比。
|
||||||
|
dser = pd.to_datetime(cdf["date"])
|
||||||
|
if dser.dt.tz is not None:
|
||||||
|
dser = dser.dt.tz_localize(None)
|
||||||
|
didx = pd.DatetimeIndex(dser)
|
||||||
|
|
||||||
|
def to_i(ts) -> int:
|
||||||
|
t = pd.Timestamp(ts)
|
||||||
|
if t.tz is not None:
|
||||||
|
t = t.tz_localize(None)
|
||||||
|
return int(didx.searchsorted(t))
|
||||||
|
|
||||||
|
op = cdf["open"].to_numpy(float)
|
||||||
|
cl = cdf["close"].to_numpy(float)
|
||||||
|
atr = cdf["atr"].to_numpy(float)
|
||||||
|
n = len(cdf)
|
||||||
|
|
||||||
|
want = {Chan_BSP_TYPE.B1: ("B1", 1), Chan_BSP_TYPE.S1: ("S1", -1),
|
||||||
|
Chan_BSP_TYPE.B2: ("B2", 1), Chan_BSP_TYPE.S2: ("S2", -1),
|
||||||
|
Chan_BSP_TYPE.B3: ("B3", 1), Chan_BSP_TYPE.S3: ("S3", -1)}
|
||||||
|
rec = []
|
||||||
|
for b in bsp:
|
||||||
|
tag = want.get(b.type)
|
||||||
|
if tag is None or b.sure_time is None:
|
||||||
|
continue
|
||||||
|
name, d = tag
|
||||||
|
# 极值根:笔末 KLC 的收盘时刻。KLC 是合并后的,取它覆盖的最后一根
|
||||||
|
i_ext, i_sure = to_i(b.klc.end_time), to_i(b.sure_time)
|
||||||
|
if not (0 <= i_ext < n and 0 <= i_sure < n):
|
||||||
|
continue
|
||||||
|
a = atr[i_ext]
|
||||||
|
if not np.isfinite(a) or a <= 0:
|
||||||
|
continue
|
||||||
|
# 理想价:极值根收盘。可执行价:确认根的**次根开盘**——与
|
||||||
|
# walk_exits / 实盘的口径一致(信号根收盘后才下单)
|
||||||
|
px_ideal = cl[i_ext]
|
||||||
|
j = min(i_sure + 1, n - 1)
|
||||||
|
px_real = op[j]
|
||||||
|
rec.append({
|
||||||
|
"sym": sym, "tf": tf, "type": name, "dir": d,
|
||||||
|
"date_ext": dser.iloc[i_ext], "date_sure": dser.iloc[i_sure],
|
||||||
|
"i_ext": i_ext, "i_sure": i_sure,
|
||||||
|
"lag_bars": i_sure - i_ext,
|
||||||
|
"atr_bp": a / px_ideal * 1e4,
|
||||||
|
# 等待期间价格顺着信号方向跑掉了多少(正 = 你追高/追空,吃亏)
|
||||||
|
"drift_atr": (px_real - px_ideal) * d / a,
|
||||||
|
"drift_bp": (px_real - px_ideal) * d / px_ideal * 1e4,
|
||||||
|
})
|
||||||
|
if not rec:
|
||||||
|
return None
|
||||||
|
r = pd.DataFrame(rec)
|
||||||
|
# 入场口径与实盘一致:确认根收盘后下单,walk_exits 用 entry_idx+1 的
|
||||||
|
# 开盘价,ATR 取 entry_idx 那根。所以 entry_idx = i_sure,**不是**
|
||||||
|
# i_ext——用极值根等于假设你能买在笔的低点,那是未来函数
|
||||||
|
r = r[(r.i_sure < len(cdf) - 2) & np.isfinite(atr[r.i_sure.values])
|
||||||
|
& (atr[r.i_sure.values] > 0)].reset_index(drop=True)
|
||||||
|
if r.empty:
|
||||||
|
return None
|
||||||
|
sig = pd.DataFrame({"entry_idx": r.i_sure.values,
|
||||||
|
"direction": r.dir.values})
|
||||||
|
res = walk_exits(cdf, sig, [SL], [RUNNER], [MAXB], scale_at=SCALE_AT,
|
||||||
|
runners=(RUNNER,), runner_stops=(RSTOP,))
|
||||||
|
cfg = cfg_name(SL, RUNNER, MAXB, RSTOP)
|
||||||
|
if len(res) != len(r):
|
||||||
|
print(f" {sym} {tf} 长度不齐 {len(res)} vs {len(r)},跳过",
|
||||||
|
flush=True)
|
||||||
|
return None
|
||||||
|
for k in ("g", "r", "c", "b"):
|
||||||
|
r[k] = res[f"{cfg}_{k}"].to_numpy()
|
||||||
|
r["atr_pct"] = atr[r.i_sure.values] / cl[r.i_sure.values]
|
||||||
|
return r
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {sym} 失败: {e!r}", flush=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def report(d: pd.DataFrame, span_days: float, n_sym: int) -> None:
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print(f"########## 一、笔数:够不够做 ##########")
|
||||||
|
t = d.groupby("type").agg(笔数=("lag_bars", "size"))
|
||||||
|
t[f"{n_sym}币每天"] = (t["笔数"] / span_days).round(2)
|
||||||
|
# 必须按币归一再比。B4 那个 5.3 是 10 币的合计,直接拿 5 币的数去除
|
||||||
|
# 等于凭空把速率打对折
|
||||||
|
t["每币每天"] = (t["笔数"] / span_days / n_sym).round(3)
|
||||||
|
t["对B4倍数"] = (t["笔数"] / span_days / n_sym /
|
||||||
|
(B4_PER_DAY_10SYM / 10)).round(2)
|
||||||
|
print(t.to_string())
|
||||||
|
print(f"\n对照基线:B4 在 1m 上 10 币 {B4_PER_DAY_10SYM} 笔/天 = "
|
||||||
|
f"**{B4_PER_DAY_10SYM/10:.3f} 笔/币/天**。"
|
||||||
|
f"本表 {n_sym} 币 / 跨 {span_days:.0f} 天。")
|
||||||
|
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("########## 二、滞后:从极值到可动手,差多少根 ##########")
|
||||||
|
rows = []
|
||||||
|
for name, g in d.groupby("type"):
|
||||||
|
q = g.lag_bars.quantile([0.25, 0.5, 0.75, 0.9]).round(1)
|
||||||
|
rows.append({"类型": name, "笔数": len(g),
|
||||||
|
"滞后中位": q[0.5], "P25": q[0.25], "P75": q[0.75],
|
||||||
|
"P90": q[0.9], "均值": round(g.lag_bars.mean(), 1),
|
||||||
|
"最大": int(g.lag_bars.max())})
|
||||||
|
print(pd.DataFrame(rows).to_string(index=False))
|
||||||
|
print("\n1m 上 1 根 = 1 分钟。B4 的滞后是 0 根——它在信号根收盘即可下单。")
|
||||||
|
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("########## 三、等待的代价:价格跑掉了多少 ##########")
|
||||||
|
print("drift 为正 = 等待期间价格顺着信号方向走了,你只能追;这一段直接从"
|
||||||
|
"余量里扣")
|
||||||
|
print("⚠️ 「追不上占比」必然是 100%,那是定义决定的不是实测发现:笔之所以"
|
||||||
|
"在那里结束,\n 正是因为价格从那个极值反向走了——`bi.end_klc` 是极值"
|
||||||
|
"点,之后必然朝信号方向偏离。\n **有信息量的是幅度,不是符号。**")
|
||||||
|
rows = []
|
||||||
|
for name, g in d.groupby("type"):
|
||||||
|
q = g.drift_atr.quantile([0.25, 0.5, 0.75]).round(2)
|
||||||
|
rows.append({
|
||||||
|
"类型": name, "笔数": len(g),
|
||||||
|
"漂移中位(ATR)": q[0.5], "P25": q[0.25], "P75": q[0.75],
|
||||||
|
"漂移均值(ATR)": round(g.drift_atr.mean(), 2),
|
||||||
|
"漂移均值(bp)": round(g.drift_bp.mean(), 1),
|
||||||
|
"中位ATR(bp)": round(g.atr_bp.median(), 1),
|
||||||
|
"追不上占比": f"{(g.drift_atr > 0).mean()*100:.0f}%"})
|
||||||
|
print(pd.DataFrame(rows).to_string(index=False))
|
||||||
|
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("########## 四、和止损宽度比:漂移吃掉多少风险预算 ##########")
|
||||||
|
print("实盘止损是 2 ATR。若漂移中位已经是 1 ATR,等于你的止损只剩一半,"
|
||||||
|
"而目标位还在原处——盈亏比被腰斩。")
|
||||||
|
rows = []
|
||||||
|
for name, g in d.groupby("type"):
|
||||||
|
rows.append({
|
||||||
|
"类型": name,
|
||||||
|
"漂移/止损(2ATR)": f"{g.drift_atr.median()/2*100:.0f}%",
|
||||||
|
"漂移超过 1ATR 占比": f"{(g.drift_atr > 1).mean()*100:.0f}%",
|
||||||
|
"漂移超过 2ATR(已穿止损)": f"{(g.drift_atr > 2).mean()*100:.0f}%"})
|
||||||
|
print(pd.DataFrame(rows).to_string(index=False))
|
||||||
|
|
||||||
|
|
||||||
|
def profit(d: pd.DataFrame) -> None:
|
||||||
|
"""各类买卖点按实盘出场结构的实际表现。
|
||||||
|
|
||||||
|
B3 是校验锚:HANDOFF §4 已记引擎 `find_all_bsp` 的 B3/S3 是系统性亏损
|
||||||
|
(PF 0.66、胜率 27.4%、t −18.76)。若这里 B3 跑出个漂亮数字,说明口径接错了,
|
||||||
|
先别信 B1/B2 的结果。
|
||||||
|
"""
|
||||||
|
from lib.exit_model import fee_of, taker_notional
|
||||||
|
|
||||||
|
if "g" not in d.columns:
|
||||||
|
print("\n(本次数据无回测列,跳过收益段;删掉 out/step55_bsp12.feather "
|
||||||
|
"重跑可得)")
|
||||||
|
return
|
||||||
|
x = d.dropna(subset=["g"]).copy()
|
||||||
|
x["net"] = x.g.values - fee_of(x.r.values, x.c.values)
|
||||||
|
x["gR"] = x.g.values / (SL * x.atr_pct.values)
|
||||||
|
x["R"] = x.net.values / (SL * x.atr_pct.values)
|
||||||
|
x["tn"] = taker_notional(x.r.values, x.c.values)
|
||||||
|
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("########## 五、按实盘出场结构(2/3/8/2/48)的实际表现 ##########")
|
||||||
|
rows = []
|
||||||
|
for name, g in x.groupby("type"):
|
||||||
|
if len(g) < 40:
|
||||||
|
continue
|
||||||
|
w, o = g.net[g.net > 0].sum(), -g.net[g.net <= 0].sum()
|
||||||
|
rows.append({
|
||||||
|
"类型": name, "笔数": len(g),
|
||||||
|
"胜率": 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,
|
||||||
|
"R夏普": round(g.R.mean() / g.R.std(ddof=1), 3),
|
||||||
|
"余量bp": round(g.net.mean() / g.tn.mean() * 1e4, 2),
|
||||||
|
"中位持仓": int(g.b.median()),
|
||||||
|
"t值": round(g.gR.mean() / (g.gR.std(ddof=1) / np.sqrt(len(g))), 2),
|
||||||
|
})
|
||||||
|
print(pd.DataFrame(rows).to_string(index=False))
|
||||||
|
print("\n⚠️ B3 是口径校验锚:HANDOFF §4 记的是 PF 0.66 / 胜率 27.4%。"
|
||||||
|
"这里若明显更好,先怀疑接错了再信 B1/B2。")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--symbols", default="BTC,ETH,SOL,LINK,DOGE")
|
||||||
|
ap.add_argument("--tfs", default="5m,15m",
|
||||||
|
help="要测的周期。1m 上滞后的绝对根数与长周期相同,"
|
||||||
|
"但每根值的钱不同,所以周期是关键变量")
|
||||||
|
ap.add_argument("--rows", type=int, default=200_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(",")]
|
||||||
|
tfs = [t.strip() for t in args.tfs.split(",")]
|
||||||
|
print(f"[一二类可行性探针] {len(syms)} 币 × {tfs} × {args.rows} 根"
|
||||||
|
f"(full 模式,find_all_bsp 要 macd_hist)\n", flush=True)
|
||||||
|
parts = []
|
||||||
|
with ProcessPoolExecutor(max_workers=args.workers) as ex:
|
||||||
|
fut = {ex.submit(collect, s, args.rows, t): (s, t)
|
||||||
|
for s in syms for t in tfs}
|
||||||
|
for i, f in enumerate(as_completed(fut), 1):
|
||||||
|
r = f.result()
|
||||||
|
s, t = fut[f]
|
||||||
|
print(f" [{i}/{len(fut)}] {s} {t} "
|
||||||
|
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_ext"] = pd.to_datetime(d["date_ext"])
|
||||||
|
for tf, x in d.groupby("tf"):
|
||||||
|
span = (x.date_ext.max() - x.date_ext.min()).total_seconds() / 86400
|
||||||
|
print("\n" + "#" * 100)
|
||||||
|
print(f"########## {tf} —— {len(x)} 个买卖点 · "
|
||||||
|
f"{x.sym.nunique()} 币 · 跨 {span:.0f} 天 ##########")
|
||||||
|
report(x, span, x.sym.nunique())
|
||||||
|
profit(x)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user