-
Geographical Analysis 6
-
Lecture1.1
-
Lecture1.2
-
Lecture1.3
-
Lecture1.4
-
Lecture1.5
-
Lecture1.6
-
-
Cap Table 3
-
Lecture2.1
-
Lecture2.2
-
Lecture2.3
-
-
Simulation 6
-
Lecture3.1
-
Lecture3.2
-
Lecture3.3
-
Lecture3.4
-
Lecture3.5
-
Lecture3.6
-
-
Search Index 8
-
Lecture4.1
-
Lecture4.2
-
Lecture4.3
-
Lecture4.4
-
Lecture4.5
-
Lecture4.6
-
Lecture4.7
-
Lecture4.8
-
-
Fund Distributions 5
-
Lecture5.1
-
Lecture5.2
-
Lecture5.3
-
Lecture5.4
-
Lecture5.5
-
Comparing Performance Fee Structures
Comparing Performance Fee Structures¶
In the following part of the lesson we will construct a function that can handle all types of performance fees for us and find the IRR for the limited partner. We will work through one piece at a time to come up with the final function.
Start with a simple performance fee that we will assume is only taken out at the end of the fund life. For this, we just need to find the net cashflows, and if they are positive, subtract out the performance fee for the fund. We will set the hurdle rate to be None by default and catch_up to false.
def find_net_IRR(cash_flows, performance_fee, hurdle_rate=None, catch_up=False):
#Copy over the cash flows
cash_flows_net = cash_flows.copy()
if hurdle_rate is None:
#Get the total distributions only if positive for the performance fee
gp_share = max(0, cash_flows_net.sum()) * performance_fee
#Subtract out the general partner share from the cash flows
cash_flows_net.iloc[-1] -= gp_share
else:
assert False, "Not implemented yet"
#Compute the IRR
IRR = minimize(sq_distance, -.2, args=(cash_flows_net.index, cash_flows_net), method="Nelder-Mead")['x'][0]
return IRR
We also will define scenarios where we take the positive cash flows and multiply them by a few different multipliers to simulate possible scenarios.
#Find a range of cash flows
x_cf = [pd.concat([cash_flows[:3], cash_flows[3:] * i/24]) for i in range(2, 49)]
#Print out the first two as an example
print(x_cf[:2])
[0 -400.000000
1 -300.000000
2 -300.000000
3 66.666667
4 133.333333
dtype: float64, 0 -400.0
1 -300.0
2 -300.0
3 100.0
4 200.0
dtype: float64]
We also want to see what the IRR before performance fee will be in these cases. I set the best guess to -20% because it ensures the optimization converges correctly.
#Find the IRR for pre performance fee
IRR_pre = [minimize(sq_distance, -.2, args=(x.index, x), method="Nelder-Mead")['x'][0] for x in x_cf]
print(IRR_pre)
[-0.459223632812501, -0.3643530273437506, -0.2891796875000003, -0.2260351562500001, -0.17111328125000008, -0.12223632812500017, -0.07800781250000023, -0.03749023437500032, -3.885780586188048e-16, 0.03495117187499955, 0.06773437499999947, 0.09866210937499942, 0.12795898437499936, 0.15583007812499927, 0.1824218749999993, 0.20788085937499923, 0.23230468749999916, 0.25579101562499906, 0.278417968749999, 0.300283203124999, 0.321425781249999, 0.3418994140624989, 0.36175781249999894, 0.3810546874999989, 0.39980468749999887, 0.4180664062499988, 0.4358593749999988, 0.45320312499999876, 0.47013671874999874, 0.48668945312499867, 0.5028710937499985, 0.5186914062499988, 0.5341796874999987, 0.5493554687499984, 0.5642382812499984, 0.5788281249999985, 0.5931445312499986, 0.6072070312499986, 0.6210058593749985, 0.6345800781249984, 0.6479199218749985, 0.6610156249999983, 0.6739453124999983, 0.6866503906249986, 0.6991503906249983, 0.7114648437499982, 0.7235937499999981]
Now to compute the net IRR we use our newly created function.
#Find the IRR for post performance fee
IRR_post = [find_net_IRR(cf, .2) for cf in x_cf]
print(IRR_post)
[-0.459223632812501, -0.3643530273437506, -0.2891796875000003, -0.2260351562500001, -0.17111328125000008, -0.12223632812500017, -0.07800781250000023, -0.03749023437500032, -3.885780586188048e-16, 0.02820312499999956, 0.0550976562499995, 0.08080566406249948, 0.10545410156249943, 0.12914062499999934, 0.1519531249999993, 0.17395507812499927, 0.19522949218749924, 0.21582031249999917, 0.23578124999999914, 0.2551562499999991, 0.27398437499999917, 0.29230468749999905, 0.31014648437499903, 0.327539062499999, 0.344511718749999, 0.361083984374999, 0.37727539062499893, 0.39312499999999895, 0.40863281249999883, 0.4238037109374988, 0.4386816406249988, 0.45326171874999877, 0.46757812499999873, 0.48162109374999873, 0.4954199218749986, 0.5089648437499987, 0.5222851562499986, 0.5353906249999986, 0.5482617187499985, 0.5609374999999985, 0.5733984374999985, 0.5857031249999985, 0.5978124999999985, 0.6097363281249986, 0.6214941406249985, 0.6330859374999984, 0.6445214843749985]
#Plot the pre vs. post IRR
plt.plot(IRR_pre, IRR_pre, color='grey', linestyle='--')
plt.plot(IRR_pre, IRR_post, color='black')
plt.legend(['Gross IRR', 'Net IRR'])
plt.xlabel("Gross IRR")
plt.ylabel("IRR")
plt.show()
Now to add in the code that takes care of the case where we don't have a catch up but we do have a hurdle rate.
def find_net_IRR(cash_flows, performance_fee, hurdle_rate=None, catch_up=False):
#Copy over the cash flows
cash_flows_net = cash_flows.copy()
if hurdle_rate is None:
#Get the total distributions only if positive for the performance fee
gp_share = max(0, cash_flows_net.sum()) * performance_fee
#Subtract out the general partner share from the cash flows
cash_flows_net.iloc[-1] -= gp_share
else:
#Find the NPV given the hurdle rate
NPV = (cash_flows / (1+hurdle_rate) ** cash_flows.index).sum()
#A negative NPV means no performance fee will be taken
if NPV < 0:
pass
else:
#Find the future value of the NPV
FV = NPV * (1+hurdle_rate) ** cash_flows_net.index[-1]
#Subtract the future value from the last year
cash_flows_net.iloc[-1] -= FV
if catch_up:
assert False, "Not implemented yet"
else:
cash_flows_net.iloc[-1] += FV * (1-performance_fee)
#Compute the IRR
IRR = minimize(sq_distance, -.2, args=(cash_flows_net.index, cash_flows_net), method="Nelder-Mead")['x'][0]
return IRR
These first graph shows the different IRRs and the second graph shows the differential between the two IRRs at different gross IRRs. You'll notice how at first they are the same but then diverge.
#Find the IRR with no hurdle rate
IRR_post1 = [find_net_IRR(cf, .2) for cf in x_cf]
#Find the IRR with a 10% hurdle rate
IRR_post2 = [find_net_IRR(cf, .2, hurdle_rate=.1) for cf in x_cf]
plt.plot(IRR_pre, IRR_pre, color='grey', linestyle='--')
plt.plot(IRR_pre, IRR_post1, color='blue')
plt.plot(IRR_pre, IRR_post2, color='red')
plt.legend(['Gross IRR', 'Net IRR No Hurdle', 'Net IRR 10% Hurdle'])
plt.xlabel("Gross IRR")
plt.ylabel("IRR")
plt.show()
difference = [x-y for x,y in zip(IRR_post1, IRR_post2)]
plt.axhline(0, color='grey', linestyle="--")
plt.plot(IRR_pre, difference, color='black')
plt.title("Differential Between IRR1 and IRR2")
plt.xlabel("Gross IRR")
plt.ylabel("IRR")
plt.show()
Let's look at a few more examples!
def find_net_IRR(cash_flows, performance_fee, hurdle_rate=None, catch_up=False):
#Copy over the cash flows
cash_flows_net = cash_flows.copy()
if hurdle_rate is None:
#Get the total distributions only if positive for the performance fee
gp_share = max(0, cash_flows_net.sum()) * performance_fee
#Subtract out the general partner share from the cash flows
cash_flows_net.iloc[-1] -= gp_share
else:
#Find the NPV given the hurdle rate
NPV = (cash_flows / (1+hurdle_rate) ** cash_flows.index).sum()
#A negative NPV means no performance fee will be taken
if NPV < 0:
pass
else:
#Find the future value of the NPV
FV = NPV * (1+hurdle_rate) ** cash_flows_net.index[-1]
#Subtract the future value from the last year
cash_flows_net.iloc[-1] -= FV
if catch_up:
#Find the maximum of what the catch up can be
c = (cash_flows_net.sum() * performance_fee) / (1- performance_fee)
#If the FV is less than the catch up, all this profit goes to the GP
if FV < c:
pass
else:
#Allocate all profit after the catch up fee given the performance fee share
FV -= c
cash_flows_net.iloc[-1] += FV * (1-performance_fee)
else:
cash_flows_net.iloc[-1] += FV * (1-performance_fee)
#Compute the IRR
IRR = minimize(sq_distance, -.2, args=(cash_flows_net.index, cash_flows_net), method="Nelder-Mead")['x'][0]
return IRR
#Find the IRR with no hurdle rate
IRR_post1 = [find_net_IRR(cf, .2) for cf in x_cf]
#Find the IRR with a 10% hurdle rate
IRR_post2 = [find_net_IRR(cf, .2, hurdle_rate=.1) for cf in x_cf]
#Find the IRR with a 10% hurdle rate and a catch up
IRR_post3 = [find_net_IRR(cf, .2, hurdle_rate=.1, catch_up=True) for cf in x_cf]
plt.plot(IRR_pre, IRR_pre, color='grey', linestyle='--')
plt.plot(IRR_pre, IRR_post1, color='blue')
plt.plot(IRR_pre, IRR_post2, color='red')
plt.plot(IRR_pre, IRR_post3, color='green')
plt.legend(['Gross IRR', 'Net IRR No Hurdle', 'Net IRR 10% Hurdle', 'Net IRR 10% Hurdle w/ Catch-Up'])
plt.xlabel("Gross IRR")
plt.ylabel("IRR")
plt.show()
#Differences between IRRs
difference1 = [x-y for x,y in zip(IRR_post1, IRR_post2)]
difference2 = [x-y for x,y in zip(IRR_post1, IRR_post3)]
plt.axhline(0, color='grey', linestyle="--")
plt.plot(IRR_pre, difference1, color='red', label="IRR1 - IRR2")
plt.plot(IRR_pre, difference2, color='green', label="IRR1 - IRR3")
plt.xlabel("Gross IRR")
plt.ylabel("IRR")
plt.legend()
plt.show()
IRR_post1 = [find_net_IRR(cf, .1,) for cf in x_cf]
IRR_post2 = [find_net_IRR(cf, .2, hurdle_rate=.15) for cf in x_cf]
plt.plot(IRR_pre, IRR_pre, color='grey', linestyle='--')
plt.plot(IRR_pre, IRR_post1, color='blue')
plt.plot(IRR_pre, IRR_post2, color='red')
plt.legend(['Gross IRR', 'Net IRR No Hurdle-10% PF', 'Net IRR 15% Hurdle-20% PF'])
plt.xlabel("Gross IRR")
plt.ylabel("IRR")
plt.show()