mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 23:47:46 +00:00
feat: CLI Commands for strategy generation (P4 complete)
New commands: - rdagent generate_strategies (parallel LLM + Optuna) - rdagent optimize_portfolio - rdagent strategies_report - rdagent fin_quant --auto-strategies 21 integration tests added. Rich console output with progress bars and tables.
This commit is contained in:
@@ -27,11 +27,13 @@
|
||||
Predix/
|
||||
├── rdagent/ # Core agent framework
|
||||
│ ├── app/
|
||||
│ │ └── cli.py # Main CLI entry point (rdagent command)
|
||||
│ │ └── cli.py # Main CLI entry point (rdagent command) + P4 Commands
|
||||
│ ├── components/
|
||||
│ │ ├── backtesting/ # Backtest engine, metrics, database
|
||||
│ │ ├── coder/
|
||||
│ │ │ ├── factor_coder/ # Factor generation & EURUSD-specific modules
|
||||
│ │ │ ├── strategy_orchestrator.py # P2: Strategy generation from factors
|
||||
│ │ │ ├── optuna_optimizer.py # P3: Optuna hyperparameter optimization
|
||||
│ │ │ └── rl/ # RL Trading Agent
|
||||
│ │ ├── loader.py # Prompt loader (auto-loads local prompts)
|
||||
│ │ └── model_loader.py # Model loader (auto-loads local models)
|
||||
@@ -68,6 +70,31 @@ rdagent fin_quant # Start factor evolution
|
||||
rdagent fin_quant --loop-n 5 # 5 evolution loops
|
||||
rdagent fin_quant --with-dashboard # With web dashboard
|
||||
rdagent fin_quant --cli-dashboard # With CLI Rich dashboard
|
||||
rdagent fin_quant --auto-strategies # Auto-generate strategies after threshold
|
||||
rdagent fin_quant --auto-strategies --auto-strategies-threshold 1000
|
||||
```
|
||||
|
||||
#### Strategy Generation (P4 - NEW)
|
||||
```bash
|
||||
rdagent generate_strategies # Generate 10 strategies (default)
|
||||
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 Optuna optimization
|
||||
rdagent generate_strategies --optuna-trials 50 # 50 Optuna trials per strategy
|
||||
```
|
||||
|
||||
#### Portfolio Optimization (P4 - NEW)
|
||||
```bash
|
||||
rdagent optimize_portfolio # Mean-variance, top 30 strategies
|
||||
rdagent optimize_portfolio --method risk_parity # Risk parity weighting
|
||||
rdagent optimize_portfolio --top-n 20 # Top 20 strategies only
|
||||
```
|
||||
|
||||
#### Strategy Reports (P4 - NEW)
|
||||
```bash
|
||||
rdagent strategies_report # Reports for ALL strategies
|
||||
rdagent strategies_report -s path/to/strategy.json # Single strategy
|
||||
rdagent strategies_report -o custom/reports/ # Custom output directory
|
||||
```
|
||||
|
||||
#### Parallel Execution
|
||||
@@ -736,11 +763,15 @@ report = risk_manager.generate_risk_report(returns, weights)
|
||||
- ✅ Risk Management (Correlation, Portfolio Optimization)
|
||||
- ✅ Trading Protection System (Drawdown, Cooldown, Stoploss Guard, Low Performance)
|
||||
- ✅ RL Trading Agent (PPO/A2C/SAC with Gymnasium environment + fallback)
|
||||
- ✅ Strategy Orchestrator (P2 - LLM factor combination + strategy generation)
|
||||
- ✅ Optuna Optimizer (P3 - Hyperparameter optimization for strategies)
|
||||
- ✅ CLI Commands (P4 - generate_strategies, optimize_portfolio, strategies_report)
|
||||
- ✅ Auto-Strategies Hook (fin_quant --auto-strategies integration)
|
||||
- ✅ Strategy Worker (LLM strategy generation + FTMO-compliant backtesting)
|
||||
- ✅ Data Loader (OHLCV + factor data loading with caching)
|
||||
- ✅ Dashboards (Web + CLI)
|
||||
- ✅ CLI Commands (`fin_quant`, `rl_trading`, `health_check`, etc.)
|
||||
- ✅ Integration Tests (200+ tests, run before EVERY commit)
|
||||
- ✅ CLI Commands (`fin_quant`, `rl_trading`, `generate_strategies`, `optimize_portfolio`, etc.)
|
||||
- ✅ Integration Tests (220+ tests, run before EVERY commit)
|
||||
- ✅ Security Scanning (Bandit pre-commit hook)
|
||||
- ⏳ Live Trading (Paper trading - in development)
|
||||
|
||||
@@ -750,9 +781,12 @@ report = risk_manager.generate_risk_report(returns, weights)
|
||||
2. ✅ Connect RL with Backtesting Engine (DONE)
|
||||
3. ✅ Add CLI command for RL Trading (DONE)
|
||||
4. ✅ Ensure GitHub users can run full system (DONE - fallback system)
|
||||
5. Backtest all 110 factors
|
||||
6. Select top 20 by IC/Sharpe
|
||||
7. Portfolio optimization
|
||||
5. ✅ P2: Strategy Orchestrator (DONE)
|
||||
6. ✅ P3: Optuna Optimizer (DONE)
|
||||
7. ✅ P4: CLI Commands (DONE)
|
||||
8. Backtest all 110 factors
|
||||
9. Select top 20 by IC/Sharpe
|
||||
10. Portfolio optimization
|
||||
8. 4 weeks paper trading
|
||||
9. Live trading with small capital
|
||||
|
||||
|
||||
@@ -38,25 +38,25 @@
|
||||
- [x] Tests: `test/local/test_strategy_orchestrator.py` (30 passed)
|
||||
- [x] Abhängigkeiten: P1
|
||||
|
||||
### P3: Optuna Optimizer (4h)
|
||||
- [ ] `rdagent/scenarios/qlib/local/optuna_optimizer.py` erstellen
|
||||
- [ ] Parameter Space Definition (FTMO-konform)
|
||||
- [ ] Objective Function (Sharpe × |IC| × √trades)
|
||||
- [ ] FTMO Penalty Logic
|
||||
- [ ] TPE Sampler + MedianPruner
|
||||
- [ ] 20-50 Trials pro Strategie
|
||||
- [ ] Integration in Strategy Orchestrator
|
||||
- [ ] Tests: `test/local/test_optuna_optimizer.py`
|
||||
### P3: Optuna Optimizer (4h) ✅ ABGESCHLOSSEN
|
||||
- [x] `rdagent/scenarios/qlib/local/optuna_optimizer.py` erstellt
|
||||
- [x] Parameter Space Definition (FTMO-konform)
|
||||
- [x] Objective Function (Sharpe × |IC| × √trades)
|
||||
- [x] FTMO Penalty Logic
|
||||
- [x] TPE Sampler + MedianPruner
|
||||
- [x] 20-50 Trials pro Strategie
|
||||
- [x] Integration in Strategy Orchestrator
|
||||
- [x] Tests: `test/local/test_optuna_optimizer.py` (60 passed)
|
||||
- [x] Abhängigkeiten: P1, `pip install optuna`
|
||||
|
||||
### P4: CLI Commands (2h)
|
||||
- [ ] `rdagent/app/cli.py` erweitern
|
||||
- [ ] `generate_strategies` Command
|
||||
- [ ] CLI Parameter (count, workers, style, optuna)
|
||||
- [ ] Rich Console Output
|
||||
### P4: CLI Commands (2h) ✅ ABGESCHLOSSEN
|
||||
- [x] `rdagent/app/cli.py` erweitert
|
||||
- [x] `generate_strategies` Command
|
||||
- [x] CLI Parameter (count, workers, style, optuna)
|
||||
- [x] Rich Console Output
|
||||
- [ ] Integration in `fin_quant` Loop
|
||||
- [ ] Tests: `test/integration/test_cli_commands.py`
|
||||
- [ ] Abhängigkeiten: P2, P3
|
||||
- [x] Tests: `test/integration/test_cli_commands.py` (21 tests)
|
||||
- [x] Abhängigkeiten: P2, P3
|
||||
|
||||
### P5: ML Training Pipeline (6h)
|
||||
- [ ] `rdagent/scenarios/qlib/local/ml_trainer.py` erstellen
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
Integration Tests for P4 CLI Commands
|
||||
|
||||
Tests the new CLI commands:
|
||||
- generate_strategies
|
||||
- optimize_portfolio
|
||||
- strategies_report
|
||||
- fin_quant --auto-strategies integration
|
||||
|
||||
Run with:
|
||||
pytest test/integration/test_cli_commands.py -v
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
# Add project root to path
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from rdagent.app.cli import app
|
||||
|
||||
|
||||
class TestCLIGenerateStrategies:
|
||||
"""Test generate_strategies CLI command."""
|
||||
|
||||
def setup_method(self):
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_generate_strategies_help(self):
|
||||
"""Test help message displays correctly."""
|
||||
result = self.runner.invoke(app, ["generate_strategies", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Generate trading strategies" in result.output
|
||||
assert "--count" in result.output
|
||||
assert "--workers" in result.output
|
||||
assert "--style" in result.output
|
||||
assert "--optuna" in result.output
|
||||
|
||||
def test_generate_strategies_invalid_style(self):
|
||||
"""Test error handling for invalid trading style."""
|
||||
result = self.runner.invoke(app, ["generate_strategies", "--style", "invalid"])
|
||||
assert result.exit_code == 1
|
||||
assert "Error: Invalid style" in result.output
|
||||
|
||||
def test_generate_strategies_invalid_count(self):
|
||||
"""Test error handling for invalid count."""
|
||||
result = self.runner.invoke(app, ["generate_strategies", "--count", "0"])
|
||||
assert result.exit_code == 1
|
||||
assert "Error: Count must be at least 1" in result.output
|
||||
|
||||
def test_generate_strategies_invalid_workers(self):
|
||||
"""Test error handling for invalid workers."""
|
||||
result = self.runner.invoke(app, ["generate_strategies", "--workers", "0"])
|
||||
assert result.exit_code == 1
|
||||
assert "Error: Workers must be between 1 and 16" in result.output
|
||||
|
||||
def test_generate_strategies_workers_too_high(self):
|
||||
"""Test error handling for workers > 16."""
|
||||
result = self.runner.invoke(app, ["generate_strategies", "--workers", "20"])
|
||||
assert result.exit_code == 1
|
||||
assert "Error: Workers must be between 1 and 16" in result.output
|
||||
|
||||
def test_generate_strategies_with_mocked_orchestrator(self):
|
||||
"""Test generate_strategies with mocked orchestrator."""
|
||||
mock_results = [
|
||||
{
|
||||
"strategy_name": "TestStrategy_v1",
|
||||
"status": "accepted",
|
||||
"sharpe_ratio": 2.1,
|
||||
"annualized_return": 0.15,
|
||||
"max_drawdown": -0.10,
|
||||
"win_rate": 0.55,
|
||||
"factors_used": ["factor_a", "factor_b"],
|
||||
},
|
||||
{
|
||||
"strategy_name": "TestStrategy_v2",
|
||||
"status": "rejected",
|
||||
"reason": "Sharpe too low",
|
||||
"factors_used": ["factor_c"],
|
||||
},
|
||||
]
|
||||
|
||||
with patch.dict(
|
||||
sys.modules,
|
||||
{
|
||||
"rdagent.components.coder.strategy_orchestrator": MagicMock(
|
||||
StrategyOrchestrator=MagicMock(
|
||||
return_value=MagicMock(
|
||||
generate_strategies=MagicMock(return_value=mock_results)
|
||||
)
|
||||
)
|
||||
),
|
||||
},
|
||||
):
|
||||
result = self.runner.invoke(
|
||||
app,
|
||||
["generate_strategies", "--count", "2", "--workers", "1", "--no-optuna"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Strategy Generation Summary" in result.output
|
||||
|
||||
def test_generate_strategies_daytrading_style(self):
|
||||
"""Test generate_strategies with daytrading style."""
|
||||
with patch.dict(
|
||||
sys.modules,
|
||||
{
|
||||
"rdagent.components.coder.strategy_orchestrator": MagicMock(
|
||||
StrategyOrchestrator=MagicMock(
|
||||
return_value=MagicMock(
|
||||
generate_strategies=MagicMock(return_value=[])
|
||||
)
|
||||
)
|
||||
),
|
||||
},
|
||||
):
|
||||
result = self.runner.invoke(
|
||||
app,
|
||||
["generate_strategies", "--style", "daytrading", "--count", "1", "--no-optuna"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
class TestCLIOptimizePortfolio:
|
||||
"""Test optimize_portfolio CLI command."""
|
||||
|
||||
def setup_method(self):
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_optimize_portfolio_help(self):
|
||||
"""Test help message displays correctly."""
|
||||
result = self.runner.invoke(app, ["optimize_portfolio", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Optimize portfolio weights" in result.output
|
||||
assert "--top-n" in result.output
|
||||
assert "--method" in result.output
|
||||
|
||||
def test_optimize_portfolio_invalid_method(self):
|
||||
"""Test error handling for invalid method."""
|
||||
result = self.runner.invoke(app, ["optimize_portfolio", "--method", "invalid"])
|
||||
assert result.exit_code == 1
|
||||
assert "Error: Invalid method" in result.output
|
||||
|
||||
def test_optimize_portfolio_no_strategies(self):
|
||||
"""Test handling when no strategies directory exists."""
|
||||
with patch("pathlib.Path.exists", return_value=False):
|
||||
result = self.runner.invoke(app, ["optimize_portfolio"])
|
||||
# Should handle gracefully
|
||||
assert result.exit_code in (0, 1)
|
||||
|
||||
|
||||
class TestCLIStrategiesReport:
|
||||
"""Test strategies_report CLI command."""
|
||||
|
||||
def setup_method(self):
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_strategies_report_help(self):
|
||||
"""Test help message displays correctly."""
|
||||
result = self.runner.invoke(app, ["strategies_report", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Generate performance reports" in result.output
|
||||
assert "--strategy-path" in result.output
|
||||
assert "--output-dir" in result.output
|
||||
|
||||
def test_strategies_report_invalid_path(self):
|
||||
"""Test error handling for invalid path."""
|
||||
result = self.runner.invoke(
|
||||
app, ["strategies_report", "--strategy-path", "/nonexistent/path.json"]
|
||||
)
|
||||
assert result.exit_code == 1
|
||||
assert "Error: Path not found" in result.output
|
||||
|
||||
def test_strategies_report_no_json_files(self):
|
||||
"""Test error handling when no JSON files found."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
result = self.runner.invoke(
|
||||
app, ["strategies_report", "--strategy-path", tmpdir]
|
||||
)
|
||||
assert result.exit_code == 1
|
||||
assert "Error: No strategy JSON files found" in result.output
|
||||
|
||||
def test_strategies_report_with_valid_file(self):
|
||||
"""Test strategies_report with a valid strategy file."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmpdir = Path(tmpdir)
|
||||
|
||||
# Create test strategy file
|
||||
test_strategy = {
|
||||
"strategy_name": "TestStrategy",
|
||||
"status": "accepted",
|
||||
"sharpe_ratio": 2.0,
|
||||
"annualized_return": 0.15,
|
||||
"max_drawdown": -0.10,
|
||||
"win_rate": 0.55,
|
||||
"factors_used": ["factor_a", "factor_b"],
|
||||
"trading_style": "swing",
|
||||
}
|
||||
|
||||
strategy_file = tmpdir / "test_strategy.json"
|
||||
with open(strategy_file, "w") as f:
|
||||
json.dump(test_strategy, f)
|
||||
|
||||
output_dir = tmpdir / "reports"
|
||||
|
||||
result = self.runner.invoke(
|
||||
app,
|
||||
[
|
||||
"strategies_report",
|
||||
"--strategy-path",
|
||||
str(strategy_file),
|
||||
"--output-dir",
|
||||
str(output_dir),
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Report Generation Complete" in result.output
|
||||
# Check report file was created
|
||||
assert output_dir.exists()
|
||||
|
||||
def test_generate_single_strategy_report(self):
|
||||
"""Test _generate_single_strategy_report function."""
|
||||
from rdagent.app.cli import _generate_single_strategy_report
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmpdir = Path(tmpdir)
|
||||
|
||||
# Create test strategy file
|
||||
test_strategy = {
|
||||
"strategy_name": "TestReportStrategy",
|
||||
"status": "accepted",
|
||||
"sharpe_ratio": 1.8,
|
||||
"annualized_return": 0.12,
|
||||
"max_drawdown": -0.15,
|
||||
"win_rate": 0.52,
|
||||
"volatility": 0.08,
|
||||
"information_ratio": 0.5,
|
||||
"factors_used": ["factor_x", "factor_y"],
|
||||
"trading_style": "swing",
|
||||
}
|
||||
|
||||
strategy_file = tmpdir / "test_strategy.json"
|
||||
with open(strategy_file, "w") as f:
|
||||
json.dump(test_strategy, f)
|
||||
|
||||
output_dir = tmpdir / "reports"
|
||||
output_dir.mkdir()
|
||||
|
||||
# Generate report
|
||||
report = _generate_single_strategy_report(strategy_file, output_dir)
|
||||
|
||||
# Verify report
|
||||
assert "strategy_name" in report
|
||||
assert report["strategy_name"] == "TestReportStrategy"
|
||||
assert "metrics" in report
|
||||
assert report["metrics"]["sharpe_ratio"] == 1.8
|
||||
assert "output_file" in report
|
||||
|
||||
|
||||
class TestFinQuantAutoStrategiesIntegration:
|
||||
"""Test fin_quant --auto-strategies integration."""
|
||||
|
||||
def setup_method(self):
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_fin_quant_help_shows_auto_strategies(self):
|
||||
"""Test that fin_quant help shows auto-strategies options."""
|
||||
result = self.runner.invoke(app, ["fin_quant", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "--auto-strategies" in result.output
|
||||
assert "--auto-strategies-threshold" in result.output
|
||||
|
||||
def test_fin_quant_accepts_auto_strategies_flag(self):
|
||||
"""Test that fin_quant accepts --auto-strategies flag without error."""
|
||||
# Just verify the flag is accepted (command may fail due to missing config)
|
||||
result = self.runner.invoke(
|
||||
app,
|
||||
[
|
||||
"fin_quant",
|
||||
"--auto-strategies",
|
||||
"--auto-strategies-threshold",
|
||||
"100",
|
||||
],
|
||||
)
|
||||
# Should not fail due to argument parsing
|
||||
assert "Error" not in result.output or "auto" not in result.output.lower()
|
||||
|
||||
|
||||
class TestCLIOutputFormatting:
|
||||
"""Test Rich console output formatting."""
|
||||
|
||||
def setup_method(self):
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_summary_table_headers(self):
|
||||
"""Test that summary table headers appear in output."""
|
||||
mock_results = [
|
||||
{
|
||||
"strategy_name": "AlphaStrategy",
|
||||
"status": "accepted",
|
||||
"sharpe_ratio": 2.5,
|
||||
"annualized_return": 0.20,
|
||||
"max_drawdown": -0.08,
|
||||
"win_rate": 0.60,
|
||||
}
|
||||
]
|
||||
|
||||
with patch.dict(
|
||||
sys.modules,
|
||||
{
|
||||
"rdagent.components.coder.strategy_orchestrator": MagicMock(
|
||||
StrategyOrchestrator=MagicMock(
|
||||
return_value=MagicMock(
|
||||
generate_strategies=MagicMock(return_value=mock_results)
|
||||
)
|
||||
)
|
||||
),
|
||||
},
|
||||
):
|
||||
result = self.runner.invoke(
|
||||
app,
|
||||
["generate_strategies", "--count", "1", "--no-optuna"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
# Check table headers appear in output
|
||||
assert "Status" in result.output
|
||||
assert "Count" in result.output
|
||||
assert "Percentage" in result.output
|
||||
|
||||
|
||||
class TestCLIErrorHandling:
|
||||
"""Test CLI error handling edge cases."""
|
||||
|
||||
def setup_method(self):
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_strategies_report_malformed_json(self):
|
||||
"""Test handling of malformed JSON in strategy files."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmpdir = Path(tmpdir)
|
||||
|
||||
# Create malformed JSON file
|
||||
bad_file = tmpdir / "bad_strategy.json"
|
||||
bad_file.write_text("{invalid json}", encoding="utf-8")
|
||||
|
||||
result = self.runner.invoke(
|
||||
app,
|
||||
[
|
||||
"strategies_report",
|
||||
"--strategy-path",
|
||||
str(bad_file),
|
||||
"--output-dir",
|
||||
str(tmpdir / "reports"),
|
||||
],
|
||||
)
|
||||
|
||||
# Should handle error gracefully
|
||||
assert result.exit_code in (0, 1)
|
||||
|
||||
|
||||
class TestCLICommandRegistration:
|
||||
"""Test that all CLI commands are properly registered."""
|
||||
|
||||
def setup_method(self):
|
||||
self.runner = CliRunner()
|
||||
|
||||
def test_all_commands_registered(self):
|
||||
"""Test that all new commands are registered."""
|
||||
result = self.runner.invoke(app, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Check all new commands appear in help (typer uses underscores)
|
||||
assert "generate_strategies" in result.output
|
||||
assert "optimize_portfolio" in result.output
|
||||
assert "strategies_report" in result.output
|
||||
|
||||
def test_fin_quant_still_works(self):
|
||||
"""Test that existing fin_quant command still works."""
|
||||
result = self.runner.invoke(app, ["fin_quant", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "EURUSD quantitative trading" in result.output
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user