Files
Chan/scripts/wyckoff_phase2_compare.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

207 lines
6.2 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 Phase2 对比:同一数据 / 同一成本 / 同一 WFO / 同一 Regime
对比:
- Wyckoff_BTC_V1_BASELINE (Spring, range off)
- Wyckoff_BTC_LPS (LPS continuation, range off)
统一看 net PFfee 计入)。
"""
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()