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,239 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Negative-domain audit
|
||||
|
||||
问题:Gate 拦掉的 Spring,是否集中死在 distribution | markdown | range(结构性错误),
|
||||
而不是偶然删掉赚钱样本?
|
||||
|
||||
方法(Spring 冻结,Gate=state_set):
|
||||
1) 跑 Baseline,取出全部 SPRING_LONG 成交
|
||||
2) 用因果 8h market_state 标注入场时状态
|
||||
3) 按 allow_spring 分成 kept vs blocked
|
||||
4) 比较各域 n / PF / winrate / sum%
|
||||
|
||||
判定:
|
||||
- blocked 主要落在 bad domains
|
||||
- blocked 整体 PF << kept(或明显更差)
|
||||
- kept 域仍以 accumulation|markup 为主
|
||||
"""
|
||||
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
|
||||
|
||||
OUT = ROOT / "user_data/Chan/scripts/wyckoff_negative_domain_audit_result.json"
|
||||
PAIR = "BTC/USDT:USDT"
|
||||
CFG = ROOT / "user_data/Chan/config/Wyckoff_BTC_V1_BASELINE.json"
|
||||
GOOD = {"accumulation", "markup"}
|
||||
BAD = {"distribution", "markdown", "range"}
|
||||
|
||||
|
||||
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, "winrate": 0.0, "sum_pct": 0.0, "avg_pct": 0.0}
|
||||
return {
|
||||
"n": len(ps),
|
||||
"pf": round(_pf(ps), 3),
|
||||
"winrate": round(100.0 * sum(1 for p in ps if p > 0) / len(ps), 1),
|
||||
"sum_pct": round(100.0 * float(np.sum(ps)), 2),
|
||||
"avg_pct": round(100.0 * float(np.mean(ps)), 2),
|
||||
}
|
||||
|
||||
|
||||
def run_baseline_spring_trades() -> list[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]
|
||||
|
||||
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()
|
||||
rows = []
|
||||
for t in LocalTrade.bt_trades:
|
||||
tag = t.enter_tag or ""
|
||||
if "SPRING" not in tag:
|
||||
continue
|
||||
rows.append(
|
||||
{
|
||||
"entry_date": t.open_date_utc.isoformat() if t.open_date_utc else "",
|
||||
"exit_date": t.close_date_utc.isoformat() if t.close_date_utc else "",
|
||||
"enter_tag": tag,
|
||||
"profit_ratio": float(t.close_profit or 0.0),
|
||||
"era": (
|
||||
"2023plus"
|
||||
if t.open_date_utc and t.open_date_utc >= pd.Timestamp("2023-01-01", tz="UTC")
|
||||
else "pre_2023"
|
||||
),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def annotate(trades: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
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()
|
||||
|
||||
out = []
|
||||
for t in trades:
|
||||
ed = pd.Timestamp(t["entry_date"])
|
||||
if ed.tzinfo is None:
|
||||
ed = ed.tz_localize("UTC")
|
||||
idx = h8.index.get_indexer([ed], method="ffill")[0]
|
||||
if idx < 0:
|
||||
continue
|
||||
row = h8.iloc[idx]
|
||||
state = str(row["market_state"])
|
||||
allowed = bool(row["allow_spring"])
|
||||
rec = {
|
||||
**t,
|
||||
"market_state": state,
|
||||
"allow_spring": allowed,
|
||||
"domain": "good" if state in GOOD else ("bad" if state in BAD else "other"),
|
||||
"accumulation_score": float(row["accumulation_score"]),
|
||||
"markup_score": float(row["markup_score"]),
|
||||
"distribution_score": float(row["distribution_score"]),
|
||||
"markdown_score": float(row["markdown_score"]),
|
||||
"range_score": float(row["range_score"]),
|
||||
}
|
||||
out.append(rec)
|
||||
return out
|
||||
|
||||
|
||||
def bucket(rows: list[dict], key: str) -> dict[str, Any]:
|
||||
g: dict[str, list[float]] = defaultdict(list)
|
||||
for r in rows:
|
||||
g[str(r[key])].append(float(r["profit_ratio"]))
|
||||
return {k: _stats(v) for k, v in sorted(g.items(), key=lambda x: -len(x[1]))}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logging.getLogger("freqtrade").setLevel(logging.ERROR)
|
||||
install_offline_markets([PAIR])
|
||||
|
||||
print("===== Baseline SPRING_LONG trades =====", flush=True)
|
||||
raw = run_baseline_spring_trades()
|
||||
print(f" spring trades={len(raw)}", flush=True)
|
||||
rows = annotate(raw)
|
||||
kept = [r for r in rows if r["allow_spring"]]
|
||||
blocked = [r for r in rows if not r["allow_spring"]]
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"pair": PAIR,
|
||||
"fee_model": "fee5bps+slip5bps",
|
||||
"n_spring_total": len(rows),
|
||||
"n_kept": len(kept),
|
||||
"n_blocked": len(blocked),
|
||||
"kept": {
|
||||
"overall": _stats([r["profit_ratio"] for r in kept]),
|
||||
"by_state": bucket(kept, "market_state"),
|
||||
"by_era": bucket(kept, "era"),
|
||||
},
|
||||
"blocked": {
|
||||
"overall": _stats([r["profit_ratio"] for r in blocked]),
|
||||
"by_state": bucket(blocked, "market_state"),
|
||||
"by_era": bucket(blocked, "era"),
|
||||
"by_domain": bucket(blocked, "domain"),
|
||||
},
|
||||
"blocked_share_by_state": {},
|
||||
"verdict": {},
|
||||
}
|
||||
|
||||
# blocked 状态占比
|
||||
if blocked:
|
||||
for st, stt in result["blocked"]["by_state"].items():
|
||||
result["blocked_share_by_state"][st] = round(stt["n"] / len(blocked), 3)
|
||||
|
||||
bad_n = sum(result["blocked"]["by_state"].get(s, {}).get("n", 0) for s in BAD)
|
||||
blocked_bad_share = (bad_n / len(blocked)) if blocked else 0.0
|
||||
kept_good_share = 0.0
|
||||
if kept:
|
||||
kg = sum(1 for r in kept if r["market_state"] in GOOD)
|
||||
kept_good_share = kg / len(kept)
|
||||
|
||||
bk = result["blocked"]["overall"]
|
||||
kp = result["kept"]["overall"]
|
||||
result["verdict"] = {
|
||||
"blocked_mostly_bad_domain": blocked_bad_share >= 0.8,
|
||||
"blocked_bad_share": round(blocked_bad_share, 3),
|
||||
"kept_mostly_good_domain": kept_good_share >= 0.95,
|
||||
"kept_good_share": round(kept_good_share, 3),
|
||||
"blocked_pf_worse_than_kept": bk["pf"] < kp["pf"],
|
||||
"blocked_pf": bk["pf"],
|
||||
"kept_pf": kp["pf"],
|
||||
"status": (
|
||||
"PASS"
|
||||
if (
|
||||
blocked_bad_share >= 0.8
|
||||
and kept_good_share >= 0.95
|
||||
and bk["pf"] < kp["pf"]
|
||||
)
|
||||
else "PARTIAL"
|
||||
if (blocked_bad_share >= 0.7 and bk["pf"] <= kp["pf"])
|
||||
else "FAIL"
|
||||
),
|
||||
"note": "PASS = Gate filters structural bad domains, not random sample deletion.",
|
||||
}
|
||||
|
||||
print("\n===== KEPT (allow_spring) =====", flush=True)
|
||||
print(json.dumps(result["kept"], indent=2, ensure_ascii=False))
|
||||
print("\n===== BLOCKED =====", flush=True)
|
||||
print(json.dumps(result["blocked"], indent=2, ensure_ascii=False))
|
||||
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()
|
||||
Reference in New Issue
Block a user