Skip to content

Commit

Permalink
Merge branch 'main' of github.com:Anexen/pyxirr into feature/ffi
Browse files Browse the repository at this point in the history
  • Loading branch information
Anexen committed Jan 28, 2024
2 parents 4416f85 + 43a7d18 commit 5557049
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 42 deletions.
26 changes: 10 additions & 16 deletions .github/workflows/bench.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,28 @@ on:
required: true
default: true

permissions:
contents: write
deployments: write

jobs:
benchmark:
name: Run Rust benchmark
runs-on: ubuntu-latest
if: ${{ startsWith(github.ref, 'refs/tags/') || github.event.inputs.build }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.9
- uses: actions-rs/toolchain@v1
python-version: 3.11
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
profile: minimal
override: true
- uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cache/pip
target
key: ubuntu-latest-cargo-${{ hashFiles('**/Cargo.lock') }}
- run: pip install -r bench-requirements.txt
- name: Run benchmark
run: cargo +nightly bench --bench comparison | tee output.txt
- name: Store benchmark result
uses: rhysd/github-action-benchmark@v1
uses: benchmark-action/github-action-benchmark@v1
with:
name: Rust Benchmark
tool: 'cargo'
Expand All @@ -47,4 +41,4 @@ jobs:
benchmark-data-dir-path: docs/bench
output-file-path: output.txt
auto-push: true
github-token: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
32 changes: 16 additions & 16 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
test:
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install dev-requirements
run: pip install -r dev-requirements.txt
- uses: actions/cache@v3
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
Expand Down Expand Up @@ -54,12 +54,12 @@ jobs:
arch: aarch64
- platform: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python.constraints || matrix.python.version }}
# allow-prereleases: ${{ matrix.python.version == "3.12" }}
- uses: actions/cache@v3
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
Expand All @@ -71,29 +71,29 @@ jobs:

- name: Build Wheels - Linux
if: matrix.os.platform == 'ubuntu-latest'
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
maturin-version: v1.3.0
manylinux: auto
args: -i python${{ matrix.python.version }} --release --strip --sdist

- name: Build Wheels - MacOS [aarch64]
if: ${{ matrix.os.platform == 'macos-latest' && matrix.os.arch == 'aarch64' }}
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
maturin-version: v1.3.0
args: -i python --release --target aarch64-apple-darwin --strip

- name: Build Wheels - MacOS [x86_64]
if: ${{ matrix.os.platform == 'macos-latest' && matrix.os.arch != 'aarch64' }}
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
maturin-version: v1.3.0
args: -i python --release --target universal2-apple-darwin --strip

- name: Build Wheels - Windows
if: matrix.os.platform == 'windows-latest'
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
maturin-version: v1.3.0
args: -i python --release --strip
Expand Down Expand Up @@ -125,9 +125,9 @@ jobs:
abi: 'cp312-cp312'
target: [aarch64, armv7, s390x, ppc64le, ppc64]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Build Wheels
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
env:
PYO3_CROSS_LIB_DIR: /opt/python/${{ matrix.python.abi }}/lib
with:
Expand All @@ -136,7 +136,7 @@ jobs:
manylinux: auto
args: -i python3.9 --release --strip
- name: Upload wheels
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
path: target/wheels
Expand All @@ -153,16 +153,16 @@ jobs:
- aarch64-unknown-linux-musl
- armv7-unknown-linux-musleabihf
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Build Wheels - musl
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
maturin-version: v1.3.0
target: ${{ matrix.target }}
manylinux: musllinux_1_2
args: --release --strip -i 3.7 3.8 3.9 3.10 3.11 3.12
- name: Upload wheels
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
path: target/wheels
Expand All @@ -172,7 +172,7 @@ jobs:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: wheels

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [0.10.2] - 2024-01-27

- (X)IRR Performance improvements

## [0.10.1] - 2023-12-08

