From 84d79463d6d1e68ab0f3ad2ea858cbd59b35a889 Mon Sep 17 00:00:00 2001 From: Anexen Date: Sat, 30 Dec 2023 23:57:07 +0100 Subject: [PATCH 1/6] build DateLike from timestamp --- src/conversions.rs | 26 +++------------------ src/core/mod.rs | 2 +- src/core/models.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/src/conversions.rs b/src/conversions.rs index cc0eb48..08b5280 100644 --- a/src/conversions.rs +++ b/src/conversions.rs @@ -11,10 +11,7 @@ use pyo3::{ }; use time::Date; -use crate::core::{DateLike, DayCount}; - -// time::Date::from_ordinal_date(1970, 1).unwrap().to_julian_day(); -static UNIX_EPOCH_JULIAN_DAY: i32 = 2440588; +use crate::core::{DateLike, DayCount, DaysSinceUnixEpoch}; pub fn float_or_none(result: f64) -> Option { if result.is_nan() { @@ -69,23 +66,9 @@ impl DayCount { } } -struct DaysSinceUnixEpoch(i32); - impl<'s> FromPyObject<'s> for DaysSinceUnixEpoch { fn extract(obj: &'s PyAny) -> PyResult { - obj.extract::().map(|x| Self(x as i32)) - } -} - -impl From for DateLike { - fn from(value: DaysSinceUnixEpoch) -> Self { - Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + value.0).unwrap().into() - } -} - -impl From for DateLike { - fn from(value: i64) -> Self { - Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + (value as i32)).unwrap().into() + obj.extract::().map(|x| x.into()) } } @@ -103,10 +86,7 @@ impl From<&PyDate> for DateLike { impl From<&datetime64> for DateLike { fn from(value: &datetime64) -> Self { - let days_since_unix_epoch: i32 = Into::::into(*value) as i32; - let date = Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + days_since_unix_epoch).unwrap(); - - date.into() + DateLike::from(DaysSinceUnixEpoch::from(i64::from(*value) as i32)) } } diff --git a/src/core/mod.rs b/src/core/mod.rs index 095dc40..c0ba831 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -6,7 +6,7 @@ pub mod periodic; mod scheduled; mod utils; -pub use models::{DateLike, InvalidPaymentsError}; +pub use models::*; pub use periodic::*; pub use scheduled::*; pub mod private_equity; diff --git a/src/core/models.rs b/src/core/models.rs index ae88fc0..94ca4d6 100644 --- a/src/core/models.rs +++ b/src/core/models.rs @@ -2,9 +2,15 @@ use std::{error::Error, fmt, str::FromStr}; use time::{macros::format_description, Date}; +// time::Date::from_ordinal_date(1970, 1).unwrap().to_julian_day(); +static UNIX_EPOCH_JULIAN_DAY: i32 = 2440588; + #[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Copy)] pub struct DateLike(Date); +pub struct Timestamp(i64); +pub struct DaysSinceUnixEpoch(i32); + impl From for Date { fn from(val: DateLike) -> Self { val.0 @@ -23,6 +29,31 @@ impl AsRef for DateLike { } } +impl From for DaysSinceUnixEpoch { + fn from(value: i32) -> Self { + Self(value) + } +} + +impl From for Timestamp { + fn from(value: i64) -> Self { + Self(value) + } +} + +impl From for DateLike { + fn from(value: DaysSinceUnixEpoch) -> Self { + Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + value.0).unwrap().into() + } +} + +impl From for DateLike { + fn from(value: Timestamp) -> Self { + let days = value.0.div_euclid(86400) as i32; + Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + days).unwrap().into() + } +} + impl FromStr for DateLike { type Err = time::error::Parse; @@ -79,3 +110,30 @@ pub fn validate(payments: &[f64], dates: Option<&[DateLike]>) -> Result<(), Inva Err(InvalidPaymentsError::new("negative and positive payments are required")) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_date_like_from_integer() { + let dt = DateLike::from(Timestamp(1335020400)); + assert_eq!(dt.0.to_string(), "2012-04-21"); + + let dt = DateLike::from(DaysSinceUnixEpoch(15801)); + assert_eq!(dt.0.to_string(), "2013-04-06"); + } + + #[test] + fn test_date_like_from_integer_leap_year() { + let dt = DateLike::from(Timestamp(1456749295)); + assert_eq!(dt.0.to_string(), "2016-02-29"); + let dt = DateLike::from(Timestamp(1456835356)); + assert_eq!(dt.0.to_string(), "2016-03-01"); + + let dt = DateLike::from(DaysSinceUnixEpoch(15399)); + assert_eq!(dt.0.to_string(), "2012-02-29"); + let dt = DateLike::from(DaysSinceUnixEpoch(15400)); + assert_eq!(dt.0.to_string(), "2012-03-01"); + } +} From f354b8aa7175d3c9e6ba4d7a161adf068854aa8c Mon Sep 17 00:00:00 2001 From: Anexen Date: Mon, 1 Jan 2024 23:27:54 +0100 Subject: [PATCH 2/6] separate the core from the python wrapper --- Cargo.lock | 11 + Cargo.toml | 2 +- core/Cargo.toml | 14 + core/src/broadcasting.rs | 78 ++++++ core/src/lib.rs | 21 ++ {src/core => core/src}/models.rs | 49 ++-- {src/core => core/src}/optimize.rs | 0 core/src/periodic.rs | 238 +++++++++++++++++ {src/core => core/src}/private_equity.rs | 0 core/src/pyo3.rs | 93 +++++++ {src/core => core/src}/scheduled/day_count.rs | 7 +- {src/core => core/src}/scheduled/mod.rs | 0 {src/core => core/src}/scheduled/xirr.rs | 2 +- {src/core => core/src}/scheduled/xnfv.rs | 2 +- {src/core => core/src}/utils.rs | 0 .../periodic.rs => core/src/vectorized.rs | 242 +----------------- src/broadcasting.rs | 76 +----- src/conversions.rs | 77 +----- src/core/mod.rs | 12 - src/lib.rs | 27 +- 20 files changed, 491 insertions(+), 460 deletions(-) create mode 100644 core/Cargo.toml create mode 100644 core/src/broadcasting.rs create mode 100644 core/src/lib.rs rename {src/core => core/src}/models.rs (73%) rename {src/core => core/src}/optimize.rs (100%) create mode 100644 core/src/periodic.rs rename {src/core => core/src}/private_equity.rs (100%) create mode 100644 core/src/pyo3.rs rename {src/core => core/src}/scheduled/day_count.rs (98%) rename {src/core => core/src}/scheduled/mod.rs (100%) rename {src/core => core/src}/scheduled/xirr.rs (99%) rename {src/core => core/src}/scheduled/xnfv.rs (98%) rename {src/core => core/src}/utils.rs (100%) rename src/core/periodic.rs => core/src/vectorized.rs (52%) delete mode 100644 src/core/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 77b2ebf..600261a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,10 +372,21 @@ dependencies = [ "ndarray", "numpy", "pyo3", + "pyxirr-core", "rstest", "time", ] +[[package]] +name = "pyxirr-core" +version = "0.0.0" +dependencies = [ + "ndarray", + "numpy", + "pyo3", + "time", +] + [[package]] name = "quote" version = "1.0.33" diff --git a/Cargo.toml b/Cargo.toml index d9c9039..071dc5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ pyo3 = "0.20" numpy = "0.20" time = { version = "0.3", features = ["parsing", "macros"] } ndarray = "0.15" -# num-complex = "0.4" +pyxirr-core = { path = "./core", features = ["vectorization", "python"] } [dev-dependencies] assert_approx_eq = "1.1" diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..e945739 --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pyxirr-core" +version = "0.0.0" +edition = "2021" + +[dependencies] +time = { version = "0.3", features = ["parsing", "macros"] } +ndarray = { version = "0.15", optional = true } +pyo3 = { version = "0.20", optional = true } +numpy = { version = "0.20", optional = true } + +[features] +vectorization = ["ndarray"] +python = ["pyo3", "numpy"] diff --git a/core/src/broadcasting.rs b/core/src/broadcasting.rs new file mode 100644 index 0000000..99fa999 --- /dev/null +++ b/core/src/broadcasting.rs @@ -0,0 +1,78 @@ +use std::{error::Error, fmt}; + +/// An error returned when the payments do not contain both negative and positive payments. +#[derive(Debug)] +pub struct BroadcastingError(String); + +impl BroadcastingError { + pub fn new(shapes: &[&[usize]]) -> Self { + Self(format!("{:?}", shapes)) + } +} + +impl fmt::Display for BroadcastingError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Error for BroadcastingError {} + +pub fn broadcast_shapes(shapes: &[&[usize]]) -> Option> { + /* Discover the broadcast number of dimensions */ + let ndim = shapes.iter().map(|s| s.len()).max()?; + let mut result = vec![0; ndim]; + + /* Discover the broadcast shape in each dimension */ + for (i, cur) in result.iter_mut().enumerate() { + *cur = 1; + for s in shapes.iter() { + /* This prepends 1 to shapes not already equal to ndim */ + if i + s.len() >= ndim { + let k = i + s.len() - ndim; + let tmp = s[k]; + if tmp == 1 { + continue; + } + if cur == &1 { + *cur = tmp; + } else if cur != &tmp { + return None; + } + } + } + } + + Some(result) +} + +#[macro_export] +macro_rules! broadcast_together { + ($($a:expr),*) => { + { + let _a = &[$($a.shape(),)*]; + + match $crate::broadcasting::broadcast_shapes(_a) { + Some(shape) => Ok(( $($a.broadcast(shape.clone()).unwrap(),)*)), + None => Err(BroadcastingError::new(_a)) + } + } + }; +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_broadcast_shapes() { + assert_eq!(broadcast_shapes(&[&[3_usize, 2], &[2, 1]]), None); + assert_eq!(broadcast_shapes(&[&[1_usize, 2], &[3, 1], &[3, 2]]), Some(vec![3_usize, 2])); + assert_eq!( + broadcast_shapes(&[&[6_usize, 7], &[5, 6, 1], &[7], &[5, 1, 7]]), + Some(vec![5, 6, 7]) + ); + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..1ffaa8f --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,21 @@ +mod models; +mod optimize; +mod scheduled; +mod utils; + +pub mod broadcasting; +pub mod periodic; +pub mod private_equity; + +pub use broadcasting::BroadcastingError; +pub use models::*; +pub use periodic::*; +pub use scheduled::*; + +#[cfg(feature = "vectorization")] +mod vectorized; +#[cfg(feature = "vectorization")] +pub use vectorized::*; + +#[cfg(feature = "pyo3")] +pub mod pyo3; diff --git a/src/core/models.rs b/core/src/models.rs similarity index 73% rename from src/core/models.rs rename to core/src/models.rs index 94ca4d6..60c5492 100644 --- a/src/core/models.rs +++ b/core/src/models.rs @@ -8,8 +8,16 @@ static UNIX_EPOCH_JULIAN_DAY: i32 = 2440588; #[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Copy)] pub struct DateLike(Date); -pub struct Timestamp(i64); -pub struct DaysSinceUnixEpoch(i32); +impl DateLike { + pub fn from_days_since_unix_epoch(days: i32) -> Self { + Self(Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + days).unwrap()) + } + + pub fn from_unix_timestamp(ts: i64) -> Self { + let days = ts.div_euclid(86400) as i32; + Self::from_days_since_unix_epoch(days) + } +} impl From for Date { fn from(val: DateLike) -> Self { @@ -29,31 +37,6 @@ impl AsRef for DateLike { } } -impl From for DaysSinceUnixEpoch { - fn from(value: i32) -> Self { - Self(value) - } -} - -impl From for Timestamp { - fn from(value: i64) -> Self { - Self(value) - } -} - -impl From for DateLike { - fn from(value: DaysSinceUnixEpoch) -> Self { - Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + value.0).unwrap().into() - } -} - -impl From for DateLike { - fn from(value: Timestamp) -> Self { - let days = value.0.div_euclid(86400) as i32; - Date::from_julian_day(UNIX_EPOCH_JULIAN_DAY + days).unwrap().into() - } -} - impl FromStr for DateLike { type Err = time::error::Parse; @@ -117,23 +100,23 @@ mod tests { #[test] fn test_date_like_from_integer() { - let dt = DateLike::from(Timestamp(1335020400)); + let dt = DateLike::from_unix_timestamp(1335020400); assert_eq!(dt.0.to_string(), "2012-04-21"); - let dt = DateLike::from(DaysSinceUnixEpoch(15801)); + let dt = DateLike::from_days_since_unix_epoch(15801); assert_eq!(dt.0.to_string(), "2013-04-06"); } #[test] fn test_date_like_from_integer_leap_year() { - let dt = DateLike::from(Timestamp(1456749295)); + let dt = DateLike::from_unix_timestamp(1456749295); assert_eq!(dt.0.to_string(), "2016-02-29"); - let dt = DateLike::from(Timestamp(1456835356)); + let dt = DateLike::from_unix_timestamp(1456835356); assert_eq!(dt.0.to_string(), "2016-03-01"); - let dt = DateLike::from(DaysSinceUnixEpoch(15399)); + let dt = DateLike::from_days_since_unix_epoch(15399); assert_eq!(dt.0.to_string(), "2012-02-29"); - let dt = DateLike::from(DaysSinceUnixEpoch(15400)); + let dt = DateLike::from_days_since_unix_epoch(15400); assert_eq!(dt.0.to_string(), "2012-03-01"); } } diff --git a/src/core/optimize.rs b/core/src/optimize.rs similarity index 100% rename from src/core/optimize.rs rename to core/src/optimize.rs diff --git a/core/src/periodic.rs b/core/src/periodic.rs new file mode 100644 index 0000000..52b5b60 --- /dev/null +++ b/core/src/periodic.rs @@ -0,0 +1,238 @@ +use std::iter::successors; + +use super::{ + models::{validate, InvalidPaymentsError}, + optimize::{brentq, brentq_grid_search, newton_raphson, newton_raphson_with_default_deriv}, + utils, +}; + +// pre calculating powers for performance +pub(crate) fn powers(base: f64, n: usize, start_from_zero: bool) -> Vec { + let (start, n) = if start_from_zero { + (1.0, n + 1) + } else { + (base, n) + }; + successors(Some(start), |x| Some(x * base)).take(n).collect() +} + +pub(crate) fn convert_pmt_at_beginning(pmt_at_beginning: bool) -> f64 { + if pmt_at_beginning { + 1. + } else { + 0. + } +} + +pub fn fv(rate: f64, nper: f64, pmt: f64, pv: f64, pmt_at_beginning: bool) -> f64 { + if rate == 0.0 { + return -(pv + pmt * nper); + } + + let pmt_at_beginning = convert_pmt_at_beginning(pmt_at_beginning); + let factor = f64::powf(1.0 + rate, nper); + + -pv * factor - pmt * (1.0 + rate * pmt_at_beginning) / rate * (factor - 1.0) +} + +pub fn pv(rate: f64, nper: f64, pmt: f64, fv: f64, pmt_at_beginning: bool) -> f64 { + if rate == 0.0 { + return -(fv + pmt * nper); + } + + let pmt_at_beginning = convert_pmt_at_beginning(pmt_at_beginning); + let exp = f64::powf(1. + rate, nper); + let factor = (1. + rate * pmt_at_beginning) * (exp - 1.) / rate; + -(fv + pmt * factor) / exp +} + +pub fn pmt(rate: f64, nper: f64, pv: f64, fv: f64, pmt_at_beginning: bool) -> f64 { + if rate == 0.0 { + return -(fv + pv) / nper; + } + + let pmt_at_beginning = convert_pmt_at_beginning(pmt_at_beginning); + + let exp = f64::powf(1.0 + rate, nper); + let factor = (1. + rate * pmt_at_beginning) * (exp - 1.) / rate; + + -(fv + pv * exp) / factor +} + +pub fn ipmt(rate: f64, per: f64, nper: f64, pv: f64, fv: f64, pmt_at_beginning: bool) -> f64 { + // let total_pmt = self::pmt(rate, nper, pv, fv, pmt_at_beginning); + // let result = rate * self::fv(rate, per - 1.0, total_pmt, pv, pmt_at_beginning); + // + // simplify r*(-P*(1+r)**(p-1)-(-(F+P*(1+r)**n)*r/((1+r*t)*((1+r)**n-1)))*(1+r*t)/r*((1+r)**(p-1)-1)) + + // payments before first period don't make any sense. + if per < 1.0 || per > nper { + return f64::NAN; + } + + // no interest if payment occurs at the beginning + // of a period and this is the first period + if per == 1.0 && pmt_at_beginning { + return 0.0; + } + + // no interest if rate == 0 + if rate == 0.0 { + return 0.0; + } + + let f1 = (rate + 1.0).powf(per); + let f2 = (rate + 1.0).powf(nper); + + let result = (rate * (pv + fv) * f1 - rate * (rate + 1.0) * (fv + pv * f2)) + / ((rate + 1.0) * (f2 - 1.0)); + + if pmt_at_beginning { + // if paying at the beginning we need to discount by one period. + result / (1.0 + rate) + } else { + result + } +} + +pub fn ppmt(rate: f64, per: f64, nper: f64, pv: f64, fv: f64, pmt_at_beginning: bool) -> f64 { + // assuming type = 1 if pmt_at_beginning else 0 + // assuming P=pv;F=fv;r=rate;n=nper;p=per;t=type, type in {1;0} + // ppmt = fv(r,p-1,pmt(r,n,P,F,t),P,t) - fv(r,p,pmt(r,n,P,F,t),P,t) + // after substitution: + // simplify (-P*(1+r)^(p-1)-(-(F+P*(1+r)^n)*r/((1+r)^n-1)/(1+r*t))*(1+r*t)/r*((1+r)^(p-1)-1)) - (-P*(1+r)^p-(-(F+P*(1+r)^n)*r/((1+r)^n-1)/(1+r*t))*(1+r*t)/r*((1+r)^p-1)) + // shorter formula: -r*(F+P)*(r+1)^(per-1)/((r+1)^n - 1) + // type correction: result /= r + 1 if type = 1 + // denominator => 1/((r+1)^p-1) => 1/(((r+1)^p-1)*(r+1)) => + // => 1/((r+1)^(p+1) - (r+1)) => 1/((r+1)^(p+t) -r*t + 1) + // + // if rate == 0: + // simplify (-P-(-(F+P)/n) *(p-1) - (-P-(-(F+P)/n)*p)) + // shorter: -(F + P) / n; + + if per < 1.0 || per > nper { + return f64::NAN; + } + + if rate == 0.0 { + return -(fv + pv) / nper; + } + + let when = convert_pmt_at_beginning(pmt_at_beginning); + -rate * (fv + pv) * (rate + 1.).powf(per - 1.) + / ((rate + 1.).powf(nper + when) - rate * when - 1.) +} + +pub fn nper(rate: f64, pmt: f64, pv: f64, fv: f64, pmt_at_beginning: bool) -> f64 { + if rate == 0.0 { + return -(fv + pv) / pmt; + } + + let pmt_at_beginning = convert_pmt_at_beginning(pmt_at_beginning); + + let z = pmt * (1. + rate * pmt_at_beginning) / rate; + f64::log10((-fv + z) / (pv + z)) / f64::log10(1. + rate) +} + +pub fn rate( + nper: f64, + pmt: f64, + pv: f64, + fv: f64, + pmt_at_beginning: bool, + guess: Option, +) -> f64 { + newton_raphson_with_default_deriv(guess.unwrap_or(0.1), |rate| { + fv - self::fv(rate, nper, pmt, pv, pmt_at_beginning) + }) +} + +// http://westclintech.com/SQL-Server-Financial-Functions/SQL-Server-NFV-function +pub fn nfv(rate: f64, nper: f64, amounts: &[f64]) -> f64 { + let pv = self::npv(rate, amounts, Some(false)); + self::fv(rate, nper, 0.0, -pv, false) +} + +pub fn npv(rate: f64, values: &[f64], start_from_zero: Option) -> f64 { + if rate == 0.0 { + return values.iter().sum(); + } + + powers(1. + rate, values.len(), start_from_zero.unwrap_or(true)) + .iter() + .zip(values.iter()) + .map(|(p, v)| v / p) + .sum() +} + +fn npv_deriv(rate: f64, values: &[f64]) -> f64 { + values.iter().enumerate().map(|(i, v)| -(i as f64) * v / (rate + 1.0).powf(i as f64 + 1.)).sum() +} + +pub fn irr(values: &[f64], guess: Option) -> Result { + // must contain at least one positive and one negative value + validate(values, None)?; + + let f = |rate| { + if rate <= -1.0 { + // bound newton_raphson + return f64::INFINITY; + } + self::npv(rate, values, Some(true)) + }; + let df = |rate| self::npv_deriv(rate, values); + let is_good_rate = |rate: f64| rate.is_finite() && f(rate).abs() < 1e-3; + + let guess = match guess { + Some(g) => g, + None => { + let (outflows, inflows) = utils::sum_negatives_positives(values); + inflows / -outflows - 1.0 + } + }; + + let rate = newton_raphson(guess, &f, &df); + + if is_good_rate(rate) { + return Ok(rate); + } + + let rate = brentq(&f, -0.999999999999999, 100., 100); + + if is_good_rate(rate) { + return Ok(rate); + } + + // strategy: closest to zero + // let breakpoints: &[f64] = &[0.0, 0.25, -0.25, 0.5, -0.5, 1.0, -0.9, -0.99999999999999, 1e9]; + // strategy: pessimistic + let breakpoints: &[f64] = &[-0.99999999999999, -0.75, -0.5, -0.25, 0., 0.25, 0.5, 1.0, 1e6]; + let rate = brentq_grid_search(&[breakpoints], &f).next(); + + Ok(rate.unwrap_or(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() + .zip(values.iter().rev()) + .filter(|(_r, &v)| v > 0.0) + .map(|(r, v)| v * r) + .sum(); + + let negative: f64 = powers(1. + finance_rate, values.len(), true) + .iter() + .zip(values.iter()) + .filter(|(_r, &v)| v < 0.0) + .map(|(&r, &v)| v / r) + .sum(); + + Ok((positive / -negative).powf(1.0 / (values.len() - 1) as f64) - 1.0) +} diff --git a/src/core/private_equity.rs b/core/src/private_equity.rs similarity index 100% rename from src/core/private_equity.rs rename to core/src/private_equity.rs diff --git a/core/src/pyo3.rs b/core/src/pyo3.rs new file mode 100644 index 0000000..f06288b --- /dev/null +++ b/core/src/pyo3.rs @@ -0,0 +1,93 @@ +use std::str::FromStr; + +use crate::{DateLike, DayCount}; +use pyo3::{ + create_exception, + exceptions::{PyException, PyTypeError, PyValueError}, + prelude::*, + types::*, +}; + +use numpy::datetime::{units, Datetime as datetime64}; + +use time::Date; + +create_exception!(pyxirr, InvalidPaymentsError, PyException); +create_exception!(pyxirr, BroadcastingError, PyException); + +impl From for PyErr { + fn from(value: crate::models::InvalidPaymentsError) -> Self { + InvalidPaymentsError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: crate::broadcasting::BroadcastingError) -> Self { + BroadcastingError::new_err(value.to_string()) + } +} + +#[pymethods] +impl DayCount { + #[staticmethod] + pub fn of(value: &str) -> PyResult { + DayCount::from_str(value).map_err(PyValueError::new_err) + } + + pub fn __str__(&self) -> String { + self.to_string() + } +} + +impl From<&PyDate> for DateLike { + fn from(value: &PyDate) -> Self { + let date = Date::from_calendar_date( + value.get_year(), + value.get_month().try_into().unwrap(), + value.get_day(), + ) + .unwrap(); + date.into() + } +} + +impl From<&datetime64> for DateLike { + fn from(value: &datetime64) -> Self { + DateLike::from_days_since_unix_epoch(i64::from(*value) as i32) + } +} + +impl<'s> FromPyObject<'s> for DateLike { + fn extract(obj: &'s PyAny) -> PyResult { + if let Ok(py_date) = obj.downcast::() { + return Ok(py_date.into()); + } + + if let Ok(py_string) = obj.downcast::() { + return py_string + .to_str()? + .parse::() + .map_err(|e| PyValueError::new_err(e.to_string())); + } + + match obj.get_type().name()? { + "datetime64" => { + let days = obj + .call_method1("astype", ("datetime64[D]",))? + .call_method1("astype", ("int",))? + .extract::()?; + Ok(DateLike::from_days_since_unix_epoch(days)) + } + + "Timestamp" => { + let date = obj.call_method0("to_pydatetime")?.downcast::()?; + Ok(date.into()) + } + + other => Err(PyTypeError::new_err(format!( + "Type {:?} is not understood. Expected: date", + other + ))), + } + } +} diff --git a/src/core/scheduled/day_count.rs b/core/src/scheduled/day_count.rs similarity index 98% rename from src/core/scheduled/day_count.rs rename to core/src/scheduled/day_count.rs index f2b09cb..6c96f86 100644 --- a/src/core/scheduled/day_count.rs +++ b/core/src/scheduled/day_count.rs @@ -5,11 +5,10 @@ use time::{ Date, Month, }; -#[pyo3::pyclass] -#[pyo3(frozen)] -#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "python", pyo3::pyclass)] +#[cfg_attr(feature = "python", pyo3(frozen))] +#[derive(Debug, Clone, Copy, Default)] #[allow(non_camel_case_types)] -#[derive(Default)] pub enum DayCount { ACT_ACT_ISDA, #[default] diff --git a/src/core/scheduled/mod.rs b/core/src/scheduled/mod.rs similarity index 100% rename from src/core/scheduled/mod.rs rename to core/src/scheduled/mod.rs diff --git a/src/core/scheduled/xirr.rs b/core/src/scheduled/xirr.rs similarity index 99% rename from src/core/scheduled/xirr.rs rename to core/src/scheduled/xirr.rs index 41a6d6e..6526dbc 100644 --- a/src/core/scheduled/xirr.rs +++ b/core/src/scheduled/xirr.rs @@ -1,5 +1,5 @@ use super::{year_fraction, DayCount}; -use crate::core::{ +use crate::{ models::{validate, DateLike, InvalidPaymentsError}, optimize::{brentq, newton_raphson}, }; diff --git a/src/core/scheduled/xnfv.rs b/core/src/scheduled/xnfv.rs similarity index 98% rename from src/core/scheduled/xnfv.rs rename to core/src/scheduled/xnfv.rs index 1db99a1..3c9a531 100644 --- a/src/core/scheduled/xnfv.rs +++ b/core/src/scheduled/xnfv.rs @@ -1,5 +1,5 @@ use super::{year_fraction, DayCount}; -use crate::core::{ +use crate::{ models::{validate, DateLike, InvalidPaymentsError}, periodic::fv, }; diff --git a/src/core/utils.rs b/core/src/utils.rs similarity index 100% rename from src/core/utils.rs rename to core/src/utils.rs diff --git a/src/core/periodic.rs b/core/src/vectorized.rs similarity index 52% rename from src/core/periodic.rs rename to core/src/vectorized.rs index acd23b8..59ccdf3 100644 --- a/src/core/periodic.rs +++ b/core/src/vectorized.rs @@ -1,42 +1,8 @@ -use std::{iter::successors, mem::MaybeUninit}; - -use ndarray::{ArrayD, ArrayViewD}; - -use super::{ - models::{validate, InvalidPaymentsError}, - optimize::{brentq, brentq_grid_search, newton_raphson, newton_raphson_with_default_deriv}, - utils, -}; +use super::periodic::convert_pmt_at_beginning; use crate::{broadcast_together, broadcasting::BroadcastingError}; +use std::mem::MaybeUninit; -// pre calculating powers for performance -pub fn powers(base: f64, n: usize, start_from_zero: bool) -> Vec { - let (start, n) = if start_from_zero { - (1.0, n + 1) - } else { - (base, n) - }; - successors(Some(start), |x| Some(x * base)).take(n).collect() -} - -fn convert_pmt_at_beginning(pmt_at_beginning: bool) -> f64 { - if pmt_at_beginning { - 1. - } else { - 0. - } -} - -pub fn fv(rate: f64, nper: f64, pmt: f64, pv: f64, pmt_at_beginning: bool) -> f64 { - if rate == 0.0 { - return -(pv + pmt * nper); - } - - let pmt_at_beginning = convert_pmt_at_beginning(pmt_at_beginning); - let factor = f64::powf(1.0 + rate, nper); - - -pv * factor - pmt * (1.0 + rate * pmt_at_beginning) / rate * (factor - 1.0) -} +use ndarray::{ArrayD, ArrayViewD}; pub fn fv_vec( rate: &ArrayViewD, @@ -70,17 +36,6 @@ pub fn fv_vec( Ok(unsafe { result.assume_init() }) } -pub fn pv(rate: f64, nper: f64, pmt: f64, fv: f64, pmt_at_beginning: bool) -> f64 { - if rate == 0.0 { - return -(fv + pmt * nper); - } - - let pmt_at_beginning = convert_pmt_at_beginning(pmt_at_beginning); - let exp = f64::powf(1. + rate, nper); - let factor = (1. + rate * pmt_at_beginning) * (exp - 1.) / rate; - -(fv + pmt * factor) / exp -} - pub fn pv_vec( rate: &ArrayViewD, nper: &ArrayViewD, @@ -114,19 +69,6 @@ pub fn pv_vec( Ok(unsafe { result.assume_init() }) } -pub fn pmt(rate: f64, nper: f64, pv: f64, fv: f64, pmt_at_beginning: bool) -> f64 { - if rate == 0.0 { - return -(fv + pv) / nper; - } - - let pmt_at_beginning = convert_pmt_at_beginning(pmt_at_beginning); - - let exp = f64::powf(1.0 + rate, nper); - let factor = (1. + rate * pmt_at_beginning) * (exp - 1.) / rate; - - -(fv + pv * exp) / factor -} - pub fn pmt_vec( rate: &ArrayViewD, nper: &ArrayViewD, @@ -160,42 +102,6 @@ pub fn pmt_vec( Ok(unsafe { result.assume_init() }) } -pub fn ipmt(rate: f64, per: f64, nper: f64, pv: f64, fv: f64, pmt_at_beginning: bool) -> f64 { - // let total_pmt = self::pmt(rate, nper, pv, fv, pmt_at_beginning); - // let result = rate * self::fv(rate, per - 1.0, total_pmt, pv, pmt_at_beginning); - // - // simplify r*(-P*(1+r)**(p-1)-(-(F+P*(1+r)**n)*r/((1+r*t)*((1+r)**n-1)))*(1+r*t)/r*((1+r)**(p-1)-1)) - - // payments before first period don't make any sense. - if per < 1.0 || per > nper { - return f64::NAN; - } - - // no interest if payment occurs at the beginning - // of a period and this is the first period - if per == 1.0 && pmt_at_beginning { - return 0.0; - } - - // no interest if rate == 0 - if rate == 0.0 { - return 0.0; - } - - let f1 = (rate + 1.0).powf(per); - let f2 = (rate + 1.0).powf(nper); - - let result = (rate * (pv + fv) * f1 - rate * (rate + 1.0) * (fv + pv * f2)) - / ((rate + 1.0) * (f2 - 1.0)); - - if pmt_at_beginning { - // if paying at the beginning we need to discount by one period. - result / (1.0 + rate) - } else { - result - } -} - pub fn ipmt_vec( rate: &ArrayViewD, per: &ArrayViewD, @@ -229,34 +135,6 @@ pub fn ipmt_vec( Ok(result) } -pub fn ppmt(rate: f64, per: f64, nper: f64, pv: f64, fv: f64, pmt_at_beginning: bool) -> f64 { - // assuming type = 1 if pmt_at_beginning else 0 - // assuming P=pv;F=fv;r=rate;n=nper;p=per;t=type, type in {1;0} - // ppmt = fv(r,p-1,pmt(r,n,P,F,t),P,t) - fv(r,p,pmt(r,n,P,F,t),P,t) - // after substitution: - // simplify (-P*(1+r)^(p-1)-(-(F+P*(1+r)^n)*r/((1+r)^n-1)/(1+r*t))*(1+r*t)/r*((1+r)^(p-1)-1)) - (-P*(1+r)^p-(-(F+P*(1+r)^n)*r/((1+r)^n-1)/(1+r*t))*(1+r*t)/r*((1+r)^p-1)) - // shorter formula: -r*(F+P)*(r+1)^(per-1)/((r+1)^n - 1) - // type correction: result /= r + 1 if type = 1 - // denominator => 1/((r+1)^p-1) => 1/(((r+1)^p-1)*(r+1)) => - // => 1/((r+1)^(p+1) - (r+1)) => 1/((r+1)^(p+t) -r*t + 1) - // - // if rate == 0: - // simplify (-P-(-(F+P)/n) *(p-1) - (-P-(-(F+P)/n)*p)) - // shorter: -(F + P) / n; - - if per < 1.0 || per > nper { - return f64::NAN; - } - - if rate == 0.0 { - return -(fv + pv) / nper; - } - - let when = convert_pmt_at_beginning(pmt_at_beginning); - -rate * (fv + pv) * (rate + 1.).powf(per - 1.) - / ((rate + 1.).powf(nper + when) - rate * when - 1.) -} - pub fn ppmt_vec( rate: &ArrayViewD, per: &ArrayViewD, @@ -291,17 +169,6 @@ pub fn ppmt_vec( Ok(result) } -pub fn nper(rate: f64, pmt: f64, pv: f64, fv: f64, pmt_at_beginning: bool) -> f64 { - if rate == 0.0 { - return -(fv + pv) / pmt; - } - - let pmt_at_beginning = convert_pmt_at_beginning(pmt_at_beginning); - - let z = pmt * (1. + rate * pmt_at_beginning) / rate; - f64::log10((-fv + z) / (pv + z)) / f64::log10(1. + rate) -} - pub fn nper_vec( rate: &ArrayViewD, pmt: &ArrayViewD, @@ -334,19 +201,6 @@ pub fn nper_vec( Ok(unsafe { result.assume_init() }) } -pub fn rate( - nper: f64, - pmt: f64, - pv: f64, - fv: f64, - pmt_at_beginning: bool, - guess: Option, -) -> f64 { - newton_raphson_with_default_deriv(guess.unwrap_or(0.1), |rate| { - fv - self::fv(rate, nper, pmt, pv, pmt_at_beginning) - }) -} - pub fn rate_vec( nper: &ArrayViewD, pmt: &ArrayViewD, @@ -407,93 +261,3 @@ fn _g_div_gp( g / gp } - -// http://westclintech.com/SQL-Server-Financial-Functions/SQL-Server-NFV-function -pub fn nfv(rate: f64, nper: f64, amounts: &[f64]) -> f64 { - let pv = self::npv(rate, amounts, Some(false)); - self::fv(rate, nper, 0.0, -pv, false) -} - -pub fn npv(rate: f64, values: &[f64], start_from_zero: Option) -> f64 { - if rate == 0.0 { - return values.iter().sum(); - } - - powers(1. + rate, values.len(), start_from_zero.unwrap_or(true)) - .iter() - .zip(values.iter()) - .map(|(p, v)| v / p) - .sum() -} - -fn npv_deriv(rate: f64, values: &[f64]) -> f64 { - values.iter().enumerate().map(|(i, v)| -(i as f64) * v / (rate + 1.0).powf(i as f64 + 1.)).sum() -} - -pub fn irr(values: &[f64], guess: Option) -> Result { - // must contain at least one positive and one negative value - validate(values, None)?; - - let f = |rate| { - if rate <= -1.0 { - // bound newton_raphson - return f64::INFINITY; - } - self::npv(rate, values, Some(true)) - }; - let df = |rate| self::npv_deriv(rate, values); - let is_good_rate = |rate: f64| rate.is_finite() && f(rate).abs() < 1e-3; - - let guess = match guess { - Some(g) => g, - None => { - let (outflows, inflows) = utils::sum_negatives_positives(values); - inflows / -outflows - 1.0 - } - }; - - let rate = newton_raphson(guess, &f, &df); - - if is_good_rate(rate) { - return Ok(rate); - } - - let rate = brentq(&f, -0.999999999999999, 100., 100); - - if is_good_rate(rate) { - return Ok(rate); - } - - // strategy: closest to zero - // let breakpoints: &[f64] = &[0.0, 0.25, -0.25, 0.5, -0.5, 1.0, -0.9, -0.99999999999999, 1e9]; - // strategy: pessimistic - let breakpoints: &[f64] = &[-0.99999999999999, -0.75, -0.5, -0.25, 0., 0.25, 0.5, 1.0, 1e6]; - let rate = brentq_grid_search(&[breakpoints], &f).next(); - - Ok(rate.unwrap_or(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() - .zip(values.iter().rev()) - .filter(|(_r, &v)| v > 0.0) - .map(|(r, v)| v * r) - .sum(); - - let negative: f64 = powers(1. + finance_rate, values.len(), true) - .iter() - .zip(values.iter()) - .filter(|(_r, &v)| v < 0.0) - .map(|(&r, &v)| v / r) - .sum(); - - Ok((positive / -negative).powf(1.0 / (values.len() - 1) as f64) - 1.0) -} diff --git a/src/broadcasting.rs b/src/broadcasting.rs index 9d22274..5215cf7 100644 --- a/src/broadcasting.rs +++ b/src/broadcasting.rs @@ -1,5 +1,3 @@ -use std::{error::Error, fmt}; - use ndarray::{ArrayD, ArrayViewD, Axis, CowArray, IxDyn}; use numpy::{npyffi, Element, PyArrayDyn, PY_ARRAY_API}; use pyo3::{ @@ -10,66 +8,6 @@ use pyo3::{ use crate::conversions::float_or_none; -/// An error returned when the payments do not contain both negative and positive payments. -#[derive(Debug)] -pub struct BroadcastingError(String); - -impl BroadcastingError { - pub fn new(shapes: &[&[usize]]) -> Self { - Self(format!("{:?}", shapes)) - } -} - -impl fmt::Display for BroadcastingError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl Error for BroadcastingError {} - -pub fn broadcast_shapes(shapes: &[&[usize]]) -> Option> { - /* Discover the broadcast number of dimensions */ - let ndim = shapes.iter().map(|s| s.len()).max()?; - let mut result = vec![0; ndim]; - - /* Discover the broadcast shape in each dimension */ - for (i, cur) in result.iter_mut().enumerate() { - *cur = 1; - for s in shapes.iter() { - /* This prepends 1 to shapes not already equal to ndim */ - if i + s.len() >= ndim { - let k = i + s.len() - ndim; - let tmp = s[k]; - if tmp == 1 { - continue; - } - if cur == &1 { - *cur = tmp; - } else if cur != &tmp { - return None; - } - } - } - } - - Some(result) -} - -#[macro_export] -macro_rules! broadcast_together { - ($($a:expr),*) => { - { - let _a = &[$($a.shape(),)*]; - - match $crate::broadcasting::broadcast_shapes(_a) { - Some(shape) => Ok(( $($a.broadcast(shape.clone()).unwrap(),)*)), - None => Err(BroadcastingError::new(_a)) - } - } - }; -} - pub fn pyiter_to_arrayd<'p, T>(pyiter: &'p PyIterator) -> PyResult> where T: FromPyObject<'p>, @@ -264,21 +202,9 @@ impl<'p, T> From<&'p PyArrayDyn> for Arg<'p, T> { #[cfg(test)] mod tests { - use rstest::rstest; - use super::*; - #[rstest] - fn test_broadcast_shapes() { - assert_eq!(broadcast_shapes(&[&[3_usize, 2], &[2, 1]]), None); - assert_eq!(broadcast_shapes(&[&[1_usize, 2], &[3, 1], &[3, 2]]), Some(vec![3_usize, 2])); - assert_eq!( - broadcast_shapes(&[&[6_usize, 7], &[5, 6, 1], &[7], &[5, 1, 7]]), - Some(vec![5, 6, 7]) - ); - } - - #[rstest] + #[test] fn test_flatten_pyiter() { Python::with_gil(|py| { let ob = py.eval("(range(i, i + 3) for i in range(3))", None, None).unwrap(); diff --git a/src/conversions.rs b/src/conversions.rs index 08b5280..34add43 100644 --- a/src/conversions.rs +++ b/src/conversions.rs @@ -1,17 +1,10 @@ -use std::str::FromStr; - use numpy::{ datetime::{units, Datetime as datetime64}, PyArray1, }; -use pyo3::{ - exceptions::{PyTypeError, PyValueError}, - prelude::*, - types::*, -}; -use time::Date; +use pyo3::{prelude::*, types::*}; -use crate::core::{DateLike, DayCount, DaysSinceUnixEpoch}; +use crate::core::{DateLike, DayCount}; pub fn float_or_none(result: f64) -> Option { if result.is_nan() { @@ -54,72 +47,6 @@ impl TryInto for PyDayCount { } } -#[pymethods] -impl DayCount { - #[staticmethod] - fn of(value: &str) -> PyResult { - DayCount::from_str(value).map_err(PyValueError::new_err) - } - - fn __str__(&self) -> String { - self.to_string() - } -} - -impl<'s> FromPyObject<'s> for DaysSinceUnixEpoch { - fn extract(obj: &'s PyAny) -> PyResult { - obj.extract::().map(|x| x.into()) - } -} - -impl From<&PyDate> for DateLike { - fn from(value: &PyDate) -> Self { - let date = Date::from_calendar_date( - value.get_year(), - value.get_month().try_into().unwrap(), - value.get_day(), - ) - .unwrap(); - date.into() - } -} - -impl From<&datetime64> for DateLike { - fn from(value: &datetime64) -> Self { - DateLike::from(DaysSinceUnixEpoch::from(i64::from(*value) as i32)) - } -} - -impl<'s> FromPyObject<'s> for DateLike { - fn extract(obj: &'s PyAny) -> PyResult { - if let Ok(py_date) = obj.downcast::() { - return Ok(py_date.into()); - } - - if let Ok(py_string) = obj.downcast::() { - return py_string - .to_str()? - .parse::() - .map_err(|e| PyValueError::new_err(e.to_string())); - } - - match obj.get_type().name()? { - "datetime64" => Ok(obj - .call_method1("astype", ("datetime64[D]",))? - .call_method1("astype", ("int",))? - .extract::()? - .into()), - - "Timestamp" => Ok(obj.call_method0("to_pydatetime")?.downcast::()?.into()), - - other => Err(PyTypeError::new_err(format!( - "Type {:?} is not understood. Expected: date", - other - ))), - } - } -} - fn extract_iterable<'a, T>(values: &'a PyAny) -> PyResult> where T: FromPyObject<'a>, diff --git a/src/core/mod.rs b/src/core/mod.rs deleted file mode 100644 index c0ba831..0000000 --- a/src/core/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -// TODO: move core module into a separate crate - -mod models; -mod optimize; -pub mod periodic; -mod scheduled; -mod utils; - -pub use models::*; -pub use periodic::*; -pub use scheduled::*; -pub mod private_equity; diff --git a/src/lib.rs b/src/lib.rs index 30a932e..7e63c51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,25 +1,14 @@ -use broadcasting::Arg; -use conversions::{fallible_float_or_none, float_or_none, AmountArray, PyDayCount}; -use pyo3::{create_exception, exceptions, prelude::*, wrap_pyfunction}; +use pyo3::{prelude::*, wrap_pyfunction}; + +pub use pyxirr_core as core; mod broadcasting; mod conversions; -mod core; - -create_exception!(pyxirr, InvalidPaymentsError, exceptions::PyException); -create_exception!(pyxirr, BroadcastingError, exceptions::PyException); -impl From for PyErr { - fn from(value: core::InvalidPaymentsError) -> Self { - InvalidPaymentsError::new_err(value.to_string()) - } -} +use crate::broadcasting::Arg; +use crate::conversions::{fallible_float_or_none, float_or_none, AmountArray, PyDayCount}; -impl From for PyErr { - fn from(value: broadcasting::BroadcastingError) -> Self { - BroadcastingError::new_err(value.to_string()) - } -} +pub use pyxirr_core::pyo3::{BroadcastingError, InvalidPaymentsError}; macro_rules! dispatch_vectorized { (infallible $py:ident, ($($vars:ident),*), $non_vec:expr, $vec:expr ) => { @@ -880,8 +869,8 @@ pub fn pyxirr(py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(is_conventional_cash_flow, m)?)?; m.add_function(wrap_pyfunction!(zero_crossing_points, m)?)?; - m.add("InvalidPaymentsError", py.get_type::())?; - m.add("BroadcastingError", py.get_type::())?; + m.add("InvalidPaymentsError", py.get_type::())?; + m.add("BroadcastingError", py.get_type::())?; Ok(()) } From 51c83ff80879783e7c68bda5b036c553f33745d0 Mon Sep 17 00:00:00 2001 From: Anexen Date: Tue, 2 Jan 2024 13:39:57 +0100 Subject: [PATCH 3/6] C API for xirr function --- .gitignore | 3 +- c/Cargo.lock | 122 +++++++++++++++++++++++++++++++++++++++++++++ c/Cargo.toml | 13 +++++ c/README.md | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++ c/src/lib.rs | 106 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 c/Cargo.lock create mode 100644 c/Cargo.toml create mode 100644 c/README.md create mode 100644 c/src/lib.rs diff --git a/.gitignore b/.gitignore index c3aab21..cdf4d28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -/target +target .mypy_cache/ __pycache__ *.so +.gdb_history diff --git a/c/Cargo.lock b/c/Cargo.lock new file mode 100644 index 0000000..a9268a4 --- /dev/null +++ b/c/Cargo.lock @@ -0,0 +1,122 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd5e8a1f1029c43224ad5898e50140c2aebb1705f19e67c918ebf5b9e797fe1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyxirr-core" +version = "0.0.0" +dependencies = [ + "time", +] + +[[package]] +name = "quote" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a37c9326af5ed140c86a46655b5278de879853be5573c01df185b6f49a580a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eae3c679c56dc214320b67a1bc04ef3dfbd6411f6443974b5e4893231298e66" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +dependencies = [ + "time-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "xirr-capi" +version = "0.0.0" +dependencies = [ + "libc", + "pyxirr-core", +] diff --git a/c/Cargo.toml b/c/Cargo.toml new file mode 100644 index 0000000..3e8398a --- /dev/null +++ b/c/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "xirr-capi" +version = "0.0.0" +edition = "2021" + +[lib] +name = "xirr" +crate-type = ["cdylib"] +doctest = false + +[dependencies] +libc = "0.2" +pyxirr-core = { path = "../core" } diff --git a/c/README.md b/c/README.md new file mode 100644 index 0000000..b61f89d --- /dev/null +++ b/c/README.md @@ -0,0 +1,137 @@ +# FFI interface + +FFI (Foreign Function Interface) is a mechanism that allows programs written in +one programming language to use functions or libraries written in another +language. + +This crate provides financial functions via FFI, so they can be used with other +languages, such as C, C#, Ruby, NodeJS and many others. + +## `C` + +```c +#include +#include + +// Function to calculate XIRR in C +extern int xirr( + long* dates, + int date_count, + double* amounts, + int amount_count, + double guess, + unsigned short day_count, + double* result +); + +int main() { + // Input data + long timestamp[] = {1577833200, 1580511600, 1583017200}; + double amounts[] = {-1000, 750, 500}; + + // Call the xirr function + double rate; + int result_code = xirr(timestamp, 3, amounts, 3, 0.1, 0, &rate); + + // Check the result code and process the result + if (result_code == 0) { + printf("XIRR result: %lf\n", rate); + } else { + printf("Error calculating XIRR. Result code: %d\n", result_code); + } + + return 0; +} +``` + +Compilation: + +```bash +gcc -L ./target/release -l xirr example.c +LD_LIBRARY_PATH=./target/release ./a.out +``` + +## `C#` + +```csharp +using System; +using System.Runtime.InteropServices; + +public class FFIExample +{ + // Change the library name accordingly + const string __DllPath = "./target/release/libxirr.so"; + + // Import the foreign functions + [DllImport(__DllPath)] + private static extern int xirr( + long[] dates, + int dates_length, + double[] values, + int values_length, + double guess, + ushort day_count, + out double result + ); + + private static DateTime UNIX_EPOCH = new DateTime(1970, 1, 1); + + public static double XIRR(DateTime[] dates, double[] values, double? guess) + { + // Prepare data for the foreign function + long[] timestamps = dates.Select(DateTimeToUnixTimestamp).ToArray(); + + // Call the foreign function + int resultCode = xirr( + timestamps, + timestamps.Length, + values, + values.Length, + guess ?? 0.1, + 0, + out double result + ); + + // Check the result code and process the result as needed + if (resultCode != 0) + { + throw new Exception($"Error Code: {resultCode}"); + } + + return result; + } + + private static long DateTimeToUnixTimestamp(DateTime datetime) + { + return (long)(datetime.Subtract(UNIX_EPOCH).TotalSeconds); + } + + public static void Main() + { + // Example data + var values = new double[] + { + -1000.0, + 750.0, + 500.0, + }; + + var dates = new DateTime[] + { + new DateTime(2020, 1, 1), + new DateTime(2020, 2, 1), + new DateTime(2020, 3, 1), + }; + + // Call foreign function + double r1 = XIRR(dates, values, null); + Console.WriteLine($"XIRR: {r1}"); + } +} +``` + +Compilation: + +```bash +dotnet run +``` diff --git a/c/src/lib.rs b/c/src/lib.rs new file mode 100644 index 0000000..e08f611 --- /dev/null +++ b/c/src/lib.rs @@ -0,0 +1,106 @@ +use libc::{c_double, c_long, c_ushort, size_t}; + +use pyxirr_core::{self as core, DateLike, DayCount}; + +fn day_count_from_code(value: u16) -> Option { + match value { + 0 => Some(DayCount::ACT_365F), + 1 => Some(DayCount::ACT_ACT_ISDA), + 2 => Some(DayCount::ACT_365_25), + 3 => Some(DayCount::ACT_364), + 4 => Some(DayCount::ACT_360), + 5 => Some(DayCount::THIRTY_360_ISDA), + 6 => Some(DayCount::THIRTY_E_360), + 7 => Some(DayCount::THIRTY_E_PLUS_360), + 8 => Some(DayCount::THIRTY_E_360_ISDA), + 9 => Some(DayCount::THIRTY_U_360), + 10 => Some(DayCount::NL_365), + 11 => Some(DayCount::NL_360), + _ => None, + } +} + +#[repr(i32)] +pub enum ReturnCode { + Success = 0, + NullReference = 1, + InvalidDayCount = 2, + ArraysOfDifferentLength = 3, + PositiveAndNegativePaymentsRequired = 4, +} + +#[no_mangle] +pub unsafe extern "C" fn xnpv( + rate: c_double, + dates_ptr: *const c_long, + dates_length: size_t, + values_ptr: *const c_double, + values_length: size_t, + day_count: c_ushort, + result: *mut c_double, +) -> ReturnCode { + if values_ptr.is_null() || dates_ptr.is_null() { + return ReturnCode::NullReference; + } + + if values_length != dates_length { + return ReturnCode::ArraysOfDifferentLength; + } + + let day_count = match day_count_from_code(day_count) { + Some(x) => x, + None => return ReturnCode::InvalidDayCount, + }; + + let values = std::slice::from_raw_parts(values_ptr, values_length); + let timestamps = std::slice::from_raw_parts(dates_ptr, dates_length); + + let dates: Vec<_> = timestamps.iter().map(|ts| DateLike::from_unix_timestamp(*ts)).collect(); + + match core::xnpv(rate, &dates, values, Some(day_count)) { + Ok(value) => { + *result = value; + ReturnCode::Success + } + // because the length of the array has already been checked + Err(_) => ReturnCode::PositiveAndNegativePaymentsRequired, + } +} + +#[no_mangle] +pub unsafe extern "C" fn xirr( + dates_ptr: *const c_long, + dates_length: size_t, + values_ptr: *const c_double, + values_length: size_t, + guess: c_double, + day_count: c_ushort, + result: *mut c_double, +) -> ReturnCode { + if values_ptr.is_null() || dates_ptr.is_null() { + return ReturnCode::NullReference; + } + + if values_length != dates_length { + return ReturnCode::ArraysOfDifferentLength; + } + + let day_count = match day_count_from_code(day_count) { + Some(x) => x, + None => return ReturnCode::InvalidDayCount, + }; + + let values = std::slice::from_raw_parts(values_ptr, values_length); + let timestamps = std::slice::from_raw_parts(dates_ptr, dates_length); + + let dates: Vec<_> = timestamps.iter().map(|ts| DateLike::from_unix_timestamp(*ts)).collect(); + + match core::xirr(&dates, values, Some(guess), Some(day_count)) { + Ok(rate) => { + *result = rate; + ReturnCode::Success + } + // because the length of the array has already been checked + Err(_) => ReturnCode::PositiveAndNegativePaymentsRequired, + } +} From 937d2b124dd6fc4b09843914224bb1b89a355a58 Mon Sep 17 00:00:00 2001 From: Anexen Date: Wed, 3 Jan 2024 01:16:20 +0100 Subject: [PATCH 4/6] update readme --- c/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/c/README.md b/c/README.md index b61f89d..5dc733b 100644 --- a/c/README.md +++ b/c/README.md @@ -7,6 +7,12 @@ language. This crate provides financial functions via FFI, so they can be used with other languages, such as C, C#, Ruby, NodeJS and many others. +## Building + +```bash +cargo build --release +``` + ## `C` ```c From 24436cbd9f43bacc0d92f2a742d2f3addfcb306f Mon Sep 17 00:00:00 2001 From: Anexen Date: Fri, 5 Jan 2024 22:18:23 +0100 Subject: [PATCH 5/6] use long long type for timestamps --- c/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/c/src/lib.rs b/c/src/lib.rs index e08f611..cf2461a 100644 --- a/c/src/lib.rs +++ b/c/src/lib.rs @@ -1,4 +1,4 @@ -use libc::{c_double, c_long, c_ushort, size_t}; +use libc::{c_double, c_longlong, c_ushort, size_t}; use pyxirr_core::{self as core, DateLike, DayCount}; @@ -32,7 +32,7 @@ pub enum ReturnCode { #[no_mangle] pub unsafe extern "C" fn xnpv( rate: c_double, - dates_ptr: *const c_long, + dates_ptr: *const c_longlong, dates_length: size_t, values_ptr: *const c_double, values_length: size_t, @@ -69,7 +69,7 @@ pub unsafe extern "C" fn xnpv( #[no_mangle] pub unsafe extern "C" fn xirr( - dates_ptr: *const c_long, + dates_ptr: *const c_longlong, dates_length: size_t, values_ptr: *const c_double, values_length: size_t, From 4416f85ebd58dd5fefed72b61230769c770bfcb6 Mon Sep 17 00:00:00 2001 From: Anexen Date: Sun, 7 Jan 2024 14:01:15 +0100 Subject: [PATCH 6/6] fixed core deps --- core/Cargo.lock | 648 ++++++++++++++++++++++++++++++++ core/Cargo.toml | 7 + core/src/private_equity.rs | 4 +- core/src/scheduled/day_count.rs | 2 +- 4 files changed, 658 insertions(+), 3 deletions(-) create mode 100644 core/Cargo.lock diff --git a/core/Cargo.lock b/core/Cargo.lock new file mode 100644 index 0000000..ec6483e --- /dev/null +++ b/core/Cargo.lock @@ -0,0 +1,648 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "assert_approx_eq" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "numpy" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef41cbb417ea83b30525259e30ccef6af39b31c240bda578889494c5392d331" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "rustc-hash", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a89dc7a5850d0e983be1ec2a463a171d20990487c3cfcd68b5363f1ee3d6fe0" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07426f0d8fe5a601f26293f300afd1a7b1ed5e78b2a705870c5f30893c5163be" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb7dec17e17766b46bca4f1a4215a85006b4c2ecde122076c562dd058da6cf1" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f738b4e40d50b5711957f142878cfa0f28e054aa0ebdfc3fd137a843f74ed3" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc910d4851847827daf9d6cdd4a823fbdaab5b8818325c5e97a86da79e8881f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pyxirr-core" +version = "0.0.0" +dependencies = [ + "assert_approx_eq", + "ndarray", + "numpy", + "pyo3", + "rstest", + "time", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "relative-path" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" + +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" + +[[package]] +name = "serde" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +dependencies = [ + "time-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/core/Cargo.toml b/core/Cargo.toml index e945739..839ab57 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -3,12 +3,19 @@ name = "pyxirr-core" version = "0.0.0" edition = "2021" +[lib] +doctest = false + [dependencies] time = { version = "0.3", features = ["parsing", "macros"] } ndarray = { version = "0.15", optional = true } pyo3 = { version = "0.20", optional = true } numpy = { version = "0.20", optional = true } +[dev-dependencies] +rstest = "0.18.2" +assert_approx_eq = "1.1" + [features] vectorization = ["ndarray"] python = ["pyo3", "numpy"] diff --git a/core/src/private_equity.rs b/core/src/private_equity.rs index 8497f97..ba86a5d 100644 --- a/core/src/private_equity.rs +++ b/core/src/private_equity.rs @@ -316,7 +316,7 @@ mod tests { #[case(&[-12., 0., 0., 40.], 0.494)] #[case(&[-12., -10., -4., 40., 0., 15., 5.], 0.324)] fn test_irr(#[case] amounts: &[f64], #[case] expected: f64) { - let result = crate::core::irr(amounts, None).unwrap(); + let result = crate::irr(amounts, None).unwrap(); assert_approx_eq!(result, expected, 1e-3); } @@ -325,7 +325,7 @@ mod tests { let amounts = &[-12.0, -10.0, -4.0, 40.0, 0.0, 15.0, 5.0]; let finance_rate = 0.07; let reinvest_rate = 0.12; - let result = crate::core::mirr(amounts, finance_rate, reinvest_rate).unwrap(); + let result = crate::mirr(amounts, finance_rate, reinvest_rate).unwrap(); assert_approx_eq!(result, 0.21, 1e-3); } diff --git a/core/src/scheduled/day_count.rs b/core/src/scheduled/day_count.rs index 6c96f86..38fd84d 100644 --- a/core/src/scheduled/day_count.rs +++ b/core/src/scheduled/day_count.rs @@ -238,7 +238,7 @@ mod tests { use rstest::rstest; use super::*; - use crate::core::DateLike; + use crate::DateLike; // test cases from http://www.deltaquants.com/day-count-conventions #[rstest]