feat: continuous strategy generator (WF, MTF, stability, ML models, auto-ensemble)

This commit is contained in:
TPTBusiness
2026-05-03 22:42:29 +02:00
parent 982b902b1a
commit 8409451ac4
+184
View File
@@ -0,0 +1,184 @@
#!/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/predix_continuous_strategies.py
python scripts/predix_continuous_strategies.py --style daytrading --rounds 100
python scripts/predix_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_ftmo
bt = backtest_signal_ftmo(
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" Predix 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()