diff --git a/Cargo.toml b/Cargo.toml index d3e86a8..85d51e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ include = [ [lib] name = "pyxirr" crate-type = ["rlib", "cdylib"] +doctest = false [dependencies] pyo3 = "0.20" diff --git a/docs/_inline/pe/direct_alpha.md b/docs/_inline/pe/direct_alpha.md new file mode 100644 index 0000000..49475ff --- /dev/null +++ b/docs/_inline/pe/direct_alpha.md @@ -0,0 +1,9 @@ +The concept of direct alpha is closely related to the KS PME as it uses the +same method to calculate an IRR which is then compared to the fund’s IRR. +However, the difference between KS PME and direct alpha method is that in +direct alpha the under or out performance is quantified by calculating the +compounded cash flows plus the fund’s NAV. + +See also: +- +- diff --git a/docs/_inline/pe/dpi.md b/docs/_inline/pe/dpi.md index d7978b1..7b93e93 100644 --- a/docs/_inline/pe/dpi.md +++ b/docs/_inline/pe/dpi.md @@ -1,3 +1,11 @@ -Calculates the DPI (Distributions to Paid-In) for a set of cash flows. This is -just the total distributions divided by the total contributions, with -contributions coerced to be positive. +Distributed to Paid-In Capital (DPI) is a term used to measure the total +capital that a private equity fund has returned thus far to its investors. It +is also referred to as the realisation multiple. The DPI value is the +cumulative value of all investor distributions expressed as a multiple of all +the capital paid into the fund up to that time. + +Formula: DPI = Cumulative Distributions / Paid-In Capital + +See also: +- +- diff --git a/docs/_inline/pe/ks_pme.md b/docs/_inline/pe/ks_pme.md new file mode 100644 index 0000000..f4e6515 --- /dev/null +++ b/docs/_inline/pe/ks_pme.md @@ -0,0 +1,10 @@ +KS PME represents a market adjusted equivalent of the Total Value to +Paid-In-Capital (TVPI). KS PME is calculated by finding the future value of +each contribution and distribution using the stock market index returns. + +Formula: FV(Distributions) + NAV / FV(Contributions) + +See also: +- `ks_pme_flows` and `tvpi` functions +- +- diff --git a/docs/_inline/pe/ks_pme_flows.md b/docs/_inline/pe/ks_pme_flows.md index bd451a3..f3f21a1 100644 --- a/docs/_inline/pe/ks_pme_flows.md +++ b/docs/_inline/pe/ks_pme_flows.md @@ -1,5 +1,11 @@ Use the Kaplan-Schoar method to re-scale the private equity flows to match the -public market equivalents (PME) for comparison. This method works as follows, -for each period, re-scale the amount as: amount * (pme_price_final_period / -pme_price_current_period). Basically you are future valuing the amount to the -final period based on the returns of the PME. +public market equivalents (PME) for comparison. + +This method works as follows, for each period, re-scale the amount as: +`amount * (index[final_period] / index[current_period])`. +Basically you are future-valuing the amount to the final period based on the +returns of the PME. + +See also: +- +- diff --git a/docs/_inline/pe/ln_pme.md b/docs/_inline/pe/ln_pme.md new file mode 100644 index 0000000..92f0a45 --- /dev/null +++ b/docs/_inline/pe/ln_pme.md @@ -0,0 +1,9 @@ +The basic idea of Long Nickels method is that the cash flows of a VC fund i.e. +contributions and distributions are invested in a stock market index and to +generate a net asset value (NAV) at the end of each period. The last NAV is +used to calculate the IRR and this IRR is the Long Nickels PME. + +See also: +- `ln_pme_nav` function +- +- diff --git a/docs/_inline/pe/ln_pme_nav.md b/docs/_inline/pe/ln_pme_nav.md index 542f139..f099a5c 100644 --- a/docs/_inline/pe/ln_pme_nav.md +++ b/docs/_inline/pe/ln_pme_nav.md @@ -4,6 +4,11 @@ the nav. Instead of relying on the given nav, it is calculated as the future valued contributions less the future valued distributions. This will look like (for two periods with a contribution and distribution in each): +``` +nav = c[1] * index[-1]/index[1] + c[2] * index[-1]/index[2] + - d[1] * index[-1]/index[1] - d[2] * index[-1]/index[2] +``` -nav = c1 * px_final/px_1 + c2 * px_final/px_2 - - d1 * px_final/px_1 - d2 * px_final/px_2 +See also: +- +- diff --git a/docs/_inline/pe/m_pme.md b/docs/_inline/pe/m_pme.md new file mode 100644 index 0000000..019fdb8 --- /dev/null +++ b/docs/_inline/pe/m_pme.md @@ -0,0 +1,8 @@ +mPME is similar to PME+ in the sense that it uses a scaling factor. However, +mPME uses different scaling factors for cash flows at different time intervals. +Thus, it attempts to improve the limitations of PME+ where a single coefficient +λ is used to scale all distributions. + +See also: +- +- diff --git a/docs/_inline/pe/moic.md b/docs/_inline/pe/moic.md new file mode 100644 index 0000000..0f7e186 --- /dev/null +++ b/docs/_inline/pe/moic.md @@ -0,0 +1,12 @@ +The Multiple on Invested Capital (MOIC) measures the performance of an +investment today relative to the initial investment. + +Formula: (Realized investment + Unrealized investment) / Initial Investment + +Unrealised value, also referred to as residual value (or NAV), is the total +value of the remaining portfolio’s active investments that have not yet been +liquidated. + +See also: +- +- diff --git a/docs/_inline/pe/pme_plus.md b/docs/_inline/pe/pme_plus.md new file mode 100644 index 0000000..ef5f924 --- /dev/null +++ b/docs/_inline/pe/pme_plus.md @@ -0,0 +1,8 @@ +PME+ discount every distribution by a factor computed so that the NAV of the +index investment matches the NAV of the fund. The PME+ returns an IRR value of +discounted distributions. + +See also: +- `pme_plus_flows` and `pme_plus_lambda` functions +- +- diff --git a/docs/_inline/pe/pme_plus_flows.md b/docs/_inline/pe/pme_plus_flows.md index 27481ca..3ca1494 100644 --- a/docs/_inline/pe/pme_plus_flows.md +++ b/docs/_inline/pe/pme_plus_flows.md @@ -1,13 +1,17 @@ Use the PME+ method to re-scale the private equity flows to match the public market equivalents (PME) for comparison. This method works as follows: create -an equation that sets the nav equal to the contributions future valued based on +an equation that sets the NAV equal to the contributions future valued based on the PME returns, minus the distributions multiplied by a scalar (lambda) future valued based on the PME returns. This will look like (for two periods with a contribution and distribution in each): - -nav = c1 * px_final/px_1 + c2 * px_final/px_2 - - d1 * lambda * px_final/px_1 - d2 * lambda * px_final/px_2 - +``` +nav = c[1] * index[-1]/index[1] + c[2] * index[-1]/index[2] + - d[1] * λ * index[-1]/index[1] - d[2] * λ * index[-1]/index[2] +``` Solve for lambda so that the two sides of the equation are equal. Then multiply all the distributions by lambda to re-scale them. + +See also: +- +- diff --git a/docs/_inline/pe/pme_plus_lambda.md b/docs/_inline/pe/pme_plus_lambda.md new file mode 100644 index 0000000..b0beafe --- /dev/null +++ b/docs/_inline/pe/pme_plus_lambda.md @@ -0,0 +1,11 @@ +Find λ used in PME+ method. + +Formula: λ = (Scaled Distributions - NAV) / Scaled Contributions +Where: +- Scaled Distributions = sum(distributions * index[last] / index[current]) +- Scaled Contributions = sum(contributions * index[last] / index[current]) + +See also: +- `pme_plus_flows` function +- +- diff --git a/docs/_inline/pe/rvpi.md b/docs/_inline/pe/rvpi.md index 9c16b20..e488d88 100644 --- a/docs/_inline/pe/rvpi.md +++ b/docs/_inline/pe/rvpi.md @@ -1,3 +1,11 @@ -Calculates RVPI (Residual Value to Paid-In) for a set of cash flows. This is -the total residual value (NAV) divided by the total contributions, with -contributions coerced to be positive. +Residual Value to Paid-In Capital (RVPI) is a term used to measure the residual +value (NAV) of a private equity fund as a multiple of the capital paid in by the +investors. The residual value is the current fair value of all assets held by +the fund and the paid-in capital by the investors is the total of all +contributed capital up to that time. + +Formula: Residual Value / Paid-In Capital + +See also: +- +- diff --git a/docs/_inline/pe/tvpi.md b/docs/_inline/pe/tvpi.md index c998665..8caa69c 100644 --- a/docs/_inline/pe/tvpi.md +++ b/docs/_inline/pe/tvpi.md @@ -1,5 +1,12 @@ -Total Value to Paid-In Capital is a measure of the performance of a private -equity fund. It represents the total value of a fund relative to the amount of -capital paid into the fund to date. The total value of a fund is the sum of -realised value (all distributions made to investors to date) plus the -unrealised value (residual value of investments) still held by the fund. +Total Value to Paid-In Capital (also known as the 'Investment Multiple') is a +measure of the performance of a private equity fund. It represents the total +value of a fund relative to the amount of capital paid into the fund to date. +The total value of a fund is the sum of realised value (all distributions made +to investors to date) plus the unrealised value (residual value of investments) +still held by the fund. + +Formula: (Distributed Capital + residual Value) / Paid-In Capital + +See also: +- +- diff --git a/docs/index.md b/docs/index.md index 1a67a43..df2d88c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,4 +24,18 @@ Rust-powered collection of financial functions. - [IRR](functions.md#irr) - [MIRR](functions.md#mirr) - [XIRR](functions.md#xirr) +- [Private Equity](private_equity.md) + - [DPI](private_equity.md#dpi) + - [RVPI](private_equity.md#rvpi) + - [TVPI](private_equity.md#tvpi) + - [MOIC](private_equity.md#moic) + - [LN-PME NAV](private_equity.md#ln_pme_nav) + - [LN-PME](private_equity.md#ln_pme) + - [KS-PME Flows](private_equity.md#ks_pme_flows) + - [KS-PME](private_equity.md#ks_pme) + - [mPME](private_equity.md#m_pme) + - [PME+ Flows](private_equity.md#pme_plus_flows) + - [PME+ Lambda](private_equity.md#pme_plus_lambda) + - [PME+](private_equity.md#pme_plus) + - [Direct Alpha](private_equity.md#direct_alpha) - [Benchmarks](bench/index.html) diff --git a/docs/private_equity.md b/docs/private_equity.md new file mode 100644 index 0000000..7117119 --- /dev/null +++ b/docs/private_equity.md @@ -0,0 +1,54 @@ +{% include head.html %} + +## DPI + +{% include_relative ./_inline/pe/dpi.md %} + +## RVPI + +{% include_relative ./_inline/pe/rvpi.md %} + +## TVPI + +{% include_relative ./_inline/pe/tvpi.md %} + +## MOIC + +{% include_relative ./_inline/pe/moic.md %} + +## LN-PME + +{% include_relative ./_inline/pe/ln_pme.md %} + +## LN-PME NAV + +{% include_relative ./_inline/pe/ln_pme_nav.md %} + +## KS-PME Flows + +{% include_relative ./_inline/pe/ks_pme_flows.md %} + +## KS-PME + +{% include_relative ./_inline/pe/ks_pme.md %} + +## mPME + +{% include_relative ./_inline/pe/m_pme.md %} + +## PME+ Flows + +{% include_relative ./_inline/pe/pme_plus_flows.md %} + +## PME+ Lambda + +{% include_relative ./_inline/pe/pme_plus_lambda.md %} + +## PME+ + +{% include_relative ./_inline/pe/pme_plus.md %} + +## Direct Alpha + +{% include_relative ./_inline/pe/direct_alpha.md %} + diff --git a/src/core/private_equity.rs b/src/core/private_equity.rs index f85452e..57252b2 100644 --- a/src/core/private_equity.rs +++ b/src/core/private_equity.rs @@ -43,6 +43,7 @@ pub fn tvpi_2(contributions: &[f64], distributions: &[f64], nav: f64) -> Result< Ok((ds + nav) / cs) } +#[doc = include_str!("../../docs/_inline/pe/moic.md")] pub fn moic(amounts: &[f64], nav: f64) -> Result { // MOIC divides the total value of the investment or fund by the total invested capital, // whereas TVPI divides it by the paid-in capital (meaning, the capital that investors have @@ -52,6 +53,7 @@ pub fn moic(amounts: &[f64], nav: f64) -> Result { tvpi(amounts, nav) } +#[doc = include_str!("../../docs/_inline/pe/moic.md")] pub fn moic_2(contributions: &[f64], distributions: &[f64], nav: f64) -> Result { tvpi_2(contributions, distributions, nav) } @@ -79,10 +81,12 @@ pub fn ks_pme_flows_2( Ok((c, d)) } +#[doc = include_str!("../../docs/_inline/pe/ks_pme.md")] pub fn ks_pme(amounts: &[f64], index: &[f64], nav: f64) -> Result { ks_pme_flows(amounts, index).and_then(|a| tvpi(&a, nav)) } +#[doc = include_str!("../../docs/_inline/pe/ks_pme.md")] pub fn ks_pme_2( contributions: &[f64], distributions: &[f64], @@ -92,11 +96,13 @@ pub fn ks_pme_2( ks_pme_flows_2(contributions, distributions, index).and_then(|(c, d)| tvpi_2(&c, &d, nav)) } +#[doc = include_str!("../../docs/_inline/pe/m_pme.md")] pub fn m_pme(amounts: &[f64], index: &[f64], nav: &[f64]) -> Result { let (contributions, distributions) = split_amounts(amounts); m_pme_2(&contributions, &distributions, index, nav) } +#[doc = include_str!("../../docs/_inline/pe/m_pme.md")] pub fn m_pme_2( contributions: &[f64], distributions: &[f64], @@ -151,6 +157,7 @@ pub fn pme_plus_flows_2( Ok(scale(distributions, lambda)) } +#[doc = include_str!("../../docs/_inline/pe/pme_plus_lambda.md")] pub fn pme_plus_lambda(amounts: &[f64], index: &[f64], nav: f64) -> Result { check_input_len(amounts, index)?; @@ -158,6 +165,7 @@ pub fn pme_plus_lambda(amounts: &[f64], index: &[f64], nav: f64) -> Result pme_plus_lambda_2(&contributions, &distributions, index, nav) } +#[doc = include_str!("../../docs/_inline/pe/pme_plus_lambda.md")] pub fn pme_plus_lambda_2( contributions: &[f64], distributions: &[f64], @@ -174,6 +182,7 @@ pub fn pme_plus_lambda_2( Ok((cs - nav) / ds) } +#[doc = include_str!("../../docs/_inline/pe/pme_plus.md")] pub fn pme_plus(amounts: &[f64], index: &[f64], nav: f64) -> Result { let mut cf = pme_plus_flows(amounts, index, nav)?; @@ -184,6 +193,7 @@ pub fn pme_plus(amounts: &[f64], index: &[f64], nav: f64) -> Result { super::irr(&cf, None) } +#[doc = include_str!("../../docs/_inline/pe/pme_plus.md")] pub fn pme_plus_2( contributions: &[f64], distributions: &[f64], @@ -214,6 +224,7 @@ pub fn ln_pme_nav_2(contributions: &[f64], distributions: &[f64], index: &[f64]) ln_pme_nav(&amounts, index) } +#[doc = include_str!("../../docs/_inline/pe/ln_pme.md")] pub fn ln_pme(amounts: &[f64], index: &[f64]) -> Result { let pme_nav = ln_pme_nav(amounts, index)?; let mut cf = amounts.to_owned(); @@ -223,6 +234,7 @@ pub fn ln_pme(amounts: &[f64], index: &[f64]) -> Result { super::irr(&cf, None) } +#[doc = include_str!("../../docs/_inline/pe/ln_pme.md")] pub fn ln_pme_2(contributions: &[f64], distributions: &[f64], index: &[f64]) -> Result { let mut amounts = combine_amounts(contributions, distributions); let pme_nav = ln_pme_nav(&amounts, index)?; @@ -232,6 +244,7 @@ pub fn ln_pme_2(contributions: &[f64], distributions: &[f64], index: &[f64]) -> super::irr(&amounts, None) } +#[doc = include_str!("../../docs/_inline/pe/direct_alpha.md")] pub fn direct_alpha(amounts: &[f64], index: &[f64], nav: f64) -> Result { let mut cf = ks_pme_flows(amounts, index)?; if let Some(last) = cf.last_mut() { @@ -240,6 +253,7 @@ pub fn direct_alpha(amounts: &[f64], index: &[f64], nav: f64) -> Result { super::irr(&cf, None) } +#[doc = include_str!("../../docs/_inline/pe/direct_alpha.md")] pub fn direct_alpha_2( contributions: &[f64], distributions: &[f64], @@ -467,7 +481,7 @@ mod tests { #[rstest] #[case(&[-25., 15., 0.], &[100., 115., 130.], 20., 0.0875)] // example from https://en.wikipedia.org/wiki/Public_Market_Equivalent#Direct_Alpha - #[case(&[-100., -50., 60., 10., 0.], &[100., 105., 115., 117., 120.], 110., 0.0108)] + #[case(&[-100., -50., 60., 10., 0.], &[100., 105., 115., 117., 120.], 110., 0.0109)] // example from https://blog.edda.co/advanced-fund-performance-methods-pme-direct-alpha/ #[case(&[-80., -140., 0., 70., 140., 85.], &[890.35, 1144.98, 1271.5, 1289.09,1466.47, 1842.37], 70., 0.028)] // example from https://directalphamethod.info/ diff --git a/src/lib.rs b/src/lib.rs index 7be3405..d702dd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -473,11 +473,13 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/moic.md")] pub fn moic(py: Python, amounts: AmountArray, nav: Option) -> PyResult { py.allow_threads(move || Ok(private_equity::moic(&amounts, nav.unwrap_or(0.0))?)) } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/moic.md")] pub fn moic_2( py: Python, contributions: AmountArray, @@ -490,6 +492,7 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/ks_pme.md")] fn ks_pme( py: Python, amounts: AmountArray, @@ -500,6 +503,7 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/ks_pme.md")] fn ks_pme_2( py: Python, contributions: AmountArray, @@ -537,6 +541,7 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/m_pme.md")] fn m_pme( py: Python, amounts: AmountArray, @@ -547,6 +552,7 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/m_pme.md")] fn m_pme_2( py: Python, contributions: AmountArray, @@ -560,6 +566,7 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/pme_plus.md")] fn pme_plus( py: Python, amounts: AmountArray, @@ -575,6 +582,7 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/pme_plus.md")] fn pme_plus_2( py: Python, contributions: AmountArray, @@ -630,6 +638,7 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/pme_plus_lambda.md")] fn pme_plus_lambda( py: Python, amounts: AmountArray, @@ -642,6 +651,7 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/pme_plus_lambda.md")] fn pme_plus_lambda_2( py: Python, contributions: AmountArray, @@ -679,6 +689,7 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/ln_pme.md")] fn ln_pme(py: Python, amounts: AmountArray, index: AmountArray) -> PyResult> { py.allow_threads(move || { fallible_float_or_none(private_equity::ln_pme(&amounts, &index), false) @@ -686,6 +697,7 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/ln_pme.md")] fn ln_pme_2( py: Python, contributions: AmountArray, @@ -701,6 +713,7 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/direct_alpha.md")] fn direct_alpha( py: Python, amounts: AmountArray, @@ -716,6 +729,7 @@ mod pe { } #[pyfunction] + #[doc = include_str!("../docs/_inline/pe/direct_alpha.md")] fn direct_alpha_2( py: Python, contributions: AmountArray,