refactor: 精简仓库为 chanlun 核心与 web 分析,移除威科夫与遗留模块

删除根目录旧 Chan 模块、策略、配置、文档及 wyckoff 相关代码;更新缠论 pipeline 与笔中枢计算;补充 research 研究与 web 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-27 01:05:12 +08:00
co-authored by Cursor
parent 5c10e35b76
commit 7f393b93ed
360 changed files with 140008 additions and 41167 deletions
+166
View File
@@ -0,0 +1,166 @@
"""Step 2Walk-forward 重放,检验缠论买卖点在实时环境下是否真实可交易。
与 Step 1 的区别:入场时刻不再取引擎自报的 sure_time,而是取信号在滑动窗口重放中
「第一次真正可被观测到」的时刻。这才是没有未来函数的口径。
"""
from __future__ import annotations
import argparse
import sys
import time
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 DEFAULT_HORIZONS, baseline_stats, run_pipeline
from lib.data import fetch_ohlcv
from lib.walkforward import analyze_stability, replay
pd.set_option("display.width", 220)
from chanlun.core.ChanEnum import Chan_BSP_DIR
def realtime_event_study(life: pd.DataFrame, df: pd.DataFrame, horizons=DEFAULT_HORIZONS) -> pd.DataFrame:
"""以 first_seen 为入场点做事件研究(无未来函数)。"""
closes = df["close"].to_numpy(dtype=float)
n = len(df)
rows = []
for _, r in life.iterrows():
i = int(r["first_seen_idx"])
d = int(r["direction"])
entry = closes[i]
row = {"bsp_type": r["bsp_type"], "direction": d,
"side": "LONG" if d == 1 else "SHORT", "entry_idx": i}
for h in horizons:
j = i + h
row[f"ret_{h}"] = d * (closes[j] - entry) / entry if j < n else np.nan
rows.append(row)
return pd.DataFrame(rows)
def summarize_rt(fwd: pd.DataFrame, df: pd.DataFrame, horizons=DEFAULT_HORIZONS) -> pd.DataFrame:
base = baseline_stats(df, horizons).set_index("horizon")
groups = [("ALL", fwd), ("ALL_LONG", fwd[fwd.direction == 1]), ("ALL_SHORT", fwd[fwd.direction == -1])]
groups += [(t, g) for t, g in fwd.groupby("bsp_type")]
rows = []
for name, g in groups:
if g.empty:
continue
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()
base_mean = float(np.mean(dirs) * base.loc[h, "base_mean_long"])
sd = r.std(ddof=1) if len(r) > 1 else np.nan
rows.append({
"group": name, "horizon": h, "n": len(r),
"mean": r.mean(), "winrate": (r > 0).mean(),
"excess": r.mean() - base_mean,
"tstat": r.mean() / (sd / np.sqrt(len(r))) if sd else np.nan,
})
return pd.DataFrame(rows)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--symbol", default="BTC/USDT:USDT")
ap.add_argument("--tf", default="1h")
ap.add_argument("--limit", type=int, default=50000)
ap.add_argument("--window", type=int, default=3000)
ap.add_argument("--step", type=int, default=6)
ap.add_argument("--tail", type=int, default=0, help="只用最后N根做快速验证,0=全量")
args = ap.parse_args()
df = fetch_ohlcv(args.symbol, args.tf, args.limit)
if args.tail:
df = df.tail(args.tail).reset_index(drop=True)
print(f"[data] {args.symbol} {args.tf} rows={len(df)} {df['date'].iloc[0]} -> {df['date'].iloc[-1]}")
_, final_bsp = run_pipeline(df, args.tf)
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
}
print(f"[final] 全量口径买卖点 = {len(final_keys)}")
n_windows = len(range(args.window, len(df) + 1, args.step))
print(f"[replay] window={args.window} step={args.step}{n_windows} 个窗口")
t0 = time.time()
result = replay(
df, args.tf, window=args.window, step=args.step,
cache_key=f"{args.symbol.replace('/', '_').replace(':', '-')}_{args.tf}",
)
print(f"[replay] 完成,用时 {time.time() - t0:.0f}s")
life = analyze_stability(result, final_keys, df, window=args.window)
out_dir = Path(__file__).parent / "out"
out_dir.mkdir(exist_ok=True)
tag = f"{args.symbol.split('/')[0]}_{args.tf}" + (f"_tail{args.tail}" if args.tail else "")
life.to_csv(out_dir / f"step2_life_{tag}.csv", index=False)
valid = life[~life["truncated"]].copy()
mature = valid[~valid["immature"]].copy()
print(f"\n[信号总数] 重放中出现 = {len(life)};剔除左截断后 = {len(valid)};其中已成熟 = {len(mature)}")
if valid.empty:
return
print("\n=== 信号稳定性 ===")
print(f"[幻影率] 实时报出但完整历史上不存在: {(~mature['in_final']).mean() * 100:.1f}%"
f" ({(~mature['in_final']).sum()}/{len(mature)})")
print(f"[撤销率] 窗口内报出后又被抹掉 : {(~mature['alive_at_scope_end']).mean() * 100:.1f}%"
f" ({(~mature['alive_at_scope_end']).sum()}/{len(mature)})")
print(f"[存活度] persist_ratio 均值 : {mature['persist_ratio'].mean():.3f}")
print(
mature.groupby("bsp_type")
.agg(n=("in_final", "size"),
phantom=("in_final", lambda s: 1 - s.mean()),
revoked=("alive_at_scope_end", lambda s: 1 - s.mean()),
persist=("persist_ratio", "mean"))
.assign(phantom=lambda d: (d["phantom"] * 100).map(lambda v: f"{v:.1f}%"),
revoked=lambda d: (d["revoked"] * 100).map(lambda v: f"{v:.1f}%"),
persist=lambda d: d["persist"].map(lambda v: f"{v:.3f}"))
.to_string()
)
print("\n=== 真实滞后 ===")
idx_of = {t: i for i, t in enumerate(df["date"].dt.strftime("%Y-%m-%d %H:%M:%S"))}
sure_lag = {}
for b in final_bsp:
if b.is_sure and b.sure_time is not None and str(b.sure_time) in idx_of:
k = str(b.end_time)
if k in idx_of:
sure_lag[k] = idx_of[str(b.sure_time)] - idx_of[k]
valid["sure_lag"] = valid["fx_time"].map(sure_lag)
print(f"[实时首见滞后] 中位数 = {valid['observed_lag'].median():.0f}"
f" 均值 = {valid['observed_lag'].mean():.1f}")
both = valid.dropna(subset=["sure_lag"])
if not both.empty:
print(f"[引擎自报滞后] 中位数 = {both['sure_lag'].median():.0f}"
f" 均值 = {both['sure_lag'].mean():.1f}")
print(f"[被低估的滞后] 中位数 = {(both['observed_lag'] - both['sure_lag']).median():.0f}"
f" 均值 = {(both['observed_lag'] - both['sure_lag']).mean():.1f}")
print("\n=== 无未来函数事件研究(以实时首见时刻入场)===")
fwd = realtime_event_study(valid, df)
s = summarize_rt(fwd, df)
out = s.copy()
for c in ("mean", "excess"):
out[c] = out[c].map(lambda v: f"{v * 100:+.2f}%")
out["winrate"] = out["winrate"].map(lambda v: f"{v * 100:.0f}%")
out["tstat"] = out["tstat"].map(lambda v: f"{v:+.2f}")
print(out.to_string(index=False))
fwd.to_csv(out_dir / f"step2_rt_events_{tag}.csv", index=False)
s.to_csv(out_dir / f"step2_rt_summary_{tag}.csv", index=False)
print(f"\n明细已写入 {out_dir}/step2_*_{tag}.csv")
if __name__ == "__main__":
main()