Python Forum
Parametric portfolio optimization by Brandt 2009
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Parametric portfolio optimization by Brandt 2009
#1
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
Reply
#2
you need to repost your code. All indentation was lost.
Reply
#3
Thank you for noticing me!
I updated my post with the new coding
Reply
#4
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!
buran write Mar-16-2021, 06:02 AM:
Merged a duplicate thread in Code Review into this one. Please, don't create multiple threads unnecessarily.
Reply
#5
(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!
Reply
#6
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
Larz60+ write Jul-27-2022, 03:10 AM:
Please post all code, output and errors (it it's entirety) between their respective tags. Refer to BBCode help topic on how to post. Use the "Preview Post" button to make sure the code is presented as you expect before hitting the "Post Reply/Thread" button.
Fixed for you this time, please use bbcode tags on future posts
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Stock clustering and portfolio diversification. Suitable features. timurkanaz 1 257 Mar-27-2024, 09:54 AM
Last Post: Larz60+
  How implement parametric ODE equation in python meesmaeili 1 2,220 Oct-16-2018, 01:39 AM
Last Post: scidam

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020