Skip to content

Cost Models

CostModel is the public API for specifying transaction costs in jQuantStats. Import it directly from the top-level package:

from jquantstats import CostModel, Portfolio

Portfolio route only

Cost modelling requires the Portfolio entry point — it needs position data to compute turnover and per-unit costs.


Overview

Every portfolio strategy incurs transaction costs. jQuantStats models these costs through CostModel, a frozen dataclass that encapsulates exactly one cost model at a time and enforces that the two models are never accidentally combined.

Model Constructor Scales with
Model A — per-unit CostModel.per_unit(cost) Units of position change
Model B — turnover-bps CostModel.turnover_bps(bps) AUM turnover
No cost CostModel.zero()

Model A: per_unit — position-delta cost

Use this model when your cost scales with the number of units traded — for example, a fixed commission per share or a tick-size cost for futures.

from jquantstats import CostModel, Portfolio

# £0.01 cost per share traded (one-way)
cost = CostModel.per_unit(0.01)

pf = Portfolio.from_cash_position(
    prices=prices,
    cash_position=positions,
    aum=1_000_000,
    cost_model=cost,
)

# NAV after deducting position-delta costs
print(pf.net_cost_nav)

# Inspect the raw per-day cost series
print(pf.position_delta_costs)
When to use per_unit
  • Equity portfolios where commissions are quoted per share.
  • Futures portfolios where tick-size friction dominates.
  • Any strategy where absolute position changes (not AUM fraction) drive costs.

Model B: turnover_bps — AUM-proportional cost

Use this model when your cost scales with notional turnover as a fraction of AUM — for example, a bid/ask spread expressed in basis points.

from jquantstats import CostModel, Portfolio

# 5 bps one-way cost on AUM turnover
cost = CostModel.turnover_bps(5.0)

pf = Portfolio.from_cash_position(
    prices=prices,
    cash_position=positions,
    aum=1_000_000,
    cost_model=cost,
)

# Sweep Sharpe ratio across 0 → 20 bps in a single call
impact = pf.trading_cost_impact(max_bps=20)
print(impact)
When to use turnover_bps
  • Macro or fund-of-funds portfolios where trades are expressed as a fraction of AUM.
  • Strategies where the bid/ask spread (in bps) is the dominant cost driver.
  • Any scenario where you want to sweep cost assumptions and see impact on risk-adjusted returns.

No transaction costs

from jquantstats import CostModel

cost = CostModel.zero()
# Equivalent to the default — no cost_model argument

Combining models is an error

CostModel enforces mutual exclusivity. Passing both a non-zero per_unit and a non-zero cost_bps raises a ValueError:

from jquantstats import CostModel

# This raises ValueError — only one model may be active at a time
CostModel(cost_per_unit=0.01, cost_bps=5.0)

Warning

Always use the named constructors — per_unit, turnover_bps, zero — rather than the dataclass constructor directly. They make your intent explicit and guarantee mutual exclusivity.


API Reference

jquantstats.CostModel dataclass

Unified representation of a portfolio transaction-cost model.

Eliminates the implicit "pick one" contract between the two independent cost parameters (cost_per_unit and cost_bps) on Portfolio. A CostModel instance encapsulates one model at a time and can be passed to any Portfolio factory method instead of specifying the raw float parameters.

Attributes:

Name Type Description
cost_per_unit float

One-way cost per unit of position change (Model A). Defaults to 0.0.

cost_bps float

One-way cost in basis points of AUM turnover (Model B). Defaults to 0.0.

Raises:

Type Description
ValueError

If cost_per_unit or cost_bps is negative, or if both are non-zero (which would silently double-count costs).

Examples:

