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