Navigation Menu

Skip to content

Commit

Permalink
release GIL for rust-only code + add silent option for all functions …
Browse files Browse the repository at this point in the history
…with validation + refactor tests + update docs
  • Loading branch information
Anexen committed Nov 29, 2021
1 parent 23710ac commit c767c46
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 302 deletions.
9 changes: 8 additions & 1 deletion README.md
Expand Up @@ -92,8 +92,8 @@ See the [docs](https://anexen.github.io/pyxirr)
- [x] Implement all functions from [numpy-financial](https://numpy.org/numpy-financial/latest/index.html)
- [ ] Improve docs, add more tests
- [ ] Type hints [](https://github.com/PyO3/maturin/blob/main/test-crates/pyo3-pure/pyo3_pure.pyi)
- [ ] Compile library for rust/javascript/python
- [ ] Vectorized versions of numpy-financial functions.
- [ ] Compile library for rust/javascript/python

# Development

Expand Down Expand Up @@ -124,6 +124,13 @@ $ maturin develop
$ LD_LIBRARY_PATH=${PYENV_ROOT}/versions/3.8.6/lib cargo test --no-default-features --features tests
```

### Benchmarks

```bash
$ pip install -r bench-requirements.txt
$ LD_LIBRARY_PATH=${PYENV_ROOT}/versions/3.8.6/lib cargo +nightly bench --no-default-features --features tests
```

# Building and distribution

This library uses [maturin](https://github.com/PyO3/maturin) to build and distribute python wheels.
Expand Down
11 changes: 1 addition & 10 deletions benches/comparison.rs
Expand Up @@ -39,16 +39,7 @@ macro_rules! bench_rust {
fn $name(b: &mut Bencher) {
Python::with_gil(|py| {
let data = PaymentsLoader::from_csv(py, $file).to_records();
b.iter(|| {
pyxirr::xirr(
py,
black_box(data),
black_box(None),
black_box(None),
black_box(None),
)
.unwrap()
});
b.iter(|| pyxirr_call_impl!(py, "xirr", (data,)).unwrap());
});
}
};
Expand Down
28 changes: 5 additions & 23 deletions benches/input.rs
Expand Up @@ -2,7 +2,7 @@

extern crate test;

use test::{black_box, Bencher};
use test::Bencher;

use pyo3::Python;

Expand All @@ -14,10 +14,7 @@ fn bench_from_records(b: &mut Bencher) {
Python::with_gil(|py| {
let input = "tests/samples/random_100.csv";
let data = common::PaymentsLoader::from_csv(py, input).to_records();
b.iter(|| {
pyxirr::xirr(py, black_box(data), black_box(None), black_box(None), black_box(None))
.unwrap()
});
b.iter(|| pyxirr_call_impl!(py, "xirr", (data,)).unwrap());
});
}

Expand All @@ -26,16 +23,7 @@ fn bench_from_columns(b: &mut Bencher) {
Python::with_gil(|py| {
let input = "tests/samples/random_100.csv";
let data = common::PaymentsLoader::from_csv(py, input).to_columns();
b.iter(|| {
pyxirr::xirr(
py,
black_box(data.0),
black_box(Some(data.1)),
black_box(None),
black_box(None),
)
.unwrap()
});
b.iter(|| pyxirr_call_impl!(py, "xirr", data).unwrap());
});
}

Expand All @@ -44,10 +32,7 @@ fn bench_from_dict(b: &mut Bencher) {
Python::with_gil(|py| {
let input = "tests/samples/random_100.csv";
let data = common::PaymentsLoader::from_csv(py, input).to_dict();
b.iter(|| {
pyxirr::xirr(py, black_box(data), black_box(None), black_box(None), black_box(None))
.unwrap()
});
b.iter(|| pyxirr_call_impl!(py, "xirr", (data,)).unwrap());
});
}

Expand All @@ -56,9 +41,6 @@ fn bench_from_pandas(b: &mut Bencher) {
Python::with_gil(|py| {
let input = "tests/samples/random_100.csv";
let data = common::pd_read_csv(py, input);
b.iter(|| {
pyxirr::xirr(py, black_box(data), black_box(None), black_box(None), black_box(None))
.unwrap()
});
b.iter(|| pyxirr_call_impl!(py, "xirr", (data,)).unwrap());
});
}
9 changes: 5 additions & 4 deletions benches/npf.rs
Expand Up @@ -7,11 +7,14 @@ use test::{black_box, Bencher};
use pyo3::types::*;
use pyo3::Python;

#[path = "../tests/common/mod.rs"]
mod common;

#[bench]
fn bench_irr(b: &mut Bencher) {
Python::with_gil(|py| {
let payments = PyList::new(py, &[-100, 39, 59, 55, 20]);
b.iter(|| pyxirr::irr(black_box(&payments), black_box(None)).unwrap().unwrap())
b.iter(|| pyxirr_call_impl!(py, "irr", (payments,)).unwrap());
});
}

Expand All @@ -28,9 +31,7 @@ fn bench_irr_npf(b: &mut Bencher) {
fn bench_mirr(b: &mut Bencher) {
Python::with_gil(|py| {
let values = PyList::new(py, &[-1000, 100, 250, 500, 500]);
b.iter(|| {
pyxirr::mirr(black_box(&values), black_box(0.1), black_box(0.1)).unwrap().unwrap()
})
b.iter(|| pyxirr_call_impl!(py, "mirr", (values, 0.1, 0.1)).unwrap())
});
}

Expand Down
97 changes: 68 additions & 29 deletions docs/functions.md
Expand Up @@ -10,8 +10,8 @@
# `None` if the calculation fails to converge or result is NaN.
# could return `inf` or `-inf`
DateLike = Union[datetime.date, datetime.datetime, numpy.datetime64, pandas.Timestamp]
Rate = float # rate as decimal, not percentage, normally between [-1, 1]
Period = Union[int, float]
Rate = Union[float, Decimal] # rate as decimal, not percentage, normally between [-1, 1]
Period = Union[int, float, Decimal]
Guess = Optional[Rate]
Amount = Union[int, float, Decimal]
Payment = Tuple[DateLike, Amount]
Expand Down Expand Up @@ -70,11 +70,13 @@ What is the future value after 10 years of saving $100 now, with an additional m
Net Future Value

```python
# raises: InvalidPaymentsError
# raises: InvalidPaymentsError (suppressed by passing silent=True flag)
def nfv(
rate: Rate, # Rate of interest per period
nper: Period, # Number of compounding periods
amounts: AmountArray,
*,
silent: bool = False
) -> Optional[float]:
...
```
Expand Down Expand Up @@ -152,11 +154,13 @@ Net Future Value of a series of irregular cash flows.
All cash flows in a group are compounded to the latest cash flow in the group.

```python
# raises InvalidPaymentsError
# raises: InvalidPaymentsError (suppressed by passing silent=True flag)
def xnfv(
rate: Rate, # annual rate
dates: Union[CashFlow, DateLikeArray],
amounts: Optional[AmountArray] = None,
*,
silent: bool = False
) -> Optional[float]:
...
```
Expand All @@ -168,13 +172,14 @@ See also: [XLeratorDB.XNFV](http://westclintech.com/SQL-Server-Financial-Functio
Compute the payment against loan principal plus interest.

```python
pmt(
def pmt(
rate: Rate, # Rate of interest per period
nper: Period, # Number of compounding periods
pv: Amount, # Present value
fv: Amount = 0, # Future value
pmt_at_begining: bool = False # When payments are due
) -> FloatOrNone
) -> Optional[float]:
...
```

```
Expand All @@ -188,14 +193,15 @@ See also: [FV](functions.md#fv), [PV](functions.md#pv), [NPER](functions.md#nper
Compute the interest portion of a payment.

```python
ipmt(
def ipmt(
rate: Rate, # Rate of interest per period
per: Period, # The payment period to calculate the interest amount.
nper: Period, # Number of compounding periods
pv: Amount, # Present value
fv: Amount = 0, # Future value
pmt_at_begining: bool = False # When payments are due
) -> FloatOrNone
) -> Optional[float]:
...
```

See also: [PMT](functions.md#pmt)
Expand All @@ -205,14 +211,15 @@ See also: [PMT](functions.md#pmt)
Compute the payment against loan principal.

```python
ppmt(
def ppmt(
rate: Rate, # Rate of interest per period
per: Period, # The payment period to calculate the interest amount.
nper: Period, # Number of compounding periods
pv: Amount, # Present value
fv: Amount = 0, # Future value
pmt_at_begining: bool = False # When payments are due
) -> FloatOrNone
) -> Optional[float]:
...
```

See also: [PMT](functions.md#pmt)
Expand All @@ -222,13 +229,14 @@ See also: [PMT](functions.md#pmt)
Compute the payment against loan principal plus interest.

```python
nper(
def nper(
rate: Rate, # Rate of interest per period
pmt: Amount, # Payment
pv: Amount, # Present value
fv: Amount = 0, # Future value
pmt_at_begining: bool = False # When payments are due
) -> FloatOrNone
) -> Optional[float]:
...
```

See also: [FV](functions.md#fv), [PV](functions.md#pv), [PMT](functions.md#pmt)
Expand All @@ -238,14 +246,16 @@ See also: [FV](functions.md#fv), [PV](functions.md#pv), [PMT](functions.md#pmt)
Compute the payment against loan principal plus interest.

```python
rate(
def rate(
nper: Period, # Number of compounding periods
pmt: Amount, # Payment
pv: Amount, # Present value
fv: Amount = 0, # Future value
pmt_at_begining: bool = False # When payments are due
*,
guess: Guess = 0.1
) -> FloatOrNone
) -> Optional[float]:
...
```

See also: [FV](functions.md#fv), [PV](functions.md#pv), [PMT](functions.md#pmt)
Expand All @@ -255,13 +265,14 @@ See also: [FV](functions.md#fv), [PV](functions.md#pv), [PMT](functions.md#pmt)
Compute the present value.

```python
pv(
def pv(
rate: Rate, # Rate of interest per period
nper: Period, # Number of compounding periods
pmt: Amount, # Payment
fv: Amount = 0, # Future value
pmt_at_begining: bool = False # When payments are due
) -> FloatOrNone
) -> Optional[float]:
...
```

The present value is computed by solving the same equation as for future value:
Expand Down Expand Up @@ -290,15 +301,24 @@ Assume the interest rate is 5% (annually) compounded monthly.
Compute the Net Present Value.

```python
npv(rate: Rate, amounts: AmountArray, start_from_zero=True) -> FloatOrNone
def npv(
rate: Rate,
amounts: AmountArray,
start_from_zero=True
) -> Optional[float]:
...
```

NPV is calculated using the following formula:

$$\sum_{i=0}^{N-1} \frac{values_i}{(1 + rate)^i}$$

> Values must begin with the initial investment, thus values[0] will typically be negative.
> NPV considers a series of cashflows starting in the present (i = 0). NPV can also be defined with a series of future cashflows, paid at the end, rather than the start, of each period. If future cashflows are used, the first cashflow values[0] must be zeroed and added to the net present value of the future cashflows.
> NPV considers a series of cashflows starting in the present (i = 0). NPV can
> also be defined with a series of future cashflows, paid at the end, rather
> than the start, of each period. If future cashflows are used, the first
> cashflow values[0] must be zeroed and added to the net present value of the
> future cashflows.
> There is a difference between numpy NPV and excel NPV.
> The [numpy docs](https://numpy.org/numpy-financial/latest/npv.html#numpy_financial.npv) show the summation from i=0 to N-1.
Expand All @@ -316,7 +336,10 @@ $$\sum_{i=0}^{N-1} \frac{values_i}{(1 + rate)^i}$$
2838.1691372032656
```

It may be preferable to split the projected cashflow into an initial investment and expected future cashflows. In this case, the value of the initial cashflow is zero and the initial investment is later added to the future cashflows net present value.
It may be preferable to split the projected cashflow into an initial investment
and expected future cashflows. In this case, the value of the initial cashflow
is zero and the initial investment is later added to the future cashflows net
present value.

```python
>>> from pyxirr import npv
Expand All @@ -331,12 +354,15 @@ Returns the Net Present Value for a schedule of cash flows that is not necessari
> To calculate the Net Present Value for a periodic cash flows, use the NPV function.
```python
# raises: InvalidPaymentsError
xnpv(
# raises: InvalidPaymentsError (suppressed by passing silent=True flag)
def xnpv(
rate: Rate,
dates: Union[CashFlow, DateLikeArray],
amounts: Optional[AmountArray] = None,
) -> FloatOrNone
*,
silent: bool = False
) -> Optional[float]:
...
```

XNPV is calculated as follows:
Expand Down Expand Up @@ -408,8 +434,14 @@ InvalidPaymentsError: negative and positive payments are required
Compute the Internal Rate of Return.

```python
# raises: InvalidPaymentsError
irr(amounts: AmountArray, guess: Guess = 0.1) -> FloatOrNone
# raises: InvalidPaymentsError (suppressed by passing silent=True flag)
def irr(
amounts: AmountArray,
*,
guess: Guess = 0.1
silent: bool = False
) -> Optional[float]:
...
```

This is the "average" periodically compounded rate of return that gives a [NPV](#npv) of 0.
Expand Down Expand Up @@ -443,11 +475,15 @@ InvalidPaymentsError: negative and positive payments are required
Modified Internal Rate of Return.

```python
mirr(
# raises: InvalidPaymentsError (suppressed by passing silent=True flag)
def mirr(
values: AmountArray, # Cash flows. Must contain at least one positive and one negative value or nan is returned.
finance_rate: Rate, # Interest rate paid on the cash flows
reinvest_rate: Rate, # Interest rate received on the cash flows upon reinvestment
) -> FloatOrNone
*,
silent: bool = False,
) -> Optional[float]:
...
```

MIRR considers both the cost of the investment and the interest received on reinvestment of cash.
Expand Down Expand Up @@ -479,12 +515,15 @@ So the result of:
Returns the internal rate of return for a schedule of cash flows that is not necessarily periodic.

```python
# raises: InvalidPaymentsError
xirr(
# raises: InvalidPaymentsError (suppressed by passing silent=True flag)
def xirr(
dates: Union[CashFlow, DateLikeArray],
amounts: Optional[AmountArray] = None,
*,
guess: Guess = 0.1,
) -> FloatOrNone
silent: bool = False,
) -> Optional[float]:
...
```

XIRR is closely related to [XNPV](#xnpv), the Net Present Value function. XIRR is the interest rate corresponding to XNPV = 0.
Expand Down

0 comments on commit c767c46

Please sign in to comment.