- XIRR improvements ([#49](https://github.com/Anexen/pyxirr/pull/49))
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pyxirr"
version = "0.10.1"
version = "0.10.2"
authors = ["Anexen"]
edition = "2021"
description = "Rust-powered collection of financial functions for Python."
Expand Down
12 changes: 6 additions & 6 deletions core/src/scheduled/xirr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::{year_fraction, DayCount};
use crate::{
models::{validate, DateLike, InvalidPaymentsError},
optimize::{brentq, newton_raphson},
utils::{self, fast_pow},
};

pub fn xirr(
Expand All @@ -22,25 +23,24 @@ pub fn xirr(
xnpv_result(amounts, deltas, rate)
};
let df = |rate| xnpv_result_deriv(amounts, deltas, rate);
let is_good_rate = |rate: f64| rate.is_finite() && f(rate).abs() < 1e-3;

let rate = newton_raphson(guess.unwrap_or(0.1), &f, &df);

if is_good_rate(rate) {
if utils::is_a_good_rate(rate, f) {
return Ok(rate);
}

let rate = brentq(&f, -0.999999999999999, 100., 100);

if is_good_rate(rate) {
if utils::is_a_good_rate(rate, f) {
return Ok(rate);
}

let mut step = 0.01;
let mut guess = -0.99999999999999;
while guess < 1.0 {
let rate = newton_raphson(guess, &f, &df);
if is_good_rate(rate) {
if utils::is_a_good_rate(rate, f) {
return Ok(rate);
}
guess += step;
Expand Down Expand Up @@ -86,15 +86,15 @@ fn day_count_factor(dates: &[DateLike], day_count: Option<DayCount>) -> Vec<f64>

// \sum_{i=1}^n \frac{P_i}{(1 + rate)^{(d_i - d_0)/365}}
fn xnpv_result(payments: &[f64], deltas: &[f64], rate: f64) -> f64 {
payments.iter().zip(deltas).map(|(p, &e)| p * (1.0 + rate).powf(-e)).sum()
payments.iter().zip(deltas).map(|(p, &e)| p * fast_pow(1.0 + rate, -e)).sum()
}

// XNPV first derivative
// \sum_{i=1}^n P_i * (d_0 - d_i) / 365 * (1 + rate)^{((d_0 - d_i)/365 - 1)}}
// simplify in order to reuse cached deltas (d_i - d_0)/365
// \sum_{i=1}^n \frac{P_i * -(d_i - d_0) / 365}{(1 + rate)^{((d_i - d_0)/365 + 1)}}
fn xnpv_result_deriv(payments: &[f64], deltas: &[f64], rate: f64) -> f64 {
payments.iter().zip(deltas).map(|(p, e)| p * -e * (1.0 + rate).powf(-e - 1.0)).sum()
payments.iter().zip(deltas).map(|(p, e)| p * -e * fast_pow(1.0 + rate, -e - 1.0)).sum()
}

#[cfg(test)]
Expand Down
18 changes: 17 additions & 1 deletion core/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
pub(crate) fn fast_pow(a: f64, b: f64) -> f64 {
// works only if a is positive
(a.log2() * b).exp2()
}

pub(crate) fn scale(values: &[f64], factor: f64) -> Vec<f64> {
values.iter().map(|v| v * factor).collect()
}
Expand All @@ -12,7 +17,11 @@ pub(crate) fn pairwise_mul(a: &[f64], b: &[f64]) -> Vec<f64> {

pub(crate) fn series_signum(a: &[f64]) -> f64 {
// returns -1. if any item is negative, otherwise +1.
if a.iter().any(|x| x.is_sign_negative()) { -1. } else { 1. }
if a.iter().any(|x| x.is_sign_negative()) {
-1.
} else {
1.
}
}

pub(crate) fn sum_negatives_positives(values: &[f64]) -> (f64, f64) {
Expand All @@ -24,3 +33,10 @@ pub(crate) fn sum_negatives_positives(values: &[f64]) -> (f64, f64) {
}
})
}

pub(crate) fn is_a_good_rate<F>(rate: f64, f: F) -> bool
where
F: Fn(f64) -> f64,
{
rate.is_finite() && f(rate).abs() < 1e-3
}
78 changes: 77 additions & 1 deletion docs/bench/data.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
window.BENCHMARK_DATA = {
"lastUpdate": 1701990793475,
"lastUpdate": 1706390745809,
"repoUrl": "https://github.com/Anexen/pyxirr",
"entries": {
"Rust Benchmark": [
Expand Down Expand Up @@ -3420,6 +3420,82 @@ window.BENCHMARK_DATA = {
"unit": "ns/iter"
}
]
},
{
"commit": {
"author": {
"name": "Anexen",
"username": "Anexen",
"email": "avolk93@gmail.com"
},
"committer": {
"name": "Anexen",
"username": "Anexen",
"email": "avolk93@gmail.com"
},
"id": "ccb6234f73fcb93c417ff8a862d2fdbf50257d9b",
"message": "fix unused import",
"timestamp": "2024-01-27T21:23:48Z",
"url": "https://github.com/Anexen/pyxirr/commit/ccb6234f73fcb93c417ff8a862d2fdbf50257d9b"
},
"date": 1706390745412,
"tool": "cargo",
"benches": [
{
"name": "bench_python_100",
"value": 8149216,
"range": "± 55209",
"unit": "ns/iter"
},
{
"name": "bench_python_1000",
"value": 20161064,
"range": "± 218730",
"unit": "ns/iter"
},
{
"name": "bench_python_500",
"value": 26305873,
"range": "± 584738",
"unit": "ns/iter"
},
{
"name": "bench_rust_100",
"value": 11250,
"range": "± 101",
"unit": "ns/iter"
},
{
"name": "bench_rust_1000",
"value": 96669,
"range": "± 1193",
"unit": "ns/iter"
},
{
"name": "bench_rust_500",
"value": 56507,
"range": "± 403",
"unit": "ns/iter"
},
{
"name": "bench_scipy_100",
"value": 701667,
"range": "± 13445",
"unit": "ns/iter"
},
{
"name": "bench_scipy_1000",
"value": 4190035,
"range": "± 112721",
"unit": "ns/iter"
},
{
"name": "bench_scipy_500",
"value": 2855297,
"range": "± 81117",
"unit": "ns/iter"
}
]
}
]
}
Expand Down
1 change: 1 addition & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod cases;
mod helpers;

#[allow(unused_imports)]
pub use cases::*;
pub use helpers::*;

0 comments on commit 5557049

Please sign in to comment.