Source code for fynance.models.loss.directional

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

""" Differentiable directional accuracy loss for PyTorch training loops. """

from __future__ import annotations

# Third-party packages
import torch

# Local packages
from ._base import BaseLoss

__all__ = ['DirectionalAccuracyLoss']


[docs] class DirectionalAccuracyLoss(BaseLoss): r""" Negative directional accuracy as a differentiable surrogate loss. The true directional accuracy (fraction of correctly predicted signs) is non-differentiable. This loss replaces the hard sign comparison with a sigmoid surrogate, making it suitable for gradient-based optimization. Notes ----- The surrogate loss is: .. math:: \mathcal{L} = -\frac{1}{T} \sum_{t=1}^{T} \sigma(\hat{y}_t \cdot y_t \cdot T_{emp}) where :math:`\sigma` is the logistic sigmoid, :math:`T_{emp}` is the temperature parameter controlling sharpness, and a higher temperature produces a harder approximation closer to the true 0/1 accuracy. **This is a training proxy** — the value is not comparable to the numpy :func:`~fynance.features.stats.directional_accuracy` metric, which returns a hard 0/1 fraction. Parameters ---------- rf : float, optional Not used; inherited from :class:`BaseLoss` for API consistency. period : int, optional Not used; inherited from :class:`BaseLoss` for API consistency. eps : float, optional Not used; inherited from :class:`BaseLoss` for API consistency. temperature : float, optional Sigmoid sharpness. Higher values push the surrogate closer to the hard sign indicator. Default is 1.0. Examples -------- >>> import torch >>> from fynance.models.loss import DirectionalAccuracyLoss >>> y_true = torch.tensor([1., -1., 1., -1., 1.]) >>> y_pred = torch.tensor([0.5, -0.3, 0.1, -0.8, 0.2]) >>> loss_fn = DirectionalAccuracyLoss() >>> loss = loss_fn(y_pred, y_true) >>> loss.shape torch.Size([]) >>> loss.item() < 0 # all directions correct → loss close to -1 True See Also -------- SharpeLoss, SortinoLoss """ def __init__( self, rf: float = 0., period: int = 252, eps: float = 1e-8, temperature: float = 1.0, ): super().__init__(rf=rf, period=period, eps=eps) self.temperature = temperature
[docs] def forward( self, y_pred: torch.Tensor, y_true: torch.Tensor, ) -> torch.Tensor: """ Compute the negative directional accuracy surrogate. Parameters ---------- y_pred : torch.Tensor Predicted values, shape ``(T,)`` or ``(T, M)``. y_true : torch.Tensor True values, same shape as ``y_pred``. Returns ------- torch.Tensor Scalar loss (negative sigmoid-based directional accuracy). Raises ------ TypeError If ``y_pred`` or ``y_true`` is not a :class:`torch.Tensor`. """ self._check_tensor(y_pred) self._check_tensor(y_true) return -torch.mean(torch.sigmoid(y_pred * y_true * self.temperature))