The BaseCost class provides a foundational structure for creating custom cost models.
Below is a step-by-step guide for extending BaseCost.
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 values_in_time
Subclass BaseCost
to implement your desired cost model.
class CustomCost(BaseCost):
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
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 h_plus
, and a series of trades indexed by asset u
, return the sum of costs for all assets.
See ShortHoldingCost for inspiration:
def get_actual_cost(
self, t: dt.datetime, h_plus: pd.Series, u: pd.Series
) -> pd.Series:
"""Method that calculates per-period (short position) holding costs given period `t` holdings and trades.
Parameters
----------
t : datetime.datetime
The datetime for associated trades `u` and holdings plus trades `h_plus`.
h_plus : pandas.Series
Holdings at beginning of period t, plus trades for period `t` (`u`). Same as `u` + `h` for `t`.
u : pandas.Series
Trades (as values) for period `t`.
"""
return sum(-np.minimum(0, h_plus) * self._get_short_rate(t))
def _get_short_rate(self, t):
return values_in_time(self.short_rates, t)
_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 w_plus
, and a numpy-like array of trade weights z
, 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, w_plus, z, 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(w_plus))
return cvx.sum(expression), []
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).
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
)