Files
Chan/scripts/wyckoff_phase2_robustness.py
T
jackyu66gitandCursor 8ee11317d3 fix(web): 自动刷新保留 K 线视窗;威科夫与图表增量更新
自动刷新改用 tail update 与 scrollToPosition 恢复视窗,避免 setData 后跳到最右;拆分 chart_tv 模块并扩展 analyze/recent API。同步威科夫分析、pipeline 增量构建及相关策略与配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 22:57:43 +08:00

186 lines
5.3 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.
#!/usr/bin/env python3
"""
Wyckoff Phase 2:鲁棒性验证(固定当前参数,不再扫参)
1) Walk-ForwardTrain 2023-2024 / Validate 2025 / Test 2026
2) 市场状态拆分:bull / bear / range8h 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()