Source code for fynance.models.tuning

#!/usr/bin/env python3
# coding: utf-8

""" Purged walk-forward hyperparameter search.

Grid- or random-search over model hyperparameters, scored **out-of-sample**
on :func:`fynance.data.split.walk_forward` folds (no lookahead, optional
purge/embargo at each train/test boundary). This is the honest alternative to
picking hyperparameters on a single in-sample fit: every configuration is
scored on the same purged folds, and the number of configurations tried
(``n_trials``) is tracked so it can be fed straight into
:func:`fynance.research.guards.deflated_sharpe_ratio` to deflate the winning
configuration's Sharpe by the multiple-testing cost of the search itself.

"""

from __future__ import annotations

# Built-in packages
import itertools
from dataclasses import dataclass
from typing import Any, Callable

# Third-party packages
import numpy as np
from numpy.typing import NDArray

# Local packages
from fynance.data.split import walk_forward

__all__ = ['SearchResult', 'walk_forward_search']


def _neg_mse(y_true: NDArray, y_pred: NDArray) -> float:
    """ Default metric: negative mean squared error (higher is better). """
    y_true = np.asarray(y_true, dtype=np.float64)
    y_pred = np.asarray(y_pred, dtype=np.float64)

    return float(-np.mean((y_true - y_pred) ** 2))


[docs] @dataclass class SearchResult: """ Result of :func:`walk_forward_search`. Attributes ---------- table : numpy.ndarray Out-of-sample scores, shape ``(n_configs, n_folds)`` -- row ``i``, column ``j`` is the score of ``params[i]`` on fold ``j``. params : list of dict Parameter dict tried for each row of ``table`` (same order). best_params : dict The configuration with the highest mean OOS score (mean over folds). best_model : Any ``factory(**best_params)`` refit on the **last** (most recent) train window -- the model to actually deploy. n_trials : int Number of configurations tried (``== len(params) == table.shape[0]``). Feed this into :func:`fynance.research.guards.deflated_sharpe_ratio` as ``n_trials`` so the winning configuration's Sharpe ratio is deflated by the number of configurations the search tried, rather than reported at face value. """ table: NDArray params: list[dict[str, Any]] best_params: dict[str, Any] best_model: Any n_trials: int