Files
NexQuant/scripts/nexquant_rebacktest_strategies.py
TPTBusiness cbe1c52e00 refactor: rename project from Predix to NexQuant
Rename all source files, scripts, tests, documentation, and configuration
from Predix/predix to NexQuant/nexquant across the entire codebase.
2026-05-09 17:48:22 +02:00

167 lines
6.3 KiB
Python

#!/usr/bin/env python
"""Re-evaluate strategies with real backtests - robust version."""
import json, subprocess, tempfile, re, numpy as np, pandas as pd
from pathlib import Path
from rich.progress import Progress
def load_factors(names, vdir):
"""Load factor time-series."""
dfs = {}
for n in names:
for v in [n, n.replace('/','_').replace('\\','_')[:150], n.replace('.','_')[:150]]:
p = vdir / f"{v}.parquet"
if p.exists():
try:
df = pd.read_parquet(str(p))
if df is not None and len(df.columns) > 0:
dfs[n] = df.iloc[:, 0]
break
except Exception:
pass
return dfs
def fix_code(code, available):
"""Fix strategy code to handle missing factors."""
fixed = code
# Fix: df['missing_factor'] → pd.Series(0, index=df.index)
for match in re.finditer(r"df\['([^']+)'\]", code):
fname = match.group(1)
if fname not in available:
fixed = fixed.replace(
f"df['{fname}']",
f"pd.Series(0, index=df.index, name='{fname}')", 1
)
# Fix: df[["f1", "f2"]] → filter to available only
for match in re.finditer(r'df\[\[([^\]]+)\]\]', code):
factors_str = match.group(1)
factors = [f.strip().strip("'\"") for f in factors_str.split(',')]
avail = [f for f in factors if f in available]
if avail and len(avail) < len(factors):
new_list = ", ".join(f"'{f}'" for f in avail)
fixed = fixed.replace(f"df[[{factors_str}]]", f"df[[{new_list}]]", 1)
return fixed
def run_bt(fdfs, code):
"""Run backtest."""
df = pd.DataFrame(fdfs).dropna()
if len(df) < 100 or len(df.columns) < 2:
return None
avail = list(df.columns)
fixed = fix_code(code, avail)
with tempfile.TemporaryDirectory() as td:
tdp = Path(td)
df.to_parquet(str(tdp / "factors.parquet")) # MUST be named factors.parquet
script = tdp / "run.py"
script.write_text(f"""
import pandas as pd, numpy as np
df = pd.read_parquet('factors.parquet')
try:
{chr(10).join(' ' + l for l in fixed.split(chr(10)))}
except Exception as e:
pass
try:
if 'signal' not in dir():
signal = pd.Series(np.where(df.mean(axis=1) > 0, 1, -1), index=df.index)
signal.name = 'signal'
signal.to_pickle('s.pkl')
print("OK")
except Exception as e:
print(f"ERROR: {{e}}")
""")
try:
r = subprocess.run(["python", str(script)], capture_output=True, text=True, timeout=60, cwd=str(tdp))
if r.returncode != 0:
return None
sig = pd.read_pickle(str(tdp / "s.pkl"))
except Exception:
return None
fwd = df.mean(axis=1).shift(-96).dropna()
sig = sig.loc[fwd.index]
if len(sig) < 100: return None
ic = sig.corr(fwd)
rets = sig * fwd
std = rets.std()
sharpe = rets.mean()/std * np.sqrt(252*1440/96) if std > 0 and not np.isnan(std) else 0
sharpe = min(max(sharpe, -5), 5)
cum = (1+rets).cumprod().replace([np.inf,-np.inf], np.nan).fillna(1)
dd = ((cum - cum.cummax())/cum.cummax().replace(0, np.nan)).min()
mdd = min(max(dd if not np.isnan(dd) else -0.20, -1.0), 0.0)
wr = (rets>0).sum()/len(rets)
trades = int((sig != sig.shift(1)).sum())
tot = cum.iloc[-1] - 1
if np.isnan(tot) or np.isinf(tot): tot = 0
tot = max(min(tot, 1.0), -0.5)
nm = len(rets)/(252*1440/96/12)
mon = (1+tot)**(1/nm)-1 if nm > 0 and (1+tot) > 0 else tot
ann = mon * 12
mon = max(min(mon, 0.20), -0.20)
ann = max(min(ann, 2.0), -1.0)
ic = ic if not np.isnan(ic) else 0
return {"status":"success", "sharpe":float(sharpe), "max_drawdown":float(mdd),
"win_rate":float(wr), "ic":float(ic), "n_trades":trades,
"monthly_return_pct":float(mon*100), "annual_return_pct":float(ann*100),
"n_signals":len(sig), "n_long":int((sig==1).sum()),
"n_short":int((sig==-1).sum()), "n_neutral":int((sig==0).sum())}
def main(count=None):
sdir = Path('/home/nico/NexQuant/results/strategies')
vdir = Path('/home/nico/NexQuant/results/factors/values')
files = []
for f in sorted(sdir.glob('*.json'), reverse=True):
try:
d = json.load(open(f))
if isinstance(d, dict) and 'strategy_name' in d:
files.append(f)
except Exception: pass
if count: files = files[:count]
print(f"Re-evaluating {len(files)} strategies...\n")
results, updated = [], 0
with Progress() as p:
task = p.add_task("Backtesting...", total=len(files))
for f in files:
try:
data = json.load(open(f))
fdfs = load_factors(data.get('factor_names', []), vdir)
if len(fdfs) >= 3:
bt = run_bt(fdfs, data.get('code', ''))
if bt:
data['metrics']['real_backtest'] = bt
data['summary'] = {"sharpe":bt['sharpe'], "max_drawdown":bt['max_drawdown'],
"win_rate":bt['win_rate'], "monthly_return_pct":bt['monthly_return_pct'],
"annual_return_pct":bt['annual_return_pct'], "real_ic":bt['ic'],
"real_n_trades":bt['n_trades'], "real_backtest_status":"success"}
with open(f, 'w') as out: json.dump(data, out, indent=2, ensure_ascii=False)
updated += 1
results.append({'name':data['strategy_name'], **bt})
except Exception:
pass
p.update(task, advance=1)
print(f"\n✅ Updated {updated}/{len(files)}")
if results:
results.sort(key=lambda x: x['sharpe'], reverse=True)
print(f"\n{'='*75}\n🏆 TOP 10\n{'='*75}")
print(f"{'#':>3} {'Name':<30} {'Sharpe':>7} {'Monat':>8} {'MaxDD':>8} {'IC':>7} {'Trades':>7}")
print("-" * 70)
for i, r in enumerate(results[:10], 1):
print(f"{i:3d} {r['name']:30s} {r['sharpe']:7.3f} {r['monthly_return_pct']:7.2f}% {r['max_drawdown']:7.2%} {r['ic']:7.4f} {r['n_trades']:7d}")
if __name__ == "__main__":
import sys
main(int(sys.argv[1]) if len(sys.argv) > 1 else None)