Source code for fynance.models.ensemble

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

""" Stacked direction + magnitude ensemble with a meta-model.

Two base models — one specialized in the *direction* of the move, one in its
*magnitude* — are combined by a meta-model trained on their **out-of-fold**
predictions (walk-forward OOF), which keeps the meta-features leak-free.
"""

from __future__ import annotations

# Built-in packages
from typing import Callable

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

# Local packages
from fynance.models.rolling import _RollingBasis

__all__ = ['StackingEnsemble']


[docs] class StackingEnsemble: r""" Direction + magnitude stacking with an out-of-fold meta-model. The two base models are evaluated with walk-forward cross-validation; their out-of-fold (OOF) predictions become the meta-features on which the meta-model is trained (e.g. with :class:`fynance.models.loss.SharpeLoss`). Using OOF predictions avoids feeding the meta-model with in-sample base predictions, the classic stacking leakage. Parameters ---------- direction_factory, magnitude_factory : callable No-arg callables returning base models (the :class:`~fynance.models._base.BaseNeuralNet` interface). Typically the direction model is trained with :class:`~fynance.models.loss.DirectionalAccuracyLoss`, the magnitude model with MSE or :class:`~fynance.models.loss.SortinoLoss`. meta_factory : callable Callable ``meta_factory(n_features) -> model`` returning the meta-model sized for the stacked features. """ def __init__( self, direction_factory: Callable, magnitude_factory: Callable, meta_factory: Callable, ): self.direction_factory = direction_factory self.magnitude_factory = magnitude_factory self.meta_factory = meta_factory
[docs] def fit_predict( self, X, y, train_period: int, test_period: int, roll_period: int, epochs: int = 1, ) -> NDArray: """ Fit the ensemble and return meta out-of-fold predictions. Parameters ---------- X, y : torch.Tensor Input and target, shapes ``(T, N)`` and ``(T, M)``. train_period, test_period, roll_period : int Walk-forward window parameters. epochs : int, optional Training passes per fold and for the meta-model. Default 1. Returns ------- np.ndarray Meta predictions of shape ``(T, M)``; rows before the first test fold (no OOF base features) are ``NaN``. """ n_out = y.shape[1] if y.ndim > 1 else 1 rb = _RollingBasis(X, y) rb(train_period=train_period, test_period=test_period, roll_period=roll_period) dir_oof = rb.cross_validate( self.direction_factory, X, y, epochs=epochs).oof_predictions mag_oof = rb.cross_validate( self.magnitude_factory, X, y, epochs=epochs).oof_predictions meta_x = np.hstack([dir_oof, mag_oof]) mask = ~np.isnan(meta_x).any(axis=1) y_np = y.numpy() if hasattr(y, 'numpy') else np.asarray(y) x_meta = torch.from_numpy(meta_x[mask].astype(np.float32)) y_meta = torch.from_numpy(y_np[mask].astype(np.float32)) meta = self.meta_factory(meta_x.shape[1]) for _ in range(epochs): meta.train_on(x_meta, y_meta) pred = meta.predict(x_meta) pred = pred.numpy() if hasattr(pred, 'numpy') else np.asarray(pred) out = np.full((X.shape[0], n_out), np.nan) out[mask] = pred.reshape(-1, n_out) return out