#!/usr/bin/env python3 """ Wyckoff Phase2 对比:同一数据 / 同一成本 / 同一 WFO / 同一 Regime 对比: - Wyckoff_BTC_V1_BASELINE (Spring, range off) - Wyckoff_BTC_LPS (LPS continuation, range off) 统一看 net PF(fee 计入)。 """ from __future__ import annotations import json import logging import sys from pathlib import Path from typing import Any, Optional ROOT = Path(__file__).resolve().parents[3] sys.path.insert(0, str(ROOT)) from user_data.Chan.scripts.wyckoff_tf_grid import install_offline_markets # noqa: E402 OUT = ROOT / "user_data/Chan/scripts/wyckoff_phase2_compare_result.json" WFO = [ ("train", "20230101-20250101"), ("validate", "20250101-20260101"), ("test", "20260101-"), ("full", "20230101-"), ] BRANCHES = [ { "name": "Spring_V1", "strategy": "Wyckoff_BTC_V1_BASELINE", "config": ROOT / "user_data/Chan/config/Wyckoff_BTC_V1_BASELINE.json", "target": {"pf": 1.3, "dd": 10.0, "note": "Spring: PF>1.3 DD<10%"}, }, { "name": "LPS_V2", "strategy": "Wyckoff_BTC_LPS", "config": ROOT / "user_data/Chan/config/Wyckoff_BTC_LPS.json", "target": {"pf": 1.2, "dd": 15.0, "note": "LPS V2: 4h SOS→1h LPS; PF>1.2; ~5-15/yr"}, }, ] def run_bt( strategy: str, config_path: Path, timerange: str, *, fee: float = 0.0005, extra_cost: float = 0.0, regime: Optional[str] = None, ) -> dict[str, Any]: from freqtrade.configuration import Configuration from freqtrade.enums import RunMode from freqtrade.optimize.backtesting import Backtesting import freqtrade.optimize.optimize_reports.bt_output as bt_output bt_output.show_backtest_results = lambda *a, **k: None # type: ignore for mod in list(sys.modules): if strategy in mod or "Wyckoff_BTC" in mod: del sys.modules[mod] # 可选:临时改 regime_mode(写文件) strat_path = ROOT / "user_data/Chan/strategies" / f"{strategy}.py" orig = None if regime is not None: import re orig = strat_path.read_text() text2, n = re.subn( r'^(\tregime_mode: str = )".*"', rf'\g<1>"{regime}"', orig, count=1, flags=re.M, ) if n == 0: raise RuntimeError(f"regime_mode not found in {strategy}") strat_path.write_text(text2) pycache = strat_path.parent / "__pycache__" if pycache.is_dir(): for p in pycache.glob(f"{strategy}*.pyc"): p.unlink(missing_ok=True) try: config = Configuration.from_files([str(config_path)]) config.update( { "strategy": strategy, "strategy_path": str(ROOT / "user_data/Chan/strategies"), "timerange": timerange, "timeframe": "1h", "export": "none", "runmode": RunMode.BACKTEST, "datadir": ROOT / "user_data/data/binance", "user_data_dir": ROOT / "user_data", "enable_protections": False, "fee": fee + extra_cost, } ) bt = Backtesting(config) loaded = getattr(bt.strategylist[0], "regime_mode", None) bt.start() st = bt.results["strategy"].get(strategy) or list(bt.results["strategy"].values())[0] profit = st.get("profit_total_pct") if profit is None: profit = float(st.get("profit_total") or 0) * 100 return { "profit_pct": float(profit), "trades": int(st.get("total_trades") or 0), "dd_pct": float(st.get("max_drawdown_account") or 0) * 100, "pf": float(st.get("profit_factor") or 0), "winrate": float(st.get("winrate") or 0) * 100, "final": float(st.get("final_balance") or 0), "fee_used": config["fee"], "regime_loaded": loaded, } finally: if orig is not None: strat_path.write_text(orig) def main() -> None: logging.getLogger("freqtrade").setLevel(logging.ERROR) install_offline_markets() results: dict[str, Any] = {"branches": {}} for br in BRANCHES: name = br["name"] print(f"\n===== {name} ({br['strategy']}) =====", flush=True) block: dict[str, Any] = {"wfo": {}, "regimes": {}, "cost_stress": {}, "target": br["target"]} print("--- WFO ---", flush=True) for wname, tr in WFO: r = run_bt(br["strategy"], br["config"], tr) block["wfo"][wname] = {"timerange": tr, **r} print( f" {wname:<8} profit={r['profit_pct']:>7.2f}% n={r['trades']:<3} " f"dd={r['dd_pct']:.1f}% pf={r['pf']:.2f} wr={r['winrate']:.1f}%", flush=True, ) print("--- Regime ---", flush=True) for mode in ["trend", "bull", "bear", "range", "all"]: r = run_bt(br["strategy"], br["config"], "20230101-", regime=mode) block["regimes"][mode] = r print( f" {mode:<6} profit={r['profit_pct']:>7.2f}% n={r['trades']:<3} " f"pf={r['pf']:.2f} (loaded={r['regime_loaded']})", flush=True, ) print("--- Cost (net PF) ---", flush=True) for label, fee, extra in [ ("fee_5bps", 0.0005, 0.0), ("fee_5bps+slip_5bps", 0.0005, 0.0005), ("fee_10bps+slip_10bps", 0.0010, 0.0010), ]: r = run_bt(br["strategy"], br["config"], "20230101-", fee=fee, extra_cost=extra) block["cost_stress"][label] = r flag = "OK" if r["pf"] >= br["target"]["pf"] else ("WEAK" if r["pf"] >= 1.0 else "FAIL") print( f" {label:<22} profit={r['profit_pct']:>7.2f}% n={r['trades']:<3} " f"pf={r['pf']:.2f} [{flag}]", flush=True, ) full = block["wfo"]["full"] mid = block["cost_stress"]["fee_5bps+slip_5bps"] years = 3.6 # ~2023→2026.6 tpy = full["trades"] / years if years else 0 block["verdict"] = { "full_pf": full["pf"], "full_dd": full["dd_pct"], "trades_per_year": tpy, "net_mid_pf": mid["pf"], "target_pf_ok": mid["pf"] >= br["target"]["pf"], "target_dd_ok": full["dd_pct"] <= br["target"]["dd"], } results["branches"][name] = block print(f"Verdict: {json.dumps(block['verdict'], ensure_ascii=False)}", flush=True) # 组合粗估:独立回测不可简单相加;只报告各自频率目标 s = results["branches"]["Spring_V1"]["verdict"] l = results["branches"]["LPS_V2"]["verdict"] results["portfolio_note"] = { "spring_tpy": s["trades_per_year"], "lps_tpy": l["trades_per_year"], "sum_tpy_approx": s["trades_per_year"] + l["trades_per_year"], "combined_target_tpy": "10-20", "warning": "频率可近似相加;PF/收益不可相加,需另做组合回测;Spring 冻结勿改", } print("\n===== Portfolio note =====") print(json.dumps(results["portfolio_note"], ensure_ascii=False, indent=2)) OUT.write_text(json.dumps(results, indent=2, ensure_ascii=False)) print(f"\nSaved {OUT}") if __name__ == "__main__": main()