Often it is desirable to incorporate multiple factors into portfolio allocation decisions. There are different ways to combine factors. For example, you could select the top quantile of stocks based on factor $A$, then within that quantile select the top 100 stocks ranked by factor $B$. Another way, which we will look at in this notebook, is to combine multiple factors into a single score. The Altman Z-Score is an example of a multi-factor score. It assigns different weights to various fundamental ratios and sums the weighted ratios into a score.
Another well-known multi-factor score is the Piotroski F-Score, which measures firm quality. Unlike the Altman Z-Score which sums weighted ratios, the Piotroski F-Score assigns companies 1 point for each of 9 true or false conditions, such as whether this year's return on assets is higher than last year's, resulting in a possible score of 0-9.
In this notebook, we'll explore a simpler example of a multi-factor score based on boolean conditions. We will assign companies 1 point if they are "zombies" having interest coverage ratios below 1, and 1 point if their Altman Z-Score is in the distress zone. This will result in a possible score of 0 (if neither condition is met), 1, or 2 (if both conditions are met).
To accomplish this in Pipeline, we create our True/False conditions, then convert them to 1s and 0s using the as_factor()
method, then add the 1s and 0s to create the score:
from zipline.pipeline import sharadar
from codeload.fundamental_factors.universe import CommonStocks, BaseUniverse
universe = BaseUniverse()
icr = sharadar.InterestCoverageRatio('ART', mask=universe)
altman = sharadar.AltmanZScore('ART', mask=universe)
distress_score = (icr < 1).as_factor() + (altman < 0).as_factor()
Let's create a Pipeline with our distress score, which we will analyze with Alphalens. We include size and volatility quantiles to be used as grouping factors:
from zipline.pipeline import Pipeline
from zipline.pipeline.factors import AnnualizedVolatility
fundamentals = sharadar.Fundamentals.slice("ART")
marketcap = fundamentals.MARKETCAP.latest
pipeline = Pipeline(
columns={
'distress_score': distress_score,
'size': marketcap.quantiles(5, mask=universe),
'volatility': AnnualizedVolatility(mask=universe).quantiles(5)
},
initial_universe=CommonStocks(),
screen=universe
)
As in the previous notebook, we need to use the bins
argument to define bin edges for Alphalens that correspond to our 3 possible scores. Since the possible scores are 0, 1, and 2, we can define the bin edges as [-1, 0, 1, 2]
. When defining bins, each bin includes the rightmost edge but not the leftmost edge. In other words, our first bin, -1, 0
, includes everything where $-1 < n <= 0$ (and thus includes 0), our second bin, 0, 1
, includes everything where $0 < n <= 1$ (and thus includes 1), and so on.
import alphalens as al
al.from_pipeline(
pipeline,
start_date="1999-02-01",
end_date="2022-12-30",
periods=[1, 5, 21],
factor="distress_score",
bins=[-1, 0, 1, 2],
groupby=[
"size",
"volatility"
],
segment="Y"
)
min | max | mean | std | count | avg daily count | count % | |
---|---|---|---|---|---|---|---|
Distress Score Quantile | |||||||
1 | 0.000 | 0.000 | 0.000 | 0.000 | 21,772,524 | 3616.7 | 89.3% |
2 | 1.000 | 1.000 | 1.000 | 0.000 | 2,548,361 | 423.3 | 10.5% |
3 | 2.000 | 2.000 | 2.000 | 0.000 | 57,142 | 9.5 | 0.2% |
1D | 21D | 5D | |
---|---|---|---|
Ann. alpha | -0.017 | -0.030 | -0.025 |
beta | 0.052 | 0.197 | 0.123 |
Mean Relative Return Top Quantile (bps) | 1.109 | 0.831 | 1.771 |
Mean Relative Return Bottom Quantile (bps) | 0.065 | 0.046 | 0.060 |
Mean Spread (bps) | 1.044 | -1.085 | -0.105 |
1D | 21D | 5D | |
---|---|---|---|
IC Mean | -0.016 | -0.033 | -0.024 |
IC Std. | 0.047 | 0.066 | 0.057 |
Risk-Adjusted IC | -0.343 | -0.502 | -0.419 |
t-stat(IC) | -26.618 | -38.953 | -32.517 |
p-value(IC) | 0.000 | 0.000 | 0.000 |
IC Skew | -0.095 | 0.392 | 0.072 |
IC Kurtosis | 1.095 | 0.541 | 0.827 |
/opt/conda/lib/python3.11/site-packages/scipy/stats/_stats_py.py:5445: ConstantInputWarning: An input array is constant; the correlation coefficient is not defined. warnings.warn(stats.ConstantInputWarning(warn_msg))
1D | 5D | 21D | |
---|---|---|---|
Quantile 1 Mean Turnover | 0.002 | 0.012 | 0.040 |
Quantile 2 Mean Turnover | 0.008 | 0.038 | 0.145 |
Quantile 3 Mean Turnover | 0.012 | 0.056 | 0.210 |
1D | 21D | 5D | |
---|---|---|---|
Mean Factor Rank Autocorrelation | 0.996 | 0.908 | 0.978 |
1D | 21D | 5D | factor | factor_quantile | size | volatility | ||
---|---|---|---|---|---|---|---|---|
date | asset | |||||||
1999-02-01 | Equity(FIBBG000HRRSR1 [AAC1]) | 0.013810 | -0.205392 | -0.081982 | 0.0 | 1 | 2 | 3 |
Equity(FIBBG000BGLR53 [AACE]) | 0.035714 | -0.098214 | 0.000000 | 0.0 | 1 | 2 | 2 | |
Equity(FIBBG000M7KQ09 [AAI]) | 0.000000 | 0.125000 | 0.250000 | 0.0 | 1 | -1 | 3 | |
Equity(FIBBG000H83MP4 [AAI1]) | 0.035429 | -0.214286 | -0.107429 | 1.0 | 2 | 2 | 4 | |
Equity(FIBBG000BD1373 [AAIC]) | -0.020376 | -0.050157 | 0.156740 | 0.0 | 1 | 2 | 3 | |
... | ... | ... | ... | ... | ... | ... | ... | ... |
2022-12-30 | Equity(FIBBG011RWR2Q4 [ACRV]) | -0.040000 | 0.350000 | -0.033333 | 0.0 | 1 | 1 | 3 |
Equity(FIBBG00ZSB4TS9 [DRS]) | -0.009302 | 0.034884 | -0.018605 | 0.0 | 1 | -1 | 3 | |
Equity(FIBBG000BCVMH9 [CP]) | -0.010874 | 0.046280 | 0.028113 | 0.0 | 1 | 4 | 0 | |
Equity(FIBBG000K5M1S8 [ENB]) | -0.004329 | 0.043290 | 0.031831 | 0.0 | 1 | 4 | 0 | |
Equity(FIBBG000C32XT3 [IMO]) | 0.004534 | 0.128607 | -0.004328 | 0.0 | 1 | 4 | 1 |
24378027 rows × 7 columns
min
/max
: Here, we can validate our bin edges. Factor quantile 1 contains score 0 (non-zombies, non-distressed); quantile 2 contains score 1 (zombies OR distressed); and quantile 3 contains score 2 (zombies AND distressed).count %
: Only 0.2% of stocks fall into quantile 3, indicating that it is very rare for a company to be both a zombie and distressed.Relative Cumulative Return By Quantile
: companies that are zombies and/or in distress perform worse than other companies. Quantile 3 (zombies AND distressed) performs similarly to quantile 2 (zombies OR distressed). In other words, being both a zombie and distressed does not result in worse returns than being only one or the other. Combined with the tiny size of quantile 3, this suggests that the best way to combine the interest coverage ratio and Altman Z-Score is to OR them, not AND them.