mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 15:37:44 +00:00
157 lines
4.8 KiB
Python
157 lines
4.8 KiB
Python
|
|
#!/usr/bin/env python
|
||
|
|
"""Fast rebacktest: only strategies with factor parquets, skip already-done."""
|
||
|
|
import json, sys, pandas as pd, subprocess, tempfile, numpy as np
|
||
|
|
from pathlib import Path
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
|
|
from rdagent.components.backtesting.vbt_backtest import backtest_signal
|
||
|
|
|
||
|
|
OHLCV = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
|
||
|
|
FACTORS_DIR = Path("results/factors/values")
|
||
|
|
STRAT_DIR = Path("results/strategies_new")
|
||
|
|
|
||
|
|
# Pre-build factor name → path map
|
||
|
|
fmap = {p.stem: str(p) for p in FACTORS_DIR.glob("*.parquet")}
|
||
|
|
|
||
|
|
# Load close once
|
||
|
|
print("Loading OHLCV...")
|
||
|
|
ohlcv = pd.read_hdf(str(OHLCV), key="data")
|
||
|
|
close = ohlcv["$close"].dropna()
|
||
|
|
if isinstance(close.index, pd.MultiIndex):
|
||
|
|
close = close.droplevel(-1)
|
||
|
|
close = close.astype(float).sort_index()
|
||
|
|
print(f"{len(close):,} bars")
|
||
|
|
|
||
|
|
# Build work list
|
||
|
|
work = []
|
||
|
|
for f in sorted(STRAT_DIR.glob("*.json")):
|
||
|
|
try:
|
||
|
|
d = json.loads(f.read_text())
|
||
|
|
except Exception:
|
||
|
|
continue
|
||
|
|
if d.get("reevaluation_status") == "verified_v2":
|
||
|
|
continue
|
||
|
|
names = d.get("factor_names", [])
|
||
|
|
code = d.get("code", "")
|
||
|
|
if not names or not code:
|
||
|
|
continue
|
||
|
|
paths = []
|
||
|
|
for n in names:
|
||
|
|
p = fmap.get(n) or fmap.get(n.replace("/", "_")[:150])
|
||
|
|
if p:
|
||
|
|
paths.append((n, p))
|
||
|
|
if len(paths) >= 2:
|
||
|
|
work.append((f, d, paths))
|
||
|
|
|
||
|
|
print(f"{len(work)} strategies to process")
|
||
|
|
|
||
|
|
if not work:
|
||
|
|
print("All done!")
|
||
|
|
sys.exit(0)
|
||
|
|
|
||
|
|
ok = skip = fail = 0
|
||
|
|
start = datetime.now()
|
||
|
|
|
||
|
|
for i, (f, data, factor_paths) in enumerate(work):
|
||
|
|
name = data.get("strategy_name", f.stem)[:45]
|
||
|
|
code = data.get("code", "")
|
||
|
|
|
||
|
|
# Load factor series
|
||
|
|
series = {}
|
||
|
|
for fn, fp in factor_paths:
|
||
|
|
try:
|
||
|
|
s = pd.read_parquet(fp).iloc[:, 0]
|
||
|
|
series[fn] = s
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
if len(series) < 2:
|
||
|
|
skip += 1
|
||
|
|
continue
|
||
|
|
|
||
|
|
df = pd.DataFrame(series).sort_index()
|
||
|
|
if isinstance(df.index, pd.MultiIndex):
|
||
|
|
df = df.droplevel(-1)
|
||
|
|
|
||
|
|
try:
|
||
|
|
df_1m = df.reindex(close.index).ffill()
|
||
|
|
except Exception:
|
||
|
|
skip += 1
|
||
|
|
continue
|
||
|
|
|
||
|
|
valid = df_1m.notna().any(axis=1)
|
||
|
|
if valid.sum() < 1000:
|
||
|
|
skip += 1
|
||
|
|
continue
|
||
|
|
|
||
|
|
ca = close.loc[valid]
|
||
|
|
fa = df_1m.loc[valid]
|
||
|
|
|
||
|
|
# Execute strategy code
|
||
|
|
try:
|
||
|
|
with tempfile.TemporaryDirectory() as td:
|
||
|
|
tdp = Path(td)
|
||
|
|
fa.to_parquet(str(tdp / "factors.parquet"))
|
||
|
|
ca.to_pickle(str(tdp / "close.pkl"))
|
||
|
|
|
||
|
|
exec_script = (
|
||
|
|
"import pandas as pd, numpy as np\n"
|
||
|
|
"factors = pd.read_parquet('factors.parquet')\n"
|
||
|
|
"close = pd.read_pickle('close.pkl')\n"
|
||
|
|
"df = factors\n"
|
||
|
|
+ code +
|
||
|
|
"\nif 'signal' not in dir():\n"
|
||
|
|
" raise SystemExit(1)\n"
|
||
|
|
"pd.Series(signal).fillna(0).to_pickle('signal.pkl')\n"
|
||
|
|
)
|
||
|
|
(tdp / "run.py").write_text(exec_script)
|
||
|
|
r = subprocess.run(
|
||
|
|
["python", "run.py"],
|
||
|
|
capture_output=True, text=True, timeout=60, cwd=str(tdp),
|
||
|
|
)
|
||
|
|
if r.returncode != 0:
|
||
|
|
fail += 1
|
||
|
|
continue
|
||
|
|
sig = pd.read_pickle(tdp / "signal.pkl")
|
||
|
|
except Exception:
|
||
|
|
fail += 1
|
||
|
|
continue
|
||
|
|
|
||
|
|
try:
|
||
|
|
sig = sig.reindex(ca.index).ffill().fillna(0)
|
||
|
|
result = backtest_signal(ca, sig, txn_cost_bps=2.14)
|
||
|
|
except Exception:
|
||
|
|
fail += 1
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Write back
|
||
|
|
data["reevaluation_status"] = "verified_v2"
|
||
|
|
data["sharpe_ratio"] = result.get("sharpe")
|
||
|
|
data["max_drawdown"] = result.get("max_drawdown")
|
||
|
|
data["win_rate"] = result.get("win_rate")
|
||
|
|
data["total_return"] = result.get("total_return")
|
||
|
|
data["summary"] = {
|
||
|
|
**data.get("summary", {}),
|
||
|
|
"sharpe": result.get("sharpe"),
|
||
|
|
"max_drawdown": result.get("max_drawdown"),
|
||
|
|
"win_rate": result.get("win_rate"),
|
||
|
|
"monthly_return_pct": result.get("monthly_return_pct"),
|
||
|
|
"real_n_trades": result.get("n_trades"),
|
||
|
|
"total_return": result.get("total_return"),
|
||
|
|
"annualized_return": result.get("annualized_return"),
|
||
|
|
"engine": "verified_v2",
|
||
|
|
"txn_cost_bps": 2.14,
|
||
|
|
}
|
||
|
|
f.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
||
|
|
ok += 1
|
||
|
|
|
||
|
|
elapsed = (datetime.now() - start).total_seconds()
|
||
|
|
rate = ok / elapsed * 60 if elapsed > 0 else 0
|
||
|
|
print(f" [{ok:4d}/{len(work)}] {rate:5.0f}/min {name:45s} "
|
||
|
|
f"S={result['sharpe']:6.1f} DD={result['max_drawdown']:7.2%} "
|
||
|
|
f"WR={result['win_rate']:5.1%} T={result['n_trades']:4d}")
|
||
|
|
|
||
|
|
elapsed = (datetime.now() - start).total_seconds()
|
||
|
|
print(f"\nDONE: ok={ok} skip={skip} fail={fail} in {elapsed:.0f}s")
|