135 lines
4.4 KiB
Python
135 lines
4.4 KiB
Python
"""
|
||
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()
|