Source code for fynance.models.training

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

""" Robust training utilities: sample weighting and early stopping. """

from __future__ import annotations

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

__all__ = ['EarlyStopping', 'exp_sample_weights']


[docs] def exp_sample_weights(n: int, halflife: float) -> NDArray: r""" Exponential-decay sample weights (recent observations weighted more). The most recent observation gets weight 1; weights halve every ``halflife`` observations into the past. Useful to down-weight stale data when training on a long history. Parameters ---------- n : int Number of observations. halflife : float Decay half-life in observations (must be > 0). Returns ------- np.ndarray Weights of shape ``(n,)``, in ``(0, 1]``, increasing with index (oldest first, most recent last). Examples -------- >>> exp_sample_weights(4, halflife=1) array([0.125, 0.25 , 0.5 , 1. ]) """ if halflife <= 0: raise ValueError("halflife must be > 0") age = (n - 1) - np.arange(n) return 0.5 ** (age / halflife)
[docs] class EarlyStopping: """ Patience-based early stopping on a monitored metric. Call :meth:`step` with the validation metric each epoch; it returns ``True`` once the metric has failed to improve for ``patience`` steps. Parameters ---------- patience : int, optional Number of non-improving steps to tolerate. Default 5. min_delta : float, optional Minimum change counted as an improvement. Default 0. mode : {'max', 'min'}, optional Whether the metric should be maximized (e.g. Sharpe) or minimized (e.g. loss). Default 'max'. Attributes ---------- best : float or None Best metric seen so far. stop : bool Whether stopping has been triggered. """ def __init__(self, patience: int = 5, min_delta: float = 0., mode: str = 'max'): if mode not in ('max', 'min'): raise ValueError("mode must be 'max' or 'min'") self.patience = patience self.min_delta = min_delta self.mode = mode self.best: float | None = None self.count = 0 self.stop = False
[docs] def step(self, metric: float) -> bool: """ Update with a new metric value; return True if training should stop. """ if self.best is None: improved = True elif self.mode == 'max': improved = metric > self.best + self.min_delta else: improved = metric < self.best - self.min_delta if improved: self.best = metric self.count = 0 else: self.count += 1 if self.count >= self.patience: self.stop = True return self.stop