6 Risikobasierte Faktoren: Stock Beta
6.1 Hintergrund und Motivation
Eine Basisimplikation des Capital Asset Pricing Modells (CAPM) ist, dass alle Investoren in das Portfolio mit der höchsten erwarteten Überschussrendite pro Risikoeinheit (Sharpe Ratio) investieren und ihre Position in diesem Portfolio entsprechend ihrer Risikopräferenz mit Fremdkapital entweder hebeln (“leveraging” - erhöhen) oder enthebeln (“de-leveraging” – reduzieren). Viele Anleger, wie z.B. Privatpersonen, Pensionsfonds und Investmentfonds, haben jedoch nur eine eingeschränkte (oder gar keine) Möglichkeiten, Leverage (Fremdkapital) einzusetzen, und müssen daher riskante (High-Beta) Wertpapiere übergewichten, anstatt gehebelte Positionen in Low-Beta Aktien einzugehen. Diese aus Leveragebeschränkungen resultierende Präferenz für High-Beta Aktien kann dazu führen, dass im Gleichgewicht risikoreiche Wertpapiere mit hohem Beta niedrigere risikobereinigte Renditen erfordern als Wertpapiere mit niedrigem Beta, für die eine Hebelwirkung (d.h., eine Position unter Einsatz von Fremdkapital) erforderlich ist. Im Ergebnis bedeutet dies: Low-Beta gleich High-Alpha, und High-Beta gleich Low-Alpha! (Alpha bezeichnet hier risikoadjustierte Renditen)
Tatsächlich ist die Wertpapiermarktlinie für US-Aktien im Vergleich zum CAPM zu flach (Black, Jensen und Scholes, 1972) und lässt sich durch das CAPM mit eingeschränkter Kreditaufnahme besser erklären als durch das Standard-CAPM (siehe schon Black, 1972).
In einem viel zitierten Aufsatz stellen Frazzini und Pedersen (FP) (“Betting against beta”, Journal of Financial Economics, 2014, p. 1-25) eine “Long minus Short” Faktorstrategie vor, mit dem Ziel die oben skizzierte Beta Anomalie auszunutzen. Sie konstruieren dabei einen “Betting Against Beta” (BAB) Faktor, der im Kern Low-Beta Aktien übergewichtet und High-Beta Aktien untergewichtet. Sie wenden die Faktorkonstruktion auf zahlreiche Assetklassen (US und 20 internationale Aktienmärkte, US-Staatsanleihen und Unternehmensanleihen, Future Märkte) an und können zeigen, dass die Faktorstrategie überall persistente, positive risikoadjustierte Renditen erwirtschaftet.
Im folgenden implementieren wir die Basisversion der FP BAB Faktorstrategie.
6.2 Beginn der Fallstudie
Wir beginnen mit dem Laden der notwendigen Pakete.
Wir setzen die BAB Faktorstrategie am Beispiel des S&P500 Universums um. Zunächst laden wir zwei Datensätze in Form von DataFrames, eines für die täglichen Aktienkurshistorien der S&P500 Mitglieder und eines für die Zeitreihe der Indexwerte des S&P500. Wir mergen beide Datensätze in dem DataFrame joined_df.
Code
# load of data: df with S&P500 constituents and df with S&P500 index values
%cd "C:\Users\Galina\Documents\Thomas\Python Projekte\examplefiles"
constituents = pd.read_csv('s&p_500_15112019.csv',
parse_dates=True, index_col=0)
index = pd.read_csv('s&p_500_012000_102019.csv',parse_dates=True, index_col=0)
sp_index = index['Adj Close']
sp_index.name = 'SP_Index'
# adjustment for different frequency of constituents and index
joined_df = pd.merge(constituents, sp_index, how='inner', on='Date')C:\Users\Galina\Documents\Thomas\Python Projekte\examplefiles
| ABT | ABBV | ABMD | ACN | ATVI | ADBE | AMD | AAP | AES | AMG | ... | WYNN | XEL | XRX | XLNX | XYL | YUM | ZBH | ZION | ZTS | SP_Index | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | |||||||||||||||||||||
| 2017-01-03 | 36.866280 | 54.056015 | 112.360001 | 110.738106 | 35.941536 | 103.480003 | 11.43 | 169.755737 | 10.266887 | 141.927734 | ... | 81.712685 | 37.324787 | 25.328613 | 55.914982 | 47.835201 | 60.320660 | 101.052895 | 40.778381 | 52.556099 | 2257.830078 |
| 2017-01-04 | 37.158947 | 54.818222 | 115.739998 | 111.004356 | 36.647812 | 104.139999 | 11.43 | 171.148819 | 10.178833 | 145.451904 | ... | 84.347366 | 37.490185 | 26.284410 | 55.507954 | 48.548149 | 60.540157 | 101.981964 | 41.363899 | 53.066067 | 2270.750000 |
| 2017-01-05 | 37.479931 | 55.233967 | 114.809998 | 109.340340 | 37.216755 | 105.910004 | 11.24 | 171.029388 | 10.046756 | 143.670349 | ... | 85.431145 | 37.490185 | 26.174128 | 54.835869 | 48.104961 | 60.740559 | 102.637207 | 40.693394 | 52.889545 | 2269.000000 |
| 2017-01-06 | 38.499535 | 55.251297 | 115.419998 | 110.585968 | 37.187328 | 108.300003 | 11.32 | 168.790543 | 10.407769 | 143.349106 | ... | 86.356071 | 37.600452 | 25.769753 | 55.896046 | 47.767757 | 61.475353 | 102.646980 | 40.957817 | 53.056259 | 2276.979980 |
| 2017-01-09 | 38.461781 | 55.615067 | 117.110001 | 109.349854 | 36.981331 | 108.570000 | 11.49 | 168.691055 | 10.152417 | 139.630264 | ... | 86.655045 | 37.030743 | 25.622707 | 55.905521 | 47.565422 | 61.647121 | 104.642014 | 40.513969 | 52.909161 | 2268.899902 |
5 rows × 503 columns
Wir werden in drei Schritten die BAB Faktorstrategie von FP implementieren. Zunächst schreiben wir eine Funktion (calc_beta), die den renditeprognostizierenden Faktor (das Aktienbeta zum Marktindex) für jede Aktie täglich auf Basis rollierender Zeitfenster berechnet. Danach verwenden wir diesen Faktor in der Funktion bab_weights um zwei Faktorportfolios zu bilden. Das Long (Low-Beta) Portfolio enthält Aktien mit niedrigem Beta, und das Short (High-Beta) Portfolio beinhaltet die Aktien, die stärker mit dem Markt variieren. In der dritten Funktion bab_strat führen wir alles zusammen, konstruieren den BAB “Long minus Short” Faktor, wobei die beiden Portfolios jeweils mit der Inversen ihres Betas skaliert werden, und berechnen für eine gegebene Halteperiode die annualisierte Sharpe-Ratio der Faktorstrategie.
6.2.1 Ex Ante Betas als Faktor
Das geschätze \(\beta\) für Aktie \(i\) ist definiert durch (siehe FP, 2014, Gleichung 14):
\[\beta^{TS}_{i}=\rho\frac{\sigma_{i}}{\sigma_{m}}\]
Hierbei sind \(\sigma_{i}\) und \(\sigma_{m}\) die historisch geschätzen Renditevolatilitäten der Aktie \(i\) und des Marktes und \(\rho\) deren geschätzte Korrelation. FP schätzen Volatilitäten und Korrelationen separat. Konkret verwenden sie 1-Tages Log-Renditen für Volatilitäten und überlappende 3-Tages Log-Renditen für Korrelationen. Die jeweiligen gleitenden Zeitfenster für die täglichen rollierenden Schätzungen betragen 1 Jahr bei Volatilitäten und 5 Jahre für Korrelationen.
Um den Einfluss von Ausreißern zu mindern werden die obigen (täglichen) Zeitreihen-Betas \(\beta^{TS}_{i}\) in Richtung des Querschnittsmittelwertes \(\beta^{XS}\) über folgende Gleichung geschrumpft (sogenannte “Shrinkage”-Schätzung; siehe FP, 2014, Gleichung 15):
\[\beta_{i}=w_{i}\beta^{TS}_{i}+(1-w_{i})\beta^{XS}\]
Zur Vereinfachung setzen FP \(w=0.6\) und \(\beta^{XS}=1\) für alle Zeitpunkte und Wertpapiere.
Wir implementieren die Berechnung der FP Betas in der Funktion calc_beta. Diese benötigt als Eingabe ein DataFrame mit täglichen Historien an Aktienkursen und Marktindexwerten. Über apply(lambda...) erstellen wir jeweils DataFrames mit täglichen Zeitreihen von 1-Tages und überlappenden (kumulativen) 3-Tages Log-Renditen. Diese DataFrames werden über die rolling-Funktionalität transformiert in DataFrames mit Standardabweichungen (stdev), dem Verhältnis der Aktienvolatilität zur Marktvolatilität (stdev_ratio), und Renditekorrelationen (corr) mit dem Marktindex (Spalte ‘SP_Index’). Die Zeitreihenbetas \(\beta^{TS}_{i}\) ergeben sich aus der zellenweisen Multiplikation von corr und stdev_ratio. Die geschrumpften Betas \(\beta_{i}\) sind im finalen DataFrame beta_shrink enthalten.
Code
# calculation of Frazzini and Pedersen (2014) beta (equations (14) and (15))
def calc_beta(joined_df):
joined_df = joined_df.asfreq('B').fillna(method='pad')
# calculation of log-returns (1 day and 3 days-overlapping)
log1_rets = joined_df.apply(lambda x: np.log(x/x.shift(1)))
log3_rets = joined_df.apply(lambda x: np.log(x/x.shift(3)))
# individual calculation of volas, correlation and finally betas
stdev = log1_rets.rolling(250, min_periods=120).std()
stdev_ratio = stdev.div(stdev['SP_Index'], axis=0)
corr = log3_rets.rolling(250, min_periods=120).corr(log3_rets['SP_Index'])
beta = np.multiply(corr, stdev_ratio)
beta_shrink = 0.6 * beta + 0.4
return beta_shrink6.2.2 Konstruktion der Long-/Short-Portfolios
Um ihren BAB-Faktor zu konstruieren, ordnen FP alle Wertpapiere im Datensatz zu jedem Zeitpunkt in aufsteigender Reihenfolge auf Grundlage der geschätzten Betas an (Perzentil-Ranking). Die geordneten Wertpapiere werden einem von zwei Portfolios zugewiesen: dem Low-Beta (\(L\)) und dem High-Beta (\(H\)) Portfolio. Der FP BAB Faktor basiert darauf, Long in das Low-Beta und Short in das High-Beta Portfolio zu gehen. Das Low- (High-) Beta Portfolio setzt sich aus allen Aktien zusammen, deren Beta unter (über) dem Median-Beta aller Wertpapiere im Datensatz liegt. In jedem Portfolio werden Wertpapiere anhand des Perzentil-Rangs ihres geschätzten Betas gewichtet. D.h., im Low-Beta Portfolio haben Wertpapiere mit niedrigerem Beta größere Gewichte, und im High-Beta Portfolio steigt entsprechend das Gewicht für Wertpapiere mit höherem Beta. Formal ergeben sich die Gewichtsvektoren der Aktien in beiden Portfolios wie folgt (siehe FP, 2014, Gleichung 16):
\[w_{H}=k(z-\mu_{z})^+\] \[w_{L}=k(z-\mu_{z})^-\]
Hierbei bezeichnet \(z\) einen \(n x 1\) Vektor der Beta-Perzentil-Ränge \(z_{i}=rank(\beta_{it})\) zum Zeitpunkt der Portfoliokonstruktion, \(\mu_{z}=1_{n}z/n\) den durchschnittlichen Rang, \(n\) die Anzahl der Aktien und \(1_{n}\) den Einheitsvektor der Dimension \(n x 1\). \(x^+\) und \(x^-\) kennzeichnen die positiven bzw. negativen Elemente eines Vektors \(x\). Um sicher zu stellen, dass sich die Gewichte in beiden Portfolios zu 1 summieren, d.h., \(1_{n}w_{H}=1\) bzw. \(1_{n}w_{L}=1\) gilt, wird die Normalisierungskonstante \(k\) definiert als \(k=2/1_{n}|z-\mu_{z}|\).
In Worten ergibt sich das Gewicht jeder Aktie aus der Rangabweichung der Aktie vom mittleren Rang skaliert mit der (d.h. geteilt durch die) Hälfte der Summe der absoluten Rangabweichungen über alle Aktien.
Zur Implementierung der FP Portfolios verwenden wir das tägliche DataFrame ranks mit den Perzentil-Rängen der Aktien gemäß ihres geschätzten Betas. Daraus erstellen wir ein neues DataFrame demeaned mit Rangabweichungen indem wir jeweils den Zeilenmittelwert (ranks.mean(axis=1)) der Ränge vom Rang einer Aktie abziehen.
Im nächsten Schritt generieren wir zwei transformierte Versionen von demeaned, die DataFrames long und short, mit Indikatoren (0/1-Variablen), die jeweils anzeigen, ob eine Aktie an einen entsprechenden Tag in das Long (High-Beta) oder das Short (Low-Beta) Portfolio gehört.
Dann erstellen wir das DataFrame abs_demeaned mit den absoluten Rangabweichungen. Wir bekommen die normalisierten Gewichte (DataFrame weights) indem wir die Zeilenwerte von abs_demeaned durch die Hälfte der korrespondierenden Zeilensumme von abs_demeaned teilen.
Wir erhalten die beiden DataFrames long_weights und short_weights mit den täglichen Zeitreihen der Portfoliogewichtsvektoren indem wir das DataFrame weights jeweils mit den DataFrames der Portfolio-Positionsindikatoren multiplizieren.
Im letzten Schritt berechnen wir die Zeitreihe der beiden Portfolio-Betas durch zellenweise Multiplikation des DataFrames der Aktienbeta-Zeitreihen (beta) mit den DataFrames der Portfoliogewichte und Bilden der Zeilensummen.
Fassen wir diese Schritte nun in der Gewichtsfunktion bab_weights zusammen. Diese Funktion benötigt als Eingabe ein DataFrame mit Aktienkurshistorien und Indexwerten. Als Ausgabe der Funktion erhalten wir die beiden DataFrames mit den Portfoliogewichten zu jedem Zeitpunkt, und zwei Zeitreihen mit den gewichteten Betas der Portfolios.
Code
# calculation of weights, equation (16) in Frazzini and Pedersen (2014)
def bab_weights(joined_df):
beta_shrink = calc_beta(joined_df)
beta_shrink = beta_shrink.dropna(thresh=10) # min 10 obs each row with non-missing betas
beta = beta_shrink.drop('SP_Index', axis=1)
ranks = beta.rank(ascending=True, axis=1, pct=True) # percentile ranks
demeaned = ranks.sub(ranks.mean(axis=1), axis='index') # cross-sectional demeaned
# indicator matrix: 1 for long position (demeaned beta negativ)
long = demeaned.copy()
for col in long.columns:
long[col] = np.where(long[col]<0, 1, 0)
#indicator matrix: 1 for short position (demeaned beta positiv)
short = demeaned.copy()
for col in short.columns:
short[col] = np.where(short[col]>0, 1, 0)
# calculation of normalized weights
abs_demeaned = abs(demeaned)
# demeaned percentile ranks normalized by 0.5 * cross-sectional sum of abs. demeaned weights
weights = abs_demeaned.div(0.5 * abs_demeaned.sum(axis=1), axis='index')
# separate matrix for long and short weights
long_weights = np.multiply(long, weights)
short_weights = np.multiply(short, weights)
# calculation of long and short portfolio betas
long_beta = (beta * long_weights).sum(axis=1)
short_beta = (beta * short_weights).sum(axis=1)
return long_weights, short_weights, long_beta, short_betaBevor wir mit dem dritten Schritt, der Konstruktion des BAB Faktors, weitermachen, definieren wir unsere bekannten Lambda Hilfs-Funktionen:
1. "compound" berechnet aus dem Eingabe-Array x kumulative Mehr-Tagesrenditen;
2. "daily_sr" berechnet aus einem Array von Tagesrenditen die tägliche Sharpe-Ratio;
6.2.3 Konstruktion des BAB Faktors
Konstruktionsbedingt ist das Portfoliobeta im Long (Low-Beta) Portfolio immer kleiner als das im Short (High-Beta) Portfolio. Wie lässt sich nun eine Long-Short BAB Faktorstrategie implementieren, die marktneutral (Zero-Beta Portfolio) ist? Hierzu müssen die Positionen in den beiden Portfolios gehebelt werden. Konkret benötigen wir eine “leveraged” Position im Low-Beta Portfolio und eine “deleveraged” Position im High-Beta Portfolio. Das Ziel ist es hierbei, dass die Positionen in den beiden Portfolios jeweils ein Beta von Eins haben. Hat das \(L\) (\(H\)) Portfolio beispielsweise ein Beta von 0,75 (1,4), müssen 1,33 (0,7) Geldeinheiten in das Portfolio investiert werden. Die Finanzierung der Positionen erfolgt dabei über entsprechend entgegengesetzte Positionen im risikolosen Zins um die Strategie selbstfinanzierend zu halten. Wichtig: Eine Geldeinheit Long und eine Geldeinheit Short führen nicht zu einem Zero-Beta Faktor! Formal ergibt sich die BAB Faktorstrategierendite folglich als (siehe FP, 2014, Gleichung 17):
\[r^{BAB}_{t+1}=\frac{1}{\beta^L_{t}}(r^L_{t+1}-r_{f})-\frac{1}{\beta^H_{t}}(r^H_{t+1}-r_{f}),\]
mit \(r^L_{t+1}=r^´_{t+1}w_{L}\), \(r^H_{t+1}=r^´_{t+1}w_{H}\), \(\beta^L_{t}=\beta^´_{t}w_{L}\), und \(\beta^H_{t}=\beta^´_{t}w_{H}\). Im folgenden unterstellen wir zur Vereinfachung einen risikolosen Zins \(r_{f}\) von Null.
Wir implementieren nun die Konstruktion der marktneutralen BAB Faktorstrategie in der Funktion bab_strat. Wie immer halten wir das Faktorportfolio konstant für eine Anzahl von Handelstagen, die durch das Argument hold angegeben wird.
Wichtig: Wir reduzieren die Zeitfrequenz der DataFrames mit den täglichen (Long/Short) Portfoliogewichtsvektoren und der Zeitreihen der Portfoliobetas auf die Länge (hold) der gewählten Portfoliohalteperiode, setzen die Gewichte/Betas auf die Werte die zu Beginn der Halteperiode gelten (über .first()), und wählen .shift(1), da in t nur die Gewichte die zum Zeitpunkt t-1 bekannt sind implementiert werden können.
Wir berechnen separate Zeitreihen der kumulierten Halteperioderenditen für das Long- und das Short-Portfolio (long_rets bzw. short_rets). Die Faktorrendite ergibt sich als Differenz (Long minus Short) der Portfoliorenditen, jeweils skaliert mit der Inversen des Portfoliobetas.
Zusätzlich zur annualisierten Sharpe-Ratio der Strategie gibt uns die Funktion auch die Sharpe-Ratio der Benchmark, d.h., der S&P500 Indexrenditen, zurück.
Code
# calculation of bab factor and strategy returns:
# equation (17) in Frazzini and Pedersen (2014)
def bab_strat(joined_df, hold):
freq = '%dB' % hold # holding period in number of business days
# weights and portfolio betas for each business day
long_weights, short_weights, long_beta, short_beta = bab_weights(joined_df)
# daily returns
daily_rets = joined_df.pct_change()
long_beta.name ='long_beta'
# adjustment of frequency for returns and beta/weights time series
daily_rets = pd.merge(daily_rets, long_beta, how='inner', on='Date').drop('long_beta', axis=1)
# calculation of benchmark sharpe ratio
bench_rets = daily_rets['SP_Index']
bench_sharpe = daily_sr(bench_rets) * np.sqrt(252)
daily_rets = daily_rets.drop('SP_Index', axis=1)
# taking weights and betas from the beginning of the holding period
long_weights = long_weights.shift(1).resample(freq).first() # time series with holding period freq
long_beta = long_beta.shift(1).resample(freq).first() # time series with holding period freq
short_weights = short_weights.shift(1).resample(freq).first() # time series with holding period freq
short_beta = short_beta.shift(1).resample(freq).first() # time series with holding period freq
# calculating holding period returns for long and short portfolio
returns = daily_rets.resample(freq).apply(compound)
long_rets = (long_weights * returns).sum(axis=1) # weighted sum of security returns
short_rets = (short_weights * returns).sum(axis=1) # weighted sum of security returns
# finally, calculating strategy returns (equation (17))
bab_rets = (1/long_beta * long_rets) - (1/short_beta * short_rets)
bab_sharpe = daily_sr(bab_rets) * np.sqrt(252/hold)
return bab_sharpe, bench_sharpeBerechnen wir nun die Sharpe-Ratios der BAB Strategie mit einer Rebalancingfrequenz von 20 Tagen und der passiven Benchmark (S&P500).