Files
Chan/research/step4_zs_source_ab.py
T
jackyu66gitandCursor 7f393b93ed refactor: 精简仓库为 chanlun 核心与 web 分析,移除威科夫与遗留模块
删除根目录旧 Chan 模块、策略、配置、文档及 wyckoff 相关代码;更新缠论 pipeline 与笔中枢计算;补充 research 研究与 web 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 01:05:12 +08:00

170 lines
6.4 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 4:中枢来源 A/B —— cal_bi_zs(seg_list) vs cal_bi_zs_list_pure(bi_list)。
假设:seg 口径下笔中枢必须等所属线段成形,多叠了一层确认滞后;
pure 口径直接在扁平笔序列上滚动,应当显著更快出信号。
用同一套 walk-forward 重放,只切换中枢来源,对比滞后与真实收益。
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
import numpy as np
import pandas as pd
sys.path.insert(0, str(Path(__file__).resolve().parent))
from lib.bsp_eval import baseline_stats, run_pipeline
from lib.data import fetch_ohlcv
from lib.walkforward import analyze_stability, replay
pd.set_option("display.width", 240)
from chanlun.core.ChanEnum import Chan_BSP_DIR
HORIZONS = (1, 3, 5, 10, 20, 40)
def evaluate(df: pd.DataFrame, tf: str, symbol: str, zs_source: str, window: int, step: int):
_, final_bsp = run_pipeline(df, tf, zs_source=zs_source)
final_keys = {
(str(b.type).replace("Chan_BSP_TYPE.", ""),
1 if b.dir == Chan_BSP_DIR.BUY else -1, str(b.end_time))
for b in final_bsp if b.is_sure and b.sure_time is not None
}
result = replay(
df, tf, window=window, step=step, zs_source=zs_source,
cache_key=f"{symbol.replace('/', '_').replace(':', '-')}_{tf}",
)
life = analyze_stability(result, final_keys, df, window=window)
valid = life[~life["truncated"]].dropna(subset=["fx_idx"]).copy()
valid["fx_idx"] = valid["fx_idx"].astype(int)
mature = valid[~valid["immature"]]
return final_keys, valid, mature
def rt_returns(valid: pd.DataFrame, df: pd.DataFrame) -> pd.DataFrame:
"""以实时首见时刻入场的方向调整收益。"""
closes = df["close"].to_numpy(dtype=float)
n = len(df)
rows = []
for _, r in valid.iterrows():
i, d = int(r["first_seen_idx"]), int(r["direction"])
row = {"bsp_type": r["bsp_type"], "direction": d}
for h in HORIZONS:
j = i + h
row[f"ret_{h}"] = d * (closes[j] - closes[i]) / closes[i] if j < n else np.nan
rows.append(row)
return pd.DataFrame(rows)
def stats_block(fwd: pd.DataFrame, df: pd.DataFrame, mask=None, label="") -> list[dict]:
base = baseline_stats(df, HORIZONS).set_index("horizon")
g = fwd if mask is None else fwd[mask]
out = []
for h in HORIZONS:
r = g[f"ret_{h}"].dropna().to_numpy()
if len(r) == 0:
continue
dirs = g.loc[g[f"ret_{h}"].notna(), "direction"].to_numpy()
sd = r.std(ddof=1) if len(r) > 1 else np.nan
out.append({
"group": label, "horizon": h, "n": len(r), "mean": r.mean(),
"winrate": (r > 0).mean(),
"excess": r.mean() - float(np.mean(dirs) * base.loc[h, "base_mean_long"]),
"tstat": r.mean() / (sd / np.sqrt(len(r))) if sd else np.nan,
})
return out
def fmt(rows: list[dict]) -> str:
d = pd.DataFrame(rows)
if d.empty:
return "(无数据)"
d["mean"] = d["mean"].map(lambda v: f"{v * 100:+.2f}%")
d["excess"] = d["excess"].map(lambda v: f"{v * 100:+.2f}%")
d["winrate"] = d["winrate"].map(lambda v: f"{v * 100:.0f}%")
d["tstat"] = d["tstat"].map(lambda v: f"{v:+.2f}")
return d.to_string(index=False)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--symbol", default="BTC/USDT:USDT")
ap.add_argument("--tf", default="1h")
ap.add_argument("--window", type=int, default=3000)
ap.add_argument("--step", type=int, default=4)
args = ap.parse_args()
df = fetch_ohlcv(args.symbol, args.tf, 50000)
print(f"[data] {args.symbol} {args.tf} rows={len(df)} "
f"{df['date'].iloc[0]} -> {df['date'].iloc[-1]}\n")
store = {}
for src in ("seg", "pure"):
print(f"===== 中枢来源: {src} =====")
final_keys, valid, mature = evaluate(df, args.tf, args.symbol, src, args.window, args.step)
fwd = rt_returns(valid, df)
store[src] = (final_keys, valid, mature, fwd)
print(f" 全量口径买卖点 = {len(final_keys)}")
print(f" 重放有效信号 = {len(valid)}(已成熟 {len(mature)}")
print(f" 实时首见滞后 = 中位数 {valid['observed_lag'].median():.0f} 根 / "
f"均值 {valid['observed_lag'].mean():.1f} 根")
print(f" 幻影率 = {(~mature['in_final']).mean() * 100:.1f}%")
print(f" 存活度 = {mature['persist_ratio'].mean():.3f}\n")
print("\n########## 滞后对比 ##########")
for src in ("seg", "pure"):
v = store[src][1]
q = v["observed_lag"].quantile([0.25, 0.5, 0.75])
print(f" {src:5s} P25={q[0.25]:5.0f} 中位数={q[0.5]:5.0f} P75={q[0.75]:5.0f} n={len(v)}")
print("\n########## 分类型滞后中位数 ##########")
comp = pd.DataFrame({
src: store[src][1].groupby("bsp_type")["observed_lag"].median()
for src in ("seg", "pure")
})
comp["改善(根)"] = comp["seg"] - comp["pure"]
print(comp.to_string())
print("\n########## 分类型信号数量与幻影率 ##########")
cnt = pd.DataFrame({
f"{src}_n": store[src][1].groupby("bsp_type").size() for src in ("seg", "pure")
})
ph = pd.DataFrame({
f"{src}_幻影": store[src][2].groupby("bsp_type")["in_final"].apply(lambda s: (1 - s.mean()) * 100)
for src in ("seg", "pure")
})
print(pd.concat([cnt, ph], axis=1).round(1).to_string())
print("\n########## 无未来函数收益:全部信号 ##########")
for src in ("seg", "pure"):
print(f"--- {src} ---")
print(fmt(stats_block(store[src][3], df, None, "ALL")))
print("\n########## 无未来函数收益:仅做多 ##########")
for src in ("seg", "pure"):
fwd = store[src][3]
print(f"--- {src} ---")
print(fmt(stats_block(fwd, df, fwd.direction == 1, "LONG")))
print("\n########## 无未来函数收益:仅做空 ##########")
for src in ("seg", "pure"):
fwd = store[src][3]
print(f"--- {src} ---")
print(fmt(stats_block(fwd, df, fwd.direction == -1, "SHORT")))
print("\n########## pure 口径分类型(持有 5 / 20 根)##########")
fwd = store["pure"][3]
rows = []
for t in sorted(fwd["bsp_type"].unique()):
rows += stats_block(fwd, df, fwd.bsp_type == t, t)
d = pd.DataFrame(rows)
d = d[d["horizon"].isin([5, 20])]
print(fmt(d.to_dict("records")))
if __name__ == "__main__":
main()