Files
AlphaMaster/scan_all_factors.py
2026-07-08 10:59:05 +08:00

135 lines
4.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.
"""
scan_all_factors.py — 扫描 strategies/best_{symbol}.json,单品种 solo 回测(只看收益)
判定有效:年化收益 > 0(忽略 MDD / Sharpe / WF)
数据:D:\\K线数据 离线 H1
"""
from __future__ import annotations
import json
import math
import re
import sys
from pathlib import Path
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 VOCAB_VERSION
from model_core.vm import StackVM
from strategy_manager.signal import compute_target_positions_stateless
PERIODS_PER_YEAR = 6240
COST = {
"forex": 0.00015,
"metals": 0.00020,
"index": 0.00030,
}
def _cost(sym: str) -> float:
if sym.startswith("XAU") or sym.startswith("XAG"):
return COST["metals"]
if ".cash" in sym or sym.startswith("US") or sym.startswith("JP"):
return COST["index"]
return COST["forex"]
def solo_backtest(formula: list[int], symbol: str) -> dict:
with MT5DataFetcher(offline=True) as fetcher:
orig = Config.SYMBOLS[:]
Config.SYMBOLS = [symbol]
try:
mgr = MT5DataManager(fetcher)
mgr.load()
if symbol not in mgr.symbols:
return {"error": "no data"}
vm = StackVM()
factor = vm.execute(formula, mgr.feat_tensor)
if factor is None:
return {"error": "vm failed"}
pos = compute_target_positions_stateless(factor)
prev = torch.zeros_like(pos)
prev[:, 1:] = pos[:, :-1]
turnover = (pos - prev).abs()
cr = _cost(symbol)
pnl = (pos * mgr.target_ret - turnover * cr).squeeze(0)
T = int(pnl.shape[0])
ann = float(pnl.mean().item() * PERIODS_PER_YEAR)
total = float(pnl.sum().item())
m = pnl.mean().item()
s = pnl.std(unbiased=False).item()
sharpe = float(m / (s + 1e-8) * math.sqrt(PERIODS_PER_YEAR))
cum = torch.cumsum(pnl, dim=0)
mdd = float((torch.cummax(cum, 0).values - cum).max().item())
return {
"T": T,
"years": T / PERIODS_PER_YEAR,
"ann_ret": ann,
"total_ret": total,
"sharpe": sharpe,
"mdd": mdd,
"valid": ann > 0,
}
finally:
Config.SYMBOLS = orig
def load_best(path: Path) -> dict | None:
data = json.loads(path.read_text(encoding="utf-8"))
if data.get("vocab_version") != VOCAB_VERSION:
return None
formula = data.get("formula") or data.get("formula_tokens")
if not formula:
return None
sym = data.get("symbol")
if not sym:
m = re.match(r"best_(.+)\.json", path.name)
sym = m.group(1) if m else None
if not sym or sym in ("index", "precious_metals", "forex", "metals_comm"):
return None
return {"symbol": sym, "formula": [int(t) for t in formula], "path": str(path)}
def main():
strategies_dir = Path("strategies")
files = sorted(strategies_dir.glob("best_*.json"))
rows = []
print(f"\nFactor Scan (returns-only) | vocab={VOCAB_VERSION} | offline\n")
print(f"{'品种':<16} {'年化%':>8} {'Sharpe':>8} {'MDD%':>8} {'年数':>6} {'有效':>6} 文件")
print("-" * 80)
for path in files:
loaded = load_best(path)
if not loaded:
continue
sym = loaded["symbol"]
bt = solo_backtest(loaded["formula"], sym)
if "error" in bt:
print(f"{sym:<16} ERROR: {bt['error']}")
continue
tag = "YES" if bt["valid"] else "NO"
print(
f"{sym:<16} {bt['ann_ret']*100:>8.2f} {bt['sharpe']:>8.3f} "
f"{bt['mdd']*100:>8.2f} {bt['years']:>6.2f} {tag:>6} {path.name}"
)
rows.append({"symbol": sym, "path": path.name, **bt})
valid = [r for r in rows if r.get("valid")]
print(f"\n有效因子(年化>0): {len(valid)}/{len(rows)}")
for r in sorted(valid, key=lambda x: -x["ann_ret"]):
print(f" {r['symbol']:<16} ann={r['ann_ret']*100:+.2f}% {r['path']}")
out = Path("backtest_output/factor_scan.json")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps({"all": rows, "valid": valid}, indent=2), encoding="utf-8")
print(f"\nJSON → {out}")
if __name__ == "__main__":
main()