#!/usr/bin/env python """ NexQuant Infinite Hypothesis Search — kombiniert und variiert Ansätze bis ein positiver OOS Sharpe gefunden wird. """ from __future__ import annotations import json, sys, time, random, itertools from pathlib import Path import numpy as np import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5") FACTORS_DIR = Path("results/factors") TXN_COST_BPS = 0.5 def load_data(): close = pd.read_hdf(DATA_PATH, key="data")["$close"] if isinstance(close.index, pd.MultiIndex): close = close.droplevel(-1) close = close.sort_index().dropna().resample("1h").last().dropna() factors_meta = [] for f in sorted(FACTORS_DIR.glob("*.json")): try: d = json.loads(f.read_text()) except Exception: continue if d.get("status") != "success" or d.get("ic") is None: continue name = d.get("factor_name", f.stem) safe = name.replace("/", "_")[:150] if (FACTORS_DIR / "values" / f"{safe}.parquet").exists(): factors_meta.append({"name": name, "ic": d["ic"]}) factors_meta.sort(key=lambda x: abs(x["ic"]), reverse=True) top = factors_meta[:15] factor_data = {} for f in top: safe = f["name"].replace("/", "_")[:150] series = pd.read_parquet(FACTORS_DIR / "values" / f"{safe}.parquet").iloc[:, 0] if isinstance(series.index, pd.MultiIndex): series = series.droplevel(-1) factor_data[f["name"]] = series.resample("1h").last() df = pd.DataFrame(factor_data) common = close.index.intersection(df.dropna(how="all").index) return close.loc[common], df.loc[common].ffill(), {f["name"]: f["ic"] for f in top} close, factors_df, ics = load_data() print(f"Data: {len(close):,} bars × {len(factors_df.columns)} factors\n") def backtest(signal) -> float: if signal is None or len(signal) < 100: return -999 common = close.index.intersection(signal.dropna().index) if len(common) < 100: return -999 r = backtest_signal_risk(close.loc[common], signal.reindex(common).fillna(0), txn_cost_bps=TXN_COST_BPS, wf_rolling=False) return r.get("oos_sharpe", -999) def composite(factor_list=None, window=20): cols = factor_list or list(factors_df.columns) c = pd.Series(0.0, index=factors_df.index) total = sum(abs(ics.get(col, 0)) for col in cols) if total == 0: return c for col in cols: ic_val = ics.get(col, 0) if abs(ic_val) < 0.001: continue z = (factors_df[col] - factors_df[col].rolling(window).mean()) / (factors_df[col].rolling(window).std() + 1e-8) c += (ic_val / total) * z return c def session_filter(sig): hours = sig.index.hour sig = sig.copy() sig[(hours < 7) | (hours >= 17)] = 0 return sig def trend_filter(sig, sma_bars=200 * 1440 // 5): sma = close.rolling(sma_bars).mean() trend_up = close > sma sig = sig.copy() sig[(sig > 0) & ~trend_up] = 0 sig[(sig < 0) & trend_up] = 0 return sig def vola_target(sig, vol_window=50): vol = close.pct_change().rolling(vol_window).std() vol_tgt = vol.median() s = sig.astype(float) * vol_tgt / (vol + 1e-8) return s.clip(-3, 3) def anti_fade(sig, sigma=3.0): ret = close.pct_change() thresh = ret.std() * sigma s = sig.copy() s[ret > thresh] = -1 s[ret < -thresh] = 1 return s def signal_decay(sig, half_life=60): d = 0.5 ** (1 / half_life) s = sig.astype(float).copy() for i in range(1, len(s)): if abs(s.iloc[i]) < 0.01: s.iloc[i] = s.iloc[i - 1] * d return s.clip(-1, 1) def kalman_composite(comp, Q=0.001, R=0.1): x, P = 0.0, 1.0 filtered = [] for v in comp.dropna().values: P += Q; K = P / (P + R); x += K * (v - x); P *= (1 - K) filtered.append(x) return pd.Series(filtered, index=comp.dropna().index) # PRIMITIVES — can be combined arbitrarily PRIMITIVES = { "session": session_filter, "trend": trend_filter, "vola_target": vola_target, "anti_fade": anti_fade, "decay": signal_decay, } BASE_PARAMS = { "entry": [0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5], "window": [10, 20, 30, 50, 100], "sigma": [2.0, 2.5, 3.0, 3.5], "half_life": [30, 60, 120, 240], } best_score = -999 best_desc = "" best_sig = None tested = set() round_num = 0 def try_combo(factor_list, entry, window, primitives_used): global best_score, best_desc, best_sig, tested, round_num key = f"{sorted(factor_list)}_{entry:.3f}_{window}_{sorted(primitives_used)}" if key in tested: return tested.add(key) comp = composite(factor_list, window) if comp is None or comp.dropna().empty: return sig = pd.Series(0, index=comp.index) sig[comp > entry] = 1 sig[comp < -entry] = -1 for p in primitives_used: if p in PRIMITIVES: sig = PRIMITIVES[p](sig.fillna(0)) sharpe = backtest(sig) if sharpe > best_score: best_score = sharpe best_desc = f"entry={entry:.2f} window={window} factors={len(factor_list)} primitives={primitives_used}" best_sig = sig t = "✅" if sharpe > 0 else "📈" if sharpe > -1 else "➖" print(f" {t} #{round_num}: Sharpe={sharpe:.4f} | {best_desc}") if sharpe > 0: print(f"\n{'='*60}") print(f" 🎯 POSITIVE SHARPE FOUND!") print(f" Sharpe={sharpe:.4f}") print(f" {best_desc}") print(f"{'='*60}") return True return False print("Starting infinite search — will run until positive OOS Sharpe found...\n") all_factors = sorted(factors_df.columns, key=lambda c: -abs(ics.get(c, 0))) while True: round_num += 1 # Pick random subset of top factors n_factors = random.randint(2, min(10, len(all_factors))) factor_subset = random.sample(all_factors[:12], n_factors) # Pick random parameters entry = random.choice(BASE_PARAMS["entry"]) window = random.choice(BASE_PARAMS["window"]) # Pick random combination of primitives (0-4) n_prim = random.randint(0, 4) prims = random.sample(list(PRIMITIVES.keys()), n_prim) if n_prim > 0 else [] found = try_combo(factor_subset, entry, window, prims) if found: break # Every 200 rounds, also try parameter sweeps around best if round_num % 200 == 0: print(f" ... {round_num} combinations tested, best={best_score:.4f}") # Fine-tune around current best for fine_entry in np.arange(max(0.05, entry - 0.15), entry + 0.16, 0.05): for fine_window in [max(5, window - 15), window, min(200, window + 15)]: if try_combo(factor_subset, fine_entry, fine_window, prims): break # Every 500 rounds, try factor-specific combos (Kronos-only, momentum-only, etc.) if round_num % 500 == 0: kronos = [f for f in all_factors if "Kronos" in f] mom = [f for f in all_factors if any(k in f.lower() for k in ["mom", "ret"])] for subset in [kronos, mom, all_factors[:3], all_factors[:6]]: if len(subset) >= 2: for e in [0.1, 0.2, 0.3]: for w in [20, 50]: for prims in [[], ["session"], ["session", "decay"]]: try_combo(subset, e, w, prims) if round_num % 1000 == 0: print(f" [{round_num} tested] best={best_score:.4f} — still searching...") if best_score <= 0: print(f"\nAfter {round_num} combinations, best is still negative ({best_score:.4f})") print("The factors lack sufficient predictive power for positive returns.")