fix(web): 自动刷新保留 K 线视窗;威科夫与图表增量更新
自动刷新改用 tail update 与 scrollToPosition 恢复视窗,避免 setData 后跳到最右;拆分 chart_tv 模块并扩展 analyze/recent API。同步威科夫分析、pipeline 增量构建及相关策略与配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user