Python Forum

Full Version: Parametric portfolio optimization by Brandt 2009
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Hello Guys!
I am fairly new to Python and I am seeking for help.
I am trying to do the parametric portfolio optimization by Brandt. (2009)
with the dataset of Kenneth French´s 3 Factor Model (Dataset can be found here: https://mba.tuck.dartmouth.edu/pages/fac...brary.html
My code so far is:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as st
import sympy as sp
import scipy as sp
# The Code for the Optimization and the Scaling is taken from: https://github.com/Seaaann/Parametric-Portfolio-Policy
#MIT License
#Copyright © 2020 SySean
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.

#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.

# importing data
ff_factors = pd.read_csv('F-F_Research_Data_Factors.csv', skiprows = 3,index_col=0, nrows = 1135)
del ff_factors["RF"]
ff_factors.index = pd.to_datetime(ff_factors.index, format= '%Y%m')
ff_factors.index = ff_factors.index + pd.offsets.MonthEnd()
ff_factors = ff_factors.apply(lambda x: x/ 100)
# creating different fama french factors
returns= ff_factors
excess = ff_factors["Mkt-RF"]
size = ff_factors["SMB"]
value = ff_factors["HML"]
# creating means and std for scaling
returns_mean = returns.mean(axis=1)
excess_mean = excess.mean()
size_mean = size.mean()
value_mean = value.mean()
returns_std = returns.std(axis=1)
excess_std = excess.std()
size_std = size.std()
value_std = value.std()
## scaling the factors
def Scale(y,c=True, sc=True):
     x = y.copy()
     if c:
        x -= x.mean()
     if sc and c:
        x /= x.std()
     elif sc:
        x /= np.sqrt(x.pow(2).sum().div(x.count() - 1))
     return x
scaled_excess = pd.DataFrame(Scale(excess.T))
scaled_size = pd.DataFrame(Scale(size.T))
scaled_value= pd.DataFrame(Scale(value.T))
Returns = returns.reset_index(drop=True)
## Parametric Portfolio Policies function
def PPS(x, wb, nt, excess, size, value, rr):
    w1= wb + nt * (x[0] * excess)
    w2= wb + nt * (x[1]* size)
    w3= wb + nt * (x[2]* value)
    wret = (w1*excess + w2*size + w3*value).sum()
    ut = ((1 + wret) ** (1 - rr)) / (1 - rr)
    u = -(ut.mean())
    return u
Scaled_excess = scaled_excess.reset_index(drop = True)
Scaled_size = scaled_size.reset_index(drop=True)
Scaled_value = scaled_value.reset_index(drop=True)
nt = wb = 1/ np.shape(returns)[1]
rr = 5
res_save = []
weights = []
x0 = np.array([0,0,0])
for i in range(0, 60):
    opt = sp.optimize.minimize(
        PPS,
        x0,
        method="BFGS",
        args=(
        wb,
        nt,
        Scaled_excess.iloc[0:1075+i],
        Scaled_size.iloc[0:1075+i],
        Scaled_value.iloc[0:1075 + i],
        rr,
        ),
    )
    print("The {} window".format(i + 1))
    print("The value:", opt["x"])
    res_save.append(opt["x"])
    w = wb + nt * (
        opt["x"][0] * Scaled_excess.iloc[i + 1075, :]
        + opt["x"][1] * Scaled_size.iloc[i + 1075, :]
        + opt["x"][2] * Scaled_value.iloc[i+ 1075, :]
    )
    print(w)
    weights.append(w)
index = returns.index[1075:1135]
char_df = pd.DataFrame(res_save, index=index, columns=["Excess","Value","Size"])
The main problem I have right now is that the optimal weights are optimized as NaN:
Output:
The 60 window The value: [0. 0. 0.] HML NaN Mkt-RF NaN SMB NaN Name: 1134, dtype: float64
Instead of the decimals of the optimal weights.
It may have to do with the Dimensions, but
I cant seem to find an answer for it.
I would be really grateful for any ideas and suggestions.

Best regards,
Steffen
you need to repost your code. All indentation was lost.
Thank you for noticing me!
I updated my post with the new coding
Hey Guys! I am struggling right now to code the parametric portfolio optimization by Brandt et al.(2009)
I have taken the data of Fama and Frenchs 3 Factor model from :Fama French 3 Factor Model.
The initial code is taken from Github
I tried to expand it on the new 3 Factors instead on the existing 2:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as st
import sympy as sp
import scipy as sp
# The Code for the Optimization and the Scaling is taken from: https://github.com/Seaaann/Parametric-Portfolio-Policy
#MIT License
#Copyright (c) 2020 SySean
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.

#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.

# importing data
ff_factors = pd.read_csv('F-F_Research_Data_Factors.csv', skiprows = 3,index_col=0, nrows =  1135)
del ff_factors["RF"]
ff_factors.index = pd.to_datetime(ff_factors.index, format= '%Y%m')
ff_factors.index = ff_factors.index + pd.offsets.MonthEnd()
ff_factors = ff_factors.apply(lambda x: x/ 100)
# creating different fama french factors
returns= ff_factors
excess = ff_factors["Mkt-RF"]
size = ff_factors["SMB"]
value = ff_factors["HML"]
# creating means and std for scaling
returns_mean = returns.mean(axis=1)
excess_mean = excess.mean()
size_mean = size.mean()
value_mean = value.mean()
returns_std = returns.std(axis=1)
excess_std = excess.std()
size_std = size.std()
value_std = value.std()
## scaling the factors
def Scale(y,c=True, sc=True):
    x = y.copy()
    if c:
        x -= x.mean()
    if sc and c:
        x /= x.std()
    elif sc:
        x /= np.sqrt(x.pow(2).sum().div(x.count() - 1))
    return x
scaled_excess = pd.DataFrame(Scale(excess.T))
scaled_size = pd.DataFrame(Scale(size.T))
scaled_value= pd.DataFrame(Scale(value.T))
Returns = returns.reset_index(drop=True)
## Parametric Portfolio Policies function
def PPS(x, wb, nt, excess, size, value, rr):
    w1= wb + nt * (x[0] * excess)
    w2= wb + nt * (x[1]* size)
    w3= wb + nt * (x[2]* value)
    wret = (w1*excess + w2*size + w3*value).sum() 
    ut = ((1 + wret) ** (1 - rr)) / (1 - rr)
    u = -(ut.mean())
    return u
Scaled_excess = scaled_excess.reset_index(drop = True)
Scaled_size = scaled_size.reset_index(drop=True)
Scaled_value = scaled_value.reset_index(drop=True)
nt = wb = 1/ np.shape(returns)[1]
rr = 5 
res_save = []
weights = []
x0 = np.array([0,0,0])
for i in range(0, 60):
    opt = sp.optimize.minimize(
        PPS,
        x0,
        method="BFGS",
        args=(
            wb,
            nt,
            Scaled_excess.iloc[0:1075+i],
            Scaled_size.iloc[0:1075+i],
            Scaled_value.iloc[0:1075 + i],
            rr,
        ),
    )
    print("The {} window".format(i + 1))
    print("The value:", opt["x"])
    res_save.append(opt["x"])
    w = wb + nt * (
        opt["x"][0] * Scaled_excess.iloc[i + 1075, :]
        + opt["x"][1] * Scaled_size.iloc[i + 1075, :]
        + opt["x"][2] * Scaled_value.iloc[i+ 1075, :]
    )
    print(w)
    weights.append(w)
index = returns.index[1075:1135]
char_df = pd.DataFrame(res_save, index=index, columns=["Excess","Value","Size"])
This produces an error in the weights:
Output:
The 60 window The value: [0. 0. 0.] HML NaN Mkt-RF NaN SMB NaN Name: 1134, dtype: float64
I think it has to do with the scaled variables. If i take a series
of the variables i get:
Output:
The 60 window The value: [0.06488726 0.0646142 0.06485851] 0.3931625835545131
but this produces only 1 weight and not 3.
The optimal output should be 3 weights, 1 weight for each
Factor and they should sum up to 1.
Can someone give me advice on my mistakes?
I would be really grateful for any comments and for suggestions.
Best Regards!
(Mar-15-2021, 10:49 PM)schnellinga Wrote: [ -> ]Hey Guys! I am struggling right now to code the parametric portfolio optimization by Brandt et al.(2009)
I have taken the data of Fama and Frenchs 3 Factor model from :Fama French 3 Factor Model.
The initial code is taken from Github
I tried to expand it on the new 3 Factors instead on the existing 2:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as st
import sympy as sp
import scipy as sp
# The Code for the Optimization and the Scaling is taken from: https://github.com/Seaaann/Parametric-Portfolio-Policy
#MIT License
#Copyright (c) 2020 SySean
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.

#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.

# importing data
ff_factors = pd.read_csv('F-F_Research_Data_Factors.csv', skiprows = 3,index_col=0, nrows =  1135)
del ff_factors["RF"]
ff_factors.index = pd.to_datetime(ff_factors.index, format= '%Y%m')
ff_factors.index = ff_factors.index + pd.offsets.MonthEnd()
ff_factors = ff_factors.apply(lambda x: x/ 100)
# creating different fama french factors
returns= ff_factors
excess = ff_factors["Mkt-RF"]
size = ff_factors["SMB"]
value = ff_factors["HML"]
# creating means and std for scaling
returns_mean = returns.mean(axis=1)
excess_mean = excess.mean()
size_mean = size.mean()
value_mean = value.mean()
returns_std = returns.std(axis=1)
excess_std = excess.std()
size_std = size.std()
value_std = value.std()
## scaling the factors
def Scale(y,c=True, sc=True):
    x = y.copy()
    if c:
        x -= x.mean()
    if sc and c:
        x /= x.std()
    elif sc:
        x /= np.sqrt(x.pow(2).sum().div(x.count() - 1))
    return x
scaled_excess = pd.DataFrame(Scale(excess.T))
scaled_size = pd.DataFrame(Scale(size.T))
scaled_value= pd.DataFrame(Scale(value.T))
Returns = returns.reset_index(drop=True)
## Parametric Portfolio Policies function
def PPS(x, wb, nt, excess, size, value, rr):
    w1= wb + nt * (x[0] * excess)
    w2= wb + nt * (x[1]* size)
    w3= wb + nt * (x[2]* value)
    wret = (w1*excess + w2*size + w3*value).sum() 
    ut = ((1 + wret) ** (1 - rr)) / (1 - rr)
    u = -(ut.mean())
    return u
Scaled_excess = scaled_excess.reset_index(drop = True)
Scaled_size = scaled_size.reset_index(drop=True)
Scaled_value = scaled_value.reset_index(drop=True)
nt = wb = 1/ np.shape(returns)[1]
rr = 5 
res_save = []
weights = []
x0 = np.array([0,0,0])
for i in range(0, 60):
    opt = sp.optimize.minimize(
        PPS,
        x0,
        method="BFGS",
        args=(
            wb,
            nt,
            Scaled_excess.iloc[0:1075+i],
            Scaled_size.iloc[0:1075+i],
            Scaled_value.iloc[0:1075 + i],
            rr,
        ),
    )
    print("The {} window".format(i + 1))
    print("The value:", opt["x"])
    res_save.append(opt["x"])
    w = wb + nt * (
        opt["x"][0] * Scaled_excess.iloc[i + 1075, :]
        + opt["x"][1] * Scaled_size.iloc[i + 1075, :]
        + opt["x"][2] * Scaled_value.iloc[i+ 1075, :]
    )
    print(w)
    weights.append(w)
index = returns.index[1075:1135]
char_df = pd.DataFrame(res_save, index=index, columns=["Excess","Value","Size"])
This produces an error in the weights:
Output:
The 60 window The value: [0. 0. 0.] HML NaN Mkt-RF NaN SMB NaN Name: 1134, dtype: float64
I think it has to do with the scaled variables. If i take a series
of the variables i get:
Output:
The 60 window The value: [0.06488726 0.0646142 0.06485851] 0.3931625835545131
but this produces only 1 weight and not 3.
The optimal output should be 3 weights, 1 weight for each
Factor and they should sum up to 1.
Can someone give me advice on my mistakes?
I would be really grateful for any comments and for suggestions.
Best Regards!
Hi schnellinga,

I think that you misunderstand the original model.
Below is your code for the PPS function.

def PPS(x, wb, nt, excess, size, value, rr):
    w1= wb + nt * (x[0] * excess)
    w2= wb + nt * (x[1]* size)
    w3= wb + nt * (x[2]* value)
    wret = (w1*excess + w2*size + w3*value).sum() 
    ut = ((1 + wret) ** (1 - rr)) / (1 - rr)
    u = -(ut.mean())
    return u
Below is the original one.

# original one
def PPS(x, wb, nt, ret, m12, mktcap, rr):
    wi = wb + nt * (x[0] * m12 + x[1] * mktcap)
    wret = (wi * ret).sum(axis=1)
    ut = ((1 + wret) ** (1 - rr)) / (1 - rr)
    u = -(ut.mean())
    return u
According to the original model, there are two parameters: 12month returns (for the momentum strategy)
and size (for the size effect). Because of this, there are two coefficients : x[0] and x[1]. If looking at the
original input data sets, you will find two associated columns: adjusted closing price and mktcap, shown below.

df.head()
Out[317]:


Moderator Note:
Output tags also work well for input

Output:
symbol adjusted close shares_held year month \ Date 2008-01-01 MSFT 25.413656 32.599998 92220260 2008 1 2008-02-01 MSFT 21.286415 27.200001 92220260 2008 2 2008-03-01 MSFT 22.209873 28.379999 92220260 2008 3 2008-04-01 MSFT 22.319431 28.520000 92220260 2008 4 2008-05-01 MSFT 22.244507 28.320000 92220260 2008 5 mktcap day Date 2008-01-01 3.006380e+09 1 2008-02-01 2.508391e+09 1 2008-03-01 2.617211e+09 1 2008-04-01 2.630122e+09 1 2008-05-01 2.611678e+09 1
For your input data set, i.e., the Fama-French monthly data set, there is no market cap, shown below.
>>>ff_factors.head()
Out[318]: 
Output:
Mkt-RF SMB HML 1926-07-31 0.0296 -0.0238 -0.0273 1926-08-31 0.0264 -0.0147 0.0414 1926-09-30 0.0036 -0.0139 0.0012 1926-10-31 -0.0324 -0.0013 0.0065 1926-11-30 0.0253 -0.0016 -0.0038
Because of this, you can test the results for just one parameter: 12-month returns.
However, in your PPS function, you use 3, x[0], x[1], and x[2], instead of one.

Hope that this would help.
Best,
Paul