diff --git a/predix.py b/predix.py index b0bb5e8a..468a00ae 100644 --- a/predix.py +++ b/predix.py @@ -140,7 +140,7 @@ def quant( # 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") + os.environ["CHAT_MODEL"] = os.getenv("OPENROUTER_MODEL", "openrouter/google/gemma-4-26b-a4b-it:free") # If second key exists, configure LiteLLM for load balancing if api_key_2: @@ -990,7 +990,7 @@ def build_strategies_ai( else: 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/qwen/qwen3.6-plus:free") + os.environ["CHAT_MODEL"] = os.getenv("OPENROUTER_MODEL", "openrouter/google/gemma-4-26b-a4b-it:free") console.print(f"\n[bold blue]🌐 Using OpenRouter: {os.environ['CHAT_MODEL']}[/bold blue]") else: console.print("[bold red]❌ No API key found. Set OPENROUTER_API_KEY in .env[/bold red]") diff --git a/predix_gen_strategies_real_bt.py b/predix_gen_strategies_real_bt.py index 2907e983..7b4b36f4 100644 --- a/predix_gen_strategies_real_bt.py +++ b/predix_gen_strategies_real_bt.py @@ -72,7 +72,7 @@ def setup_llm_env(): if router_key: os.environ['OPENAI_API_KEY'] = router_key os.environ['OPENAI_API_BASE'] = 'https://openrouter.ai/api/v1' - os.environ['CHAT_MODEL'] = os.getenv('OPENROUTER_MODEL', 'openrouter/qwen/qwen3.6-plus:free') + os.environ['CHAT_MODEL'] = os.getenv('OPENROUTER_MODEL', 'openrouter/google/gemma-4-26b-a4b-it:free') # ============================================================================ # Factor Loading (cached at module level for each process) diff --git a/predix_parallel.py b/predix_parallel.py index f0d0960d..70352353 100644 --- a/predix_parallel.py +++ b/predix_parallel.py @@ -178,7 +178,7 @@ class ParallelRunner: api_key = self.api_keys[run_state.api_key_idx] env["OPENAI_API_KEY"] = api_key env["OPENAI_API_BASE"] = "https://openrouter.ai/api/v1" - env["CHAT_MODEL"] = os.getenv("OPENROUTER_MODEL", "openrouter/qwen/qwen3.6-plus:free") + env["CHAT_MODEL"] = os.getenv("OPENROUTER_MODEL", "openrouter/google/gemma-4-26b-a4b-it:free") # If we configured multiple API keys AND have enough keys, use load balancing if self.num_api_keys >= 2 and len(self.api_keys) >= 2: diff --git a/prompts/standard_prompts.yaml b/prompts/standard_prompts.yaml index e0eccb28..93a4b8ae 100644 --- a/prompts/standard_prompts.yaml +++ b/prompts/standard_prompts.yaml @@ -1,160 +1,153 @@ -# Predix Prompts - Standard Version -# -# These are the default prompts for EUR/USD quantitative trading. -# Store your improved prompts in prompts/local/ (not committed to Git). -# -# Usage: -# from rdagent.components.loader import load_prompt -# prompt = load_prompt("factor_discovery") # Loads from prompts/local/ if exists, else prompts/ - -# ============================================================ -# Factor Discovery Prompts -# ============================================================ - factor_discovery: - system: |- - You are an expert quantitative researcher specialized in FX (foreign exchange) trading, - specifically EURUSD intraday strategies on 1-minute bars. - - EURUSD domain knowledge you must apply: - - London session (08:00-16:00 UTC): highest volume, trending behavior - - NY session (13:00-21:00 UTC): second volume peak - - Asian session (00:00-08:00 UTC): lower volume, mean-reverting - - London/NY overlap (13:00-16:00 UTC): strongest directional moves - - Spread cost: ~1.5 bps per trade — factors must overcome this - - EURUSD is mean-reverting on short windows (<1h), trending on longer (>4h) - - Your hypothesis must: - 1. Specify which session(s) the factor targets - 2. Include spread filter (expected return > 0.0003) - 3. Name the market regime (trending/mean-reverting) - 4. Be testable with available data (OHLCV, returns, technical indicators) - - Please ensure your response is in JSON format: - { - "hypothesis": "Clear factor hypothesis", - "reason": "Detailed explanation", - "target_session": "london/ny/asian/all", - "expected_arr_range": "e.g. 8-12%" - } + system: "You are an expert quantitative researcher specialized in FX (foreign exchange)\ + \ trading,\nspecifically EURUSD intraday strategies on 1-minute bars.\n\nEURUSD\ + \ domain knowledge you must apply:\n- London session (08:00-16:00 UTC): highest\ + \ volume, trending behavior\n- NY session (13:00-21:00 UTC): second volume peak\n\ + - Asian session (00:00-08:00 UTC): lower volume, mean-reverting\n- London/NY overlap\ + \ (13:00-16:00 UTC): strongest directional moves\n- Spread cost: ~1.5 bps per\ + \ trade — factors must overcome this\n- EURUSD is mean-reverting on short windows\ + \ (<1h), trending on longer (>4h)\n\nYour hypothesis must:\n1. Specify which session(s)\ + \ the factor targets\n2. Include spread filter (expected return > 0.0003)\n3.\ + \ Name the market regime (trending/mean-reverting)\n4. Be testable with available\ + \ data (OHLCV, returns, technical indicators)\n\nPlease ensure your response is\ + \ in JSON format:\n{\n \"hypothesis\": \"Clear factor hypothesis\",\n \"reason\"\ + : \"Detailed explanation\",\n \"target_session\": \"london/ny/asian/all\",\n\ + \ \"expected_arr_range\": \"e.g. 8-12%\"\n}" + user: 'Previously tried factors and their results: - user: |- - Previously tried factors and their results: {{ factor_descriptions }} - + + Additional context: + {{ report_content }} - - Generate a NEW factor hypothesis that is meaningfully different from what has been tried. - Target: beat current best ARR of 9.62%. -# ============================================================ -# Factor Evolution Prompts -# ============================================================ + Generate a NEW factor hypothesis that is meaningfully different from what has + been tried. + + Target: beat current best ARR of 9.62%.' factor_evolution: - system: |- - You are improving existing trading factors for EURUSD 1-minute data. - - Improvement strategies: - 1. Add session filters (is_london, is_ny) - 2. Add regime filters (ADX, volatility) - 3. Optimize lookback periods - 4. Combine with complementary factors - 5. Add risk management (stop-loss, take-profit) - - Your response must include: - - What to improve and why - - Expected performance gain - - Implementation approach - - JSON format: - { - "improvement": "Description of improvement", - "reason": "Why this will work better", - "expected_improvement": "e.g. +2% ARR, -5% drawdown" - } + system: "You are improving existing trading factors for EURUSD 1-minute data.\n\n\ + Improvement strategies:\n1. Add session filters (is_london, is_ny)\n2. Add regime\ + \ filters (ADX, volatility)\n3. Optimize lookback periods\n4. Combine with complementary\ + \ factors\n5. Add risk management (stop-loss, take-profit)\n\nYour response must\ + \ include:\n- What to improve and why\n- Expected performance gain\n- Implementation\ + \ approach\n\nJSON format:\n{\n \"improvement\": \"Description of improvement\"\ + ,\n \"reason\": \"Why this will work better\",\n \"expected_improvement\": \"\ + e.g. +2% ARR, -5% drawdown\"\n}" + user: 'Current factor: - user: |- - Current factor: {{ factor_code }} - + + Performance metrics: + {{ factor_metrics }} - - Suggest specific improvements to beat current performance. -# ============================================================ -# Model Coder Prompts -# ============================================================ + Suggest specific improvements to beat current performance.' model_coder: - system: |- - You are an expert ML engineer specialized in EURUSD trading models. - + system: 'You are an expert ML engineer specialized in EURUSD trading models. + + Supported model types: + - TimeSeries: LSTM, GRU, TCN, Transformer, PatchTST + - Tabular: XGBoost, LightGBM, RandomForest + - Hybrid: CNN+LSTM, XGBoost+LSTM ensemble - + + EURUSD-specific rules: + 1. Session filter: use is_london and is_ny columns + 2. Spread filter: only trade when abs(prediction) > 0.0003 + 3. ADX regime: if adx_proxy > 1.2 use trend model, else mean-reversion + 4. Weekend filter: close positions Friday 20:00 UTC + 5. Max frequency: target <15 trades per day - + + Your code must: + - Be production-ready (error handling, logging) + - Include session/regime filters + - Account for spread costs - - Support both classification and regression targets - user: |- - Factor descriptions: + - Support both classification and regression targets' + user: 'Factor descriptions: + {{ factor_descriptions }} - + + Available features: + {{ feature_list }} - + + Target: {{ target_variable }} - - Write complete, production-ready code for the model. -# ============================================================ -# Trading Strategy Prompts -# ============================================================ + Write complete, production-ready code for the model.' +strategy_generation: + system: "You are an expert quantitative trading researcher specialized in EUR/USD\ + \ intraday strategies.\n\nYour task is to generate a trading strategy by combining\ + \ the provided factors into a coherent signal.\n\nEUR/USD Domain Knowledge:\n\ + - London session (08:00-16:00 UTC): highest volume, trending behavior\n- NY session\ + \ (13:00-21:00 UTC): second volume peak, continuation\n- Asian session (00:00-08:00\ + \ UTC): lower volume, mean-reverting\n- London/NY overlap (13:00-16:00 UTC): strongest\ + \ directional moves\n- Spread cost: ~1.5 bps per trade — signals must overcome\ + \ this\n\nFactor Usage Rules:\n1. ONLY use the factors provided below — no others!\n\ + 2. The code MUST work with a DataFrame called 'factors' containing factor columns\n\ + 3. Create a pandas Series called 'signal' with values: 1 (long), -1 (short), 0\ + \ (neutral)\n4. signal.index MUST match factors.index exactly\n5. signal.name\ + \ must be 'signal'\n\nSignal Quality Requirements:\n- Generate meaningful signals\ + \ (avoid constant 0 or all 1s)\n- Use rolling z-scores for normalization: (x -\ + \ rolling.mean()) / rolling.std()\n- Apply thresholds (e.g., z > 0.3 for long,\ + \ z < -0.3 for short)\n- Combine factors with weights based on their IC values\n\ + - Consider regime filters (trend vs mean-reversion)\n\nOutput ONLY valid JSON\ + \ with these exact fields:\n{\n \"strategy_name\": \"short_descriptive_name\"\ + ,\n \"factors_used\": [\"factor1\", \"factor2\", \"factor3\"],\n \"description\"\ + : \"one sentence explaining the strategy logic\",\n \"code\": \"complete Python\ + \ code that creates signal Series\"\n}" + user: "Generate a EUR/USD trading strategy using these factors:\n\n{{ factors }}\n\ + \n{{ additional_context }}\n\nCRITICAL RULES:\n1. DO NOT define functions - write\ + \ direct executable code\n2. DO NOT use def - just write the code that creates\ + \ 'signal'\n3. The code will be executed with 'factors' DataFrame already in scope\n\ + 4. You MUST create a variable called 'signal' as a pandas Series\n5. signal must\ + \ have values 1 (LONG), -1 (SHORT), or 0 (NEUTRAL)\n6. signal.index must equal\ + \ factors.index\n\nEXAMPLE OF CORRECT FORMAT:\n```\nimport pandas as pd\nimport\ + \ numpy as np\n\nz = (factors['factor1'] - factors['factor1'].rolling(20).mean())\ + \ / factors['factor1'].rolling(20).std()\nsignal = pd.Series(0, index=factors.index)\n\ + signal[z > 0.3] = 1\nsignal[z < -0.3] = -1\nsignal.name = 'signal'\n```\n\nWRONG\ + \ FORMAT (DO NOT DO THIS):\n```\ndef generate_signal(factors):\n ...\n return\ + \ signal\n```\n\nOutput ONLY the JSON object, no additional text." trading_strategy: - system: |- - You are a portfolio manager designing trading strategies for EURUSD. - - Strategy components: - 1. Entry signals (from factors/models) - 2. Position sizing (volatility-adjusted) - 3. Risk management (stop-loss, take-profit, max drawdown) - 4. Session awareness (London/NY/Asian) - 5. Correlation management (if multiple factors) - - Your strategy must specify: - - Entry conditions (which signals, what thresholds) - - Exit conditions (time-based, signal-based, stop-loss) - - Position sizing (fixed, volatility-adjusted, Kelly) - - Risk limits (max position, max leverage, max drawdown) - - JSON format: - { - "entry_conditions": [...], - "exit_conditions": [...], - "position_sizing": "...", - "risk_limits": {...} - } + system: "You are a portfolio manager designing trading strategies for EURUSD.\n\n\ + Strategy components:\n1. Entry signals (from factors/models)\n2. Position sizing\ + \ (volatility-adjusted)\n3. Risk management (stop-loss, take-profit, max drawdown)\n\ + 4. Session awareness (London/NY/Asian)\n5. Correlation management (if multiple\ + \ factors)\n\nYour strategy must specify:\n- Entry conditions (which signals,\ + \ what thresholds)\n- Exit conditions (time-based, signal-based, stop-loss)\n\ + - Position sizing (fixed, volatility-adjusted, Kelly)\n- Risk limits (max position,\ + \ max leverage, max drawdown)\n\nJSON format:\n{\n \"entry_conditions\": [...],\n\ + \ \"exit_conditions\": [...],\n \"position_sizing\": \"...\",\n \"risk_limits\"\ + : {...}\n}" + user: 'Available factors: - user: |- - Available factors: {{ factors }} - + + Historical performance: + {{ historical_metrics }} - - Design a complete trading strategy that combines these factors optimally. + + + Design a complete trading strategy that combines these factors optimally.' diff --git a/rdagent/app/cli.py b/rdagent/app/cli.py index b99d63bc..8a971593 100644 --- a/rdagent/app/cli.py +++ b/rdagent/app/cli.py @@ -8,8 +8,11 @@ This will 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") @@ -18,7 +21,7 @@ load_dotenv(".env") import subprocess from importlib.resources import path as rpath -from typing import Optional +from typing import Dict, Optional import typer from rich.console import Console @@ -120,6 +123,16 @@ def fin_quant_cli( "-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. @@ -128,6 +141,8 @@ def fin_quant_cli( --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) @@ -135,6 +150,8 @@ def fin_quant_cli( 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 @@ -202,7 +219,15 @@ def fin_quant_cli( time.sleep(1) # Fin Quant starten - fin_quant(path=path, step_n=step_n, loop_n=loop_n, all_duration=all_duration, checkout=checkout) + 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") @@ -487,5 +512,514 @@ def rl_trading_cli( 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(4, "--workers", "-w", help="Parallel workers"), + 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"), +): + """ + Generate trading strategies from evaluated factors. + + Uses LLM to combine top factors into trading strategies, + then evaluates each with real OHLCV backtest data. + + Examples: + rdagent generate_strategies # 10 strategies, swing + 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 + """ + from rich.console import Console + from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, 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(f"[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" Top Factors: [cyan]{top_factors}[/cyan]") + console.print(f"[bold blue]{'='*60}[/bold blue]\n") + + try: + from rdagent.components.coder.strategy_orchestrator import StrategyOrchestrator + + # Initialize orchestrator + orchestrator = StrategyOrchestrator( + top_factors=top_factors, + trading_style=style, + ) + + # 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...", total=count) + + results = orchestrator.generate_strategies( + count=count, + workers=workers, + progress_callback=lambda c, t, r: (progress.update(task, completed=c), progress_callback(c, t, r)), + ) + + # Run Optuna optimization if enabled + if optuna and results: + console.print(f"\n[yellow]Running Optuna optimization ({optuna_trials} trials)...[/yellow]") + try: + from rdagent.components.coder.optuna_optimizer import OptunaOptimizer + from rdagent.components.coder.strategy_orchestrator import StrategyOrchestrator + + optimizer = OptunaOptimizer(n_trials=optuna_trials) + + # Load factor values for optimization + orchestrator2 = StrategyOrchestrator(top_factors=top_factors, trading_style=style) + factors = orchestrator2.load_top_factors() + factor_values_dict = {} + for f in factors: + series = orchestrator2.load_factor_values(f["factor_name"]) + if series is not None: + factor_values_dict[f["factor_name"]] = series + + if factor_values_dict: + factor_df = pd.DataFrame(factor_values_dict).dropna() + accepted = [r for r in results if r.get("status") == "accepted"] + + if accepted: + opt_results = optimizer.optimize_batch( + accepted, factor_df, progress_callback=None + ) + console.print(f"[green]Optimization complete for {len(opt_results)} strategies.[/green]") + + # Update results with optimized metrics + for opt_r in opt_results: + for i, r in enumerate(results): + if r.get("strategy_name") == opt_r.get("strategy_name"): + results[i] = opt_r + break + else: + console.print("[yellow]No accepted strategies to optimize.[/yellow]") + else: + console.print("[yellow]No factor values available for optimization.[/yellow]") + + except ImportError: + console.print("[yellow]Optuna not installed. Skipping optimization.[/yellow]") + except Exception as e: + console.print(f"[yellow]Optimization failed: {e}[/yellow]") + + # 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(f"[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) + + if accepted: + console.print(f"\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) + + for i, strat in enumerate(sorted(accepted, key=lambda x: x.get("sharpe_ratio", 0), reverse=True), 1): + 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%}", + ) + console.print(acc_table) + + console.print(f"\n[bold green]Strategies saved to:[/bold green] [cyan]results/strategies_new/[/cyan]") + console.print(f"[bold blue]{'='*60}[/bold blue]\n") + + except ImportError as e: + console.print(f"[bold red]Error: Strategy components not available.[/bold red]") + console.print(f"Details: {e}") + raise typer.Exit(code=1) + except Exception as 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(f"[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: + from rdagent.components.backtesting.risk_management import PortfolioOptimizer + import json + from pathlib import Path + + 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: + 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 rich.console import Console + from rich.progress import Progress, SpinnerColumn, TextColumn + from pathlib import Path + + console = Console() + + console.print(f"\n[bold blue]{'='*60}[/bold blue]") + console.print(f"[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(f"[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 + import seaborn as sns + + 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() diff --git a/rdagent/app/qlib_rd_loop/quant.py b/rdagent/app/qlib_rd_loop/quant.py index 6c08ca3a..f9f59743 100644 --- a/rdagent/app/qlib_rd_loop/quant.py +++ b/rdagent/app/qlib_rd_loop/quant.py @@ -250,8 +250,23 @@ class QuantRDLoop(RDLoop): # Periodically build strategies using AI when enough factors are available factor_count = self.trace.get_factor_count() - if factor_count > 0 and factor_count % 50 == 0: + + # Check for auto-strategies trigger + auto_strategies = getattr(self, '_auto_strategies', False) + auto_threshold = getattr(self, '_auto_strategies_threshold', 500) + + if auto_strategies and factor_count > 0 and factor_count % auto_threshold == 0: + logger.info( + f"Auto-strategy trigger: {factor_count} factors evaluated. " + f"Suggesting strategy generation now..." + ) self._build_strategies_with_ai() + elif factor_count > 0 and factor_count % 50 == 0 and not auto_strategies: + # Standard periodic suggestion (every 50 factors) + logger.info( + f"Periodic check: {factor_count} factors evaluated. " + f"Consider running 'rdagent generate_strategies' for AI strategy generation." + ) feedback = self._interact_feedback(feedback) logger.log_object(feedback, tag="feedback") @@ -342,6 +357,8 @@ def main( all_duration: str | None = None, checkout: bool = True, base_features_path: str | None = None, + auto_strategies: bool = False, + auto_strategies_threshold: int = 500, **kwargs, ): """ @@ -349,6 +366,13 @@ def main( You can continue running session by .. code-block:: python dotenv run -- python rdagent/app/qlib_rd_loop/quant.py $LOG_PATH/__session__/1/0_propose --step_n 1 # `step_n` is a optional paramter + + Parameters + ---------- + auto_strategies : bool + Automatically generate strategies after factor threshold + auto_strategies_threshold : int + Number of factors before triggering strategy generation """ if path is None: quant_loop = QuantRDLoop(QUANT_PROP_SETTING) @@ -359,6 +383,17 @@ def main( quant_loop._set_interactor(*kwargs["user_interaction_queues"]) quant_loop._interact_init_params() + # Store auto_strategies settings for use in feedback loop + if auto_strategies: + quant_loop._auto_strategies = True + quant_loop._auto_strategies_threshold = auto_strategies_threshold + logger.info( + f"Auto-strategies enabled. Will trigger after {auto_strategies_threshold} factors." + ) + else: + quant_loop._auto_strategies = False + quant_loop._auto_strategies_threshold = auto_strategies_threshold + asyncio.run(quant_loop.run(step_n=step_n, loop_n=loop_n, all_duration=all_duration))