walk_forward_search¶
Defined in fynance.models.tuning
- walk_forward_search(factory, param_grid, X, y, *, train=252, test=63, step=None, purge=0, metric=None, n_iter=None, seed=0)[source]
Purged walk-forward hyperparameter search.
Every configuration in
param_gridis scored on the same purged walk-forward folds (fynance.data.split.walk_forward), so configurations are compared honestly on out-of-sample data only – no configuration ever sees a fold’s test window during its own fit.- Parameters:
- factorycallable
factory(**params)must return a fresh model exposingfit(X, y)(returns the model, or ignored) andpredict(X).- param_griddict of list
Maps parameter name to the list of candidate values. With
n_iterNone(default) the full grid is tried, in the deterministic order ofitertools.productoverparam_grid.values()(insertion order ofparam_grid’s keys). Withn_iterset, a size-n_itersubsample of that same full grid is drawn without replacement, seeded byseed.- X, yarray_like
Full, time-ordered feature matrix and target, both of length
n(the number of observations along axis 0).- train, testint
Train and test window lengths forwarded to
walk_forward. Default 252/63 (roughly one trading year / one quarter of daily bars).- stepint, optional
Roll step forwarded to
walk_forward(defaults totest, i.e. non-overlapping test windows).- purgeint
Embargo forwarded to
walk_forward(observations dropped at the train/test boundary of every fold).- metriccallable, optional
metric(y_true, y_pred) -> float, higher is better. Defaults to negative mean squared error (-mean((y_true - y_pred) ** 2)), so the default search maximizes prediction accuracy; pass a Sharpe-like metric to search directly for OOS risk-adjusted return.- n_iterint, optional
If given, draw this many configurations at random (without replacement) from the full grid instead of trying every configuration; capped at the full grid size.
None(default) tries the full grid.- seedint
Seed for the
n_iterrandom subsample. Ignored whenn_iterisNone. The sameseedalways draws the same subsample.
- Returns:
- SearchResult
See
SearchResult.
- Raises:
- ValueError
If
param_gridis empty or any of its value lists is empty (there would be nothing to search over), or iftrain + testexceeds the number of observations inX(no walk-forward fold would ever be produced, which would otherwise fail silently downstream with an emptytable).
Examples
A tiny closed-form ridge regression, searched over its regularization strength; the data is (near) noiseless and linear, so the unregularized fit (
alpha=0.0) wins:>>> import numpy as np >>> from fynance.models.tuning import walk_forward_search >>> class RidgeModel: ... ''' Closed-form ridge regression. ''' ... def __init__(self, alpha=0.0): ... self.alpha = alpha ... def fit(self, X, y): ... n_features = X.shape[1] ... gram = X.T @ X + self.alpha * np.eye(n_features) ... self.coef_ = np.linalg.solve(gram, X.T @ y) ... return self ... def predict(self, X): ... return X @ self.coef_ >>> rng = np.random.default_rng(0) >>> T, F = 400, 2 >>> X = rng.standard_normal((T, F)) >>> w_true = np.array([1.5, -0.5]) >>> y = X @ w_true + 0.01 * rng.standard_normal(T) >>> result = walk_forward_search( ... RidgeModel, {"alpha": [0.0, 1.0, 10.0]}, X, y, ... train=200, test=50, step=50, ... ) >>> result.table.shape (3, 4) >>> result.n_trials 3 >>> result.best_params {'alpha': 0.0} >>> pred = result.best_model.predict(X[:3]) >>> pred.shape (3,)
The trial count plugs directly into the deflated Sharpe ratio, so the winning configuration’s Sharpe can be reported honestly rather than at face value:
from fynance.research.guards import deflated_sharpe_ratio dsr = deflated_sharpe_ratio(sr, n_obs, result.n_trials)