mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 15:37:44 +00:00
4758de0eee
- 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
167 lines
5.7 KiB
Python
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()
|