>>> CostModel.per_unit(0.01)
CostModel(cost_per_unit=0.01, cost_bps=0.0)
>>> CostModel.turnover_bps(5.0)
CostModel(cost_per_unit=0.0, cost_bps=5.0)
>>> CostModel.zero()
CostModel(cost_per_unit=0.0, cost_bps=0.0)
Source code in src/jquantstats/_cost_model.py
@dataclasses.dataclass(frozen=True)
class CostModel:
    """Unified representation of a portfolio transaction-cost model.

    Eliminates the implicit "pick one" contract between the two independent
    cost parameters (``cost_per_unit`` and ``cost_bps``) on
    `Portfolio`.  A ``CostModel``
    instance encapsulates one model at a time and can be passed to any
    Portfolio factory method instead of specifying the raw float parameters.

    Attributes:
        cost_per_unit: One-way cost per unit of position change (Model A).
            Defaults to 0.0.
        cost_bps: One-way cost in basis points of AUM turnover (Model B).
            Defaults to 0.0.

    Raises:
        ValueError: If ``cost_per_unit`` or ``cost_bps`` is negative, or if
            both are non-zero (which would silently double-count costs).

    Examples:
        >>> CostModel.per_unit(0.01)
        CostModel(cost_per_unit=0.01, cost_bps=0.0)
        >>> CostModel.turnover_bps(5.0)
        CostModel(cost_per_unit=0.0, cost_bps=5.0)
        >>> CostModel.zero()
        CostModel(cost_per_unit=0.0, cost_bps=0.0)
    """

    cost_per_unit: float = 0.0
    cost_bps: float = 0.0

    def __post_init__(self) -> None:
        if self.cost_per_unit < 0:
            raise ValueError(f"cost_per_unit must be non-negative, got {self.cost_per_unit}")  # noqa: TRY003
        if self.cost_bps < 0:
            raise ValueError(f"cost_bps must be non-negative, got {self.cost_bps}")  # noqa: TRY003
        if self.cost_per_unit > 0 and self.cost_bps > 0:
            raise ValueError(  # noqa: TRY003
                "Only one cost model may be active at a time: "
                f"got cost_per_unit={self.cost_per_unit} and cost_bps={self.cost_bps}. "
                "Use CostModel.per_unit() or CostModel.turnover_bps() to make intent explicit."
            )

    # ── Named constructors ────────────────────────────────────────────────────

    @classmethod
    def per_unit(cls, cost: float) -> CostModel:
        """Create a Model A (position-delta) cost model.

        Args:
            cost: One-way cost per unit of position change.  Must be
                non-negative.

        Returns:
            A `CostModel` with ``cost_per_unit=cost`` and
            ``cost_bps=0.0``.

        Examples:
            >>> CostModel.per_unit(0.01)
            CostModel(cost_per_unit=0.01, cost_bps=0.0)
        """
        return cls(cost_per_unit=cost, cost_bps=0.0)

    @classmethod
    def turnover_bps(cls, bps: float) -> CostModel:
        """Create a Model B (turnover-bps) cost model.

        Args:
            bps: One-way cost in basis points of AUM turnover.  Must be
                non-negative.

        Returns:
            A `CostModel` with ``cost_per_unit=0.0`` and
            ``cost_bps=bps``.

        Examples:
            >>> CostModel.turnover_bps(5.0)
            CostModel(cost_per_unit=0.0, cost_bps=5.0)
        """
        return cls(cost_per_unit=0.0, cost_bps=bps)

    @classmethod
    def zero(cls) -> CostModel:
        """Create a zero-cost model (no transaction costs).

        Returns:
            A `CostModel` with both parameters set to 0.0.

        Examples:
            >>> CostModel.zero()
            CostModel(cost_per_unit=0.0, cost_bps=0.0)
        """
        return cls(cost_per_unit=0.0, cost_bps=0.0)

per_unit(cost) classmethod

Create a Model A (position-delta) cost model.

Parameters:

Name Type Description Default
cost float

One-way cost per unit of position change. Must be non-negative.

required

Returns:

Type Description
CostModel

A CostModel with cost_per_unit=cost and

CostModel

cost_bps=0.0.

Examples:

>>> CostModel.per_unit(0.01)
CostModel(cost_per_unit=0.01, cost_bps=0.0)
Source code in src/jquantstats/_cost_model.py
@classmethod
def per_unit(cls, cost: float) -> CostModel:
    """Create a Model A (position-delta) cost model.

    Args:
        cost: One-way cost per unit of position change.  Must be
            non-negative.

    Returns:
        A `CostModel` with ``cost_per_unit=cost`` and
        ``cost_bps=0.0``.

    Examples:
        >>> CostModel.per_unit(0.01)
        CostModel(cost_per_unit=0.01, cost_bps=0.0)
    """
    return cls(cost_per_unit=cost, cost_bps=0.0)

turnover_bps(bps) classmethod

Create a Model B (turnover-bps) cost model.

Parameters:

Name Type Description Default
bps float

One-way cost in basis points of AUM turnover. Must be non-negative.

required

Returns:

Type Description
CostModel

A CostModel with cost_per_unit=0.0 and

CostModel

cost_bps=bps.

Examples:

>>> CostModel.turnover_bps(5.0)
CostModel(cost_per_unit=0.0, cost_bps=5.0)
Source code in src/jquantstats/_cost_model.py
@classmethod
def turnover_bps(cls, bps: float) -> CostModel:
    """Create a Model B (turnover-bps) cost model.

    Args:
        bps: One-way cost in basis points of AUM turnover.  Must be
            non-negative.

    Returns:
        A `CostModel` with ``cost_per_unit=0.0`` and
        ``cost_bps=bps``.

    Examples:
        >>> CostModel.turnover_bps(5.0)
        CostModel(cost_per_unit=0.0, cost_bps=5.0)
    """
    return cls(cost_per_unit=0.0, cost_bps=bps)

zero() classmethod

Create a zero-cost model (no transaction costs).

Returns:

Type Description
CostModel

A CostModel with both parameters set to 0.0.

Examples:

>>> CostModel.zero()
CostModel(cost_per_unit=0.0, cost_bps=0.0)
Source code in src/jquantstats/_cost_model.py
@classmethod
def zero(cls) -> CostModel:
    """Create a zero-cost model (no transaction costs).

    Returns:
        A `CostModel` with both parameters set to 0.0.

    Examples:
        >>> CostModel.zero()
        CostModel(cost_per_unit=0.0, cost_bps=0.0)
    """
    return cls(cost_per_unit=0.0, cost_bps=0.0)