From ba43514e77ce665131d7d56b52cfa183a0054c1d Mon Sep 17 00:00:00 2001 From: Satvik-Singh192 Date: Wed, 12 Nov 2025 13:44:49 +0530 Subject: [PATCH 1/9] feat: optuna --- README.md | 37 ++- examples/autotune_config.yaml | 27 ++ pyproject.toml | 2 + src/quant_research_starter/cli.py | 133 ++++++++ src/quant_research_starter/tuning/__init__.py | 6 + .../tuning/optuna_runner.py | 284 ++++++++++++++++++ 6 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 examples/autotune_config.yaml create mode 100644 src/quant_research_starter/tuning/__init__.py create mode 100644 src/quant_research_starter/tuning/optuna_runner.py diff --git a/README.md b/README.md index 363b73e..bb23021 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ QuantResearchStarter aims to provide a clean, well-documented starting point for * **Factor library** — example implementations of momentum, value, size, and volatility factors. * **Vectorized backtesting engine** — supports transaction costs, slippage, portfolio constraints, and configurable rebalancing frequencies (daily, weekly, monthly). * **Risk & performance analytics** — returns, drawdowns, Sharpe, turnover, and other risk metrics. -* **CLI & scripts** — small tools to generate data, compute factors, and run backtests from the terminal. +* **Hyperparameter optimization** — automated tuning with Optuna, pruning, and distributed study support. +* **CLI & scripts** — small tools to generate data, compute factors, run backtests, and optimize hyperparameters from the terminal. * **Production-ready utilities** — type hints, tests, continuous integration, and documentation scaffolding. --- @@ -153,9 +154,42 @@ Run `python -m quant_research_starter.cli --help` or `python -m quant_research_s * `python -m quant_research_starter.cli generate-data` — create synthetic price series or download data from adapters * `python -m quant_research_starter.cli compute-factors` — calculate and export factor scores * `python -m quant_research_starter.cli backtest` — run the vectorized backtest and export results +* `python -m quant_research_starter.cli autotune` — optimize hyperparameters with Optuna **Note:** If you have the `qrs` command in your PATH, you can use `qrs` instead of `python -m quant_research_starter.cli`. +### Hyperparameter Tuning (Autotune) + +The `autotune` command automates hyperparameter search using Optuna with pruning support for efficient optimization. + +**Basic usage:** +```bash +# Optimize momentum factor hyperparameters +qrs autotune -f momentum -n 100 -m sharpe_ratio + +# Use YAML configuration file +qrs autotune -c examples/autotune_config.yaml +``` + +**Key features:** +- **Pruning**: Early stopping of bad trials to save computation time +- **Distributed tuning**: Optional RDB storage (SQLite, PostgreSQL, MySQL) for multi-worker setups +- **Flexible objectives**: Optimize any metric (Sharpe ratio, total return, CAGR, etc.) +- **Factor support**: Optimize momentum, volatility, and other factor hyperparameters + +**Example YAML configuration:** +```yaml +data_file: "data_sample/sample_prices.csv" +factor_type: "momentum" +n_trials: 100 +metric: "sharpe_ratio" +output: "output/tuning_results.json" +pruner: "median" # Options: none, median, percentile +storage: "sqlite:///optuna.db" # Optional: for distributed runs +``` + +See `examples/autotune_config.yaml` for a complete example configuration. + --- ## Project structure (overview) @@ -167,6 +201,7 @@ QuantResearchStarter/ │ ├─ factors/ # factor implementations │ ├─ backtest/ # backtester & portfolio logic │ ├─ analytics/ # performance and risk metrics +│ ├─ tuning/ # Optuna hyperparameter optimization │ ├─ cli/ # command line entry points │ └─ dashboard/ # optional Streamlit dashboard ├─ examples/ # runnable notebooks & example strategies diff --git a/examples/autotune_config.yaml b/examples/autotune_config.yaml new file mode 100644 index 0000000..cd5b36d --- /dev/null +++ b/examples/autotune_config.yaml @@ -0,0 +1,27 @@ +# Example configuration for hyperparameter tuning with Optuna +# Usage: qrs autotune -c examples/autotune_config.yaml + +# Data configuration +data_file: "data_sample/sample_prices.csv" + +# Factor to optimize +factor_type: "momentum" # Options: momentum, value, size, volatility + +# Optimization settings +n_trials: 100 # Number of trials to run +metric: "sharpe_ratio" # Metric to optimize (sharpe_ratio, total_return, cagr, etc.) + +# Output configuration +output: "output/tuning_results.json" +study_name: "momentum_factor_study" + +# Pruning configuration (for early stopping of bad trials) +# Options: none, median, percentile +pruner: "median" + +# Optional: RDB storage for distributed tuning runs +# Uncomment and configure for multi-worker setups +# storage: "sqlite:///optuna.db" +# For PostgreSQL: "postgresql://user:password@localhost/dbname" +# For MySQL: "mysql://user:password@localhost/dbname" + diff --git a/pyproject.toml b/pyproject.toml index 4eecd4f..0e9378b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,8 @@ dependencies = [ "uvicorn>=0.23.0", "python-dotenv>=1.0.0", "requests>=2.31.0", + "optuna>=3.0.0", + "pyyaml>=6.0", ] [project.optional-dependencies] diff --git a/src/quant_research_starter/cli.py b/src/quant_research_starter/cli.py index 12a71c4..d659369 100644 --- a/src/quant_research_starter/cli.py +++ b/src/quant_research_starter/cli.py @@ -6,12 +6,14 @@ import click import matplotlib.pyplot as plt import pandas as pd +import yaml from tqdm import tqdm from .backtest import VectorizedBacktest from .data import SampleDataLoader, SyntheticDataGenerator from .factors import MomentumFactor, SizeFactor, ValueFactor, VolatilityFactor from .metrics import RiskMetrics, create_equity_curve_plot +from .tuning import OptunaRunner, create_backtest_objective @click.group() @@ -247,5 +249,136 @@ def backtest(data_file, signals_file, initial_capital, output, plot, plotly): click.echo(f"Results saved -> {output}") +@cli.command() +@click.option( + "--config", + "-c", + type=click.Path(exists=True), + help="YAML configuration file for hyperparameter tuning", +) +@click.option( + "--data-file", + "-d", + default="data_sample/sample_prices.csv", + help="Price data file path", +) +@click.option( + "--factor-type", + "-f", + type=click.Choice(["momentum", "value", "size", "volatility"]), + default="momentum", + help="Factor type to optimize", +) +@click.option( + "--n-trials", + "-n", + default=100, + help="Number of optimization trials", +) +@click.option( + "--metric", + "-m", + default="sharpe_ratio", + help="Metric to optimize (sharpe_ratio, total_return, cagr, etc.)", +) +@click.option( + "--output", + "-o", + default="output/tuning_results.json", + help="Output file for tuning results", +) +@click.option( + "--storage", + "-s", + default=None, + help="RDB storage URL (e.g., sqlite:///optuna.db) for distributed tuning", +) +@click.option( + "--pruner", + "-p", + type=click.Choice(["none", "median", "percentile"]), + default="median", + help="Pruning strategy for early stopping", +) +@click.option( + "--study-name", + default="optuna_study", + help="Name of the Optuna study", +) +def autotune( + config, + data_file, + factor_type, + n_trials, + metric, + output, + storage, + pruner, + study_name, +): + """Run hyperparameter optimization with Optuna.""" + click.echo("Starting hyperparameter optimization...") + + # Load configuration from YAML if provided + if config: + with open(config, "r") as f: + config_data = yaml.safe_load(f) + data_file = config_data.get("data_file", data_file) + factor_type = config_data.get("factor_type", factor_type) + n_trials = config_data.get("n_trials", n_trials) + metric = config_data.get("metric", metric) + output = config_data.get("output", output) + storage = config_data.get("storage", storage) + pruner = config_data.get("pruner", pruner) + study_name = config_data.get("study_name", study_name) + + # Load data + if Path(data_file).exists(): + prices = pd.read_csv(data_file, index_col=0, parse_dates=True) + else: + click.echo("Data file not found, using sample data...") + loader = SampleDataLoader() + prices = loader.load_sample_prices() + + click.echo(f"Optimizing {factor_type} factor with {n_trials} trials...") + click.echo(f"Optimizing metric: {metric}") + + # Create objective function + objective = create_backtest_objective( + prices=prices, + factor_type=factor_type, + metric=metric, + ) + + # Create and run Optuna runner + runner = OptunaRunner( + search_space={}, # Not used when using create_backtest_objective + objective=objective, + n_trials=n_trials, + study_name=study_name, + storage=storage, + pruner=pruner, + direction=( + "maximize" + if metric in ["sharpe_ratio", "total_return", "cagr"] + else "minimize" + ), + ) + + # Run optimization + results = runner.optimize() + + # Save results + runner.save_results(output) + + click.echo("\n" + "=" * 60) + click.echo("Optimization Results") + click.echo("=" * 60) + click.echo(f"Best parameters: {results['best_params']}") + click.echo(f"Best {metric}: {results['best_value']:.4f}") + click.echo(f"Total trials: {len(results['trial_history'])}") + click.echo(f"Results saved -> {output}") + + if __name__ == "__main__": cli() diff --git a/src/quant_research_starter/tuning/__init__.py b/src/quant_research_starter/tuning/__init__.py new file mode 100644 index 0000000..2c821ad --- /dev/null +++ b/src/quant_research_starter/tuning/__init__.py @@ -0,0 +1,6 @@ +"""Hyperparameter tuning with Optuna.""" + +from .optuna_runner import OptunaRunner + +__all__ = ["OptunaRunner"] + diff --git a/src/quant_research_starter/tuning/optuna_runner.py b/src/quant_research_starter/tuning/optuna_runner.py new file mode 100644 index 0000000..9d50091 --- /dev/null +++ b/src/quant_research_starter/tuning/optuna_runner.py @@ -0,0 +1,284 @@ +import json +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Union + +import optuna +import pandas as pd +from optuna.pruners import MedianPruner, NopPruner, PercentilePruner +from optuna.storages import RDBStorage +from optuna.trial import Trial + +from ..backtest import VectorizedBacktest +from ..factors import MomentumFactor, SizeFactor, ValueFactor, VolatilityFactor +from ..metrics import RiskMetrics + + +class OptunaRunner: + + def __init__( + self, + search_space: Dict[str, Any], + objective: Callable[[Trial], float], + n_trials: int = 100, + study_name: Optional[str] = None, + storage: Optional[Union[str, RDBStorage]] = None, + pruner: Optional[Union[str, optuna.pruners.BasePruner]] = None, + direction: str = "maximize", + random_state: Optional[int] = None, + ): + + self.search_space = search_space + self.objective = objective + self.n_trials = n_trials + self.study_name = study_name or "optuna_study" + self.direction = direction + self.random_state = random_state + + if storage is None: + self.storage = None + elif isinstance(storage, str): + self.storage = storage + elif isinstance(storage, RDBStorage): + self.storage = storage + else: + raise ValueError(f"Invalid storage type: {type(storage)}") + + if pruner is None or pruner == "none": + self.pruner = NopPruner() + elif pruner == "median": + self.pruner = MedianPruner() + elif pruner == "percentile": + self.pruner = PercentilePruner(percentile=25.0) + elif isinstance(pruner, optuna.pruners.BasePruner): + self.pruner = pruner + else: + raise ValueError(f"Invalid pruner: {pruner}") + + self.study: Optional[optuna.Study] = None + self.trial_history: List[Dict[str, Any]] = [] + + def optimize(self) -> Dict[str, Any]: + """ + Run hyperparameter optimization. + + Returns: + Dictionary with: + - best_params: Best hyperparameters found + - best_value: Best objective value + - trial_history: List of all trial results + - study: Optuna study object + """ + self.study = optuna.create_study( + study_name=self.study_name, + storage=self.storage, + load_if_exists=True, + direction=self.direction, + pruner=self.pruner, + sampler=optuna.samplers.TPESampler(seed=self.random_state), + ) + + self.study.optimize( + self.objective, + n_trials=self.n_trials, + show_progress_bar=True, + ) + + self.trial_history = self._collect_trial_history() + + return { + "best_params": self.study.best_params, + "best_value": self.study.best_value, + "trial_history": self.trial_history, + "study": self.study, + } + + def _collect_trial_history(self) -> List[Dict[str, Any]]: + """Collect history of all trials.""" + history = [] + for trial in self.study.trials: + history.append( + { + "number": trial.number, + "value": trial.value, + "params": trial.params, + "state": trial.state.name, + "datetime_start": ( + trial.datetime_start.isoformat() + if trial.datetime_start + else None + ), + "datetime_complete": ( + trial.datetime_complete.isoformat() + if trial.datetime_complete + else None + ), + } + ) + return history + + def save_results(self, output_path: Union[str, Path]) -> None: + """Save optimization results to JSON file.""" + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + results = { + "best_params": self.study.best_params if self.study else {}, + "best_value": self.study.best_value if self.study else None, + "n_trials": self.n_trials, + "direction": self.direction, + "trial_history": self.trial_history, + } + + with open(output_path, "w") as f: + json.dump(results, f, indent=2, default=str) + + +def create_backtest_objective( + prices: pd.DataFrame, + factor_type: str, + initial_capital: float = 1_000_000, + transaction_cost: float = 0.001, + metric: str = "sharpe_ratio", +) -> Callable[[Trial], float]: + """ + Create an objective function for backtest-based hyperparameter tuning. + + Args: + prices: Price data DataFrame. + factor_type: Type of factor to optimize ("momentum", "value", "size", "volatility"). + initial_capital: Initial capital for backtest. + transaction_cost: Transaction cost rate. + metric: Metric to optimize ("sharpe_ratio", "total_return", "cagr", etc.). + + Returns: + Objective function that takes a Trial and returns a float. + """ + factor_classes = { + "momentum": MomentumFactor, + "value": ValueFactor, + "size": SizeFactor, + "volatility": VolatilityFactor, + } + + if factor_type not in factor_classes: + raise ValueError( + f"Unknown factor type: {factor_type}. " + f"Supported: {list(factor_classes.keys())}" + ) + + FactorClass = factor_classes[factor_type] + + def objective(trial: Trial) -> float: + """Objective function for Optuna trial.""" + if factor_type == "momentum": + lookback = trial.suggest_int("lookback", 10, 252, step=1) + skip_period = trial.suggest_int("skip_period", 0, 5, step=1) + factor = FactorClass(lookback=lookback, skip_period=skip_period) + elif factor_type == "volatility": + lookback = trial.suggest_int("lookback", 10, 126, step=1) + factor = FactorClass(lookback=lookback) + else: + factor = FactorClass() + signals = factor.compute(prices) + if signals.empty: + return ( + float("-inf") + if metric in ["sharpe_ratio", "total_return"] + else float("inf") + ) + + signal_series = signals.mean(axis=1) + signal_matrix = pd.DataFrame( + dict.fromkeys(prices.columns, signal_series), index=signal_series.index + ) + + common_dates = prices.index.intersection(signal_matrix.index) + if len(common_dates) == 0: + return ( + float("-inf") + if metric in ["sharpe_ratio", "total_return"] + else float("inf") + ) + + prices_aligned = prices.loc[common_dates] + signals_aligned = signal_matrix.loc[common_dates] + + try: + backtest = VectorizedBacktest( + prices=prices_aligned, + signals=signals_aligned, + initial_capital=initial_capital, + transaction_cost=transaction_cost, + ) + results = backtest.run(weight_scheme="rank") + + metrics_calc = RiskMetrics(results["returns"]) + metrics = metrics_calc.calculate_all() + + if trial.should_prune(): + raise optuna.TrialPruned() + + metric_value = metrics.get(metric, 0.0) + return float(metric_value) + + except Exception: + return ( + float("-inf") + if metric in ["sharpe_ratio", "total_return"] + else float("inf") + ) + + return objective + + +def suggest_hyperparameters( + trial: Trial, search_space: Dict[str, Any] +) -> Dict[str, Any]: + """ + Suggest hyperparameters from search space definition. + + Args: + trial: Optuna trial object. + search_space: Dictionary defining search space. + Format: + { + "param_name": { + "type": "int" | "float" | "categorical", + "low": , # for int/float + "high": , # for int/float + "choices": [], # for categorical + "log": True/False, # for float (optional) + "step": # for int/float (optional) + } + } + + Returns: + Dictionary of suggested hyperparameters. + """ + params = {} + for param_name, param_config in search_space.items(): + param_type = param_config.get("type", "float") + + if param_type == "int": + low = param_config["low"] + high = param_config["high"] + step = param_config.get("step", 1) + params[param_name] = trial.suggest_int(param_name, low, high, step=step) + + elif param_type == "float": + low = param_config["low"] + high = param_config["high"] + log = param_config.get("log", False) + step = param_config.get("step", None) + params[param_name] = trial.suggest_float( + param_name, low, high, log=log, step=step + ) + + elif param_type == "categorical": + choices = param_config["choices"] + params[param_name] = trial.suggest_categorical(param_name, choices) + + else: + raise ValueError(f"Unknown parameter type: {param_type}") + + return params From 06021308c450d7095d825fda5c37d1a0bbcf98af Mon Sep 17 00:00:00 2001 From: Satvik-Singh192 Date: Wed, 12 Nov 2025 13:56:26 +0530 Subject: [PATCH 2/9] fix: fixed some checks --- src/quant_research_starter/tuning/__init__.py | 1 - src/quant_research_starter/tuning/optuna_runner.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/quant_research_starter/tuning/__init__.py b/src/quant_research_starter/tuning/__init__.py index 2c821ad..1c5d904 100644 --- a/src/quant_research_starter/tuning/__init__.py +++ b/src/quant_research_starter/tuning/__init__.py @@ -3,4 +3,3 @@ from .optuna_runner import OptunaRunner __all__ = ["OptunaRunner"] - diff --git a/src/quant_research_starter/tuning/optuna_runner.py b/src/quant_research_starter/tuning/optuna_runner.py index 9d50091..4187214 100644 --- a/src/quant_research_starter/tuning/optuna_runner.py +++ b/src/quant_research_starter/tuning/optuna_runner.py @@ -26,7 +26,7 @@ def __init__( direction: str = "maximize", random_state: Optional[int] = None, ): - + self.search_space = search_space self.objective = objective self.n_trials = n_trials From cbe108560f6c4b63df80121eaffa5a536a99f472 Mon Sep 17 00:00:00 2001 From: Satvik Singh Date: Wed, 12 Nov 2025 21:06:39 +0530 Subject: [PATCH 3/9] fix: fixed suggestions Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/quant_research_starter/tuning/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/quant_research_starter/tuning/__init__.py b/src/quant_research_starter/tuning/__init__.py index 1c5d904..3e1e5c8 100644 --- a/src/quant_research_starter/tuning/__init__.py +++ b/src/quant_research_starter/tuning/__init__.py @@ -1,5 +1,5 @@ """Hyperparameter tuning with Optuna.""" -from .optuna_runner import OptunaRunner +from .optuna_runner import OptunaRunner, create_backtest_objective -__all__ = ["OptunaRunner"] +__all__ = ["OptunaRunner", "create_backtest_objective"] From 87b180bb85c4086d6260670d6eb12980ed4911f8 Mon Sep 17 00:00:00 2001 From: Satvik Singh Date: Wed, 12 Nov 2025 21:06:55 +0530 Subject: [PATCH 4/9] fix: fixed su Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../tuning/optuna_runner.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/quant_research_starter/tuning/optuna_runner.py b/src/quant_research_starter/tuning/optuna_runner.py index 4187214..39357d2 100644 --- a/src/quant_research_starter/tuning/optuna_runner.py +++ b/src/quant_research_starter/tuning/optuna_runner.py @@ -212,12 +212,21 @@ def objective(trial: Trial) -> float: ) results = backtest.run(weight_scheme="rank") - metrics_calc = RiskMetrics(results["returns"]) + returns = results["returns"] + n_steps = 5 # Number of intermediate steps for reporting + step_size = max(1, len(returns) // n_steps) + for step in range(step_size, len(returns) + 1, step_size): + partial_returns = returns.iloc[:step] + metrics_calc = RiskMetrics(partial_returns) + metrics = metrics_calc.calculate_all() + metric_value = metrics.get(metric, 0.0) + trial.report(metric_value, step) + if trial.should_prune(): + raise optuna.TrialPruned() + + # Final metric on all returns + metrics_calc = RiskMetrics(returns) metrics = metrics_calc.calculate_all() - - if trial.should_prune(): - raise optuna.TrialPruned() - metric_value = metrics.get(metric, 0.0) return float(metric_value) From a3bfccb899b0041ce456ab26605f6a87754efd5e Mon Sep 17 00:00:00 2001 From: Satvik Singh Date: Wed, 12 Nov 2025 21:07:15 +0530 Subject: [PATCH 5/9] fix: fixed suggestions Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/quant_research_starter/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/quant_research_starter/cli.py b/src/quant_research_starter/cli.py index d659369..4c41d45 100644 --- a/src/quant_research_starter/cli.py +++ b/src/quant_research_starter/cli.py @@ -352,7 +352,6 @@ def autotune( # Create and run Optuna runner runner = OptunaRunner( - search_space={}, # Not used when using create_backtest_objective objective=objective, n_trials=n_trials, study_name=study_name, From 3879de5a97a899661a43cfd8838874676cbf43d9 Mon Sep 17 00:00:00 2001 From: Satvik Singh Date: Wed, 12 Nov 2025 21:07:50 +0530 Subject: [PATCH 6/9] fix: fixed suggestions Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/quant_research_starter/tuning/optuna_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quant_research_starter/tuning/optuna_runner.py b/src/quant_research_starter/tuning/optuna_runner.py index 39357d2..b9f0467 100644 --- a/src/quant_research_starter/tuning/optuna_runner.py +++ b/src/quant_research_starter/tuning/optuna_runner.py @@ -189,7 +189,7 @@ def objective(trial: Trial) -> float: signal_series = signals.mean(axis=1) signal_matrix = pd.DataFrame( - dict.fromkeys(prices.columns, signal_series), index=signal_series.index + {col: signal_series.copy() for col in prices.columns}, index=signal_series.index ) common_dates = prices.index.intersection(signal_matrix.index) From f53695049266f0deffd1f405b9d8b89e1d31558a Mon Sep 17 00:00:00 2001 From: Satvik-Singh192 Date: Wed, 12 Nov 2025 22:07:36 +0530 Subject: [PATCH 7/9] fix: black formatting --- src/quant_research_starter/tuning/optuna_runner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/quant_research_starter/tuning/optuna_runner.py b/src/quant_research_starter/tuning/optuna_runner.py index b9f0467..14d9720 100644 --- a/src/quant_research_starter/tuning/optuna_runner.py +++ b/src/quant_research_starter/tuning/optuna_runner.py @@ -189,7 +189,8 @@ def objective(trial: Trial) -> float: signal_series = signals.mean(axis=1) signal_matrix = pd.DataFrame( - {col: signal_series.copy() for col in prices.columns}, index=signal_series.index + {col: signal_series.copy() for col in prices.columns}, + index=signal_series.index, ) common_dates = prices.index.intersection(signal_matrix.index) From 5203024d6e2fd4324c75841765e93a52604403f5 Mon Sep 17 00:00:00 2001 From: Satvik-Singh192 Date: Thu, 13 Nov 2025 21:09:09 +0530 Subject: [PATCH 8/9] fix: pulled frontend --- src/quant_research_starter/backtest/vectorized.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/quant_research_starter/backtest/vectorized.py b/src/quant_research_starter/backtest/vectorized.py index 4fc2bbf..12f3cfd 100644 --- a/src/quant_research_starter/backtest/vectorized.py +++ b/src/quant_research_starter/backtest/vectorized.py @@ -4,7 +4,6 @@ import pandas as pd - class VectorizedBacktest: """ Vectorized backtester for quantitative strategies. From 4a3dda8b4ddd3798dc8c11ae991fd9627f94a5c1 Mon Sep 17 00:00:00 2001 From: Satvik-Singh192 Date: Thu, 13 Nov 2025 21:23:11 +0530 Subject: [PATCH 9/9] fix: ruff --- src/quant_research_starter/backtest/vectorized.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/quant_research_starter/backtest/vectorized.py b/src/quant_research_starter/backtest/vectorized.py index 12f3cfd..4fc2bbf 100644 --- a/src/quant_research_starter/backtest/vectorized.py +++ b/src/quant_research_starter/backtest/vectorized.py @@ -4,6 +4,7 @@ import pandas as pd + class VectorizedBacktest: """ Vectorized backtester for quantitative strategies.