Files
NexQuant/scripts/nexquant_unified.py
TPTBusiness 4758de0eee refactor: remove all proprietary terms from codebase and git history
- Rename FTMO_* constants → generic names (RISK_PER_TRADE, MAX_DAILY_LOSS, etc.)
- Rename backtest_signal_ftmo → backtest_signal_risk
- Rename _apply_ftmo_mask → _apply_risk_mask
- Clean all FTMO/riskMgmt mentions from commit messages via filter-branch
- AGENTS.md: add non-negotiable rule — NEVER mention proprietary terms in commits/releases
- Code variables and function names sanitized project-wide
- Force-pushed rewritten history to remote
2026-05-22 15:10:36 +02:00

167 lines
5.7 KiB
Python

#!/usr/bin/env python
"""
NexQuant Unified Loop — fin_quant + autopilot combined.
Flow:
1. fin_quant generates a factor → auto-evaluates
2. New factor tested in quick strategy (1h/30min SMA combo)
3. Strategy OOS Sharpe feeds back to LLM for better hypotheses
4. Factors that produce profitable strategies get priority
5. Single process, no wasted LLM calls on dead-end factors
"""
from __future__ import annotations
import json, sys, time
from datetime import datetime
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
# ── Config ──
DATA_PATH = Path("git_ignore_folder/factor_implementation_source_data/intraday_pv.h5")
TXN_COST_BPS = 2.14
MIN_MONTHLY_PCT = 0.1 # Minimum monthly return to keep a strategy
def load_daily_close():
close = pd.read_hdf(DATA_PATH, key="data")["$close"]
if isinstance(close.index, pd.MultiIndex):
close = close.droplevel(-1)
return close.sort_index().dropna()
def test_factor_as_signal(factor_path: Path, close: pd.Series, freq: str = "1h") -> dict | None:
"""Quick-test a factor as a trading signal. Returns metrics or None if unprofitable."""
try:
series = pd.read_parquet(factor_path).iloc[:, 0]
if isinstance(series.index, pd.MultiIndex):
series = series.droplevel(-1)
fac = series.resample(freq).last().reindex(close.index).ffill()
except Exception:
return None
is_sess = (close.index.hour >= 7) & (close.index.hour < 17)
best_result = None
for direction in [1, -1]:
sig = pd.Series(direction * np.sign(fac).fillna(0), index=close.index)
sig[~is_sess] = 0
if sig.abs().sum() < 20:
continue
r = backtest_signal_risk(close, sig.fillna(0), txn_cost_bps=TXN_COST_BPS)
oos_m = r.get("oos_monthly_return_pct", 0) or 0
if oos_m > (best_result["monthly"] if best_result else MIN_MONTHLY_PCT):
best_result = {
"direction": direction,
"monthly": oos_m,
"oos_sharpe": r.get("oos_sharpe", -999),
"max_dd": r.get("oos_max_drawdown", 0),
"trades": r.get("oos_n_trades", 0),
}
return best_result
def scan_all_factors():
"""Scan ALL factors and rank them by strategy profitability (not IC)."""
close = load_daily_close().resample("1h").last().dropna()
factors_dir = Path("results/factors")
values_dir = factors_dir / "values"
results = []
for i, jf in enumerate(sorted(factors_dir.glob("*.json"))):
try:
meta = json.loads(jf.read_text())
except Exception:
continue
if meta.get("status") != "success":
continue
name = meta.get("factor_name", jf.stem)
safe = name.replace("/", "_")[:150]
pf = values_dir / f"{safe}.parquet"
if not pf.exists():
continue
bt = test_factor_as_signal(pf, close)
if bt:
results.append({
"factor": name,
"ic": meta.get("ic", 0),
**bt,
})
if i % 100 == 0:
profitable = sum(1 for r in results if r.get("monthly", 0) > 0.5)
print(f" Scanned {i}... {profitable} profitable (>0.5%/mon)")
results.sort(key=lambda x: x.get("monthly", 0), reverse=True)
return results
def main():
print(f"\n{'='*60}")
print(" NexQuant Unified Loop — Factor-to-Strategy Pipeline")
print(f"{'='*60}")
print("\n=== PHASE 1: Scan all existing factors as strategies ===\n")
t0 = time.time()
ranked = scan_all_factors()
profitable = [r for r in ranked if r.get("monthly", 0) > 0.5]
print(f"\n Scanned {len(ranked)} factors in {time.time()-t0:.0f}s")
print(f" Profitable (>0.5%/month): {len(profitable)}")
if profitable:
print(f"\n TOP 10 by Strategy Profitability:")
for i, r in enumerate(profitable[:10]):
print(f" {i+1:2d}. {r['factor'][:45]:45s} Mon={r['monthly']:+.2f}% IC={r['ic']:+.4f} Dir={r['direction']:+d}")
# Build combo from top signals
print(f"\n=== PHASE 2: Build best combo ===\n")
c = load_daily_close().resample("1h").last().dropna()
is_sess = (c.index.hour >= 7) & (c.index.hour < 17)
signals = {}
for r in profitable[:10]:
safe = r["factor"].replace("/", "_")[:150]
pf = Path("results/factors/values") / f"{safe}.parquet"
try:
s = pd.read_parquet(pf).iloc[:, 0]
if isinstance(s.index, pd.MultiIndex):
s = s.droplevel(-1)
fac = s.resample("1h").last().reindex(c.index).ffill()
sig = pd.Series(r["direction"] * np.sign(fac).fillna(0), index=c.index)
sig[~is_sess] = 0
signals[r["factor"]] = sig
except Exception:
pass
df = pd.DataFrame(signals, index=c.index).fillna(0)
cols = list(df.columns)
for n in [2, 3, 5, len(cols)]:
combo = df[cols[:n]].mean(axis=1)
r = backtest_signal_risk(c, combo.fillna(0), txn_cost_bps=TXN_COST_BPS, wf_rolling=True)
m = r.get("oos_monthly_return_pct", 0) or 0
dd = (r.get("oos_max_drawdown", 0) or 0) * 100
t = r.get("oos_n_trades", 0)
gap = 10 - m
hit = "🎯" if m >= 4 else ""
print(f" {n:2d} sig: Mon={m:+.2f}% DD={dd:+.1f}% T={t} Gap2_10%={gap:+.1f} {hit}")
print(f"\n Next: feed top factors back to fin_quant LLM for improved hypotheses")
print(f" Run: python scripts/nexquant_unified.py")
return ranked
if __name__ == "__main__":
main()