
PyXIRR
Rust-powered collection of financial functions.
PyXIRR stands for "Python XIRR" (for historical reasons), but contains many other financial functions such as IRR, FV, NPV, etc.
Features:
- correct
- supports different day count conventions (e.g. ACT/360, 30E/360, etc.)
- works with different input data types (iterators, numpy arrays, pandas DataFrames)
- no external dependencies
- type annotations
- blazingly fast
Installation
pip install pyxirr
WASM wheels for pyodide are also available,
but unfortunately are not supported by PyPI.
You can find them on the GitHub Releases page.
Benchmarks
Rust implementation has been tested against existing xirr package
(uses scipy.optimize under the hood)
and the implementation from the Stack Overflow (pure python).

PyXIRR is much faster than the other implementations.
Powered by github-action-benchmark and plotly.js.
Live benchmarks are hosted on Github Pages.
Example
from datetime import date
from pyxirr import xirr
dates = [date(2020, 1, 1), date(2021, 1, 1), date(2022, 1, 1)]
amounts = [-1000, 750, 500]
xirr(dates, amounts)
xirr(iter(dates), (x / 2 for x in amounts))
xirr(zip(dates, amounts))
xirr(dict(zip(dates, amounts)))
xirr(['2020-01-01', '2021-01-01'], [-1000, 1200])
Multiple IRR problem
The Multiple IRR problem occurs when the signs of cash flows change more than
once. In this case, we say that the project has non-conventional cash flows.
This leads to situation, where it can have more the one IRR or have no IRR at all.
PyXIRR addresses the Multiple IRR problem as follows:
- It looks for positive result around 0.1 (the same as Excel with the default guess=0.1).
- If it can't find a result, it uses several other attempts and selects the lowest IRR to be conservative.
Here is an example illustrating how to identify multiple IRRs:
import numpy as np
import pyxirr
cf = pd.read_csv("tests/samples/30-22.csv", names=["date", "amount"])
print(pyxirr.is_conventional_cash_flow(cf["amount"]))
rates = np.linspace(-0.5, 0.5, 50)
values = pyxirr.xnpv(rates, cf)
print("NPV profile:")
for rate, value in zip(rates, values):
print(rate, value)
import pandas as pd
series = pd.Series(values, index=rates)
pd.DataFrame(series[series > -1e6]).assign(zero=0).plot()
indexes = pyxirr.zero_crossing_points(values)
print("Zero crossing points:")
for idx in indexes:
print("between", rates[idx], "and", rates[idx+1])
for i, idx in enumerate(indexes, start=1):
rate = pyxirr.xirr(cf, guess=rates[idx])
npv = pyxirr.xnpv(rate, cf)
print(f"{i}) {rate}; XNPV = {npv}")
More Examples
Numpy and Pandas
import numpy as np
import pandas as pd
xirr(np.array([dates, amounts]))
xirr(np.array(dates), np.array(amounts))
xirr(pd.DataFrame({"a": dates, "b": amounts}))
xirr(pd.Series(amounts, index=pd.to_datetime(dates)))
df = pd.DataFrame(
index=pd.date_range("2021", "2022", freq="MS", inclusive="left"),
data={
"one": [-100] + [20] * 11,
"two": [-80] + [19] * 11,
},
)
df.apply(xirr)
Day count conventions
Check out the available options on the docs/day-count-conventions.
from pyxirr import DayCount
xirr(dates, amounts, day_count=DayCount.ACT_360)
xirr(dates, amounts, day_count="30E/360")
Private equity performance metrics
from pyxirr import pe
pe.pme_plus([-20, 15, 0], index=[100, 115, 130], nav=20)
pe.direct_alpha([-20, 15, 0], index=[100, 115, 130], nav=20)
Docs
Other financial functions
import pyxirr
pyxirr.fv(0.05/12, 10*12, -100, -100)
pyxirr.npv(0, [-40_000, 5_000, 8_000, 12_000, 30_000])
pyxirr.irr([-100, 39, 59, 55, 20])
Docs
Vectorization
PyXIRR supports numpy-like vectorization.
If all input is scalar, returns a scalar float. If any input is array_like,
returns values for each input element. If multiple inputs are
array_like, performs broadcasting and returns values for each element.
import pyxirr
pyxirr.fv([0.05/12, 0.06/12], 10*12, -100, -100)
pyxirr.fv([0.05/12, 0.06/12], [10*12, 9*12], [-100, -200], -100)
import numpy as np
rates = np.array([0.05, 0.06, 0.07])/12
pyxirr.fv(rates, 10*12, -100, -100)
pyxirr.fv(
np.linspace(0.01, 0.2, 10),
(x + 1 for x in range(10)),
range(-100, -1100, -100),
tuple(range(-100, -200, -10))
)
rates = [[[[[[0.01], [0.02]]]]]]
pyxirr.fv(rates, 10*12, -100, -100)
API reference
See the docs
Roadmap
Development
Running tests with pyo3 is a bit tricky. In short, you need to compile your tests without extension-module
feature to avoid linking errors.
See the following issues for the details: #341, #771.
If you are using pyenv
, make sure you have the shared library installed (check for ${PYENV_ROOT}/versions/<version>/lib/libpython3.so
file).
$ PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install <version>
Install dev-requirements
$ pip install -r dev-requirements.txt
Building
$ maturin develop
Testing
$ LD_LIBRARY_PATH=${PYENV_ROOT}/versions/3.10.8/lib cargo test
Benchmarks
$ pip install -r bench-requirements.txt
$ LD_LIBRARY_PATH=${PYENV_ROOT}/versions/3.10.8/lib cargo +nightly bench
Building and distribution
This library uses maturin to build and distribute python wheels.
$ docker run --rm -v $(pwd):/io ghcr.io/pyo3/maturin build --release --manylinux 2010 --strip
$ maturin upload target/wheels/pyxirr-${version}*