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 theNullSelector
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).
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.
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