Source code for fynance.models.loss.omega
#!/usr/bin/env python3
# coding: utf-8
""" Differentiable Omega-ratio loss. """
from __future__ import annotations
# Third-party packages
import torch
# Local packages
from ._base import MAX_RATIO as _MAX_RATIO
from ._base import BaseLoss
__all__ = ['OmegaLoss']
[docs]
class OmegaLoss(BaseLoss):
r""" Negative Omega ratio as a differentiable loss.
:math:`\Omega = \frac{E[\max(r - L, 0)]}{E[\max(L - r, 0)] + \varepsilon}`,
the ratio of expected gains to expected losses relative to a threshold
``L``. Fully differentiable through :func:`torch.relu`. Minimizing the
loss maximizes the Omega ratio.
Notes
-----
Both gains and losses are :math:`O(|r - L|)`, so a fixed absolute
``eps`` is dimensionally wrong: on an all-gains batch (zero losses) the
ratio would explode (e.g. ``-1e6``) and dominate gradients. The
denominator is therefore floored with a **returns-scaled** value
``|r - L|.mean() / MAX_RATIO`` (plus a bare ``eps`` backstop for the
degenerate all-zero-diff batch), capping the ratio at roughly
``MAX_RATIO`` in the low-loss regime regardless of the return scale.
The ratio is then passed through a **smooth saturating map**,
``MAX_RATIO * tanh(ratio / MAX_RATIO)``, instead of a hard clamp. A
hard clamp pins the loss to a constant on an all-gains batch and so
zeroes the gradient in exactly the regime training still wants to push
on; ``tanh`` is near-linear for normal-regime ratios (leaving their
numerics unchanged) yet keeps a residual, non-zero gradient when the
ratio is large. This keeps the loss finite and bounded while
preserving the sign convention (minimizing it maximizes the ratio).
Parameters
----------
threshold : float, optional
Return threshold ``L`` separating gains from losses. Default 0.
**kwargs
Forwarded to :class:`BaseLoss` (``rf``, ``period``, ``eps``).
"""
def __init__(self, threshold: float = 0., **kwargs):
super().__init__(**kwargs)
self.threshold = threshold
[docs]
def forward(
self, y_pred: torch.Tensor, y_true: torch.Tensor | None = None,
) -> torch.Tensor:
""" Compute the negative Omega ratio (scalar). """
self._check_tensor(y_pred)
diff = y_pred - self.threshold
gains = torch.relu(diff).mean()
losses = torch.relu(-diff).mean()
# Returns-scaled floor: a fixed absolute eps is dimensionally wrong for
# O(|r - L|) losses and lets the ratio explode on an all-gains batch.
# Scaling by ``|r - L|.mean() / MAX_RATIO`` caps the ratio at ~MAX_RATIO
# in the low-loss regime; the bare eps backstop guards the degenerate
# all-zero-diff case.
floor = diff.abs().mean() / _MAX_RATIO + self.eps
ratio = gains / torch.clamp(losses, min=floor)
# Smooth saturating map instead of a hard clamp: tanh is near-linear for
# normal-regime ratios (numerics unchanged) but keeps a non-zero
# gradient when the ratio is large, unlike a clamp that zeroes it.
return -_MAX_RATIO * torch.tanh(ratio / _MAX_RATIO)