Files
AlphaMaster/backtest_elite.py
2026-07-04 10:34:51 +08:00

275 lines
9.4 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.
"""
backtest_elite.py — 对 elite pool 中的最优因子批量回测,发现问题
用法:
python backtest_elite.py --offline
"""
import sys, json, math
from pathlib import Path
import numpy as np
import torch
sys.path.insert(0, str(Path(__file__).parent))
from config import Config
from data_pipeline.data_manager import MT5DataManager
from data_pipeline.fetcher import MT5DataFetcher
from model_core.vocab import FORMULA_VOCAB, VOCAB_VERSION
from model_core.vm import StackVM
from model_core.features import MT5FeatureEngineer
from strategy_manager.signal import compute_target_positions_stateless
_H1_PER_YEAR = 6240
def decode(toks):
names = FORMULA_VOCAB.token_names
return " -> ".join(names[t] if 0 <= t < len(names) else f"?{t}" for t in toks)
def calc_sharpe(pnl):
m, s = pnl.mean(), pnl.std()
return float(m / (s + 1e-10) * math.sqrt(_H1_PER_YEAR))
def calc_sortino(pnl):
m = pnl.mean()
down = pnl[pnl < 0]
ds = down.std() if len(down) > 0 else 1e-10
ds = max(ds, abs(m), 1e-10)
return float(np.clip(m / ds * math.sqrt(_H1_PER_YEAR), -20, 20))
def calc_mdd(cum_pnl):
peak = np.maximum.accumulate(cum_pnl)
return float((peak - cum_pnl).max())
def calc_ic(factor, target_ret):
"""时序 IC:factor[t] vs target_ret[t+1],逐品种再取均值。"""
N, T = factor.shape
ic_list = []
for n in range(N):
x = factor[n, :-1]
y = target_ret[n, 1:]
xm = x - x.mean()
ym = y - y.mean()
sx = np.sqrt((xm**2).mean())
sy = np.sqrt((ym**2).mean())
if sx < 1e-6 or sy < 1e-6:
continue
ic = (xm * ym).mean() / (sx * sy + 1e-8)
ic_list.append(float(ic))
return float(np.mean(ic_list)) if ic_list else 0.0
def backtest_one(formula, feat, raw_dict, target_ret, cost_rate=0.0001):
"""对一个公式跑完整回测,返回统计结果。"""
vm = StackVM()
factor = vm.execute(formula, feat) # [N, T]
if factor is None:
return None
N, T = factor.shape
factor_np = factor.detach().numpy()
target_np = target_ret.detach().numpy()
# 持仓
pos = compute_target_positions_stateless(factor) # [N, T]
pos_np = pos.detach().numpy()
# 换手
prev = np.zeros_like(pos_np)
prev[:, 1:] = pos_np[:, :-1]
turnover = np.abs(pos_np - prev)
pnl = pos_np * target_np - turnover * cost_rate # [N, T]
# 分品种统计
per_sym = {}
sym_names = ["EURUSD", "USDJPY"][:N]
for i, sym in enumerate(sym_names):
p = pnl[i]
cum = np.cumsum(p)
per_sym[sym] = {
"pnl": p,
"cum": cum,
"total_ret": float(cum[-1]),
"sharpe": calc_sharpe(p),
"sortino": calc_sortino(p),
"mdd": calc_mdd(cum),
"n_trades": int((np.abs(np.diff(pos_np[i])) > 0.5).sum()),
"avg_hold": float(T / max(1, int((np.abs(np.diff(pos_np[i])) > 0.5).sum()))),
}
# 组合
port_pnl = pnl.mean(axis=0)
port_cum = np.cumsum(port_pnl)
# IC
ic = calc_ic(factor_np, target_np)
# 分段分析:前50% vs 后50%(检查过拟合)
split = T // 2
p1 = port_pnl[:split]
p2 = port_pnl[split:]
return {
"formula": formula,
"readable": decode(formula),
"per_sym": per_sym,
"port_total_ret": float(port_cum[-1]),
"port_sharpe": calc_sharpe(port_pnl),
"port_sortino": calc_sortino(port_pnl),
"port_mdd": calc_mdd(port_cum),
"ic": ic,
"n_pos_syms": sum(1 for d in per_sym.values() if d["total_ret"] > 0),
"n_syms": N,
# 稳定性:前后半段
"half1_sharpe": calc_sharpe(p1),
"half2_sharpe": calc_sharpe(p2),
"half1_sortino": calc_sortino(p1),
"half2_sortino": calc_sortino(p2),
# 换手
"avg_turnover": float(turnover.mean()),
"avg_hold_h": float(per_sym[sym_names[0]]["avg_hold"]) if sym_names else 0,
}
def main():
offline = "--offline" in sys.argv
# ── 候选因子(从 elite pool 手工提取,按分数降序)
CANDIDATES = [
# score=6.636 — 当前最优
[40, 51, 81, 83, 86, 125, 88, 125],
# score=6.224 — 变体(TANH_SQUASH→TS_SUM_5)
[40, 51, 81, 83, 86, 125, 88, 112],
# score=5.960 — TRIX × HURST 类型
[50, 105, 94, 103, 59, 66, 119, 88],
# score=5.827 — TRIX × HURST(4 个等分变体取代表)
[50, 105, 89, 103, 59, 88, 112, 66],
# score=5.811
[50, 105, 82, 103, 59, 88, 112, 66],
# 冒烟测试最优(供参比)
[3, 23, 110, 42, 53, 104, 72, 109],
]
print(f"\n{'='*70}")
print(f" Elite Pool 批量回测 | vocab={VOCAB_VERSION} | offline={offline}")
print(f"{'='*70}\n")
# ── 加载数据
print("加载数据...")
with MT5DataFetcher(offline=offline) as fetcher:
mgr = MT5DataManager(fetcher)
mgr.load()
raw_dict = mgr.raw_dict
syms = mgr.symbols
T = raw_dict["open"].shape[1]
print(f" 品种: {syms} T={T} bars")
# 只取 forex 组(EURUSD/USDJPY)
forex_syms = ["EURUSD", "USDJPY"]
forex_idx = [syms.index(s) for s in forex_syms if s in syms]
forex_raw = {k: v[forex_idx] for k, v in raw_dict.items()}
forex_feat = MT5FeatureEngineer.compute_features(forex_raw)
target_ret = mgr.target_ret[forex_idx]
print(f" Forex 品种: {[syms[i] for i in forex_idx]} feat={forex_feat.shape}\n")
# ── 批量回测
results = []
for i, formula in enumerate(CANDIDATES):
res = backtest_one(formula, forex_feat, forex_raw, target_ret)
if res is None:
print(f" [{i+1}] ❌ 公式执行失败: {formula}")
continue
results.append(res)
# ── 打印结果
print(f"\n{'='*70}")
print(f" 回测结果汇总({T} bars H1,forex 组)")
print(f"{'='*70}")
hdr = f" {'#':>3} {'Score*':>7} {'TotRet':>7} {'Sharpe':>7} {'Sortino':>8} {'MDD':>6} {'IC':>7} {'H1':>5} {'H2':>5} {'AvgHold':>8} {'公式类型':}"
print(hdr)
print(f" {'─'*95}")
for i, r in enumerate(results):
# 判断前后半段一致性
h1, h2 = r["half1_sharpe"], r["half2_sharpe"]
consistency = "✓" if h1 > 0 and h2 > 0 else ("⚠" if h1 * h2 > 0 else "✗")
print(f" [{i+1:2d}] {r['port_sortino']:>7.3f} "
f"{r['port_total_ret']:>7.3f} "
f"{r['port_sharpe']:>7.3f} "
f"{r['port_sortino']:>8.3f} "
f"{r['port_mdd']:>6.3f} "
f"{r['ic']:>7.4f} "
f"{h1:>5.2f} {h2:>5.2f} "
f"{r['avg_hold_h']:>8.1f}h "
f"{consistency} {r['readable'][:55]}")
# ── 品种级详情
print(f"\n{'─'*70}")
print(f" 品种级详情(前 3 个公式)")
print(f"{'─'*70}")
for i, r in enumerate(results[:3]):
print(f"\n [{i+1}] {r['readable']}")
for sym, d in r["per_sym"].items():
sig = "✓" if d["total_ret"] > 0 else "✗"
print(f" {sym}: TotRet={d['total_ret']:+.3f} Sharpe={d['sharpe']:+.3f} "
f"Sortino={d['sortino']:+.3f} MDD={d['mdd']:.3f} "
f"Trades={d['n_trades']} AvgHold={d['avg_hold']:.0f}h {sig}")
# ── 关键诊断
print(f"\n{'='*70}")
print(f" 关键诊断")
print(f"{'='*70}")
for i, r in enumerate(results):
issues = []
h1, h2 = r["half1_sharpe"], r["half2_sharpe"]
if h1 > 0 and h2 < 0:
issues.append(f"过拟合嫌疑: 前半Sharpe={h1:.2f} 后半={h2:.2f}")
if r["port_mdd"] > 0.5:
issues.append(f"最大回撤过大: {r['port_mdd']:.3f}")
if r["avg_hold_h"] < 2:
issues.append(f"持仓太短(>{r['avg_hold_h']:.1f}h),点差侵蚀大")
if r["avg_hold_h"] > 500:
issues.append(f"持仓极长({r['avg_hold_h']:.0f}h),实际交易数极少")
if r["n_pos_syms"] < r["n_syms"]:
issues.append(f"仅{r['n_pos_syms']}/{r['n_syms']}品种盈利,跨品种一致性差")
if abs(r["ic"]) < 0.005:
issues.append(f"IC≈0({r['ic']:.4f}),预测力存疑")
if issues:
print(f" [{i+1}] {decode(r['formula'])[:50]}")
for iss in issues:
print(f" ⚠ {iss}")
else:
print(f" [{i+1}] ✓ 无明显问题: {decode(r['formula'])[:55]}")
print()
# 保存
report_path = "backtest_output/elite_backtest.json"
Path("backtest_output").mkdir(exist_ok=True)
summary = []
for r in results:
summary.append({
"formula": r["formula"],
"readable": r["readable"],
"port_total_ret": round(r["port_total_ret"], 4),
"port_sharpe": round(r["port_sharpe"], 4),
"port_sortino": round(r["port_sortino"], 4),
"port_mdd": round(r["port_mdd"], 4),
"ic": round(r["ic"], 5),
"half1_sharpe": round(r["half1_sharpe"], 4),
"half2_sharpe": round(r["half2_sharpe"], 4),
"avg_hold_h": round(r["avg_hold_h"], 1),
})
with open(report_path, "w") as f:
json.dump(summary, f, indent=2, ensure_ascii=False)
print(f"报告已保存 → {report_path}\n")
if __name__ == "__main__":
main()