From c767c46fedf3eee44ae08b6b896c506728bfafb6 Mon Sep 17 00:00:00 2001 From: Anexen Date: Tue, 30 Nov 2021 00:06:40 +0300 Subject: [PATCH] release GIL for rust-only code + add silent option for all functions with validation + refactor tests + update docs --- README.md | 9 +- benches/comparison.rs | 11 +- benches/input.rs | 28 +--- benches/npf.rs | 9 +- docs/functions.md | 97 ++++++++---- src/core/periodic.rs | 16 +- src/lib.rs | 137 +++++++++++------ tests/test_periodic.rs | 323 +++++++++++++++++++++------------------- tests/test_scheduled.rs | 48 +++--- 9 files changed, 376 insertions(+), 302 deletions(-) diff --git a/README.md b/README.md index def7487..0594336 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/benches/comparison.rs b/benches/comparison.rs index 06c9bca..e3c1cb8 100644 --- a/benches/comparison.rs +++ b/benches/comparison.rs @@ -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()); }); } }; diff --git a/benches/input.rs b/benches/input.rs index c5d035b..aec7c6b 100644 --- a/benches/input.rs +++ b/benches/input.rs @@ -2,7 +2,7 @@ extern crate test; -use test::{black_box, Bencher}; +use test::Bencher; use pyo3::Python; @@ -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()); }); } @@ -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()); }); } @@ -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()); }); } @@ -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()); }); } diff --git a/benches/npf.rs b/benches/npf.rs index a3b3830..9fcbe0e 100644 --- a/benches/npf.rs +++ b/benches/npf.rs @@ -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()); }); } @@ -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()) }); } diff --git a/docs/functions.md b/docs/functions.md index 5302acc..9c0b33c 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -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] @@ -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]: ... ``` @@ -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]: ... ``` @@ -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]: + ... ``` ``` @@ -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) @@ -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) @@ -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) @@ -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) @@ -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: @@ -290,7 +301,12 @@ 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: @@ -298,7 +314,11 @@ 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. @@ -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 @@ -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: @@ -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. @@ -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. @@ -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. diff --git a/src/core/periodic.rs b/src/core/periodic.rs index cd74bad..8eaf0b8 100644 --- a/src/core/periodic.rs +++ b/src/core/periodic.rs @@ -150,6 +150,7 @@ fn npv_deriv(rate: f64, values: &[f64]) -> f64 { } pub fn irr(values: &[f64], guess: Option) -> Result { + // must contain at least one positive and one negative value validate(values, None)?; Ok(find_root_newton_raphson_with_brute_force( @@ -160,12 +161,13 @@ pub fn irr(values: &[f64], guess: Option) -> Result f64 { - // must contain at least one positive and one negative value or nan is returned - // make it consistent with numpy_financial - if validate(values, None).is_err() { - return f64::NAN; - } +pub fn mirr( + values: &[f64], + finance_rate: f64, + reinvest_rate: f64, +) -> Result { + // must contain at least one positive and one negative value + validate(values, None)?; let positive: f64 = powers(1. + reinvest_rate, values.len(), true) .iter() @@ -181,5 +183,5 @@ pub fn mirr(values: &[f64], finance_rate: f64, reinvest_rate: f64) -> f64 { .map(|(&r, &v)| v / r) .sum(); - (positive / -negative).powf(1.0 / (values.len() - 1) as f64) - 1.0 + Ok((positive / -negative).powf(1.0 / (values.len() - 1) as f64) - 1.0) } diff --git a/src/lib.rs b/src/lib.rs index a6dda7f..e2262dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,7 @@ where /// Internal Rate of Return for a non-periodic cash flows. #[pyfunction(amounts = "None", "*", guess = "0.1", silent = "false")] #[pyo3(text_signature = "(dates, amounts, *, guess=0.1, silent=False)")] -pub fn xirr( +fn xirr( py: Python, dates: &PyAny, amounts: Option<&PyAny>, @@ -47,28 +47,43 @@ pub fn xirr( silent: Option, ) -> PyResult> { let (dates, amounts) = conversions::extract_payments(dates, amounts)?; - py.allow_threads(|| { + py.allow_threads(move || { let result = core::xirr(&dates, &amounts, guess); fallible_float_or_none(result, silent.unwrap_or(false)) }) } /// Net Present Value for a non-periodic cash flows. -#[pyfunction(amounts = "None")] -#[pyo3(text_signature = "(rate, dates, amounts)")] -pub fn xnpv(rate: f64, dates: &PyAny, amounts: Option<&PyAny>) -> PyResult> { +#[pyfunction(amounts = "None", "*", silent = "false")] +#[pyo3(text_signature = "(rate, dates, amounts, *, silent=False)")] +fn xnpv( + py: Python, + rate: f64, + dates: &PyAny, + amounts: Option<&PyAny>, + silent: Option, +) -> PyResult> { let (dates, amounts) = conversions::extract_payments(dates, amounts)?; - let result = core::xnpv(rate, &dates, &amounts); - fallible_float_or_none(result, false) + py.allow_threads(move || { + let result = core::xnpv(rate, &dates, &amounts); + fallible_float_or_none(result, silent.unwrap_or(false)) + }) } /// Internal Rate of Return -#[pyfunction(guess = "0.1")] -#[pyo3(text_signature = "(amounts, guess=0.1)")] -pub fn irr(amounts: &PyAny, guess: Option) -> PyResult> { +#[pyfunction("*", guess = "0.1", silent = "false")] +#[pyo3(text_signature = "(amounts, *, guess=0.1, silent=False)")] +fn irr( + py: Python, + amounts: &PyAny, + guess: Option, + silent: Option, +) -> PyResult> { let amounts = conversions::extract_amount_series(amounts)?; - let result = core::irr(&amounts, guess); - fallible_float_or_none(result, false) + py.allow_threads(move || { + let result = core::irr(&amounts, guess); + fallible_float_or_none(result, silent.unwrap_or(false)) + }) } /// Net Present Value. @@ -79,25 +94,39 @@ pub fn irr(amounts: &PyAny, guess: Option) -> PyResult> { /// but you can call it with `start_from_zero=False` parameter to make it Excel compatible. #[pyfunction(start_from_zero = "true")] #[pyo3(text_signature = "(rate, amounts, start_from_zero = True)")] -pub fn npv(rate: f64, amounts: &PyAny, start_from_zero: Option) -> PyResult> { +fn npv( + py: Python, + rate: f64, + amounts: &PyAny, + start_from_zero: Option, +) -> PyResult> { let payments = conversions::extract_amount_series(amounts)?; - let result = core::npv(rate, &payments, start_from_zero); - Ok(float_or_none(result)) + py.allow_threads(move || { + let result = core::npv(rate, &payments, start_from_zero); + Ok(float_or_none(result)) + }) } /// Future Value. #[pyfunction(pmt_at_begining = "false")] #[pyo3(text_signature = "(rate, nper, pmt, pv, pmt_at_begining=False)")] -pub fn fv(rate: f64, nper: f64, pmt: f64, pv: f64, pmt_at_begining: Option) -> Option { - float_or_none(core::fv(rate, nper, pmt, pv, pmt_at_begining)) +fn fv( + py: Python, + rate: f64, + nper: f64, + pmt: f64, + pv: f64, + pmt_at_begining: Option, +) -> Option { + py.allow_threads(move || float_or_none(core::fv(rate, nper, pmt, pv, pmt_at_begining))) } /// Net Future Value. #[pyfunction] #[pyo3(text_signature = "(rate, nper, amounts)")] -pub fn nfv(rate: f64, nper: f64, amounts: &PyAny) -> PyResult> { +fn nfv(py: Python, rate: f64, nper: f64, amounts: &PyAny) -> PyResult> { let amounts = conversions::extract_amount_series(amounts)?; - Ok(float_or_none(core::nfv(rate, nper, &amounts))) + py.allow_threads(move || Ok(float_or_none(core::nfv(rate, nper, &amounts)))) } /// Extended Future Value. @@ -106,7 +135,8 @@ pub fn nfv(rate: f64, nper: f64, amounts: &PyAny) -> PyResult> { #[pyo3( text_signature = "(start_date, cash_flow_date, end_date, cash_flow_rate, end_rate, cash_flow)" )] -pub fn xfv( +fn xfv( + py: Python, start_date: core::DateLike, cash_flow_date: core::DateLike, end_date: core::DateLike, @@ -114,63 +144,75 @@ pub fn xfv( end_rate: f64, cash_flow: f64, ) -> PyResult> { - Ok(float_or_none(core::xfv( - &start_date, - &cash_flow_date, - &end_date, - cash_flow_rate, - end_rate, - cash_flow, - ))) + py.allow_threads(move || { + Ok(float_or_none(core::xfv( + &start_date, + &cash_flow_date, + &end_date, + cash_flow_rate, + end_rate, + cash_flow, + ))) + }) } /// Net future value of a series of irregular cash flows #[pyfunction(amounts = "None")] #[pyo3(text_signature = "(rate, dates, amounts)")] -pub fn xnfv(rate: f64, dates: &PyAny, amounts: Option<&PyAny>) -> PyResult> { +fn xnfv(py: Python, rate: f64, dates: &PyAny, amounts: Option<&PyAny>) -> PyResult> { let (dates, amounts) = conversions::extract_payments(dates, amounts)?; - Ok(float_or_none(core::xnfv(rate, &dates, &amounts)?)) + py.allow_threads(move || Ok(float_or_none(core::xnfv(rate, &dates, &amounts)?))) } /// Present Value #[pyfunction(fv = "0.0", pmt_at_begining = "false")] #[pyo3(text_signature = "(rate, nper, pmt, fv=0, pmt_at_begining=False)")] -pub fn pv( +fn pv( + py: Python, rate: f64, nper: f64, pmt: f64, fv: Option, pmt_at_begining: Option, ) -> Option { - float_or_none(core::pv(rate, nper, pmt, fv, pmt_at_begining)) + py.allow_threads(move || float_or_none(core::pv(rate, nper, pmt, fv, pmt_at_begining))) } /// Modified Internal Rate of Return. -#[pyfunction] -#[pyo3(text_signature = "(amounts, finance_rate, reinvest_rate)")] -pub fn mirr(values: &PyAny, finance_rate: f64, reinvest_rate: f64) -> PyResult> { +#[pyfunction("*", silent = "false")] +#[pyo3(text_signature = "(amounts, finance_rate, reinvest_rate, *, silent=False)")] +fn mirr( + py: Python, + values: &PyAny, + finance_rate: f64, + reinvest_rate: f64, + silent: Option, +) -> PyResult> { let values = conversions::extract_amount_series(values)?; - let result = core::mirr(&values, finance_rate, reinvest_rate); - Ok(float_or_none(result)) + py.allow_threads(move || { + let result = core::mirr(&values, finance_rate, reinvest_rate); + fallible_float_or_none(result, silent.unwrap_or(false)) + }) } /// Compute the payment against loan principal plus interest. #[pyfunction(fv = "0.0", pmt_at_begining = "false")] #[pyo3(text_signature = "(rate, nper, pv, fv=0, pmt_at_begining=False)")] -pub fn pmt( +fn pmt( + py: Python, rate: f64, nper: f64, pv: f64, fv: Option, pmt_at_begining: Option, ) -> Option { - float_or_none(core::pmt(rate, nper, pv, fv, pmt_at_begining)) + py.allow_threads(move || float_or_none(core::pmt(rate, nper, pv, fv, pmt_at_begining))) } /// Compute the interest portion of a payment. #[pyfunction(fv = "0.0", pmt_at_begining = "false")] #[pyo3(text_signature = "(rate, per, nper, pv, fv=0, pmt_at_begining=False)")] -pub fn ipmt( +fn ipmt( rate: f64, per: f64, nper: f64, @@ -184,7 +226,8 @@ pub fn ipmt( /// Compute the payment against loan principal. #[pyfunction(fv = "0.0", pmt_at_begining = "false")] #[pyo3(text_signature = "(rate, per, nper, pv, fv=0, pmt_at_begining=False)")] -pub fn ppmt( +fn ppmt( + py: Python, rate: f64, per: f64, nper: f64, @@ -192,26 +235,28 @@ pub fn ppmt( fv: Option, pmt_at_begining: Option, ) -> Option { - float_or_none(core::ppmt(rate, per, nper, pv, fv, pmt_at_begining)) + py.allow_threads(move || float_or_none(core::ppmt(rate, per, nper, pv, fv, pmt_at_begining))) } /// Compute the number of periodic payments. #[pyfunction(fv = "0.0", pmt_at_begining = "false")] #[pyo3(text_signature = "(rate, pmt, pv, fv=0, pmt_at_begining=False)")] -pub fn nper( +fn nper( + py: Python, rate: f64, pmt: f64, pv: f64, fv: Option, pmt_at_begining: Option, ) -> Option { - float_or_none(core::nper(rate, pmt, pv, fv, pmt_at_begining)) + py.allow_threads(move || float_or_none(core::nper(rate, pmt, pv, fv, pmt_at_begining))) } /// Compute the number of periodic payments. #[pyfunction(fv = "0.0", pmt_at_begining = "false", guess = "0.1")] #[pyo3(text_signature = "(nper, pmt, pv, fv=0, pmt_at_begining=False, guess=0.1)")] -pub fn rate( +fn rate( + py: Python, nper: f64, pmt: f64, pv: f64, @@ -219,7 +264,7 @@ pub fn rate( pmt_at_begining: Option, guess: Option, ) -> Option { - float_or_none(core::rate(nper, pmt, pv, fv, pmt_at_begining, guess)) + py.allow_threads(move || float_or_none(core::rate(nper, pmt, pv, fv, pmt_at_begining, guess))) } #[pymodule] diff --git a/tests/test_periodic.rs b/tests/test_periodic.rs index e2e95e9..9af3900 100644 --- a/tests/test_periodic.rs +++ b/tests/test_periodic.rs @@ -26,102 +26,103 @@ fn test_fv_macro_working() { #[rstest] fn test_fv_pmt_at_end() { - let result = pyxirr::fv(0.05 / 12.0, 10.0 * 12.0, -100.0, -100.0, None).unwrap(); - assert_almost_eq!(result, 15692.9288943357); + Python::with_gil(|py| { + let args = (0.05 / 12.0, 10.0 * 12.0, -100.0, -100.0); + let result: f64 = pyxirr_call!(py, "fv", args); + assert_almost_eq!(result, 15692.9288943357); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_fv = py.import("numpy_financial").unwrap().getattr("fv").unwrap(); - let npf_result = npf_fv.call1((0.05 / 12.0, 10.0 * 12.0, -100.0, -100.0)); + let npf_result = npf_fv.call1(args); assert_almost_eq!(result, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_fv_pmt_at_begining() { - let result = pyxirr::fv(0.05 / 12.0, 10.0 * 12.0, -100.0, -100.0, Some(true)).unwrap(); - assert_almost_eq!(result, 15757.6298441047); + Python::with_gil(|py| { + let result: f64 = pyxirr_call!(py, "fv", (0.05 / 12.0, 10 * 12, -100, -100, true)); + assert_almost_eq!(result, 15757.6298441047); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_fv = py.import("numpy_financial").unwrap().getattr("fv").unwrap(); let npf_result = npf_fv.call1((0.05 / 12.0, 10.0 * 12.0, -100.0, -100.0, "start")); assert_almost_eq!(result, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_fv_zero_rate() { - let result = pyxirr::fv(0.0, 10.0 * 12.0, -100.0, -100.0, None).unwrap(); - assert_almost_eq!(result, 12100.0); + Python::with_gil(|py| { + let result: f64 = pyxirr_call!(py, "fv", (0, 10 * 12, -100, -100)); + assert_almost_eq!(result, 12100.0); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_fv = py.import("numpy_financial").unwrap().getattr("fv").unwrap(); let npf_result = npf_fv.call1((0.0, 10.0 * 12.0, -100.0, -100.0)); assert_almost_eq!(result, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } // ------------ PV ---------------- #[rstest] fn test_pv_pmt_at_end() { - let result = pyxirr::pv(0.05 / 12.0, 10.0 * 12.0, -100.0, Some(15692.93), None).unwrap(); - assert_almost_eq!(result, -100.0006713162); + Python::with_gil(|py| { + let result: f64 = pyxirr_call!(py, "pv", (0.05 / 12.0, 10 * 12, -100, 15692.93)); + assert_almost_eq!(result, -100.0006713162); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_pv = py.import("numpy_financial").unwrap().getattr("pv").unwrap(); let npf_result = npf_pv.call1((0.05 / 12.0, 10.0 * 12.0, -100.0, 15692.93)); assert_almost_eq!(result, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_pv_pmt_at_begining() { - let result = pyxirr::pv(0.05 / 12.0, 10.0 * 12.0, -100.0, Some(15692.93), Some(true)).unwrap(); - assert_almost_eq!(result, -60.71677534615); + Python::with_gil(|py| { + let result: f64 = pyxirr_call!(py, "pv", (0.05 / 12.0, 10 * 12, -100, 15692.93, true)); + assert_almost_eq!(result, -60.71677534615); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_pv = py.import("numpy_financial").unwrap().getattr("pv").unwrap(); let npf_result = npf_pv.call1((0.05 / 12.0, 10.0 * 12.0, -100.0, 15692.93, "start")); assert_almost_eq!(result, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_pv_zero_rate() { - let result = pyxirr::pv(0.0, 10.0 * 12.0, -100.0, Some(15692.93), None).unwrap(); - assert_almost_eq!(result, -3692.93); + Python::with_gil(|py| { + let result: f64 = pyxirr_call!(py, "pv", (0, 10 * 12, -100, 15692.93)); + assert_almost_eq!(result, -3692.93); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_pv = py.import("numpy_financial").unwrap().getattr("pv").unwrap(); let npf_result = npf_pv.call1((0.0, 10.0 * 12.0, -100.0, 15692.93)); assert_almost_eq!(result, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_pv_default_pv() { - let result = pyxirr::pv(0.05 / 12.0, 10.0 * 12.0, -100.0, None, None).unwrap(); - assert_almost_eq!(result, 9428.1350328234); + Python::with_gil(|py| { + let result: f64 = pyxirr_call!(py, "pv", (0.05 / 12.0, 10 * 12, -100)); + assert_almost_eq!(result, 9428.1350328234); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_pv = py.import("numpy_financial").unwrap().getattr("pv").unwrap(); let npf_result = npf_pv.call1((0.05 / 12.0, 10.0 * 12.0, -100.0)); assert_almost_eq!(result, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } // ------------ NPV ---------------- @@ -130,7 +131,7 @@ fn test_pv_default_pv() { fn test_npv_works() { Python::with_gil(|py| { let values = PyList::new(py, &[-40_000., 5_000., 8_000., 12_000., 30_000.]); - let result = pyxirr::npv(0.08, values, None).unwrap().unwrap(); + let result: f64 = pyxirr_call!(py, "npv", (0.08, values)); assert_almost_eq!(result, 3065.222668179); if cfg!(not(feature = "nonumpy")) { @@ -145,7 +146,7 @@ fn test_npv_works() { fn test_npv_start_from_zero() { Python::with_gil(|py| { let values = PyList::new(py, &[-40_000., 5_000., 8_000., 12_000., 30_000.]); - let result = pyxirr::npv(0.08, values, Some(false)).unwrap().unwrap(); + let result: f64 = pyxirr_call!(py, "npv", (0.08, values, false)); assert_almost_eq!(result, 2838.169137203); }); } @@ -154,7 +155,7 @@ fn test_npv_start_from_zero() { fn test_npv_zero_rate() { Python::with_gil(|py| { let values = PyList::new(py, &[-40_000., 5_000., 8_000., 12_000., 30_000.]); - let result = pyxirr::npv(0., values, Some(false)).unwrap().unwrap(); + let result: f64 = pyxirr_call!(py, "npv", (0, values, false)); assert_almost_eq!(result, 15_000.0); if cfg!(not(feature = "nonumpy")) { @@ -169,234 +170,243 @@ fn test_npv_zero_rate() { #[rstest] fn test_pmt_pmt_at_end() { - let pmt = pyxirr::pmt(INTEREST_RATE, PERIODS, PV, None, None).unwrap(); - assert_future_value!(INTEREST_RATE, PERIODS, pmt, PV, None, None); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + Python::with_gil(|py| { + let pmt: f64 = pyxirr_call!(py, "pmt", (INTEREST_RATE, PERIODS, PV)); + assert_future_value!(INTEREST_RATE, PERIODS, pmt, PV, None, None); + if cfg!(not(feature = "nonumpy")) { let npf_pmt = py.import("numpy_financial").unwrap().getattr("pmt").unwrap(); let npf_result = npf_pmt.call1((INTEREST_RATE, PERIODS, PV)); assert_almost_eq!(pmt, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_pmt_pmt_at_begining() { - let pmt = pyxirr::pmt(INTEREST_RATE, PERIODS, PV, None, Some(true)).unwrap(); - assert_future_value!(INTEREST_RATE, PERIODS, pmt, PV, None, Some(true)); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + Python::with_gil(|py| { + let pmt: f64 = pyxirr_call!(py, "pmt", (INTEREST_RATE, PERIODS, PV, 0, true)); + assert_future_value!(INTEREST_RATE, PERIODS, pmt, PV, None, Some(true)); + if cfg!(not(feature = "nonumpy")) { let npf_pmt = py.import("numpy_financial").unwrap().getattr("pmt").unwrap(); let npf_result = npf_pmt.call1((INTEREST_RATE, PERIODS, PV, 0, "start")); assert_almost_eq!(pmt, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_pmt_non_zero_fv() { - let pmt = pyxirr::pmt(INTEREST_RATE, PERIODS, PV, Some(FV), None).unwrap(); - assert_future_value!(INTEREST_RATE, PERIODS, pmt, PV, Some(FV), None); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + Python::with_gil(|py| { + let pmt: f64 = pyxirr_call!(py, "pmt", (INTEREST_RATE, PERIODS, PV, FV)); + assert_future_value!(INTEREST_RATE, PERIODS, pmt, PV, Some(FV), None); + if cfg!(not(feature = "nonumpy")) { let npf_pmt = py.import("numpy_financial").unwrap().getattr("pmt").unwrap(); let npf_result = npf_pmt.call1((INTEREST_RATE, PERIODS, PV, FV)); assert_almost_eq!(pmt, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_pmt_zero_rate() { - let pmt = pyxirr::pmt(0.0, PERIODS, PV, Some(FV), None).unwrap(); - assert_future_value!(0.0, PERIODS, pmt, PV, Some(FV), None); + Python::with_gil(|py| { + let pmt: f64 = pyxirr_call!(py, "pmt", (0, PERIODS, PV, FV)); + assert_future_value!(0.0, PERIODS, pmt, PV, Some(FV), None); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_pmt = py.import("numpy_financial").unwrap().getattr("pmt").unwrap(); let npf_result = npf_pmt.call1((0, PERIODS, PV, FV)); assert_almost_eq!(pmt, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } // ------------ IPMT ---------------- #[rstest] fn test_ipmt_works() { - let result = pyxirr::ipmt(INTEREST_RATE, 2.0, PERIODS, PAYMENT, None, None).unwrap(); - assert_almost_eq!(result, 2301.238562586); + Python::with_gil(|py| { + let result: f64 = pyxirr_call!(py, "ipmt", (INTEREST_RATE, 2.0, PERIODS, PAYMENT)); + assert_almost_eq!(result, 2301.238562586); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_ipmt = py.import("numpy_financial").unwrap().getattr("ipmt").unwrap(); let npf_result = npf_ipmt.call1((INTEREST_RATE, 2, PERIODS, PAYMENT)); assert_almost_eq!(result, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_ipmt_pmt_at_begining() { - let result = pyxirr::ipmt(INTEREST_RATE, 2.0, PERIODS, PAYMENT, None, Some(true)).unwrap(); - assert_almost_eq!(result, 2191.6557738917); + Python::with_gil(|py| { + let result: f64 = pyxirr_call!(py, "ipmt", (INTEREST_RATE, 2.0, PERIODS, PAYMENT, 0, true)); + assert_almost_eq!(result, 2191.6557738917); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_ipmt = py.import("numpy_financial").unwrap().getattr("ipmt").unwrap(); let npf_result = npf_ipmt.call1((INTEREST_RATE, 2, PERIODS, PAYMENT, 0, "start")); assert_almost_eq!(result, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_ipmt_non_zero_fv() { - let result = pyxirr::ipmt(INTEREST_RATE, 2.0, PERIODS, PAYMENT, Some(FV), Some(true)).unwrap(); - assert_almost_eq!(result, 2608.108309425); + Python::with_gil(|py| { + let result: f64 = + pyxirr_call!(py, "ipmt", (INTEREST_RATE, 2.0, PERIODS, PAYMENT, FV, true)); + assert_almost_eq!(result, 2608.108309425); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_ipmt = py.import("numpy_financial").unwrap().getattr("ipmt").unwrap(); let npf_result = npf_ipmt.call1((INTEREST_RATE, 2, PERIODS, PAYMENT, FV, "start")); assert_almost_eq!(result, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_ipmt_first_period() { - let result = pyxirr::ipmt(INTEREST_RATE, 1.0, PERIODS, PAYMENT, None, None).unwrap(); - assert_almost_eq!(result, -PAYMENT * INTEREST_RATE); + Python::with_gil(|py| { + let result: f64 = pyxirr_call!(py, "ipmt", (INTEREST_RATE, 1.0, PERIODS, PAYMENT)); + assert_almost_eq!(result, -PAYMENT * INTEREST_RATE); + }) } #[rstest] fn test_ipmt_zero_period() { - assert!(pyxirr::ipmt(INTEREST_RATE, 0.0, PERIODS, PAYMENT, None, None).is_none()); + Python::with_gil(|py| { + let result: Option = pyxirr_call!(py, "ipmt", (INTEREST_RATE, 0.0, PERIODS, PAYMENT)); + assert!(result.is_none()); + }) } #[rstest] fn test_ipmt_per_greater_than_nper() { - let result = pyxirr::ipmt(INTEREST_RATE, PERIODS + 2.0, PERIODS, PAYMENT, None, None).unwrap(); - assert_almost_eq!(result, -323.7614374136); + Python::with_gil(|py| { + let result: f64 = + pyxirr_call!(py, "ipmt", (INTEREST_RATE, PERIODS + 2.0, PERIODS, PAYMENT)); + assert_almost_eq!(result, -323.7614374136); + }) } // ------------ PPMT ---------------- #[rstest] fn test_ppmt_works() { - let result = pyxirr::ppmt(INTEREST_RATE, 2.0, PERIODS, PAYMENT, None, None).unwrap(); - assert_almost_eq!(result, 4173.9901856864); + Python::with_gil(|py| { + let result: f64 = pyxirr_call!(py, "ppmt", (INTEREST_RATE, 2.0, PERIODS, PAYMENT)); + assert_almost_eq!(result, 4173.9901856864); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_ppmt = py.import("numpy_financial").unwrap().getattr("ppmt").unwrap(); let npf_result = npf_ppmt.call1((INTEREST_RATE, 2, PERIODS, PAYMENT)); assert_almost_eq!(result, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } // ------------ NPER ---------------- #[rstest] fn test_nper_pmt_at_end() { - let nper = pyxirr::nper(INTEREST_RATE, PAYMENT, PV, None, None).unwrap(); - assert_future_value!(INTEREST_RATE, nper, PAYMENT, PV, None, None); + Python::with_gil(|py| { + let nper: f64 = pyxirr_call!(py, "nper", (INTEREST_RATE, PAYMENT, PV)); + assert_future_value!(INTEREST_RATE, nper, PAYMENT, PV, None, None); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_nper = py.import("numpy_financial").unwrap().getattr("nper").unwrap(); let npf_result = npf_nper.call1((INTEREST_RATE, PAYMENT, PV)); assert_almost_eq!(nper, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_nper_pmt_at_begining() { - let nper = pyxirr::nper(INTEREST_RATE, PAYMENT, PV, None, Some(true)).unwrap(); - assert_future_value!(INTEREST_RATE, nper, PAYMENT, PV, None, Some(true)); + Python::with_gil(|py| { + let nper: f64 = pyxirr_call!(py, "nper", (INTEREST_RATE, PAYMENT, PV, 0, true)); + assert_future_value!(INTEREST_RATE, nper, PAYMENT, PV, None, Some(true)); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_nper = py.import("numpy_financial").unwrap().getattr("nper").unwrap(); let npf_result = npf_nper.call1((INTEREST_RATE, PAYMENT, PV, 0, "start")); assert_almost_eq!(nper, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_nper_non_zero_fv() { - let nper = pyxirr::nper(INTEREST_RATE, PAYMENT, PV, Some(FV), None).unwrap(); - assert_future_value!(INTEREST_RATE, nper, PAYMENT, PV, Some(FV), None); + Python::with_gil(|py| { + let nper: f64 = pyxirr_call!(py, "nper", (INTEREST_RATE, PAYMENT, PV, FV)); + assert_future_value!(INTEREST_RATE, nper, PAYMENT, PV, Some(FV), None); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_nper = py.import("numpy_financial").unwrap().getattr("nper").unwrap(); let npf_result = npf_nper.call1((INTEREST_RATE, PAYMENT, PV, FV)); assert_almost_eq!(nper, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_nper_zero_rate() { - let nper = pyxirr::nper(0.0, PAYMENT, PV, Some(FV), None).unwrap(); - assert_future_value!(0.0, nper, PAYMENT, PV, Some(FV), None); + Python::with_gil(|py| { + let nper: f64 = pyxirr_call!(py, "nper", (0.0, PAYMENT, PV, FV)); + assert_future_value!(0.0, nper, PAYMENT, PV, Some(FV), None); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_nper = py.import("numpy_financial").unwrap().getattr("nper").unwrap(); let npf_result = npf_nper.call1((0, PERIODS, PAYMENT, FV)); assert_almost_eq!(nper, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } // ------------ RATE ---------------- #[rstest] fn test_rate_works() { - let rate = pyxirr::rate(PERIODS, PAYMENT, PV, None, None, None).unwrap(); - assert_future_value!(rate, PERIODS, PAYMENT, PV, None, None); + Python::with_gil(|py| { + let rate: f64 = pyxirr_call!(py, "rate", (PERIODS, PAYMENT, PV)); + assert_future_value!(rate, PERIODS, PAYMENT, PV, None, None); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_rate = py.import("numpy_financial").unwrap().getattr("rate").unwrap(); let npf_result = npf_rate.call1((PERIODS, PAYMENT, PV, 0)); assert_almost_eq!(rate, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_rate_non_zero_fv() { - let rate = pyxirr::rate(PERIODS, PAYMENT, PV, Some(FV), None, None).unwrap(); - assert_future_value!(rate, PERIODS, PAYMENT, PV, Some(FV), None); + Python::with_gil(|py| { + let rate: f64 = pyxirr_call!(py, "rate", (PERIODS, PAYMENT, PV, FV)); + assert_future_value!(rate, PERIODS, PAYMENT, PV, Some(FV), None); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_rate = py.import("numpy_financial").unwrap().getattr("rate").unwrap(); let npf_result = npf_rate.call1((PERIODS, PAYMENT, PV, FV)); assert_almost_eq!(rate, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } #[rstest] fn test_rate_pmt_at_begining() { - let rate = pyxirr::rate(PERIODS, PAYMENT, PV, Some(FV), Some(true), None).unwrap(); - assert_future_value!(rate, PERIODS, PAYMENT, PV, Some(FV), Some(true)); + Python::with_gil(|py| { + let rate: f64 = pyxirr_call!(py, "rate", (PERIODS, PAYMENT, PV, FV, true)); + assert_future_value!(rate, PERIODS, PAYMENT, PV, Some(FV), Some(true)); - if cfg!(not(feature = "nonumpy")) { - Python::with_gil(|py| { + if cfg!(not(feature = "nonumpy")) { let npf_rate = py.import("numpy_financial").unwrap().getattr("rate").unwrap(); let npf_result = npf_rate.call1((PERIODS, PAYMENT, PV, FV, "start")); assert_almost_eq!(rate, npf_result.unwrap().extract::().unwrap()); - }) - } + } + }) } // ------------ NFV ---------------- @@ -406,7 +416,7 @@ fn test_nfv() { // example from https://www.youtube.com/watch?v=775ljhriB8U Python::with_gil(|py| { let amounts = PyList::new(py, &[1050.0, 1350.0, 1350.0, 1450.0]); - let result = pyxirr::nfv(0.03, 6.0, amounts).unwrap().unwrap(); + let result: f64 = pyxirr_call!(py, "nfv", (0.03, 6.0, amounts)); assert_almost_eq!(result, 5750.16, 0.01); }); } @@ -421,7 +431,7 @@ fn test_nfv() { fn test_irr_works(#[case] input: &[f64], #[case] expected: f64) { Python::with_gil(|py| { let values = PyList::new(py, input); - let result = pyxirr::irr(values, None).unwrap().unwrap(); + let result: f64 = pyxirr_call!(py, "irr", (values,)); assert_almost_eq!(result, expected); if cfg!(not(feature = "nonumpy")) { @@ -440,17 +450,18 @@ fn test_irr_works(#[case] input: &[f64], #[case] expected: f64) { fn test_irr_samples(#[case] input: &str) { Python::with_gil(|py| { let payments = PaymentsLoader::from_csv(py, input).to_columns(); - let result = pyxirr::irr(payments.1, None).unwrap().unwrap(); + let rate: f64 = pyxirr_call!(py, "irr", (payments.1,)); - assert_almost_eq!(result, irr_expected_result(input)); + assert_almost_eq!(rate, irr_expected_result(input)); // test net present value of all cash flows equal to zero - assert_almost_eq!(pyxirr::npv(result, payments.1, None).unwrap().unwrap(), 0.0); + let npv: f64 = pyxirr_call!(py, "npv", (rate, payments.1)); + assert_almost_eq!(npv, 0.0); // npf returns wrong results (npv is not equal to zero): // if cfg!(not(feature = "nonumpy")) { // let npf_irr = py.import("numpy_financial").unwrap().getattr("irr").unwrap(); // let npf_result = npf_irr.call1((payments.1,)); - // assert_almost_eq!(result, npf_result.unwrap().extract::().unwrap()); + // assert_almost_eq!(rate, npf_result.unwrap().extract::().unwrap()); // } }); } @@ -461,7 +472,7 @@ fn test_irr_samples(#[case] input: &str) { fn test_mirr_works() { Python::with_gil(|py| { let values = PyList::new(py, &[-1000, 100, 250, 500, 500]); - let result = pyxirr::mirr(values, 0.1, 0.1).unwrap().unwrap(); + let result: f64 = pyxirr_call!(py, "mirr", (values, 0.1, 0.1)); assert_almost_eq!(result, 0.10401626745); if cfg!(not(feature = "nonumpy")) { @@ -475,10 +486,20 @@ fn test_mirr_works() { #[rstest] fn test_mirr_same_sign() { Python::with_gil(|py| { - let values = PyList::new(py, &[100_000.0, 50_000.0, 25_000.0]); - assert!(pyxirr::mirr(values, 0.1, 0.1).unwrap().is_none()); + let kwargs = py_dict!(py, "silent" => true); + + let values = PyList::new(py, &[100_000, 50_000, 25_000]); + let err = pyxirr_call_impl!(py, "mirr", (values, 0.1, 0.1)).unwrap_err(); + assert!(err.is_instance::(py)); + + let result: Option = pyxirr_call!(py, "mirr", (values, 0.1, 0.1), kwargs); + assert!(result.is_none()); let values = PyList::new(py, &[-100_000.0, -50_000.0, -25_000.0]); - assert!(pyxirr::mirr(values, 0.1, 0.1).unwrap().is_none()); + let err = pyxirr_call_impl!(py, "mirr", (values, 0.1, 0.1)).unwrap_err(); + assert!(err.is_instance::(py)); + + let result: Option = pyxirr_call!(py, "mirr", (values, 0.1, 0.1), kwargs); + assert!(result.is_none()); }); } diff --git a/tests/test_scheduled.rs b/tests/test_scheduled.rs index a5acd8d..b7acc28 100644 --- a/tests/test_scheduled.rs +++ b/tests/test_scheduled.rs @@ -1,6 +1,6 @@ use rstest::rstest; -use pyo3::types::{IntoPyDict, PyDate, PyList}; +use pyo3::types::{PyDate, PyList}; use pyo3::Python; mod common; @@ -112,16 +112,15 @@ fn test_xirr_silent() { fn test_xfv() { // http://westclintech.com/SQL-Server-Financial-Functions/SQL-Server-XFV-function Python::with_gil(|py| { - let result = pyxirr::xfv( - PyDate::new(py, 2011, 2, 1).unwrap().into(), - PyDate::new(py, 2011, 3, 1).unwrap().into(), - PyDate::new(py, 2012, 2, 1).unwrap().into(), + let args = ( + PyDate::new(py, 2011, 2, 1).unwrap(), + PyDate::new(py, 2011, 3, 1).unwrap(), + PyDate::new(py, 2012, 2, 1).unwrap(), 0.00142, 0.00246, 100000., - ) - .unwrap() - .unwrap(); + ); + let result: f64 = pyxirr_call!(py, "xfv", args); assert_almost_eq!(result, 100235.088391894); }); } @@ -130,7 +129,7 @@ fn test_xfv() { fn test_xnfv() { Python::with_gil(|py| { let payments = PaymentsLoader::from_csv(py, "tests/samples/xnfv.csv").to_records(); - let result = pyxirr::xnfv(0.0250, payments, None).unwrap().unwrap(); + let result: f64 = pyxirr_call!(py, "xnfv", (0.0250, payments)); assert_almost_eq!(result, 57238.1249299303); }); } @@ -141,33 +140,20 @@ fn test_sum_xfv_eq_xnfv() { let rate = 0.0250; let (dates, amounts) = PaymentsLoader::from_csv(py, "tests/samples/xnfv.csv").to_columns(); - let xnfv_result = pyxirr::xnfv(rate, dates, Some(amounts)).unwrap().unwrap(); + let xnfv_result: f64 = pyxirr_call!(py, "xnfv", (rate, dates, amounts)); let builtins = py.import("builtins").unwrap(); - let locals = vec![ - ("dates", dates), - ("min", builtins.getattr("min").unwrap()), - ("max", builtins.getattr("max").unwrap()), - ] - .into_py_dict(py); - - let min_date = py.eval("min(dates)", Some(locals), None).unwrap(); - let max_date = py.eval("max(dates)", Some(locals), None).unwrap(); + let locals = py_dict!(py, "dates" => dates); + let min_date = py.eval("min(dates)", Some(locals), Some(builtins.dict())).unwrap(); + let max_date = py.eval("max(dates)", Some(locals), Some(builtins.dict())).unwrap(); + let sum_xfv_result: f64 = dates .iter() .unwrap() - .zip(amounts.iter().unwrap()) - .map(|(d, a)| { - pyxirr::xfv( - min_date.extract().unwrap(), - d.unwrap().extract().unwrap(), - max_date.extract().unwrap(), - rate, - rate, - a.unwrap().extract().unwrap(), - ) - .unwrap() - .unwrap() + .map(Result::unwrap) + .zip(amounts.iter().unwrap().map(Result::unwrap)) + .map(|(date, amount)| -> f64 { + pyxirr_call!(py, "xfv", (min_date, date, max_date, rate, rate, amount)) }) .sum();