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:
jackyu66git
2026-08-25 22:57:43 +08:00
co-authored by Cursor
parent 1e60ab3bfa
commit 8ee11317d3
104 changed files with 21452 additions and 4988 deletions
+221
View File
@@ -0,0 +1,221 @@
#!/usr/bin/env python3
"""
Market State Gate OOS — Baseline vs GatedSpring 冻结)
比较:
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()