Files
Chan/scripts/wyckoff_negative_domain_audit.py
T
jackyu66gitandCursor 8ee11317d3 fix(web): 自动刷新保留 K 线视窗;威科夫与图表增量更新
自动刷新改用 tail update 与 scrollToPosition 恢复视窗,避免 setData 后跳到最右;拆分 chart_tv 模块并扩展 analyze/recent API。同步威科夫分析、pipeline 增量构建及相关策略与配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 22:57:43 +08:00

240 lines
7.1 KiB
Python

#!/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()