自动刷新改用 tail update 与 scrollToPosition 恢复视窗,避免 setData 后跳到最右;拆分 chart_tv 模块并扩展 analyze/recent API。同步威科夫分析、pipeline 增量构建及相关策略与配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
240 lines
7.1 KiB
Python
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()
|