Files
Chan/scripts/wyckoff_phase3_evidence.py
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

262 lines
8.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Phase3 — Evidence Expansion(不改 Spring 规则)
目标: 将样本从 N=20 推向 N>=50
手段:
- 多品种外部验证(本地有数据的 pair)
- 分开统计 SPRING_LONG / UTAD_SHORT
- 同一净成本模型(fee+slip
- 不引入 LPS、不扫参
用法:
.venv/bin/python user_data/Chan/scripts/wyckoff_phase3_evidence.py
缺 4h/8h 时从 1h resample(离线,不依赖 API)。
BTC 若无 2019 更早数据,脚本会标明 gap,不伪造历史。
"""
from __future__ import annotations
import json
import logging
import sys
from pathlib import Path
from typing import Any, Optional
import pandas as pd
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
from user_data.Chan.scripts.wyckoff_tf_grid import install_offline_markets # noqa: E402
DATADIR = ROOT / "user_data/data/binance/futures"
STRAT = "Wyckoff_BTC_V1_BASELINE"
CONFIG = ROOT / "user_data/Chan/config/Wyckoff_BTC_V1_BASELINE.json"
OUT = ROOT / "user_data/Chan/scripts/wyckoff_phase3_evidence_result.json"
# 候选外部验证(规则冻结;用本地最长可用历史)
CANDIDATES = [
{"pair": "BTC/USDT:USDT", "file": "BTC_USDT_USDT", "timerange": "20190901-"},
{"pair": "ETH/USDT:USDT", "file": "ETH_USDT_USDT", "timerange": "20191101-"},
{"pair": "SOL/USDT:USDT", "file": "SOL_USDT_USDT", "timerange": "20200901-"},
]
MIN_1H_BARS = 4000 # ~ema200@8h 需要足够历史;过短 skip
def ensure_tf(file_stub: str, tf: str, source_tf: str = "1h") -> bool:
"""从更细周期 resample 生成 tf feather;已存在则跳过。"""
out = DATADIR / f"{file_stub}-{tf}-futures.feather"
src = DATADIR / f"{file_stub}-{source_tf}-futures.feather"
if out.exists():
return True
if not src.exists():
return False
df = pd.read_feather(src)
df["date"] = pd.to_datetime(df["date"], utc=True)
df = df.set_index("date").sort_index()
rule = tf.replace("m", "min") if tf.endswith("m") else tf
ohlc = df.resample(rule).agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
).dropna(subset=["open", "close"])
ohlc = ohlc.reset_index()
ohlc.to_feather(out)
print(f" resampled {out.name} n={len(ohlc)}", flush=True)
return True
def pair_ready(file_stub: str) -> tuple[bool, str]:
p1 = DATADIR / f"{file_stub}-1h-futures.feather"
if not p1.exists():
return False, "missing 1h"
df = pd.read_feather(p1)
n = len(df)
if n < MIN_1H_BARS:
return False, f"1h bars={n} < {MIN_1H_BARS} (insufficient for 8h ema200)"
ok4 = ensure_tf(file_stub, "4h")
ok8 = ensure_tf(file_stub, "8h")
if not (ok4 and ok8):
return False, "cannot build 4h/8h"
return True, f"1h={n}"
def run_bt(pair: str, timerange: str, fee: float = 0.0005, extra: float = 0.0) -> dict[str, Any]:
from freqtrade.configuration import Configuration
from freqtrade.enums import RunMode
from freqtrade.optimize.backtesting import Backtesting
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]
config = Configuration.from_files([str(CONFIG)])
config.update(
{
"strategy": STRAT,
"strategy_path": str(ROOT / "user_data/Chan/strategies"),
"timerange": timerange,
"timeframe": "1h",
"export": "none",
"runmode": RunMode.BACKTEST,
"datadir": ROOT / "user_data/data/binance",
"user_data_dir": ROOT / "user_data",
"enable_protections": False,
"fee": fee + extra,
"exchange": {
**config.get("exchange", {}),
"pair_whitelist": [pair],
"name": config.get("exchange", {}).get("name", "binance"),
},
}
)
bt = Backtesting(config)
bt.start()
st = bt.results["strategy"].get(STRAT) or list(bt.results["strategy"].values())[0]
profit = st.get("profit_total_pct")
if profit is None:
profit = float(st.get("profit_total") or 0) * 100
# 按 enter_tag 拆分(freqtrade 可能是 dict 或 list[dict]
by_tag: dict[str, dict[str, Any]] = {}
trades = st.get("trades") or []
tag_stats = st.get("results_per_enter_tag") or {}
items = []
if isinstance(tag_stats, dict):
items = list(tag_stats.items())
elif isinstance(tag_stats, list):
items = [
(x.get("key") or x.get("enter_tag") or x.get("tag") or "unknown", x)
for x in tag_stats
if isinstance(x, dict)
]
if items:
for tag, info in items:
if not isinstance(info, dict):
continue
by_tag[str(tag)] = {
"trades": int(info.get("trades") or info.get("total_trades") or 0),
"profit_pct": float(
info.get("profit_total_pct")
if info.get("profit_total_pct") is not None
else (float(info.get("profit_total") or 0) * 100)
),
"pf": float(info.get("profit_factor") or 0),
}
elif trades:
from collections import defaultdict
agg: dict[str, list] = defaultdict(list)
for t in trades:
tag = t.get("enter_tag") or "unknown"
agg[tag].append(float(t.get("profit_ratio") or 0))
for tag, profits in agg.items():
wins = [p for p in profits if p > 0]
losses = [-p for p in profits if p <= 0]
gross_win = sum(wins)
gross_loss = sum(losses)
pf = (gross_win / gross_loss) if gross_loss > 0 else (999.0 if gross_win > 0 else 0.0)
by_tag[tag] = {
"trades": len(profits),
"profit_pct": sum(profits) * 100,
"pf": float(pf),
}
return {
"pair": pair,
"timerange": timerange,
"profit_pct": float(profit),
"trades": int(st.get("total_trades") or 0),
"dd_pct": float(st.get("max_drawdown_account") or 0) * 100,
"pf": float(st.get("profit_factor") or 0),
"winrate": float(st.get("winrate") or 0) * 100,
"fee_used": config["fee"],
"by_setup": by_tag,
}
def main() -> None:
logging.getLogger("freqtrade").setLevel(logging.ERROR)
install_offline_markets([c["pair"] for c in CANDIDATES])
results: dict[str, Any] = {
"phase": "Phase3 Evidence Expansion",
"strategy": STRAT,
"rule": "frozen Spring-only; no LPS; no param change",
"pairs": {},
"skipped": {},
"notes": [],
}
# BTC 历史缺口说明
btc_1h = DATADIR / "BTC_USDT_USDT-1h-futures.feather"
if btc_1h.exists():
d0 = pd.read_feather(btc_1h)["date"].min()
results["notes"].append(
f"BTC local 1h starts {d0}; 2019-2022 not in datadir — download separately for deeper N"
)
print("===== Phase3: prepare TF data =====", flush=True)
run_list = []
for c in CANDIDATES:
ok, msg = pair_ready(c["file"])
if ok:
print(f" READY {c['pair']}: {msg}", flush=True)
run_list.append(c)
else:
print(f" SKIP {c['pair']}: {msg}", flush=True)
results["skipped"][c["pair"]] = msg
print("\n===== Phase3: backtests (fee 5bps, then fee+slip) =====", flush=True)
total_n = 0
spring_n = 0
utad_n = 0
for c in run_list:
print(f"\n--- {c['pair']} ---", flush=True)
base = run_bt(c["pair"], c["timerange"], fee=0.0005, extra=0.0)
mid = run_bt(c["pair"], c["timerange"], fee=0.0005, extra=0.0005)
block = {"base_fee": base, "net_mid": mid}
results["pairs"][c["pair"]] = block
total_n += base["trades"]
for tag, info in base.get("by_setup", {}).items():
if "SPRING" in tag:
spring_n += info["trades"]
if "UTAD" in tag:
utad_n += info["trades"]
print(
f" fee5bps profit={base['profit_pct']:.2f}% n={base['trades']} "
f"dd={base['dd_pct']:.1f}% pf={base['pf']:.2f}",
flush=True,
)
print(
f" net_mid profit={mid['profit_pct']:.2f}% n={mid['trades']} "
f"pf={mid['pf']:.2f}",
flush=True,
)
print(f" by_setup {base.get('by_setup')}", flush=True)
results["aggregate"] = {
"pairs_tested": len(run_list),
"total_trades": total_n,
"spring_long_trades": spring_n,
"utad_short_trades": utad_n,
"target_n": 50,
"target_met": total_n >= 50,
"next": (
"目标 N>=50 已达成 — 再看跨品种 net PF 是否仍>1.3"
if total_n >= 50
else "继续补历史数据(BTC 2019+)或更多品种 1h/4h/8h"
),
}
print("\n===== Aggregate =====")
print(json.dumps(results["aggregate"], ensure_ascii=False, indent=2))
OUT.write_text(json.dumps(results, indent=2, ensure_ascii=False))
print(f"\nSaved {OUT}")
if __name__ == "__main__":
main()