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