Skip to content

Hyperparameter Optimisation

Optuna-based hyperparameter-optimisation layer (tinycta.hyper).

Installed via the optional hyper extra:

pip install "tinycta[hyper]"

tinycta.hyper

Hyperparameter optimisation support via Optuna.

Public API

  • Study: Frozen dataclass wrapping a completed Optuna study.
  • optimize: Convenience wrapper: build objective, run study, print, return Study.
  • get_config: Set up logger and config sections for a notebook experiment.
  • ExperimentConfig: NamedTuple returned by get_config.

ExperimentConfig

Bases: NamedTuple

Resources bundled for a notebook experiment run.

Source code in src/tinycta/hyper/_setup.py
class ExperimentConfig(NamedTuple):
    """Resources bundled for a notebook experiment run."""

    name: str
    logger: Any
    params: dict[str, Any] | None = None
    optuna: dict[str, Any] | None = None
    data: dict[str, Any] | None = None

Study dataclass

Frozen wrapper around a completed Optuna study.

Source code in src/tinycta/hyper/_study.py
@dataclass(frozen=True)
class Study:
    """Frozen wrapper around a completed Optuna study."""

    best_params: dict[str, Any]
    best_value: float
    n_completed: int
    n_trials: int
    optuna_study: optuna.Study = field(repr=False)

    def __str__(self) -> str:
        """Return a human-readable summary of the best trial."""
        if self.n_completed == 0:
            return "No completed trials — all returned NaN Sharpe."
        lines = ["=== Best parameters ==="]
        for k, v in self.best_params.items():
            lines.append(f"  {k:<12} = {v}")
        lines.append(f"  {'Sharpe':<12} = {self.best_value:.4f}")
        lines.append(f"  {'Completed':<12} = {self.n_completed} / {self.n_trials} trials")
        return "\n".join(lines)

    @classmethod
    def from_optuna(cls, s: optuna.Study) -> Study:
        """Wrap a completed optuna.Study in a frozen Study."""
        n_completed = sum(1 for t in s.trials if t.state == optuna.trial.TrialState.COMPLETE)
        if n_completed == 0:
            best_params, best_value = {}, float("nan")
        else:
            best_params, best_value = s.best_params, s.best_value
        return cls(
            best_params=best_params,
            best_value=best_value,
            n_completed=n_completed,
            n_trials=len(s.trials),
            optuna_study=s,
        )

    def plot(self, output_dir: Path) -> None:
        """Write Optuna visualisation plots to output_dir (HTML, PNG if kaleido available)."""
        output_dir.mkdir(parents=True, exist_ok=True)
        figures = {
            "optuna_history": optuna.visualization.plot_optimization_history(self.optuna_study),
            "optuna_importance": optuna.visualization.plot_param_importances(self.optuna_study),
            "optuna_parallel": optuna.visualization.plot_parallel_coordinate(self.optuna_study),
            "optuna_contour": optuna.visualization.plot_contour(self.optuna_study),
        }
        for name, fig in figures.items():
            fig.write_html(str(output_dir / f"{name}.html"))
            try:
                fig.write_image(str(output_dir / f"{name}.png"), scale=2)
            except (ValueError, ImportError) as exc:
                # PNG export needs the optional `kaleido` backend; skip if unavailable.
                logger.debug(f"Skipping PNG export for {name}: {exc}")

__str__()

Return a human-readable summary of the best trial.

Source code in src/tinycta/hyper/_study.py
def __str__(self) -> str:
    """Return a human-readable summary of the best trial."""
    if self.n_completed == 0:
        return "No completed trials — all returned NaN Sharpe."
    lines = ["=== Best parameters ==="]
    for k, v in self.best_params.items():
        lines.append(f"  {k:<12} = {v}")
    lines.append(f"  {'Sharpe':<12} = {self.best_value:.4f}")
    lines.append(f"  {'Completed':<12} = {self.n_completed} / {self.n_trials} trials")
    return "\n".join(lines)

from_optuna(s) classmethod

Wrap a completed optuna.Study in a frozen Study.

