Weights Model Pipeline

The ModelPipeline class provides a convenient mechanism to chain together several Selector models with a Portfolio Optimization model (i.e., Risk-based, Naïve, or Greedy) to build a complex multi-stage portfolio weights model.

Its constructor takes as a parameter a list of models, [S_1, ..., S_N, Opt], where S_i for i=1, ...,N is a Selector Model objects and Opt is an Optimizer Model object. It is imperative that the last, and only the last, element of the list is an Optimizer Model object (e.g., instances of az.CVaRAnalyzer, az.InvVol, az.KellyEngine, etc. classes).

Note the following exceptions:

  • A single element list will contain only an optimizer and no selectors, i.e., [Opt],

  • A None element in the list will be ignored, therefore the following expressions are equivalent: [Opt], [None, Opt], [NullSelector(), Opt]. Although, in the last sequence the NullSelector instance will be executed,

  • In general, Opt is a valid instance of a Risk-based, Naïve, or Greedy portfolio weights classes. An exception is made for Equal Weighted Portfolio where the string "EWP" can be passed as a shortcut, e.g., [S_1, ..., S_N, "EWP"].

Once constructed, the ModelPipeline object can be interrogated for the optimal weights or it can be passed to a Port_Generator object for backtesting (out-of-sample testing).

TOP

ModelPipeline class

class azapy.Generators.ModelPipeline.ModelPipeline(sequence=[<azapy.Selectors.NullSelector.NullSelector object>, 'EWP'])

Bases: object

Construct a portfolio weights model from a sequence of elementary models. The last element of the sequence must be an optimizer model while the rest could be any number of selector models.

Attributes
  • sequence : list - the sequence of elementary models

  • capital : flat - the capital at risk as a fraction of the total capital

  • mktdata : pandas.DataFrame - historical market data of selected symbols

  • active_symb : list - the list of selected symbols

  • ww : pandas.Series - portfolio weights per symbol (all symbols)

Note All unselected symbols have 0 weight. However, some of the selected symbols can have 0 weight after portfolio optimization stage.

Methods

getPositions([nshares, cash, ww, nsh_round, ...])

Computes the rebalanced number of shares.

getWeights(mktdata, **params)

Computes the portfolio weights.

__init__(sequence=[<azapy.Selectors.NullSelector.NullSelector object>, 'EWP'])

Constructor

Parameters:
sequencelist, optional

List of elementary models. The last element of the list must be an optimizer while the rest could be any number of selectors. The sequence is executed from right to left. The default is [NullSelector(), “EWP”].

Returns:
The object.
getPositions(nshares=None, cash=0.0, ww=None, nsh_round=True, verbose=True)

Computes the rebalanced number of shares.

Parameters:
nsharespanda.Series, optional

Initial number of shares per portfolio component. A missing component entry will be considered 0. A None value assumes that all components entries are 0. The name of the components must be present in the mrkdata. The default is None.

cashfloat, optional

Additional cash to be added to the capital. A negative entry assumes a reduction in the total capital available for rebalance. The total capital cannot be < 0. The default is 0.

wwpanda.Series, optional

External overwrite portfolio weights. If it not set to None these weights will overwrite the calibration results. The default is None.

nsh_roundBoolean, optional

If it is True the invested numbers of shares are round to the nearest integer and the residual cash capital (positive or negative) is carried to the next reinvestment cycle. A value of False assumes investments with fractional number of shares (no rounding). The default is True.

verboseBoolean, optional

Is it set to True the function prints the closing prices date. The default is True.

Returns:
`pandas.DataFrame`the rolling information.
Columns:
  • ‘old_nsh’ :

    initial number of shares per portfolio component and the additional cash. These are input values.

  • ‘new_nsh’ :

    the new number of shares per component plus the residual cash (due to the rounding to an integer number of shares). A negative entry means that the investor needs to add more cash to cover for the roundup shortfall. It has a small value.

  • ‘diff_nsh’ :

    number of shares (buy/sale) needed to rebalance the portfolio.

  • ‘weights’ :

    portfolio weights used for rebalancing. The cash entry is the new portfolio value (invested capital).

  • ‘prices’ :

    the share prices used for rebalance evaluations.

Note: Since the prices are closing prices, the rebalance can be computed after the market close and before the trading execution (next day). Additional cash slippage may occur due to share price differential between the previous day closing and execution time.

getWeights(mktdata, **params)

Computes the portfolio weights.

Parameters:
mktdatapandas.DataFrame

Historical daily market data as returned by azapy.readMkT function.

**paramsoptional

Additional parameters that may be required by the elementary models. An example is verbose=True.

Returns:
`pandas.Series`Portfolio weights per symbol.

TOP

Example ModelPipeline

# Examples
import numpy as np

import azapy as az
print(f"azapy version {az.version()}", flush=True)

#==============================================================================
# collect market data
mktdir = '../../MkTdata'
sdate = '2012-01-01'
edate = '2021-07-27'

symb = ['GLD', 'TLT', 'IHI', 'VGT', 'OIH',
        'XAR', 'XBI', 'XHE', 'XHS', 'XLB',
        'XLE', 'XLF', 'XLI', 'XLK', 'XLU', 
        'XLV', 'XLY', 'XRT', 'SPY', 'ONEQ', 
        'QQQ', 'DIA', 'ILF', 'XSW', 'PGF', 
        'IDV', 'JNK', 'HYG', 'SDIV', 'VIG', 
        'SLV', 'AAPL', 'MSFT', 'AMZN', 'GOOG', 
        'IYT', 'VIG', 'IWM', 'BRK-B', 'ITA']

mktdata = az.readMkT(symb, sdate=sdate, edate=edate, file_dir=mktdir, 
                     verbose=False)

print(f"mktdata type {type(mktdata)}")

#==============================================================================
# build CorrClusterSelector
ccs = az.CorrClusterSelector()

# build a DualMomentumSelector

# maximum number of selected symbol
nw = 5 
# minimum number of symbols with positive momentum 
#   for a full capital allocation -
#   in our case roughly 80% of the initial number of symbols
ths = np.floor(len(symb) * 0.8)

dms = az.DualMomentumSelector(nw=nw, threshold=ths)

# buid a CVaR optimizer
alpha = [0.95, 0.9]
hlength = 1.25
freq = 'Q'

cvar = az.CVaRAnalyzer(alpha=alpha, freq=freq, hlength=hlength)

# build the ModelPipeline
model = az.ModelPipeline([ccs, dms, cvar])

# compute 
ww = model.getWeights(mktdata, verbose=True)
capital_at_risk = model.capital
active_symb = model.active_symb

print("\n")
print(f"active symbols {active_symb}")
print(f"capital at risk {capital_at_risk}")
print(f"active symbols weights\n{ww[active_symb]}")
# Note: the sum of the weights is the vale of capital at risk
# the rest is assumed to be allocated in cash

TOP