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,221 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Market State Gate OOS — Baseline vs Gated(Spring 冻结)
|
||||
|
||||
比较:
|
||||
A) Wyckoff_BTC_V1_BASELINE — Spring always (within trend regime)
|
||||
B) Wyckoff_BTC_GATED — Spring only when causal state gate opens
|
||||
|
||||
阈值先验固定,不对 2023+ 做网格搜索。
|
||||
|
||||
指标: net PF / DD / n / worst year / max consecutive losses
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
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_gate_oos_result.json"
|
||||
PAIR = "BTC/USDT:USDT"
|
||||
|
||||
WINDOWS = [
|
||||
("define_pre2023", "20190901-20230101"), # 观察区(不调参)
|
||||
("oos_2023plus", "20230101-"),
|
||||
("full", "20190901-"),
|
||||
("y2020", "20200101-20210101"),
|
||||
("y2021", "20210101-20220101"),
|
||||
("y2022", "20220101-20230101"),
|
||||
("y2023", "20230101-20240101"),
|
||||
("y2024", "20240101-20250101"),
|
||||
("y2025", "20250101-20260101"),
|
||||
]
|
||||
|
||||
STRATS = [
|
||||
{
|
||||
"name": "baseline",
|
||||
"strategy": "Wyckoff_BTC_V1_BASELINE",
|
||||
"config": ROOT / "user_data/Chan/config/Wyckoff_BTC_V1_BASELINE.json",
|
||||
},
|
||||
{
|
||||
"name": "gated",
|
||||
"strategy": "Wyckoff_BTC_GATED",
|
||||
"config": ROOT / "user_data/Chan/config/Wyckoff_BTC_GATED.json",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _max_consecutive_losses(profits: list[float]) -> int:
|
||||
best = cur = 0
|
||||
for p in profits:
|
||||
if p <= 0:
|
||||
cur += 1
|
||||
best = max(best, cur)
|
||||
else:
|
||||
cur = 0
|
||||
return best
|
||||
|
||||
|
||||
def _worst_year(trades: list[dict]) -> dict[str, Any]:
|
||||
by_y: dict[str, float] = {}
|
||||
for t in trades:
|
||||
ed = t.get("open_date") or t.get("entry_date") or ""
|
||||
y = str(ed)[:4]
|
||||
if len(y) < 4:
|
||||
continue
|
||||
by_y[y] = by_y.get(y, 0.0) + float(t.get("profit_ratio") or 0.0) * 100
|
||||
if not by_y:
|
||||
return {"year": None, "sum_pct": 0.0}
|
||||
y, v = min(by_y.items(), key=lambda x: x[1])
|
||||
return {"year": y, "sum_pct": round(v, 2)}
|
||||
|
||||
|
||||
def run_one(strategy: str, config_path: Path, timerange: str) -> dict[str, Any]:
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.persistence import LocalTrade
|
||||
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 "Wyckoff_BTC" in mod:
|
||||
del sys.modules[mod]
|
||||
|
||||
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": 0.0010, # 5bps fee + 5bps slip
|
||||
"exchange": {
|
||||
**config.get("exchange", {}),
|
||||
"name": "binance",
|
||||
"pair_whitelist": [PAIR],
|
||||
},
|
||||
}
|
||||
)
|
||||
bt = Backtesting(config)
|
||||
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
|
||||
|
||||
trade_rows = []
|
||||
profits = []
|
||||
for t in LocalTrade.bt_trades:
|
||||
pr = float(t.close_profit or 0.0)
|
||||
profits.append(pr)
|
||||
trade_rows.append(
|
||||
{
|
||||
"open_date": t.open_date_utc.isoformat() if t.open_date_utc else "",
|
||||
"enter_tag": t.enter_tag or "",
|
||||
"profit_ratio": pr,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"timerange": timerange,
|
||||
"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,
|
||||
"max_consec_loss": _max_consecutive_losses(profits),
|
||||
"worst_year": _worst_year(trade_rows),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logging.getLogger("freqtrade").setLevel(logging.ERROR)
|
||||
install_offline_markets([PAIR])
|
||||
|
||||
results: dict[str, Any] = {
|
||||
"pair": PAIR,
|
||||
"fee_model": "fee 5bps + slip 5bps",
|
||||
"gate": {
|
||||
"version": "v1.1_state_set",
|
||||
"spring": "market_state ∈ {accumulation, markup}",
|
||||
"utad": "market_state ∈ {distribution, markdown}",
|
||||
"note": "Causal 8h EMA/slope rules (= attribution labels). Scores kept for observability. Not grid-searched on 2023+.",
|
||||
"v1_score_threshold": "FAILED OOS (destroyed 2023+ PF 1.45→0.67); archived as too misaligned",
|
||||
},
|
||||
"windows": {},
|
||||
"verdict": {},
|
||||
}
|
||||
|
||||
print("===== Market State Gate OOS (BTC) =====", flush=True)
|
||||
for wname, tr in WINDOWS:
|
||||
print(f"\n--- {wname} {tr} ---", flush=True)
|
||||
block = {}
|
||||
for s in STRATS:
|
||||
r = run_one(s["strategy"], s["config"], tr)
|
||||
block[s["name"]] = r
|
||||
print(
|
||||
f" {s['name']:<9} profit={r['profit_pct']:>7.2f}% n={r['trades']:<3} "
|
||||
f"dd={r['dd_pct']:.1f}% pf={r['pf']:.2f} "
|
||||
f"mcl={r['max_consec_loss']} worst={r['worst_year']}",
|
||||
flush=True,
|
||||
)
|
||||
# delta gated - baseline
|
||||
b, g = block["baseline"], block["gated"]
|
||||
block["delta_gated_minus_baseline"] = {
|
||||
"pf": round(g["pf"] - b["pf"], 3),
|
||||
"dd_pct": round(g["dd_pct"] - b["dd_pct"], 3),
|
||||
"trades": g["trades"] - b["trades"],
|
||||
"profit_pct": round(g["profit_pct"] - b["profit_pct"], 3),
|
||||
"max_consec_loss": g["max_consec_loss"] - b["max_consec_loss"],
|
||||
}
|
||||
results["windows"][wname] = block
|
||||
|
||||
oos_b = results["windows"]["oos_2023plus"]["baseline"]
|
||||
oos_g = results["windows"]["oos_2023plus"]["gated"]
|
||||
full_b = results["windows"]["full"]["baseline"]
|
||||
full_g = results["windows"]["full"]["gated"]
|
||||
pre_b = results["windows"]["define_pre2023"]["baseline"]
|
||||
pre_g = results["windows"]["define_pre2023"]["gated"]
|
||||
|
||||
results["verdict"] = {
|
||||
"oos_gated_pf_ge_baseline": oos_g["pf"] >= oos_b["pf"] - 1e-9,
|
||||
"oos_gated_pf_ge_1_2": oos_g["pf"] >= 1.2,
|
||||
"oos_gated_dd_le_baseline": oos_g["dd_pct"] <= oos_b["dd_pct"] + 1e-9,
|
||||
"full_gated_pf_gt_baseline": full_g["pf"] > full_b["pf"],
|
||||
"pre2023_not_catastrophically_worse": pre_g["pf"] >= pre_b["pf"] - 0.15,
|
||||
"status": (
|
||||
"PASS"
|
||||
if (
|
||||
oos_g["pf"] >= 1.2
|
||||
and oos_g["dd_pct"] <= oos_b["dd_pct"] + 0.5
|
||||
and full_g["pf"] > full_b["pf"]
|
||||
)
|
||||
else "PARTIAL"
|
||||
if (oos_g["pf"] >= oos_b["pf"] and full_g["pf"] >= full_b["pf"])
|
||||
else "FAIL"
|
||||
),
|
||||
"note": "Gate must not destroy 2023+ edge; should improve or stabilize full-sample robustness.",
|
||||
}
|
||||
print("\n===== Verdict =====")
|
||||
print(json.dumps(results["verdict"], indent=2, ensure_ascii=False))
|
||||
OUT.write_text(json.dumps(results, indent=2, ensure_ascii=False))
|
||||
print(f"Saved {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user