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
185 lines
6.9 KiB
Python
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()
|