Skip to content

Commit c6245a9

Browse files
feat: optuna
1 parent 0e3246d commit c6245a9

File tree

6 files changed

+488
-1
lines changed

6 files changed

+488
-1
lines changed

README.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ QuantResearchStarter aims to provide a clean, well-documented starting point for
2424
* **Factor library** — example implementations of momentum, value, size, and volatility factors.
2525
* **Vectorized backtesting engine** — supports transaction costs, slippage, portfolio constraints, and configurable rebalancing frequencies (daily, weekly, monthly).
2626
* **Risk & performance analytics** — returns, drawdowns, Sharpe, turnover, and other risk metrics.
27-
* **CLI & scripts** — small tools to generate data, compute factors, and run backtests from the terminal.
27+
* **Hyperparameter optimization** — automated tuning with Optuna, pruning, and distributed study support.
28+
* **CLI & scripts** — small tools to generate data, compute factors, run backtests, and optimize hyperparameters from the terminal.
2829
* **Production-ready utilities** — type hints, tests, continuous integration, and documentation scaffolding.
2930

3031
---
@@ -153,9 +154,42 @@ Run `python -m quant_research_starter.cli --help` or `python -m quant_research_s
153154
* `python -m quant_research_starter.cli generate-data` — create synthetic price series or download data from adapters
154155
* `python -m quant_research_starter.cli compute-factors` — calculate and export factor scores
155156
* `python -m quant_research_starter.cli backtest` — run the vectorized backtest and export results
157+
* `python -m quant_research_starter.cli autotune` — optimize hyperparameters with Optuna
156158

157159
**Note:** If you have the `qrs` command in your PATH, you can use `qrs` instead of `python -m quant_research_starter.cli`.
158160

161+
### Hyperparameter Tuning (Autotune)
162+
163+
The `autotune` command automates hyperparameter search using Optuna with pruning support for efficient optimization.
164+
165+
**Basic usage:**
166+
```bash
167+
# Optimize momentum factor hyperparameters
168+
qrs autotune -f momentum -n 100 -m sharpe_ratio
169+
170+
# Use YAML configuration file
171+
qrs autotune -c examples/autotune_config.yaml
172+
```
173+
174+
**Key features:**
175+
- **Pruning**: Early stopping of bad trials to save computation time
176+
- **Distributed tuning**: Optional RDB storage (SQLite, PostgreSQL, MySQL) for multi-worker setups
177+
- **Flexible objectives**: Optimize any metric (Sharpe ratio, total return, CAGR, etc.)
178+
- **Factor support**: Optimize momentum, volatility, and other factor hyperparameters
179+
180+
**Example YAML configuration:**
181+
```yaml
182+
data_file: "data_sample/sample_prices.csv"
183+
factor_type: "momentum"
184+
n_trials: 100
185+
metric: "sharpe_ratio"
186+
output: "output/tuning_results.json"
187+
pruner: "median" # Options: none, median, percentile
188+
storage: "sqlite:///optuna.db" # Optional: for distributed runs
189+
```
190+
191+
See `examples/autotune_config.yaml` for a complete example configuration.
192+
159193
---
160194

161195
## Project structure (overview)
@@ -167,6 +201,7 @@ QuantResearchStarter/
167201
│ ├─ factors/ # factor implementations
168202
│ ├─ backtest/ # backtester & portfolio logic
169203
│ ├─ analytics/ # performance and risk metrics
204+
│ ├─ tuning/ # Optuna hyperparameter optimization
170205
│ ├─ cli/ # command line entry points
171206
│ └─ dashboard/ # optional Streamlit dashboard
172207
├─ examples/ # runnable notebooks & example strategies

examples/autotune_config.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Example configuration for hyperparameter tuning with Optuna
2+
# Usage: qrs autotune -c examples/autotune_config.yaml
3+
4+
# Data configuration
5+
data_file: "data_sample/sample_prices.csv"
6+
7+
# Factor to optimize
8+
factor_type: "momentum" # Options: momentum, value, size, volatility
9+
10+
# Optimization settings
11+
n_trials: 100 # Number of trials to run
12+
metric: "sharpe_ratio" # Metric to optimize (sharpe_ratio, total_return, cagr, etc.)
13+
14+
# Output configuration
15+
output: "output/tuning_results.json"
16+
study_name: "momentum_factor_study"
17+
18+
# Pruning configuration (for early stopping of bad trials)
19+
# Options: none, median, percentile
20+
pruner: "median"
21+
22+
# Optional: RDB storage for distributed tuning runs
23+
# Uncomment and configure for multi-worker setups
24+
# storage: "sqlite:///optuna.db"
25+
# For PostgreSQL: "postgresql://user:password@localhost/dbname"
26+
# For MySQL: "mysql://user:password@localhost/dbname"
27+

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ dependencies = [
3131
"uvicorn>=0.23.0",
3232
"python-dotenv>=1.0.0",
3333
"requests>=2.31.0",
34+
"optuna>=3.0.0",
35+
"pyyaml>=6.0",
3436
]
3537

3638
[project.optional-dependencies]

src/quant_research_starter/cli.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
import click
77
import matplotlib.pyplot as plt
88
import pandas as pd
9+
import yaml
910
from tqdm import tqdm
1011

1112
from .backtest import VectorizedBacktest
1213
from .data import SampleDataLoader, SyntheticDataGenerator
1314
from .factors import MomentumFactor, SizeFactor, ValueFactor, VolatilityFactor
1415
from .metrics import RiskMetrics, create_equity_curve_plot
16+
from .tuning import OptunaRunner, create_backtest_objective
1517

1618

1719
@click.group()
@@ -247,5 +249,136 @@ def backtest(data_file, signals_file, initial_capital, output, plot, plotly):
247249
click.echo(f"Results saved -> {output}")
248250

249251

252+
@cli.command()
253+
@click.option(
254+
"--config",
255+
"-c",
256+
type=click.Path(exists=True),
257+
help="YAML configuration file for hyperparameter tuning",
258+
)
259+
@click.option(
260+
"--data-file",
261+
"-d",
262+
default="data_sample/sample_prices.csv",
263+
help="Price data file path",
264+
)
265+
@click.option(
266+
"--factor-type",
267+
"-f",
268+
type=click.Choice(["momentum", "value", "size", "volatility"]),
269+
default="momentum",
270+
help="Factor type to optimize",
271+
)
272+
@click.option(
273+
"--n-trials",
274+
"-n",
275+
default=100,
276+
help="Number of optimization trials",
277+
)
278+
@click.option(
279+
"--metric",
280+
"-m",
281+
default="sharpe_ratio",
282+
help="Metric to optimize (sharpe_ratio, total_return, cagr, etc.)",
283+
)
284+
@click.option(
285+
"--output",
286+
"-o",
287+
default="output/tuning_results.json",
288+
help="Output file for tuning results",
289+
)
290+
@click.option(
291+
"--storage",
292+
"-s",
293+
default=None,
294+
help="RDB storage URL (e.g., sqlite:///optuna.db) for distributed tuning",
295+
)
296+
@click.option(
297+
"--pruner",
298+
"-p",
299+
type=click.Choice(["none", "median", "percentile"]),
300+
default="median",
301+
help="Pruning strategy for early stopping",
302+
)
303+
@click.option(
304+
"--study-name",
305+
default="optuna_study",
306+
help="Name of the Optuna study",
307+
)
308+
def autotune(
309+
config,
310+
data_file,
311+
factor_type,
312+
n_trials,
313+
metric,
314+
output,
315+
storage,
316+
pruner,
317+
study_name,
318+
):
319+
"""Run hyperparameter optimization with Optuna."""
320+
click.echo("Starting hyperparameter optimization...")
321+
322+
# Load configuration from YAML if provided
323+
if config:
324+
with open(config, "r") as f:
325+
config_data = yaml.safe_load(f)
326+
data_file = config_data.get("data_file", data_file)
327+
factor_type = config_data.get("factor_type", factor_type)
328+
n_trials = config_data.get("n_trials", n_trials)
329+
metric = config_data.get("metric", metric)
330+
output = config_data.get("output", output)
331+
storage = config_data.get("storage", storage)
332+
pruner = config_data.get("pruner", pruner)
333+
study_name = config_data.get("study_name", study_name)
334+
335+
# Load data
336+
if Path(data_file).exists():
337+
prices = pd.read_csv(data_file, index_col=0, parse_dates=True)
338+
else:
339+
click.echo("Data file not found, using sample data...")
340+
loader = SampleDataLoader()
341+
prices = loader.load_sample_prices()
342+
343+
click.echo(f"Optimizing {factor_type} factor with {n_trials} trials...")
344+
click.echo(f"Optimizing metric: {metric}")
345+
346+
# Create objective function
347+
objective = create_backtest_objective(
348+
prices=prices,
349+
factor_type=factor_type,
350+
metric=metric,
351+
)
352+
353+
# Create and run Optuna runner
354+
runner = OptunaRunner(
355+
search_space={}, # Not used when using create_backtest_objective
356+
objective=objective,
357+
n_trials=n_trials,
358+
study_name=study_name,
359+
storage=storage,
360+
pruner=pruner,
361+
direction=(
362+
"maximize"
363+
if metric in ["sharpe_ratio", "total_return", "cagr"]
364+
else "minimize"
365+
),
366+
)
367+
368+
# Run optimization
369+
results = runner.optimize()
370+
371+
# Save results
372+
runner.save_results(output)
373+
374+
click.echo("\n" + "=" * 60)
375+
click.echo("Optimization Results")
376+
click.echo("=" * 60)
377+
click.echo(f"Best parameters: {results['best_params']}")
378+
click.echo(f"Best {metric}: {results['best_value']:.4f}")
379+
click.echo(f"Total trials: {len(results['trial_history'])}")
380+
click.echo(f"Results saved -> {output}")
381+
382+
250383
if __name__ == "__main__":
251384
cli()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Hyperparameter tuning with Optuna."""
2+
3+
from .optuna_runner import OptunaRunner
4+
5+
__all__ = ["OptunaRunner"]
6+

0 commit comments

Comments
 (0)