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.

Code
import numpy as np
import pandas as pd

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
Code
joined_df.head(5)
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_shrink

6.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_beta

Bevor 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;
Code
# cumulative returns
compound = lambda x: (1 + x).prod() - 1

# daily sharpe ratio
daily_sr = lambda x: x.mean() / x.std()

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_sharpe

Berechnen wir nun die Sharpe-Ratios der BAB Strategie mit einer Rebalancingfrequenz von 20 Tagen und der passiven Benchmark (S&P500).

Code
bab_strat(joined_df, 20)
(0.9439961839745289, 0.7395384250144774)