-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathpme.py
146 lines (129 loc) · 4.72 KB
/
pme.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""Calculate PME (Public Market Equivalent) for both evenly and unevenly spaced
cashflows. Calculation according to
https://en.wikipedia.org/wiki/Public_Market_Equivalent#Modified_PME
Args:
- `dates`: The points in time. (Only for `xpme` variants.)
- `cashflows`: The cashflows from a transaction account perspective.
- `prices`: Asset's prices at each interval / point in time.
- `pme_prices`: PME's prices at each interval / point in time.
Note:
- Both `prices` and `pme_prices` need an additional item at the end for the last
interval / point in time, for which the PME is calculated.
- `cashflows` has one fewer entry than the other lists because the last cashflow is
implicitly assumed to be the current NAV at that time.
Verbose versions return a tuple with:
- PME IRR
- Asset IRR
- Dataframe containing all the cashflows, prices, and values used to derive the PME
"""
from typing import List, Tuple
from datetime import date
import pandas as pd
import numpy_financial as npf
from xirr.math import listsXirr
def verbose_pme(
cashflows: List[float],
prices: List[float],
pme_prices: List[float],
) -> Tuple[float, float, pd.DataFrame]:
"""Calculate PME for evenly spaced cashflows and return vebose information."""
if len(cashflows) == 0:
raise ValueError("Must have at least one cashflow")
if not any(x < 0 for x in cashflows):
raise ValueError(
"At least one cashflow must be negative, i.e., a buy of some of the asset"
)
if not all(x > 0 for x in prices + pme_prices):
raise ValueError("All prices must be > 0")
if len(prices) != len(pme_prices) or len(cashflows) != len(prices) - 1:
raise ValueError("Inconsistent input data")
current_asset_pre = 0 # The current NAV of the asset
current_pme_pre = 0 # The current NAV of the PME
df_rows = [] # To build the dataframe
for cf, asset_price, asset_price_next, pme_price, pme_price_next in zip(
cashflows, prices[:-1], prices[1:], pme_prices[:-1], pme_prices[1:]
):
if cf < 0:
# Simply buy from the asset and the PME the cashflow amount:
asset_cf = pme_cf = -cf
else:
# Calculate the cashflow's ratio of the asset's NAV at this point in time
# and sell that ratio of the PME:
asset_cf = -cf
ratio = cf / current_asset_pre
pme_cf = -current_pme_pre * ratio
df_rows.append(
[
cf,
asset_price,
current_asset_pre,
asset_cf,
current_asset_pre + asset_cf,
pme_price,
current_pme_pre,
pme_cf,
current_pme_pre + pme_cf,
]
)
# Calculate next:
current_asset_pre = (
(current_asset_pre + asset_cf) * asset_price_next / asset_price
)
current_pme_pre = (current_pme_pre + pme_cf) * pme_price_next / pme_price
df_rows.append(
[
current_asset_pre,
asset_price_next,
current_asset_pre,
-current_asset_pre,
0,
pme_price_next,
current_pme_pre,
-current_pme_pre,
0,
]
)
df = pd.DataFrame(
df_rows,
columns=pd.MultiIndex.from_arrays(
[
["Account"] + ["Asset"] * 4 + ["PME"] * 4,
["CF"] + ["Price", "NAVpre", "CF", "NAVpost"] * 2,
]
),
)
return (npf.irr(df["PME", "CF"]), npf.irr(df["Asset", "CF"]), df)
def pme(
cashflows: List[float],
prices: List[float],
pme_prices: List[float],
) -> float:
"""Calculate PME for evenly spaced cashflows and return the PME IRR only."""
return verbose_pme(cashflows, prices, pme_prices)[0]
def verbose_xpme(
dates: List[date],
cashflows: List[float],
prices: List[float],
pme_prices: List[float],
) -> Tuple[float, float, pd.DataFrame]:
"""Calculate PME for unevenly spaced / scheduled cashflows and return vebose
information.
Requires the points in time as `dates` as an input parameter in addition to the ones
required by `pme()`.
"""
if len(dates) != len(prices):
raise ValueError("Inconsistent input data")
df = verbose_pme(cashflows, prices, pme_prices)[2]
df["Dates"] = dates
df.set_index("Dates", inplace=True)
return listsXirr(dates, df["PME", "CF"]), listsXirr(dates, df["Asset", "CF"]), df
def xpme(
dates: List[date],
cashflows: List[float],
prices: List[float],
pme_prices: List[float],
) -> float:
"""Calculate PME for unevenly spaced / scheduled cashflows and return the PME IRR
only.
"""
return verbose_xpme(dates, cashflows, prices, pme_prices)[0]