Creating Custom Cost Models

Extending BaseCost

The BaseCost class provides a foundational structure for creating custom cost models.

Below is a step-by-step guide for extending BaseCost.

Import Required Modules:

First, ensure you have the necessary modules imported:

import datetime as dt
import pandas as pd
import numpy as np
from investos.portfolio.cost_model import BaseCost
from investos.util import get_value_at_t

Define the Custom Cost Class:

Subclass BaseCost to implement your desired cost model.

class CustomCost(BaseCost):

Initialize Custom Attributes (Optional):

You may want to add additional attributes specific to your cost model. Override the __init__ method:

def __init__(self, *args, custom_param=None, **kwargs):
    super().__init__(*args, **kwargs)
    self.custom_param = custom_param

Implement the get_actual_cost Method:

This is the core method where your cost logic resides.

Given a datetime t, a series of holdings indexed by asset dollars_holdings_plus_trades, and a series of trades indexed by asset dollars_trades, return the sum of costs for all assets.

See ShortHoldingCost for inspiration:

def get_actual_cost(
    self, t: dt.datetime, dollars_holdings_plus_trades: pd.Series, dollars_trades: pd.Series
) -> pd.Series:
    """Method that calculates per-period (short position) holding costs given period `t` holdings and trades.
    """
    return sum(
        -np.minimum(0, dollars_holdings_plus_trades) * self._get_short_rate(t)
    )

def _get_short_rate(self, t):
    return get_value_at_t(self.short_rates, t)

Implement the _estimated_cost_for_optimization Method (Optional):

If you're using a convex optimization based investment strategy, _estimated_cost_for_optimization is used to return a cost expression for optimization.

Given a datetime t, a numpy-like array of holding weights weights_portfolio_plus_trades, a numpy-like array of trade weights weights_trades, and portfolio_value, return a two item tuple containing a cvx.sum(expression) and a (possibly empty) list of constraints.

See ShortHoldingCost for inspiration:

def _estimated_cost_for_optimization(
    self, t, weights_portfolio_plus_trades, weights_trades, portfolio_value
):
    """Estimated holding costs.

    Used by optimization strategy to determine trades.

    Not used to calculate simulated holding costs for backtest performance.
    """
    expression = cvx.multiply(
        self._get_short_rate(t), cvx.neg(weights_portfolio_plus_trades)
    )

    return cvx.sum(expression), []

Implement Helper Methods (Optional):

You can add custom helper methods to factor in specific logic or utilities that help in constructing your cost model (and help in keeping your logic understandable).

Test Your Cost Model:

You can test that your custom cost model generates costs as expected for a specific datetime period:

actual_returns = pd.DataFrame(...)  # Add your data here. Each asset should be a column, and it should be indexed by datetime
initial_holdings = pd.Series(...)  # Holding values, indexed by asset

strategy = SPO(
    actual_returns=actual_returns,
    costs=[CustomCost]
)

trade_list = strategy.generate_trade_list(
    initial_holdings,
    dt.datetime.now()
)

You can also plug your custom cost model into BacktestController (through your investment strategy) to run a full backtest!

backtest_controller = inv.portfolio.BacktestController(
    strategy=strategy
)