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
+240
View File
@@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""
Gate v1.1 冻结前小范围稳健性确认(不改 Spring / 不调 soft-score
在 Baseline SPRING_LONG 全集上:
- 按年份、era 切片
- 看 blocked 是否仍主要来自 distribution
- kept vs blocked 的 PF 关系是否稳定
range 只作观察桶,不改交易规则。
"""
from __future__ import annotations
import json
import logging
import sys
from collections import defaultdict
from pathlib import Path
from typing import Any
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "user_data/Chan"))
from engine.market_state import compute_market_state_8h # noqa: E402
from user_data.Chan.scripts.wyckoff_tf_grid import install_offline_markets # noqa: E402
AUDIT = ROOT / "user_data/Chan/scripts/wyckoff_negative_domain_audit_result.json"
OUT = ROOT / "user_data/Chan/scripts/wyckoff_gate_robustness_slices_result.json"
PAIR = "BTC/USDT:USDT"
CFG = ROOT / "user_data/Chan/config/Wyckoff_BTC_V1_BASELINE.json"
def _pf(ps: list[float]) -> float:
wins = [p for p in ps if p > 0]
losses = [-p for p in ps if p <= 0]
gw, gl = sum(wins), sum(losses)
if gl <= 0:
return 999.0 if gw > 0 else 0.0
return gw / gl
def _stats(ps: list[float]) -> dict[str, Any]:
if not ps:
return {"n": 0, "pf": 0.0, "sum_pct": 0.0, "winrate": 0.0}
return {
"n": len(ps),
"pf": round(_pf(ps), 3),
"sum_pct": round(100.0 * float(np.sum(ps)), 2),
"winrate": round(100.0 * sum(1 for p in ps if p > 0) / len(ps), 1),
}
def load_annotated_springs() -> list[dict[str, Any]]:
"""复用 audit 逻辑,产出逐笔 annotated SPRING。"""
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]
cfg = Configuration.from_files([str(CFG)])
cfg.update(
{
"strategy": "Wyckoff_BTC_V1_BASELINE",
"strategy_path": str(ROOT / "user_data/Chan/strategies"),
"timerange": "20190901-",
"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,
"exchange": {
**cfg.get("exchange", {}),
"name": "binance",
"pair_whitelist": [PAIR],
},
}
)
bt = Backtesting(cfg)
bt.start()
h8 = pd.read_feather(ROOT / "user_data/data/binance/futures/BTC_USDT_USDT-8h-futures.feather")
h8["date"] = pd.to_datetime(h8["date"], utc=True)
h8 = compute_market_state_8h(h8).set_index("date").sort_index()
rows = []
for t in LocalTrade.bt_trades:
if "SPRING" not in (t.enter_tag or ""):
continue
ed = pd.Timestamp(t.open_date_utc)
if ed.tzinfo is None:
ed = ed.tz_localize("UTC")
idx = h8.index.get_indexer([ed], method="ffill")[0]
if idx < 0:
continue
st = h8.iloc[idx]
state = str(st["market_state"])
rows.append(
{
"entry_date": ed.isoformat(),
"year": str(ed.year),
"era": "2023plus" if ed >= pd.Timestamp("2023-01-01", tz="UTC") else "pre_2023",
"market_state": state,
"allow_spring": bool(st["allow_spring"]),
"profit_ratio": float(t.close_profit or 0.0),
}
)
return rows
def slice_report(rows: list[dict], key: str) -> dict[str, Any]:
out: dict[str, Any] = {}
groups: dict[str, list[dict]] = defaultdict(list)
for r in rows:
groups[str(r[key])].append(r)
for k, rs in sorted(groups.items()):
kept = [x for x in rs if x["allow_spring"]]
blocked = [x for x in rs if not x["allow_spring"]]
b_by_state: dict[str, list[float]] = defaultdict(list)
for x in blocked:
b_by_state[x["market_state"]].append(x["profit_ratio"])
blocked_states = {s: _stats(ps) for s, ps in b_by_state.items()}
dist_n = blocked_states.get("distribution", {}).get("n", 0)
blocked_n = len(blocked)
out[k] = {
"n_total": len(rs),
"kept": _stats([x["profit_ratio"] for x in kept]),
"blocked": _stats([x["profit_ratio"] for x in blocked]),
"blocked_by_state": blocked_states,
"blocked_distribution_share": round(dist_n / blocked_n, 3) if blocked_n else None,
"blocked_all_bad": (
all(s in ("distribution", "markdown", "range") for s in blocked_states)
if blocked_n
else True
),
}
return out
def main() -> None:
logging.getLogger("freqtrade").setLevel(logging.ERROR)
install_offline_markets([PAIR])
print("===== Annotate SPRING_LONG =====", flush=True)
rows = load_annotated_springs()
print(f" n={len(rows)}", flush=True)
by_year = slice_report(rows, "year")
by_era = slice_report(rows, "era")
# 稳定性:有 blocked 的切片里,distribution 是否为第一大来源
dist_primary = []
for label, block in {**{f"year:{k}": v for k, v in by_year.items()}, **{f"era:{k}": v for k, v in by_era.items()}}.items():
bn = block["blocked"]["n"]
if bn < 2:
continue
states = block["blocked_by_state"]
top = max(states.items(), key=lambda x: x[1]["n"])[0] if states else None
dist_primary.append(
{
"slice": label,
"blocked_n": bn,
"top_blocked_state": top,
"distribution_share": block["blocked_distribution_share"],
"blocked_pf": block["blocked"]["pf"],
"kept_pf": block["kept"]["pf"],
}
)
n_slices = len(dist_primary)
n_dist_top = sum(1 for x in dist_primary if x["top_blocked_state"] == "distribution")
n_dist_ge_50 = sum(
1 for x in dist_primary if (x["distribution_share"] or 0) >= 0.5
)
result = {
"n_spring": len(rows),
"by_year": by_year,
"by_era": by_era,
"slice_summaries": dist_primary,
"range_observation_only": {
"note": "range 不作交易规则;仅观察 blocked 中的占比与 PF",
"blocked_range_global": _stats(
[r["profit_ratio"] for r in rows if (not r["allow_spring"] and r["market_state"] == "range")]
),
},
"verdict": {
"slices_with_blocked_ge_2": n_slices,
"distribution_is_top_blocked_state": n_dist_top,
"distribution_share_ge_50pct_slices": n_dist_ge_50,
"distribution_attribution_stable": (
n_slices > 0 and (n_dist_top / n_slices) >= 0.6
),
"status": (
"PASS"
if n_slices > 0 and (n_dist_top / n_slices) >= 0.6
else "PARTIAL"
if n_dist_ge_50 >= max(1, n_slices // 2)
else "FAIL"
),
"note": "PASS = across year/era slices, blocked mass still led by distribution.",
},
}
print("\n===== By year (blocked focus) =====", flush=True)
for y, b in by_year.items():
print(
f" {y}: total={b['n_total']} kept_pf={b['kept']['pf']} "
f"blocked_n={b['blocked']['n']} blocked_pf={b['blocked']['pf']} "
f"dist_share={b['blocked_distribution_share']} states={list(b['blocked_by_state'])}",
flush=True,
)
print("\n===== By era =====", flush=True)
for e, b in by_era.items():
print(
f" {e}: total={b['n_total']} kept_pf={b['kept']['pf']} "
f"blocked_n={b['blocked']['n']} blocked_pf={b['blocked']['pf']} "
f"dist_share={b['blocked_distribution_share']} states={list(b['blocked_by_state'])}",
flush=True,
)
print("\n===== Verdict =====", flush=True)
print(json.dumps(result["verdict"], indent=2, ensure_ascii=False))
OUT.write_text(json.dumps(result, indent=2, ensure_ascii=False))
print(f"\nSaved {OUT}")
if __name__ == "__main__":
main()