Files
NexQuant/predix.py
T
TPTBusiness 8b7eb87546 feat: Add parallel run system with API key distribution
- Add predix_parallel.py: Run multiple factor experiments concurrently
  * python predix_parallel.py --runs 5 --api-keys 2 -m openrouter
  * Round-robin API key distribution across available keys
  * Rich live dashboard with per-run status, elapsed time, exit codes
  * Graceful shutdown (Ctrl+C kills all children cleanly)

- Add --run-id parameter to predix.py for isolated single runs
  * Separate log files: fin_quant_run{N}.log
  * Separate results: results/runs/run{N}/
  * Separate workspace: RD-Agent_workspace_run{N}/
  * Separate databases per run

- Modify CoSTEER and FactorRunner for PARALLEL_RUN_ID isolation
  * _save_intermediate_results uses run-specific directories
  * _save_result_to_database and _write_run_log isolated per run
  * _ensure_results_dirs creates run-specific paths

- Reduce max_loop from 10 to 3 for faster iterations
- Add docs/parallel_runs.md with full documentation

Tests: 103 passed
2026-04-04 09:39:12 +02:00

238 lines
8.2 KiB
Python

#!/usr/bin/env python
"""
Predix CLI - Wrapper for rdagent with LLM model selection.
Usage:
predix quant # Local llama.cpp (default)
predix quant --model local # Explicit local
predix quant --model openrouter # OpenRouter cloud model
predix quant -d # With web dashboard
"""
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent / ".env")
import typer
from rich.console import Console
app = typer.Typer(help="Predix - AI Quantitative Trading Agent")
console = Console()
@app.command()
def quant(
model: str = typer.Option(
"local",
"--model", "-m",
help="LLM backend: 'local' (llama.cpp) or 'openrouter' (cloud)",
),
dashboard: bool = typer.Option(
False,
"--dashboard/-d",
help="Start web dashboard",
),
cli_dashboard: bool = typer.Option(
False,
"--cli-dashboard/-c",
help="Start CLI dashboard",
),
log_file: str = typer.Option(
None, # None means auto-detect based on run_id
"--log-file",
help="Log file path (default: auto-detected). Use 'none' to disable.",
),
step_n: int = typer.Option(None, help="Number of steps to run"),
loop_n: int = typer.Option(None, help="Number of loops to run"),
run_id: int = typer.Option(
0,
"--run-id",
help="Parallel run ID (for isolated results). 0 = single run mode.",
),
):
"""
Start EURUSD quantitative trading loop.
Examples:
predix quant # Local llama.cpp
predix quant -m openrouter # OpenRouter cloud model
predix quant -d # With web dashboard
predix quant -m openrouter -d # Both
predix quant --run-id 1 # Parallel run #1 (isolated)
"""
import subprocess
import threading
import time
import sys
# ---- Parallel Run Isolation ----
# When run_id > 0, isolate all outputs (logs, results, workspace)
if run_id > 0:
os.environ["PARALLEL_RUN_ID"] = str(run_id)
console.print(f"\n[bold yellow]🔀 Parallel Run Mode:[/bold yellow] [cyan]ID={run_id}[/cyan]")
# Auto-detect log file for parallel run
if log_file is None:
log_file = f"fin_quant_run{run_id}.log"
# Isolate results directories
results_base = Path(__file__).parent / "results" / "runs" / f"run{run_id}"
results_base.mkdir(parents=True, exist_ok=True)
# Isolate workspace directory
workspace_dir = Path(__file__).parent / f"RD-Agent_workspace_run{run_id}"
os.environ["RD_AGENT_WORKSPACE"] = str(workspace_dir)
console.print(f" [dim]Log: {log_file}[/dim]")
console.print(f" [dim]Results: results/runs/run{run_id}/[/dim]")
console.print(f" [dim]Workspace: {workspace_dir.name}/[/dim]")
else:
# Single run mode: default log file
if log_file is None:
log_file = "fin_quant.log"
# ---- Log File Setup ----
if log_file.lower() != "none":
log_path = Path(__file__).parent / log_file
log_path.parent.mkdir(parents=True, exist_ok=True)
# Open log file for appending
log_f = open(log_path, "a", encoding="utf-8")
# Redirect stdout and stderr to both console and log file
class TeeWriter:
def __init__(self, *streams):
self._streams = streams
def write(self, data):
for s in self._streams:
try:
s.write(data)
s.flush()
except:
pass
def flush(self):
for s in self._streams:
try:
s.flush()
except:
pass
sys.stdout = TeeWriter(sys.__stdout__, log_f)
sys.stderr = TeeWriter(sys.__stderr__, log_f)
console.print(f"\n[dim]📝 Logging to: {log_path}[/dim]")
else:
console.print("\n[dim]⚠️ Logging disabled (console only)[/dim]")
# ---- LLM Model Selection ----
if model == "openrouter":
api_key = os.getenv("OPENROUTER_API_KEY", "")
api_key_2 = os.getenv("OPENROUTER_API_KEY_2", "")
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:[/yellow]")
console.print(' OPENROUTER_API_KEY=sk-or-your-key-here')
raise typer.Exit(code=1)
# Setup both API keys for load balancing
os.environ["OPENAI_API_BASE"] = "https://openrouter.ai/api/v1"
os.environ["CHAT_MODEL"] = os.getenv("OPENROUTER_MODEL", "openrouter/qwen/qwen3.6-plus:free")
# If second key exists, configure LiteLLM for load balancing
if api_key_2:
os.environ["OPENAI_API_KEY"] = f"{api_key},{api_key_2}"
os.environ["LITELLM_PARALLEL_CALLS"] = "2"
console.print(f"\n[bold blue]🌐 Using OpenRouter (2 API Keys):[/bold blue] [cyan]{os.environ['CHAT_MODEL']}[/cyan]")
console.print(f" [dim]Keys: {api_key[:15]}*** + {api_key_2[:15]}***[/dim]")
console.print(f" [dim]Parallel: 2 concurrent requests[/dim]")
else:
os.environ["OPENAI_API_KEY"] = api_key
console.print(f"\n[bold blue]🌐 Using OpenRouter:[/bold blue] [cyan]{os.environ['CHAT_MODEL']}[/cyan]")
console.print(f" [dim]Key: {api_key[:15]}***[/dim]")
elif model == "local":
os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "local")
os.environ["OPENAI_API_BASE"] = os.getenv("OPENAI_API_BASE", "http://localhost:8081/v1")
os.environ["CHAT_MODEL"] = os.getenv("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: {os.environ['OPENAI_API_BASE']}[/dim]")
else:
console.print(f"\n[yellow]⚠️ Unknown model: '{model}'. Using .env settings.[/yellow]")
# ---- Dashboards ----
if dashboard:
def start_web_dashboard():
console.print(f"\n[bold green]🚀 Web Dashboard: http://localhost:5000[/bold green]")
subprocess.run(
["python", "web/dashboard_api.py"],
cwd=str(Path(__file__).parent),
env={**os.environ, "FLASK_ENV": "development"},
)
threading.Thread(target=start_web_dashboard, daemon=True).start()
time.sleep(2)
if cli_dashboard:
def start_cli_dash():
from rdagent.log.ui.predix_dashboard import run_dashboard
run_dashboard(log_path="fin_quant.log", refresh_interval=3)
threading.Thread(target=start_cli_dash, daemon=True).start()
time.sleep(1)
# ---- Start fin_quant ----
from rdagent.app.qlib_rd_loop.quant import main as fin_quant
console.print(f"\n[bold cyan]📊 Starting EURUSD Trading Loop...[/bold cyan]\n")
fin_quant(
step_n=step_n,
loop_n=loop_n,
)
@app.command()
def health():
"""Check system health and configuration."""
from rdagent.app.utils.health_check import health_check
health_check()
@app.command()
def status():
"""Show current trading loop status."""
import sqlite3
# Process check
result = subprocess.run(
["pgrep", "-f", "fin_quant"],
capture_output=True, text=True
)
if result.returncode == 0:
console.print("[bold green]✅ Trading Loop: RUNNING[/bold green]")
else:
console.print("[bold yellow]⏸️ Trading Loop: STOPPED[/bold yellow]")
# DB stats
db_path = Path(__file__).parent / "results" / "db" / "backtest_results.db"
if db_path.exists():
conn = sqlite3.connect(str(db_path))
c = conn.cursor()
c.execute("SELECT COUNT(*) FROM backtest_runs")
runs = c.fetchone()[0]
c.execute("SELECT COUNT(*) FROM factors")
factors = c.fetchone()[0]
conn.close()
console.print(f"\n📊 Results:")
console.print(f" Backtest runs: {runs}")
console.print(f" Factors: {factors}")
if __name__ == "__main__":
app()