#!/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)