Source code for fynance.backtest.cost
#!/usr/bin/env python3
# coding: utf-8
""" Transaction cost models for the vectorized backtest engine.
Concretizes the :class:`~fynance.core.protocols.CostModel` seam. Two models
ship: :class:`ProportionalCost` (linear, turnover-based) and
:class:`MarketImpactCost` (adds a convex, super-linear market-impact term — the
square-root impact law).
"""
from __future__ import annotations
# Third-party packages
import numpy as np
from numpy.typing import NDArray
# Local packages
from fynance.portfolio.sizing import transaction_cost
__all__ = ['ProportionalCost', 'MarketImpactCost']
[docs]
class ProportionalCost:
""" Proportional transaction cost: ``(fee + slippage) * turnover``.
Turnover at each step is the absolute traded weight
:math:`\\sum_i |w_{t,i} - w_{t-1,i}|` (the first step charges the initial
position). Conforms to :class:`~fynance.core.protocols.CostModel`.
Parameters
----------
fee : float
Proportional fee per unit traded (e.g. ``0.001`` = 10 bps).
slippage : float
Additional proportional slippage per unit traded.
Examples
--------
>>> import numpy as np
>>> cost = ProportionalCost(fee=0.01)
>>> cost(np.array([[1.0, 0.0], [0.5, 0.5], [0.5, 0.5]]))
array([0.01, 0.01, 0. ])
"""
def __init__(self, fee: float = 0.0, slippage: float = 0.0):
""" Store the cost rates. """
self.fee = fee
self.slippage = slippage
[docs]
def __call__(self, weights: NDArray) -> NDArray[np.float64]:
""" Return the per-step proportional cost of a weight book. """
rate = self.fee + self.slippage
if rate == 0.0:
w = np.asarray(weights, dtype=np.float64)
return np.zeros(w.shape[0], dtype=np.float64)
return transaction_cost(weights, fee=rate)
[docs]
class MarketImpactCost:
""" Non-linear market-impact cost: linear fee + convex impact term.
Charges, per step, a proportional fee plus a **super-linear** market-impact
term on the same turnover (the standard square-root impact law, where the
cost grows faster than the trade size):
.. math::
c_t = fee \\cdot \\tau_t + impact \\cdot \\tau_t^{\\,exponent}
where the turnover :math:`\\tau_t = \\sum_i |w_{t,i} - w_{t-1,i}|` is defined
exactly as in :class:`ProportionalCost` (the first step charges the initial
position). With ``exponent=1`` and ``impact=0`` it reduces to
:class:`ProportionalCost`. Conforms to
:class:`~fynance.core.protocols.CostModel`.
Parameters
----------
fee : float
Proportional (linear) fee per unit traded (e.g. ``0.001`` = 10 bps).
impact : float
Coefficient of the convex impact term, in the same units as ``fee``.
exponent : float
Convexity exponent of the impact term (``> 1`` is super-linear).
Default ``1.5`` (the square-root impact law: cost per unit traded grows
as :math:`\\sqrt{\\tau}`).
Examples
--------
>>> import numpy as np
>>> cost = MarketImpactCost(fee=0.0, impact=0.1, exponent=2.0)
>>> cost(np.array([[1.0, 0.0], [0.0, 1.0], [0.0, 1.0]]))
array([0.1, 0.4, 0. ])
"""
def __init__(
self,
fee: float = 0.0,
impact: float = 0.0,
exponent: float = 1.5,
):
""" Store the cost rates and the impact convexity exponent. """
if exponent <= 0:
raise ValueError(f"exponent must be positive, got {exponent}")
self.fee = fee
self.impact = impact
self.exponent = exponent
[docs]
def __call__(self, weights: NDArray) -> NDArray[np.float64]:
""" Return the per-step (linear + convex impact) cost of a weight book. """
# transaction_cost with unit fee returns the raw per-step turnover.
turnover = transaction_cost(weights, fee=1.0)
return self.fee * turnover + self.impact * turnover ** self.exponent