自动刷新改用 tail update 与 scrollToPosition 恢复视窗,避免 setData 后跳到最右;拆分 chart_tv 模块并扩展 analyze/recent API。同步威科夫分析、pipeline 增量构建及相关策略与配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
207 lines
6.2 KiB
Python
207 lines
6.2 KiB
Python
#!/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()
|