Source code in src/tinycta/hyper/_study.py
@classmethod
def from_optuna(cls, s: optuna.Study) -> Study:
    """Wrap a completed optuna.Study in a frozen Study."""
    n_completed = sum(1 for t in s.trials if t.state == optuna.trial.TrialState.COMPLETE)
    if n_completed == 0:
        best_params, best_value = {}, float("nan")
    else:
        best_params, best_value = s.best_params, s.best_value
    return cls(
        best_params=best_params,
        best_value=best_value,
        n_completed=n_completed,
        n_trials=len(s.trials),
        optuna_study=s,
    )

plot(output_dir)

Write Optuna visualisation plots to output_dir (HTML, PNG if kaleido available).

Source code in src/tinycta/hyper/_study.py
def plot(self, output_dir: Path) -> None:
    """Write Optuna visualisation plots to output_dir (HTML, PNG if kaleido available)."""
    output_dir.mkdir(parents=True, exist_ok=True)
    figures = {
        "optuna_history": optuna.visualization.plot_optimization_history(self.optuna_study),
        "optuna_importance": optuna.visualization.plot_param_importances(self.optuna_study),
        "optuna_parallel": optuna.visualization.plot_parallel_coordinate(self.optuna_study),
        "optuna_contour": optuna.visualization.plot_contour(self.optuna_study),
    }
    for name, fig in figures.items():
        fig.write_html(str(output_dir / f"{name}.html"))
        try:
            fig.write_image(str(output_dir / f"{name}.png"), scale=2)
        except (ValueError, ImportError) as exc:
            # PNG export needs the optional `kaleido` backend; skip if unavailable.
            logger.debug(f"Skipping PNG export for {name}: {exc}")

get_config(name, config_path=None)

Return logger and config sections for an experiment.

Accepts either a shared config.yml or an experiment-specific config/{name}.yml. Paths in the config are resolved relative to the notebooks directory (one level above any config/ subdirectory). NOTEBOOK_OUTPUT_FOLDER env var overrides the output directory used for the log file sink.

Source code in src/tinycta/hyper/_setup.py
def get_config(name: str, config_path: Path | str | None = None) -> ExperimentConfig:
    """Return logger and config sections for an experiment.

    Accepts either a shared ``config.yml`` or an experiment-specific
    ``config/{name}.yml``.  Paths in the config are resolved relative to the
    notebooks directory (one level above any ``config/`` subdirectory).
    ``NOTEBOOK_OUTPUT_FOLDER`` env var overrides the output directory used for
    the log file sink.
    """
    config_path = Path(config_path) if config_path else Path.cwd() / "config.yml"
    cfg = _load_yaml(config_path)
    # Resolve paths relative to the notebooks dir, not the config subdir.
    base = config_path.parent.parent if config_path.parent.name == "config" else config_path.parent
    sibling = _load_yaml(base / "config" / f"{name}.yml")

    data = cfg.get("data") or sibling.get("data") or {}
    params = cfg.get("params") or sibling.get("params") or {}
    optuna_cfg = cfg.get("optuna") or sibling.get("optuna") or {}

    env_folder = os.environ.get("NOTEBOOK_OUTPUT_FOLDER")
    if env_folder:
        output_dir = Path(env_folder)
    else:
        folder = data.get("output_path", "output")
        output_dir = (base / folder / name).resolve()
    output_dir.mkdir(parents=True, exist_ok=True)

    log_path = output_dir / "output.log"
    key = str(log_path.resolve())
    if key not in _FILE_SINKS:
        _FILE_SINKS[key] = logger.add(log_path)
    logger.info(f"Writing output to: {output_dir}\nCurrent working directory: {os.getcwd()}")

    return ExperimentConfig(
        name=name,
        logger=logger,
        params=params,
        optuna=optuna_cfg,
        data=data,
    )

optimize(suggest_portfolio_fn, n_trials=100, seed=42)

Build objective, run study, log the summary and return a frozen Study.

Source code in src/tinycta/hyper/_study.py
def optimize(
    suggest_portfolio_fn: Callable[[optuna.Trial], Portfolio],
    n_trials: int = 100,
    seed: int = 42,
) -> Study:
    """Build objective, run study, log the summary and return a frozen Study."""
    s = _run_study(_build_objective(suggest_portfolio_fn), n_trials=n_trials, seed=seed)
    study = Study.from_optuna(s)
    logger.info(str(study))
    return study