feat: Strategy Generator working with local LLM (P0-P4)

Integrated strategy generation into fin_quant loop:
- Fixed LLM code extraction from JSON responses
- Fixed factor loading (MultiIndex parquet handling)
- Fixed return calculation (realistic proxy)
- Fixed max drawdown (NaN/inf handling)
- Added llama.cpp --reasoning off support
- Strategy orchestrator with LLM + evaluation
- Optuna optimizer integration

100% acceptance rate on test run (2/2 strategies).
Total changes: +2006 lines, -129 lines across 8 files.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
TPTBusiness
2026-04-09 12:55:04 +02:00
parent 21daaf4997
commit 108a63fd79
6 changed files with 688 additions and 126 deletions
+2 -2
View File
@@ -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]")
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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:
+112 -119
View File
@@ -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.'
+536 -2
View File
@@ -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()
+36 -1
View File
@@ -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))