删除根目录旧 Chan 模块、策略、配置、文档及 wyckoff 相关代码;更新缠论 pipeline 与笔中枢计算;补充 research 研究与 web 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
170 lines
6.4 KiB
Python
170 lines
6.4 KiB
Python
"""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()
|