#!/usr/bin/env python3 """ Wyckoff Phase 2:鲁棒性验证(固定当前参数,不再扫参) 1) Walk-Forward:Train 2023-2024 / Validate 2025 / Test 2026 2) 市场状态拆分:bull / bear / range(8h EMA200 语境) 3) 成本压力:抬高手续费 + 滑点后是否仍 PF>1.3 """ from __future__ import annotations import json import logging import re 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 ( # noqa: E402 CONFIG_PATH, STRAT_PATH, install_offline_markets, patch_strategy, ) OUT = ROOT / "user_data/Chan/scripts/wyckoff_phase2_result.json" WFO = [ ("train", "20230101-20250101"), ("validate", "20250101-20260101"), ("test", "20260101-"), ("full", "20230101-"), ] def set_regime(mode: str) -> None: text = STRAT_PATH.read_text() text2, n = re.subn( r'^(\tregime_mode: str = )".*"', rf'\g<1>"{mode}"', text, count=1, flags=re.M, ) if n == 0: raise RuntimeError("regime_mode not found in strategy") STRAT_PATH.write_text(text2) # 清掉 bytecode,避免连续切换时读到旧 class 属性 pycache = STRAT_PATH.parent / "__pycache__" if pycache.is_dir(): for p in pycache.glob("Wyckoff_BTC*.pyc"): p.unlink(missing_ok=True) def run_bt( timerange: str, *, fee: Optional[float] = None, 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 if regime is not None: set_regime(regime) for mod in list(sys.modules): if "Wyckoff_BTC" in mod: del sys.modules[mod] config = Configuration.from_files([str(CONFIG_PATH)]) config.update( { "strategy": "Wyckoff_BTC", "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, } ) base_fee = 0.0005 if fee is None else fee config["fee"] = base_fee + extra_cost bt = Backtesting(config) loaded_regime = getattr(bt.strategylist[0], "regime_mode", None) bt.start() st = bt.results["strategy"].get("Wyckoff_BTC") 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_regime, } def main() -> None: logging.getLogger("freqtrade").setLevel(logging.ERROR) install_offline_markets() orig = STRAT_PATH.read_text() results: dict[str, Any] = {"wfo": {}, "regimes": {}, "cost_stress": {}} try: patch_strategy("1h", "4h", "8h") set_regime("all") print("===== 1) Walk-Forward (fixed params, no re-opt) =====") for name, tr in WFO: r = run_bt(tr) results["wfo"][name] = {"timerange": tr, **r} print( f" {name:<8} {tr:<22} profit={r['profit_pct']:>7.2f}% " f"n={r['trades']:<3} dd={r['dd_pct']:.1f}% pf={r['pf']:.2f} wr={r['winrate']:.1f}%", flush=True, ) print("\n===== 2) Regime split (20230101-) =====") for mode in ["all", "bull", "bear", "range"]: r = run_bt("20230101-", regime=mode) results["regimes"][mode] = r print( f" {mode:<6} profit={r['profit_pct']:>7.2f}% n={r['trades']:<3} " f"dd={r['dd_pct']:.1f}% pf={r['pf']:.2f} wr={r['winrate']:.1f}% " f"(loaded={r.get('regime_loaded')})", flush=True, ) set_regime("all") print("\n===== 3) Cost stress (20230101-) =====") for label, fee, extra in [ ("fee_5bps", 0.0005, 0.0), ("fee_10bps", 0.0010, 0.0), ("fee_5bps+slip_5bps", 0.0005, 0.0005), ("fee_10bps+slip_10bps", 0.0010, 0.0010), ]: r = run_bt("20230101-", fee=fee, extra_cost=extra) results["cost_stress"][label] = r flag = "OK" if r["pf"] >= 1.3 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, ) wfo = results["wfo"] results["verdict"] = { "validate_profit_ok": wfo["validate"]["profit_pct"] > 0, "validate_pf_ge_1": wfo["validate"]["pf"] >= 1.0, "test_pf_ge_1": wfo["test"]["pf"] >= 1.0, "cost_mid_pf_ge_1_3": results["cost_stress"]["fee_5bps+slip_5bps"]["pf"] >= 1.3, "next": [ "若 validate/test 稳定 → paper / 小资金", "若仅 train 好 → 参数过拟合,冻结开发", "可并行加 SOS/LPS 趋势跟随以提高频率", ], } print("\n===== Verdict =====") print(json.dumps(results["verdict"], ensure_ascii=False, indent=2)) finally: STRAT_PATH.write_text(orig) print("\nRestored strategy file", flush=True) OUT.write_text(json.dumps(results, indent=2, ensure_ascii=False)) print(f"Saved {OUT}") if __name__ == "__main__": main()