Files

133 lines
4.4 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python
"""
NexQuant Auto-Pilot — vollautomatischer Strategie-Generator.
Läuft unbegrenzt, kein menschlicher Eingriff nötig.
Jede Runde: Factors laden → LLM Code → Pre-Flight → Backtest → Optuna → Ensemble
Bei Crash: auto-restart nach 30s.
Usage:
python scripts/nexquant_autopilot.py
"""
from __future__ import annotations
import json, logging, os, sys, time, traceback
from datetime import datetime
from pathlib import Path
import numpy as np, pandas as pd
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
# Load .env before any rdagent imports (required for pydantic-settings)
try:
from dotenv import load_dotenv
_env_path = Path(__file__).resolve().parent.parent / ".env"
load_dotenv(_env_path)
except ImportError:
pass
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("autopilot")
LOG_FILE = Path(__file__).resolve().parent.parent / "git_ignore_folder" / "logs" / f"autopilot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
fh = logging.FileHandler(str(LOG_FILE))
fh.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
logger.addHandler(fh)
BATCH_SIZE = 2
OPTUNA_TRIALS = 10
COOLDOWN = 30
MAX_CONSECUTIVE_FAILS = 5
def main_round(style: str, round_num: int) -> int:
"""Run one round. Returns number of accepted strategies."""
from rdagent.scenarios.qlib.local.strategy_orchestrator import StrategyOrchestrator
accepted_count = 0
try:
orch = StrategyOrchestrator(
top_factors=20, trading_style=style,
min_sharpe=0.1, use_optuna=True, optuna_trials=OPTUNA_TRIALS,
)
except Exception as e:
logger.error(f"Orchestrator init failed: {e}")
return 0
try:
results = orch.generate_strategies(count=BATCH_SIZE, workers=1)
except Exception as e:
logger.error(f"generate_strategies failed: {e}")
return 0
for r in results:
status = r.get("status", "?")
if status == "accepted":
accepted_count += 1
logger.info(f" ✓ {r.get('strategy_name','?')[:40]:40s} S={r.get('sharpe_ratio',0):.1f} OOS={r.get('oos_sharpe',0):.1f}")
else:
reason = r.get("reason", "?")[:80]
logger.debug(f" ✗ {r.get('strategy_name','?')[:40]:40s} {reason}")
if accepted_count >= 2:
try:
ensemble = orch.build_ensemble(results)
if ensemble and ensemble.get("status") == "success":
logger.info(f" Ensemble: S={ensemble['sharpe_ratio']:.1f} OOS={ensemble['oos_sharpe']:.1f} ({len(ensemble['members'])} members)")
except Exception:
pass
return accepted_count
def main():
print(f"\n{'='*50}")
print(f" NexQuant Auto-Pilot")
print(f" Log: {LOG_FILE}")
print(f" Batch: {BATCH_SIZE} | Optuna: {OPTUNA_TRIALS} trials")
print(f"{'='*50}\n")
round_num = 0
total_accepted = 0
consecutive_fails = 0
start_time = datetime.now()
styles = ["swing", "daytrading"]
while True:
round_num += 1
style = styles[round_num % 2]
print(f"\n[Round {round_num}] {style} | {datetime.now().strftime('%H:%M:%S')}", flush=True)
try:
accepted = main_round(style, round_num)
total_accepted += accepted
if accepted == 0:
consecutive_fails += 1
else:
consecutive_fails = 0
elapsed = (datetime.now() - start_time).total_seconds()
rate = total_accepted / (elapsed / 3600) if elapsed > 0 else 0
print(f" Accepted: {accepted} | Total: {total_accepted} | Rate: {rate:.1f}/h | Fails: {consecutive_fails}", flush=True)
if consecutive_fails >= MAX_CONSECUTIVE_FAILS:
logger.warning(f"{consecutive_fails} consecutive failures — cooling down {COOLDOWN*2}s")
time.sleep(COOLDOWN * 2)
consecutive_fails = 0
except KeyboardInterrupt:
print(f"\n\nStopped after {round_num} rounds. Total accepted: {total_accepted}")
break
except Exception as e:
logger.error(f"Round {round_num} crashed: {e}\n{traceback.format_exc()[-500:]}")
consecutive_fails += 1
time.sleep(COOLDOWN)
time.sleep(COOLDOWN)
if __name__ == "__main__":
main()