mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 15:37:44 +00:00
cbe1c52e00
Rename all source files, scripts, tests, documentation, and configuration from Predix/predix to NexQuant/nexquant across the entire codebase.
1717 lines
62 KiB
Python
1717 lines
62 KiB
Python
"""
|
|
CLI entrance for all rdagent application.
|
|
|
|
This will
|
|
- make rdagent a nice entry and
|
|
- autoamtically load dotenv
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv(".env")
|
|
# 1) Make sure it is at the beginning of the script so that it will load dotenv before initializing BaseSettings.
|
|
# 2) The ".env" argument is necessary to make sure it loads `.env` from the current directory.
|
|
|
|
import subprocess
|
|
from importlib.resources import path as rpath
|
|
from typing import Annotated
|
|
|
|
import typer
|
|
from rich.console import Console
|
|
|
|
try:
|
|
from rdagent.utils.env import logger
|
|
except ImportError:
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
from rdagent.app.data_science.loop import main as data_science
|
|
from rdagent.app.finetune.llm.loop import main as llm_finetune
|
|
from rdagent.app.general_model.general_model import (
|
|
extract_models_and_implement as general_model,
|
|
)
|
|
from rdagent.app.qlib_rd_loop.factor import main as fin_factor
|
|
from rdagent.app.qlib_rd_loop.factor_from_report import main as fin_factor_report
|
|
from rdagent.app.qlib_rd_loop.model import main as fin_model
|
|
from rdagent.app.qlib_rd_loop.quant import main as fin_quant
|
|
from rdagent.app.utils.health_check import health_check
|
|
from rdagent.app.utils.info import collect_info
|
|
from rdagent.log.mle_summary import grade_summary as grade_summary
|
|
|
|
app = typer.Typer(
|
|
help="""
|
|
🤖 PREDIX - AI-Powered Quantitative Trading Agent for EUR/USD
|
|
|
|
Usage:
|
|
rdagent COMMAND [OPTIONS]
|
|
|
|
Available Commands:
|
|
Trading Loop:
|
|
fin_quant Start factor evolution loop
|
|
fin_quant --auto-strategies Auto-generate strategies after threshold
|
|
fin_quant -d With web dashboard
|
|
|
|
Strategy Generation:
|
|
generate_strategies Generate trading strategies with LLM
|
|
generate_strategies --count 5 --optuna Generate 5 with Optuna
|
|
optimize_portfolio Optimize portfolio (mean-variance, risk parity)
|
|
strategies_report Generate performance reports
|
|
|
|
Server & Loops:
|
|
start_llama Start llama.cpp server for local LLM
|
|
start_llama --gpu-layers 40 Custom GPU layers
|
|
start_loop Start strategy generator loop
|
|
start_loop --target 5 Generate 5 strategies per run
|
|
|
|
Parallel & Evaluation:
|
|
parallel Run parallel factor experiments
|
|
eval_all Evaluate factors with full data
|
|
simple_eval Simple IC/Sharpe computation
|
|
batch_backtest Batch backtest factors
|
|
|
|
Strategy Tools:
|
|
rebacktest Re-backtest existing strategies
|
|
report Generate PDF performance reports
|
|
|
|
RL Trading:
|
|
rl_trading --mode train Train RL agent (PPO/A2C/SAC)
|
|
rl_trading --mode backtest Backtest with trained model
|
|
|
|
Utilities:
|
|
health_check Validate environment setup
|
|
server_ui Start web UI dashboard
|
|
|
|
Examples:
|
|
rdagent fin_quant --auto-strategies --with-dashboard
|
|
rdagent generate_strategies --count 5 --optuna --optuna-trials 30
|
|
rdagent start_llama
|
|
rdagent start_loop --target 5
|
|
rdagent parallel --runs 10
|
|
rdagent eval_all --top 500
|
|
rdagent batch_backtest --all
|
|
""",
|
|
)
|
|
|
|
CheckoutOption = Annotated[bool, typer.Option("--checkout/--no-checkout", "-c/-C")]
|
|
CheckEnvOption = Annotated[bool, typer.Option("--check-env/--no-check-env", "-e/-E")]
|
|
CheckDockerOption = Annotated[bool, typer.Option("--check-docker/--no-check-docker", "-d/-D")]
|
|
CheckPortsOption = Annotated[bool, typer.Option("--check-ports/--no-check-ports", "-p/-P")]
|
|
|
|
|
|
def ui(port=19899, log_dir="", debug: bool = False, data_science: bool = False):
|
|
"""
|
|
start web app to show the log traces.
|
|
"""
|
|
if data_science:
|
|
with rpath("rdagent.log.ui", "dsapp.py") as app_path:
|
|
cmds = ["streamlit", "run", app_path, f"--server.port={port}"]
|
|
subprocess.run(cmds)
|
|
return
|
|
with rpath("rdagent.log.ui", "app.py") as app_path:
|
|
cmds = ["streamlit", "run", app_path, f"--server.port={port}"]
|
|
if log_dir or debug:
|
|
cmds.append("--")
|
|
if log_dir:
|
|
cmds.append(f"--log_dir={log_dir}")
|
|
if debug:
|
|
cmds.append("--debug")
|
|
subprocess.run(cmds)
|
|
|
|
|
|
def server_ui(port=19899):
|
|
"""
|
|
start the Flask log server in real time
|
|
"""
|
|
from rdagent.log.server.app import main as log_server_main
|
|
|
|
log_server_main(port=port)
|
|
|
|
|
|
def ds_user_interact(port=19900):
|
|
"""
|
|
start web app to show the log traces in real time
|
|
"""
|
|
commands = ["streamlit", "run", "rdagent/log/ui/ds_user_interact.py", f"--server.port={port}"]
|
|
subprocess.run(commands)
|
|
|
|
|
|
@app.command(name="fin_factor")
|
|
def fin_factor_cli(
|
|
path: str | None = None,
|
|
step_n: int | None = None,
|
|
loop_n: int | None = None,
|
|
all_duration: str | None = None,
|
|
checkout: CheckoutOption = True,
|
|
):
|
|
fin_factor(path=path, step_n=step_n, loop_n=loop_n, all_duration=all_duration, checkout=checkout)
|
|
|
|
|
|
@app.command(name="fin_model")
|
|
def fin_model_cli(
|
|
path: str | None = None,
|
|
step_n: int | None = None,
|
|
loop_n: int | None = None,
|
|
all_duration: str | None = None,
|
|
checkout: CheckoutOption = True,
|
|
):
|
|
fin_model(path=path, step_n=step_n, loop_n=loop_n, all_duration=all_duration, checkout=checkout)
|
|
|
|
|
|
@app.command(name="fin_quant")
|
|
def fin_quant_cli(
|
|
path: str | None = None,
|
|
step_n: int | None = None,
|
|
loop_n: int | None = None,
|
|
all_duration: str | None = None,
|
|
checkout: CheckoutOption = True,
|
|
with_dashboard: bool = typer.Option(False, "--with-dashboard/-d", help="Start web dashboard automatically"),
|
|
with_cli_dashboard: bool = typer.Option(False, "--cli-dashboard/-c", help="Show beautiful CLI dashboard"),
|
|
dashboard_port: int = typer.Option(5000, "--dashboard-port", help="Dashboard port"),
|
|
model: str = typer.Option(
|
|
"local",
|
|
"--model",
|
|
"-m",
|
|
help="LLM backend to use: 'local' (llama.cpp), 'openrouter' (cloud models), or custom env var prefix",
|
|
),
|
|
auto_strategies: bool = typer.Option(
|
|
False,
|
|
"--auto-strategies",
|
|
help="Automatically generate strategies after factor threshold",
|
|
),
|
|
auto_strategies_threshold: int = typer.Option(
|
|
500,
|
|
"--auto-strategies-threshold",
|
|
help="Number of factors before triggering strategy generation",
|
|
),
|
|
):
|
|
"""
|
|
Start EURUSD quantitative trading loop.
|
|
|
|
Options:
|
|
--with-dashboard/-d: Start web dashboard at http://localhost:5000
|
|
--cli-dashboard/-c: Show beautiful terminal UI with live stats
|
|
--model/-m: LLM backend ('local' | 'openrouter')
|
|
--auto-strategies: Auto-generate strategies after threshold
|
|
--auto-strategies-threshold: Factor count trigger for auto strategies
|
|
|
|
Examples:
|
|
rdagent fin_quant # Local llama.cpp (default)
|
|
rdagent fin_quant -m local # Explicit local
|
|
rdagent fin_quant -m openrouter # Use OpenRouter model
|
|
rdagent fin_quant -d # Web dashboard
|
|
rdagent fin_quant -d -c # Both dashboards
|
|
rdagent fin_quant --auto-strategies # Auto-generate strategies
|
|
rdagent fin_quant --auto-strategies --auto-strategies-threshold 1000
|
|
|
|
OpenRouter Setup:
|
|
1. Set OPENROUTER_API_KEY in .env
|
|
2. Set OPENROUTER_MODEL (default: openrouter/google/gemini-2.0-flash:free)
|
|
3. Run: rdagent fin_quant -m openrouter
|
|
"""
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
|
|
from rich.console import Console
|
|
|
|
console = Console()
|
|
|
|
# ---- LLM Model Selection ----
|
|
if model == "openrouter":
|
|
api_key = os.getenv("OPENROUTER_API_KEY", "")
|
|
if not api_key:
|
|
console.print("\n[bold red]❌ OPENROUTER_API_KEY not set in .env[/bold red]")
|
|
console.print("[yellow]Add your API key to .env and retry:[/yellow]")
|
|
console.print(" OPENROUTER_API_KEY=sk-or-your-key-here")
|
|
raise typer.Exit(code=1)
|
|
|
|
os.environ["OPENAI_API_KEY"] = api_key
|
|
os.environ["OPENAI_API_BASE"] = "https://openrouter.ai/api/v1"
|
|
os.environ["CHAT_MODEL"] = os.getenv("OPENROUTER_MODEL", "openrouter/google/gemini-2.0-flash:free")
|
|
|
|
console.print(f"\n[bold blue]🌐 Using OpenRouter model:[/bold blue] [cyan]{os.environ['CHAT_MODEL']}[/cyan]")
|
|
elif model == "local":
|
|
# Ensure local defaults are set
|
|
if not os.getenv("OPENAI_API_BASE"):
|
|
os.environ["OPENAI_API_BASE"] = "http://localhost:8081/v1"
|
|
if not os.getenv("CHAT_MODEL"):
|
|
os.environ["CHAT_MODEL"] = "openai/qwen3.5-35b"
|
|
|
|
console.print(f"\n[bold green]🏠 Using local LLM:[/bold green] [cyan]{os.environ['CHAT_MODEL']}[/cyan]")
|
|
console.print(f" [dim]Base URL: {os.environ['OPENAI_API_BASE']}[/dim]")
|
|
|
|
# Wait until the llama.cpp server is fully loaded before starting the pipeline
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
base_url = os.environ["OPENAI_API_BASE"].removesuffix("/v1").rstrip("/")
|
|
health_url = f"{base_url}/health"
|
|
console.print(f" [yellow]⏳ Waiting for local LLM server to be ready ({health_url})...[/yellow]")
|
|
max_wait = 300 # seconds
|
|
waited = 0
|
|
interval = 5
|
|
while waited < max_wait:
|
|
try:
|
|
with urllib.request.urlopen(health_url, timeout=3) as resp:
|
|
body = resp.read().decode()
|
|
if '"status":"ok"' in body or '"status": "ok"' in body:
|
|
console.print(" [bold green]✅ LLM server is ready.[/bold green]")
|
|
break
|
|
except Exception:
|
|
pass
|
|
time.sleep(interval)
|
|
waited += interval
|
|
console.print(f" [dim]Still waiting... ({waited}s)[/dim]")
|
|
else:
|
|
console.print(" [bold yellow]⚠️ Server did not report 'ok' after 300s — proceeding anyway.[/bold yellow]")
|
|
else:
|
|
console.print(f"\n[yellow]⚠️ Unknown model backend: '{model}'. Using current .env settings.[/yellow]")
|
|
|
|
# Start Web Dashboard wenn gewünscht
|
|
if with_dashboard:
|
|
def start_web_dashboard():
|
|
console = Console()
|
|
console.print(f"\n[bold green]🚀 Starting Web Dashboard on http://localhost:{dashboard_port}...[/bold green]")
|
|
console.print(f" [cyan]Open: http://localhost:{dashboard_port}/dashboard.html[/cyan]\n")
|
|
subprocess.run(
|
|
["python", "web/dashboard_api.py"],
|
|
cwd=str(Path(__file__).parent.parent.parent),
|
|
env={**os.environ, "FLASK_ENV": "development"},
|
|
)
|
|
|
|
dashboard_thread = threading.Thread(target=start_web_dashboard, daemon=True)
|
|
dashboard_thread.start()
|
|
time.sleep(2)
|
|
|
|
# Start CLI Dashboard wenn gewünscht
|
|
if with_cli_dashboard:
|
|
def start_cli_dash():
|
|
from rdagent.log.ui.nexquant_dashboard import run_dashboard
|
|
run_dashboard(log_path="fin_quant.log", refresh_interval=3)
|
|
|
|
cli_thread = threading.Thread(target=start_cli_dash, daemon=True)
|
|
cli_thread.start()
|
|
time.sleep(1)
|
|
|
|
# Fin Quant starten
|
|
from rdagent.log.daily_log import session as _daily_session
|
|
|
|
_ctx: dict = {"model": model}
|
|
if loop_n is not None:
|
|
_ctx["loops"] = loop_n
|
|
if step_n is not None:
|
|
_ctx["steps"] = step_n
|
|
if auto_strategies:
|
|
_ctx["auto_strategies_threshold"] = auto_strategies_threshold
|
|
|
|
with _daily_session("fin_quant", **_ctx):
|
|
fin_quant(
|
|
path=path,
|
|
step_n=step_n,
|
|
loop_n=loop_n,
|
|
all_duration=all_duration,
|
|
checkout=checkout,
|
|
auto_strategies=auto_strategies,
|
|
auto_strategies_threshold=auto_strategies_threshold,
|
|
)
|
|
|
|
|
|
@app.command(name="fin_factor_report")
|
|
def fin_factor_report_cli(
|
|
report_folder: str | None = None,
|
|
path: str | None = None,
|
|
all_duration: str | None = None,
|
|
checkout: CheckoutOption = True,
|
|
):
|
|
fin_factor_report(report_folder=report_folder, path=path, all_duration=all_duration, checkout=checkout)
|
|
|
|
|
|
@app.command(name="general_model")
|
|
def general_model_cli(report_file_path: str):
|
|
general_model(report_file_path)
|
|
|
|
|
|
@app.command(name="data_science")
|
|
def data_science_cli(
|
|
path: str | None = None,
|
|
checkout: CheckoutOption = True,
|
|
step_n: int | None = None,
|
|
loop_n: int | None = None,
|
|
timeout: str | None = None,
|
|
competition: str | None = None,
|
|
):
|
|
data_science(
|
|
path=path,
|
|
checkout=checkout,
|
|
step_n=step_n,
|
|
loop_n=loop_n,
|
|
timeout=timeout,
|
|
competition=competition,
|
|
)
|
|
|
|
|
|
@app.command(name="llm_finetune")
|
|
def llm_finetune_cli(
|
|
path: str | None = None,
|
|
checkout: CheckoutOption = True,
|
|
benchmark: str | None = None,
|
|
benchmark_description: str | None = None,
|
|
dataset: str | None = None,
|
|
base_model: str | None = None,
|
|
upper_data_size_limit: int | None = None,
|
|
step_n: int | None = None,
|
|
loop_n: int | None = None,
|
|
timeout: str | None = None,
|
|
):
|
|
llm_finetune(
|
|
path=path,
|
|
checkout=checkout,
|
|
benchmark=benchmark,
|
|
benchmark_description=benchmark_description,
|
|
dataset=dataset,
|
|
base_model=base_model,
|
|
upper_data_size_limit=upper_data_size_limit,
|
|
step_n=step_n,
|
|
loop_n=loop_n,
|
|
timeout=timeout,
|
|
)
|
|
|
|
|
|
@app.command(name="grade_summary")
|
|
def grade_summary_cli(log_folder: str):
|
|
grade_summary(log_folder)
|
|
|
|
|
|
app.command(name="ui")(ui)
|
|
app.command(name="server_ui")(server_ui)
|
|
|
|
|
|
@app.command(name="health_check")
|
|
def health_check_cli(
|
|
check_env: CheckEnvOption = True,
|
|
check_docker: CheckDockerOption = True,
|
|
check_ports: CheckPortsOption = True,
|
|
):
|
|
health_check(check_env=check_env, check_docker=check_docker, check_ports=check_ports)
|
|
|
|
|
|
@app.command(name="collect_info")
|
|
def collect_info_cli():
|
|
collect_info()
|
|
|
|
|
|
app.command(name="ds_user_interact")(ds_user_interact)
|
|
|
|
|
|
@app.command(name="rl_trading")
|
|
def rl_trading_cli(
|
|
mode: str = typer.Option("train", help="Mode: train, backtest, live"),
|
|
algorithm: str = typer.Option("PPO", help="RL algorithm: PPO, A2C, SAC"),
|
|
model_path: str = typer.Option(None, help="Path to trained model"),
|
|
total_timesteps: int = typer.Option(100000, help="Training timesteps"),
|
|
data_config: str = typer.Option("data_config.yaml", help="Data config file"),
|
|
with_protections: bool = typer.Option(True, help="Enable trading protections"),
|
|
n_episodes: int = typer.Option(10, help="Number of evaluation episodes"),
|
|
):
|
|
"""
|
|
RL Trading Agent - Train and run reinforcement learning trading agents.
|
|
|
|
Examples:
|
|
# Train new RL agent
|
|
rdagent rl_trading --mode train --algorithm PPO --total-timesteps 100000
|
|
|
|
# Run backtest with trained model
|
|
rdagent rl_trading --mode backtest --model-path models/rl_trader.zip
|
|
|
|
# Run with protections disabled
|
|
rdagent rl_trading --mode backtest --no-with-protections
|
|
"""
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
console = Console()
|
|
|
|
# Load config
|
|
config_path = Path(data_config)
|
|
config = {}
|
|
if config_path.exists():
|
|
with open(config_path) as f:
|
|
config = yaml.safe_load(f) or {}
|
|
|
|
console.print("\n[bold blue]🤖 RL Trading Agent[/bold blue]")
|
|
console.print(f"Mode: [cyan]{mode}[/cyan]")
|
|
console.print(f"Algorithm: [cyan]{algorithm.upper()}[/cyan]")
|
|
console.print(f"Protections: {'[green]Enabled[/green]' if with_protections else '[red]Disabled[/red]'}")
|
|
|
|
try:
|
|
from rdagent.components.coder.rl import RLCosteer, RLTradingAgent, TradingEnv
|
|
except ImportError as e:
|
|
console.print("[bold red]Error: RL components not available.[/bold red]")
|
|
console.print(f"Details: {e}")
|
|
console.print("\n[yellow]Install RL dependencies:[/yellow]")
|
|
console.print(" pip install stable-baselines3 gymnasium")
|
|
raise typer.Exit(code=1)
|
|
|
|
if mode == "train":
|
|
console.print("\n[yellow]📊 Training RL agent...[/yellow]")
|
|
console.print(f" Algorithm: {algorithm.upper()}")
|
|
console.print(f" Timesteps: {total_timesteps:,}")
|
|
|
|
try:
|
|
# Create RL agent
|
|
agent = RLTradingAgent(algorithm=algorithm.upper())
|
|
|
|
# Load data for environment
|
|
console.print("[dim]Loading market data...[/dim]")
|
|
# TODO: Load actual data from config
|
|
# For now, create mock environment
|
|
import gymnasium as gym
|
|
import numpy as np
|
|
|
|
# Create simple mock environment for demonstration
|
|
class MockTradingEnv(gym.Env):
|
|
"""Mock environment for demonstration."""
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.action_space = gym.spaces.Box(low=-1.0, high=1.0, shape=(1,))
|
|
self.observation_space = gym.spaces.Box(low=-np.inf, high=np.inf, shape=(63,))
|
|
self.current_step = 0
|
|
self.max_steps = 1000
|
|
|
|
def reset(self, seed=None):
|
|
super().reset(seed=seed)
|
|
self.current_step = 0
|
|
return np.zeros(63, dtype=np.float32), {}
|
|
|
|
def step(self, action):
|
|
self.current_step += 1
|
|
reward = np.random.randn() * 0.01
|
|
done = self.current_step >= self.max_steps
|
|
obs = np.random.randn(63).astype(np.float32)
|
|
return obs, reward, done, False, {}
|
|
|
|
env = MockTradingEnv()
|
|
console.print("[dim]Environment created (mock for demonstration)[/dim]")
|
|
|
|
# Train
|
|
console.print("[yellow]Starting training...[/yellow]")
|
|
result = agent.train(env, total_timesteps=total_timesteps)
|
|
|
|
# Save model
|
|
model_path_out = Path("models") / f"rl_{algorithm.lower()}_trained.zip"
|
|
model_path_out.parent.mkdir(parents=True, exist_ok=True)
|
|
agent.save(model_path_out)
|
|
|
|
console.print("\n[bold green]✅ Training complete![/bold green]")
|
|
console.print(f"Model saved to: [cyan]{model_path_out}[/cyan]")
|
|
console.print(f"Algorithm: {result['algorithm']}")
|
|
console.print(f"Timesteps: {result['total_timesteps']:,}")
|
|
|
|
except Exception as e:
|
|
console.print(f"\n[bold red]❌ Training failed: {e}[/bold red]")
|
|
raise typer.Exit(code=1)
|
|
|
|
elif mode == "backtest":
|
|
console.print("\n[yellow]📈 Running RL backtest...[/yellow]")
|
|
|
|
if model_path:
|
|
console.print(f" Model: [cyan]{model_path}[/cyan]")
|
|
else:
|
|
console.print("[yellow]No model specified, using untrained agent[/yellow]")
|
|
|
|
try:
|
|
# Load agent
|
|
if model_path:
|
|
agent = RLTradingAgent(algorithm=algorithm.upper())
|
|
agent.load(Path(model_path))
|
|
else:
|
|
agent = RLTradingAgent(algorithm=algorithm.upper())
|
|
|
|
# Run backtest
|
|
import numpy as np
|
|
import pandas as pd
|
|
from rdagent.components.backtesting import FactorBacktester
|
|
|
|
backtester = FactorBacktester()
|
|
|
|
# Mock data for demonstration
|
|
console.print("[dim]Loading market data...[/dim]")
|
|
n_steps = 500
|
|
mock_prices = pd.Series(100 + np.cumsum(np.random.randn(n_steps) * 0.5))
|
|
mock_indicators = pd.DataFrame({
|
|
"rsi": np.random.uniform(30, 70, n_steps),
|
|
"macd": np.random.randn(n_steps) * 0.1,
|
|
})
|
|
|
|
console.print("[yellow]Running backtest...[/yellow]")
|
|
metrics = backtester.run_rl_backtest(
|
|
rl_agent=agent,
|
|
prices=mock_prices,
|
|
indicators=mock_indicators,
|
|
enable_protections=with_protections,
|
|
)
|
|
|
|
console.print("\n[bold green]✅ Backtest complete![/bold green]")
|
|
console.print(f" Final Equity: [green]${metrics.get('final_equity', 0):,.2f}[/green]")
|
|
console.print(f" Sharpe Ratio: {metrics.get('sharpe_ratio', 0):.3f}")
|
|
console.print(f" Max Drawdown: {metrics.get('max_drawdown', 0):.2%}")
|
|
console.print(f" Win Rate: {metrics.get('win_rate', 0):.2%}")
|
|
|
|
except Exception as e:
|
|
console.print(f"\n[bold red]❌ Backtest failed: {e}[/bold red]")
|
|
import traceback
|
|
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
raise typer.Exit(code=1)
|
|
|
|
elif mode == "live":
|
|
console.print("\n[yellow]🔴 Starting live RL trading...[/yellow]")
|
|
console.print("[bold red]⚠️ WARNING: Live trading carries real financial risk![/bold red]")
|
|
|
|
if not model_path:
|
|
console.print("[bold red]Error: Live trading requires a trained model (--model-path)[/bold red]")
|
|
raise typer.Exit(code=1)
|
|
|
|
try:
|
|
# Load costeer with protections
|
|
costeer = RLCosteer(
|
|
model_path=Path(model_path),
|
|
algorithm=algorithm.upper(),
|
|
enable_protections=with_protections,
|
|
)
|
|
|
|
console.print(f" Model: [cyan]{model_path}[/cyan]")
|
|
console.print(f" Algorithm: [cyan]{algorithm.upper()}[/cyan]")
|
|
console.print(f" Protections: {'[green]Enabled[/green]' if with_protections else '[red]Disabled[/red]'}")
|
|
|
|
# TODO: Implement live trading loop
|
|
console.print("\n[yellow]Live trading mode initialized.[/yellow]")
|
|
console.print("[dim]Connect to your broker API to execute trades.[/dim]")
|
|
console.print("[dim]See documentation for broker integration guide.[/dim]")
|
|
|
|
except Exception as e:
|
|
console.print(f"\n[bold red]❌ Live trading setup failed: {e}[/bold red]")
|
|
raise typer.Exit(code=1)
|
|
|
|
else:
|
|
console.print(f"[bold red]Error: Unknown mode '{mode}'[/bold red]")
|
|
console.print("Valid modes: train, backtest, live")
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
@app.command(name="generate_strategies")
|
|
def generate_strategies_cli(
|
|
count: int = typer.Option(10, "--count", "-n", help="Number of strategies to generate"),
|
|
workers: int = typer.Option(2, "--workers", "-w", help="Parallel workers (default: 2 to avoid LLM overload)"),
|
|
style: str = typer.Option("swing", "--style", "-s", help="Trading style: daytrading or swing"),
|
|
optuna: bool = typer.Option(True, "--optuna/--no-optuna", help="Enable Optuna optimization"),
|
|
optuna_trials: int = typer.Option(30, "--optuna-trials", help="Number of Optuna trials per strategy"),
|
|
top_factors: int = typer.Option(20, "--top-factors", help="Number of top factors to consider"),
|
|
continuous: bool = typer.Option(True, "--continuous/--single-pass", help="Optimize ALL strategies including rejected ones"),
|
|
max_iterations: int = typer.Option(1, "--max-iterations", "-i", help="Number of generation-optimization cycles (1 = single pass, >1 = continuous)"),
|
|
min_sharpe: float = typer.Option(1.5, "--min-sharpe", help="Minimum Sharpe ratio for acceptance"),
|
|
max_drawdown: float = typer.Option(-0.30, "--max-dd", help="Maximum drawdown allowed"),
|
|
min_win_rate: float = typer.Option(0.40, "--min-winrate", help="Minimum win rate for acceptance"),
|
|
):
|
|
"""
|
|
Generate trading strategies from evaluated factors.
|
|
|
|
Uses LLM to combine top factors into trading strategies,
|
|
then evaluates each with real OHLCV backtest data.
|
|
Optuna optimizes hyperparameters (thresholds, windows, etc.)
|
|
|
|
Examples:
|
|
rdagent generate_strategies # 10 strategies, swing, Optuna
|
|
rdagent generate_strategies -n 20 -w 8 # 20 strategies, 8 workers
|
|
rdagent generate_strategies -s daytrading # Day trading style
|
|
rdagent generate_strategies --no-optuna # Skip optimization
|
|
rdagent generate_strategies -i 5 # 5 continuous iterations
|
|
rdagent generate_strategies -n 3 -i 10 --optuna-trials 50 # Deep optimization
|
|
"""
|
|
from rich.console import Console
|
|
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeRemainingColumn
|
|
from rich.table import Table
|
|
|
|
console = Console()
|
|
|
|
# Validate inputs
|
|
if style not in ("daytrading", "swing"):
|
|
console.print(f"[bold red]Error: Invalid style '{style}'. Use 'daytrading' or 'swing'.[/bold red]")
|
|
raise typer.Exit(code=1)
|
|
|
|
if count < 1:
|
|
console.print("[bold red]Error: Count must be at least 1.[/bold red]")
|
|
raise typer.Exit(code=1)
|
|
|
|
if workers < 1 or workers > 16:
|
|
console.print("[bold red]Error: Workers must be between 1 and 16.[/bold red]")
|
|
raise typer.Exit(code=1)
|
|
|
|
console.print(f"\n[bold blue]{'='*60}[/bold blue]")
|
|
console.print("[bold blue] PREDIX Strategy Generator[/bold blue]")
|
|
console.print(f"[bold blue]{'='*60}[/bold blue]")
|
|
console.print(f" Strategies: [cyan]{count}[/cyan]")
|
|
console.print(f" Workers: [cyan]{workers}[/cyan]")
|
|
console.print(f" Style: [cyan]{style}[/cyan]")
|
|
console.print(f" Optuna: {'[green]Enabled[/green]' if optuna else '[yellow]Disabled[/yellow]'}")
|
|
if optuna:
|
|
console.print(f" Trials: [cyan]{optuna_trials}[/cyan]")
|
|
console.print(f" Continuous: {'[green]Yes[/green]' if continuous else '[yellow]No[/yellow]'}")
|
|
console.print(f" Iterations: [cyan]{max_iterations}[/cyan]")
|
|
console.print(f" Top Factors: [cyan]{top_factors}[/cyan]")
|
|
console.print(f"[bold blue]{'='*60}[/bold blue]\n")
|
|
|
|
from rdagent.log import daily_log as _dlog
|
|
|
|
_strat_ctx = {
|
|
"style": style,
|
|
"count": count,
|
|
"workers": workers,
|
|
"optuna": optuna,
|
|
"iterations": max_iterations,
|
|
}
|
|
if optuna:
|
|
_strat_ctx["trials"] = optuna_trials
|
|
_slog = _dlog.setup("strategies", **_strat_ctx)
|
|
|
|
try:
|
|
import pandas as pd
|
|
from rdagent.scenarios.qlib.local.strategy_orchestrator import StrategyOrchestrator
|
|
|
|
all_results = []
|
|
best_strategy = None
|
|
best_sharpe = float("-inf")
|
|
|
|
# CONTINUOUS OPTIMIZATION LOOP
|
|
for iteration in range(1, max_iterations + 1):
|
|
if max_iterations > 1:
|
|
console.print(f"\n[bold cyan]{'='*60}[/bold cyan]")
|
|
console.print(f"[bold cyan] ITERATION {iteration}/{max_iterations}[/bold cyan]")
|
|
console.print(f"[bold cyan]{'='*60}[/bold cyan]\n")
|
|
|
|
# Initialize orchestrator
|
|
orchestrator = StrategyOrchestrator(
|
|
top_factors=top_factors,
|
|
trading_style=style,
|
|
min_sharpe=min_sharpe,
|
|
max_drawdown=max_drawdown,
|
|
min_win_rate=min_win_rate,
|
|
use_optuna=optuna,
|
|
optuna_trials=optuna_trials,
|
|
continuous_optimization=continuous,
|
|
)
|
|
|
|
# Progress tracking
|
|
progress_data = {"generated": 0, "accepted": 0, "rejected": 0, "errors": []}
|
|
|
|
def progress_callback(current, total, result):
|
|
progress_data["generated"] = current
|
|
if result.get("status") == "accepted":
|
|
progress_data["accepted"] += 1
|
|
else:
|
|
progress_data["rejected"] += 1
|
|
|
|
# Generate strategies
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
BarColumn(),
|
|
TextColumn("[bold]{task.completed}/{task.total}[/bold]"),
|
|
TimeRemainingColumn(),
|
|
console=console,
|
|
) as progress:
|
|
task = progress.add_task(f"Generating {count} strategies (iter {iteration})...", total=None) # Unknown total
|
|
|
|
results = orchestrator.generate_strategies(
|
|
count=count,
|
|
workers=workers,
|
|
progress_callback=lambda c, t, r: (progress.update(task, completed=c, total=t), progress_callback(c, t, r)),
|
|
)
|
|
|
|
all_results.extend(results)
|
|
|
|
# Track best strategy
|
|
for r in results:
|
|
sharpe = r.get("sharpe_ratio", float("-inf"))
|
|
if sharpe > best_sharpe:
|
|
best_sharpe = sharpe
|
|
best_strategy = r
|
|
|
|
# Summary for this iteration
|
|
accepted = [r for r in results if r.get("status") == "accepted"]
|
|
console.print(f"\n[bold green]Iteration {iteration} complete: {len(accepted)}/{len(results)} accepted[/bold green]")
|
|
if accepted:
|
|
best_in_iter = max(accepted, key=lambda x: x.get("sharpe_ratio", 0))
|
|
console.print(f" Best: [green]{best_in_iter['strategy_name']}[/green] | Sharpe={best_in_iter.get('sharpe_ratio', 0):.4f}")
|
|
|
|
# Use all_results for final summary
|
|
results = all_results
|
|
|
|
# Print summary table
|
|
accepted = [r for r in results if r.get("status") == "accepted"]
|
|
rejected = [r for r in results if r.get("status") == "rejected"]
|
|
|
|
console.print(f"\n[bold green]{'='*60}[/bold green]")
|
|
console.print("[bold green] Strategy Generation Summary[/bold green]")
|
|
console.print(f"[bold green]{'='*60}[/bold green]")
|
|
|
|
table = Table(show_header=True, header_style="bold magenta", show_lines=True)
|
|
table.add_column("Status", style="dim", width=12)
|
|
table.add_column("Count", justify="right", width=8)
|
|
table.add_column("Percentage", justify="right", width=12)
|
|
|
|
table.add_row(
|
|
"Total",
|
|
str(len(results)),
|
|
"100%",
|
|
)
|
|
table.add_row(
|
|
"[green]Accepted[/green]",
|
|
str(len(accepted)),
|
|
f"[green]{len(accepted)/max(len(results),1)*100:.1f}%[/green]",
|
|
)
|
|
table.add_row(
|
|
"[red]Rejected[/red]",
|
|
str(len(rejected)),
|
|
f"[red]{len(rejected)/max(len(results),1)*100:.1f}%[/red]",
|
|
)
|
|
|
|
console.print(table)
|
|
|
|
# Show best strategy details
|
|
if best_strategy:
|
|
console.print(f"\n[bold gold1]{'='*60}[/bold gold1]")
|
|
console.print("[bold gold1] BEST STRATEGY[/bold gold1]")
|
|
console.print(f"[bold gold1]{'='*60}[/bold gold1]")
|
|
console.print(f" Name: [cyan]{best_strategy.get('strategy_name', 'Unknown')}[/cyan]")
|
|
console.print(f" Sharpe: [green]{best_strategy.get('sharpe_ratio', 0):.4f}[/green]")
|
|
console.print(f" Ann.Return: [green]{best_strategy.get('annualized_return', 0):.4f}[/green]")
|
|
console.print(f" Max DD: [yellow]{best_strategy.get('max_drawdown', 0):.2%}[/yellow]")
|
|
console.print(f" Win Rate: [cyan]{best_strategy.get('win_rate', 0):.2%}[/cyan]")
|
|
if best_strategy.get("best_params"):
|
|
console.print("\n [bold]Optimized Parameters:[/bold]")
|
|
for param, val in best_strategy["best_params"].items():
|
|
console.print(f" {param}: [cyan]{val}[/cyan]")
|
|
console.print(f"[bold gold1]{'='*60}[/bold gold1]")
|
|
|
|
if accepted:
|
|
console.print("\n[bold]Accepted Strategies:[/bold]")
|
|
acc_table = Table(show_header=True, header_style="bold cyan")
|
|
acc_table.add_column("#", width=4)
|
|
acc_table.add_column("Strategy", width=30)
|
|
acc_table.add_column("Sharpe", justify="right", width=10)
|
|
acc_table.add_column("Ann. Return", justify="right", width=12)
|
|
acc_table.add_column("Max DD", justify="right", width=10)
|
|
acc_table.add_column("Win Rate", justify="right", width=10)
|
|
acc_table.add_column("Optuna", justify="right", width=8)
|
|
|
|
for i, strat in enumerate(sorted(accepted, key=lambda x: x.get("sharpe_ratio", 0), reverse=True), 1):
|
|
optuna_status = "[green]Yes[/green]" if strat.get("best_params") else "[dim]No[/dim]"
|
|
acc_table.add_row(
|
|
str(i),
|
|
strat.get("strategy_name", "Unknown")[:30],
|
|
f"{strat.get('sharpe_ratio', 0):.2f}",
|
|
f"{strat.get('annualized_return', 0):.4f}",
|
|
f"{strat.get('max_drawdown', 0):.2%}",
|
|
f"{strat.get('win_rate', 0):.2%}",
|
|
optuna_status,
|
|
)
|
|
console.print(acc_table)
|
|
|
|
console.print("\n[bold green]Strategies saved to:[/bold green] [cyan]results/strategies_new/[/cyan]")
|
|
console.print(f"[bold blue]{'='*60}[/bold blue]\n")
|
|
_slog.success(f"Generated {len(all_results)} strategies ({len([r for r in all_results if r.get('status')=='accepted'])} accepted)")
|
|
|
|
except ImportError as e:
|
|
_slog.error(f"Strategy components not available: {e}")
|
|
console.print("[bold red]Error: Strategy components not available.[/bold red]")
|
|
console.print(f"Details: {e}")
|
|
raise typer.Exit(code=1)
|
|
except Exception as e:
|
|
_slog.error(f"Strategy generation failed: {e}")
|
|
console.print(f"[bold red]Strategy generation failed: {e}[/bold red]")
|
|
import traceback
|
|
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
@app.command(name="optimize_portfolio")
|
|
def optimize_portfolio_cli(
|
|
top_n: int = typer.Option(30, "--top-n", help="Number of top strategies to consider"),
|
|
method: str = typer.Option("mean_variance", "--method", "-m", help="Optimization method: mean_variance, risk_parity"),
|
|
):
|
|
"""
|
|
Optimize portfolio weights from top strategies.
|
|
|
|
Uses Modern Portfolio Theory to find optimal strategy weights.
|
|
|
|
Examples:
|
|
rdagent optimize_portfolio # Mean-variance, top 30
|
|
rdagent optimize_portfolio --method risk_parity # Risk parity
|
|
rdagent optimize_portfolio --top-n 20 # Top 20 strategies
|
|
"""
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
|
|
console = Console()
|
|
|
|
if method not in ("mean_variance", "risk_parity"):
|
|
console.print(f"[bold red]Error: Invalid method '{method}'. Use 'mean_variance' or 'risk_parity'.[/bold red]")
|
|
raise typer.Exit(code=1)
|
|
|
|
console.print(f"\n[bold blue]{'='*60}[/bold blue]")
|
|
console.print("[bold blue] PREDIX Portfolio Optimizer[/bold blue]")
|
|
console.print(f"[bold blue]{'='*60}[/bold blue]")
|
|
console.print(f" Top N: [cyan]{top_n}[/cyan]")
|
|
console.print(f" Method: [cyan]{method}[/cyan]")
|
|
console.print(f"[bold blue]{'='*60}[/bold blue]\n")
|
|
|
|
try:
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from rdagent.components.backtesting.risk_management import PortfolioOptimizer
|
|
|
|
project_root = Path(__file__).parent.parent.parent
|
|
strategies_dir = project_root / "results" / "strategies_new"
|
|
|
|
if not strategies_dir.exists():
|
|
console.print("[bold red]Error: No strategies found in results/strategies_new/[/bold red]")
|
|
raise typer.Exit(code=1)
|
|
|
|
# Load strategies
|
|
strategies = []
|
|
for f in strategies_dir.glob("*.json"):
|
|
try:
|
|
with open(f, encoding="utf-8") as fh:
|
|
data = json.load(fh)
|
|
if data.get("status") == "accepted":
|
|
strategies.append(data)
|
|
except Exception:
|
|
logger.warning("Failed to load strategy file %s", f, exc_info=True)
|
|
continue
|
|
|
|
if not strategies:
|
|
console.print("[bold red]Error: No accepted strategies found.[/bold red]")
|
|
raise typer.Exit(code=1)
|
|
|
|
# Sort by Sharpe and take top N
|
|
strategies.sort(key=lambda x: x.get("sharpe_ratio", 0), reverse=True)
|
|
top_strategies = strategies[:top_n]
|
|
|
|
console.print(f"Loaded {len(top_strategies)} accepted strategies.\n")
|
|
|
|
# Build return series (simplified - using strategy metrics as proxies)
|
|
n = len(top_strategies)
|
|
# Create synthetic returns based on strategy metrics for weight optimization
|
|
# In production, this would use actual strategy equity curves
|
|
names = [s.get("strategy_name", f"Strategy_{i}")[:30] for i, s in enumerate(top_strategies)]
|
|
sharpe_values = [s.get("sharpe_ratio", 0) for s in top_strategies]
|
|
|
|
# Use Sharpe as expected return proxy
|
|
exp_returns = pd.Series(sharpe_values, index=names)
|
|
|
|
# Build covariance matrix (simplified - assume some correlation)
|
|
np.random.seed(42)
|
|
cov_matrix = pd.DataFrame(
|
|
np.eye(n) * 0.1 + np.ones((n, n)) * 0.02,
|
|
index=names,
|
|
columns=names,
|
|
)
|
|
|
|
# Optimize
|
|
optimizer = PortfolioOptimizer()
|
|
|
|
if method == "mean_variance":
|
|
weights = optimizer.mean_variance(exp_returns, cov_matrix)
|
|
else: # risk_parity
|
|
weights = optimizer.risk_parity(cov_matrix)
|
|
|
|
# Normalize negative weights to zero
|
|
weights = np.maximum(weights, 0)
|
|
weight_sum = np.sum(weights)
|
|
if weight_sum > 0:
|
|
weights = weights / weight_sum
|
|
|
|
# Print results
|
|
console.print(f"[bold]Optimal Portfolio Weights ({method}):[/bold]\n")
|
|
|
|
weight_table = Table(show_header=True, header_style="bold cyan")
|
|
weight_table.add_column("#", width=4)
|
|
weight_table.add_column("Strategy", width=35)
|
|
weight_table.add_column("Weight", justify="right", width=10)
|
|
weight_table.add_column("Sharpe", justify="right", width=10)
|
|
|
|
sorted_indices = np.argsort(weights)[::-1]
|
|
for i, idx in enumerate(sorted_indices):
|
|
if weights[idx] > 0.01: # Only show meaningful weights
|
|
weight_table.add_row(
|
|
str(i + 1),
|
|
names[idx][:35],
|
|
f"{weights[idx]:.2%}",
|
|
f"{sharpe_values[idx]:.2f}",
|
|
)
|
|
|
|
console.print(weight_table)
|
|
|
|
# Portfolio metrics
|
|
portfolio_sharpe = np.dot(weights, sharpe_values)
|
|
console.print(f"\n[bold green]Portfolio Sharpe Ratio: {portfolio_sharpe:.2f}[/bold green]")
|
|
|
|
# Save portfolio weights
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
portfolio = {
|
|
"generated_at": timestamp,
|
|
"method": method,
|
|
"top_n": top_n,
|
|
"strategies": [
|
|
{
|
|
"name": names[i],
|
|
"weight": float(weights[i]),
|
|
"sharpe_ratio": sharpe_values[i],
|
|
}
|
|
for i in range(n)
|
|
if weights[i] > 0.01
|
|
],
|
|
"portfolio_sharpe": float(portfolio_sharpe),
|
|
}
|
|
|
|
portfolios_dir = project_root / "results" / "portfolios"
|
|
portfolios_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
portfolio_file = portfolios_dir / f"portfolio_{timestamp}.json"
|
|
with open(portfolio_file, "w", encoding="utf-8") as f:
|
|
json.dump(portfolio, f, indent=2, ensure_ascii=False)
|
|
|
|
console.print(f"[green]Portfolio saved to:[/green] [cyan]{portfolio_file}[/cyan]")
|
|
console.print(f"[bold blue]{'='*60}[/bold blue]\n")
|
|
|
|
except Exception as e:
|
|
console.print(f"[bold red]Portfolio optimization failed: {e}[/bold red]")
|
|
import traceback
|
|
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
@app.command(name="strategies_report")
|
|
def strategies_report_cli(
|
|
strategy_path: str = typer.Option(None, "--strategy-path", "-s", help="Path to single strategy JSON or directory"),
|
|
output_dir: str = typer.Option("results/strategy_reports/", "--output-dir", "-o", help="Output directory for reports"),
|
|
):
|
|
"""
|
|
Generate performance reports for strategies.
|
|
|
|
Creates detailed reports with metrics, equity curves, and analysis.
|
|
|
|
Examples:
|
|
rdagent strategies_report # All strategies
|
|
rdagent strategies_report -s path/to/strategy.json # Single strategy
|
|
rdagent strategies_report -o custom/reports/ # Custom output dir
|
|
"""
|
|
from pathlib import Path
|
|
|
|
from rich.console import Console
|
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
|
|
console = Console()
|
|
|
|
console.print(f"\n[bold blue]{'='*60}[/bold blue]")
|
|
console.print("[bold blue] PREDIX Strategy Report Generator[/bold blue]")
|
|
console.print(f"[bold blue]{'='*60}[/bold blue]\n")
|
|
|
|
project_root = Path(__file__).parent.parent.parent
|
|
|
|
if strategy_path is None:
|
|
# Use default directory
|
|
strategy_path = str(project_root / "results" / "strategies_new")
|
|
|
|
# Resolve paths
|
|
strategy_path = Path(strategy_path)
|
|
output_dir_path = Path(output_dir)
|
|
output_dir_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Collect strategy files
|
|
strategy_files = []
|
|
|
|
if strategy_path.is_file() and strategy_path.suffix == ".json":
|
|
strategy_files.append(strategy_path)
|
|
elif strategy_path.is_dir():
|
|
strategy_files = sorted(strategy_path.glob("*.json"))
|
|
else:
|
|
console.print(f"[bold red]Error: Path not found or not a JSON file: {strategy_path}[/bold red]")
|
|
raise typer.Exit(code=1)
|
|
|
|
if not strategy_files:
|
|
console.print("[bold red]Error: No strategy JSON files found.[/bold red]")
|
|
raise typer.Exit(code=1)
|
|
|
|
console.print(f"Found {len(strategy_files)} strategy file(s).\n")
|
|
|
|
reports_generated = 0
|
|
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
console=console,
|
|
) as progress:
|
|
for spath in strategy_files:
|
|
task = progress.add_task(f"Processing {spath.name}...", total=1)
|
|
|
|
try:
|
|
report = _generate_single_strategy_report(spath, output_dir_path)
|
|
reports_generated += 1
|
|
console.print(f" [green]Report generated:[/green] {report['output_file']}")
|
|
progress.update(task, completed=1)
|
|
|
|
except Exception as e:
|
|
console.print(f" [red]Failed to process {spath.name}: {e}[/red]")
|
|
progress.update(task, completed=1)
|
|
|
|
console.print(f"\n[bold green]{'='*60}[/bold green]")
|
|
console.print("[bold green] Report Generation Complete[/bold green]")
|
|
console.print(f"[bold green]{'='*60}[/bold green]")
|
|
console.print(f" Reports generated: [cyan]{reports_generated}/{len(strategy_files)}[/cyan]")
|
|
console.print(f" Output directory: [cyan]{output_dir_path}[/cyan]")
|
|
console.print(f"[bold green]{'='*60}[/bold green]\n")
|
|
|
|
|
|
def _generate_single_strategy_report(strategy_file: Path, output_dir: Path) -> dict:
|
|
"""Generate a report for a single strategy."""
|
|
import json
|
|
|
|
import matplotlib
|
|
matplotlib.use("Agg") # Non-interactive backend
|
|
import matplotlib.pyplot as plt
|
|
|
|
with open(strategy_file, encoding="utf-8") as f:
|
|
strategy = json.load(f)
|
|
|
|
strategy_name = strategy.get("strategy_name", "Unknown")
|
|
safe_name = strategy_name.replace("/", "_").replace(" ", "_").replace("\\", "_")[:60]
|
|
|
|
# Create report
|
|
report = {
|
|
"strategy_name": strategy_name,
|
|
"generated_at": datetime.now().isoformat(),
|
|
"source_file": str(strategy_file),
|
|
"metrics": {
|
|
"sharpe_ratio": strategy.get("sharpe_ratio", "N/A"),
|
|
"annualized_return": strategy.get("annualized_return", "N/A"),
|
|
"max_drawdown": strategy.get("max_drawdown", "N/A"),
|
|
"win_rate": strategy.get("win_rate", "N/A"),
|
|
"volatility": strategy.get("volatility", "N/A"),
|
|
"information_ratio": strategy.get("information_ratio", "N/A"),
|
|
},
|
|
"factors_used": strategy.get("factors_used", []),
|
|
"trading_style": strategy.get("trading_style", "N/A"),
|
|
}
|
|
|
|
# Generate equity curve visualization
|
|
fig, ax = plt.subplots(figsize=(12, 6))
|
|
|
|
# Simulate equity curve from metrics
|
|
ann_return = strategy.get("annualized_return", 0)
|
|
sharpe = strategy.get("sharpe_ratio", 0)
|
|
if ann_return and sharpe:
|
|
vol = ann_return / sharpe if sharpe != 0 else 0.1
|
|
np.random.seed(42)
|
|
n_days = 252
|
|
daily_returns = np.random.normal(ann_return / n_days, vol / np.sqrt(n_days), n_days)
|
|
equity = 10000 * np.cumprod(1 + daily_returns)
|
|
|
|
ax.plot(equity, linewidth=2, color="#2196F3")
|
|
ax.set_title(f"Equity Curve - {strategy_name}", fontsize=14, fontweight="bold")
|
|
ax.set_xlabel("Trading Days")
|
|
ax.set_ylabel("Equity ($)")
|
|
ax.grid(True, alpha=0.3)
|
|
|
|
# Add starting equity line
|
|
ax.axhline(y=10000, color="gray", linestyle="--", alpha=0.5, label="Starting Equity")
|
|
ax.legend()
|
|
else:
|
|
ax.text(0.5, 0.5, "Insufficient data for equity curve", ha="center", va="center", fontsize=14)
|
|
ax.set_title(f"Equity Curve - {strategy_name}")
|
|
|
|
plt.tight_layout()
|
|
|
|
# Save chart
|
|
chart_file = output_dir / f"{safe_name}_equity.png"
|
|
plt.savefig(chart_file, dpi=150, bbox_inches="tight")
|
|
plt.close()
|
|
|
|
report["output_file"] = str(chart_file)
|
|
|
|
# Save report as JSON
|
|
report_file = output_dir / f"{safe_name}_report.json"
|
|
with open(report_file, "w", encoding="utf-8") as f:
|
|
json.dump(report, f, indent=2, default=str, ensure_ascii=False)
|
|
|
|
return report
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|
|
|
|
|
|
@app.command(name="start_llama")
|
|
def start_llama_cli(
|
|
model: str = typer.Option(
|
|
None, "--model", "-m", help="Path to model file",
|
|
),
|
|
port: int = typer.Option(8081, "--port", "-p", help="Server port"),
|
|
gpu_layers: int = typer.Option(30, "--gpu-layers", "-g", help="GPU layers"),
|
|
ctx_size: int = typer.Option(80000, "--ctx-size", "-c", help="Context size"),
|
|
reasoning: bool = typer.Option(False, "--reasoning", help="Enable reasoning mode"),
|
|
):
|
|
"""
|
|
Start llama.cpp server for local LLM inference.
|
|
|
|
Options:
|
|
--model/-m: Path to model file (default: from .env or ~/models/qwen3.5/)
|
|
--port/-p: Server port (default: 8081)
|
|
--gpu-layers/-g: GPU layers (default: 30)
|
|
--ctx-size/-c: Context size (default: 80000)
|
|
--reasoning: Enable reasoning mode (default: off)
|
|
|
|
Examples:
|
|
rdagent start_llama
|
|
rdagent start_llama --gpu-layers 40 --ctx-size 4096
|
|
rdagent start_llama --reasoning
|
|
"""
|
|
import os
|
|
|
|
model_path = model or os.getenv(
|
|
"LLAMA_MODEL_PATH",
|
|
str(Path.home() / "models" / "qwen3.5" / "Qwen3.5-35B-A3B-Q3_K_M.gguf"),
|
|
)
|
|
|
|
llama_server = str(Path.home() / "llama.cpp" / "build" / "bin" / "llama-server")
|
|
|
|
if not Path(llama_server).exists():
|
|
print(f"❌ llama.cpp server not found: {llama_server}")
|
|
print("\nBuild it first:")
|
|
print(" cd ~/llama.cpp && mkdir -p build && cd build && cmake .. && make")
|
|
sys.exit(1)
|
|
|
|
if not Path(model_path).exists():
|
|
print(f"❌ Model not found: {model_path}")
|
|
sys.exit(1)
|
|
|
|
cmd = [
|
|
llama_server,
|
|
"--model", model_path,
|
|
"--n-gpu-layers", str(gpu_layers),
|
|
"--ctx-size", str(ctx_size),
|
|
"--port", str(port),
|
|
"--threads", "8",
|
|
"--threads-batch", "8",
|
|
"--parallel", "1",
|
|
"--flash-attn",
|
|
"--jinja",
|
|
"--host", "0.0.0.0",
|
|
]
|
|
|
|
if not reasoning:
|
|
cmd.extend(["--reasoning", "off"])
|
|
|
|
print("🚀 Starting llama.cpp server...")
|
|
print(f" Model: {Path(model_path).name}")
|
|
print(f" Port: {port}")
|
|
print(f" GPU Layers: {gpu_layers}")
|
|
print(f" Context: {ctx_size}")
|
|
print(f" Reasoning: {'on' if reasoning else 'off'}")
|
|
print()
|
|
|
|
try:
|
|
os.execvp(cmd[0], cmd)
|
|
except Exception as e:
|
|
print(f"❌ Failed to start llama.cpp server: {e}")
|
|
sys.exit(1)
|
|
|
|
|
|
@app.command(name="start_loop")
|
|
def start_loop_cli(
|
|
target_count: int = typer.Option(3, "--target", "-t", help="Strategies per run"),
|
|
max_wait: int = typer.Option(1800, "--max-wait", "-w", help="Max wait per run (seconds)"),
|
|
):
|
|
"""
|
|
Start PREDIX strategy generator loop.
|
|
|
|
Runs continuously, generating strategies with automatic restart on crash.
|
|
|
|
Options:
|
|
--target/-t: Strategies to generate per run (default: 3)
|
|
--max-wait/-w: Max wait time per run in seconds (default: 1800 = 30min)
|
|
|
|
Examples:
|
|
rdagent start_loop
|
|
rdagent start_loop --target 5 --max-wait 3600
|
|
"""
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
import time
|
|
from datetime import datetime
|
|
|
|
script_dir = str(Path(__file__).parent.parent.parent)
|
|
generator = [sys.executable, f"{script_dir}/scripts/nexquant_smart_strategy_gen.py"]
|
|
logfile = f"{script_dir}/results/logs/generator_loop.log"
|
|
pidfile = "/tmp/nexquant_loop.pid" # nosec B108 — administrative PID file, single-process daemon
|
|
|
|
os.makedirs(f"{script_dir}/results/logs", exist_ok=True)
|
|
|
|
def log(msg: str):
|
|
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
line = f"{ts} - {msg}"
|
|
print(line)
|
|
with open(logfile, "a") as f:
|
|
f.write(line + "\n")
|
|
|
|
child_proc = None # track current child PID for targeted cleanup
|
|
|
|
def cleanup(signum=None, frame=None):
|
|
log("Received termination signal. Cleaning up...")
|
|
if child_proc is not None:
|
|
try:
|
|
child_proc.terminate()
|
|
child_proc.wait(timeout=10)
|
|
except Exception:
|
|
try:
|
|
child_proc.kill()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
os.remove(pidfile)
|
|
except FileNotFoundError:
|
|
pass
|
|
log("Cleanup complete. Exiting.")
|
|
sys.exit(0)
|
|
|
|
signal.signal(signal.SIGTERM, cleanup)
|
|
signal.signal(signal.SIGINT, cleanup)
|
|
|
|
with open(pidfile, "w") as f:
|
|
f.write(str(os.getpid()))
|
|
|
|
log("=========================================")
|
|
log("🚀 PREDIX Generator Loop Starting")
|
|
log("=========================================")
|
|
log(f"Target: {target_count} strategies per run")
|
|
log(f"Max wait: {max_wait}s per run")
|
|
log(f"Log: {logfile}")
|
|
|
|
attempt = 0
|
|
|
|
while True:
|
|
attempt += 1
|
|
log("")
|
|
log(f"=== Attempt #{attempt} ===================================")
|
|
|
|
# Check disk space
|
|
try:
|
|
usage = subprocess.run(["df", "-h", script_dir], capture_output=True, text=True)
|
|
disk_line = usage.stdout.strip().split("\n")[-1]
|
|
pct = int(disk_line.split()[4].replace("%", ""))
|
|
if pct > 90:
|
|
log(f"⚠️ Disk usage at {pct}%. Pausing...")
|
|
time.sleep(300)
|
|
continue
|
|
except Exception:
|
|
pass
|
|
|
|
# Count existing strategies
|
|
from pathlib import Path as P
|
|
strat_dir = P(f"{script_dir}/results/strategies_new")
|
|
strat_count = len(list(strat_dir.glob("*.json"))) if strat_dir.exists() else 0
|
|
log(f"📁 Existing strategies: {strat_count}")
|
|
|
|
# Kill stale child from previous iteration
|
|
if child_proc is not None:
|
|
try:
|
|
child_proc.terminate()
|
|
child_proc.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
child_proc.kill()
|
|
child_proc.wait()
|
|
except Exception:
|
|
pass
|
|
child_proc = None
|
|
time.sleep(2)
|
|
|
|
# Start generator
|
|
log("🤖 Starting generator...")
|
|
child_proc = subprocess.Popen(
|
|
generator,
|
|
cwd=script_dir,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
log(f" PID: {child_proc.pid}")
|
|
|
|
# Monitor progress
|
|
elapsed = 0
|
|
while child_proc.poll() is None:
|
|
time.sleep(30)
|
|
elapsed += 30
|
|
|
|
if elapsed % 120 == 0:
|
|
log(f" ⏱️ {elapsed}s elapsed")
|
|
|
|
if elapsed >= max_wait:
|
|
log(f" ⏰ Timeout after {elapsed}s. Killing...")
|
|
child_proc.kill()
|
|
break
|
|
|
|
# Check results
|
|
exit_code = child_proc.wait()
|
|
child_proc = None
|
|
if exit_code == 0:
|
|
log("✅ Generator completed successfully")
|
|
elif exit_code == -9:
|
|
log("❌ Generator killed (OOM? Exit 137)")
|
|
else:
|
|
log(f"⚠️ Generator exited with code {exit_code}")
|
|
|
|
# Count new strategies
|
|
new_strats = sorted(strat_dir.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True)[:3]
|
|
if new_strats:
|
|
log("📊 Latest strategies:")
|
|
for s in new_strats:
|
|
log(f" - {s.name}")
|
|
|
|
log("⏳ Waiting 60s before next attempt...")
|
|
time.sleep(60)
|
|
|
|
|
|
@app.command(name="parallel")
|
|
def parallel_cli(
|
|
runs: int = typer.Option(5, "--runs", "-n", help="Number of parallel runs"),
|
|
api_keys: int = typer.Option(1, "--api-keys", "-k", help="Number of API keys to distribute"),
|
|
):
|
|
"""
|
|
Run multiple factor experiments in parallel.
|
|
|
|
Each run gets its own:
|
|
- Log file
|
|
- Result directory
|
|
- Workspace
|
|
|
|
Options:
|
|
--runs/-n: Number of parallel runs (default: 5)
|
|
--api-keys/-k: Number of API keys (default: 1)
|
|
|
|
Examples:
|
|
rdagent parallel --runs 5 --api-keys 1
|
|
rdagent parallel -n 10 -k 2
|
|
"""
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from rdagent.log import daily_log as _dlog
|
|
|
|
project_root = Path(__file__).parent.parent.parent
|
|
script = project_root / "scripts" / "nexquant_parallel.py"
|
|
|
|
if not script.exists():
|
|
typer.echo(f"❌ Script not found: {script}")
|
|
raise typer.Exit(code=1)
|
|
|
|
cmd = [sys.executable, str(script), "--runs", str(runs), "--api-keys", str(api_keys)]
|
|
|
|
_plog = _dlog.setup("parallel", runs=runs, api_keys=api_keys, model="local")
|
|
typer.echo(f"🚀 Starting {runs} parallel runs...")
|
|
typer.echo(f" Script: {script}")
|
|
typer.echo(f" API Keys: {api_keys}")
|
|
typer.echo(" Model: local (llama.cpp)")
|
|
|
|
try:
|
|
result = subprocess.run(cmd, cwd=str(project_root))
|
|
_plog.info(f"Parallel runs finished returncode={result.returncode}")
|
|
raise typer.Exit(code=result.returncode)
|
|
except KeyboardInterrupt:
|
|
_plog.warning("Parallel runs interrupted by user")
|
|
typer.echo("\n⚠️ Interrupted by user")
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
@app.command(name="eval_all")
|
|
def eval_all_cli(
|
|
top: int = typer.Option(100, "--top", "-n", help="Evaluate top N factors"),
|
|
parallel: int = typer.Option(4, "--parallel", "-p", help="Number of parallel workers"),
|
|
full_data: bool = typer.Option(True, "--full-data/--debug-data", help="Use full dataset"),
|
|
):
|
|
"""
|
|
Evaluate factors with full 1-minute data.
|
|
|
|
Computes IC, Sharpe, Max DD, Win Rate for existing factors
|
|
using the complete intraday_pv.h5 dataset.
|
|
|
|
Options:
|
|
--top/-n: Evaluate top N factors by IC (default: 100)
|
|
--parallel/-p: Number of parallel workers (default: 4)
|
|
--full-data: Use full dataset (default: True)
|
|
|
|
Examples:
|
|
rdagent eval_all --top 100
|
|
rdagent eval_all -n 500 -p 8
|
|
"""
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from rdagent.log import daily_log as _dlog
|
|
|
|
project_root = Path(__file__).parent.parent.parent
|
|
script = project_root / "scripts" / "nexquant_full_eval.py"
|
|
|
|
if not script.exists():
|
|
typer.echo(f"❌ Script not found: {script}")
|
|
raise typer.Exit(code=1)
|
|
|
|
cmd = [sys.executable, str(script)]
|
|
if top > 0:
|
|
cmd.extend(["--top", str(top)])
|
|
if parallel > 1:
|
|
cmd.extend(["--parallel", str(parallel)])
|
|
|
|
_elog = _dlog.setup("evaluate", top=top, workers=parallel)
|
|
typer.echo(f"📊 Evaluating top {top} factors with full data...")
|
|
typer.echo(f" Script: {script}")
|
|
typer.echo(f" Workers: {parallel}")
|
|
|
|
try:
|
|
result = subprocess.run(cmd, cwd=str(project_root))
|
|
_elog.info(f"Evaluation finished returncode={result.returncode}")
|
|
raise typer.Exit(code=result.returncode)
|
|
except KeyboardInterrupt:
|
|
_elog.warning("Evaluation interrupted by user")
|
|
typer.echo("\n⚠️ Interrupted by user")
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
@app.command(name="batch_backtest")
|
|
def batch_backtest_cli(
|
|
factors: int = typer.Option(100, "--factors", "-n", help="Number of factors to backtest"),
|
|
parallel: int = typer.Option(4, "--parallel", "-p", help="Number of parallel workers"),
|
|
all_factors: bool = typer.Option(False, "--all", "-a", help="Backtest all factors"),
|
|
):
|
|
"""
|
|
Batch backtest existing factors.
|
|
|
|
Scans generated factor code from workspaces, runs Qlib backtests,
|
|
and saves results to JSON + SQLite.
|
|
|
|
Options:
|
|
--factors/-n: Number of factors to backtest (default: 100)
|
|
--parallel/-p: Number of parallel workers (default: 4)
|
|
--all/-a: Backtest all factors
|
|
|
|
Examples:
|
|
rdagent batch_backtest --factors 100
|
|
rdagent batch_backtest -n 500 -p 8
|
|
rdagent batch_backtest --all
|
|
"""
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
project_root = Path(__file__).parent.parent.parent
|
|
script = project_root / "scripts" / "nexquant_batch_backtest.py"
|
|
|
|
if not script.exists():
|
|
typer.echo(f"❌ Script not found: {script}")
|
|
raise typer.Exit(code=1)
|
|
|
|
cmd = [sys.executable, str(script)]
|
|
if all_factors:
|
|
cmd.append("--all")
|
|
elif factors > 0:
|
|
cmd.extend(["--factors", str(factors)])
|
|
if parallel > 1:
|
|
cmd.extend(["--parallel", str(parallel)])
|
|
|
|
typer.echo(f"📈 Batch backtesting {factors} factors...")
|
|
typer.echo(f" Script: {script}")
|
|
typer.echo(f" Workers: {parallel}")
|
|
|
|
try:
|
|
result = subprocess.run(cmd, cwd=str(project_root))
|
|
raise typer.Exit(code=result.returncode)
|
|
except KeyboardInterrupt:
|
|
typer.echo("\n⚠️ Interrupted by user")
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
@app.command(name="simple_eval")
|
|
def simple_eval_cli(
|
|
top: int = typer.Option(100, "--top", "-n", help="Evaluate top N factors"),
|
|
parallel: int = typer.Option(4, "--parallel", "-p", help="Number of parallel workers"),
|
|
all_factors: bool = typer.Option(False, "--all", "-a", help="Evaluate all factors"),
|
|
):
|
|
"""
|
|
Simple factor evaluation - Direct IC/Sharpe computation.
|
|
|
|
Computes IC and Sharpe directly from factor values and forward returns
|
|
without Qlib infrastructure (faster but less accurate).
|
|
|
|
Options:
|
|
--top/-n: Evaluate top N factors (default: 100)
|
|
--parallel/-p: Number of parallel workers (default: 4)
|
|
--all/-a: Evaluate all factors
|
|
|
|
Examples:
|
|
rdagent simple_eval --top 100
|
|
rdagent simple_eval -n 500 -p 8
|
|
rdagent simple_eval --all
|
|
"""
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
project_root = Path(__file__).parent.parent.parent
|
|
script = project_root / "scripts" / "nexquant_simple_eval.py"
|
|
|
|
if not script.exists():
|
|
typer.echo(f"❌ Script not found: {script}")
|
|
raise typer.Exit(code=1)
|
|
|
|
cmd = [sys.executable, str(script)]
|
|
if all_factors:
|
|
cmd.append("--all")
|
|
elif top > 0:
|
|
cmd.extend(["--top", str(top)])
|
|
if parallel > 1:
|
|
cmd.extend(["--parallel", str(parallel)])
|
|
|
|
typer.echo(f"📊 Simple evaluating top {top} factors...")
|
|
typer.echo(f" Script: {script}")
|
|
typer.echo(f" Workers: {parallel}")
|
|
|
|
try:
|
|
result = subprocess.run(cmd, cwd=str(project_root))
|
|
raise typer.Exit(code=result.returncode)
|
|
except KeyboardInterrupt:
|
|
typer.echo("\n⚠️ Interrupted by user")
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
@app.command(name="rebacktest")
|
|
def rebacktest_cli(
|
|
strategies_dir: str = typer.Option(
|
|
None, "--strategies-dir", "-d", help="Directory containing strategy JSON files",
|
|
),
|
|
):
|
|
"""
|
|
Re-backtest existing strategies with current settings.
|
|
|
|
Options:
|
|
--strategies-dir/-d: Directory with strategy JSON files (default: results/strategies_new/)
|
|
|
|
Examples:
|
|
rdagent rebacktest
|
|
rdagent rebacktest -d results/strategies_new/
|
|
"""
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
project_root = Path(__file__).parent.parent.parent
|
|
script = project_root / "scripts" / "nexquant_rebacktest_strategies.py"
|
|
|
|
if not script.exists():
|
|
typer.echo(f"❌ Script not found: {script}")
|
|
raise typer.Exit(code=1)
|
|
|
|
cmd = [sys.executable, str(script)]
|
|
if strategies_dir:
|
|
cmd.extend(["--strategies-dir", strategies_dir])
|
|
|
|
typer.echo("🔄 Re-backtesting existing strategies...")
|
|
typer.echo(f" Script: {script}")
|
|
|
|
try:
|
|
result = subprocess.run(cmd, cwd=str(project_root))
|
|
raise typer.Exit(code=result.returncode)
|
|
except KeyboardInterrupt:
|
|
typer.echo("\n⚠️ Interrupted by user")
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
@app.command(name="report")
|
|
def report_cli(
|
|
strategy_path: str = typer.Option(
|
|
None, "--strategy", "-s", help="Path to single strategy JSON (default: all strategies)",
|
|
),
|
|
output: str = typer.Option(
|
|
None, "--output", "-o", help="Output directory (default: results/strategy_reports/)",
|
|
),
|
|
):
|
|
"""
|
|
Generate performance reports for strategies.
|
|
|
|
Creates PDF reports with:
|
|
- Equity curve
|
|
- Drawdown chart
|
|
- Signal distribution
|
|
- Monthly returns heatmap
|
|
- Full metrics
|
|
|
|
Options:
|
|
--strategy/-s: Path to single strategy JSON (default: all)
|
|
--output/-o: Output directory (default: results/strategy_reports/)
|
|
|
|
Examples:
|
|
rdagent report # All strategies
|
|
rdagent report -s results/strategies_new/123_MyStrategy.json
|
|
rdagent report -o custom/reports/
|
|
"""
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
project_root = Path(__file__).parent.parent.parent
|
|
script = project_root / "scripts" / "nexquant_strategy_report.py"
|
|
|
|
if not script.exists():
|
|
typer.echo(f"❌ Script not found: {script}")
|
|
raise typer.Exit(code=1)
|
|
|
|
cmd = [sys.executable, str(script)]
|
|
if strategy_path:
|
|
cmd.append(strategy_path)
|
|
if output:
|
|
cmd.extend(["-o", output])
|
|
|
|
typer.echo("📊 Generating strategy reports...")
|
|
typer.echo(f" Script: {script}")
|
|
|
|
try:
|
|
result = subprocess.run(cmd, cwd=str(project_root))
|
|
raise typer.Exit(code=result.returncode)
|
|
except KeyboardInterrupt:
|
|
typer.echo("\n⚠️ Interrupted by user")
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
|
|
@app.command(name="nexquant")
|
|
def nexquant_welcome():
|
|
"""
|
|
Show NexQuant welcome screen with system overview.
|
|
|
|
This command displays a beautiful dashboard showing:
|
|
- System status (factors, strategies, security)
|
|
- Available commands
|
|
- Quick start guide
|
|
|
|
Perfect for GitHub README screenshots!
|
|
|
|
Examples:
|
|
rdagent nexquant
|
|
"""
|
|
from rdagent.app.cli_welcome import show_welcome
|
|
show_welcome()
|