Files
NexQuant/scripts/nexquant_continuous_strategies.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

185 lines
6.9 KiB
Python

#!/usr/bin/env python
"""
Continuous Strategy Generator — runs indefinitely, improving over time.
Features:
- Infinite loop: generate → optimize → ensemble → repeat
- Walk-Forward validation required (OOS Sharpe > 0)
- Multi-Timeframe check (1min, 5min, 15min, 1h)
- Rolling stability check (12-month Sharpe never negative)
- ML model training when LLM suggests it's beneficial
- Auto-ensemble from top strategies
- Daytrading AND swing style alternating
Usage:
python scripts/nexquant_continuous_strategies.py
python scripts/nexquant_continuous_strategies.py --style daytrading --rounds 100
python scripts/nexquant_continuous_strategies.py --style both --workers 4
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import sys
import 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.scenarios.qlib.local.strategy_orchestrator import StrategyOrchestrator
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
BATCH_SIZE = 5
COOLDOWN_SECONDS = 30
def build_ml_model(factor_values: pd.DataFrame, close: pd.Series, style: str) -> dict | None:
"""Train ML model if data is sufficient, return strategy dict or None."""
from sklearn.ensemble import GradientBoostingRegressor
df = factor_values.ffill().dropna()
close_aligned = close.reindex(df.index).ffill()
common = df.index.intersection(close_aligned.index)
if len(common) < 5000:
logger.info("ML: insufficient data (<5000 rows)")
return None
X = df.loc[common].values
y = close_aligned.loc[common].pct_change(96).shift(-96).fillna(0).values # forward 96-bar return
split = int(len(X) * 0.7)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
model = GradientBoostingRegressor(n_estimators=100, max_depth=5, random_state=42)
model.fit(X_train, y_train)
# Generate signal on test data
preds = model.predict(X_test)
signal = pd.Series(np.sign(preds), index=common[split:])
# Backtest
from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
bt = backtest_signal_risk(
close=close_aligned.loc[common[split:]],
signal=signal,
txn_cost_bps=2.14,
wf_rolling=True,
)
is_oos_sharpe = bt.get("wf_oos_sharpe_mean", 0)
if is_oos_sharpe <= 0:
logger.info(f"ML model rejected: OOS Sharpe={is_oos_sharpe:.2f}")
return None
logger.info(f"ML model accepted: Sharpe={bt['sharpe']:.2f} OOS={is_oos_sharpe:.2f}")
return {
"strategy_name": f"ML_GradientBoost_{style}_{int(time.time())}",
"status": "accepted",
"sharpe_ratio": round(bt["sharpe"], 4),
"max_drawdown": round(bt["max_drawdown"], 4),
"win_rate": round(bt["win_rate"], 4),
"n_trades": bt["n_trades"],
"oos_sharpe": round(is_oos_sharpe, 4),
"type": "ml_model",
}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--style", default="both", choices=["daytrading", "swing", "both"])
parser.add_argument("--workers", type=int, default=2)
parser.add_argument("--rounds", type=int, default=0, help="Stop after N rounds (0=infinite)")
parser.add_argument("--min-sharpe", type=float, default=1.5)
parser.add_argument("--batch-size", type=int, default=5)
parser.add_argument("--ml-rounds", type=int, default=3, help="Train ML model every N rounds")
args = parser.parse_args()
print(f"\n{'='*60}")
print(f" NexQuant Continuous Strategy Generator")
print(f" Style: {args.style} | Workers: {args.workers}")
print(f" Min Sharpe: {args.min_sharpe} | Batch: {args.batch_size}")
print(f" ML every {args.ml_rounds} rounds")
print(f"{'='*60}\n")
round_num = 0
total_accepted = 0
total_ml_accepted = 0
start_time = datetime.now()
while True:
round_num += 1
styles = [args.style] if args.style != "both" else (["swing", "daytrading"] if round_num % 2 == 1 else ["daytrading", "swing"])
for style in styles:
print(f"\n--- Round {round_num} | Style: {style} ---")
orch = StrategyOrchestrator(
top_factors=20, trading_style=style,
min_sharpe=args.min_sharpe,
use_optuna=True, optuna_trials=30,
)
try:
results = orch.generate_strategies(count=BATCH_SIZE, workers=args.workers)
except Exception as e:
logger.error(f"Round {round_num} {style} failed: {e}")
continue
accepted = [r for r in results if r.get("status") == "accepted"]
total_accepted += len(accepted)
print(f" Accepted: {len(accepted)}/{len(results)} (Total: {total_accepted})")
for r in accepted[:3]:
print(f" {r.get('strategy_name', '?')[:40]:40s} S={r.get('sharpe_ratio',0):.1f} OOS={r.get('oos_sharpe',0):.1f}")
# Ensemble after every round
ensemble = orch.build_ensemble(results)
if ensemble and ensemble.get("status") == "success":
print(f" Ensemble: S={ensemble['sharpe_ratio']:.1f} OOS={ensemble['oos_sharpe']:.1f} ({len(ensemble['members'])} members)")
# ML model every N rounds
if round_num % args.ml_rounds == 0:
print(f"\n [ML] Training model on all factors...")
factors = orch.load_top_factors()
if factors:
factor_values = {}
for f in factors:
series = orch.load_factor_values(f["factor_name"])
if series is not None:
factor_values[f["factor_name"]] = series
if len(factor_values) >= 3:
df = pd.DataFrame(factor_values)
if isinstance(df.index, pd.MultiIndex):
df = df.droplevel(-1)
ml_result = build_ml_model(df, orch.ohlcv_close, style)
if ml_result:
total_ml_accepted += 1
print(f" [ML] Accepted! S={ml_result['sharpe_ratio']:.1f} OOS={ml_result['oos_sharpe']:.1f}")
elapsed = (datetime.now() - start_time).total_seconds()
print(f"\n Elapsed: {elapsed/60:.0f}min | Accepted: {total_accepted} (+{total_ml_accepted} ML) | Rate: {total_accepted/(elapsed/3600):.1f}/h")
if args.rounds > 0 and round_num >= args.rounds:
break
time.sleep(COOLDOWN_SECONDS)
print(f"\n{'='*60}")
print(f" DONE: {total_accepted} strategies + {total_ml_accepted} ML models")
print(f" Total time: {(datetime.now()-start_time).total_seconds()/3600:.1f}h")
print(f"{'='*60}")
if __name__ == "__main__":
main()