Source code for fynance.signal.mappers

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

""" Signal mappers: turn a model prediction into a position.

Pure numpy and causal: the position at ``t`` depends only on the prediction at
``t`` (the engine in :mod:`fynance.backtest` applies the causal one-step shift).

"""

from __future__ import annotations

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

# Local packages
from fynance.portfolio.sizing import vol_target

__all__ = ['sign', 'threshold', 'rank', 'vol_target_position']


[docs] def sign(pred: NDArray) -> NDArray[np.float64]: """ Long/short by the sign of the prediction (``+1`` / ``-1`` / ``0``). Examples -------- >>> import numpy as np >>> sign(np.array([0.3, -0.1, 0.0])) array([ 1., -1., 0.]) """ return np.sign(np.asarray(pred, dtype=np.float64))
[docs] def threshold( pred: NDArray, long: float = 0.0, short: float = 0.0, ) -> NDArray[np.float64]: """ Position with a flat dead-band. ``+1`` where ``pred > long``, ``-1`` where ``pred < short``, ``0`` between. Examples -------- >>> import numpy as np >>> threshold(np.array([0.5, 0.05, -0.5]), long=0.1, short=-0.1) array([ 1., 0., -1.]) """ p = np.asarray(pred, dtype=np.float64) pos = np.zeros_like(p) pos[p > long] = 1.0 pos[p < short] = -1.0 return pos
[docs] def rank(pred: NDArray, top: int, bottom: int) -> NDArray[np.float64]: """ Cross-sectional long/short by rank. For each time step (row of a 2-D ``(T, n_assets)`` prediction), go long the ``top`` highest-ranked assets and short the ``bottom`` lowest, equally weighted within each leg (dollar-neutral). Parameters ---------- pred : array-like, shape (T, n_assets) Cross-sectional predictions. top, bottom : int Number of assets in the long and short legs. Returns ------- numpy.ndarray Weights of shape ``(T, n_assets)``. """ p = np.asarray(pred, dtype=np.float64) if p.ndim != 2: raise ValueError("rank expects a 2-D (T, n_assets) prediction") weights = np.zeros_like(p) order = np.argsort(p, axis=1) for t in range(p.shape[0]): if bottom > 0: weights[t, order[t, :bottom]] = -1.0 / bottom if top > 0: weights[t, order[t, -top:]] = 1.0 / top return weights
[docs] def vol_target_position( signal: NDArray, prices: NDArray, target_vol: float = 0.15, period: int = 252, w: int = 21, max_leverage: float = 5.0, ) -> NDArray[np.float64]: """ Scale a directional signal by causal volatility-targeting leverage. Multiplies a directional ``signal`` by the leverage from :func:`fynance.portfolio.sizing.vol_target` (inverse of *past* realized volatility). Strictly causal. """ sig = np.asarray(signal, dtype=np.float64) lev = vol_target( prices, target_vol=target_vol, period=period, w=w, max_leverage=max_leverage, ) return sig * lev