Skip to content

Commit

Permalink
PE functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Anexen committed Nov 25, 2023
1 parent 7600d48 commit 9eb91b1
Show file tree
Hide file tree
Showing 16 changed files with 756 additions and 63 deletions.
50 changes: 32 additions & 18 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ name = "pyxirr"
crate-type = ["rlib", "cdylib"]

[dependencies]
pyo3 = "0.19"
numpy = "0.19"
pyo3 = "0.20"
numpy = "0.20"
time = { version = "0.3", features = ["parsing", "macros"] }
ndarray = "0.15"

[dev-dependencies]
assert_approx_eq = "1.1"
rstest = "0.18.2"
pyo3 = { version = "0.19", features = ["auto-initialize"]}
pyo3 = { version = "0.20", features = ["auto-initialize"]}

[features]
nonumpy = []
25 changes: 25 additions & 0 deletions docs/_inline/day_count_conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
The [day count convention](https://en.wikipedia.org/wiki/Day_count_convention)
determines how interest accrues over time in a variety of transactions,
including bonds, swaps, bills and loans.

The following conventions are supported:

| Name | Constant | Also known |
| ------------------ | -------------------------- | ------------------------------- |
| Actual/Actual ISDA | DayCount.ACT_ACT_ISDA | Act/Act ISDA |
| Actual/365 Fixed | DayCount.ACT_365F | Act/365F, English |
| Actual/365.25 | DayCount.ACT_365_25 | |
| Actual/364 | DayCount.ACT_364 | |
| Actual/360 | DayCount.ACT_360 | French |
| 30/360 ISDA | DayCount.THIRTY_360_ISDA | Bond basis |
| 30E/360 | DayCount.THIRTY_E_360 | 30/360 ISMA, Eurobond basis |
| 30E+/360 | DayCount.THIRTY_E_PLUS_360 | |
| 30E/360 ISDA | DayCount.THIRTY_E_360_ISDA | 30E/360 German, German |
| 30U/360 | DayCount.THIRTY_U_360 | 30/360 US, 30US/360, 30/360 SIA |
| NL/365 | DayCount.NL_365 | Actual/365 No leap year |
| NL/360 | DayCount.NL_360 | |

See also:

- [2006 ISDA definitions](https://www.rbccm.com/assets/rbccm/docs/legal/doddfrank/Documents/ISDALibrary/2006%20ISDA%20Definitions.pdf)
- http://www.deltaquants.com/day-count-conventions
3 changes: 3 additions & 0 deletions docs/_inline/pe/dpi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
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.
5 changes: 5 additions & 0 deletions docs/_inline/pe/ks_pme_flows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
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.
9 changes: 9 additions & 0 deletions docs/_inline/pe/ln_pme_nav.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Use the Long-Nickels method to re-calculate the private equity nav to match the
public market equivalents (PME) for comparison. This method just re-calculates
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 = c1 * px_final/px_1 + c2 * px_final/px_2
- d1 * px_final/px_1 - d2 * px_final/px_2
13 changes: 13 additions & 0 deletions docs/_inline/pe/pme_plus_flows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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
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

Solve for lambda so that the two sides of the equation are equal. Then multiply
all the distributions by lambda to re-scale them.
3 changes: 3 additions & 0 deletions docs/_inline/pe/rvpi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
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.
5 changes: 5 additions & 0 deletions docs/_inline/pe/tvpi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
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.
26 changes: 1 addition & 25 deletions docs/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,7 @@ CashFlow = Union[CashFlowSeries, CashFlowTable, CashFlowDict]

## Day Count Conventions

The [day count convention](https://en.wikipedia.org/wiki/Day_count_convention)
determines how interest accrues over time in a variety of transactions,
including bonds, swaps, bills and loans.

The following conventions are supported:

| Name | Constant | Also known |
| ------------------ | -------------------------- | ------------------------------- |
| Actual/Actual ISDA | DayCount.ACT_ACT_ISDA | Act/Act ISDA |
| Actual/365 Fixed | DayCount.ACT_365F | Act/365F, English |
| Actual/365.25 | DayCount.ACT_365_25 | |
| Actual/364 | DayCount.ACT_364 | |
| Actual/360 | DayCount.ACT_360 | French |
| 30/360 ISDA | DayCount.THIRTY_360_ISDA | Bond basis |
| 30E/360 | DayCount.THIRTY_E_360 | 30/360 ISMA, Eurobond basis |
| 30E+/360 | DayCount.THIRTY_E_PLUS_360 | |
| 30E/360 ISDA | DayCount.THIRTY_E_360_ISDA | 30E/360 German, German |
| 30U/360 | DayCount.THIRTY_U_360 | 30/360 US, 30US/360, 30/360 SIA |
| NL/365 | DayCount.NL_365 | Actual/365 No leap year |
| NL/360 | DayCount.NL_360 | |
{% include_relative ./_inline/day_count_conventions.md %}

Definition:

Expand All @@ -67,11 +48,6 @@ year_fraction("2019-11-09", "2020-03-05", DayCount.THIRTY_E_360)
year_fraction("2019-11-09", "2020-03-05", "act/360")
```

See also:

- [2006 ISDA definitions](https://www.rbccm.com/assets/rbccm/docs/legal/doddfrank/Documents/ISDALibrary/2006%20ISDA%20Definitions.pdf)
- http://www.deltaquants.com/day-count-conventions

## Exceptions

- `InvalidPaymentsError`. Occurs if either:
Expand Down
1 change: 0 additions & 1 deletion src/broadcasting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use pyo3::{
exceptions::{PyTypeError, PyValueError},
prelude::*,
types::{PyIterator, PyList, PySequence, PyTuple},
AsPyPointer,
};

use crate::conversions::float_or_none;
Expand Down
16 changes: 16 additions & 0 deletions src/conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,22 @@ fn extract_records(data: &PyAny) -> PyResult<(Vec<DateLike>, Vec<f64>)> {
Ok((_dates, _amounts))
}

pub struct AmountArray(Vec<f64>);

impl<'s> FromPyObject<'s> for AmountArray {
fn extract(obj: &'s PyAny) -> PyResult<Self> {
extract_amount_series(obj).map(|v| AmountArray(v))
}
}

impl std::ops::Deref for AmountArray {
type Target = [f64];

fn deref(&self) -> &[f64] {
self.0.as_ref()
}
}

pub fn extract_amount_series(series: &PyAny) -> PyResult<Vec<f64>> {
match series.get_type().name()? {
"Series" => extract_amount_series_from_numpy(series.getattr("values")?),
Expand Down
1 change: 1 addition & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ mod scheduled;
pub use models::{DateLike, InvalidPaymentsError};
pub use periodic::*;
pub use scheduled::{days_between, xfv, xirr, xnfv, xnpv, year_fraction, DayCount};
pub mod private_equity;
12 changes: 9 additions & 3 deletions src/core/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ impl FromStr for DateLike {
#[derive(Debug)]
pub struct InvalidPaymentsError(String);

impl InvalidPaymentsError {
pub fn new<T: fmt::Display>(message: T) -> Self {
Self(message.to_string())
}
}

impl fmt::Display for InvalidPaymentsError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
Expand All @@ -59,8 +65,8 @@ impl Error for InvalidPaymentsError {}

pub fn validate(payments: &[f64], dates: Option<&[DateLike]>) -> Result<(), InvalidPaymentsError> {
if dates.is_some() && payments.len() != dates.unwrap_or_default().len() {
return Err(InvalidPaymentsError(
"the amounts and dates arrays are of different lengths".into(),
return Err(InvalidPaymentsError::new(
"the amounts and dates arrays are of different lengths",
));
}

Expand All @@ -70,6 +76,6 @@ pub fn validate(payments: &[f64], dates: Option<&[DateLike]>) -> Result<(), Inva
if positive && negative {
Ok(())
} else {
Err(InvalidPaymentsError("negative and positive payments are required".into()))
Err(InvalidPaymentsError::new("negative and positive payments are required"))
}
}
Loading

0 comments on commit 9eb91b1

Please sign in to comment.